perf(treesitter): do not scan past given line for predicate match

Problem
---
If a highlighter query returns a significant number of predicate
non-matches, the highlighter will scan well past the end of the window.

Solution
---
In the iterator returned from `iter_captures`, accept an optional
parameter `end_line`. If no parameter provided, the behavior is
unchanged, hence this is a non-invasive tweak.

Fixes: #25113 nvim-treesitter/nvim-treesitter#5057
This commit is contained in:
L Lllvvuu
2023-09-16 02:48:49 -07:00
committed by Lewis Russell
parent 091b57d766
commit 07080f67fe
3 changed files with 41 additions and 34 deletions

View File

@@ -976,8 +976,8 @@ Query:iter_captures({node}, {source}, {start}, {stop})
• {stop} (integer) Stopping line for the search (end-exclusive) • {stop} (integer) Stopping line for the search (end-exclusive)
Return: ~ Return: ~
(fun(): integer, TSNode, TSMetadata): capture id, capture node, (fun(end_line: integer|nil): integer, TSNode, TSMetadata): capture id,
metadata capture node, metadata
*Query:iter_matches()* *Query:iter_matches()*
Query:iter_matches({node}, {source}, {start}, {stop}, {opts}) Query:iter_matches({node}, {source}, {start}, {stop}, {opts})

View File

@@ -2,7 +2,7 @@ local api = vim.api
local query = vim.treesitter.query local query = vim.treesitter.query
local Range = require('vim.treesitter._range') local Range = require('vim.treesitter._range')
---@alias TSHlIter fun(): integer, TSNode, TSMetadata ---@alias TSHlIter fun(end_line: integer|nil): integer, TSNode, TSMetadata
---@class TSHighlightState ---@class TSHighlightState
---@field next_row integer ---@field next_row integer
@@ -241,40 +241,43 @@ local function on_line_impl(self, buf, line, is_spell_nav)
end end
while line >= state.next_row do while line >= state.next_row do
local capture, node, metadata = state.iter() local capture, node, metadata = state.iter(line)
if capture == nil then local range = { root_end_row + 1, 0, root_end_row + 1, 0 }
break if node then
range = vim.treesitter.get_range(node, buf, metadata and metadata[capture])
end end
local range = vim.treesitter.get_range(node, buf, metadata[capture])
local start_row, start_col, end_row, end_col = Range.unpack4(range) local start_row, start_col, end_row, end_col = Range.unpack4(range)
local hl = highlighter_query.hl_cache[capture]
local capture_name = highlighter_query:query().captures[capture] if capture then
local spell = nil ---@type boolean? local hl = highlighter_query.hl_cache[capture]
if capture_name == 'spell' then
spell = true local capture_name = highlighter_query:query().captures[capture]
elseif capture_name == 'nospell' then local spell = nil ---@type boolean?
spell = false if capture_name == 'spell' then
spell = true
elseif capture_name == 'nospell' then
spell = false
end
-- Give nospell a higher priority so it always overrides spell captures.
local spell_pri_offset = capture_name == 'nospell' and 1 or 0
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter)
+ spell_pri_offset
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
hl_group = hl,
ephemeral = true,
priority = priority,
conceal = metadata.conceal,
spell = spell,
})
end
end end
-- Give nospell a higher priority so it always overrides spell captures.
local spell_pri_offset = capture_name == 'nospell' and 1 or 0
if hl and end_row >= line and (not is_spell_nav or spell ~= nil) then
local priority = (tonumber(metadata.priority) or vim.highlight.priorities.treesitter)
+ spell_pri_offset
api.nvim_buf_set_extmark(buf, ns, start_row, start_col, {
end_line = end_row,
end_col = end_col,
hl_group = hl,
ephemeral = true,
priority = priority,
conceal = metadata.conceal,
spell = spell,
})
end
if start_row > line then if start_row > line then
state.next_row = start_row state.next_row = start_row
end end

View File

@@ -708,7 +708,8 @@ end
---@param start integer Starting line for the search ---@param start integer Starting line for the search
---@param stop integer Stopping line for the search (end-exclusive) ---@param stop integer Stopping line for the search (end-exclusive)
--- ---
---@return (fun(): integer, TSNode, TSMetadata): capture id, capture node, metadata ---@return (fun(end_line: integer|nil): integer, TSNode, TSMetadata):
--- capture id, capture node, metadata
function Query:iter_captures(node, source, start, stop) function Query:iter_captures(node, source, start, stop)
if type(source) == 'number' and source == 0 then if type(source) == 'number' and source == 0 then
source = api.nvim_get_current_buf() source = api.nvim_get_current_buf()
@@ -717,7 +718,7 @@ function Query:iter_captures(node, source, start, stop)
start, stop = value_or_node_range(start, stop, node) start, stop = value_or_node_range(start, stop, node)
local raw_iter = node:_rawquery(self.query, true, start, stop) local raw_iter = node:_rawquery(self.query, true, start, stop)
local function iter() local function iter(end_line)
local capture, captured_node, match = raw_iter() local capture, captured_node, match = raw_iter()
local metadata = {} local metadata = {}
@@ -725,7 +726,10 @@ function Query:iter_captures(node, source, start, stop)
local active = self:match_preds(match, match.pattern, source) local active = self:match_preds(match, match.pattern, source)
match.active = active match.active = active
if not active then if not active then
return iter() -- tail call: try next match if end_line and captured_node:range() > end_line then
return nil, captured_node, nil
end
return iter(end_line) -- tail call: try next match
end end
self:apply_directives(match, match.pattern, source, metadata) self:apply_directives(match, match.pattern, source, metadata)