diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index b1f0e6038a..57af11ac71 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -179,6 +179,9 @@ OPTIONS PERFORMANCE +• |treesitter-highlight| performance on large injection-heavy files improves + by 50% to 100% by reusing edited child-tree ranges. + • Nvim architecture allows pure-Lua implementations of some `vim.fn` functions, which skips the Vimscript <=> Lua "bridge" (no data conversion/marshalling) entirely, if the `vim.fn` function is called from diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 4da4ea3ee1..2fa1b730e2 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -103,6 +103,7 @@ local TSCallbackNames = { ---@field private _parent? vim.treesitter.LanguageTree Parent LanguageTree ---@field private _source (integer|string) Buffer or string to parse ---@field private _trees table Reference to parsed tree (one for each language). +---@field private _regions_from_tree_ranges boolean Whether _regions were copied from TSTree:included_ranges(true). ---Each key is the index of region, which is synced with _regions and _valid. ---@field private _valid_regions table Set of valid region IDs. ---@field private _num_valid_regions integer Number of valid regions @@ -144,6 +145,7 @@ function LanguageTree.new(source, lang, opts) _lang = lang, _children = {}, _trees = {}, + _regions_from_tree_ranges = false, _opts = opts, _injection_query = injections[lang] and query.parse(lang, injections[lang]) or query.get(lang, 'injections'), @@ -264,10 +266,9 @@ function LanguageTree:invalidate(reload) -- buffer was reloaded, reparse all trees if reload then - for _, t in pairs(self._trees) do - self:_do_callback('changedtree', t:included_ranges(true), t) - end + self:_do_changedtree_callbacks() self._trees = {} + self._regions_from_tree_ranges = false end for _, child in pairs(self._children) do @@ -458,6 +459,7 @@ function LanguageTree:_parse_regions(range, thread_state) self:_do_callback('changedtree', tree_changes, tree) self._trees[i] = tree + self._regions_from_tree_ranges = false vim.list_extend(changes, tree_changes) total_parse_time = total_parse_time + parse_time @@ -862,10 +864,9 @@ function LanguageTree:set_included_regions(new_regions) -- outdated_regions is invalidated by _iter_regions in else branch. if #self:included_regions() ~= #new_regions then -- TODO(lewis6991): inefficient; invalidate trees incrementally - for _, t in pairs(self._trees) do - self:_do_callback('changedtree', t:included_ranges(true), t) - end + self:_do_changedtree_callbacks() self._trees = {} + self._regions_from_tree_ranges = false self:invalidate() else self:_iter_regions(function(i, region) @@ -874,6 +875,7 @@ function LanguageTree:set_included_regions(new_regions) end self._regions = new_regions + self._regions_from_tree_ranges = false self._num_regions = #new_regions end @@ -1146,6 +1148,25 @@ function LanguageTree:_get_injections(range, thread_state) return result end +---@private +---@param cb_name TSCallbackName +function LanguageTree:_has_callback(cb_name) + return #self._callbacks[cb_name] > 0 or #self._callbacks_rec[cb_name] > 0 +end + +---@private +function LanguageTree:_do_changedtree_callbacks() + if not self:_has_callback('changedtree') then + return + end + + local tree_ranges = self._regions_from_tree_ranges and self._regions or nil + for i, tree in pairs(self._trees) do + local ranges = tree_ranges and tree_ranges[i] or tree:included_ranges(true) + self:_do_callback('changedtree', ranges, tree) + end +end + ---@private ---@param cb_name TSCallbackName function LanguageTree:_do_callback(cb_name, ...) @@ -1191,6 +1212,7 @@ function LanguageTree:_edit( regions[i] = tree:included_ranges(true) end self._regions = regions + self._regions_from_tree_ranges = true end local changed_range = { diff --git a/test/functional/treesitter/parser_spec.lua b/test/functional/treesitter/parser_spec.lua index 8678761e6e..c26ff56e6b 100644 --- a/test/functional/treesitter/parser_spec.lua +++ b/test/functional/treesitter/parser_spec.lua @@ -665,6 +665,34 @@ describe('treesitter parser API', function() { 6, 15, 6, 18 }, -- VALUE2 123 }, get_ranges()) end) + + it('notifies changedtree callbacks when replacing injection regions', function() + exec_lua(function() + _G.parser = vim.treesitter._create_parser(0, 'c', { + injections = { + c = '(preproc_def (preproc_arg) @injection.content (#set! injection.language "c"))', + }, + }) + _G.parser:parse(true) + + local child = _G.parser:children().c + vim.api.nvim_buf_set_lines(0, 3, 4, false, { '#define VALUE changed' }) + + _G.changedtree = 0 + _G.precise_changedtree = true + child:register_cbs({ + on_changedtree = function(changes, tree) + _G.changedtree = _G.changedtree + 1 + _G.precise_changedtree = _G.precise_changedtree + and vim.deep_equal(changes, tree:included_ranges(true)) + end, + }) + + _G.parser:parse({ 3, 4 }) + end) + + eq(true, exec_lua('return _G.changedtree > 0 and _G.precise_changedtree')) + end) end) describe('when parsing regions combined', function()