LSP nativo: el cerebro del IDE
Configura el Language Server Protocol con la API nativa de Neovim 0.12: vim.lsp.config, vim.lsp.enable y la carpeta lsp/. Diagnósticos, ir a definición, code actions, inlay hints y el comando :lsp, sin plugins obligatorios.
El LSP es lo que convierte a Neovim en un IDE: autocompletado semántico, ir a definición, renombrado seguro, diagnósticos en tiempo real y refactors. En Neovim 0.12 todo eso vive en el núcleo: describes cada servidor en un fichero, lo activas con una línea, y no necesitas ningún plugin para que funcione.
- Entender qué es un language server y cómo habla con Neovim.
- Configurar LSP con la API nativa:
vim.lsp.config,vim.lsp.enabley la carpetalsp/. - Saber qué aportan hoy
nvim-lspconfigy Mason, y por qué ambos son opcionales. - Añadir solo los atajos que Neovim 0.12 no trae ya, sin pisar teclas ocupadas.
Qué es el LSP
El Language Server Protocol separa la inteligencia del lenguaje del editor. Un “language server” (p. ej. rust-analyzer, clangd, lua_ls) es un proceso independiente que entiende tu código y responde preguntas: “¿dónde se define esto?”, “¿qué autocompleta aquí?”, “¿hay errores?”. Neovim es el cliente que le pregunta.
flowchart LR N[Neovim cliente LSP] -->|peticiones| S[Servidor de lenguaje] S -->|diagnosticos completado definiciones| N M[Gestor de binarios opcional] -.instala.-> S style N fill:#89b4fa,color:#11111b style S fill:#cba6f7,color:#11111b style M fill:#a6e3a1,color:#11111b
La forma nativa: describir y activar
En 0.12 configurar un servidor son dos gestos separados. vim.lsp.config describe un servidor: qué binario lanzar (cmd), para qué tipos de fichero (filetypes), qué marca la raíz del proyecto (root_markers) y qué opciones enviarle (settings). vim.lsp.enable lo activa: a partir de ahí, cuando abras un buffer del tipo adecuado, Neovim resuelve la raíz, arranca el proceso y adjunta el cliente.
Puedes llamar a vim.lsp.config desde tu init.lua, pero la forma canónica es otra: cualquier fichero lsp/nombre.lua dentro del runtimepath se interpreta como la configuración del servidor nombre, y solo se lee cuando ese servidor hace falta. Un fichero por servidor, y el arranque se queda con una sola línea.
-- Devuelve la tabla; no llames a vim.lsp.config desde aquí.
return {
cmd = { "lua-language-server" },
filetypes = { "lua" },
-- La lista nested indica "igual prioridad": cualquiera de los dos vale.
root_markers = { { ".luarc.json", ".luarc.jsonc" }, ".git" },
settings = {
Lua = {
runtime = { version = "LuaJIT" },
workspace = { checkThirdParty = false },
telemetry = { enable = false },
},
},
}
return {
cmd = { "clangd", "--background-index", "--clang-tidy" },
filetypes = { "c", "cpp", "objc", "objcpp" },
root_markers = { "compile_commands.json", ".clangd", ".git" },
}
-- Una sola línea decide qué se activa.
vim.lsp.enable({
"lua_ls", -- Lua (tu config)
"clangd", -- C / C++
"ts_ls", -- TypeScript / JavaScript
"tailwindcss",
"cssls", "html", "jsonls", "yamlls",
})
Lo transversal —lo que quieres para todos los servidores— se declara una vez bajo el nombre comodín *:
vim.lsp.config("*", {
root_markers = { ".git" },
capabilities = {
textDocument = { semanticTokens = { multilineTokenSupport = true } },
},
})
No habilites rust_analyzer aquí si usas rustaceanvim (lección 2.6): ese plugin arranca y gestiona el servidor por su cuenta y tendrías dos clientes peleándose. Y sourcekit-lsp viene con Xcode, no hay nada que descargar (lección 2.7).
nvim-lspconfig y Mason: qué aportan hoy
Ninguno de los dos es necesario. Conviene saber exactamente qué hace cada uno para decidir con criterio.
nvim-lspconfig ya no es una capa, es un catálogo. El repositorio contiene hoy un directorio lsp/ con centenares de ficheros como los que acabas de escribir a mano, mantenidos por gente que ha averiguado qué argumento necesita cada binario, qué marcadores identifican la raíz de cada ecosistema y qué tipos de fichero reclama cada servidor. Al instalarlo, ese directorio entra en tu runtimepath y sus fichas quedan disponibles con menor precedencia que las tuyas. No aporta mecanismo: sigues escribiendo vim.lsp.config y vim.lsp.enable. Lo instalas si trabajas con muchos lenguajes y no quieres averiguar los detalles de cada uno; lo omites si tu abanico es corto.
return {
"neovim/nvim-lspconfig",
lazy = false, -- son datos en el runtimepath: nada que cargar en diferido
}vim.pack.add({ { src = "https://github.com/neovim/nvim-lspconfig" } })Opcional. Solo añade su carpeta lsp/ al runtimepath: no arranca, no activa y no configura nada por su cuenta.
Mason resuelve un problema distinto y ortogonal: conseguir el binario. Instala servidores, formateadores y depuradores en un directorio propio y lo añade al camino de búsqueda de ejecutables. Ni declara ni activa nada; solo hace que el cmd de tus fichas exista en la máquina. Si instalas tus servidores con el gestor de paquetes del sistema (brew, apt, npm, cargo), no lo necesitas.
return {
"mason-org/mason.nvim",
lazy = false,
priority = 100, -- el PATH debe estar listo antes del primer LspAttach
opts = {},
}vim.pack.add({ { src = "https://github.com/mason-org/mason.nvim" } })
require("mason").setup({})Opcional. Tiene que cargarse antes de que arranque ningún servidor, porque lo que hace es poner su directorio de binarios en el PATH.
No tienes que elegir. vim.pack es el gestor nativo de 0.12, con lockfile para reproducir versiones exactas, y es perfecto para todo lo que no necesita carga perezosa —que es mucho: temas, catálogos como nvim-lspconfig, librerías. lazy.nvim sigue valiendo la pena cuando quieres lazy loading fino o su DSL de specs. Muchas configuraciones de 2026 usan las dos a la vez. Por eso cada instalación de esta guía se muestra en ambas vías: elige la que encaje con ese plugin concreto.
Novedades de 0.12 que vas a notar
El comando :lsp
:lsp enable, :lsp disable, :lsp restart y :lsp stop gestionan clientes de forma interactiva. Sustituyen a los viejos comandos de plugin del estilo LspInfo o LspRestart.
Semantic tokens por viewport
El cliente pide textDocument/semanticTokens/range: solo procesa lo visible en pantalla. En ficheros grandes la diferencia de fluidez es evidente.
Code lens en líneas virtuales
El code lens se reimplementó y ahora se dibuja como líneas virtuales sobre el código, no como texto virtual al final de la línea. grx ejecuta el que tengas bajo el cursor.
Capacidades nuevas
selectionRange (selección incremental), inlineCompletion, linkedEditingRange, documentLink (gx abre el enlace bajo el cursor) y diagnósticos de workspace.
Keymaps: mapea solo lo que falta
Este es el cambio de hábito más grande. Neovim 0.12 crea atajos globales de LSP y de diagnósticos al arrancar, sin que configures nada:
| Tecla | Hace |
|---|---|
grn |
renombrar símbolo |
gra |
code action (normal y visual) |
grr |
referencias |
gri |
implementaciones |
grt |
ir a la definición del tipo |
grx |
ejecutar el code lens del cursor |
gO |
símbolos del documento |
K |
documentación flotante |
CTRL-] |
ir a definición (vía tagfunc) |
CTRL-S en Insert |
signature help |
]d [d ]D [D |
saltar entre diagnósticos |
CTRL-W d |
ver el diagnóstico del cursor en flotante |
an in en Visual |
ampliar o reducir la selección |
Antes de inventarte atajos, tres reglas que valen para todo el track. Ctrl-Espacio está reservado al autocompletado: es lo que dispara el completado nativo de 0.12 (vim.o.autocomplete) y también el atajo por defecto de blink.cmp y nvim-cmp. Si lo mapeas a otra cosa, tu completado deja de responder en Insert. Lo mismo con Tab: parece libre y no lo está. Y **Y s y S son de flash.nvim, con mini.surround en el prefijo gs (lección 2.3).
Añade a la lista un cuarto caso propio del LSP: no mapees gr a secas. Si lo haces, tapas la familia entera grn, gra, grr, gri, grt y grx. Tampoco gi, que en Vim vuelve al último punto de inserción, ni gh, que arranca el modo Select.
Con eso claro, lo que queda por mapear es poco. Los atajos que dependen del LSP viven en el evento LspAttach, que es el único momento en que existen buffer y cliente:
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("lsp_attach", { clear = true }),
callback = function(ev)
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if not client then return end
local map = function(keys, fn, desc)
vim.keymap.set("n", keys, fn, { buffer = ev.buf, desc = "LSP: " .. desc })
end
-- Solo lo que Neovim no trae ya mapeado
map("<leader>lf", function() vim.lsp.buf.format({ timeout_ms = 2000 }) end, "Formatear")
map("<leader>lw", vim.lsp.buf.workspace_symbol, "Buscar simbolo en el proyecto")
map("<leader>ld", vim.diagnostic.setqflist, "Diagnosticos al quickfix")
-- Inlay hints (tipos en linea): estan apagados por defecto
if client:supports_method("textDocument/inlayHint") then
vim.lsp.inlay_hint.enable(true, { bufnr = ev.buf })
map("<leader>lh", function()
local on = vim.lsp.inlay_hint.is_enabled({ bufnr = ev.buf })
vim.lsp.inlay_hint.enable(not on, { bufnr = ev.buf })
end, "Alternar inlay hints")
end
-- Code lens: tambien apagado por defecto; se dibuja como lineas virtuales
if client:supports_method("textDocument/codeLens") then
vim.lsp.codelens.enable(true, { bufnr = ev.buf })
end
end,
})
client:supports_method(...) pregunta qué acordaron cliente y servidor durante el arranque. Invocar una capacidad que el servidor no ofrece produce errores silenciosos que luego cuesta rastrear. Y recuerda que LspAttach se dispara una vez por cada pareja de cliente y buffer: con dos servidores en el mismo fichero, tu función corre dos veces.
Diagnósticos con estilo
Configura cómo se ven los errores y avisos. Esto no depende del LSP: vim.diagnostic es un subsistema propio y el cliente es solo uno de sus productores.
vim.diagnostic.config({
virtual_text = { severity = { min = vim.diagnostic.severity.WARN }, prefix = "●" },
virtual_lines = { current_line = true }, -- el mensaje completo, solo en la linea del cursor
underline = true,
update_in_insert = false,
severity_sort = true,
float = { border = "rounded", source = true },
signs = {
text = {
[vim.diagnostic.severity.ERROR] = " ",
[vim.diagnostic.severity.WARN] = " ",
[vim.diagnostic.severity.HINT] = " ",
[vim.diagnostic.severity.INFO] = " ",
},
},
})
Plugins que hacen el LSP aún mejor
Ninguno hace falta para que el LSP funcione. Estos tres se ganan el sitio por otras razones.
return {
"folke/lazydev.nvim",
ft = "lua",
opts = {
library = { { path = "${3rd}/luv/library", words = { "vim%.uv" } } },
},
}vim.pack.add({ { src = "https://github.com/folke/lazydev.nvim" } })
require("lazydev").setup({
library = { { path = "${3rd}/luv/library", words = { "vim%.uv" } } },
})Completado y tipos correctos para tu propia config de Neovim en Lua. Con esto no necesitas el truco de declarar vim como global en los settings de lua_ls.
Muestra el progreso del LSP (indexando, cargando) en una esquina. Ojo: 0.12 ya expone vim.ui.progress_status() en la línea de estado por defecto, así que esto es cosmética, no necesidad.
Lista navegable de diagnósticos, referencias y símbolos del proyecto. Un buen sustituto del quickfix si te resulta árido.
Si un servidor no arranca o algo va raro, ejecuta :checkhealth vim.lsp. Verás las configuraciones habilitadas, la configuración resuelta de cada una tras la fusión, qué clientes están vivos, a qué buffers están adjuntos, la raíz detectada y la ruta del registro de la sesión. Antes de buscar en internet, mira ahí: casi siempre es un binario que falta en el PATH, un filetype que no coincide con el que creías o una raíz de proyecto mal resuelta. Para reintentar sin salir del editor, :lsp restart.
- Crea
lsp/lua_ls.luacon la ficha del servidor y añade una única llamada avim.lsp.enableen tuinit.lua. Sin instalar ningún plugin. - Abre un archivo
.luade tu propia config. Deberías ver diagnósticos si hay errores, yKdebería mostrar documentación sobrevim.opt. - Ejecuta
:checkhealth vim.lspy localiza la raíz resuelta y la configuración efectiva delua_ls. - Prueba los atajos que ya vienen de fábrica:
grr,grn,gra,gO,]d. Comprueba que no necesitas mapear nada para tenerlos. - Desactiva el servidor en caliente con
:lsp disable lua_lsy vuelve a activarlo con:lsp enable lua_ls. Observa qué pasa con los diagnósticos del buffer abierto.
Con el LSP vivo, en la siguiente lección le damos un buen autocompletado.