wandres.dev
APRENDIZ · Setup y toolchain

El editor como IDE

clangd convierte cualquier editor en un IDE de C: compile_commands.json como contrato, autocompletado con tipos reales, diagnósticos en vivo y clang-tidy integrado, todo cableado dentro de Neovim.

⏱ 15 min

Un IDE de C no es un editor con colores: es un editor que entiende tu programa. Y entender un programa en C exige saber exactamente con qué banderas, con qué macros y contra qué cabeceras se compila cada fichero. clangd resuelve ese problema aplicando la única estrategia honesta: usar el compilador de verdad como motor de análisis. Lo que sigue es cómo darle esa información y cómo conectarlo a Neovim.

🎯 Al terminar esta lección sabrás
  • Entender qué es clangd y por qué necesita las banderas reales de compilación.
  • Generar compile_commands.json desde cualquier sistema de construcción.
  • Cablear clangd en Neovim con la API nativa de LSP.
  • Afinar diagnósticos en vivo, clang-tidy y pistas de tipos en línea.

clangd: el compilador como servidor de lenguaje

clangd no analiza tu código con heurísticas ni con expresiones regulares: reutiliza el front-end de Clang, construye el AST completo del fichero y responde preguntas sobre él a través del protocolo LSP. Ir a la definición, renombrar, listar referencias, completar miembros de un struct, avisar de un error de tipos: todo sale del mismo análisis que produciría el binario.

Esa fidelidad tiene un precio: clangd necesita saber cómo se compila cada fichero. Un #include que resuelve o no según -I, un bloque que existe o no según -D, una construcción de C23 que solo se acepta con -std=c23. Sin esa información, clangd adivina, y adivinar en C significa inundar el búfer de errores falsos.

flowchart LR
M[meson o cmake o bear] --> J[compile commands json]
J --> D[clangd con las banderas reales]
D --> N[Neovim via LSP]
N --> A[Autocompletado con tipos]
N --> B[Diagnosticos en vivo]
N --> C[Ir a definicion y renombrar]
N --> E[clang-tidy integrado]
style J fill:#f9e2af,color:#11111b
style D fill:#a6e3a1,color:#11111b

compile_commands.json: el contrato

Es un fichero JSON en la raíz del proyecto —o en el directorio de construcción— con una entrada por unidad de traducción: directorio de trabajo, comando exacto y fichero. Es un formato estándar de LLVM, no un invento de clangd, y lo consumen también clang-tidy y include-what-you-use.

[
  {
    "directory": "/home/dev/proyecto/build",
    "command": "cc -std=c23 -Wall -Wextra -I../include -DDEBUG=1 -c ../src/lista.c",
    "file": "../src/lista.c"
  }
]

Casi nunca se escribe a mano. Cada sistema de construcción lo genera:

# Meson: lo genera siempre, en el directorio de build
meson setup build && ln -sf build/compile_commands.json .

# CMake
cmake -S . -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
ln -sf build/compile_commands.json .

# Make y cualquier otra cosa: bear intercepta las llamadas al compilador
bear -- make clean all

Para un proyecto de un puñado de ficheros sin sistema de construcción, existe el atajo compile_flags.txt: una bandera por línea, aplicada a todo el árbol.

-std=c23
-Wall
-Wextra
-Iinclude
⚠️
El fichero tiene que estar donde clangd mire

clangd busca compile_commands.json en el directorio del fichero abierto y va subiendo por los padres. Si vive dentro de build/, crea un enlace simbólico en la raíz o dile dónde está con la bandera --compile-commands-dir. Un clangd sin base de datos es un clangd que inventa.

Integrar clangd en Neovim

Neovim 0.11 trajo una API de LSP nativa que hace innecesaria la capa de configuración externa para servidores sencillos. Bastan dos llamadas: describir el servidor y activarlo.

-- ~/.config/nvim/lua/config/clangd.lua
vim.lsp.config('clangd', {
  cmd = {
    'clangd',
    '--background-index',        -- indexa el proyecto entero en segundo plano
    '--clang-tidy',              -- diagnosticos de clang-tidy en vivo
    '--completion-style=detailed',
    '--header-insertion=iwyu',   -- inserta el include correcto al completar
    '--function-arg-placeholders',
  },
  filetypes = { 'c', 'cpp' },
  root_markers = { 'compile_commands.json', 'compile_flags.txt', '.git' },
})

