diff --git a/cli/src/tests/query_test.rs b/cli/src/tests/query_test.rs index 6634c62f..215a8ee1 100644 --- a/cli/src/tests/query_test.rs +++ b/cli/src/tests/query_test.rs @@ -3091,6 +3091,53 @@ fn test_query_captures_with_matches_removed() { }); } +#[test] +fn test_query_captures_with_matches_removed_before_they_finish() { + allocations::record(|| { + let language = get_language("javascript"); + // When Tree-sitter detects that a pattern is guaranteed to match, + // it will start to eagerly return the captures that it has found, + // even though it hasn't matched the entire pattern yet. A + // namespace_import node always has "*", "as" and then an identifier + // for children, so captures will be emitted eagerly for this pattern. + let query = Query::new( + language, + r#" + (namespace_import + "*" @star + "as" @as + (identifier) @identifier) + "#, + ) + .unwrap(); + + let source = " + import * as name from 'module-name'; + "; + + let mut parser = Parser::new(); + parser.set_language(language).unwrap(); + let tree = parser.parse(&source, None).unwrap(); + let mut cursor = QueryCursor::new(); + + let mut captured_strings = Vec::new(); + for (m, i) in cursor.captures(&query, tree.root_node(), source.as_bytes()) { + let capture = m.captures[i]; + let text = capture.node.utf8_text(source.as_bytes()).unwrap(); + if text == "as" { + m.remove(); + continue; + } + captured_strings.push(text); + } + + // .remove() removes the match before it is finished. The identifier + // "name" is part of this match, so we expect that removing the "as" + // capture from the match should prevent "name" from matching: + assert_eq!(captured_strings, &["*",]); + }); +} + #[test] fn test_query_captures_and_matches_iterators_are_fused() { allocations::record(|| { diff --git a/highlight/src/lib.rs b/highlight/src/lib.rs index 58d7e88c..0d097201 100644 --- a/highlight/src/lib.rs +++ b/highlight/src/lib.rs @@ -835,6 +835,7 @@ where // highlighting patterns that are disabled for local variables. if definition_highlight.is_some() || reference_highlight.is_some() { while layer.config.non_local_variable_patterns[match_.pattern_index] { + match_.remove(); if let Some((next_match, next_capture_index)) = layer.captures.peek() { let next_capture = next_match.captures[*next_capture_index]; if next_capture.node == capture.node { diff --git a/lib/src/query.c b/lib/src/query.c index ef83d5f9..7878cf55 100644 --- a/lib/src/query.c +++ b/lib/src/query.c @@ -3727,6 +3727,20 @@ void ts_query_cursor_remove_match( return; } } + + // Remove unfinished query states as well to prevent future + // captures for a match being removed. + for (unsigned i = 0; i < self->states.size; i++) { + const QueryState *state = &self->states.contents[i]; + if (state->id == match_id) { + capture_list_pool_release( + &self->capture_list_pool, + state->capture_list_id + ); + array_erase(&self->states, i); + return; + } + } } bool ts_query_cursor_next_capture(