use std::collections::{BTreeMap, BTreeSet, HashSet};
use tracing::{debug, warn};
use super::abbrev::unique_prefixes;
use super::extract::{Cmd, Cmds};
pub(super) fn compile_recursive(
mut pfx: String,
cmd: &Cmd,
long: &str,
lbuf: &str,
all: bool,
) -> BTreeMap<String, String> {
debug!("Prefix: {pfx}");
let mut m = BTreeMap::new();
let mut bind = |k: String, v: String| -> bool {
debug!("considering binding '{k}' to '{v}'");
debug_assert!(!k.ends_with(' '));
debug_assert!(v.ends_with(' '));
if !all && !k.starts_with(lbuf) && !v.starts_with(lbuf) {
return false;
}
debug!("binding '{k}' to '{v}'");
if let Some(existing) = m.get(k.as_str()) {
warn!("Map already contained key! {k} -> {v}, {existing}");
}
m.insert(k, v);
true
};
if !pfx.is_empty() && !pfx.ends_with(' ') {
pfx.push(' ');
}
if !all && pfx.len() > lbuf.len() + 1 {
return m;
}
debug_assert!(all || lbuf.starts_with(&pfx) || pfx.starts_with(lbuf));
let short = &cmd.short;
let pre_short = format!("{pfx}{short}");
let mut pre_long = format!("{pfx}{long}");
debug_assert!(pre_short.len() <= pre_long.len());
let doesnt_start_with_prefix = !lbuf.starts_with(&pre_long);
if !all
&& !pre_short.starts_with(lbuf)
&& !pre_long.starts_with(lbuf)
&& !lbuf.starts_with(&pre_short)
&& doesnt_start_with_prefix
{
return m;
}
debug!("binding root");
pre_long.push(' ');
bind(pre_short, pre_long);
for (f, fl) in &cmd.flags {
let expanded = format!("{pfx}{long} {f} ");
bind(format!("{pfx}{long} -{}", fl.short), expanded.clone());
if cmd.no_args {
bind(format!("{pfx}{long} {}", fl.short), expanded.clone());
}
if fl.squish {
bind(format!("{pfx}{short}{}", fl.short), expanded);
}
}
for (sub_long, sub) in &cmd.subs.0 {
let sub_short = &sub.short;
debug!("binding sub: {sub_long}");
let k = format!("{pfx}{short}{sub_short}");
let starts_with_key = lbuf.starts_with(&k);
bind(k, format!("{pfx}{long} {sub_long} "));
if !starts_with_key {
continue;
}
for (sub_sub_long, sub_sub) in &sub.subs.0 {
let k = format!("{sub_short}{}", sub_sub.short);
if !cmd.subs.0.contains_key(&k) {
bind(
format!("{pfx}{short}{k}"),
format!("{pfx}{long} {sub_long} {sub_sub_long} "),
);
}
}
for (f, fl) in &sub.flags {
if fl.squish {
bind(
format!("{pfx}{short}{sub_short}{}", fl.short),
format!("{pfx}{long} {sub_long} {f} "),
);
}
}
}
for (sub_long, sub) in &cmd.subs.0 {
debug!("considering binding sub: {sub_long}");
if !all && doesnt_start_with_prefix {
continue;
}
debug!("binding sub: {sub_long}");
let prefix = format!("{pfx}{long}");
for (short, long) in compile_recursive(prefix, sub, sub_long, lbuf, all) {
bind(short, long);
}
}
m
}
pub(super) fn compile(cmds: &Cmds, lbuf: &str, all: bool) -> BTreeMap<String, String> {
let mut r = BTreeMap::new();
for (long, cmd) in &cmds.0 {
if lbuf.is_empty() || lbuf.starts_with(&cmd.short) || lbuf.starts_with(long) {
r.extend(compile_recursive(String::new(), cmd, long, lbuf, all));
}
}
r
}
pub(super) fn compile_with_prefixes(
cmds: &Cmds,
lbuf: &str,
all: bool,
) -> BTreeMap<String, String> {
let compiled = compile(cmds, lbuf, all);
add_prefixes(compiled, lbuf, all)
}
fn prefixes(s: &str, l: usize) -> Vec<String> {
assert!(l < s.len());
let mut r = Vec::with_capacity(s.len() - l);
for len in l..s.len() - 1 {
r.push(s[..len].to_string());
}
r
}
fn add_prefixes(
mut compiled: BTreeMap<String, String>,
lbuf: &str,
all: bool,
) -> BTreeMap<String, String> {
let words = lbuf.split_whitespace().count();
let strings = compiled
.values()
.filter(|s| s.starts_with(lbuf) && s.split_whitespace().count() == words)
.cloned()
.collect::<BTreeSet<String>>();
let strings = strings.into_iter().collect::<Vec<_>>();
let denylist = HashSet::from_iter(compiled.keys().map(String::as_str));
let pfxs = unique_prefixes(strings.as_slice(), &denylist);
for (s, pfx) in BTreeMap::from_iter(pfxs.into_iter()).into_iter() {
debug_assert!(pfx.len() <= s.len());
if s == pfx {
continue;
}
for p in prefixes(&s, pfx.len()) {
let plen = p.len();
if !all && lbuf.len() > plen {
continue;
}
debug!("binding prefix '{p}' to '{s}'");
compiled.insert(p, s.clone());
if !all && plen - lbuf.len() > 0 {
break;
}
}
}
compiled
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashMap};
use crate::zle::extract::{Cmd, Cmds};
use super::compile;
#[test]
fn test_compile() {
let cmds = Cmds(BTreeMap::new());
assert_eq!(compile(&cmds, "", false).iter().collect::<Vec<_>>(), []);
let cmds = Cmds(BTreeMap::from([(
String::from("git"),
Cmd {
short: String::from("g"),
flags: HashMap::new(),
no_args: false,
subs: Cmds(BTreeMap::from([(
String::from("submodule"),
Cmd {
short: String::from("su"),
flags: HashMap::new(),
no_args: false,
subs: Cmds::default(),
},
)])),
},
)]));
assert_eq!(
compile(&cmds, "g", false)
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect::<Vec<_>>(),
[("g", "git "), ("gsu", "git submodule ")]
);
}
}