import { CancellationToken, InlineCompletionContext, InlineCompletionItem, InlineCompletionItemProvider, InlineCompletionList, Position, ProviderResult, Range, TextDocument, workspace, } from "vscode"; import { CompletionResponse, CancelablePromise } from "tabby-agent"; import { agent } from "./agent"; import { sleep } from "./utils"; export class TabbyCompletionProvider implements InlineCompletionItemProvider { private uuid = Date.now(); private latestTimestamp: number = 0; private pendingCompletion: CancelablePromise | null = null; // User Settings private enabled: boolean = true; private suggestionDelay: number = 150; private maxPrefixLines: number = 20; private maxSuffixLines: number = 20; constructor() { this.updateConfiguration(); workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration("tabby")) { this.updateConfiguration(); } }); } //@ts-ignore because ASYNC and PROMISE //prettier-ignore public async provideInlineCompletionItems(document: TextDocument, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult { const emptyResponse = Promise.resolve([] as InlineCompletionItem[]); if (!this.enabled) { console.debug("Extension not enabled, skipping."); return emptyResponse; } const currentTimestamp = Date.now(); this.latestTimestamp = currentTimestamp; await sleep(this.suggestionDelay); if (currentTimestamp < this.latestTimestamp) { return emptyResponse; } const replaceRange = this.calculateReplaceRange(document, position); if (this.pendingCompletion) { this.pendingCompletion.cancel(); } const request = { filepath: document.uri.fsPath, language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers text: document.getText(), position: document.offsetAt(position), maxPrefixLines: this.maxPrefixLines, maxSuffixLines: this.maxSuffixLines, }; this.pendingCompletion = agent().getCompletions(request); const completion = await this.pendingCompletion.catch((_: Error) => { return null; }); this.pendingCompletion = null; const completions = this.toInlineCompletions(completion, replaceRange); return Promise.resolve(completions); } private updateConfiguration() { const configuration = workspace.getConfiguration("tabby"); this.enabled = configuration.get("codeCompletion", true); this.suggestionDelay = configuration.get("developerOptions.suggestionDelay", 150); this.maxPrefixLines = configuration.get("developerOptions.maxPrefixLines", 20); this.maxSuffixLines = configuration.get("developerOptions.maxSuffixLines", 20); } private toInlineCompletions(tabbyCompletion: CompletionResponse | null, range: Range): InlineCompletionItem[] { return ( tabbyCompletion?.choices?.map((choice: any) => { let event = { type: "select", completion_id: tabbyCompletion.id, choice_index: choice.index, }; return new InlineCompletionItem(choice.text, range, { title: "", command: "tabby.emitEvent", arguments: [event], }); }) || [] ); } private hasSuffixParen(document: TextDocument, position: Position) { const suffix = document.getText( new Range(position.line, position.character, position.line, position.character + 1) ); return ")]}".indexOf(suffix) > -1; } private calculateReplaceRange(document: TextDocument, position: Position): Range { const hasSuffixParen = this.hasSuffixParen(document, position); if (hasSuffixParen) { return new Range(position.line, position.character, position.line, position.character + 1); } else { return new Range(position, position); } } }