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.luay activarlo convim.lsp.enable. - Entender por qué clangd necesita
compile_commands.jsony 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.
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" } },
}
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 (gd, K, <leader>rn, ]d y [d) y los diagnósticos ya los montaste en la lección 2.1 sobre el evento LspAttach, así que valen para clangd sin tocar nada.
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.
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.
return {
"neovim/nvim-lspconfig",
lazy = false,
}vim.pack.add({
{ src = "https://github.com/neovim/nvim-lspconfig" },
})Instalarlo no activa nada: solo añade su carpeta lsp al runtimepath. Sigues necesitando tu vim.lsp.enable, y cualquier campo que declares tú gana sobre el suyo.
Con el catálogo instalado, tu ficha se reduce a lo que quieras cambiar:
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:
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)
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
# enlaza el json a la raíz para que clangd lo encuentre
ln -s build/compile_commands.json .
Con Makefiles: usa Bear
bear envuelve tu compilación y captura los comandos reales:
# macOS: brew install bear · Debian: apt install bear
bear -- make
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
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 la lección 2.3.
Instalaste el plugin en la lección 2.3 con branch = "main", lazy = false y build = ":TSUpdate". Aquí solo añades el parser y enciendes el resaltado para C:
require("nvim-treesitter").install({ "c" })
-- Sin foldlevelstart = 99, cada archivo se abre ENTERO PLEGADO.
-- Ponlo una vez en tus opciones (leccion 2.3).
vim.o.foldlevelstart = 99
vim.api.nvim_create_autocmd("FileType", {
pattern = { "c", "cpp" },
callback = function()
vim.treesitter.start() -- resaltado
vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()" -- plegado
vim.wo[0][0].foldmethod = "expr"
end,
})
install es asíncrona. Si necesitas que el arranque de una máquina nueva no siga hasta tener el parser, encadena la espera: require("nvim-treesitter").install({ "c" }):wait(300000).
Con el parser puesto, los objetos de texto de la lección 2.3 empiezan a valer 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 2.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:
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: 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 2.4:
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,
},
}
Compila con símbolos de depuración (gcc -g -o programa programa.c), pon un breakpoint con <leader>db y arranca con <leader>dc.
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.
- Escribe tu
lsp/clangd.luay activa el servidor con una sola línea eninit.lua. - Comprueba con
:lua vim.print(vim.lsp.config["clangd"])que la tabla resuelta es la que esperabas. - Genera
compile_commands.jsonen un proyecto C (CMake o Bear) y confirma con:checkhealth vim.lspque la raíz detectada es la correcta. - Abre un
.c: navega congda través de un#includehasta la definición. - Instala el parser de C, pliega una función con
zay vacía su cuerpo condif. - Compila con
-g, pon un breakpoint y depura con<leader>dc+<leader>du.