2023-03-28 07:53:57 +00:00
|
|
|
import {
|
|
|
|
|
CancellationToken,
|
|
|
|
|
InlineCompletionContext,
|
|
|
|
|
InlineCompletionItem,
|
|
|
|
|
InlineCompletionItemProvider,
|
2023-08-17 14:28:41 +00:00
|
|
|
InlineCompletionTriggerKind,
|
2023-03-28 07:53:57 +00:00
|
|
|
Position,
|
|
|
|
|
Range,
|
|
|
|
|
TextDocument,
|
|
|
|
|
workspace,
|
|
|
|
|
} from "vscode";
|
2023-09-19 09:01:36 +00:00
|
|
|
import { EventEmitter } from "events";
|
2023-09-25 01:07:25 +00:00
|
|
|
import { CompletionRequest, CompletionResponse, LogEventRequest } from "tabby-agent";
|
2023-06-15 15:53:21 +00:00
|
|
|
import { agent } from "./agent";
|
2023-03-28 07:53:57 +00:00
|
|
|
|
2023-09-19 09:01:36 +00:00
|
|
|
export class TabbyCompletionProvider extends EventEmitter implements InlineCompletionItemProvider {
|
2023-09-25 01:07:25 +00:00
|
|
|
static instance: TabbyCompletionProvider;
|
|
|
|
|
static getInstance(): TabbyCompletionProvider {
|
|
|
|
|
if (!TabbyCompletionProvider.instance) {
|
|
|
|
|
TabbyCompletionProvider.instance = new TabbyCompletionProvider();
|
|
|
|
|
}
|
|
|
|
|
return TabbyCompletionProvider.instance;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-19 09:01:36 +00:00
|
|
|
private triggerMode: "automatic" | "manual" | "disabled" = "automatic";
|
|
|
|
|
private onGoingRequestAbortController: AbortController | null = null;
|
|
|
|
|
private loading: boolean = false;
|
2023-09-25 01:07:25 +00:00
|
|
|
private latestCompletions: CompletionResponse | null = null;
|
2023-07-06 16:31:15 +00:00
|
|
|
|
2023-09-25 01:07:25 +00:00
|
|
|
private constructor() {
|
2023-09-19 09:01:36 +00:00
|
|
|
super();
|
2023-03-28 07:53:57 +00:00
|
|
|
this.updateConfiguration();
|
|
|
|
|
workspace.onDidChangeConfiguration((event) => {
|
2023-08-10 07:57:59 +00:00
|
|
|
if (event.affectsConfiguration("tabby") || event.affectsConfiguration("editor.inlineSuggest")) {
|
2023-03-28 07:53:57 +00:00
|
|
|
this.updateConfiguration();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-19 09:01:36 +00:00
|
|
|
public getTriggerMode(): "automatic" | "manual" | "disabled" {
|
|
|
|
|
return this.triggerMode;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public isLoading(): boolean {
|
|
|
|
|
return this.loading;
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
public async provideInlineCompletionItems(
|
|
|
|
|
document: TextDocument,
|
|
|
|
|
position: Position,
|
|
|
|
|
context: InlineCompletionContext,
|
|
|
|
|
token: CancellationToken,
|
2023-09-19 09:01:36 +00:00
|
|
|
): Promise<InlineCompletionItem[] | null> {
|
|
|
|
|
if (context.triggerKind === InlineCompletionTriggerKind.Automatic && this.triggerMode === "manual") {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (context.triggerKind === InlineCompletionTriggerKind.Invoke && this.triggerMode === "automatic") {
|
|
|
|
|
return null;
|
2023-03-28 07:53:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-08 04:06:00 +00:00
|
|
|
// Check if autocomplete widget is visible
|
|
|
|
|
if (context.selectedCompletionInfo !== undefined) {
|
|
|
|
|
console.debug("Autocomplete widget is visible, skipping.");
|
2023-09-19 09:01:36 +00:00
|
|
|
return null;
|
2023-09-08 04:06:00 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
if (token?.isCancellationRequested) {
|
2023-09-25 01:07:25 +00:00
|
|
|
console.debug("Completion request is canceled before agent request.");
|
2023-09-19 09:01:36 +00:00
|
|
|
return null;
|
2023-04-17 16:00:08 +00:00
|
|
|
}
|
2023-06-02 03:58:34 +00:00
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
const replaceRange = this.calculateReplaceRange(document, position);
|
|
|
|
|
|
2023-09-25 01:07:25 +00:00
|
|
|
const request: CompletionRequest = {
|
2023-06-02 03:58:34 +00:00
|
|
|
filepath: document.uri.fsPath,
|
2023-09-15 03:05:46 +00:00
|
|
|
language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers
|
2023-06-02 03:58:34 +00:00
|
|
|
text: document.getText(),
|
|
|
|
|
position: document.offsetAt(position),
|
2023-08-17 14:28:41 +00:00
|
|
|
manually: context.triggerKind === InlineCompletionTriggerKind.Invoke,
|
2023-06-02 03:58:34 +00:00
|
|
|
};
|
2023-04-17 16:00:08 +00:00
|
|
|
|
2023-09-25 01:07:25 +00:00
|
|
|
this.latestCompletions = null;
|
|
|
|
|
|
2023-09-15 03:05:46 +00:00
|
|
|
const abortController = new AbortController();
|
2023-09-19 09:01:36 +00:00
|
|
|
this.onGoingRequestAbortController = abortController;
|
2023-09-15 03:05:46 +00:00
|
|
|
token?.onCancellationRequested(() => {
|
2023-09-25 01:07:25 +00:00
|
|
|
console.debug("Completion request is canceled.");
|
2023-09-15 03:05:46 +00:00
|
|
|
abortController.abort();
|
2023-04-05 06:27:23 +00:00
|
|
|
});
|
2023-09-15 03:05:46 +00:00
|
|
|
|
2023-09-19 09:01:36 +00:00
|
|
|
try {
|
|
|
|
|
this.loading = true;
|
|
|
|
|
this.emit("loadingStatusUpdated");
|
|
|
|
|
const result = await agent().provideCompletions(request, { signal: abortController.signal });
|
|
|
|
|
this.loading = false;
|
|
|
|
|
this.emit("loadingStatusUpdated");
|
2023-09-25 01:07:25 +00:00
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
}),
|
|
|
|
|
];
|
|
|
|
|
}
|
2023-09-19 09:01:36 +00:00
|
|
|
} catch (error: any) {
|
|
|
|
|
if (this.onGoingRequestAbortController === abortController) {
|
|
|
|
|
// the request was not replaced by a new request, set loading to false safely
|
|
|
|
|
this.loading = false;
|
|
|
|
|
this.emit("loadingStatusUpdated");
|
|
|
|
|
}
|
|
|
|
|
if (error.name !== "AbortError") {
|
|
|
|
|
console.debug("Error when providing completions", { error });
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-28 07:53:57 +00:00
|
|
|
|
2023-09-19 09:01:36 +00:00
|
|
|
return null;
|
2023-03-28 07:53:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-09-25 01:07:25 +00:00
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-28 07:53:57 +00:00
|
|
|
private updateConfiguration() {
|
2023-09-19 09:01:36 +00:00
|
|
|
if (!workspace.getConfiguration("editor").get("inlineSuggest.enabled", true)) {
|
|
|
|
|
this.triggerMode = "disabled";
|
|
|
|
|
this.emit("triggerModeUpdated");
|
|
|
|
|
} else {
|
|
|
|
|
this.triggerMode = workspace.getConfiguration("tabby").get("inlineCompletion.triggerMode", "automatic");
|
|
|
|
|
this.emit("triggerModeUpdated");
|
2023-08-10 07:57:59 +00:00
|
|
|
}
|
2023-03-28 07:53:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private hasSuffixParen(document: TextDocument, position: Position) {
|
|
|
|
|
const suffix = document.getText(
|
2023-07-14 06:54:24 +00:00
|
|
|
new Range(position.line, position.character, position.line, position.character + 1),
|
2023-03-28 07:53:57 +00:00
|
|
|
);
|
|
|
|
|
return ")]}".indexOf(suffix) > -1;
|
|
|
|
|
}
|
2023-05-11 14:55:34 +00:00
|
|
|
|
2023-07-13 08:31:20 +00:00
|
|
|
// FIXME: move replace range calculation to tabby-agent
|
2023-05-11 14:55:34 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-28 07:53:57 +00:00
|
|
|
}
|