use crate::compsys::ported::_complete::_complete;
use crate::compsys::ported::_description::_description;
use crate::compsys::ported::_requested::_requested;
use crate::compsys::ported::_shadow::{_shadow, _unshadow};
use crate::compsys::ported::_tags::_tags;
use crate::ported::modules::zutil::{lookupstyle, testforstyle};
use crate::ported::params::{getaparam, getiparam, getsparam, setaparam, setsparam, unsetparam};
use crate::ported::zle::compcore::{get_compstate_str, set_compstate_str};
use crate::ported::zle::complete::{
bin_compadd, clear_compadd_prefix_injector, set_compadd_prefix_injector, COMPADD_ARGV_SHADOW,
};
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 is_u_flag_word(a: &str) -> bool {
match a.strip_prefix('-') {
Some(rest) => {
!rest.is_empty()
&& rest.bytes().all(|b| b.is_ascii_alphabetic())
&& rest.bytes().any(|b| b == b'U')
}
None => false,
}
}
fn is_group_flag_word(a: &str) -> bool {
a.starts_with('-') && a.len() >= 2 && (a.ends_with('J') || a.ends_with('V'))
}
fn argv_flag_slice(argv: &[String]) -> &[String] {
match argv.iter().position(|a| a == "-" || a == "--") {
Some(i) => &argv[..=i],
None => argv,
}
}
fn approximate_compadd_shadow(argv: &[String]) -> Option<Vec<String>> {
let comp_correct = getiparam("_comp_correct");
if !argv.iter().any(|a| is_u_flag_word(a)) {
let len = getsparam("PREFIX").unwrap_or_default().chars().count()
+ getsparam("SUFFIX").unwrap_or_default().chars().count();
if (len as i64) <= comp_correct {
return None;
}
}
let mut expl = getaparam("_correct_expl").unwrap_or_default();
let correct_group = getiparam("_correct_group");
if correct_group > 0 {
let slice = argv_flag_slice(argv);
let repl = slice
.iter()
.rfind(|a| is_group_flag_word(a))
.cloned()
.unwrap_or_default();
let joined = slice.join(" ");
let guard = joined
.rfind(['J', 'V'])
.is_some_and(|j| joined[..j].contains('-'));
if guard {
if let Some(slot) = expl.get_mut(correct_group as usize - 1) {
*slot = repl;
setaparam("_correct_expl", expl.clone());
}
}
}
expl.extend_from_slice(argv);
Some(expl)
}
pub fn _approximate(args: &[String]) -> i32 {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_approximate");
if getiparam("_matcher_num") > 1 {
return 1;
}
let prefix = getsparam("PREFIX").unwrap_or_default();
let suffix = getsparam("SUFFIX").unwrap_or_default();
if prefix.len() + suffix.len() <= 1 {
return 1;
}
let curcontext = getsparam("curcontext").unwrap_or_default();
let cfgacc = if let Some(a) = args.first() {
if let Some(rest) = a.strip_prefix("-a") {
if !rest.is_empty() {
rest.to_string()
} else if args.len() > 1 {
args[1].clone()
} else {
"2 numeric".to_string()
}
} else {
lookupstyle(&format!(":completion:{}:", curcontext), "max-errors")
.first()
.cloned()
.unwrap_or_else(|| "2 numeric".to_string())
}
} else {
lookupstyle(&format!(":completion:{}:", curcontext), "max-errors")
.first()
.cloned()
.unwrap_or_else(|| "2 numeric".to_string())
};
let numeric = getiparam("NUMERIC");
let comax: i64 = if cfgacc.contains("numeric") && numeric != 1 {
if cfgacc.contains("not-numeric") {
return 1;
}
if numeric < 1 {
1
} else {
numeric
}
} else {
cfgacc
.chars()
.filter(|c| c.is_ascii_digit())
.collect::<String>()
.parse()
.unwrap_or(0)
};
if comax < 1 {
return 1;
}
let _ = _tags(&["corrections".to_string(), "original".to_string()]);
let opm = get_compstate_str("pattern_match").unwrap_or_default();
if opm.is_empty() {
set_compstate_str("pattern_match", "*");
}
let shargs = [
"-s".to_string(),
"_approximate".to_string(),
"compadd".to_string(),
];
let _ = crate::compsys::ported::shared::call_compfn("_shadow", &shargs, || _shadow(&shargs));
let mut ret: i32 = 1;
let mut comp_correct: i64 = 1;
let oldcontext = curcontext.clone();
let pre_suf = format!("{}{}", prefix, suffix);
let pre_suf_len = pre_suf.chars().count() as i64;
while comp_correct <= comax {
let _ = setsparam("_comp_correct", &comp_correct.to_string());
let new_ctx = replace_completer_field(&oldcontext, comp_correct);
let _ = setsparam("curcontext", &new_ctx);
let _ = _description(&[
"corrections".to_string(),
"_correct_expl".to_string(),
"corrections".to_string(),
format!("e:{}", comp_correct),
format!("o:{}", pre_suf),
]);
let correct_group = getaparam("_correct_expl")
.unwrap_or_default()
.iter()
.rposition(|a| is_group_flag_word(a))
.map(|i| i + 1)
.unwrap_or(0);
let _ = setsparam("_correct_group", &correct_group.to_string());
set_compadd_prefix_injector(format!("(#a{})", comp_correct));
*COMPADD_ARGV_SHADOW.lock().unwrap() = Some(approximate_compadd_shadow);
let comp_ret = _complete();
*COMPADD_ARGV_SHADOW.lock().unwrap() = None;
clear_compadd_prefix_injector();
if comp_ret == 0 {
let unambig = get_compstate_str("unambiguous").unwrap_or_default();
if testforstyle(&format!(":completion:{}:", new_ctx), "insert-unambiguous") == 0
&& unambig.chars().count() >= pre_suf.chars().count()
{
set_compstate_str("pattern_insert", "unambiguous");
} else if _requested(&["original".to_string()]) == 0 {
let nm: i64 = get_compstate_str("nmatches")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if nm > 1 || testforstyle(&format!(":completion:{}:", new_ctx), "original") == 0 {
let _ = _description(&[
"-V".to_string(),
"original".to_string(),
"expl".to_string(),
"original".to_string(),
]);
let expl = getaparam("expl").unwrap_or_default();
let mut compadd_argv = expl;
compadd_argv.push("-U".to_string());
compadd_argv.push("-Q".to_string());
compadd_argv.push("-".to_string());
compadd_argv.push(pre_suf.clone());
let _ = bin_compadd("compadd", &compadd_argv, &make_ops(), 0);
let list = get_compstate_str("list").unwrap_or_default();
if !list.starts_with("list") {
set_compstate_str("list", format!("{} force", list).trim());
}
}
}
set_compstate_str("pattern_match", &opm);
ret = 0;
break;
}
if pre_suf_len <= comp_correct + 1 {
break;
}
comp_correct += 1;
}
let _ = crate::compsys::ported::shared::call_compfn("_unshadow", &[], _unshadow);
let _ = unsetparam("_comp_correct");
let _ = unsetparam("_correct_expl");
let _ = unsetparam("_correct_group");
let _ = setsparam("curcontext", &oldcontext);
if ret != 0 {
set_compstate_str("pattern_match", &opm);
}
ret
}
fn replace_completer_field(ctx: &str, n: i64) -> String {
match ctx.char_indices().filter(|(_, c)| *c == ':').nth(1) {
Some((second, _)) => format!("{}-{}:{}", &ctx[..second], n, &ctx[second + 1..]),
None => ctx.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn short_input_returns_one() {
let _g = crate::test_util::global_state_lock();
let _ = setsparam("PREFIX", "a");
let _ = setsparam("SUFFIX", "");
assert_eq!(_approximate(&[]), 1);
}
#[test]
fn completer_field_carries_the_error_count() {
assert_eq!(
replace_completer_field(":approximate::", 1),
":approximate-1::"
);
assert_eq!(replace_completer_field(":correct:cd:", 2), ":correct-2:cd:");
assert_eq!(replace_completer_field(":approximate", 1), ":approximate");
}
#[test]
fn flag_word_patterns_match_the_shell_globs() {
assert!(is_u_flag_word("-U"));
assert!(is_u_flag_word("-QU"));
assert!(is_u_flag_word("-Uf"));
assert!(!is_u_flag_word("-Qf"));
assert!(!is_u_flag_word("-p/Users"));
assert!(!is_u_flag_word("plain"));
assert!(is_group_flag_word("-J"));
assert!(is_group_flag_word("-V"));
assert!(is_group_flag_word("-1V"));
assert!(!is_group_flag_word("-2V-default-"));
assert!(!is_group_flag_word("-X"));
}
#[test]
fn group_flag_slice_stops_at_end_of_options() {
let argv: Vec<String> = ["-J", "files", "-", "-V", "x"]
.iter()
.map(|s| s.to_string())
.collect();
let slice = argv_flag_slice(&argv);
assert_eq!(slice.len(), 3);
assert_eq!(slice.iter().rfind(|a| is_group_flag_word(a)).unwrap(), "-J");
let argv2: Vec<String> = ["-1V", "files"].iter().map(|s| s.to_string()).collect();
assert_eq!(argv_flag_slice(&argv2).len(), 2);
}
#[test]
fn shadow_prepends_expl_and_adopts_the_callers_group_variant() {
let _g = crate::test_util::global_state_lock();
let _ = setsparam("PREFIX", "abcdef");
let _ = setsparam("SUFFIX", "");
let _ = setsparam("_comp_correct", "1");
let _ = setsparam("_correct_group", "1");
setaparam(
"_correct_expl",
vec!["-J".into(), "corrections".into(), "-X".into(), "fmt".into()],
);
let argv: Vec<String> = ["-1V", "local-directories", "-", "foo"]
.iter()
.map(|s| s.to_string())
.collect();
let out = approximate_compadd_shadow(&argv).expect("call must not be swallowed");
assert_eq!(
out,
vec![
"-1V",
"corrections",
"-X",
"fmt",
"-1V",
"local-directories",
"-",
"foo"
]
);
let _ = unsetparam("_correct_expl");
let _ = unsetparam("_correct_group");
let _ = unsetparam("_comp_correct");
}
#[test]
fn shadow_swallows_calls_that_cannot_beat_the_error_count() {
let _g = crate::test_util::global_state_lock();
let _ = setsparam("PREFIX", "ab");
let _ = setsparam("SUFFIX", "");
let _ = setsparam("_comp_correct", "2");
let _ = setsparam("_correct_group", "0");
setaparam("_correct_expl", vec!["-J".into(), "corrections".into()]);
let plain: Vec<String> = ["-", "foo"].iter().map(|s| s.to_string()).collect();
assert!(approximate_compadd_shadow(&plain).is_none());
let unmatched: Vec<String> = ["-U", "-", "foo"].iter().map(|s| s.to_string()).collect();
assert!(approximate_compadd_shadow(&unmatched).is_some());
let _ = unsetparam("_correct_expl");
let _ = unsetparam("_correct_group");
let _ = unsetparam("_comp_correct");
}
}