use std::path::{Path, PathBuf};
use super::{
data::*, drop_path_end, filepaths, find_executables_in_path, Completer, Completion,
CompletionCtx, ReplaceMethod,
};
use crate::prelude::Builtins;
pub type Action = Box<dyn Fn(&CompletionCtx) -> Vec<Completion>>;
pub struct Pred {
pred: Box<dyn Fn(&CompletionCtx) -> bool>,
}
impl Pred {
pub fn new(pred: impl Fn(&CompletionCtx) -> bool + 'static) -> Self {
Self {
pred: Box::new(pred),
}
}
pub fn and(self, pred: impl Fn(&CompletionCtx) -> bool + 'static) -> Self {
Self {
pred: Box::new(move |ctx: &CompletionCtx| -> bool { (*self.pred)(ctx) && pred(ctx) }),
}
}
pub fn test(&self, ctx: &CompletionCtx) -> bool {
(self.pred)(ctx)
}
}
pub type Filter = Box<dyn Fn(&String) -> bool>;
pub type Format = Box<dyn Fn(String) -> Completion>;
pub struct Rule {
pub pred: Pred,
pub completions: Action,
}
impl Rule {
pub fn new(pred: Pred, action: impl Fn(&CompletionCtx) -> Vec<Completion> + 'static) -> Self {
Self {
pred,
completions: Box::new(action),
}
}
}
pub struct DefaultCompleter {
rules: Vec<Rule>,
}
impl DefaultCompleter {
pub fn new() -> Self {
Self { rules: vec![] }
}
fn complete_helper(&self, ctx: &CompletionCtx) -> Vec<Completion> {
let rules: Vec<&Rule> = self.rules.iter().filter(|p| (p.pred).test(ctx)).collect();
let mut output = vec![];
if rules.is_empty() {
return filename_action(ctx)
.into_iter()
.filter(|s| {
s.accept()
.starts_with(ctx.cur_word().unwrap_or(&String::new()))
})
.collect::<Vec<_>>();
}
for rule in rules {
let mut comps = (rule.completions)(ctx)
.into_iter()
.filter(|s| {
s.accept()
.starts_with(ctx.cur_word().unwrap_or(&String::new()))
})
.collect::<Vec<_>>();
output.append(&mut comps);
}
output
}
}
impl Completer for DefaultCompleter {
fn complete(&self, ctx: &CompletionCtx) -> Vec<Completion> {
self.complete_helper(ctx)
}
fn register(&mut self, rule: Rule) {
self.rules.push(rule);
}
}
impl Default for DefaultCompleter {
fn default() -> Self {
let mut comp = DefaultCompleter::new();
comp.register(Rule::new(
Pred::new(ls_pred).and(short_flag_pred),
Box::new(ls_short_flag_action),
));
comp.register(Rule::new(
Pred::new(ls_pred).and(long_flag_pred),
Box::new(ls_long_flag_action),
));
comp
}
}
pub fn cmdname_action(path_str: String) -> impl Fn(&CompletionCtx) -> Vec<Completion> {
move |_ctx: &CompletionCtx| -> Vec<Completion> {
default_format(find_executables_in_path(&path_str))
}
}
pub fn builtin_cmdname_action(builtin: &Builtins) -> impl Fn(&CompletionCtx) -> Vec<Completion> {
let builtin_names = builtin
.iter()
.map(|(name, _)| name.to_owned().to_string())
.collect::<Vec<_>>();
move |_ctx: &CompletionCtx| -> Vec<Completion> { default_format(builtin_names.clone()) }
}
pub fn filename_action(ctx: &CompletionCtx) -> Vec<Completion> {
let cur_word = ctx.cur_word().unwrap();
let drop_end = drop_path_end(cur_word);
let cur_path = to_absolute(&drop_end, &dirs::home_dir().unwrap());
let output = filepaths(&cur_path).unwrap_or_default();
output
.iter()
.map(|x| {
let filename = x.file_name().unwrap().to_str().unwrap().to_string();
let mut filename = sanitize_file_name(filename);
let is_dir = x.is_dir();
if is_dir {
filename += "/";
}
Completion {
add_space: !is_dir,
display: Some(filename.to_owned()),
completion: drop_end.to_owned() + &filename,
replace_method: ReplaceMethod::Replace,
comment: None,
}
})
.collect::<Vec<_>>()
}
fn to_absolute(path_str: &str, home_dir: &Path) -> PathBuf {
let path_buf = PathBuf::from(path_str);
let absolute = if path_buf.has_root() {
path_buf
} else {
if let Ok(stripped) = path_buf.strip_prefix("~/") {
home_dir.join(stripped)
} else {
std::env::current_dir().unwrap().join(path_buf)
}
};
absolute
}
pub fn cmdname_pred(ctx: &CompletionCtx) -> bool {
ctx.arg_num() == 0
}
pub fn git_pred(ctx: &CompletionCtx) -> bool {
cmdname_eq_pred("git".into())(ctx)
}
pub fn arg_pred(ctx: &CompletionCtx) -> bool {
ctx.arg_num() != 0
}
pub fn cmdname_eq_pred(cmd_name: String) -> impl Fn(&CompletionCtx) -> bool {
move |ctx: &CompletionCtx| ctx.cmd_name() == Some(&cmd_name)
}
pub fn flag_pred(ctx: &CompletionCtx) -> bool {
long_flag_pred(ctx) || short_flag_pred(ctx)
}
pub fn short_flag_pred(ctx: &CompletionCtx) -> bool {
ctx.cur_word().unwrap_or(&String::new()).starts_with('-') && !long_flag_pred(ctx)
}
pub fn long_flag_pred(ctx: &CompletionCtx) -> bool {
ctx.cur_word().unwrap_or(&String::new()).starts_with("--")
}
pub fn path_pred(ctx: &CompletionCtx) -> bool {
let cur_word = ctx.cur_word().unwrap();
let cur_path = to_absolute(&drop_path_end(cur_word), &dirs::home_dir().unwrap());
cur_path.is_dir()
}
pub fn default_format(s: Vec<String>) -> Vec<Completion> {
s.iter()
.map(|x| Completion {
add_space: true,
display: None,
completion: x.to_owned(),
replace_method: ReplaceMethod::Replace,
comment: None,
})
.collect::<Vec<_>>()
}
pub fn default_format_with_comment(s: Vec<(String, String)>) -> Vec<Completion> {
s.iter()
.map(|x| Completion {
add_space: true,
display: None,
completion: x.0.to_string(),
replace_method: ReplaceMethod::Replace,
comment: Some(x.1.to_string()),
})
.collect::<Vec<_>>()
}
fn sanitize_file_name(filename: String) -> String {
filename.replace(' ', "\\ ")
}
#[cfg(test)]
mod tests {
use super::{flag_pred, DefaultCompleter};
use crate::completion::CompletionCtx;
#[test]
fn simple() {
let _comp = DefaultCompleter::new();
}
#[test]
fn test_is_flag() {
let ctx = CompletionCtx::new(vec!["git".into(), "-".into()]);
assert!(flag_pred(&ctx));
let ctx = CompletionCtx::new(vec![]);
assert!(!flag_pred(&ctx));
}
}