El modelo de eventos: el editor que reacciona
Qué es exactamente un autocomando y por qué casi todo lo que «hace Neovim solo» es uno. El bus síncrono que atraviesa el editor, el ciclo de vida de un buffer estación a estación, la anatomía completa del objeto que recibe tu callback y el mapa de eventos que sostiene una configuración de 0.12.
Casi todo lo que crees que hace Neovim por sí solo lo hace en realidad un autocomando. El resaltado que aparece al abrir un archivo, el formateo al guardar, los atajos del LSP que solo existen donde hay servidor: nada de eso está cableado en el núcleo. Son reacciones registradas a eventos con nombre. Tú ya escribiste cuatro en la lección 2.1 —el resalte del yank, recordar la posición, las vistas— siguiendo la receta. Este nivel es la teoría que hay detrás, y entenderla es dejar de configurar Neovim como una lista de ajustes y empezar a programarlo como el sistema reactivo que es.
- Definir un autocomando como la tripleta evento, patrón y acción, y escribirlo con
nvim_create_autocmd. - Situar los eventos en el ciclo de vida de un buffer y de la sesión, y elegir la estación correcta.
- Leer y explotar cada campo del objeto que recibe el callback:
buf,match,file,data. - Reconocer las consecuencias de que el bus sea síncrono y bloqueante.
- Ubicar los eventos que sostienen una configuración de 0.12:
FileType,BufWritePre,LspAttach,TextYankPost,PackChanged.
Un autocomando es una tripleta
Un autocomando declara: cuando ocurra este evento, si el contexto casa con este patrón, ejecuta esta acción. El núcleo emite eventos con nombre en puntos concretos de su ejecución —abrir un archivo, cambiar de buffer, salir del modo Insert, adjuntar un servidor de lenguaje— y en cada emisión recorre la tabla de autocomandos registrados para ese nombre, ejecutando en orden los que casan.
Este es el de la lección 2.1, mirado ahora con ojos de nivel 9:
vim.api.nvim_create_autocmd("TextYankPost", { desc = "Resalta brevemente el texto copiado", callback = function() vim.hl.on_yank({ higroup = "IncSearch", timeout = 150 }) end,})Es vim.hl.on_yank, no vim.highlight.on_yank
Si esto lo copias de una guía anterior a 0.11 verás vim.highlight.on_yank. Es la misma función: el módulo vim.highlight se renombró a vim.hl y el nombre viejo está deprecado — hoy funciona con aviso, mañana no existirá. :checkhealth vim.deprecated te lista todo lo que tu configuración arrastre de esa época.
La función recibe el evento (o una lista de eventos) y una tabla de opciones. El repertorio completo es corto y conviene tenerlo entero delante desde el principio, porque el resto del nivel consiste en dominar cada fila:
| Clave | Qué hace | A fondo en |
|---|---|---|
callback |
función Lua que recibe la tabla de contexto | esta lección |
command |
alternativa: una cadena de comandos Ex | esta lección |
desc |
descripción que verás en los listados | lección 9.5 |
group |
espacio de nombres que posee el autocomando | lección 9.2 |
pattern |
acota contra qué contexto casa | lección 9.3 |
buffer |
ata el autocomando a un buffer concreto; excluyente con pattern |
lección 9.3 |
once |
se borra tras el primer disparo | lección 9.3 |
nested |
permite que sus acciones emitan a su vez eventos | lección 9.3 |
Tres detalles importan desde el primer minuto. El primero: callback y command son excluyentes, y callback es la forma con futuro — una función Lua con cierre léxico, depurable, en el lenguaje que ya dominas del Nivel 7; command ejecuta una cadena de Vimscript y sigue ahí por compatibilidad (en la 2.1 la usaste para mkview, donde una línea de comando era exactamente suficiente). El segundo: desc no es decorativo, es lo que te salvará al listar autocomandos cuando algo falle dentro de seis meses. El tercero, y el más sorprendente: si el callback devuelve true, el autocomando se borra a sí mismo. Es la vía canónica para reacciones que se autodestruyen bajo una condición, distinta de once, que se borra siempre tras el primer disparo.
Y la propiedad estructural que colorea todo el nivel: el bus es síncrono. Neovim no encola tu callback ni lo ejecuta en otro hilo — detiene lo que estaba haciendo, lo ejecuta hasta el final y continúa. Un callback lento en BufWritePre es latencia que sientes al pulsar guardar; un callback lento en CursorMoved es un editor que se arrastra. Cómo sacar el trabajo pesado del bus sin bloquear es exactamente el tema del Nivel 12 (vim.schedule, el event loop); en este nivel basta la regla: dentro de un callback, poco y rápido.
El ciclo de vida de un buffer, evento a evento
La mayoría de los eventos útiles no son sucesos aislados sino estaciones de un recorrido. Abrir un archivo dispara una secuencia ordenada, y saber en qué punto engancharse es la diferencia entre una configuración que funciona y una que funciona a veces.
BufReadPre ocurre antes de tener contenido: es el lugar para decidir cosas por el nombre o el tamaño del archivo —desactivar plugins en archivos enormes, por ejemplo—. BufReadPost ya tiene las líneas cargadas pero todavía no hay tipo de archivo. La detección de tipo es un paso propio que culmina emitiendo FileType, y por eso FileType es el único punto correcto para ajustes que dependen del lenguaje — es donde encendiste Treesitter en la lección 3.3, y no fue casualidad.
-- INCORRECTO: en BufReadPost el tipo de archivo aun no existevim.api.nvim_create_autocmd("BufReadPost", { callback = function(args) if vim.bo[args.buf].filetype == "python" then -- casi siempre cadena vacia vim.bo[args.buf].shiftwidth = 4 end end,})-- CORRECTO: FileType se emite justo despues de la deteccionvim.api.nvim_create_autocmd("FileType", { pattern = "python", callback = function(args) vim.bo[args.buf].shiftwidth = 4 vim.bo[args.buf].expandtab = true end,})En la escritura, BufWritePre es tu última oportunidad de modificar el buffer antes de que toque el disco: formateadores, borrado de espacios finales, ordenación de imports. BufWritePost ya no puede cambiar nada del archivo escrito, pero es perfecto para efectos externos: recargar un servicio, disparar un linter, notificar.
Fuera del ciclo del buffer hay una segunda familia, la de la sesión, con su propio orden estricto: VimEnter marca el final del arranque y el primer instante en que todo lo demás está cargado; UIEnter se emite cuando una interfaz se adjunta —para la TUI de siempre, justo después de VimEnter; para una interfaz remota, cuando se conecte—; VimLeavePre es el último momento en que puedes guardar estado —una sesión, un registro, un archivo temporal— con los buffers todavía vivos; VimLeave llega cuando ya casi nada es seguro. Y entre medias existe hasta un evento para el reposo: SafeState se emite cuando no queda nada pendiente y Neovim va a quedarse esperando una tecla. Esa asimetría entre lo que existe antes y después de cada evento es el motivo por el que el orden de la secuencia importa más que la lista de nombres.
La anatomía del objeto del callback
Neovim invoca tu callback con un único argumento: una tabla con el contexto completo del disparo. Ignorarla y tirar del estado implícito —vim.fn.expand("%"), vim.bo sin índice, el «buffer actual»— es el sello del principiante; leerla es lo que permite escribir autocomandos correctos cuando hay muchos buffers en juego.
vim.api.nvim_create_autocmd("BufWritePre", { callback = function(args) -- args.id numero del autocomando; con el puedes borrarlo via nvim_del_autocmd -- args.event nombre del evento: util si registraste varios en la misma llamada -- args.group identificador numerico del grupo, o nil -- args.match el texto contra el que se compara el patron -- args.buf numero del buffer afectado -- args.file nombre de archivo expandido del buffer -- args.data carga util arbitraria, solo en ciertos eventos vim.print(args) end,})El campo decisivo es args.buf. Un autocomando puede dispararse para un buffer que no es el actual —una escritura en segundo plano, un evento emitido mientras el foco está en otra ventana— y cualquier operación que asuma el buffer visible introduce un fallo intermitente imposible de reproducir. La regla no admite matices: usa args.buf como destino explícito, nunca el cero implícito.
La distinción entre match y file es la sutileza que separa a quien ha leído la documentación de quien la ha entendido. En los eventos de buffer, ambos rondan la misma ruta. Pero en FileType, match es el nombre del tipo —la cadena lua, rust, python— mientras file sigue siendo la ruta. Y en el evento User, match es el nombre del evento propio que alguien publicó. El patrón se compara siempre contra match, jamás contra file: de ahí que un patrón correcto en BufWritePre no signifique nada en FileType. La lección 9.3 explota esta regla a fondo.
args.data es el canal de carga útil
Solo algunos eventos rellenan args.data, pero son justo los que más importan hoy: LspAttach entrega ahí el client_id del servidor recién adjuntado, PackChanged entrega qué plugin cambió y cómo, y el evento User entrega lo que tú decidas publicar (lección 9.4). Es lo que convierte un autocomando de notificación vacía en mensaje con contenido.
El mapa: los eventos que sostienen una configuración
Existen más de cien eventos y no vas a memorizarlos: :h autocmd-events es el catálogo. Lo que sí conviene fijar es el reparto real del trabajo. Tres eventos cargan con la inmensa mayoría:
BufWritePre
El punto de intervención sobre el contenido antes de tocar el disco. Formateo, saneado, imports. Síncrono por definición: lo que tarde aquí lo esperas tú, con la tecla de guardar pulsada.
FileType
El único lugar correcto para ajustes por lenguaje. Se dispara tras la detección, con el buffer ya identificado, y su patrón casa contra el nombre del tipo. Es donde vive tu arranque de Treesitter de la 3.3.
LspAttach
El que ya usaste en la lección 3.1: se dispara cuando un servidor se adjunta a un buffer y entrega el cliente en args.data.client_id. Sustituyó al viejo patrón on_attach por cliente.
Y un segundo escalón que cubre casi todo lo demás:
| Evento | Se dispara | Dónde lo has visto o lo verás |
|---|---|---|
TextYankPost |
tras copiar o borrar texto | 2.1: el resalte con vim.hl.on_yank |
TermOpen |
al crear un buffer de terminal | 4.2: números fuera, startinsert dentro |
InsertLeave |
al salir de Insert — pero no con <C-c> |
guardado automático de prosa, 9.3 |
CursorHold |
el cursor lleva quieto 'updatetime' ms, solo en Normal, una vez |
trabajo diferido barato |
LspDetach |
un servidor se separa del buffer | deshacer lo que montó LspAttach |
PackChangedPre / PackChanged |
vim.pack va a cambiar / cambió un plugin; data trae el plugin y el tipo de cambio |
hooks de instalación y build |
User |
nunca: lo emites tú | lección 9.4 |
El fragmento que condensa la doctrina del nivel entero es el LspAttach que ya escribiste en la 3.1:
-- Atajos del LSP que solo existen donde hay servidor adjuntovim.api.nvim_create_autocmd("LspAttach", { callback = function(args) local cliente = vim.lsp.get_client_by_id(args.data.client_id) if not cliente then return end local opts = { buffer = args.buf, silent = true } -- Nada de gd ni K: en 0.12 el hover ya esta en K y la definicion en -- Ctrl-]. Aqui solo se añade lo que NO viene de fabrica. vim.keymap.set("n", "<leader>cf", function() vim.lsp.buf.format({ async = true }) end, opts) if cliente:supports_method("textDocument/inlayHint") then vim.lsp.inlay_hint.enable(true, { bufnr = args.buf }) end end,})Reacciona a un hecho concreto, lee el contexto del argumento, comprueba la capacidad real antes de usarla y aplica el efecto acotado al buffer que corresponde. Ninguna variable global, ningún estado compartido, ninguna suposición sobre el orden de carga.
Tu configuración no es un archivo de ajustes: es un programa reactivo
El salto conceptual de este nivel es abandonar la idea de que configurar Neovim consiste en asignar valores. Un archivo de opciones describe un estado inicial; los autocomandos describen una función del tiempo. En el momento en que registras el primero, tu configuración deja de ser una fotografía y pasa a ser un sistema de control con realimentación: el editor emite señales, tú declaras respuestas, y el comportamiento observable emerge de la composición de todas ellas. Esto trae exactamente los mismos problemas que cualquier arquitectura orientada a eventos —orden de suscripción no determinista, acoplamiento temporal invisible, efectos en cascada, dificultad para razonar sobre el estado global— y exige exactamente las mismas disciplinas: nombrar los espacios (los grupos, lección 9.2), acotar el alcance (patrones y buffers, lección 9.3), definir contratos explícitos entre partes (el evento User, lección 9.4) y poder observar el sistema en marcha (depuración, lección 9.5). Quien trata los autocomandos como trucos sueltos acumula una configuración que se rompe al recargarla y nadie sabe por qué. Quien los trata como lo que son —un bus de mensajes síncrono dentro de un editor programable— construye algo que sigue siendo comprensible tres años después. El resto de este nivel es, sencillamente, ingeniería de software aplicada a tu propio editor.
Ahí está tu resalte del yank de la 2.1, con su grupo y su desc — y quizá compañía que no sabías que tenías.
vim.print sirve directamente como callback: recibe la tabla de argumentos y la vuelca. Abre un par de archivos y observa match, file y buf con valores reales.
Abre un .py que no estuviera ya cargado: la notificación enseña corchetes vacíos. La detección llega en la estación siguiente.
Instrumenta el bus
- Registra un autocomando sobre
BufReadPosty otro sobreFileTypeque imprimanargs.matchyargs.file, abre un archivo Lua y explica por qué difieren. - Escribe un autocomando en
BufWritePreque cuente las líneas del buffer usando exclusivamenteargs.buf, sin recurrir nunca al buffer actual implícito. - Crea un autocomando cuyo callback devuelva
truebajo una condición y comprueba con:autocmdque, tras cumplirse, ha desaparecido de la tabla. - Abre
:h autocmd-events, elige un evento que no conozcas de nada y móntale una sonda convim.notifyhasta entender exactamente cuándo se dispara. - Mide el coste real del bus: pon un callback con un bucle de un millón de iteraciones en
CursorMoved, muévete, y describe qué le pasa al editor y por qué. (Luego bórralo: en la lección 9.5 aprenderás a encontrar culpables como este.)