Saltar al contenido

C: clangd, compile_commands y debugging

C y C++ en Neovim 0.12: clangd declarado como ficha en la carpeta lsp del runtimepath, compile_commands.json, parser de Treesitter con la API viva, clang-format y depuración con codelldb.

C es el terreno donde clangd brilla: análisis preciso, ir a definición a través de cabeceras y diagnósticos del compilador en tiempo real. La clave está en que clangd sepa cómo compilas tu proyecto. Y en Neovim 0.12 declararlo no requiere ningún plugin: es un fichero de Lua que devuelve una tabla.

  • Declarar clangd en lsp/clangd.lua y activarlo con vim.lsp.enable.
  • Entender por qué clangd necesita compile_commands.json y cómo generarlo.
  • Instalar el parser de C con la API viva de nvim-treesitter.
  • Formatear con clang-format y depurar con codelldb.

El servidor es una ficha, no un plugin

Desde Neovim 0.12 (la estable de hoy es la 0.12.4) un servidor de lenguaje se describe con vim.lsp.config y se activa con vim.lsp.enable. La forma canónica es poner cada servidor en su propio fichero dentro de una carpeta lsp/ del runtimepath: el fichero devuelve la tabla y Neovim lo lee solo cuando ese servidor hace falta.

lsp/clangd.luaLUA
return {  cmd = {    "clangd",    "--background-index",    "--clang-tidy",              -- diagnósticos de clang-tidy    "--header-insertion=iwyu",   -- inserta la cabecera dueña del símbolo    "--completion-style=detailed",    "--function-arg-placeholders",  },  filetypes = { "c", "cpp", "objc", "objcpp", "cuda" },  root_markers = {    ".clangd",    "compile_commands.json",    "compile_flags.txt",    ".git",  },  init_options = { fallbackFlags = { "-std=c17" } },}
init.luaLUA
vim.lsp.enable("clangd")

Eso es todo: no hay setup(), ni plugin intermediario, ni orden de carga que respetar. Al abrir un .c, Neovim busca los servidores habilitados cuyo filetypes encaje, resuelve la raíz con root_markers, lanza el proceso y adjunta el cliente. Los atajos globales de 0.12 (grn, gra, grr, K, ]d y [d) y los extras que montaste en la lección 3.1 sobre el evento LspAttach valen para clangd sin tocar nada. Recuerda añadir "clangd" a la lista de vim.lsp.enable de tu init.lua — o la línea suelta de arriba, que es equivalente.

Dos banderas que ya no hacen falta

En el clangd actual, --background-index y --clang-tidy vienen activados por defecto. Dejarlos escritos no cambia el comportamiento, pero documenta la intención de la configuración; si prefieres una ficha mínima, bórralos sin miedo. El resto sí cambia cosas: --header-insertion=iwyu inserta el #include que corresponde al aceptar un completado, y --function-arg-placeholders rellena los argumentos como huecos de snippet.

Comprobar qué configuración se aplicó de verdad

Cuando algo no arranca, mira la tabla resuelta antes de teorizar: :lua vim.print(vim.lsp.config["clangd"]) te enseña la fusión final, :checkhealth vim.lsp lista los clientes vivos con su raíz, y el comando :lsp de 0.12 te da el estado del cliente en el buffer actual. Casi siempre el problema es un binario que no está en el PATH o una raíz mal detectada.

nvim-lspconfig y Mason, hoy: opcionales

Puedes escribir esa ficha a mano —acabas de hacerlo— o dejar que te la den ya escrita. Ese es exactamente el papel que le queda a nvim-lspconfig: es un catálogo de fichas por defecto, un directorio lsp/ con centenares de ficheros como el de arriba que entra en tu runtimepath con menos precedencia que el tuyo. No es un motor y ya no hace falta para que el LSP funcione. Si lo instalaste en la lección 3.1, tu ficha se reduce a lo que quieras cambiar — cualquier campo que declares tú gana sobre el suyo:

lsp/clangd.lua (solo lo que difiere del catálogo)LUA
return {  cmd = { "clangd", "--header-insertion=iwyu", "--function-arg-placeholders" },  init_options = { fallbackFlags = { "-std=c17" } },}

Además, la ficha de clangd del catálogo crea dos comandos de buffer que Neovim no trae: :LspClangdSwitchSourceHeader, para saltar entre .c y .h, y :LspClangdShowSymbolInfo. Si los quieres a mano de teclado:

LUA
vim.keymap.set("n", "<leader>ch", "<cmd>LspClangdSwitchSourceHeader<cr>", { desc = "C: cabecera o fuente" })

Mason es la tercera pieza y resuelve un problema distinto: conseguir el binario. Ni declara ni activa nada; instala ejecutables en un directorio propio y lo añade al camino de búsqueda. En C rara vez lo necesitas, porque clangd viene con LLVM y lo instalas con el gestor de paquetes del sistema. Conseguir el binario, describir el servidor y activarlo son tres responsabilidades separadas: confundirlas es el origen de la mayoría de configuraciones enredadas que circulan por ahí.

El corazón: compile_commands.json

clangd necesita saber con qué flags compilas cada archivo (includes, defines, estándar). Esa información vive en un compile_commands.json en la raíz del proyecto. Sin él, clangd adivina —usa el fallbackFlags de tu ficha— y verás falsos errores en los #include.

