2023-03-28 07:53:57 +00:00
|
|
|
import {
|
|
|
|
|
CancellationToken,
|
|
|
|
|
InlineCompletionContext,
|
|
|
|
|
InlineCompletionItem,
|
|
|
|
|
InlineCompletionItemProvider,
|
|
|
|
|
InlineCompletionList,
|
|
|
|
|
Position,
|
|
|
|
|
ProviderResult,
|
|
|
|
|
Range,
|
|
|
|
|
TextDocument,
|
|
|
|
|
workspace,
|
|
|
|
|
} from "vscode";
|
2023-04-17 16:00:08 +00:00
|
|
|
import { CompletionResponse, EventType, ChoiceEvent, ApiError, CancelablePromise, CancelError } from "./generated";
|
2023-04-13 07:28:30 +00:00
|
|
|
import { TabbyClient } from "./TabbyClient";
|
2023-05-11 14:55:34 +00:00
|
|
|
import { CompletionCache } from "./CompletionCache";
|
2023-03-29 10:30:13 +00:00
|
|
|
import { sleep } from "./utils";
|
2023-03-28 07:53:57 +00:00
|
|
|
|
|
|
|
|
export class TabbyCompletionProvider implements InlineCompletionItemProvider {
|
|
|
|
|
private uuid = Date.now();
|
|
|
|
|
private latestTimestamp: number = 0;
|
2023-04-17 16:00:08 +00:00
|
|
|
private pendingCompletion: CancelablePromise<CompletionResponse> | null = null;
|
2023-03-28 07:53:57 +00:00
|
|
|
|
2023-03-29 10:30:13 +00:00
|
|
|
private tabbyClient = TabbyClient.getInstance();
|
2023-05-11 14:55:34 +00:00
|
|
|
private completionCache = new CompletionCache();
|
2023-03-28 07:53:57 +00:00
|
|
|
// User Settings
|
|
|
|
|
private enabled: boolean = true;
|
2023-04-17 01:12:16 +00:00
|
|
|
private suggestionDelay: number = 150;
|
2023-03-28 07:53:57 +00:00
|
|
|
|
|
|
|
|
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<InlineCompletionItem[] | InlineCompletionList> {
|
|
|
|
|
const emptyResponse = Promise.resolve([] as InlineCompletionItem[]);
|
|
|
|
|
if (!this.enabled) {
|
|
|
|
|
console.debug("Extension not enabled, skipping.");
|
|
|
|
|
return emptyResponse;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-11 14:55:34 +00:00
|
|
|
const promptRange = this.calculatePromptRange(position);
|
|
|
|
|
const prompt = document.getText(promptRange);
|
2023-03-28 07:53:57 +00:00
|
|
|
if (this.isNil(prompt)) {
|
|
|
|
|
console.debug("Prompt is empty, skipping");
|
|
|
|
|
return emptyResponse;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const currentTimestamp = Date.now();
|
|
|
|
|
this.latestTimestamp = currentTimestamp;
|
|
|
|
|
|
2023-04-17 01:12:16 +00:00
|
|
|
await sleep(this.suggestionDelay);
|
2023-03-28 07:53:57 +00:00
|
|
|
if (currentTimestamp < this.latestTimestamp) {
|
|
|
|
|
return emptyResponse;
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-11 14:55:34 +00:00
|
|
|
const replaceRange = this.calculateReplaceRange(document, position);
|
|
|
|
|
|
|
|
|
|
const compatibleCache = this.completionCache.findCompatible(document.uri, document.getText(), document.offsetAt(position));
|
|
|
|
|
if (compatibleCache) {
|
|
|
|
|
const completions = this.toInlineCompletions(compatibleCache, replaceRange);
|
|
|
|
|
console.debug("Use cached completions: ", compatibleCache);
|
|
|
|
|
return Promise.resolve(completions);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-28 07:53:57 +00:00
|
|
|
console.debug(
|
2023-03-28 12:35:59 +00:00
|
|
|
"Requesting: ",
|
2023-03-28 07:53:57 +00:00
|
|
|
{
|
|
|
|
|
uuid: this.uuid,
|
|
|
|
|
timestamp: currentTimestamp,
|
2023-04-05 06:27:23 +00:00
|
|
|
prompt,
|
|
|
|
|
language: document.languageId
|
2023-03-28 07:53:57 +00:00
|
|
|
}
|
|
|
|
|
);
|
2023-04-13 07:28:30 +00:00
|
|
|
|
2023-04-17 16:00:08 +00:00
|
|
|
if (this.pendingCompletion) {
|
|
|
|
|
this.pendingCompletion.cancel();
|
|
|
|
|
}
|
|
|
|
|
this.pendingCompletion = this.tabbyClient.api.default.completionsV1CompletionsPost({
|
2023-04-13 07:28:30 +00:00
|
|
|
prompt: prompt as string, // Prompt is already nil-checked
|
2023-04-13 13:42:59 +00:00
|
|
|
language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers
|
2023-04-17 16:00:08 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const completion = await this.pendingCompletion.then((response: CompletionResponse) => {
|
2023-04-13 07:28:30 +00:00
|
|
|
this.tabbyClient.changeStatus("ready");
|
|
|
|
|
return response;
|
2023-04-17 16:00:08 +00:00
|
|
|
}).catch((_: CancelError) => {
|
|
|
|
|
return null;
|
2023-04-13 07:28:30 +00:00
|
|
|
}).catch((err: ApiError) => {
|
|
|
|
|
console.error(err);
|
|
|
|
|
this.tabbyClient.changeStatus("disconnected");
|
|
|
|
|
return null;
|
2023-04-05 06:27:23 +00:00
|
|
|
});
|
2023-04-17 16:00:08 +00:00
|
|
|
this.pendingCompletion = null;
|
2023-03-28 07:53:57 +00:00
|
|
|
|
2023-05-11 14:55:34 +00:00
|
|
|
if (completion) {
|
|
|
|
|
this.completionCache.add({
|
|
|
|
|
documentId: document.uri,
|
|
|
|
|
promptRange: { start: document.offsetAt(promptRange.start), end: document.offsetAt(promptRange.end) },
|
|
|
|
|
prompt,
|
|
|
|
|
completion,
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-03-29 10:30:13 +00:00
|
|
|
const completions = this.toInlineCompletions(completion, replaceRange);
|
2023-03-28 07:53:57 +00:00
|
|
|
console.debug("Result completions: ", completions);
|
|
|
|
|
return Promise.resolve(completions);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateConfiguration() {
|
|
|
|
|
const configuration = workspace.getConfiguration("tabby");
|
|
|
|
|
this.enabled = configuration.get("enabled", true);
|
2023-04-17 01:12:16 +00:00
|
|
|
this.suggestionDelay = configuration.get("suggestionDelay", 150);
|
2023-03-28 07:53:57 +00:00
|
|
|
}
|
|
|
|
|
|
2023-04-13 07:28:30 +00:00
|
|
|
private isNil(value: string | undefined | null): boolean {
|
2023-03-28 07:53:57 +00:00
|
|
|
return value === undefined || value === null || value.length === 0;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-13 07:28:30 +00:00
|
|
|
private toInlineCompletions(tabbyCompletion: CompletionResponse | null, range: Range): InlineCompletionItem[] {
|
2023-03-28 07:53:57 +00:00
|
|
|
return (
|
2023-04-13 07:28:30 +00:00
|
|
|
tabbyCompletion?.choices?.map((choice: any) => {
|
|
|
|
|
let event: ChoiceEvent = {
|
|
|
|
|
type: EventType.SELECT,
|
|
|
|
|
completion_id: tabbyCompletion.id,
|
|
|
|
|
choice_index: choice.index,
|
|
|
|
|
};
|
|
|
|
|
return new InlineCompletionItem(choice.text, range, {
|
|
|
|
|
title: "Tabby: Emit Event",
|
|
|
|
|
command: "tabby.emitEvent",
|
|
|
|
|
arguments: [event],
|
|
|
|
|
});
|
|
|
|
|
}) || []
|
2023-03-28 07:53:57 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
2023-05-11 14:55:34 +00:00
|
|
|
|
|
|
|
|
private calculatePromptRange(position: Position): Range {
|
|
|
|
|
const maxLines = 20;
|
|
|
|
|
const firstLine = Math.max(position.line - maxLines, 0);
|
|
|
|
|
return new Range(firstLine, 0, position.line, position.character);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
}
|