Depurar queries: el árbol, el playground y los errores
Las herramientas nativas para ver el árbol e iterar patrones en vivo, la lectura precisa de los mensajes de error del compilador de queries, la trampa de la caché y un método reproducible para pasar de una intuición a un patrón correcto.
Escribir queries a ciegas es un ejercicio de frustración: el patrón no coincide con nada y el motor no te dice por qué, porque desde su punto de vista no ha pasado nada malo. La solución no es adivinar mejor, es dejar de adivinar. Neovim trae de serie un inspector de árboles y un editor de queries en vivo que convierten el proceso en un bucle cerrado de segundos.
- Leer el árbol real de un búfer con el inspector nativo.
- Iterar patrones en vivo con el editor de queries y su autocompletado.
- Diagnosticar los mensajes de error del compilador de queries.
- Aplicar un método reproducible que va de la intuición al patrón correcto.
Ver el árbol antes de escribir nada
La causa número uno de un query que no coincide con nada es una suposición sobre el nombre de un nodo. Los tipos de nodo los decide la gramática, no la intuición, y varían entre lenguajes que parecen gemelos. El primer paso siempre es mirar.
:InspectTree abre una ventana con la representación textual del árbol del búfer actual, sincronizada con el cursor. Dentro de esa ventana hay cuatro teclas que lo cambian todo: a muestra u oculta los nodos anónimos —los tokens literales que por defecto se omiten—, I muestra el lenguaje de origen de cada nodo, lo que resulta imprescindible cuando hay inyecciones, o abre el editor de queries acoplado, y la tecla de retorno salta en el búfer original al nodo bajo el cursor. Los plegados funcionan con normalidad, así que zc y zo te permiten navegar árboles enormes.
Su complemento es :Inspect, que responde a la pregunta inversa: dado este punto del búfer, ¿qué capturas se están aplicando, con qué prioridad y desde qué lenguaje? Es la herramienta con la que se diagnostica un conflicto de precedencia, porque enumera todas las fuentes que compiten por el mismo carácter.
-- equivalentes programáticos, útiles dentro de un mapeo propio
vim.treesitter.inspect_tree({ lang = "lua" })
vim.show_pos()
-- el nodo bajo el cursor y su tipo, sin abrir ninguna ventana
local nodo = vim.treesitter.get_node()
print(nodo:type(), vim.inspect({ nodo:range() }))
El editor de queries en vivo
:EditQuery abre un búfer de patrones cuyo efecto se ve inmediatamente sobre el búfer de origen: cada vez que guardas o escribes, las coincidencias se resaltan en el código. Ese bucle de retroalimentación es el equivalente para queries de lo que un REPL es para un lenguaje, y cambia por completo la economía del proceso: dejas de escribir un patrón entero y rezar, y pasas a construirlo nodo a nodo comprobando en cada paso que sigues coincidiendo con algo.
Dentro de ese búfer, el autocompletado por omnifunción está enganchado a la gramática del lenguaje que estás consultando. Invocarlo te ofrece los tipos de nodo y los nombres de campo que existen de verdad, lo que elimina de raíz la clase entera de errores por nombre inventado.
Merece la pena dejar las tres herramientas a un atajo de distancia, porque el coste de invocarlas determina si las usarás o si volverás a adivinar.
vim.keymap.set("n", "<leader>ti", vim.treesitter.inspect_tree, { desc = "Arbol" })
vim.keymap.set("n", "<leader>tq", "<Cmd>EditQuery<CR>", { desc = "Editor de queries" })
vim.keymap.set("n", "<leader>th", vim.show_pos, { desc = "Capturas aqui" })
Neovim trae un comprobador estático de ficheros .scm que verifica que los nodos existen en la gramática, que los predicados y directivas están registrados y que las expresiones simbólicas de primer nivel están bien formadas, y publica los hallazgos como diagnósticos normales. Lo que ya no hace es activarse solo al abrir un fichero de query, así que engánchalo tú:
vim.api.nvim_create_autocmd({ "BufWritePost", "BufEnter" }, {
pattern = "*.scm",
callback = function(args) vim.treesitter.query.lint(args.buf) end,
})El lenguaje a comprobar se deduce del directorio que contiene el fichero: una ruta que acaba en lua/highlights.scm se valida contra la gramática de Lua. Si el fichero está en otro sitio, pásale la opción con la lista de lenguajes.
InspectTree
Muestra el árbol. Teclas a para nodos anónimos, I para el lenguaje, o para el editor y retorno para saltar al nodo.
EditQuery
Editor en vivo. Escribes el patrón, ves las coincidencias resaltadas al instante y completas nombres con la omnifunción.
Inspect
Enumera las capturas activas bajo el cursor con su prioridad y su lenguaje. Es el diagnóstico de los conflictos de precedencia.
checkhealth
:checkhealth vim.treesitter confirma qué parsers están instalados y detecta desajustes de versión entre gramática y queries.
flowchart LR
A[Elige un ejemplo minimo en el buffer] --> B[InspectTree para ver los tipos reales]
B --> C[EditQuery y escribe el patron mas simple]
C --> D{Coincide}
D -->|no| E[Quita una restriccion y reintenta]
E --> C
D -->|si| F[Anade campos y predicados uno a uno]
F --> G[Inspect para verificar prioridad]
G --> H[Guarda en after queries]Los errores más comunes
El compilador de queries es estricto y sus mensajes son precisos, pero solo si sabes traducirlos. Estos son los que vas a encontrar, ordenados por frecuencia real.
Tipo de nodo inválido. El mensaje señala fila y columna e indica el nombre ofensivo. Casi siempre significa una de tres cosas: escribiste un nodo de otro lenguaje, la gramática se actualizó y renombró el nodo, o confundiste un nodo con nombre con un token anónimo. Este último caso es traicionero porque parece razonable: la palabra clave de retorno no es (return), es "return" entrecomillado.
Patrón imposible. El motor detecta estáticamente que la forma que describes no puede existir en esa gramática, típicamente porque un campo no admite el tipo de hijo que le pides o porque el anidamiento va en el orden contrario al real. No es un aviso: es una demostración de que ningún fichero del mundo hará coincidir ese patrón.
Sintaxis o estructura inválida. Paréntesis o corchetes desequilibrados, o un predicado escrito fuera del par extra de paréntesis que debe envolver al patrón. Es el error que más se resuelve simplemente reindentando.
Predicado desconocido. Estás usando un predicado que existe en un plugin pero no en el núcleo, o al revés. El diagnóstico exacto lo da vim.treesitter.query.list_predicates, que enumera lo que hay registrado ahora mismo en tu sesión, plugins incluidos; si de verdad lo necesitas y no está, regístralo tú.
Y luego está la categoría más incómoda de todas: el patrón que compila sin quejarse y no coincide con nada. No hay mensaje porque no hay error; simplemente no existe en el árbol ninguna forma como la que describiste. Las tres causas habituales son un campo que la gramática no define para ese nodo, un anidamiento intermedio que olvidaste —entre la función y su nombre suele haber más nodos de los que parece— y un ancla colocada donde los hermanos no son realmente adyacentes. El procedimiento de diagnóstico es siempre el mismo: quitar restricciones hasta que vuelva a coincidir, y reponerlas una a una.
; MAL: return es un token anonimo, no un nodo con nombre
(return_statement (return)) @keyword.return
; BIEN
(return_statement "return" @keyword.return)
; MAL: el predicado queda fuera del patron
(identifier) @constant
(#match? @constant "^[A-Z]+$")
; BIEN: el par extra de parentesis envuelve patron y predicado
((identifier) @constant
(#match? @constant "^[A-Z]+$"))
La query compuesta de un lenguaje se memoriza la primera vez que se pide, con lo que editar un fichero .scm y volver al búfer no basta: seguirás viendo la versión anterior y creerás que tu cambio no hace nada. Solo hay dos cosas que la invalidan sin reiniciar: cambiar el runtimepath, que borra la caché entera, y llamar a vim.treesitter.query.set para ese lenguaje y esa query, que borra su entrada. Durante la iteración usa :EditQuery, que compila el texto que tienes delante en vez de leer del disco, y reserva el reinicio del editor para la comprobación final. Es la explicación de la inmensa mayoría de los cambios que aparentemente no surten efecto.
El método
Reduce siempre el ejemplo a la construcción mínima que quieres capturar, en un búfer temporal. Mira el árbol antes de escribir el primer paréntesis. Empieza por el patrón más laxo que puedas —un solo tipo de nodo con una captura— y confirma que coincide de más. Después, y solo después, ve añadiendo restricciones de una en una: primero los campos, luego las anclas, luego los predicados. Cuando algo deje de coincidir, sabrás exactamente qué línea lo rompió, porque acabas de escribirla. Termina con :Inspect para verificar la prioridad frente a los patrones que ya existían, y solo entonces mueve el resultado a tu directorio de queries.
Lo que hace fuerte a este método no es su originalidad, sino su propiedad estructural: cada paso mantiene el sistema en un estado conocido. Nunca hay más de un cambio entre una versión que funciona y una que no, así que la localización del fallo es inmediata y no requiere hipótesis. Es depuración por bisección aplicada al proceso de escritura en lugar de al historial de versiones, y funciona por la misma razón que funciona la bisección: porque convierte una búsqueda en un espacio grande en una secuencia de decisiones binarias sobre un espacio de uno.
-- comprobacion final desde Lua: cuenta las coincidencias reales
local consulta = vim.treesitter.query.parse("lua", [[
(function_declaration name: (identifier) @nombre)
]])
local raiz = assert(vim.treesitter.get_parser(0, "lua")):parse()[1]:root()
local total = 0
for _, _, _ in consulta:iter_captures(raiz, 0) do
total = total + 1
end
print(("coincidencias: %d"):format(total))
Cierra el nivel y date cuenta de dónde estás. Empezaste viendo el resaltado de sintaxis como un hecho de la naturaleza, algo que el editor simplemente hace, opaco y ajeno. Ahora sabes que detrás hay un parser incremental que mantiene un árbol correcto mientras escribes, un lenguaje declarativo de patrones que interroga ese árbol, un vocabulario de nombres que actúa como protocolo entre extraños, un mecanismo de composición por camino de búsqueda que te deja modificar sin poseer, y un juego de herramientas nativas para cerrar el bucle en segundos. Ninguna de esas piezas es magia, y las cinco están a tu alcance con un fichero de texto. Esa es la diferencia real entre un usuario avanzado y alguien que programa su entorno: no es la cantidad de plugins instalados, es saber a qué nivel de abstracción hay que bajar cuando algo no encaja, y tener las herramientas para bajar hasta ahí sin miedo. Lo mismo que acabas de hacer con el resaltado se aplica idéntico al plegado, a la indentación, a los objetos de texto y a la inyección de lenguajes: mismo árbol, mismo lenguaje de patrones, mismo método de depuración. Has aprendido una técnica, no un truco, y las técnicas se transfieren. El editor ya no es una herramienta que usas: es un sistema que sabes leer, extender y arreglar.
- Abre un fichero de tu lenguaje principal y explora su árbol con
:InspectTree, activando y desactivando los nodos anónimos. - Con
:EditQuery, construye desde cero un patrón que capture solo las funciones que reciben más de dos parámetros. - Provoca a propósito los cuatro errores de la sección, engancha
vim.treesitter.query.linta*.scmy anota el mensaje exacto que devuelve cada uno. - Escribe un patrón imposible deliberadamente y explica por qué el motor puede demostrar estáticamente que nunca coincidirá.
- Edita un fichero
.scmsin reiniciar, comprueba que el cambio no se aplica y confirma que la caché era la culpable modificando después elruntimepath. - Lleva el patrón final a
after/queries, verifica su prioridad con:Inspecty documenta en un comentario por qué existe.