perf(treesitter): only search for injections within the parse range

Co-authored-by: Jaehwang Jung <tomtomjhj@gmail.com>
This commit is contained in:
Riley Bruins
2025-02-13 16:57:44 -08:00
committed by Christian Clason
parent b533c0f222
commit 562056c875
4 changed files with 63 additions and 27 deletions

View File

@@ -340,6 +340,9 @@ PERFORMANCE
• Treesitter highlighting is now asynchronous. To force synchronous parsing, • Treesitter highlighting is now asynchronous. To force synchronous parsing,
use `vim.g._ts_force_sync_parsing = true`. use `vim.g._ts_force_sync_parsing = true`.
• Treesitter folding is now calculated asynchronously. • Treesitter folding is now calculated asynchronously.
• |LanguageTree:parse()| now only runs the injection query on the provided
range (as long as the language does not have a combined injection),
significantly improving |treesitter-highlight| performance.
PLUGINS PLUGINS

View File

@@ -46,6 +46,9 @@ local Range = require('vim.treesitter._range')
local default_parse_timeout_ms = 3 local default_parse_timeout_ms = 3
---@type Range2
local entire_document_range = { 0, math.huge }
---@alias TSCallbackName ---@alias TSCallbackName
---| 'changedtree' ---| 'changedtree'
---| 'bytes' ---| 'bytes'
@@ -77,7 +80,7 @@ local TSCallbackNames = {
---@field package _callbacks_rec table<TSCallbackName,function[]> Callback handlers (recursive) ---@field package _callbacks_rec table<TSCallbackName,function[]> Callback handlers (recursive)
---@field private _children table<string,vim.treesitter.LanguageTree> Injected languages ---@field private _children table<string,vim.treesitter.LanguageTree> Injected languages
---@field private _injection_query vim.treesitter.Query Queries defining injected languages ---@field private _injection_query vim.treesitter.Query Queries defining injected languages
---@field private _injections_processed boolean ---@field private _processed_injection_range Range? Range for which injections have been processed
---@field private _opts table Options ---@field private _opts table Options
---@field private _parser TSParser Parser for language ---@field private _parser TSParser Parser for language
---Table of regions for which the tree is currently running an async parse ---Table of regions for which the tree is currently running an async parse
@@ -137,7 +140,7 @@ function LanguageTree.new(source, lang, opts)
_opts = opts, _opts = opts,
_injection_query = injections[lang] and query.parse(lang, injections[lang]) _injection_query = injections[lang] and query.parse(lang, injections[lang])
or query.get(lang, 'injections'), or query.get(lang, 'injections'),
_injections_processed = false, _processed_injection_range = nil,
_valid_regions = {}, _valid_regions = {},
_num_valid_regions = 0, _num_valid_regions = 0,
_num_regions = 1, _num_regions = 1,
@@ -334,7 +337,10 @@ function LanguageTree:is_valid(exclude_children, range)
end end
if not exclude_children then if not exclude_children then
if not self._injections_processed then if
not self._processed_injection_range
or not Range.contains(self._processed_injection_range, range or entire_document_range)
then
return false return false
end end
@@ -416,11 +422,12 @@ function LanguageTree:_parse_regions(range, thread_state)
end end
--- @private --- @private
--- @param range Range|true
--- @return number --- @return number
function LanguageTree:_add_injections() function LanguageTree:_add_injections(range)
local seen_langs = {} ---@type table<string,boolean> local seen_langs = {} ---@type table<string,boolean>
local query_time, injections_by_lang = tcall(self._get_injections, self) local query_time, injections_by_lang = tcall(self._get_injections, self, range)
for lang, injection_regions in pairs(injections_by_lang) do for lang, injection_regions in pairs(injections_by_lang) do
local has_lang = pcall(language.add, lang) local has_lang = pcall(language.add, lang)
@@ -604,13 +611,21 @@ function LanguageTree:_parse(range, thread_state)
end end
-- Need to run injections when we parsed something -- Need to run injections when we parsed something
if no_regions_parsed > 0 then if no_regions_parsed > 0 then
self._injections_processed = false self._processed_injection_range = nil
end end
end end
if not self._injections_processed and range then if
query_time = self:_add_injections() range
self._injections_processed = true and not (
self._processed_injection_range
and Range.contains(
self._processed_injection_range,
range ~= true and range or entire_document_range
)
)
then
query_time = self:_add_injections(range)
end end
self:_log({ self:_log({
@@ -986,18 +1001,27 @@ end
--- TODO: Allow for an offset predicate to tailor the injection range --- TODO: Allow for an offset predicate to tailor the injection range
--- instead of using the entire nodes range. --- instead of using the entire nodes range.
--- @private --- @private
--- @param range Range|true
--- @return table<string, Range6[][]> --- @return table<string, Range6[][]>
function LanguageTree:_get_injections() function LanguageTree:_get_injections(range)
if not self._injection_query or #self._injection_query.captures == 0 then if not self._injection_query or #self._injection_query.captures == 0 then
self._processed_injection_range = entire_document_range
return {} return {}
end end
---@type table<integer,vim.treesitter.languagetree.Injection> ---@type table<integer,vim.treesitter.languagetree.Injection>
local injections = {} local injections = {}
local full_scan = range == true or self._injection_query.has_combined_injections
for index, tree in pairs(self._trees) do for index, tree in pairs(self._trees) do
local root_node = tree:root() local root_node = tree:root()
local start_line, _, end_line, _ = root_node:range() local start_line, end_line ---@type integer, integer
if full_scan then
start_line, _, end_line = root_node:range()
else
start_line, _, end_line = Range.unpack4(range --[[@as Range]])
end
for pattern, match, metadata in for pattern, match, metadata in
self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1)
@@ -1034,6 +1058,12 @@ function LanguageTree:_get_injections()
end end
end end
if full_scan then
self._processed_injection_range = entire_document_range
else
self._processed_injection_range = range --[[@as Range]]
end
return result return result
end end

View File

@@ -30,9 +30,11 @@ end
--- Splits the query patterns into predicates and directives. --- Splits the query patterns into predicates and directives.
---@param patterns table<integer, (integer|string)[][]> ---@param patterns table<integer, (integer|string)[][]>
---@return table<integer, vim.treesitter.query.ProcessedPattern> ---@return table<integer, vim.treesitter.query.ProcessedPattern>
---@return boolean
local function process_patterns(patterns) local function process_patterns(patterns)
---@type table<integer, vim.treesitter.query.ProcessedPattern> ---@type table<integer, vim.treesitter.query.ProcessedPattern>
local processed_patterns = {} local processed_patterns = {}
local has_combined = false
for k, pattern_list in pairs(patterns) do for k, pattern_list in pairs(patterns) do
---@type vim.treesitter.query.ProcessedPredicate[] ---@type vim.treesitter.query.ProcessedPredicate[]
@@ -47,6 +49,9 @@ local function process_patterns(patterns)
if is_directive(pred_name) then if is_directive(pred_name) then
table.insert(directives, pattern) table.insert(directives, pattern)
if vim.deep_equal(pattern, { 'set!', 'injection.combined' }) then
has_combined = true
end
else else
local should_match = true local should_match = true
if pred_name:match('^not%-') then if pred_name:match('^not%-') then
@@ -60,7 +65,7 @@ local function process_patterns(patterns)
processed_patterns[k] = { predicates = predicates, directives = directives } processed_patterns[k] = { predicates = predicates, directives = directives }
end end
return processed_patterns return processed_patterns, has_combined
end end
---@nodoc ---@nodoc
@@ -71,6 +76,7 @@ end
---@field captures string[] list of (unique) capture names defined in query ---@field captures string[] list of (unique) capture names defined in query
---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives) ---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives)
---@field query TSQuery userdata query object ---@field query TSQuery userdata query object
---@field has_combined_injections boolean whether the query contains combined injections
---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern> ---@field private _processed_patterns table<integer, vim.treesitter.query.ProcessedPattern>
local Query = {} local Query = {}
Query.__index = Query Query.__index = Query
@@ -90,7 +96,7 @@ function Query.new(lang, ts_query)
patterns = query_info.patterns, patterns = query_info.patterns,
} }
self.captures = self.info.captures self.captures = self.info.captures
self._processed_patterns = process_patterns(self.info.patterns) self._processed_patterns, self.has_combined_injections = process_patterns(self.info.patterns)
return self return self
end end

