C++ 项目规模变大以后,代码编辑器需要解决的问题就不再只是语法高亮和文件编辑。

一个类可能有多个声明和实现,一个函数可能在几十个地方被调用,一个变量的名字可能和其他作用域中的变量完全相同。仅依靠文本搜索,很难准确判断这些代码之间的语义关系。

代码跳转、引用查找、符号搜索、重命名等功能,本质上都依赖编辑器对代码的理解能力。

LSP,也就是 Language Server Protocol,就是解决这类问题的一套通用协议。对于 C++ 开发,clangd 则是比较常见的 Language Server 实现。

LSP 是什么

LSP 是编辑器和语言服务器之间的通信协议。编辑器负责展示代码和接收用户操作,语言服务器负责理解具体的编程语言。

以 C++ 为例,clangd 会负责解析源代码、建立符号信息、分析类型和引用关系,并通过 LSP 将结果提供给 VS Code。

flowchart TB VSCode["VS Code"] LSP["Language Server Protocol"] Clangd["clangd"] Project["C++ Project"] VSCode <--> LSP LSP <--> Clangd Clangd --> Project

这样做的一个重要意义是,编辑器不需要自己实现完整的 C++ 语义分析。

同一个 clangd 可以服务于不同的编辑器,而编辑器只需要实现 LSP 客户端即可。

LSP 和传统文本搜索的区别

文本搜索解决的是“这个字符串出现在哪里”。

LSP 解决的则是“这个符号是什么,以及它和其他代码之间有什么关系”。

例如想要查找 renderer->render(scene); 的调用,通过文本搜索可以找到所有包含 render 的代码,但并不能准确区分不同命名空间中的同名函数。

而 clangd 可以知道这里调用的是哪个具体函数,也可以进一步找到它的声明、定义以及所有引用。

因此,LSP 提供的能力更接近 IDE 中的“代码理解”,而不是单纯的文本处理。

clangd

clangd 是 LLVM 项目中的 C/C++ Language Server。

它基于 Clang 的解析能力理解 C++ 代码,并通过 LSP 向编辑器提供代码补全、诊断、代码跳转、引用查找、重命名等功能。

常见能力包括:

功能 用途
Code Completion 代码补全
Go to Definition 跳转到定义
Go to Declaration 跳转到声明
Find References 查找引用
Call Hierarchy 查看调用关系
Document Symbol 查看当前文件符号
Workspace Symbol 搜索整个项目符号
Rename Symbol 重命名符号
Diagnostics 实时诊断
Code Action 自动修复

在 VS Code 中安装 clangd

VS Code 可以通过 clangd 扩展接入 clangd Language Server。

在 Extensions 中搜索:

clangd

安装对应扩展后,可以在终端检查 clangd 是否已经安装:

clangd --version

Debian / Ubuntu 可以直接安装:

sudo apt install clangd

Arch Linux:

sudo pacman -S clang

VS Code 的 clangd 扩展负责启动 clangd,并通过 LSP 与它通信。

sequenceDiagram participant User as 用户 participant VSCode as VS Code participant Clangd as clangd User->>VSCode: 打开 C++ 项目 VSCode->>Clangd: 启动 Language Server VSCode->>Clangd: initialize Clangd-->>VSCode: 返回 Server Capabilities VSCode-->>User: 开始提供代码智能

compile_commands.json

clangd 能否正确理解 C++ 项目,很大程度上取决于它是否知道这个项目实际是如何编译的。

C++ 项目中的头文件路径、宏定义、语言标准、编译器选项都可能影响语义分析。

例如同一个源文件:

clang++ \
-std=c++20 \
-I./include \
-I./third_party/foo/include \
-DENABLE_FEATURE \
-c src/main.cpp

如果 clangd 不知道这些参数,它分析出来的结果就可能和真实构建环境不一致。compile_commands.json 就是用来提供这些编译信息的。

CMake 生成编译数据库

CMake 可以直接生成 compile_commands.json

CMakeLists.txt 中:

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

也可以在配置阶段指定:

cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON

构建目录中会得到:

build/compile_commands.json

一个典型项目可能是:

project/
├── CMakeLists.txt
├── src/
├── include/
└── build/
└── compile_commands.json

compile_commands.json 的内容

它本质上是一组源文件和对应编译命令的映射。

例如:

[
{
"directory": "/home/user/project/build",
"command": "/usr/bin/clang++ -I../include -std=c++20 -c ../src/main.cpp",
"file": "/home/user/project/src/main.cpp"
}
]

其中比较重要的字段是:

  • directory:执行编译命令时使用的工作目录
  • command:实际的编译命令
  • file:对应的源文件

clangd 可以根据这些信息还原出源文件真实的编译环境。

让 clangd 找到编译数据库

最简单的方式是在项目根目录创建软链接:

ln -s build/compile_commands.json compile_commands.json

最终目录结构:

project/
├── compile_commands.json
├── CMakeLists.txt
├── src/
├── include/
└── build/
└── compile_commands.json

另一种方式是显式指定编译数据库目录。

VS Code settings.json

{
"clangd.arguments": [
"--compile-commands-dir=build"
]
}

对于复杂的 CMake 工程,通常更适合根据实际构建目录设置这个参数。

代码导航

完成 clangd 和 compile_commands.json 配置以后,最明显的变化就是代码导航。

跳转到定义

例如:

workspace->update();

将光标放在 update 上,就可以直接跳转到函数定义。

