Saltar al contenido

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:

LUA
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.

LUA
-- 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.

LUA
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:

LUA
-- 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

  1. Registra un autocomando sobre BufReadPost y otro sobre FileType que impriman args.match y args.file, abre un archivo Lua y explica por qué difieren.
  2. Escribe un autocomando en BufWritePre que cuente las líneas del buffer usando exclusivamente args.buf, sin recurrir nunca al buffer actual implícito.
  3. Crea un autocomando cuyo callback devuelva true bajo una condición y comprueba con :autocmd que, tras cumplirse, ha desaparecido de la tabla.
  4. Abre :h autocmd-events, elige un evento que no conozcas de nada y móntale una sonda con vim.notify hasta entender exactamente cuándo se dispara.
  5. 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.)