Saltar al contenido

Buffers por código: crear, listar, leer y escribir

El buffer como objeto de primera clase desde Lua: identidad numérica frente a nombre y archivo, la diferencia entre válido y cargado, la aritmética exacta de nvim_buf_get_lines y set_lines con base cero y final exclusivo, la edición quirúrgica con nvim_buf_set_text, y el entorno de toda escritura: modifiable, el árbol de deshacer y los eventos que emite.

En la interfaz, un buffer parece ser el archivo que tienes abierto. Desde Lua es algo mucho más austero y mucho más potente: un identificador entero, una secuencia de líneas indexada desde cero y un conjunto de opciones locales. Toda la manipulación de texto de Neovim —la tuya, la de los plugins, la del cliente LSP— se apoya en esa abstracción, y casi todos los errores de desplazamiento por uno que arrastran los plugins nacen de no haber interiorizado su aritmética. Esta lección la instala de una vez: después de ella, insertar, borrar y reemplazar dejan de ser tres operaciones y se convierten en una sola con parámetros distintos.

  • Crear, listar y validar buffers con nvim_create_buf, nvim_list_bufs, nvim_buf_is_valid y nvim_buf_is_loaded.
  • Leer texto con nvim_buf_get_lines dominando el rango base cero y final exclusivo, y los índices negativos.
  • Escribir con nvim_buf_set_lines entendiendo que insertar, borrar y reemplazar son la misma operación.
  • Editar dentro de una línea con nvim_buf_set_text sabiendo que sus columnas cuentan bytes.
  • Entender el entorno de una escritura: modifiable, el árbol de deshacer y los eventos de texto.

El buffer es un número, no un archivo

Un buffer se identifica por un entero, el bufnr. Ese número es la única identidad estable: el nombre puede cambiar con :file, el archivo puede no existir jamás —lo verás a fondo en la lección siguiente— y el contenido puede ser efímero. En toda la API, el valor 0 es un alias de «el buffer actual», lo que permite escribir código breve al precio de perder explicitud. En la lección 1.1 aprendiste a manejar buffers con comandos; aquí el mismo objeto se vuelve programable.

LUA
local api = vim.api-- crear un buffer: listado en :ls y no efimerolocal buf = api.nvim_create_buf(true, false)-- el buffer que ve ahora mismo la ventana actuallocal actual = api.nvim_get_current_buf()-- todos los buffers que el editor conoce, listados o nofor _, b in ipairs(api.nvim_list_bufs()) do  print(b, api.nvim_buf_get_name(b))end

Hay dos preguntas distintas que la gente confunde, y la API las separa con dos funciones. nvim_buf_is_valid responde si el identificador sigue existiendo; nvim_buf_is_loaded responde si además tiene su contenido en memoria —de hecho comprueba las dos cosas a la vez, así que en la práctica basta con ella—. Un buffer descargado con :bunload o por su opción bufhidden sigue siendo válido y conserva su nombre y sus opciones, pero sus líneas se liberaron: nvim_buf_line_count devuelve 0 y nvim_buf_get_lines devuelve una tabla vacía, no un error y tampoco una línea en blanco.

LUA
if api.nvim_buf_is_loaded(buf) then  local n = api.nvim_buf_line_count(buf)  print(("el buffer %d tiene %d lineas"):format(buf, n))end

Para destruir un buffer está nvim_buf_delete, y su nombre engaña un poco: se comporta como :bwipeout, no como :bdelete. Elimina el buffer y sus metadatos —marcas, variables locales, entrada en la lista de saltos—. Si lo que quieres es el comportamiento de :bdelete, que lo saca de la lista pero conserva su identidad, la receta documentada es quitar buflisted y pasar unload = true.

LUA
-- borrado total, como :bwipeoutapi.nvim_buf_delete(buf, { force = false })-- equivalente de :bdeletevim.bo[buf].buflisted = falseapi.nvim_buf_delete(buf, { unload = true })

El campo force descarta los cambios sin guardar en lugar de fallar. Sin él, intentar borrar un buffer modificado lanza un error de Lua capturable con pcall, que es exactamente la clase de fallo ruidoso que quieres en código de limpieza: como viste en la lección 8.1, los errores de la API se propagan y se capturan, no se pierden en silencio.

Índices: base cero y final exclusivo

Aquí está la fricción conceptual del nivel. Los comandos ex numeran líneas desde 1 y sus rangos son inclusivos: :2,4d borra tres líneas. La API numera desde 0 y su final es exclusivo, exactamente como una rebanada de Python: el rango [1, 4) cubre las mismas tres líneas. Convivir con los dos sistemas sin un punto único de traducción es la causa número uno de errores por uno en configuraciones y plugins.

