on master help sign in
Allow capturing multiple nodes in textobject queries
e6c36e8Gokul Soumya
helix-core/src/syntax.rs+104−10
@@ -244,16 +244,55 @@ pub struct TextObjectQuery {
244 pub query: Query,
244 pub query: Query,
245}
245}
246
246
247+pub enum CapturedNode<'a> {
248+ Single(Node<'a>),
249+ Grouped(Vec<Node<'a>>),
250+}
251+
252+impl<'a> CapturedNode<'a> {
253+ pub fn start_byte(&self) -> usize {
254+ match self {
255+ Self::Single(n) => n.start_byte(),
256+ Self::Grouped(ns) => ns[0].start_byte(),
257+ }
258+ }
259+
260+ pub fn end_byte(&self) -> usize {
261+ match self {
262+ Self::Single(n) => n.end_byte(),
263+ Self::Grouped(ns) => ns.last().unwrap().end_byte(),
264+ }
265+ }
266+
267+ pub fn byte_range(&self) -> std::ops::Range<usize> {
268+ self.start_byte()..self.end_byte()
269+ }
270+}
271+
247impl TextObjectQuery {
272impl TextObjectQuery {
248 /// Run the query on the given node and return sub nodes which match given
273 /// Run the query on the given node and return sub nodes which match given
249 /// capture ("function.inside", "class.around", etc).
274 /// capture ("function.inside", "class.around", etc).
275+ ///
276+ /// Captures may contain multiple nodes by using quantifiers (+, *, etc),
277+ /// and support for this is partial and could use improvement.
278+ ///
279+ /// ```query
280+ /// ;; supported:
281+ /// (comment)+ @capture
282+ ///
283+ /// ;; unsupported:
284+ /// (
285+ /// (comment)+
286+ /// (function)
287+ /// ) @capture
288+ /// ```
250 pub fn capture_nodes<'a>(
289 pub fn capture_nodes<'a>(
251 &'a self,
290 &'a self,
252 capture_name: &str,
291 capture_name: &str,
253 node: Node<'a>,
292 node: Node<'a>,
254 slice: RopeSlice<'a>,
293 slice: RopeSlice<'a>,
255 cursor: &'a mut QueryCursor,
294 cursor: &'a mut QueryCursor,
256− ) -> Option<impl Iterator<Item = Node<'a>>> {
295+ ) -> Option<impl Iterator<Item = CapturedNode<'a>>> {
257 self.capture_nodes_any(&[capture_name], node, slice, cursor)
296 self.capture_nodes_any(&[capture_name], node, slice, cursor)
258 }
297 }
259
298
@@ -265,17 +304,28 @@ impl TextObjectQuery {
265 node: Node<'a>,
304 node: Node<'a>,
266 slice: RopeSlice<'a>,
305 slice: RopeSlice<'a>,
267 cursor: &'a mut QueryCursor,
306 cursor: &'a mut QueryCursor,
268− ) -> Option<impl Iterator<Item = Node<'a>>> {
307+ ) -> Option<impl Iterator<Item = CapturedNode<'a>>> {
269 let capture_idx = capture_names
308 let capture_idx = capture_names
270 .iter()
309 .iter()
271 .find_map(|cap| self.query.capture_index_for_name(cap))?;
310 .find_map(|cap| self.query.capture_index_for_name(cap))?;
272− let captures = cursor.captures(&self.query, node, RopeProvider(slice));
311+ let captures = cursor.matches(&self.query, node, RopeProvider(slice));
273
312
274− captures
313+ let nodes = captures.flat_map(move |mat| {
275− .filter_map(move |(mat, idx)| {
276− (mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node)
314+ let captures = mat.captures.iter().filter(move |c| c.index == capture_idx);
315+ let nodes = captures.map(|c| c.node);
316+ let pattern_idx = mat.pattern_index;
317+ let quantifier = self.query.capture_quantifiers(pattern_idx)[capture_idx as usize];
318+
319+ let iter: Box<dyn Iterator<Item = CapturedNode>> = match quantifier {
320+ CaptureQuantifier::OneOrMore | CaptureQuantifier::ZeroOrMore => {
321+ Box::new(std::iter::once(CapturedNode::Grouped(nodes.collect())))
277− })
322+ }
278− .into()
323+ _ => Box::new(nodes.map(CapturedNode::Single)),
324+ };
325+
326+ iter
327+ });
328+ Some(nodes)
279 }
329 }
280}
330}
281
331
@@ -1075,8 +1125,8 @@ pub(crate) fn generate_edits(
1075use std::sync::atomic::{AtomicUsize, Ordering};
1125use std::sync::atomic::{AtomicUsize, Ordering};
1076use std::{iter, mem, ops, str, usize};
1126use std::{iter, mem, ops, str, usize};
1077use tree_sitter::{
1127use tree_sitter::{
1078− Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
1128+ CaptureQuantifier, Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor,
1079− QueryMatch, Range, TextProvider, Tree,
1129+ QueryError, QueryMatch, Range, TextProvider, Tree,
1080};
1130};
1081
1131
1082const CANCELLATION_CHECK_INTERVAL: usize = 100;
1132const CANCELLATION_CHECK_INTERVAL: usize = 100;
@@ -1928,6 +1978,50 @@ mod test {
1928 use super::*;
1978 use super::*;
1929 use crate::{Rope, Transaction};
1979 use crate::{Rope, Transaction};
1930
1980
1981+ #[test]
1982+ fn test_textobject_queries() {
1983+ let query_str = r#"
1984+ (line_comment)+ @quantified_nodes
1985+ ((line_comment)+) @quantified_nodes_grouped
1986+ ((line_comment) (line_comment)) @multiple_nodes_grouped
1987+ "#;
1988+ let source = Rope::from_str(
1989+ r#"
1990+/// a comment on
1991+/// mutiple lines
1992+ "#,
1993+ );
1994+
1995+ let loader = Loader::new(Configuration { language: vec![] });
1996+ let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
1997+
1998+ let query = Query::new(language, query_str).unwrap();
1999+ let textobject = TextObjectQuery { query };
2000+ let mut cursor = QueryCursor::new();
2001+
2002+ let config = HighlightConfiguration::new(language, "", "", "").unwrap();
2003+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
2004+
2005+ let root = syntax.tree().root_node();
2006+ let mut test = |capture, range| {
2007+ let matches: Vec<_> = textobject
2008+ .capture_nodes(capture, root, source.slice(..), &mut cursor)
2009+ .unwrap()
2010+ .collect();
2011+
2012+ assert_eq!(
2013+ matches[0].byte_range(),
2014+ range,
2015+ "@{capture} expected {range:?}"
2016+ )
2017+ };
2018+
2019+ test("quantified_nodes", 1..35);
2020+ // NOTE: Enable after implementing proper node group capturing
2021+ // test("quantified_nodes_grouped", 1..35);
2022+ // test("multiple_nodes_grouped", 1..35);
2023+ }
2024+
1931 #[test]
2025 #[test]
1932 fn test_parser() {
2026 fn test_parser() {
1933 let highlight_names: Vec<String> = [
2027 let highlight_names: Vec<String> = [