Skip to content

Commit 93b8316

Browse files
committed
feat(diff): init stream diff code #257
1 parent 0db978c commit 93b8316

File tree

6 files changed

+1080
-0
lines changed

6 files changed

+1080
-0
lines changed
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
/**
2+
* Copyright 2023 Continue Dev, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package cc.unitmesh.devti.diff
17+
18+
import com.intellij.openapi.application.ApplicationManager
19+
import com.intellij.openapi.command.WriteCommandAction
20+
import com.intellij.openapi.command.undo.UndoManager
21+
import com.intellij.openapi.editor.Editor
22+
import com.intellij.openapi.editor.markup.HighlighterLayer
23+
import com.intellij.openapi.editor.markup.RangeHighlighter
24+
import com.intellij.openapi.fileEditor.FileDocumentManager
25+
import com.intellij.openapi.fileEditor.FileEditorManager
26+
import com.intellij.openapi.fileEditor.TextEditor
27+
import com.intellij.openapi.fileTypes.PlainTextLanguage
28+
import com.intellij.openapi.project.Project
29+
import com.intellij.openapi.vfs.VirtualFile
30+
import cc.unitmesh.devti.diff.model.DiffLine
31+
import cc.unitmesh.devti.diff.model.streamDiff
32+
import cc.unitmesh.devti.llms.LlmFactory
33+
import kotlinx.coroutines.flow.Flow
34+
import kotlinx.coroutines.flow.cancellable
35+
import kotlinx.coroutines.flow.flowOf
36+
import kotlinx.coroutines.launch
37+
import kotlin.math.min
38+
39+
40+
/**
41+
*
42+
* JButton("Apply Patch").apply {
43+
*
44+
* addActionListener {
45+
* val lookupFile =
46+
* project.lookupFile("src/main/java/com/phodal/shire/demo/service/BlogService.java")!!
47+
* val editor = FileEditorManager.getInstance(project).selectedTextEditor
48+
* val code = lookupFile.inputStream.bufferedReader().use { it.readText() }
49+
*
50+
* val diffStreamHandler = DiffStreamHandler(
51+
* project,
52+
* editor = editor!!, 0, code.lines().size,
53+
* onClose = {
54+
* },
55+
* onFinish = {
56+
* ShirelangNotifications.info(project, "Patch Applied")
57+
* }
58+
* )
59+
*
60+
* runInEdt {
61+
* diffStreamHandler
62+
* .streamDiffLinesToEditor(
63+
* code,
64+
* "使用为如下的代码添加删除功能,请使用 Markdown code 返回完整代码块: $code"
65+
* )
66+
* }
67+
* }
68+
* }
69+
*/
70+
class DiffStreamHandler(
71+
private val project: Project,
72+
private val editor: Editor,
73+
private val startLine: Int,
74+
private val endLine: Int,
75+
private val onClose: () -> Unit,
76+
private val onFinish: (response: String) -> Unit,
77+
) {
78+
private data class CurLineState(
79+
var index: Int, var highlighter: RangeHighlighter? = null, var diffBlock: VerticalDiffBlock? = null,
80+
)
81+
82+
private var curLine = CurLineState(startLine)
83+
84+
private var isRunning: Boolean = false
85+
private var hasAcceptedOrRejectedBlock: Boolean = false
86+
87+
private val unfinishedHighlighters: MutableList<RangeHighlighter> = mutableListOf()
88+
private val diffBlocks: MutableList<VerticalDiffBlock> = mutableListOf()
89+
90+
private val curLineKey = createTextAttributesKey("CONTINUE_DIFF_CURRENT_LINE", 0x40888888, editor)
91+
private val unfinishedKey = createTextAttributesKey("CONTINUE_DIFF_UNFINISHED_LINE", 0x20888888, editor)
92+
93+
init {
94+
initUnfinishedRangeHighlights()
95+
}
96+
97+
fun acceptAll() {
98+
editor.markupModel.removeAllHighlighters()
99+
resetState()
100+
}
101+
102+
fun rejectAll() {
103+
// The ideal action here is to undo all changes we made to return the user's edit buffer to the state prior
104+
// to our changes. However, if the user has accepted or rejected one or more diff blocks, there isn't a simple
105+
// way to undo our changes without also undoing the diff that the user accepted or rejected.
106+
if (hasAcceptedOrRejectedBlock) {
107+
diffBlocks.forEach { it.handleReject() }
108+
} else {
109+
undoChanges()
110+
}
111+
112+
resetState()
113+
}
114+
115+
fun streamDiffLinesToEditor(originContent: String, prompt: String) {
116+
val lines = originContent.lines()
117+
118+
isRunning = true
119+
val flow: Flow<String> = LlmFactory.instance.create(project).stream(prompt, "", false)
120+
var lastLineNo = 0
121+
cc.unitmesh.devti.util.AutoDevCoroutineScope.scope(project).launch {
122+
val suggestion = StringBuilder()
123+
flow.cancellable().collect { char ->
124+
suggestion.append(char)
125+
val code = cc.unitmesh.devti.util.parser.CodeFence.parse(suggestion.toString())
126+
if (PlainTextLanguage.INSTANCE != code.language && code.language.displayName != "Markdown" && code.text.isNotEmpty()) {
127+
var value: List<String> = code.text.lines()
128+
value = value.dropLast(1)
129+
130+
if (value.isEmpty()) return@collect
131+
132+
val newLines = if (lastLineNo < value.size) {
133+
value.subList(lastLineNo, value.size)
134+
} else {
135+
listOf()
136+
}
137+
138+
if (newLines.isEmpty()) return@collect
139+
140+
val flowValue: Flow<String> = flowOf(*newLines.toTypedArray())
141+
val oldLinesContent = if (lastLineNo + newLines.size <= lines.size) {
142+
lines.subList(lastLineNo, lastLineNo + newLines.size)
143+
} else {
144+
listOf()
145+
}
146+
lastLineNo = value.size
147+
148+
streamDiff(oldLinesContent, flowValue).collect {
149+
ApplicationManager.getApplication().invokeLater {
150+
WriteCommandAction.runWriteCommandAction(project) {
151+
updateByDiffType(it)
152+
}
153+
}
154+
}
155+
}
156+
}
157+
158+
handleFinishedResponse(suggestion.toString())
159+
}
160+
}
161+
162+
private fun updateByDiffType(diffLine: DiffLine) {
163+
when (diffLine) {
164+
is DiffLine.New -> handleNewLine(diffLine.line)
165+
is DiffLine.Old -> handleOldLine()
166+
is DiffLine.Same -> handleSameLine()
167+
}
168+
169+
updateProgressHighlighters(diffLine)
170+
}
171+
172+
private fun initUnfinishedRangeHighlights() {
173+
for (i in startLine..endLine) {
174+
val highlighter = editor.markupModel.addLineHighlighter(
175+
unfinishedKey, min(
176+
i, editor.document.lineCount - 1
177+
), HighlighterLayer.LAST
178+
)
179+
unfinishedHighlighters.add(highlighter)
180+
}
181+
}
182+
183+
private fun handleDiffBlockAcceptOrReject(diffBlock: VerticalDiffBlock, didAccept: Boolean) {
184+
hasAcceptedOrRejectedBlock = true
185+
186+
diffBlocks.remove(diffBlock)
187+
188+
if (!didAccept) {
189+
updatePositionsOnReject(diffBlock.startLine, diffBlock.addedLines.size, diffBlock.deletedLines.size)
190+
}
191+
192+
if (diffBlocks.isEmpty()) {
193+
onClose()
194+
}
195+
}
196+
197+
198+
private fun createDiffBlock(): VerticalDiffBlock {
199+
val diffBlock = VerticalDiffBlock(
200+
editor, project, curLine.index, ::handleDiffBlockAcceptOrReject
201+
)
202+
203+
diffBlocks.add(diffBlock)
204+
205+
return diffBlock
206+
}
207+
208+
private fun handleSameLine() {
209+
if (curLine.diffBlock != null) {
210+
curLine.diffBlock!!.onLastDiffLine()
211+
}
212+
213+
curLine.diffBlock = null
214+
215+
curLine.index++
216+
}
217+
218+
private fun handleNewLine(text: String) {
219+
if (curLine.diffBlock == null) {
220+
curLine.diffBlock = createDiffBlock()
221+
}
222+
223+
curLine.diffBlock!!.addNewLine(text, curLine.index)
224+
225+
curLine.index++
226+
}
227+
228+
private fun handleOldLine() {
229+
if (curLine.diffBlock == null) {
230+
curLine.diffBlock = createDiffBlock()
231+
}
232+
233+
curLine.diffBlock!!.deleteLineAt(curLine.index)
234+
}
235+
236+
private fun updateProgressHighlighters(type: DiffLine) {
237+
// Update the highlighter to show the current line
238+
curLine.highlighter?.let { editor.markupModel.removeHighlighter(it) }
239+
curLine.highlighter = editor.markupModel.addLineHighlighter(
240+
curLineKey, min(curLine.index, editor.document.lineCount - 1), HighlighterLayer.LAST
241+
)
242+
243+
// Remove the unfinished lines highlighter
244+
if (type is DiffLine.Old && unfinishedHighlighters.isNotEmpty()) {
245+
editor.markupModel.removeHighlighter(unfinishedHighlighters.removeAt(0))
246+
}
247+
}
248+
249+
250+
private fun updatePositionsOnReject(startLine: Int, numAdditions: Int, numDeletions: Int) {
251+
val offset = -numAdditions + numDeletions
252+
253+
diffBlocks.forEach { block ->
254+
if (block.startLine > startLine) {
255+
block.updatePosition(block.startLine + offset)
256+
}
257+
}
258+
}
259+
260+
private fun resetState() {
261+
// Clear the editor of highlighting/inlays
262+
editor.markupModel.removeAllHighlighters()
263+
diffBlocks.forEach { it.clearEditorUI() }
264+
265+
// Clear state vars
266+
diffBlocks.clear()
267+
curLine = CurLineState(startLine)
268+
isRunning = false
269+
270+
// Close the Edit input
271+
onClose()
272+
}
273+
274+
275+
private fun undoChanges() {
276+
WriteCommandAction.runWriteCommandAction(project) {
277+
val undoManager = UndoManager.getInstance(project)
278+
val virtualFile = getVirtualFile() ?: return@runWriteCommandAction
279+
val fileEditor = FileEditorManager.getInstance(project).getSelectedEditor(virtualFile) as TextEditor
280+
281+
if (undoManager.isUndoAvailable(fileEditor)) {
282+
val numChanges = diffBlocks.sumOf { it.deletedLines.size + it.addedLines.size }
283+
284+
repeat(numChanges) {
285+
undoManager.undo(fileEditor)
286+
}
287+
}
288+
}
289+
}
290+
291+
private fun getVirtualFile(): VirtualFile? {
292+
return FileDocumentManager.getInstance().getFile(editor.document) ?: return null
293+
}
294+
295+
private fun handleFinishedResponse(response: String) {
296+
ApplicationManager.getApplication().invokeLater {
297+
// Since we only call onLastDiffLine() when we reach a "same" line, we need to handle the case where
298+
// the last line in the diff stream is in the middle of a diff block.
299+
curLine.diffBlock?.onLastDiffLine()
300+
301+
onFinish(response)
302+
cleanupProgressHighlighters()
303+
}
304+
}
305+
306+
private fun cleanupProgressHighlighters() {
307+
curLine.highlighter?.let { editor.markupModel.removeHighlighter(it) }
308+
unfinishedHighlighters.forEach { editor.markupModel.removeHighlighter(it) }
309+
}
310+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright 2023 Continue Dev, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package cc.unitmesh.devti.diff
17+
18+
import cc.unitmesh.devti.diff.DiffStreamHandler
19+
import com.intellij.openapi.components.Service
20+
import com.intellij.openapi.editor.Editor
21+
22+
@Service(Service.Level.PROJECT)
23+
class DiffStreamService {
24+
private val handlers = mutableMapOf<Editor, DiffStreamHandler>()
25+
26+
fun register(handler: DiffStreamHandler, editor: Editor) {
27+
if (handlers.containsKey(editor)) {
28+
handlers[editor]?.rejectAll()
29+
}
30+
handlers[editor] = handler
31+
}
32+
33+
fun reject(editor: Editor) {
34+
handlers[editor]?.rejectAll()
35+
handlers.remove(editor)
36+
}
37+
38+
fun accept(editor: Editor) {
39+
handlers[editor]?.acceptAll()
40+
handlers.remove(editor)
41+
}
42+
}

0 commit comments

Comments
 (0)