Saltar al contenido

Ventanas desde Lua: abrir, cerrar, mover y medir

La ventana como viewport sobre un buffer: nvim_open_win unifica divisiones y flotantes en una sola función, nvim_win_set_config reconfigura en caliente y convierte una familia en la otra, el cierre seguro distingue close de hide, y las opciones locales de ventana esconden la trampa de la plantilla que la lección 8.2 dejó armada.

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. En la lección 1.1 esa independencia era un modelo mental para navegar; desde Lua se vuelve la interfaz con la que construyes cualquier disposición, y una sola función —nvim_open_win— abre las dos familias de ventanas que existen: las divisiones que reparten la pantalla y las flotantes que se dibujan encima.

  • 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, y decidir con enter quién recibe el foco.
  • Reconfigurar en caliente con nvim_win_set_config, incluida la conversión entre flotante y división.
  • Cerrar con criterio: nvim_win_close frente a nvim_win_hide, y por qué solo una acepta forzar.
  • Fijar opciones locales de ventana sin dejar plantillas envenenadas, con el índice doble de la lección 8.2.

La ventana como viewport

Cada ventana tiene un identificador entero, el winid, único durante la sesión y nunca reciclado para otra ventana. 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.

LUA
local api = vim.apilocal win = api.nvim_get_current_win()local buf = api.nvim_win_get_buf(win)-- todas las ventanas del editor, de todas las pestanaslocal todas = api.nvim_list_wins()-- solo las de la pestana actuallocal aqui = api.nvim_tabpage_list_wins(0)-- cambiar el buffer que muestra una ventana, sin moverte a ellaapi.nvim_win_set_buf(win, otro_buf)

Un matiz de esa última llamada que la firma no delata: nvim_win_set_buf ejecuta los autocomandos BufEnter y BufLeave como efecto colateral documentado. No te mueve de ventana, pero para los plugins que escuchan esos eventos es como si el usuario hubiera cambiado de buffer. Lo retomaremos en la lección 10.4, donde los efectos colaterales del contexto son el tema entero.

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.

Que dos ventanas compartan buffer tiene una consecuencia que sorprende la primera vez: el cursor, el desplazamiento vertical y el estado de los pliegues pertenecen a la ventana, no al buffer. La propia documentación de nvim_win_get_cursor lo subraya: ventanas distintas sobre el mismo buffer tienen posiciones de cursor independientes. Por eso puedes leer el mismo archivo por dos sitios 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 solo para flotantes. Desde la 0.10, nvim_open_win cubre ambos. Recibe tres argumentos: el buffer a mostrar, un booleano enter que decide si la ventana nueva se convierte en la actual, y la tabla de configuración, cuyo contenido determina la familia.

Para una división, basta el lado y la referencia:

LUA
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 sistema de coordenadas y la geometría:

LUA
local ancho, alto = 60, 12local 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 ",  title_pos = "center",  zindex   = 50,})

style = "minimal" desactiva de golpe números, columna de signos, pliegues, cursorline y demás decoración: es el punto de partida correcto para cualquier superficie que no sea código editable. border acepta los estilos con nombre —"rounded", "single", "double"— o una lista de caracteres, y desde la 0.11, si no lo indicas, hereda el valor de la opción global winborder. title y footer admiten una cadena o una lista de parejas [texto, grupo_de_resaltado], con title_pos y footer_pos para alinearlos. Y zindex decide qué flotante tapa a cuál; los elementos del propio editor tienen valores reservados altos, como el menú de completado en 100.

Dos campos gobiernan la interacción. focusable a falso saca la ventana de la rotación de <C-w>w: el usuario no puede aterrizar en ella por accidente, aunque tu código sí puede entrar con nvim_set_current_win. Y mouse, que por defecto copia el valor de focusable, decide si los clics interactúan con la ventana o la atraviesan. La combinación de ambos a falso es la receta de las superficies puramente informativas.

Existe un campo silencioso pero decisivo: noautocmd. Con él activado, la llamada entera se ejecuta con los autocomandos bloqueados: ni WinNew, ni BufWinEnter, ni nada. Para una previsualización que se abre y se cierra decenas de veces, evitar esa cascada es la diferencia entre fluido y pegajoso. Es una decisión de nacimiento: nvim_win_set_config no puede cambiarlo después.

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, con dos reglas: las claves ausentes no se tocan, y para mover una flotante debes pasar relative junto con row y col, porque se reconfiguran juntos. La conversión entre familias funciona en ambos sentidos, y es la base de trucos como «expandir el panel lateral a flotante centrada».

LUA
-- mover y agrandar una flotante ya existenteapi.nvim_win_set_config(win, { relative = "editor", row = 2, col = 4,                               width = 80, height = 20 })-- convertir una flotante en division a la derechaapi.nvim_win_set_config(win, { split = "right", win = 0 })

nvim_win_get_config devuelve la configuración vigente, y es la forma canónica de saber si una ventana es flotante: si su campo relative es la cadena vacía, no lo es. Este es el criterio que usa la propia documentación, y el que verás en todos los plugins serios.

LUA
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; para geometría completa, vim.fn.getwininfo(win) devuelve en una sola consulta la primera y última línea visibles, la posición en pantalla y textoff, el ancho que roban el número de línea y la columna de signos. El problema clásico es que Neovim reequilibra las divisiones cada vez que se abre o cierra alguna, y tu panel de cuarenta columnas amanece con veinte. Las opciones winfixwidth y winfixheight marcan la ventana como exenta de ese reequilibrio.

