Buffers por código: crear, listar, leer y escribir
El buffer como objeto de primera clase desde Lua: identidad numérica, ciclo de vida y la aritmética exacta de nvim_buf_get_lines y nvim_buf_set_lines con índices base cero y final exclusivo.
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, un array de líneas indexado desde cero y un conjunto de opciones locales. Toda la manipulación de texto de Neovim se apoya en esa abstracción, y casi todos los errores de los plugins nacen de no haber interiorizado su aritmética.
- Crear, listar y validar buffers con
nvim_create_buf,nvim_list_bufsynvim_buf_is_valid. - Leer texto con
nvim_buf_get_linesdominando el rango base cero y final exclusivo. - Escribir con
nvim_buf_set_linesy editar dentro de una línea connvim_buf_set_text. - 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, el archivo puede no existir jamás 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.
local api = vim.api
-- crear un buffer: listado en :ls y no efimero
local buf = api.nvim_create_buf(true, false)
-- el buffer que ve ahora mismo la ventana actual
local actual = api.nvim_get_current_buf()
-- todos los buffers que el editor conoce, listados o no
for _, 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. nvim_buf_is_valid responde si el identificador sigue existiendo; nvim_buf_is_loaded responde si además tiene su contenido en memoria. Un buffer descargado sigue siendo válido, conserva sus marcas y su nombre, pero su array de líneas está vacío: leerlo devolverá una única línea vacía, no un error.
if api.nvim_buf_is_valid(buf) and 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
-- destruirlo explicitamente
api.nvim_buf_delete(buf, { force = false, unload = false })
Í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. La API de Lua numera desde 0 y su final es exclusivo, exactamente como una rebanada de Python. Convivir con los dos sistemas sin traducirlos mentalmente es la causa número uno de errores por uno.
flowchart LR U1[Linea 1 para el usuario] --> A0[Indice 0 en la API] U2[Linea 2 para el usuario] --> A1[Indice 1 en la API] U3[Linea 3 para el usuario] --> A2[Indice 2 en la API] A2 --> F[Rango 0 a 3 devuelve las tres] F --> N[Indice menos uno significa el final] style F fill:#a6e3a1,color:#11111b style N fill:#89b4fa,color:#11111b
La firma es nvim_buf_get_lines(bufnr, start, fin, strict). Los índices negativos cuentan desde el final, y -1 como final significa “hasta la última línea incluida”, porque equivale a la posición justo después del último elemento.
-- todo el buffer
local todo = api.nvim_buf_get_lines(buf, 0, -1, false)
-- las diez primeras lineas
local cabecera = api.nvim_buf_get_lines(buf, 0, 10, false)
-- solo la ultima linea
local ultima = api.nvim_buf_get_lines(buf, -2, -1, false)[1]
-- la linea donde esta el cursor, traduciendo de base uno a base cero
local fila = api.nvim_win_get_cursor(0)[1]
local aqui = api.nvim_buf_get_lines(0, fila - 1, fila, true)[1]
El cuarto parámetro, strict, decide qué pasa si el rango se sale del buffer. Con false el rango se recorta en silencio, 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 indica un bug tuyo. Elegir strict 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.
-- reemplazar: las lineas 1 y 2 se convierten en una sola
api.nvim_buf_set_lines(buf, 0, 2, false, { "cabecera unificada" })
-- insertar: rango vacio, no se borra nada
api.nvim_buf_set_lines(buf, 3, 3, false, { "linea nueva" })
-- borrar: reemplazo vacio
api.nvim_buf_set_lines(buf, 3, 5, false, {})
-- anadir al final sin conocer el tamano
api.nvim_buf_set_lines(buf, -1, -1, false, { "ultima" })
-- vaciar por completo
api.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. Si tienes un texto crudo, pártelo antes con vim.split(texto, "\n", { plain = true }).
Cuando el cambio ocurre dentro de una línea, nvim_buf_set_lines es un martillo demasiado grande. Para eso está nvim_buf_set_text, que trabaja con cuatro coordenadas: fila y columna inicial, fila y columna final. Las filas siguen siendo base cero, pero las columnas se cuentan en bytes, no en caracteres: con acentos o emojis, cortar por la mitad de un carácter multibyte produce texto inválido.
-- sustituir las columnas 4 a 9 de la linea 2, contando en bytes
api.nvim_buf_set_text(buf, 1, 4, 1, 9, { "nuevo" })
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.
local function escribir(b, lineas)
vim.bo[b].modifiable = true
api.nvim_buf_set_lines(b, 0, -1, false, lineas)
vim.bo[b].modifiable = false
end
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.
api.nvim_buf_call(buf, function()
pcall(vim.cmd, "undojoin")
api.nvim_buf_set_lines(buf, 0, -1, false, nuevas)
end)
El pcall no es superstición: undojoin falla si el estado anterior no admite fusión, por ejemplo justo después de un deshacer.
Toda escritura emite eventos. Los autocomandos TextChanged reaccionan, las marcas y los extmarks se desplazan según su gravedad, y cualquier observador registrado con nvim_buf_attach recibe una llamada on_lines con el rango exacto que cambió. Ese callback es la base de todo lo que necesita saber qué cambió sin recorrer el buffer entero.
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 las líneas 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. Los plugins que envejecen mal son los que traducen constantemente entre base uno y base cero en puntos dispersos del código. Los que envejecen bien traducen una sola vez, en la frontera con el usuario, y por dentro viven enteros en base cero con final exclusivo.
- Escribe una función que devuelva la línea bajo el cursor sin usar
vim.fn.getline, traduciendo el índice tú mismo. - Escribe
duplicar_linea()que inserte una copia justo debajo, usando un rango vacío. - Escribe
invertir_buffer()que lea todas las líneas, las invierta y las devuelva en una sola llamada. - Añade
undojoinpara que las tres operaciones anteriores encadenadas se deshagan con una solau. - Investiga: compara el coste de mil llamadas a
nvim_buf_set_linesde una línea frente a una sola llamada de mil líneas.