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
+50
View File
@@ -805,6 +805,11 @@ core_actions![
Action::Submit,
&[Mode::Command, Mode::Search],
),
(
"core.command_complete",
Action::CommandComplete,
&[Mode::Command],
),
(
"core.palette",
Action::Palette,
@@ -1584,6 +1589,10 @@ 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 mode in [Mode::Command, Mode::Search] {
for (key, action) in [(Input::Up, Up), (Input::Ctrl('k'), Up), (Input::Down, Down)] {
bindings
@@ -1751,6 +1760,7 @@ struct ViewState {
search_highlight: bool,
search_match: Option<SearchMatch>,
command_line: String,
command_completion_prefix: Option<String>,
prompt_history_index: Option<usize>,
prompt_draft: String,
command_from_visual: bool,
@@ -1775,6 +1785,7 @@ impl ViewState {
search_highlight: false,
search_match: None,
command_line: String::new(),
command_completion_prefix: None,
prompt_history_index: None,
prompt_draft: String::new(),
command_from_visual: false,
@@ -1799,6 +1810,7 @@ impl ViewState {
search_highlight: self.search_highlight,
search_match: self.search_match,
command_line: self.command_line.clone(),
command_completion_prefix: self.command_completion_prefix.clone(),
prompt_history_index: self.prompt_history_index,
prompt_draft: self.prompt_draft.clone(),
command_from_visual: self.command_from_visual,
@@ -8006,6 +8018,12 @@ impl Editor {
fn handle_input(&mut self, input: Input) -> Option<Invocation> {
let mode = self.binding_mode();
if !matches!(
self.bindings.get(&mode).and_then(|keys| keys.get(&input)),
Some(Binding::Action(Action::CommandComplete))
) {
self.active.view.command_completion_prefix = None;
}
if self.completion.is_some() && (self.diff().is_none() || self.diff_editing()) {
let request = matches!(
self.bindings
@@ -8419,6 +8437,37 @@ impl Editor {
self.active.view.mode = Mode::Command;
}
fn complete_command(&mut self) {
let view = &mut self.active.view;
let command = view.command_line.trim_start();
let name = command.strip_prefix(':').unwrap_or(command);
if name.contains(char::is_whitespace) {
return;
}
let start = view.command_line.len() - name.len();
let cycling = view.command_completion_prefix.is_some();
let prefix = view
.command_completion_prefix
.get_or_insert_with(|| name.to_owned());
let mut first = None;
let mut next = None;
for candidate in self.features.values().flat_map(|feature| &feature.commands) {
let candidate = candidate.as_str();
if !candidate.starts_with(prefix.as_str()) {
continue;
}
if first.is_none_or(|first| candidate < first) {
first = Some(candidate);
}
if cycling && candidate > name && next.is_none_or(|next| candidate < next) {
next = Some(candidate);
}
}
if let Some(candidate) = next.or(first) {
view.command_line.replace_range(start.., candidate);
}
}
fn navigate_prompt_history(&mut self, previous: bool) {
let history = if self.active.view.mode == Mode::Search {
&self.search_history
@@ -8514,6 +8563,7 @@ impl Editor {
let count = self.count.take().unwrap_or(1);
self.clear_sequence();
match action {
Action::CommandComplete => self.complete_command(),
Action::SnippetNext => {
if self.snippet.is_some() {
self.navigate_snippet(true);
+1
View File
@@ -57,6 +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)
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]