wandres.dev
AUTOCOMANDOS · reaccionar a eventos

Patrones y filtros: acotar exactamente dónde reacciona

La semántica real del emparejamiento de patrones y su regla oculta sobre la barra, el ámbito de buffer como alternativa superior, y qué significan de verdad once y nested cuando el bus se realimenta a sí mismo.

⏱ 18 min

Un autocomando mal acotado no falla: hace su trabajo demasiadas veces, en buffers donde no debía, y su coste aparece disperso por toda la sesión como una lentitud que nadie sabe atribuir. Acotar es la mitad de la ingeniería de este nivel. Neovim ofrece cuatro instrumentos de filtrado —patrón, buffer, cardinalidad y reentrada— y cada uno responde a una pregunta distinta que conviene no confundir.

🎯 Al terminar esta lección sabrás
  • Aplicar la regla de la barra en el emparejamiento de patrones de archivo.
  • Elegir entre filtrar por patrón y acotar por buffer con criterio.
  • Usar once para reacciones de cardinalidad uno y entender su relación con el borrado propio.
  • Razonar sobre nested y la realimentación del bus de eventos.

Patrón: el arte de casar rutas

El patrón se compara siempre contra args.match, nunca contra el nombre visible del archivo, y su sintaxis no es una expresión regular: es el emparejamiento de nombres de archivo de Vim, donde el asterisco cubre cualquier cadena incluidas las barras y la interrogación cubre un solo carácter.

La regla que casi nadie conoce es esta: si el patrón contiene una barra, se compara contra la ruta completa del archivo; si no la contiene, se compara solo contra el nombre final. Esa asimetría explica de golpe la mitad de los autocomandos que “no funcionan”.

local grupo = vim.api.nvim_create_augroup("dios_patrones", { clear = true })

-- Sin barra: casa contra el nombre final, en cualquier directorio
vim.api.nvim_create_autocmd("BufWritePre", {
  group = grupo,
  pattern = "*.lua",
  callback = function(args) vim.print("cualquier Lua: " .. args.file) end,
})

-- Con barra: casa contra la ruta completa expandida
vim.api.nvim_create_autocmd("BufWritePre", {
  group = grupo,
  pattern = "*/nvim/lua/*.lua",
  callback = function(args) vim.print("solo mi config: " .. args.file) end,
})

-- Lista de patrones: la union de todos ellos
vim.api.nvim_create_autocmd("BufWritePre", {
  group = grupo,
  pattern = { "*.ts", "*.tsx", "*.js", "*.jsx" },
  callback = function(args) vim.lsp.buf.format({ bufnr = args.buf }) end,
})

El valor por defecto del patrón es el asterisco solo, es decir, todo. Dejarlo implícito en eventos frecuentes como BufEnter o CursorHold es la causa más habitual de degradación de rendimiento en configuraciones grandes: el callback se invoca para cada buffer de la sesión, incluidos los auxiliares, los del explorador de archivos y los del terminal.

En los eventos que no son de archivo el patrón cambia de dominio por completo. En FileType casa contra el nombre del tipo, de modo que pattern = "lua" es correcto y pattern = "*.lua" jamás casará con nada. En User casa contra el nombre del evento propio. En TermOpen o CmdlineEnter casa contra otros valores documentados en la ayuda de cada evento. Confundir dominios produce autocomandos silenciosamente muertos, que es el peor tipo de fallo: no hay error, simplemente no ocurre nada.

⚠️
FileType no es un filtro sobre extensiones

Un archivo .h puede ser C o C plus plus. Un .ts puede ser TypeScript o un script de otra herramienta. Un buffer sin nombre puede tener tipo asignado por un plugin. El tipo de archivo es el resultado de un proceso de detección con heurísticas de contenido, no una función de la extensión. Filtrar por FileType con el nombre del tipo es semánticamente correcto; filtrar por extensión en BufReadPost es una aproximación que fallará precisamente en los casos difíciles.

El buffer como ámbito

Existe un filtro más fuerte que cualquier patrón: la clave buffer, que ata el autocomando a un buffer concreto por su número. Un autocomando de buffer muere con el buffer, sin que tengas que limpiarlo, y no se evalúa jamás para ningún otro.

pattern y buffer son mutuamente excluyentes. No son dos filtros que se combinen: son dos formas alternativas de expresar el ámbito, y pasar ambos es un error.

-- Patron de dos fases: FileType decide, el autocomando de buffer actua
vim.api.nvim_create_autocmd("FileType", {
  group = vim.api.nvim_create_augroup("dios_markdown", { clear = true }),
  pattern = { "markdown", "text", "gitcommit" },
  callback = function(args)
    vim.bo[args.buf].textwidth = 80
    vim.wo.spell = true

    -- A partir de aqui, solo para ESTE buffer
    vim.api.nvim_create_autocmd("InsertLeave", {
      buffer = args.buf,
      desc = "Guarda la prosa al salir de insercion",
      callback = function(ev)
        if vim.bo[ev.buf].modified and vim.api.nvim_buf_get_name(ev.buf) ~= "" then
          vim.cmd.write({ mods = { silent = true } })
        end
      end,
    })
  end,
})

