feat: cycle command completions with Tab

This commit is contained in:
2026-10-17 18:05:45 +02:00
parent 5697a08c0a
commit 5d75964ee2
7 changed files with 175 additions and 8 deletions
+115
View File
@@ -1067,6 +1067,121 @@ fn focused_text_fields_keep_printable_normal_bindings_as_text() {
}
}
#[test]
fn command_completion_cycles_core_alias_and_live_plugin_names_without_executing() {
let mut editor = editor();
editor.register(plugin("fixture", &["save_note"])).unwrap();
let document = editor.context().text;
type_text(&mut editor, ":sav");
for expected in ["save", "save_and_quit", "save_note", "save"] {
assert_eq!(editor.handle(Input::Char('\t')), None);
assert_eq!(editor.command_line(), expected);
assert_eq!(editor.mode(), Mode::Command);
assert_eq!(editor.context().text, document);
assert!(!editor.should_quit());
}
editor.handle(Input::Char('\t'));
editor.handle(Input::Char('\t'));
let invocation = editor.handle(Input::Enter).unwrap();
assert_eq!(invocation.plugin, "fixture");
assert_eq!(invocation.command, "save_note");
assert_eq!(invocation.args, "");
type_text(&mut editor, ":q");
for expected in ["q", "q!", "quit", "q"] {
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), expected);
assert!(!editor.should_quit());
}
editor.handle(Input::Escape);
type_text(&mut editor, ":sav");
editor.handle(Input::Char('\t'));
editor.disable_plugin("fixture", "test");
for expected in ["save_and_quit", "save"] {
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), expected);
}
}
#[test]
fn command_completion_restarts_after_edits_history_and_cancellation_and_ignores_arguments() {
let mut editor = editor();
type_text(&mut editor, ":plugins\n:sav");
editor.handle(Input::Char('\t'));
type_text(&mut editor, "_");
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "save_and_quit");
editor.handle(Input::Ctrl('u'));
type_text(&mut editor, "q!");
editor.handle(Input::Backspace);
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "q");
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "q!");
editor.handle(Input::Up);
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "plugins");
editor.handle(Input::Down);
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "q!");
editor.handle(Input::Escape);
for (draft, expected) in [
("", "bdelete"),
(" :sav", " :save"),
("no_such_command", "no_such_command"),
("界e\u{301}", "界e\u{301}"),
("edit ", "edit "),
("save 界 file.txt", "save 界 file.txt"),
] {
type_text(&mut editor, ":");
type_text(&mut editor, draft);
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), expected);
editor.handle(Input::Escape);
}
}
#[test]
fn command_completion_respects_bindings_and_preserves_visual_diff_and_search_input() {
let directory = Directory::new();
let mut editor = editor();
editor
.bind("command", "ctrl+n", Some("core.command_complete".into()))
.unwrap();
editor.bind("command", "tab", 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();
type_text(&mut editor, ":diff.com");
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "diff.com");
editor.handle(Input::Ctrl('n'));
assert_eq!(editor.command_line(), "diff.comment");
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);
type_text(&mut editor, "/sav");
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "sav\t");
editor.handle(Input::Escape);
editor
.bind("command", "tab", Some("core.up".into()))
.unwrap();
type_text(&mut editor, ":plugins\n:sav");
editor.handle(Input::Char('\t'));
assert_eq!(editor.command_line(), "plugins");
}
#[test]
fn prompt_history_browses_without_executing_and_restores_unicode_drafts() {
for prefix in [':', '/'] {