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 { if opts.ordered_captures {
let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice()); let mut captures = query_cursor.captures(&query, tree.root_node(), source_code.as_slice());
while let Some((mat, capture_index)) = captures.next() { 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]; let capture_name = &query.capture_names()[capture.index as usize];
if !opts.quiet && !should_test { if !opts.quiet && !should_test {
writeln!( writeln!(
@ -102,7 +102,7 @@ pub fn query_file_at_path(
if !opts.quiet && !should_test { if !opts.quiet && !should_test {
writeln!(&mut stdout, " pattern: {}", m.pattern_index)?; writeln!(&mut stdout, " pattern: {}", m.pattern_index)?;
} }
for capture in m.captures { for capture in m.captures() {
let start = capture.node.start_position(); let start = capture.node.start_position();
let end = capture.node.end_position(); let end = capture.node.end_position();
let capture_name = &query.capture_names()[capture.index as usize]; 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() { while let Some(m) = matches.next() {
result.push(( result.push((
m.pattern_index, 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 result
@ -347,7 +347,7 @@ pub fn collect_captures<'a>(
query: &'a Query, query: &'a Query,
source: &'a str, source: &'a str,
) -> Vec<(&'a str, &'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>( 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() { 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()); 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() { 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()); 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() { 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()); 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 results = Vec::new();
let mut first_five = captures.by_ref().take(5); let mut first_five = captures.by_ref().take(5);
while let Some((mat, capture_ix)) = first_five.next() { while let Some((mat, capture_ix)) = first_five.next() {
let capture = mat.captures[*capture_ix]; let capture = mat.captures()[*capture_ix];
results.push(( results.push((
query.capture_names()[capture.index as usize], query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()], &source[capture.node.byte_range()],
@ -3031,7 +3031,7 @@ fn test_query_captures_within_byte_range_assigned_after_iterating() {
results.clear(); results.clear();
captures.set_byte_range(source.find("Ok").unwrap()..source.len()); captures.set_byte_range(source.find("Ok").unwrap()..source.len());
while let Some((mat, capture_ix)) = captures.next() { while let Some((mat, capture_ix)) = captures.next() {
let capture = mat.captures[*capture_ix]; let capture = mat.captures()[*capture_ix];
results.push(( results.push((
query.capture_names()[capture.index as usize], query.capture_names()[capture.index as usize],
&source[capture.node.byte_range()], &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() { while let Some(m) = match_iter.next() {
let captures = m let captures = m
.captures .captures()
.iter() .iter()
.map(|c| { .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()); let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() { 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(); let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "a" { if text == "a" {
m.remove(); m.remove();
@ -4356,7 +4356,7 @@ fn test_query_captures_with_matches_removed_before_they_finish() {
let mut captured_strings = Vec::new(); let mut captured_strings = Vec::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes()); let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes());
while let Some((m, i)) = captures.next() { 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(); let text = capture.node.utf8_text(source.as_bytes()).unwrap();
if text == "as" { if text == "as" {
m.remove(); m.remove();
@ -4397,18 +4397,18 @@ fn test_query_captures_and_matches_iterators_are_fused() {
let mut cursor = QueryCursor::new(); let mut cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), source.as_bytes()); 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()); assert!(captures.next().is_none());
assert!(captures.next().is_none()); assert!(captures.next().is_none());
drop(captures); drop(captures);
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes()); 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()); 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()) .matches(&query, node, source.as_bytes())
.next() .next()
.unwrap() .unwrap()
.captures[0] .captures()[0]
.node .node
} }
@ -4609,7 +4609,7 @@ fn test_query_lifetime_is_separate_from_nodes_lifetime() {
.next() .next()
.unwrap() .unwrap()
.0 .0
.captures[0] .captures()[0]
.node .node
} }
@ -4819,7 +4819,7 @@ fn test_query_random() {
let transformed_match = Match { let transformed_match = Match {
last_node: None, last_node: None,
captures: mat captures: mat
.captures .captures()
.iter() .iter()
.map(|c| (query.capture_names()[c.index as usize], c.node)) .map(|c| (query.capture_names()[c.index as usize], c.node))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
@ -5833,10 +5833,10 @@ fn test_consecutive_zero_or_modifiers() {
let mut len_1 = false; let mut len_1 = false;
while let Some(m) = matches.next() { while let Some(m) = matches.next() {
if m.captures.len() == 3 { if m.captures().len() == 3 {
len_3 = true; len_3 = true;
} }
if m.captures.len() == 1 { if m.captures().len() == 1 {
len_1 = true; len_1 = true;
} }
} }
@ -5898,7 +5898,7 @@ fn test_query_max_start_depth_more() {
let query = Query::new(&language, "(compound_statement) @capture").unwrap(); let query = Query::new(&language, "(compound_statement) @capture").unwrap();
let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes()); 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"); assert_eq!(node.kind(), "compound_statement");
for row in rows { 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 cursor = QueryCursor::new();
let mut captures = cursor.captures(&query, tree.root_node(), text); let mut captures = cursor.captures(&query, tree.root_node(), text);
let (match_, idx) = captures.next().unwrap(); 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!(capture.index as usize, *idx);
assert_eq!("comment", capture.node.kind()); assert_eq!("comment", capture.node.kind());
} }

View file

@ -800,7 +800,7 @@ impl<'a> HighlightIterLayer<'a> {
let next_start = self let next_start = self
.captures .captures
.peek() .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(); let next_end = self.highlight_end_stack.last().copied();
match (next_start, next_end) { match (next_start, next_end) {
(Some(start), Some(end)) => { (Some(start), Some(end)) => {
@ -926,7 +926,7 @@ where
let range; let range;
let layer = &mut self.layers[0]; let layer = &mut self.layers[0];
if let Some((next_match, capture_index)) = layer.captures.peek() { 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(); range = next_capture.node.byte_range();
// If any previous highlight ends before this node starts, then before // 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 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 this capture represents an injection, then process the injection.
if match_.pattern_index < layer.config.locals_pattern_index { if match_.pattern_index < layer.config.locals_pattern_index {
@ -1037,7 +1037,7 @@ where
let scope = layer.scope_stack.last_mut().unwrap(); let scope = layer.scope_stack.last_mut().unwrap();
let mut value_range = 0..0; 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 { if Some(capture.index) == layer.config.local_def_value_capture_index {
value_range = capture.node.byte_range(); value_range = capture.node.byte_range();
} }
@ -1080,7 +1080,7 @@ where
// Continue processing any additional matches for the same node. // Continue processing any additional matches for the same node.
if let Some((next_match, next_capture_index)) = layer.captures.peek() { 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 { if next_capture.node == capture.node {
capture = next_capture; capture = next_capture;
match_ = layer.captures.next().unwrap().0; match_ = layer.captures.next().unwrap().0;
@ -1110,7 +1110,7 @@ where
// captures are guaranteed to be for highlighting, not injections or // captures are guaranteed to be for highlighting, not injections or
// local variables. // local variables.
while let Some((next_match, next_capture_index)) = layer.captures.peek() { 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 { if next_capture.node == capture.node {
let following_match = layer.captures.next().unwrap().0; let following_match = layer.captures.next().unwrap().0;
// If the current node was found to be a local variable, then ignore // 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 language_name = None;
let mut content_node = None; let mut content_node = None;
for capture in query_match.captures { for capture in query_match.captures() {
let index = Some(capture.index); let index = Some(capture.index);
if index == language_capture_index { if index == language_capture_index {
language_name = capture.node.utf8_text(source).ok(); 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]; let pattern_info = &self.config.pattern_info[mat.pattern_index];
if mat.pattern_index < self.config.tags_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 index = Some(capture.index);
let range = capture.node.byte_range(); let range = capture.node.byte_range();
if index == self.config.local_scope_capture_index { if index == self.config.local_scope_capture_index {
@ -405,7 +405,7 @@ where
let mut docs_adjacent_node = None; let mut docs_adjacent_node = None;
let mut is_ignored = false; let mut is_ignored = false;
for capture in mat.captures { for capture in mat.captures() {
let index = Some(capture.index); let index = Some(capture.index);
if index == self.config.ignore_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. /// A match of a [`Query`] to a particular set of [`Node`]s.
pub struct QueryMatch<'cursor, 'tree> { pub struct QueryMatch<'cursor, 'tree> {
pub pattern_index: usize, pub pattern_index: usize,
pub captures: &'cursor [QueryCapture<'tree>], captures: &'cursor [QueryCapture<'tree>],
id: u32, id: u32,
cursor: *mut ffi::TSQueryCursor, cursor: *mut ffi::TSQueryCursor,
} }
@ -3434,6 +3434,11 @@ impl<'tree> QueryMatch<'_, 'tree> {
self.id self.id
} }
#[must_use]
pub const fn captures(&self) -> &[QueryCapture<'tree>] {
self.captures
}
#[doc(alias = "ts_query_cursor_remove_match")] #[doc(alias = "ts_query_cursor_remove_match")]
pub fn remove(&self) { pub fn remove(&self) {
unsafe { ffi::ts_query_cursor_remove_match(self.cursor, self.id) } unsafe { ffi::ts_query_cursor_remove_match(self.cursor, self.id) }