wandres.dev
CONSTRUIR INTERFAZ · ventanas flotantes y UI

Statusline y winbar propias sin matar el rendimiento

El lenguaje de plantillas de la linea de estado y cuando lo evalua el editor. Codigos de formato, expresiones incrustadas, resaltado por tramos, composicion desde Lua con una funcion global, ambito global frente a por ventana, la winbar como gemela superior, y las tres tecnicas que evitan que una barra bonita degrade cada redibujado.

⏱ 24 min

La línea de estado es el único elemento de interfaz que el usuario mira cientos de veces por sesión sin darse cuenta, y es también el más fácil de estropear. Su naturaleza es peculiar: no es una ventana ni un buffer, sino una cadena de plantilla que el editor reevalúa en cada redibujado, muchas veces por segundo mientras te mueves. Esa reevaluación constante es lo que le permite mostrar información viva, y también lo que convierte cualquier operación cara metida ahí dentro en una degradación medible del editor entero. Entender cuándo se evalúa, en qué contexto y con qué frecuencia es lo que separa una barra que informa de una barra que estorba.

🎯 Al terminar esta lección sabrás
  • Leer y escribir el lenguaje de plantillas de statusline, incluidos los tramos de resaltado.
  • Componer la barra desde Lua con una función global y decidir dónde se evalúa cada parte.
  • Distinguir el ámbito global de la barra del ámbito por ventana, y aplicar lo mismo a winbar.
  • Identificar y eliminar el trabajo caro que se ejecuta en cada redibujado.

Un lenguaje de plantilla que se evalúa sin parar

El valor de la opción statusline es una cadena con dos clases de contenido: texto literal y códigos de formato que empiezan por un signo de porcentaje. El editor la procesa cada vez que necesita repintar la línea, sustituyendo cada código por el dato correspondiente.

Código Significado
%f ruta del fichero relativa al directorio actual
%m marca de modificado
%r marca de solo lectura
%y tipo de fichero
%l y %c línea y columna del cursor
%p porcentaje de avance en el buffer
%= punto de separación: lo anterior va a la izquierda, lo siguiente a la derecha
%< punto por donde truncar si no cabe

Hay dos formas de incrustar una expresión, y confundirlas es la causa del error de rendimiento más frecuente. La forma con llaves simples evalúa la expresión y escapa el resultado, tratándolo como texto plano; es lo que quieres para datos que vienen de fuera y podrían contener un signo de porcentaje. La forma con llaves y porcentajes interiores evalúa la expresión y reinterpreta el resultado como plantilla, lo que permite que la función devuelva a su vez códigos de formato y grupos de resaltado. Esta segunda forma es la que necesitas para componer desde Lua, y es más eficiente porque evalúa una sola vez por redibujado en lugar de una vez por ventana en algunos contextos.

El resaltado se aplica por tramos, no por caracteres: el código de grupo abre un tramo que dura hasta el siguiente código de grupo o hasta el final. Los grupos deben ser numéricos y se corresponden con User1 hasta User9, o bien puedes usar la forma que nombra un grupo de resaltado arbitrario y cerrarlo con la marca de restauración.

-- Ejemplo directo, sin Lua de por medio
vim.o.statusline = table.concat({
  "%#StatusLineModo# %{mode()} ",   -- tramo con grupo propio
  "%#StatusLine# %<%f %m%r",        -- ruta truncable, marcas
  "%=",                             -- separador
  "%y  %l:%c  %p%% ",               -- tipo, posicion, porcentaje
})
💡
El porcentaje literal se escribe duplicado

Dentro de la plantilla, un signo de porcentaje suelto abre un código de formato. Para mostrarlo como carácter hay que duplicarlo, y ese es el motivo del par final del ejemplo anterior: %p da el número y el par siguiente dibuja el símbolo.

Componer desde Lua

Escribir la barra entera como una cadena de códigos deja de ser sostenible en cuanto quieres lógica condicional. La solución idiomática es delegar en una función de Lua expuesta en el espacio global y llamarla desde la plantilla con la forma que reinterpreta el resultado.

