1.初始化-功能注册
typescript
// connection是插件服务,同vscode通信
initialize(connection: Connection, clientCapabilities: ClientCapabilities): ServerCapabilities {
this.lspConnection = connection;
this.clientCapabilities = clientCapabilities;
let serverCapabilities: ServerCapabilities = {};
// 注册代码补全功能,当vscode发起completion请求时会执行该回调
if (clientCapabilities.textDocument?.completion) {
connection.onCompletion(async (params, token) => {
return this.provideCompletion(params, token);
});
serverCapabilities = {
...serverCapabilities,
completionProvider: {},
};
}
if (clientCapabilities.textDocument?.inlineCompletion) {
connection.onRequest(InlineCompletionRequest.type, async (params, token) => {
return this.provideInlineCompletion(params, token);
});
serverCapabilities = {
...serverCapabilities,
inlineCompletionProvider: true,
};
}
// ...
return serverCapabilities;
}
2.补全过程
1.provideCompletion
函数体
在supermind
场景下,用户编写的代码为单文件,不存在额外的上下文需要处理
typescript
async provideCompletion(params: CompletionParams, token: CancellationToken): Promise<CompletionList | null> {
// tabbyApiClient是同服务端通信的示例,查询服务端能否提供服务
if (!this.tabbyApiClient.isCodeCompletionApiAvailable()) {
throw {
name: "CodeCompletionFeatureNotAvailableError",
message: "Code completion feature not available",
};
}
// 如果是取消请求,则不处理
if (token.isCancellationRequested) {
return null;
}
// 中断控制器,这一请求有可能会被频繁中断取消
const abortController = new AbortController();
token.onCancellationRequested(() => abortController.abort());
try {
// 向vscode查询一些必要信息组装为请求参数,关键是text:上下文(带前后缀),position:应该是光标位置
// 前后缀同代码语言的额外上下文代码文本
const request = await this.completionParamsToCompletionRequest(params, token);
if (!request) {
return null;
}
// 向服务端发起代码补全请求
//
const response = await this.provideCompletions(request.request, abortController.signal);
if (!response) {
return null;
}
// 执行补全操作
return this.toCompletionList(response, params, request.additionalPrefixLength);
} catch (error) {
return null;
}
}
2.关于请求组装函数completionParamsToCompletionRequest
typescript
private async completionParamsToCompletionRequest(
params: CompletionParams,
token?: CancellationToken,
): Promise<{ request: CompletionRequest; additionalPrefixLength?: number } | null> {
// 调用了`textDocumentPositionParamsToCompletionRequest`
const result = await this.textDocumentPositionParamsToCompletionRequest(params, token);
if (!result) {
return null;
}
result.request.manually = params.context?.triggerKind === CompletionTriggerKind.Invoked;
return result;
}
// textDocumentPositionParamsToCompletionRequest
private async textDocumentPositionParamsToCompletionRequest(
params: TextDocumentPositionParams,
token?: CancellationToken,
): Promise<{ request: CompletionRequest; additionalPrefixLength?: number } | null> {
const { textDocument, position } = params;
this.logger.trace("Building completion context...", { uri: textDocument.uri });
const document = this.documents.get(textDocument.uri);
if (!document) {
this.logger.trace("Document not found, cancelled.");
return null;
}
// 组装当前补全文件的信息参数
const request: CompletionRequest = {
filepath: document.uri,
language: document.languageId,
text: document.getText(),
position: document.offsetAt(position),
};
// 处理补充额外的上下文
const notebookCell = this.notebooks.getNotebookCell(textDocument.uri);
let additionalContext: { prefix: string; suffix: string } | undefined = undefined;
if (notebookCell) {
this.logger.trace("Notebook cell found:", { cell: notebookCell.kind });
additionalContext = this.buildNotebookAdditionalContext(document, notebookCell);
}
if (additionalContext) {
this.logger.trace("Applying notebook additional context...", { additionalContext });
request.text = additionalContext.prefix + request.text + additionalContext.suffix;
request.position += additionalContext.prefix.length;
}
const connection = this.lspConnection;
if (connection && this.clientCapabilities?.tabby?.editorOptions) {
this.logger.trace("Collecting editor options...");
const editorOptions: EditorOptions | null = await connection.sendRequest(
EditorOptionsRequest.type,
{
uri: params.textDocument.uri,
},
token,
);
this.logger.trace("Collected editor options:", { editorOptions });
// 添加缩进信息
request.indentation = editorOptions?.indentation;
}
if (connection && this.clientCapabilities?.workspace) {
this.logger.trace("Collecting workspace folders...");
const workspaceFolders = await connection.workspace.getWorkspaceFolders();
this.logger.trace("Collected workspace folders:", { workspaceFolders });
// 添加工作区
request.workspace = workspaceFolders?.find((folder) => document.uri.startsWith(folder.uri))?.uri;
}
this.logger.trace("Collecting git context...");
// 若是远程文件,则添加远程信息
const repo: GitRepository | null = await this.gitContextProvider.getRepository({ uri: document.uri }, token);
this.logger.trace("Collected git context:", { repo });
if (repo) {
request.git = {
root: repo.root,
remotes: repo.remoteUrl ? [{ name: "", url: repo.remoteUrl }] : repo.remotes ?? [],
};
}
if (connection && this.clientCapabilities?.tabby?.languageSupport) {
request.declarations = await this.collectDeclarationSnippets(connection, document, position, token);
}
request.relevantSnippetsFromChangedFiles = await this.collectSnippetsFromRecentlyChangedFiles(document, position);
request.relevantSnippetsFromOpenedFiles = await this.collectSnippetsFromOpenedFiles();
this.logger.trace("Completed completion context:", { request });
return { request, additionalPrefixLength: additionalContext?.prefix.length };
}
3.远程服务请求处理provideCompletions
关于请求信号、缓存、防抖,需要做相同处理
typescript
private async provideCompletions(
request: CompletionRequest,
signal?: AbortSignal,
): Promise<CompletionSolution | null> {
this.logger.debug("Function providedCompletions called.");
const config = this.configurations.getMergedConfig();
// 互斥锁控制
if (this.mutexAbortController && !this.mutexAbortController.signal.aborted) {
this.mutexAbortController.abort(new MutexAbortError());
}
this.mutexAbortController = new AbortController();
const signals = abortSignalFromAnyOf([this.mutexAbortController.signal, signal]);
// 处理请求参数,组装成上下文
const context = new CompletionContext(request);
// 过滤非法请求------补全前需要有内容
if (!context.isValid()) {
return null;
}
// 做了缓存处理,hash组成:
/**
*
hashObject({
filepath: this.filepath,
language: this.language,
prefix: this.prefix,
currentLineSuffix: lineEnd ? "" : this.currentLineSuffix,
nextLines: this.suffixLines.slice(1).join(""),
position: this.position,
clipboard: this.clipboard,
declarations: this.declarations,
relevantSnippetsFromChangedFiles: this.relevantSnippetsFromChangedFiles,
});
*/
let solution: CompletionSolution | undefined = undefined;
let cachedSolution: CompletionSolution | undefined = undefined;
if (this.completionCache.has(context.hash)) {
cachedSolution = this.completionCache.get(context.hash);
}
try {
// 命中缓存
if (cachedSolution && (!request.manually || cachedSolution.isCompleted)) {
// Found cached solution
// TriggerKind is Automatic, or the solution is completed
// Return cached solution, do not need to fetch more choices
// Debounce before continue processing cached solution
await this.completionDebounce.debounce(
{
request,
config: config.completion.debounce,
responseTime: 0,
},
signals,
);
solution = cachedSolution.withContext(context);
this.logger.info("Completion cache hit.");
} else if (!request.manually) {
// No cached solution
// TriggerKind is Automatic
// We need to fetch the first choice
// 防抖
const averageResponseTime = this.tabbyApiClient.getCompletionRequestStats().stats().stats.averageResponseTime;
await this.completionDebounce.debounce(
{
request,
config: config.completion.debounce,
responseTime: averageResponseTime,
},
signals,
);
solution = new CompletionSolution(context);
// 请求远程
this.logger.info(`Fetching completion...`);
try {
const response = await this.tabbyApiClient.fetchCompletion(
{
language: context.language,
segments: context.buildSegments(config.completion.prompt),
temperature: undefined,
},
signals,
this.completionStats,
);
const completionItem = CompletionItem.createFromResponse(context, response);
// 处理缓存
solution.add(...(await preCacheProcess([completionItem], config.postprocess)));
} catch (error) {
if (isCanceledError(error)) {
this.logger.info(`Fetching completion canceled.`);
solution = undefined;
}
}
} else {
// 没有缓存或未完成补全
solution = cachedSolution?.withContext(context) ?? new CompletionSolution(context);
this.logger.info(`Fetching more completions...`);
try {
let tries = 0;
while (
solution.items.length < config.completion.solution.maxItems &&
tries < config.completion.solution.maxTries
) {
tries++;
const response = await this.tabbyApiClient.fetchCompletion(
{
language: context.language,
segments: context.buildSegments(config.completion.prompt),
temperature: config.completion.solution.temperature,
},
signals,
this.completionStats,
);
const completionItem = CompletionItem.createFromResponse(context, response);
// postprocess: preCache
solution.add(...(await preCacheProcess([completionItem], config.postprocess)));
if (signals.aborted) {
throw signals.reason;
}
}
// Mark the solution as completed
solution.isCompleted = true;
} catch (error) {
if (isCanceledError(error)) {
this.logger.info(`Fetching completion canceled.`);
solution = undefined;
}
}
}
// Postprocess solution
if (solution) {
// 更新缓存
this.completionCache.update(solution);
// postprocess: postCache
solution = solution.withItems(...(await postCacheProcess(solution.items, config.postprocess)));
if (signals.aborted) {
throw signals.reason;
}
}
} catch (error) {
if (!isCanceledError(error)) {
this.logger.error(`Providing completions failed.`, error);
}
}
if (solution) {
this.completionStats.addProviderStatsEntry({ triggerMode: request.manually ? "manual" : "auto" });
this.logger.info(`Completed processing completions, choices returned: ${solution.items.length}.`);
this.logger.trace("Completion solution:", { solution: solution.toInlineCompletionList() });
}
return solution ?? null;
}
4.编辑器补全操作
这一部分为向vscode
返回一段json
数据,不展开
3.设计参考
- 使用防抖避免重复请求,防抖时长需要具体测试
- 需要添加缓存机制,参考实现类似的
hash
签名 - 后端服务正常与否的状态管理,能够避免在服务无法工作的情况下,发送不必要的请求。
4.基本结构
typescript
class codeCompletionHelper {
health = false;
abortController = new AbortController();
completionCache = new Map();
constructor() {
}
initialize () {};
async checkHealth () {};
async getCompleteSolution () {};
updateCompletionCache () {};
}
export codeCompletionHelper = new codeCompletionHelper()
const debounce = async (delay, signal) => {
}