Saltar al contenido

Buffers scratch: superficies de texto sin archivo

Buffers que no representan ningún fichero: qué opciones activa exactamente el flag scratch de nvim_create_buf, el cuarteto buftype, bufhidden, swapfile y buflisted que define el contrato con el editor, acwrite y BufWriteCmd para redefinir qué significa guardar, y cómo vestir la superficie con filetype, nombre y teclas locales sin acumular buffers huérfanos.

Casi todo lo que un plugin muestra —un listado de resultados, una previsualización, un panel de estado, la salida de un comando— vive en un buffer que no corresponde a ningún archivo del disco. Neovim llama a esa categoría buffers scratch, y no son un tipo especial de objeto: son un buffer normal con un puñado de opciones puestas de forma que el editor deje de tratarlo como un documento. Entender exactamente qué promete cada opción es lo que separa un panel que convive bien con el resto del editor de uno que pregunta por cambios sin guardar, deja ficheros de intercambio por el disco o se acumula invisible hasta que :ls! da miedo.

  • Saber qué opciones activa exactamente el flag scratch de nvim_create_buf, y qué añade por su cuenta.
  • Distinguir los valores de buftype —incluido acwrite con BufWriteCmd— y elegir el correcto para cada superficie.
  • Controlar el ciclo de vida con bufhidden, buflisted y swapfile.
  • Vestir el buffer con filetype, nombre y teclas locales, y reutilizarlo en vez de acumular huérfanos.

Scratch no es un tipo, es un preajuste

La función nvim_create_buf(listed, scratch) recibe dos booleanos. El primero fija buflisted: decide si el buffer aparece en :ls, en la navegación con :bnext y en el picker que montaste en la lección 4.1. El segundo, pese a su nombre, no crea una clase distinta de buffer: aplica el preajuste que :help scratch-buffer documenta —buftype a nofile, bufhidden a hide, swapfile desactivado— y añade dos detalles propios de la función: el buffer nace siempre con modified a falso y con modeline desactivada, para que ningún contenido que vuelques en él pueda colar una línea de modo.

LUA
local api = vim.api-- la forma cortalocal buf = api.nvim_create_buf(false, true)-- practicamente equivalente, escrito a manolocal buf2 = api.nvim_create_buf(false, false)vim.bo[buf2].buftype   = "nofile"vim.bo[buf2].bufhidden = "hide"vim.bo[buf2].swapfile  = falsevim.bo[buf2].modeline  = false

Que sea un preajuste y no un tipo tiene una consecuencia práctica: puedes desviarte de él. Un buffer scratch al que luego le pones bufhidden a wipe sigue siendo perfectamente legítimo, y de hecho suele ser lo que quieres para superficies efímeras que se reconstruyen en cada invocación.

listed = false

Fuera de :ls, de :bnext y del picker. El usuario no tropieza con él por accidente; sigue visible en :ls!.

scratch = true

buftype a nofile, bufhidden a hide, sin fichero de intercambio. Además: sin modeline y nunca marcado como modificado al nacer.

No es inmutable

Sigue siendo modifiable. Si quieres que el usuario no lo edite, tienes que pedirlo tú, como viste en la lección 10.1.

El cuarteto que define el comportamiento

Cuatro opciones locales de buffer determinan cómo lo trata el editor. Se leen y escriben con vim.bo[bufnr] o, con ámbito explícito, con nvim_set_option_value pasando el buffer como destino —las dos formas que ya conoces de la lección 8.2—.

LUA
api.nvim_set_option_value("buftype", "nofile", { buf = buf })api.nvim_set_option_value("bufhidden", "wipe", { buf = buf })api.nvim_set_option_value("swapfile", false, { buf = buf })api.nvim_set_option_value("buflisted", false, { buf = buf })

buftype declara qué representa el contenido. Vacío significa «un archivo normal». nofile significa que el texto no corresponde a nada persistente: :w a secas falla con E382 y el buffer nunca se considera modificado de cara a la salida del editor. nowrite es parecido pero el nombre sí se trata como un fichero real: sirve para vistas de solo lectura de algo que existe. Y acwrite es el caso sofisticado que merece su propia sección más abajo. Los valores help, quickfix, terminal y prompt los gestiona el propio editor —los dos últimos los explorarás en el Nivel 13— y no debes fijarlos a mano.

