XF

Sublime Text 4, LSP, VS Code and clangd

Sublime Text 4

前几天 Sublime Text 4 Stable 发布了。在此之前我是 Sublime Text 3 的付费用户。大约 2014 - 2015 年,我开始转用 LaTeX 写当时所学课程大大小小的课程作业,顺便发现了 Sublime Text 和 LaTeXTools 插件。彼时没有怎么见过世面的我,因为这种丝滑的智能提示和自动补全功能深受感动,觉得这么让人精神愉悦的软件不充点钱简直对不起作者。后来一激动一咬牙,就为信仰充值了:

Sublime-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 SpecificationCompilation database - JetBrains Clion Help Center.

效果

一切就绪之后,打开一个 C++ 源文件,鼠标悬停于某些变量或函数之上,应该可以看到弹出的提示;也可以点击 Definition、Declaration 等链接进行跳转。打开 LSP Log,还可以看到 Editor 与 clangd 之间的通信过程:

clangd-leveldb-hover

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 提示:

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 还是编译代码,都比我的笔记本爽快不少。