wandres.dev
EXTMARKS · anotar el texto

Virtual text: escribir sin escribir en el buffer

El texto virtual se ve pero no existe: no se guarda, no cuenta para las columnas y no lo toca ningun comando de edicion. Trozos con virt_text, las cuatro posiciones eol overlay inline y right_align, lineas virtuales completas con virt_lines, y el coste real de dibujar informacion que el usuario no puede seleccionar.

⏱ 20 min

El texto virtual es la ilusión mejor construida de Neovim: caracteres que aparecen en pantalla, se alinean con tu código y se mueven con él, pero que no están en el buffer. No se guardan al escribir el archivo, no cuentan para la posición del cursor, no los copia yank y ningún comando de edición puede tocarlos. Es la forma en que el editor te dice cosas —el valor de una variable, el error del compilador, el autor de la línea— sin contaminar el documento que estás editando. Esta lección va de las cuatro maneras de colocarlo y de la disciplina que exige, porque la ilusión se rompe con facilidad.

🎯 Al terminar esta lección sabrás
  • Dibujar trozos de texto con virt_text y asignarles grupo de resaltado.
  • Distinguir las cuatro posiciones: eol, overlay, right_align e inline.
  • Insertar bloques completos bajo o sobre una línea con virt_lines.
  • Reconocer cuándo el texto virtual desalinea el buffer y qué opciones lo mitigan.

Trozos al final de la línea

La forma canónica es una extmark con la opción virt_text, que recibe una lista de trozos: cada trozo es una pareja formada por la cadena a mostrar y el grupo de resaltado con el que pintarla. Varios trozos seguidos se concatenan en pantalla, lo que te permite colorear partes distintas del mismo anuncio.

local ns = vim.api.nvim_create_namespace("demo.virt")

vim.api.nvim_buf_set_extmark(0, ns, 4, 0, {
  virt_text = {
    { "  ", "Comment" },
    { "18 ms", "DiagnosticHint" },
  },
  virt_text_pos = "eol",
})

El grupo de resaltado puede omitirse dejando solo la cadena, en cuyo caso el trozo hereda el color normal del texto; y puede darse como lista de varios grupos, que se aplican combinados de menor a mayor precedencia. Dividir un mensaje en trozos no cuesta nada en rendimiento y compra mucha claridad: un icono en un color, la etiqueta en gris tenue y el dato en el color de la severidad se leen de un vistazo, mientras que la misma frase en un solo tono obliga a leerla entera.

Con virt_text_pos en eol —el valor por defecto— el texto aparece tras el final real de la línea, separado por el hueco que marque la opción virt_text_win_col o simplemente pegado al último carácter. Es la posición más segura: no tapa nada, no desplaza nada y el usuario entiende de inmediato que lo que ve a la derecha es un comentario del editor y no parte del archivo. Es la que usan los diagnósticos por defecto y la que deberías elegir si dudas.

⚠️
Los saltos de línea están prohibidos

Un trozo de virt_text no puede contener un carácter de nueva línea. Si intentas dibujar un mensaje multilínea concatenando saltos, Neovim rechaza la llamada. Para varias líneas existe virt_lines, que es otro mecanismo con otras reglas; y para un mensaje largo de una sola línea conviene recortarlo tú, porque el texto virtual no se parte solo al llegar al borde de la ventana.

Superponer y entrometerse

Las otras tres posiciones cambian la relación con el texto real, y cada una compra una ventaja pagando un precio distinto.

Posición Dónde dibuja Qué le hace al texto real
eol tras el final de la línea nada
right_align pegado al borde derecho de la ventana nada
overlay encima, desde la columna de la marca lo tapa mientras dure el trozo
inline intercalado en la columna de la marca lo empuja hacia la derecha

La distinción entre las dos primeras filas y las dos últimas es la que de verdad importa, y se puede formular así: eol y right_align escriben en espacio que estaba vacío, mientras que overlay e inline escriben en espacio ocupado. Solo las segundas pueden hacer que el usuario lea mal su propio código.

overlay sustituye visualmente los caracteres que quedan debajo: el texto sigue ahí y el cursor puede recorrerlo, pero no lo ves. Es lo correcto para sustituciones conceptuales —mostrar un color en vez de su código hexadecimal, un icono en lugar de una palabra clave— y es peligroso para cualquier otra cosa, porque el usuario puede quedarse mirando una línea que no dice lo que dice.

