mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
fix(vscode): remove next edit deleted lines cleanly
This commit is contained in:
+9
-5
@@ -160,7 +160,8 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
|
||||
const diffStartLineInFile = suggestion.editableRegionStartLine + prefixLines
|
||||
const diffEndLineInFile = suggestion.editableRegionStartLine + currentLines.length - 1 - suffixLines
|
||||
const trimmedReplacement = proposedLines.slice(prefixLines, proposedLines.length - suffixLines).join("\n")
|
||||
const trimmedLines = proposedLines.slice(prefixLines, proposedLines.length - suffixLines)
|
||||
const trimmedReplacement = trimmedLines.join("\n")
|
||||
|
||||
nesLog(`diff at lines [${diffStartLineInFile}..${diffEndLineInFile}], cursor at line ${position.line}, ${trimmedReplacement.length} chars`)
|
||||
|
||||
@@ -168,8 +169,9 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
// For off-cursor diffs, stash the suggestion in the manager — it renders a
|
||||
// decoration-based "jump to next edit" affordance and Tab handles the move/apply.
|
||||
const isPureInsertion = diffEndLineInFile < diffStartLineInFile
|
||||
if (isPureInsertion || diffStartLineInFile !== position.line) {
|
||||
this.stashOffCursorSuggestion(document, diffStartLineInFile, diffEndLineInFile, trimmedReplacement, isPureInsertion, suggestion)
|
||||
const removesLines = trimmedLines.length === 0
|
||||
if (isPureInsertion || removesLines || diffStartLineInFile !== position.line) {
|
||||
this.stashOffCursorSuggestion(document, diffStartLineInFile, diffEndLineInFile, trimmedReplacement, isPureInsertion, removesLines, suggestion)
|
||||
return undefined
|
||||
}
|
||||
// Same-line diff: clear any prior off-cursor pending state so we don't render
|
||||
@@ -201,7 +203,7 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
// Native ghost text cannot alter text before the cursor; present that edit
|
||||
// through the decoration/apply flow rather than silently discarding it.
|
||||
if (!cursorLineProposed.startsWith(cursorLineText.slice(0, position.character))) {
|
||||
this.stashOffCursorSuggestion(document, diffStartLine, diffEndLine, trimmedReplacement, false, suggestion)
|
||||
this.stashOffCursorSuggestion(document, diffStartLine, diffEndLine, trimmedReplacement, false, false, suggestion)
|
||||
return undefined
|
||||
}
|
||||
const insertText = [cursorLineProposed.slice(position.character), ...proposedLines.slice(prefixLines + 1, proposedLines.length - suffixLines)].join("\n")
|
||||
@@ -209,7 +211,7 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
// A single-line insert spanning non-blank lines below the cursor can't be
|
||||
// represented as inline ghost text — route it to the decoration path.
|
||||
if (renderEndLine > position.line && !insertText.includes("\n")) {
|
||||
this.stashOffCursorSuggestion(document, diffStartLine, diffEndLine, trimmedReplacement, false, suggestion)
|
||||
this.stashOffCursorSuggestion(document, diffStartLine, diffEndLine, trimmedReplacement, false, false, suggestion)
|
||||
return undefined
|
||||
}
|
||||
const renderRange = new vscode.Range(position, new vscode.Position(renderEndLine, document.lineAt(renderEndLine).range.end.character))
|
||||
@@ -246,6 +248,7 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
diffEndLine: number,
|
||||
trimmedReplacement: string,
|
||||
isPureInsertion: boolean,
|
||||
removesLines: boolean,
|
||||
suggestion: SuggestionResult,
|
||||
): void {
|
||||
const mgr = this.deps.suggestionManager
|
||||
@@ -287,6 +290,7 @@ export class NextEditInlineCompletionProvider implements vscode.InlineCompletion
|
||||
diffStartLine,
|
||||
diffEndLine,
|
||||
replacement: trimmedReplacement,
|
||||
removesLines,
|
||||
originalText: document.getText(originalRange),
|
||||
})
|
||||
nesLog(`replace suggestion stashed at lines [${diffStartLine}..${diffEndLine}]`)
|
||||
|
||||
+12
-2
@@ -1,6 +1,6 @@
|
||||
import * as vscode from "vscode"
|
||||
import { nesLog } from "./log"
|
||||
import { planInsertion } from "./pendingEdit"
|
||||
import { planInsertion, planReplacement } from "./pendingEdit"
|
||||
|
||||
const PENDING_CONTEXT_KEY = "kilo-code.nextEdit.hasPendingSuggestion"
|
||||
const CHAIN_DELAY_MS = 60
|
||||
@@ -15,6 +15,8 @@ export type PendingNextEdit =
|
||||
diffEndLine: number
|
||||
/** New text to substitute for [diffStartLine, diffEndLine]. */
|
||||
replacement: string
|
||||
/** Whether the suggestion omits complete lines rather than rewriting one as blank. */
|
||||
removesLines: boolean
|
||||
/** Snapshot of the original text — used to detect drift. */
|
||||
originalText: string
|
||||
}
|
||||
@@ -210,7 +212,15 @@ export class NextEditSuggestionManager implements vscode.Disposable {
|
||||
nesLog(`document drifted since suggestion was made — dropping range [${p.diffStartLine}..${p.diffEndLine}]`)
|
||||
return
|
||||
}
|
||||
ok = await editor.edit((b) => b.replace(range, p.replacement))
|
||||
const edit = planReplacement(p, {
|
||||
lineCount: editor.document.lineCount,
|
||||
end: (line) => editor.document.lineAt(line).range.end.character,
|
||||
})
|
||||
const target = new vscode.Range(
|
||||
new vscode.Position(edit.start.line, edit.start.character),
|
||||
new vscode.Position(edit.end.line, edit.end.character),
|
||||
)
|
||||
ok = await editor.edit((b) => b.replace(target, edit.text))
|
||||
nesLog(`applied replace at lines [${p.diffStartLine}..${p.diffEndLine}] (ok=${ok})`)
|
||||
}
|
||||
if (ok) chainNextPrediction()
|
||||
|
||||
+54
@@ -46,6 +46,18 @@ type Subject = {
|
||||
): vscode.InlineCompletionItem[] | undefined
|
||||
}
|
||||
|
||||
function doc(text: string): vscode.TextDocument {
|
||||
const lines = text.split("\n")
|
||||
return {
|
||||
lineCount: lines.length,
|
||||
lineAt: (line: number) => ({
|
||||
text: lines[line],
|
||||
range: { end: new vscode.Position(line, lines[line].length) },
|
||||
}),
|
||||
getText: () => text,
|
||||
} as unknown as vscode.TextDocument
|
||||
}
|
||||
|
||||
describe("NextEditInlineCompletionProvider", () => {
|
||||
it("stashes same-line rewrites before the cursor for decorated acceptance", () => {
|
||||
const mgr = { clear: vi.fn(), setPending: vi.fn() }
|
||||
@@ -73,4 +85,46 @@ describe("NextEditInlineCompletionProvider", () => {
|
||||
)
|
||||
provider.dispose()
|
||||
})
|
||||
|
||||
it("stashes complete-line deletion intent for acceptance", () => {
|
||||
const mgr = { clear: vi.fn(), setPending: vi.fn() }
|
||||
const provider = new NextEditInlineCompletionProvider({
|
||||
connectionService: {} as KiloConnectionService,
|
||||
suggestionManager: mgr as unknown as NextEditSuggestionManager,
|
||||
})
|
||||
|
||||
const out = (provider as unknown as Subject).toCompletionItems(doc("before\nremove\nafter"), new vscode.Position(1, 0), {
|
||||
replacement: "before\nafter",
|
||||
editableRegionStartLine: 0,
|
||||
editableRegionEndLine: 2,
|
||||
latencyMs: 1,
|
||||
})
|
||||
|
||||
expect(out).toBeUndefined()
|
||||
expect(mgr.setPending).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ kind: "replace", replacement: "", removesLines: true }),
|
||||
)
|
||||
provider.dispose()
|
||||
})
|
||||
|
||||
it("does not classify a blank-line rewrite as deletion", () => {
|
||||
const mgr = { clear: vi.fn(), setPending: vi.fn() }
|
||||
const provider = new NextEditInlineCompletionProvider({
|
||||
connectionService: {} as KiloConnectionService,
|
||||
suggestionManager: mgr as unknown as NextEditSuggestionManager,
|
||||
})
|
||||
|
||||
const out = (provider as unknown as Subject).toCompletionItems(doc("before\nremove\nafter"), new vscode.Position(0, 0), {
|
||||
replacement: "before\n\nafter",
|
||||
editableRegionStartLine: 0,
|
||||
editableRegionEndLine: 2,
|
||||
latencyMs: 1,
|
||||
})
|
||||
|
||||
expect(out).toBeUndefined()
|
||||
expect(mgr.setPending).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ kind: "replace", replacement: "", removesLines: false }),
|
||||
)
|
||||
provider.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
type Input = {
|
||||
diffStartLine: number
|
||||
replacement: string
|
||||
}
|
||||
|
||||
type Document = {
|
||||
lineCount: number
|
||||
end(line: number): number
|
||||
}
|
||||
|
||||
export function planInsertion(input: Input, document: Document) {
|
||||
type Insertion = {
|
||||
diffStartLine: number
|
||||
replacement: string
|
||||
}
|
||||
|
||||
type Replacement = Insertion & {
|
||||
diffEndLine: number
|
||||
removesLines: boolean
|
||||
}
|
||||
|
||||
export function planInsertion(input: Insertion, document: Document) {
|
||||
if (input.diffStartLine < document.lineCount) {
|
||||
return { line: input.diffStartLine, character: 0, text: input.replacement }
|
||||
}
|
||||
@@ -16,3 +21,23 @@ export function planInsertion(input: Input, document: Document) {
|
||||
const text = input.replacement.endsWith("\n") ? input.replacement.slice(0, -1) : input.replacement
|
||||
return { line, character: document.end(line), text: `\n${text}` }
|
||||
}
|
||||
|
||||
export function planReplacement(input: Replacement, document: Document) {
|
||||
const end = { line: input.diffEndLine, character: document.end(input.diffEndLine) }
|
||||
if (!input.removesLines) {
|
||||
return { start: { line: input.diffStartLine, character: 0 }, end, text: input.replacement }
|
||||
}
|
||||
if (input.diffEndLine < document.lineCount - 1) {
|
||||
return {
|
||||
start: { line: input.diffStartLine, character: 0 },
|
||||
end: { line: input.diffEndLine + 1, character: 0 },
|
||||
text: input.replacement,
|
||||
}
|
||||
}
|
||||
if (input.diffStartLine === 0) return { start: { line: 0, character: 0 }, end, text: input.replacement }
|
||||
return {
|
||||
start: { line: input.diffStartLine - 1, character: document.end(input.diffStartLine - 1) },
|
||||
end,
|
||||
text: input.replacement,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "bun:test"
|
||||
import { planInsertion } from "../../src/services/autocomplete/next-edit/pendingEdit"
|
||||
import { planInsertion, planReplacement } from "../../src/services/autocomplete/next-edit/pendingEdit"
|
||||
|
||||
describe("planInsertion", () => {
|
||||
it("appends after the final unterminated line at EOF", () => {
|
||||
@@ -20,3 +20,44 @@ describe("planInsertion", () => {
|
||||
expect(edit).toEqual({ line: 1, character: 0, text: "second\n" })
|
||||
})
|
||||
})
|
||||
|
||||
describe("planReplacement", () => {
|
||||
it("removes a middle line through the following separator", () => {
|
||||
const edit = planReplacement(
|
||||
{ diffStartLine: 1, diffEndLine: 1, replacement: "", removesLines: true },
|
||||
{ lineCount: 3, end: (line) => [6, 6, 5][line] },
|
||||
)
|
||||
|
||||
expect(edit).toEqual({
|
||||
start: { line: 1, character: 0 },
|
||||
end: { line: 2, character: 0 },
|
||||
text: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("removes a final line through the preceding separator", () => {
|
||||
const edit = planReplacement(
|
||||
{ diffStartLine: 1, diffEndLine: 1, replacement: "", removesLines: true },
|
||||
{ lineCount: 2, end: (line) => [6, 6][line] },
|
||||
)
|
||||
|
||||
expect(edit).toEqual({
|
||||
start: { line: 0, character: 6 },
|
||||
end: { line: 1, character: 6 },
|
||||
text: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("preserves a line intentionally rewritten as blank", () => {
|
||||
const edit = planReplacement(
|
||||
{ diffStartLine: 1, diffEndLine: 1, replacement: "", removesLines: false },
|
||||
{ lineCount: 3, end: (line) => [6, 6, 5][line] },
|
||||
)
|
||||
|
||||
expect(edit).toEqual({
|
||||
start: { line: 1, character: 0 },
|
||||
end: { line: 1, character: 6 },
|
||||
text: "",
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user