fix(vscode): completion view/select event. (#471)

* fix(vscode): completion view/select event.

* chore: bump tabby-agent version 0.3.0-dev.
release-0.2
Zhiming Ma 2023-09-25 09:07:25 +08:00 committed by GitHub
parent 63309d6d6e
commit cc83e4d269
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 95 additions and 46 deletions

View File

@ -10,6 +10,6 @@
"devDependencies": { "devDependencies": {
"cpy-cli": "^4.2.0", "cpy-cli": "^4.2.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"tabby-agent": "0.2.0" "tabby-agent": "0.3.0-dev"
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "tabby-agent", "name": "tabby-agent",
"version": "0.2.0", "version": "0.3.0-dev",
"description": "Generic client agent for Tabby AI coding assistant IDE extensions.", "description": "Generic client agent for Tabby AI coding assistant IDE extensions.",
"repository": "https://github.com/TabbyML/tabby", "repository": "https://github.com/TabbyML/tabby",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@ -19,7 +19,9 @@ export type CompletionRequest = {
export type CompletionResponse = ApiComponents["schemas"]["CompletionResponse"]; 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 }; export type AbortSignalOption = { signal: AbortSignal };

View File

@ -429,7 +429,19 @@ export class TabbyAgent extends EventEmitter implements Agent {
if (this.status === "notInitialized") { if (this.status === "notInitialized") {
throw new Error("Agent is not initialized"); 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; return true;
} }
} }

View File

@ -10,6 +10,6 @@
"devDependencies": { "devDependencies": {
"cpy-cli": "^4.2.0", "cpy-cli": "^4.2.0",
"rimraf": "^5.0.1", "rimraf": "^5.0.1",
"tabby-agent": "0.2.0" "tabby-agent": "0.3.0-dev"
} }
} }

View File

@ -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). 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. 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`. 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`.

View File