bufhidden decide qué ocurre cuando el buffer deja de estar visible en ninguna ventana. Su valor por defecto es la cadena vacía, que delega en la opción global hidden; los otros cuatro valores toman la decisión localmente. Con hide sobrevive indefinidamente en memoria; con unload se descarga —recuerda la lección anterior: válido pero con cero líneas— conservando identidad y opciones; con delete se descarga y sale de la lista, como un :bdelete; con wipe desaparece por completo, marcas y variables incluidas, como un :bwipeout.

swapfile desactivado evita crear ficheros de intercambio para contenido que no vale la pena recuperar, y de paso elimina el diálogo de «swap file exists» cuando dos instancias de Neovim generan una superficie con el mismo nombre.

buflisted solo afecta a la visibilidad en listados y a la navegación secuencial; no cambia el ciclo de vida.

acwrite: redefinir qué significa guardar

Con buftype a acwrite, el buffer no sabe guardarse solo: cuando el usuario ejecuta :w, Neovim dispara el autocomando BufWriteCmd y delega en él la escritura entera. Tu callback decide qué significa guardar —enviar el texto por HTTP, aplicarlo a una base de datos, confirmar una edición en un panel de renombrado masivo— y al terminar marca el buffer como no modificado.

LUA
vim.bo[buf].buftype = "acwrite"api.nvim_buf_set_name(buf, "consulta://usuarios")api.nvim_create_autocmd("BufWriteCmd", {  buffer = buf,  callback = function()    local lineas = api.nvim_buf_get_lines(buf, 0, -1, false)    aplicar(lineas)                 -- tu definicion de "guardar"    vim.bo[buf].modified = false    -- sin esto, :wq seguira quejandose  end,})

Este es el mecanismo detrás de herramientas tipo «edita los resultados del grep y escribe para aplicar»: la superficie parece un fichero, el flujo de guardar es el de siempre, y la semántica es tuya. Los eventos Cmd que sustituyen a una operación en vez de acompañarla los viste en el Nivel 9; BufWriteCmd es el más útil de la familia.

Vestir la superficie

Un buffer scratch recién creado es texto plano y mudo. Tres gestos lo convierten en una interfaz.

El primero es el filetype, que dispara el autocomando FileType y con él el resaltado, sea de Treesitter o de sintaxis clásica. Puedes reutilizar un lenguaje real para colorear una salida —filetype = "diff" para un parche, "markdown" para documentación— o inventar uno propio para que tus autocomandos y los de cualquier usuario puedan reconocer tu panel sin conocer tu código.

LUA
vim.bo[buf].filetype = "milateral"

El segundo es el nombre. nvim_buf_set_name da al buffer una etiqueta visible en la línea de estado y en los listados, pero lanza un error si otro buffer ya tiene ese nombre exacto. En superficies que pueden existir varias veces, incluye el identificador en el nombre; el prefijo con pseudo-esquema es una convención extendida que además deja claro que aquello no es una ruta.

LUA
pcall(api.nvim_buf_set_name, buf, "milateral://" .. buf)

El tercero son las teclas locales. Un panel que no se cierra con q resulta hostil, y una tecla global para eso sería una invasión. La opción buffer de vim.keymap.set —que estudiarás a fondo en el Nivel 11— limita el mapeo a ese buffer y desaparece con él.

LUA
vim.keymap.set("n", "q", function()  local win = vim.fn.bufwinid(buf)  if win ~= -1 then api.nvim_win_close(win, true) endend, { buffer = buf, nowait = true, desc = "cerrar el panel" })

Sobre la escritura: un buffer scratch nace modifiable. Si el contenido lo genera tu código, ciérralo de cara al usuario y ábrelo solo durante el render, con el patrón de la lección 10.1. Y desactiva modified después de escribir: aunque nofile ya evita los avisos al salir, otros valores de buftype no lo hacen, y dejar la marca limpia es gratis.

LUA
local function render(b, lineas)  if not api.nvim_buf_is_valid(b) then return end  vim.bo[b].modifiable = true  api.nvim_buf_set_lines(b, 0, -1, false, lineas)  vim.bo[b].modifiable = false  vim.bo[b].modified = falseend

El nombre no identifica: la variable local sí

