语言服务器协议

适用于:yesVisual Studio noVisual Studio for Mac noVisual Studio Code

什么是语言服务器协议?

支持编辑器或 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 使用具有 JSON 有效负载的 HTTP 协议,并已集成到多个编辑器中,包括 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 flow diagram

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

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

  • 用户对编辑器中的符号执行“转到定义”:该工具发送具有两个参数的“textDocument/definition”请求: (1) 文档 URI, (2) 从服务器启动 Go to Definition 请求的文本位置。 服务器使用文档 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 and sass

功能

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

集成语言服务器

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

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

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