vim.lsp.enable('clangd')

Y los atajos que convierten el servidor en un IDE de verdad. Neovim ya trae por defecto grn para renombrar, gra para acciones de código y grr para referencias; el resto conviene fijarlo a mano:

vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local opts = { buffer = args.buf }
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
    vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
    vim.keymap.set('n', 'K',  vim.lsp.buf.hover, opts)
    vim.keymap.set('n', '<leader>h', '<cmd>LspClangdSwitchSourceHeader<cr>', opts)
    vim.lsp.inlay_hint.enable(true, { bufnr = args.buf })
  end,
})

El salto entre .c y .h merece atención: no es una función del protocolo LSP, es una extensión propia de clangd que Neovim expone como comando. Es el atajo que más se usa en un proyecto real de C.

Diagnósticos en vivo y afinado

Un fichero .clangd en la raíz del proyecto permite corregir la base de datos sin tocar el sistema de construcción: añadir banderas, quitar las que clangd no entiende, y elegir qué comprobaciones de clang-tidy se ejecutan.

# .clangd
CompileFlags:
  Add: [-std=c23, -Wall, -Wextra]
  Remove: [-mno-direct-extern-access]
Diagnostics:
  ClangTidy:
    Add: [bugprone-*, cert-*, performance-*]
    Remove: [bugprone-easily-swappable-parameters]
  UnusedIncludes: Strict
InlayHints:
  Enabled: true
  ParameterNames: true
  DeducedTypes: true

Con --clang-tidy activo dejas de tener solo errores del compilador: tienes análisis semántico mientras escribes. bugprone-* caza patrones de bug conocidos, cert-* aplica reglas de código seguro. Aparecen subrayados en el búfer antes de guardar, y muchos traen acción de código para arreglarlos.

⚙️

Índice de fondo

--background-index construye un índice persistente del proyecto en .cache/clangd. Es lo que hace que buscar referencias sea instantáneo en un árbol grande.

🩺

Cuando algo falla

:checkhealth vim.lsp y :LspLog responden casi siempre. El error más común es un compile_commands.json desactualizado tras añadir ficheros.

El editor no adivina: consume la misma verdad que el compilador

La diferencia entre un editor con resaltado y un entorno de desarrollo real es de naturaleza, no de grado. El resaltado es sintáctico: reconoce formas. clangd es semántico: conoce tipos, ámbitos, expansiones de macro y resolución de nombres, porque es literalmente el mismo front-end que produce el binario. Por eso el paso decisivo de esta lección no es instalar un plugin, sino generar compile_commands.json: ese fichero elimina la brecha entre lo que el editor cree que tienes y lo que el compilador realmente compila. En C esa brecha es peculiarmente letal, porque el preprocesador hace que el mismo texto signifique cosas distintas según las macros y las rutas de inclusión activas; un editor sin las banderas reales no está viendo tu programa, está viendo una hipótesis sobre tu programa. Cuando el circuito se cierra, el ciclo de realimentación se comprime de minutos a milisegundos: el error de tipos aparece bajo el cursor, no en un registro de CI veinte minutos después. Esa compresión temporal es la mejora de productividad más grande disponible en C, y cuesta una línea de configuración.

⚔️ Cablea tu entorno
  1. Genera compile_commands.json en un proyecto tuyo con bear, Meson o CMake, y enlázalo en la raíz.
  2. Configura clangd en Neovim con vim.lsp.config y vim.lsp.enable, y verifica con :checkhealth vim.lsp que se ha adjuntado al búfer.
  3. Escribe un error de tipos deliberado y comprueba que el subrayado aparece sin guardar.
  4. Activa --clang-tidy con las comprobaciones bugprone-* y arregla el primer aviso usando la acción de código.
  5. Crea un .clangd con InlayHints activo y compara la legibilidad de una función con muchos parámetros antes y después.
  6. Borra el compile_commands.json a propósito, observa los errores falsos que aparecen, y entiende exactamente qué información perdió el servidor.