local M = {}

local ICONO_MODO = {
  n = "NORMAL", i = "INSERTAR", v = "VISUAL", V = "VISUAL LIN",
  R = "REEMPLAZAR", c = "COMANDO", t = "TERMINAL",
}

function M.render()
  local modo = ICONO_MODO[vim.api.nvim_get_mode().mode:sub(1, 1)] or "OTRO"
  local partes = {
    "%#StatusLineModo# ", modo, " %#StatusLine# ",
    "%<%f %m%r",
    "%=",
    M.diagnosticos(),                       -- ya cacheado, ver mas abajo
    " %y  %l:%c  %P ",
  }
  return table.concat(partes)
end

_G.MiBarra = M                               -- accesible desde la plantilla
vim.o.statusline = "%{%v:lua.MiBarra.render()%}"

La expresión v:lua es el puente entre el lenguaje de comandos de Vim y Lua, y permite invocar cualquier función accesible desde el espacio global sin definir un envoltorio en el lenguaje antiguo. Es la vía recomendada y la que usan las barras modernas por debajo.

Un detalle que sorprende: la función se ejecuta con la ventana actual como contexto, no necesariamente con la ventana que se está dibujando. Para saber para qué ventana estás componiendo existe la variable que expone el identificador de la ventana en curso, y consultarla es obligatorio si quieres que cada división muestre su propio nombre de fichero en lugar del de la ventana activa.

Winbar: la misma máquina, arriba

La winbar es una línea que se dibuja en la parte superior de cada ventana y usa exactamente el mismo lenguaje de plantilla. Todo lo que sabes de la línea de estado se aplica igual. Sus diferencias son de propósito y de ámbito.

📊

Statusline

Con laststatus a 3 hay una sola barra global para todo el editor, lo que libera una línea por división y evita repetir la misma información en cada ventana. Es la opción moderna por defecto.

🧭

Winbar

Vive dentro de cada ventana y se presta a información local: la ruta del fichero de esa división, el contexto sintáctico donde está el cursor, el nombre de la función que lo contiene.

-- Solo en ventanas normales con fichero: nunca en flotantes ni en paneles
vim.api.nvim_create_autocmd({ "BufWinEnter", "WinNew" }, {
  callback = function(args)
    local win = vim.api.nvim_get_current_win()
    local flotante = vim.api.nvim_win_get_config(win).relative ~= ""
    local especial = vim.bo[args.buf].buftype ~= ""
    vim.wo[win].winbar = (flotante or especial) and "" or "%{%v:lua.MiBarra.winbar()%}"
  end,
})

Esa comprobación no es opcional. Poner la winbar globalmente la dibuja también dentro de tus ventanas flotantes, dentro del terminal integrado y dentro de los paneles de plugins, donde roba una línea y descuadra todos los cálculos de altura de la lección anterior.

El coste que no ves

La barra se reevalúa en cada redibujado: al mover el cursor, al escribir, al cambiar de ventana, al llegar un mensaje asíncrono. Cualquier trabajo que hagas dentro se multiplica por esa frecuencia. Hay tres operaciones que jamás deben ejecutarse en la evaluación y que aparecen constantemente en configuraciones copiadas: lanzar un proceso externo para preguntar la rama de git, recorrer el árbol sintáctico completo para averiguar el contexto, y consultar el estado de un servidor de lenguaje construyendo tablas nuevas.

El principio correcto es la inversión del flujo: la barra no pregunta, la barra lee. El dato se calcula cuando cambia —en respuesta a un evento— y se guarda en una variable; la plantilla solo la concatena.

local cache = { diag = "", rama = "" }

-- Se recalcula cuando cambian los diagnosticos, no en cada redibujado
vim.api.nvim_create_autocmd("DiagnosticChanged", {
  callback = function(args)
    local c = vim.diagnostic.count(args.buf)
    local e = c[vim.diagnostic.severity.ERROR] or 0
    local w = c[vim.diagnostic.severity.WARN] or 0
    cache.diag = (e + w > 0) and string.format(" E%d W%d ", e, w) or ""
    vim.cmd.redrawstatus()
  end,
})

