记一次魔改clangd

  • ~6.40K 字
  • 次阅读
  • 条评论
  1. 1. 1. 通过编译
  2. 2. 2. 修改 clangd 前
  3. 3. 3. 开始魔改 clangd

记一次魔改 clangd。本文内容均为个人理解,如有错误欢迎指出。

1. 通过编译

第一件事自然是把原版给编译起来啦。

  • 第一步:clone 仓库
  • 第二步:用最新 release 对应 tag 创建新分支,博主行动时是 21.1.4
  • 第三步:设置配置/编译选项

我使用 vscode,这里给出我的 settings.json:

.vscode/settings.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"cmake.sourceDirectory": "${workspaceFolder}/llvm",
"cmake.configureArgs": [
"-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra",
"-DLLVM_ENABLE_RUNTIMES=libcxx;libcxxabi;libunwind",
"-DLLVM_TARGETS_TO_BUILD=X86",
"-DCMAKE_INSTALL_PREFIX=${workspaceFolder}/build/installed"
],
"cmake.buildArgs": ["-j8"],
"cmake.generator": "Ninja",
"cmake.buildDirectory": "${workspaceFolder}/build"
}
  • 第四步:完成 cmake 配置
  • 第五步:编译 install-clangdinstall-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进行配置,但是clangd回退至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
2
3
CompileFlags:
Add:
- --target=x86_64-w64-windows-msvc

如果更换编译器,则这个--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 的信息:

  1. 命令行显式指定 vc 路径。
  2. 环境变量,VCToolsInstallDirVCINSTALLDIR,以及 PATH。
  3. 使用 com 接口(ISetupConfiguration)获取,这个是微软推荐的现代方式
  4. 注册表。

正常来讲,用 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删除

llvm/lib/WindowsDriver/MSVCPaths.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#ifdef _MSC_VER
// Don't support SetupApi on MinGW.
#define USE_MSVC_SETUP_API

// Make sure this comes before MSVCSetupApi.h
#include <comdef.h>

#include "llvm/Support/COM.h"
#ifdef __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-virtual-dtor"
#endif
#include "llvm/WindowsDriver/MSVCSetupApi.h"
#ifdef __clang__
#pragma clang diagnostic pop
#endif
_COM_SMARTPTR_TYPEDEF(ISetupConfiguration, __uuidof(ISetupConfiguration));
_COM_SMARTPTR_TYPEDEF(ISetupConfiguration2, __uuidof(ISetupConfiguration2));
_COM_SMARTPTR_TYPEDEF(ISetupHelper, __uuidof(ISetupHelper));
_COM_SMARTPTR_TYPEDEF(IEnumSetupInstances, __uuidof(IEnumSetupInstances));
_COM_SMARTPTR_TYPEDEF(ISetupInstance, __uuidof(ISetupInstance));
_COM_SMARTPTR_TYPEDEF(ISetupInstance2, __uuidof(ISetupInstance2));
#endif

MSVCSetupApi.h 的结尾新增代码:

llvm/include/llvm/WindowsDriver/MSVCSetupApi.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#ifdef __cplusplus
}
#endif

// vvv新增开始vvv
#if (defined(__cplusplus) && !defined(CINTERFACE) && !defined(_MSC_VER)) || \
(defined(__clang__) && !defined(_MSC_VER))
#include <guiddef.h>
__CRT_UUID_DECL(ISetupInstance, 0xB41463C3, 0x8866, 0x43B5, 0xBC, 0x33, 0x2B,
0x06, 0x76, 0xF7, 0xF4, 0x2E);
__CRT_UUID_DECL(ISetupInstance2, 0x89143C9A, 0x05AF, 0x49B0, 0xB7, 0x17, 0x72,
0xE2, 0x18, 0xA2, 0x18, 0x5C);
__CRT_UUID_DECL(IEnumSetupInstances, 0x6380BCFF, 0x41D3, 0x4B2E, 0x8B, 0x2E,
0xBF, 0x8A, 0x68, 0x10, 0xC8, 0x48);
__CRT_UUID_DECL(ISetupConfiguration, 0x42843719, 0xDB4C, 0x46C2, 0x8E, 0x7C,
0x64, 0xF1, 0x81, 0x6E, 0xFD, 0x5B);
__CRT_UUID_DECL(ISetupConfiguration2, 0x26AAB78C, 0x4A60, 0x49D6, 0xAF, 0x3B,
0x3C, 0x35, 0xBC, 0x93, 0x36, 0x5D);
__CRT_UUID_DECL(ISetupPackageReference, 0xda8d8a16, 0xb2b6, 0x4487, 0xa2, 0xf1,
0x59, 0x4c, 0xcc, 0xcd, 0x6b, 0xf5);
__CRT_UUID_DECL(ISetupHelper, 0x42b21b78, 0x6192, 0x463e, 0x87, 0xbf, 0xd5,
0x77, 0x83, 0x8f, 0x1d, 0x5c);
__CRT_UUID_DECL(SetupConfiguration, 0x177F0C4A, 0x1CD3, 0x4DE7, 0xA3, 0x2C,
0x71, 0xDB, 0xBB, 0x9F, 0xA3, 0x6D);
#endif
// ^^^结束^^^

#ifdef __clang__
#pragma clang diagnostic pop
#endif

其余见549f2114133029

3. 开始魔改 clangd

先说以下我为什么要去改 clangd。
其实就是我嫌 clangd 的悬浮提示的格式不合口味。clangd 的悬浮提示给我一种信息很多,但是没几个有用的感觉。例如这是 GNU C++标准库的 make_unique 的悬浮提示:

make_unique

首先上来就是一个加粗 function,但是我不知道它是函数吗?
下一句provided by <memory>,我能不知道它属于哪个库吗?再不济我跳转过去不就行了?
再下一句,最迷惑的一集。
现 在 是 抢答时间!!问:
__detail 位于哪个命名空间?

答案是 std,你猜对了吗?

如果你才对了那么下一题:
_MakeUniq<_Tp> 位于哪个命名空间?

答案是 std::__detail,你猜对了吗?

至于Parameters:,基本就是把最后面的签名中的参数列表拷过来。

doxygen 不支持就不用说了(虽然 llvm 22 支持了)。

那么我希望的格式是什么样的呢?其实我个人比较喜欢 intellisense (cpptools 里用的那个)的格式,简单来说,就是上面一大坨都基本不要了,尽量都集成在签名中,不多说,直接看魔改后的效果:

alt text

alt text

怎么做到的呢?

首先 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
2
3
4
inline std::basic_ostream<char, std::char_traits<char>> &std::operator<<<std::char_traits<char>>(
std::basic_ostream<char, std::char_traits<char>> &__out,
const char *__s
)
1
2
3
4
inline std::ostream &std::operator<<<std::char_traits<char>>(
std::ostream &__out,
const char *__s
)

parseDocumentation 函数
这个函数内包含了简易的 doxygen 渲染逻辑


模板实参列表

用于获取实例化后的模板实参列表

alt text


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

分享这一刻
让朋友们也来瞅瞅!