参加你附近的 ,了解 VS Code 中的 AI 辅助开发。

语言服务器索引格式 (LSIF)

2019 年 2 月 19 日,作者:Dirk Bäumer

无需检出代码即可实现丰富的代码导航

作为一名开发人员,您会花费大量时间来阅读和审查代码,而不一定是在编写新的源代码。例如,您可能想要在 GitHub 这样的代码仓库中浏览现有的代码库,或者想要审查同事的拉取请求 (Pull Request)。

通常,您需要检出一个分支或克隆一个仓库,将源代码拉取到本地计算机上,然后打开您喜欢的开发工具,最后才能阅读和导航代码。如果无需先克隆仓库就能做到这一点,那岂不是很酷?想象一下,无需下载源代码就能获得悬停信息、转到定义和查找所有引用等智能代码功能。博文《丰富的代码导航体验初探》就阐述了在拉取请求审查中的这种场景。

语言服务器索引格式 (LSIF,发音类似“else if”) 的目标是在开发工具或 Web UI 中支持丰富的代码导航,而无需本地源代码副本。该格式在精神上类似于语言服务器协议 (LSP),LSP 简化了将丰富的代码编辑功能集成到开发工具中的过程。

为什么不直接使用现有的 LSP 语言服务器呢?LSP 提供了丰富的代码编写功能,如自动完成、键入时格式化和丰富的代码导航。为了高效地提供这些功能,语言服务器要求所有源代码文件都必须在本地磁盘上可用。LSP 语言服务器还可能将部分或全部文件读入内存并计算抽象语法树来支持这些功能。而语言服务器索引格式的目标是扩展 LSP 协议,以便在没有这些要求的情况下支持丰富的代码导航功能。LSIF 为语言服务器或其他编程工具定义了一种标准格式,用于输出它们关于代码工作区的知识。这些持久化的信息随后可以用来响应针对同一工作区的 LSP 请求,而无需运行语言服务器。

语言服务器索引格式

LSIF 建立在 LSP 之上,并使用与 LSP 中定义相同的数据类型。从高层次来看,LSIF 对语言服务器请求返回的数据进行建模。与 LSP 相同,LSIF 不包含任何程序符号信息,也不定义任何符号语义(例如,什么构成了符号的定义,或者一个方法是否覆盖了另一个方法)。因此,LSIF 并不定义符号数据库,这与 LSP 的方法是一致的。

使用现有的 LSP 数据类型作为 LSIF 的基础还有另一个优势,因为 LSIF 可以轻松地集成到已经理解 LSP 的工具或服务器中。

我们来看一个例子。我们从一个名为 sample.ts 的简单 Typescript 文件开始,其内容如下:

function bar(): void {}

在 Visual Studio Code 中,将鼠标悬停在 bar() 上会显示以下悬停信息:


Hover over Bar


此悬停信息在 LSP 中使用 Hover 类型表示:

export interface Hover {
  /**
   * The hover's content
   */
  contents: MarkupContent | MarkedString | MarkedString[];
  /**
   * An optional range
   */
  range?: Range;
}

在上面的例子中,具体的值是:

{
  contents: [{ language: 'typescript', value: 'function bar(): void' }];
}

客户端工具会通过向服务器发送一个针对文档 file:///Users/username/sample.ts 中位置 {line: 0, character: 10}textDocument/hover 请求,来从语言服务器获取悬停内容。

LSIF 定义了一种格式,语言服务器或独立工具通过输出这种格式来描述元组 ['textDocument/hover', 'file:///Users/username/sample.ts', {line: 0, character: 10}] 解析为上述悬停信息。这些数据随后可以被提取并持久化到数据库中。

