wandres.dev
TREESITTER: EL ÁRBOL · parseo incremental

Inspeccionar: el visor del árbol y tu propio código

InspectTree como microscopio del buffer, Inspect para saber qué pinta cada byte, cómo diagnosticar nodos ERROR y lenguajes inyectados, y el bucle de trabajo que convierte una duda sobre el árbol en un tipo de nodo concreto.

⏱ 18 min

Todo lo anterior —el parser, el árbol, los campos, la gramática— sería teoría inaplicable sin una forma de mirar la estructura que Neovim construye sobre tu buffer. Esa forma existe y viene de serie: un visor que muestra el árbol en vivo, sincronizado con el cursor, y un comando que te dice exactamente qué está pintando cada byte de la pantalla. A partir de aquí dejas de suponer cómo se llama un nodo y pasas a leerlo, que es el requisito para escribir consultas en el nivel siguiente.

🎯 Al terminar esta lección sabrás
  • Abrir el visor del árbol y navegarlo en sincronía con el buffer.
  • Distinguir en el visor nodos con nombre, anónimos, campos y lenguajes inyectados.
  • Usar la inspección de posición para saber qué grupo de resaltado gana en un punto dado.
  • Diagnosticar nodos ERROR y MISSING y convertir esa lectura en un tipo de nodo utilizable.

El visor del árbol

El comando es :InspectTree, y equivale a llamar a vim.treesitter.inspect_tree. Abre una ventana lateral con el árbol del buffer actual en notación de expresiones simbólicas, y mantiene los dos lados sincronizados: al mover el cursor por el código se resalta el nodo correspondiente, y al moverte por el árbol se resalta el rango del buffer que ocupa ese nodo.

-- Abrir el visor para el buffer actual: sin opciones deduce el lenguaje del filetype
vim.keymap.set("n", "<leader>ti", function()
  vim.treesitter.inspect_tree()
end, { desc = "Inspeccionar el arbol" })

-- Abrir el editor de consultas, equivalente al comando :EditQuery
vim.keymap.set("n", "<leader>tq", function()
  vim.treesitter.query.edit()
end, { desc = "Editar una query" })

Dentro de la ventana del visor hay cuatro teclas que cambian por completo lo que ves:

🎯

Enter

Salta en el buffer al primer byte del nodo bajo el cursor del visor.

🔣

a

Muestra u oculta los nodos anónimos: la puntuación y las palabras reservadas.

🌐

I

Muestra el lenguaje de cada nodo, imprescindible cuando hay inyecciones.

🔎

o

Abre el editor de consultas contra el árbol que estás viendo, con resultados en vivo.

Empieza siempre con los nodos anónimos ocultos: el árbol se lee como un esquema del programa. Actívalos solo cuando necesites un token concreto —una llave, un operador, una palabra reservada— porque casi siempre es lo que hay que capturar para colorearlo aparte.

ℹ️
Lo que el visor no te dice a simple vista

El visor muestra la estructura, no las razones. Si un nodo aparece donde no lo esperabas, la explicación está en la gramática: una regla oculta que no genera nodo, una precedencia que agrupó los operandos de otra forma, un extra que se coló en medio. Cuando el árbol te sorprenda, la pregunta correcta no es qué le pasa a Treesitter, sino cómo está escrita esa regla.

Qué pinta cada byte

El árbol responde a la pregunta estructural. La otra pregunta cotidiana —por qué este identificador sale de este color— la responde :Inspect, que informa de todo lo que afecta a la posición del cursor: capturas de Treesitter con su prioridad, grupos de sintaxis clásica si sigue activa, extmarks que aportan resaltado y tokens semánticos del servidor de lenguaje.

-- Volcar en bruto todo lo que afecta a la posicion del cursor
vim.print(vim.inspect_pos())

-- Comprobar si hay arbol y de que lenguaje es.
-- get_parser devuelve nil (y un mensaje) cuando no puede crear el parser:
-- no lanza excepcion, asi que se comprueba con un if, no con pcall.
local parser, err = vim.treesitter.get_parser(0)
print(parser and parser:lang() or err)

Leer esa salida enseña el orden real de mando. Sobre el mismo byte pueden competir una captura de Treesitter, un extmark de un plugin y un token semántico del LSP; gana el de mayor prioridad, y por eso un color puede cambiar segundos después de abrir el archivo, cuando el servidor de lenguaje responde y sus tokens se superponen a los del árbol. Sin esta herramienta, ese comportamiento parece un fallo aleatorio; con ella, es una lista ordenada.

flowchart TD
D[Duda sobre el codigo] --> I[InspectTree con anonimos ocultos]
I --> N[Identificar el tipo de nodo y su campo]
N --> Q[Editor de consultas con o]
Q --> V[Ver las capturas resaltadas en vivo]
V --> OK[La consulta captura lo que querias]
V --> NO[Captura de mas o de menos]
NO --> I
OK --> U[Usarla en highlights o textobjects]
C[Color inesperado] --> P[Inspect sobre la posicion]
P --> L[Lista de capturas extmarks y tokens por prioridad]
style I fill:#89b4fa,color:#11111b
style OK fill:#a6e3a1,color:#11111b
style NO fill:#f38ba8,color:#11111b
style P fill:#cba6f7,color:#11111b

Diagnosticar el árbol de tu propio código

Con el visor abierto, tres situaciones se vuelven legibles de inmediato.

