From 69ea52d097e5bcb0413f774813745e4283b06805 Mon Sep 17 00:00:00 2001 From: Wolf Vollprecht Date: Fri, 6 Sep 2024 21:41:18 +0200 Subject: [PATCH] feat: add completion (#42) --- crates/shell/src/completion.rs | 102 +++++++++++++++++++++++++++++++++ crates/shell/src/main.rs | 15 ++++- 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 crates/shell/src/completion.rs diff --git a/crates/shell/src/completion.rs b/crates/shell/src/completion.rs new file mode 100644 index 0000000..7de7937 --- /dev/null +++ b/crates/shell/src/completion.rs @@ -0,0 +1,102 @@ +use rustyline::completion::{Completer, Pair}; +use rustyline::error::ReadlineError; +use rustyline::highlight::Highlighter; +use rustyline::hint::Hinter; +use rustyline::validate::Validator; +use rustyline::{Context, Helper}; +use std::borrow::Cow::{self, Owned}; +use std::env; +use std::fs; + +pub struct ShellCompleter; + +impl Completer for ShellCompleter { + type Candidate = Pair; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> Result<(usize, Vec), ReadlineError> { + let mut matches = Vec::new(); + let (start, word) = extract_word(line, pos); + + // Complete filenames + complete_filenames(word, &mut matches); + + // Complete shell commands + complete_shell_commands(word, &mut matches); + + // Complete executables in PATH + complete_executables_in_path(word, &mut matches); + + Ok((start, matches)) + } +} + +fn extract_word(line: &str, pos: usize) -> (usize, &str) { + let words: Vec<_> = line[..pos].split_whitespace().collect(); + let word_start = words.last().map_or(0, |w| line.rfind(w).unwrap()); + (word_start, &line[word_start..pos]) +} + +fn complete_filenames(word: &str, matches: &mut Vec) { + if let Ok(entries) = fs::read_dir(".") { + for entry in entries.flatten() { + if let Ok(name) = entry.file_name().into_string() { + if name.starts_with(word) { + matches.push(Pair { + display: name.clone(), + replacement: name, + }); + } + } + } + } +} + +fn complete_shell_commands(word: &str, matches: &mut Vec) { + let shell_commands = ["ls", "cat", "cd", "pwd", "echo", "grep"]; + for &cmd in &shell_commands { + if cmd.starts_with(word) { + matches.push(Pair { + display: cmd.to_string(), + replacement: cmd.to_string(), + }); + } + } +} + +fn complete_executables_in_path(word: &str, matches: &mut Vec) { + if let Ok(paths) = env::var("PATH") { + for path in env::split_paths(&paths) { + if let Ok(entries) = fs::read_dir(path) { + for entry in entries.flatten() { + if let Ok(name) = entry.file_name().into_string() { + if name.starts_with(word) && entry.path().is_file() { + matches.push(Pair { + display: name.clone(), + replacement: name, + }); + } + } + } + } + } + } +} + +impl Hinter for ShellCompleter { + type Hint = String; +} + +impl Highlighter for ShellCompleter { + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + Owned("\x1b[1m".to_owned() + hint + "\x1b[m") + } +} + +impl Validator for ShellCompleter {} + +impl Helper for ShellCompleter {} diff --git a/crates/shell/src/main.rs b/crates/shell/src/main.rs index 0ba916a..a6fff98 100644 --- a/crates/shell/src/main.rs +++ b/crates/shell/src/main.rs @@ -4,14 +4,16 @@ use std::rc::Rc; use anyhow::Context; use clap::Parser; +use completion::ShellCompleter; use deno_task_shell::{ execute_sequential_list, AsyncCommandBehavior, ExecuteResult, ShellCommand, ShellPipeReader, ShellPipeWriter, ShellState, }; use rustyline::error::ReadlineError; -use rustyline::DefaultEditor; +use rustyline::{CompletionType, Config, Editor}; mod commands; +mod completion; fn commands() -> HashMap> { HashMap::from([( @@ -67,7 +69,16 @@ fn init_state() -> ShellState { } async fn interactive() -> anyhow::Result<()> { - let mut rl = DefaultEditor::new()?; + let config = Config::builder() + .history_ignore_space(true) + .completion_type(CompletionType::List) + .build(); + + let mut rl = Editor::with_config(config)?; + + let h = ShellCompleter {}; + + rl.set_helper(Some(h)); let mut state = init_state();