LSP 请求是基于位置的,但结果通常只在某个范围内变化,而不是针对单个位置。在上面的悬停示例中,对于标识符 bar 的所有位置,悬停值都是相同的。这意味着当用户将鼠标悬停在 bar 中的 br 上时,返回的悬停值是相同的。为了使输出的数据更紧凑,LSIF 使用范围 (range) 而不是位置 (position)。对于此示例,LSIF 工具会输出元组 ['textDocument/hover', 'file:///Users/username/sample.ts', { start: { line: 0, character: 9 }, end: { line: 0, character: 12 }],其中包含了范围信息。

LSIF 使用图 (graph) 来输出这些信息。在图中,一个 LSP 请求由一条边 (edge) 表示。文档、范围或请求结果(例如悬停信息)则由顶点 (vertex) 表示。这种格式有以下好处:

  • 对于给定的代码范围,可以有不同的结果。对于给定的标识符范围,用户可能对悬停值、定义位置或查找所有引用感兴趣。因此,LSIF 将这些结果与该范围关联起来。
  • 通过添加新的边或顶点类型,可以轻松地用额外的请求类型或结果来扩展该格式。
  • 一旦数据可用就可以立即输出。这使得流式处理成为可能,而无需在内存中存储大量数据。例如,对于一个文档,应该在解析过程中为每个文件输出数据。

对于悬停示例,输出的 LSIF 图数据如下所示:

// a vertex representing the document
{ id: 1, type: "vertex", label: "document", uri: "file:///Users/username/sample.ts", languageId: "typescript" }
// a vertex representing the range for the identifier bar
{ id: 4, type: "vertex", label: "range", start: { line: 0, character: 9}, end: { line: 0, character: 12 } }
// an edge saying that the document with id 1 contains the range with id 4
{ id: 5, type: "edge", label: "contains", outV: 1, inV: 4}
// a vertex representing the actual hover result
{ id: 6, type: "vertex", label: "hoverResult",
  result: {
    contents: [
      { language: "typescript", value: "function bar(): void" }
    ]
  }
}
// an edge linking the hover result to the range.
{ id: 7, type: "edge", label: "textDocument/hover", outV: 4, inV: 6 }

对应的图如下所示:

LSIF graph for a hover

LSP 还支持仅接受文档作为参数的请求(它们不是基于位置的)。对于代码理解有用的示例请求包括获取所有文档符号的列表或计算所有折叠范围。这些请求在 LSIF 中以 [请求, 文档] -> 结果 的形式建模。

我们再看另一个例子:

function bar(): void {
  console.log('Hello World!');
}

对于包含上述函数 bar 的文档,其折叠范围结果会像这样输出:

// a vertex representing the document
{ id: 1, type: "vertex", label: "document", uri: "file:///Users/username/sample.ts", languageId: "typescript" }
// a vertex representing the folding result
{ id: 2, type: "vertex", label: "foldingRangeResult", result: [ { startLine: 0, startCharacter: 20, endLine: 2, endCharacter: 1 } ] }
// an edge connecting the folding result to the document.
{ id: 3, type: "edge", label: "textDocument/foldingRange", outV: 1, inV: 2 }

LSIF graph for a folding range result

这只是 LSIF 支持的两个 LSP 请求示例。当前版本的 LSIF 规范 还支持文档符号、文档链接、转到定义、转到声明、转到类型定义、查找所有引用以及转到实现。

我们需要您的反馈!

我们已经在 LSIF 规范上取得了良好的初步进展,我们希望向社区开放对话,以便大家可以了解我们正在进行的工作。如有反馈,请在问题 Language Server Index Format 中发表评论。

如何开始

要开始使用 LSIF,您可以查看以下资源:

  • LSIF 规范 - 该文档还描述了一些为保持输出数据紧凑而做的额外优化。
  • TypeScript 的 LSIF 索引器 - 一个为 TypeScript 生成 LSIF 的工具。README 文件提供了该工具的使用说明。
  • 用于 LSIF 的 Visual Studio Code 扩展 - 一个 VS Code 扩展,它使用 LSIF JSON 转储文件来提供语言理解功能。如果您实现一个新的 LSIF 生成器,可以使用此扩展来验证其在任意源代码上的效果。