Con CMake (lo genera solo)

Shell
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON# enlaza el json a la raíz para que clangd lo encuentreln -s build/compile_commands.json .

Con Makefiles: usa Bear

bear envuelve tu compilación y captura los comandos reales:

Shell
# macOS: brew install bear   ·   Debian: apt install bearbear -- make

Proyectos de un solo archivo

Para pruebas rápidas de un .c suelto no hay base de datos que generar: clangd cae en el fallbackFlags de la ficha. clangd también reconoce un compile_flags.txt en la raíz, que es uno de los marcadores con los que detecta el proyecto. Para cualquier proyecto real, genera el compile_commands.json: es la diferencia entre clangd útil y clangd frustrante.

El parser de C, con la API viva

Casi todo lo que encuentres escrito sobre Treesitter está muerto

La rama main es una reescritura incompatible: nvim-treesitter.configs y sus opciones (ensure_installed, highlight, indent, incremental_selection) ya no existen. Lo tienes explicado en las lecciones 2.2 y 3.3.

Aquí no hay nada nuevo que configurar, y eso es lo elegante del diseño que ya tienes montado. El parser de C entró en tu lista de install() en la lección 3.3 (además, C es uno de los seis que Neovim trae de serie), y el autocomando genérico de lua/config/treesitter.lua —el de la 2.2, sin lista de lenguajes— enciende resaltado, plegado e indentación en cuanto detecta que hay parser. Abrir un .c basta.

install es asíncrona. Si necesitas que el arranque de una máquina nueva no siga hasta tener un parser, encadena la espera: require("nvim-treesitter").install({ "c" }):wait(300000).

Con el parser puesto, los objetos de texto de la lección 3.3 valen en C: dif vacía el cuerpo de una función, vaf la selecciona entera, ]f salta a la siguiente y <leader>cp intercambia dos argumentos de una llamada.

Formateo con clang-format

Ya configuraste c = { "clang_format" } en conform (lección 3.4). El binario clang-format viene con LLVM; instálalo con el gestor de paquetes del sistema o con Mason, que para esto es exactamente lo que necesitas. Personaliza el estilo con un .clang-format en la raíz:

.clang-formatYAML
BasedOnStyle: LLVMIndentWidth: 4ColumnLimit: 100AllowShortFunctionsOnASingleLine: None

Debugging con codelldb

codelldb es el adaptador para lenguajes nativos. Define una configuración de debug para C sobre el nvim-dap de la lección 3.4:

lua/config/dap-c.luaLUA
local dap = require("dap")dap.adapters.codelldb = {  type = "server",  port = "${port}",  executable = {    command = vim.fn.exepath("codelldb"),    args = { "--port", "${port}" },  },}dap.configurations.c = {  {    name = "Lanzar ejecutable",    type = "codelldb",    request = "launch",    program = function()      return vim.fn.input("Ruta al ejecutable: ", vim.fn.getcwd() .. "/", "file")    end,    cwd = "${workspaceFolder}",    stopOnEntry = false,  },}

Y la parte que casi todo el mundo olvida: cargar el módulo. Ojo con el matiz según tu vía de instalación de nvim-dap. Con vim.pack el plugin ya está cargado, así que un require normal en el init.lua funciona. Con lazy.nvim lo instalaste diferido por keys, y un require("dap") en el arranque lo forzaría a cargar, tirando la carga perezosa a la basura — ahí el sitio correcto es dentro del config de la spec:

init.lua (vía vim.pack)LUA
require("config.dap-c")
lua/plugins/dap.lua (vía lazy.nvim: añade al config existente)LUA
config = function()  -- ...lo que ya tenías de la lección 3.4...  require("config.dap-c")end,

Compila con símbolos de depuración (gcc -g -o programa programa.c), pon un breakpoint con <leader>db y arranca con <leader>dc.

clang-tidy es tu revisor gratis

Con clang-tidy activo, clangd te avisa de bugs sutiles: fugas de memoria potenciales, comparaciones peligrosas, usos tras liberar. Configúralo con un .clang-tidy en la raíz activando familias de checks (bugprone-*, performance-*). Fíjate en el patrón: ese fichero, el .clang-format, el compile_commands.json y tu lsp/clangd.lua son todos datos declarativos que viven en el proyecto o en tu configuración, no comportamiento escondido en un plugin. Cuando toda tu configuración tiene esa forma, cambiar de máquina deja de dar miedo y depurar el editor se parece a depurar código.

C con superpoderes

  1. Escribe tu lsp/clangd.lua y activa el servidor con una sola línea en init.lua.
  2. Comprueba con :lua vim.print(vim.lsp.config["clangd"]) que la tabla resuelta es la que esperabas.
  3. Genera compile_commands.json en un proyecto C (CMake o Bear) y confirma con :checkhealth vim.lsp que la raíz detectada es la correcta.
  4. Abre un .c: navega con gd a través de un #include hasta la definición.
  5. Instala el parser de C, pliega una función con za y vacía su cuerpo con dif.
  6. Compila con -g, pon un breakpoint y depura con <leader>dc + <leader>du.