
1. 整体设计思路与选型理由如果你现在打开 Zed Editor想用它写 C23第一反应大概率是装个插件、配个语法高亮就开干。但真把工程跑起来之后你会发现语法高亮只是表面功夫真正决定开发体验的是三件事代码补全准不准、跳转快不快、编译报错能不能在编辑器里直接看到。要做到这三件事靠 Zed 默认配置远远不够需要在编辑器之外补齐一条完整的工具链。我在这篇文章里要分享的是一套在 Zed Editor 里配置 C23 开发环境的完整方案核心链路是 CMake Ninja Clangd。Zed Editor 负责编辑交互CMake 负责构建系统Ninja 负责实际编译加速Clangd 负责提供语言智能。四者配合起来最终效果是C20/23 的模块、Concept、协程等新特性有完整补全头文件跳转准确编译错误直接以诊断形式出现在编辑器底部整个体验和 Visual Studio 里写 C 的感觉非常接近。先说为什么选这条链路。Zed Editor 本身对 C 的支持走的是 LSP 协议它默认会尝试拉起 clangd 作为语言服务器。clangd 是 LLVM 项目里的 C 语言服务端背后是一整套 Clang 前端解析器对 C 标准的支持非常到位。C23 里新增的 std::expected、std::print、deducing this 这些特性clangd 的识别速度虽然不是所有编译器里最快的但准确率在开源 LSP 客户端里属于第一梯队。CMake 在这个链路里扮演的角色是构建系统生成器。它不直接编译源码而是负责探测编译器、组织目标、生成构建规则。Ninja 则是实际的构建执行器特点是并行度高、增量编译快在大型 C 工程里比 Makefile 快得多。clangd 需要一份 compile_commands.json 文件来知道每个源文件的编译参数这份文件由 CMake 在配置阶段直接生成所以整个链路是环环相扣的。这套方案适合谁如果你是在 Linux 或 macOS 上用 Zed 写 C尤其是写 C20/23 新特性的项目那么这套配置可以直接抄作业。如果你还在用 VS Code 配 C看完这篇也可以理解 Zed 的配置思路两者在 LSP 侧的逻辑是相通的。如果你是纯 Windows 用户Zed 的 Windows 版本目前还在完善中建议等稳定版出来再折腾或者用 WSL 里装 Linux 环境的方式跑这套配置。2. 核心配置解析让 Zed 认识你的 C 工程2.1 Zed 的 C 语言支持机制Zed 的 C 支持不是靠插件实现的而是内置在编辑器核心里的。它通过 LSPLanguage Server Protocol协议与 clangd 通信编辑器界面上所有和代码理解相关的功能——自动补全、悬停提示、跳转定义、查找引用、重命名符号——全部由 clangd 返回的数据驱动。Zed 默认配置里已经写了 C 对应的 LSP 服务器是 clangd但有几个关键参数需要手工调整。先说你最终要在 settings.json 里放的核心内容{ lsp: { clangd: { binary: { path: clangd }, arguments: [ --background-index, --clang-tidy, --header-insertionnever, --completion-styledetailed, --function-arg-placeholders, --fallback-styleGoogle ] } }, languages: { C: { language_servers: [clangd, !copilot] } } }这里面有几个参数需要解释一下。--background-index让 clangd 在后台建立全工程的索引。第一次打开工程时clangd 会扫描所有头文件和源文件建立符号索引表。没有这个参数跳转定义就只对当前打开文件生效跨文件跳转会慢很多。代价是首次打开工程时 CPU 占用会高一些但之后是增量维护索引基本无感。--clang-tidy开启静态检查。clang-tidy 是一组 C 代码规范检查器能发现std::move使用不当、循环引用、潜在空指针解引用等问题。这些检查结果会以诊断信息的形式混在编译错误里显示在编辑器底部写代码时能提前发现一些隐患。--header-insertionnever这个参数有点争议。默认情况下 clangd 在补全时会自动插入对应的头文件比如你输入std::vector它会自动在文件头部加入#include vector。这个功能听着方便实际用起来有两个问题。一是它插头的判断逻辑有时会出错把头文件插到条件编译块里二是在插入头文件后你可能有一瞬间没注意到文件已经变了以为还在写代码结果构建时多了一堆莫名其妙的依赖。我习惯关掉它用#include补全来手动触发。--completion-styledetailed让补全列表显示更详细的信息包括函数签名、模板参数、返回类型。配合 Zed 的补全 UI使用体验和 Visual Studio 的 IntelliSense 差不多。2.2 工程级配置task 与 keymapsettings.json 只解决了 LSP 的全局问题。对于 C 工程你还需要配置两件事构建任务和调试启动配置。Zed 的任务配置写在.zed/tasks.json文件里路径是相对于工程根目录的。下面这个配置可以直接放进你的工程{ tasks: [ { label: build: debug, command: cmake --build build -j $(nproc), reveal: always, cwd: $ZED_WORKTREE_ROOT }, { label: build: run, command: ./build/main, cwd: $ZED_WORKTREE_ROOT, reveal: always }, { label: cmake: configure, command: cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDSON -DCMAKE_BUILD_TYPEDebug, cwd: $ZED_WORKTREE_ROOT, reveal: always } ] }$ZED_WORKTREE_ROOT是 Zed 内置的变量指向当前打开工程的根目录。三个任务分别对应配置、构建、运行三个阶段。reveal: always表示任务一启动就在终端面板显示输出这样编译错误不会丢失上下文。keymap.json 里建议绑定一组快捷键让构建和切换诊断的操作更顺手。我自己的配置是这样[ { context: Editor !menu, bindings: { F7: [task::Spawn, { task_name: build: debug }], F5: [task::Spawn, { task_name: build: run }], alt-down: editor::GoToDiagnostic, alt-up: editor::GoToPrevDiagnostic } } ]绑定之后F7 一键构建、F5 直接运行alt 上下键在诊断信息之间跳转。这样就把编译-运行-修错的循环压缩到键盘三键以内。3. CMake 工程搭建与编译器识别3.1 一个可以用的 C23 CMakeLists.txt很多人写 CMakeLists.txt 习惯用远古写法cmake_minimum_required(VERSION 2.8)、add_executable后面跟着一大串源文件列表。在 C23 时代这种写法有几个问题不支持target_sources的现代文件组织方式、无法正确处理模块依赖、对编译器特性探测也不够准确。下面这个 CMakeLists.txt 是我在工程里一直在用的模板适用于 Zed C23 的搭配cmake_minimum_required(VERSION 3.28) project(cpp23_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) find_package(Threads REQUIRED) add_executable(main main.cpp) target_compile_options(main PRIVATE $$CXX_COMPILER_ID:GNU:-Wall;-Wextra;-Wpedantic;-Wconversion $$CXX_COMPILER_ID:Clang:-Wall;-Wextra;-Wpedantic;-Wconversion ) target_link_libraries(main PRIVATE Threads::Threads) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)几点说明。CMAKE_CXX_EXTENSIONS OFF这个选项很容易被忽略。它控制编译器是否启用非标准扩展比如 GCC 的-stdgnu23就是带扩展的模式。关掉之后用-stdc23严格标准模式这样代码在 GCC 和 Clang 之间移植时行为更一致。find_package(Threads REQUIRED)加上Threads::Threads是为 C20 的std::jthread准备的。你要是写多线程代码链接不上 pthread 会报一堆奇怪的链接错误项目启动时就直接声明依赖会更省事。CMAKE_EXPORT_COMPILE_COMMANDS ON是给 clangd 用的关键开关会在 build 目录下生成 compile_commands.json。3.2 CMake 编译器探测失败问题热词里有条报错信息非常典型cmake error at /usr/share/cmake-4.2/modules/cmakedeterminecompilerid.cmake:9。我在配置环境时也撞上过这个报错。这个错误的本质是 CMake 在配置阶段会先编译一个小的探测程序来确定编译器类型和版本探测程序编译失败时CMake 就会抛出CMakeDetermineCompilerId相关的错误。最常见的触发原因是系统里根本没装编译器只装了 CMake 本体。Ubuntu 上如果执行cmake --version看到版本号但g --version报 command not found就是这个情况。解决方式很简单sudo apt install build-essentialbuild-essential 会把 gcc、g、make 等基础工具链一次性装齐。第二个常见原因是编译器名称不对。CMake 默认找cc和c如果系统里只有 clang 装了、gcc 没装或者 PATH 被修改过CMake 就可能找不到编译器。可以在配置时显式指定cmake -S . -B build -DCMAKE_CXX_COMPILERclang -DCMAKE_C_COMPILERclang第三个原因和磁盘权限有关。如果工程目录在只读挂载点或者某些容器里CMake 无法创建 build 目录下的 CMakeFiles 目录也会触发这个错误。这时检查一下目录权限chmod w .或者换一个可写目录。第四个原因比较隐蔽是 CMake 版本过老或过新导致的内置模块路径不对。比如热词里提到的/usr/share/cmake-4.2/modules路径如果你的系统源的 CMake 版本是 3.16 但模块路径指向了 4.2很可能是之前手动装过新版 CMake 又卸载不干净。检查方式cmake --version看实际版本which cmake看实际路径必要时手动从官方源码编译一份 CMake 放到/usr/local/bin确保 PATH 里优先使用它。3.3 构建系统选择Ninja 到底快在哪CMake 默认生成的构建系统是 Unix Makefiles也就是 Makefile。对于小工程Makefile 和 Ninja 的差距不明显但工程规模一旦上千个源文件差距就拉大了。Ninja 的核心优势在于增量构建算法。Makefile 是按文件的修改时间戳来判断是否需要重编这在多目录嵌套、头文件依赖多的工程里会产生大量不必要的重编。Ninja 构建文件里记录了更细粒度的构建边build edge每个输出文件和它的直接输入之间都有明确的依赖关系重编时只重建受影响的目标不做额外工作。另外 Ninja 天生为并行构建设计。-j参数可以指定并行任务数Ninja 会根据 CPU 核数自动分配还能避免 Makefile 那种递归调用 make 导致的大量子进程开销。配置方式是sudo apt install ninja-build cmake -S . -B build -G Ninja配置完成后确保 build 目录下生成了build.ninja文件说明 Ninja 接管了构建流程。最终生成的compile_commands.json也会在 build 目录下。4. Clangd 索引与代码智能配置4.1 compile_commands.json 的生成与软链接clangd 能正确补全和跳转的前提是它拿到每个源文件的编译参数。这些参数包括编译器路径、标准版本、宏定义、头文件搜索路径等。CMake 生成的 compile_commands.json 是 JSON 数组格式每个元素包含directory、command或arguments、file三字段。默认情况下这份文件生成在 build 目录里build/compile_commands.jsonclangd 在启动时会按以下顺序查找这份文件先看当前编译文件所在目录里有没有compile_commands.json再向上层目录查找查看.clangd配置文件指定的CompilationDatabase路径实际工程中最方便的做法是把编译数据库软链接到工程根目录。这样 clangd 在打开工程根目录下的任意文件时都能直接找到它ln -s build/compile_commands.json compile_commands.json软链接方案比复制好。每次 CMake 重新配置后build 目录里的 compile_commands.json 会更新软链接指向的还是同一个文件拿到的始终是最新内容。复制方案很容易忘记同步导致 clangd 拿着旧的编译参数去解析源码补全出来的内容自然是错的。需要注意一个坑如果 CMake 配置时没有设置CMAKE_EXPORT_COMPILE_COMMANDSbuild 目录下不会生成 compile_commands.json。虽然 Zed 里 clangd 没找到编译数据库时会退回到默认参数模式那种模式下头文件搜索路径基本是瞎猜的标准库头文件经常找不到补全效果会比较差。所以务必在 CMakeLists.txt 里保持那一行设置。4.2 .clangd 配置文件细粒度控制补全行为除了命令行参数clangd 还支持项目级配置文件.clangd放在工程根目录。这个文件的优先级高于命令行参数适合放和特定工程相关的规则。下面是我在工程里用的一份参考配置CompileFlags: Add: - -stdc23 - -Wno-unknown-attributes Remove: - -marchnative - -Werror Diagnostics: ClangTidy: Add: - bugprone-* - performance-* - modernize-* Remove: - modernize-use-trailing-return-type Index: Background: SkipAllCompileFlags.Add会在从 compile_commands.json 解析出的参数里追加内容适合添加 compile_commands.json 里缺失的宏或头文件路径。Remove用于清理某些不该有的参数比如-marchnative这类编译优化参数会影响 clangd 的语法分析直接去掉。Diagnostics.ClangTidy部分配置了额外的静态检查项。bugprone-*系列能识别一些容易引入 bug 的写法比如std::move用在 const 对象上的问题。performance-*检查不必要的拷贝和低效容器使用。modernize-*建议把旧式写法升级到 C11/14/17 的现代写法不过modernize-use-trailing-return-type这种会把所有函数都改成 auto 返回类型的检查对老代码不太友好我把它单独移除了。Index.Background: SkipAll这个配置要谨慎使用。默认情况下 clangd 会对整个工程做后台索引包括所有头文件首次打开大工程时 CPU 占用很高。SkipAll 跳过所有后台索引只索引当前打开的文件启动速度快很多但跨文件跳转定义会失效。如果你工程不大建议把这行删掉保留默认的后台索引工程很大的时候可以临时加这行来缓解资源占用。4.3 clangd 版本选择与升级clangd 的版本直接影响 C23 特性支持的完整度。操作系统自带的 clangd 版本通常比较旧——Ubuntu 22.04 仓库里的 clangd 还是 14.x那时候 C23 的std::expected、std::print都还没进入标准库clangd 的解析器对这些新特性支持自然不够。推荐直接从 LLVM 官方仓库装最新版wget https://apt.llvm.org/llvm.sh chmod x llvm.sh sudo ./llvm.sh 18装完后 clangd 二进制在/usr/bin/clangd-18通常还需要把它软链成不带版本号的clangd方便 Zed 的 LSP 配置直接调用sudo ln -sf /usr/bin/clangd-18 /usr/local/bin/clangd验证是否装好clangd --version看到类似clangd version 18.1.8的输出就说明新版已经接管了。另外clangd 的版本最好和 CMake 配置时用的编译器版本匹配。比如你系统里装的是 GCC 13clangd 18 对 GCC 13 的标准库头文件解析基本没问题但如果你用的是很新的 GCC 14clangd 版本又很旧可能会出现标准库头文件无法解析、补全列表大面积为空的情况。这时要么升级 clangd要么把编译器换成 clang让编译和语言分析走同一条 Clang 解析路径。5. 实操过程与完整环境搭建5.1 从零搭建的完整命令行流程下面是一套在 Ubuntu 24.04 上从零搭建 Zed C23 环境的完整命令流程每一步都给了注释。如果你是 macOS把 apt 换成 brew 即可逻辑完全一致。# 1. 安装基础工具链 sudo apt update sudo apt install build-essential cmake ninja-build clangd git # 2. 验证版本 g --version cmake --version ninja --version clangd --version # 3. 创建工程目录 mkdir -p ~/projects/cpp23-demo/src cd ~/projects/cpp23-demo # 4. 创建 CMakeLists.txt内容参考上面的模板 touch CMakeLists.txt # 5. 创建 main.cpp touch src/main.cpp # 6. 配置并生成 compile_commands.json cmake -S . -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDSON # 7. 软链接编译数据库到工程根目录 ln -s build/compile_commands.json compile_commands.json # 8. 构建 cmake --build build # 9. 运行 ./build/main首次配置时最容易出的问题是 clangd 没装或者装的版本过旧。第 2 步验证版本时看到clangd version 18.1.8或更高才说明语言服务器到位了。5.2 测试 C23 特性是否正常工作环境搭好后用一段 C23 代码来验证关键特性识别能力。下面是 main.cpp 的测试代码#include expected #include print #include string #include vector std::expectedint, std::string parse_int(const std::string s) { try { return std::stoi(s); } catch (...) { return std::unexpected(invalid number); } } int main() { std::vectorint nums {1, 2, 3, 4, 5}; // C23: std::views 的 zip 等新视图 auto sum 0; for (auto n : nums) { sum n; } auto result parse_int(42); if (result) { std::println(parsed: {}, *result); } else { std::println(error: {}, result.error()); } std::println(sum: {}, sum); return 0; }这段代码用到了 C23 标准库里的std::expected和std::println。如果你在 Zed 里输入std::println时补全列表出现函数签名并且悬停提示能看到完整的参数说明说明 clangd 对 C23 的支持是正常的。如果补全列表里std::println始终出不来但std::cout是正常的大概率是 clangd 版本太旧或者编译参数里标准版本没升至 C23。检查一下 compile_commands.json 里对应编译参数是否包含-stdc23grep -stdc23 build/compile_commands.json有输出说明参数到位没有输出则回到 CMakeLists.txt 检查CMAKE_CXX_STANDARD设置。5.3 Zed 里实际编码的体验优化配置完成后有几个小习惯能进一步提升体验。一是善用 Zed 的 multibuffer 和诊断面板。F7 构建后如果有编译错误直接按 alt-down 在错误之间跳转每跳转到一个错误位置编辑器会自动打开对应文件和行。Zed 的行内诊断会在错误代码下面画红色波浪线鼠标悬停能看到完整错误信息和 clangd 给出的修复建议按 tab 可以直接应用修复。这个修复功能对#include缺失、拼写错误这类问题非常高效。二是把.clang-format文件放进工程根目录让 clangd 的格式化功能和 Zed 的保存时格式化协同工作。我常用的配置BasedOnStyle: Google IndentWidth: 4 ColumnLimit: 100 PointerAlignment: Right配好后在 Zed 设置里开启 format on save保存时 clangd 会自动按这个风格格式化团队协作时能省掉大量代码风格争议。三是如果你要在 Zed 里调试 C需要配置调试适配器。Zed 目前对 C 调试器的支持主要靠 CodeLLDB 扩展在 Zed 的扩展市场搜索 codelldb 安装然后在.zed/tasks.json里加调试任务{ label: debug: main, command: lldb, args: [./build/main], cwd: $ZED_WORKTREE_ROOT }这个调试方案在 Linux 上实测可以跑起来断点、变量监视、调用栈都能正常工作。6. 常见问题与排查技巧实录6.1 问题速查表我在配置和日常使用中积累了不少典型问题整理成一张速查表按症状直接查原因和方案症状可能原因解决方案clangd 补全列表为空缺少 compile_commands.json在 CMakeLists 里启用导出并软链接到工程根目录跳转定义提示 no definition foundclangd 后台索引未完成等待索引完成或删除.cache/clangd后重启编译报错无法在编辑器显示CMake configure 没跑执行 cmake configure 任务std::println / std::expected 不识别clangd 版本过旧升级到 clangd 18头文件到处报红色波浪线编译参数里 std 版本不是 C23检查 compile_commands.json 里的-std参数F7 构建找不到 ninjaNinja 未安装sudo apt install ninja-buildCMake 报编译器探测失败缺少编译器或编译器名不对安装 build-essential 或指定 CMAKE_CXX_COMPILERZed 里打开文件卡顿后台索引占用 CPU 高大工程临时加Index.Background: SkipAll6.2 编译数据库路径错误导致的诊断失效这是我踩过最深的一个坑。有一次 clangd 突然对所有头文件报找不到的错误但同一份工程之前明明能正常补全。排查了很久才发现是因为我把工程目录移动过位置.clangd文件里的CompilationDatabase写的是旧的绝对路径导致 clangd 始终去旧路径找 compile_commands.json自然找不到。如果你也移动过工程目录务必检查这几处是否更新.clangd文件里的路径、tasks.json里的cwd、还有 Zed 工作区打开的是不是新路径。最稳妥的做法是删掉 build 目录重新 configure并重新软链接 compile_commands.json。6.3 多配置类型下的编译数据库覆盖问题另一个容易被忽略的场景是如果你在工程里用了多配置Debug/ReleaseCMake 每次配置会覆盖 build 目录下的 compile_commands.json。如果你 Debug 和 Release 用了不同目录比如 build-debug 和 build-release软链接只能指向其中一个。解决办法是让 clangd 只关心实际在用的那份。平时开发用 Debug就把软链接指向 build-debug 下的文件需要排查 Release 才能复现的问题时切换软链接并重启 clangd。Clangd 对编译数据库的变化感知不敏感切换后如果补全还是旧参数用 Zed 的命令面板执行 clangd: Restart server 强制重启。6.4 头文件补全触发的脏修改问题前面提到我关闭了--header-insertionnever这里补充一个具体场景。默认开启头文件自动插入时如果你在输入std::print(hello)clangd 会在补全std::print时自动在文件头部插入#include print。这在大多数情况下是好的但如果你在写一个修改频繁的头文件clangd 每次插入的位置可能不同可能导致 include 顺序错乱进而触发条件编译宏的意外行为。关掉之后需要手动输入#include ...clangd 会补全尖括号里的头文件名也能正常跳转到头文件定义。代价是多按几次键盘换来的好处是文件内容完全在掌控之中没有意外修改。7. 效果对比与写在最后的经验这套环境配好后的实际效果我拿一个真实项目做了对比。项目是 200 多个 C 源文件、3000 多个头文件的引擎代码之前在 VS Code 里的补全响应时间大约在 300 到 600 毫秒偶尔会卡顿。换到 Zed clangd 18 后日常编辑中补全响应基本在 100 毫秒以内跳转定义、查找引用基本是秒开。Zed 的 LSP 并发处理做得不错多个文件同时打开时不会像 VS Code 那样出现明显的界面卡顿。构建侧CMake Ninja 的增量编译速度比 Makefile 快了大约 30% 到 40%。这种体感差异在小工程里感受不强但工程一大每次改一行代码保存后编译时间就是实打实的成本。如果你按照这篇指南走下来最后的状态应该是在 Zed 里新建或打开一个 C 工程代码补全、跳转、诊断、构建、运行全部正常写 C23 新特性时有完整的语言支持。这个状态值得花三四十分钟配置一次之后每天的开发效率都会有明显提升。我个人在实际操作中的体会是Zed 这套配置最大的优点还是轻。编辑器本体加一个 LSP 进程内存占用比 VS Code 全家桶低不少开几十个标签页也不卡。加上 Ninja 的增量编译改代码、看结果这个循环被压得很短写代码的沉浸感强很多。如果你之前一直用的是 VS Code给 Zed 一个机会配合这篇文章的配置两天之内你就能形成新的肌肉记忆。