语言服务器协议

什么是语言服务器协议?

在编辑器或 IDE 中为编程语言支持丰富的编辑功能(如源代码自动完成或 转到定义 )传统上非常具有挑战性和耗时。 通常,它需要使用编辑器或 IDE 的编程语言编写域模型(扫描程序、分析程序、类型检查器、生成器等)。 例如,Eclipse CDT 插件(在 Eclipse IDE 中提供对 C/C++的支持)是用 Java 编写的,因为 Eclipse IDE 本身是用 Java 编写的。 遵循此方法意味着在 TypeScript for Visual Studio Code 中实现 C/C++ 域模型,并在 C# for Visual Studio 中实现单独的域模型。

如果开发工具可以重复使用现有的特定于语言的库,则创建特定于语言的域模型也容易得多。 但是,这些库通常以编程语言本身实现(例如,良好的 C/C++域模型在 C/C++ 中实现)。 从技术上讲,将 C/C++ 库集成到使用 TypeScript 编写的编辑器中是可能的,但很难做到。

语言服务器

另一种方法是在其自己的进程中运行库,并使用进程间通信与之通信。 来回发送的消息构成协议。 语言服务器协议(LSP)是标准化开发工具和语言服务器进程之间交换的消息的产物。 使用语言服务器或恶魔不是一个新的或新颖的想法。 Vim 和 Emacs 等编辑器一直在执行此作,以提供语义自动完成支持。 LSP 的目标是简化此类集成,并提供一个有用的框架,用于向各种工具公开语言功能。

使用通用协议,可以通过重用语言域模型的现有实现,将编程语言功能集成到开发工具中,只需少加大惊小怪。 语言服务器后端可以用 PHP、Python 或 Java 编写,LSP 使它可以轻松集成到各种工具中。 该协议在抽象的常见级别工作,以便工具可以提供丰富的语言服务,而无需完全了解特定于基础域模型的细微差别。

如何开展 LSP 的工作

LSP 随时间推移而演变,目前版本为 3.0。 当 OmniSharp 开始采用语言服务器的概念时,它为 C# 提供了丰富的编辑功能。 最初,OmniSharp 将 HTTP 协议与 JSON 有效负载配合使用,并已集成到多个编辑器中,包括 Visual Studio Code

与此同时,Microsoft开始在 TypeScript 语言服务器上工作,并考虑在 Emacs 和 Sublime Text 等编辑器中支持 TypeScript。 在此实现中,编辑器通过 stdin/stdout 与 TypeScript 服务器进程进行通信,并使用受 V8 调试器协议启发的 JSON 有效负载来请求和响应。 TypeScript 服务器已集成到 TypeScript Sublime 插件和 VS Code 中,以便进行丰富的 TypeScript 编辑。

集成两个不同的语言服务器后,VS Code 团队开始探索编辑器和 IDE 的公共语言服务器协议。 公共协议使语言提供程序能够创建可由不同 IDE 使用的单一语言服务器。 语言服务器使用者只需实现协议的客户端一次。 这会导致语言提供程序和语言使用者都处于双赢局面。

语言服务器协议从 TypeScript 服务器使用的协议开始,通过 VS Code 语言 API 启发的更多语言功能来扩展它。 由于协议的简单性和现有库,因此支持 JSON-RPC 进行远程调用。

VS Code 团队通过实现多个 linter 语言服务器来对该协议进行原型开发,这些服务器响应对文件进行 lint(扫描)的请求,并返回检测到的警告和错误。 目标是在用户编辑文档中时对文件进行 lint,这意味着在编辑器会话期间将有许多 linting 请求。 为了避免每次用户编辑都需要启动新的 linting 进程,保持服务器正常运行是明智的。 实现了多个 linter 服务器,包括 VS Code 的 ESLint 和 TSLint 扩展。 这两个 linter 服务器在 TypeScript/JavaScript 中实现,并在 Node.js上运行。 它们共享实现协议的客户端和服务器部分的库。

LSP 的工作原理

语言服务器在其自己的进程中运行,Visual Studio 或 VS Code 等工具使用 JSON-RPC 语言协议与服务器通信。 在专用进程中运行的语言服务器的另一个优点是避免与单个进程模型相关的性能问题。 如果客户端和服务器都以 Node.js写入,则实际传输通道可以是 stdio、socket、named pipes 或 node ipc。

下面是在例程编辑会话期间工具和语言服务器如何通信的示例:

lsp 流图

  • 用户在工具中打开文件(称为文档):该工具通知语言服务器打开文档('textDocument/didOpen')。 从现在起,有关文档内容的真相不再位于文件系统上,而是由工具保留在内存中。

  • 用户进行编辑:该工具通知服务器有关文档更改('textDocument/didChange')的信息,程序语义信息由语言服务器更新。 发生这种情况时,语言服务器会分析此信息,并通知工具检测到的错误和警告('textDocument/publishDiagnostics')。

  • 用户在编辑器中的符号上执行“转到定义”:该工具发送一个包含两个参数的“textDocument/definition”请求:(1)文档 URI 和(2)从服务器启动“转到定义”请求的文本位置。 服务器使用文档 URI 和符号定义在文档中的位置进行响应。

  • 用户关闭文档(文件):从工具发送“textDocument/didClose”通知,告知语言服务器该文档现在不再在内存中,并且当前内容现在在文件系统上是最新的。

此示例说明了协议如何在编辑器功能级别(如“转到定义”、“查找所有引用”)与语言服务器通信。 协议使用的数据类型是编辑器或 IDE“数据类型”,如当前打开的文本文档和光标的位置。 数据类型不在编程语言领域模型的级别,该模型通常提供抽象语法树和编译器符号,例如已解析的类型、命名空间等。这大大简化了协议。

现在,让我们更详细地查看“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
            }
        }
    }
}

回顾一下,描述编辑器级别而不是编程语言模型级别的数据类型是语言服务器协议成功的原因之一。 与在不同的编程语言中标准化抽象语法树和编译器符号相比,标准化文本文档 URI 或游标位置要简单得多。

当用户使用不同语言时,VS Code 通常会为每个编程语言启动语言服务器。 以下示例显示了用户对 Java 和 SASS 文件工作的会话。

java 和 sass

Capabilities

并非每个语言服务器都支持协议定义的所有功能。 因此,客户端和服务器通过“功能”宣布其支持的功能集。 例如,服务器会报出它可以处理“textDocument/definition”请求,但它可能不会处理“workspace/symbol”请求。 同样,客户端可以宣布,他们可以在保存文档之前提供“即将保存”通知,以便服务器可以计算文本编辑以自动设置编辑文档的格式。

集成语言服务器

语言服务器与特定工具的实际集成不是由语言服务器协议定义的,并且留给工具实现者。 某些工具通常通过具有可以启动和与任何类型的语言服务器通信的扩展来集成语言服务器。 其他扩展(如 VS Code)为每个语言服务器创建自定义扩展,以便扩展仍能够提供一些自定义语言功能。

为了简化语言服务器与客户端的实现,有专门针对客户端和服务器端的库和 SDK 可供使用。 这些库针对不同的语言提供。 例如,有一个 语言客户端 npm 模块 可以简化语言服务器与 VS Code 扩展的集成,使用另一 个语言服务器 npm 模块 来使用 Node.js编写语言服务器。 这是支持库的当前 列表

在 Visual Studio 中使用语言服务器协议