function M.diagnosticos() return cache.diag end

La segunda técnica es amortiguar lo que sí depende del cursor pero es caro, como el contexto sintáctico de la winbar: un temporizador que reinicia su cuenta con cada movimiento y calcula al cesar convierte una operación por pulsación en una operación por pausa. La tercera es acotar el trabajo por ámbito: nada de recorrer todos los buffers ni todos los clientes cuando la barra habla de uno solo.

flowchart LR
a[Evento raro: DiagnosticChanged, BufEnter, evento de git] --> b[Calcular el dato caro una vez]
b --> c[Guardar cadena ya formateada en cache]
d[Redibujado muy frecuente] --> e[Evaluar la plantilla]
e --> f[Leer cache y concatenar]
f --> g[Dibujar la barra]
style b fill:#89b4fa,color:#11111b
style f fill:#a6e3a1,color:#11111b

Para medir en vez de suponer, el propio editor ofrece una función que evalúa una plantilla y devuelve el resultado con su desglose, lo que permite cronometrar la composición completa fuera del bucle de dibujado. Si una evaluación tarda más de un milisegundo, se nota al desplazarse rápido, y la culpa casi siempre está en una de las tres operaciones prohibidas.

La barra de estado es el único código tuyo dentro del bucle de dibujado

Hay una razón estructural por la que las líneas de estado hechas en casa arruinan la experiencia con tanta frecuencia, y no tiene que ver con la habilidad de quien las escribe: es el único punto donde tu código se ejecuta dentro del lazo de repintado del editor, sincrónicamente, sin posibilidad de aplazarse. Todo lo demás que escribes vive en el mundo de los eventos: un autocomando se dispara al guardar, un comando cuando lo invocas, un manejador cuando llega una respuesta. Si cualquiera de esos tarda cincuenta milisegundos, nadie lo percibe. Cincuenta milisegundos en la evaluación de la barra son un editor roto, porque esa función se ejecuta entre la pulsación de la tecla y la aparición del carácter en pantalla, en el único tramo del sistema donde el usuario tiene una expectativa de latencia medida en unidades de percepción y no en unidades de reloj. Esa asimetría convierte la línea de estado en un laboratorio insuperable de una lección que se aplica a toda la ingeniería de interfaces: la ruta caliente no es un lugar donde se optimiza, es un lugar donde no se calcula. La disciplina correcta no consiste en hacer rápido lo que la barra necesita; consiste en sacar el cálculo de la barra por completo, moverlo al momento en que el dato cambia —que es órdenes de magnitud menos frecuente que el redibujado—, y dejar en el camino crítico únicamente una lectura de memoria y una concatenación. Es exactamente el mismo movimiento que hay detrás de la memoización, de las vistas materializadas de una base de datos, del renderizado precalculado de un motor gráfico y de los índices invertidos de un buscador: reconocer que hay una frontera entre lo que cambia y lo que se consulta, y pagar el coste del lado que cambia menos. Cuando lo interiorizas aquí, con un elemento tan pequeño que puedes medirlo entero en una tarde, dejas de escribirlo como una plantilla que consulta el mundo y empiezas a escribirlo como una proyección que el mundo actualiza. Y esa inversión, una vez vista, ya no se olvida.

⚔️ Una barra que informa y no cuesta
  1. Escribe una statusline en Lua con modo, ruta truncable, separador y posición, y actívala con v:lua.
  2. Añade un tramo de diagnósticos alimentado por DiagnosticChanged y verifica que la función de plantilla no consulta nada.
  3. Pon laststatus a 3 y contrasta el resultado con el valor 2, argumentando qué información deja de tener sentido repetir.
  4. Añade una winbar con el nombre del fichero, excluyéndola de flotantes y de buffers especiales.
  5. Cronometra mil evaluaciones de tu función de barra; después mete a propósito una llamada a un proceso externo dentro y vuelve a medir para cuantificar el daño.