LSP(Language Server Protocol)简介
概述
Language Server Protocol(LSP)是微软2016年提出的一项通讯协议方案。该方案定义了一套协议,用于在IDE或编辑器和提供代码补全、转到定义等功能的Language Server之间通信。
官方释义如下:
The Language Server Protocol (LSP) defines the protocol used between an editor or IDE and a language server that provides language features like auto complete, go to definition, find all references etc. The goal of the Language Server Index Format (LSIF, pronounced like "else if") is to support rich code navigation in development tools or a Web UI without needing a local copy of the source code.
通俗地说,就是定义了一套通信规范,用于给IDE实现代码编辑工具的功能。
解决的问题
微软提出 LSP 的目的是,之前各个编辑器(VSCode, Vim, Atom, Sublime...)各自为战,编辑器内部实现的特性和协议都不同。每换一个编辑器,就有可能要给该编辑器中支持的每门语言写一个对应的 Language Server,也就是说假设有 n 门语言,m 个编辑器,那全部编辑器适配所有语言的开发成本和复杂度为 n * m。
能不能在中间层做一个抽象,让语言的「静态分析服务」和「编辑器 / IDE」分离开来?这样上述情景下开发成本和复杂度就可以降低为线性的 n + m。
例如,每个编辑器(客户端)都在用户产生某些通用的行为时(比如点击跳转到定义)负责生成标准中的行为事件,然后以 JSON-RPC 的形式去调用 Language Server 的接口方法。Language Server 相对应地,也必须实现全部 LSP 规范(或者至少实现其中关键部分)定义的接口。
这么做的好处在于,对于某门编程语言,一个编辑器工具不需要再去关心怎么去做代码分析,而是只需要关注如何在界面上发起或响应 LSP 规定的 RPC 事件。而在语言服务器这边也是同理,只需要关注协议本身的事件并响应 & 发起事件即可。
另外,由于编辑器和 Language Server 是两个进程,所以如果 Language Server 挂了,编辑器进程本身也还会存在,用户不用担心还没修改好的代码因此丢失的问题。
有没有缺点?肯定有,那就是市面上所有的 编辑器 和 Language Server 的 maintainer 都需要花时间和精力去兼容这个协议,并且这个协议本身也会随着自身版本更新而要求服务端 / 客户端响应新的协议行为。但是总体来说,利大于弊。
在计算机科学中的任何问题,都可以用加上另一层间接参照来解决。(Any problem in computer science can be solved with another level of indirection) ——鲁迅
果然所有问题都可以通过增加一层解决,如果一层不够,就再加一层
运行机制
LSP 是一个「双工协议」。
不只是开发者工具(客户端)会主动向 Language Server (服务端)通信,服务端也可能主动向开发者工具发起 RPC 请求。
在 LSP 规范定义文档 中,每个 RPC 事件会标注可能的发起方以及是否需要对方做出响应。
举个例子具体了解一下LSP的运作。
比如要实现Goto Type Definition的功能:
右键菜单点击了Goto Type Definition选项后,IDE会向 Language Server 进程以 IPC 形式发送如下信息(仅举例,实际参数结构比较复杂):
"jsonrpc": "2.0",
"id": 24,
"method": "textDocument/typeDefinition",
"params": {
"textDocument": {
"uri": "file:///User/bytedance/java-hello/src/main/java/Main.java"
},
"position": {
"line": 3,
"character": 13
},
// ...其他参数
},
}
然后 Language Server 拿到这条指令,会执行如下动作:
调用的方法是 textDocument/typeDefinition,也就是分析一个符号的类型定义信息。
根据参数,指令的来源文件是 Main.java 第 3 行第 13 个字符 —— 分析后可知是 foo 这个符号。
Server 寻找 foo 的符号对应的类型 Foo 所在位置。找到之后,同样通过 IPC 返回结果 JSON-RPC:
"jsonrpc": "2.0",
// Request 中的 id 为 24,因此 Server 端对应的 Response id 也必须为 24
"id": 24,
"result": {
"uri": "file:///User/bytedance/java-hello/src/main/java/Main.java",
"range": {
"start": { "line": 7, "character": 25 },
"end": { "line": 7, "character": 28 }
}
},
}
最后客户端根据返回值中的参数,让当前用户的编辑光标跳转到指定位置。