Skip to content

Commit 14c0bbf

Browse files
committed
feat: implement AI tab completion feature
- Add configuration options for AI tab completion in package.json - Create AiCompletionProvider class with inline completion support - Register completion provider in extension.ts - Add comprehensive tests for the completion provider - Support multiple AI providers (Anthropic, OpenAI, OpenRouter, etc.) - Implement debouncing and proper error handling - Add localization strings for configuration options Closes #6644
1 parent a1439c1 commit 14c0bbf

File tree

5 files changed

+689
-1
lines changed

5 files changed

+689
-1
lines changed

src/extension.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { MdmService } from "./services/mdm/MdmService"
3030
import { migrateSettings } from "./utils/migrateSettings"
3131
import { autoImportSettings } from "./utils/autoImportSettings"
3232
import { API } from "./extension/api"
33+
import { AiCompletionProvider } from "./integrations/completion/AiCompletionProvider"
3334

3435
import {
3536
handleUri,
@@ -192,6 +193,45 @@ export async function activate(context: vscode.ExtensionContext) {
192193
registerCodeActions(context)
193194
registerTerminalActions(context)
194195

196+
// Register AI completion provider
197+
const aiCompletionEnabled = vscode.workspace
198+
.getConfiguration(Package.name)
199+
.get<boolean>("aiTabCompletion.enabled", false)
200+
if (aiCompletionEnabled) {
201+
const aiCompletionProvider = new AiCompletionProvider(outputChannel)
202+
context.subscriptions.push(
203+
vscode.languages.registerInlineCompletionItemProvider({ pattern: "**/*" }, aiCompletionProvider),
204+
aiCompletionProvider,
205+
)
206+
outputChannel.appendLine("AI Tab Completion: Provider registered")
207+
}
208+
209+
// Watch for configuration changes to enable/disable AI completion
210+
context.subscriptions.push(
211+
vscode.workspace.onDidChangeConfiguration((e) => {
212+
if (e.affectsConfiguration(`${Package.name}.aiTabCompletion.enabled`)) {
213+
const nowEnabled = vscode.workspace
214+
.getConfiguration(Package.name)
215+
.get<boolean>("aiTabCompletion.enabled", false)
216+
if (nowEnabled) {
217+
outputChannel.appendLine(
218+
"AI Tab Completion: Configuration changed - reloading extension recommended to enable",
219+
)
220+
vscode.window
221+
.showInformationMessage(
222+
"AI Tab Completion has been enabled. Please reload the window for changes to take effect.",
223+
"Reload Window",
224+
)
225+
.then((selection) => {
226+
if (selection === "Reload Window") {
227+
vscode.commands.executeCommand("workbench.action.reloadWindow")
228+
}
229+
})
230+
}
231+
}
232+
}),
233+
)
234+
195235
// Allows other extensions to activate once Roo is ready.
196236
vscode.commands.executeCommand(`${Package.name}.activationCompleted`)
197237

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
import * as vscode from "vscode"
2+
import { Anthropic } from "@anthropic-ai/sdk"
3+
import debounce from "lodash.debounce"
4+
5+
import { buildApiHandler, ApiHandler } from "../../api"
6+
import { ProviderSettings } from "@roo-code/types"
7+
import { Package } from "../../shared/package"
8+
9+
export class AiCompletionProvider implements vscode.InlineCompletionItemProvider {
10+
private apiHandler: ApiHandler | null = null
11+
private outputChannel: vscode.OutputChannel
12+
private debouncedProvideCompletions: ReturnType<typeof debounce>
13+
private lastCompletionRequestId = 0
14+
private activeCompletionRequest: AbortController | null = null
15+
16+
constructor(outputChannel: vscode.OutputChannel) {
17+
this.outputChannel = outputChannel
18+
19+
// Initialize debounced completion function
20+
const debounceDelay = vscode.workspace
21+
.getConfiguration(Package.name)
22+
.get<number>("aiTabCompletion.debounceDelay", 300)
23+
this.debouncedProvideCompletions = debounce(this.provideCompletionsInternal.bind(this), debounceDelay)
24+
25+
// Update configuration when settings change
26+
vscode.workspace.onDidChangeConfiguration((e) => {
27+
if (e.affectsConfiguration(`${Package.name}.aiTabCompletion`)) {
28+
this.updateConfiguration()
29+
}
30+
})
31+
32+
this.updateConfiguration()
33+
}
34+
35+
private updateConfiguration() {
36+
const config = vscode.workspace.getConfiguration(Package.name)
37+
const enabled = config.get<boolean>("aiTabCompletion.enabled", false)
38+
39+
if (!enabled) {
40+
this.apiHandler = null
41+
return
42+
}
43+
44+
const provider = config.get<string>("aiTabCompletion.provider", "anthropic")
45+
const model = config.get<string>("aiTabCompletion.model", "claude-3-haiku-20240307")
46+
47+
// Build provider settings based on configuration
48+
const providerSettings: ProviderSettings = {
49+
apiProvider: provider as any,
50+
apiModelId: model,
51+
}
52+
53+
// Add API keys based on provider
54+
switch (provider) {
55+
case "anthropic": {
56+
const anthropicKey = config.get<string>("anthropicApiKey")
57+
if (anthropicKey) {
58+
providerSettings.apiKey = anthropicKey
59+
}
60+
break
61+
}
62+
case "openai": {
63+
const openaiKey = config.get<string>("openaiApiKey")
64+
if (openaiKey) {
65+
providerSettings.openAiApiKey = openaiKey
66+
}
67+
break
68+
}
69+
case "openrouter": {
70+
const openrouterKey = config.get<string>("openRouterApiKey")
71+
if (openrouterKey) {
72+
providerSettings.openRouterApiKey = openrouterKey
73+
}
74+
break
75+
}
76+
// Add other providers as needed
77+
}
78+
79+
try {
80+
this.apiHandler = buildApiHandler(providerSettings)
81+
this.outputChannel.appendLine(`AI Tab Completion: Initialized with provider ${provider} and model ${model}`)
82+
} catch (error) {
83+
this.outputChannel.appendLine(`AI Tab Completion: Failed to initialize - ${error}`)
84+
this.apiHandler = null
85+
}
86+
87+
// Update debounce delay
88+
const newDebounceDelay = config.get<number>("aiTabCompletion.debounceDelay", 300)
89+
this.debouncedProvideCompletions = debounce(this.provideCompletionsInternal.bind(this), newDebounceDelay)
90+
}
91+
92+
async provideInlineCompletionItems(
93+
document: vscode.TextDocument,
94+
position: vscode.Position,
95+
context: vscode.InlineCompletionContext,
96+
token: vscode.CancellationToken,
97+
): Promise<vscode.InlineCompletionItem[] | undefined> {
98+
if (!this.apiHandler) {
99+
return undefined
100+
}
101+
102+
// Cancel any existing completion request
103+
if (this.activeCompletionRequest) {
104+
this.activeCompletionRequest.abort()
105+
}
106+
107+
// Create new abort controller for this request
108+
this.activeCompletionRequest = new AbortController()
109+
const requestId = ++this.lastCompletionRequestId
110+
111+
// Use debounced function
112+
return new Promise((resolve) => {
113+
this.debouncedProvideCompletions(document, position, context, token, requestId, resolve)
114+
})
115+
}
116+
117+
private async provideCompletionsInternal(
118+
document: vscode.TextDocument,
119+
position: vscode.Position,
120+
context: vscode.InlineCompletionContext,
121+
token: vscode.CancellationToken,
122+
requestId: number,
123+
resolve: (value: vscode.InlineCompletionItem[] | undefined) => void,
124+
) {
125+
// Check if this request is still the latest
126+
if (requestId !== this.lastCompletionRequestId) {
127+
resolve(undefined)
128+
return
129+
}
130+
131+
try {
132+
const config = vscode.workspace.getConfiguration(Package.name)
133+
const maxTokens = config.get<number>("aiTabCompletion.maxTokens", 150)
134+
const temperature = config.get<number>("aiTabCompletion.temperature", 0.2)
135+
136+
// Get context around cursor
137+
const linePrefix = document.lineAt(position.line).text.substring(0, position.character)
138+
const lineSuffix = document.lineAt(position.line).text.substring(position.character)
139+
140+
// Get preceding lines for context (up to 50 lines)
141+
const precedingLines: string[] = []
142+
for (let i = Math.max(0, position.line - 50); i < position.line; i++) {
143+
precedingLines.push(document.lineAt(i).text)
144+
}
145+
146+
// Get following lines for context (up to 10 lines)
147+
const followingLines: string[] = []
148+
for (let i = position.line + 1; i < Math.min(document.lineCount, position.line + 10); i++) {
149+
followingLines.push(document.lineAt(i).text)
150+
}
151+
152+
// Build prompt for completion
153+
const prompt = this.buildCompletionPrompt(
154+
document.languageId,
155+
precedingLines.join("\n"),
156+
linePrefix,
157+
lineSuffix,
158+
followingLines.join("\n"),
159+
)
160+
161+
// Create messages for API
162+
const messages: Anthropic.Messages.MessageParam[] = [
163+
{
164+
role: "user",
165+
content: prompt,
166+
},
167+
]
168+
169+
// Use streaming for faster response
170+
const stream = this.apiHandler!.createMessage(
171+
"You are a code completion assistant. Complete the code at the cursor position. Only provide the completion text, no explanations.",
172+
messages,
173+
{
174+
taskId: `completion-${requestId}`,
175+
mode: "completion",
176+
},
177+
)
178+
179+
let completion = ""
180+
for await (const chunk of stream) {
181+
if (token.isCancellationRequested || requestId !== this.lastCompletionRequestId) {
182+
break
183+
}
184+
185+
if (chunk.type === "text") {
186+
completion += chunk.text
187+
}
188+
}
189+
190+
// Clean up the completion
191+
completion = this.cleanCompletion(completion, linePrefix, lineSuffix)
192+
193+
if (completion && !token.isCancellationRequested && requestId === this.lastCompletionRequestId) {
194+
const item = new vscode.InlineCompletionItem(completion, new vscode.Range(position, position))
195+
resolve([item])
196+
} else {
197+
resolve(undefined)
198+
}
199+
} catch (error) {
200+
this.outputChannel.appendLine(`AI Tab Completion Error: ${error}`)
201+
resolve(undefined)
202+
} finally {
203+
if (requestId === this.lastCompletionRequestId) {
204+
this.activeCompletionRequest = null
205+
}
206+
}
207+
}
208+
209+
private buildCompletionPrompt(
210+
languageId: string,
211+
precedingContext: string,
212+
linePrefix: string,
213+
lineSuffix: string,
214+
followingContext: string,
215+
): string {
216+
return `Language: ${languageId}
217+
218+
Context before cursor:
219+
${precedingContext}
220+
221+
Current line before cursor: ${linePrefix}
222+
Current line after cursor: ${lineSuffix}
223+
224+
Context after cursor:
225+
${followingContext}
226+
227+
Complete the code at the cursor position. Provide only the code to insert, nothing else. The completion should fit naturally between the prefix and suffix.`
228+
}
229+
230+
private cleanCompletion(completion: string, linePrefix: string, lineSuffix: string): string {
231+
// Remove any markdown code blocks
232+
completion = completion.replace(/```[\w]*\n?/g, "").replace(/```$/g, "")
233+
234+
// Trim whitespace
235+
completion = completion.trim()
236+
237+
// Remove duplicate prefix if AI included it
238+
if (completion.startsWith(linePrefix.trimEnd())) {
239+
completion = completion.substring(linePrefix.trimEnd().length)
240+
}
241+
242+
// Remove duplicate suffix if AI included it
243+
if (lineSuffix && completion.endsWith(lineSuffix.trimStart())) {
244+
completion = completion.substring(0, completion.length - lineSuffix.trimStart().length)
245+
}
246+
247+
// Handle proper spacing
248+
if (
249+
linePrefix &&
250+
!linePrefix.endsWith(" ") &&
251+
completion &&
252+
!completion.startsWith(" ") &&
253+
/\w$/.test(linePrefix)
254+
) {
255+
// Add space if needed between words
256+
completion = " " + completion
257+
}
258+
259+
return completion
260+
}
261+
262+
dispose() {
263+
this.debouncedProvideCompletions.cancel()
264+
if (this.activeCompletionRequest) {
265+
this.activeCompletionRequest.abort()
266+
}
267+
}
268+
}

0 commit comments

Comments
 (0)