VS Code 中最常用的是:

F12

对应 Go to Definition

clangd 会根据当前符号的语义信息找到真正的定义,而不是简单搜索字符串。

跳转到声明

.cpp 中的函数实现,可以直接跳转到头文件中的声明。

例如:

void Workspace::update()
{
}

可以跳转到:

class Workspace
{
public:
void update();
};

对于大型项目,这种在声明和实现之间快速切换的能力非常高频。

查找引用

选中一个函数、类、变量或者成员,可以执行 Find All References

例如查找 workspace->update(); 的调用,clangd 会找出项目中真正引用这个符号的位置。这比直接使用 rg "update" 更加准确,因为 rg 只知道文本,而 clangd 知道符号关系。

调用层级

C++ 项目中经常需要回答这样的问题:

这个函数是谁调用的?

或者:

这个函数里面又调用了什么?

VS Code 可以通过 Call Hierarchy 查看调用关系。

例如一个功能的调用关系可能形成这样的结构:

flowchart TB Main["main()"] Run["Application::run()"] Update["Workspace::update()"] Render["Renderer::render()"] Draw["Scene::draw()"] Main --> Run Run --> Update Update --> Render Render --> Draw

这里的重点并不是图形化本身,而是 clangd 已经建立了这些符号之间的关系,因此编辑器可以直接展示调用层级。

符号搜索

在大型工程中,直接按文件查找会越来越低效。

VS Code 可以通过符号搜索定位类、函数和变量。

常用快捷键:

Ctrl + Shift + O

搜索当前文件中的符号。

Ctrl + T

搜索整个工作区中的符号。

例如搜索:

Workspace

可以快速定位相关的类和成员函数。

代码重命名

重命名也是 LSP 非常实用的功能。

假设有一个成员:

Window *activeWindow;

现在需要把它改成:

Window *currentWindow;

直接进行文本替换很容易误伤其他作用域中同名变量、字符串或者注释。

LSP 的 Rename 操作针对的是符号本身,而不是字符串。

在 VS Code 中可以使用:

F2

执行 Rename Symbol

clangd 会查找这个符号的引用,并统一修改相关代码。

这种能力对于修改公共 API、类名以及成员变量尤其重要。

诊断与 Code Action

clangd 还会实时分析代码,并通过 LSP 向编辑器发送诊断信息。

例如:

std::string value;

value.foo();

clangd 可以识别出 std::string 没有 foo() 成员,并在编辑器中标记问题。

部分问题还支持 Code Action,可以直接执行自动修复。

需要注意的是,clangd 的实时诊断并不能取代实际构建。真正的编译结果仍然以项目使用的编译器和构建系统为准。

LSP 的完整工作过程

打开一个 C++ 文件以后,VS Code、clangd 和 CMake 生成的编译数据库之间实际上形成了一套完整的工作关系。

sequenceDiagram participant User as 用户 participant VSCode as VS Code participant Clangd as clangd participant DB as compile_commands.json User->>VSCode: 打开 C++ 文件 VSCode->>Clangd: 初始化 LSP Clangd->>DB: 读取编译参数 DB-->>Clangd: 返回源文件编译命令 Clangd->>Clangd: 解析代码并建立索引 Clangd-->>VSCode: 返回语义信息 User->>VSCode: F12 跳转到定义 VSCode->>Clangd: textDocument/definition Clangd-->>VSCode: 返回目标位置 VSCode-->>User: 打开定义 User->>VSCode: Find References VSCode->>Clangd: textDocument/references Clangd-->>VSCode: 返回引用列表 VSCode-->>User: 展示引用 User->>VSCode: F2 重命名 VSCode->>Clangd: textDocument/rename Clangd-->>VSCode: 返回修改结果 VSCode-->>User: 应用修改

一套完整的 CMake + clangd 配置

一个简单的 CMake 项目:

project/
├── CMakeLists.txt
├── compile_commands.json
├── include/
│ └── window.h
├── src/
│ ├── main.cpp
│ └── window.cpp
└── build/
└── compile_commands.json

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)

project(example LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

add_executable(example
src/main.cpp
src/window.cpp
)

target_include_directories(example PRIVATE
include
)

执行:

cmake -B build
ln -sf build/compile_commands.json compile_commands.json

VS Code 安装 clangd 扩展以后,打开项目目录即可开始使用代码导航。

常见操作包括:

F12
跳转到定义

Go to Declaration
跳转到声明

Find All References
查找引用

Call Hierarchy
查看调用关系

Ctrl + Shift + O
查看当前文件符号

Ctrl + T
搜索工作区符号

F2
重命名符号

总结

LSP 最重要的意义并不是提供一个“跳转代码”的快捷键,而是建立了一套编辑器与语言分析工具之间的标准通信方式。

在 C++ 环境中,完整的工作链路通常由三个部分组成:

flowchart LR CMake["CMake"] DB["compile_commands.json"] Clangd["clangd"] LSP["LSP"] Editor["VS Code"] CMake --> DB DB --> Clangd Clangd <--> LSP LSP <--> Editor

其中,compile_commands.json 负责告诉 clangd 项目到底是如何编译的,clangd 负责理解 C++ 代码,而 LSP 则负责把这些能力提供给编辑器。

这几个部分配置完成以后,代码补全、定义跳转、引用查找、调用层级、符号搜索以及重命名就可以成为编辑器中的一套统一能力。