nvim-lsp-clangd

2025-09-09 00:00    #nvim   #oi  

在 Neovim 中不使用 nvim-lspconfig 插件来手动配置 clangd 是一个很好的学习 Neovim 内置 LSP API 的方式。这能让你更深入地理解 LSP 客户端是如何工作的。

下面是详细的步骤和一份完整的 Lua 配置示例。

核心思路

不使用 nvim-lspconfig 的核心是直接调用 Neovim 的原生 LSP 函数 vim.lsp.start()。这个函数负责启动一个 LSP 服务进程,并将其附加到当前的缓冲区。

主要步骤如下:

  1. 确保 clangd 已安装:Neovim 不会帮你安装 LSP 服务器,你需要自己通过系统包管理器安装。
  2. 定义 on_attach 函数:这个函数在 LSP 成功附加到一个缓冲区后执行。我们在这里设置快捷键、高亮等。
  3. 定义 LSP 服务器配置:创建一个配置表,告诉 Neovim 如何启动 clangd(命令、根目录、文件类型等)。
  4. 使用 autocmd 触发 LSP:创建一个自动命令,在打开 C/C++ 文件时,调用 vim.lsp.start() 来启动 clangd

步骤 1: 安装 clangd

首先,确保你的系统上已经安装了 clangd

安装后,在终端运行 clangd --version 确认它已经安装并且在你的 PATH 中。


步骤 2: 创建 compile_commands.json (非常重要!)

clangd 需要 compile_commands.json 文件来了解你的项目是如何编译的(比如头文件路径、宏定义等)。没有这个文件,clangd 的功能会大打折扣,尤其是在大型项目中。

如果你的项目使用 CMake,生成它非常简单:

1# 在你的构建目录中
2cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=1 ..

这会在构建目录中生成一个 compile_commands.json 文件。clangd 会自动在当前文件所在目录及父目录中寻找它。


步骤 3: Neovim Lua 配置

将以下代码放入你的 Neovim 配置文件中(例如 ~/.config/nvim/lua/lsp/clangd.lua),然后在你的 init.luarequire 它。

这是一个完整且带有详细注释的示例:

  1-- ~/.config/nvim/lua/lsp/clangd.lua
  2
  3print("加载自定义 clangd 配置...")
  4
  5-- 1. 定义 on_attach 函数
  6--    这个函数会在 LSP 客户端成功附加到缓冲区时调用
  7--    我们在这里为该缓冲区设置 LSP 相关的快捷键
  8local on_attach = function(client, bufnr)
  9  -- 启用代码补全 (如果你使用 nvim-cmp)
 10  -- client.server_capabilities.completionProvider = true -- 这行通常不需要,因为 capabilities 已经处理了
 11
 12  print("clangd 已附加到缓冲区: " .. bufnr)
 13
 14  -- 设置快捷键
 15  local opts = { noremap = true, silent = true, buffer = bufnr }
 16  local keymap = vim.keymap.set
 17
 18  -- 跳转到定义
 19  keymap('n', 'gd', vim.lsp.buf.definition, opts)
 20  -- 查看悬浮文档
 21  keymap('n', 'K', vim.lsp.buf.hover, opts)
 22  -- 跳转到实现
 23  keymap('n', 'gi', vim.lsp.buf.implementation, opts)
 24  -- 列出引用
 25  keymap('n', 'gr', vim.lsp.buf.references, opts)
 26  -- 重命名符号
 27  keymap('n', '<F2>', vim.lsp.buf.rename, opts)
 28  -- 显示代码动作 (修复、重构等)
 29  keymap('n', '<leader>ca', vim.lsp.buf.code_action, opts)
 30  -- 显示当前行的诊断信息 (错误、警告)
 31  keymap('n', '<leader>e', vim.diagnostic.open_float, opts)
 32  -- 跳转到上一个/下一个诊断
 33  keymap('n', '[d', vim.diagnostic.goto_prev, opts)
 34  keymap('n', ']d', vim.diagnostic.goto_next, opts)
 35  
 36  -- 设置高亮
 37  -- 当光标移动到有引用的符号上时,高亮所有引用
 38  vim.api.nvim_create_autocmd('CursorHold', {
 39    buffer = bufnr,
 40    callback = function()
 41      vim.lsp.buf.document_highlight()
 42    end,
 43  })
 44  vim.api.nvim_create_autocmd('CursorMoved', {
 45    buffer = bufnr,
 46    callback = function()
 47      vim.lsp.buf.clear_references()
 48    end,
 49  })
 50
 51end
 52
 53-- 2. 定义 LSP 能力 (Capabilities)
 54--    这告诉 LSP 服务器,客户端(Neovim)支持哪些功能
 55--    如果你使用 nvim-cmp 进行补全,需要从 cmp_nvim_lsp 获取 capabilities
 56local capabilities = require('cmp_nvim_lsp').default_capabilities(vim.lsp.protocol.make_client_capabilities())
 57
 58-- 3. 创建自动命令来启动 clangd
 59--    当打开 C/C++/Objective-C 文件时,会触发这个自动命令
 60vim.api.nvim_create_autocmd('FileType', {
 61  pattern = { 'c', 'cpp', 'objc', 'objcpp', 'cuda' }, -- 触发 clangd 的文件类型
 62  callback = function()
 63    -- 使用 vim.lsp.start() 启动客户端
 64    vim.lsp.start({
 65      -- 客户端的名称,可以自定义
 66      name = 'my-clangd-server',
 67
 68      -- 启动 LSP 服务器的命令
 69      -- 如果 clangd 不在你的 PATH 中,你需要提供完整路径
 70      -- 你也可以在这里传递 clangd 的命令行参数
 71      cmd = {
 72        'clangd',
 73        '--query-driver=/usr/bin/gcc,**/gcc-*,/usr/bin/g++,**/g++-*', -- 帮助 clangd 找到系统头文件
 74        '--background-index',       -- 后台索引
 75        '--clang-tidy',             -- 启用 clang-tidy
 76        '--completion-style=detailed',
 77      },
 78
 79      -- 查找项目根目录的逻辑
 80      -- clangd 会自动寻找 compile_commands.json 或 .git 目录
 81      -- 这里使用 Neovim 内置的工具函数
 82      root_dir = vim.lsp.util.root_pattern('.git', 'compile_commands.json', 'compile_flags.txt'),
 83
 84      -- 文件类型
 85      filetypes = { 'c', 'cpp', 'objc', 'objcpp', 'cuda' },
 86
 87      -- 附加函数
 88      on_attach = on_attach,
 89
 90      -- 能力
 91      capabilities = capabilities,
 92
 93      -- 服务器特定设置 (settings)
 94      -- 对于 clangd,大多数配置通过命令行参数或 .clangd 配置文件完成
 95      settings = {
 96        -- 这里可以放一些 clangd 的特定配置,但通常留空
 97      },
 98    })
 99  end,
100})
101
102-- (可选) 配置诊断信息的显示方式
103vim.diagnostic.config({
104  virtual_text = true, -- 在行尾显示错误信息
105  signs = true,        -- 在行号列显示图标
106  underline = true,
107  update_in_insert = false,
108  severity_sort = true,
109})
110
111-- (可选) 设置诊断图标 (需要 Nerd Font 字体支持)
112local signs = { Error = " ", Warn = " ", Hint = " ", Info = " " }
113for type, icon in pairs(signs) do
114  local hl = "DiagnosticSign" .. type
115  vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
116end
117
118print("自定义 clangd 配置加载完毕。")

