Skip to content

Commit 97ce14f

Browse files
authored
Merge pull request Kilo-Org#4308 from Kilo-Org/mark/skip-end-of-line-autocomplete
feat(autocomplete): add contextual skip logic for smart autocomplete triggering
2 parents 966744b + 8685c7d commit 97ce14f

File tree

4 files changed

+466
-0
lines changed

4 files changed

+466
-0
lines changed

.changeset/seven-seas-show.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+
Minor tuning to autocomplete

src/services/ghost/classic-auto-complete/GhostInlineCompletionProvider.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { RecentlyVisitedRangesService } from "../../continuedev/core/vscode-test
2121
import { RecentlyEditedTracker } from "../../continuedev/core/vscode-test-harness/src/autocomplete/recentlyEdited"
2222
import type { GhostServiceSettings } from "@roo-code/types"
2323
import { postprocessGhostSuggestion } from "./uselessSuggestionFilter"
24+
import { shouldSkipAutocomplete } from "./contextualSkip"
2425
import { RooIgnoreController } from "../../../core/ignore/RooIgnoreController"
2526
import { ClineProvider } from "../../../core/webview/ClineProvider"
2627
import { AutocompleteTelemetry } from "./AutocompleteTelemetry"
@@ -385,13 +386,20 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
385386

386387
const { prefix, suffix } = extractPrefixSuffix(document, position)
387388

389+
// Check cache first - allow mid-word lookups from cache
388390
const matchingResult = findMatchingSuggestion(prefix, suffix, this.suggestionsHistory)
389391

390392
if (matchingResult !== null) {
391393
this.telemetry?.captureCacheHit(matchingResult.matchType, telemetryContext, matchingResult.text.length)
392394
return stringToInlineCompletions(matchingResult.text, position)
393395
}
394396

397+
// Only skip new LLM requests during mid-word typing or at end of statement
398+
// Cache lookups above are still allowed
399+
if (shouldSkipAutocomplete(prefix, suffix, document.languageId)) {
400+
return []
401+
}
402+
395403
const { prompt, prefix: promptPrefix, suffix: promptSuffix } = await this.getPrompt(document, position)
396404

