在 VS Code 中尝试

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

2019 年 2 月 19 日,由 Dirk Bäumer 撰写

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

作为开发者,您花费大量时间阅读和审查代码,而不一定是在编写新的源代码。例如,您可能想在像 GitHub 这样的存储库中浏览现有代码库,或者您可能想审查同事的拉取请求。

通常,您会检出一个分支或克隆一个存储库,将源代码拉取到本地计算机,打开首选的开发工具,然后才能最终阅读和导航代码。如果无需先克隆存储库就能做到这一点,岂不是很酷?想象一下,无需下载源代码即可获得智能代码功能,例如悬停信息、转到定义和查找所有引用。这篇博客文章,初探丰富的代码导航体验,阐述了针对拉取请求审查的这种场景。

语言服务器索引格式 (LSIF,发音类似于 "else if") 的目标是支持在开发工具或 Web UI 中实现丰富的代码导航,而无需本地源代码副本。这种格式在精神上类似于 语言服务器协议 (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 {}

将鼠标悬停在 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 中的 br 上时,都会返回相同的悬停值。为了使发出的数据更紧凑,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] -> 结果 的形式建模。

我们来看另一个示例

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 规范方面取得了良好的初步进展,我们希望向社区开放讨论,以便您了解我们正在进行的工作。如需反馈,请在议题 语言服务器索引格式 下评论。

如何开始

要开始使用 LSIF,您可以参考以下资源

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