mirror of
https://github.com/neovim/neovim.git
synced 2025-09-06 11:28:22 +00:00
perf(treesitter): only search for injections within the parse range
Co-authored-by: Jaehwang Jung <tomtomjhj@gmail.com>
This commit is contained in:

committed by
Christian Clason

parent
b533c0f222
commit
562056c875
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
@@ -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',
|
exec_lua('vim.treesitter.get_parser():parse({5, 7})')
|
||||||
function()
|
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
|
||||||
exec_lua('vim.treesitter.get_parser():parse({5, 7})')
|
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(nil, {5, 7})'))
|
||||||
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid(true)'))
|
end)
|
||||||
eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()'))
|
|
||||||
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(
|
||||||
|
Reference in New Issue
Block a user