Ventanas desde Lua: abrir, cerrar, mover y medir
La ventana como viewport sobre un buffer: nvim_open_win para divisiones y flotantes, reconfiguración en caliente con nvim_win_set_config, cierre seguro y el terreno resbaladizo de las opciones locales de ventana.
Un buffer es texto; una ventana es una mirada sobre ese texto. Son objetos independientes con ciclos de vida distintos: el mismo buffer puede verse desde cinco ventanas a la vez, y una ventana puede cambiar de buffer sin dejar de existir. Desde Lua esa independencia deja de ser teoría y se vuelve la interfaz con la que construyes cualquier disposición.
- Manejar el identificador de ventana y su relación con buffers y pestañas.
- Abrir divisiones y flotantes con la única función
nvim_open_win. - Reconfigurar, redimensionar y cerrar ventanas sin dejar el editor inconsistente.
- Fijar opciones locales de ventana entendiendo su herencia y sus trampas.
La ventana como viewport
Cada ventana tiene un identificador entero, el winid, único mientras viva y jamás reutilizado. Como con los buffers, el valor 0 significa “la ventana actual”. Una ventana pertenece siempre a una pestaña, y toda ventana muestra exactamente un buffer.
local api = vim.api
local win = api.nvim_get_current_win()
local buf = api.nvim_win_get_buf(win)
-- todas las ventanas del editor, de todas las pestanas
local todas = api.nvim_list_wins()
-- solo las de la pestana actual
local aqui = api.nvim_tabpage_list_wins(0)
-- cambiar el buffer que muestra una ventana, sin moverte a ella
api.nvim_win_set_buf(win, otro_buf)
La relación inversa también es útil: vim.fn.bufwinid(buf) devuelve la primera ventana de la pestaña actual que muestra ese buffer, o -1 si ninguna. Para buscar en todas las pestañas, vim.fn.win_findbuf(buf) devuelve la lista completa.
flowchart TD T[Pestana] --> W1[Ventana A] T --> W2[Ventana B] T --> W3[Ventana flotante] W1 --> B1[Buffer 1] W2 --> B1 W3 --> B2[Buffer scratch] B1 --> N[Un buffer visto por dos ventanas] style N fill:#a6e3a1,color:#11111b style W3 fill:#89b4fa,color:#11111b
Que dos ventanas compartan buffer tiene una consecuencia que sorprende: el cursor, el desplazamiento vertical y el estado de los pliegues son de la ventana, no del buffer. Por eso puedes leer el mismo archivo por dos sitios distintos a la vez.
nvim_open_win abre las dos familias
Durante años hubo dos mundos separados: los comandos :split y :vsplit para divisiones, y nvim_open_win para flotantes. Hoy nvim_open_win cubre ambos. Su segundo argumento decide si entras en la ventana nueva; el tercero es la configuración.
Para una división, el campo split indica el lado y win la ventana respecto a la cual se abre.
local win = api.nvim_open_win(buf, true, {
split = "right", -- left, right, above, below
win = 0, -- respecto a la ventana actual
width = 40,
})
Para una flotante, el campo relative fija el sistema de coordenadas: editor para la pantalla completa, win para otra ventana, cursor para la posición del cursor y mouse para el puntero.
local ancho, alto = 60, 12
local win = api.nvim_open_win(buf, true, {
relative = "editor",
width = ancho,
height = alto,
row = math.floor((vim.o.lines - alto) / 2) - 1,
col = math.floor((vim.o.columns - ancho) / 2),
style = "minimal",
border = "rounded",
title = " resultados ",
zindex = 50,
})
El campo style con valor minimal desactiva de golpe números, signos, pliegues y la columna de conceal: es el punto de partida correcto para cualquier superficie que no sea código editable. zindex decide qué flotante tapa a cuál. Y noactivate junto con focusable desactivado sirve para ventanas informativas que el usuario nunca debe poder alcanzar con <C-w>w.
Existe un campo silencioso pero decisivo: noautocmd. Con él activado, abrir la ventana no dispara BufEnter, BufWinEnter ni WinEnter. Para una previsualización que se abre y cierra decenas de veces por segundo, evitar esa cascada es la diferencia entre fluido y pegajoso.
Reconfigurar, medir y cerrar
Una ventana abierta no es inmutable. nvim_win_set_config acepta la misma tabla que nvim_open_win y aplica los cambios en caliente, incluida la conversión de flotante a división y viceversa.
-- mover y agrandar una flotante ya existente
api.nvim_win_set_config(win, { relative = "editor", row = 2, col = 4,
width = 80, height = 20 })
-- convertir una flotante en division a la derecha
api.nvim_win_set_config(win, { split = "right", win = 0 })
nvim_win_get_config es además la forma canónica de saber si una ventana es flotante: si el campo relative de la tabla devuelta es la cadena vacía, no lo es.
local function es_flotante(w)
return api.nvim_win_get_config(w).relative ~= ""
end
El tamaño se lee y escribe con nvim_win_get_width, nvim_win_set_width y sus equivalentes de altura. El problema es que Neovim reequilibra las divisiones cada vez que se abre o cierra alguna, y tu panel de cuarenta columnas acaba con veinte. Las opciones winfixwidth y winfixheight marcan la ventana como exenta de ese reequilibrio.
vim.wo[win].winfixwidth = true
api.nvim_win_set_width(win, 40)
Para cerrar hay dos verbos con semánticas distintas. nvim_win_close cierra la ventana y, según bufhidden, puede llevarse el buffer por delante. nvim_win_hide la cierra conservando el buffer con más cuidado. Ambos aceptan un force para ventanas con cambios sin guardar.
if api.nvim_win_is_valid(win) then
api.nvim_win_close(win, true)
end
La comprobación de validez es obligatoria, no defensiva de más: entre que guardaste el winid y que lo usas pueden haber pasado un :only, un :qa parcial o un cierre del propio usuario. Y cerrar la última ventana de la última pestaña es un error, no una salida del editor.
Opciones locales de ventana
Aquí vive la sutileza más incomprendida de toda la API. Muchas opciones —number, wrap, cursorline, signcolumn, foldcolumn, winbar, winhighlight— son locales de ventana. Se fijan con vim.wo[win] o con la forma explícita.
api.nvim_set_option_value("number", false, { win = win })
api.nvim_set_option_value("wrap", false, { win = win })
api.nvim_set_option_value("winhighlight", "Normal:NormalFloat", { win = win })
La trampa es que casi todas tienen además un valor global que actúa de plantilla. Cuando abres una ventana nueva, las opciones locales se heredan de la ventana desde la que divides, no de tu configuración. Y cuando escribes con vim.wo[win].number = false estás fijando el valor local, mientras que el global sigue intacto para las ventanas futuras.
Por eso conviene fijar las opciones después de abrir la ventana y sobre el winid concreto, nunca antes ni sobre la actual esperando que se propague. Y por eso la forma con scope explícito es la más honesta cuando quieres desambiguar.
api.nvim_set_option_value("list", true, { win = win, scope = "local" })
local valor = api.nvim_get_option_value("list", { win = win })
Existe un tercer nivel, el win-local frente al buf-local heredado: opciones como foldmethod son de ventana, pero se recalculan al cambiar de buffer. Si tu panel cambia de contenido y pierde la configuración visual, casi siempre es este mecanismo.
La API te entrega ventanas como identificadores planos, y eso invita a pensar la pantalla como una colección. No lo es: internamente Neovim mantiene un árbol de contenedores, donde cada nodo divide horizontal o verticalmente y las hojas son las ventanas visibles. nvim_open_win con split inserta un nodo; cerrar una ventana colapsa su padre y redistribuye el espacio entre los hermanos. Todo el comportamiento aparentemente caprichoso del redimensionado —por qué tu panel se encoge al abrir un quickfix, por qué winfixwidth funciona a veces y otras no, por qué equalalways reordena cosas que no tocaste— se vuelve predecible en cuanto piensas en el árbol y no en la lista. Puedes verlo tú mismo con vim.fn.winlayout, que devuelve exactamente esa estructura anidada. Un plugin que negocia bien el espacio no es el que fuerza tamaños, sino el que declara sus restricciones y deja que el árbol resuelva.
- Escribe una función que abra una flotante centrada del sesenta por ciento del ancho y la mitad del alto.
- Añade
borderredondeado,stylemínimo y un título, y mapeaqlocal al buffer para cerrarla. - Detecta si ya está abierta con
nvim_win_is_validy conviértela en un interruptor. - Añade un autocomando
VimResizedque la recentre connvim_win_set_config. - Investiga: abre una división lateral con
winfixwidth, abre luego un quickfix y comprueba si conserva el ancho.