right_align empuja el texto al extremo derecho de la ventana, lo que da columnas visualmente ordenadas cuando anotas muchas líneas seguidas con datos comparables: tiempos, tamaños, contadores. Su desventaja es que la anotación se despega del código que describe, así que funciona bien con una sola familia de datos y fatal con varias mezcladas.

inline es la incorporación moderna y la más ambiciosa: el texto virtual se abre hueco de verdad entre los caracteres reales y desplaza el resto de la línea hacia la derecha. Es lo que hace posible que una pista de tipo aparezca justo detrás del nombre de la variable en vez de al final de la línea. Su coste es que rompe la correspondencia entre columna del buffer y columna de pantalla: dos líneas con el mismo contenido pueden dibujarse desalineadas si una lleva anotación en línea y la otra no, y el bloque visual vertical deja de ser un rectángulo predecible.

flowchart TD
m[Extmark en fila y columna] --> p{virt_text_pos}
p -->|eol| e[Tras el fin de linea sin tocar nada]
p -->|right_align| r[Pegado al borde derecho de la ventana]
p -->|overlay| o[Encima del texto tapando caracteres]
p -->|inline| i[Intercalado empujando el resto a la derecha]
style e fill:#a6e3a1,color:#11111b
style o fill:#fab387,color:#11111b
style i fill:#f38ba8,color:#11111b
💡
hl_mode decide cómo se mezclan los colores

Cuando el texto virtual cae sobre texto ya resaltado, hl_mode gobierna el resultado: combine fusiona tu grupo con el de debajo —el valor por defecto y casi siempre el acertado—, replace impone el tuyo entero y blend mezcla solo el fondo, que es lo que quieres para sombreados sutiles que no deben alterar el color de la letra.

Líneas virtuales completas

Cuando el mensaje no cabe a la derecha, la respuesta es virt_lines: una lista de líneas enteras, cada una a su vez una lista de trozos, que Neovim inserta entre las líneas reales del buffer. Aparecen debajo de la línea marcada, o encima si activas virt_lines_above, y desplazan verticalmente todo lo que venga después sin que el archivo cambie ni una coma.

vim.api.nvim_buf_set_extmark(0, ns, 12, 0, {
  virt_lines = {
    { { "  error: no se puede prestar dos veces", "DiagnosticError" } },
    { { "  ayuda: clona el valor o reordena el prestamo", "DiagnosticHint" } },
  },
  virt_lines_above = false,
})

La estructura anidada confunde al principio: virt_lines es una lista de líneas, y cada línea es a su vez una lista de trozos. Un descuido habitual es pasar directamente la lista de trozos, lo que hace que Neovim interprete cada trozo como una línea entera y el mensaje salga desmenuzado en vertical. Si ves una palabra por renglón, te falta un nivel de anidamiento.

Este es el mecanismo con el que un plugin muestra un error del compilador completo bajo la línea culpable, o un diff en línea, o el resultado de evaluar un bloque. Su virtud es que el espacio es tuyo y no compites con nada; su peligro es que desplaza el resto del archivo, así que anotar cuarenta líneas a la vez convierte la ventana en una lista de mensajes con algo de código intercalado. La regla práctica del ecosistema es reservar virt_lines para lo que el usuario ha pedido explícitamente o para lo que está bajo el cursor, y dejar eol para lo que se muestra siempre.

Lo que la ilusión no puede hacer

Conviene enumerar las fronteras, porque casi todas las preguntas de quien empieza son variaciones de la misma: ¿y si quiero que el usuario pueda hacer algo con el texto virtual? No puede, por construcción.

Pregunta Respuesta
¿Se guarda al escribir el archivo? no, el buffer no cambia
¿Lo copia yank o lo ve una búsqueda? no, es invisible para toda la edición
¿Puede el cursor posarse encima? no, el cursor solo recorre texto real
¿Se ajusta solo al ancho de la ventana? no, si no cabe se recorta en pantalla
¿Puede llevar un enlace o reaccionar al ratón? no directamente, hace falta lógica aparte

Todas esas negativas se derivan de una sola premisa, y por eso conviene memorizar la premisa en lugar de la lista: el texto virtual no forma parte del buffer, y en Vim todo lo que se puede editar, buscar, copiar o recorrer se define sobre el buffer. No hay excepciones que aprender porque no hay reglas nuevas: hay una frontera y todo lo virtual está del otro lado.

