mirror of
https://github.com/neovim/neovim.git
synced 2026-07-11 20:09:44 +00:00
perf(treesitter): reuse edited tree ranges for callbacks
After an edit, LanguageTree:_edit() updates the current trees. When the LanguageTree manages explicit regions, _edit() also refreshes _regions from tree:included_ranges(true), so those regions have the edited byte offsets. A later injection pass may call set_included_regions() with a different number of child regions. That path discards the old trees and emits changedtree callbacks for them. invalidate(true) does the same when a buffer is reloaded. Before this change, both discard paths called tree:included_ranges(true) for every old tree, even if _edit() had just collected those exact ranges. That duplicate range extraction is expensive with many injection trees. Realistic shapes include generated C files with many macro bodies parsed by the C preproc_arg injection, Markdown documents with many fenced blocks of the same language, and template files with many embedded-language islands. The stock highlighter registers recursive changedtree callbacks, so this is on the normal highlighting edit path. Track whether _regions currently came from tree:included_ranges(true) with _regions_from_tree_ranges. _do_changedtree_callbacks() reuses _regions only in that state; otherwise it falls back to calling tree:included_ranges(true). Clear the marker when regions are replaced by injection ranges, when a tree is reparsed, or when trees are discarded. This avoids keeping a second copy of the ranges while preserving callback precision: changedtree still receives tree:included_ranges(true) for the old tree, not the broader managed region. Benchmark on 100k C macro injections, one-line edit, recursive changedtree callback: - HEAD median: edited parse 84.2 ms, child region replacement 58.7 ms - This change: edited parse 34.6 ms, child region replacement 8.5 ms That is about 2.4x faster for the edit parse and 6.9x faster for child region replacement in this workload. Add a regression test that replacing injection regions still fires changedtree and still reports the old tree's exact included ranges. AI-assisted: Codex
This commit is contained in:
committed by
Lewis Russell
parent
b9431b340f
commit
33ea63011c
@@ -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
|
||||
|
||||
@@ -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<integer, TSTree> 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<integer,true> 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 = {
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user