use std::fs::{self, DirEntry};
use std::path::{Path, PathBuf};
use rustyline::completion::{Completer, Pair};
use rustyline::highlight::Highlighter;
use rustyline::hint::Hinter;
use rustyline::validate::Validator;
use rustyline::{Context, Helper};
use crate::handler::Handler;
use crate::shell_parser::{escape_word, split, unescape_word};
pub type Dispatcher<S> = Vec<Handler<S>>;
pub struct ClapCmdHelper<State = ()>
where
State: Clone,
{
pub(crate) dispatcher: Dispatcher<State>,
pub(crate) state: Option<State>,
}
impl<State> ClapCmdHelper<State>
where
State: Clone,
{
pub(crate) fn new(state: Option<State>) -> Self {
ClapCmdHelper {
dispatcher: Dispatcher::new(),
state,
}
}
pub(crate) fn get_state(&self) -> Option<&State> {
self.state.as_ref()
}
pub(crate) fn get_state_mut(&mut self) -> Option<&mut State> {
self.state.as_mut()
}
pub(crate) fn set_state(&mut self, new_state: Option<State>) {
self.state = new_state;
}
}
impl<State: Clone> Helper for ClapCmdHelper<State> {}
impl<State: Clone> Hinter for ClapCmdHelper<State> {
type Hint = String;
fn hint(&self, _line: &str, _pos: usize, _ctx: &Context) -> Option<Self::Hint> {
None
}
}
impl<State: Clone> Highlighter for ClapCmdHelper<State> {}
impl<State: Clone> Validator for ClapCmdHelper<State> {}
impl<State: Clone> Completer for ClapCmdHelper<State> {
type Candidate = Pair;
fn complete(
&self,
line: &str,
pos: usize,
_ctx: &Context,
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
let mut matches = vec![];
let statements = split(&line[..pos]);
let Some(statement) = statements.last() else {
return Ok((pos, matches));
};
let words = &statement.words;
let Some(curr_word) = &words.last() else {
return Ok((pos, matches));
};
let curr_word = &unescape_word(line, curr_word);
let mut current_command = None;
let mut current_commands = self
.dispatcher
.iter()
.map(|command| &command.command)
.collect::<Vec<&clap::Command>>();
let mut i_current_command = None;
for (i, word) in words.iter().enumerate() {
let mut found = false;
let word = unescape_word(line, word);
for command in ¤t_commands {
if word == command.get_name() {
current_command = Some(<&clap::Command>::clone(command));
i_current_command = Some(i);
found = true;
break;
}
}
if !found {
break;
}
current_commands =
current_command.map_or(vec![], |command| command.get_subcommands().collect());
}
let i_last_command_word = i_current_command.map_or(0, |i| i + 1);
if words.len() - 1 == i_last_command_word {
for command in ¤t_commands {
let name = command.get_name();
if let Some(stripped) = name.strip_prefix(curr_word) {
let completion = escape_word(stripped);
let pair = Pair {
display: completion.clone(),
replacement: completion,
};
matches.push(pair);
}
}
}
if current_command.is_none() {
return Ok((pos, matches));
};
let current_command = current_command.expect("current_command should never be none here");
if let Some(curr_arg) = curr_word.strip_prefix("--") {
let args = current_command.get_arguments();
for arg in args {
let longs = arg.get_long_and_visible_aliases().unwrap_or_default();
for long in longs {
if let Some(stripped) = long.strip_prefix(curr_arg) {
let completion = escape_word(stripped);
let pair = Pair {
display: completion.clone(),
replacement: completion.clone(),
};
matches.push(pair);
}
}
}
return Ok((pos, matches));
}
if !curr_word.starts_with('-') {
if words.len() < 3 {
return Ok((pos, matches));
}
let prev_word = &words[words.len() - 2];
let prev_word = &line[prev_word.start..prev_word.end];
if !prev_word.starts_with('-') {
return Ok((pos, matches));
}
let args = current_command.get_arguments();
let add_values_from_parser = |arg: &clap::Arg| {
let possible_values = arg.get_value_parser().possible_values();
if possible_values.is_none() {
return vec![];
}
let possible_values = possible_values.unwrap();
possible_values
.flat_map(|value| {
let mut matches = vec![];
for name in value.get_name_and_aliases() {
if let Some(stripped) = name.strip_prefix(curr_word) {
let completion = escape_word(stripped);
matches.push(Pair {
display: completion.clone(),
replacement: completion.clone(),
});
}
}
matches
})
.collect()
};
let add_values_from_hint = |arg: &clap::Arg| {
let value_hint = arg.get_value_hint();
let add_path = |filter: fn(&DirEntry) -> bool| {
let mut path = PathBuf::from(curr_word);
if curr_word.ends_with('/') {
path.push("ignore");
}
let parent = path.parent().unwrap_or_else(|| Path::new(""));
let parent = if parent.to_string_lossy() == "" {
Path::new(".")
} else {
parent
};
let entries = fs::read_dir(parent);
if entries.is_err() {
return vec![];
}
let entries = entries.unwrap();
entries
.flat_map(|value| {
let mut matches = vec![];
if value.is_err() {
return matches;
}
let value = value.unwrap();
if filter(&value) {
return matches;
}
if let Some(stripped) =
value.path().to_string_lossy().strip_prefix(curr_word)
{
let completion = escape_word(stripped);
matches.push(Pair {
display: completion.clone(),
replacement: completion,
});
} else if let Some(stripped) =
value.path().to_string_lossy().strip_prefix("./")
{
if let Some(stripped) = stripped.strip_prefix(curr_word) {
let completion = escape_word(stripped);
matches.push(Pair {
display: completion.clone(),
replacement: completion,
});
}
}
matches
})
.collect()
};
match value_hint {
clap::ValueHint::AnyPath => add_path(|_| false),
clap::ValueHint::DirPath => add_path(|entry: &DirEntry| {
let metadata = entry.metadata();
if metadata.is_err() {
return true;
}
!metadata.unwrap().is_dir()
}),
clap::ValueHint::FilePath => add_path(|entry: &DirEntry| {
let metadata = entry.metadata();
if metadata.is_err() {
return true;
}
!metadata.unwrap().is_file()
}),
_ => vec![],
}
};
if prev_word.starts_with("--") {
for arg in args {
for long in arg.get_long_and_visible_aliases().unwrap_or_default() {
if long == &prev_word[2..] {
matches.extend(add_values_from_parser(arg));
matches.extend(add_values_from_hint(arg));
}
}
}
} else if prev_word.starts_with('-') {
for arg in args {
for short in arg.get_short_and_visible_aliases().unwrap_or_default() {
if short == prev_word.chars().last().unwrap_or_default() {
matches.extend(add_values_from_parser(arg));
matches.extend(add_values_from_hint(arg));
}
}
}
}
}
Ok((pos, matches))
}
}