Skip to content

Commit

Permalink
Search selection with word boundary detection
Browse files Browse the repository at this point in the history
Inspired by Kakoune's behavior.
  • Loading branch information
MilanVasko committed Nov 25, 2024
1 parent cbbeca6 commit 9825476
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 2 deletions.
58 changes: 57 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 search pattern and detect 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",
Expand Down Expand Up @@ -2243,14 +2244,33 @@ fn extend_search_prev(cx: &mut Context) {
}

fn search_selection(cx: &mut Context) {
search_selection_generic(cx, |selection, contents| {
regex::escape(&selection.fragment(contents))
})
}

fn search_selection_detect_word_boundaries(cx: &mut Context) {
search_selection_generic(cx, |selection, contents| {
let is_bow = is_bow(selection.from(), contents);
let is_eow = is_eow(selection.to(), contents);
format!(
"{}{}{}",
if is_bow { "\\b" } else { "" },
regex::escape(&selection.fragment(contents)),
if is_eow { "\\b" } else { "" }
)
})
}

fn search_selection_generic(cx: &mut Context, mapper: impl Fn(&Range, RopeSlice) -> String) {
let register = cx.register.unwrap_or('/');
let (view, doc) = current!(cx.editor);
let contents = doc.text().slice(..);

let regex = doc
.selection(view.id)
.iter()
.map(|selection| regex::escape(&selection.fragment(contents)))
.map(|selection| mapper(selection, contents))
.collect::<HashSet<_>>() // Collect into hashset to deduplicate identical regexes
.into_iter()
.collect::<Vec<_>>()
Expand All @@ -2266,6 +2286,42 @@ fn search_selection(cx: &mut Context) {
}
}

fn is_bow(index: usize, contents: RopeSlice) -> bool {
match index.checked_sub(1) {
Some(prev_index) => get_successive_chars(prev_index, contents)
.map(|(c1, c2)| !char_is_word(c1) && char_is_word(c2))
.unwrap_or(false),
// we are at the beginning of the file
None => contents.get_char(index).map(char_is_word).unwrap_or(false),
}
}

fn is_eow(index: usize, contents: RopeSlice) -> bool {
match index.checked_sub(1) {
Some(prev_index) => {
// we are at the end of the file
if index == contents.len_chars() {
contents
.get_char(prev_index)
.map(char_is_word)
.unwrap_or(false)
} else {
get_successive_chars(prev_index, contents)
.map(|(c1, c2)| char_is_word(c1) && !char_is_word(c2))
.unwrap_or(false)
}
}
None => false,
}
}

fn get_successive_chars(start: usize, contents: RopeSlice) -> Option<(char, char)> {
let mut it = contents.chars_at(start);
let c1 = it.next()?;
let c2 = it.next()?;
Some((c1, c2))
}

fn make_search_word_bounded(cx: &mut Context) {
// Defaults to the active search register instead `/` to be more ergonomic assuming most people
// would use this command following `search_selection`. This avoids selecting the register
Expand Down
3 changes: 2 additions & 1 deletion helix-term/src/keymap/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"?" => rsearch,
"n" => search_next,
"N" => search_prev,
"*" => search_selection,
"*" => search_selection_detect_word_boundaries,
"A-*" => search_selection,

"u" => undo,
"U" => redo,
Expand Down

0 comments on commit 9825476

Please sign in to comment.