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 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:
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 encuentreln -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 bearbear -- makeProyectos 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:
BasedOnStyle: LLVMIndentWidth: 4ColumnLimit: 100AllowShortFunctionsOnASingleLine: NoneDebugging 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:
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:
require("config.dap-c")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
- 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.