use crate::ported::exec::dispatch_function_call;
use crate::ported::params::{getsparam, setaparam};
use crate::ported::pattern::{patcompile, pattry};
use crate::ported::zle::complete::bin_compadd;
use crate::ported::zsh_h::{options, MAX_OPS};
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
pub fn _combination(args: &[String]) -> i32 {
crate::compsys::ported::shared::call_compfn("_combination", args, || _combination_impl(args))
}
fn glob_prog(pat: &str) -> Option<crate::ported::pattern::Patprog> {
let mut tok = pat.to_string();
crate::ported::glob::tokenize(&mut tok);
patcompile(&tok, 0, None)
}
fn nth_index(keys: &[String], key: &str, num: i64) -> usize {
let prog = glob_prog(key);
let mut seen = 0i64;
for (i, k) in keys.iter().enumerate() {
let hit = match prog.as_ref() {
Some(p) => pattry(p, k),
None => k == key,
};
if hit {
seen += 1;
if seen == num {
return i + 1;
}
}
}
keys.len() + 1
}
fn key_and_num(tmp: &str, whole: &str) -> (String, i64) {
let key = match tmp.rfind(':') {
Some(i) => &tmp[..i], None => tmp,
};
let num = if whole.contains(':') {
let n = tmp.rsplit(':').next().unwrap_or(tmp); n.trim().parse::<i64>().unwrap_or(0)
} else {
1
};
(key.to_string(), num)
}
pub fn _combination_impl(args: &[String]) -> i32 {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_combination");
crate::compsys::ported::shared::declare_locals(
&["sep", "tag", "style", "keys", "pats", "key", "num"],
0,
);
crate::compsys::ported::shared::declare_locals(&["tmp"], crate::ported::zsh_h::PM_ARRAY);
let mut idx = 0usize;
let mut sep = ":".to_string();
if let Some(a) = args.first() {
if a == "-s" {
sep = args.get(1).cloned().unwrap_or_default(); idx = 2;
} else if let Some(rest) = a.strip_prefix("-s") {
sep = rest.to_string(); idx = 1;
}
}
let arg = |i: usize| args.get(i).cloned().unwrap_or_default();
let tag = arg(idx);
let style = arg(idx + 1);
idx += 2;
let keys: Vec<String> = style
.split('-')
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect();
let mut pats: Vec<String> = keys.iter().map(|_| "*".to_string()).collect();
while idx < args.len() && args[idx].contains('=') {
let whole = &args[idx];
let eq = whole.find('=').unwrap();
let (key, num) = key_and_num(&whole[..eq], whole); let at = nth_index(&keys, &key, num); if pats.len() < at {
pats.resize(at, String::new()); }
pats[at - 1] = whole[eq + 1..].to_string(); idx += 1;
}
let last = arg(idx);
let (key, num) = key_and_num(&last, &last);
idx += 1;
let extras: &[String] = args.get(idx..).unwrap_or(&[]);
let call_key = || -> i32 {
let f = format!("_{}", key);
if !crate::compsys::ported::shared::plus_functions(&f) {
return 1; }
dispatch_function_call(&f, extras).unwrap_or(1)
};
let curcontext = getsparam("curcontext").unwrap_or_default();
let style_ctx = format!(":completion:{}:{}", curcontext, tag);
let zs = crate::ported::modules::zutil::bin_zstyle(
"zstyle",
&["-a".to_string(), style_ctx, style.clone(), "tmp".to_string()],
&make_ops(),
0,
);
if zs != 0 {
return call_key(); }
let mut tmp: Vec<String> = crate::ported::params::getaparam("tmp").unwrap_or_default();
if let Some(p) = glob_prog(&pats.join(&sep)) {
tmp.retain(|e| pattry(&p, e));
} else {
tmp.clear();
}
let at = nth_index(&keys, &key, num);
if at != 1 {
let nstar = at.min(keys.len()).saturating_sub(1);
let lead = format!("{}{}", vec!["*"; nstar].join(&sep), sep);
if let Some(p) = glob_prog(&lead) {
for e in tmp.iter_mut() {
let cut = e
.char_indices()
.map(|(i, _)| i)
.chain(std::iter::once(e.len()))
.find(|&i| pattry(&p, &e[..i]));
if let Some(i) = cut {
*e = e[i..].to_string();
}
}
}
}
if let Some(p) = glob_prog(&format!("{}*", sep)) {
for e in tmp.iter_mut() {
let cut = e
.char_indices()
.map(|(i, _)| i)
.chain(std::iter::once(e.len()))
.find(|&i| pattry(&p, &e[i..]));
if let Some(i) = cut {
e.truncate(i);
}
}
}
setaparam("tmp", tmp);
let mut compadd_argv: Vec<String> = extras.to_vec();
compadd_argv.push("-a".to_string());
compadd_argv.push("tmp".to_string());
if bin_compadd("compadd", &compadd_argv, &make_ops(), 0) == 0 {
0
} else {
call_key()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_and_num_follow_sh74_79() {
assert_eq!(key_and_num("hosts:2", "hosts:2"), ("hosts".to_string(), 2));
assert_eq!(key_and_num("hosts", "hosts"), ("hosts".to_string(), 1));
assert_eq!(key_and_num("hosts", "hosts=a:b"), ("hosts".to_string(), 0));
}
#[test]
fn nth_index_counts_matches_and_misses_past_end() {
let keys: Vec<String> = ["users", "hosts", "users"].iter().map(|s| s.to_string()).collect();
assert_eq!(nth_index(&keys, "users", 1), 1);
assert_eq!(nth_index(&keys, "users", 2), 3);
assert_eq!(nth_index(&keys, "ports", 1), 4);
}
#[test]
fn returns_one_without_style() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
_combination_impl(&[
"mytag".to_string(),
"users-hosts".to_string(),
"users".to_string(),
]),
1
);
}
#[test]
fn absent_key_function_is_not_called() {
let _g = crate::test_util::global_state_lock();
const KEY: &str = "zzq_no_such_key";
assert!(
!crate::compsys::ported::shared::plus_functions(&format!("_{KEY}")),
"premise: `_{KEY}` is neither a shell function nor a routed port"
);
assert_eq!(
_combination_impl(&[
"mytag".to_string(),
format!("users-{KEY}"),
KEY.to_string(),
]),
1
);
}
}