🚀 在 VS Code 中

Language Server Index Format (LSIF)

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

无需检出的丰富代码导航

作为一名开发者,您的大部分时间都在阅读和审查代码,而不是编写新的源代码。例如,您可能想要浏览 GitHub 等仓库中现有的代码库,或者您可能想要审查同事的拉取请求。

通常,您会检出一个分支或克隆一个仓库,将源代码拉取到本地机器上,打开您偏好的开发工具,然后最终您可以阅读和导航代码。如果您可以在不首先克隆仓库的情况下做到这一点,岂不是很酷?想象一下,无需下载源代码即可获得智能代码功能,例如悬停信息、“转到定义”和“查找所有引用”。博文 Rich code navigation experience 初探 说明了拉取请求审查的这种场景。

Language Server Index Format(LSIF,发音类似于“else if”)的目标是在开发工具或 Web UI 中支持丰富的代码导航,而无需源代码的本地副本。该格式在精神上类似于 Language Server Protocol (LSP),后者简化了将丰富的代码编辑功能集成到开发工具中的过程。

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

Language Server Index Format

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

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

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

function bar(): void {}

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


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 中的 b 上或 bar 中的 r 上时,返回的悬停值是相同的。为了使发出的数据更紧凑,LSIF 使用范围而不是位置。对于此示例,LSIF 工具发出元组 ['textDocument/hover', 'file:///Users/username/sample.ts', { start: { line: 0, character: 9 }, end: { line: 0, character: 12 }],其中包括范围信息。

LSIF 使用图来发出此信息。在图中,LSP 请求使用边来表示。文档、范围或请求结果(例如,悬停)使用顶点来表示。此格式具有以下优点

  • 对于给定的代码范围,可能会有不同的结果。对于给定的标识符范围,用户对悬停值、定义的位置或查找所有引用感兴趣。因此,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 中以 [request, document] -> result 的形式建模。

让我们看另一个例子

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 生成器,则可以使用此扩展来使用任意源代码对其进行验证。