Sublime Text 4, LSP, VS Code and clangd
Sublime Text 4
前几天Sublime Text 4 Stable发布了。在此之前我是Sublime Text 3的付费用户。大约六七年前,我开始转用LaTeX写大学课程的各种作业,发现了Sublime Text和LaTeXTools插件。当时市面上尚无多少现代化的文本编辑器供选择,Sublime Text是其中体验相当不错的一个。那时我刚刚有软件付费的意识,就买了一个license。

后来Visual Studio Code问世,并且在之后的几年里发展十分迅速,如今应该已经是最(?)受欢迎的Code Editor了。VS Code的更新维护十分活跃,插件的质量也很高。
很长一段时间里我用Text Editor最重要的用途是写LaTeX。后来VS Code + LaTeX Workshop的组合已经十分令我满意了,我也不再去折腾Sublime Text及其LaTeXTools插件。对我而言,Sublime Text相比VS Code还剩下的为数不多的优势就是启动快。另外,Sublime Text的Key binding也是先入为主,我至今依然习惯在VS Code和JetBrains的IDE里面使用Sublime Text快捷键键位。
这次Sublime Text 4的License Upgrade(折扣)价格是70USD,与6年前我原价购买ST3时价格一致。现如今我倒暂时没有什么续费欲望了。不过Sublime Text是一个「唠叨软件(Nagware)」,如果不付费,它只会不时地弹出一个窗口提醒你付费,除此之外没有功能或时间限制。
于是这次ST4我也及时升级了。升级之后趁着新鲜,去搜索围观别人有什么推荐的Sublime Text插件。这次首先留意到的是LSP。
Language Server Protocol
Language Server Protocol (LSP)是Microsoft发布的一个协议,用于Language Server和Development Tools(比如Text Editor或IDE)之间的跨进程通信。
Language Server是一种将「语言特性」和「编辑工具」解耦的尝试。这种想法现在看来非常自然:
我们使用编辑器浏览、编辑代码,常常希望Editor能够有智能提示、自动补全、语法检查、引用检查、跳转等功能。而且我们希望这些功能足够智能,以至于Editor需要理解具体编程语言(如C++,Python)的语义,乃至了解整个项目中多个源文件的关系。
以前,如果要求每一个编辑器或自动补全插件的作者自己去实现这些功能,那将是非常浩大的工程:编程界的语种多如牛毛,且其中不乏复杂庞大的语言。由于Editor插件的开发者精力有限,必然导致这样的智能补全/提示工具质量参差不齐。
那么,何不让专业的工具做专业的事呢?假设有一个与编程语言相关的程序在后台运行,这个程序负责理解特定语言(如C++)的代码;而Editor负责与用户交互:检测用户的行为,比如打开文件、输入字符、鼠标悬停在文件的某个位置、执行了「Go to definition」命令等等;然后Editor将这些信息传递给后台那个负责理解语言的后台程序,该程序根据语言的语义给出响应,比如对于鼠标悬停,就给出悬停位置那个变量或函数的文档;对于「Go to definition」,就返回Definition所在的文件及行号。
这个在后台运行的、理解语言特性的程序,就是Language Server。而Language与Editor之前互传信息所使用的通信协议,即LSP。
有了LSP这个设计,一个Language Server就可以支持多种Editor的智能编辑;而一个Editor如果要想增强对一个语言的支持,它无需花费太大成本自行开发那个语言的解析功能,而只要能够兼容LSP,能够连接对应语言的Language Server就可以了。
clangd
clangd是C++的Language Server之一(其他的一些Language Servers见此页面)。clangd的Slogan很有意思:
teach your editor C++
macOS安装clangd
由于clangd是LLVM Project的一部分,实际上Apple的工具链已经包含了clangd,但却可能没有被添加到默认PATH变量中。
如果安装过Xcode Command Line Tools或者Xcode.app二者之一(如果没有,可以用xcode-select --install安装,或直接去Mac App Store下载Xcode),clangd大概就在下面的2个位置之一:
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clangd --version
# Apple clangd version 12.0.5 (clang-1205.0.22.9)
/Library/Developer/CommandLineTools/usr/bin/clangd --version
# Apple clangd version 12.0.5 (clang-1205.0.22.9)
或者也可以使用Homebrew安装(注意这依然不在默认PATH里面):
brew install llvm
/usr/local/opt/llvm/bin/clangd --version
# clangd version 12.0.0
**注:**Apple Toolchains里面的clangd似乎不能正确读取下面将要讨论的clangd配置文件(config.yaml)里面的键。所以我暂且用Homebrew安装的clangd.
Linux安装clangd
通常直接使用包管理器安装即可,如apt install clangd.
Sublime Text安装LSP并使用clangd
Sublime Text安装插件管理器PackageControl并用PackageControl安装LSP。对于一个项目,在使用Sublime Text打开后,应该用Cmd/Ctrl + Shift + P然后Enable Language Server in Project/Globally。
如果你已经自行把clangd所在目录添加到了PATH环境变量中,那么这套Sublime Text + LSP + clangd的组合应该是works out of the box。如果没有,也可以在LSP插件的配置文件(Sublime菜单 - Preferences - Package Settings - LSP - Settings)中指定clangd的路径:
"clients": {
"clangd": {
"command": [
"/usr/local/opt/llvm/bin/clangd"
]
}
}
如果你的项目非常简单,比如只有一个hello.cpp源文件,clangd默认你的项目只要简单地
clang hello.cpp
就能编译成功,那clangd同样也能很好地完成智能语法检查、提示。但如果你的项目较大,使用某种构建系统,涉及到多文件管理,依赖也比较复杂:例如,某个源码引用了一个Header#include "a.h",并且在编译时要用-I/some/special/path/to/a指定搜索路径才能找到该Header;但默认情况下,clangd并不知道要去搜索额外的路径,它只会在默认搜索路径下寻找,找不到a.h然后给出(不该给的)报错。
CMake项目输出compile_commands.json
解决上述问题的方法自然是告诉clangd你将要用的所有编译命令。如果项目使用CMake构建系统,则可以在CMakeLists.txt里加入
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
或者在执行CMake Configure的时候直接带命令行参数
cmake -H. -Bbuild -DCMAKE_EXPORT_COMPILE_COMMANDS=1
这样CMake便会在构建目录下生成一个compile_commands.json文件(即Compilation Database),把它放在项目源码根目录下即可。
如果使用CMake之外的其他构建系统,这里有一些生成Compilation Database的说明:JSON Compilation Database Format Specification;Compilation database - JetBrains Clion Help Center.
效果
一切就绪之后,打开一个C++源文件,鼠标悬停于某些变量或函数之上,应该可以看到弹出的提示;也可以点击Definition、Declaration等链接进行跳转。打开LSP Log,还可以看到Editor与clangd之间的通信过程:

