语言服务器索引格式 (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 也不定义任何符号语义(例如,什么构成符号的定义,或者一个方法是否覆盖另一个方法)。因此,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]
-> 结果的形式建模。
让我们看另一个例子
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 提供了有关使用该工具的说明。
- 用于 LSIF 的 Visual Studio Code 扩展- 用于 VS Code 的扩展,该扩展使用 LSIF JSON 转储提供语言理解功能。如果您实现了新的 LSIF 生成器,则可以使用此扩展来使用任意源代码对其进行验证。