From 99b57181d5833a609996e88c4885c2b9b479a66d Mon Sep 17 00:00:00 2001 From: Sumandora Date: Sun, 27 Apr 2025 21:04:56 +0200 Subject: [PATCH] Improve auto completion for shell commands (#12883) Co-authored-by: Michael Davis --- helix-term/src/commands.rs | 2 +- helix-term/src/commands/typed.rs | 9 +++-- helix-term/src/ui/mod.rs | 61 ++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f80c277b2..2669d8dd0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -6350,7 +6350,7 @@ fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBeha cx, prompt, Some('|'), - ui::completers::filename, + ui::completers::shell, move |cx, input: &str, event: PromptEvent| { if event != PromptEvent::Validate { return; diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 5274c2801..8b58c7e0b 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -2566,6 +2566,9 @@ fn noop(_cx: &mut compositor::Context, _args: Args, _event: PromptEvent) -> anyh Ok(()) } +// TODO: SHELL_SIGNATURE should specify var args for arguments, so that just completers::filename can be used, +// but Signature does not yet allow for var args. + /// This command handles all of its input as-is with no quoting or flags. const SHELL_SIGNATURE: Signature = Signature { positionals: (1, Some(2)), @@ -2574,10 +2577,10 @@ const SHELL_SIGNATURE: Signature = Signature { }; const SHELL_COMPLETER: CommandCompleter = CommandCompleter::positional(&[ - // Command name (TODO: consider a command completer - Kakoune has prior art) - completers::none, + // Command name + completers::program, // Shell argument(s) - completers::filename, + completers::repeating_filenames, ]); pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index a76adbe21..47b046c9d 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -371,6 +371,7 @@ fn directory_content(path: &Path) -> Result, std::io::Error pub mod completers { use super::Utf8PathBuf; use crate::ui::prompt::Completion; + use helix_core::command_line::{self, Tokenizer}; use helix_core::fuzzy::fuzzy_match; use helix_core::syntax::LanguageServerFeature; use helix_view::document::SCRATCH_BUFFER_NAME; @@ -378,6 +379,7 @@ pub mod completers { use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; + use std::collections::BTreeSet; use tui::text::Span; pub type Completer = fn(&Editor, &str) -> Vec; @@ -677,4 +679,63 @@ pub mod completers { .map(|(name, _)| ((0..), name.into())) .collect() } + + pub fn program(_editor: &Editor, input: &str) -> Vec { + static PROGRAMS_IN_PATH: Lazy> = Lazy::new(|| { + // Go through the entire PATH and read all files into a set. + let Some(path) = std::env::var_os("PATH") else { + return Default::default(); + }; + + std::env::split_paths(&path) + .filter_map(|path| std::fs::read_dir(path).ok()) + .flatten() + .filter_map(|res| { + let entry = res.ok()?; + if entry.metadata().ok()?.is_file() { + entry.file_name().into_string().ok() + } else { + None + } + }) + .collect() + }); + + fuzzy_match(input, PROGRAMS_IN_PATH.iter(), false) + .into_iter() + .map(|(name, _)| ((0..), name.clone().into())) + .collect() + } + + /// This expects input to be a raw string of arguments, because this is what Signature's raw_after does. + pub fn repeating_filenames(editor: &Editor, input: &str) -> Vec { + let token = match Tokenizer::new(input, false).last() { + Some(token) => token.unwrap(), + None => return filename(editor, input), + }; + + let offset = token.content_start; + + let mut completions = filename(editor, &input[offset..]); + for completion in completions.iter_mut() { + completion.0.start += offset; + } + completions + } + + pub fn shell(editor: &Editor, input: &str) -> Vec { + let (command, args, complete_command) = command_line::split(input); + + if complete_command { + return program(editor, command); + } + + let mut completions = repeating_filenames(editor, args); + for completion in completions.iter_mut() { + // + 1 for separator between `command` and `args` + completion.0.start += command.len() + 1; + } + + completions + } }