tabby-vscode代码补全的一些阅读笔记

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) => {
}
相关推荐
鱼樱前端9 分钟前
重度Cursor用户 最强 Cursor Rules 和 Cursor 配置 mcp 以及最佳实践配置方式
前端
曼陀罗10 分钟前
Path<T> 、 keyof T 什么情况下用合适
前端
锈儿海老师15 分钟前
AST 工具大PK!Biome 的 GritQL 插件 vs. ast-grep,谁是你的菜?
前端·javascript·eslint
飞龙AI17 分钟前
鸿蒙Next实现瀑布流布局
前端
令狐寻欢18 分钟前
JavaScript中 的 Object.defineProperty 和 defineProperties
javascript
快起来别睡了19 分钟前
代理模式:送花风波
前端·javascript·架构
海底火旺21 分钟前
电影应用开发:从代码细节到用户体验优化
前端·css·html
陈随易30 分钟前
Gitea v1.24.0发布,自建github神器
前端·后端·程序员
前端付豪32 分钟前
汇丰银行技术架构揭秘:全球交易稳定背后的“微服务+容灾+零信任安全体系”
前端·后端·架构
邹荣乐35 分钟前
uni-app开发微信小程序的报错[渲染层错误]排查及解决
前端·微信小程序·uni-app