diff --git a/book/src/keymap.md b/book/src/keymap.md index 28113ea0b..2797eaee2 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -160,7 +160,8 @@ Search commands all operate on the `/` register by default. To use a different r | `?` | Search for previous pattern | `rsearch` | | `n` | Select next search match | `search_next` | | `N` | Select previous search match | `search_prev` | -| `*` | Use current selection as the search pattern | `search_selection` | +| `*` | Use current selection as the search pattern, automatically wrapping with `\b` on word boundaries | `search_selection_detect_word_boundaries` | +| `Alt-*` | Use current selection as the search pattern | `search_selection` | ### Minor modes diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 628f6fd27..55f8fb9c3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -353,6 +353,7 @@ impl MappableCommand { extend_search_next, "Add next search match to selection", extend_search_prev, "Add previous 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", global_search, "Global search in workspace folder", extend_line, "Select current line, if already selected, extend to another line based on the anchor", @@ -2243,14 +2244,53 @@ fn extend_search_prev(cx: &mut Context) { } fn search_selection(cx: &mut Context) { + search_selection_impl(cx, false) +} + +fn search_selection_detect_word_boundaries(cx: &mut Context) { + search_selection_impl(cx, true) +} + +fn search_selection_impl(cx: &mut Context, detect_word_boundaries: bool) { + fn is_at_word_start(text: RopeSlice, index: usize) -> bool { + let ch = text.char(index); + if index == 0 { + return char_is_word(ch); + } + let prev_ch = text.char(index - 1); + + !char_is_word(prev_ch) && char_is_word(ch) + } + + fn is_at_word_end(text: RopeSlice, index: usize) -> bool { + if index == 0 || index == text.len_chars() { + return false; + } + let ch = text.char(index); + let prev_ch = text.char(index - 1); + + char_is_word(prev_ch) && !char_is_word(ch) + } + let register = cx.register.unwrap_or('/'); let (view, doc) = current!(cx.editor); - let contents = doc.text().slice(..); + let text = doc.text().slice(..); let regex = doc .selection(view.id) .iter() - .map(|selection| regex::escape(&selection.fragment(contents))) + .map(|selection| { + let add_boundary_prefix = + detect_word_boundaries && is_at_word_start(text, selection.from()); + let add_boundary_suffix = + detect_word_boundaries && is_at_word_end(text, selection.to()); + + let prefix = if add_boundary_prefix { "\\b" } else { "" }; + let suffix = if add_boundary_suffix { "\\b" } else { "" }; + + let word = regex::escape(&selection.fragment(text)); + format!("{}{}{}", prefix, word, suffix) + }) .collect::>() // Collect into hashset to deduplicate identical regexes .into_iter() .collect::>() diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 5a3e8eed4..c6cefd927 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -140,7 +140,8 @@ pub fn default() -> HashMap { "?" => rsearch, "n" => search_next, "N" => search_prev, - "*" => search_selection, + "*" => search_selection_detect_word_boundaries, + "A-*" => search_selection, "u" => undo, "U" => redo,