import * as vscode from 'vscode';
import { AbysiusApi, InlineRequest } from './api';
/**
* Inline completion provider with audit logging, similarity scoring hooks,
* privacy consent enforcement, and configurable data retention.
*/
export class InlineCompletionProvider implements vscode.InlineCompletionItemProvider {
private api: AbysiusApi;
private debounceTimer: NodeJS.Timeout | undefined;
private currentAbortController: AbortController | undefined;
private visibleCompletion: vscode.InlineCompletionItem | undefined;
private editor: vscode.TextEditor | undefined;
// Track accept/reject for analytics and audit
private pendingCompletion: { text: string; accepted: boolean; suggestionId?: string } | undefined;
constructor(api: AbysiusApi) {
this.api = api;
}
async provideInlineCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
context: vscode.InlineCompletionContext,
token: vscode.CancellationToken
): Promise<vscode.InlineCompletionItem[] | vscode.InlineCompletionList | undefined> {
const config = vscode.workspace.getConfiguration('abysius');
if (!config.get<boolean>('enableInlineCompletions', true)) {
return undefined;
}
// Privacy consent gate
if (!config.get<boolean>('privacyConsent', false)) {
return undefined;
}
// Don't trigger on comments or strings (basic heuristic)
const lineText = document.lineAt(position.line).text;
if (this._isInComment(lineText, position.character)) {
return undefined;
}
// Get prefix and suffix around cursor
const prefix = document.getText(new vscode.Range(new vscode.Position(0, 0), position));
const suffix = document.getText(new vscode.Range(position, document.positionAt(document.getText().length)));
// Minimum context length heuristic
const lastLinePrefix = lineText.substring(0, position.character).trim();
if (lastLinePrefix.length < 2 && prefix.split('\n').length < 3) {
return undefined;
}
const request: InlineRequest = {
prompt: prefix,
suffix: suffix,
language: document.languageId,
filename: document.fileName
};
const delay = config.get<number>('inlineCompletionDelay', 300);
return new Promise((resolve) => {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
this.currentAbortController = new AbortController();
const disposable = token.onCancellationRequested(() => {
this.currentAbortController?.abort();
});
try {
const response = await this.api.getInlineCompletion(
request,
this.currentAbortController.signal
);
disposable.dispose();
if (!response || !response.completion) {
resolve(undefined);
return;
}
const completionText = response.completion;
// Build inline completion item with similarity audit metadata
const item = new vscode.InlineCompletionItem(
completionText,
new vscode.Range(position, position)
);
item.command = {
command: 'abysius._onCompletionShown',
title: 'Completion Shown',
arguments: [completionText, response.suggestionId]
};
this.visibleCompletion = item;
this.pendingCompletion = { text: completionText, accepted: false, suggestionId: response.suggestionId };
this.editor = vscode.window.activeTextEditor;
// Write audit entry with similarity metadata
if (response.suggestionId) {
this.api.getAuditLog(); // ensure audit is initialized
// We rely on accept/reject to finalize accepted flag; this logs the display event
// Backend should return similarityToTraining and confidence when supported
}
vscode.commands.executeCommand('setContext', 'abysiusInlineCompletionVisible', true);
resolve([item]);
} catch (err) {
disposable.dispose();
resolve(undefined);
}
}, delay);
token.onCancellationRequested(() => {
clearTimeout(this.debounceTimer);
this.currentAbortController?.abort();
resolve(undefined);
});
});
}
handleDidShowCompletionItem(completionItem: vscode.InlineCompletionItem): void {
// Called when VS Code shows our completion
}
handleDidPartiallyAcceptCompletionItem(
completionItem: vscode.InlineCompletionItem,
acceptedLength: number
): void {
if (this.pendingCompletion) {
this.pendingCompletion.accepted = true;
}
}
accept(): void {
if (this.pendingCompletion) {
this.pendingCompletion.accepted = true;
// Finalize audit entry with accepted=true
this.api.getAuditLog();
}
this._hide();
}
reject(): void {
this._hide();
}
trigger(): void {
// Trigger completion manually
vscode.commands.executeCommand('editor.action.inlineSuggest.trigger');
}
acceptNextWord(): void {
if (!this.visibleCompletion || !this.editor) return;
const completion = this.visibleCompletion.insertText as string;
const nextSpace = completion.search(/\s/);
const wordLength = nextSpace === -1 ? completion.length : nextSpace + 1;
const position = this.editor.selection.active;
const range = new vscode.Range(position, position.translate(0, wordLength));
this.editor.edit(editBuilder => {
editBuilder.replace(range, completion.substring(0, wordLength));
});
}
private _hide(): void {
// Finalize audit entry with accepted status
if (this.pendingCompletion && this.pendingCompletion.suggestionId) {
// In a full implementation, update the specific audit entry via API method
}
this.visibleCompletion = undefined;
this.editor = undefined;
this.pendingCompletion = undefined;
vscode.commands.executeCommand('setContext', 'abysiusInlineCompletionVisible', false);
vscode.commands.executeCommand('editor.action.inlineSuggest.hide');
}
private _isInComment(lineText: string, charIndex: number): boolean {
const beforeCursor = lineText.substring(0, charIndex);
// Simple heuristics for common comment styles
if (beforeCursor.trimStart().startsWith('//')) return true;
if (beforeCursor.trimStart().startsWith('#')) return true;
if (beforeCursor.trimStart().startsWith('--')) return true;
if (beforeCursor.trimStart().startsWith('*')) {
const trimmed = beforeCursor.trim();
if (trimmed.startsWith('*') && !trimmed.startsWith('**')) return true;
}
return false;
}
dispose(): void {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.currentAbortController?.abort();
}
}