Extender: capacidades, formateo y convivencia
Las capacidades que el cliente de Neovim 0.12 no activa solo: inlay hints, code lens en líneas virtuales, completado en línea y edición enlazada, más los semantic tokens limitados al viewport. Formateo al guardar hecho de forma segura y arquitectura para convivir con formateadores externos sin que dos herramientas discutan sobre el mismo fichero.
Un servidor de lenguaje sabe más de tu código de lo que muestra por defecto, y la razón de esa reserva no es técnica sino prudente: las capacidades más ricas son también las más caras, y activarlas indiscriminadamente convierte un editor ágil en uno que se arrastra. El manual de Neovim 0.12 lo dice sin rodeos y hasta te da la lista: hay funciones del cliente que están apagadas y tienes que encenderlas a mano. Este último capítulo del nivel las recorre —tipos inferidos en línea, acciones incrustadas sobre el código, completado en línea, edición enlazada— y de paso explica por qué el resaltado semántico, que sí viene encendido, dejó de ser caro. Después aterriza en el problema más prosaico y más conflictivo de todos: quién tiene derecho a reescribir tu fichero cuando lo guardas, y cómo se organiza eso cuando hay más de un candidato.
- Activar y controlar
inlay hints,code lensy las capacidades nuevas de 0.12 con conciencia de su coste. - Entender por qué los
semantic tokensya no penalizan los ficheros grandes. - Condicionar cada capacidad a lo que el servidor realmente negoció, no a lo que se supone.
- Implementar formateo al guardar de forma segura, con tiempo máximo y sin sorpresas.
- Diseñar una convivencia clara entre el servidor y los formateadores externos del proyecto.
Capacidades que hay que pedir
Todas se comportan igual en lo esencial: el servidor las ofrece si sabe, pero es tu configuración la que decide si se usan y dónde. La regla de oro es condicionarlas siempre a lo negociado, porque preguntarle a un servidor por algo que no soporta produce errores silenciosos que luego cuesta rastrear. Y todas comparten forma en la API: un módulo por capacidad con una función enable que recibe un valor booleano y un filtro por buffer o por cliente, más su is_enabled para poder alternar.
Las pistas en línea insertan texto no editable con información que el código omite: el tipo inferido de una variable, el nombre del parámetro en una llamada, el tipo de retorno de una función. Su valor es alto en lenguajes con inferencia agresiva y su coste también, porque cada cambio obliga a recalcularlas. La configuración razonable no es activarlas o desactivarlas, sino hacerlas conmutables.
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
if not client then return end
local buf = args.buf
if client:supports_method("textDocument/inlayHint") then
vim.lsp.inlay_hint.enable(true, { bufnr = buf })
vim.keymap.set("n", "<leader>lh", function()
local on = vim.lsp.inlay_hint.is_enabled({ bufnr = buf })
vim.lsp.inlay_hint.enable(not on, { bufnr = buf })
end, { buffer = buf, desc = "alternar pistas en linea" })
end
end,
})
La tentación es colgar estas alternancias de una tecla corta y libre, y ahí es donde se rompen configuraciones. Ctrl-Espacio está reservado al autocompletado —el nativo de 0.12 con vim.o.autocomplete, y también blink.cmp y nvim-cmp—, y Tab lo mismo.Y s y S son de flash.nvim, con mini.surround en el prefijo gs (lección 2.3). Y las teclas cortas del espacio g tampoco están tan libres como parecen: gh arranca el modo Select y gr es el prefijo de la familia grn, gra, grr, gri, grt y grx que 0.12 crea de fábrica. La regla del track es simple: los saltos estructurales van en ] y [, y las acciones en <leader>.
Los tokens semánticos son el otro extremo del resaltado. El análisis sintáctico local sabe que algo es un identificador; el servidor sabe si ese identificador es un parámetro, una constante, un tipo, una función obsoleta o una variable capturada por un cierre. Esa información llega como una lista de tokens con modificadores que se traducen a grupos de resaltado con nombres derivados del tipo y de cada modificador, y el resultado es que dos palabras idénticas pueden verse distintas porque significan cosas distintas. Personalizarlos es tocar esos grupos como cualquier otro, y silenciarlos es vaciarlos.
Aquí está el cambio de 0.12 que más se nota sin que nadie lo anuncie: el cliente pide ahora los tokens del rango visible en lugar del documento completo. Antes, abrir un fichero de miles de líneas implicaba que el servidor calculara y transmitiera tokens para todo, la mayoría de los cuales no se iban a ver nunca. Con la petición por rango, el coste pasa a ser proporcional a la pantalla y no al fichero, y con él desaparece la razón principal por la que mucha gente los desactivaba. Si construyes resaltado propio sobre ellos, el evento LspTokenUpdate se dispara cuando un token visible llega, cambia o entra en pantalla por primera vez, y dentro de él la única operación soportada es pintar con vim.lsp.semantic_tokens.highlight_token.
Las viejas vim.lsp.semantic_tokens.start() y stop() están obsoletas: hoy hay una única vim.lsp.semantic_tokens.enable() con el mismo patrón que el resto de capacidades. Si un fragmento que encuentras por ahí llama a start, está escrito para 0.11 o anterior.
Code lens: acciones incrustadas en el código
La capacidad menos conocida y la más peculiar. Un code lens es una acción contextual que el servidor coloca sobre una línea concreta: ejecutar esta prueba, mostrar las implementaciones de esta interfaz, contar las referencias de este método, regenerar este fichero.
En 0.12 se reimplementó entera, y hay dos consecuencias que invalidan casi todo lo escrito antes. La primera es de presentación: ya no se dibuja como texto virtual pegado a la línea, sino como líneas virtuales propias sobre el código, de modo que un lens largo se lee entero y no compite por el espacio con el final de la sentencia. La segunda es de uso: desapareció el ciclo manual de refrescar. Antes había que llamar a una función de refresco y engancharla a una selección de eventos, con el dilema entre pedir demasiado y quedarse con acciones obsoletas apuntando a líneas que ya se movieron. Ahora se activa y ya está; el cliente se encarga de mantenerlo al día, incluso cuando es el servidor quien pide el refresco.
if client:supports_method("textDocument/codeLens") then
-- Una linea. Sin autocomandos de refresco, sin temporizadores.
vim.lsp.codelens.enable(true, { bufnr = buf })
end
Para ejecutarlo no necesitas mapear nada: grx es uno de los atajos globales que 0.12 crea al arrancar y llama a vim.lsp.codelens.run() sobre la línea del cursor. Si quieres una alternancia rápida para cuando las líneas virtuales estorben, el patrón es el mismo de siempre y va en el prefijo de líder.
vim.keymap.set("n", "<leader>lc", function()
vim.lsp.codelens.enable(not vim.lsp.codelens.is_enabled())
end, { desc = "alternar code lens" })
Lo que 0.12 añadió al catálogo
Tres capacidades nuevas merecen conocerse, porque resuelven cosas para las que hasta ahora se instalaban plugins. El completado en línea (textDocument/inlineCompletion) es la forma que el protocolo da a las sugerencias de bloque que aparecen en gris mientras escribes; se activa con vim.lsp.inline_completion.enable(), se acepta o se descarta con las funciones del módulo y se navega entre candidatos con su select. La edición enlazada (textDocument/linkedEditingRange) sincroniza regiones de texto que deben cambiar a la vez, que es el caso de la etiqueta de apertura y la de cierre en HTML o JSX: al renombrar una, la otra sigue. Y los enlaces de documento (textDocument/documentLink) hacen que gx abra lo que haya bajo el cursor cuando el servidor lo reconoce como enlace, por ejemplo la documentación de un paquete importado.
A esas se suma una que cambia un gesto de edición muy usado: textDocument/selectionRange. En Visual, an amplía la selección al siguiente nodo con sentido y in la reduce, con Treesitter si está activo y con el servidor de lenguaje si no. Es la selección incremental que durante años se montó a mano, ahora nativa y sin robar ninguna tecla.
El completado en línea es precisamente el sitio donde más gente se equivoca. Como la sugerencia aparece en Insert, la tentación de aceptarla con Tab o con Ctrl-Espacio es enorme, y ambas están ocupadas: la segunda dispara el autocompletado —nativo o de tu motor— y la primera es la tecla de navegación del menú de completado además de la indentación. Un atajo en Insert que no colisione suele estar en el territorio de Ctrl con una letra poco usada, y la decisión merece pensarse una vez y anotarse, porque el fallo se manifiesta como “el completado dejó de funcionar” y nadie relaciona ese síntoma con la línea que escribiste tres semanas antes.
flowchart TB s[Servidor] --> a[Inlay hints con tipos y nombres en linea] s --> b[Semantic tokens por viewport] s --> c[Code lens como lineas virtuales] s --> d[Formateo del documento] d --> e[Quien tiene autoridad para reescribir] e -->|servidor| f[Formateo LSP con limite de espera al guardar] e -->|herramienta del proyecto| g[Formateador externo con su config versionada] style e fill:#cba6f7,color:#11111b style g fill:#a6e3a1,color:#11111b
Formatear al guardar sin romper nada
Formatear al guardar es la automatización más satisfactoria y la que más daño hace cuando está mal montada. Tres precauciones la separan de ser un riesgo. La primera es el tiempo máximo de espera: la operación es síncrona por necesidad —hay que terminar antes de escribir el fichero— y sin límite un servidor atascado congela el editor indefinidamente. La segunda es elegir el cliente explícitamente en lugar de dejar que responda el primero que pueda, porque si hay dos formateadores adjuntos el resultado se vuelve no determinista. La tercera es una puerta de escape, porque tarde o temprano necesitarás guardar sin formatear, y no tener cómo hacerlo empuja a desactivarlo todo.
local grupo = vim.api.nvim_create_augroup("formato_al_guardar", { clear = true })
vim.api.nvim_create_autocmd("BufWritePre", {
group = grupo,
callback = function(args)
if vim.g.formato_desactivado or vim.b[args.buf].formato_desactivado then return end
vim.lsp.buf.format({
bufnr = args.buf,
timeout_ms = 2000,
filter = function(c) return c.name ~= "ts_ls" end,
})
end,
})
vim.api.nvim_create_user_command("FormatoAlternar", function()
vim.g.formato_desactivado = not vim.g.formato_desactivado
vim.notify("formato al guardar: " .. (vim.g.formato_desactivado and "off" or "on"))
end, { desc = "alterna el formateo automatico" })
Merece la pena entender por qué el formateo es distinto de las demás peticiones: no consulta, modifica. La respuesta del servidor no es información sino una lista de ediciones de texto que se aplican al buffer, y esa naturaleza destructiva es la que justifica toda la prudencia. Si el fichero cambia entre la petición y la aplicación, las ediciones apuntan a posiciones que ya no existen; por eso la operación es síncrona y por eso conviene que nada más escriba en el buffer mientras ocurre.
Convivir con formateadores externos
El servidor de lenguaje casi nunca es el formateador canónico de un proyecto real. El equipo ha elegido una herramienta, la ha configurado en un fichero versionado y la ejecuta en integración continua; si tu editor formatea con otro criterio, cada guardado introduce ruido en el control de versiones y cada revisión de código empieza discutiendo espacios. La regla es sencilla y no admite muchos matices: manda la herramienta del proyecto, y el editor se limita a ejecutarla con la misma configuración.
Eso deja al servidor fuera del formateo, y hay dos formas de conseguirlo. La primera es el filtro que acabas de ver, que decide en el momento de formatear. La segunda es más definitiva y se aplica al adjuntarse: apagar la capacidad de formateo del cliente, de modo que el servidor deje de anunciarla y nadie —ni tu configuración, ni un plugin, ni un atajo heredado— pueda invocarla por error. Para lo demás, un plugin que orqueste herramientas externas resuelve lo aburrido: encontrar el binario, pasarle el fichero, aplicar la salida como ediciones sobre el buffer en vez de reescribirlo entero, y no hacer nada si la herramienta falla.
-- El servidor deja de anunciar formateo; que lo haga la herramienta del proyecto
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(args)
local c = vim.lsp.get_client_by_id(args.data.client_id)
if c and c.name == "lua_ls" then
c.server_capabilities.documentFormattingProvider = false
end
end,
})
conform.nvim está montado en la lección 3.1; lo traemos aquí solo para ver cómo convive con el formateo del servidor.
Una última pieza cierra el nivel entero: las acciones de código al guardar. Muchos servidores exponen arreglos automáticos —ordenar importaciones, eliminar las no usadas, aplicar la corrección obvia de un aviso— como acciones con un identificador de tipo, y filtrar por ese identificador te permite pedir exactamente una sin abrir ningún menú. La función de conveniencia acepta el filtro y, si tras aplicarlo solo queda una acción candidata, la ejecuta sin preguntar.
vim.keymap.set("n", "<leader>lo", function()
vim.lsp.buf.code_action({
apply = true,
context = { only = { "source.organizeImports" }, diagnostics = {} },
})
end, { desc = "ordenar importaciones" })
Colgar esto de BufWritePre es más frágil de lo que parece, y conviene decirlo con claridad: la petición de acciones de código es asíncrona, así que la escritura puede completarse antes de que lleguen las ediciones. Hay dos salidas honestas. La primera es dejar de considerarlo automático y ejecutarlo como el gesto explícito que es, que es lo que hace el atajo de arriba. La segunda es delegar en el propio protocolo: muchos servidores aplican sus arreglos al guardar mediante textDocument/willSaveWaitUntil, que el cliente atiende por su cuenta y de forma sincronizada con la escritura. Antes de montar maquinaria propia, comprueba con supports_method si el servidor lo ofrece; si lo hace, tu mejor código es el que no escribes.
Pregunta antes de pedir
Toda capacidad extra se condiciona a lo que el servidor negoció. Invocar lo que no soporta produce fallos silenciosos difíciles de rastrear.
Alternable, no permanente
Las pistas en línea son valiosas a ratos y estorban en otros. Un atajo por buffer vale más que cualquier decisión global, y su sitio es el prefijo de líder.
Solo lo visible
Los tokens semánticos se piden por rango: el coste va con la pantalla, no con el tamaño del fichero. La razón clásica para desactivarlos ya no existe.
El code lens ya no se refresca a mano
Se activa con enable y se dibuja como líneas virtuales. Los autocomandos de refresco que circulan por ahí son de una versión anterior.
Formatear modifica
No es una consulta: aplica ediciones. De ahí el carácter síncrono, el límite de espera y la elección explícita de cliente.
Manda el repositorio
Si el proyecto versiona la configuración de un formateador, esa herramienta tiene autoridad y el servidor debe callarse.
Este nivel entero termina en un problema que parece de fontanería y es en realidad de gobierno. Tienes un servidor de lenguaje que puede formatear, un formateador externo que puede formatear, quizá un plugin que también puede, y el fichero es uno solo. La pregunta no es cuál es mejor —los tres producen salida válida— sino cuál tiene autoridad, y esa es una pregunta que ninguna herramienta puede responder por ti porque la respuesta no está en el editor sino en el equipo. El fichero de configuración del formateador que está versionado en el repositorio no es un detalle de implementación: es un acuerdo, y tu editor no tiene legitimidad para ignorarlo por muy capaz que sea el servidor que tienes adjunto. Cuando alguien monta un entorno donde dos herramientas pueden reescribir el mismo fichero y no decide explícitamente cuál lo hace, no ha creado flexibilidad, ha creado un no determinismo que se manifestará como diferencias que aparecen y desaparecen según quién guarde y en qué máquina. Y esto generaliza con una limpieza incómoda a casi todo lo que harás como ingeniero. Los sistemas no fallan porque falte capacidad; fallan porque sobra capacidad sin jerarquía. Dos servicios que pueden escribir el mismo registro, dos procesos que pueden reclamar el mismo recurso, dos configuraciones que pueden fijar el mismo valor, dos personas que pueden aprobar el mismo cambio. En todos esos casos la solución no es técnica sino declarativa: alguien tiene que decir quién manda, escribirlo en un sitio donde se lea y hacer que el resto se calle explícitamente. Apagar la capacidad de formateo de un servidor que sabe formatear perfectamente es exactamente eso, y por eso es una línea de código con mucho más significado del que aparenta. Configurar bien un editor, al final, se parece muchísimo a diseñar bien un sistema: casi todo el trabajo consiste en decidir quién es responsable de qué, y en asegurarse de que nadie más lo intente.
- Activa las pistas en línea condicionadas a la capacidad real y añade un atajo bajo
<leader>que las alterne solo en el buffer actual. Trabaja un día con ellas y otro sin ellas. - Compara el resaltado con tokens semánticos activados y desactivados sobre un fichero denso, y localiza al menos tres casos donde el servidor distingue lo que el análisis local no puede. Después desplázate rápido por el fichero y observa que el coste no crece con su tamaño.
- Activa
code lensen un lenguaje que los emita —Go, Rust y Java son buenos candidatos—, comprueba que aparecen como líneas virtuales y ejecuta uno congrxsin definir ningún atajo propio. - Implementa formateo al guardar con tiempo máximo, filtro de cliente y comando de alternancia. Provoca deliberadamente un servidor lento y comprueba que el editor no se cuelga.
- Elige un proyecto con formateador propio, apaga la capacidad de formateo del servidor y verifica que el resultado coincide byte a byte con lo que produce la integración continua.
- Revisa tus atajos actuales y busca colisiones: algo mapeado a
Ctrl-Espacio, a Tab, asoS, o agra secas. Mueve lo que encuentres a<leader>o a los corchetes y anota qué se rompía.