diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 446337411..0ddfb2a74 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -374,6 +374,7 @@ impl MappableCommand { search_prev, "Select previous search match", extend_search_next, "Add next search match to selection", extend_search_prev, "Add previous search match to selection", + extend_search_all, "Add every search match to selection", search_selection, "Use current selection as search pattern", search_selection_detect_word_boundaries, "Use current selection as the search pattern, automatically wrapping with `\\b` on word boundaries", make_search_word_bounded, "Modify current search to make it word bounded", @@ -2132,6 +2133,7 @@ fn search_impl( editor: &mut Editor, regex: &rope::Regex, movement: Movement, + start_location: Direction, direction: Direction, scrolloff: usize, wrap_around: bool, @@ -2143,7 +2145,7 @@ fn search_impl( // Get the right side of the primary block cursor for forward search, or the // grapheme before the start of the selection for reverse search. - let start = match direction { + let start = match start_location { Direction::Forward => text.char_to_byte(graphemes::ensure_grapheme_boundary_next( text, selection.primary().to(), @@ -2263,6 +2265,7 @@ fn searcher(cx: &mut Context, direction: Direction) { ®ex, movement, direction, + direction, scrolloff, wrap_around, false, @@ -2300,6 +2303,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir ®ex, movement, direction, + direction, scrolloff, wrap_around, true, @@ -2327,6 +2331,68 @@ fn extend_search_prev(cx: &mut Context) { search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward); } +fn extend_search_all(cx: &mut Context) { + let register = cx + .register + .unwrap_or(cx.editor.registers.last_search_register); + let config = cx.editor.config(); + let scrolloff = config.scrolloff; + if let Some(query) = cx.editor.registers.first(register, cx.editor) { + let search_config = &config.search; + let case_insensitive = if search_config.smart_case { + !query.chars().any(char::is_uppercase) + } else { + false + }; + if let Ok(regex) = rope::RegexBuilder::new() + .syntax( + rope::Config::new() + .case_insensitive(case_insensitive) + .multi_line(true), + ) + .build(&query) + { + search_impl( + cx.editor, + ®ex, + Movement::Extend, + // Start the first search before the current location. + // This is so after ["search_selection", "extend_search_all"] + // the primary selection remains where it started. + Direction::Backward, + Direction::Forward, + scrolloff, + true, + true, + ); + + // extend_search_next until we reach the first match + let (view, doc) = current_ref!(cx.editor); + let first_match = doc.selection(view.id).primary(); + loop { + search_impl( + cx.editor, + ®ex, + Movement::Extend, + Direction::Forward, + Direction::Forward, + scrolloff, + true, + true, + ); + let (view, doc) = current_ref!(cx.editor); + let current_match = doc.selection(view.id).primary(); + if current_match == first_match { + break; + } + } + } else { + let error = format!("Invalid regex: {}", query); + cx.editor.set_error(error); + } + } +} + fn search_selection(cx: &mut Context) { search_selection_impl(cx, false) } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 29f76cfb8..55172382a 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -2,6 +2,7 @@ use helix_term::application::Application; use super::*; +mod extend_search_all; mod insert; mod movement; mod write; diff --git a/helix-term/tests/test/commands/extend_search_all.rs b/helix-term/tests/test/commands/extend_search_all.rs new file mode 100644 index 000000000..fc62dbea6 --- /dev/null +++ b/helix-term/tests/test/commands/extend_search_all.rs @@ -0,0 +1,78 @@ +use super::*; + +use helix_core::hashmap; +use helix_term::keymap; +use helix_view::document::Mode; + +#[tokio::test(flavor = "multi_thread")] +async fn extend_search_all() -> anyhow::Result<()> { + let mut config = Config::default(); + config.keys.insert( + Mode::Normal, + keymap!({"Normal Mode" + // Typically you would want to bind these to the same key. + // e.g. "key" = ["search_selection", "extend_search_all"] + "a" => search_selection, + "b" => extend_search_all, + }), + ); + + // Test extend_search_all + test_with_config( + AppBuilder::new().with_config(config.clone()), + ( + indoc! {"\ + one_cat one cat three + two_dog two dog xthreex + #[three|]#_cow three cow + "}, + "ab", + indoc! {"\ + one_cat one cat #(three|)# + two_dog two dog x#(three|)#x + #[three|]#_cow #(three|)# cow + "}, + ), + ) + .await?; + + // Test extend_search_all preserves selection direction. + test_with_config( + AppBuilder::new().with_config(config.clone()), + ( + indoc! {"\ + one_cat one cat three + two_dog two dog xthreex + #[|three]#_cow three cow + "}, + "ab", + indoc! {"\ + one_cat one cat #(|three)# + two_dog two dog x#(|three)#x + #[|three]#_cow #(|three)# cow + "}, + ), + ) + .await?; + + // Test extend_search_all with multiple queries. + test_with_config( + AppBuilder::new().with_config(config.clone()), + ( + indoc! {"\ + one_#(|cat)# one cat three + two_dog two dog xthreex + #[|three]#_cow three cow cattle + "}, + "ab", + indoc! {"\ + one_#(|cat)# one #(|cat)# #(|three)# + two_dog two dog x#(|three)#x + #[|three]#_cow #(|three)# cow #(|cat)#tle + "}, + ), + ) + .await?; + + Ok(()) +}