wandres.dev
BUFFERS Y VENTANAS · la API desde dentro

Buffers scratch: superficies de texto sin archivo

Buffers que no representan ningún fichero: qué hace exactamente el flag scratch, cómo lo definen buftype, bufhidden, swapfile y buflisted, y cómo vestirlos con filetype, nombre y teclas locales sin dejar fugas.

⏱ 17 min

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.

🎯 Al terminar esta lección sabrás
  • Saber qué opciones activa exactamente el flag scratch de nvim_create_buf.
  • Distinguir los valores de buftype 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 sin dejar buffers huérfanos.

Scratch no es un tipo, es un preajuste

La función nvim_create_buf(listed, scratch) recibe dos booleanos. El primero decide si el buffer aparece en :ls y en la navegación con :bnext. El segundo, pese a su nombre, no crea una clase distinta de buffer: se limita a fijar tres opciones de golpe.

local api = vim.api

-- la forma corta
local buf = api.nvim_create_buf(false, true)

-- exactamente equivalente, escrito a mano
local buf2 = api.nvim_create_buf(false, false)
vim.bo[buf2].buftype  = "nofile"
vim.bo[buf2].bufhidden = "hide"
vim.bo[buf2].swapfile = 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.

🪟

listed = false

Fuera de :ls, fuera de :bnext. El usuario no tropieza con él por accidente.

🧪

scratch = true

Aplica buftype a nofile, bufhidden a hide y desactiva el fichero de intercambio.

♻️

No es inmutable

Sigue siendo modifiable. Si quieres solo lectura, tienes que pedirlo.

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, de forma más explícita, con nvim_set_option_value pasando el buffer como destino.

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 y que :w no tiene sentido. nowrite permite un nombre de archivo real pero prohíbe escribirlo. acwrite es el caso sofisticado: el buffer no se guarda por sí solo, pero dispara BufWriteCmd para que tu código decida qué significa guardar. Los valores help, quickfix, terminal y prompt los gestiona el propio editor y no deberías fijarlos a mano.

bufhidden decide qué ocurre cuando el buffer deja de estar visible en ninguna ventana. Con hide sobrevive indefinidamente en memoria; con unload se descarga pero conserva su identidad y sus marcas; con delete se borra de la lista; con wipe desaparece por completo, incluidas marcas y variables locales. Para un panel que reconstruyes cada vez, wipe es la elección higiénica.

swapfile desactivado evita crear ficheros de intercambio para contenido que no vale la pena recuperar, y de paso evita el diálogo de fichero de intercambio existente cuando dos instancias generan el mismo nombre.

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

flowchart TD
C[Creo el buffer] --> V[Visible en alguna ventana]
V -->|se cierra la ventana| H[Deja de estar visible]
H -->|bufhidden hide| M[Sigue en memoria]
H -->|bufhidden unload| D[Descargado pero valido]
H -->|bufhidden wipe| X[Destruido por completo]
M --> R[Puedo reutilizarlo]
D --> R
X --> C
style X fill:#f38ba8,color:#11111b
style R fill:#a6e3a1,color:#11111b

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 la salida, o inventar uno propio para que otros plugins y tus propios autocomandos puedan reconocer tu panel.

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 falla con error si otro buffer ya tiene ese nombre exacto. En superficies que puedes abrir varias veces, incluye el identificador en el nombre.

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 limita el mapeo a ese buffer y desaparece con él.

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

Sobre la escritura: un buffer scratch nace modifiable. Si el contenido lo genera tu código, deja modifiable desactivada de cara al usuario y ábrela solo durante el render. Y desactiva modified después de escribir, para que el editor no pregunte por cambios sin guardar.

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 = false
end

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 bufhidden por defecto en scratch es hide, esos buffers no se van: se acumulan invisibles, cada uno con sus autocomandos y sus extmarks. Tras una sesión larga hay decenas.

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

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].bufhidden = "hide"
  vim.bo[M.buf].filetype = "milateral"
  return M.buf
end

Si además quieres detectar el buffer desde fuera del módulo, marca su identidad con una variable de buffer en lugar de fiarte del nombre. Las variables locales sobreviven a los renombrados y desaparecen con el buffer.

vim.b[M.buf].milateral = true

local function es_mio(b)
  local ok, v = pcall(function() return vim.b[b].milateral end)
  return ok and v == true
end

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.

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. 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 su contenido es reconstruible, y a cambio el editor te libera la memoria sin preguntar. 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. Cuando dudes qué valor poner, no preguntes qué te conviene a ti, pregunta qué le estás prometiendo al editor.

⚔️ Tu primera superficie sin archivo
  1. Crea un buffer scratch con nvim_create_buf y comprueba con :setlocal buftype? que vale nofile.
  2. Escribe la misma configuración a mano sobre un buffer normal y verifica que se comporta igual.
  3. Dale un filetype inventado y añade un autocomando FileType que active cursorline en él.
  4. Implementa la función obtener con reutilización y comprueba que abrir el panel diez veces deja un solo buffer en :ls!.
  5. Investiga: cambia bufhidden entre hide y wipe y observa en :ls! qué sobrevive al cerrar la ventana.