View File

@@ -633,7 +633,7 @@ int x = INT_MAX;
}, get_ranges()) }, get_ranges())
n.feed('7ggI//<esc>') n.feed('7ggI//<esc>')
exec_lua([[parser:parse({5, 6})]]) exec_lua([[parser:parse(true)]])
eq('table', exec_lua('return type(parser:children().c)')) eq('table', exec_lua('return type(parser:children().c)'))
eq(2, exec_lua('return #parser:children().c:trees()')) eq(2, exec_lua('return #parser:children().c:trees()'))
eq({ eq({
@@ -1122,7 +1122,7 @@ print()
) )
eq( eq(
2, 1,
exec_lua(function() exec_lua(function()
_G.parser:parse({ 2, 6 }) _G.parser:parse({ 2, 6 })
return #_G.parser:children().lua:trees() return #_G.parser:children().lua:trees()
@@ -1172,10 +1172,10 @@ print()
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()')) eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end) end)
it('is fully valid after a parsing a range on parsed tree', function() it('is valid within a range on parsed tree after parsing it', function()
exec_lua('vim.treesitter.get_parser():parse({5, 7})') exec_lua('vim.treesitter.get_parser():parse({5, 7})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)')) eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()')) eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(nil, {5, 7})'))
end) end)
describe('when adding content with injections', function() describe('when adding content with injections', function()
@@ -1200,14 +1200,11 @@ print()
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()')) eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end) end)
it( it('is valid within a range on parsed tree after parsing it', function()
'is fully valid after a range parse that leads to parsing not parsed injections',
function()
exec_lua('vim.treesitter.get_parser():parse({5, 7})') exec_lua('vim.treesitter.get_parser():parse({5, 7})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)')) eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()')) eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(nil, {5, 7})'))
end end)
)
it( it(
'is valid excluding, invalid including children after a range parse that does not lead to parsing not parsed injections', 'is valid excluding, invalid including children after a range parse that does not lead to parsing not parsed injections',
@@ -1249,10 +1246,10 @@ print()
eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()')) eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()'))
end) end)
it('is fully valid after a range parse that leads to parsing modified child tree', function() it('is valid within a range parse that leads to parsing modified child tree', function()
exec_lua('vim.treesitter.get_parser():parse({5, 7})') exec_lua('vim.treesitter.get_parser():parse({5, 7})')
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)')) eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()')) eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(nil, {5, 7})'))
end) end)
it( it(