diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 76de63628..f74e1ab90 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -783,13 +783,15 @@ pub fn select_on_matches( text: RopeSlice, selection: &Selection, regex: &rope::Regex, -) -> Option { +) -> Option<(Selection, Vec>)> { let mut result = SmallVec::with_capacity(selection.len()); + let mut captures = vec![vec![]; regex.captures_len()]; for sel in selection { - for mat in regex.find_iter(text.regex_input_at(sel.from()..sel.to())) { + for cap in regex.captures_iter(text.regex_input_at(sel.from()..sel.to())) { // TODO: retain range direction + let mat = cap.get_match().unwrap(); let start = text.byte_to_char(mat.start()); let end = text.byte_to_char(mat.end()); @@ -798,13 +800,20 @@ pub fn select_on_matches( // These invalid matches can come from using RegEx anchors like `^`, `$` if range != Range::point(sel.to()) { result.push(range); + for (i, captures) in captures.iter_mut().enumerate() { + captures.push( + cap.get_group(i) + .map(|group| text.slice(group.range()).to_string()) + .unwrap_or_default(), + ); + } } } } // TODO: figure out a new primary index if !result.is_empty() { - return Some(Selection::new(result, 0)); + return Some((Selection::new(result, 0), captures)); } None @@ -1099,9 +1108,9 @@ mod test { let selection = Selection::single(0, r.len_chars()); assert_eq!( select_on_matches(s, &selection, &rope::Regex::new(r"[A-Z][a-z]*").unwrap()), - Some(Selection::new( - smallvec![Range::new(0, 6), Range::new(19, 26)], - 0 + Some(( + Selection::new(smallvec![Range::new(0, 6), Range::new(19, 26)], 0), + vec![vec!["Nobody".to_string(), "Spanish".to_string()]] )) ); @@ -1120,7 +1129,7 @@ mod test { // line without ending assert_eq!( select_on_matches(s, &Selection::single(0, 4), &start_of_line), - Some(Selection::single(0, 0)) + Some((Selection::single(0, 0), vec![vec!["".to_string()]])) ); assert_eq!( select_on_matches(s, &Selection::single(0, 4), &end_of_line), @@ -1129,23 +1138,23 @@ mod test { // line with ending assert_eq!( select_on_matches(s, &Selection::single(0, 5), &start_of_line), - Some(Selection::single(0, 0)) + Some((Selection::single(0, 0), vec![vec!["".to_string()]])) ); assert_eq!( select_on_matches(s, &Selection::single(0, 5), &end_of_line), - Some(Selection::single(4, 4)) + Some((Selection::single(4, 4), vec![vec!["".to_string()]])) ); // line with start of next line assert_eq!( select_on_matches(s, &Selection::single(0, 6), &start_of_line), - Some(Selection::new( - smallvec![Range::point(0), Range::point(5)], - 0 + Some(( + Selection::new(smallvec![Range::point(0), Range::point(5)], 0), + vec![vec!["".to_string(), "".to_string()]] )) ); assert_eq!( select_on_matches(s, &Selection::single(0, 6), &end_of_line), - Some(Selection::single(4, 4)) + Some((Selection::single(4, 4), vec![vec!["".to_string()]])) ); // multiple lines @@ -1158,9 +1167,16 @@ mod test { .build(r"^[a-z ]*$") .unwrap() ), - Some(Selection::new( - smallvec![Range::point(12), Range::new(13, 30), Range::new(31, 36)], - 0 + Some(( + Selection::new( + smallvec![Range::point(12), Range::new(13, 30), Range::new(31, 36)], + 0 + ), + vec![vec![ + "".to_string(), + "contains multiple".to_string(), + "lines".to_string() + ]] )) ); } diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ecaa18a0e..12fbd43a3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2012,10 +2012,11 @@ fn select_regex(cx: &mut Context) { return; } let text = doc.text().slice(..); - if let Some(selection) = + if let Some((selection, captures)) = selection::select_on_matches(text, doc.selection(view.id), ®ex) { doc.set_selection(view.id, selection); + cx.editor.registers.write_search_results(captures); } else { cx.editor.set_error("nothing selected"); } diff --git a/helix-view/src/register.rs b/helix-view/src/register.rs index d286a85cc..d34b94867 100644 --- a/helix-view/src/register.rs +++ b/helix-view/src/register.rs @@ -80,7 +80,9 @@ impl Registers { pub fn write(&mut self, name: char, mut values: Vec) -> Result<()> { match name { '_' => Ok(()), - '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support writing")), + '#' | '.' | '%' | '&' | '1'..='9' => { + Err(anyhow::anyhow!("Register {name} does not support writing")) + } '*' | '+' => { self.clipboard_provider.load().set_contents( &values.join(NATIVE_LINE_ENDING.as_str()), @@ -105,7 +107,9 @@ impl Registers { pub fn push(&mut self, name: char, mut value: String) -> Result<()> { match name { '_' => Ok(()), - '#' | '.' | '%' => Err(anyhow::anyhow!("Register {name} does not support pushing")), + '#' | '.' | '%' | '&' | '1'..='9' => { + Err(anyhow::anyhow!("Register {name} does not support pushing")) + } '*' | '+' => { let clipboard_type = match name { '+' => ClipboardType::Clipboard, @@ -140,6 +144,17 @@ impl Registers { } } + pub fn write_search_results(&mut self, search_results: Vec>) { + let len = search_results.len(); + for (i, mut values) in search_results.into_iter().enumerate() { + values.reverse(); + self.inner.insert(search_register_name(i), values); + } + for i in len..=9 { + self.inner.remove(&search_register_name(i)); + } + } + pub fn first<'a>(&'a self, name: char, editor: &'a Editor) -> Option> { self.read(name, editor).and_then(|mut values| values.next()) } @@ -168,6 +183,16 @@ impl Registers { ('%', ""), ('+', ""), ('*', ""), + ('&', ""), + ('1', ""), + ('2', ""), + ('3', ""), + ('4', ""), + ('5', ""), + ('6', ""), + ('7', ""), + ('8', ""), + ('9', ""), ] .iter() .copied(), @@ -192,7 +217,7 @@ impl Registers { true } - '_' | '#' | '.' | '%' => false, + '_' | '#' | '.' | '%' | '&' | '1'..='9' => false, _ => self.inner.remove(&name).is_some(), } } @@ -218,6 +243,14 @@ impl Registers { } } +fn search_register_name(i: usize) -> char { + if i == 0 { + '&' + } else { + char::from(i as u8 + b'0') + } +} + fn read_from_clipboard<'a>( provider: &ClipboardProvider, saved_values: Option<&'a Vec>,