Skip to content

Commit

Permalink
feat: add completion (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
wolfv authored Sep 6, 2024
1 parent 6dfb9fa commit 69ea52d
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 2 deletions.
102 changes: 102 additions & 0 deletions crates/shell/src/completion.rs
Original file line number Diff line number Diff line change
@@ -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<Pair>), 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<Pair>) {
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<Pair>) {
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<Pair>) {
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 {}
15 changes: 13 additions & 2 deletions crates/shell/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Rc<dyn ShellCommand>> {
HashMap::from([(
Expand Down Expand Up @@ -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();

Expand Down

0 comments on commit 69ea52d

Please sign in to comment.