use crate::compsys::ported::_alternative::_alternative;
use crate::compsys::ported::_message::_message;
use crate::ported::glob::remnulargs;
use crate::ported::lex::{parse_subst_string, untokenize};
use crate::ported::modules::zutil::bin_zregexparse;
use crate::ported::params::{
getaparam, getiparam, getsparam, setaparam, unsetparam,
};
use crate::ported::zle::compcore::get_compstate_str;
use crate::ported::zle::complete::bin_compset;
use crate::ported::zsh_h::{options, QT_DOLLARS, MAX_OPS};
use std::collections::HashMap;
use std::sync::Mutex;
static REGEX_FUNCS: Mutex<Option<HashMap<String, Vec<String>>>> = Mutex::new(None);
fn registry() -> std::sync::MutexGuard<'static, Option<HashMap<String, Vec<String>>>> {
let mut g = REGEX_FUNCS.lock().unwrap();
if g.is_none() {
*g = Some(HashMap::new());
}
g
}
fn make_ops() -> options {
options {
ind: [0u8; MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
}
}
fn dequote_q(s: &str) -> String {
match parse_subst_string(s) {
Ok(mut r) => {
remnulargs(&mut r);
untokenize(&r)
}
Err(_) => s.to_string(),
}
}
pub fn _ra_comp(args: &[String]) -> i32 {
let mut acts = getaparam("_ra_actions").unwrap_or_default();
if let Some(a) = args.first() {
acts.push(a.clone());
}
setaparam("_ra_actions", acts);
0
}
pub fn _regex_arguments(args: &[String]) -> i32 {
let funcname = match args.first() {
Some(f) => f.clone(),
None => return 1,
};
let regex: Vec<String> = args[1..]
.iter()
.map(|e| match e.strip_prefix(':') {
Some(rest) => format!(
":_ra_comp {}",
crate::ported::utils::quotestring(rest, QT_DOLLARS)
),
None => e.clone(),
})
.collect();
if let Some(map) = registry().as_mut() {
map.insert(funcname, regex);
}
0
}
pub fn dispatch_if_registered(funcname: &str) -> Option<i32> {
let known = {
let g = registry();
g.as_ref().map(|m| m.contains_key(funcname)).unwrap_or(false)
};
if known {
Some(dispatch_registered(funcname))
} else {
None
}
}
pub fn dispatch_registered(funcname: &str) -> i32 {
let regex = {
let g = registry();
g.as_ref().and_then(|m| m.get(funcname).cloned())
};
let regex = match regex {
Some(r) => r,
None => return 1,
};
let nm: i64 = get_compstate_str("nmatches")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let current: usize = getsparam("CURRENT")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let words = getaparam("words").unwrap_or_default();
let upto = current.saturating_sub(1); let dequoted: Vec<String> = words.iter().take(upto).map(|w| dequote_q(w)).collect();
let prefix = getsparam("PREFIX").unwrap_or_default();
let mut _ra_line = dequoted.join("\0");
_ra_line.push('\0');
_ra_line.push_str(&prefix);
setaparam("_ra_actions", Vec::new());
let mut zargs: Vec<String> = vec![
"_ra_p1".to_string(),
"_ra_p2".to_string(),
_ra_line.clone(),
];
zargs.extend(regex.iter().cloned());
let mut ops = make_ops();
ops.ind[b'c' as usize] = 1; let rc = bin_zregexparse("zregexparse", &zargs, &ops, 0);
let p1 = getiparam("_ra_p1");
let p2 = getiparam("_ra_p2");
let line_bytes = _ra_line.as_bytes();
match rc {
0 | 2 => {
let _ = _message(&["no more arguments".to_string()]);
}
1 => {
let tail_has_nul = line_bytes
.get(p1.max(0) as usize..)
.map(|b| b.contains(&0u8))
.unwrap_or(false);
if tail_has_nul {
let _ = _message(&["parse failed before current word".to_string()]);
} else {
let lo = p1.max(0) as usize;
let hi = (p2.max(0) as usize).min(line_bytes.len()).max(lo);
let _ra_left = String::from_utf8_lossy(&line_bytes[lo..hi]).into_owned();
let _ra_right =
String::from_utf8_lossy(line_bytes.get(hi..).unwrap_or(&[])).into_owned();
let _ = (&_ra_left, &_ra_right);
let n = prefix.chars().count() as i64 - _ra_line.chars().count() as i64 + p1;
let _ = bin_compset(
"compset",
&["-p".to_string(), n.to_string()],
&make_ops(),
0,
);
let actions = getaparam("_ra_actions").unwrap_or_default();
if !actions.is_empty() {
let _ = _alternative(&actions);
}
}
}
3 => {
let _ = _message(&["invalid regex".to_string()]);
}
_ => {}
}
unsetparam("_ra_p1");
unsetparam("_ra_p2");
unsetparam("_ra_actions");
let nm2: i64 = get_compstate_str("nmatches")
.and_then(|s| s.parse().ok())
.unwrap_or(0);
if nm != nm2 {
0
} else {
1
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registration_rewrites_cactions() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
_regex_arguments(&[
"_tst".to_string(),
"/x/".to_string(),
":compadd aaa".to_string(),
]),
0
);
let g = registry();
let stored = g.as_ref().unwrap().get("_tst").cloned().unwrap();
assert_eq!(stored[0], "/x/");
assert!(
stored[1].starts_with(":_ra_comp "),
"caction should funnel through _ra_comp, got {:?}",
stored[1]
);
assert!(
stored[1].contains("compadd aaa"),
"original spec must be preserved (quoted), got {:?}",
stored[1]
);
}
#[test]
fn ra_comp_appends_to_actions() {
let _g = crate::test_util::global_state_lock();
setaparam("_ra_actions", Vec::new());
assert_eq!(_ra_comp(&["compadd aaa".to_string()]), 0);
assert_eq!(_ra_comp(&["compadd bbb".to_string()]), 0);
assert_eq!(
getaparam("_ra_actions").unwrap_or_default(),
vec!["compadd aaa".to_string(), "compadd bbb".to_string()]
);
}
#[test]
fn missing_registration_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(dispatch_registered("never_registered"), 1);
}
#[test]
fn dispatch_runs_zregexparse_and_cleans_up() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
_regex_arguments(&[
"_tst2".to_string(),
"/[^\0]#\0/".to_string(),
"/[^\0]#\0/".to_string(),
]),
0
);
let rc = dispatch_registered("_tst2");
assert!(rc == 0 || rc == 1);
assert!(crate::ported::params::getaparam("_ra_actions").is_none());
}
}