use crate::compsys::ported::_description::_description;
use crate::compsys::ported::_requested::_requested;
use crate::compsys::ported::_tags::_tags;
use crate::compsys::ported::shared::{
caller_is_prefix, expansion_description_args, expansion_partition_argv, right_pad_or_truncate,
LocalScope, PM_ARRAY,
};
use crate::ported::exec::dispatch_function_call;
use crate::ported::modules::zutil::lookupstyle;
use crate::ported::params::{getaparam, getiparam, getsparam, setaparam, setsparam};
use crate::ported::zle::compcore::{get_compstate_str, set_compstate_str};
use crate::ported::zle::complete::bin_compadd;
use crate::ported::zsh_h::{isset, options, MAX_OPS, MULTIOS};
use std::path::Path;
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
pub fn _user_expand() -> i32 {
let _fn_scope = crate::compsys::ported::shared::FnScope::enter("_user_expand");
if getiparam("_matcher_num") > 1 {
return 1;
}
let mut _scope = LocalScope::declare(&["dir", "space", "normal", "dstr", "expl"], PM_ARRAY);
_scope.also(&["REPLY"], 0);
let iprefix = getsparam("IPREFIX").unwrap_or_default();
let prefix = getsparam("PREFIX").unwrap_or_default();
let suffix = getsparam("SUFFIX").unwrap_or_default();
let isuffix = getsparam("ISUFFIX").unwrap_or_default();
let word = if caller_is_prefix() {
format!("{}{}{}", iprefix, prefix, suffix) } else {
format!("{}{}{}{}", iprefix, prefix, suffix, isuffix) };
let ctx = format!(":completion:{}:", getsparam("curcontext").unwrap_or_default());
let specs = lookupstyle(&ctx, "user-expand");
if specs.is_empty() {
return 1;
}
let mut exp: Vec<String> = vec![word.clone()];
for spec in &specs {
let _ = setsparam("REPLY", "");
if let Some(name) = spec.strip_prefix('$') {
let arr = getaparam(name).unwrap_or_default();
let val = arr
.chunks(2)
.find(|kv| kv.first().map(|k| k == &word).unwrap_or(false))
.and_then(|kv| kv.get(1).cloned())
.unwrap_or_default();
if !val.is_empty() {
exp = vec![val]; break;
}
} else if spec.starts_with('_') {
setaparam("reply", Vec::new());
let _ = dispatch_function_call(spec, &[word.clone()]);
let reply = getaparam("reply").unwrap_or_default();
if !reply.is_empty() {
exp = reply;
break;
}
}
}
if exp.len() == 1 && exp[0] == word {
return 1;
}
let sort = lookupstyle(&ctx, "sort").join(" ");
if matches!(sort.as_str(), "yes" | "true" | "1" | "on") {
exp.sort();
}
let mut asp = String::new();
{
let vals = lookupstyle(&ctx, "add-space");
if !vals.is_empty() {
let tmp = vals.join(" ");
if !tmp.contains("subst")
|| !word.contains('$')
|| exp.first().map(|e| e.contains('$')).unwrap_or(false)
{
if tmp.contains("file") {
asp = "file".to_string(); }
if ["yes", "true", "1", "on", "subst"]
.iter()
.any(|k| tmp.contains(k))
{
asp = format!("yes{}", asp); }
}
} else {
asp = "file".to_string(); }
}
let mut suf = " ".to_string();
if exp.len() == 1 {
if Path::new(&exp[0]).is_dir() && !exp[0].ends_with('/') {
suf = "/".to_string(); } else if asp.starts_with("yes")
|| (asp.ends_with("file") && Path::new(&exp[0]).is_file())
{
suf = " ".to_string(); } else {
suf = String::new(); }
}
let reply_tail = match getsparam("REPLY").unwrap_or_default() {
r if r.is_empty() => String::new(),
r => format!(": {}", r),
};
if get_compstate_str("insert").unwrap_or_default().is_empty() {
setaparam("exp", exp.clone());
let _ = _description(&expansion_description_args(
&sort,
"expansions",
&format!("expansions{}", reply_tail),
&word,
));
let mut argv = getaparam("expl").unwrap_or_default();
argv.extend([
"-UQ".to_string(),
"-qS".to_string(),
suf.clone(),
"-a".to_string(),
"exp".to_string(),
]);
return bin_compadd("compadd", &argv, &make_ops(), 0); }
let _ = _tags(&[
"all-expansions".to_string(),
"expansions".to_string(),
"original".to_string(),
]);
if !exp.is_empty() && _requested(&["expansions".to_string()]) == 0 {
let _ = _description(&expansion_description_args(
&sort,
"expansions",
&format!("expansions{}", reply_tail),
&word,
));
let mut normal: Vec<String> = Vec::new();
let mut space: Vec<String> = Vec::new();
let mut dir: Vec<String> = Vec::new();
for i in &exp {
if Path::new(i).is_dir() && !i.ends_with('/') {
dir.push(i.clone()); } else if asp.starts_with("yes") || (asp.ends_with("file") && Path::new(i).is_file()) {
space.push(i.clone()); } else {
normal.push(i.clone()); }
}
let expl = getaparam("expl").unwrap_or_default();
if !dir.is_empty() {
setaparam("dir", dir);
let _ = bin_compadd(
"compadd",
&expansion_partition_argv(&expl, None, "/", "dir"),
&make_ops(),
0,
);
}
if !space.is_empty() {
setaparam("space", space);
let _ = bin_compadd(
"compadd",
&expansion_partition_argv(&expl, None, " ", "space"),
&make_ops(),
0,
);
}
if !normal.is_empty() {
setaparam("normal", normal);
let _ = bin_compadd(
"compadd",
&expansion_partition_argv(&expl, None, "", "normal"),
&make_ops(),
0,
);
}
}
if _requested(&["all-expansions".to_string()]) == 0 {
let _ = _description(&expansion_description_args(
&sort,
"all-expansions",
&format!("all expansions{}", reply_tail),
&word,
));
let mut disp: Vec<String> = Vec::new();
let columns = getiparam("COLUMNS");
let joined_len = exp.join(" ").chars().count() as i64; if columns > 5 && joined_len >= columns {
setaparam(
"dstr",
vec![format!(
"{} ...",
right_pad_or_truncate(&exp.join(" "), (columns - 5) as usize)
)], );
disp = vec!["-ld".to_string(), "dstr".to_string()]; }
if isset(MULTIOS) {
let redirect = get_compstate_str("redirect").unwrap_or_default();
let mut rebuilt: Vec<String> = Vec::new();
if let Some(first) = exp.first() {
rebuilt.push(first.clone());
}
rebuilt.extend(exp.iter().skip(1).map(|e| format!("{}{}", redirect, e)));
exp = rebuilt;
}
let mut argv = disp;
argv.extend(getaparam("expl").unwrap_or_default());
argv.extend([
"-UQ".to_string(),
"-qS".to_string(),
suf.clone(),
"-".to_string(),
exp.join(" "),
]);
let _ = bin_compadd("compadd", &argv, &make_ops(), 0);
}
if _requested(&[
"original".to_string(),
"expl".to_string(),
"original".to_string(),
]) == 0
{
let mut argv = getaparam("expl").unwrap_or_default();
argv.extend(["-UQ".to_string(), "-".to_string(), word.clone()]);
let _ = bin_compadd("compadd", &argv, &make_ops(), 0);
}
set_compstate_str("insert", "menu");
0
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::modules::zutil::bin_zstyle;
use crate::ported::params::setsparam;
use crate::ported::zle::comp_h::Cmatch;
fn drive(word: &str, expansion: &str, insert: &str) -> (Vec<Cmatch>, String) {
use crate::ported::zle::compcore::set_compstate_str;
use std::sync::atomic::Ordering;
let _ = setsparam("IPREFIX", "");
let _ = setsparam("PREFIX", word);
let _ = setsparam("SUFFIX", "");
let _ = setsparam("ISUFFIX", "");
let _ = setsparam("curcontext", "");
crate::ported::params::setiparam("_matcher_num", 1);
setaparam(
"ZZQ_UE_MAP",
vec![word.to_string(), expansion.to_string()],
);
let _ = bin_zstyle(
"zstyle",
&[
":completion:*".to_string(),
"user-expand".to_string(),
"$ZZQ_UE_MAP".to_string(),
],
&make_ops(),
0,
);
set_compstate_str("insert", insert);
crate::comp_match_handles::matches_arc().lock().unwrap().clear();
crate::ported::zle::complete::INCOMPFUNC.store(1, Ordering::Relaxed);
let _ = _user_expand();
crate::ported::zle::complete::INCOMPFUNC.store(0, Ordering::Relaxed);
let arc = crate::comp_match_handles::matches_arc();
let got = arc.lock().unwrap().clone();
(
got,
crate::ported::zle::compcore::get_compstate_str("insert").unwrap_or_default(),
)
}
#[test]
fn lone_directory_expansion_gets_the_slash_suffix() {
let _g = crate::test_util::global_state_lock();
let _g2 = crate::ported::zle::zle_main::zle_test_setup();
let dir = std::env::temp_dir();
let dir = dir.to_string_lossy().trim_end_matches('/').to_string();
let (matches, _) = drive("zzqdir", &dir, "");
assert_eq!(
matches.last().and_then(|m| m.suf.clone()),
Some("/".to_string()),
"sh:78 — a lone directory expansion is added with `-qS /`"
);
let (matches, _) = drive("zzqplain", "zzqNOTAPATH", "");
assert_eq!(
matches.last().and_then(|m| m.suf.clone()),
Some(String::new()),
"sh:83 — a lone non-path expansion is added with an empty `-qS`"
);
}
#[test]
fn non_empty_compstate_insert_takes_the_menu_arm() {
let _g = crate::test_util::global_state_lock();
let _g2 = crate::ported::zle::zle_main::zle_test_setup();
let (_, insert) = drive("zzqmenu", "zzqEXPANSION", "automenu");
assert_eq!(insert, "menu", "sh:144 — `compstate[insert]=menu`");
let (_, insert) = drive("zzqmenu", "zzqEXPANSION", "");
assert_eq!(insert, "", "sh:87 — the empty-insert arm never sets it");
}
#[test]
fn returns_one_when_user_expand_unset() {
let _g = crate::test_util::global_state_lock();
let _ = setsparam("PREFIX", "");
let _ = setsparam("SUFFIX", "");
let _ = setsparam("IPREFIX", "");
let _ = setsparam("ISUFFIX", "");
crate::ported::params::setiparam("_matcher_num", 1);
assert_eq!(_user_expand(), 1);
}
#[test]
fn prefix_caller_drops_isuffix_from_the_word() {
let _g = crate::test_util::global_state_lock();
let _ = setsparam("IPREFIX", "");
let _ = setsparam("PREFIX", "foo");
let _ = setsparam("SUFFIX", "");
let _ = setsparam("ISUFFIX", "bar");
let _ = setsparam("curcontext", "");
crate::ported::params::setiparam("_matcher_num", 1);
setaparam(
"ZZQ_UE_MAP",
vec!["foo".to_string(), "ZZQ_HIT".to_string()],
);
setaparam("exp", Vec::new());
let _ = bin_zstyle(
"zstyle",
&[
":completion:*".to_string(),
"user-expand".to_string(),
"$ZZQ_UE_MAP".to_string(),
],
&make_ops(),
0,
);
assert_eq!(_user_expand(), 1, "sh:21 arm must look up `foobar`");
{
let mut stack = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
for name in ["_prefix", "_user_expand"] {
stack.push(crate::ported::zsh_h::funcstack {
prev: None,
name: name.to_string(),
filename: None,
caller: None,
flineno: 0,
lineno: 0,
tp: 0,
});
}
}
let _ = _user_expand();
{
let mut stack = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
stack.pop();
stack.pop();
}
assert_eq!(
getaparam("exp").unwrap_or_default(),
vec!["ZZQ_HIT".to_string()],
"sh:19 arm must look up `foo`, not `foo`+$ISUFFIX"
);
}
}