语言服务器索引格式 (LSIF)
2019 年 2 月 19 日 作者:Dirk Bäumer
无需检出即可实现丰富的代码导航
作为开发人员,您会花费大量时间阅读和审查代码,而不一定总是编写新的源代码。例如,您可能希望浏览 GitHub 等仓库中的现有代码库,或者您可能希望审查同事的 Pull Request。
通常,您需要检出分支或克隆仓库,将源代码拉取到本地机器上,打开您首选的开发工具,然后才能阅读和导航代码。如果没有先克隆仓库就能做到这一点,那不是很酷吗?想象一下,无需下载源代码就能获得智能代码功能,例如悬停信息、转到定义和查找所有引用。博文《丰富的代码导航体验初探》说明了 Pull Request 审查的这种场景。
语言服务器索引格式 (LSIF,发音类似于 "else if") 的目标是在不需要本地源代码副本的情况下,支持开发工具或 Web UI 中的丰富代码导航。该格式在精神上与语言服务器协议 (LSP) 相似,LSP 简化了将丰富的代码编辑功能集成到开发工具中的过程。
为什么不直接使用现有的 LSP 语言服务器呢?LSP 提供了自动完成、键入时格式化和丰富的代码导航等丰富的代码编写功能。为了高效地提供这些功能,语言服务器需要所有源代码文件都可以在本地磁盘上使用。LSP 语言服务器也可能将部分或全部文件读入内存,并计算抽象语法树来支持这些功能。语言服务器索引格式的目标是扩展 LSP 协议,以支持丰富的代码导航功能,而无需这些要求。LSIF 定义了一种标准格式,供语言服务器或其他编程工具发出其关于代码工作区的知识。然后可以使用这些持久化的信息来回答同一工作区的 LSP 请求,而无需运行语言服务器。
语言服务器索引格式
LSIF 基于 LSP,并使用 LSP 中定义的数据类型。从高层次上看,LSIF 对语言服务器请求返回的数据进行建模。与 LSP 相同,LSIF 不包含任何程序符号信息,LSIF 也不定义任何符号语义(例如,什么构成了符号的定义,或者一个方法是否覆盖了另一个方法)。因此,LSIF 不定义符号数据库,这与 LSP 的方法是一致的。
使用现有的 LSP 数据类型作为 LSIF 的基础还有另一个优点,因为 LSIF 可以轻松集成到已经理解 LSP 的工具或服务器中。
让我们来看一个例子。我们从一个名为 sample.ts 的简单 Typescript 文件开始,其内容如下
function bar(): void {}
将鼠标悬停在 bar() 上时,Visual Studio Code 中会显示以下悬停信息

此悬停信息在 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 }
相应的图如下所示

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 支持的 LSP 请求的两个示例。当前版本的LSIF 规范还支持文档符号、文档链接、转到定义、转到声明、转到类型定义、查找所有引用和转到实现。
我们需要您的反馈!
我们在 LSIF 规范方面取得了良好的初步进展,我们希望向社区开放讨论,以便您可以了解我们正在做什么。有关反馈,请评论问题 语言服务器索引格式。
如何开始
要开始使用 LSIF,您可以查看以下资源
- LSIF 规范 - 该文档还描述了为保持发出的数据紧凑所做的一些额外优化。
- TypeScript 的 LSIF 索引 - 一个为 TypeScript 生成 LSIF 的工具。README 提供了使用该工具的说明。
- Visual Studio Code 的 LSIF 扩展 - 一个用于 VS Code 的扩展,它使用 LSIF JSON 转储提供语言理解功能。如果您实现了新的 LSIF 生成器,您可以使用此扩展来验证它对任意源代码的效果。