fix(rust)!: expose QueryMatch::captures as a method

The field handed out a slice whose lifetime outlived the streaming-iterator
loan it came from. An accessor reborrowing through `&self` bounds it correctly.

BREAKING CHANGE: In the rust bindings, replace `m.captures` with `m.captures()`.
This commit is contained in:
Will Lillis 2026-08-29 17:44:18 -04:00
parent efa0dd9a36
commit 5ddf71b9f4
7 changed files with 40 additions and 35 deletions

View file

@ -75,7 +75,7 @@ pub fn query_file_at_path(
if opts.ordered_captures {
let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice());
while let Some((mat, capture_index)) = captures.next() {
let capture = mat.captures[*capture_index];
let capture = mat.captures()[*capture_index];
let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test {
writeln!(
@ -102,7 +102,7 @@ pub fn query_file_at_path(
if !opts.quiet && !should_test {
writeln!(&mut stdout, " pattern: {}", m.pattern_index)?;
}
for capture in m.captures {
for capture in m.captures() {
let start = capture.node.start_position();
let end = capture.node.end_position();
let capture_name = &query.capture_names()[capture.index as usize];

View file

@ -336,7 +336,7 @@ pub fn collect_matches<'a>(
while let Some(m) = matches.next() {
result.push((
m.pattern_index,
format_captures(m.captures.iter().into_streaming_iter_ref(), query, source),
format_captures(m.captures().iter().into_streaming_iter_ref(), query, source),
));
}
result
@ -347,7 +347,7 @@ pub fn collect_captures<'a>(
query: &'a Query,
source: &'a str,
) -> Vec<(&'a str, &'a str)> {
format_captures(captures.map(|(m, i)| m.captures[*i]), query, source)
format_captures(captures.map(|(m, i)| m.captures()[*i]), query, source)
}
fn format_captures<'a>(

View file

@ -2918,7 +2918,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2934,7 +2934,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -2950,7 +2950,7 @@ fn test_query_matches_with_wildcard_at_root_intersecting_byte_range() {
);
while let Some(mat) = match_iter.next() {
if let Some(capture) = mat.captures.first() {
if let Some(capture) = mat.captures().first() {
matches.push(capture.node.kind());
}
}
@ -3008,7 +3008,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
let mut results = Vec::new();
let mut first_five = captures.by_ref().take(5);
while let Some((mat, capture_ix)) = first_five.next() {
let capture = mat.captures[*capture_ix];
let capture = mat.captures()[*capture_ix];
results.push((
query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()],
@ -3031,7 +3031,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
results.clear();
captures.set_byte_range(source.find("Ok").unwrap()..source.len());
while let Some((mat, capture_ix)) = captures.next() {
let capture = mat.captures[*capture_ix];
let capture = mat.captures()[*capture_ix];
results.push((
query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()],
@ -3330,7 +3330,7 @@ fn test_query_matches_with_captured_wildcard_at_root() {
while let Some(m) = match_iter.next() {
let captures = m
.captures
.captures()
.iter()
.map(|c| {
(
@ -4311,7 +4311,7 @@ fn test_query_captures_with_matches_removed() {
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() {
let capture = m.captures[*i];
let capture = m.captures()[*i];
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "a" {
m.remove();
@ -4356,7 +4356,7 @@ fn test_query_captures_with_matches_removed_before_they_finish() {
let mut captured_strings = Vec::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() {
let capture = m.captures[*i];
let capture = m.captures()[*i];
let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "as" {
m.remove();
@ -4397,18 +4397,18 @@ fn test_query_captures_and_matches_iterators_are_fused() {
let mut cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert_eq!(captures.next().unwrap().0.captures()[0].index, 0);
assert!(captures.next().is_none());
assert!(captures.next().is_none());
assert!(captures.next().is_none());
drop(captures);
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert_eq!(matches.next().unwrap().captures()[0].index, 0);
assert!(matches.next().is_none());
assert!(matches.next().is_none());
assert!(matches.next().is_none());
@ -4588,7 +4588,7 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
.matches(&query, node, source.as_bytes())
.next()
.unwrap()
.captures[0]
.captures()[0]
.node
}
@ -4609,7 +4609,7 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
.next()
.unwrap()
.0
.captures[0]
.captures()[0]
.node
}
@ -4819,7 +4819,7 @@ fn test_query_random() {
let transformed_match = Match {
last_node: None,
captures: mat
.captures
.captures()
.iter()
.map(|c| (query.capture_names()[c.index as usize], c.node))
.collect::<Vec<_>>(),
@ -5833,10 +5833,10 @@ fn test_consecutive_zero_or_modifiers() {
let mut len_1 = false;
while let Some(m) = matches.next() {
if m.captures.len() == 3 {
if m.captures().len() == 3 {
len_3 = true;
}
if m.captures.len() == 1 {
if m.captures().len() == 1 {
len_1 = true;
}
}
@ -5898,7 +5898,7 @@ fn test_query_max_start_depth_more() {
let query = Query::new(&language, "(compound_statement) @capture").unwrap();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
let node = matches.next().unwrap().captures[0].node;
let node = matches.next().unwrap().captures()[0].node;
assert_eq!(node.kind(), "compound_statement");
for row in rows {

View file

@ -30,7 +30,7 @@ fn tree_query<I: AsRef<[u8]>>(tree: &Tree, text: impl TextProvider<I>, language:
let mut cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), text);
let (match_, idx) = captures.next().unwrap();
let capture = match_.captures[*idx];
let capture = match_.captures()[*idx];
assert_eq!(capture.index as usize, *idx);
assert_eq!("comment", capture.node.kind());
}

View file

@ -800,7 +800,7 @@ impl<'a> HighlightIterLayer<'a> {
let next_start = self
.captures
.peek()
.map(|(m, i)| m.captures[*i].node.start_byte());
.map(|(m, i)| m.captures()[*i].node.start_byte());
let next_end = self.highlight_end_stack.last().copied();
match (next_start, next_end) {
(Some(start), Some(end)) => {
@ -926,7 +926,7 @@ where
let range;
let layer = &mut self.layers[0];
if let Some((next_match, capture_index)) = layer.captures.peek() {
let next_capture = next_match.captures[*capture_index];
let next_capture = next_match.captures()[*capture_index];
range = next_capture.node.byte_range();
// If any previous highlight ends before this node starts, then before
@ -950,7 +950,7 @@ where
}
let (mut match_, capture_index) = layer.captures.next().unwrap();
let mut capture = match_.captures[capture_index];
let mut capture = match_.captures()[capture_index];
// If this capture represents an injection, then process the injection.
if match_.pattern_index < layer.config.locals_pattern_index {
@ -1037,7 +1037,7 @@ where
let scope = layer.scope_stack.last_mut().unwrap();
let mut value_range = 0..0;
for capture in match_.captures {
for capture in match_.captures() {
if Some(capture.index) == layer.config.local_def_value_capture_index {
value_range = capture.node.byte_range();
}
@ -1080,7 +1080,7 @@ where
// Continue processing any additional matches for the same node.
if let Some((next_match, next_capture_index)) = layer.captures.peek() {
let next_capture = next_match.captures[*next_capture_index];
let next_capture = next_match.captures()[*next_capture_index];
if next_capture.node == capture.node {
capture = next_capture;
match_ = layer.captures.next().unwrap().0;
@ -1110,7 +1110,7 @@ where
// captures are guaranteed to be for highlighting, not injections or
// local variables.
while let Some((next_match, next_capture_index)) = layer.captures.peek() {
let next_capture = next_match.captures[*next_capture_index];
let next_capture = next_match.captures()[*next_capture_index];
if next_capture.node == capture.node {
let following_match = layer.captures.next().unwrap().0;
// If the current node was found to be a local variable, then ignore
@ -1323,7 +1323,7 @@ fn injection_for_match<'a>(
let mut language_name = None;
let mut content_node = None;
for capture in query_match.captures {
for capture in query_match.captures() {
let index = Some(capture.index);
if index == language_capture_index {
language_name = capture.node.utf8_text(source).ok();

View file

@ -375,7 +375,7 @@ where
let pattern_info = &self.config.pattern_info[mat.pattern_index];
if mat.pattern_index < self.config.tags_pattern_index {
for capture in mat.captures {
for capture in mat.captures() {
let index = Some(capture.index);
let range = capture.node.byte_range();
if index == self.config.local_scope_capture_index {
@ -405,7 +405,7 @@ where
let mut docs_adjacent_node = None;
let mut is_ignored = false;
for capture in mat.captures {
for capture in mat.captures() {
let index = Some(capture.index);
if index == self.config.ignore_capture_index {

View file

@ -401,7 +401,7 @@ pub struct QueryPredicate {
/// A match of a [`Query`] to a particular set of [`Node`]s.
pub struct QueryMatch<'cursor, 'tree> {
pub pattern_index: usize,
pub captures: &'cursor [QueryCapture<'tree>],
captures: &'cursor [QueryCapture<'tree>],
id: u32,
cursor: *mut ffi::TSQueryCursor,
}
@ -3434,6 +3434,11 @@ impl<'tree> QueryMatch<'_, 'tree> {
self.id
}
#[must_use]
pub const fn captures(&self) -> &[QueryCapture<'tree>] {
self.captures
}
#[doc(alias = "ts_query_cursor_remove_match")]
pub fn remove(&self) {
unsafe { ffi::ts_query_cursor_remove_match(self.cursor, self.id) }