wandres.dev
AUTOCOMANDOS · reaccionar a eventos

Depurar autocomandos: listar, entender por qué no dispara y el orden

Inspeccionar la tabla de autocomandos con el comando autocmd y con la API programable, el catálogo completo de causas por las que una reacción no ocurre, y cómo se resuelve el orden cuando varios escuchan el mismo evento.

⏱ 18 min

Los autocomandos fallan de la peor manera posible: en silencio. No hay excepción, no hay mensaje, simplemente el efecto que esperabas no aparece, y no existe un punto de interrupción donde detenerse porque el problema es que nadie te llamó. Depurar aquí no consiste en leer un error sino en interrogar a la tabla de suscripciones y reconstruir por qué el emparejamiento no ocurrió. Con las herramientas correctas es un proceso de cinco minutos; sin ellas, una tarde entera de conjeturas.

🎯 Al terminar esta lección sabrás
  • Inspeccionar la tabla de autocomandos con el comando interactivo y con la API.
  • Recorrer sistemáticamente las causas por las que una reacción no se ejecuta.
  • Determinar y controlar el orden entre varios suscriptores del mismo evento.
  • Instrumentar el bus para medir coste y aislar al culpable.

Listar: el comando autocmd y su versión programable

El comando :autocmd sin argumentos vuelca la tabla completa, lo cual es inútil por volumen. Su valor está en los filtros, que se aplican en el orden grupo, evento y patrón, cualquiera de ellos omitible.

:autocmd BufWritePre              " todo lo que escucha ese evento
:autocmd dios_formato             " todo lo del grupo, sea cual sea el evento
:autocmd dios_formato BufWritePre " la interseccion de ambos
:autocmd FileType lua             " el evento con ese patron concreto
:verbose autocmd BufWritePre      " ademas, desde que archivo se definio cada uno

:verbose autocmd es la herramienta decisiva y la más infrautilizada. Añade a cada entrada la línea que indica desde qué archivo y qué línea se registró, lo cual convierte “hay algo que formatea mi buffer y no sé qué” en una respuesta inmediata. Cuando el culpable es un plugin de terceros, esto es lo único que te dice cuál.

Para trabajo serio, sin embargo, la API devuelve datos estructurados en lugar de texto para leer con los ojos.

-- Inventario legible de todo lo que escucha un evento
local entradas = vim.api.nvim_get_autocmds({ event = "BufWritePre" })
for _, a in ipairs(entradas) do
  print(("[%s] %s -> %s"):format(a.group_name or "sin grupo", a.pattern, a.desc or "sin descripcion"))
end

-- Cuantos autocomandos ha registrado cada grupo
local por_grupo = {}
for _, a in ipairs(vim.api.nvim_get_autocmds({ event = "*" })) do
  local g = a.group_name or "ANONIMO"
  por_grupo[g] = (por_grupo[g] or 0) + 1
end
vim.print(por_grupo)

Ese segundo fragmento merece ejecutarse en cualquier configuración madura. El recuento bajo la clave de los anónimos es una métrica de deuda técnica: son exactamente los autocomandos que ya no puedes borrar ni identificar.

💡
El campo desc es tu futura sesión de depuración

Un autocomando sin desc aparece en los listados como una función Lua sin nombre y sin contexto. Escribir una descripción cuesta cinco segundos hoy y ahorra media hora dentro de un año, cuando estés mirando una tabla de ochenta entradas intentando averiguar cuál de ellas mueve tu cursor.

Por qué no dispara: el catálogo de causas

Cuando una reacción no ocurre, la causa está casi siempre en esta lista, y conviene recorrerla en orden porque está ordenada por frecuencia real.

El patrón no casa

Dominio equivocado: *.lua en FileType, o un patrón con barra comparado contra una ruta que no esperabas. Imprime args.match con un autocomando sin patrón para ver el valor real.

🧩

Llegaste tarde

El evento ya ocurrió antes de que tu autocomando existiera. Es el fallo clásico con VimEnter, con el primer BufEnter y con cualquier plugin de carga perezosa.

🧵

Alguien limpió tu grupo

Otro archivo creó un grupo del mismo nombre con clear = true después que tú. Sin prefijo propio, la colisión es cuestión de tiempo.

🗓️

Los eventos estaban suprimidos

La opción eventignore, el prefijo noautocmd en un comando, o la ausencia de nested dentro de otro callback. El evento no se emitió, no es que no lo escucharas.

Hay dos causas más que no caben en tarjetas y que producen los casos más desconcertantes. La primera: el buffer no tiene nombre. Muchos eventos comparan el patrón contra una ruta vacía, de modo que cualquier patrón distinto del asterisco falla en buffers recién creados, en los del terminal y en los de los plugins de interfaz. La segunda: el autocomando existe pero su callback aborta. Un error dentro de un callback se reporta, pero si envolviste el cuerpo en pcall o comprobaste una condición que resultó falsa, el efecto no ocurre y la tabla te dice que todo está correcto. Antes de dudar del emparejamiento, pon una notificación en la primera línea del callback.

-- Sonda universal: que llega realmente y con que valores
vim.api.nvim_create_autocmd(
  { "BufReadPost", "BufWritePre", "FileType", "LspAttach", "User" },
  {
    group = vim.api.nvim_create_augroup("dios_sonda", { clear = true }),
    desc = "Traza de diagnostico, borrar cuando termines",
    callback = function(args)
      vim.notify(("%s | match=%s | buf=%d"):format(args.event, args.match, args.buf))
    end,
  }
)

El orden de ejecución