397405
// Update context with strategy now that we know it
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import { getTerminatorsForLanguage, shouldSkipAutocomplete } from "../contextualSkip"
2+
3+
/**
4+
* Tests for shouldSkipAutocomplete behavior.
5+
* These tests verify that shouldSkipAutocomplete correctly handles
6+
* end-of-statement detection (returning true when at end of statement).
7+
*/
8+
describe("shouldSkipAutocomplete - end of statement detection", () => {
9+
describe("C-like languages (JavaScript, TypeScript, etc.)", () => {
10+
const languageId = "typescript"
11+
12+
it("should skip when cursor is after semicolon at end of line", () => {
13+
expect(shouldSkipAutocomplete("console.info('foo');", "\n", languageId)).toBe(true)
14+
expect(shouldSkipAutocomplete("const x = 5;", "\nconst y = 10;", languageId)).toBe(true)
15+
expect(shouldSkipAutocomplete("return value;", "\n}", languageId)).toBe(true)
16+
})
17+
18+
it("should skip when cursor is after closing parenthesis at end of line", () => {
19+
expect(shouldSkipAutocomplete("myFunction()", "\n", languageId)).toBe(true)
20+
expect(shouldSkipAutocomplete("if (condition)", "\n{", languageId)).toBe(true)
21+
expect(shouldSkipAutocomplete("console.log(x)", "", languageId)).toBe(true)
22+
})
23+
24+
it("should skip when cursor is after closing brace at end of line", () => {
25+
expect(shouldSkipAutocomplete("}", "\n", languageId)).toBe(true)
26+
expect(shouldSkipAutocomplete("const obj = {}", "\n", languageId)).toBe(true)
27+
expect(shouldSkipAutocomplete("function test() {}", "", languageId)).toBe(true)
28+
})
29+
30+
it("should NOT skip when cursor is after closing bracket (not a statement terminator)", () => {
31+
// Brackets are not statement terminators in C-like languages
32+
expect(shouldSkipAutocomplete("const arr = []", "\n", languageId)).toBe(false)
33+
expect(shouldSkipAutocomplete("items[0]", "\n", languageId)).toBe(false)
34+
})
35+
36+
it("should NOT skip when cursor is after comma (not a statement terminator)", () => {
37+
// Commas are not terminators - they indicate continuation
38+
expect(shouldSkipAutocomplete(" item1,", "\n item2,", languageId)).toBe(false)
39+
expect(shouldSkipAutocomplete("const x = 1,", "\n", languageId)).toBe(false)
40+
})
41+
42+
it("should NOT skip when cursor is after colon", () => {
43+
// Colons are not terminators in JS/TS
44+
expect(shouldSkipAutocomplete(" key:", "\n", languageId)).toBe(false)
45+
expect(shouldSkipAutocomplete("case 'test':", "\n", languageId)).toBe(false)
46+
})
47+
48+
it("should skip when cursor is after angle bracket followed by ) terminator", () => {
49+
// ) is the terminator here
50+
expect(shouldSkipAutocomplete("if (x > 5)", "\n", languageId)).toBe(true)
51+
})
52+
53+
it("should NOT skip when cursor is after generic type (not a terminator)", () => {
54+
expect(shouldSkipAutocomplete("Array<string>", "\n", languageId)).toBe(false)
55+
})
56+
57+
it("should NOT skip when cursor is after quote (not a statement terminator)", () => {
58+
// Quotes are not statement terminators - the statement might continue
59+
expect(shouldSkipAutocomplete('const s = "hello"', "\n", languageId)).toBe(false)
60+
expect(shouldSkipAutocomplete("const s = 'world'", "\n", languageId)).toBe(false)
61+
})
62+
63+
it("should skip with trailing whitespace after terminator", () => {
64+
expect(shouldSkipAutocomplete("console.log(); ", "\n", languageId)).toBe(true)
65+
expect(shouldSkipAutocomplete("const x = 5;\t", "\n", languageId)).toBe(true)
66+
})
67+
})
68+
69+
describe("Python", () => {
70+
const languageId = "python"
71+
72+
it("should skip when cursor is after closing parenthesis", () => {
73+
expect(shouldSkipAutocomplete("print('hello')", "\n", languageId)).toBe(true)
74+
expect(shouldSkipAutocomplete("my_func()", "\n", languageId)).toBe(true)
75+
})
76+
77+
it("should skip when cursor is after closing bracket", () => {
78+
expect(shouldSkipAutocomplete("my_list = []", "\n", languageId)).toBe(true)
79+
expect(shouldSkipAutocomplete("items[0]", "\n", languageId)).toBe(true)
80+
})
81+
82+
it("should skip when cursor is after closing brace", () => {
83+
expect(shouldSkipAutocomplete("my_dict = {}", "\n", languageId)).toBe(true)
84+
})
85+
86+
it("should NOT skip when cursor is after colon (starts a block)", () => {
87+
// In Python, colon starts a block - autocomplete should suggest the block body
88+
expect(shouldSkipAutocomplete("def foo():", "\n", languageId)).toBe(false)
89+
expect(shouldSkipAutocomplete("if condition:", "\n", languageId)).toBe(false)
90+
expect(shouldSkipAutocomplete("for i in range(10):", "\n", languageId)).toBe(false)
91+
})
92+
93+
it("should NOT skip when cursor is after semicolon (rare in Python)", () => {
94+
// Python doesn't use semicolons as statement terminators
95+
expect(shouldSkipAutocomplete("x = 5;", "\n", languageId)).toBe(false)
96+
})
97+
})
98+
99+
describe("HTML/Markup languages", () => {
100+
const languageId = "html"
101+
102+
it("should NOT skip in markup languages (no terminators defined)", () => {
103+
// Markup languages don't have statement terminators - > could be mid-line
104+
expect(shouldSkipAutocomplete("<div>", "\n", languageId)).toBe(false)
105+
expect(shouldSkipAutocomplete("</div>", "\n", languageId)).toBe(false)
106+
expect(shouldSkipAutocomplete("<br />", "\n", languageId)).toBe(false)
107+
})
108+
109+
it("should skip when cursor is mid-word in incomplete tag", () => {
110+
// Mid-word typing is blocked (word length > 2)
111+
expect(shouldSkipAutocomplete("<div", "\n", languageId)).toBe(true)
112+
})
113+
114+
it("should NOT skip when cursor is after equals in attribute", () => {
115+
// = is not a terminator
116+
expect(shouldSkipAutocomplete("<div class=", "\n", languageId)).toBe(false)
117+
})
118+
})
119+
120+
describe("Shell/Bash", () => {
121+
const languageId = "shellscript"
122+
123+
it("should skip when cursor is after semicolon", () => {
124+
expect(shouldSkipAutocomplete("echo hello;", "\n", languageId)).toBe(true)
125+
})
126+
127+
it("should skip when cursor is after fi (shell terminator)", () => {
128+
// "fi" is a statement terminator in shell scripts (ends if blocks)
129+
expect(shouldSkipAutocomplete("fi", "\n", languageId)).toBe(true)
130+
})
131+
132+
it("should skip when cursor is after done (mid-word typing, > 2 chars)", () => {
133+
// "done" is 4 chars, triggers mid-word skip logic
134+
expect(shouldSkipAutocomplete("done", "\n", languageId)).toBe(true)
135+
})
136+
137+
it("should skip when fi/done is part of a larger word (mid-word typing)", () => {
138+
expect(shouldSkipAutocomplete("wifi", "\n", languageId)).toBe(true) // mid-word typing
139+
expect(shouldSkipAutocomplete("undone", "\n", languageId)).toBe(true) // mid-word typing
140+
})
141+
})
142+
143+
describe("SQL", () => {
144+
const languageId = "sql"
145+
146+
it("should skip when cursor is after semicolon", () => {
147+
expect(shouldSkipAutocomplete("SELECT * FROM users;", "\n", languageId)).toBe(true)
148+
})
149+
150+
it("should skip when cursor is mid-word in incomplete statement", () => {
151+
// "FROM" is mid-word typing (word length > 2)
152+
expect(shouldSkipAutocomplete("SELECT * FROM", "\n", languageId)).toBe(true)
153+
})
154+
155+
it("should NOT skip when cursor is after space in incomplete statement", () => {
156+
// Space is not a terminator
157+
expect(shouldSkipAutocomplete("SELECT * FROM ", "\n", languageId)).toBe(false)
158+
})
159+
})
160+
161+
describe("should NOT skip autocomplete in valid positions", () => {
162+
const languageId = "typescript"
163+
164+
it("should NOT skip when cursor is mid-line with content after", () => {
165+
// When cursor is after a non-word character (like `.` or space), we should NOT skip
166+
// even if the suffix starts with a word character
167+
expect(shouldSkipAutocomplete("console.", "log();\n", languageId)).toBe(true)
168+
expect(shouldSkipAutocomplete("const x = ", " + 1;\n", languageId)).toBe(false)
169+
// When suffix starts with a word character, skip is triggered
170+
expect(shouldSkipAutocomplete("if (", "condition) {\n", languageId)).toBe(true)
171+
})
172+
173+
it("should NOT skip when cursor is at empty line", () => {
174+
expect(shouldSkipAutocomplete("function test() {\n", "\n}", languageId)).toBe(false)
175+
expect(shouldSkipAutocomplete("", "\n", languageId)).toBe(false)
176+
expect(shouldSkipAutocomplete(" ", "\n", languageId)).toBe(false)
177+
})
178+
179+
it("should NOT skip when line ends with incomplete statement", () => {
180+
expect(shouldSkipAutocomplete("const x =", "\n", languageId)).toBe(false)
181+
expect(shouldSkipAutocomplete("const x = 5 +", "\n", languageId)).toBe(false)
182+
expect(shouldSkipAutocomplete("if (x", "\n", languageId)).toBe(false)
183+
expect(shouldSkipAutocomplete("function test(", "\n", languageId)).toBe(false)
184+
})
185+
186+
it("should NOT skip when line ends with opening brace", () => {
187+
expect(shouldSkipAutocomplete("function test() {", "\n", languageId)).toBe(false)
188+
expect(shouldSkipAutocomplete("if (condition) {", "\n", languageId)).toBe(false)
189+
expect(shouldSkipAutocomplete("const obj = {", "\n", languageId)).toBe(false)
190+
})
191+
192+
it("should NOT skip when line ends with opening bracket", () => {
193+
expect(shouldSkipAutocomplete("const arr = [", "\n", languageId)).toBe(false)
194+
})
195+
196+
it("should NOT skip when line ends with opening parenthesis", () => {
197+
expect(shouldSkipAutocomplete("myFunction(", "\n", languageId)).toBe(false)
198+
expect(shouldSkipAutocomplete("console.log(", "\n", languageId)).toBe(false)
199+
})
200+
201+
it("should NOT skip when line ends with operator", () => {
202+
expect(shouldSkipAutocomplete("const x = a +", "\n", languageId)).toBe(false)
203+
expect(shouldSkipAutocomplete("const y = b &&", "\n", languageId)).toBe(false)
204+
expect(shouldSkipAutocomplete("const z = c ||", "\n", languageId)).toBe(false)
205+
})
206+
207+
it("should NOT skip when line ends with dot (property access)", () => {
208+
expect(shouldSkipAutocomplete("object.", "\n", languageId)).toBe(false)
209+
expect(shouldSkipAutocomplete("this.", "\n", languageId)).toBe(false)
210+
})
211+
212+
it("should NOT skip when line ends with equals sign (incomplete assignment)", () => {
213+
expect(shouldSkipAutocomplete("const fn =", "\n", languageId)).toBe(false)
214+
})
215+
})
216+
217+
describe("edge cases", () => {
218+
it("should NOT skip for empty strings", () => {
219+
expect(shouldSkipAutocomplete("", "")).toBe(false)
220+
})
221+
222+
it("should skip with whitespace suffix after terminator", () => {
223+
expect(shouldSkipAutocomplete("const x = 5;", " ", "typescript")).toBe(true)
224+
expect(shouldSkipAutocomplete("const x = 5;", "\t\t", "typescript")).toBe(true)
225+
})
226+
227+
it("should skip for multiline prefix ending with terminator", () => {
228+
const prefix = "function test() {\n const x = 5;\n return x;"
229+
expect(shouldSkipAutocomplete(prefix, "\n}", "typescript")).toBe(true)
230+
})
231+
232+
it("should skip for multiline suffix with terminator", () => {
233+
const suffix = "\n const y = 10;\n}"
234+
expect(shouldSkipAutocomplete("const x = 5;", suffix, "typescript")).toBe(true)
235+
})
236+
237+
it("should use default terminators when no languageId provided", () => {
238+
// Default terminators are ; } )
239+
expect(shouldSkipAutocomplete("const x = 5;", "\n")).toBe(true)
240+
expect(shouldSkipAutocomplete("}", "\n")).toBe(true)
241+
expect(shouldSkipAutocomplete("foo()", "\n")).toBe(true)
242+
})
243+
244+
it("should use default terminators for unknown languages", () => {
245+
expect(shouldSkipAutocomplete("const x = 5;", "\n", "unknown-language")).toBe(true)
246+
expect(shouldSkipAutocomplete("}", "\n", "unknown-language")).toBe(true)
247+
})
248+
})
249+
250+
describe("getTerminatorsForLanguage", () => {
251+
it("should return c-like terminators for JavaScript/TypeScript", () => {
252+
const terminators = getTerminatorsForLanguage("typescript")
253+
expect(terminators.includes(";")).toBe(true)
254+
expect(terminators.includes("}")).toBe(true)
255+
expect(terminators.includes(")")).toBe(true)
256+
expect(terminators.includes(",")).toBe(false)
257+
expect(terminators.includes(":")).toBe(false)
258+
})
259+
260+
it("should return python terminators", () => {
261+
const terminators = getTerminatorsForLanguage("python")
262+
expect(terminators.includes(")")).toBe(true)
263+
expect(terminators.includes("]")).toBe(true)
264+
expect(terminators.includes("}")).toBe(true)
265+
expect(terminators.includes(";")).toBe(false)
266+
expect(terminators.includes(":")).toBe(false)
267+
})
268+
269+
it("should return empty terminators for HTML (markup)", () => {
270+
const terminators = getTerminatorsForLanguage("html")
271+
expect(terminators.length).toBe(0)
272+
expect(terminators.includes(">")).toBe(false)
273+
expect(terminators.includes(";")).toBe(false)
274+
})
275+
276+
it("should return shell terminators", () => {
277+
const terminators = getTerminatorsForLanguage("shellscript")
278+
expect(terminators.includes(";")).toBe(true)
279+
expect(terminators.includes("fi")).toBe(true)
280+
expect(terminators.includes("done")).toBe(true)
281+
})
282+
283+
it("should return default terminators for unknown languages", () => {
284+
const terminators = getTerminatorsForLanguage("some-unknown-language")
285+
expect(terminators.includes(";")).toBe(true)
286+
expect(terminators.includes("}")).toBe(true)
287+
expect(terminators.includes(")")).toBe(true)
288+
})
289+
})
290+
})
291+
292+
describe("shouldSkipAutocomplete - mid-word typing", () => {
293+
it("should skip when typing in the middle of a long word", () => {
294+
expect(shouldSkipAutocomplete("myVaria", "\n", "typescript")).toBe(true)
295+
expect(shouldSkipAutocomplete("functionN", "\n", "typescript")).toBe(true)
296+
expect(shouldSkipAutocomplete("console", "\n", "typescript")).toBe(true)
297+
})
298+
299+
it("should NOT skip for short words (1-2 chars)", () => {
300+
// Short words might be the start of a new identifier
301+
expect(shouldSkipAutocomplete("my", "\n", "typescript")).toBe(false)
302+
expect(shouldSkipAutocomplete("x", "\n", "typescript")).toBe(false)
303+
})
304+
})
305+
306+
describe("shouldSkipAutocomplete - general behavior", () => {
307+
it("should NOT skip for empty prefix", () => {
308+
expect(shouldSkipAutocomplete("", "")).toBe(false)
309+
expect(shouldSkipAutocomplete("", "some content")).toBe(false)
310+
})
311+
312+
it("should work without languageId", () => {
313+
expect(shouldSkipAutocomplete("const ", "\n")).toBe(false)
314+
expect(shouldSkipAutocomplete("object.", "\n")).toBe(false)
315+
expect(shouldSkipAutocomplete("const x = 5;", "\n")).toBe(true)
316+
})
317+
})

0 commit comments

Comments
 (0)