feat: cycle command completions backward with Shift+Tab

This commit is contained in:
2026-10-17 18:23:04 +02:00
parent 5d75964ee2
commit 0a01d9a361
6 changed files with 83 additions and 17 deletions
+29 -9
View File
@@ -810,6 +810,11 @@ core_actions![
Action::CommandComplete,
&[Mode::Command],
),
(
"core.command_complete_previous",
Action::CommandCompletePrevious,
&[Mode::Command],
),
(
"core.palette",
Action::Palette,
@@ -1589,10 +1594,15 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
.insert(key, Binding::Action(action));
}
}
bindings
.entry(Mode::Command)
.or_default()
.insert(Input::Char('\t'), Binding::Action(CommandComplete));
for (key, action) in [
(Input::Char('\t'), CommandComplete),
(Input::BackTab, CommandCompletePrevious),
] {
bindings
.entry(Mode::Command)
.or_default()
.insert(key, Binding::Action(action));
}
for mode in [Mode::Command, Mode::Search] {
for (key, action) in [(Input::Up, Up), (Input::Ctrl('k'), Up), (Input::Down, Down)] {
bindings
@@ -8020,7 +8030,9 @@ impl Editor {
let mode = self.binding_mode();
if !matches!(
self.bindings.get(&mode).and_then(|keys| keys.get(&input)),
Some(Binding::Action(Action::CommandComplete))
Some(Binding::Action(
Action::CommandComplete | Action::CommandCompletePrevious
))
) {
self.active.view.command_completion_prefix = None;
}
@@ -8437,7 +8449,7 @@ impl Editor {
self.active.view.mode = Mode::Command;
}
fn complete_command(&mut self) {
fn complete_command(&mut self, forward: bool) {
let view = &mut self.active.view;
let command = view.command_line.trim_start();
let name = command.strip_prefix(':').unwrap_or(command);
@@ -8449,6 +8461,9 @@ impl Editor {
let prefix = view
.command_completion_prefix
.get_or_insert_with(|| name.to_owned());
let precedes = |left: &str, right: &str| {
if forward { left < right } else { left > right }
};
let mut first = None;
let mut next = None;
for candidate in self.features.values().flat_map(|feature| &feature.commands) {
@@ -8456,10 +8471,13 @@ impl Editor {
if !candidate.starts_with(prefix.as_str()) {
continue;
}
if first.is_none_or(|first| candidate < first) {
if first.is_none_or(|first| precedes(candidate, first)) {
first = Some(candidate);
}
if cycling && candidate > name && next.is_none_or(|next| candidate < next) {
if cycling
&& precedes(name, candidate)
&& next.is_none_or(|next| precedes(candidate, next))
{
next = Some(candidate);
}
}
@@ -8563,7 +8581,9 @@ impl Editor {
let count = self.count.take().unwrap_or(1);
self.clear_sequence();
match action {
Action::CommandComplete => self.complete_command(),
Action::CommandComplete | Action::CommandCompletePrevious => {
self.complete_command(action == Action::CommandComplete);
}
Action::SnippetNext => {
if self.snippet.is_some() {
self.navigate_snippet(true);
+1 -1
View File
@@ -57,7 +57,7 @@ Insert: Ctrl+n requests LSP completion at primary caret, then cycles next; Ctrl+
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
Command/Search: Up/Ctrl+k older history; Down/Ctrl+j newer history or unfinished input; Enter submits
Command: Tab completes command names; repeated presses cycle alphabetical matches (not arguments)
Command: Tab/Shift+Tab completes command names and cycles forward/backward with wraparound (not arguments)
Search: Enter accepts current/next literal match, Esc cancels (case-sensitive, wraps)
Commands: :save [PATH], :force_save [PATH], :quit, :force_quit,
:save_and_quit [PATH], :force_save_and_quit [PATH], :plugins, :PLUGIN [ARGS]