From 500ae05e69ef39978dc16d3d3ce638ca96b46793 Mon Sep 17 00:00:00 2001 From: Jackson Chen <90215880+Sma1lboy@users.noreply.github.com> Date: Sun, 17 Nov 2024 18:36:24 -0600 Subject: [PATCH] feat(agent): add context support for auto complete widget in InlineCompletion (#3362) * feat(codeCompletion): add support for auto complete widget in CompletionContext * feat(codeCompletion): improve auto complete widget handling in CompletionContext * fix: update minCompletionChars to 4 * chore(codeCompletion): remove some comments and logger call * feat(codeCompletion): optimize completion item filtering in CompletionContext --- .../src/codeCompletion/contexts.ts | 85 +++++++++++++++++-- .../tabby-agent/src/codeCompletion/index.ts | 26 +++++- .../src/codeCompletion/solution.ts | 34 ++++++-- .../vscode/src/InlineCompletionProvider.ts | 6 -- 4 files changed, 128 insertions(+), 23 deletions(-) diff --git a/clients/tabby-agent/src/codeCompletion/contexts.ts b/clients/tabby-agent/src/codeCompletion/contexts.ts index 751d9b04f554..8b75e87f7818 100644 --- a/clients/tabby-agent/src/codeCompletion/contexts.ts +++ b/clients/tabby-agent/src/codeCompletion/contexts.ts @@ -23,6 +23,13 @@ export type CompletionRequest = { declarations?: Declaration[]; relevantSnippetsFromChangedFiles?: CodeSnippet[]; relevantSnippetsFromOpenedFiles?: CodeSnippet[]; + //auto complete part + autoComplete?: { + completionItem?: string; + insertPosition?: number; + insertSeg?: string; + currSeg?: string; + }; }; export type Declaration = { @@ -76,25 +83,38 @@ export class CompletionContext { mode: "default" | "fill-in-line"; hash: string; + // example of auto complete part + // cons| -> console + // completionItem: console + // insertPosition: 4 + // insertSeg: ole + // currSeg: cons + completionItem: string = ""; + insertPosition: number = 0; + insertSeg: string = ""; + currSeg: string = ""; + withCorrectCompletionItem: boolean = false; // weather we are using completionItem or not + constructor(request: CompletionRequest) { this.filepath = request.filepath; this.language = request.language; - this.text = request.text; - this.position = request.position; this.indentation = request.indentation; + this.position = request.position; + this.text = request.text; + this.prefix = this.text.slice(0, this.position); + this.suffix = this.text.slice(this.position); + + if (request.autoComplete?.completionItem) { + this.handleAutoComplete(request); + } - this.prefix = request.text.slice(0, request.position); - this.suffix = request.text.slice(request.position); this.prefixLines = splitLines(this.prefix); this.suffixLines = splitLines(this.suffix); this.currentLinePrefix = this.prefixLines[this.prefixLines.length - 1] ?? ""; this.currentLineSuffix = this.suffixLines[0] ?? ""; - this.clipboard = request.clipboard?.trim() ?? ""; - this.workspace = request.workspace; this.git = request.git; - this.declarations = request.declarations; this.relevantSnippetsFromChangedFiles = request.relevantSnippetsFromChangedFiles; this.snippetsFromOpenedFiles = request.relevantSnippetsFromOpenedFiles; @@ -111,6 +131,10 @@ export class CompletionContext { clipboard: this.clipboard, declarations: this.declarations, relevantSnippetsFromChangedFiles: this.relevantSnippetsFromChangedFiles, + completionItem: this.completionItem, + insertPosition: this.insertPosition, + insertSeg: this.insertSeg, + currSeg: this.currSeg, }); } @@ -121,6 +145,7 @@ export class CompletionContext { // Generate a CompletionContext based on this CompletionContext. // Simulate as if the user input new text based on this CompletionContext. + // FIXME: generate the context according to `selectedCompletionInfo` forward(delta: string) { return new CompletionContext({ filepath: this.filepath, @@ -132,9 +157,55 @@ export class CompletionContext { git: this.git, declarations: this.declarations, relevantSnippetsFromChangedFiles: this.relevantSnippetsFromChangedFiles, + relevantSnippetsFromOpenedFiles: this.snippetsFromOpenedFiles, + autoComplete: { + completionItem: this.completionItem, + insertPosition: this.insertPosition, + insertSeg: this.insertSeg, + currSeg: this.currSeg, + }, }); } + /** + * The method handles the auto complete part of the completion request. + * @param request completion request + * @returns void + */ + private handleAutoComplete(request: CompletionRequest): void { + if (!request.autoComplete?.completionItem) return; + this.completionItem = request.autoComplete.completionItem; + this.currSeg = request.autoComplete.currSeg ?? ""; + this.insertSeg = request.autoComplete.insertSeg ?? ""; + + // check if the completion item is the same as the insert segment + if (!this.completionItem.startsWith(this.insertSeg)) { + return; + } + + const prefixText = request.text.slice(0, request.position); + const lastIndex = prefixText.lastIndexOf(this.currSeg); + + if (lastIndex !== -1) { + this.insertPosition = lastIndex + this.currSeg.length; + + this.text = request.text.slice(0, lastIndex) + this.completionItem + request.text.slice(this.insertPosition); + + this.position = lastIndex + this.completionItem.length; + + this.prefix = this.text.slice(0, this.position); + this.suffix = this.text.slice(this.position); + this.withCorrectCompletionItem = true; + } + } + isWithCorrectAutoComplete(): boolean { + return this.withCorrectCompletionItem; + } + + getFullCompletionItem(): string | null { + return this.isWithCorrectAutoComplete() ? this.completionItem : null; + } + // Build segments for TabbyApi buildSegments(config: ConfigData["completion"]["prompt"]): TabbyApiComponents["schemas"]["Segments"] { // prefix && suffix diff --git a/clients/tabby-agent/src/codeCompletion/index.ts b/clients/tabby-agent/src/codeCompletion/index.ts index d77c41182bb1..b39e5f83bab9 100644 --- a/clients/tabby-agent/src/codeCompletion/index.ts +++ b/clients/tabby-agent/src/codeCompletion/index.ts @@ -262,6 +262,30 @@ export class CompletionProvider implements Feature { return null; } result.request.manually = params.context?.triggerKind === InlineCompletionTriggerKind.Invoked; + + if (params.context.selectedCompletionInfo) { + const customContext = params.context as { + triggerKind: InlineCompletionTriggerKind; + selectedCompletionInfo?: { + range: [{ line: number; character: number }, { line: number; character: number }]; + text: string; + }; + }; + if (!customContext.selectedCompletionInfo) { + return result; + } + const info = customContext.selectedCompletionInfo; + + const rangeLength = info.range[1].character - info.range[0].character; + const currentText = info.text.substring(0, rangeLength); + + result.request.autoComplete = { + completionItem: info.text, + insertSeg: info.text.slice(currentText.length), + currSeg: currentText, + }; + this.logger.debug("received AutoCompleteWidgetItem: " + JSON.stringify(params.context.selectedCompletionInfo)); + } return result; } @@ -407,8 +431,8 @@ export class CompletionProvider implements Feature { }, signals, ); - solution = new CompletionSolution(context); + // Fetch the completion this.logger.info(`Fetching completion...`); try { diff --git a/clients/tabby-agent/src/codeCompletion/solution.ts b/clients/tabby-agent/src/codeCompletion/solution.ts index c9b2ecc64727..cb9b6a6596b7 100644 --- a/clients/tabby-agent/src/codeCompletion/solution.ts +++ b/clients/tabby-agent/src/codeCompletion/solution.ts @@ -35,6 +35,9 @@ export class CompletionItem { readonly currentLine: string; // first item of `lines` readonly isBlank: boolean; // whether the item is a blank line. + readonly processedText: string; // text to be inserted. + readonly processedRange: { start: number; end: number }; // range to be replaced. + constructor( // The context which the completion was generated for. readonly context: CompletionContext, @@ -54,6 +57,25 @@ export class CompletionItem { this.lines = splitLines(this.text); this.currentLine = this.lines[0] ?? ""; this.isBlank = isBlank(this.text); + + // if with auto complete item, insert the completion item with the predicted text + if (this.context.isWithCorrectAutoComplete()) { + this.processedText = this.context.insertSeg + this.fullText; + this.processedRange = { + start: this.context.insertPosition, + end: this.context.insertPosition, + }; + } else { + this.processedText = this.fullText; + this.processedRange = { + start: this.context.currentLinePrefix.endsWith(this.replacePrefix) + ? this.context.position - this.replacePrefix.length + : this.context.position, + end: this.context.currentLineSuffix.startsWith(this.replaceSuffix) + ? this.context.position + this.replaceSuffix.length + : this.context.position, + }; + } } static createBlankItem(context: CompletionContext): CompletionItem { @@ -106,6 +128,7 @@ export class CompletionItem { forward(chars: number): CompletionItem { if (chars <= 0) return this; const delta = this.text.substring(0, chars); + // Forward in the current line if (chars < this.currentLine.length) { return new CompletionItem( @@ -141,15 +164,8 @@ export class CompletionItem { toInlineCompletionItem(): InlineCompletionItem { return { - insertText: this.fullText, - range: { - start: this.context.currentLinePrefix.endsWith(this.replacePrefix) - ? this.context.position - this.replacePrefix.length - : this.context.position, - end: this.context.currentLineSuffix.startsWith(this.replaceSuffix) - ? this.context.position + this.replaceSuffix.length - : this.context.position, - }, + insertText: this.processedText, + range: this.processedRange, data: { eventId: this.eventId }, }; } diff --git a/clients/vscode/src/InlineCompletionProvider.ts b/clients/vscode/src/InlineCompletionProvider.ts index a4d1970fc8d2..4ef1d797a32f 100644 --- a/clients/vscode/src/InlineCompletionProvider.ts +++ b/clients/vscode/src/InlineCompletionProvider.ts @@ -73,12 +73,6 @@ export class InlineCompletionProvider extends EventEmitter implements InlineComp return null; } - // Check if autocomplete widget is visible - if (context.selectedCompletionInfo !== undefined) { - this.logger.debug("Autocomplete widget is visible, skipping."); - return null; - } - if (token.isCancellationRequested) { this.logger.debug("Completion request is canceled before send request."); return null;