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
+ }
0 commit comments