Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
jdx committed Feb 10, 2024
1 parent 0088a4d commit 8057154
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 71 deletions.
134 changes: 103 additions & 31 deletions cli/src/cli/complete_word.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use std::collections::VecDeque;
use std::env;
use std::fmt::{Debug, Formatter};
use std::fmt::{Debug, Display, Formatter};
use std::path::{Path, PathBuf};

use clap::Args;
use indexmap::IndexMap;
use itertools::Itertools;
use strum::EnumTryAs;

use usage::{Spec, SpecArg, SpecCommand, SpecFlag};

Expand Down Expand Up @@ -72,6 +73,8 @@ impl CompleteWord {
complete_long_flag_names(parsed.cmd, &ctoken)
} else if ctoken.starts_with('-') {
complete_short_flag_names(parsed.cmd, &ctoken)
} else if let Some(flag) = parsed.flag_awaiting_value {
complete_arg(spec, flag.arg.as_ref().unwrap(), &ctoken)?
} else if let Some(arg) = parsed.cmd.args.get(parsed.args.len()) {
complete_arg(spec, arg, &ctoken)?
} else {
Expand All @@ -84,8 +87,28 @@ impl CompleteWord {
struct ParseOutput<'a> {
cmd: &'a SpecCommand,
cmds: Vec<&'a SpecCommand>,
args: Vec<(&'a SpecArg, Vec<String>)>,
flags: IndexMap<&'a SpecFlag, Vec<String>>,
args: IndexMap<&'a SpecArg, ParseValue>,
flags: IndexMap<&'a SpecFlag, ParseValue>,
flag_awaiting_value: Option<&'a SpecFlag>,
}

#[derive(EnumTryAs)]
enum ParseValue {
Bool(bool),
String(String),
MultiBool(Vec<bool>),
MultiString(Vec<String>),
}

impl Display for ParseValue {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ParseValue::Bool(b) => write!(f, "{}", b),
ParseValue::String(s) => write!(f, "{}", s),
ParseValue::MultiBool(b) => write!(f, "{:?}", b),
ParseValue::MultiString(s) => write!(f, "{:?}", s),
}
}
}

fn parse(spec: &Spec, mut words: VecDeque<String>) -> miette::Result<ParseOutput> {
Expand All @@ -103,62 +126,111 @@ fn parse(spec: &Spec, mut words: VecDeque<String>) -> miette::Result<ParseOutput
}
}

let mut args = vec![];
let mut flags: IndexMap<&SpecFlag, Vec<String>> = IndexMap::new();
let mut arg_specs = cmd.args.iter().collect_vec();
let mut args: IndexMap<&SpecArg, ParseValue> = IndexMap::new();
let mut flags: IndexMap<&SpecFlag, ParseValue> = IndexMap::new();
let mut next_arg = cmd.args.first();
let mut flag_awaiting_value: Option<&SpecFlag> = None;
let mut enable_flags = true;

