语言服务器索引格式 (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 中显示以下悬停信息
此悬停信息使用 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 提供了使用该工具的说明。
- VS Code 的 LSIF 扩展 - VS Code 的扩展,使用 LSIF JSON 转储提供语言理解功能。如果您实现了一个新的 LSIF 生成器,可以使用此扩展来验证它是否适用于任意源代码。