diff options
-rw-r--r-- | runtime/doc/news.txt | 3 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 52 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 10 | ||||
-rw-r--r-- | test/functional/treesitter/parser_spec.lua | 25 |
4 files changed, 63 insertions, 27 deletions
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index f743bfc78b..314ac23338 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -340,6 +340,9 @@ PERFORMANCE • Treesitter highlighting is now asynchronous. To force synchronous parsing, use `vim.g._ts_force_sync_parsing = true`. • 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 diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index ac7c08b0f8..ab53341c33 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -46,6 +46,9 @@ local Range = require('vim.treesitter._range') local default_parse_timeout_ms = 3 +---@type Range2 +local entire_document_range = { 0, math.huge } + ---@alias TSCallbackName ---| 'changedtree' ---| 'bytes' @@ -77,7 +80,7 @@ local TSCallbackNames = { ---@field package _callbacks_rec table<TSCallbackName,function[]> Callback handlers (recursive) ---@field private _children table<string,vim.treesitter.LanguageTree> 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 _parser TSParser Parser for language ---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, _injection_query = injections[lang] and query.parse(lang, injections[lang]) or query.get(lang, 'injections'), - _injections_processed = false, + _processed_injection_range = nil, _valid_regions = {}, _num_valid_regions = 0, _num_regions = 1, @@ -334,7 +337,10 @@ function LanguageTree:is_valid(exclude_children, range) end 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 end @@ -416,11 +422,12 @@ function LanguageTree:_parse_regions(range, thread_state) end --- @private +--- @param range Range|true --- @return number -function LanguageTree:_add_injections() +function LanguageTree:_add_injections(range) 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 local has_lang = pcall(language.add, lang) @@ -604,13 +611,21 @@ function LanguageTree:_parse(range, thread_state) end -- Need to run injections when we parsed something if no_regions_parsed > 0 then - self._injections_processed = false + self._processed_injection_range = nil end end - if not self._injections_processed and range then - query_time = self:_add_injections() - self._injections_processed = true + if + range + 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 self:_log({ @@ -986,18 +1001,27 @@ end --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. --- @private +--- @param range Range|true --- @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 + self._processed_injection_range = entire_document_range return {} end ---@type table<integer,vim.treesitter.languagetree.Injection> local injections = {} + local full_scan = range == true or self._injection_query.has_combined_injections + for index, tree in pairs(self._trees) do 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 self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) @@ -1034,6 +1058,12 @@ function LanguageTree:_get_injections() end end + if full_scan then + self._processed_injection_range = entire_document_range + else + self._processed_injection_range = range --[[@as Range]] + end + return result end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 10fb82e533..d26aa8e604 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -30,9 +30,11 @@ end --- Splits the query patterns into predicates and directives. ---@param patterns table<integer, (integer|string)[][]> ---@return table<integer, vim.treesitter.query.ProcessedPattern> +---@return boolean local function process_patterns(patterns) ---@type table<integer, vim.treesitter.query.ProcessedPattern> local processed_patterns = {} + local has_combined = false for k, pattern_list in pairs(patterns) do ---@type vim.treesitter.query.ProcessedPredicate[] @@ -47,6 +49,9 @@ local function process_patterns(patterns) if is_directive(pred_name) then table.insert(directives, pattern) + if vim.deep_equal(pattern, { 'set!', 'injection.combined' }) then + has_combined = true + end else local should_match = true if pred_name:match('^not%-') then @@ -60,7 +65,7 @@ local function process_patterns(patterns) processed_patterns[k] = { predicates = predicates, directives = directives } end - return processed_patterns + return processed_patterns, has_combined end ---@nodoc @@ -71,6 +76,7 @@ end ---@field captures string[] list of (unique) capture names defined in query ---@field info vim.treesitter.QueryInfo query context (e.g. captures, predicates, directives) ---@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> local Query = {} Query.__index = Query @@ -90,7 +96,7 @@ function Query.new(lang, ts_query) patterns = query_info.patterns, } 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 end diff --git a/test/functional/treesitter/parser_spec.lua b/test/functional/treesitter/parser_spec.lua index eb4651a81d..ebf9f5a11b 100644 --- a/test/functional/treesitter/parser_spec.lua +++ b/test/functional/treesitter/parser_spec.lua @@ -633,7 +633,7 @@ int x = INT_MAX; }, get_ranges()) 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(2, exec_lua('return #parser:children().c:trees()')) eq({ @@ -1122,7 +1122,7 @@ print() ) eq( - 2, + 1, exec_lua(function() _G.parser:parse({ 2, 6 }) return #_G.parser:children().lua:trees() @@ -1172,10 +1172,10 @@ print() eq(true, exec_lua('return vim.treesitter.get_parser():is_valid()')) 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})') 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) describe('when adding content with injections', function() @@ -1200,14 +1200,11 @@ print() eq(false, exec_lua('return vim.treesitter.get_parser():is_valid()')) end) - it( - 'is fully valid after a range parse that leads to parsing not parsed injections', - function() - 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()')) - end - ) + it('is valid within a range on parsed tree after parsing it', function() + 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(nil, {5, 7})')) + end) it( '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()')) 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})') 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) it( |