语言服务器索引格式 (LSIF)
2019年2月19日,作者:Dirk Bäumer
无需签出的丰富代码导航
作为开发者,您花费大量时间阅读和审查代码,而不一定是编写新的源代码。例如,您可能想要浏览像GitHub这样的代码库中的现有代码库,或者您可能想要审查同事的Pull Request。
通常,您会检出分支或克隆存储库,将源代码拉取到本地机器上,打开您首选的开发工具,然后最终可以阅读和导航代码。如果您可以在不首先克隆存储库的情况下执行此操作,那不是很酷吗?想象一下,无需下载源代码即可获得智能代码功能,如悬停信息、转到定义和查找所有引用。博客文章首次体验丰富的代码导航体验,展示了在拉取请求审查中的这一场景。
语言服务器索引格式(LSIF,发音类似于“else if”)的目标是支持在开发工具或Web UI中进行丰富的代码导航,而无需本地源代码副本。该格式在精神上与语言服务器协议(LSP)相似,后者简化了将丰富的代码编辑功能集成到开发工具中的过程。
为什么不直接使用现有的LSP语言服务器?LSP提供了丰富的代码编写功能,如自动完成、类型格式化以及丰富的代码导航。为了高效地提供这些功能,语言服务器需要所有源代码文件都可在本地磁盘上使用。LSP语言服务器还可能将部分或全部文件读入内存,并计算抽象语法树以支持这些功能。语言服务器索引格式(LSIF)的目标是增强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' }];
}
客户端工具将通过发送textDocument/hover
请求来从语言服务器检索悬停内容,请求针对文档file:///Users/username/sample.ts
在位置{line: 0, character: 10}
处。
LSIF 定义了一种格式,语言服务器或独立工具可以发出这种格式来描述元组 ['textDocument/hover', 'file:///Users/username/sample.ts', {line: 0, character: 10}]
解析为上述悬停信息。然后可以获取这些数据并将其持久化到数据库中。
LSP请求是基于位置的,然而结果通常只在范围内变化,而不是单个位置。在上面的悬停示例中,标识符bar
的所有位置的悬停值都是相同的。这意味着当用户悬停在bar
中的b
或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 规范 - 该文档还描述了一些额外的优化,这些优化是为了保持生成的数据紧凑。
- LSIF Index for TypeScript - 一个为TypeScript生成LSIF的工具。README提供了使用该工具的说明。
- Visual Studio Code extension for LSIF - 一个为VS Code提供的扩展,使用LSIF JSON转储提供语言理解功能。如果你实现了一个新的LSIF生成器,你可以使用这个扩展来验证它与任意源代码的兼容性。