clangd配置
clangd有一些特性并未默认开启,比如Clang-Tidy.
可以创建一个clangd配置文件开启这些特性:
# macOS: ~/Library/Preferences/clangd/config.yaml
# Linux: $XDG_CONFIG_HOME/clangd/config.yaml, ~/.config/clangd/config.yaml
Diagnostics:
ClangTidy:
Add: [performance-*, bugprone-*, portability-*, modernize*]
Remove: modernize-use-trailing-return-type
重启一下Language Server,便可获得Clang-Tidy提示:

Visual Studio Code
借Sublime Text 4发布的机缘,我了解了LSP的机制。不过最后我还是没有用ST4写C++代码,而是又转回了VS Code。VS Code官方(即Microsoft)也有一个C++的Language Server,即C/C++插件,智能辅助的功能也叫IntelliSense。不过经过我肤浅的体验,我觉得IntelliSense没有clangd用起来舒服,所以将VS Code的C++ Language Server也设置成了clangd。
这个过程当然很简单,只有2点:安装并启用clangd,并关闭IntelliSense:
// settings.json
"C_Cpp.intelliSenseEngine": "Disabled",
"C_Cpp.autocomplete": "Disabled",
"C_Cpp.errorSquiggles": "Disabled",
// If clangd is not in PATH:
"clangd.path": "/path/to/clangd"
换到VS Code的原因是VS Code的Remote插件实在是太易用了(个人感觉远好于我常用的CLion),我可以轻松地连接到我在实验室的台式机,直接打开台式机里面的项目源码进行编辑。而由于clangd这类Language Server的存在,VS Code对代码的「理解」能力大大增强,与IDE之间的鸿沟也显著缩小。另外,那台台式机的i7 - 9700K CPU无论是indexing还是编译代码,都比我的笔记本爽快不少。