use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
use std::sync::{Mutex, OnceLock};
use std::time::{Duration, 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() + Duration::from_millis(200),
allow_exec: true,
}
}
pub fn new_with_budget(line: &'a str, cursor: usize, budget: Duration) -> Self {
Self {
line,
cursor,
deadline: Instant::now() + budget,
allow_exec: true,
}
}
}
#[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(dat: &crate::ported::zle::comp_h::Cadata, words: &[String]) -> bool {
use crate::ported::zle::comp_h::{CAF_ARRAYS, CAF_KEYS};
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,
};
if dat.opar.is_some() || dat.apar.is_some() || !dat.dpar.is_empty() {
return false;
}
let matches: Vec<String> = if (dat.aflags & CAF_ARRAYS) != 0 {
words
.iter()
.flat_map(|name| crate::ported::params::getaparam(name).unwrap_or_default())
.collect()
} else if (dat.aflags & CAF_KEYS) != 0 {
words
.iter()
.flat_map(|name| {
crate::ported::params::gethkparam(name)
.or_else(|| crate::ported::params::getaparam(name))
.unwrap_or_default()
})
.collect()
} else {
words.to_vec()
};
let descs: Vec<String> = dat
.disp
.as_deref()
.and_then(crate::ported::params::getaparam)
.unwrap_or_default();
if let Some(path) = std::env::var_os("ZSHRS_CAPDBG") {
use std::io::Write as _;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
let _ = writeln!(
f,
"capture disp={:?} exp={:?} group={:?} matches={:?} descs={:?}",
dat.disp,
dat.exp,
dat.group,
matches.iter().take(4).collect::<Vec<_>>(),
descs.iter().take(4).collect::<Vec<_>>(),
);
}
}
for (idx, m) in matches.iter().enumerate() {
buf.push(CompsysMatch {
completion: m.clone(),
description: descs.get(idx).cloned().or_else(|| dat.exp.clone()),
group: dat.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(()))
}
struct Job {
line: String,
cursor: usize,
allow_exec: bool,
deadline: Instant,
reply: SyncSender<CompsysResponse>,
}
static SHELL_READY: AtomicBool = AtomicBool::new(false);
static SHELL_BUSY: AtomicBool = AtomicBool::new(false);
static LAST_RESULT: Mutex<Option<(String, usize, Vec<CompsysMatch>, Instant)>> = Mutex::new(None);
const LAST_RESULT_TTL: Duration = Duration::from_secs(3);
static SHARD_ROWS: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn shard_rows() -> usize {
SHARD_ROWS.load(Ordering::SeqCst)
}
pub fn is_ready() -> bool {
SHELL_READY.load(Ordering::SeqCst)
}
pub fn bootstrap() {
let _ = shell_thread();
}
fn bootstrap_shell() {
let t0 = Instant::now();
let executor: &'static mut crate::vm_helper::ShellExecutor =
Box::leak(Box::new(crate::vm_helper::ShellExecutor::new()));
#[cfg(feature = "daemon")]
{
let rows = crate::canonical_apply::apply_all(executor);
SHARD_ROWS.store(rows, Ordering::SeqCst);
if rows == 0 {
tracing::warn!(
target: "zshrs::compsys::in_editor",
"no canonical rkyv shard applied — run `zshrs-recorder` for user fpath/compdef state",
);
}
}
crate::ported::exec::install_session_executor(executor);
SHELL_READY.store(true, Ordering::SeqCst);
tracing::info!(
target: "zshrs::compsys::in_editor",
shard_rows = SHARD_ROWS.load(Ordering::SeqCst),
elapsed_ms = t0.elapsed().as_millis() as u64,
"shell thread ready",
);
}
fn shell_thread() -> &'static SyncSender<Job> {
static TX: OnceLock<SyncSender<Job>> = OnceLock::new();
TX.get_or_init(|| {
let (tx, rx) = sync_channel::<Job>(4);
std::thread::Builder::new()
.name("zshrs-compsys-editor".to_string())
.spawn(move || shell_thread_main(rx))
.expect("spawn compsys editor shell thread");
tx
})
}
thread_local! {
static EXEC_POLICY: std::cell::Cell<Option<(bool, Instant)>> =
const { std::cell::Cell::new(None) };
}
fn set_exec_policy(allow_exec: bool, deadline: Instant) {
EXEC_POLICY.with(|c| c.set(Some((allow_exec, deadline))));
}
fn clear_exec_policy() {
EXEC_POLICY.with(|c| c.set(None));
}
pub fn exec_policy() -> Option<(bool, Instant)> {
EXEC_POLICY.with(|c| c.get())
}
fn shell_thread_main(rx: Receiver<Job>) {
bootstrap_shell();
for job in rx {
if Instant::now() >= job.deadline {
continue;
}
SHELL_BUSY.store(true, Ordering::SeqCst);
let resp = dispatch_on_shell_thread(&job);
SHELL_BUSY.store(false, Ordering::SeqCst);
if !resp.matches.is_empty() {
if let Ok(mut g) = LAST_RESULT.lock() {
*g = Some((
job.line.clone(),
job.cursor,
resp.matches.clone(),
Instant::now(),
));
}
}
let _ = job.reply.send(resp);
}
}
pub fn complete_at(req: CompsysRequest<'_>) -> CompsysResponse {
let tx = shell_thread();
if !is_ready() {
return CompsysResponse {
matches: Vec::new(),
is_incomplete: true,
};
}
if let Ok(mut g) = LAST_RESULT.lock() {
let hit = g
.as_ref()
.filter(|(line, cursor, _, at)| {
line == req.line && *cursor == req.cursor && at.elapsed() < LAST_RESULT_TTL
})
.map(|(_, _, matches, _)| matches.clone());
if let Some(matches) = hit {
*g = None;
return CompsysResponse {
matches,
is_incomplete: false,
};
}
}
let (reply_tx, reply_rx) = sync_channel::<CompsysResponse>(1);
let job = Job {
line: req.line.to_string(),
cursor: req.cursor,
allow_exec: req.allow_exec,
deadline: req.deadline,
reply: reply_tx,
};
if tx.try_send(job).is_err() {
return CompsysResponse {
matches: Vec::new(),
is_incomplete: true,
};
}
let wait = req
.deadline
.saturating_duration_since(Instant::now())
.saturating_add(Duration::from_millis(20));
match reply_rx.recv_timeout(wait) {
Ok(resp) => resp,
Err(_) => CompsysResponse {
matches: Vec::new(),
is_incomplete: true,
},
}
}
fn dispatch_on_shell_thread(job: &Job) -> CompsysResponse {
let _guard = complete_at_lock().lock().unwrap();
let req = CompsysRequest {
line: &job.line,
cursor: job.cursor,
deadline: job.deadline,
allow_exec: job.allow_exec,
};
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_ed_line = crate::ported::zle::zle_main::ZLELINE
.lock()
.map(|g| g.clone())
.unwrap_or_default();
let saved_ed_cs = crate::ported::zle::zle_main::ZLECS.load(std::sync::atomic::Ordering::SeqCst);
let saved_ed_ll = crate::ported::zle::zle_main::ZLELL.load(std::sync::atomic::Ordering::SeqCst);
let saved_curcontext = crate::ported::params::getsparam("curcontext");
let line_chars: Vec<char> = req.line.chars().collect();
let cursor_chars = req.line[..req.cursor.min(req.line.len())].chars().count();
let line_len_chars = line_chars.len();
if let Ok(mut g) = crate::ported::zle::zle_main::ZLELINE.lock() {
*g = line_chars;
}
crate::ported::zle::zle_main::ZLECS.store(cursor_chars, std::sync::atomic::Ordering::SeqCst);
crate::ported::zle::zle_main::ZLELL.store(line_len_chars, std::sync::atomic::Ordering::SeqCst);
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(cursor_chars as i32, std::sync::atomic::Ordering::SeqCst);
crate::ported::zle::compcore::ZLELL
.store(line_len_chars 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 saved_columns = crate::ported::params::getiparam("COLUMNS");
let saved_lines = crate::ported::params::getiparam("LINES");
let _ = crate::ported::params::setiparam("COLUMNS", 80);
let _ = crate::ported::params::setiparam("LINES", 24);
crate::ported::zle::zle_tricky::MENUCMP.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::compcore::OLDMENUCMP.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::zle_tricky::LASTAMBIG.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::zle_tricky::VALIDLIST.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::zle_tricky::SHOWAGAIN.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::zle_refresh::SHOWINGLIST.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::zle_refresh::LISTSHOWN.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::compcore::hasoldlist.store(0, std::sync::atomic::Ordering::Relaxed);
crate::ported::zle::compcore::lastpermmnum.store(0, std::sync::atomic::Ordering::Relaxed);
if let Some(m) = crate::ported::zle::compcore::MINFO.get() {
if let Ok(mut mi) = m.lock() {
mi.cur = None;
mi.group = None;
mi.cur_idx = 0;
mi.group_idx = 0;
}
}
let saved_compfunc = {
let g = crate::ported::zle::compcore::compfunc.get_or_init(|| Mutex::new(None));
let mut lock = g.lock().unwrap_or_else(|e| e.into_inner());
let prev = lock.clone();
*lock = Some("_main_complete".to_string());
prev
};
set_exec_policy(req.allow_exec, req.deadline);
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()
};
clear_exec_policy();
let _ = crate::ported::params::setiparam("COLUMNS", saved_columns);
let _ = crate::ported::params::setiparam("LINES", saved_lines);
{
let g = crate::ported::zle::compcore::compfunc.get_or_init(|| Mutex::new(None));
let mut lock = g.lock().unwrap_or_else(|e| e.into_inner());
*lock = saved_compfunc;
}
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);
if let Ok(mut g) = crate::ported::zle::zle_main::ZLELINE.lock() {
*g = saved_ed_line;
}
crate::ported::zle::zle_main::ZLECS.store(saved_ed_cs, std::sync::atomic::Ordering::SeqCst);
crate::ported::zle::zle_main::ZLELL.store(saved_ed_ll, std::sync::atomic::Ordering::SeqCst);
match saved_curcontext {
Some(v) => {
let _ = crate::ported::params::setsparam("curcontext", &v);
}
None => {
let _ = 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_exec_allowed_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 exec_policy_is_unset_outside_a_dispatch() {
assert!(exec_policy().is_none());
}
#[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<_>>(),
);
}
}