Language Server 架构细节:LSP在代码编辑器界,某种意义上,类似 LLVM 的存在,被称之为 LSP,即
什么是 LSP
1 个小问题,你是否曾听过或了解 LLVM,当然没听过也不重要,重要的是 LLVM 背后的思想常常能够带给我们许多启发。具体来说,LLVM 的出现,在编程语言和运行机器之间搭建起了一道公共的桥梁。
此处,我们简单了解一下 LLVM 的思想。一门高级语言,以往要支持多个架构的机器,便需要就不同的机器生成不同类型的机器码,支持起来非常麻烦。
有了 LLVM 之后,若我们发明了一种新的编程语言,只需要按照 LLVM 规范设计一种新的编译器前端即可;与之类似,若是需要支持一种全新架构的硬件设备,也只需要设计一个对应的编译器后端即可,大大简化了以往的工作。
在代码编辑器界,某种意义上,类似 LLVM 的存在,被称之为 LSP,即 Language Server Protocol,同样由 Microsoft 提出。其革命性意义便在于语言服务器的标准化,使得不同的编程语言在不同的代码编辑器中对用户提供一致的编程体验。
如下图所示,在 LSP 的两端,分别是代码编辑工具,比如 VSCode、Vim 等,和语言智能服务,也就是 Language Server。介于二者,LSP 提供了一种整合方式,将自动补全、定义跳转等功能整合到代码编辑工具之中。
如此一来,假若我们开发了一款新的代码编辑器,按照 LSP 定制客户端,就可以直接使用现成的语言服务器;亦或者我们为一门新的编程语言开发了语言服务器,同样可以很快应用到各个代码编辑器之中。在官方社区看来,LSP 的存在,将一个复杂度为 m×nm \times nm×n的问题,简化成了一个复杂度为 m+nm + nm+n 的问题。
LSP 架构
上图描绘了一个广义的 Language Server 系统的基本架构,可以说这是一个典型的两层 C/S 架构(Client-Server 架构)。客户端(即代码编辑器)与服务端(即语言服务器)之间基于 JSON-RPC 协议进行通信。此处,我们不对 JSON-RPC 协议进行展开,留待后续章节作深入了解。
就连接方式而言,客户端与服务端之间常见的连接方式包括 Websocket 和 IPC(进程间通信)。选择以上两种连接方式,本质原因在于:
- 客户端不仅仅会发消息给服务端,比如定义跳转功能(Go To Definition),一般是由用户发起,从服务端获取变量定义所在的位置信息,代码编辑器根据位置信息自动跳转到对应的位置;
- 服务端也需要主动向客户端发送消息,比如代码诊断,用户实时编辑代码,服务端实时诊断新增代码,并向客户端发送诊断信息。
基于这样一种双向通信场景,使用前文提到的两种连接方式,使用起来可以说得心应手。当然,此处提到的 IPC 更多的是一种狭义的基于套接字的进程间通信方式。
广义的进程间通信大部分也适用于 LSP 场景,比如在 vscode-language-server
框架内部,同时支持四种通信方式:
export declare enum TransportKind {
stdio = 0, // 标准输入输出
ipc = 1, // 进程间通信
pipe = 2, // 管道
socket = 3, // Websocket
}
在这里,我们可以得到一个结论,对于任意一个服务,不管是基于什么代码编写,只要满足如下三个条件:
- 实现一个 Server,支持使用 Websocket / IPC 等方式通信;
- 基于 JSON-RPC 协议暴露接口;
- 接口逻辑遵循 LSP 规范 进行实现;
那么,此服务便可以被称之为 Language Server。
LSP 规范
通过前文的链接,我们能够粗窥 LSP 规范全貌。总的来说,LSP 规范更像是一份极其详细的接口文档,此处我们暂时略过细节,从整体面貌上,对其内容形成一个初印象。
首先,LSP 规范介绍了代码编辑器与语言服务器之间通信内容的基本格式,也就是 JSON-RPC 协议报文,并定义了各种数据类型,我们将在下一节全面学习 JSON-RPC 协议之后,再来细看这部分内容。
接下来,LSP 规范分别从四个方面,即:
- 文档同步(Document Synchronization)
- 语言功能(Language Features)
- 工作区功能(Workspace Features)
- 窗口功能(Window Features)
共同定义了代码编辑器与语言服务器之间的通信接口,包括接口方法和接口参数。
其中,文档同步着眼于用户对代码文档的操作行为,比如打开文档,关闭文档,文档同步,Notebook 文档支持等。以文档同步模式为例,其主要针对用户对文档的变更,以何种方式向语言服务器同步,共计三种:
/**
* Defines how the host (editor) should sync document changes to the language
* server.
*/
export namespace TextDocumentSyncKind {
/**
* Documents should not be synced at all.
*/
export const None = 0;
/**
* Documents are synced by always sending the full content
* of the document.
*/
export const Full = 1;
/**
* Documents are synced by sending the full content on open.
* After that only incremental updates to the document are
* sent.
*/
export const Incremental = 2;
}
export type TextDocumentSyncKind = 0 | 1 | 2;
如上所示,文档所发生的变更,可以选择整体同步,也可以选择增量同步,甚至可以选择不同步。
语言功能是 LSP 规范的精髓部分,其约定了各种丰富的功能的基本交互形式,比如声明跳转、定义跳转、引用跳转、代码诊断等。正是对这一部分规范的实现,构成了我们对 Language Server 的基本认知。在后续的文章中,我们将会结合一系列示例,以较大的篇幅深入而详细的学习相关内容。
工作区功能与窗口功能则更加关注项目和代码编辑器本身。前者涵盖符号、配置、项目文件等元素的变更,一般是从代码编辑器发送请求消息到语言服务器;后者涵盖诸如展示消息、预览文档等代码编辑器自身的操作,一般是从语言服务器发送消息到代码编辑器。
有关 LSP 规范,我们先介绍到这里。更多细节内容,我们会在后续的章节逐渐了解和学习,乃至应用。接下来,我们将继续学习 Language Server 架构中的另一基石:JSON-RPC,一起期待吧!
转载自:https://juejin.cn/post/7426558698723999782