记一次魔改 clangd。本文内容均为个人理解,如有错误欢迎指出。
1. 通过编译
第一件事自然是把原版给编译起来啦。
- 第一步:clone 仓库
- 第二步:用最新 release 对应 tag 创建新分支,博主行动时是 21.1.4
- 第三步:设置配置/编译选项
我使用 vscode,这里给出我的 settings.json:
1 | { |
- 第四步:完成 cmake 配置
- 第五步:编译
install-clangd和install-clang-resource-headers这两个 target
博主使用 gcc 15.1.0 (msys2) 完成编译,后面的修改都只在此环境下进行编译。
2. 修改 clangd 前
在魔改 clangd 之前,我来先打点补丁。
相信很多自己编译过 clangd 的人都发现过一个很奇怪的现象。
如果你用 msvc 编译,则 clangd 能自行找到电脑中的 vs 环境,并自动配置头文件路径等,并且这甚至不需要你把 vs 加入 path,只要项目有 compile_commands.json,但是 clangd 无法找到其它编译器的 include 路径,然后通通回退到 msvc 的头文件:

但是,如果你使用 gcc 进行编译,或者使用 mingw 环境的 clang 进行编译,你就会发现 clangd 又能找到 gcc 的头文件路径,随后你又发现它可能找不到 msvc 的路径。
要理解这一点,我们需要了解 clangd 如何读取并解析编译器的信息。
clangd 依靠 clang 的 Driver 层来读取编译器信息,Driver 层首先根据编译参数确定三元组(Triplet),然后根据 Triplet 选择对应的 Toolchain,例如 msvc 编译器对应 x86_64-w64-windows-msvc。如果编译参数包含了目标 Triplet,那么 Driver 层就会直接使用这个 Triplet。
因此如果你在.clangd文件中添加一个 CompileFlags,指定 target,上面的问题就能解决,例如:
1 | CompileFlags: |
如果更换编译器,则这个--target需要一起修改,否则 clangd 很可能会爆出大量无效诊断。
如果没有这个 target 参数,那么默认的目标 Triplet 就是你编译 llvm 时的宿主环境。
因此对于 msvc 编译的 clangd,msvc 项目,默认目标 Triplet 是 x86_64-w64-windows-msvc,期望的 Triplet 也是这个;
对于 gcc 编译的 clangd,msvc 项目,默认目标 Triplet (例如)是 x86_64-w64-mingw32,Driver 层需要识别出期望的 Triplet 是 x86_64-w64-windows-msvc;
其它情况同理。
在 gcc 编译的 clangd,msvc 项目下,Driver 层确实能够正确求出 Triplet 是 x86_64-w64-windows-msvc,但是这个 Triplet 对应的 MSVCToolChain 还需要获取更完整的编译器信息,例如标准库头文件路径。
在此 target 下 MSVCToolChain 会通过以下几个方案来寻找 msvc 的信息:
- 命令行显式指定 vc 路径。
- 环境变量,
VCToolsInstallDir,VCINSTALLDIR,以及 PATH。 - 使用 com 接口(ISetupConfiguration)获取,这个是微软推荐的现代方式
- 注册表。
正常来讲,用 vc 的不会去手动指定 vc 路径,而那几个环境变量一般在 Developer Command Prompt for VS 内被指定,但是没多少人会想到把 clangd 跑在这玩意内,如今的 Visual Studio 已经不在注册表内写 vc 路径,所以 124 方案很容易都失败,但所幸第 3 个方案几乎必然成功。
除 非 你 没 有 去 实 现 它。
由于开发者没有实现 MinGW 环境下 com 接口探测 vc 工具链,如果你使用 MinGW 下的 gcc 或 clang 编译的 clangd,clangd 就会找不到 msvc 的头文件。
对于 msvc 编译的 clangd,gcc 项目,期望的 Triplet 比如是 x86_64-w64-mingw32,但是 Driver 层没有能力检测出来,所以实际 Triplet 是默认的x86_64-w64-windows-msvc,最终这个项目被当成使用 vc 工具链。
所以修复思路就明确了,以下是一个简易的补丁。
把#ifdef _MSC_VER和结尾的#endif删除
1 |
|
MSVCSetupApi.h 的结尾新增代码:
1 |
|
3. 开始魔改 clangd
先说以下我为什么要去改 clangd。
其实就是我嫌 clangd 的悬浮提示的格式不合口味。clangd 的悬浮提示给我一种信息很多,但是没几个有用的感觉。例如这是 GNU C++标准库的 make_unique 的悬浮提示:

首先上来就是一个加粗 function,但是我不知道它是函数吗?
下一句provided by <memory>,我能不知道它属于哪个库吗?再不济我跳转过去不就行了?
再下一句,最迷惑的一集。
现 在 是 抢答时间!!问:__detail 位于哪个命名空间?
答案是 std,你猜对了吗?
如果你才对了那么下一题:_MakeUniq<_Tp> 位于哪个命名空间?
答案是 std::__detail,你猜对了吗?
至于Parameters:,基本就是把最后面的签名中的参数列表拷过来。
doxygen 不支持就不用说了(虽然 llvm 22 支持了)。
那么我希望的格式是什么样的呢?其实我个人比较喜欢 intellisense (cpptools 里用的那个)的格式,简单来说,就是上面一大坨都基本不要了,尽量都集成在签名中,不多说,直接看魔改后的效果:


怎么做到的呢?
首先 clangd 使用 LSP 协议这一点各位应当都知道。每当 clangd 收到一个悬浮提示请求时,都会调用clang-tools-extra/clangd/Hover.h内的getHover函数,如果函数返回 std::nullopt,说明没有悬浮提示,否则返回一个clang::clangd::HoverInfo对象,这个对象包含悬浮提示所需的所有数据,并通过 HoverInfo::present() 成员函数将数据转换成 markdown 文本,返回给客户端,然后客户端将 markdown 文本渲染出来,就得到了悬浮提示。
所以总共就两大步,getHover函数获取所需数据,present函数将数据组成 md 文本。而悬浮提示的格式就由present函数决定。
魔改后的present函数见 Hover.cpp#L2435。
当然,只魔改present函数肯定是远远不够滴,为了更好了的效果,getHover函数也被大面积修改。
但是可惜全部列出来只会让我的重度懒癌变成印度懒癌,所以我只能挑一部分细节讲。
printPrettyLambdaType 函数
这个函数用于打印 lambda 类型,显示成包含operator()参数信息的样子:
1 | (lambda [](int) -> void) |
formatAPValue 函数
用于在常量表达式中打印求值结果。
TypeSimplifier 类型
用于给类型寻找一个别名,自动简化
1 | inline std::basic_ostream<char, std::char_traits<char>> &std::operator<<<std::char_traits<char>>( |
1 | inline std::ostream &std::operator<<<std::char_traits<char>>( |
parseDocumentation 函数
这个函数内包含了简易的 doxygen 渲染逻辑
用于获取实例化后的模板实参列表

好了先写这么点,再写就死了。完整修改见仓库