Skip to content

Commit eb58d2d

Browse files
committed
extract method
1 parent a6e6da0 commit eb58d2d

File tree

2 files changed

+252
-33
lines changed

2 files changed

+252
-33
lines changed

src/services/ghost/GhostInlineCompletionProvider.ts

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,61 @@ import { extractPrefixSuffix } from "./types"
44

55
const MAX_SUGGESTIONS_HISTORY = 20
66

7+
/**
8+
* Find a matching suggestion from the history based on current prefix and suffix
9+
* @param prefix - The text before the cursor position
10+
* @param suffix - The text after the cursor position
11+
* @param suggestionsHistory - Array of previous suggestions (most recent last)
12+
* @returns The matching suggestion text, or null if no match found
13+
*/
14+
export function findMatchingSuggestion(
15+
prefix: string,
16+
suffix: string,
17+
suggestionsHistory: FillInAtCursorSuggestion[],
18+
): string | null {
19+
// Search from most recent to least recent
20+
for (let i = suggestionsHistory.length - 1; i >= 0; i--) {
21+
const fillInAtCursor = suggestionsHistory[i]
22+
23+
// First, try exact prefix/suffix match
24+
if (prefix === fillInAtCursor.prefix && suffix === fillInAtCursor.suffix) {
25+
return fillInAtCursor.text
26+
}
27+
28+
// If no exact match, check for partial typing
29+
// The user may have started typing the suggested text
30+
if (prefix.startsWith(fillInAtCursor.prefix) && suffix === fillInAtCursor.suffix) {
31+
// Extract what the user has typed between the original prefix and current position
32+
const typedContent = prefix.substring(fillInAtCursor.prefix.length)
33+
34+
// Check if the typed content matches the beginning of the suggestion
35+
if (fillInAtCursor.text.startsWith(typedContent)) {
36+
// Return the remaining part of the suggestion (with already-typed portion removed)
37+
return fillInAtCursor.text.substring(typedContent.length)
38+
}
39+
}
40+
}
41+
42+
return null
43+
}
44+
745
export class GhostInlineCompletionProvider implements vscode.InlineCompletionItemProvider {
846
private suggestionsHistory: FillInAtCursorSuggestion[] = []
947

1048
public updateSuggestions(suggestions: GhostSuggestionsState): void {
1149
const fillInAtCursor = suggestions.getFillInAtCursor()
1250

13-
// Only store if we have a fill-in suggestion
1451
if (!fillInAtCursor) {
1552
return
1653
}
1754

18-
// Check if this suggestion already exists in the history
1955
const isDuplicate = this.suggestionsHistory.some(
2056
(existing) =>
2157
existing.text === fillInAtCursor.text &&
2258
existing.prefix === fillInAtCursor.prefix &&
2359
existing.suffix === fillInAtCursor.suffix,
2460
)
2561

26-
// Skip adding if it's a duplicate
2762
if (isDuplicate) {
2863
return
2964
}
@@ -45,36 +80,14 @@ export class GhostInlineCompletionProvider implements vscode.InlineCompletionIte
4580
): vscode.ProviderResult<vscode.InlineCompletionItem[] | vscode.InlineCompletionList> {
4681
const { prefix, suffix } = extractPrefixSuffix(document, position)
4782

48-
// Search from most recent to least recent
49-
for (let i = this.suggestionsHistory.length - 1; i >= 0; i--) {
50-
const fillInAtCursor = this.suggestionsHistory[i]
51-
52-
// First, try exact prefix/suffix match
53-
if (prefix === fillInAtCursor.prefix && suffix === fillInAtCursor.suffix) {
54-
const item: vscode.InlineCompletionItem = {
55-
insertText: fillInAtCursor.text,
56-
range: new vscode.Range(position, position),
57-
}
58-
return [item]
59-
}
83+
const matchingText = findMatchingSuggestion(prefix, suffix, this.suggestionsHistory)
6084

61-
// If no exact match, check for partial typing
62-
// The user may have started typing the suggested text
63-
if (prefix.startsWith(fillInAtCursor.prefix) && suffix === fillInAtCursor.suffix) {
64-
// Extract what the user has typed between the original prefix and current position
65-
const typedContent = prefix.substring(fillInAtCursor.prefix.length)
66-
67-
// Check if the typed content matches the beginning of the suggestion
68-
if (fillInAtCursor.text.startsWith(typedContent)) {
69-
// Return the remaining part of the suggestion (with already-typed portion removed)
70-
const remainingText = fillInAtCursor.text.substring(typedContent.length)
71-
const item: vscode.InlineCompletionItem = {
72-
insertText: remainingText,
73-
range: new vscode.Range(position, position),
74-
}
75-
return [item]
76-
}
85+
if (matchingText !== null) {
86+
const item: vscode.InlineCompletionItem = {
87+
insertText: matchingText,
88+
range: new vscode.Range(position, position),
7789
}
90+
return [item]
7891
}
7992

8093
return []

src/services/ghost/__tests__/GhostInlineCompletionProvider.test.ts

Lines changed: 208 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,214 @@
11
import * as vscode from "vscode"
2-
import { GhostInlineCompletionProvider } from "../GhostInlineCompletionProvider"
3-
import { GhostSuggestionsState } from "../GhostSuggestions"
2+
import { GhostInlineCompletionProvider, findMatchingSuggestion } from "../GhostInlineCompletionProvider"
3+
import { GhostSuggestionsState, FillInAtCursorSuggestion } from "../GhostSuggestions"
44
import { MockTextDocument } from "../../mocking/MockTextDocument"
55

6+
describe("findMatchingSuggestion", () => {
7+
describe("exact matching", () => {
8+
it("should return suggestion text when prefix and suffix match exactly", () => {
9+
const suggestions: FillInAtCursorSuggestion[] = [
10+
{
11+
text: "console.log('Hello, World!');",
12+
prefix: "const x = 1",
13+
suffix: "\nconst y = 2",
14+
},
15+
]
16+
17+
const result = findMatchingSuggestion("const x = 1", "\nconst y = 2", suggestions)
18+
expect(result).toBe("console.log('Hello, World!');")
19+
})
20+
21+
it("should return null when prefix does not match", () => {
22+
const suggestions: FillInAtCursorSuggestion[] = [
23+
{
24+
text: "console.log('test');",
25+
prefix: "const x = 1",
26+
suffix: "\nconst y = 2",
27+
},
28+
]
29+
30+
const result = findMatchingSuggestion("different prefix", "\nconst y = 2", suggestions)
31+
expect(result).toBeNull()
32+
})
33+
34+
it("should return null when suffix does not match", () => {
35+
const suggestions: FillInAtCursorSuggestion[] = [
36+
{
37+
text: "console.log('test');",
38+
prefix: "const x = 1",
39+
suffix: "\nconst y = 2",
40+
},
41+
]
42+
43+
const result = findMatchingSuggestion("const x = 1", "different suffix", suggestions)
44+
expect(result).toBeNull()
45+
})
46+
47+
it("should return null when suggestions array is empty", () => {
48+
const result = findMatchingSuggestion("const x = 1", "\nconst y = 2", [])
49+
expect(result).toBeNull()
50+
})
51+
})
52+
53+
describe("partial typing support", () => {
54+
it("should return remaining suggestion when user has partially typed", () => {
55+
const suggestions: FillInAtCursorSuggestion[] = [
56+
{
57+
text: "console.log('Hello, World!');",
58+
prefix: "const x = 1",
59+
suffix: "\nconst y = 2",
60+
},
61+
]
62+
63+
// User typed "cons" after the prefix
64+
const result = findMatchingSuggestion("const x = 1cons", "\nconst y = 2", suggestions)
65+
expect(result).toBe("ole.log('Hello, World!');")
66+
})
67+
68+
it("should return full suggestion when no partial typing", () => {
69+
const suggestions: FillInAtCursorSuggestion[] = [
70+
{
71+
text: "console.log('test');",
72+
prefix: "const x = 1",
73+
suffix: "\nconst y = 2",
74+
},
75+
]
76+
77+
const result = findMatchingSuggestion("const x = 1", "\nconst y = 2", suggestions)
78+
expect(result).toBe("console.log('test');")
79+
})
80+
81+
it("should return null when partially typed content does not match suggestion", () => {
82+
const suggestions: FillInAtCursorSuggestion[] = [
83+
{
84+
text: "console.log('test');",
85+
prefix: "const x = 1",
86+
suffix: "\nconst y = 2",
87+
},
88+
]
89+
90+
// User typed "xyz" which doesn't match the suggestion
91+
const result = findMatchingSuggestion("const x = 1xyz", "\nconst y = 2", suggestions)
92+
expect(result).toBeNull()
93+
})
94+
95+
it("should return empty string when user has typed entire suggestion", () => {
96+
const suggestions: FillInAtCursorSuggestion[] = [
97+
{
98+
text: "console.log('test');",
99+
prefix: "const x = 1",
100+
suffix: "\nconst y = 2",
101+
},
102+
]
103+
104+
const result = findMatchingSuggestion("const x = 1console.log('test');", "\nconst y = 2", suggestions)
105+
expect(result).toBe("")
106+
})
107+
108+
it("should return null when suffix has changed during partial typing", () => {
109+
const suggestions: FillInAtCursorSuggestion[] = [
110+
{
111+
text: "console.log('test');",
112+
prefix: "const x = 1",
113+
suffix: "\nconst y = 2",
114+
},
115+
]
116+
117+
// User typed partial content but suffix changed
118+
const result = findMatchingSuggestion("const x = 1cons", "\nconst y = 3", suggestions)
119+
expect(result).toBeNull()
120+
})
121+
122+
it("should handle multi-character partial typing", () => {
123+
const suggestions: FillInAtCursorSuggestion[] = [
124+
{
125+
text: "function test() { return 42; }",
126+
prefix: "const x = 1",
127+
suffix: "\nconst y = 2",
128+
},
129+
]
130+
131+
// User typed "function te"
132+
const result = findMatchingSuggestion("const x = 1function te", "\nconst y = 2", suggestions)
133+
expect(result).toBe("st() { return 42; }")
134+
})
135+
136+
it("should be case-sensitive in partial matching", () => {
137+
const suggestions: FillInAtCursorSuggestion[] = [
138+
{
139+
text: "Console.log('test');",
140+
prefix: "const x = 1",
141+
suffix: "\nconst y = 2",
142+
},
143+
]
144+
145+
// User typed "cons" (lowercase) but suggestion starts with "Console" (uppercase)
146+
const result = findMatchingSuggestion("const x = 1cons", "\nconst y = 2", suggestions)
147+
expect(result).toBeNull()
148+
})
149+
})
150+
151+
describe("multiple suggestions", () => {
152+
it("should prefer most recent matching suggestion", () => {
153+
const suggestions: FillInAtCursorSuggestion[] = [
154+
{
155+
text: "first suggestion",
156+
prefix: "const x = 1",
157+
suffix: "\nconst y = 2",
158+
},
159+
{
160+
text: "second suggestion",
161+
prefix: "const x = 1",
162+
suffix: "\nconst y = 2",
163+
},
164+
]
165+
166+
const result = findMatchingSuggestion("const x = 1", "\nconst y = 2", suggestions)
167+
expect(result).toBe("second suggestion")
168+
})
169+
170+
it("should match different suggestions based on context", () => {
171+
const suggestions: FillInAtCursorSuggestion[] = [
172+
{
173+
text: "first suggestion",
174+
prefix: "const x = 1",
175+
suffix: "\nconst y = 2",
176+
},
177+
{
178+
text: "second suggestion",
179+
prefix: "const a = 1",
180+
suffix: "\nconst b = 2",
181+
},
182+
]
183+
184+
const result1 = findMatchingSuggestion("const x = 1", "\nconst y = 2", suggestions)
185+
expect(result1).toBe("first suggestion")
186+
187+
const result2 = findMatchingSuggestion("const a = 1", "\nconst b = 2", suggestions)
188+
expect(result2).toBe("second suggestion")
189+
})
190+
191+
it("should prefer exact match over partial match", () => {
192+
const suggestions: FillInAtCursorSuggestion[] = [
193+
{
194+
text: "console.log('partial');",
195+
prefix: "const x = 1",
196+
suffix: "\nconst y = 2",
197+
},
198+
{
199+
text: "exact match",
200+
prefix: "const x = 1cons",
201+
suffix: "\nconst y = 2",
202+
},
203+
]
204+
205+
// User is at position that matches exact prefix of second suggestion
206+
const result = findMatchingSuggestion("const x = 1cons", "\nconst y = 2", suggestions)
207+
expect(result).toBe("exact match")
208+
})
209+
})
210+
})
211+
6212
describe("GhostInlineCompletionProvider", () => {
7213
let provider: GhostInlineCompletionProvider
8214
let mockDocument: vscode.TextDocument

0 commit comments

Comments
 (0)