while !words.is_empty() {
if words[0] == "--" {
words.pop_front();
let w = words.pop_front().unwrap();

if let Some(flag) = flag_awaiting_value {
flag_awaiting_value = None;
if flag.var {
let arr = flags
.entry(flag)
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(w);
} else {
flags.insert(flag, ParseValue::String(w));
}
continue;
}

if w == "--" {
enable_flags = false;
continue;
}

// long flags
if enable_flags && words[0].starts_with("--") {
let long = words[0].strip_prefix("--").unwrap().to_string();
if enable_flags && w.starts_with("--") {
let (word, val) = w.split_once('=').unwrap_or_else(|| (&w, ""));
if !val.is_empty() {
words.push_front(val.to_string());
}
let long = word.strip_prefix("--").unwrap().to_string();
if let Some(f) = cmd.flags.iter().find(|f| f.long.contains(&long)) {
let word = words.pop_front().unwrap().to_string();
flags.entry(f).or_default().push(word);
if f.arg.is_some() {
flag_awaiting_value = Some(f);
} else if f.var {
let arr = flags
.entry(f)
.or_insert_with(|| ParseValue::MultiBool(vec![]))
.try_as_multi_bool_mut()
.unwrap();
arr.push(true);
} else {
flags.insert(f, ParseValue::Bool(true));
}
continue;
}
}

// short flags
if enable_flags && words[0].starts_with('-') && words[0].len() > 1 {
let short = words[0].chars().nth(1).unwrap();
if enable_flags && w.starts_with('-') && w.len() > 1 {
let short = w.chars().nth(1).unwrap();
if let Some(f) = cmd.flags.iter().find(|f| f.short.contains(&short)) {
if words[0].len() > 2 {
words.push_front(format!("-{}", &words[0][2..]));
let mut next = format!("-{}", &w[2..]);
if f.arg.is_some() {
flag_awaiting_value = Some(f);
next = w[2..].to_string();
}
if next != "-" {
words.push_front(next);
}
if f.var {
let arr = flags
.entry(f)
.or_insert_with(|| ParseValue::MultiBool(vec![]))
.try_as_multi_bool_mut()
.unwrap();
arr.push(true);
} else {
words.pop_front();
flags.insert(f, ParseValue::Bool(true));
}
flags.entry(f).or_default().push(format!("-{}", short));
continue;
}
}

if !arg_specs.is_empty() {
let arg_spec = arg_specs[0];
let word = words.pop_front().unwrap();
args.push((arg_spec, vec![word.clone()]));
if arg_spec.var {
// TODO: handle var_min/var_max
continue;
if let Some(arg) = next_arg {
if arg.var {
let arr = args
.entry(arg)
.or_insert_with(|| ParseValue::MultiString(vec![]))
.try_as_multi_string_mut()
.unwrap();
arr.push(w);
if arr.len() >= arg.var_max.unwrap_or(usize::MAX) {
next_arg = cmd.args.get(args.len());
}
} else {
arg_specs.pop();
continue;
args.insert(arg, ParseValue::String(w));
next_arg = cmd.args.get(args.len());
}
continue;
}
panic!("unexpected word: {:?}", words[0]);
panic!("unexpected word: {w}");
}

Ok(ParseOutput {
cmd,
cmds,
args,
flags,
flag_awaiting_value,
})
}

Expand Down Expand Up @@ -204,7 +276,7 @@ fn complete_short_flag_names(cmd: &SpecCommand, ctoken: &str) -> Vec<String> {
}

fn complete_arg(spec: &Spec, arg: &SpecArg, ctoken: &str) -> miette::Result<Vec<String>> {
trace!("complete_arg: {} {ctoken}", &arg.name);
trace!("complete_arg: {arg} {ctoken}");
let name = arg.name.to_lowercase();

if let Ok(cwd) = env::current_dir() {
Expand Down Expand Up @@ -276,15 +348,15 @@ impl Debug for ParseOutput<'_> {
&self
.args
.iter()
.map(|(a, w)| format!("{}: {}", a.name, w.join(",")))
.map(|(a, w)| format!("{a}: {w}"))
.collect_vec(),
)
.field(
"flags",
&self
.flags
.iter()
.map(|(f, w)| format!("{}: {}", &f.name, w.join(",")))
.map(|(f, w)| format!("{f}: {w}"))
.collect_vec(),
)
.finish()
Expand Down
21 changes: 10 additions & 11 deletions cli/tests/complete_word.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::process::Command;

use assert_cmd::prelude::*;
use predicates::prelude::*;
use predicates::str::contains;

#[test]
fn complete_word_completer() {
Expand All @@ -11,34 +12,32 @@ fn complete_word_completer() {

#[test]
fn complete_word_subcommands() {
assert_cmd(&["plugins", "install"]).stdout(predicate::str::contains("install"));
assert_cmd(&["plugins", "install"]).stdout(contains("install"));
}

#[test]
fn complete_word_cword() {
assert_cmd(&["--cword=2", "plugins", "install"]).stdout(predicate::str::contains("plugin-2"));
assert_cmd(&["--cword=2", "plugins", "install"]).stdout(contains("plugin-2"));
}

#[test]
fn complete_word_long_flag() {
assert_cmd(&["--", "plugins", "install", "--"]).stdout("--dir\n--global\n");
assert_cmd(&["--", "plugins", "install", "--g"]).stdout("--global\n");
assert_cmd(&["--", "plugins", "install", "--global", "pl"])
.stdout(predicate::str::contains("plugin-2"));
assert_cmd(&["--", "plugins", "install", "--global", "pl"]).stdout(contains("plugin-2"));
}

// #[test]
// fn complete_word_long_flag_val() {
// assert_cmd(&["--", "plugins", "install", "--dir", "mydir"])
// .stdout(predicate::str::contains("plugin-2"));
// }
#[test]
fn complete_word_long_flag_val() {
assert_cmd(&["--", "plugins", "install", "--dir", ""])
.stdout(contains("src").and(contains("tests")));
}

#[test]
fn complete_word_short_flag() {
assert_cmd(&["--", "plugins", "install", "-"]).stdout("-d\n-g\n--dir\n--global\n");
assert_cmd(&["--", "plugins", "install", "-g"]).stdout("-g\n");
assert_cmd(&["--", "plugins", "install", "-g", "pl"])
.stdout(predicate::str::contains("plugin-2"));
assert_cmd(&["--", "plugins", "install", "-g", "pl"]).stdout(contains("plugin-2"));
}

fn cmd() -> Command {
Expand Down
51 changes: 25 additions & 26 deletions src/parse/arg.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt::Display;
use std::hash::Hash;
use std::str::FromStr;

#[cfg(feature = "clap")]
Expand All @@ -9,16 +11,16 @@ use crate::error::UsageErr;
use crate::parse::context::ParsingContext;
use crate::parse::helpers::NodeHelper;

#[derive(Debug, Default, Clone, Serialize, Hash)]
#[derive(Debug, Default, Clone, Serialize)]
pub struct SpecArg {
pub name: String,
pub usage: String,
pub help: Option<String>,
pub long_help: Option<String>,
pub required: bool,
pub var: bool,
pub var_min: Option<i64>,
pub var_max: Option<i64>,
pub var_min: Option<usize>,
pub var_max: Option<usize>,
pub hide: bool,
pub default: Option<String>,
}
Expand All @@ -33,8 +35,8 @@ impl SpecArg {
"required" => arg.required = v.ensure_bool()?,
"var" => arg.var = v.ensure_bool()?,
"hide" => arg.hide = v.ensure_bool()?,
"var_min" => arg.var_min = v.ensure_i64().map(Some)?,
"var_max" => arg.var_max = v.ensure_i64().map(Some)?,
"var_min" => arg.var_min = v.ensure_usize().map(Some)?,
"var_max" => arg.var_max = v.ensure_usize().map(Some)?,
"default" => arg.default = v.ensure_string().map(Some)?,
k => bail_parse!(ctx, *v.entry.span(), "unsupported arg key {k}"),
}
Expand Down Expand Up @@ -72,10 +74,10 @@ impl From<&SpecArg> for KdlNode {
node.push(KdlEntry::new_prop("var", true));
}
if let Some(min) = arg.var_min {
node.push(KdlEntry::new_prop("var_min", min));
node.push(KdlEntry::new_prop("var_min", min as i64));
}
if let Some(max) = arg.var_max {
node.push(KdlEntry::new_prop("var_max", max));
node.push(KdlEntry::new_prop("var_max", max as i64));
}
if arg.hide {
node.push(KdlEntry::new_prop("hide", true));
Expand Down Expand Up @@ -161,22 +163,19 @@ impl From<&clap::Arg> for SpecArg {
}
}

// #[cfg(feature = "clap")]
// impl From<&SpecArg> for clap::Arg {
// fn from(arg: &SpecArg) -> Self {
// let mut a = clap::Arg::new(&arg.name);
// if let Some(desc) = &arg.help {
// a = a.help(desc);
// }
// if arg.required {
// a = a.required(true);
// }
// // if arg.multiple {
// // a = a.multiple(true);
// // }
// if arg.hide {
// a = a.hide_possible_values(true);
// }
// a
// }
// }
impl Display for SpecArg {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.usage())
}
}
impl PartialEq for SpecArg {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
impl Eq for SpecArg {}
impl Hash for SpecArg {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
6 changes: 3 additions & 3 deletions src/parse/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ impl<'a> ParseEntry<'a> {
}

impl<'a> ParseEntry<'a> {
pub fn ensure_i64(&self) -> Result<i64, UsageErr> {
pub fn ensure_usize(&self) -> Result<usize, UsageErr> {
match self.value.as_i64() {
Some(i) => Ok(i),
None => bail_parse!(self.ctx, self.span(), "expected integer"),
Some(i) => Ok(i as usize),
None => bail_parse!(self.ctx, self.span(), "expected usize"),
}
}
#[allow(dead_code)]
Expand Down

0 comments on commit 8057154

Please sign in to comment.