fix: move word search activation onto its match

This commit is contained in:
Valdemar Lorenzen
2026-10-15 12:38:50 +02:00
parent f5c6d098f8
commit 3a7e2074b4
3 changed files with 38 additions and 11 deletions
+2 -2
View File
@@ -174,11 +174,11 @@ Search is case-sensitive and single-line, with no regular expressions or Unicode
Submitting `/` keeps each caret in place if it is at or inside an occurrence of the query; otherwise, it moves that caret to the next match, wrapping as needed. This also applies to an empty submission repeating the previous query. Use `n`/`N` to explicitly move to the next/previous match. Submitting `/` keeps each caret in place if it is at or inside an occurrence of the query; otherwise, it moves that caret to the next match, wrapping as needed. This also applies to an empty submission repeating the previous query. Use `n`/`N` to explicitly move to the next/previous match.
Normal `*` highlights the primary caret's word without moving any caret when it selects a new query or changes the matching mode. Pressing `*` again for the same active word navigates forward; counts such as `2*` then skip to the requested occurrence, with every caret searching the primary word from its own position. Selecting a different word starts a new highlight-only search. It uses the same Unicode letters/digits/underscore boundaries as `w`, so `cat` skips `scatter`, `cat_cat`, and `cat2`. If the caret is not on a keyword, it takes the next keyword on that line; with none, it uses the punctuation run under or after the caret as a literal substring. A blank remainder reports no word and preserves the previous search. This also works in read-only diff text. Bind `config.core.search_word` to remap it. Normal `*` highlights the primary caret's word when it selects a new query or changes the matching mode. Carets at or inside a matching occurrence stay put; carets outside one move to the next occurrence. Pressing `*` again for the same active word navigates forward; counts such as `2*` then skip to the requested occurrence, with every caret searching the primary word from its own position. Selecting a different word activates its search using the same positioning rules. It uses the same Unicode letters/digits/underscore boundaries as `w`, so `cat` skips `scatter`, `cat_cat`, and `cat2`. If the caret is not on a keyword, it takes the next keyword on that line; with none, it uses the punctuation run under or after the caret as a literal substring. A blank remainder reports no word and preserves the previous search. This also works in read-only diff text. Bind `config.core.search_word` to remap it.
`n`/`N` and an empty `/` submission retain the whole-word setting; a nonempty `/` query returns to literal substring search. Cancelling `/` preserves both the previous query and its matching behavior. `n`/`N` and an empty `/` submission retain the whole-word setting; a nonempty `/` query returns to literal substring search. Cancelling `/` preserves both the previous query and its matching behavior.
Normal-mode Escape hides search highlights without forgetting the query or matching mode. The next `*` re-highlights without moving; `n`/`N` or submitting `/` resumes navigation and restores highlighting. Escape used to cancel a prompt, selection, or pending motion keeps its existing behavior. Normal-mode Escape hides search highlights without forgetting the query or matching mode. The next `*` reactivates highlighting, staying put only when at or inside a match; `n`/`N` or submitting `/` resumes navigation and restores highlighting. Escape used to cancel a prompt, selection, or pending motion keeps its existing behavior.
Submitting an empty search repeats the last query. Esc cancels without moving the cursor or replacing the previous query/match. A missing match reports the query and leaves the cursor unchanged; searching without a previous query also leaves the cursor unchanged. Search never edits text or adds undo history. Buffer edits and undo/redo retain the query and recompute its visible highlights, so subsequent navigation searches the current text again. Submitting an empty search repeats the last query. Esc cancels without moving the cursor or replacing the previous query/match. A missing match reports the query and leaves the cursor unchanged; searching without a previous query also leaves the cursor unchanged. Search never edits text or adds undo history. Buffer edits and undo/redo retain the query and recompute its visible highlights, so subsequent navigation searches the current text again.
+6 -7
View File
@@ -8482,20 +8482,19 @@ impl Editor {
)) ))
} }
fn seek_search(&mut self, action: Action, count: usize) { fn seek_search(&mut self, mut action: Action, mut count: usize) {
self.reveal_caret(); self.reveal_caret();
let primary_origin = if action == Action::SearchWord { let primary_origin = if action == Action::SearchWord {
let Some((origin, newly_active)) = self.set_word_search() else { let Some((origin, newly_active)) = self.set_word_search() else {
return; return;
}; };
if newly_active { if newly_active {
self.active.view.search_match = Some(SearchMatch { action = Action::Search;
start: origin, count = 1;
end_column: origin.column + grapheme_count(&self.active.view.search_query), None
}); } else {
return;
}
Some(origin) Some(origin)
}
} else { } else {
None None
}; };
+30 -2
View File
@@ -120,6 +120,36 @@ fn wait_until(session: &mut EditorSession, ready: impl Fn(&EditorSession) -> boo
} }
} }
#[test]
fn star_activation_moves_onto_the_selected_word_only_when_outside_it() {
let fixture = Fixture::new();
for (text, column, expected) in [
(" word word", 0, 2),
(" word word", 1, 2),
(" word word", 2, 2),
(" word word", 4, 4),
("-- word word", 0, 3),
("\t界e\u{301} 界e\u{301}", 0, 1),
(" .* .*", 0, 1),
] {
let mut editor = fixture.editor(text);
keys(&mut editor, &"l".repeat(column));
keys(&mut editor, "*");
assert_eq!(
editor.cursor(),
Cursor {
row: 0,
column: expected
},
"{text:?}"
);
let first = editor.search_match().unwrap().start;
keys(&mut editor, "*");
assert!(editor.cursor().column > first.column, "{text:?}");
assert!(!editor.dirty());
}
}
#[test] #[test]
fn submitting_search_keeps_each_caret_inside_a_current_match() { fn submitting_search_keeps_each_caret_inside_a_current_match() {
let fixture = Fixture::new(); let fixture = Fixture::new();
@@ -253,9 +283,7 @@ fn star_search_uses_unicode_words_and_vim_same_line_fallbacks() {
] { ] {
let mut editor = fixture.editor(text); let mut editor = fixture.editor(text);
keys(&mut editor, positioning); keys(&mut editor, positioning);
let origin = editor.cursor();
editor.handle(Input::Char('*')); editor.handle(Input::Char('*'));
assert_eq!(editor.cursor(), origin, "{text:?}");
editor.handle(Input::Char('*')); editor.handle(Input::Char('*'));
assert_eq!(editor.cursor(), Cursor { row, column: 0 }, "{text:?}"); assert_eq!(editor.cursor(), Cursor { row, column: 0 }, "{text:?}");
assert_eq!( assert_eq!(