refactor: replace inline completion diffing with editor suffix matching

This commit is contained in:
Carl-Robert Linnupuu 2024-11-14 11:58:56 +00:00
parent 25d8835db1
commit 808fdfaf7e
3 changed files with 33 additions and 113 deletions

View file

@ -18,7 +18,6 @@ import ee.carlrobert.codegpt.settings.service.custom.CustomServiceSettings
import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings
import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings
import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings
import ee.carlrobert.codegpt.util.StringUtil.findCompletionParts
import kotlinx.coroutines.channels.ProducerScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.channelFlow
@ -26,6 +25,7 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import okhttp3.sse.EventSource
import java.util.concurrent.atomic.AtomicReference
import kotlin.math.min
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@ -129,8 +129,8 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() {
override fun onLineReceived(completionLine: String) {
runInEdt {
val editorLineSuffix = editor.getLineSuffixAfterCaret()
if (editorLineSuffix.isEmpty()) {
var editorLineSuffix = editor.getLineSuffixAfterCaret()
if (editorLineSuffix.isBlank()) {
trySend(
CodeCompletionTextElement(
completionLine,
@ -139,31 +139,46 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() {
)
)
} else {
var prevStartOffset = infillRequest.caretOffset
val completionParts =
findCompletionParts(editorLineSuffix, completionLine.trimEnd())
var caretShift = 0
completionParts.forEach { (completionPart, offsetDelta) ->
val element = CodeCompletionTextElement(
completionPart,
infillRequest.caretOffset + offsetDelta,
TextRange.from(prevStartOffset + offsetDelta, completionPart.length),
offsetDelta,
// TODO: Handle other scenarios
val processedCompletion =
if (completionLine.startsWith(editorLineSuffix.first())) {
caretShift++
editorLineSuffix = editorLineSuffix.substring(1)
completionLine.substring(1)
} else {
completionLine
}
val completionWithRemovedSuffix =
processedCompletion.removeSuffix(editorLineSuffix)
trySend(
CodeCompletionTextElement(
completionWithRemovedSuffix,
infillRequest.caretOffset + caretShift,
TextRange.from(
infillRequest.caretOffset + caretShift,
completionWithRemovedSuffix.length
),
caretShift,
completionLine
)
prevStartOffset += completionPart.length
trySend(element)
}
)
}
}
}
}
private fun Editor.getLineSuffixAfterCaret(): String {
val lineEndOffset = document.getLineEndOffset(document.getLineNumber(caretModel.offset))
return document.getText(TextRange(caretModel.offset, lineEndOffset))
return document.getText(
TextRange(
caretModel.offset,
min(lineEndOffset + 1, document.textLength)
)
)
}
private fun sendNextSuggestion(

View file

@ -1,7 +1,6 @@
package ee.carlrobert.codegpt.util
import ai.grazie.nlp.utils.takeWhitespaces
import com.intellij.util.diff.Diff
object StringUtil {
@ -23,44 +22,4 @@ object StringUtil {
return completionLine
}
fun findCompletionParts(
editorLineSuffix: String,
completionLine: String
): List<Pair<String, Int>> {
val nonOverlappingPart = findNonOverlappingPart(editorLineSuffix, completionLine)
if (nonOverlappingPart.length == completionLine.length) {
return listOf(Pair(completionLine, 0))
}
val result = ArrayList<Pair<String, Int>>()
val editorChars: IntArray = editorLineSuffix.chars().toArray()
val completionChars: IntArray = completionLine.chars().toArray()
val changes: List<Diff.Change> =
Diff.buildChanges(editorChars, completionChars)?.toList() ?: emptyList()
for (change in changes) {
val part = completionLine.substring(change.line1, change.line1 + change.inserted)
result.add(Pair(part, change.line0))
}
return result
}
private fun findNonOverlappingPart(
editorLineSuffix: String,
completionLine: String
): String {
var i = editorLineSuffix.length - 1
var j = completionLine.length - 1
while (i >= 0 && j >= 0 && editorLineSuffix[i] == completionLine[j]) {
i--
j--
}
if (j >= 0) {
return completionLine.substring(0, j + 1)
}
return ""
}
}

View file

@ -1,54 +0,0 @@
package ee.carlrobert.codegpt.util
import ee.carlrobert.codegpt.util.StringUtil.findCompletionParts
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class StringUtilTest {
@Test
fun `should parse completion without brackets and braces`() {
val completionLine = "root != null"
val editorLineSuffix = ") {\n"
val result = findCompletionParts(editorLineSuffix, completionLine)
assertThat(result[0].second).isEqualTo(0)
assertThat(result[0].first).isEqualTo("root != null")
}
@Test
fun `should parse completion with closing bracket and brace into separate parts`() {
val completionLine = "root != null) {\n"
val editorLineSuffix = ")\n"
val result = findCompletionParts(editorLineSuffix, completionLine)
assertThat(result[0].second).isEqualTo(0)
assertThat(result[0].first).isEqualTo("root != null")
assertThat(result[1].second).isEqualTo(1)
assertThat(result[1].first).isEqualTo(" {")
}
@Test
fun `should parse completion when editor suffix contains closing bracket and brace`() {
val completionLine = "root != null) {\n"
val editorLineSuffix = ") {\n"
val result = findCompletionParts(editorLineSuffix, completionLine)
assertThat(result[0].second).isEqualTo(0)
assertThat(result[0].first).isEqualTo("root != null")
}
@Test
fun `should parse completion between opening and closing brackets`() {
val completionLine = "(root != null) {\n"
val editorLineSuffix = "() {\n"
val result = findCompletionParts(editorLineSuffix, completionLine)
assertThat(result[0].second).isEqualTo(1)
assertThat(result[0].first).isEqualTo("root != null")
}
}