-- Integración de treesitter para Neovim 0.13 + nvim-treesitter rama `main`. -- -- La rama `main` es una reescritura sin sistema de módulos: sólo instala parsers -- y provee queries. Highlight, indent, folds e injections los da Neovim, así que -- el enganche lo hacemos nosotros en un autocomando FileType. local M = {} local install_dir = vim.fs.joinpath(vim.fn.stdpath "data", "site") --- Parsers que queremos siempre disponibles. M.ensure_installed = { "bash", "blade", "css", "dockerfile", "cpp", "elixir", "erlang", "go", "gomod", "html", "javascript", "json", "latex", "lua", "luadoc", "markdown", "markdown_inline", "nix", "php", "php_only", "phpdoc", "python", "query", "regex", "rust", "sql", "svelte", "twig", "typescript", "typst", "vim", "yaml", } --- Filetypes de buffers de plugins que reusan el parser de otro lenguaje. M.language_aliases = { codecompanion = "markdown", snacks_notif = "markdown", oil = "markdown", } --- Filetypes donde no queremos highlight ni indent de treesitter. M.disabled_filetypes = { oil = true, snacks_notif = true, } ---@param lang string ---@return boolean local function parser_available(lang) return #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".so", false) > 0 end --- Reemplazo de `auto_install`: instala el parser que falte, una sola vez por --- lenguaje y sin bloquear, y engancha el buffer cuando termina. local installing = {} ---@type table ---@param lang string ---@param on_ready fun() local function install_missing(lang, on_ready) if installing[lang] then return end local ok, parsers = pcall(require, "nvim-treesitter.parsers") if not ok or not parsers[lang] then return end installing[lang] = true require("nvim-treesitter").install({ lang }):await(function() installing[lang] = nil vim.schedule(on_ready) end) end ---@param buf integer local function attach(buf) if not vim.api.nvim_buf_is_valid(buf) then return end local ft = vim.bo[buf].filetype if ft == "" or M.disabled_filetypes[ft] then return end local lang = vim.treesitter.language.get_lang(ft) if not lang then return end if not parser_available(lang) then install_missing(lang, function() attach(buf) end) return end if not pcall(vim.treesitter.start, buf, lang) then return end -- Indent de nvim-treesitter (experimental, era el módulo `indent` en master). -- Sólo si el lenguaje trae query `indents`; si no, dejamos el indentexpr del -- ftplugin, que es mejor que un fallback vacío. if vim.treesitter.query.get(lang, "indents") then vim.bo[buf].indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()" end end function M.setup() require("nvim-treesitter").setup { install_dir = install_dir } for ft, lang in pairs(M.language_aliases) do vim.treesitter.language.register(lang, ft) end vim.filetype.add { pattern = { [".*%.blade%.php"] = "blade", }, } require("nvim-treesitter").install(M.ensure_installed) vim.api.nvim_create_autocmd("FileType", { group = vim.api.nvim_create_augroup("pi_treesitter", { clear = true }), callback = function(args) attach(args.buf) end, }) -- El autocomando llega tarde para los buffers que ya se abrieron al arrancar. for _, buf in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_is_loaded(buf) then attach(buf) end end M.setup_incremental_selection() end -------------------------------------------------------------------------------- -- Selección incremental -- -- El módulo `incremental_selection` desapareció con la rama `main` y Neovim no -- trae equivalente. Esto reimplementa los cuatro mapeos que había: gnn / grn / -- grc / grm. -------------------------------------------------------------------------------- local stacks = {} ---@type table ---@param node TSNode local function select_node(node) local srow, scol, erow, ecol = node:range() if ecol == 0 and erow > srow then erow = erow - 1 ecol = #(vim.api.nvim_buf_get_lines(0, erow, erow + 1, false)[1] or "") end if vim.fn.mode() ~= "n" then vim.cmd "normal! \27" end vim.fn.setpos("'<", { 0, srow + 1, scol + 1, 0 }) vim.fn.setpos("'>", { 0, erow + 1, math.max(ecol, 1), 0 }) vim.cmd "normal! gv" end ---@return TSNode[] local function stack() local buf = vim.api.nvim_get_current_buf() stacks[buf] = stacks[buf] or {} return stacks[buf] end local function init_selection() local node = vim.treesitter.get_node() if not node then return end local s = stack() for i = #s, 1, -1 do s[i] = nil end s[1] = node select_node(node) end --- Sube al ancestro más chico que sea estrictamente más grande que el actual. ---@param predicate? fun(node: TSNode, from: TSNode): boolean local function grow(predicate) local s = stack() local node = s[#s] or vim.treesitter.get_node() if not node then return end local parent = node:parent() while parent do local same = vim.deep_equal({ parent:range() }, { node:range() }) if not same and (not predicate or predicate(parent, node)) then s[#s + 1] = parent select_node(parent) return end parent = parent:parent() end select_node(node) end local function shrink() local s = stack() if #s > 1 then s[#s] = nil end local node = s[#s] if node then select_node(node) end end function M.setup_incremental_selection() local map = vim.keymap.set map("n", "gnn", init_selection, { desc = "Treesitter: iniciar selección" }) map("x", "grn", function() grow() end, { desc = "Treesitter: expandir al nodo padre" }) map("x", "grc", function() -- "scope": el primer ancestro que abarque más líneas que el nodo actual. grow(function(parent, from) local psrow, _, perow = parent:range() local srow, _, erow = from:range() return (perow - psrow) > (erow - srow) end) end, { desc = "Treesitter: expandir al scope" }) map("x", "grm", shrink, { desc = "Treesitter: reducir selección" }) end return M