步骤 4: 在 init.lua 中加载配置

确保在你的主配置文件 init.lua 中加载上面创建的 Lua 文件:

1-- ~/.config/nvim/init.lua
2
3-- ... 其他配置 ...
4
5-- 加载 LSP 相关配置
6require('lsp.clangd') -- 假设你把文件放在 lua/lsp/clangd.lua
7
8-- ... 其他配置 ...

配置解释

  1. on_attach 函数: 这是自定义 LSP 行为的关键。它只会在 LSP 客户端成功连接到当前文件后执行,确保了 vim.lsp.buf.* 系列函数可用。将快捷键设置在这里可以避免在没有 LSP 的缓冲区中创建无效的快捷键。

  2. capabilities: 这个配置非常重要,特别是当你使用 nvim-cmp 这样的补全插件时。它告诉 clangd,Neovim 客户端支持哪些功能(例如,代码片段补全、动态注册等)。cmp_nvim_lsp 提供了一个方便的函数来生成这个配置。如果你不使用补全插件,可以使用默认的 vim.lsp.protocol.make_client_capabilities()

  3. vim.lsp.start(): 这是核心函数。

    • name: 一个唯一的标识符,方便调试。
    • cmd: 一个包含命令及其参数的 table。这是告诉 Neovim 如何启动 clangd 进程的地方。你可以添加 clangd 支持的各种命令行参数来微调其行为。
    • root_dir: vim.lsp.util.root_pattern 是一个辅助函数,它会从当前文件所在的目录开始向上查找,直到找到 .git 目录或 compile_commands.json 文件,并将其所在的目录作为项目根目录。
    • on_attach: 引用我们之前定义的函数。
  4. autocmd: FileType 事件是最适合用来启动 LSP 的时机。当 Neovim 识别到一个文件的类型是 ccpp 等时,就会执行回调函数,从而启动 clangd

总结

通过这种手动方式,你可以完全控制 clangd 的启动参数和行为,而无需依赖任何中间层插件。虽然 nvim-lspconfig 通过预设的社区配置简化了这个过程,但了解其底层原理对于调试问题和进行深度定制非常有帮助。

现在,重启 Neovim 并打开一个 C/C++ 项目(确保有 compile_commands.json),你应该能看到 clangd 成功启动,并且你设置的快捷键也能正常工作了。