Enlaces entre grupos: la herencia del color
El campo link convierte la tabla de grupos en un grafo. La jerarquía canónica de Neovim, el modificador default que cede ante el usuario y la razón estructural por la que un tema puede colorear plugins que nunca ha visto.
Si cada grupo de resaltado tuviera que declarar sus propios colores, un tema decente necesitaría definir varios miles de entradas y quedaría obsoleto con cada plugin nuevo. No ocurre así, y la razón cabe en una palabra: link. Un grupo puede no tener color propio y limitarse a apuntar a otro, de modo que la tabla plana de nombres se convierte en un grafo dirigido donde unas dos docenas de nodos raíz sostienen a todos los demás.
- Crear enlaces con
linky con el comandohi link. - Reconocer la jerarquía canónica de grupos base de Neovim.
- Distinguir un enlace normal de uno marcado con
default. - Resolver cadenas de enlaces para depurar de dónde sale un color.
Un grupo que apunta a otro
Un enlace declara que un grupo no tiene apariencia propia y que debe mostrarse exactamente como otro. En Lua es un campo más de la tabla, y es excluyente: si pasas link, el resto de atributos se ignora.
vim.api.nvim_set_hl(0, "MiGrupoDeTitulo", { link = "Title" })
En Vimscript el mismo gesto es el comando hi link, que todavía verás por todas partes en código heredado:
highlight link luaFunction Function
Lo importante es que el enlace se resuelve en el momento de dibujar, no en el momento de declararlo. Si más tarde alguien cambia Title, tu MiGrupoDeTitulo cambia con él sin que nadie lo notifique. Esa resolución tardía es exactamente lo que permite cambiar de tema en caliente: los cientos de grupos enlazados siguen apuntando a los mismos nodos raíz, y basta con repintar esos nodos.
Los enlaces se encadenan. Un grupo puede apuntar a otro que a su vez apunta a un tercero, y solo el último de la cadena tiene atributos reales. Al depurar, esto significa que el color que ves puede estar definido a tres saltos de distancia del nombre que Inspect te mostró.
Romper un enlace requiere un gesto explícito. Asignar atributos normales lo sustituye, y pasar una tabla vacía deja el grupo limpio, sin color propio ni destino, que es la forma correcta de neutralizar algo que no quieres ver:
-- desconectar el grupo y dejarlo transparente
vim.api.nvim_set_hl(0, "MiGrupoDeTitulo", {})
Esa distinción entre sin definir y definido como vacío no es una sutileza: la primera situación deja que otros hablen, la segunda los silencia. Es exactamente la palanca con la que se apagan capas enteras de resaltado sin desactivar el motor que las produce.
La jerarquía canónica
Neovim documenta un conjunto reducido de grupos base que actúan como raíces del grafo. Se agrupan en familias, y cada familia tiene un nodo principal y varios especializados que, por defecto, enlazan hacia él:
Comment— sin subfamilia, es su propia raíz.Constant— conString,Character,Number,BooleanyFloatcolgando de ella.Identifier— conFunction.Statement— conConditional,Repeat,Label,Operator,KeywordyException.PreProc— conInclude,Define,MacroyPreCondit.Type— conStorageClass,StructureyTypedef.Special— conSpecialChar,Tag,Delimiter,SpecialCommentyDebug.Underlined,Error,TodoyIgnorecomo raíces sueltas.
A esa taxonomía sintáctica se suma una segunda familia, la de la interfaz: Normal, CursorLine, LineNr, Visual, Search, Pmenu, StatusLine, WinSeparator y compañía. No describen código sino cromo del editor, y no se enlazan a las anteriores porque su función perceptual es distinta.
Un tema mínimo pero completo consiste, literalmente, en dar atributos a esas dos listas. Todo lo demás del ecosistema se cuelga por enlace.
Raíces
Las dos docenas de nombres que Neovim documenta como base. Son los únicos que un tema tiene la obligación de colorear, y su elección de tono decide el aspecto de todo lo demás.
Grupos por lenguaje
Nombres como luaFunction que emite el motor clásico. Nacen ya enlazados desde el propio archivo de sintaxis y rara vez merecen atención directa.
Capturas
Los grupos con arroba que produce Treesitter. Traen enlaces por defecto hacia las raíces, de modo que funcionan incluso en temas anteriores a su existencia.
Grupos de plugin
Nombres propios de terceros. Cuando su autor hace bien el trabajo, entran al grafo por enlace marcado como valor por defecto y desaparecen dentro de tu tema.
flowchart TD A[Grupo de un plugin: MiPluginTitulo] -->|link| B[Grupo de lenguaje: luaFunction] B -->|link| C[Grupo especializado: Function] C -->|link| D[Grupo raiz: Identifier] D --> E[Atributos reales definidos por el tema] F[Captura de Treesitter] -->|link| C style D fill:#f9e2af,color:#11111b style E fill:#a6e3a1,color:#11111b
default: quién gana el conflicto
Aquí aparece la pieza que hace convivir a tres actores con opiniones —Neovim, el plugin y tú— sin que se pisen. Un enlace puede marcarse como valor por defecto:
vim.api.nvim_set_hl(0, "MiPluginTitulo", { link = "Title", default = true })
La semántica es sencilla y muy poderosa: un enlace default solo se aplica si el grupo no estaba definido ya. Si el usuario, o su tema, había dicho algo sobre ese nombre, la declaración por defecto se descarta en silencio. En Vimscript es el familiar hi default link, y el sufijo de admiración en hi! link es lo contrario: fuerza el enlace pisando lo que hubiera.
La regla de convivencia que se deriva es nítida. Un plugin declara sus grupos con default, porque su trabajo es proponer un aspecto razonable sin secuestrar la decisión final. Un tema declara sin default, porque su trabajo es precisamente imponer una apariencia. Y tú, en tu configuración, declaras después de que ambos hayan hablado.
Como default depende del estado previo, todo el sistema es sensible al orden. Si tu configuración define un grupo antes de que se cargue el tema, el tema lo pisará; si lo define después, ganarás tú. Por eso los retoques personales de color deben ejecutarse tras aplicar el esquema, y no en cualquier punto del arranque.
Por qué los temas funcionan así
Reunidas las tres piezas, el resultado es un sistema con una propiedad que merece nombrarse: extensibilidad sin coordinación. El autor de un plugin publicado hoy y el autor de un tema publicado hace años nunca se han hablado, no comparten repositorio ni versión, y sin embargo el plugin se ve integrado en el tema desde el primer segundo. Basta con que el plugin enlace sus nombres nuevos a nodos antiguos del grafo.
Esa propiedad tiene un precio, y es justo reconocerlo. El grafo solo funciona mientras el vocabulario de raíces permanezca estable, lo que congela una taxonomía diseñada hace décadas para lenguajes con macros y preprocesador. Que hoy sigamos colgando conceptos modernos de un nodo llamado PreProc es un fósil visible, y la respuesta del ecosistema no ha sido renombrar nada —eso rompería miles de temas— sino añadir una capa nueva de nombres encima que enlaza hacia abajo, que es exactamente el papel de las capturas de Treesitter de la lección siguiente.
Depurar en ese mundo consiste en recorrer la cadena. Neovim devuelve por defecto el enlace declarado, pero también sabe resolverlo hasta el final:
-- lo que el grupo declara: probablemente solo un link
vim.print(vim.api.nvim_get_hl(0, { name = "luaFunction" }))
-- lo que el grupo acaba mostrando, con la cadena ya resuelta
vim.print(vim.api.nvim_get_hl(0, { name = "luaFunction", link = false }))
Cuando algo se ve del color equivocado, la pregunta correcta rara vez es “qué color le puso el tema a este grupo”. Casi siempre es “a qué raíz acaba llegando este grupo”, y la respuesta está a una llamada de distancia.
Hay un patrón de personalización que se deriva directamente de todo esto y que conviene tener en el dedo. Para cambiar el aspecto de una familia entera, redefine su raíz. Para cambiar solo un miembro, rompe su enlace dándole atributos propios. Y para que dos cosas que hoy se ven distintas pasen a verse iguales, enlaza una a la otra en lugar de copiar sus colores:
-- unificar en vez de duplicar: si mañana cambia Function, esto cambia con él
vim.api.nvim_set_hl(0, "@function.builtin", { link = "Function" })
Copiar valores hexadecimales de un grupo a otro parece equivalente y no lo es: crea una réplica que envejece por su cuenta y que ningún cambio de tema mantendrá sincronizada. El enlace es la única forma de expresar “esto es lo mismo que aquello” de manera que siga siendo cierta después.
Un tema no es una lista de colores: es una poda del grafo de herencia. Cuando decides que String deje de heredar de Constant y tenga tono propio, no estás eligiendo un verde, estás afirmando que la distinción entre una cadena y un número merece un canal perceptual dedicado en el presupuesto limitadísimo de la visión periférica. Y ese presupuesto es real: la discriminación fiable de colores en visión no foveal se agota alrededor de media docena de tonos, muy por debajo de los cientos de nombres que el grafo contiene. De ahí que los temas maduros converjan todos hacia la misma disciplina, la de definir pocas raíces con contraste amplio y dejar que el resto herede. Los temas que fracasan suelen ser los que dan color propio a demasiados nodos: técnicamente correctos, perceptualmente ruidosos, y además condenados a envejecer, porque cada grupo nuevo del ecosistema es una entrada que olvidaron cubrir. Menos nodos coloreados no es pereza; es la única estrategia que escala.
- Con
Inspectsobre una llamada a función, obtén el nombre del grupo que la colorea. - Consulta ese grupo con
nvim_get_hlsin resolver y comprueba si lo que ves es un enlace o atributos reales. - Repite la consulta resolviendo la cadena y anota los saltos hasta llegar a la raíz.
- Redefine esa raíz con un color chillón y observa cuántas cosas distintas cambian a la vez.
- Declara un grupo propio enlazado con
defaultaTitle. Después defínelo otra vez sindefaulty comprueba cuál de las dos declaraciones sobrevive según el orden.