Skip to content

Commit 096d6f8

Browse files
authored
Merge pull request #3194 from Kilo-Org/mark/prevent-autocomplete-trigger-without-typing
try to prevent autocomplete to trigger when the user wasnt typing
2 parents e1802f3 + beb8d6b commit 096d6f8

File tree

3 files changed

+277
-0
lines changed

3 files changed

+277
-0
lines changed

.changeset/funny-shoes-hang.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"kilo-code": patch
3+
---
4+
5+
Do not trigger autocomplete for external events, like git changes

src/services/ghost/GhostProvider.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,9 +204,37 @@ export class GhostProvider {
204204
if (this.workspaceEdit.isLocked()) {
205205
return
206206
}
207+
208+
// Filter out undo/redo operations
209+
if (event.reason !== undefined) {
210+
return
211+
}
212+
207213
if (event.contentChanges.length === 0) {
208214
return
209215
}
216+
217+
// Heuristic to filter out bulk changes (git operations, external edits)
218+
const isBulkChange = event.contentChanges.some((change) => change.rangeLength > 100 || change.text.length > 100)
219+
if (isBulkChange) {
220+
return
221+
}
222+
223+
// Heuristic to filter out changes far from cursor (likely external or LLM edits)
224+
const editor = vscode.window.activeTextEditor
225+
if (!editor || editor.document !== event.document) {
226+
return
227+
}
228+
229+
const cursorPos = editor.selection.active
230+
const isNearCursor = event.contentChanges.some((change) => {
231+
const distance = Math.abs(cursorPos.line - change.range.start.line)
232+
return distance <= 2
233+
})
234+
if (!isNearCursor) {
235+
return
236+
}
237+
210238
await this.documentStore.storeDocument({ document: event.document })
211239
this.lastTextChangeTime = Date.now()
212240
this.handleTypingEvent(event)

src/services/ghost/__tests__/GhostProvider.spec.ts

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,4 +267,248 @@ console.log('test');]]></replace></change>`
267267
expect(result.suggestions.hasSuggestions()).toBe(true)
268268
})
269269
})
270+
271+
describe("onDidChangeTextDocument filtering", () => {
272+
it("should process user typing (small changes near cursor)", async () => {
273+
const initialContent = `console.log('test');`
274+
const { testUri, mockDocument } = await setupTestDocument("typing.js", initialContent)
275+
276+
// Mock active editor with cursor at line 0
277+
;(vscode.window as any).activeTextEditor = {
278+
document: mockDocument,
279+
selection: {
280+
active: new vscode.Position(0, 10),
281+
},
282+
}
283+
284+
// Simulate user typing - small change near cursor
285+
const event = {
286+
document: mockDocument,
287+
contentChanges: [
288+
{
289+
range: new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 10)),
290+
rangeLength: 0,
291+
text: "a",
292+
},
293+
],
294+
reason: undefined, // User typing has no reason
295+
}
296+
297+
// This should be processed (not filtered out)
298+
// We can't directly test the private method, but we can verify the logic
299+
expect(event.reason).toBeUndefined()
300+
expect(event.contentChanges.length).toBeGreaterThan(0)
301+
expect(event.contentChanges[0].text.length).toBeLessThanOrEqual(100)
302+
})
303+
304+
it("should filter out undo operations", async () => {
305+
const initialContent = `console.log('test');`
306+
const { mockDocument } = await setupTestDocument("undo.js", initialContent)
307+
308+
const event = {
309+
document: mockDocument,
310+
contentChanges: [
311+
{
312+
range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)),
313+
rangeLength: 10,
314+
text: "",
315+
},
316+
],
317+
reason: 1, // Undo
318+
}
319+
320+
// Should be filtered out
321+
expect(event.reason).toBe(1)
322+
})
323+
324+
it("should filter out redo operations", async () => {
325+
const initialContent = `console.log('test');`
326+
const { mockDocument } = await setupTestDocument("redo.js", initialContent)
327+
328+
const event = {
329+
document: mockDocument,
330+
contentChanges: [
331+
{
332+
range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 10)),
333+
rangeLength: 0,
334+
text: "console.log",
335+
},
336+
],
337+
reason: 2, // Redo
338+
}
339+
340+
// Should be filtered out
341+
expect(event.reason).toBe(2)
342+
})
343+
344+
it("should filter out bulk changes (git operations)", async () => {
345+
const initialContent = `console.log('test');`
346+
const { mockDocument } = await setupTestDocument("bulk.js", initialContent)
347+
348+
// Simulate git checkout - large text replacement
349+
const largeText = "a".repeat(150)
350+
const event = {
351+
document: mockDocument,
352+
contentChanges: [
353+
{
354+
range: new vscode.Range(new vscode.Position(0, 0), new vscode.Position(0, 20)),
355+
rangeLength: 20,
356+
text: largeText,
357+
},
358+
],
359+
reason: undefined,
360+
}
361+
362+
// Should be filtered out due to bulk change
363+
const isBulkChange = event.contentChanges.some(
364+
(change) => change.rangeLength > 100 || change.text.length > 100,
365+
)
366+
expect(isBulkChange).toBe(true)
367+
})
368+
369+
it("should filter out changes far from cursor (LLM edits)", async () => {
370+
const initialContent = `line 1\nline 2\nline 3\nline 4\nline 5\nline 6\nline 7\nline 8`
371+
const { mockDocument } = await setupTestDocument("far.js", initialContent)
372+
373+
// Mock active editor with cursor at line 0
374+
;(vscode.window as any).activeTextEditor = {
375+
document: mockDocument,
376+
selection: {
377+
active: new vscode.Position(0, 0),
378+
},
379+
}
380+
381+
// Simulate change at line 7 (far from cursor at line 0)
382+
const event = {
383+
document: mockDocument,
384+
contentChanges: [
385+
{
386+
range: new vscode.Range(new vscode.Position(7, 0), new vscode.Position(7, 6)),
387+
rangeLength: 6,
388+
text: "modified",
389+
},
390+
],
391+
reason: undefined,
392+
}
393+
394+
// Should be filtered out - change is more than 2 lines away
395+
const cursorPos = (vscode.window as any).activeTextEditor.selection.active
396+
const isNearCursor = event.contentChanges.some((change) => {
397+
const distance = Math.abs(cursorPos.line - change.range.start.line)
398+
return distance <= 2
399+
})
400+
expect(isNearCursor).toBe(false)
401+
})
402+
403+
it("should allow changes within 2 lines of cursor", async () => {
404+
const initialContent = `line 1\nline 2\nline 3\nline 4\nline 5`
405+
const { mockDocument } = await setupTestDocument("near.js", initialContent)
406+
407+
// Mock active editor with cursor at line 2
408+
;(vscode.window as any).activeTextEditor = {
409+
document: mockDocument,
410+
selection: {
411+
active: new vscode.Position(2, 0),
412+
},
413+
}
414+
415+
// Simulate change at line 4 (2 lines away from cursor)
416+
const event = {
417+
document: mockDocument,
418+
contentChanges: [
419+
{
420+
range: new vscode.Range(new vscode.Position(4, 0), new vscode.Position(4, 6)),
421+
rangeLength: 6,
422+
text: "modified",
423+
},
424+
],
425+
reason: undefined,
426+
}
427+
428+
// Should NOT be filtered out - change is within 2 lines
429+
const cursorPos = (vscode.window as any).activeTextEditor.selection.active
430+
const isNearCursor = event.contentChanges.some((change) => {
431+
const distance = Math.abs(cursorPos.line - change.range.start.line)
432+
return distance <= 2
433+
})
434+
expect(isNearCursor).toBe(true)
435+
})
436+
437+
it("should filter out changes to non-active documents", async () => {
438+
const initialContent = `console.log('test');`
439+
const { mockDocument } = await setupTestDocument("inactive.js", initialContent)
440+
441+
// Create a different document for the active editor
442+
const activeContent = `console.log('active');`
443+
const activeUri = vscode.Uri.parse("file://active.js")
444+
mockWorkspace.addDocument(activeUri, activeContent)
445+
const activeDocument = await mockWorkspace.openTextDocument(activeUri)
446+
447+
// Mock active editor with a different document
448+
;(vscode.window as any).activeTextEditor = {
449+
document: activeDocument,
450+
selection: {
451+
active: new vscode.Position(0, 10),
452+
},
453+
}
454+
455+
// Simulate change to the non-active document
456+
const event = {
457+
document: mockDocument,
458+
contentChanges: [
459+
{
460+
range: new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 10)),
461+
rangeLength: 0,
462+
text: "a",
463+
},
464+
],
465+
reason: undefined,
466+
}
467+
468+
// Should be filtered out - document doesn't match active editor
469+
const editor = (vscode.window as any).activeTextEditor
470+
const shouldProcess = editor && editor.document === event.document
471+
expect(shouldProcess).toBe(false)
472+
})
473+
474+
it("should allow small paste operations near cursor", async () => {
475+
const initialContent = `console.log('test');`
476+
const { mockDocument } = await setupTestDocument("paste.js", initialContent)
477+
478+
// Mock active editor with cursor at line 0
479+
;(vscode.window as any).activeTextEditor = {
480+
document: mockDocument,
481+
selection: {
482+
active: new vscode.Position(0, 10),
483+
},
484+
}
485+
486+
// Simulate paste of 50 characters (under threshold)
487+
const pastedText = "a".repeat(50)
488+
const event = {
489+
document: mockDocument,
490+
contentChanges: [
491+
{
492+
range: new vscode.Range(new vscode.Position(0, 10), new vscode.Position(0, 10)),
493+
rangeLength: 0,
494+
text: pastedText,
495+
},
496+
],
497+
reason: undefined,
498+
}
499+
500+
// Should NOT be filtered out - paste is small and near cursor
501+
const isBulkChange = event.contentChanges.some(
502+
(change) => change.rangeLength > 100 || change.text.length > 100,
503+
)
504+
expect(isBulkChange).toBe(false)
505+
506+
const cursorPos = (vscode.window as any).activeTextEditor.selection.active
507+
const isNearCursor = event.contentChanges.some((change) => {
508+
const distance = Math.abs(cursorPos.line - change.range.start.line)
509+
return distance <= 2
510+
})
511+
expect(isNearCursor).toBe(true)
512+
})
513+
})
270514
})

0 commit comments

Comments
 (0)