use std::sync::{Mutex, OnceLock};
use std::time::Instant;
#[derive(Debug)]
pub struct CompsysRequest<'a> {
pub line: &'a str,
pub cursor: usize,
pub deadline: Instant,
pub allow_exec: bool,
}
impl<'a> CompsysRequest<'a> {
pub fn new_with_default_budget(line: &'a str, cursor: usize) -> Self {
Self {
line,
cursor,
deadline: Instant::now() + std::time::Duration::from_millis(200),
allow_exec: false,
}
}
}
#[derive(Debug, Clone)]
pub struct CompsysMatch {
pub completion: String,
pub description: Option<String>,
pub group: Option<String>,
pub replace_start: usize,
}
#[derive(Debug, Default)]
pub struct CompsysResponse {
pub matches: Vec<CompsysMatch>,
pub is_incomplete: bool,
}
pub fn try_capture_compadd_argv(argv: &[String]) -> bool {
let mut guard = match COMPADD_CAPTURE_BUFFER.lock() {
Ok(g) => g,
Err(_) => return false,
};
let buf = match guard.as_mut() {
Some(b) => b,
None => return false,
};
let mut group: Option<String> = None;
let mut description: Option<String> = None;
fn takes_arg(c: char) -> bool {
matches!(
c,
'X' | 'x'
| 'd'
| 'J'
| 'V'
| 'P'
| 'S'
| 'p'
| 's'
| 'W'
| 'i'
| 'I'
| 'O'
| 'A'
| 'D'
| 'F'
| 'M'
| 'n'
| 'r'
| 'R'
| 'q'
| 'Q'
| 'T'
| 'U'
| 'C'
| 'y'
| 'e'
)
}
fn pull_arg(argv: &[String], i: &mut usize, a: &str) -> String {
if a.len() > 2 {
a[2..].to_string()
} else if *i + 1 < argv.len() {
*i += 1;
argv[*i].clone()
} else {
String::new()
}
}
let mut i = 0;
while i < argv.len() {
let a = &argv[i];
if a == "--" {
i += 1;
break;
}
if !a.starts_with('-') || a.len() < 2 {
break;
}
let c = a.as_bytes()[1] as char;
match c {
'J' | 'V' => group = Some(pull_arg(argv, &mut i, a)),
'X' => description = Some(pull_arg(argv, &mut i, a)),
_ if takes_arg(c) => {
if a.len() == 2 && i + 1 < argv.len() {
i += 1;
}
}
_ => {}
}
i += 1;
}
for m in argv[i..].iter() {
buf.push(CompsysMatch {
completion: m.clone(),
description: description.clone(),
group: group.clone(),
replace_start: 0, });
}
true
}
pub static COMPADD_CAPTURE_BUFFER: Mutex<Option<Vec<CompsysMatch>>> = Mutex::new(None);
fn complete_at_lock() -> &'static Mutex<()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(()))
}
pub fn complete_at(req: CompsysRequest<'_>) -> CompsysResponse {
let _guard = complete_at_lock().lock().unwrap();
let started = Instant::now();
let saved_zleline = crate::ported::zle::compcore::ZLELINE
.get_or_init(|| std::sync::Mutex::new(String::new()))
.lock()
.map(|g| g.clone())
.unwrap_or_default();
let saved_zlecs = crate::ported::zle::compcore::ZLECS.load(std::sync::atomic::Ordering::SeqCst);
let saved_zlell = crate::ported::zle::compcore::ZLELL.load(std::sync::atomic::Ordering::SeqCst);
let saved_curcontext = crate::ported::params::getsparam("curcontext");
if let Ok(mut g) = crate::ported::zle::compcore::ZLELINE
.get_or_init(|| std::sync::Mutex::new(String::new()))
.lock()
{
*g = req.line.to_string();
}
crate::ported::zle::compcore::ZLECS
.store(req.cursor as i32, std::sync::atomic::Ordering::SeqCst);
crate::ported::zle::compcore::ZLELL
.store(req.line.len() as i32, std::sync::atomic::Ordering::SeqCst);
let _ = crate::ported::params::setsparam("curcontext", ":::");
{
let mut g = COMPADD_CAPTURE_BUFFER.lock().unwrap();
*g = Some(Vec::new());
}
let _ret = crate::ported::zle::zle_tricky::docomplete(crate::ported::zle::zle_h::COMP_COMPLETE);
let matches = {
let mut g = COMPADD_CAPTURE_BUFFER.lock().unwrap();
g.take().unwrap_or_default()
};
if let Ok(mut g) = crate::ported::zle::compcore::ZLELINE
.get_or_init(|| std::sync::Mutex::new(String::new()))
.lock()
{
*g = saved_zleline;
}
crate::ported::zle::compcore::ZLECS.store(saved_zlecs, std::sync::atomic::Ordering::SeqCst);
crate::ported::zle::compcore::ZLELL.store(saved_zlell, std::sync::atomic::Ordering::SeqCst);
match saved_curcontext {
Some(v) => {
let _ = crate::ported::params::setsparam("curcontext", &v);
}
None => crate::ported::params::unsetparam("curcontext"),
}
let is_incomplete = started.elapsed() >= req.deadline.saturating_duration_since(started);
tracing::debug!(
target: "zshrs::compsys::in_editor",
line = req.line,
cursor = req.cursor,
match_count = matches.len(),
elapsed_us = started.elapsed().as_micros() as u64,
"complete_at done",
);
CompsysResponse {
matches,
is_incomplete,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_defaults_to_safe_mode_and_lsp_budget() {
let req = CompsysRequest::new_with_default_budget("ls -", 4);
assert!(!req.allow_exec);
let remaining = req.deadline.duration_since(Instant::now());
assert!(remaining.as_millis() <= 200);
assert!(remaining.as_millis() >= 150);
}
#[test]
fn complete_at_smoke_does_not_panic() {
let req = CompsysRequest::new_with_default_budget("setopt ext", 10);
let resp = complete_at(req);
eprintln!(
"setopt ext -> {} matches: {:?}",
resp.matches.len(),
resp.matches
.iter()
.take(5)
.map(|m| &m.completion)
.collect::<Vec<_>>(),
);
}
}