feat: cycle command completions with Tab
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user