La firma es nvim_buf_get_lines(buf, start, fin, strict_indexing). Los índices negativos se interpretan como longitud + 1 + índice: -1 denota la posición justo después de la última línea, y por eso «la última línea» se pide con el rango (-2, -1).

LUA
-- todo el bufferlocal todo = api.nvim_buf_get_lines(buf, 0, -1, false)-- las diez primeras lineaslocal cabecera = api.nvim_buf_get_lines(buf, 0, 10, false)-- solo la ultima linealocal ultima = api.nvim_buf_get_lines(buf, -2, -1, false)[1]-- la linea del cursor: get_cursor devuelve fila en base UNOlocal fila = api.nvim_win_get_cursor(0)[1]local aqui = api.nvim_buf_get_lines(0, fila - 1, fila, true)[1]

Ese último ejemplo cruza la excepción documentada que ya conociste en la lección 8.1: nvim_win_get_cursor devuelve la fila en base uno y la columna en base cero, así que la traducción a coordenadas de líneas es restar uno a la fila y nada más.

El cuarto parámetro, strict_indexing, decide qué pasa si el rango se sale del buffer. Con false el rango se recorta en silencio al valor válido más cercano, que es lo que quieres al leer datos de origen incierto. Con true se lanza un error, que es lo que quieres cuando un desbordamiento indicaría un bug tuyo. Elegirlo no es cosmético: es declarar si esa lectura es una consulta tolerante o una aserción.

Escribir es reemplazar un rango

nvim_buf_set_lines no tiene hermanas para insertar y borrar. Hay una sola operación —sustituir el rango indicado por la lista de líneas que pasas— y las demás son casos particulares suyos. Interiorizar esto reduce toda la escritura de texto a un único modelo mental.

LUA
-- reemplazar: las lineas 1 y 2 se convierten en una solaapi.nvim_buf_set_lines(buf, 0, 2, false, { "cabecera unificada" })-- insertar: rango vacio, no se borra nadaapi.nvim_buf_set_lines(buf, 3, 3, false, { "linea nueva" })-- borrar: reemplazo vacioapi.nvim_buf_set_lines(buf, 3, 5, false, {})-- anadir al final sin conocer el tamanoapi.nvim_buf_set_lines(buf, -1, -1, false, { "ultima" })-- vaciar por completoapi.nvim_buf_set_lines(buf, 0, -1, false, {})

Las cadenas de la lista no pueden contener saltos de línea: cada elemento es exactamente una línea, porque —como verás en el callout final— el buffer no es una cadena con \n sino una secuencia de líneas. Si tienes texto crudo, pártelo antes con vim.split(texto, "\n", { plain = true }).

Cirugía dentro de la línea: nvim_buf_set_text

Cuando el cambio ocurre dentro de una línea, nvim_buf_set_lines es un martillo demasiado grande: reescribe la línea entera. Para eso está nvim_buf_set_text, que trabaja con cuatro coordenadas —fila y columna inicial, fila y columna final— y tiene dos ventajas concretas. La primera es que puede cruzar líneas: un rango que empieza en una fila y termina en otra reemplaza el trozo intermedio, saltos incluidos. La segunda importa para el Nivel 14: al tocar solo el rango indicado, las extmarks de las partes no modificadas de esas líneas se conservan, mientras que set_lines las arrastra o las invalida al sustituir la línea completa.

LUA
-- sustituir las columnas 4 a 8 de la segunda linea (la 9 es exclusiva)api.nvim_buf_set_text(buf, 1, 4, 1, 9, { "nuevo" })-- insertar en un punto: las cuatro coordenadas igualesapi.nvim_buf_set_text(buf, 0, 0, 0, 0, { "prefijo " })

set_text mezcla convenciones: filas inclusivas, columnas exclusivas y en bytes

nvim_buf_set_text indexa desde cero como toda la API, pero con una asimetría documentada: las filas del rango son de final inclusivo y las columnas de final exclusivo. Y las columnas se cuentan en bytes, no en caracteres ni en celdas de pantalla: en un archivo UTF-8, «á» ocupa dos bytes y un emoji cuatro, así que una columna calculada a ojo puede caer a mitad de un carácter y producir texto inválido. Cuando la posición venga de contar caracteres, conviértela con vim.str_byteindex antes de llamar. La columna que devuelve nvim_win_get_cursor ya está en bytes, así que esa sí se pasa tal cual.

El entorno de una escritura

Escribir no es solo mover bytes. Antes de que tu llamada tenga efecto, Neovim comprueba modifiable; si está desactivada, la API lanza un error en lugar de fallar en silencio. El patrón correcto para un buffer que el usuario no debe tocar es abrir, escribir y volver a cerrar —lo usarás constantemente a partir de la lección 10.2—.

