changeset 6749:4d779359617c

Improve auto completion for shell commands (#12883) Co-authored-by: Michael Davis <[email protected]>
author Sumandora <johannesmiesenhardt@gmail.com>
date Sun, 27 Apr 2025 21:04:56 +0200
parents f1e113eff6fb
children 34defb465c67
files helix-term/src/commands.rs helix-term/src/commands/typed.rs helix-term/src/ui/mod.rs
diffstat 3 files changed, 68 insertions(+), 4 deletions(-) [+]
line wrap: on
line diff
--- a/helix-term/src/commands.rs	Sun Apr 27 20:23:47 2025 +0200
+++ b/helix-term/src/commands.rs	Sun Apr 27 21:04:56 2025 +0200
@@ -6350,7 +6350,7 @@
         cx,
         prompt,
         Some('|'),
-        ui::completers::filename,
+        ui::completers::shell,
         move |cx, input: &str, event: PromptEvent| {
             if event != PromptEvent::Validate {
                 return;
--- a/helix-term/src/commands/typed.rs	Sun Apr 27 20:23:47 2025 +0200
+++ b/helix-term/src/commands/typed.rs	Sun Apr 27 21:04:56 2025 +0200
@@ -2566,6 +2566,9 @@
     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_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] = &[
--- a/helix-term/src/ui/mod.rs	Sun Apr 27 20:23:47 2025 +0200
+++ b/helix-term/src/ui/mod.rs	Sun Apr 27 21:04:56 2025 +0200
@@ -371,6 +371,7 @@
 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 @@
     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<Completion>;
@@ -677,4 +679,63 @@
             .map(|(name, _)| ((0..), name.into()))
             .collect()
     }
+
+    pub fn program(_editor: &Editor, input: &str) -> Vec<Completion> {
+        static PROGRAMS_IN_PATH: Lazy<BTreeSet<String>> = 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<Completion> {
+        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<Completion> {
+        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
+    }
 }