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.
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.
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.
Instalar los parsers
Solo vim.pack: la rama main no admite carga perezosa, asi que declararlo en lazy solo serviria para escribir lazy = false.
vim.pack.add({
{ src = "https://github.com/nvim-treesitter/nvim-treesitter", version = "main" },
})
require("nvim-treesitter").install({
"lua", "vim", "vimdoc", "c", "rust", "swift",
"javascript", "typescript", "tsx", "html", "css",
"json", "yaml", "toml", "markdown", "bash",
})Con vim.pack no hay spec ni evento: el plugin ya está en el runtimepath cuando arranca tu init.lua, así que la instalación de parsers se pide directamente.
El resaltado y el plegado son de Neovim, no del plugin. Se activan por tipo de archivo:
-- IMPRESCINDIBLE si activas el plegado: sin esto, cada archivo se abre
-- ENTERO PLEGADO y parece que has roto el editor. Ver más abajo.
vim.o.foldlevelstart = 99
vim.api.nvim_create_autocmd("FileType", {
callback = function(args)
-- Sin lista de lenguajes: pregunta si hay parser y actúa.
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
vim.wo[0][0].foldexpr = "v:lua.vim.treesitter.foldexpr()" -- plegado
vim.wo[0][0].foldmethod = "expr"
vim.bo[args.buf].indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
end,
})
Instala un parser nuevo y funciona solo: no hay ninguna lista que mantener sincronizada a mano.
Qué acabas de activar: el plegado
Un fold es un trozo de código colapsado en una sola línea. No borra nada ni toca el archivo: solo lo oculta de la vista. Antes de Treesitter tenías que decirle a Vim dónde estaban los pliegues con reglas de indentación o marcas en comentarios; ahora los deduce del árbol de sintaxis, así que una función se pliega por ser una función, no porque esté indentada de cierta manera.
Estas son las teclas, todas con prefijo z porque en Vim z es el mundo de la pantalla y los pliegues:
| Tecla | Qué hace |
|---|---|
za |
La única que necesitas al principio. Abre o cierra el pliegue bajo el cursor |
zo / zc |
Abrir / cerrar explícitamente |
zR / zM |
Abrir todos / cerrar todos los del archivo |
zr / zm |
Abrir / cerrar un nivel de anidamiento en todo el archivo |
zj / zk |
Saltar al siguiente / anterior pliegue |
zv |
Abrir lo justo para ver la línea del cursor |
Por defecto Vim empieza con foldlevel a cero, que significa todo plegado. Si activas foldmethod = "expr" sin poner foldlevelstart = 99, abres cualquier archivo y te encuentras cinco líneas colapsadas donde había quinientas. Es la reacción de “he roto el editor” más común al descubrir los folds, y la causa es esta línea que falta. Con foldlevelstart = 99 entras con todo desplegado y pliegas solo cuando tú quieras, que es lo que espera cualquiera.
Si algún día abres un archivo y aparece colapsado sin querer, zR lo abre entero y te devuelve la cordura.
Con esto tienes lo suficiente para usarlo a diario: za sobre una función para quitarla de en medio, zR cuando quieras verlo todo. El plegado da bastante más de sí —persistirlo entre sesiones, plegar por diagnósticos del LSP, un foldtext que diga cuántas líneas oculta cada pliegue— y eso es la lección 4.2, dedicada entera al tema.
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.
return {
"nvim-treesitter/nvim-treesitter-textobjects",
branch = "main",
lazy = false,
init = function()
vim.g.no_plugin_maps = true
end,
config = function()
require("nvim-treesitter-textobjects").setup({
select = { lookahead = true },
move = { set_jumps = true },
})
require("config.textobjects") -- las teclas, aparte
end,
}vim.g.no_plugin_maps = true
vim.pack.add({
{ src = "https://github.com/nvim-treesitter/nvim-treesitter-textobjects", version = "main" },
})
require("nvim-treesitter-textobjects").setup({
select = { lookahead = true },
move = { set_jumps = true },
})
require("config.textobjects")vim.g.no_plugin_maps evita que los ftplugin nativos de Neovim pisen los mismos atajos. Ponlo antes de que el plugin cargue.
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") end
end
for 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: NO uses <leader>ca — esa es la code action del LSP (lección 2.1).
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:
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 golpe
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.
return {
"folke/flash.nvim",
event = "VeryLazy",
opts = {},
keys = {
{ "s", mode = { "n", "x", "o" }, function() require("flash").jump() end, desc = "Flash: saltar" },
{ "S", mode = { "n", "x", "o" }, function() require("flash").treesitter() end, desc = "Flash: nodo" },
},
}vim.pack.add({ { src = "https://github.com/folke/flash.nvim" } })
require("flash").setup({})
vim.keymap.set({ "n", "x", "o" }, "s", function() require("flash").jump() end, { desc = "Flash: saltar" })
vim.keymap.set({ "n", "x", "o" }, "S", function() require("flash").treesitter() end, { desc = "Flash: nodo" })Deliberadamente NO mapeamos s ni S. Ver el aviso de abajo.
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: surround, pairs, comentarios
mini.nvim es una colección de módulos pequeños y sólidos. Tres son imprescindibles:
return {
{ "nvim-mini/mini.surround", version = false, event = "VeryLazy", opts = {} },
{ "nvim-mini/mini.pairs", version = false, event = "InsertEnter", opts = {} },
{ "nvim-mini/mini.ai", version = false, event = "VeryLazy", opts = {} },
}vim.pack.add({
{ src = "https://github.com/nvim-mini/mini.surround" },
{ src = "https://github.com/nvim-mini/mini.pairs" },
{ src = "https://github.com/nvim-mini/mini.ai" },
})
require("mini.surround").setup({
-- Prefijo gs: nativamente "dormir N segundos", o sea libre.
-- Deja s y S para flash.nvim (leccion 2.3).
mappings = {
add = "gsa", delete = "gsd", replace = "gsr",
find = "gsf", find_left = "gsF", highlight = "gsh",
},
})
require("mini.pairs").setup({})
require("mini.ai").setup({})Los módulos de mini son independientes: puedes instalar el repo completo o cada módulo por separado.
mini.surround
Añade, cambia o quita delimitadores: gsaiw" rodea una palabra con comillas, gsd" las quita, gsr"' cambia " por '. Ojo: usa s como prefijo.
mini.pairs
Cierra automáticamente paréntesis, comillas y llaves.
mini.ai
Mejora los objetos de texto: ci) funciona aunque el paréntesis esté lejos, y añade objetos por Treesitter.
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:
return {
"windwp/nvim-ts-autotag",
ft = { "html", "javascriptreact", "typescriptreact", "tsx", "vue", "svelte", "xml" },
opts = {},
}vim.pack.add({ { src = "https://github.com/windwp/nvim-ts-autotag" } })
require("nvim-ts-autotag").setup({})Escribe una etiqueta de apertura y se cierra sola; renombra la de apertura y la de cierre se actualiza.
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”.
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.