LUA
local function escribir(b, lineas)  vim.bo[b].modifiable = true  api.nvim_buf_set_lines(b, 0, -1, false, lineas)  vim.bo[b].modifiable = falseend

Cada llamada a nvim_buf_set_lines genera su propia entrada en el árbol de deshacer. Si una operación tuya hace cinco escrituras, el usuario necesitará cinco u para revertirla, lo cual es hostil. La solución es fundirlas con undojoin, ejecutado en el contexto del buffer afectado mediante nvim_buf_call.

LUA
api.nvim_buf_call(buf, function()  pcall(vim.cmd, "undojoin")  api.nvim_buf_set_lines(buf, 0, -1, false, nuevas)end)

El pcall del undojoin no es superstición

undojoin falla con E790 si el estado anterior no admite fusión —por ejemplo, justo después de que el usuario haya pulsado u—. Sin el pcall, ese caso límite rompería tu operación entera por proteger una comodidad. Con él, en el peor de los casos el usuario necesita un u de más, que es un precio aceptable.

Toda escritura emite señales, y conviene conocer las tres capas. Los autocomandos TextChanged que aprendiste en el Nivel 9 se disparan al volver el control al bucle principal. El contador b:changedtick, legible con nvim_buf_get_changedtick, se incrementa con cada cambio y es la forma barata de preguntar «¿cambió algo desde la última vez?» sin comparar contenido. Y cualquier observador registrado con nvim_buf_attach recibe una llamada on_lines con el rango exacto que cambió: primera línea afectada, última línea vieja y última línea nueva, todas en base cero. Ese callback es la base de todo lo que necesita reaccionar a ediciones sin recorrer el buffer entero, del resaltado incremental a los indicadores de git.

El array de líneas es la única verdad

Neovim no guarda el texto como una cadena con saltos de línea: lo guarda como una secuencia de líneas, y esa decisión estructural explica toda la API. Por eso nvim_buf_set_lines es una operación de rebanada y no una edición de cadena; por eso los elementos de la lista no pueden contener \n; por eso las columnas van en bytes mientras las filas van en elementos. Cuando pienses una transformación, no la pienses como texto que se reescribe sino como un rango de índices que se sustituye por otra lista: en ese lenguaje, insertar, borrar, mover y reordenar son la misma operación con parámetros distintos, y la pregunta «¿cómo hago X?» se convierte en «¿qué rango sustituyo por qué lista?». Los plugins que envejecen mal son los que traducen entre base uno y base cero en puntos dispersos del código, una resta aquí y una suma allá, hasta que nadie sabe en qué sistema está cada variable. Los que envejecen bien traducen una sola vez, en la frontera con el usuario —al leer el cursor, al recibir un rango de un comando— y por dentro viven enteros en base cero con final exclusivo. No es una manía estética: es eliminar la clase entera de errores por uno reduciendo a uno el número de sitios donde pueden nacer.

El rango [0, 1) cubre exactamente una línea, la primera. get_lines devuelve siempre una tabla, aunque pidas una sola: el [1] extrae la cadena.

-1 es la posición tras la última línea; un rango vacío ahí es una inserción al final. No hace falta consultar line_count primero.

Borrar es reemplazar por la lista vacía. El rango [0, 3) son las líneas que el usuario ve numeradas 1, 2 y 3.

Devuelve 0: el buffer sigue siendo válido pero sus líneas se liberaron, y get_lines devolvería una tabla vacía. is_valid seguiría diciendo true; is_loaded, false.

Las cuatro coordenadas iguales definen un punto de inserción. Fíjate en el - 1: el cursor llega en base uno y set_text espera base cero.

Una utilidad de líneas

  1. Escribe una función que devuelva la línea bajo el cursor sin usar vim.fn.getline, traduciendo el índice tú mismo, y comprueba que coincide con getline('.').
  2. Escribe duplicar_linea() que inserte una copia justo debajo, usando un rango vacío como punto de inserción.
  3. Escribe invertir_buffer() que lea todas las líneas, las invierta en Lua y las devuelva en una sola llamada a set_lines.
  4. Encadena las tres operaciones anteriores con undojoin dentro de nvim_buf_call y comprueba que una sola u lo deshace todo.
  5. Descarga un buffer con :bunload y verifica desde Lua la tripleta: is_valid verdadero, is_loaded falso, line_count cero.
  6. Investiga: compara con vim.uv.hrtime() el coste de mil llamadas a set_lines de una línea frente a una sola llamada de mil líneas, y explica el resultado con lo que sabes del árbol de deshacer y los eventos.