LUA
vim.wo[win].winfixwidth = trueapi.nvim_win_set_width(win, 40)

Para cerrar hay dos verbos con semánticas distintas, y aquí conviene precisión porque circula mucha versión confusa. nvim_win_close(win, force) es :close con identificador: cierra la ventana, y el buffer queda a merced de su bufhidden, como viste en la lección 10.2. Su parámetro force se comporta como :close! y permite cerrar la última ventana de un buffer con cambios sin guardar. nvim_win_hide(win) es :hide con identificador: cierra la ventana garantizando que el buffer pasa a oculto en vez de descargarse —salvo que su propio bufhidden diga unload, delete o wipe, que siempre tiene la última palabra—. Y no acepta force: no lo necesita, porque esconder un buffer nunca pierde cambios.

LUA
if api.nvim_win_is_valid(win) then  api.nvim_win_close(win, true)end

Valida el winid siempre, y cuenta con el error de la última ventana

Entre que guardaste el winid y que lo usas pueden haber pasado un :only, un cierre a mano o un autocomando ajeno: nvim_win_is_valid antes de tocar no es paranoia, es el precio de guardar identificadores. Y hay un caso que ninguna validación evita: cerrar la última ventana de la última pestaña es un error (E444), no una salida del editor. Si tu código de limpieza puede ejecutarse en ese escenario —por ejemplo, en un autocomando de cierre—, envuélvelo en pcall.

Opciones locales de ventana

Aquí vive la sutileza más incomprendida de toda la API, y ya tienes la teoría para entenderla: es exactamente el sistema de ámbitos de la lección 8.2 aplicado a ventanas. Opciones como number, wrap, cursorline, signcolumn, winbar o winhighlight son locales de ventana, pero casi todas tienen además una copia «global de ventana» que actúa de plantilla para las ventanas y buffers futuros.

La trampa concreta: escribir vim.wo[win].number = false se comporta como :set, es decir, fija el valor de esa ventana y deja la plantilla cambiada, de modo que el siguiente buffer que se muestre en esa ventana —o una división nacida de ella— hereda tu configuración de panel. Para un panel dedicado quieres el equivalente de :setlocal, y eso es el índice doble o el ámbito explícito:

LUA
-- como :setlocal, sin envenenar la plantillavim.wo[win][0].number = falsevim.wo[win][0].wrap = false-- lo mismo, con la API y ambito declaradoapi.nvim_set_option_value("winhighlight", "Normal:NormalFloat",                          { win = win, scope = "local" })

Por eso conviene fijar las opciones después de abrir la ventana y sobre el winid concreto, nunca antes sobre la ventana actual esperando que se propaguen. Las ventanas nuevas heredan de la ventana desde la que se crean, no de tu configuración, y style = "minimal" existe precisamente para no depender de esa herencia.

La disposición es un árbol, no una lista

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 —las flotantes viven fuera del árbol, en una capa superior ordenada por zindex, y por eso no participan del reparto—. 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 el quickfix de la lección 1.6, por qué winfixwidth protege de unos reequilibrios y no de otros, 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 de row, col y hojas. Un plugin que negocia bien el espacio no es el que fuerza tamaños en cada evento, sino el que declara sus restricciones —tamaño fijo aquí, flexible allá— y deja que el árbol resuelva. Es la misma lección que las interfaces declarativas llevan décadas enseñando: describe lo que quieres, no pelees cada píxel.

El primer 0 es el buffer actual; el false deja tu cursor donde está. La misma línea con enter = true te llevaría a la ventana nueva.

El criterio canónico: relative vacío significa división, no vacío significa flotante. Pruébalo también dentro de un flotante de ayuda de un plugin.

nvim_win_set_config cambia de familia sin cerrar ni reabrir: el buffer, el cursor y el historial de la ventana se conservan. El camino de vuelta es pasar split y win. Necesitas otra división porque no puede quedar solo una flotante en pantalla: convertir la última ventana normal es un error.

Con dos divisiones verás algo como { 'row', { { 'leaf', id }, { 'leaf', id } } }. Cada leaf es un winid; los nodos row y col son los contenedores que reparten el espacio.

Una flotante centrada y honesta

  1. Escribe una función que abra una flotante centrada del sesenta por ciento del ancho y la mitad del alto, calculando row y col desde vim.o.columns y vim.o.lines.
  2. Añade border redondeado, style mínimo y un título centrado con title_pos, y mapea q local al buffer para cerrarla con nvim_win_close.
  3. Detecta si ya está abierta con nvim_win_is_valid sobre el id guardado y conviértela en un interruptor: la misma llamada abre y cierra.
  4. Añade un autocomando VimResized que la recentre con nvim_win_set_config, recordando pasar relative, row y col juntos.
  5. Configura sus opciones con vim.wo[win][0], luego repite a propósito con vim.wo[win], abre otro fichero en esa ventana y comprueba la plantilla envenenada.
  6. Investiga: abre una división lateral con winfixwidth, abre después un quickfix con :copen y comprueba si tu panel conserva el ancho; explica lo que ves con vim.fn.winlayout().