use crate::ported::exec::dispatch_function_call;
use crate::ported::modules::zutil::bin_zformat;
use crate::ported::params::{getaparam, getsparam, setaparam, setsparam};
use crate::ported::zle::compcore::set_compstate_str;
use crate::ported::zle::complete::bin_compadd;
use crate::ported::zle::computil::bin_comptry;
use crate::ported::zsh_h::{options, MAX_OPS};
const HELP_SCAN_FUNCSTACK: &str = "main_complete|complete|approximate|normal";
const HELP_FILTER_FUNCSTACK: &str =
"alternative|call_function|describe|dispatch|wanted|requested|all_labels|next_label";
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
fn assoc_get(name: &str, key: &str) -> String {
getaparam(name)
.unwrap_or_default()
.chunks(2)
.find(|kv| kv.first().map(|k| k == key).unwrap_or(false))
.and_then(|kv| kv.get(1).cloned())
.unwrap_or_default()
}
fn assoc_set(name: &str, key: &str, val: &str) {
let mut flat = getaparam(name).unwrap_or_default();
let mut i = 0;
let mut done = false;
while i + 1 < flat.len() {
if flat[i] == key {
flat[i + 1] = val.to_string();
done = true;
break;
}
i += 2;
}
if !done {
flat.push(key.to_string());
flat.push(val.to_string());
}
let _ = setaparam(name, flat);
}
fn assoc_keys_sorted(name: &str) -> Vec<String> {
let mut keys: Vec<String> = getaparam(name)
.unwrap_or_default()
.chunks(2)
.filter_map(|kv| kv.first().cloned())
.collect();
keys.sort();
keys
}
fn append_context_report(
text: &mut String,
funcs_assoc: &str,
tags_assoc: &str,
heading: impl Fn(&str) -> String,
) {
for i in assoc_keys_sorted(funcs_assoc) {
text.push('\n');
text.push_str(&heading(&i));
let mut tmp: Vec<String> = Vec::new();
let funcs_val = assoc_get(funcs_assoc, &i);
for j in nul_split_from_2(&funcs_val) {
let tags_val = assoc_get(tags_assoc, &format!("{}{}", i, j));
tmp.extend(comma_split_from_2(&tags_val));
}
let aligned = zformat_align(" (", &tmp);
let wrapped: Vec<String> = aligned.iter().map(|e| format!("\n {})", e)).collect();
text.push_str(&wrapped.join(" "));
}
}
fn nul_split_from_2(s: &str) -> Vec<String> {
let rest: String = s.chars().skip(1).collect();
if rest.is_empty() {
return Vec::new();
}
rest.split('\0').map(|x| x.to_string()).collect()
}
fn comma_split_from_2(s: &str) -> Vec<String> {
let rest: String = s.chars().skip(1).collect();
if rest.is_empty() {
return Vec::new();
}
rest.split(',').map(|x| x.to_string()).collect()
}
fn zformat_align(sep: &str, specs: &[String]) -> Vec<String> {
if specs.is_empty() {
return Vec::new();
}
let tmp_name = ".complete_help.zf";
let mut argv = vec!["-a".to_string(), tmp_name.to_string(), sep.to_string()];
argv.extend(specs.iter().cloned());
let _ = setaparam(tmp_name, Vec::new());
let _ = bin_zformat("zformat", &argv, &make_ops(), 0);
let out = getaparam(tmp_name).unwrap_or_default();
let _ = setaparam(tmp_name, Vec::new());
out
}
pub fn _complete_help(args: &[String]) -> i32 {
for a in ["help_funcs", "help_tags", "help_sfuncs", "help_styles"] {
let _ = setaparam(a, Vec::new());
}
let _ = setsparam("_help_scan_funcstack", HELP_SCAN_FUNCSTACK);
let _ = setsparam("_help_filter_funcstack", HELP_FILTER_FUNCSTACK);
let saved_sort_tags = getsparam("_sort_tags");
let _ = setsparam("_sort_tags", "_help_sort_tags");
let _ = dispatch_function_call(
"_shadow",
&[
"compadd".to_string(),
"compcall".to_string(),
"zstyle".to_string(),
],
);
crate::ported::modules::parameter::setfunction("compadd", "return 1".to_string(), 0);
crate::ported::modules::parameter::setfunction(
"compcall",
"_help_sort_tags use-compctl".to_string(),
0,
);
let target = args
.first()
.filter(|s| !s.is_empty())
.cloned()
.unwrap_or_else(|| "_main_complete".to_string());
let ret = dispatch_function_call(&target, &[]).unwrap_or(1);
if let Ok(mut tab) = crate::ported::hashtable::shfunctab_lock().write() {
tab.remove("compadd");
tab.remove("compcall");
}
let _ = dispatch_function_call("_unshadow", &[]);
let mut text = String::new();
append_context_report(&mut text, "help_funcs", "help_tags", |i| {
format!("tags in context :completion:{}:", i)
});
let numeric: i64 = getsparam("NUMERIC")
.filter(|s| !s.is_empty())
.and_then(|s| s.parse().ok())
.unwrap_or(1);
if numeric != 1 {
text.push('\n');
append_context_report(&mut text, "help_sfuncs", "help_styles", |i| {
format!("styles in context {}", i)
});
}
set_compstate_str("list", "list force");
set_compstate_str("insert", "");
let body: String = text.chars().skip(1).collect();
let _ = bin_compadd(
"compadd",
&[
"-U".to_string(),
"-X".to_string(),
body,
"-n".to_string(),
"".to_string(),
],
&make_ops(),
0,
);
match saved_sort_tags {
Some(v) => {
let _ = setsparam("_sort_tags", &v);
}
None => {
let _ = setsparam("_sort_tags", "");
}
}
ret
}
pub fn _help_sort_tags(args: &[String]) -> i32 {
let f = derive_responsible_func();
let curcontext = getsparam("curcontext").unwrap_or_default();
let argv_joined = args.join(" ");
let funcs_val = assoc_get("help_funcs", &curcontext);
let tags_key = format!("{}{}", curcontext, f);
let tags_val = assoc_get("help_tags", &tags_key);
let f_recorded = funcs_val.contains(&f);
let any_tag_present = !args.is_empty() && args.iter().any(|t| tags_val.contains(t.as_str()));
if !f_recorded || !any_tag_present {
if !f_recorded {
assoc_set("help_funcs", &curcontext, &format!("{}\0{}", funcs_val, f));
}
assoc_set(
"help_tags",
&tags_key,
&format!("{},{}:{}", tags_val, argv_joined, f),
);
return bin_comptry("comptry", args, &make_ops(), 0);
}
0
}
fn read_funcstack() -> Vec<String> {
crate::ported::modules::parameter::FUNCSTACK
.lock()
.map(|f| f.iter().rev().map(|fs| fs.name.clone()).collect())
.unwrap_or_default()
}
fn derive_responsible_func() -> String {
let funcstack = read_funcstack();
let len = funcstack.len();
let scan: Vec<String> = HELP_SCAN_FUNCSTACK
.split('|')
.map(|s| format!("_{}", s))
.collect();
let mut end_1based = len + 1;
for (idx, el) in funcstack.iter().enumerate() {
if scan.iter().any(|s| s == el) {
end_1based = idx + 1;
break;
}
}
let start = 2usize;
let end = end_1based.min(len);
let slice: &[String] = if start < end {
&funcstack[start..end]
} else {
&[]
};
let filter: Vec<String> = HELP_FILTER_FUNCSTACK
.split('|')
.map(|s| format!("_{}", s))
.collect();
let kept: Vec<String> = slice
.iter()
.filter(|el| {
let e = el.as_str();
!(filter.iter().any(|f| f == e) || e == "(eval)" || e == "(anon)")
})
.map(|el| match el.rfind(' ') {
Some(pos) => el[..pos].to_string(),
None => el.clone(),
})
.collect();
kept.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
fn set_test_funcstack(names_innermost_first: &[&str]) {
let mut stack = crate::ported::modules::parameter::FUNCSTACK.lock().unwrap();
stack.clear();
for name in names_innermost_first.iter().rev() {
stack.push(crate::ported::zsh_h::funcstack {
prev: None,
name: name.to_string(),
filename: None,
caller: None,
flineno: 0,
lineno: 0,
tp: 0,
});
}
}
fn clear_test_funcstack() {
crate::ported::modules::parameter::FUNCSTACK
.lock()
.unwrap()
.clear();
}
#[test]
fn returns_one_without_executor() {
let _g = crate::test_util::global_state_lock();
assert_eq!(_complete_help(&[]), 1);
}
#[test]
fn clears_help_assocs_before_dispatch() {
let _g = crate::test_util::global_state_lock();
let _ = setaparam("help_funcs", vec!["ctx".to_string(), "\0stale".to_string()]);
let _ = _complete_help(&[]);
let after = getaparam("help_funcs").unwrap_or_default();
assert!(
!after.iter().any(|s| s == "\0stale"),
"help_funcs must be cleared at widget entry"
);
}
#[test]
fn sets_sort_tags_hook_during_run_and_restores_after() {
let _g = crate::test_util::global_state_lock();
let _ = setsparam("_sort_tags", "");
let _ = _complete_help(&[]);
assert_eq!(
getsparam("_sort_tags").unwrap_or_default(),
"",
"_sort_tags must be restored to its prior (empty) value"
);
}
#[test]
fn help_sort_tags_records_func_and_tags() {
let _g = crate::test_util::global_state_lock();
let _ = setaparam("help_funcs", Vec::new());
let _ = setaparam("help_tags", Vec::new());
let _ = setsparam("curcontext", ":completion::complete:mycmd:");
set_test_funcstack(&["_help_sort_tags", "_tags", "_files", "_main_complete"]);
let _ = _help_sort_tags(&["files".to_string(), "directories".to_string()]);
let funcs = assoc_get("help_funcs", ":completion::complete:mycmd:");
assert!(
funcs.contains("_files"),
"help_funcs must record the responsible completer, got {:?}",
funcs
);
let tags = assoc_get(
"help_tags",
&format!(":completion::complete:mycmd:{}", "_files _main_complete"),
);
assert!(
tags.contains("files directories"),
"help_tags must record the tag list, got {:?}",
tags
);
clear_test_funcstack();
}
#[test]
fn derive_responsible_func_slices_and_filters() {
let _g = crate::test_util::global_state_lock();
set_test_funcstack(&[
"_help_sort_tags",
"_tags",
"_wanted",
"_files",
"_main_complete",
"_normal",
]);
let f = derive_responsible_func();
assert_eq!(f, "_files _main_complete");
clear_test_funcstack();
}
}