什么是语言服务器协议?
为编程语言实现诸如自动补全、转到定义或悬停文档等功能的支持是一项重大的工作。传统上,这项工作必须为每个开发工具重复进行,因为每个工具都提供不同的API来实现相同的功能。
语言服务器背后的理念是,在服务器内部提供特定于语言的智能功能,该服务器可以通过启用进程间通信的协议与开发工具进行通信。
语言服务器协议(LSP)背后的理念是标准化工具和服务器之间如何通信的协议,这样单个语言服务器可以在多个开发工具中重复使用,并且工具可以以最小的努力支持多种语言。
LSP 对语言提供商和工具供应商来说都是一大福音!
工作原理
语言服务器作为一个单独的进程运行,开发工具使用 JSON-RPC 通过语言协议与服务器通信。下面是一个工具和语言服务器在日常编辑会话中如何通信的示例。

-
用户在工具中打开一个文件(称为文档):工具通知语言服务器文档已打开(“textDocument/didOpen”)。从现在开始,文档内容的真实性不再在文件系统上,而是由工具保存在内存中。现在内容必须在工具和语言服务器之间同步。
-
用户进行编辑:工具通知服务器文档更改(“textDocument/didChange”),语言服务器更新文档的语言表示。在此过程中,语言服务器分析此信息,并通知工具检测到的错误和警告(“textDocument/publishDiagnostics”)。
-
用户对打开文档中的符号执行“转到定义”:工具向服务器发送一个“textDocument/definition”请求,包含两个参数:(1) 文档 URI 和 (2) 启动“转到定义”请求的文本位置。服务器响应文档 URI 和符号定义在文档中的位置。
-
用户关闭文档(文件):工具发送一个“textDocument/didClose”通知,告知语言服务器文档现在不再在内存中。当前内容现在在文件系统上是最新的。
这个例子说明了协议如何在文档引用(URIs)和文档位置的级别与语言服务器通信。这些数据类型是编程语言中立的,适用于所有编程语言。这些数据类型并非处于编程语言领域模型的级别,后者通常提供抽象语法树和编译器符号(例如,解析的类型、命名空间等)。数据类型简单且与编程语言无关,这显著简化了协议。与在不同编程语言中标准化抽象语法树和编译器符号相比,标准化文本文档 URI 或光标位置要简单得多。
现在我们来更详细地看看“textDocument/definition”请求。下面是 C++ 文档中“转到定义”请求在开发工具和语言服务器之间传递的有效载荷。
这是请求
{
"jsonrpc": "2.0",
"id" : 1,
"method": "textDocument/definition",
"params": {
"textDocument": {
"uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/use.cpp"
},
"position": {
"line": 3,
"character": 12
}
}
}
这是响应
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"uri": "file:///p%3A/mseng/VSCode/Playgrounds/cpp/provide.cpp",
"range": {
"start": {
"line": 0,
"character": 4
},
"end": {
"line": 0,
"character": 11
}
}
}
}
当用户使用不同的语言时,开发工具通常会为每种编程语言启动一个语言服务器。下面的示例展示了用户处理 Java 和 SASS 文件的会话。

功能
并非每个语言服务器都能支持协议定义的所有功能。因此,LSP 提供了“能力”。一项能力将一组语言功能进行分组。开发工具和语言服务器使用能力声明它们支持的功能。例如,服务器声明它可以处理“textDocument/definition”请求,但可能不处理“workspace/symbol”请求。类似地,开发工具声明它能够在文档保存之前提供“即将保存”通知,以便服务器可以在保存之前计算文本编辑以格式化已编辑的文档。
请注意,语言服务器实际集成到特定工具的方式不由语言服务器协议定义,而是留给工具实现者。
LSP 提供商和消费者的库(SDK)
为了简化语言服务器和客户端的实现,有可用的库或 SDK。
-
开发工具 SDK 每个开发工具通常都提供一个用于集成语言服务器的库。例如,对于 JavaScript/TypeScript,有 语言客户端 npm 模块。
-
语言服务器 SDK 针对不同的实现语言,有用于以特定语言实现语言服务器的 SDK。例如,要使用 Node.js 实现语言服务器,有 语言服务器 npm 模块。