use crate::ported::lex::untokenize;
use crate::ported::zsh_h::{Qstring, Snull, Stringg};
use std::sync::{Mutex, OnceLock};
static COMPS_TOK: OnceLock<Mutex<Option<String>>> = OnceLock::new();
fn cell() -> &'static Mutex<Option<String>> {
COMPS_TOK.get_or_init(|| Mutex::new(None))
}
pub fn set(tok: Option<String>) {
if let Ok(mut g) = cell().lock() {
*g = tok;
}
}
pub fn get() -> Option<String> {
cell().lock().ok().and_then(|g| g.clone())
}
pub fn span(tok: &str, ubeg: usize, ulen: usize) -> Option<String> {
let uwhole = untokenize(tok);
let uend = ubeg.checked_add(ulen)?;
if uend > uwhole.len() || !uwhole.is_char_boundary(ubeg) || !uwhole.is_char_boundary(uend) {
return None;
}
let want = &uwhole[ubeg..uend];
let chars: Vec<(usize, char)> = tok.char_indices().collect();
let mut upos = 0usize;
let mut beg: Option<usize> = None;
let mut end = tok.len();
for (n, &(bi, c)) in chars.iter().enumerate() {
if (c == Stringg || c == Qstring) && chars.get(n + 1).map(|x| x.1) == Some(Snull) {
return None;
}
if beg.is_none() && upos >= ubeg {
beg = Some(bi);
}
if upos >= uend {
end = bi;
break;
}
upos += untokenize(&c.to_string()).len();
}
if beg.is_none() && upos >= ubeg {
beg = Some(tok.len());
}
let cand = tok.get(beg?..end)?.to_string();
if untokenize(&cand) == want {
Some(cand)
} else {
None
}
}
pub fn tok_index(tok: &str, ubyte: usize) -> Option<usize> {
let chars: Vec<(usize, char)> = tok.char_indices().collect();
let mut upos = 0usize;
for (n, &(bi, c)) in chars.iter().enumerate() {
let ulen = untokenize(&c.to_string()).len();
if ulen == 0 {
continue;
}
if upos == ubyte {
return Some(bi);
}
if (c == Stringg || c == Qstring) && chars.get(n + 1).map(|x| x.1) == Some(Snull) {
return None;
}
upos += ulen;
if upos > ubyte {
return None;
}
}
if upos == ubyte {
Some(tok.len())
} else {
None
}
}
pub fn untok_index(tok: &str, tbyte: usize) -> Option<usize> {
tok.get(..tbyte).map(|head| untokenize(head).len())
}
pub fn strip_nulls(tok: &str) -> String {
use crate::ported::ztype_h::inull;
let chars: Vec<char> = tok.chars().collect();
let mut out = String::with_capacity(tok.len());
let mut i = 0usize;
while i < chars.len() {
let c = chars[i];
if (c == Stringg || c == Qstring) && chars.get(i + 1) == Some(&Snull) {
out.push(c);
out.push(Snull);
i += 2;
while i < chars.len() {
let d = chars[i];
out.push(d);
i += 1;
if d == Snull {
break;
}
}
continue;
}
let b = if (c as u32) < 0x100 { c as u32 as u8 } else { 0 };
if inull(b) {
i += 1;
continue;
}
out.push(c);
i += 1;
}
out
}
pub fn multiquoted(tok: Option<&str>) -> Option<String> {
tok.map(|t| untokenize(&crate::ported::zle::compcore::multiquote(&strip_nulls(t), 0)))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ported::zsh_h::Dnull;
#[test]
fn span_maps_past_dropped_quote_markers() {
let tok = format!("{Dnull}{Qstring}PA");
assert_eq!(untokenize(&tok), "$PA");
assert_eq!(span(&tok, 1, 2).as_deref(), Some("PA"));
assert_eq!(span(&tok, 0, 3), Some(tok.clone()));
}
#[test]
fn tok_index_and_untok_index_are_inverses() {
let tok = format!("{Dnull}{Qstring}PA");
assert_eq!(untokenize(&tok), "$PA");
assert_eq!(tok_index(&tok, 0), Some(Dnull.len_utf8()));
assert_eq!(
tok_index(&tok, 1),
Some(Dnull.len_utf8() + Qstring.len_utf8())
);
assert_eq!(tok_index(&tok, 3), Some(tok.len()));
assert_eq!(tok_index(&tok, 4), None);
for u in 0..=3 {
let t = tok_index(&tok, u).unwrap();
assert_eq!(untok_index(&tok, t), Some(u));
}
}
#[test]
fn tok_index_refuses_a_dollar_quote_region() {
let tok = format!("{Stringg}{Snull}a{Snull}");
assert_eq!(tok_index(&tok, 1), None);
}
#[test]
fn span_refuses_a_dollar_quote_region() {
let tok = format!("{Stringg}{Snull}a{Snull}");
assert_eq!(span(&tok, 0, 1), None);
}
#[test]
fn strip_nulls_chucks_quote_markers_but_keeps_dollar_quote() {
let _g = crate::test_util::global_state_lock();
assert_eq!(strip_nulls(&format!("{Dnull}a b")), "a b");
let dq = format!("{Stringg}{Snull}a{Snull}");
assert_eq!(strip_nulls(&dq), dq);
}
}