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,
LocalOptions, LocalScope, PM_ARRAY,
};
use crate::ported::modules::zutil::lookupstyle;
use crate::ported::params::{getaparam, getiparam, getsparam, setaparam, setsparam};
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");
let _local_options = LocalOptions::enter();
crate::ported::options::opt_state_set("nomatch", false);
if getiparam("_matcher_num") > 1 {
return 1;
}
let mut _scope = LocalScope::declare(
&["dir", "space", "normal", "dstr", "expl", "exp", "reply"],
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('$')
.filter(|n| !n.is_empty() && n.chars().all(|c| c.is_alphanumeric() || c == '_'))
{
let val = if crate::ported::params::gethkparam(name).is_some() {
crate::compsys::ported::shared::assoc_get(name, &word).unwrap_or_default()
} else {
match (getaparam(name), word.trim().parse::<i64>()) {
(Some(arr), Ok(i)) => {
let at = if i < 0 { arr.len() as i64 + i } else { i - 1 };
usize::try_from(at)
.ok()
.and_then(|k| arr.get(k).cloned())
.unwrap_or_default()
}
_ => String::new(),
}
};
if !val.is_empty() {
exp = vec![val]; break;
}
} else if spec.starts_with('_') {
setaparam("reply", Vec::new());
crate::compsys::ported::shared::dispatch_action_command(spec, &[word.clone()], 48);
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 crate::ported::params::getsparam("compstate[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 = crate::ported::params::getsparam("compstate[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);
}
let _ = crate::ported::params::setsparam("compstate[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 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);
let _ = crate::ported::params::sethparam(
"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,
);
let _ = crate::ported::params::setsparam("compstate[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::params::getsparam("compstate[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 _scope = crate::test_util::comp_scope();
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 _g2 = crate::ported::zle::zle_main::zle_test_setup();
let _ = setsparam("IPREFIX", "");
let _ = setsparam("PREFIX", "foo");
let _ = setsparam("SUFFIX", "");
let _ = setsparam("ISUFFIX", "bar");
let _ = setsparam("curcontext", "");
crate::ported::params::setiparam("_matcher_num", 1);
let _ = crate::ported::params::sethparam(
"ZZQ_UE_MAP",
vec!["foo".to_string(), "ZZQ_HIT".to_string()],
);
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,
});
}
}
crate::comp_match_handles::matches_arc().lock().unwrap().clear();
crate::ported::zle::complete::INCOMPFUNC.store(1, std::sync::atomic::Ordering::Relaxed);
let _ = crate::ported::params::setsparam("compstate[insert]", "");
let _ = _user_expand();
crate::ported::zle::complete::INCOMPFUNC.store(0, std::sync::atomic::Ordering::Relaxed);
{
let mut stack = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
stack.pop();
stack.pop();
}
let got = crate::comp_match_handles::matches_arc().lock().unwrap().clone();
assert_eq!(
got.last().and_then(|m| m.str.clone()),
Some("ZZQ_HIT".to_string()),
"sh:19 arm must look up `foo`, not `foo`+$ISUFFIX"
);
}
}