上个月,Apple 在 Swift.org 论坛上宣布,它正在开始采用 Swift 和 C 语言的语言服务器协议(LSP)。
在 Apple,我们优先考虑为所有 Swift 开发者提供高质量的工具,包括那些在不在 Apple 平台上开发的开发者。我们希望和开源社区合作并且集中精力建设可以由 Xcode 和其他编辑器和平台共享的公共基础设施,因此,我们选择采用 LSP。
Argyrios Kyrtzidis,2018年10月15日
这可以说是自 2014 年 Swift 开源发布以来苹果公司对 Swift 做出的最重要的决定。这是苹果应用开发者的大事件,更是在其它平台的 Swift 开发者的大事件。
为什么这件事情这么重要?本文会对语言服务协议进行介绍,讲解它能解决什么问题,如何解决这些问题,以及可能会带来什么样的长期影响。
我们假设有这样一个表格,它的行代表不同的编程语言(Swift、JavaScript、Ruby、Python 等),每一列代表不同的代码编辑器(XCode、Visual Studio、Vim、Atom 等)。这样一来,表内的每个单元格都表示某个编辑器对某种语言的支持程度。
现在,你会发现不同的组合之间兼容性很差。有一些编辑器对少数几种语言进行了深度集成,几乎不支持任何其他语言,也有一些编辑器则致力于支持语言的通用特性,对大量的语言进行了较为浅层的支持。(IDE 通常是指前者。)
举例来说,不使用 XCode 开发苹果应用是件很难理解的事情,而使用 XCode 开发其它东西简直就是傻。
为了让编辑器对语言支持得更好,它需要编写集成语言特性的代码 —— 直接在项目代码中编写,或者采用插件系统的方式。语言和编辑器之间会存在差异,比如在 Vim 中改进了对 Ruby 的支持并不能同时让 Vim 对 Python 支持得更好,也不能让 Ruby 在 Atom 中更好地工作。结果就是:对各种技术之的支持不能达成一致,会浪费大量的精力。
这里描述的情况通常被称为 M × N 问题,它要处理的问题数是 M 种编辑器和 N 语言的乘积。语言服务协议要做的就是把 M × N 的问题变成 M + N 的问题。
编辑器不用支持每一种语言,只需要支持 LSP。然后,所有支持 LSP 的语言都会在这个编辑器中得到相同级别的支持。
Tomohiro Matsuyama 在 2010 年写了一篇标题为 “Emacs は死んだ” (“Emacs 已死”) 的文章,对这个问题进行了很好地总结。Matsuyama 在描述 Emacs 脚本语言的局限(没有多线程、低层次的 API 少、用户群不大)时提出他的观点。他认为插件应该与外部程序接口,而不应该是本地实现。
语言服务器协议对其支持的语言提供了一系列通用的功能,包括:
语法高亮
自动格式化
自动完成
语法(检查)
提示
内部诊断
跳转到定义
在项目中查找引用
高级文本或符合搜索
在 LSP 的支持下,工具和编辑器可以更加专注于处理可用性和高级功能,而不需要为每个新技术做重复性的工作。
如果你是 iOS 开发者,你可能最熟悉的“服务”和“协议”概念是建立在 Web 应用基于 HTTP 和 JSON 的通信技术之上。这实际上与语言服务协议如何工作并没多大关系。
对于 LSP 来说,客户端就是编辑器 —— 或者更普适性的说法是工具 —— 而服务端是指运行在本地另一个进程中的外部程序。
作为协议本身,LSP 有点像 HTTP 的简单版:
每个消息都包含头和内容两个部分。
头部需要用 Content-Length
参数来描述内容部分的字节数,以及可选的 Content-Type
参数(默认是 application/vscode-jsonrpc; charset=utf-8
)
内容部分是请求、响应和通知的内容,其数据结构遵循 JSON-RPC 规范。
一旦工具有变化,比如用户条到了一个标记的定义之类的,工具都会向服务端发送一个请求。服务端收到这个请求之后返回对应的响应。
例如,想象一下用户在一个支持编程语言服务协议的类Xcode的编辑器中打开如下Swfit代码:
Swift
class Parent {} class Child: Parent {}
当用户 ?-单击第二行的继承语句中的 Parent
标记的时候,编辑器跳到了第一行的 Parent
类定义处。
以下是LSP如何在银幕背后让这个交互实现的:
首先,当用户打开 Swfit 代码的时候,编辑器在一个单独的进程中启动了他的 Swift语言服务器,如果没有运行起来的话,会实施一些额外的设置。
当用户执行 “跳转到定义” 的命令时,编辑器将如下请求发送到 Swift语言服务器:
JSON
{ "jsonrpc": "2.0", "id": 1, "method": "textDocument/definition", "params": { "textDocument": { "uri": "file:///Users/NSHipster/Example.swift" }, "position": { "line": 1, "character": 13 } } }
收到这个请求之后, Swift 语言服务使用像 SourceKit 这样的编译工具把代码实体标识出来,并在前面的代码中查找其声明位置。语言服务随后响应这样的消息:
JSON
{ "jsonrpc": "2.0", "id": 1, "result": { "uri": "file:///Users/NSHipster/Example.swift", "range": { "start": { "line": 0, "character": 6 }, "end": { "line": 0, "character": 12 } } } }
最后,编辑器导航到相应的文件(示例中就是打开的这个文件),将光标移动到响应指示的位置,把标识高亮显示出来。
这种方法的美妙之处在于编辑器在做这些事情的时候,不需要知道任何与 Swift 编译语言相关的事情,它在乎 .swift
文件与 Swift 代码之间的关联。编辑器需要做的事情就是与语言服务对话,并更新 UI。只要编辑器能做这件事情,那就可以按照相同的处理过程,实现任意语言的代码与相应的语言服务之间进行交互。
如果上面的M + N的图看起来很熟悉,可能是因为LLVM采用了相同的方法。
LLVM的核心是一个中间表示(IR)。支持的语言使用前端编译器生成IR,然后该IR可以生成任何后端编译器所支持的平台的机器码。
如果你对关于Swift代码如何编译感兴趣,请阅读我们关于SwiftSyntax的文章。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。 2KB翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。2KB项目(www.2kb.com,源码交易平台),提供担保交易、源码交易、虚拟商品、在家创业、在线创业、任务交易、网站设计、软件设计、网络兼职、站长交易、域名交易、链接买卖、网站交易、广告买卖、站长培训、建站美工等服务