De esa lista, la que más decisiones de diseño provoca es la del cursor. Como no puedes posarte sobre una anotación, cualquier interacción con ella —expandir un mensaje, saltar a la definición que anuncia— tiene que colgarse de la posición real más cercana: la línea marcada, el rango subyacente, o un atajo que consulte con nvim_buf_get_extmarks qué hay anotado bajo el cursor. Es el mismo patrón que usan los diagnósticos cuando abres el mensaje completo en una ventana flotante: el texto virtual es solo el cartel, y la lógica cuelga de la posición del texto verdadero.

La segunda limitación con consecuencias prácticas es el recorte. Un mensaje de compilador de doscientos caracteres colocado en eol no se parte en varias líneas: se dibuja hasta donde llega la ventana y el resto simplemente no está. Por eso los plugins serios truncan el texto ellos mismos —a un número de columnas calculado desde el ancho de la ventana menos la longitud real de la línea— y ofrecen la versión completa por otro canal, típicamente una ventana flotante bajo demanda.

local ancho_ventana = vim.api.nvim_win_get_width(0)
local ancho_linea = vim.fn.strdisplaywidth(vim.api.nvim_get_current_line())
local hueco = math.max(10, ancho_ventana - ancho_linea - 6)
local mensaje = "el tipo esperado era numero y se recibio cadena"

if vim.fn.strdisplaywidth(mensaje) > hueco then
  mensaje = vim.fn.strcharpart(mensaje, 0, hueco - 1) .. "…"
end
Una capa de presentación sobre un documento inmutable

Lo que el texto virtual introduce en un editor de texto es algo que suena inofensivo y no lo es: la separación entre el documento y su representación. Durante décadas la premisa de Vim fue que lo que ves es el archivo, carácter por carácter, y de esa premisa salían casi todas sus virtudes: el cursor tenía una posición y era la misma en el buffer y en la pantalla, un movimiento vertical caía en la columna que esperabas, y cualquier operación de edición podía razonarse sobre el texto sin pensar en el dibujo. El texto virtual rompe esa identidad a cambio de una capacidad enorme, la de anotar sin modificar, y el precio es que ahora existen dos sistemas de coordenadas que hay que traducir el uno al otro. Ahí es donde nace la distinción, que parece burocrática y es fundamental, entre columna del buffer, columna de pantalla y celda visual: en cuanto una marca inline empuja caracteres, la columna 20 del texto ya no está en la celda 20 de la ventana. Neovim resuelve el problema con honestidad, exponiendo funciones distintas para cada sistema de coordenadas en lugar de fingir que sigue habiendo uno solo, pero la carga de saber en cuál estás recae sobre quien escribe el plugin. Y de ahí sale el criterio de diseño que separa las anotaciones bien hechas de las que estorban: cada carácter virtual que dibujas es una promesa de que la información que aporta vale más que la certidumbre visual que quita. Al final de la línea esa promesa es casi gratis, porque a la derecha no había nada. Superpuesto o intercalado es cara, porque el usuario deja de poder confiar en que la pantalla sea el archivo. Los plugins que envejecen bien son los que cobran esa promesa con parsimonia: mucho eol discreto, inline solo donde el ahorro cognitivo es evidente, y virt_lines reservado para cuando el usuario ha pedido ver el detalle.

📝
Lo esencial del texto virtual

virt_text dibuja trozos —texto más grupo de resaltado— que no existen en el buffer; virt_text_pos elige entre eol, right_align, overlay e inline, y solo las dos últimas alteran la correspondencia con el texto real. Los saltos de línea no están permitidos en un trozo: para varias líneas se usa virt_lines, que inserta bloques enteros bajo o sobre la línea marcada y desplaza el resto de la ventana. hl_mode decide cómo se mezcla tu color con el de debajo. Nada de esto se guarda, se copia ni se puede seleccionar.

⚔️ Las cuatro posiciones en una sesión
  1. Coloca una extmark con virt_text en eol que muestre el número de caracteres de la línea, y comprueba con yank y p que el texto virtual no se copia.
  2. Repite la misma anotación con overlay en la columna 4 y describe qué parte del código real ha dejado de verse.
  3. Prueba inline en mitad de una línea y compara visualmente esa línea con la de arriba: identifica el desfase entre columna del buffer y columna de pantalla.
  4. Añade una marca con dos virt_lines bajo la línea del cursor y luego con virt_lines_above a verdadero; razona en qué casos conviene cada una.
  5. Intenta meter un salto de línea dentro de un trozo de virt_text, lee el error exacto que devuelve la API y explica por qué la restricción es necesaria.