Este es el patrón arquitectónico más importante del nivel: usar un evento de clasificación para instalar reacciones acotadas. En vez de registrar un autocomando global sobre InsertLeave que comprueba en cada disparo si el buffer es de prosa, registras uno por buffer justo cuando sabes que lo es. El coste se paga una vez en la detección en lugar de en cada pulsación, y la lógica queda expresada donde corresponde. LspAttach funciona con la misma forma: el evento clasifica, y dentro instalas atajos y comportamientos ligados a args.buf.

once y nested: cardinalidad y reentrada

Los dos últimos filtros no restringen dónde sino cuántas veces y bajo qué reglas de reentrada.

once

El autocomando se ejecuta y se borra a sí mismo tras el primer disparo que case. Cardinalidad exactamente uno, decidida en el momento del registro.

🧩

Borrado condicional

Devolver true desde el callback también lo elimina, pero la decisión se toma en tiempo de ejecución. Es once con guarda.

🧵

nested

Permite que las acciones del callback disparen a su vez otros autocomandos. Sin él, el bus queda mudo mientras tu callback corre.

🗓️

Profundidad máxima

El anidamiento tiene un límite duro. Al alcanzarlo Neovim aborta la cadena en vez de colgarse, pero el estado resultante rara vez es el que esperabas.

nested merece una explicación cuidadosa porque su comportamiento por defecto es contraintuitivo. Mientras se ejecuta un callback, Neovim suprime la emisión de nuevos autocomandos provocados por las acciones de ese callback. Es una protección deliberada contra la recursión: sin ella, un autocomando de BufWritePre que escriba el buffer se llamaría a sí mismo indefinidamente.

flowchart TD
E[Evento original] --> C[Se ejecuta tu callback]
C --> A[El callback abre un buffer nuevo]
A --> N1[Sin nested: BufEnter no se emite]
A --> N2[Con nested: BufEnter si se emite]
N2 --> O[Se ejecutan los autocomandos de BufEnter]
O --> L[Limite de profundidad si la cadena se realimenta]
style N1 fill:#89b4fa,color:#11111b
style N2 fill:#f9e2af,color:#11111b
style L fill:#f38ba8,color:#11111b

La consecuencia práctica es que un autocomando que abre un archivo, cambia de buffer o modifica el tipo no provocará las reacciones que esperarías, salvo que declares nested = true. Y en cuanto lo declares, asumes la responsabilidad de que la cadena termine.

-- Recargar la vista del explorador tras escribir, sin cadenas infinitas
vim.api.nvim_create_autocmd("BufWritePost", {
  group = vim.api.nvim_create_augroup("dios_recarga", { clear = true }),
  nested = true,
  desc = "Permite que la recarga dispare deteccion de tipo",
  callback = function(args)
    if vim.bo[args.buf].filetype == "" then
      vim.cmd("filetype detect")
    end
  end,
})
Acotar no es optimizar: es definir el significado de tu reacción

Es tentador leer los patrones como un asunto de rendimiento —filtrar para no gastar ciclos— pero esa lectura se queda corta y produce configuraciones frágiles. El ámbito de un autocomando es parte de su semántica, no un adorno posterior. Cuando escribes un autocomando global sobre BufEnter con una guarda dentro del callback que comprueba el tipo de archivo, has escrito dos cosas distintas mezcladas: una suscripción universal y una condición imperativa. El sistema no puede saber que solo te interesan los buffers de Python; solo tú lo sabes, y lo sabes dentro de una función que el motor no puede inspeccionar. Cuando en cambio lo expresas como FileType con patrón python que instala un autocomando de buffer, la restricción se ha vuelto declarativa y visible desde fuera: aparece en el listado de autocomandos, la puede leer otra persona, la puede leer tu yo futuro y la puede aprovechar el propio motor para no invocarte. La diferencia es la misma que hay entre un índice en una base de datos y un filtro en el código cliente: ambos devuelven el mismo resultado, pero solo uno le dice al sistema qué estás buscando. Toda configuración madura tiende hacia el mismo punto: cada vez menos guardas dentro de los callbacks y cada vez más precisión en el registro. Cuando llegas ahí, tus autocomandos dejan de ser funciones que deciden si actuar y pasan a ser afirmaciones sobre cuándo actúan, y el conjunto se vuelve inspeccionable sin ejecutar una sola línea.

⚔️ Afina el alcance
  1. Registra dos autocomandos con *.lua y con un patrón que incluya una barra hacia tu directorio de configuración, y comprueba con archivos de dos proyectos cuál dispara en cada caso.
  2. Intenta filtrar FileType con *.lua, verifica que nunca dispara y corrígelo al patrón correcto.
  3. Sustituye un autocomando global con guarda interna por la pareja FileType más autocomando de buffer y compara los listados resultantes.
  4. Escribe un autocomando con once y otro que se borre devolviendo true bajo condición, y describe cuándo es preferible cada uno.
  5. Provoca deliberadamente una cadena anidada con nested = true hasta alcanzar el límite de profundidad y explica qué gesto rompe el ciclo.