diff --git a/.gitignore b/.gitignore index 4a07354..255dda1 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ out/ build/ vscode-out/ -*.vsix \ No newline at end of file +*.vsix +package-lock.json \ No newline at end of file diff --git a/README.md b/README.md index 123fd0f..9ee00f5 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,27 @@ Rest assured, you can take a break safely. - AI chat interface in sidebar - Inline code editing with `Ctrl+I` / `Cmd+I` +- Inline code suggestions while typing (configurable) - Agent mode with shell command execution - Safety agent mode (Auto Approve) - Diff preview and apply - Auto context (file list, git status, package.json, tsconfig.json) - Multiple AI model support (OpenAI, Anthropic, Google, etc.) +## Usage + +### Inline Suggestions + +Inline suggestions provide contextual code completions as you type, similar to GitHub Copilot. To enable: + +1. Open VS Code Settings (`Ctrl+,` / `Cmd+,`) +2. Search for "Flixa: Inline Completion Enabled" +3. Enable the setting + +Once enabled, suggestions will appear as grayed-out ghost text while typing. Press `Tab` or `Enter` to accept a suggestion. + +**Note:** Inline suggestions are disabled by default to avoid unexpected API usage. Enable only when needed. + ## License MIT diff --git a/package.json b/package.json index c92f3da..611b44e 100644 --- a/package.json +++ b/package.json @@ -139,6 +139,18 @@ "bun.lockb" ], "description": "Patterns to exclude from file list" + }, + "flixa.inlineCompletion.enabled": { + "type": "boolean", + "default": false, + "description": "Enable inline code suggestions while typing" + }, + "flixa.inlineCompletion.maxLength": { + "type": "number", + "default": 120, + "minimum": 40, + "maximum": 200, + "description": "Maximum character length for inline suggestions" } } }, diff --git a/src/extension.ts b/src/extension.ts index 6412c19..50e8aa2 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -6,6 +6,7 @@ import { showDiffPreview } from './diff/preview'; import { applyPendingDiff } from './diff/apply'; import { PendingDiff, ImplementRequest, ApprovalMode } from './types'; import { setOutputChannel } from './logger'; +import { FlixaInlineCompletionProvider } from './inline'; let pendingDiff: PendingDiff | null = null; let chatViewProvider: ChatViewProvider | null = null; @@ -36,6 +37,17 @@ export function activate(context: vscode.ExtensionContext): void { console.log('[Flixa] CodeLens provider registered'); context.subscriptions.push(codeLensDisposable); + const inlineCompletionProvider = new FlixaInlineCompletionProvider(); + const inlineCompletionDisposable = vscode.languages.registerInlineCompletionItemProvider( + [ + { scheme: 'file', pattern: '**/*' }, + { scheme: 'untitled' } + ], + inlineCompletionProvider + ); + console.log('[Flixa] Inline completion provider registered'); + context.subscriptions.push(inlineCompletionDisposable); + chatViewProvider = new ChatViewProvider( context.extensionUri, context, diff --git a/src/inline/index.ts b/src/inline/index.ts new file mode 100644 index 0000000..254832d --- /dev/null +++ b/src/inline/index.ts @@ -0,0 +1 @@ +export { FlixaInlineCompletionProvider } from './provider'; diff --git a/src/inline/provider.ts b/src/inline/provider.ts new file mode 100644 index 0000000..4d69cb9 --- /dev/null +++ b/src/inline/provider.ts @@ -0,0 +1,100 @@ +import * as vscode from 'vscode'; +import { generateText } from 'ai'; +import { getAnthropicProvider, getModel } from '../llm/provider'; + +export class FlixaInlineCompletionProvider implements vscode.InlineCompletionItemProvider { + private lastRequestTime = 0; + private debounceDelay = 300; + private cache: Map = new Map(); + private cacheTimeout = 5000; + + async provideInlineCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + context: vscode.InlineCompletionContext, + token: vscode.CancellationToken + ): Promise { + const config = vscode.workspace.getConfiguration('flixa'); + const enabled = config.get('inlineCompletion.enabled', false); + const maxLength = config.get('inlineCompletion.maxLength', 120); + + if (!enabled) { + return undefined; + } + + if (token.isCancellationRequested) { + return undefined; + } + + const now = Date.now(); + if (now - this.lastRequestTime < this.debounceDelay) { + return undefined; + } + this.lastRequestTime = now; + + const currentLine = document.lineAt(position.line).text; + const precedingText = currentLine.substring(0, position.character); + + if (!precedingText.trim()) { + return undefined; + } + + const cacheKey = `${document.fileName}:${position.line}:${precedingText}`; + const cached = this.cache.get(cacheKey); + if (cached && now - cached.timestamp < this.cacheTimeout) { + return [{ + insertText: cached.completion, + range: new vscode.Range(position, position), + }]; + } + + try { + const provider = getAnthropicProvider(); + const model = getModel(); + + const contextLines = 5; + const startLine = Math.max(0, position.line - contextLines); + const endLine = Math.min(document.lineCount - 1, position.line + contextLines); + const contextText = []; + for (let i = startLine; i <= endLine; i++) { + contextText.push(document.lineAt(i).text); + } + + const abortController = new AbortController(); + const listener = token.onCancellationRequested(() => { + abortController.abort(); + }); + + const { text } = await generateText({ + model: provider(model), + system: `You are a code completion AI. Suggest a natural continuation of the code. Keep it concise (under ${maxLength} chars). Return ONLY the completion text that follows the cursor, no explanation, no markdown.`, + prompt: `File: ${document.fileName}\nLanguage: ${document.languageId}\n\nContext:\n${contextText.join('\n')}\n\nCursor is at the end of line ${position.line + 1}. Complete the code naturally:`, + abortSignal: abortController.signal, + }); + + listener.dispose(); + + if (token.isCancellationRequested) { + return undefined; + } + + const completion = text?.trim() || ''; + if (!completion) { + return undefined; + } + + this.cache.set(cacheKey, { completion, timestamp: now }); + + return [{ + insertText: completion, + range: new vscode.Range(position, position), + }]; + } catch (error) { + if (token.isCancellationRequested) { + return undefined; + } + console.error('[Flixa] Inline completion error:', error); + return undefined; + } + } +}