feat: navigate command and search input history

This commit is contained in:
2026-10-17 01:50:28 +02:00
parent 2cb3736b92
commit 5697a08c0a
7 changed files with 242 additions and 13 deletions
+147 -1
View File
@@ -1067,6 +1067,147 @@ fn focused_text_fields_keep_printable_normal_bindings_as_text() {
}
}
#[test]
fn prompt_history_browses_without_executing_and_restores_unicode_drafts() {
for prefix in [':', '/'] {
let mut editor = editor();
editor.register(plugin("fixture", &["greet"])).unwrap();
editor
.apply(vec![Effect::Replace(
"greet first\ngreet 界e\u{301}\nend".into(),
)])
.unwrap();
for text in ["greet first", "greet 界e\u{301}"] {
editor.handle(Input::Char(prefix));
type_text(&mut editor, text);
editor.handle(Input::Enter);
}
let cursor = editor.cursor();
let document = editor.context().text;
editor.handle(Input::Char(prefix));
type_text(&mut editor, "draft 👩‍💻");
for (key, expected) in [
(Input::Down, "draft 👩‍💻"),
(Input::Up, "greet 界e\u{301}"),
(Input::Ctrl('k'), "greet first"),
(Input::Up, "greet first"),
(Input::Down, "greet 界e\u{301}"),
] {
assert_eq!(editor.handle(key), None);
assert_eq!(editor.command_line(), expected);
assert_eq!(editor.cursor(), cursor);
assert_eq!(editor.context().text, document);
}
type_text(&mut editor, " edited");
editor.handle(Input::Ctrl('j'));
assert_eq!(editor.command_line(), "draft 👩‍💻");
editor.handle(Input::Ctrl('k'));
assert_eq!(editor.command_line(), "greet 界e\u{301}");
editor.handle(Input::Escape);
editor.handle(Input::Char(prefix));
editor.handle(Input::Up);
editor.handle(Input::Down);
assert_eq!(editor.command_line(), "");
editor.handle(Input::Up);
editor.handle(Input::Up);
let invocation = editor.handle(Input::Enter);
if prefix == ':' {
let invocation = invocation.unwrap();
assert_eq!(invocation.command, "greet");
assert_eq!(invocation.args, "first");
} else {
assert_eq!(editor.active.view.search_query, "greet first");
assert_eq!(editor.cursor().row, 0);
}
assert_eq!(editor.mode(), Mode::Normal);
}
}
#[test]
fn prompt_history_is_separate_bounded_and_skips_empty_cancelled_and_consecutive_duplicates() {
let mut editor = editor();
for prefix in [':', '/'] {
editor.handle(Input::Char(prefix));
type_text(&mut editor, "draft");
editor.handle(Input::Up);
editor.handle(Input::Down);
assert_eq!(editor.command_line(), "draft");
editor.handle(Input::Escape);
}
for command in [":plugins", ":plugins", ":", "/needle", "/needle", "/"] {
type_text(&mut editor, command);
editor.handle(Input::Enter);
}
for (prefix, expected) in [(':', "plugins"), ('/', "needle")] {
editor.handle(Input::Char(prefix));
type_text(&mut editor, "cancelled");
editor.handle(Input::Escape);
editor.handle(Input::Char(prefix));
editor.handle(Input::Up);
editor.handle(Input::Up);
assert_eq!(editor.command_line(), expected);
editor.handle(Input::Down);
assert_eq!(editor.command_line(), "");
editor.handle(Input::Escape);
}
for index in 0..=PROMPT_HISTORY_LIMIT {
type_text(&mut editor, &format!("/query {index}"));
editor.handle(Input::Enter);
}
editor.handle(Input::Char('/'));
for _ in 0..PROMPT_HISTORY_LIMIT + 2 {
editor.handle(Input::Up);
}
assert_eq!(editor.command_line(), "query 1");
for _ in 0..PROMPT_HISTORY_LIMIT {
editor.handle(Input::Down);
}
assert_eq!(editor.command_line(), "");
}
#[test]
fn prompt_history_is_session_wide_remappable_and_preserves_visual_diff_targets() {
let directory = Directory::new();
let mut editor = editor();
type_text(&mut editor, ":plugins\n/needle\n");
editor.execute("tab.new");
editor
.bind("command", "ctrl+b", Some("core.up".into()))
.unwrap();
editor
.bind("search", "ctrl+f", Some("core.down".into()))
.unwrap();
editor.bind("command", "up", None).unwrap();
historical_diff(
&mut editor,
&directory.0,
vec!["needle before".into()],
vec!["needle after".into()],
);
editor.handle(Input::Char('v'));
let selections = editor.selections();
let cursor = editor.cursor();
editor.handle(Input::Char(':'));
editor.handle(Input::Up);
assert_eq!(editor.command_line(), "");
editor.handle(Input::Ctrl('b'));
assert_eq!(editor.command_line(), "plugins");
assert_eq!(editor.selections(), selections);
assert_eq!(editor.cursor(), cursor);
editor.handle(Input::Escape);
assert_eq!(editor.mode(), Mode::Visual);
assert_eq!(editor.selections(), selections);
editor.handle(Input::Escape);
editor.handle(Input::Char('/'));
type_text(&mut editor, "draft");
editor.handle(Input::Up);
assert_eq!(editor.command_line(), "needle");
editor.handle(Input::Ctrl('f'));
assert_eq!(editor.command_line(), "draft");
editor.handle(Input::Escape);
assert_eq!(editor.cursor(), cursor);
}
#[test]
fn empty_notification_does_not_swallow_escape_but_cursor_diagnostic_does() {
let mut editor = editor();
@@ -8408,12 +8549,17 @@ fn ctrl_j_is_distinct_from_enter_and_preserves_editor_defaults() {
editor.handle(Input::Escape);
editor.register(plugin("fixture", &["greet"])).unwrap();
type_text(&mut editor, ":greet arguments");
let invocation = editor.handle(Input::Ctrl('j')).unwrap();
assert_eq!(editor.handle(Input::Ctrl('j')), None);
assert_eq!(editor.mode(), Mode::Command);
assert_eq!(editor.command_line(), "greet arguments");
let invocation = editor.handle(Input::Enter).unwrap();
assert_eq!(invocation.command, "greet");
assert_eq!(invocation.args, "arguments");
assert_eq!(editor.mode(), Mode::Normal);
type_text(&mut editor, "/two");
editor.handle(Input::Ctrl('j'));
assert_eq!(editor.mode(), Mode::Search);
editor.handle(Input::Enter);
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(editor.active.view.search_query, "two");
assert_eq!(editor.cursor().row, 1);