Add extend_search_all command.

This allows me to replicate my kakoune config:
  map global normal <a-%> '*%s<ret>'
With helix config:
  "A-%" = ["search_selection", "extend_search_all"]
pull/13683/head
Andrew Browne 2025-06-03 23:27:24 -07:00
parent f6878f62f7
commit 6a20aae363
3 changed files with 146 additions and 1 deletions

View File

@ -374,6 +374,7 @@ impl MappableCommand {
search_prev, "Select previous search match", search_prev, "Select previous search match",
extend_search_next, "Add next search match to selection", extend_search_next, "Add next search match to selection",
extend_search_prev, "Add previous 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, "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", 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", make_search_word_bounded, "Modify current search to make it word bounded",
@ -2132,6 +2133,7 @@ fn search_impl(
editor: &mut Editor, editor: &mut Editor,
regex: &rope::Regex, regex: &rope::Regex,
movement: Movement, movement: Movement,
start_location: Direction,
direction: Direction, direction: Direction,
scrolloff: usize, scrolloff: usize,
wrap_around: bool, wrap_around: bool,
@ -2143,7 +2145,7 @@ fn search_impl(
// Get the right side of the primary block cursor for forward search, or the // Get the right side of the primary block cursor for forward search, or the
// grapheme before the start of the selection for reverse search. // 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( Direction::Forward => text.char_to_byte(graphemes::ensure_grapheme_boundary_next(
text, text,
selection.primary().to(), selection.primary().to(),
@ -2263,6 +2265,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
&regex, &regex,
movement, movement,
direction, direction,
direction,
scrolloff, scrolloff,
wrap_around, wrap_around,
false, false,
@ -2300,6 +2303,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
&regex, &regex,
movement, movement,
direction, direction,
direction,
scrolloff, scrolloff,
wrap_around, wrap_around,
true, true,
@ -2327,6 +2331,68 @@ fn extend_search_prev(cx: &mut Context) {
search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward); 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,
&regex,
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,
&regex,
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) { fn search_selection(cx: &mut Context) {
search_selection_impl(cx, false) search_selection_impl(cx, false)
} }

View File

@ -2,6 +2,7 @@ use helix_term::application::Application;
use super::*; use super::*;
mod extend_search_all;
mod insert; mod insert;
mod movement; mod movement;
mod write; mod write;

View File

@ -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(())
}