Patrones y filtros: acotar exactamente dónde reacciona
La semántica real del emparejamiento de patrones y su regla oculta sobre la barra, por qué el dominio del patrón cambia según el evento, el ámbito de buffer como alternativa superior, y qué significan de verdad once y nested cuando el bus se realimenta a sí mismo.
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 —patrón, buffer, cardinalidad y reentrada— y cada uno responde a una pregunta distinta que conviene no confundir: dónde, para quién, cuántas veces y con qué reglas de encadenamiento.
- Aplicar la regla de la barra en el emparejamiento de patrones de archivo.
- Reconocer que el dominio del patrón cambia según el evento: rutas, tipos, nombres.
- Elegir entre filtrar por patrón y acotar por
buffer, y saber por qué son excluyentes. - Usar
oncepara reacciones de cardinalidad uno y distinguirlo del borrado porreturn true. - Razonar sobre
nestedy la realimentación del bus, con su límite de 10 niveles.
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 (:h autocmd-patterns). El asterisco cubre cualquier cadena —incluidas las barras, al contrario que en un glob de shell—, la interrogación cubre un solo carácter, y {js,ts} agrupa alternativas.
La regla que casi nadie conoce es esta: si el patrón contiene una barra, se compara contra la ruta completa expandida; si no la contiene, se compara solo contra el nombre final del archivo. 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 directoriovim.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.-- El * inicial cubre el prefijo (/home/tu-usuario/…): sin el, no casa nada.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 ellosvim.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 en configuraciones grandes: el callback se invoca para cada buffer de la sesión, incluidos los auxiliares, los del explorador y los del terminal.
El dominio cambia con el evento
En los eventos que no son de archivo, el patrón cambia de dominio por completo, porque args.match contiene otra cosa. En FileType casa contra el nombre del tipo: pattern = "lua" es correcto y pattern = "*.lua" jamás casará con nada. En User casa contra el nombre del evento propio (lección 9.4). En TermOpen casa contra el nombre del buffer de terminal — esa URI term:// que incluye el comando lanzado. Confundir dominios produce autocomandos silenciosamente muertos, que es el peor tipo de fallo: no hay error, simplemente no ocurre nada. Ante la duda, la ayuda de cada evento documenta qué llega en match — y la sonda de la lección 9.1 (callback = vim.print) te lo enseña con valores reales en diez segundos.
FileType no es un filtro sobre extensiones
Un archivo .h puede ser C o C++. 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 con FileType y 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 autocomando FileType con el que arrancaste Treesitter en la lección 3.3 se apoya en esto: pregunta por el tipo ya detectado, no adivina por el nombre.
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 de la API.
-- Patron de dos fases: FileType clasifica, el autocomando de buffer actuavim.api.nvim_create_autocmd("FileType", { group = vim.api.nvim_create_augroup("dios_prosa", { 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,})(Un matiz de rigor sobre ese ejemplo: InsertLeave se emite al salir de Insert con <Esc>, pero no cuando sales con <C-c>. Si tienes la costumbre del control-c, tu autoguardado tendrá un agujero exactamente con la forma de esa costumbre.)
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 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 clasificación, en lugar de en cada pulsación. LspAttach (lección 3.1) tiene la misma forma: el evento clasifica, y dentro instalas atajos ligados a args.buf. Y el TermOpen de la lección 4.2, igual: el evento te dice «esto es un terminal» y tú ajustas ese buffer, no todos.
En el mundo Vimscript esta acotación se escribe con el patrón especial <buffer>; te la cruzarás en configuraciones ajenas y en :autocmd al listar. En Lua no la necesitas: la clave buffer es la misma idea con número explícito.
once y nested: cardinalidad y reentrada
Los dos últimos filtros no restringen dónde sino cuántas veces y bajo qué reglas de encadenamiento.
once
El autocomando se ejecuta y se borra 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: «ejecútate hasta conseguirlo, luego desaparece».
nested merece la explicación más cuidadosa de la lección, porque su comportamiento por defecto es contraintuitivo y deliberado: mientras se ejecuta un callback, Neovim suprime la emisión de nuevos autocomandos provocados por las acciones de ese callback. Es una protección contra la recursión: sin ella, un autocomando de BufWritePre que escribiera el buffer se llamaría a sí mismo para siempre.
La consecuencia práctica: un autocomando que abre un archivo, cambia de buffer o asigna el tipo no provocará las reacciones que esperarías, salvo que declares nested = true. Y en cuanto lo declares, asumes que la cadena termina — el límite de 10 niveles de anidamiento es una red de seguridad, no una estrategia.
-- El buffer se quedo sin tipo tras escribirse: relanzar la deteccion.-- nested = true para que el FileType resultante dispare a sus suscriptores.vim.api.nvim_create_autocmd("BufWritePost", { group = vim.api.nvim_create_augroup("dios_redeteccion", { clear = true }), nested = true, desc = "Redetecta el tipo si falta, dejando que FileType se emita", 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, 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 al 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 entero se vuelve inspeccionable sin ejecutar una sola línea.
Abre tres archivos distintos: las notificaciones enseñan lua, python, markdown — tipos, no rutas. Esa es la cadena contra la que casa tu patrón en este evento.
La forma buflocal del listado: el patrón especial <buffer> filtra a lo que morirá con el buffer actual. Pruébalo en un terminal de la 4.2 o tras abrir un markdown con el ejemplo de prosa.
Guarda dos veces: la notificación sale una. once retiró la entrada de la tabla tras el primer disparo — compruébalo con :autocmd BufWritePost.
Afina el alcance
- Registra dos autocomandos con
*.luay con un patrón con barra hacia tu directorio de configuración, y comprueba con archivos de dos proyectos distintos cuál dispara en cada caso. - Intenta filtrar
FileTypecon*.lua, verifica que nunca dispara, y corrígelo al patrón del dominio correcto. - Sustituye un autocomando global con guarda interna por la pareja
FileTypemás autocomando de buffer, y compara los listados de:autocmdantes y después. - Escribe un autocomando con
oncey otro que se borre devolviendotruebajo condición, y explica cuándo es preferible cada uno. - Quita el
nestedal ejemplo de redetección, provoca el caso (un archivo sin tipo) y observa qué reacciones dejan de ocurrir. Devuélvelo y descríbelo con precisión.