@@ -354,6 +354,25 @@ impl<'a> CapturedNode<'a> {
357+/// The number of matches a TS cursor can at once to avoid performance problems for medium to large files.
358+/// Set with `set_match_limit`.
359+/// Using such a limit means that we lose valid captures in, so there is fundamentally a tradeoff here.
362+/// Old tree sitter versions used a limit of 32 by default until this limit was removed in version `0.19.5` (must now be set manually).
363+/// However, this causes performance issues for medium to large files.
364+/// In helix, this problem caused treesitter motions to take multiple seconds to complete in medium-sized rust files (3k loc).
365+/// Neovim also encountered this problem and reintroduced this limit after it was removed upstream
366+/// (see <https://github.com/neovim/neovim/issues/14897> and <https://github.com/neovim/neovim/pull/14915>).
367+/// The number used here is fundamentally a tradeoff between breaking some obscure edge cases and performance.
370+/// A value of 64 was chosen because neovim uses that value.
371+/// Neovim chose this value somewhat arbitrarily (<https://github.com/neovim/neovim/pull/18397>) adjusting it whenever issues occur in practice.
372+/// However this value has been in use for a long time and due to the large userbase of neovim it is probably a good choice.
373+/// If this limit causes problems for a grammar in the future, it could be increased.
374+const TREE_SITTER_MATCH_LIMIT: u32 = 64;
357impl TextObjectQuery {
376impl TextObjectQuery {
358 /// Run the query on the given node and return sub nodes which match given
377 /// Run the query on the given node and return sub nodes which match given
359 /// capture ("function.inside", "class.around", etc).
378 /// capture ("function.inside", "class.around", etc).
@@ -394,6 +413,8 @@ impl TextObjectQuery {
395 .find_map(|cap| self.query.capture_index_for_name(cap))?;
414 .find_map(|cap| self.query.capture_index_for_name(cap))?;
416+ cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
397 let nodes = cursor
418 let nodes = cursor
398 .captures(&self.query, node, RopeProvider(slice))
419 .captures(&self.query, node, RopeProvider(slice))
399 .filter_map(move |(mat, _)| {
420 .filter_map(move |(mat, _)| {
@@ -843,6 +864,7 @@ impl Syntax {
843 let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
864 let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
844 // TODO: might need to set cursor range
865 // TODO: might need to set cursor range
845 cursor.set_byte_range(0..usize::MAX);
866 cursor.set_byte_range(0..usize::MAX);
867+ cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
847 let source_slice = source.slice(..);
869 let source_slice = source.slice(..);
@@ -1032,6 +1054,7 @@ impl Syntax {
1033 // if reusing cursors & no range this resets to whole range
1055 // if reusing cursors & no range this resets to whole range
1034 cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
1056 cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
1057+ cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
1036 let mut captures = cursor_ref
1059 let mut captures = cursor_ref
1037 .captures(
1060 .captures(