Treesitter y edición avanzada
Objetos de texto por estructura con la API de la rama main, la selección incremental nativa de 0.12, saltos con flash.nvim, surround, autopairs y comentarios. Edita por sintaxis, no por líneas.
En el Nivel 0 aprendiste objetos de texto genéricos (iw, i", i(). Treesitter los lleva a otro nivel: ahora puedes decir “dentro de la función”, “este argumento”, “esta clase” — objetos que entienden la estructura real de tu código. Eso sí: en Neovim 0.12 el mundo de Treesitter cambió de raíz, y casi todo lo que encuentres escrito antes de 2026 ya no compila.
- Configurar
nvim-treesittercon la API de la ramamain, la única viva. - Objetos de texto semánticos con la API explícita de teclas (
af,ia,ac…). - Saltar por estructura sin pisar atajos nativos ni de otros plugins.
- Surround, autopairs, autotag y comentarios.
Lee esto antes de copiar nada de internet
La rama main de nvim-treesitter es una reescritura incompatible, no una actualización. El módulo nvim-treesitter.configs ya no existe, y con él desaparecieron ensure_installed, highlight, indent e incremental_selection. El resaltado, el plegado y la indentación los da ahora Neovim directamente; el plugin solo instala parsers y aporta queries. La rama master sigue publicada pero está congelada y solo sirve para Neovim 0.11. Además, nvim-treesitter de main no admite carga perezosa: va con lazy = false. Si un tutorial te enseña require("nvim-treesitter.configs").setup({ highlight = { enable = true } }), está escrito para la rama muerta.
Para qué sirve el plugin si Neovim ya trae Treesitter
Aquí hay que pararse, porque en 0.12 la pregunta legítima es por qué instalar este plugin si Neovim ya hace todo eso. La respuesta es que no hace todo eso.
Neovim trae la librería de tree-sitter y las funciones que la usan: vim.treesitter.start() para resaltar, vim.treesitter.foldexpr() para plegar, la selección incremental que verás más abajo. Pero una librería de parseo sin un parser del lenguaje concreto no puede hacer nada. Y de parsers, Neovim trae exactamente seis:
Para Rust, Python, TypeScript, Go, HTML, CSS, JSON, YAML, Swift o Kotlin no viene nada: ni el parser ni las queries que le dicen a Neovim qué nodo es un comentario y cuál una función. Abre un .rs con la configuración pelada y vim.treesitter.start() falla, porque no hay parser de Rust que arrancar.
Es la misma forma que el LSP, y por eso se entiende bien
Neovim trae el cliente LSP, pero tú instalas los servidores: rust-analyzer, clangd, lua-language-server. A nadie le extraña; el cliente sin servidor no analiza nada. Con Treesitter pasa exactamente lo mismo: el editor trae el motor y las funciones, y nvim-treesitter es de dónde salen los parsers y las queries de los otros doscientos lenguajes. La confusión viene de que antes el plugin hacía las dos cosas —traía los parsers y implementaba el resaltado, el plegado, la indentación y la selección incremental—, así que instalarlo se sentía como instalar “la funcionalidad”. La reescritura de la rama main le quitó todo lo segundo, que se fue al editor, y le dejó lo primero. Sigue siendo imprescindible, pero por un motivo distinto al de antes: ya no es el que enciende la luz, es el que trae las bombillas.
Así que el plugin sirve para tres cosas concretas, y ninguna la cubre Neovim:
Descargar y compilar parsers
install(), :TSUpdate, :TSInstall. Doscientos lenguajes, compilados para tu máquina y mantenidos al día con el ABI que espera tu versión de Neovim.
Aportar las queries
highlights.scm, folds.scm, indents.scm, injections.scm por lenguaje. Sin ellas el árbol existe pero nadie sabe qué colorear ni por dónde plegar.
Ser el banco de pruebas
Lo que funciona ahí acaba subiendo al core. La selección incremental hizo ese viaje: era del plugin, ahora es del editor.
Ampliar lo que montaste en la 2.2
El plugin ya está en tu config: lo instalaste en la lección 2.2 como ejemplo estrella de vim.pack, junto con el autocomando de lua/config/treesitter.lua que enciende resaltado y plegado por tipo de archivo. No lo vuelvas a declarar. Lo que toca aquí son dos ampliaciones sobre esa base.
Primera: más parsers. La 2.2 instaló seis para arrancar; amplía la lista a los lenguajes que uses de verdad. install() es idempotente — los que ya están, ni los toca:
require("nvim-treesitter").install({ "lua", "vim", "vimdoc", "c", "rust", "swift", "javascript", "typescript", "tsx", "html", "css", "json", "yaml", "toml", "markdown", "bash",})Segunda: la indentación. El autocomando de la 2.2 activaba vim.treesitter.start() (resaltado) y vim.treesitter.foldexpr() (plegado), las dos funciones del editor. La indentación es distinta: Neovim no trae indentexpr por Treesitter — esa pieza sí la implementa el plugin, con sus queries indents.scm. Añade una línea dentro del callback:
vim.api.nvim_create_autocmd("FileType", { callback = function(args) local lang = vim.treesitter.language.get_lang(vim.bo[args.buf].filetype) if not lang or not vim.treesitter.language.add(lang) then return end vim.treesitter.start(args.buf, lang) -- resaltado (ya la tenías) vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()" -- plegado (ya la tenías) vim.wo[0][0].foldmethod = "expr" -- NUEVA: indentación calculada sobre el árbol (experimental, del plugin) vim.bo[args.buf].indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()" end,})Instala un parser nuevo y todo funciona solo: no hay ninguna lista que mantener sincronizada a mano.
El plegado, ahora por sintaxis
Los folds los dominaste en la lección 1.7 con el editor desnudo: za, zR, zM, niveles, foldmethod. Lo que cambia aquí es quién decide dónde están los pliegues: con foldexpr = vim.treesitter.foldexpr() los deduce el árbol de sintaxis, así que una función se pliega por ser una función, no porque esté indentada de cierta manera. Recuerda que tu config.treesitter de la 2.2 ya lleva el foldlevelstart = 99 que evita abrir cada archivo entero plegado — si algún día te pasa, zR te devuelve la cordura.
La lección 1.7 te dejó dos deudas anunciadas para este nivel, y esta es su casa:
Plegar por el servidor de lenguaje. Desde la 0.11 existe vim.lsp.foldexpr(): pide los rangos plegables al servidor en vez de al árbol. Un servidor sabe cosas que la sintaxis no —qué es una #region, dónde acaba lógicamente el bloque de imports—. Se usa cambiando una función por otra en el autocomando, idealmente con condición:
-- dentro del callback de FileType, en vez de la línea de treesitter:if #vim.lsp.get_clients({ bufnr = args.buf, method = "textDocument/foldingRange" }) > 0 then vim.wo[0][0].foldexpr = "v:lua.vim.lsp.foldexpr()"endUna interfaz mejor para los pliegues. Si quieres las dos fuentes a la vez con prioridad (LSP → Treesitter → indentación), vista previa del pliegue sin abrirlo y un foldtext que diga cuántas líneas oculta, eso es exactamente lo que empaqueta nvim-ufo:
Cascada de proveedores de folds (LSP → Treesitter → indent), vista previa flotante con K sobre un pliegue cerrado y contador de líneas ocultas. Necesita promise-async como dependencia. Es azúcar sobre lo nativo — pruébalo cuando lo nativo se te quede corto, no antes.
Objetos de texto por sintaxis
El plugin de objetos de texto también se reescribió: ya no mapea nada por ti. Tú declaras las teclas, y eso es bueno — es la única forma de no chocar con lo que ya tienes.
Las teclas van en un módulo propio, porque ahora son responsabilidad tuya:
local sel = require("nvim-treesitter-textobjects.select")local mov = require("nvim-treesitter-textobjects.move")local swp = require("nvim-treesitter-textobjects.swap")local function objeto(captura) return function() sel.select_textobject(captura, "textobjects") endendfor tecla, captura in pairs({ af = "@function.outer", ["if"] = "@function.inner", ac = "@class.outer", ic = "@class.inner", aa = "@parameter.outer", ia = "@parameter.inner", al = "@loop.outer", il = "@loop.inner",}) do vim.keymap.set({ "x", "o" }, tecla, objeto(captura), { desc = "objeto " .. captura })end-- Navegación: usa los prefijos ] y [ que Vim reserva justo para esto.vim.keymap.set({ "n", "x", "o" }, "]f", function() mov.goto_next_start("@function.outer", "textobjects") end)vim.keymap.set({ "n", "x", "o" }, "[f", function() mov.goto_previous_start("@function.outer", "textobjects") end)vim.keymap.set({ "n", "x", "o" }, "]a", function() mov.goto_next_start("@parameter.inner", "textobjects") end)vim.keymap.set({ "n", "x", "o" }, "[a", function() mov.goto_previous_start("@parameter.inner", "textobjects") end)-- Intercambiar argumentos sin tocar el ratón.-- OJO: evita <leader>ca — media internet la usa para la code action del-- LSP (aquí es gra, pero no invites a la colisión).vim.keymap.set("n", "<leader>cp", function() swp.swap_next("@parameter.inner") end, { desc = "mover parametro adelante" })vim.keymap.set("n", "<leader>cP", function() swp.swap_previous("@parameter.inner") end, { desc = "mover parametro atras" })Ahora tienes objetos nuevos que combinan con los verbos del Nivel 0:
Repetir el salto con ; y ,
El módulo repeatable_move hace que ; y , repitan el último salto estructural, y de paso arregla f/t para que ; siempre vaya hacia delante:
local rep = require("nvim-treesitter-textobjects.repeatable_move")vim.keymap.set({ "n", "x", "o" }, ";", rep.repeat_last_move_next)vim.keymap.set({ "n", "x", "o" }, ",", rep.repeat_last_move_previous)vim.keymap.set({ "n", "x", "o" }, "f", rep.builtin_f_expr, { expr = true })vim.keymap.set({ "n", "x", "o" }, "F", rep.builtin_F_expr, { expr = true })Selección incremental: ya no se configura
Aquí está la sorpresa agradable de 0.12. El módulo incremental_selection desapareció con la rama master, pero no hace falta sustituirlo: Neovim trae la selección incremental de serie, como mapeos por defecto del modo Visual. No instalas nada, no configuras nada.
| Tecla | Qué hace |
|---|---|
an |
Selecciona el nodo padre (acepta un contador) |
in |
Selecciona el primer hijo o el hijo anterior |
]n / [n |
Selecciona el nodo siguiente / anterior |
]N / [N |
Extiende la selección al nodo siguiente / anterior |
Entras en Visual con v y pulsas an repetidamente: la selección sube por el árbol nodo a nodo (identificador → expresión → argumento → llamada → sentencia → bloque). in baja. Y hay un detalle bien pensado: si el buffer no tiene parser de Treesitter, an e in caen automáticamente en vim.lsp.buf.selection_range(), la capacidad selectionRange que 0.12 añadió al cliente LSP. Es decir, funciona igual en un lenguaje sin parser mientras haya un servidor conectado.
Por debajo todo esto es vim.treesitter.select(), por si quieres construir algo encima:
-- target: 'parent' | 'child' | 'next' | 'prev' | 'extend_next' | 'extend_prev'vim.treesitter.select("parent", 2) -- sube dos niveles de golpeCtrl-Espacio está ocupado
Media internet mapea la selección incremental a Ctrl-Space, herencia de la época en que había que configurarla. Hoy no tiene ningún sentido: las teclas ya existen, y esa en concreto está ocupada. En Neovim 0.12 Ctrl-Space es la tecla de invocación del completado —la que la propia ayuda propone para vim.lsp.completion.get() y la que atan los presets de blink.cmp y nvim-cmp—. Si la robas, tu autocompletado deja de responder y tardarás una tarde en descubrir por qué, porque no da ningún error. Lo mismo con <Tab>, que gobierna el menú. Si vienes de una configuración vieja o de una distro, empieza por borrar ese mapeo: pcall(vim.keymap.del, { "n", "x", "o" }, "<C-Space>").
flash.nvim: saltar a cualquier parte
f/t saltan dentro de la línea. flash.nvim salta a cualquier punto visible con dos o tres teclas: invocas el salto, escribes un par de caracteres del destino y aparecen etiquetas.
Por qué flash sí se queda con s y S, y quién paga la factura
Aquí hay una decisión de diseño que merece explicarse, porque es el conflicto de teclas más común del ecosistema y casi siempre se resuelve por inercia.
Empecemos por el coste real de ceder s y S. Son comandos nativos de Vim, sí, pero redundantes: s es exactamente cl y S es exactamente cc; en Visual, s es c. No pierdes ninguna capacidad, solo dos formas cortas de algo que ya sabes hacer. Si tienes cl y cc en los dedos, el precio es prácticamente cero.
El problema de verdad no es plugin contra Vim: es plugin contra plugin. mini.surround usa s como prefijo (sa, sd, sr, sf, sF, sh) y flash lo quiere como moción. Los dos llaman a vim.keymap.set con el mismo s, y ahí no hay negociación posible: el segundo sobrescribe al primero y uno desaparece entero. Gana el que cargue último, y con carga perezosa ese orden no es evidente ni estable entre arranques. Por eso el síntoma clásico es “mi surround funcionaba ayer”.
La salida es dar a cada uno lo que le corresponde por naturaleza. Un salto es una moción: se usa decenas de veces por hora y debe costar dos teclas, incluido el modo operador, donde ds significa “borra hasta donde salte” y es genuinamente útil. Surround es un operador: se usa menos y aguanta bien una tecla más. Y hay un sitio perfecto para él: gs, que nativamente significa “dormir N segundos” y no sirve absolutamente para nada. Es, de hecho, adonde lo mueve el propio extra de LazyVim.
Un matiz para quien venga de vim-surround: si usas nvim-surround en vez de mini, sus atajos son ys, ds y cs, y entonces flash sí te rompe ds y cs al tomar s en modo operador. La solución en ese caso es mapear flash solo en n y x, dejando o libre. La lección general es la que importa: antes de aceptar el mapeo por defecto de un plugin, pregúntate qué prefijo ocupa y quién más lo quiere.
mini.nvim: lo que ya tienes y lo que falta
mini.nvim también viene de la lección 2.2: allí instalaste el repo completo y configuraste mini.surround (en el prefijo gs) y mini.pairs. No hay nada que reinstalar. Lo que falta por encender es un tercer módulo que pertenece a esta lección porque es, precisamente, de objetos de texto:
require("mini.ai").setup({})mini.surround (de la 2.2)
Añade, cambia o quita delimitadores: gsaiw" rodea una palabra con comillas, gsd" las quita, gsr"' cambia " por '. Vive en el prefijo gs; el s pelado es de flash.
mini.pairs (de la 2.2)
Cierra automáticamente paréntesis, comillas y llaves.
mini.ai (nuevo)
Mejora los objetos de texto: ci) funciona aunque el paréntesis esté lejos (busca hacia delante), añade a/i para argumentos y dígitos, y acepta objetos definidos por queries de Treesitter. Convive sin conflicto con los de textobjects de arriba: mini.ai cubre los genéricos con esteroides, textobjects los semánticos (af, ac).
Para comentar, Neovim ya trae gc nativo desde 0.10: gcc comenta la línea, gcap un párrafo, gc en Visual la selección. No necesitas plugin.
Autotag para JSX/HTML
Al escribir frontend, cerrar y renombrar etiquetas a mano es tedioso. nvim-ts-autotag lo automatiza con Treesitter:
Escribe una etiqueta de apertura y se cierra sola; renombra la de apertura y la de cierre se actualiza.
La suma es el superpoder
Combina todo: s para saltar a una función lejana, vaf para seleccionarla, cif para reescribir su cuerpo, ]a para saltar entre argumentos, <leader>cp para reordenarlos. Cuando estos reflejos se encadenan sin pensar, editas más rápido de lo que la mayoría lee. Pero fíjate en lo que ha pasado en esta lección: la mitad del trabajo no ha sido añadir potencia, sino repartir el teclado sin colisiones. Un editor con veinte plugins tiene un presupuesto de teclas finito, y el conflicto no avisa con un error: simplemente una función deja de responder y crees que el plugin está roto. Por eso las convenciones importan tanto — ] y [ para saltos, <leader> para acciones, los prefijos de operador respetados — y por eso :checkhealth y :map deberían ser lo primero que mires cuando algo “no funciona”.
Edición por estructura
Abre un archivo con funciones y clases:
- Entra en Visual con
vy pulsaanrepetidamente para subir por el árbol;inpara bajar;]ny[npara moverte entre hermanos. difpara vaciar una función;upara deshacer.s+ 2 letras (flash) para saltar al otro extremo del archivo.gsaiw)para rodear una palabra con paréntesis;gsd)para quitarlos.]f/[fpara navegar entre funciones, y;para repetir el último salto.- Ejecuta
:map sy:map <C-Space>y comprueba tú mismo que nadie los ha robado.