use std::collections::HashMap;
use crate::compsys::ported::_description::_description;
use crate::ported::glob::{matchpat, shtokenize};
use crate::ported::modules::zutil::{bin_zparseopts, lookupstyle, zformat_substring};
use crate::ported::params::{getaparam, getsparam, setaparam, setsparam, unsetparam};
use crate::ported::zle::compcore::{get_compstate_str, set_compstate_str};
use crate::ported::zle::complete::{bin_compadd, bin_compset, cond_psfix, CVT_PREPAT};
use crate::ported::zsh_h::{options, MAX_OPS};
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
fn before_first_colon(s: &str) -> &str {
match s.find(':') {
Some(i) => &s[..i],
None => s,
}
}
fn after_first_colon(s: &str) -> &str {
match s.find(':') {
Some(i) => &s[i + 1..],
None => s,
}
}
fn style_or(context: &str, style: &str, default: &str) -> String {
let vals = lookupstyle(context, style);
if vals.is_empty() {
default.to_string()
} else {
vals.join(" ")
}
}
pub fn _numbers(args: &[String]) -> i32 {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_numbers");
let src = "__compsys_argv";
setaparam(src, args.to_vec());
for name in &[
"opts_flat",
"keep",
"tags",
"units",
"min",
"max",
"default",
"type",
] {
setaparam(name, Vec::new());
}
let _ = bin_zparseopts(
"zparseopts",
&[
"-K".to_string(),
"-D".to_string(),
"-v".to_string(),
src.to_string(),
"-a".to_string(),
"opts_flat".to_string(),
"M+:=keep".to_string(),
"q+=keep".to_string(),
"s+:=keep".to_string(),
"S+:=keep".to_string(),
"J+:".to_string(),
"V+:".to_string(),
"1".to_string(),
"2".to_string(),
"o+:".to_string(),
"n".to_string(),
"F:".to_string(),
"x+:".to_string(),
"X+:".to_string(),
"t:=tags".to_string(),
"u:=units".to_string(),
"l:=min".to_string(),
"m:=max".to_string(),
"d:=default".to_string(),
"f=type".to_string(),
"e=type".to_string(),
"N=type".to_string(),
],
&make_ops(),
0,
);
let positional = getaparam(src).unwrap_or_default();
unsetparam(src);
let keep = getaparam("keep").unwrap_or_default();
let tags = getaparam("tags").unwrap_or_default();
let units = getaparam("units").unwrap_or_default();
let min = getaparam("min").unwrap_or_default();
let max = getaparam("max").unwrap_or_default();
let default = getaparam("default").unwrap_or_default();
let typ_arr = getaparam("type").unwrap_or_default();
let mut desc = positional
.first()
.filter(|s| !s.is_empty())
.cloned()
.unwrap_or_else(|| "number".to_string());
let tag = tags
.get(1)
.cloned()
.unwrap_or_else(|| "numbers".to_string());
let argv: Vec<String> = if positional.is_empty() {
Vec::new()
} else {
positional[1..].to_vec()
};
let f_flag = typ_arr.iter().any(|t| t == "-f"); let n_flag = typ_arr.iter().any(|t| t == "-N") || min.get(1).map(|v| v.starts_with('-')).unwrap_or(false)
|| max.get(1).map(|v| v.starts_with('-')).unwrap_or(false);
let mut pat = if f_flag {
"(<->.[0-9]#|[0-9]#.<->|<->)".to_string() } else {
"<->".to_string() };
let mut partial = if f_flag {
"(|.)".to_string() } else {
String::new() };
if n_flag {
pat = format!("(|-){}", pat); partial = format!("(|-){}", partial); }
let curcontext = getsparam("curcontext").unwrap_or_default();
if !argv.is_empty()
&& bin_compset("compset", &["-P".to_string(), pat.clone()], &make_ops(), 0) == 0
{
let sep = style_or(
&format!(":completion:{}:units", curcontext),
"list-separator",
"--",
);
let _ = _description(&[
"-V".to_string(),
"units".to_string(),
"expl".to_string(),
"unit".to_string(),
]);
let expl = getaparam("expl").unwrap_or_default();
let pad = argv
.iter()
.map(|e| before_first_colon(e).chars().count())
.max()
.unwrap_or(0);
let disp: Vec<String> = argv
.iter()
.map(|e| {
let e = e.strip_prefix(':').unwrap_or(e); let name = before_first_colon(e);
let padded = format!(
"{}{}",
name,
" ".repeat(pad.saturating_sub(name.chars().count()))
);
let rest = &e[name.len()..];
match rest.strip_prefix(':') {
Some(tail) => format!("{} {} {}", padded, sep, tail),
None => padded,
}
})
.collect();
setaparam("disp", disp);
let mut cargs: Vec<String> = vec![
"-M".to_string(),
"r:|/=* r:|=*".to_string(),
"-d".to_string(),
"disp".to_string(),
];
cargs.extend(keep);
cargs.extend(expl);
cargs.push("-".to_string());
cargs.extend(
argv.iter()
.map(|e| before_first_colon(e.strip_prefix(':').unwrap_or(e)).to_string()),
);
return bin_compadd("compadd", &cargs, &make_ops(), 0);
}
let prefix_matches = {
let mut tokenized = pat.clone();
shtokenize(&mut tokenized);
cond_psfix(&[tokenized], CVT_PREPAT) != 0
};
let prefix = getsparam("PREFIX").unwrap_or_default();
if !prefix_matches && !matchpat(&partial, &prefix, true, true) {
return 1;
}
let mut formats: Vec<String> = vec![format!("h:{}", desc)]; if let Some(u) = units.get(1) {
formats.push(format!("m:{}", u));
desc.push_str(&format!(" ({})", u));
}
let mut range = String::new();
if let Some(m) = min.get(1) {
range = format!("{}-", m); }
if let Some(m) = max.get(1) {
if range.is_empty() {
range.push('-');
}
range.push_str(m);
}
if !range.is_empty() {
formats.push(format!("r:{}", range));
desc.push_str(&format!(" ({})", range));
}
if let Some(d) = default.get(1) {
formats.push(format!("o:{}", d));
desc.push_str(&format!(" [{}]", d));
}
let suffixfmt = style_or(
&format!(":completion:{}:unit-suffixes", curcontext),
"format",
"%(d.%U.)%x%(d.%u.)%(r..|)",
);
let mut suffixes = String::new();
let n = argv.len();
for i in 0..n {
let elem = &argv[i];
let stripped = elem.strip_prefix(':').unwrap_or(elem); let mut specs: HashMap<char, String> = HashMap::new();
specs.insert('%', "%".to_string()); specs.insert(')', ")".to_string()); specs.insert('x', before_first_colon(stripped).replace('%', "%%")); specs.insert('X', after_first_colon(stripped).replace('%', "%%")); let d = match elem.chars().next() {
None | Some(':') => 0,
Some(_) => 1,
};
specs.insert('d', d.to_string());
specs.insert('i', "i".to_string());
specs.insert('r', (n - i - 1).to_string()); suffixes.push_str(&zformat_substring(&suffixfmt, &specs, false)); }
if !suffixes.is_empty() {
formats.push(format!("x:{}", suffixes));
}
let _ = setsparam("_comp_mesg", "yes");
let mut desc_argv: Vec<String> = vec!["-x".to_string(), tag, "expl".to_string(), desc];
desc_argv.extend(formats);
let _ = _description(&desc_argv);
let insert = get_compstate_str("insert").unwrap_or_default();
if insert.contains("unambiguous") {
set_compstate_str("insert", "");
}
let expl = getaparam("expl").unwrap_or_default();
let _ = bin_compadd("compadd", &expl, &make_ops(), 0);
0 }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn returns_zero_for_simple_case() {
let _g = crate::test_util::global_state_lock();
let _ = crate::ported::params::setsparam("PREFIX", "");
let _r = _numbers(&[]);
}
#[test]
fn unit_suffix_display_pads_to_longest_name() {
let argv = [":s:seconds", "ms:milliseconds", "min:minutes"];
let pad = argv
.iter()
.map(|e| before_first_colon(e).chars().count())
.max()
.unwrap();
assert_eq!(pad, 3);
let rendered: Vec<String> = argv
.iter()
.map(|e| {
let e = e.strip_prefix(':').unwrap_or(e);
let name = before_first_colon(e);
let padded = format!(
"{}{}",
name,
" ".repeat(pad.saturating_sub(name.chars().count()))
);
match e[name.len()..].strip_prefix(':') {
Some(tail) => format!("{} {} {}", padded, "--", tail),
None => padded,
}
})
.collect();
assert_eq!(
rendered,
vec![
"s -- seconds".to_string(),
"ms -- milliseconds".to_string(),
"min -- minutes".to_string(),
]
);
}
#[test]
fn unit_suffix_format_underlines_default_and_bars_all_but_last() {
let argv = [":s:seconds", "m:minutes", "h:hours"];
let fmt = "%(d.%U.)%x%(d.%u.)%(r..|)";
let n = argv.len();
let mut out = String::new();
for i in 0..n {
let elem = argv[i];
let stripped = elem.strip_prefix(':').unwrap_or(elem);
let mut specs: HashMap<char, String> = HashMap::new();
specs.insert('%', "%".to_string());
specs.insert(')', ")".to_string());
specs.insert('x', before_first_colon(stripped).to_string());
specs.insert('X', after_first_colon(stripped).to_string());
specs.insert(
'd',
match elem.chars().next() {
None | Some(':') => 0,
Some(_) => 1,
}
.to_string(),
);
specs.insert('i', "i".to_string());
specs.insert('r', (n - i - 1).to_string());
out.push_str(&zformat_substring(fmt, &specs, false));
}
assert_eq!(out, "%Us%u|m|h");
}
#[test]
fn after_first_colon_keeps_a_colonless_spec() {
assert_eq!(after_first_colon("bytes"), "bytes");
assert_eq!(after_first_colon("s:seconds"), "seconds");
assert_eq!(after_first_colon("a:b:c"), "b:c");
assert_eq!(before_first_colon("bytes"), "bytes");
assert_eq!(before_first_colon(":s:seconds"), "");
}
}