Si necesitas detectar tu panel desde fuera del módulo —un autocomando global, otro plugin—, no compares nombres: el usuario puede renombrar, y dos instancias chocan. Marca la identidad con una variable de buffer, que sobrevive a los renombrados y muere con el buffer:

LUA
vim.b[buf].milateral = truelocal function es_mio(b)return vim.b[b].milateral == trueend

Las variables de ámbito b: las conociste en la lección 8.3; esta es su aplicación más útil en plugins.

Reutilizar en lugar de acumular

El fallo más común en plugins con paneles es crear un buffer nuevo en cada invocación. Como el preajuste scratch deja bufhidden en hide, esos buffers no mueren al cerrar su ventana: se acumulan invisibles, cada uno con sus autocomandos, sus teclas y sus extmarks. Tras una sesión larga hay decenas, y cada uno cuesta memoria y trabajo en cada evento.

El patrón robusto guarda el identificador en el estado del módulo y lo valida antes de reutilizarlo.

LUA
local M = { buf = nil }function M.obtener()  if M.buf and api.nvim_buf_is_valid(M.buf) then    return M.buf  end  M.buf = api.nvim_create_buf(false, true)  vim.bo[M.buf].filetype = "milateral"  vim.b[M.buf].milateral = true  return M.bufend

La validación no es opcional. El usuario puede haber hecho :bwipeout sobre tu buffer, o un :%bd masivo puede habérselo llevado por delante. Un identificador guardado sin comprobar es una bomba de relojería que estalla como error críptico varios minutos después, lejos del código que la armó.

Un buffer scratch es un contrato, no un contenedor

La tentación es ver el scratch como «un sitio donde volcar texto». La lectura correcta es que cada opción que fijas es una promesa que le haces al resto del editor, y el editor paga cada promesa con un comportamiento. Al poner buftype a nofile prometes que nadie debe intentar guardarlo, y a cambio el editor deja de avisar de cambios sin guardar y de bloquear la salida. Al poner bufhidden a wipe prometes que el contenido es reconstruible, y a cambio te libera la memoria sin preguntar. Al declarar acwrite prometes que tú sabes qué significa guardar, y a cambio recibes el :w del usuario como un evento tuyo. Al ponerle un filetype propio abres una puerta pública: cualquiera puede escribir un autocomando sobre tu panel sin conocer tu API. Ese es el verdadero motivo por el que los plugins bien diseñados se integran entre sí sin acuerdos previos: no comparten funciones, comparten los mismos contratos declarados en opciones de buffer, legibles por cualquiera con un :setlocal buftype?. Cuando dudes qué valor poner, no preguntes qué te conviene a ti: pregunta qué le estás prometiendo al editor, y si vas a poder cumplirlo.

Debe responder nofile, hide y false: las tres opciones del preajuste documentado en :help scratch-buffer.

E382: Cannot write, 'buftype' option is set. El contrato en acción: declaraste que el contenido no corresponde a nada persistente y el editor te toma la palabra.

La exclamación muestra también los no listados. Cada línea con u delante es un buffer que algún plugin creó con listed = false; si ves decenas del mismo, ese plugin no reutiliza.

Al perder su última ventana, el buffer desaparece por completo, como con :bwipeout. Si tu módulo guarda el id, la validación con is_valid lo detectará en la siguiente invocación.

Tu primera superficie sin archivo

  1. Crea un buffer scratch con nvim_create_buf(false, true), muéstralo en una división y comprueba con :setlocal buftype? bufhidden? swapfile? los tres valores del preajuste.
  2. Reconstruye la misma configuración a mano sobre un buffer normal y verifica que se comporta igual, incluido el E382 al intentar :w.
  3. Dale un filetype inventado y añade un autocomando FileType que active cursorline en él: acabas de integrar tu panel con el sistema de eventos sin tocar tu propio código.
  4. Implementa la función obtener con reutilización y comprueba que abrir el panel diez veces deja un solo buffer en :ls!.
  5. Cambia bufhidden entre hide y wipe, cierra la ventana en cada caso y observa en :ls! qué sobrevive.
  6. Extra: crea un buffer acwrite con un BufWriteCmd que imprima las líneas con vim.print y limpie modified; comprueba que :w ejecuta tu callback y que :wq sale sin quejarse.