Qué es una extmark: la posición que se mueve sola
Una extmark es una posición dentro del buffer que Neovim reubica por ti cada vez que el texto cambia. Coordenadas base cero, rangos con end_row y end_col, gravedad de los extremos, el arbol de marcadores que lo hace barato, y por que este modelo sustituyo a los signos y a las marcas de toda la vida.
Una extmark —abreviatura de extended mark— es una posición dentro de un buffer que Neovim mantiene actualizada por ti. La colocas en la fila 12, columna 4; alguien inserta tres líneas más arriba y la marca pasa sola a la fila 15 sin que nadie tenga que avisarla. Esa única propiedad, la de sobrevivir a la edición sin perder el sitio, es lo que convirtió a las extmarks en el sustrato común de casi todo lo que hoy dibuja información sobre el texto: diagnósticos, resaltado incremental de Treesitter, indicadores de git, pistas de tipo en línea. Entender su modelo es entender cómo Neovim anota el texto sin tocarlo.
- Colocar una extmark con
nvim_buf_set_extmarky leer sus coordenadas base cero. - Explicar por qué una marca sigue al texto cuando el buffer cambia bajo ella.
- Definir rangos con
end_rowyend_coly controlar la gravedad de los extremos. - Consultar, mover y borrar marcas por identificador.
La posición que sobrevive a la edición
La primitiva es una sola función: vim.api.nvim_buf_set_extmark. Recibe el buffer, un namespace, una fila, una columna y una tabla de opciones, y devuelve un identificador numérico. Ese identificador es lo importante: no guardas una posición, guardas un nombre para una posición, y le preguntas a Neovim dónde está esa posición ahora.
local ns = vim.api.nvim_create_namespace("demo.extmarks")
local bufnr = 0
-- Fila 11 (la duodecima linea), columna 4. Ambas base cero.
local id = vim.api.nvim_buf_set_extmark(bufnr, ns, 11, 4, {})
-- Mas tarde: devuelve la posicion VIGENTE, no la original.
local pos = vim.api.nvim_buf_get_extmark_by_id(bufnr, ns, id, {})
print(pos[1], pos[2])
Dos detalles muerden a todo el mundo la primera vez. Las filas son base cero, al contrario que las líneas del lenguaje de comandos de Vim, que empiezan en 1: la fila 11 de la API es la línea 12 que ves en el número lateral. Y las columnas se cuentan en bytes, no en caracteres ni en celdas de pantalla: en un archivo UTF-8 con acentos o emojis, la columna 4 puede caer a mitad de un carácter si haces la aritmética a ojo. Ese doble desfase es la fuente número uno de errores en plugins que empiezan con extmarks.
Cuando conviertas desde una posición del cursor obtenida con nvim_win_get_cursor, recuerda que esa función devuelve la fila en base uno y la columna en base cero. Es la única API de Neovim con esa mezcla, y es exactamente la que usarás para colocar marcas donde está el usuario: resta 1 a la fila y deja la columna tal cual.
Que la marca conserve su identidad no significa que conserve su posición: significa exactamente lo contrario. Guardar el par de números que te devolvió la consulta y reutilizarlo diez pulsaciones después es volver a cometer el error que las extmarks vinieron a eliminar. La regla es simple y no admite excepciones: guarda el id, pregunta la posición cada vez que la necesites. Consultar es barato; suponer es incorrecto.
Lo que ocurre por dentro cuando editas es la parte elegante. Neovim no guarda las marcas en una lista que recorre y corrige entera en cada pulsación: las guarda en un árbol de marcadores con coordenadas relativas entre nodos. Al insertar o borrar texto solo hay que ajustar el camino del árbol afectado por el cambio, lo que hace que el coste sea logarítmico en el número de marcas y no lineal. Por eso un buffer puede sostener decenas de miles de extmarks —una por cada token resaltado por Treesitter, por ejemplo— sin que escribir se vuelva pastoso.
flowchart TD a[Insertas tres lineas por encima de la marca] --> b[El arbol de marcadores ajusta solo la rama afectada] b --> c[La extmark conserva su id y cambia de fila] c --> d[Tu plugin la consulta y sigue apuntando al mismo texto] style b fill:#89b4fa,color:#11111b style c fill:#a6e3a1,color:#11111b
Rangos y gravedad
Una extmark no tiene por qué ser un punto. Si le das end_row y end_col pasa a cubrir un rango, y ese rango también se estira, se encoge y se desplaza con la edición. Un rango es lo que necesitas para resaltar una función entera, para marcar el ámbito de un error del compilador o para recordar dónde estaba un bloque que el usuario está reescribiendo.
Cuando el texto se inserta justo en el borde de una marca aparece una pregunta sutil: ¿la marca se queda antes o después de lo insertado? Eso se llama gravedad. Con right_gravity a verdadero —el valor por defecto— la marca se pega al texto de la derecha y lo insertado queda a su izquierda; con falso, ocurre lo contrario. Para el extremo final hay un interruptor propio, end_right_gravity, cuyo valor por defecto es falso. Esa combinación es deliberada: hace que el texto escrito justo dentro del rango se incluya en él, y que el escrito justo fuera quede fuera.
-- Un rango que cubre de la fila 3 col 0 a la fila 5 col 10
vim.api.nvim_buf_set_extmark(0, ns, 3, 0, {
end_row = 5,
end_col = 10,
right_gravity = true, -- el inicio se pega al texto de su derecha
end_right_gravity = false, -- el final se pega al texto de su izquierda
hl_group = "Visual",
})
| Opción | Qué controla |
|---|---|
id |
reutiliza un identificador concreto en vez de pedir uno nuevo |
end_row / end_col |
convierten la marca en un rango |
right_gravity |
a qué lado se pega el inicio ante una inserción en el borde |
end_right_gravity |
lo mismo para el extremo final |
invalidate |
borra la marca sola cuando se elimina el texto que cubre |
strict |
si es falso, tolera coordenadas fuera del buffer en vez de fallar |
El valor por defecto de strict es verdadero, lo que significa que colocar una marca fuera de los límites del buffer lanza un error en vez de recortar en silencio. Ponerlo a falso es cómodo mientras exploras, pero en producción suele ser preferible que el error salte: una coordenada fuera de rango casi siempre delata un desfase de base cero o un cálculo hecho sobre una versión anterior del buffer, y enterarte tarde de eso cuesta mucho más que un fallo ruidoso.
Pasar un id explícito es el patrón de actualización: si vuelves a llamar a nvim_buf_set_extmark con el mismo identificador, no creas una segunda marca, sino que mueves y reconfiguras la que ya existía. Es más barato que borrar y volver a poner, y evita el parpadeo visual cuando la marca lleva texto virtual asociado.
Consultar, mover y desechar
Colocar es la mitad del trabajo. La otra mitad es preguntar qué hay puesto, y para eso existen dos funciones complementarias. nvim_buf_get_extmark_by_id responde por una marca concreta y admite details para devolver además toda su configuración. nvim_buf_get_extmarks barre un rango entero y acepta límites, filtros y una opción overlap que incluye las marcas que empiezan antes del rango pero lo cruzan.
-- Todas las marcas del namespace en las lineas 0 a 49
local lista = vim.api.nvim_buf_get_extmarks(0, ns, 0, 49, {
details = true,
overlap = true,
})
-- Solo la marca que cubre la posicion del cursor
local fila = vim.api.nvim_win_get_cursor(0)[1] - 1
local aqui = vim.api.nvim_buf_get_extmarks(0, ns, { fila, 0 }, { fila, -1 }, {})
La opción overlap merece un aviso, porque su ausencia produce un fallo silencioso muy típico: sin ella, una marca que empieza en la línea 2 y termina en la 40 no aparece al pedir las marcas del rango 10 a 20, aunque visualmente cubra esas líneas por completo. La API responde por dónde empiezan las marcas, no por dónde se ven. Si trabajas con rangos largos, actívala siempre.
Las coordenadas de esas funciones aceptan tanto un número de fila como una pareja de fila y columna, lo que permite acotar con precisión de carácter. El resultado siempre llega ordenado por posición, propiedad que conviene aprovechar: si necesitas la anotación más cercana al cursor, no ordenes tú, pide un rango pequeño y toma el primero.
Para retirar una marca suelta está nvim_buf_del_extmark, que recibe el namespace y el id y devuelve un booleano indicando si existía. En la práctica se usa poco, porque el patrón dominante es borrar el grupo entero, y de eso trata la lección siguiente.
Queda un comportamiento que sorprende y que conviene fijar. Una extmark no desaparece cuando borras el texto sobre el que estaba: colapsa a un punto y se queda ahí, informando de una posición que ya no significa nada. Si prefieres que se autodestruya, existe la opción invalidate, que la elimina automáticamente cuando el texto que cubría deja de existir; combinada con undo_restore puedes además decidir si debe reaparecer al deshacer el borrado. Esa pareja de opciones es la que hace que un diagnóstico no se quede colgado tras eliminar la línea culpable, y que vuelva a su sitio si el usuario pulsa deshacer.
Marca puntual
Una posición sin extremo final. Sirve para anclar algo a un punto del texto: un signo, una anotación al final de la línea, el sitio al que volver.
Marca de rango
Con end_row y end_col. Describe una región que se estira y se encoge: el ámbito de un error, el bloque a resaltar, el tramo que cambió respecto a git.
Por qué reemplazó a los signos y a las marcas viejas
Antes de las extmarks, anotar texto en Vim significaba elegir entre tres mecanismos incompatibles, cada uno con su propia mutilación. Las marcas clásicas de la a a la z eran veintiséis por buffer, con granularidad de línea y sin ningún dato asociado: servían para volver a un sitio, no para describirlo. Los signos, colocados con el comando sign place, vivían en una columna aparte, se referían a la línea entera y no sabían nada de columnas ni de rangos. Y matchadd resaltaba mediante expresiones regulares evaluadas por ventana en cada redibujado, lo que ataba el resaltado al patrón y no al texto concreto, y se degradaba en archivos grandes.
Ninguno de los tres podía expresar la petición más natural del mundo: acuérdate de estos dieciocho caracteres concretos, en esta línea, y sígueles la pista pase lo que pase. Los servidores de lenguaje, que empezaron a llegar en masa justo cuando Neovim maduraba, hablan exactamente en ese idioma: sus diagnósticos, sus acciones de código y sus tokens semánticos son todos rangos de texto. Sin una primitiva que sostuviera rangos vivos, cada plugin tenía que reimplementar la corrección de coordenadas tras cada edición, y todos lo hacían mal de maneras distintas.
El resultado era un ecosistema donde cada anotación se comportaba de forma distinta ante la misma edición. Insertabas una línea y el signo de git seguía bien, el resaltado de la búsqueda se recalculaba solo, y el marcador que había dejado un plugin de pruebas apuntaba tres líneas más arriba de lo debido. Nada de eso era un fallo de sus autores: era la consecuencia inevitable de no tener una primitiva compartida.
Las extmarks unifican los tres casos bajo una sola abstracción con una sola pregunta detrás: ¿qué le pasa a esta anotación cuando el texto se mueve? Una vez respondida esa pregunta en el núcleo, el resaltado, la columna de signos, el texto virtual y las líneas virtuales dejan de ser mecanismos distintos y se convierten en campos opcionales de la misma marca. La columna de signos moderna no es otro subsistema: es una extmark con sign_text. El resaltado no es un patrón: es una extmark con hl_group y un rango.
Detente en el desplazamiento conceptual que hay aquí, porque es más profundo que una API cómoda. Un editor ingenuo tiene una única fuente de verdad: la secuencia de caracteres. Todo lo demás —dónde está el error, qué línea cambió respecto a git, qué tipo devuelve esta expresión— o se recalcula entero en cada pulsación, o se guarda en coordenadas absolutas que quedan obsoletas en cuanto alguien pulsa una tecla. Ambas salidas son malas: la primera es cara, la segunda es incorrecta. La extmark es la tercera vía, y consiste en admitir que las anotaciones necesitan una identidad estable bajo transformación. La marca no dice “fila 12”; dice “este punto del texto”, y es el editor quien asume la obligación de traducir ese punto a coordenadas cada vez que se lo preguntas. Es exactamente el mismo movimiento intelectual que hacen los identificadores estables de un CRDT o los punteros de un recolector de basura compactador: el objeto conserva su identidad mientras su dirección cambia bajo él, porque la capa de abajo se compromete a mantener el mapa. Y como toda buena abstracción, su valor no está en lo que añade sino en lo que elimina: al garantizar que las posiciones sobreviven a la edición, desaparece de golpe la clase entera de errores en la que un plugin muestra un diagnóstico tres líneas por debajo de donde estaba el fallo. Ese bug ya no se arregla: no se puede escribir. Por eso el resto de este nivel no trata de trucos de dibujo, sino de todo lo que se vuelve posible cuando una anotación puede confiar en dónde está.
Una extmark es un identificador para una posición o rango del buffer que Neovim reubica en cada edición. Se crea con nvim_buf_set_extmark, con filas base cero y columnas en bytes, y devuelve un id que puedes reutilizar para mover la marca en vez de duplicarla. Con end_row y end_col cubre un rango; la gravedad decide de qué lado queda ante inserciones en el borde. Internamente viven en un árbol de marcadores, así que el coste de ajustarlas es logarítmico y un buffer aguanta decenas de miles.
- Crea un
namespacey coloca una extmark en la fila 5, columna 0, guardando el id en una variable global de Lua. - Inserta dos líneas al principio del archivo y vuelve a consultar la marca con
nvim_buf_get_extmark_by_id; comprueba que ahora informa de la fila 7. - Coloca una segunda marca con
end_rowyend_colcubriendo tres líneas yhl_groupaVisual; escribe texto en medio del rango y observa que el resaltado crece con él. - Repite el paso anterior con
end_right_gravitya verdadero y describe con precisión qué cambió al escribir justo en el borde final. - Vuelve a llamar a
nvim_buf_set_extmarkpasando elidde la primera marca y una fila distinta; confirma connvim_buf_get_extmarksque sigue habiendo dos marcas y no tres.