From cc83e4d2691f5438a236d4bb735ea6b9da436234 Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Mon, 25 Sep 2023 09:07:25 +0800 Subject: [PATCH] fix(vscode): completion view/select event. (#471) * fix(vscode): completion view/select event. * chore: bump tabby-agent version 0.3.0-dev. --- clients/intellij/package.json | 2 +- clients/tabby-agent/package.json | 2 +- clients/tabby-agent/src/Agent.ts | 4 +- clients/tabby-agent/src/TabbyAgent.ts | 14 ++- clients/vim/package.json | 2 +- clients/vscode/README.md | 2 +- .../assets/walkthroughs/codeCompletion.md | 4 - clients/vscode/package.json | 2 +- clients/vscode/src/TabbyCompletionProvider.ts | 92 ++++++++++++++----- clients/vscode/src/commands.ts | 15 +-- clients/vscode/src/extension.ts | 2 +- 11 files changed, 95 insertions(+), 46 deletions(-) diff --git a/clients/intellij/package.json b/clients/intellij/package.json index 180034a..6261b1b 100644 --- a/clients/intellij/package.json +++ b/clients/intellij/package.json @@ -10,6 +10,6 @@ "devDependencies": { "cpy-cli": "^4.2.0", "rimraf": "^5.0.1", - "tabby-agent": "0.2.0" + "tabby-agent": "0.3.0-dev" } } diff --git a/clients/tabby-agent/package.json b/clients/tabby-agent/package.json index de770bf..8579c72 100644 --- a/clients/tabby-agent/package.json +++ b/clients/tabby-agent/package.json @@ -1,6 +1,6 @@ { "name": "tabby-agent", - "version": "0.2.0", + "version": "0.3.0-dev", "description": "Generic client agent for Tabby AI coding assistant IDE extensions.", "repository": "https://github.com/TabbyML/tabby", "main": "./dist/index.js", diff --git a/clients/tabby-agent/src/Agent.ts b/clients/tabby-agent/src/Agent.ts index fd4b6c6..8f39621 100644 --- a/clients/tabby-agent/src/Agent.ts +++ b/clients/tabby-agent/src/Agent.ts @@ -19,7 +19,9 @@ export type CompletionRequest = { export type CompletionResponse = ApiComponents["schemas"]["CompletionResponse"]; -export type LogEventRequest = ApiComponents["schemas"]["LogEventRequest"]; +export type LogEventRequest = ApiComponents["schemas"]["LogEventRequest"] & { + select_kind?: "line"; +}; export type AbortSignalOption = { signal: AbortSignal }; diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index 6cef49b..2562afd 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -429,7 +429,19 @@ export class TabbyAgent extends EventEmitter implements Agent { if (this.status === "notInitialized") { throw new Error("Agent is not initialized"); } - await this.post("/v1/events", { body: request, parseAs: "text" }, options); + await this.post( + "/v1/events", + { + body: request, + params: { + query: { + select_kind: request.select_kind, + }, + }, + parseAs: "text", + }, + options, + ); return true; } } diff --git a/clients/vim/package.json b/clients/vim/package.json index fb11126..953f855 100644 --- a/clients/vim/package.json +++ b/clients/vim/package.json @@ -10,6 +10,6 @@ "devDependencies": { "cpy-cli": "^4.2.0", "rimraf": "^5.0.1", - "tabby-agent": "0.2.0" + "tabby-agent": "0.3.0-dev" } } diff --git a/clients/vscode/README.md b/clients/vscode/README.md index 374f623..4df0537 100644 --- a/clients/vscode/README.md +++ b/clients/vscode/README.md @@ -25,6 +25,6 @@ Once you have installed the Tabby VSCode extension, you can easily get started b 1. **Setup the Tabby server**: You have two options to set up your Tabby server. You can either get a Tabby Cloud hosted server [here](https://app.tabbyml.com) or build your own self-hosted Tabby server following [this guide](https://tabby.tabbyml.com/docs/installation). 2. **Connect the extension to your Tabby server**: Use the command `Tabby: Specify API Endpoint of Tabby` to connect the extension to your Tabby server. If you are using a Tabby Cloud server endpoint, follow the instructions provided in the popup messages to complete the authorization process. -Once the setup is complete, Tabby will automatically provide inline suggestions. You can accept the suggestions by simply pressing the `Tab` key. Hovering over the inline suggestion text will display additional useful actions, such as partially accepting suggestions by word or by line. +Once the setup is complete, Tabby will automatically provide inline suggestions. You can accept the suggestions by simply pressing the `Tab` key. If you prefer to trigger code completion manually, you can select the manual trigger option in the settings. After that, use the shortcut `Alt + \` to trigger code completion. To access the settings page, use the command `Tabby: Open Settings`. diff --git a/clients/vscode/assets/walkthroughs/codeCompletion.md b/clients/vscode/assets/walkthroughs/codeCompletion.md index 18fa53c..736d4c7 100644 --- a/clients/vscode/assets/walkthroughs/codeCompletion.md +++ b/clients/vscode/assets/walkthroughs/codeCompletion.md @@ -10,10 +10,6 @@ Tabby will show inline suggestions when you stop typing, and you can accept sugg If you select manual trigger in the [settings](command:tabby.openSettings), you can trigger code completion by pressing `Alt + \`. -## Cycling Through Choices - -When multiple choices are available, you can cycle through them by pressing `Alt + [` and `Alt + ]`. - ## Keybindings You can select a keybinding profile in the [settings](command:tabby.openSettings), or customize your own [keybindings](command:tabby.openKeybindings). diff --git a/clients/vscode/package.json b/clients/vscode/package.json index a1d4f24..8b4d8a6 100644 --- a/clients/vscode/package.json +++ b/clients/vscode/package.json @@ -216,6 +216,6 @@ }, "dependencies": { "@xstate/fsm": "^2.0.1", - "tabby-agent": "0.2.0" + "tabby-agent": "0.3.0-dev" } } diff --git a/clients/vscode/src/TabbyCompletionProvider.ts b/clients/vscode/src/TabbyCompletionProvider.ts index ad43a4d..75060ad 100644 --- a/clients/vscode/src/TabbyCompletionProvider.ts +++ b/clients/vscode/src/TabbyCompletionProvider.ts @@ -10,15 +10,24 @@ import { workspace, } from "vscode"; import { EventEmitter } from "events"; -import { CompletionResponse } from "tabby-agent"; +import { CompletionRequest, CompletionResponse, LogEventRequest } from "tabby-agent"; import { agent } from "./agent"; export class TabbyCompletionProvider extends EventEmitter implements InlineCompletionItemProvider { + static instance: TabbyCompletionProvider; + static getInstance(): TabbyCompletionProvider { + if (!TabbyCompletionProvider.instance) { + TabbyCompletionProvider.instance = new TabbyCompletionProvider(); + } + return TabbyCompletionProvider.instance; + } + private triggerMode: "automatic" | "manual" | "disabled" = "automatic"; private onGoingRequestAbortController: AbortController | null = null; private loading: boolean = false; + private latestCompletions: CompletionResponse | null = null; - constructor() { + private constructor() { super(); this.updateConfiguration(); workspace.onDidChangeConfiguration((event) => { @@ -57,13 +66,13 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl } if (token?.isCancellationRequested) { - console.debug("Cancellation was requested."); + console.debug("Completion request is canceled before agent request."); return null; } const replaceRange = this.calculateReplaceRange(document, position); - const request = { + const request: CompletionRequest = { filepath: document.uri.fsPath, language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers text: document.getText(), @@ -71,10 +80,12 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl manually: context.triggerKind === InlineCompletionTriggerKind.Invoke, }; + this.latestCompletions = null; + const abortController = new AbortController(); this.onGoingRequestAbortController = abortController; token?.onCancellationRequested(() => { - console.debug("Cancellation requested."); + console.debug("Completion request is canceled."); abortController.abort(); }); @@ -84,7 +95,30 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl const result = await agent().provideCompletions(request, { signal: abortController.signal }); this.loading = false; this.emit("loadingStatusUpdated"); - return this.toInlineCompletions(result, replaceRange); + + if (token?.isCancellationRequested) { + console.debug("Completion request is canceled after agent request."); + return null; + } + + // Assume only one choice is provided, do not support multiple choices for now + if (result.choices.length > 0) { + this.latestCompletions = result; + + this.postEvent("show"); + + return [ + new InlineCompletionItem(result.choices[0].text, replaceRange, { + title: "", + command: "tabby.applyCallback", + arguments: [ + () => { + this.postEvent("accept"); + }, + ], + }), + ]; + } } catch (error: any) { if (this.onGoingRequestAbortController === abortController) { // the request was not replaced by a new request, set loading to false safely @@ -99,6 +133,35 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl return null; } + public postEvent(event: "show" | "accept" | "accept_word" | "accept_line") { + const completion = this.latestCompletions; + if (completion && completion.choices.length > 0) { + let postBody: LogEventRequest = { + type: event === "show" ? "view" : "select", + completion_id: completion.id, + // Assume only one choice is provided for now + choice_index: completion.choices[0].index, + }; + switch (event) { + case "accept_word": + // select_kind should be "word" but not supported by Tabby Server yet, use "line" instead + postBody = { ...postBody, select_kind: "line" }; + break; + case "accept_line": + postBody = { ...postBody, select_kind: "line" }; + break; + default: + break; + } + console.debug(`Post event ${event}`, { postBody }); + try { + agent().postEvent(postBody); + } catch (error: any) { + console.debug("Error when posting event", { error }); + } + } + } + private updateConfiguration() { if (!workspace.getConfiguration("editor").get("inlineSuggest.enabled", true)) { this.triggerMode = "disabled"; @@ -109,23 +172,6 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl } } - 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), diff --git a/clients/vscode/src/commands.ts b/clients/vscode/src/commands.ts index 34b61d5..cfe8377 100644 --- a/clients/vscode/src/commands.ts +++ b/clients/vscode/src/commands.ts @@ -11,6 +11,7 @@ import { import { strict as assert } from "assert"; import { agent } from "./agent"; import { notifications } from "./notifications"; +import { TabbyCompletionProvider } from "./TabbyCompletionProvider"; const configTarget = ConfigurationTarget.Global; @@ -109,14 +110,6 @@ const gettingStarted: Command = { }, }; -const emitEvent: Command = { - command: "tabby.emitEvent", - callback: (event) => { - console.debug("Emit Event: ", event); - agent().postEvent(event); - }, -}; - const openAuthPage: Command = { command: "tabby.openAuthPage", callback: (callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) => { @@ -185,7 +178,7 @@ const acceptInlineCompletion: Command = { const acceptInlineCompletionNextWord: Command = { command: "tabby.inlineCompletion.acceptNextWord", callback: () => { - // FIXME: sent event when partially accept? + TabbyCompletionProvider.getInstance().postEvent("accept_word"); commands.executeCommand("editor.action.inlineSuggest.acceptNextWord"); }, }; @@ -193,7 +186,8 @@ const acceptInlineCompletionNextWord: Command = { const acceptInlineCompletionNextLine: Command = { command: "tabby.inlineCompletion.acceptNextLine", callback: () => { - // FIXME: sent event when partially accept? + TabbyCompletionProvider.getInstance().postEvent("accept_line"); + // FIXME: this command move cursor to next line, but we want to move cursor to the end of current line commands.executeCommand("editor.action.inlineSuggest.acceptNextLine"); }, }; @@ -206,7 +200,6 @@ export const tabbyCommands = () => openTabbyAgentSettings, openKeybindings, gettingStarted, - emitEvent, openAuthPage, applyCallback, triggerInlineCompletion, diff --git a/clients/vscode/src/extension.ts b/clients/vscode/src/extension.ts index 2a91480..a2504df 100644 --- a/clients/vscode/src/extension.ts +++ b/clients/vscode/src/extension.ts @@ -11,7 +11,7 @@ import { TabbyStatusBarItem } from "./TabbyStatusBarItem"; export async function activate(context: ExtensionContext) { console.debug("Activating Tabby extension", new Date()); await createAgentInstance(context); - const completionProvider = new TabbyCompletionProvider(); + const completionProvider = TabbyCompletionProvider.getInstance(); const statusBarItem = new TabbyStatusBarItem(completionProvider); context.subscriptions.push( languages.registerInlineCompletionItemProvider({ pattern: "**" }, completionProvider),