@ -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 + \`. 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 ## Keybindings
You can select a keybinding profile in the [settings](command:tabby.openSettings), or customize your own [keybindings](command:tabby.openKeybindings). You can select a keybinding profile in the [settings](command:tabby.openSettings), or customize your own [keybindings](command:tabby.openKeybindings).

View File

@ -216,6 +216,6 @@
}, },
"dependencies": { "dependencies": {
"@xstate/fsm": "^2.0.1", "@xstate/fsm": "^2.0.1",
"tabby-agent": "0.2.0" "tabby-agent": "0.3.0-dev"
} }
} }

View File

@ -10,15 +10,24 @@ import {
workspace, workspace,
} from "vscode"; } from "vscode";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
import { CompletionResponse } from "tabby-agent"; import { CompletionRequest, CompletionResponse, LogEventRequest } from "tabby-agent";
import { agent } from "./agent"; import { agent } from "./agent";
export class TabbyCompletionProvider extends EventEmitter implements InlineCompletionItemProvider { 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 triggerMode: "automatic" | "manual" | "disabled" = "automatic";
private onGoingRequestAbortController: AbortController | null = null; private onGoingRequestAbortController: AbortController | null = null;
private loading: boolean = false; private loading: boolean = false;
private latestCompletions: CompletionResponse | null = null;
constructor() { private constructor() {
super(); super();
this.updateConfiguration(); this.updateConfiguration();
workspace.onDidChangeConfiguration((event) => { workspace.onDidChangeConfiguration((event) => {
@ -57,13 +66,13 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl
} }
if (token?.isCancellationRequested) { if (token?.isCancellationRequested) {
console.debug("Cancellation was requested."); console.debug("Completion request is canceled before agent request.");
return null; return null;
} }
const replaceRange = this.calculateReplaceRange(document, position); const replaceRange = this.calculateReplaceRange(document, position);
const request = { const request: CompletionRequest = {
filepath: document.uri.fsPath, filepath: document.uri.fsPath,
language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers
text: document.getText(), text: document.getText(),
@ -71,10 +80,12 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl
manually: context.triggerKind === InlineCompletionTriggerKind.Invoke, manually: context.triggerKind === InlineCompletionTriggerKind.Invoke,
}; };
this.latestCompletions = null;
const abortController = new AbortController(); const abortController = new AbortController();
this.onGoingRequestAbortController = abortController; this.onGoingRequestAbortController = abortController;
token?.onCancellationRequested(() => { token?.onCancellationRequested(() => {
console.debug("Cancellation requested."); console.debug("Completion request is canceled.");
abortController.abort(); abortController.abort();
}); });
@ -84,7 +95,30 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl
const result = await agent().provideCompletions(request, { signal: abortController.signal }); const result = await agent().provideCompletions(request, { signal: abortController.signal });
this.loading = false; this.loading = false;
this.emit("loadingStatusUpdated"); 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) { } catch (error: any) {
if (this.onGoingRequestAbortController === abortController) { if (this.onGoingRequestAbortController === abortController) {
// the request was not replaced by a new request, set loading to false safely // 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; 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() { private updateConfiguration() {
if (!workspace.getConfiguration("editor").get("inlineSuggest.enabled", true)) { if (!workspace.getConfiguration("editor").get("inlineSuggest.enabled", true)) {
this.triggerMode = "disabled"; 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) { private hasSuffixParen(document: TextDocument, position: Position) {
const suffix = document.getText( const suffix = document.getText(
new Range(position.line, position.character, position.line, position.character + 1), new Range(position.line, position.character, position.line, position.character + 1),

View File

@ -11,6 +11,7 @@ import {
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import { agent } from "./agent"; import { agent } from "./agent";
import { notifications } from "./notifications"; import { notifications } from "./notifications";
import { TabbyCompletionProvider } from "./TabbyCompletionProvider";
const configTarget = ConfigurationTarget.Global; 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 = { const openAuthPage: Command = {
command: "tabby.openAuthPage", command: "tabby.openAuthPage",
callback: (callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) => { callback: (callbacks?: { onAuthStart?: () => void; onAuthEnd?: () => void }) => {
@ -185,7 +178,7 @@ const acceptInlineCompletion: Command = {
const acceptInlineCompletionNextWord: Command = { const acceptInlineCompletionNextWord: Command = {
command: "tabby.inlineCompletion.acceptNextWord", command: "tabby.inlineCompletion.acceptNextWord",
callback: () => { callback: () => {
// FIXME: sent event when partially accept? TabbyCompletionProvider.getInstance().postEvent("accept_word");
commands.executeCommand("editor.action.inlineSuggest.acceptNextWord"); commands.executeCommand("editor.action.inlineSuggest.acceptNextWord");
}, },
}; };
@ -193,7 +186,8 @@ const acceptInlineCompletionNextWord: Command = {
const acceptInlineCompletionNextLine: Command = { const acceptInlineCompletionNextLine: Command = {
command: "tabby.inlineCompletion.acceptNextLine", command: "tabby.inlineCompletion.acceptNextLine",
callback: () => { 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"); commands.executeCommand("editor.action.inlineSuggest.acceptNextLine");
}, },
}; };
@ -206,7 +200,6 @@ export const tabbyCommands = () =>
openTabbyAgentSettings, openTabbyAgentSettings,
openKeybindings, openKeybindings,
gettingStarted, gettingStarted,
emitEvent,
openAuthPage, openAuthPage,
applyCallback, applyCallback,
triggerInlineCompletion, triggerInlineCompletion,

View File

@ -11,7 +11,7 @@ import { TabbyStatusBarItem } from "./TabbyStatusBarItem";
export async function activate(context: ExtensionContext) { export async function activate(context: ExtensionContext) {
console.debug("Activating Tabby extension", new Date()); console.debug("Activating Tabby extension", new Date());
await createAgentInstance(context); await createAgentInstance(context);
const completionProvider = new TabbyCompletionProvider(); const completionProvider = TabbyCompletionProvider.getInstance();
const statusBarItem = new TabbyStatusBarItem(completionProvider); const statusBarItem = new TabbyStatusBarItem(completionProvider);
context.subscriptions.push( context.subscriptions.push(
languages.registerInlineCompletionItemProvider({ pattern: "**" }, completionProvider), languages.registerInlineCompletionItemProvider({ pattern: "**" }, completionProvider),