fix: clear search highlighting on Normal escape

This commit is contained in:
Valdemar Lorenzen
2026-10-15 12:21:17 +02:00
parent de62467208
commit adc44d22db
3 changed files with 51 additions and 3 deletions
+2
View File
@@ -176,6 +176,8 @@ Normal `*` highlights the primary caret's word without moving any caret when 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.
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.
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.
## Commands, saving, and pasted input
+18 -2
View File
@@ -1575,6 +1575,7 @@ struct ViewState {
linewise: bool,
search_query: String,
search_whole_word: bool,
search_highlight: bool,
search_match: Option<SearchMatch>,
command_line: String,
caret_reveal_revision: u64,
@@ -1594,6 +1595,7 @@ impl ViewState {
linewise: false,
search_query: String::new(),
search_whole_word: false,
search_highlight: false,
search_match: None,
command_line: String::new(),
caret_reveal_revision: 0,
@@ -1613,6 +1615,7 @@ impl ViewState {
linewise: self.linewise,
search_query: self.search_query.clone(),
search_whole_word: self.search_whole_word,
search_highlight: self.search_highlight,
search_match: self.search_match,
command_line: self.command_line.clone(),
caret_reveal_revision: self.caret_reveal_revision,
@@ -6491,7 +6494,11 @@ impl Editor {
self.active.view.search_match
}
pub fn search_matches_on_row(&self, row: usize) -> impl Iterator<Item = SearchMatch> + '_ {
let query = self.active.view.search_query.as_str();
let query = if self.active.view.search_highlight {
self.active.view.search_query.as_str()
} else {
""
};
let line = &self.active.lines[row];
let columns = grapheme_count(query);
search_match_bytes(line, query, self.active.view.search_whole_word).map(move |byte| {
@@ -7934,6 +7941,8 @@ impl Editor {
&& (!self.message.is_empty() || self.diagnostic_at_cursor().is_some())
{
self.notification_visible = false;
self.active.view.search_highlight = false;
self.active.view.search_match = None;
return None;
}
if matches!(self.active.view.mode, Mode::Normal | Mode::Visual)
@@ -8379,6 +8388,8 @@ impl Editor {
}
Action::Normal => {
if self.active.view.mode == Mode::Normal {
self.active.view.search_highlight = false;
self.active.view.search_match = None;
self.collapse_primary();
} else if self.active.view.mode == Mode::Insert {
self.finish_insert();
@@ -8452,11 +8463,15 @@ impl Editor {
.map_or(line.len(), |(position, _)| position);
let query = &line[start..end];
let whole_word = kind == WordKind::Keyword;
let newly_active = self.active.view.search_query != query
let newly_active = !self.active.view.search_highlight
|| self.active.view.search_query != query
|| self.active.view.search_whole_word != whole_word;
if newly_active {
if self.active.view.search_query != query {
self.active.view.search_query = query.into();
}
self.active.view.search_whole_word = whole_word;
self.active.view.search_highlight = true;
}
Some((
Cursor {
@@ -8491,6 +8506,7 @@ impl Editor {
self.set_message("No previous search");
return;
}
self.active.view.search_highlight = true;
let query = self.active.view.search_query.as_str();
let whole_word = self.active.view.search_whole_word;
// ponytail: O(buffer graphemes × query bytes) scan; index matches if large buffers need it.
+30
View File
@@ -120,6 +120,36 @@ fn wait_until(session: &mut EditorSession, ready: impl Fn(&EditorSession) -> boo
}
}
#[test]
fn normal_escape_hides_highlights_but_retains_search_and_rearms_star() {
let fixture = Fixture::new();
let mut editor = fixture.editor("cat cat");
keys(&mut editor, "*");
assert_eq!(editor.search_matches_on_row(0).count(), 2);
editor.handle(Input::Escape);
assert_eq!(editor.search_matches_on_row(0).count(), 0);
assert_eq!(editor.search_match(), None);
assert_eq!(editor.cursor(), Cursor::default());
keys(&mut editor, "*");
assert_eq!(editor.cursor(), Cursor::default());
assert_eq!(editor.search_matches_on_row(0).count(), 2);
keys(&mut editor, "*");
assert_eq!(editor.cursor(), Cursor { row: 0, column: 4 });
editor.handle(Input::Escape);
keys(&mut editor, "n");
assert_eq!(editor.cursor(), Cursor::default());
assert_eq!(editor.search_matches_on_row(0).count(), 2);
keys(&mut editor, "/missing");
editor.handle(Input::Escape);
assert_eq!(editor.search_matches_on_row(0).count(), 2);
editor.handle(Input::Escape);
editor.handle(Input::Char('/'));
editor.handle(Input::Enter);
assert_eq!(editor.cursor(), Cursor { row: 0, column: 4 });
assert_eq!(editor.search_matches_on_row(0).count(), 2);
assert!(!editor.dirty());
}
#[test]
fn star_search_arms_without_moving_then_navigates_and_rearms_for_a_new_word_or_mode() {
let fixture = Fixture::new();