Un nodo ERROR significa que el parser no supo encajar ese fragmento en ninguna regla. Casi siempre es código a medio escribir y desaparece solo; si persiste en código válido, tienes un desfase entre la versión de la gramática y la del lenguaje. Un nodo MISSING es más interesante: el analizador dedujo que falta un token concreto —una llave, un end, un paréntesis— e insertó un marcador vacío para poder continuar. Es tolerancia a errores en acción, y te señala el punto exacto donde el código está incompleto, a menudo mejor que cualquier mensaje del compilador.

La tercera situación son las inyecciones. Un bloque de código dentro de Markdown, una consulta SQL dentro de una cadena, HTML dentro de una plantilla: cada región se parsea con su propio lenguaje y el conjunto se organiza como un árbol de árboles. En el visor pulsa I y verás el lenguaje de cada rama; si un fragmento no se colorea, lo primero es comprobar ahí si la inyección se está reconociendo y si el parser de ese lenguaje está instalado.

💡
El bucle que deberías interiorizar

Ante cualquier duda estructural, el ciclo es siempre el mismo: sitúa el cursor en el código, abre :InspectTree, lee el tipo de nodo y su campo, prueba una consulta con el editor integrado y comprueba las capturas en vivo. Cuatro pasos que sustituyen a media hora de leer gramáticas ajenas, y que son exactamente el flujo de trabajo del nivel siguiente.

Construir tu propia inspección

El visor es una ventana aparte, y hay preguntas que quieres tener contestadas todo el rato, sin abrir nada. Con lo que ya sabes del árbol puedes fabricarlas en unas pocas líneas.

Una migaja de pan con la cadena de nodos que te contiene, lista para colgar de la winbar o de la línea de estado:

local function ruta_sintactica()
  local nodo = vim.treesitter.get_node()
  local partes = {}
  while nodo do
    if nodo:named() then
      table.insert(partes, 1, nodo:type())
    end
    nodo = nodo:parent()
  end
  return table.concat(partes, " > ")
end

vim.api.nvim_create_autocmd({ "CursorMoved", "BufEnter" }, {
  callback = function()
    local ok, ruta = pcall(ruta_sintactica)
    vim.wo.winbar = ok and ruta or ""
  end,
})

Y un comando que te diga si el buffer tiene errores de análisis y dónde está el primero, útil para distinguir un problema de tu código de un problema de la gramática:

vim.api.nvim_create_user_command("TSErrores", function()
  local parser = vim.treesitter.get_parser(0)
  if not parser then
    return vim.notify("este buffer no tiene parser")
  end
  local raiz = parser:parse()[1]:root()
  if not raiz:has_error() then
    return vim.notify("arbol limpio")
  end
  for nodo in raiz:iter_children() do
    if nodo:has_error() then
      vim.notify("rama con error en la linea " .. (nodo:start() + 1))
    end
  end
end, {})

Los propios nodos de error son consultables, cosa que casi nadie sabe. (ERROR) empareja el nodo que el analizador no supo encajar y (MISSING) el marcador de anchura cero que insertó para poder continuar; puedes incluso pedir un token concreto con (MISSING ";"). Con eso, localizar el punto exacto donde el buffer se rompe deja de ser un paseo visual por el visor y pasa a ser una consulta.

Ninguna de estas dos piezas es un plugin ni pretende serlo. Son la demostración de que, una vez el editor tiene un modelo estructural del buffer, la instrumentación deja de ser un privilegio de los autores de plugins y pasa a estar a tu alcance en veinte líneas de Lua.

Inspeccionar no es depurar: es aprender a leer el modelo que el editor ya tiene de tu código

Es tentador archivar :InspectTree como herramienta de diagnóstico, algo que se abre cuando algo falla. Ese encuadre desperdicia lo que de verdad ofrece. Lo que el visor muestra no es un informe sobre Treesitter, es tu propio código visto como estructura, y esa vista revela cosas que la vista lineal esconde por completo: la profundidad real de anidamiento de una función que creías plana, expresiones que se agrupan de forma distinta a como las lees por culpa de la precedencia, condicionales cuya forma en el árbol delata que están pidiendo una extracción. Un programador que abre el visor con regularidad no está depurando el editor: está adquiriendo el hábito de pensar su código en dos representaciones a la vez, la que escribe y la que la máquina reconstruye, y de notar cuándo divergen. Esa divergencia —entre la estructura que crees haber escrito y la que el parser reconoce— es una de las señales más honestas de complejidad accidental que existen, porque no depende de opiniones de estilo sino de la forma que la gramática es capaz de encontrar. Y hay un corolario técnico igual de importante: cada consulta, cada objeto de texto sintáctico y cada regla de plegado que escribas a partir de ahora se apoya en nombres de nodo concretos, y esos nombres solo se averiguan mirando. El visor es, en ese sentido, la documentación viva de la gramática que tienes instalada, siempre exacta y siempre para tu versión. Quien lo usa deja de programar el editor por conjetura y empieza a hacerlo por lectura.

⚔️ Leer tu código como árbol
  1. Abre tu archivo más largo, ejecuta :InspectTree y recorre el árbol con los nodos anónimos ocultos. Localiza la función más profunda.
  2. Pulsa a para mostrar los anónimos y encuentra el nodo exacto de una palabra reservada; anota su tipo.
  3. Borra deliberadamente un end o una llave de cierre y observa dónde aparecen los nodos ERROR y MISSING. Deshaz el cambio.
  4. Abre un archivo Markdown con un bloque de código, pulsa I en el visor y comprueba qué lenguaje se inyecta en esa rama.
  5. Coloca el cursor sobre un identificador coloreado de forma dudosa, ejecuta :Inspect y determina qué capa gana: árbol, extmark o token semántico.