Cuando varios autocomandos escuchan el mismo evento y todos casan, se ejecutan en orden de registro. No hay prioridades, no hay pesos, y los grupos no reordenan nada: un autocomando de un grupo creado después se ejecuta después, sin más.

flowchart TD
E[Se emite BufWritePre] --> T[Recorrido de la tabla en orden de registro]
T --> A1[Autocomando 1 registrado al arrancar]
A1 --> A2[Autocomando 2 de un plugin]
A2 --> A3[Autocomando 3 de tu configuracion]
A3 --> F[Se escribe el archivo]
A1 -.si aborta la escritura.-> X[Los siguientes no llegan a correr]
style T fill:#cba6f7,color:#11111b
style X fill:#f38ba8,color:#11111b

La consecuencia inmediata es que el orden depende del orden de carga de los plugins, que no controlas y que cambia cuando instalas o quitas cualquier cosa. Construir lógica sobre la suposición de que tu formateador corre antes que el de otro plugin es construir sobre arena.

Cuando el orden importa de verdad hay tres soluciones legítimas, en orden de preferencia. La primera y mejor: no depender del orden, haciendo cada autocomando independiente del efecto de los demás. La segunda: fusionar en uno solo, un único suscriptor que llama a las dos acciones en la secuencia que quieres, con lo que el orden pasa de ser emergente a ser código explícito. La tercera, para cuando no controlas al otro: reregistrar para ir al final, borrando tu grupo y volviéndolo a crear en un momento posterior, típicamente desde VeryLazy o desde un VimEnter diferido.

-- Orden explicito: un suscriptor que secuencia, en vez de dos que compiten
vim.api.nvim_create_autocmd("BufWritePre", {
  group = vim.api.nvim_create_augroup("dios_pipeline_guardado", { clear = true }),
  desc = "Saneado y formato en secuencia determinista",
  callback = function(args)
    require("dios.saneado").quitar_espacios(args.buf)
    require("dios.formato").aplicar(args.buf)
  end,
})

Instrumentar: medir y aislar

Cuando el problema no es que no dispare sino que dispara demasiado o cuesta demasiado, la técnica cambia: hay que medir. Los eventos de alta frecuencia —CursorMoved, TextChanged, BufEnter— son los sospechosos habituales de un editor perezoso.

-- Cuenta disparos por evento durante una sesion de trabajo
local cuentas = {}
vim.api.nvim_create_autocmd(
  { "CursorMoved", "CursorMovedI", "TextChanged", "BufEnter", "InsertLeave" },
  {
    group = vim.api.nvim_create_augroup("dios_metricas", { clear = true }),
    callback = function(args)
      cuentas[args.event] = (cuentas[args.event] or 0) + 1
    end,
  }
)
vim.api.nvim_create_user_command("Disparos", function() vim.print(cuentas) end, {})

Para aislar al culpable de una lentitud concreta, la bisección sobre grupos es infalible y rápida: borra la mitad de tus grupos con nvim_del_augroup_by_name, comprueba si el síntoma persiste y repite. Como los grupos son idempotentes, restaurarlos es volver a evaluar el archivo. Si el síntoma sobrevive a borrar todos tus grupos, el origen está en un plugin, y ahí :verbose autocmd te da el nombre del archivo en una línea.

Un sistema de eventos sin observabilidad es indistinguible de uno roto

La lección que este nivel deja más allá de Neovim es que el desacoplamiento no es gratis: se paga en observabilidad, y hay que reponerlo deliberadamente. Una llamada directa se depura leyendo código, porque la relación entre quien llama y quien es llamado está escrita en el texto fuente y cualquier búsqueda la encuentra. Una suscripción a un evento no está escrita en ninguna parte que puedas leer: existe solo en tiempo de ejecución, dentro de una tabla que se construye por acumulación durante el arranque, y su contenido depende de qué plugins se cargaron, en qué orden y bajo qué condiciones. Has cambiado un grafo estático y verificable por uno dinámico e invisible, y ese cambio es exactamente el mismo que separa una llamada a función de un mensaje por cola, un import de una inyección de dependencias, o un monolito de una arquitectura de microservicios. En los tres casos la respuesta profesional es idéntica y no es renunciar al desacoplamiento, sino invertir en instrumentación proporcional al acoplamiento que has eliminado: nombres descriptivos en cada suscripción, espacios de nombres que permitan filtrar, capacidad de enumerar el grafo en caliente, trazas que registren qué se emitió y quién reaccionó, y métricas que digan cuánto costó. Neovim te da todo eso —desc, grupos, nvim_get_autocmds, :verbose autocmd— y la diferencia entre una configuración que puedes mantener durante años y una que abandonarás cuando se vuelva incomprensible no está en qué autocomandos escribiste, sino en si puedes responder en treinta segundos a la pregunta de quién reacciona a qué. Escribir el evento es la parte fácil. Poder verlo funcionando es la ingeniería.

⚔️ Audita tu propio bus
  1. Ejecuta el recuento por grupo y calcula cuántos autocomandos anónimos arrastra tu configuración.
  2. Usa :verbose autocmd BufWritePre e identifica el archivo exacto de cada entrada, tuya o de un plugin.
  3. Instala la sonda universal, provoca un fallo de patrón deliberado en FileType y diagnostícalo leyendo args.match.
  4. Mide con el contador cuántas veces dispara CursorMoved en un minuto de edición normal y razona qué implica para cualquier callback que cuelgues ahí.
  5. Aísla por bisección de grupos una lentitud al guardar y reescribe los dos autocomandos implicados como un único pipeline con orden explícito.