Grupos de resaltado: el átomo del color
Neovim no colorea caracteres, colorea nombres. Qué es un grupo de resaltado, qué atributos admite, por qué termguicolors lo cambia todo y cómo interrogar al editor con Inspect para saber quién pinta cada cosa.
Entre el texto de tu buffer y el color que enciende la terminal hay una indirección deliberada: el grupo de resaltado. Nadie declara que una palabra concreta sea morada; se declara que esa palabra es un Keyword y, por separado y en otro archivo, que los Keyword son morados. Esa separación entre clasificar y pintar es la arquitectura completa del color en Neovim, y entenderla convierte el tema de un accidente estético en un sistema que puedes razonar, depurar y reescribir.
- Definir qué es un grupo de resaltado y qué atributos admite.
- Entender
termguicolorsy la frontera entre los 16 colores y los 24 bits. - Usar
nvim_set_hlcomo API canónica y leer connvim_get_hl. - Descubrir con
Inspectqué grupo colorea exactamente cada carácter.
La indirección que lo explica todo
Un grupo de resaltado es un nombre con atributos visuales asociados: color de frente, color de fondo, negrita, cursiva, subrayado. Nada más. No sabe qué lenguaje muestras ni en qué línea estás; es una entrada en una tabla global que va del nombre a la apariencia.
El color ocurre entonces en dos fases estrictamente desacopladas:
- Clasificar. Alguien decide que cierto rango de bytes de la línea 8 pertenece al grupo
Keyword. Ese alguien puede ser el motorsyntaxclásico, Treesitter, el servidor de lenguaje mediante semantic tokens, un diagnóstico o cualquier plugin que coloque una extmark. - Pintar. Otro alguien —el esquema de color— decide qué aspecto tiene
Keyword, sin saber ni preguntar qué lenguaje se está mostrando.
La consecuencia es profunda y casi nunca se enuncia: un tema escrito hace siete años puede colorear correctamente un lenguaje publicado ayer, siempre que ese lenguaje clasifique sus tokens con nombres que el tema ya conoce. El vocabulario compartido de nombres es el contrato, y todo el ecosistema de temas descansa sobre él.
flowchart LR A[Bytes del buffer] --> B[Fase de clasificacion] B --> B1[Motor syntax clasico] B --> B2[Treesitter] B --> B3[Semantic tokens del LSP] B1 --> C[Nombre de grupo] B2 --> C B3 --> C C --> D[Fase de pintado: el tema] D --> E[Atributos fg bg negrita cursiva] E --> F[Secuencias de escape a la terminal] style C fill:#a6e3a1,color:#11111b style D fill:#89b4fa,color:#11111b
termguicolors: la frontera de los 24 bits
Cada grupo guarda en realidad dos definiciones paralelas. La familia cterm (ctermfg, ctermbg, cterm) usa índices en la paleta de la terminal, del 0 al 255: no eliges un color, eliges una casilla cuyo valor real lo decide el perfil de tu emulador. La familia gui (fg, bg, sp y los atributos) usa RGB de 24 bits: eliges el color exacto.
La opción termguicolors es el interruptor que decide cuál de las dos familias se emite:
vim.o.termguicolors = true
- Apagado: solo cuentan los atributos
cterm. Unfg = "#89b4fa"se ignora por completo y ves los dieciséis colores de tu terminal. - Encendido: Neovim emite secuencias SGR con el triplete RGB literal y el tema se ve tal como su autor lo diseñó.
Desde Neovim 0.10 el editor intenta detectar el soporte de la terminal y activarlo solo. Aun así conviene saber diagnosticarlo: si tu tema de mil colores se ve como ocho colores feos, el culpable casi nunca es el tema. Comprueba que la variable de entorno COLORTERM valga truecolor y, si vives dentro de un multiplexor, que este anuncie la capacidad de color verdadero en su configuración de terminfo.
Hay una asimetría que conviene interiorizar. Con la familia cterm no controlas el color: el índice 4 es “el cuarto color de la paleta del usuario”, y qué azul sea eso lo decide el perfil de la terminal. Eso tiene una virtud —el editor se integra con el resto de la consola— y un defecto fatal para un tema, que es la imposibilidad de garantizar contraste. Con la familia gui ocurre lo contrario: controlas el color exacto y, por tanto, eres el único responsable de que se lea.
Un tema serio define ambas familias. La segunda para verse como su autor quiso, y la primera para degradar con dignidad en la consola de un servidor remoto donde nadie ha configurado nada.
nvim_set_hl: la API canónica
La forma canónica de definir un grupo en Neovim moderno es una función Lua que recibe una tabla de atributos:
vim.api.nvim_set_hl(0, "Keyword", {
fg = "#cba6f7",
bold = true,
italic = true,
})
El primer argumento es el namespace. El 0 significa el espacio global, el que afecta a todo el editor; los namespaces creados con nvim_create_namespace permiten definiciones con alcance local a una ventana, y los verás en la lección 15.5.
La forma clásica, el comando :highlight, sigue existiendo y la verás en configuraciones heredadas, con sus pares de clave y valor separados por espacios dentro de una sola cadena:
highlight Keyword guifg=#cba6f7 gui=bold,italic ctermfg=141
Ambas hacen lo mismo, pero la versión Lua es preferible por razones que no son de estilo: recibe datos estructurados en lugar de una cadena que hay que construir concatenando, permite calcular atributos con lógica normal y falla de forma explícita cuando le pasas una clave inexistente.
Hay una semántica que rompe a mucha gente: nvim_set_hl reemplaza, no fusiona. Si un grupo tenía fondo y tú llamas a la función pasando solo fg, el fondo desaparece. Para retocar un único atributo hay que leer, modificar y volver a escribir:
local hl = vim.api.nvim_get_hl(0, { name = "Comment" })
hl.italic = true
vim.api.nvim_set_hl(0, "Comment", hl)
Color
Los campos fg, bg y sp aceptan una cadena hexadecimal o un entero. El tercero, sp, es el color del subrayado y solo lo pintan las terminales que soportan subrayados con color propio.
Énfasis
Booleanos bold, italic, reverse, standout, strikethrough y nocombine. El reverse intercambia frente y fondo y es la forma más portable de destacar algo sin elegir color.
Subrayados
Además de underline existen undercurl, underdouble, underdotted y underdashed. Son la señal preferida para diagnósticos porque no compiten con el color semántico del token.
Compatibilidad
Los campos ctermfg, ctermbg y cterm viven en la misma tabla y solo se usan con termguicolors apagado. Definirlos es lo que hace que tu tema degrade con dignidad en una consola pobre.
Inspect: interrogar al pixel
La pregunta operativa nunca es “qué grupos existen”, sino “qué grupo está coloreando este carácter”. Neovim responde con un comando:
:Inspect
Bajo el cursor te devuelve la pila completa: las capturas de Treesitter que cubren esa posición, los semantic tokens del servidor de lenguaje, el grupo de syntax clásico si lo hubiera y las extmarks activas. Su equivalente programático es vim.inspect_pos, útil cuando quieres el dato dentro de un script en vez de en una ventana flotante.
Cuando ya tienes el nombre, el siguiente paso es ver su definición efectiva. El comando :hi Keyword la imprime en formato clásico, y nvim_get_hl la devuelve como tabla:
vim.print(vim.api.nvim_get_hl(0, { name = "Keyword" }))
Este bucle de tres pasos —posición, nombre, definición— es el método universal para depurar color. No adivines qué grupo debería colorear algo: pregúntaselo al editor.
Dos comandos completan el arsenal. El primero, :hi sin argumentos, vuelca la tabla completa de grupos definidos; es abrumador, pero resulta insustituible cuando buscas el nombre de algo cuya posición no puedes situar bajo el cursor, como un elemento del menú de completado. El segundo, :InspectTree, abre el árbol sintáctico en una ventana lateral y sincroniza el cursor con él, lo que convierte la pregunta “por qué esto se clasifica así” en algo observable en vez de deducible.
Un último detalle que ahorra confusión: un mismo carácter puede estar cubierto por varios grupos a la vez, y Inspect te los muestra todos ordenados. Lo que ves en pantalla es el resultado de una resolución de prioridades entre capas, no la opinión de un único responsable. Esa pila es el asunto de la lección 15.3, pero conviene salir de aquí sabiendo que existe: buena parte de los “el tema no me aplica este color” son en realidad “otra capa lo está tapando”.
Lo que de verdad está ocurriendo aquí es una inversión de dependencias clásica, resuelta con un vocabulario compartido. El resaltador de un lenguaje no depende de tu tema y tu tema no depende de ningún lenguaje: ambos dependen de un conjunto de nombres abstractos. Ese es el motivo por el que puedes instalar un parser nuevo y verlo coloreado al instante, y también el motivo por el que la única decisión verdaderamente irreversible al escribir un tema no es la paleta —los hexadecimales se cambian en un minuto— sino qué distinciones semánticas decides hacer visibles. Un tema que pinta igual una constante y una variable ha tomado una decisión epistemológica sobre tu código, no estética. Cuando entiendes que el color es una proyección de una taxonomía sobre un espacio perceptual, dejas de coleccionar temas y empiezas a diseñar el tuyo: primero decides qué quieres poder distinguir de un vistazo, y solo después buscas colores que lo hagan posible.
- Comprueba el estado de
termguicolorsy, si estaba apagado, enciéndelo y observa el cambio. - Abre un archivo de tu lenguaje habitual, sitúa el cursor sobre una palabra clave y ejecuta
Inspect. Anota la pila completa que devuelve. - Imprime la definición efectiva de ese grupo con
nvim_get_hly localiza sus atributos. - Redefine el grupo con
nvim_set_hlpasando solofg. Observa qué otros atributos has destruido sin querer y explica por qué. - Repara el destrozo con el patrón leer, modificar y volver a escribir, y deja el resultado en tu configuración.