fix: retain current occurrence when submitting search
This commit is contained in:
+3
-1
@@ -50,7 +50,7 @@ Indicators refresh asynchronously. Typing and undo/redo retain the last complete
|
|||||||
| Insert | Ctrl+n requests completion at the primary caret or cycles next; Ctrl+j/k cycles next/previous while completion is open |
|
| Insert | Ctrl+n requests completion at the primary caret or cycles next; Ctrl+j/k cycles next/previous while completion is open |
|
||||||
| Insert, active snippet | Type to replace the highlighted argument and its mirrors; Tab / Shift+Tab moves next/previous |
|
| Insert, active snippet | Type to replace the highlighted argument and its mirrors; Tab / Shift+Tab moves next/previous |
|
||||||
| Command | Enter executes; Backspace edits; Esc cancels |
|
| Command | Enter executes; Backspace edits; Esc cancels |
|
||||||
| Search | Type a literal query; Enter finds the next match; Backspace edits; Esc cancels |
|
| Search | Type a literal query; Enter accepts the current occurrence or finds the next match; Backspace edits; Esc cancels |
|
||||||
| Insert, Command, Search | Ctrl+u deletes backward to the logical line start |
|
| Insert, Command, Search | Ctrl+u deletes backward to the logical line start |
|
||||||
| Insert, Command, Search (macOS) | Option+Backspace deletes backward by word; Command+Backspace deletes backward to the logical line start |
|
| Insert, Command, Search (macOS) | Option+Backspace deletes backward by word; Command+Backspace deletes backward to the logical line start |
|
||||||
|
|
||||||
@@ -172,6 +172,8 @@ The last recipe is session-wide across buffers, but pending Insert groups and un
|
|||||||
|
|
||||||
Search is case-sensitive and single-line, with no regular expressions or Unicode normalization. Matches start and end on whole grapheme boundaries, including combining marks and emoji. Next/previous navigation skips the current position and wraps through the buffer; a sole match wraps back to itself. All occurrences of the active query are highlighted yellow.
|
Search is case-sensitive and single-line, with no regular expressions or Unicode normalization. Matches start and end on whole grapheme boundaries, including combining marks and emoji. Next/previous navigation skips the current position and wraps through the buffer; a sole match wraps back to itself. All occurrences of the active query are highlighted yellow.
|
||||||
|
|
||||||
|
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 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.
|
||||||
|
|
||||||
`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.
|
||||||
|
|||||||
+25
-9
@@ -8426,7 +8426,7 @@ impl Editor {
|
|||||||
self.active.view.search_query = command;
|
self.active.view.search_query = command;
|
||||||
self.active.view.search_whole_word = false;
|
self.active.view.search_whole_word = false;
|
||||||
}
|
}
|
||||||
self.seek_search(Action::SearchNext, 1);
|
self.seek_search(Action::Search, 1);
|
||||||
} else {
|
} else {
|
||||||
return self.execute(&command);
|
return self.execute(&command);
|
||||||
}
|
}
|
||||||
@@ -8531,7 +8531,16 @@ impl Editor {
|
|||||||
cursor.row,
|
cursor.row,
|
||||||
grapheme_byte(&self.active.lines[cursor.row], cursor.column),
|
grapheme_byte(&self.active.lines[cursor.row], cursor.column),
|
||||||
);
|
);
|
||||||
let first = if forward {
|
let current = (action == Action::Search)
|
||||||
|
.then(|| matches.partition_point(|position| *position <= origin))
|
||||||
|
.and_then(|end| end.checked_sub(1))
|
||||||
|
.filter(|index| {
|
||||||
|
let found = matches[*index];
|
||||||
|
found.0 == origin.0 && origin.1 < found.1 + query.len()
|
||||||
|
});
|
||||||
|
let first = if let Some(current) = current {
|
||||||
|
current
|
||||||
|
} else if forward {
|
||||||
matches.partition_point(|position| *position <= origin) % matches.len()
|
matches.partition_point(|position| *position <= origin) % matches.len()
|
||||||
} else {
|
} else {
|
||||||
matches
|
matches
|
||||||
@@ -8546,15 +8555,18 @@ impl Editor {
|
|||||||
(first + matches.len() - distance) % matches.len()
|
(first + matches.len() - distance) % matches.len()
|
||||||
};
|
};
|
||||||
let found = matches[destination];
|
let found = matches[destination];
|
||||||
target.active = Cursor {
|
let start = Cursor {
|
||||||
row: found.0,
|
row: found.0,
|
||||||
column: column_after_byte(&self.active.lines[found.0], found.1),
|
column: column_after_byte(&self.active.lines[found.0], found.1),
|
||||||
};
|
};
|
||||||
|
if current.is_none() {
|
||||||
|
target.active = start;
|
||||||
target.preferred_columns = None;
|
target.preferred_columns = None;
|
||||||
|
}
|
||||||
if index == self.active.view.primary {
|
if index == self.active.view.primary {
|
||||||
self.active.view.search_match = Some(SearchMatch {
|
self.active.view.search_match = Some(SearchMatch {
|
||||||
start: target.active,
|
start,
|
||||||
end_column: target.active.column + grapheme_count(query),
|
end_column: start.column + grapheme_count(query),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16112,7 +16124,7 @@ mod tests {
|
|||||||
type_text(&mut editor, "Vjy");
|
type_text(&mut editor, "Vjy");
|
||||||
assert!(editor.clipboard_linewise);
|
assert!(editor.clipboard_linewise);
|
||||||
assert_eq!(editor.clipboard, ["alpha beta alpha\nsecond line\n"]);
|
assert_eq!(editor.clipboard, ["alpha beta alpha\nsecond line\n"]);
|
||||||
type_text(&mut editor, "gt/alpha\n");
|
type_text(&mut editor, "gt/alpha\nn");
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 11 });
|
assert_eq!(editor.cursor(), Cursor { row: 0, column: 11 });
|
||||||
editor.handle(Input::Char('n'));
|
editor.handle(Input::Char('n'));
|
||||||
assert_eq!(editor.cursor(), Cursor::default());
|
assert_eq!(editor.cursor(), Cursor::default());
|
||||||
@@ -17364,7 +17376,7 @@ mod tests {
|
|||||||
editor
|
editor
|
||||||
.apply(vec![Effect::Replace("hit\n中👩💻 hit\nhit".into())])
|
.apply(vec![Effect::Replace("hit\n中👩💻 hit\nhit".into())])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
type_text(&mut editor, "/hit\n");
|
type_text(&mut editor, "/hit\nn");
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 1, column: 3 });
|
assert_eq!(editor.cursor(), Cursor { row: 1, column: 3 });
|
||||||
type_text(&mut editor, "nN");
|
type_text(&mut editor, "nN");
|
||||||
assert_eq!(editor.jumps.len(), 4);
|
assert_eq!(editor.jumps.len(), 4);
|
||||||
@@ -17789,7 +17801,7 @@ mod tests {
|
|||||||
editor
|
editor
|
||||||
.apply(vec![Effect::Replace("hit\nhit".into())])
|
.apply(vec![Effect::Replace("hit\nhit".into())])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
type_text(&mut editor, "/hit\n");
|
type_text(&mut editor, "/hit\nn");
|
||||||
editor.handle(Input::Ctrl('k'));
|
editor.handle(Input::Ctrl('k'));
|
||||||
assert_eq!(editor.carets().len(), 2);
|
assert_eq!(editor.carets().len(), 2);
|
||||||
let targets = editor.active.view.targets.clone();
|
let targets = editor.active.view.targets.clone();
|
||||||
@@ -22061,6 +22073,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
set_carets(&mut editor, &[(0, 0), (1, 0)], 1);
|
set_carets(&mut editor, &[(0, 0), (1, 0)], 1);
|
||||||
submit_search(&mut editor, "cat");
|
submit_search(&mut editor, "cat");
|
||||||
|
editor.handle(Input::Char('n'));
|
||||||
type_text(&mut editor, "2n");
|
type_text(&mut editor, "2n");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.carets(),
|
editor.carets(),
|
||||||
@@ -22771,6 +22784,7 @@ mod tests {
|
|||||||
.apply(vec![Effect::Replace("hit a\nz\nhit b\nz\nhit c\n".into())])
|
.apply(vec![Effect::Replace("hit a\nz\nhit b\nz\nhit c\n".into())])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
submit_search(&mut editor, "hit");
|
submit_search(&mut editor, "hit");
|
||||||
|
editor.handle(Input::Char('n'));
|
||||||
assert_eq!(editor.cursor().row, 2);
|
assert_eq!(editor.cursor().row, 2);
|
||||||
type_text(&mut editor, "Vn");
|
type_text(&mut editor, "Vn");
|
||||||
assert_eq!(editor.linewise_rows().collect::<Vec<_>>(), [2..=4]);
|
assert_eq!(editor.linewise_rows().collect::<Vec<_>>(), [2..=4]);
|
||||||
@@ -23477,6 +23491,7 @@ mod tests {
|
|||||||
assert_eq!(editor.buffer_id(), 2);
|
assert_eq!(editor.buffer_id(), 2);
|
||||||
set_carets(&mut editor, &[(0, 0), (1, 0)], 0);
|
set_carets(&mut editor, &[(0, 0), (1, 0)], 0);
|
||||||
submit_search(&mut editor, "dog");
|
submit_search(&mut editor, "dog");
|
||||||
|
editor.handle(Input::Char('n'));
|
||||||
let b_carets = editor.carets();
|
let b_carets = editor.carets();
|
||||||
let b_primary = editor.cursor();
|
let b_primary = editor.cursor();
|
||||||
editor.handle(Input::Char('i'));
|
editor.handle(Input::Char('i'));
|
||||||
@@ -24794,6 +24809,7 @@ mod tests {
|
|||||||
assert_eq!(editor.selection_count(), 2);
|
assert_eq!(editor.selection_count(), 2);
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 1, column: 0 });
|
assert_eq!(editor.cursor(), Cursor { row: 1, column: 0 });
|
||||||
submit_search(&mut editor, "cat");
|
submit_search(&mut editor, "cat");
|
||||||
|
editor.handle(Input::Char('n'));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
editor.carets(),
|
editor.carets(),
|
||||||
vec![Cursor { row: 0, column: 4 }, Cursor { row: 1, column: 4 }]
|
vec![Cursor { row: 0, column: 4 }, Cursor { row: 1, column: 4 }]
|
||||||
@@ -24950,7 +24966,7 @@ mod tests {
|
|||||||
assert_eq!(editor.search_match(), active);
|
assert_eq!(editor.search_match(), active);
|
||||||
assert!(editor.command_line().is_empty());
|
assert!(editor.command_line().is_empty());
|
||||||
submit_search(&mut editor, "");
|
submit_search(&mut editor, "");
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 1, column: 4 });
|
assert_eq!(editor.cursor(), cursor);
|
||||||
|
|
||||||
let cursor = editor.cursor();
|
let cursor = editor.cursor();
|
||||||
submit_search(&mut editor, "missing");
|
submit_search(&mut editor, "missing");
|
||||||
|
|||||||
+1
-1
@@ -47,7 +47,7 @@ Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+
|
|||||||
Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends
|
Snippets expand fully: type replaces argument, Tab/Shift+Tab next/previous, final $0 ends
|
||||||
Outside snippets Enter inserts newline, Tab inserts a tab or configured spaces; Esc returns to Normal
|
Outside snippets Enter inserts newline, Tab inserts a tab or configured spaces; Esc returns to Normal
|
||||||
Insert/Command/Search: Ctrl+u deletes backward to the logical line start
|
Insert/Command/Search: Ctrl+u deletes backward to the logical line start
|
||||||
Search: Enter finds next literal match, Esc cancels (case-sensitive, wraps)
|
Search: Enter accepts current/next literal match, Esc cancels (case-sensitive, wraps)
|
||||||
Commands: :save [PATH], :force_save [PATH], :quit, :force_quit,
|
Commands: :save [PATH], :force_save [PATH], :quit, :force_quit,
|
||||||
:save_and_quit [PATH], :force_save_and_quit [PATH], :plugins, :PLUGIN [ARGS]
|
:save_and_quit [PATH], :force_save_and_quit [PATH], :plugins, :PLUGIN [ARGS]
|
||||||
Aliases: :w=save, :w!=force_save, :q=quit, :q!=force_quit,
|
Aliases: :w=save, :w!=force_save, :q=quit, :q!=force_quit,
|
||||||
|
|||||||
@@ -4311,6 +4311,8 @@ end
|
|||||||
session.handle(Input::Enter);
|
session.handle(Input::Enter);
|
||||||
assert_eq!(session.editor().mode(), crate::editor::Mode::Search);
|
assert_eq!(session.editor().mode(), crate::editor::Mode::Search);
|
||||||
session.handle(Input::Ctrl('x'));
|
session.handle(Input::Ctrl('x'));
|
||||||
|
assert_eq!(session.editor().cursor().row, 0);
|
||||||
|
session.handle(Input::Ctrl('n'));
|
||||||
assert_eq!(session.editor().cursor().row, 1);
|
assert_eq!(session.editor().cursor().row, 1);
|
||||||
session.handle(Input::Ctrl('n'));
|
session.handle(Input::Ctrl('n'));
|
||||||
assert_eq!(session.editor().cursor().row, 2);
|
assert_eq!(session.editor().cursor().row, 2);
|
||||||
|
|||||||
+34
-2
@@ -120,6 +120,36 @@ fn wait_until(session: &mut EditorSession, ready: impl Fn(&EditorSession) -> boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn submitting_search_keeps_each_caret_inside_a_current_match() {
|
||||||
|
let fixture = Fixture::new();
|
||||||
|
for (text, query, column, end) in [
|
||||||
|
("jj next jj", "jj", 0, 2),
|
||||||
|
("jj next jj", "jj", 1, 2),
|
||||||
|
("界e\u{301} next 界e\u{301}", "界e\u{301}", 1, 2),
|
||||||
|
("aaaa next aa", "aa", 1, 3),
|
||||||
|
] {
|
||||||
|
let mut editor = fixture.editor(text);
|
||||||
|
keys(&mut editor, &"l".repeat(column));
|
||||||
|
let cursor = editor.cursor();
|
||||||
|
keys(&mut editor, &format!("/{query}"));
|
||||||
|
editor.handle(Input::Enter);
|
||||||
|
assert_eq!(editor.cursor(), cursor, "{text:?}");
|
||||||
|
assert_eq!(editor.search_match().unwrap().end_column, end);
|
||||||
|
keys(&mut editor, "n");
|
||||||
|
assert_ne!(editor.cursor(), cursor);
|
||||||
|
assert!(!editor.dirty());
|
||||||
|
}
|
||||||
|
let mut editor = fixture.editor("jj next jj\n-- next jj");
|
||||||
|
editor.handle(Input::Ctrl('e'));
|
||||||
|
keys(&mut editor, "/jj");
|
||||||
|
editor.handle(Input::Enter);
|
||||||
|
assert_eq!(
|
||||||
|
editor.carets(),
|
||||||
|
[Cursor::default(), Cursor { row: 1, column: 8 }]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normal_escape_hides_highlights_but_retains_search_and_rearms_star() {
|
fn normal_escape_hides_highlights_but_retains_search_and_rearms_star() {
|
||||||
let fixture = Fixture::new();
|
let fixture = Fixture::new();
|
||||||
@@ -145,7 +175,7 @@ fn normal_escape_hides_highlights_but_retains_search_and_rearms_star() {
|
|||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
editor.handle(Input::Char('/'));
|
editor.handle(Input::Char('/'));
|
||||||
editor.handle(Input::Enter);
|
editor.handle(Input::Enter);
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 0, column: 4 });
|
assert_eq!(editor.cursor(), Cursor::default());
|
||||||
assert_eq!(editor.search_matches_on_row(0).count(), 2);
|
assert_eq!(editor.search_matches_on_row(0).count(), 2);
|
||||||
assert!(!editor.dirty());
|
assert!(!editor.dirty());
|
||||||
}
|
}
|
||||||
@@ -192,7 +222,7 @@ fn star_search_keeps_whole_word_counts_history_and_literal_search_distinct() {
|
|||||||
editor.handle(Input::Escape);
|
editor.handle(Input::Escape);
|
||||||
editor.handle(Input::Char('/'));
|
editor.handle(Input::Char('/'));
|
||||||
editor.handle(Input::Enter);
|
editor.handle(Input::Enter);
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 6, column: 0 });
|
assert_eq!(editor.cursor(), Cursor::default());
|
||||||
keys(&mut editor, "gb*");
|
keys(&mut editor, "gb*");
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 9, column: 0 });
|
assert_eq!(editor.cursor(), Cursor { row: 9, column: 0 });
|
||||||
keys(&mut editor, "n");
|
keys(&mut editor, "n");
|
||||||
@@ -200,6 +230,7 @@ fn star_search_keeps_whole_word_counts_history_and_literal_search_distinct() {
|
|||||||
|
|
||||||
keys(&mut editor, "/cat");
|
keys(&mut editor, "/cat");
|
||||||
editor.handle(Input::Enter);
|
editor.handle(Input::Enter);
|
||||||
|
keys(&mut editor, "n");
|
||||||
assert_eq!(editor.cursor(), Cursor { row: 1, column: 1 });
|
assert_eq!(editor.cursor(), Cursor { row: 1, column: 1 });
|
||||||
assert_eq!(editor.search_match().unwrap().end_column, 4);
|
assert_eq!(editor.search_match().unwrap().end_column, 4);
|
||||||
assert_eq!(editor.context().text, text);
|
assert_eq!(editor.context().text, text);
|
||||||
@@ -260,6 +291,7 @@ fn star_search_rebinding_moves_all_carets_and_remains_view_local() {
|
|||||||
assert_eq!(session.editor().cursor(), Cursor { row: 1, column: 0 });
|
assert_eq!(session.editor().cursor(), Cursor { row: 1, column: 0 });
|
||||||
session_keys(&mut session, "/cat");
|
session_keys(&mut session, "/cat");
|
||||||
session.handle(Input::Enter);
|
session.handle(Input::Enter);
|
||||||
|
session_keys(&mut session, "n");
|
||||||
assert_eq!(session.editor().cursor(), Cursor { row: 1, column: 4 });
|
assert_eq!(session.editor().cursor(), Cursor { row: 1, column: 4 });
|
||||||
session.handle(Input::Alt('h'));
|
session.handle(Input::Alt('h'));
|
||||||
session_keys(&mut session, "2N");
|
session_keys(&mut session, "2N");
|
||||||
|
|||||||
Reference in New Issue
Block a user