语言服务器索引格式 (LSIF)
2019年2月19日,作者:Dirk Bäumer
无需检出(Checkout)即可实现丰富的代码导航
作为开发者,你大部分时间都在阅读和审查代码,而不一定是在编写新源代码。例如,你可能想要浏览 GitHub 等仓库中现有的代码库,或者想要审查同事的拉取请求(Pull Request)。
通常情况下,你需要检出(check out)分支或克隆(clone)一个仓库,将源代码下载到本地机器,打开你偏好的开发工具,然后才能阅读和导航代码。如果不需要先克隆仓库就能做到这一点,那该多酷啊?想象一下,无需下载源代码即可获得智能代码功能,如悬停信息、转到定义(Go to Definition)和查找所有引用(Find All References)。这篇博客文章 《代码导航体验初探》 展示了拉取请求审查中的这一场景。
语言服务器索引格式(Language Server Index Format,简称 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 {}
在 Visual Studio Code 中,悬停在 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 使用范围(Range)而不是位置(Position)。对于此示例,LSIF 工具输出元组 ['textDocument/hover', 'file:///Users/username/sample.ts', { start: { line: 0, character: 9 }, end: { line: 0, character: 12 }],其中包含范围信息。
LSIF 使用图表来输出这些信息。在图中,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 }
相应的图表如下所示

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 规范方面已经取得了良好的初步进展,我们希望向社区开放对话,让大家了解我们正在做的事情。如需反馈,请在 Language Server Index Format 的议题下发表评论。
如何开始
要开始使用 LSIF,你可以查看以下资源
- LSIF 规范 - 该文档还描述了一些为保持输出数据紧凑而进行的其他优化。
- TypeScript 的 LSIF 索引 - 一个为 TypeScript 生成 LSIF 的工具。README 提供了使用该工具的说明。
- 适用于 LSIF 的 Visual Studio Code 扩展 - 一个 VS Code 扩展,它使用 LSIF JSON 转储提供代码理解功能。如果你实现了新的 LSIF 生成器,可以使用此扩展通过任意源代码来验证它。