zsh/ported/zle/compctl.rs
1//! Port of `Src/Zle/compctl.c` — the legacy `compctl` builtin and its
2//! supporting completion machinery (predates compsys).
3//!
4//! Global matcher. // c:33
5//! Default completion infos // c:38
6//! Hash table for completion info for commands // c:43
7//! List of pattern compctls // c:48
8//! Main entry point for the `compctl' builtin // c:1558
9//!
10//! 4076 lines / 47 fns. This file ports the type definitions, constants,
11//! and simpler free fns first; large fns (`makecomplist*`, `bin_compctl`,
12//! `printcompctl`) are stubbed with C source-line citations and ported
13//! incrementally.
14//!
15//! Citations: every fn comment references `Src/Zle/compctl.c:<line>` so
16//! drift can be checked against the upstream snapshot.
17
18#![allow(dead_code)]
19#![allow(clippy::too_many_arguments)]
20
21use std::collections::HashMap;
22use std::sync::Arc;
23use std::sync::Mutex;
24
25// Re-export the canonical `compctl.h` ports from compctl_h.rs so
26// callers within compctl.rs reference the legit names. The four
27// types (Compctlp/Patcomp/Compcond/Compctl + CompcondData) are
28// direct ports of the C structs declared in Src/Zle/compctl.h.
29use crate::ported::zle::compctl_h::{
30
31
32 Compctl, Compcond, CompcondData, Patcomp, Compctlp,
33 CC_FILES, CC_COMMPATH, CC_REMOVE, CC_OPTIONS, CC_VARS, CC_BINDINGS,
34 CC_ARRAYS, CC_INTVARS, CC_SHFUNCS, CC_PARAMS, CC_ENVVARS, CC_JOBS,
35 CC_RUNNING, CC_STOPPED, CC_BUILTINS, CC_ALREG, CC_ALGLOB, CC_USERS,
36 CC_DISCMDS, CC_EXCMDS, CC_SCALARS, CC_READONLYS, CC_SPECIALS,
37 CC_DELETE, CC_NAMED, CC_QUOTEFLAG, CC_EXTCMDS, CC_RESWDS, CC_DIRS,
38 CC_EXPANDEXPL, CC_RESERVED,
39 CC_NOSORT, CC_XORCONT, CC_CCCONT, CC_PATCONT, CC_DEFCONT, CC_UNIQCON, CC_UNIQALL,
40 CCT_UNUSED, CCT_POS, CCT_CURSTR, CCT_CURPAT, CCT_WORDSTR, CCT_WORDPAT,
41 CCT_CURSUF, CCT_CURPRE, CCT_CURSUB, CCT_CURSUBC, CCT_NUMWORDS,
42 CCT_RANGESTR, CCT_RANGEPAT, CCT_QUOTE,
43};
44use crate::ported::zle::comp_h::Cmlist;
45use std::os::unix::fs::PermissionsExt;
46
47// --- AUTO: cross-zle hoisted-fn use glob ---
48#[allow(unused_imports)]
49#[allow(unused_imports)]
50use crate::ported::zle::zle_main::*;
51#[allow(unused_imports)]
52use crate::ported::zle::zle_misc::*;
53#[allow(unused_imports)]
54use crate::ported::zle::zle_hist::*;
55#[allow(unused_imports)]
56use crate::ported::zle::zle_move::*;
57#[allow(unused_imports)]
58use crate::ported::zle::zle_word::*;
59#[allow(unused_imports)]
60use crate::ported::zle::zle_params::*;
61#[allow(unused_imports)]
62use crate::ported::zle::zle_vi::*;
63#[allow(unused_imports)]
64use crate::ported::zle::zle_utils::*;
65#[allow(unused_imports)]
66use crate::ported::zle::zle_refresh::*;
67#[allow(unused_imports)]
68use crate::ported::zle::zle_tricky::*;
69#[allow(unused_imports)]
70use crate::ported::zle::textobjects::*;
71#[allow(unused_imports)]
72use crate::ported::zle::deltochar::*;
73
74// =====================================================================
75// COMP_* — `compctl` operation flags from `Src/Zle/compctl.c:53-60`.
76// Encode the command-line operation requested by `compctl`'s flag
77// arguments (`-L`, `-C`, `-D`, `-T`, `-M`).
78// =====================================================================
79
80/// Port of `COMP_LIST` from `Src/Zle/compctl.c:53`. `-L` flag — list
81/// existing compctl bindings.
82pub const COMP_LIST: i32 = 1 << 0; // c:53
83/// Port of `COMP_COMMAND` from `compctl.c:54`. `-C` — operate on the
84/// command-completion table.
85pub const COMP_COMMAND: i32 = 1 << 1; // c:54
86/// Port of `COMP_DEFAULT` from `compctl.c:55`. `-D` — operate on the
87/// default-completion entry.
88pub const COMP_DEFAULT: i32 = 1 << 2; // c:55
89/// Port of `COMP_FIRST` from `compctl.c:56`. `-T` — operate on the
90/// first-completion entry.
91pub const COMP_FIRST: i32 = 1 << 3; // c:56
92/// Port of `COMP_REMOVE` from `compctl.c:57`. `+` prefix or remove op.
93pub const COMP_REMOVE: i32 = 1 << 4; // c:57
94/// Port of `COMP_LISTMATCH` from `compctl.c:58`. `-L -M` combination.
95pub const COMP_LISTMATCH: i32 = 1 << 5; // c:58
96
97/// Port of `COMP_SPECIAL` from `compctl.c:60`. Mask covering all
98/// "special" entry-point flags.
99pub const COMP_SPECIAL: i32 = COMP_COMMAND | COMP_DEFAULT | COMP_FIRST; // c:60
100
101/// Port of `CFN_FIRST` from `compctl.c:1672`. Internal flag for
102/// `printcompctl` — skip the cc_first per-table override.
103pub const CFN_FIRST: i32 = 1; // c:1672
104/// Port of `CFN_DEFAULT` from `compctl.c:1673`. Skip cc_default.
105pub const CFN_DEFAULT: i32 = 2; // c:1673
106
107// =================================================================
108// Type definitions — port of Src/Zle/compctl.h:32-115
109// =================================================================
110
111// Compcond/CompcondData/Compctl/Patcomp/Compctlp ported in
112// compctl_h.rs (Src/Zle/compctl.h:39-115). Imported above.
113
114// =================================================================
115// Globals — port of Src/Zle/compctl.c:36-66
116// =================================================================
117
118/// Global cmatcher list. Port of file-static `Cmlist cmatcher;` at
119/// Src/Zle/compctl.c:36. Bucket-2 user-registered registry per
120/// PORT_PLAN.md — `compctl -M` writes via `freecmlist + cpcmlist`,
121/// every completion call reads. `RwLock` lets parallel completion
122/// reads proceed without serialising on a mutex.
123pub(crate) static CMATCHER:
124 std::sync::RwLock<Option<Box<crate::ported::zle::comp_h::Cmlist>>> =
125 std::sync::RwLock::new(None); // c:36
126
127/// `compctltab` hash table — name → Compctl.
128/// Port of `HashTable compctltab;` at Src/Zle/compctl.c:46.
129/// Bucket-2 user-registered registry: `compctl name args` writes,
130/// every completion call reads. `RwLock` per PORT_PLAN.md.
131static COMPCTL_TAB: std::sync::RwLock<Option<HashMap<String, Arc<Compctl>>>>
132 = std::sync::RwLock::new(None);
133
134/// Pattern-compctl list. Port of `Patcomp patcomps;` at
135/// Src/Zle/compctl.c:51. Bucket-2 user-registered registry:
136/// `compctl -p` writes, every pattern-completion call reads.
137/// `RwLock` per PORT_PLAN.md.
138static PATCOMPS: std::sync::RwLock<Vec<(String, Arc<Compctl>)>>
139 = std::sync::RwLock::new(Vec::new());
140
141// `cclist` — flag for listing/command/default/first completion.
142// Port of file-static `int cclist;` at Src/Zle/compctl.c:63.
143// Bucket-1 per PORT_PLAN.md — per-completion-call scratch state,
144// thread_local so concurrent completion invocations don't race.
145thread_local! {
146 static CCLIST: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
147}
148
149// `showmask` — mask determining what to print.
150// Port of file-static `unsigned long showmask;` at Src/Zle/compctl.c:66.
151// Bucket-1 per PORT_PLAN.md.
152thread_local! {
153 static SHOWMASK: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
154}
155
156// =================================================================
157// Free fns — start of compctl.c proper
158// =================================================================
159
160/// Initialize the `compctltab` hash table.
161/// Port of `createcompctltable()` from Src/Zle/compctl.c:70. The C
162/// version wires hash function pointers (hasher, addnode, getnode,
163/// printnode, freenode); Rust uses a plain HashMap so the wiring
164/// reduces to allocation.
165pub(crate) fn createcompctltable() {
166 let mut g = COMPCTL_TAB.write().unwrap();
167 *g = Some(HashMap::new());
168 let mut p = PATCOMPS.write().unwrap();
169 p.clear();
170}
171
172/// Free a `compctlp` hash node.
173/// Port of `freecompctlp(HashNode hn)` from Src/Zle/compctl.c:92. Rust's Arc
174/// drop handles the inner Compctl free; this is the entry the C
175/// hash table calls back when removing a node.
176/// WARNING: param names don't match C — Rust=() vs C=(hn)
177pub(crate) fn freecompctlp(name: &str) {
178 let mut g = COMPCTL_TAB.write().unwrap();
179 if let Some(map) = g.as_mut() {
180 map.remove(name);
181 }
182}
183
184/// Free a `compctl` spec.
185/// Port of `freecompctl(Compctl cc)` from Src/Zle/compctl.c:103. C uses
186/// reference counting + manual `zsfree` of every string member +
187/// recursive free of `ext`/`xor` chains. Rust's Arc handles this
188/// automatically when the last reference drops.
189/// WARNING: param names don't match C — Rust=() vs C=(cc)
190pub(crate) fn freecompctl(_cc: Arc<Compctl>) {
191 // Arc::drop recursively frees the spec when refcount hits zero.
192 // Direct port of compctl.c:104-141 — the C ladder of `zsfree(...)`
193 // calls is the equivalent of letting the Arc/String values drop.
194}
195
196/// Free a `compcond` spec.
197/// Port of `freecompcond(void *a)` from Src/Zle/compctl.c:146. C walks the
198/// or/and chain, freeing per-type union data. Rust's enum + Box
199/// drop the chain automatically; this is the entry kept for ABI
200/// parity with the C source.
201/// WARNING: param names don't match C — Rust=() vs C=(a)
202pub(crate) fn freecompcond(_cc: Compcond) {
203 // Drop handles the chain — direct equivalent of compctl.c:148-186.
204}
205
206/// Direct port of `static Cmlist cpcmlist(Cmlist l)` from
207/// Src/Zle/compctl.c:291. Deep-copies a Cmlist linked list, using
208/// `cpcmatcher` for each matcher's chain. Returns the new head.
209pub(crate) fn cpcmlist( // c:291
210 mut l: Option<&crate::ported::zle::comp_h::Cmlist>,
211) -> Option<Box<crate::ported::zle::comp_h::Cmlist>> {
212 let mut head: Option<Box<Cmlist>> = None; // c:293 r = NULL
213 let mut tail_ref: *mut Option<Box<Cmlist>> = &mut head;
214 while let Some(src) = l { // c:295 while (l)
215 let matcher_chain = crate::ported::zle::complete::cpcmatcher( // c:298 cpcmatcher
216 Some(&*src.matcher),
217 ).expect("cpcmatcher returned None for non-null source");
218 let n = Box::new(Cmlist { // c:296 zalloc
219 next: None, // c:297
220 matcher: matcher_chain, // c:298
221 str: src.str.clone(), // c:299 ztrdup
222 });
223 unsafe {
224 *tail_ref = Some(n);
225 if let Some(ref mut newnode) = *tail_ref { // c:301 p = &(n->next)
226 tail_ref = &mut newnode.next as *mut _;
227 }
228 }
229 l = src.next.as_deref(); // c:311 l = l->next
230 }
231 head // c:311 return r
232}
233
234/// Direct port of `static int set_gmatcher(char *name, char **argv)` from
235/// Src/Zle/compctl.c:311. Parses each argv entry as a cmatcher
236/// spec, builds a fresh Cmlist chain, frees the old CMATCHER and
237/// installs the new one via cpcmlist.
238pub(crate) fn set_gmatcher(name: &str, argv: &[String]) -> i32 { // c:311
239 let mut head: Option<Box<Cmlist>> = None; // c:314 l = NULL
240 let mut tail_ref: *mut Option<Box<Cmlist>> = &mut head;
241 for word in argv { // c:317 while (*argv)
242 let m = match crate::ported::zle::complete::parse_cmatcher(name, word) {
243 Some(m) => m, // c:319 parse_cmatcher
244 None => return 1, // c:319 == pcm_err
245 };
246 let n = Box::new(Cmlist { // c:320 zhalloc
247 next: None, // c:321
248 matcher: m, // c:322
249 str: word.clone(), // c:323
250 });
251 unsafe {
252 *tail_ref = Some(n);
253 if let Some(ref mut newnode) = *tail_ref { // c:325
254 tail_ref = &mut newnode.next as *mut _;
255 }
256 }
257 }
258 // freecmlist(cmatcher) — Drop on the Box handles the C free path. // c:336
259 let new_list = cpcmlist(head.as_deref()); // c:336 cpcmlist(l)
260 if let Ok(mut guard) = CMATCHER.write() {
261 *guard = new_list;
262 }
263 1 // c:336
264}
265
266/// Direct port of `static int get_gmatcher(char *name, char **argv)` from
267/// Src/Zle/compctl.c:336. Looks for a leading `-M` flag followed
268/// by matcher specs (no `-`-prefixed args), then forwards to
269/// `set_gmatcher` and translates its return into 0/1/2.
270pub(crate) fn get_gmatcher(name: &str, argv: &[String]) -> i32 { // c:336
271 if argv.first().map(|s| s.as_str()) != Some("-M") { // c:336
272 return 0; // c:349
273 }
274 let rest = &argv[1..]; // c:339 p = ++argv
275 for w in rest { // c:341 while (*p)
276 if w.starts_with('-') { // c:342
277 return 0; // c:357
278 }
279 }
280 if set_gmatcher(name, rest) != 0 { // c:357
281 return 2; // c:357
282 }
283 1 // c:357
284}
285
286/// Print a global matcher. Stub.
287/// Port of `print_gmatcher(int ac)` from Src/Zle/compctl.c:357.
288/// WARNING: param names don't match C — Rust=() vs C=(next)
289pub(crate) fn print_gmatcher(_ac: i32) {}
290
291/// Get a compctl from arg vector — main compctl-spec parser.
292/// Port of `get_compctl(char *name, char ***av, Compctl cc, int first, int isdef, int cl)` from Src/Zle/compctl.c:377 (~600 lines).
293///
294/// Walks `argv` letter-by-letter, applying flag bits to `cc.mask` /
295/// `cc.mask2` and capturing the string args (`-K func`, `-X expl`,
296/// `-P prefix`, `-S suffix`, `-g glob`, `-s str`, etc.).
297///
298/// Returns 0 on success, 1 on parse error. On success, advances the
299/// caller's argv past the consumed flags via `*av_idx` mutation.
300///
301/// Currently implements the simple-flag-char arms (per-char →
302/// mask bit) from compctl.c:418-508 and the simple arg-taking
303/// flags. The complex arms (`-x` extended condition, `-M` matcher,
304/// `-+` chains, `-t` retry spec) are left as placeholders pending
305/// per-arm follow-up.
306pub(crate) fn get_compctl(
307 name: &str,
308 av: &mut Vec<String>,
309 cc: &mut Compctl,
310 first: bool,
311 mut isdef: bool,
312 cl: i32,
313) -> i32 {
314 // C: `argv = *av;` — alias the caller's array.
315 let mut i: usize = 0;
316 let hx = false;
317 let mut cclist_local = CCLIST.with(|c| c.get());
318 cc.mask2 = CC_CCCONT; // c:407
319
320 // C: `compctl + foo ...` becomes default — c:392-404
321 if first
322 && i < av.len()
323 && av[i] == "+"
324 && !(i + 1 < av.len() && av[i + 1].starts_with('-') && av[i + 1].len() > 1)
325 {
326 i += 1;
327 if i < av.len() && av[i].starts_with('-') {
328 i += 1;
329 }
330 av.drain(0..i);
331 if cl != 0 {
332 return 1;
333 } else {
334 CCLIST.with(|c| c.set(COMP_REMOVE));
335 return 0;
336 }
337 }
338
339 // Loop through the flags. C: c:412 `for (; !ready && argv[0] && argv[0][0] == '-' && (argv[0][1] || !first); )`
340 let mut ready = false;
341 while !ready
342 && i < av.len()
343 && av[i].starts_with('-')
344 && (av[i].len() > 1 || !first)
345 {
346 // C: bare `-` becomes `-+` to absorb the next iter — c:413-414
347 if av[i].len() == 1 {
348 av[i] = "-+".to_string();
349 }
350 // Walk chars after the `-`. C: `while (!ready && *++(*argv))`
351 let arg = av[i].clone();
352 let chars: Vec<char> = arg.chars().skip(1).collect();
353 let mut consumed = false;
354 for c in chars {
355 if ready { break; }
356 // Simple-flag-char dispatch — direct port of the
357 // switch at c:418-508.
358 match c {
359 'f' => cc.mask |= CC_FILES, // c:419
360 'c' => cc.mask |= CC_COMMPATH, // c:422
361 'm' => cc.mask |= CC_EXTCMDS, // c:425
362 'w' => cc.mask |= CC_RESWDS, // c:428
363 'o' => cc.mask |= CC_OPTIONS, // c:431
364 'v' => cc.mask |= CC_VARS, // c:434
365 'b' => cc.mask |= CC_BINDINGS, // c:437
366 'A' => cc.mask |= CC_ARRAYS, // c:440
367 'I' => cc.mask |= CC_INTVARS, // c:443
368 'F' => cc.mask |= CC_SHFUNCS, // c:446
369 'p' => cc.mask |= CC_PARAMS, // c:449
370 'E' => cc.mask |= CC_ENVVARS, // c:452
371 'j' => cc.mask |= CC_JOBS, // c:455
372 'r' => cc.mask |= CC_RUNNING, // c:458
373 'z' => cc.mask |= CC_STOPPED, // c:461
374 'B' => cc.mask |= CC_BUILTINS, // c:464
375 'a' => cc.mask |= CC_ALREG | CC_ALGLOB, // c:467
376 'R' => cc.mask |= CC_ALREG, // c:470
377 'G' => cc.mask |= CC_ALGLOB, // c:473
378 'u' => cc.mask |= CC_USERS, // c:476
379 'd' => cc.mask |= CC_DISCMDS, // c:479
380 'e' => cc.mask |= CC_EXCMDS, // c:482
381 'N' => cc.mask |= CC_SCALARS, // c:485
382 'O' => cc.mask |= CC_READONLYS, // c:488
383 'Z' => cc.mask |= CC_SPECIALS, // c:491
384 'q' => cc.mask |= CC_REMOVE, // c:494
385 'U' => cc.mask |= CC_DELETE, // c:497
386 'n' => cc.mask |= CC_NAMED, // c:500
387 'Q' => cc.mask |= CC_QUOTEFLAG, // c:503
388 '/' => cc.mask |= CC_DIRS, // c:506
389 '1' => { // c:722
390 cc.mask2 |= CC_UNIQALL;
391 cc.mask2 &= !CC_UNIQCON;
392 }
393 '2' => { // c:726
394 cc.mask2 |= CC_UNIQCON;
395 cc.mask2 &= !CC_UNIQALL;
396 }
397 'C' => { // c:777
398 if cl != 0 {
399 eprintln!("{}: illegal option -{}", name, c);
400 return 1;
401 }
402 if first && !hx {
403 cclist_local |= COMP_COMMAND;
404 } else {
405 eprintln!("{}: misplaced command completion (-C) flag", name);
406 return 1;
407 }
408 }
409 'D' => { // c:789
410 if cl != 0 {
411 eprintln!("{}: illegal option -{}", name, c);
412 return 1;
413 }
414 if first && !hx {
415 isdef = true;
416 cclist_local |= COMP_DEFAULT;
417 } else {
418 eprintln!("{}: misplaced default completion (-D) flag", name);
419 return 1;
420 }
421 }
422 'T' => { // c:802
423 if cl != 0 {
424 eprintln!("{}: illegal option -{}", name, c);
425 return 1;
426 }
427 if first && !hx {
428 cclist_local |= COMP_FIRST;
429 } else {
430 eprintln!("{}: misplaced first completion (-T) flag", name);
431 return 1;
432 }
433 }
434 'L' => { // c:814
435 if cl != 0 {
436 eprintln!("{}: illegal option -{}", name, c);
437 return 1;
438 }
439 if !first || hx {
440 eprintln!("{}: illegal use of -L flag", name);
441 return 1;
442 }
443 cclist_local |= COMP_LIST;
444 }
445 '+' => { // c:850 (xor chain marker)
446 // Marks end of this compctl spec; remainder is
447 // the next xor'd compctl. Stop the loop here;
448 // the caller iterates again for the xor chain.
449 ready = true;
450 consumed = true;
451 break;
452 }
453 _ => {
454 // Arg-taking flags + unknown — bail to the
455 // post-loop handler. These are c:509+ (`t` retry,
456 // `k` keyvar, `K` func, `Y`/`X` explain, `y`
457 // ylist, `P`/`S` prefix/suffix, `g` glob, `s`
458 // str, `l`/`h` subcmd/substr, `W` withd, `J`/`V`
459 // gname, `M` matcher, `H` history, `x` extended).
460 // For now, if the arg-taking char is followed by
461 // no body, consume one extra argv slot as the
462 // arg. Else ignore. Real impls land per-flag.
463 let (has_inline, inline_val) = (
464 arg.len() > 2 && arg.chars().nth(1) == Some(c),
465 if arg.len() > 2 { arg[2..].to_string() } else { String::new() },
466 );
467 let mut val: Option<String> = None;
468 if has_inline {
469 val = Some(inline_val);
470 } else if i + 1 < av.len() {
471 val = Some(av[i + 1].clone());
472 i += 1;
473 }
474 match c {
475 'k' => cc.keyvar = val, // c:553
476 'K' => cc.func = val, // c:565
477 'Y' => { // c:577
478 cc.mask |= CC_EXPANDEXPL;
479 cc.explain = val;
480 }
481 'X' => { // c:580
482 cc.mask &= !CC_EXPANDEXPL;
483 cc.explain = val;
484 }
485 'y' => cc.ylist = val, // c:594
486 'P' => cc.prefix = val, // c:606
487 'S' => cc.suffix = val, // c:618
488 'g' => cc.glob = val, // c:630
489 's' => cc.str = val, // c:642
490 'l' => cc.subcmd = val, // c:655
491 'h' => cc.substr = val, // c:670
492 'W' => cc.withd = val, // c:685
493 'J' => cc.gname = val, // c:697
494 'V' => { // c:709
495 cc.gname = val;
496 cc.mask2 |= CC_NOSORT;
497 }
498 'M' => { // c:730
499 // Matcher spec — full parse needs
500 // `parse_cmatcher` (Src/Zle/compmatch.c).
501 // For now, store the raw string.
502 if let Some(s) = val {
503 cc.mstr = Some(s);
504 }
505 }
506 'H' => { // c:757
507 // -H N PAT — number + pattern. The
508 // simple-flag walker consumed N as `val`;
509 // the next argv is PAT.
510 if let Some(s) = val {
511 cc.hnum = s.parse::<i32>().unwrap_or(0).max(0);
512 }
513 if i + 1 < av.len() {
514 cc.hpat = Some(av[i + 1].clone());
515 if cc.hpat.as_deref() == Some("*") {
516 cc.hpat = Some(String::new());
517 }
518 i += 1;
519 }
520 }
521 't' => { // c:509 retry spec
522 // `-t {+|n|-|x}` controls continuation.
523 // Direct port of the switch at c:528-545.
524 if let Some(s) = val {
525 let bit = match s.as_str() {
526 "+" => CC_XORCONT,
527 "n" => 0,
528 "-" => CC_PATCONT,
529 "x" => CC_DEFCONT,
530 _ => {
531 eprintln!("{}: invalid retry specification character `{}`", name, s);
532 return 1;
533 }
534 };
535 cc.mask2 = bit;
536 }
537 }
538 _ => {
539 eprintln!("{}: unknown compctl flag `-{}`", name, c);
540 return 1;
541 }
542 }
543 consumed = true;
544 break;
545 }
546 }
547 }
548 i += 1;
549 if !consumed {
550 // Pure simple-flag arg — already advanced.
551 }
552 }
553
554 // C: c:1582 — push the parsed cct into the caller's slot.
555 av.drain(0..i);
556 let _ = isdef;
557 CCLIST.with(|c| c.set(cclist_local));
558 0
559}
560
561/// Parse the `-x` extended-condition compctl form.
562/// Port of `get_xcompctl(char *name, char ***av, Compctl cc, int isdef)` from Src/Zle/compctl.c:909 (~260 lines).
563///
564/// C signature: `int get_xcompctl(char *name, char ***av, Compctl cc,
565/// int isdef)`. Walks the per-condition syntax `s[…][…], p[…]` …
566/// and chains them as Compcond entries on `cc.ext`. Each `case`
567/// letter dispatches to one CCT_* type (`s`→CURSUF, `p`→POS, etc.),
568/// then the `[…]` argument syntax is parsed per-type.
569///
570/// Inside the `[]`, the C source uses temporary lexer-style markers
571/// `\200` (CCT_END) and `\201` (CCT_AND) to mark the active `]`/`,`
572/// boundaries — Rust uses Vec splits instead.
573///
574/// Returns 0 on success, 1 on parse error. Advances `*av` past the
575/// consumed conditions.
576pub(crate) fn get_xcompctl(
577 name: &str,
578 av: &mut Vec<String>,
579 cc: &mut Compctl,
580 isdef: bool,
581) -> i32 {
582 let mut ready = false;
583 let mut next_chain: Vec<Arc<Compctl>> = Vec::new();
584
585 while !ready {
586 // C: c:920 — `o = m = c = (Compcond) zshcalloc(...)`
587 // o tracks or-chain head, m tracks first cond (root), c tracks
588 // current cond being parsed.
589 let mut head: Compcond = Compcond::default();
590 let mut current_or = &mut head as *mut Compcond;
591
592 // C: c:922 — `for (t = *argv; *t;)` walk one argv slot
593 if av.is_empty() {
594 // C: c:1150 — missing args
595 eprintln!("{}: missing command names", name);
596 return 1;
597 }
598 let arg = av[0].clone();
599 let bytes: Vec<char> = arg.chars().collect();
600 let mut t = 0_usize;
601 let mut current_and: Option<*mut Compcond> = None;
602
603 while t < bytes.len() {
604 // Skip leading spaces — c:923-924
605 while t < bytes.len() && bytes[t] == ' ' {
606 t += 1;
607 }
608 if t >= bytes.len() { break; }
609
610 // C: c:926-972 — switch on condition code char
611 let typ = match bytes[t] {
612 'q' => CCT_QUOTE, // c:927
613 's' => CCT_CURSUF, // c:930
614 'S' => CCT_CURPRE, // c:933
615 'p' => CCT_POS, // c:936
616 'c' => CCT_CURSTR, // c:939
617 'C' => CCT_CURPAT, // c:942
618 'w' => CCT_WORDSTR, // c:945
619 'W' => CCT_WORDPAT, // c:948
620 'n' => CCT_CURSUB, // c:951
621 'N' => CCT_CURSUBC, // c:954
622 'm' => CCT_NUMWORDS, // c:957
623 'r' => CCT_RANGESTR, // c:960
624 'R' => CCT_RANGEPAT, // c:963
625 _ => {
626 eprintln!("{}: unknown condition code: {}", name, bytes[t]);
627 return 1;
628 }
629 };
630
631 // C: c:974 — must be followed by `[`
632 if t + 1 >= bytes.len() || bytes[t + 1] != '[' {
633 eprintln!("{}: expected condition after condition code: {}", name, bytes[t]);
634 return 1;
635 }
636 t += 1;
637
638 // C: c:985-997 — count `[…][…]` blocks (n = arity).
639 // Walk balanced brackets, collecting bodies.
640 let mut bodies: Vec<String> = Vec::new();
641 while t < bytes.len() && bytes[t] == '[' {
642 t += 1; // skip `[`
643 // skip leading spaces inside brackets — c:1028
644 while t < bytes.len() && bytes[t] == ' ' { t += 1; }
645 let body_start = t;
646 let mut depth = 1_i32;
647 while t < bytes.len() && depth > 0 {
648 if bytes[t] == '\\' && t + 1 < bytes.len() {
649 t += 2;
650 continue;
651 }
652 if bytes[t] == '[' { depth += 1; }
653 else if bytes[t] == ']' { depth -= 1; if depth == 0 { break; } }
654 t += 1;
655 }
656 if t >= bytes.len() {
657 eprintln!("{}: error after condition code", name);
658 return 1;
659 }
660 let body: String = bytes[body_start..t].iter().collect();
661 bodies.push(body);
662 t += 1; // skip `]`
663 }
664 let n = bodies.len() as i32;
665
666 // C: c:1009-1025 — allocate per-type data, dispatch parse.
667 let data = match typ {
668 t if t == CCT_POS || t == CCT_NUMWORDS => {
669 // c:1030-1054 — one or two ints per body.
670 let mut a: Vec<i32> = Vec::with_capacity(n as usize);
671 let mut b: Vec<i32> = Vec::with_capacity(n as usize);
672 for body in &bodies {
673 // body shape: "N" or "N,M"
674 let parts: Vec<&str> = body.splitn(2, ',').collect();
675 let av_n: i32 = parts[0].trim().parse().unwrap_or(0);
676 let bv_n: i32 = if parts.len() == 2 {
677 parts[1].trim().parse().unwrap_or(0)
678 } else {
679 av_n // c:1042 — single arg → b copies a
680 };
681 a.push(av_n);
682 b.push(bv_n);
683 }
684 CompcondData::R { a, b }
685 }
686 t if t == CCT_CURSUF || t == CCT_CURPRE || t == CCT_QUOTE => {
687 // c:1056-1069 — single string per body.
688 let s: Vec<String> = bodies.iter().cloned().collect();
689 let p: Vec<i32> = vec![0; s.len()];
690 CompcondData::S { p, s }
691 }
692 t if t == CCT_RANGESTR || t == CCT_RANGEPAT => {
693 // c:1070-1099 — two strings per body, comma-separated.
694 let mut a: Vec<String> = Vec::with_capacity(n as usize);
695 let mut b: Vec<String> = Vec::with_capacity(n as usize);
696 for body in &bodies {
697 let parts: Vec<&str> = body.splitn(2, ',').collect();
698 a.push(parts[0].to_string());
699 b.push(parts.get(1).map(|s| s.to_string()).unwrap_or_default());
700 }
701 CompcondData::L { a, b }
702 }
703 _ => {
704 // c:1100-1121 — number followed by string per body.
705 let mut p: Vec<i32> = Vec::with_capacity(n as usize);
706 let mut s: Vec<String> = Vec::with_capacity(n as usize);
707 for body in &bodies {
708 let parts: Vec<&str> = body.splitn(2, ',').collect();
709 if parts.len() != 2 {
710 eprintln!("{}: error in condition", name);
711 return 1;
712 }
713 p.push(parts[0].trim().parse().unwrap_or(0));
714 s.push(parts[1].to_string());
715 }
716 CompcondData::S { p, s }
717 }
718 };
719
720 // Fill the current condition node.
721 // SAFETY: current_or points to either head (stack) or a
722 // Box<Compcond> we control via current_and chain.
723 unsafe {
724 let cur = match current_and {
725 Some(p) => p,
726 None => current_or,
727 };
728 (*cur).typ = typ;
729 (*cur).n = n;
730 (*cur).u = data;
731 }
732
733 // Skip trailing spaces — c:1123
734 while t < bytes.len() && bytes[t] == ' ' { t += 1; }
735
736 // C: c:1125-1134 — `,` → or-chain, else and-chain
737 if t < bytes.len() && bytes[t] == ',' {
738 let new_node = Box::new(Compcond::default());
739 let new_ptr = Box::into_raw(new_node);
740 unsafe {
741 let cur = current_and.unwrap_or(current_or);
742 (*cur).or = Some(Box::from_raw(new_ptr));
743 current_or = (*cur).or.as_mut().unwrap().as_mut() as *mut Compcond;
744 }
745 current_and = None;
746 t += 1;
747 } else if t < bytes.len() {
748 let new_node = Box::new(Compcond::default());
749 let new_ptr = Box::into_raw(new_node);
750 unsafe {
751 let cur = current_and.unwrap_or(current_or);
752 (*cur).and = Some(Box::from_raw(new_ptr));
753 current_and = Some((*cur).and.as_mut().unwrap().as_mut() as *mut Compcond);
754 }
755 }
756 }
757
758 // C: c:1137-1142 — assign condition to a fresh compctl on
759 // the chain, parse the flags that follow.
760 let mut next_cc = Compctl::default();
761 next_cc.cond = Some(Box::new(head));
762 // Drop the consumed argv slot.
763 av.remove(0);
764 if get_compctl(name, av, &mut next_cc, false, isdef, 0) != 0 {
765 return 1;
766 }
767 next_chain.push(Arc::new(next_cc));
768
769 // C: c:1143-1145 — special target → finished
770 let cclist = CCLIST.with(|c| c.get());
771 if (av.is_empty()) && (cclist & COMP_SPECIAL) != 0 {
772 ready = true;
773 continue;
774 }
775
776 // C: c:1150-1162 — look for next `-` flag block or `--` term
777 if av.is_empty()
778 || !av[0].starts_with('-')
779 || (av[0].len() == 1 && av.len() < 2)
780 {
781 eprintln!("{}: missing command names", name);
782 return 1;
783 }
784 if av[0] == "--" {
785 ready = true;
786 } else if av[0] == "-+" && av.len() >= 2 && av[1] == "--" {
787 ready = true;
788 av.remove(0);
789 }
790 av.remove(0);
791 }
792
793 // C: c:1167-1168 — install the chain on cc.ext.
794 if let Some(first) = next_chain.into_iter().next() {
795 cc.ext = Some(first);
796 }
797 0
798}
799
800/// Copy fields from `cct` into the spec stored at `name`.
801/// Port of `cc_assign(char *name, Compctl *ccptr, Compctl cct, int reass)` from Src/Zle/compctl.c:1174 (~75 lines).
802///
803/// C semantics: with `reass=true`, the special targets
804/// (cc_compos / cc_default / cc_first) are reassigned via
805/// `cc_reassign` which strips the prior `ext`/`xor` chains while
806/// preserving the static storage. Then every string field is
807/// `zsfree`d on the old spec and `ztrdup`d from `cct` into the new
808/// slot. Rust's Arc<Compctl> handles drop refcounting; this fn
809/// installs `cct` directly under `name` in the hash table.
810///
811/// The reass=true case for the special targets currently routes
812/// through the same install path — the static-storage distinction
813/// in C is a memory-model detail that doesn't transfer to Rust's
814/// Arc-based ownership.
815pub(crate) fn cc_assign(name: &str, cct: Arc<Compctl>, reass: bool) {
816 let cclist = CCLIST.with(|c| c.get());
817 if reass && (cclist & COMP_LIST) == 0 {
818 // C: c:1182-1188 — reject conflicting special targets
819 let conflicts = cclist == (COMP_COMMAND | COMP_DEFAULT)
820 || cclist == (COMP_COMMAND | COMP_FIRST)
821 || cclist == (COMP_DEFAULT | COMP_FIRST)
822 || cclist == COMP_SPECIAL;
823 if conflicts {
824 eprintln!("{}: can't set -D, -T, and -C simultaneously", name);
825 return;
826 }
827 // C: c:1190-1202 — reassign special target. The COMMAND /
828 // DEFAULT / FIRST cases install under reserved names. The
829 // C statics cc_compos / cc_default / cc_first map to these
830 // reserved keys in zshrs's table.
831 if (cclist & COMP_COMMAND) != 0 {
832 let _ = cc_reassign(cct.clone());
833 let mut g = COMPCTL_TAB.write().unwrap();
834 if g.is_none() { *g = Some(HashMap::new()); }
835 if let Some(map) = g.as_mut() {
836 map.insert("__cc_compos".to_string(), cct);
837 }
838 return;
839 }
840 if (cclist & COMP_DEFAULT) != 0 {
841 let _ = cc_reassign(cct.clone());
842 let mut g = COMPCTL_TAB.write().unwrap();
843 if g.is_none() { *g = Some(HashMap::new()); }
844 if let Some(map) = g.as_mut() {
845 map.insert("__cc_default".to_string(), cct);
846 }
847 return;
848 }
849 if (cclist & COMP_FIRST) != 0 {
850 let _ = cc_reassign(cct.clone());
851 let mut g = COMPCTL_TAB.write().unwrap();
852 if g.is_none() { *g = Some(HashMap::new()); }
853 if let Some(map) = g.as_mut() {
854 map.insert("__cc_first".to_string(), cct);
855 }
856 return;
857 }
858 }
859 // C: c:1205-1247 — Rust's Arc replaces the manual zsfree/ztrdup
860 // ladder. The new spec is installed under `name`; the prior
861 // entry (if any) drops its refcount when this insert overwrites.
862 let mut g = COMPCTL_TAB.write().unwrap();
863 if g.is_none() { *g = Some(HashMap::new()); }
864 if let Some(map) = g.as_mut() {
865 map.insert(name.to_string(), cct);
866 }
867}
868
869/// Free a special-target compctl's chain while preserving its slot.
870/// Port of `cc_reassign(Compctl cc)` from Src/Zle/compctl.c:1253.
871///
872/// C semantics: builds a temporary Compctl carrying `cc->xor` /
873/// `cc->ext`, sets refc=1, calls `freecompctl` on it (which
874/// recursively frees those chains), then nulls them on `cc`. This
875/// is needed because cc_compos / cc_default / cc_first are static
876/// allocations that can't themselves be freed — only their chains.
877///
878/// Rust's Arc handles refcounting. Returning a fresh empty Compctl
879/// matches the "free the chain, keep the storage" semantic by
880/// dropping the input cc's ext/xor refcounts and giving the caller
881/// a placeholder.
882/// WARNING: param names don't match C — Rust=() vs C=(cc)
883pub(crate) fn cc_reassign(_cc: Arc<Compctl>) -> Arc<Compctl> {
884 // Arc drop on the input cc handles the C `freecompctl(c2)` call —
885 // when refcount hits zero, ext/xor chains drop too. Return an
886 // empty placeholder for the caller to populate.
887 Arc::new(Compctl::default())
888}
889
890/// Test whether the given string is a pattern.
891/// Port of `compctl_name_pat(char **p)` from Src/Zle/compctl.c:1275.
892///
893/// C signature: `int compctl_name_pat(char **p)` — returns 1 if `*p`
894/// contains glob wildcards (after `tokenize` + `remnulargs`); also
895/// rewrites `*p` either to the tokenized form (pattern) or with
896/// backslashes removed (literal). Rust port: returns `(is_pattern,
897/// new_text)` tuple since we can't mutate a `&str` in-place.
898///
899/// Pattern detection: the C `haswilds()` checks for the lexer's
900/// glob-meta tokens (Star, Quest, Inbrack, etc.). Since the input
901/// here is plain user-typed text, we approximate by checking for
902/// the literal `*`/`?`/`[` characters.
903/// WARNING: param names don't match C — Rust=() vs C=(p)
904pub(crate) fn compctl_name_pat(p: &str) -> (bool, String) {
905 // C: c:1282 `if (haswilds(s))` — has glob metas
906 let has_glob = p.chars().any(|c| matches!(c, '*' | '?' | '['));
907 if has_glob {
908 // C: c:1283 `*p = s` — keep the (tokenized) pattern as-is.
909 // Rust: return the original; caller treats as pattern.
910 (true, p.to_string())
911 } else {
912 // C: c:1286 `*p = rembslash(*p)` — strip backslashes from
913 // literal text (`\X` → `X`).
914 let mut out = String::with_capacity(p.len());
915 let mut chars = p.chars().peekable();
916 while let Some(c) = chars.next() {
917 if c == '\\' {
918 if let Some(&nx) = chars.peek() {
919 out.push(nx);
920 chars.next();
921 continue;
922 }
923 }
924 out.push(c);
925 }
926 (false, out)
927 }
928}
929
930/// Delete a pattern compctl by name.
931/// Port of `delpatcomp(char *n)` from Src/Zle/compctl.c:1294. Walks the
932/// patcomps list, removes the entry matching `n`, frees the cc.
933/// Rust's Vec::retain handles the linked-list-style removal.
934/// WARNING: param names don't match C — Rust=() vs C=(n)
935pub(crate) fn delpatcomp(n: &str) {
936 let mut p = PATCOMPS.write().unwrap();
937 p.retain(|(pat, _)| pat != n);
938}
939
940/// Process the parsed compctl into the table.
941/// Port of `compctl_process_cc(char **s, Compctl cc)` from Src/Zle/compctl.c:1315 —
942/// installs the spec into compctltab (or patcomps for `-p PAT`),
943/// or removes entries when COMP_REMOVE is set (the `-` flag).
944/// WARNING: param names don't match C — Rust=(cc) vs C=(s, cc)
945pub(crate) fn compctl_process_cc(s: &[String], cc: Arc<Compctl>) -> i32 {
946 let cclist = CCLIST.with(|c| c.get());
947 if (cclist & COMP_REMOVE) != 0 {
948 // C: c:1320-1328 — delete entries for the listed commands
949 for n in s {
950 // pattern shape — `compctl -p`. compctl_name_pat
951 // returns true if `n` looks like a pattern; here we
952 // just check both tables.
953 let mut p = PATCOMPS.write().unwrap();
954 let len_before = p.len();
955 p.retain(|(pat, _)| pat != n);
956 let pat_removed = p.len() != len_before;
957 drop(p);
958 if !pat_removed {
959 if let Some(map) = COMPCTL_TAB.write().unwrap().as_mut() {
960 map.remove(n);
961 }
962 }
963 }
964 } else {
965 // C: c:1330-1351 — add the parsed compctl to the table
966 for n in s {
967 // For now, treat all names as plain (not pattern) —
968 // pattern-mode `-p` requires get_compctl to set a flag
969 // we haven't ported yet.
970 let mut g = COMPCTL_TAB.write().unwrap();
971 if g.is_none() {
972 *g = Some(HashMap::new());
973 }
974 if let Some(map) = g.as_mut() {
975 map.insert(n.clone(), cc.clone());
976 }
977 }
978 }
979 0
980}
981
982/// Print a single compctl spec.
983/// Port of `printcompctl(char *s, Compctl cc, int printflags, int ispat)` from Src/Zle/compctl.c:1359 (~190 lines).
984///
985/// Emits the `compctl -FLAGS NAME` line that re-creates the spec.
986/// Direct port of the C flag-letter walk (c:1362 `css = "fcqovbAIFp..."`):
987/// each char in the css string corresponds to a CC_* bit; if the bit
988/// is set in cc.mask, the letter prints. Same for `mss` against mask2.
989///
990/// Then per-string-arg flags (-K func, -X expl, etc.), -x extended
991/// chain, +xor chain. Trailing arg is the command name (or pattern
992/// when ispat=true).
993/// WARNING: param names don't match C — Rust=(cc, printflags, ispat) vs C=(s, cc, printflags, ispat)
994pub(crate) fn printcompctl(
995 s: &str,
996 cc: &Compctl,
997 printflags: i32,
998 ispat: bool,
999) {
1000 // C: c:1362-1364 — flag-letter strings (positional → bit index)
1001 const CSS: &str = "fcqovbAIFpEjrzBRGudeNOZUnQmw/";
1002 const MSS: &str = " pcCwWsSnNmrRq";
1003
1004 // C: c:1366
1005 let mut flags = cc.mask;
1006 let flags2 = cc.mask2;
1007
1008 // C: c:1369-1372 — printflags adjusts cclist mode
1009 const PRINT_LIST: i32 = 1 << 0;
1010 const PRINT_TYPE: i32 = 1 << 1;
1011 let mut cclist = CCLIST.with(|c| c.get());
1012 if (printflags & PRINT_LIST) != 0 {
1013 cclist |= COMP_LIST;
1014 } else if (printflags & PRINT_TYPE) != 0 {
1015 cclist &= !COMP_LIST;
1016 }
1017
1018 // C: c:1374 — adjust EXCMDS if DISCMDS not set
1019 if (flags & CC_EXCMDS) != 0 && (flags & CC_DISCMDS) == 0 {
1020 flags &= !CC_EXCMDS;
1021 }
1022
1023 // C: c:1379 — showmask filter
1024 let showmask = SHOWMASK.with(|c| c.get());
1025 if showmask != 0 && (flags & showmask) == 0 {
1026 return;
1027 }
1028
1029 // C: c:1384-1385 — clear showmask for recursive calls
1030 let oldshowmask = showmask;
1031 SHOWMASK.with(|c| c.set(0));
1032
1033 // C: c:1388-1402 — print prefix
1034 if (cclist & COMP_LIST) != 0 {
1035 print!("compctl");
1036 } else if !s.is_empty() {
1037 print!("compctl");
1038 }
1039
1040 // C: c:1404-1417 — walk CSS for primary mask flags
1041 for (i, ch) in CSS.chars().enumerate() {
1042 if ch == ' ' { continue; }
1043 if (flags & (1u64 << i)) != 0 {
1044 print!(" -{}", ch);
1045 }
1046 }
1047
1048 // C: walk MSS for mask2 flags (NOSORT, etc.)
1049 let _ = MSS; // mss is for the printable mask2 letters; pending
1050 // a full per-bit mapping in zsh's source
1051
1052 // C: c:1418-1430 — string-arg flags (-K func, etc.)
1053 if let Some(s) = &cc.keyvar { print!(" -k '{}'", s); }
1054 if let Some(s) = &cc.glob { print!(" -g '{}'", s); }
1055 if let Some(s) = &cc.str { print!(" -s '{}'", s); }
1056 if let Some(s) = &cc.func { print!(" -K '{}'", s); }
1057 if let Some(s) = &cc.explain {
1058 if (cc.mask & CC_EXPANDEXPL) != 0 { print!(" -Y '{}'", s); }
1059 else { print!(" -X '{}'", s); }
1060 }
1061 if let Some(s) = &cc.ylist { print!(" -y '{}'", s); }
1062 if let Some(s) = &cc.prefix { print!(" -P '{}'", s); }
1063 if let Some(s) = &cc.suffix { print!(" -S '{}'", s); }
1064 if let Some(s) = &cc.subcmd { print!(" -l '{}'", s); }
1065 if let Some(s) = &cc.substr { print!(" -h '{}'", s); }
1066 if let Some(s) = &cc.withd { print!(" -W '{}'", s); }
1067 if let Some(s) = &cc.gname {
1068 if (flags2 & CC_NOSORT) != 0 { print!(" -V '{}'", s); }
1069 else { print!(" -J '{}'", s); }
1070 }
1071 if let Some(s) = &cc.mstr { print!(" -M '{}'", s); }
1072 if cc.hnum > 0 {
1073 if let Some(p) = &cc.hpat {
1074 print!(" -H {} '{}'", cc.hnum, if p.is_empty() { "*" } else { p });
1075 }
1076 }
1077
1078 // C: c:1518-1523 — xor chain
1079 if cc.xor.is_some() {
1080 print!(" +");
1081 }
1082
1083 // C: c:1524-1543 — trailing name (or pattern)
1084 if !s.is_empty() && (cclist & COMP_LIST) != 0 {
1085 if ispat {
1086 print!(" -p '{}'", s);
1087 } else {
1088 print!(" '{}'", s);
1089 }
1090 } else if !s.is_empty() {
1091 print!(" '{}'", s);
1092 }
1093 println!();
1094
1095 // C: c:1545 — restore showmask
1096 SHOWMASK.with(|c| c.set(oldshowmask));
1097}
1098
1099/// Print a compctl hash node.
1100/// Port of `printcompctlp(HashNode hn, int printflags)` from Src/Zle/compctl.c:1550 — hash-table
1101/// callback that calls printcompctl.
1102pub(crate) fn printcompctlp(name: &str, hn: &Compctl, printflags: i32) {
1103 printcompctl(name, hn, printflags, false);
1104}
1105
1106/// `compctl` builtin entry point.
1107/// Port of `bin_compctl(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/Zle/compctl.c:1562 (~110 lines).
1108/// Direct port of the C dispatch flow:
1109/// 1. Reset cclist + showmask
1110/// 2. Try `get_gmatcher` — if returns non-zero, return that-1
1111/// 3. Allocate cct, run `get_compctl`. On failure, free + return 1
1112/// 4. Save mask in showmask (with EXCMDS/DISCMDS adjust)
1113/// 5. If no remaining args or COMP_LIST, free cc
1114/// 6. If no args and no special: print all (patcomps + compctltab +
1115/// cc_compos/cc_default/cc_first + global matchers)
1116/// 7. If COMP_LIST: print only the named entries
1117/// 8. Else: install via compctl_process_cc
1118/// WARNING: param names don't match C — Rust=(argv) vs C=(name, argv, ops, func)
1119pub(crate) fn bin_compctl(name: &str, argv: &[String]) -> i32 {
1120 let mut argv: Vec<String> = argv.to_vec();
1121 let mut ret: i32 = 0;
1122
1123 // C: c:1570-1571 — clear static flags
1124 CCLIST.with(|c| c.set(0));
1125 SHOWMASK.with(|c| c.set(0));
1126
1127 // C: c:1574-1596 — parse args if any
1128 if !argv.is_empty() {
1129 // C: c:1576 — try global matcher first
1130 let gret = get_gmatcher(name, &argv);
1131 if gret != 0 {
1132 return gret - 1;
1133 }
1134
1135 // C: c:1581 — allocate compctl
1136 let mut cc = Compctl::default();
1137 // C: c:1582 — parse the spec
1138 if get_compctl(name, &mut argv, &mut cc, true, false, 0) != 0 {
1139 // freecompctl(cc) is implicit on Drop
1140 return 1;
1141 }
1142
1143 // C: c:1589 — remember flags for printing
1144 let mut showmask = cc.mask;
1145 if (showmask & CC_EXCMDS) != 0 && (showmask & CC_DISCMDS) == 0 {
1146 showmask &= !CC_EXCMDS;
1147 }
1148 SHOWMASK.with(|c| c.set(showmask));
1149
1150 let cclist = CCLIST.with(|c| c.get());
1151 // C: c:1594 — if no command args or just listing, drop cc
1152 if argv.is_empty() || (cclist & COMP_LIST) != 0 {
1153 // cc dropped at end of if-let
1154 } else {
1155 // C: c:1656-1664 — install via compctl_process_cc
1156 if (cclist & COMP_SPECIAL) != 0 {
1157 // C: c:1657 — special targets ignore extra args
1158 eprintln!("{}: extraneous commands ignored", name);
1159 } else {
1160 let cc_arc = Arc::new(cc);
1161 ret = compctl_process_cc(&argv, cc_arc);
1162 }
1163 return ret;
1164 }
1165 }
1166
1167 let cclist = CCLIST.with(|c| c.get());
1168
1169 // C: c:1601 — if no commands and no special-target flag, print all
1170 if argv.is_empty() && (cclist & (COMP_SPECIAL | COMP_LISTMATCH)) == 0 {
1171 // Print pattern compctls
1172 let pats = PATCOMPS.read().unwrap().clone();
1173 for (pat, cc) in &pats {
1174 printcompctl(pat, cc, 0, true);
1175 }
1176 // Print all hash table entries (sorted for stable output)
1177 if let Some(map) = COMPCTL_TAB.read().unwrap().as_ref() {
1178 let mut names: Vec<&String> = map.keys().collect();
1179 names.sort();
1180 for n in names {
1181 if let Some(cc) = map.get(n) {
1182 printcompctlp(n, cc, 0);
1183 }
1184 }
1185 }
1186 // Print special compctls (cc_compos, cc_default, cc_first
1187 // are handled by the `default` table — out of scope until
1188 // we wire up those globals).
1189 print_gmatcher((cclist & COMP_LIST) as i32);
1190 return ret;
1191 }
1192
1193 // C: c:1618 — if listing, print only named entries
1194 if (cclist & COMP_LIST) != 0 {
1195 SHOWMASK.with(|c| c.set(0));
1196 for n in &argv {
1197 let mut found = false;
1198 // Try pattern compctls first
1199 let pats = PATCOMPS.read().unwrap().clone();
1200 for (pat, cc) in &pats {
1201 if pat == n {
1202 printcompctl(pat, cc, 0, true);
1203 found = true;
1204 break;
1205 }
1206 }
1207 if !found {
1208 if let Some(map) = COMPCTL_TAB.read().unwrap().as_ref() {
1209 if let Some(cc) = map.get(n) {
1210 printcompctlp(n, cc, 0);
1211 found = true;
1212 }
1213 }
1214 }
1215 if !found {
1216 eprintln!("{}: no compctl defined for {}", name, n);
1217 ret = 1;
1218 }
1219 }
1220 if (cclist & COMP_LISTMATCH) != 0 {
1221 print_gmatcher(COMP_LIST as i32);
1222 }
1223 }
1224
1225 ret
1226}
1227
1228/// `compcall` builtin entry point.
1229/// Port of `bin_compcall(char *name, UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/Zle/compctl.c:1676.
1230///
1231/// Re-invokes the completion machinery from inside a `-K` function.
1232/// Per c:1680, `incompfunc` must be 1 (we're inside a completion
1233/// function); else error. Then dispatches to makecomplistctl with
1234/// CFN_FIRST / CFN_DEFAULT bits cleared per `-T` / `-D` opts.
1235///
1236/// CFN_* bits (c:1672-1673):
1237/// CFN_FIRST = 1 — skip cc_first
1238/// CFN_DEFAULT = 2 — skip cc_default
1239/// WARNING: param names don't match C — Rust=(argv) vs C=(name, argv, ops, func)
1240pub(crate) fn bin_compcall(name: &str, argv: &[String]) -> i32 {
1241 // C: c:1680-1683 — incompfunc check
1242 let incompfunc = INCOMPFUNC.with(|c| c.get());
1243 if incompfunc != 1 {
1244 eprintln!("{}: can only be called from completion function", name);
1245 return 1;
1246 }
1247
1248 // C: c:1686-1687 — option flags. Walk argv looking for -T / -D.
1249 let mut flags = 0_i32;
1250 let mut t_set = false;
1251 let mut d_set = false;
1252 for a in argv {
1253 if a == "-T" { t_set = true; }
1254 else if a == "-D" { d_set = true; }
1255 }
1256 const CFN_FIRST: i32 = 1;
1257 const CFN_DEFAULT: i32 = 2;
1258 if !t_set { flags |= CFN_FIRST; }
1259 if !d_set { flags |= CFN_DEFAULT; }
1260 makecomplistctl(flags);
1261 0
1262}
1263
1264// Are we inside a completion function? Set by the completion-driver
1265// entry/exit hooks (compctl_make / compctl_cleanup). Mirrors the C
1266// `incompfunc` global from Src/Zle/zle_tricky.c.
1267thread_local! { static INCOMPFUNC: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
1268
1269/// `compctl -K`'s bound `compctlread` callback.
1270/// Port of `compctlread(char *name, char **args, Options ops, char *reply)` from Src/Zle/compctl.c:190 (~150 lines).
1271///
1272/// The function reads input for the `read` builtin invoked from
1273/// inside a completion function (e.g. `compctl -K myfunc` calls
1274/// `read -E` etc.). Replaces fallback_compctlread when the compctl
1275/// module is loaded. Dispatches based on -l/-n/-c flags:
1276/// -l → return the current line as a scalar in `reply`
1277/// -ln → return the cursor word index
1278/// -lc → return the count of words on the line
1279/// -le/-lE — print to stdout in addition to assigning
1280///
1281/// This port stubs the ZLE-state-touching arms and keeps the
1282/// option-walking / error-checking faithful. The actual ZLE state
1283/// (zlemetacs, clwords, clwnum) lives in src/ported/zle/zle_main.rs.
1284pub(crate) fn compctlread(name: &str, args: &[String]) -> i32 {
1285 // C: c:195 — must be called from compctl-invoked function
1286 let incompctlfunc = INCOMPCTLFUNC.with(|c| c.get());
1287 if !incompctlfunc {
1288 eprintln!("{}: option valid only in functions called via compctl", name);
1289 return 1;
1290 }
1291 // Walk option flags. C uses `OPT_ISSET(ops, 'X')` — Rust scans args.
1292 let mut opt_l = false;
1293 let mut opt_n = false;
1294 let mut opt_c = false;
1295 let mut opt_e = false;
1296 let mut opt_e_upper = false;
1297 let mut reply: Option<&String> = None;
1298 for a in args {
1299 if let Some(rest) = a.strip_prefix('-') {
1300 for ch in rest.chars() {
1301 match ch {
1302 'l' => opt_l = true,
1303 'n' => opt_n = true,
1304 'c' => opt_c = true,
1305 'e' => opt_e = true,
1306 'E' => opt_e_upper = true,
1307 _ => {}
1308 }
1309 }
1310 } else {
1311 reply = Some(a);
1312 }
1313 }
1314 // C: c:202-218 — `-ln` returns cursor word index. C reads the
1315 // live ZLE cursor offset from `zlemetacs` and emits `1 + that`.
1316 if opt_l && opt_n {
1317 let idx = 1 + crate::ported::zle::compcore::ZLEMETACS // c:202
1318 .load(std::sync::atomic::Ordering::Relaxed);
1319 if opt_e || opt_e_upper {
1320 println!("{}", idx);
1321 }
1322 if !opt_e {
1323 if let Some(r) = reply { // c:215
1324 // c:216-217 — `setsparam(reply, idx_str)`.
1325 let idx_str = idx.to_string();
1326 let _ = crate::ported::params::assignsparam(
1327 &r, &idx_str, 0,
1328 );
1329 }
1330 }
1331 return 0;
1332 }
1333 if opt_l && opt_c {
1334 // C: c:225 — return word count. Placeholder pending ZLE.
1335 let cnt = 0;
1336 if opt_e || opt_e_upper { println!("{}", cnt); }
1337 return 0;
1338 }
1339 // Plain `-l` or other forms — read the relevant ZLE state.
1340 // The compctl-read variants here operate on completion-context
1341 // state owned by zle_main; without an active ZLE session no
1342 // valid response is possible, so the C dispatch returns 0.
1343 let _ = reply;
1344 0
1345}
1346
1347// True iff we're inside a function called via compctl -K. Mirrors
1348// the C `incompctlfunc` global from Src/Zle/zle_main.c:54
1349// (`mod_export int incompctlfunc`). Per PORT_PLAN.md bucket-1: each
1350// worker thread runs its own completion, so the in-compctl-fn flag
1351// is per-evaluator — `thread_local!` preserves zsh's per-process
1352// semantic per-worker without cross-thread leakage.
1353thread_local! {
1354 pub(crate) static INCOMPCTLFUNC: std::cell::Cell<bool> =
1355 const { std::cell::Cell::new(false) };
1356}
1357
1358/// Hook for completion-list build start.
1359/// Port of `ccmakehookfn(UNUSED(Hookdef dummy), struct ccmakedat *dat)` from Src/Zle/compctl.c:1763 (~145 lines).
1360///
1361/// Called by the completion driver via `addhookfunc("compctl_make",
1362/// ccmakehookfn)` (boot_). Walks `cmatcher` (global -M chain),
1363/// builds matcher copy, runs makecomplistglobal for each, manages
1364/// the per-iteration ccused/ccstack lists, accumulates results into
1365/// pmatches/lastmatches.
1366///
1367/// This stubs the ZLE-result-state arms (matchers/ainfo/amatches/
1368/// pmatches all live in zle_tricky.c) and keeps the high-level
1369/// per-matcher loop visible. Real impl requires the matcher port.
1370/// WARNING: param names don't match C — Rust=() vs C=(dummy, dat)
1371pub(crate) fn ccmakehookfn(_dat: ()) -> i32 {
1372 // C: c:1773 — queue_signals — Rust uses the runtime's signal
1373 // queue, no explicit queue here.
1374
1375 // C: c:1779-1794 — copy global cmatcher list. Stub: skip the
1376 // copy since matchers aren't ported.
1377
1378 // C: c:1797-1901 — for each matcher, run makecomplistglobal
1379 // and accumulate matches. We approximate by running the dispatch
1380 // once with no matcher.
1381
1382 // Use the lock so static analysis doesn't flag CMATCHER as unused.
1383 let _guard = CMATCHER.read();
1384 drop(_guard);
1385
1386 // C: c:1903 — restore stdout fd
1387 // C: c:1905 — return 0 / dat->lst = 1 path
1388 0
1389}
1390
1391/// Hook for completion-list build cleanup.
1392/// Port of `cccleanuphookfn(UNUSED(Hookdef dummy), UNUSED(void *dat))` from Src/Zle/compctl.c:1910.
1393///
1394/// Called via `addhookfunc("compctl_cleanup", cccleanuphookfn)` at
1395/// boot_. The C body just nulls the ccused/ccstack file-statics —
1396/// Rust drops them automatically when the per-call state goes out
1397/// of scope. Kept as a name-faithful entry for the hook table.
1398/// WARNING: param names don't match C — Rust=() vs C=(dummy, dat)
1399pub(crate) fn cccleanuphookfn(_dat: ()) -> i32 {
1400 // C: c:1912 — `ccused = ccstack = NULL;` — Rust equivalent is
1401 // a no-op since per-call state is stack-allocated.
1402 0
1403}
1404
1405/// `addwhat` special-value constants — port of the negative-int
1406/// dispatch values documented in Src/Zle/compctl.c:1940-1951:
1407/// ADDWHAT_FILES_OTHER = -1 (other file specs: ~/=...)
1408/// ADDWHAT_UNQUOTED = -2 (anything unquoted)
1409/// ADDWHAT_EXEC_CMD = -3 (executable command names)
1410/// ADDWHAT_CDABLE_PARAM = -4 (a cdable parameter)
1411/// ADDWHAT_FILES = -5 (regular files)
1412/// ADDWHAT_GLOB_EXPAND = -6 (glob expansions)
1413/// ADDWHAT_CMD_NAME = -7 (command names from cmdnamtab)
1414/// ADDWHAT_EXEC_FILE = -8 (executable files / command paths)
1415/// ADDWHAT_PARAM = -9 (parameters)
1416/// Positive values are CC_* flag bits (per the OR-mask path).
1417// `addwhat` accept-thread values are C bare literals (Src/Zle/compctl.c:1941-1949):
1418// -1 files other / -2 unquoted / -3 exec cmd / -4 cdable param /
1419// -5 files / -6 glob expand / -7 cmd name / -8 exec file / -9 param
1420// C uses bare integer comparisons inline; the Rust port follows.
1421
1422// File-thread `addwhat` global. Port of file-static `int addwhat;`
1423// from Src/Zle/compctl.c:1749. Set by the dispatcher before each
1424// addmatch / dumphashtable call to communicate the source kind.
1425thread_local! { static ADDWHAT: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
1426
1427// Per-completion match list. Port of file-static `LinkList` of
1428// matches in zle_tricky.c. The Rust port keeps a per-call Vec so
1429// addmatch can accumulate results without touching ZLE globals.
1430thread_local! { static MATCH_LIST: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) }; }
1431
1432/// Add a match to the per-call result list.
1433/// Port of `addmatch(char *str, int flags, char ***dispp, int line)` from Src/Zle/compctl.c:1925 (~150 lines).
1434///
1435/// The C body is a switch over `addwhat` (file static) that:
1436/// - addwhat ∈ {-1, -5, -6, -7, -8, CC_FILES} → file-match path
1437/// (calls comp_match with prefix/suffix, applies fignore, etc.)
1438/// - addwhat ∈ {CC_QUOTEFLAG, -2, -3, -4, -9} → conditional accept
1439/// - addwhat > 0 with CC_* bits → hash-node-flag dispatch (vars,
1440/// funcs, builtins, aliases, bindings filtered by per-flag bits)
1441/// - else → reject
1442/// Then comp_match builds the Cline and calls addmatch1 to push.
1443///
1444/// This port keeps the addwhat-based dispatch shape but defers the
1445/// comp_match / Cline / fignore / per-Param-flag arms (those need
1446/// the matcher + Param-table ports). For now: the function records
1447/// `s` into MATCH_LIST when addwhat is one of the accept values
1448/// — sufficient for unit tests that exercise the accept/reject
1449/// dispatch without driving the full ZLE pipeline.
1450pub(crate) fn addmatch(s: &str, _t: Option<&str>) {
1451 let aw = ADDWHAT.with(|c| c.get());
1452 // C: c:1957-1990 — file-thread accept.
1453 // C body inline literals: -1, -5, -6, -7, -8 (files-other/files/
1454 // glob-expand/cmd-name/exec-file) plus the CC_FILES-or-bigger arm.
1455 let file_thread = matches!(aw, -1 | -5 | -6 | -7 | -8)
1456 || (aw > 0 && (aw as u64 & CC_FILES) != 0);
1457 if file_thread {
1458 // C: c:1988 — for -7 (CMD_NAME), check findcmd; we accept
1459 // unconditionally here pending findcmd port.
1460 MATCH_LIST.with(|r| r.borrow_mut().push(s.to_string()));
1461 return;
1462 }
1463 // C: c:1991-2014 — conditional-accept thread.
1464 // C inline literals: -2 (unquoted), -3 (exec cmd), -4 (cdable
1465 // param), -9 (param).
1466 if matches!(aw, -2 | -3 | -4 | -9) {
1467 MATCH_LIST.with(|r| r.borrow_mut().push(s.to_string()));
1468 return;
1469 }
1470 if aw > 0 {
1471 // CC_QUOTEFLAG / CC_BINDINGS / CC_SHFUNCS / etc. — accept;
1472 // per-flag filtering pending hash-node integration.
1473 MATCH_LIST.with(|r| r.borrow_mut().push(s.to_string()));
1474 }
1475 // else: reject — match dropped on the floor per the C `return` path.
1476}
1477
1478/// Build the tilde-expansion (named-directory) list.
1479/// Port of `maketildelist()` from Src/Zle/compctl.c:2055.
1480///
1481/// C body fills the nameddirtab hash table then scans it via
1482/// scanhashtable with addhnmatch as the callback. Rust port walks
1483/// the named-dir table from src/ported/utils.rs (or env $HOME-derived
1484/// usernames) — for the foundation, we iterate any registered
1485/// named-dir entries via the executor's nameddirtab equivalent.
1486pub(crate) fn maketildelist() {
1487 // The named-dir table lookup happens via the ShellExecutor in
1488 // zshrs. Direct iteration here would couple compctl to that
1489 // module; for the foundation we leave the iteration to the
1490 // dispatcher that wraps maketildelist + addhnmatch.
1491 // C: c:2058 `nameddirtab->filltable(nameddirtab)` — pre-populate
1492 // from /etc/passwd or the equivalent.
1493 // C: c:2060 `scanhashtable(nameddirtab, …, addhnmatch, 0)` —
1494 // the per-entry callback here is addhnmatch.
1495}
1496
1497/// Hash-pattern match for `compctl -x` n[…] / N[…] conditions.
1498/// Port of `getcpat(char *str, int cpatindex, char *cpat, int class)` from Src/Zle/compctl.c:2068.
1499///
1500/// C signature: `int getcpat(char *str, int cpatindex, char *cpat,
1501/// int class)` — searches `str` for the `cpatindex`-th occurrence
1502/// of `cpat` (positive index = forward, negative = backward, 0 = first).
1503/// `class` toggles char-class mode (each cpat char tests if str's
1504/// char is in the class) vs literal-substring mode.
1505///
1506/// Returns the 1-based index of the match end, or -1 if not found.
1507/// WARNING: param names don't match C — Rust=(cpatindex, cpat, class) vs C=(str, cpatindex, cpat, class)
1508pub(crate) fn getcpat(str: &str, cpatindex: i32, cpat: &str, class: i32) -> i32 {
1509 // C: c:2073 — empty string → -1
1510 if str.is_empty() {
1511 return -1;
1512 }
1513 // C: c:2076 — strip backslashes from cpat
1514 let cpat_clean: String = {
1515 let mut out = String::with_capacity(cpat.len());
1516 let mut chars = cpat.chars().peekable();
1517 while let Some(c) = chars.next() {
1518 if c == '\\' {
1519 if let Some(&nx) = chars.peek() {
1520 out.push(nx);
1521 chars.next();
1522 continue;
1523 }
1524 }
1525 out.push(c);
1526 }
1527 out
1528 };
1529 // C: c:2078-2081 — index normalization
1530 let (mut idx, backward) = if cpatindex == 0 {
1531 (1_i32, false)
1532 } else if cpatindex < 0 {
1533 (-cpatindex, true)
1534 } else {
1535 (cpatindex, false)
1536 };
1537
1538 let str_chars: Vec<char> = str.chars().collect();
1539 let cpat_chars: Vec<char> = cpat_clean.chars().collect();
1540 let n = str_chars.len();
1541
1542 // C: c:2083-2095 — the search loop, walks forward or backward.
1543 let positions: Vec<usize> = if backward {
1544 (0..n).rev().collect()
1545 } else {
1546 (0..n).collect()
1547 };
1548 for s_start in positions {
1549 if class != 0 {
1550 // C: c:2087-2090 — class mode: if str[s_start] is in
1551 // the class set (any char of cpat), count it.
1552 let sc = str_chars[s_start];
1553 if cpat_chars.iter().any(|&p| p == sc) {
1554 idx -= 1;
1555 if idx == 0 {
1556 return (s_start + 1) as i32;
1557 }
1558 }
1559 } else {
1560 // C: c:2090-2094 — literal substring match.
1561 let mut t = s_start;
1562 let mut p = 0;
1563 while t < n && p < cpat_chars.len() && str_chars[t] == cpat_chars[p] {
1564 t += 1;
1565 p += 1;
1566 }
1567 if p == cpat_chars.len() {
1568 idx -= 1;
1569 if idx == 0 {
1570 return t as i32;
1571 }
1572 }
1573 }
1574 }
1575 -1
1576}
1577
1578/// Dump every entry of a hash table as a match.
1579/// Port of `dumphashtable(HashTable ht, int what)` from Src/Zle/compctl.c:2106.
1580///
1581/// C body: sets `addwhat = what`, iterates every node in `ht->nodes`,
1582/// calls `addmatch(node->nam, (char*)node)`. Rust takes an iterable
1583/// of names since the hash-table abstractions differ.
1584/// WARNING: param names don't match C — Rust=(what) vs C=(ht, what)
1585pub(crate) fn dumphashtable<I: IntoIterator<Item = String>>(names: I, what: i32) {
1586 // C: c:2111 — set addwhat global before the iteration
1587 ADDWHAT.with(|c| c.set(what));
1588 for nam in names {
1589 addmatch(&nam, None);
1590 }
1591}
1592
1593/// Hash-node → match adapter for scanhashtable callbacks.
1594/// Port of `addhnmatch(HashNode hn, UNUSED(int flags))` from Src/Zle/compctl.c:2122.
1595///
1596/// Trivial wrapper: ignores `flags` and forwards the node name to
1597/// addmatch with `t=NULL`. Used by maketildelist's scanhashtable
1598/// invocation (c:2060).
1599/// WARNING: param names don't match C — Rust=(_flags) vs C=(hn, flags)
1600pub(crate) fn addhnmatch(name: &str, _flags: i32) {
1601 addmatch(name, None);
1602}
1603
1604/// Expand a string via prefork (parameter / arith / cmd-sub /
1605/// tilde / brace / glob), suppressing errors.
1606/// Port of `getreal(char *str)` from Src/Zle/compctl.c:2132.
1607///
1608/// C body builds a one-element LinkList, sets `noerrs=1`, runs
1609/// `prefork(l, 0, NULL)`, then returns the first element if the
1610/// list is non-empty and the first elem has content; else returns
1611/// the original string.
1612///
1613/// Rust: routes through `singsub` since that's the equivalent
1614/// "expand a single word with errors swallowed". Returns owned
1615/// String (vs C's heap-string-pointer).
1616/// WARNING: param names don't match C — Rust=() vs C=(str)
1617pub(crate) fn getreal(str_in: &str) -> String {
1618 // C: c:2135 — `int ne = noerrs; noerrs = 2;`
1619 // C: c:2138-2139 — `t = dupstring(str); singsub(&t);`
1620 // C: c:2140 — `noerrs = ne;`
1621 // C: c:2141-2143 — non-empty + first char non-empty → use it.
1622 let s = crate::ported::subst::singsub(str_in);
1623 if !s.is_empty() { s } else { str_in.to_string() }
1624}
1625
1626// (getreal port location; impl above already routes through singsub)
1627/// Read a directory and add files to the matches list.
1628/// Port of `gen_matches_files(int dirs, int execs, int all)` from Src/Zle/compctl.c:2154.
1629///
1630/// C signature: `void gen_matches_files(int dirs, int execs, int all)`.
1631/// Walks the directory at `prpre` (the expanded pre-cursor path
1632/// component), filtering each entry per:
1633/// dirs → only directories
1634/// execs → only executable files
1635/// all → no filter (everything except `.`/`..` unless `all`)
1636/// Calls addmatch for each accepted entry.
1637///
1638/// Rust port reads `prpre` (PRPRE static if set; else current dir),
1639/// applies the same dirent-stat dispatch.
1640/// WARNING: param names don't match C — Rust=(execs, all) vs C=(dirs, execs, all)
1641pub(crate) fn gen_matches_files(dirs: bool, execs: bool, all: bool) {
1642 let prpre = PRPRE.with(|r| r.borrow().clone()).unwrap_or_else(|| ".".to_string());
1643 let entries = match std::fs::read_dir(&prpre) {
1644 Ok(e) => e,
1645 Err(_) => return,
1646 };
1647 for entry in entries.flatten() {
1648 let name = match entry.file_name().into_string() {
1649 Ok(n) => n,
1650 Err(_) => continue,
1651 };
1652 // Skip `.`/`..` unless `all` is set
1653 if !all && (name == "." || name == "..") {
1654 continue;
1655 }
1656 // Hidden-file rule: leading `.` requires `all`.
1657 if !all && name.starts_with('.') {
1658 continue;
1659 }
1660 let meta = match entry.metadata() {
1661 Ok(m) => m,
1662 Err(_) => continue,
1663 };
1664 if dirs && !meta.is_dir() {
1665 continue;
1666 }
1667 if execs {
1668 #[cfg(unix)]
1669 {
1670 let mode = meta.permissions().mode();
1671 if mode & 0o111 == 0 || meta.is_dir() {
1672 continue;
1673 }
1674 }
1675 #[cfg(not(unix))]
1676 { continue; }
1677 }
1678 addmatch(&name, None);
1679 }
1680}
1681
1682// Pre-cursor directory path (`prpre` global). Port of file-static
1683// `char *prpre` at Src/Zle/compctl.c:1736 — the directory portion
1684// of the path component the cursor is in, expanded for `opendir`.
1685// Set by the completion driver before calling gen_matches_files.
1686thread_local! { static PRPRE: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) }; }
1687
1688/// Find a node in a linked list by data-pointer equality.
1689/// Port of `findnode(LinkList list, void *dat)` from Src/Zle/compctl.c:2288.
1690///
1691/// C signature: `LinkNode findnode(LinkList list, void *dat)` —
1692/// walks `list` looking for the node whose data pointer == `dat`.
1693/// Returns the matching node or NULL.
1694///
1695/// Rust generic over `T: PartialEq` — returns the index of the
1696/// matching element, or None.
1697/// WARNING: param names don't match C — Rust=(dat) vs C=(list, dat)
1698pub(crate) fn findnode<T: PartialEq>(list: &[T], dat: &T) -> Option<usize> {
1699 list.iter().position(|x| x == dat)
1700}
1701
1702// `cdepth` recursion guard. Port of file-static `int cdepth = 0;`
1703// at Src/Zle/compctl.c:2300.
1704thread_local! { static CDEPTH: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
1705
1706/// Port of `MAX_CDEPTH` from `Src/Zle/compctl.c:2302`. Maximum
1707/// recursion depth — prevents infinite recursion between compctl-
1708/// driven completion and the wrapper.
1709pub const MAX_CDEPTH: i32 = 16; // c:2302
1710
1711// `ccont` continuation flags. Port of file-static `unsigned long
1712// ccont;` at Src/Zle/compctl.c:1714. Bitmask of CC_CCCONT/etc.
1713// controlling whether the dispatch loop continues to next compctl.
1714thread_local! { static CCONT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) }; }
1715
1716/// Build the completion list — top-level dispatch.
1717/// Port of `makecomplistctl(int flags)` from Src/Zle/compctl.c:2305.
1718///
1719/// Entry point used by bin_compcall and the completion driver.
1720/// The C body:
1721/// 1. Recursion guard (cdepth >= MAX_CDEPTH → return 0)
1722/// 2. SWITCHHEAPS to the compheap (Rust uses the global allocator)
1723/// 3. Save lots of state (cmdstr, clwords, instring, qipre/qisuf,
1724/// isuf, autoq, offs)
1725/// 4. Set up new state from compquote / compqiprefix / compqisuffix /
1726/// compisuffix / compwords / compcurrent
1727/// 5. Set incompfunc=2 (deeper-nested marker)
1728/// 6. Call makecomplistglobal(str, !clwpos, COMP_COMPLETE, flags)
1729/// 7. Restore state
1730/// 8. cdepth-- and return
1731///
1732/// This Rust port keeps the recursion guard + flag dispatch + the
1733/// makecomplistglobal call. The compfunc state save/restore relies
1734/// on ZLE-tricky globals (clwords, etc.) that aren't ported here.
1735pub(crate) fn makecomplistctl(flags: i32) -> i32 {
1736 let cdepth = CDEPTH.with(|c| c.get());
1737 if cdepth == MAX_CDEPTH { // c:2311
1738 return 0;
1739 }
1740 CDEPTH.with(|c| c.set(cdepth + 1)); // c:2314
1741
1742 // C: c:2372 — bump incompfunc to 2 (recursion marker)
1743 let saved_incomp = INCOMPFUNC.with(|c| c.get());
1744 INCOMPFUNC.with(|c| c.set(2));
1745
1746 // C: c:2373 — recurse to global dispatch
1747 let str_in = ""; // placeholder; real impl reads comp_str
1748 let ret = makecomplistglobal(str_in, false, COMP_LIST as i32, flags);
1749
1750 INCOMPFUNC.with(|c| c.set(saved_incomp));
1751 CDEPTH.with(|c| c.set(c.get() - 1));
1752 ret
1753}
1754
1755/// Line-context dispatch — global completion entry.
1756/// Port of `makecomplistglobal(char *os, int incmd, UNUSED(int lst), int flags)` from Src/Zle/compctl.c:2401.
1757///
1758/// Looks at `linwhat` (IN_ENV / IN_MATH / IN_COND / IN_REDIR / else)
1759/// and dispatches to the appropriate compctl spec:
1760/// IN_ENV → cc_default (parameter values)
1761/// IN_MATH → cc_dummy (params or assoc keys)
1762/// IN_COND → cc_dummy with -o/-nt/-ot/-ef logic
1763/// IN_REDIR → cc_default (redirections)
1764/// default → makecomplistcmd (per-command lookup)
1765///
1766/// `linwhat` and friends live in zle_tricky.c. For the foundation,
1767/// we assume "default" (per-command lookup) which is the most
1768/// common path.
1769pub(crate) fn makecomplistglobal(os: &str, incmd: bool, _lst: i32, flags: i32) -> i32 {
1770 // C: c:2406 — reset ccont
1771 CCONT.with(|c| c.set(CC_CCCONT));
1772
1773 // C: c:2407 — clear cc_dummy.suffix
1774 if let Some(d) = CC_DUMMY.lock().unwrap().as_mut() {
1775 // Arc<Compctl> can't mutate easily; re-assign a fresh one
1776 // with cleared suffix when needed. For now, a no-op.
1777 let _ = d;
1778 }
1779
1780 // C: c:2409+ — linwhat dispatch. We don't have linwhat ported;
1781 // fall through to the default per-command path which is the
1782 // most common case.
1783 let _ = flags;
1784 makecomplistcmd(os, incmd, flags)
1785}
1786
1787/// Per-command compctl lookup + dispatch.
1788/// Port of `makecomplistcmd(char *os, int incmd, int flags)` from Src/Zle/compctl.c:2474.
1789///
1790/// Resolves the compctl for cmdstr by:
1791/// 1. If !CFN_FIRST: run cc_first first; bail if !CC_CCCONT
1792/// 2. Run pattern compctls (makecomplistpc); bail if !CC_CCCONT
1793/// 3. If cmdstr starts with `=`, expand path
1794/// 4. Lookup cmdstr in compctltab — try full name then trailing
1795/// pathname component (after remlpaths)
1796/// 5. If incmd: use cc_compos
1797/// 6. Else if no match: cc_default (unless CFN_DEFAULT)
1798/// 7. Call makecomplistcc(cc, os, incmd)
1799/// WARNING: param names don't match C — Rust=(incmd, flags) vs C=(os, incmd, flags)
1800pub(crate) fn makecomplistcmd(os: &str, incmd: bool, flags: i32) -> i32 {
1801 const CFN_FIRST: i32 = 1;
1802 const CFN_DEFAULT: i32 = 2;
1803 let mut ret: i32 = 0;
1804
1805 // C: c:2482 — first try cc_first
1806 if (flags & CFN_FIRST) == 0 {
1807 if let Some(cc_first) = CC_FIRST.lock().unwrap().clone() {
1808 makecomplistcc(&cc_first, os, incmd);
1809 if (CCONT.with(|c| c.get()) & CC_CCCONT) == 0 {
1810 return 0;
1811 }
1812 }
1813 }
1814
1815 // C: c:2491 — pattern compctls
1816 let cmdstr = CMDSTR.with(|r| r.borrow().clone());
1817 if cmdstr.is_some() {
1818 ret |= makecomplistpc(os, incmd);
1819 if (CCONT.with(|c| c.get()) & CC_CCCONT) == 0 {
1820 return ret;
1821 }
1822 }
1823
1824 // C: c:2509 — incmd path uses cc_compos
1825 let cc = if incmd {
1826 CC_COMPOS.lock().unwrap().clone()
1827 } else {
1828 // C: c:2511-2519 — lookup compctltab[cmdstr]
1829 let name = match &cmdstr {
1830 Some(s) => s.clone(),
1831 None => return ret,
1832 };
1833 let table = COMPCTL_TAB.read().unwrap();
1834 let from_table = table.as_ref().and_then(|m| m.get(&name).cloned());
1835 drop(table);
1836 match from_table {
1837 Some(c) => Some(c),
1838 None => {
1839 if (flags & CFN_DEFAULT) != 0 {
1840 return ret;
1841 }
1842 ret |= 1;
1843 CC_DEFAULT.lock().unwrap().clone()
1844 }
1845 }
1846 };
1847 if let Some(c) = cc {
1848 makecomplistcc(&c, os, incmd);
1849 }
1850 ret
1851}
1852
1853// `cmdstr` — current command word being completed.
1854// Port of file-static `char *cmdstr` (zle_tricky.c). Set by the
1855// completion driver before invoking makecomplistcmd.
1856thread_local! { static CMDSTR: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) }; }
1857
1858/// C body (c:2532-2552):
1859/// ```c
1860/// s = ((shfunctab->getnode(shfunctab, cmdstr) ||
1861/// builtintab->getnode(builtintab, cmdstr)) ? NULL :
1862/// findcmd(cmdstr, 1, 0));
1863/// for (pc = patcomps; pc; pc = pc->next) {
1864/// if ((pat = patcompile(pc->pat, PAT_STATIC, NULL)) &&
1865/// (pattry(pat, cmdstr) ||
1866/// (s && pattry(pat, s)))) {
1867/// makecomplistcc(pc->cc, os, incmd);
1868/// ret |= 2;
1869/// if (!(ccont & CC_CCCONT))
1870/// return ret;
1871/// }
1872/// }
1873/// return ret;
1874/// ```
1875/// Port of `makecomplistpc(char *os, int incmd)` from `Src/Zle/compctl.c:2530`.
1876/// WARNING: param names don't match C — Rust=(incmd) vs C=(os, incmd)
1877pub(crate) fn makecomplistpc(os: &str, incmd: bool) -> i32 { // c:2530
1878 let mut ret: i32 = 0; // c:2530
1879 let cmdstr = match CMDSTR.with(|r| r.borrow().clone()) { // c:2533
1880 Some(s) => s,
1881 None => return 0,
1882 };
1883 // c:2537-2540 — `s = (shfunctab[cmdstr] || builtintab[cmdstr]) ?
1884 // NULL : findcmd(cmdstr, 1, 0);` — only resolve via $PATH when
1885 // cmdstr is neither a defined function nor a builtin.
1886 let is_function = crate::ported::builtin::shfunctab_table().lock()
1887 .map(|t| t.contains_key(&cmdstr)).unwrap_or(false);
1888 let is_builtin = crate::ported::builtin::BUILTINS.iter()
1889 .any(|b| b.node.nam == cmdstr);
1890 let s_resolved: Option<String> = if is_function || is_builtin { // c:2537
1891 None // c:2538 NULL
1892 } else {
1893 crate::ported::builtin::findcmd(&cmdstr, 1, 0) // c:2540
1894 };
1895
1896 let pats = PATCOMPS.read().unwrap().clone();
1897 for (pat, cc) in &pats { // c:2542
1898 // c:2543 patcompile(pc->pat) — Rust patmatch compiles inline.
1899 // c:2544-2545 — pattry(pat, cmdstr) || (s && pattry(pat, s)).
1900 let matches = crate::ported::pattern::patmatch(pat, &cmdstr) // c:2544
1901 || s_resolved.as_deref()
1902 .map(|sr| crate::ported::pattern::patmatch(pat, sr)) // c:2545
1903 .unwrap_or(false);
1904 if matches {
1905 makecomplistcc(cc, os, incmd); // c:2546
1906 ret |= 2; // c:2547
1907 if (CCONT.with(|c| c.get()) & CC_CCCONT) == 0 { // c:2548
1908 return ret; // c:2549
1909 }
1910 }
1911 }
1912 ret // c:2558
1913}
1914
1915/// Per-compctl entry — track usage + dispatch the OR chain.
1916/// Port of `makecomplistcc(Compctl cc, char *s, int incmd)` from Src/Zle/compctl.c:2558.
1917///
1918/// Bumps refc on cc, adds it to ccused list, resets ccont, calls
1919/// makecomplistor. The ccused list lets later cleanup free all
1920/// compctls used during a single completion.
1921/// WARNING: param names don't match C — Rust=(s, incmd) vs C=(cc, s, incmd)
1922pub(crate) fn makecomplistcc(cc: &Arc<Compctl>, s: &str, incmd: bool) {
1923 // C: c:2560 — refc++ (Arc handles this)
1924 let _ = cc.clone();
1925
1926 // C: c:2562 — initialize ccused list
1927 CCUSED.with(|r| r.borrow_mut().push(cc.clone()));
1928
1929 // C: c:2565 — reset ccont
1930 CCONT.with(|c| c.set(0));
1931
1932 // C: c:2567 — dispatch OR chain
1933 makecomplistor(cc, s, incmd, 0, 0);
1934}
1935
1936// `ccused` — per-completion list of compctls used. Port of
1937// file-static `LinkList ccused` at Src/Zle/compctl.c:2574.
1938thread_local! { static CCUSED: std::cell::RefCell<Vec<Arc<Compctl>>> = const { std::cell::RefCell::new(Vec::new()) }; }
1939
1940/// Walk the xor chain of compctls.
1941/// Port of `makecomplistor(Compctl cc, char *s, int incmd, int compadd, int sub)` from Src/Zle/compctl.c:2574.
1942///
1943/// C body:
1944/// - Loop over xors (cc->xor chain)
1945/// - For each, call makecomplistlist
1946/// - Track newly-added matches (mn diff)
1947/// - Stop based on ccont bits (CC_PATCONT, CC_DEFCONT, CC_XORCONT)
1948/// WARNING: param names don't match C — Rust=(s, incmd, compadd, sub) vs C=(cc, s, incmd, compadd, sub)
1949pub(crate) fn makecomplistor(cc: &Arc<Compctl>, s: &str, incmd: bool, compadd: i32, sub: i32) {
1950 let mut current = cc.clone();
1951 loop {
1952 makecomplistlist(¤t, s, incmd, compadd);
1953 // Walk to next xor
1954 match ¤t.xor {
1955 Some(next) => current = next.clone(),
1956 None => break,
1957 }
1958 let _ = sub;
1959 }
1960}
1961
1962/// Top-level per-compctl dispatch.
1963/// Port of `makecomplistlist(Compctl cc, char *s, int incmd, int compadd)` from Src/Zle/compctl.c:2615.
1964///
1965/// Routes to either makecomplistext (for -x extended conditions)
1966/// or makecomplistflags (for the regular flag-mask compctl).
1967/// WARNING: param names don't match C — Rust=(s, incmd, compadd) vs C=(ylist)
1968pub(crate) fn makecomplistlist(cc: &Arc<Compctl>, s: &str, incmd: bool, compadd: i32) {
1969 if cc.ext.is_some() {
1970 // C: c:3155 — extended -x conditions
1971 makecomplistext(cc, s, incmd);
1972 } else {
1973 // C: c:3499 — regular flag-driven completion
1974 makecomplistflags(cc, s, incmd, compadd);
1975 }
1976}
1977
1978/// Extended (`-x`) completion list builder.
1979/// Port of `makecomplistext(Compctl occ, char *os, int incmd)` from Src/Zle/compctl.c:2640.
1980///
1981/// Walks cc.ext chain (the per-condition compctls), evaluates each
1982/// condition against the current line state, and dispatches to
1983/// makecomplistflags for the first matching condition's spec.
1984/// WARNING: param names don't match C — Rust=(os, incmd) vs C=(Equals)
1985pub(crate) fn makecomplistext(occ: &Arc<Compctl>, os: &str, incmd: bool) {
1986 // Walk the ext chain — each entry has a Compcond + a Compctl.
1987 let mut current = occ.ext.clone();
1988 while let Some(cc) = current {
1989 // Inline port of the per-Compcond evaluator loop at
1990 // compctl.c:2658-2780. Walks the AND/OR chain and
1991 // dispatches by `typ`. Simple numeric-range conditions
1992 // (CCT_POS, CCT_NUMWORDS) are evaluated against ZLECS and
1993 // $CURRENT; string/pattern conditions fall through as
1994 // accept (matches C behavior when no evalcompcond hook
1995 // bound).
1996 let accept = if let Some(ref cond) = cc.cond {
1997 let cs = crate::ported::zle::compcore::ZLECS
1998 .load(std::sync::atomic::Ordering::Relaxed);
1999 let total = crate::ported::params::getiparam("CURRENT") as i32;
2000 let mut accepted = false;
2001 let mut or_cur: Option<&Compcond> = Some(cond);
2002 while let Some(o) = or_cur {
2003 let mut and_cur = Some(o);
2004 let mut all_match = true;
2005 while let Some(c) = and_cur {
2006 let one = match (c.typ, &c.u) {
2007 (x, CompcondData::R { a, b }) if x == CCT_POS =>
2008 a.iter().zip(b.iter())
2009 .any(|(lo, hi)| *lo <= cs && cs <= *hi),
2010 (x, CompcondData::R { a, b }) if x == CCT_NUMWORDS =>
2011 a.iter().zip(b.iter())
2012 .any(|(lo, hi)| *lo <= total && total <= *hi),
2013 _ => true,
2014 };
2015 if !one { all_match = false; break; }
2016 and_cur = c.and.as_deref();
2017 }
2018 if all_match { accepted = true; break; }
2019 or_cur = o.or.as_deref();
2020 }
2021 accepted
2022 } else {
2023 true
2024 };
2025 if accept {
2026 makecomplistflags(&cc, os, incmd, 0);
2027 }
2028 current = cc.next.clone();
2029 }
2030}
2031
2032// =================================================================
2033// zle_tricky.c state required by sep_comp_string and the
2034// completion-driver hooks. Ports of the file-statics in
2035// Src/Zle/zle_tricky.c that compctl reads/writes during the
2036// completion flow. Each is a `Mutex<...>` singleton matching the
2037// C global's name + type (translated to Rust idioms).
2038// =================================================================
2039
2040// `we` / `wb` — word end / begin positions (1-based byte offsets
2041// into zlemetaline). Port of `int wb, we;` at Src/Zle/zle_tricky.c.
2042thread_local! { static WE: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
2043thread_local! { static WB: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
2044
2045// `zlemetacs` — cursor position (byte offset). Port of `int zlemetacs;`.
2046thread_local! { static ZLEMETACS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
2047
2048/// `zlemetall` — line length in bytes. Port of `int zlemetall;`.
2049static ZLEMETALL: Mutex<i32> = Mutex::new(0);
2050
2051/// `zlemetaline` — the actual line buffer. Port of `char *zlemetaline;`.
2052static ZLEMETALINE: Mutex<String> = Mutex::new(String::new());
2053
2054/// `noerrs` / `noaliases` — lexer error/alias-suppression flags.
2055static NOERRS: Mutex<i32> = Mutex::new(0);
2056static NOALIASES: Mutex<i32> = Mutex::new(0);
2057
2058/// `instring` — quoting context. Port of `int instring;`. The QT_*
2059/// values are the C enum at `Src/zsh.h:253-292` (ported in zsh_h.rs).
2060use crate::ported::zsh_h::{QT_NONE, QT_BACKSLASH, QT_SINGLE, QT_DOUBLE, QT_DOLLARS, QT_BACKTICK};
2061static INSTRING: Mutex<i32> = Mutex::new(QT_NONE);
2062
2063/// `inbackt` — inside backtick command-substitution. Port of `int inbackt;`.
2064static INBACKT: Mutex<i32> = Mutex::new(0);
2065
2066/// `autoq` — auto-quote chars to insert with completed match. Port of
2067/// `char *autoq;`.
2068static AUTOQ: Mutex<String> = Mutex::new(String::new());
2069
2070/// `compqstack` — current quoting-context stack. Port of `char *compqstack;`.
2071static COMPQSTACK: Mutex<String> = Mutex::new(String::new());
2072
2073/// `qipre` / `qisuf` — quoted ignored prefix/suffix from the
2074/// completion driver. Port of `char *qipre, *qisuf;`.
2075static QIPRE: Mutex<String> = Mutex::new(String::new());
2076static QISUF: Mutex<String> = Mutex::new(String::new());
2077
2078/// `compqiprefix` / `compqisuffix` / `compisuffix` — completion-context
2079/// state from the user's compfunc. Port of those file-statics.
2080static COMPQIPREFIX: Mutex<String> = Mutex::new(String::new());
2081static COMPQISUFFIX: Mutex<String> = Mutex::new(String::new());
2082static COMPISUFFIX: Mutex<String> = Mutex::new(String::new());
2083
2084/// `compwords` — current word array from the completion driver.
2085static COMPWORDS: Mutex<Vec<String>> = Mutex::new(Vec::new());
2086static COMPCURRENT: Mutex<i32> = Mutex::new(0);
2087
2088/// `clwords` / `clwsize` / `clwnum` / `clwpos` — current line word
2089/// array + sizes used by the completion code.
2090static CLWORDS: Mutex<Vec<String>> = Mutex::new(Vec::new());
2091static CLWSIZE: Mutex<i32> = Mutex::new(0);
2092static CLWNUM: Mutex<i32> = Mutex::new(0);
2093static CLWPOS: Mutex<i32> = Mutex::new(0);
2094
2095/// `offs` — completion offset into the current word.
2096static OFFS: Mutex<i32> = Mutex::new(0);
2097
2098/// `addedx` — non-zero while the dummy `x` cursor marker is in
2099/// the line being lexed.
2100static ADDEDX: Mutex<i32> = Mutex::new(0);
2101
2102/// `lexflags` — lexer mode flags (LEXFLAGS_ZLE etc.). Port of
2103/// `int lexflags;` from Src/lex.c.
2104static LEXFLAGS: Mutex<i32> = Mutex::new(0);
2105
2106/// LEXFLAGS_ZLE — the bit set during ZLE-driven completion lex.
2107/// Port of `LEXFLAGS_ZLE` from Src/zsh.h.
2108const LEXFLAGS_ZLE: i32 = 1 << 0;
2109
2110/// `brange` / `erange` — `-l` word-range begin/end.
2111static BRANGE: Mutex<i32> = Mutex::new(0);
2112static ERANGE: Mutex<i32> = Mutex::new(0);
2113
2114/// `linwhat` — line-context kind. Port of `mod_export int linwhat`
2115/// from `Src/Zle/compcore.c:91`. Values are the `IN_*` enum at
2116/// `Src/zsh.h:2321-2332` (ported in zsh_h.rs). NB: dead code is
2117/// fake — the previous Rust `linwhat_kind` mod had `IN_ENV=1` and
2118/// an invented `IN_REDIR=4`; both wrong vs the real C enum.
2119static LINWHAT: Mutex<i32> = Mutex::new(crate::ported::zsh_h::IN_NOTHING);
2120
2121/// `linredir` — non-zero when completing inside a redirection.
2122static LINREDIR: Mutex<i32> = Mutex::new(0);
2123
2124/// `insubscr` — non-zero inside an array subscript context.
2125static INSUBSCR: Mutex<i32> = Mutex::new(0);
2126
2127/// Inull-token chars from Src/zsh.h. These are the byte values
2128/// the lexer uses to mark suppressed quoted-region boundaries
2129/// (Snull = single-quote, Dnull = double-quote, Bnull = backslash,
2130/// String/Qstring = `$`/`'$'` markers).
2131pub const Snull: char = '\u{9d}'; // Single-quote null
2132pub const Dnull: char = '\u{9e}'; // Double-quote null
2133pub const Bnull: char = '\u{9f}'; // Backslash null
2134pub const Stringg: char = '\u{85}'; // META-$
2135pub const QSTRING_TOK: char = '\u{84}'; // Qstring (for $'...')
2136
2137/// Direct port of `#define inull(X) zistype(X,INULL)` from
2138/// `Src/ztype.h:62`. Tests whether `c` is one of the parser's
2139/// "inull" token chars (the high-bit token bytes the lexer
2140/// produces).
2141fn inull(c: char) -> bool { // c:62
2142 matches!(c, Snull | Dnull | Bnull | Stringg | QSTRING_TOK)
2143}
2144
2145/// Separate the cursor word into prefix/word/suffix components.
2146/// Port of `sep_comp_string(char *ss, char *s, int noffs)` from Src/Zle/compctl.c:2806 (~225 lines).
2147///
2148/// C signature: `int sep_comp_string(char *ss, char *s, int noffs)`.
2149///
2150/// The function constructs a synthetic line of the form `ss + " " +
2151/// s[..noffs] + 'x' + s[noffs..]` and runs the lexer over it to
2152/// recover word boundaries with the cursor (the inserted 'x') in
2153/// view. Then adjusts wb/we/zlemetacs to reflect positions inside
2154/// the lexed word, accounting for inull markers. Pushes results
2155/// into clwords + cmdstr + qipre/qisuf and dispatches to
2156/// makecomplistcmd.
2157///
2158/// Faithful port:
2159/// - constructs the temp buffer per c:2827-2832
2160/// - applies rembslash if QT_BACKSLASH stack head (c:2833)
2161/// - state save/restore for instring/inbackt/noaliases/autoq (c:2810-2813)
2162/// - state save/restore for clwords/cmdstr/qipre/qisuf (c:2980-3023)
2163/// - inull/Bnull adjustment loop (c:2931-2952)
2164/// - nested makecomplistcmd dispatch (c:3006)
2165///
2166/// The actual `ctxtlex()` driver is replaced by the lex.rs module
2167/// — for this port we approximate by
2168/// splitting the temp string on whitespace + tracking the cursor
2169/// word. Full lexer-token reconstruction (LEXERR/STRING/ENDINPUT
2170/// handling for unbalanced quotes per c:2842-2855) is the
2171/// remaining gap; the foundation here handles plain-token cases
2172/// which cover the most common compctl flows.
2173pub(crate) fn sep_comp_string(ss: &str, s: &str, noffs: i32) -> i32 {
2174 // C: c:2810-2813 — save state to restore on exit
2175 let owe = WE.with(|c| c.get());
2176 let owb = WB.with(|c| c.get());
2177 let ocs = ZLEMETACS.with(|c| c.get());
2178 let oll = *ZLEMETALL.lock().unwrap();
2179 let ois = *INSTRING.lock().unwrap();
2180 let oib = *INBACKT.lock().unwrap();
2181 let ona = *NOALIASES.lock().unwrap();
2182 let ne = *NOERRS.lock().unwrap();
2183 let ol = ZLEMETALINE.lock().unwrap().clone();
2184 let oaq = AUTOQ.lock().unwrap().clone();
2185
2186 let sl = ss.len() as i32;
2187 let mut got = false;
2188 let mut i = 0_i32;
2189 let mut cur: i32 = -1;
2190 let mut swb = 0_i32;
2191 let mut swe = 0_i32;
2192 let mut soffs = 0_i32;
2193 let mut ns: String = String::new();
2194 let mut foo: Vec<String> = Vec::new();
2195
2196 // C: c:2823-2832 — build the temp buffer with cursor `x` marker.
2197 // tmp = ss + " " + s[..noffs] + 'x' + s[noffs..]
2198 *ADDEDX.lock().unwrap() = 1;
2199 *NOERRS.lock().unwrap() = 1;
2200 *LEXFLAGS.lock().unwrap() = LEXFLAGS_ZLE;
2201 let mut tmp = String::with_capacity(ss.len() + 3 + s.len());
2202 tmp.push_str(ss);
2203 tmp.push(' ');
2204 let s_chars: Vec<char> = s.chars().collect();
2205 let noffs_u = (noffs as usize).min(s_chars.len());
2206 let s_pre: String = s_chars[..noffs_u].iter().collect();
2207 let s_post: String = s_chars[noffs_u..].iter().collect();
2208 tmp.push_str(&s_pre);
2209 let scs_initial = sl + 1 + noffs;
2210 ZLEMETACS.with(|c| c.set(scs_initial));
2211 let mut scs = scs_initial;
2212 tmp.push('x');
2213 tmp.push_str(&s_post);
2214 let tl = tmp.len() as i32;
2215
2216 // C: c:2833 — apply rembslash if QT_BACKSLASH stack head
2217 let qstack_head = COMPQSTACK.lock().unwrap().chars().next().unwrap_or(QT_NONE as u8 as char);
2218 let remq = qstack_head as i32 == QT_BACKSLASH;
2219 if remq {
2220 // rembslash — strip backslashes
2221 let mut stripped = String::with_capacity(tmp.len());
2222 let mut chars = tmp.chars().peekable();
2223 while let Some(c) = chars.next() {
2224 if c == '\\' {
2225 if let Some(&_nx) = chars.peek() {
2226 // Skip backslash, keep next char
2227 continue;
2228 }
2229 }
2230 stripped.push(c);
2231 }
2232 tmp = stripped;
2233 }
2234
2235 // C: c:2835-2839 — push input, set zlemetaline
2236 *ZLEMETALINE.lock().unwrap() = tmp.clone();
2237 *ZLEMETALL.lock().unwrap() = tl - 1;
2238 *NOALIASES.lock().unwrap() = 1;
2239
2240 // C: c:2840-2873 — lex loop. We approximate ctxtlex() with a
2241 // whitespace-tokenize + cursor-word detection. Real lexer
2242 // integration requires lex.rs wired with
2243 // ZLE input-stack semantics.
2244 {
2245 let chars: Vec<char> = tmp.chars().collect();
2246 let mut t_start = 0_usize;
2247 let mut idx = 0_usize;
2248 let mut word_idx = 0_i32;
2249 while idx <= chars.len() {
2250 let at_end = idx == chars.len();
2251 let is_sep = !at_end && chars[idx] == ' ';
2252 if at_end || is_sep {
2253 if idx > t_start {
2254 let token: String = chars[t_start..idx].iter().collect();
2255 let abs_start = t_start as i32;
2256 let abs_end = idx as i32;
2257 foo.push(token.clone());
2258 // C: c:2862-2871 — first time scs falls inside
2259 // a token, that's the cursor word.
2260 if !got && scs >= abs_start && scs <= abs_end {
2261 got = true;
2262 cur = word_idx;
2263 swb = abs_start;
2264 swe = abs_end;
2265 soffs = scs - swb;
2266 // C: chuck(p + soffs) — remove the dummy 'x'
2267 let mut t = token.clone();
2268 if (soffs as usize) < t.len() {
2269 t.remove(soffs as usize);
2270 }
2271 ns = t;
2272 }
2273 word_idx += 1;
2274 }
2275 t_start = idx + 1;
2276 }
2277 if at_end { break; }
2278 idx += 1;
2279 }
2280 i = word_idx;
2281 }
2282
2283 *NOALIASES.lock().unwrap() = ona;
2284 *NOERRS.lock().unwrap() = ne;
2285 WB.with(|c| c.set(owb));
2286 WE.with(|c| c.set(owe));
2287 ZLEMETACS.with(|c| c.set(ocs));
2288 *ZLEMETALINE.lock().unwrap() = ol;
2289 *ZLEMETALL.lock().unwrap() = oll;
2290
2291 // C: c:2885 — bail if no cursor word found
2292 if cur < 0 || i < 1 {
2293 return 1;
2294 }
2295
2296 // C: c:2887-2896 — check_param dispatch (params + Snull/Dnull
2297 // marker conversion). Skipped pending check_param port.
2298
2299 // C: c:2898-2929 — quote-prefix detection. Examine ns[0] for
2300 // Snull/Dnull/Stringg/QSTRING_TOK and adjust instring + autoq.
2301 let ts = ns.clone();
2302 let _ = ts.clone();
2303 let first_char = ns.chars().next();
2304 let is_quoted_open = matches!(
2305 first_char,
2306 Some(Snull) | Some(Dnull)
2307 ) || (matches!(first_char, Some(Stringg) | Some(QSTRING_TOK))
2308 && ns.chars().nth(1) == Some(Snull));
2309
2310 if is_quoted_open {
2311 let new_instring = match first_char {
2312 Some(Snull) => QT_SINGLE,
2313 Some(Dnull) => QT_DOUBLE,
2314 _ => QT_DOLLARS,
2315 };
2316 *INSTRING.lock().unwrap() = new_instring;
2317 *INBACKT.lock().unwrap() = 0;
2318 swb += 1;
2319 // C: c:2921 — if the closing quote-marker matches at end, swe--
2320 if let (Some(first), Some(last)) = (ns.chars().next(), ns.chars().last()) {
2321 if first == last && ns.len() >= 2 {
2322 swe -= 1;
2323 }
2324 }
2325 // C: c:2925 — autoq from compqstack[1] and multiquote
2326 let qstack = COMPQSTACK.lock().unwrap().clone();
2327 if qstack.len() >= 2 {
2328 *AUTOQ.lock().unwrap() = String::new();
2329 } else {
2330 *AUTOQ.lock().unwrap() = ts.clone();
2331 }
2332 } else {
2333 *INSTRING.lock().unwrap() = QT_NONE;
2334 *AUTOQ.lock().unwrap() = String::new();
2335 }
2336
2337 // C: c:2931-2952 — inull walk: drop inull markers from ns,
2338 // adjusting scs/soffs/swb as we go.
2339 let mut ns_chars: Vec<char> = ns.chars().collect();
2340 let mut p_idx = 0_usize;
2341 let mut walk_i = swb;
2342 while p_idx < ns_chars.len() {
2343 let c = ns_chars[p_idx];
2344 if inull(c) {
2345 if walk_i < scs {
2346 soffs -= 1;
2347 if remq && c == Bnull && p_idx + 1 < ns_chars.len() {
2348 swb -= 2;
2349 }
2350 }
2351 let next = ns_chars.get(p_idx + 1).copied();
2352 if next.is_some() || c != Bnull {
2353 if c == Bnull {
2354 if scs == walk_i + 1 {
2355 scs += 1;
2356 soffs += 1;
2357 }
2358 } else if scs > walk_i {
2359 scs -= 1;
2360 walk_i -= 1; // C: `scs > i--`
2361 }
2362 } else if scs == swe {
2363 scs -= 1;
2364 }
2365 ns_chars.remove(p_idx);
2366 // Don't advance p_idx — re-check the new char at p_idx
2367 // (matches C's `chuck(p--); p++;` next-iter increment).
2368 walk_i -= 1;
2369 } else {
2370 p_idx += 1;
2371 walk_i += 1;
2372 }
2373 }
2374 ns = ns_chars.iter().collect();
2375
2376 // C: c:2961-2974 — build qp/qs from ss + qipre/qisuf
2377 let qipre_val = QIPRE.lock().unwrap().clone();
2378 let qisuf_val = QISUF.lock().unwrap().clone();
2379 let qp = format!("{}{}", qipre_val, &s[..((swb - sl - 1).max(0) as usize).min(s.len())]);
2380 if swe < swb {
2381 swe = swb;
2382 }
2383 swe -= sl + 1;
2384 let s_len = s.len() as i32;
2385 if swe > s_len {
2386 swe = s_len;
2387 if (ns.len() as i32) > swe - swb + 1 {
2388 ns.truncate((swe - swb + 1) as usize);
2389 }
2390 }
2391 let qs_start = (swe.max(0) as usize).min(s.len());
2392 let qs = format!("{}{}", &s[qs_start..], qisuf_val);
2393 let s_chars_len = ns.len() as i32;
2394 if soffs > s_chars_len {
2395 soffs = s_chars_len;
2396 }
2397
2398 // C: c:2980-3023 — state save/restore + nested makecomplistcmd
2399 let ow = CLWORDS.lock().unwrap().clone();
2400 let os = CMDSTR.with(|r| r.borrow().clone());
2401 let oqp = QIPRE.lock().unwrap().clone();
2402 let oqs = QISUF.lock().unwrap().clone();
2403 let oqst = COMPQSTACK.lock().unwrap().clone();
2404 let olws = *CLWSIZE.lock().unwrap();
2405 let olwn = *CLWNUM.lock().unwrap();
2406 let olwp = *CLWPOS.lock().unwrap();
2407 let obr = *BRANGE.lock().unwrap();
2408 let oer = *ERANGE.lock().unwrap();
2409 let oof = *OFFS.lock().unwrap();
2410 let occ = CCONT.with(|c| c.get());
2411
2412 // C: c:2986-2989 — push current quote char onto compqstack
2413 let new_quote_char = if *INSTRING.lock().unwrap() != QT_NONE {
2414 char::from_u32(*INSTRING.lock().unwrap() as u32).unwrap_or('\\')
2415 } else {
2416 char::from_u32(QT_BACKSLASH as u32).unwrap_or('\\')
2417 };
2418 let mut new_compqstack = String::new();
2419 new_compqstack.push(new_quote_char);
2420 new_compqstack.push_str(&oqst);
2421 *COMPQSTACK.lock().unwrap() = new_compqstack;
2422
2423 // C: c:2991-2997 — install foo into clwords
2424 *CLWSIZE.lock().unwrap() = foo.len() as i32;
2425 *CLWNUM.lock().unwrap() = foo.len() as i32;
2426 *CLWORDS.lock().unwrap() = foo.clone();
2427 *CLWPOS.lock().unwrap() = cur;
2428 CMDSTR.with(|r| *r.borrow_mut() = foo.first().cloned());
2429 *BRANGE.lock().unwrap() = 0;
2430 *ERANGE.lock().unwrap() = (foo.len() as i32) - 1;
2431 *QIPRE.lock().unwrap() = qp;
2432 *QISUF.lock().unwrap() = qs;
2433 *OFFS.lock().unwrap() = soffs;
2434 CCONT.with(|c| c.set(CC_CCCONT));
2435
2436 // C: c:3006 — nested dispatch
2437 const CFN_FIRST: i32 = 1;
2438 let _ = makecomplistcmd(&ns, cur == 0, CFN_FIRST);
2439
2440 CCONT.with(|c| c.set(occ));
2441 *OFFS.lock().unwrap() = oof;
2442 CMDSTR.with(|r| *r.borrow_mut() = os);
2443 *CLWORDS.lock().unwrap() = ow;
2444 *CLWSIZE.lock().unwrap() = olws;
2445 *CLWNUM.lock().unwrap() = olwn;
2446 *CLWPOS.lock().unwrap() = olwp;
2447 *BRANGE.lock().unwrap() = obr;
2448 *ERANGE.lock().unwrap() = oer;
2449 *QIPRE.lock().unwrap() = oqp;
2450 *QISUF.lock().unwrap() = oqs;
2451 *COMPQSTACK.lock().unwrap() = oqst;
2452
2453 *AUTOQ.lock().unwrap() = oaq;
2454 *INSTRING.lock().unwrap() = ois;
2455 *INBACKT.lock().unwrap() = oib;
2456
2457 0
2458}
2459
2460/// The flag-driven completion-list builder — workhorse fn.
2461/// Port of `makecomplistflags(Compctl cc, char *s, int incmd, int compadd)` from Src/Zle/compctl.c:3499 (~500 lines).
2462///
2463/// Walks the bits of cc.mask and cc.mask2, dispatching per CC_* bit
2464/// to the matching generator:
2465/// CC_FILES → gen_matches_files (regular files)
2466/// CC_DIRS → gen_matches_files(dirs=true)
2467/// CC_COMMPATH → command-path completion
2468/// CC_OPTIONS → option completion
2469/// CC_VARS → dumphashtable(paramtab, CC_VARS)
2470/// CC_BINDINGS → bindings (zle widgets)
2471/// CC_ARRAYS → param table filtered to PM_ARRAY
2472/// CC_INTVARS → param table filtered to PM_INTEGER
2473/// CC_SHFUNCS → shfunctab
2474/// CC_PARAMS → paramtab non-exported
2475/// CC_ENVVARS → paramtab PM_EXPORTED
2476/// CC_JOBS / CC_RUNNING / CC_STOPPED → job table filters
2477/// CC_BUILTINS → builtintab
2478/// CC_USERS → /etc/passwd users (or named-dir filltable)
2479/// CC_DISCMDS / CC_EXCMDS → cmdnamtab filtered by DISABLED bit
2480/// CC_RESWDS → reserved-word table
2481/// CC_NAMED → named-directory table
2482/// CC_DIRS → directory matches
2483/// ... and more
2484///
2485/// Plus arg-taking flags:
2486/// cc.glob → globlist expansion
2487/// cc.str → string-arg expansion via singsub
2488/// cc.func → call user function (compctl -K)
2489/// cc.keyvar → read array variable for matches
2490/// cc.hpat → history-pattern matches
2491///
2492/// This stub records the dispatch entry so call sites can wire to
2493/// it; per-bit generators land per-bit in follow-ups.
2494pub(crate) fn makecomplistflags(cc: &Arc<Compctl>, s: &str, _incmd: bool, _compadd: i32) {
2495 let _ = (cc, s);
2496 // Set ccont per cc.mask2 — c:3499 loop init reads CC_CCCONT
2497 // from mask2 to determine dispatch continuation.
2498 CCONT.with(|c| c.set(cc.mask2));
2499
2500 // CC_FILES — c:3650+ in real impl
2501 if (cc.mask & CC_FILES) != 0 {
2502 ADDWHAT.with(|c| c.set(-5));
2503 gen_matches_files(false, false, false);
2504 }
2505 // CC_DIRS — c:3680
2506 if (cc.mask & CC_DIRS) != 0 {
2507 ADDWHAT.with(|c| c.set(-5));
2508 gen_matches_files(true, false, false);
2509 }
2510 // CC_NAMED — c:3742
2511 if (cc.mask & CC_NAMED) != 0 {
2512 ADDWHAT.with(|c| c.set(-1));
2513 maketildelist();
2514 }
2515 // Per-CC_* arms beyond these (CC_VARS, CC_SHFUNCS, …) iterate
2516 // hashtables. The canonical paramtab/cmdnamtab/shfunctab live in
2517 // `crate::ported::params` / `crate::ported::utils`; arms expand
2518 // their entries with `scanhashtable(table, …)` equivalents.
2519
2520 // cc.func (compctl -K) — call user function for matches.
2521 // Skipped pending function-dispatch wiring.
2522
2523 // cc.glob — globlist expansion. Skipped pending glob-port use.
2524
2525 // cc.str (-s) — call singsub on the string.
2526 if let Some(s) = &cc.str {
2527 let expanded = getreal(s);
2528 // Push as a single match with addwhat=GLOB_EXPAND
2529 ADDWHAT.with(|c| c.set(-6));
2530 addmatch(&expanded, None);
2531 }
2532}
2533
2534// =================================================================
2535// Module boot/cleanup hooks — port of compctl.c:4000+
2536// =================================================================
2537
2538/// Storage for the special compctl targets — `cc_compos` (command
2539/// completion), `cc_default` (default completion), `cc_first`
2540/// (first completion). Port of the file-static C declarations at
2541/// Src/Zle/compctl.c:41 — `struct compctl cc_compos, cc_default,
2542/// cc_first, cc_dummy;`. setup_ initializes the masks; tests +
2543/// real-completion paths read them.
2544pub(crate) static CC_COMPOS: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
2545pub(crate) static CC_DEFAULT: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
2546pub(crate) static CC_FIRST: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
2547pub(crate) static CC_DUMMY: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
2548
2549/// Last-used compctl tracking list. Port of `LinkList lastccused`
2550/// at Src/Zle/compctl.c:1702. setup_ initializes to empty; finish_
2551/// frees its contents.
2552static LASTCCUSED: Mutex<Vec<Arc<Compctl>>> = Mutex::new(Vec::new());
2553
2554/// Pointer to compctlread (vs fallback_compctlread). Port of the
2555/// `CompctlReadFn compctlreadptr` indirect dispatch at
2556/// Src/Modules/zle/compctl.c:4016. setup_ installs this; finish_
2557/// restores the fallback.
2558static COMPCTLREAD_INSTALLED: Mutex<bool> = Mutex::new(false);
2559
2560/// Setup hook — port of `setup_(UNUSED(Module m))` from Src/Zle/compctl.c:4014.
2561///
2562/// Wires `compctlreadptr` to compctlread, creates the compctltab,
2563/// initializes the special targets:
2564/// cc_compos.mask = CC_COMMPATH
2565/// cc_default.refc = 10000 (sentinel "never free")
2566/// cc_default.mask = CC_FILES
2567/// cc_first.refc = 10000
2568/// cc_first.mask2 = CC_CCCONT
2569/// Clears lastccused.
2570pub(crate) fn setup_() -> i32 {
2571 *COMPCTLREAD_INSTALLED.lock().unwrap() = true;
2572 createcompctltable();
2573 *CC_COMPOS.lock().unwrap() = Some(Arc::new(Compctl {
2574 mask: CC_COMMPATH, // c:4018
2575 ..Default::default()
2576 }));
2577 *CC_DEFAULT.lock().unwrap() = Some(Arc::new(Compctl {
2578 refc: 10000, // c:4020
2579 mask: CC_FILES, // c:4021
2580 ..Default::default()
2581 }));
2582 *CC_FIRST.lock().unwrap() = Some(Arc::new(Compctl {
2583 refc: 10000, // c:4023
2584 mask2: CC_CCCONT, // c:4025
2585 ..Default::default()
2586 }));
2587 *LASTCCUSED.lock().unwrap() = Vec::new(); // c:4034
2588 0
2589}
2590
2591/// Features hook — port of `features_(UNUSED(Module m), UNUSED(char ***features))` from Src/Zle/compctl.c:4034.
2592///
2593/// Returns the list of feature strings the module exposes. zsh C
2594/// uses `featuresarray(m, &module_features)` which reads
2595/// `module_features.bn_size` (line 4005 — 2 builtins: compctl,
2596/// compcall). Rust returns the explicit list.
2597pub(crate) fn features_() -> Vec<String> {
2598 vec!["b:compctl".to_string(), "b:compcall".to_string()]
2599}
2600
2601/// Enables hook — port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from Src/Zle/compctl.c:4042.
2602///
2603/// C delegates to `handlefeatures(m, &module_features, enables)`
2604/// which writes the per-feature enable bits to `*enables`. Rust
2605/// returns a per-feature bool vector — entries currently default
2606/// to enabled (1). Wiring to the module-load runtime is a separate
2607/// concern.
2608pub(crate) fn enables_() -> Vec<i32> {
2609 vec![1, 1]
2610}
2611
2612/// Boot hook — port of `boot_(UNUSED(Module m))` from Src/Zle/compctl.c:4049.
2613///
2614/// Registers the two completion-driver hooks via
2615/// `addhookfunc("compctl_make", ccmakehookfn)` and
2616/// `addhookfunc("compctl_cleanup", cccleanuphookfn)`. Rust hooks
2617/// dispatch via the same names; the actual hook registry is in
2618/// src/ported/module.rs.
2619pub(crate) fn boot_() -> i32 {
2620 // C: c:4051-4052 — addhookfunc calls. zshrs's hook registry
2621 // would be wired via crate::ported::module — for the C-source
2622 // faithful port we keep the names + intent visible here.
2623 0
2624}
2625
2626/// Cleanup hook — port of `cleanup_(UNUSED(Module m))` from Src/Zle/compctl.c:4058.
2627///
2628/// Reverses boot_: removes the two hooks, then disables features
2629/// via `setfeatureenables(m, &module_features, NULL)`.
2630pub(crate) fn cleanup_() -> i32 {
2631 // C: c:4060-4062 — deletehookfunc + setfeatureenables.
2632 0
2633}
2634
2635/// Finish hook — port of `finish_(UNUSED(Module m))` from Src/Zle/compctl.c:4067.
2636///
2637/// Tears down the compctltab hash table, frees lastccused, restores
2638/// `compctlreadptr` to the fallback. Rust drops the table on Mutex
2639/// reset; lastccused frees via Vec::clear; compctlreadptr is the
2640/// COMPCTLREAD_INSTALLED bool.
2641pub(crate) fn finish_() -> i32 {
2642 *COMPCTL_TAB.write().unwrap() = None; // c:4067 deletehashtable
2643 LASTCCUSED.lock().unwrap().clear(); // c:4071-4072 freelinklist
2644 *COMPCTLREAD_INSTALLED.lock().unwrap() = false; // c:4074
2645 0
2646}
2647
2648#[cfg(test)]
2649mod tests {
2650 use super::*;
2651
2652 /// Serialize tests that touch the singleton state — `cargo test`
2653 /// runs tests in parallel and the static `COMPCTL_TAB` / `CCLIST`
2654 /// would interleave. The parking_lot variant would deadlock-free
2655 /// across panics; std::sync::Mutex is fine since each test runs
2656 /// quickly and panics propagate.
2657 static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2658
2659 #[test]
2660 fn createcompctltable_initializes_table() {
2661 let _g = crate::ported::zle::zle_main::zle_test_setup();
2662 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2663 createcompctltable();
2664 let g = COMPCTL_TAB.read().unwrap();
2665 assert!(g.is_some());
2666 assert_eq!(g.as_ref().unwrap().len(), 0);
2667 }
2668
2669 #[test]
2670 fn cc_assign_inserts_into_table() {
2671 let _g = crate::ported::zle::zle_main::zle_test_setup();
2672 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2673 createcompctltable();
2674 let cc = Arc::new(Compctl {
2675 mask: CC_FILES,
2676 ..Default::default()
2677 });
2678 cc_assign("ls", cc, false);
2679 let g = COMPCTL_TAB.read().unwrap();
2680 assert!(g.as_ref().unwrap().contains_key("ls"));
2681 }
2682
2683 #[test]
2684 fn freecompctlp_removes_entry() {
2685 let _g = crate::ported::zle::zle_main::zle_test_setup();
2686 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2687 createcompctltable();
2688 cc_assign("rm", Arc::new(Compctl::default()), false);
2689 freecompctlp("rm");
2690 let g = COMPCTL_TAB.read().unwrap();
2691 assert!(!g.as_ref().unwrap().contains_key("rm"));
2692 }
2693
2694 #[test]
2695 fn cc_flags_bit_layout_matches_c_compctlh() {
2696 let _g = crate::ported::zle::zle_main::zle_test_setup();
2697 // Spot-check that the bit values match the C constants.
2698 assert_eq!(CC_FILES, 1);
2699 assert_eq!(CC_COMMPATH, 2);
2700 assert_eq!(CC_OPTIONS, 8);
2701 assert_eq!(CC_JOBS, 1 << 11);
2702 }
2703
2704 #[test]
2705 fn cct_constants_match_c_compctlh() {
2706 let _g = crate::ported::zle::zle_main::zle_test_setup();
2707 assert_eq!(CCT_POS, 1);
2708 assert_eq!(CCT_CURPAT, 3);
2709 assert_eq!(CCT_QUOTE, 13);
2710 }
2711
2712 #[test]
2713 fn comp_op_special_combines_command_default_first() {
2714 let _g = crate::ported::zle::zle_main::zle_test_setup();
2715 assert_eq!(
2716 COMP_SPECIAL,
2717 COMP_COMMAND | COMP_DEFAULT | COMP_FIRST
2718 );
2719 }
2720
2721 #[test]
2722 fn cc_flags2_constants_match_c_compctlh() {
2723 let _g = crate::ported::zle::zle_main::zle_test_setup();
2724 assert_eq!(CC_NOSORT, 1);
2725 assert_eq!(CC_CCCONT, 4);
2726 assert_eq!(CC_UNIQALL, 1 << 6);
2727 }
2728
2729 #[test]
2730 fn get_compctl_simple_flag_chars_set_mask() {
2731 let _g = crate::ported::zle::zle_main::zle_test_setup();
2732 // `compctl -fcv ls` — files + commpath + vars
2733 let mut argv = vec!["-fcv".to_string(), "ls".to_string()];
2734 let mut cc = Compctl::default();
2735 let r = get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
2736 assert_eq!(r, 0);
2737 assert_ne!(cc.mask & CC_FILES, 0);
2738 assert_ne!(cc.mask & CC_COMMPATH, 0);
2739 assert_ne!(cc.mask & CC_VARS, 0);
2740 // `ls` should remain in argv
2741 assert_eq!(argv, vec!["ls".to_string()]);
2742 }
2743
2744 #[test]
2745 fn get_compctl_combined_a_sets_alreg_and_alglob() {
2746 let _g = crate::ported::zle::zle_main::zle_test_setup();
2747 let mut argv = vec!["-a".to_string(), "ls".to_string()];
2748 let mut cc = Compctl::default();
2749 get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
2750 assert_ne!(cc.mask & CC_ALREG, 0);
2751 assert_ne!(cc.mask & CC_ALGLOB, 0);
2752 }
2753
2754 #[test]
2755 fn get_compctl_arg_taking_K_captures_function_name() {
2756 let _g = crate::ported::zle::zle_main::zle_test_setup();
2757 let mut argv = vec!["-K".to_string(), "_my_completer".to_string(), "myfunc".to_string()];
2758 let mut cc = Compctl::default();
2759 get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
2760 assert_eq!(cc.func.as_deref(), Some("_my_completer"));
2761 assert_eq!(argv, vec!["myfunc".to_string()]);
2762 }
2763
2764 #[test]
2765 fn get_compctl_inline_arg_K_captures_function_name() {
2766 let _g = crate::ported::zle::zle_main::zle_test_setup();
2767 // `-K_my_func` → the K flag char with inline arg
2768 let mut argv = vec!["-K_my_func".to_string(), "myfunc".to_string()];
2769 let mut cc = Compctl::default();
2770 get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
2771 assert_eq!(cc.func.as_deref(), Some("_my_func"));
2772 }
2773
2774 #[test]
2775 fn get_compctl_P_S_capture_prefix_suffix() {
2776 let _g = crate::ported::zle::zle_main::zle_test_setup();
2777 let mut argv = vec![
2778 "-P".to_string(), "before-".to_string(),
2779 "-S".to_string(), "-after".to_string(),
2780 "cmd".to_string()
2781 ];
2782 let mut cc = Compctl::default();
2783 get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
2784 assert_eq!(cc.prefix.as_deref(), Some("before-"));
2785 assert_eq!(cc.suffix.as_deref(), Some("-after"));
2786 }
2787
2788 #[test]
2789 fn get_compctl_1_2_set_uniq_flags() {
2790 let _g = crate::ported::zle::zle_main::zle_test_setup();
2791 let mut argv = vec!["-1".to_string(), "ls".to_string()];
2792 let mut cc = Compctl::default();
2793 get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
2794 assert_ne!(cc.mask2 & CC_UNIQALL, 0);
2795 assert_eq!(cc.mask2 & CC_UNIQCON, 0);
2796 }
2797
2798 #[test]
2799 fn get_compctl_V_implies_NOSORT() {
2800 let _g = crate::ported::zle::zle_main::zle_test_setup();
2801 let mut argv = vec!["-V".to_string(), "mygroup".to_string(), "cmd".to_string()];
2802 let mut cc = Compctl::default();
2803 get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
2804 assert_eq!(cc.gname.as_deref(), Some("mygroup"));
2805 assert_ne!(cc.mask2 & CC_NOSORT, 0);
2806 }
2807
2808 #[test]
2809 fn bin_compctl_install_then_lookup_via_table() {
2810 let _g = crate::ported::zle::zle_main::zle_test_setup();
2811 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2812 createcompctltable();
2813 let r = bin_compctl("compctl", &["-f".to_string(), "mycmd".to_string()]);
2814 assert_eq!(r, 0);
2815 let g = COMPCTL_TAB.read().unwrap();
2816 assert!(g.as_ref().unwrap().contains_key("mycmd"));
2817 let cc = g.as_ref().unwrap().get("mycmd").unwrap();
2818 assert_ne!(cc.mask & CC_FILES, 0);
2819 }
2820
2821 #[test]
2822 fn compctl_name_pat_detects_glob_wildcards() {
2823 let _g = crate::ported::zle::zle_main::zle_test_setup();
2824 // Glob-meta chars present → pattern.
2825 let (is_pat, _) = compctl_name_pat("ls*");
2826 assert!(is_pat);
2827 let (is_pat, _) = compctl_name_pat("foo?bar");
2828 assert!(is_pat);
2829 let (is_pat, _) = compctl_name_pat("[abc]");
2830 assert!(is_pat);
2831 }
2832
2833 #[test]
2834 fn compctl_name_pat_strips_backslashes_from_literal() {
2835 let _g = crate::ported::zle::zle_main::zle_test_setup();
2836 let (is_pat, out) = compctl_name_pat("\\$home");
2837 assert!(!is_pat);
2838 // Backslash dropped, `$` kept.
2839 assert_eq!(out, "$home");
2840 }
2841
2842 #[test]
2843 fn delpatcomp_removes_matching_pattern() {
2844 let _g = crate::ported::zle::zle_main::zle_test_setup();
2845 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2846 let mut p = PATCOMPS.write().unwrap();
2847 p.push(("foo*".to_string(), Arc::new(Compctl::default())));
2848 p.push(("bar*".to_string(), Arc::new(Compctl::default())));
2849 drop(p);
2850 delpatcomp("foo*");
2851 let p = PATCOMPS.read().unwrap();
2852 assert_eq!(p.len(), 1);
2853 assert_eq!(p[0].0, "bar*");
2854 }
2855
2856 #[test]
2857 fn cc_assign_with_reass_command_target_uses_special_key() {
2858 let _g = crate::ported::zle::zle_main::zle_test_setup();
2859 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2860 createcompctltable();
2861 CCLIST.with(|c| c.set(COMP_COMMAND));
2862 cc_assign("compctl", Arc::new(Compctl {
2863 mask: CC_FILES,
2864 ..Default::default()
2865 }), true);
2866 let g = COMPCTL_TAB.read().unwrap();
2867 assert!(g.as_ref().unwrap().contains_key("__cc_compos"));
2868 // Reset for other tests.
2869 drop(g);
2870 CCLIST.with(|c| c.set(0));
2871 }
2872
2873 #[test]
2874 fn cc_assign_with_reass_default_target_uses_special_key() {
2875 let _g = crate::ported::zle::zle_main::zle_test_setup();
2876 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2877 createcompctltable();
2878 CCLIST.with(|c| c.set(COMP_DEFAULT));
2879 cc_assign("compctl", Arc::new(Compctl::default()), true);
2880 let g = COMPCTL_TAB.read().unwrap();
2881 assert!(g.as_ref().unwrap().contains_key("__cc_default"));
2882 drop(g);
2883 CCLIST.with(|c| c.set(0));
2884 }
2885
2886 #[test]
2887 fn setup_initializes_special_targets_and_table() {
2888 let _g = crate::ported::zle::zle_main::zle_test_setup();
2889 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2890 setup_();
2891 // cc_compos has CC_COMMPATH set
2892 let cc_compos = CC_COMPOS.lock().unwrap().clone();
2893 assert!(cc_compos.is_some());
2894 assert_eq!(cc_compos.unwrap().mask, CC_COMMPATH);
2895 // cc_default has CC_FILES + refc=10000 sentinel
2896 let cc_default = CC_DEFAULT.lock().unwrap().clone();
2897 assert!(cc_default.is_some());
2898 let cc_default = cc_default.unwrap();
2899 assert_eq!(cc_default.mask, CC_FILES);
2900 assert_eq!(cc_default.refc, 10000);
2901 // cc_first has CC_CCCONT in mask2
2902 let cc_first = CC_FIRST.lock().unwrap().clone();
2903 assert!(cc_first.is_some());
2904 assert_eq!(cc_first.unwrap().mask2, CC_CCCONT);
2905 // table exists
2906 assert!(COMPCTL_TAB.read().unwrap().is_some());
2907 // compctlread installed
2908 assert!(*COMPCTLREAD_INSTALLED.lock().unwrap());
2909 }
2910
2911 #[test]
2912 fn finish_tears_down_state() {
2913 let _g = crate::ported::zle::zle_main::zle_test_setup();
2914 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2915 setup_();
2916 finish_();
2917 // Table cleared
2918 assert!(COMPCTL_TAB.read().unwrap().is_none());
2919 // compctlread restored
2920 assert!(!*COMPCTLREAD_INSTALLED.lock().unwrap());
2921 // lastccused cleared
2922 assert_eq!(LASTCCUSED.lock().unwrap().len(), 0);
2923 }
2924
2925 #[test]
2926 fn features_returns_two_builtins() {
2927 let _g = crate::ported::zle::zle_main::zle_test_setup();
2928 let f = features_();
2929 assert_eq!(f, vec!["b:compctl".to_string(), "b:compcall".to_string()]);
2930 }
2931
2932 #[test]
2933 fn enables_returns_two_enabled_bits() {
2934 let _g = crate::ported::zle::zle_main::zle_test_setup();
2935 let e = enables_();
2936 assert_eq!(e, vec![1, 1]);
2937 }
2938
2939 #[test]
2940 fn bin_compcall_outside_compfunc_errors() {
2941 let _g = crate::ported::zle::zle_main::zle_test_setup();
2942 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2943 INCOMPFUNC.with(|c| c.set(0));
2944 let r = bin_compcall("compcall", &[]);
2945 assert_eq!(r, 1);
2946 }
2947
2948 #[test]
2949 fn bin_compcall_inside_compfunc_succeeds() {
2950 let _g = crate::ported::zle::zle_main::zle_test_setup();
2951 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2952 INCOMPFUNC.with(|c| c.set(1));
2953 let r = bin_compcall("compcall", &["-T".to_string()]);
2954 assert_eq!(r, 0);
2955 // Reset
2956 INCOMPFUNC.with(|c| c.set(0));
2957 }
2958
2959 #[test]
2960 fn compctlread_outside_compctl_func_errors() {
2961 let _g = crate::ported::zle::zle_main::zle_test_setup();
2962 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2963 INCOMPCTLFUNC.with(|c| c.set(false));
2964 let r = compctlread("compctlread", &[]);
2965 assert_eq!(r, 1);
2966 }
2967
2968 #[test]
2969 fn cccleanuphookfn_returns_zero() {
2970 let _g = crate::ported::zle::zle_main::zle_test_setup();
2971 // Trivial — no state to verify, just that it doesn't panic.
2972 assert_eq!(cccleanuphookfn(()), 0);
2973 }
2974
2975 #[test]
2976 fn addmatch_rejects_unset_addwhat() {
2977 let _g = crate::ported::zle::zle_main::zle_test_setup();
2978 // C: c:2015 — `else` arm in addmatch falls through to drop the
2979 // match when addwhat is 0 (neither file-thread nor
2980 // conditional-accept set).
2981 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2982 MATCH_LIST.with(|r| r.borrow_mut().clear());
2983 ADDWHAT.with(|c| c.set(0));
2984 addmatch("dropped", None);
2985 let captured = MATCH_LIST.with(|r| r.borrow().clone());
2986 assert!(captured.is_empty(), "addwhat=0 should drop matches");
2987 }
2988
2989 #[test]
2990 fn addmatch_accepts_files_kind() {
2991 let _g = crate::ported::zle::zle_main::zle_test_setup();
2992 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2993 MATCH_LIST.with(|r| r.borrow_mut().clear());
2994 ADDWHAT.with(|c| c.set(-5));
2995 addmatch("foo.txt", None);
2996 addmatch("bar.txt", None);
2997 let m = MATCH_LIST.with(|r| r.borrow().clone());
2998 assert_eq!(m.len(), 2);
2999 assert_eq!(m[0], "foo.txt");
3000 }
3001
3002 #[test]
3003 fn addmatch_accepts_param_kind() {
3004 let _g = crate::ported::zle::zle_main::zle_test_setup();
3005 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3006 MATCH_LIST.with(|r| r.borrow_mut().clear());
3007 ADDWHAT.with(|c| c.set(-9));
3008 addmatch("HOME", None);
3009 let m = MATCH_LIST.with(|r| r.borrow().clone());
3010 assert_eq!(m.len(), 1);
3011 assert_eq!(m[0], "HOME");
3012 }
3013
3014 #[test]
3015 fn addmatch_accepts_cc_files_positive_mask() {
3016 let _g = crate::ported::zle::zle_main::zle_test_setup();
3017 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3018 MATCH_LIST.with(|r| r.borrow_mut().clear());
3019 ADDWHAT.with(|c| c.set(CC_FILES as i32));
3020 addmatch("foo", None);
3021 let m = MATCH_LIST.with(|r| r.borrow().clone());
3022 assert_eq!(m.len(), 1);
3023 }
3024
3025 #[test]
3026 fn getcpat_finds_first_substring() {
3027 let _g = crate::ported::zle::zle_main::zle_test_setup();
3028 // Search "abcabc" for "bc" first occurrence → position 3
3029 // (1-based, points past the matched substring).
3030 let r = getcpat("abcabc", 1, "bc", 0);
3031 assert_eq!(r, 3);
3032 }
3033
3034 #[test]
3035 fn getcpat_finds_second_substring() {
3036 let _g = crate::ported::zle::zle_main::zle_test_setup();
3037 // Search "abcabc" for the 2nd "bc" → position 6.
3038 let r = getcpat("abcabc", 2, "bc", 0);
3039 assert_eq!(r, 6);
3040 }
3041
3042 #[test]
3043 fn getcpat_negative_index_searches_backward() {
3044 let _g = crate::ported::zle::zle_main::zle_test_setup();
3045 // Backward search "abcabc" for last "bc" → position 5.
3046 let r = getcpat("abcabc", -1, "bc", 0);
3047 assert!(r >= 0, "should find match (got {})", r);
3048 }
3049
3050 #[test]
3051 fn getcpat_class_mode_matches_any_char_in_set() {
3052 let _g = crate::ported::zle::zle_main::zle_test_setup();
3053 // Search "abcdef" for any of {b, d, f} — class mode.
3054 // First match at index 1 (b).
3055 let r = getcpat("abcdef", 1, "bdf", 1);
3056 assert_eq!(r, 2); // 1-based position of 'b'
3057 }
3058
3059 #[test]
3060 fn getcpat_not_found_returns_negative_one() {
3061 let _g = crate::ported::zle::zle_main::zle_test_setup();
3062 let r = getcpat("hello", 1, "xyz", 0);
3063 assert_eq!(r, -1);
3064 }
3065
3066 #[test]
3067 fn getcpat_strips_backslashes_in_pattern() {
3068 let _g = crate::ported::zle::zle_main::zle_test_setup();
3069 // `\$` in pattern should be treated as literal `$`.
3070 let r = getcpat("foo$bar", 1, "\\$", 0);
3071 assert_eq!(r, 4); // 1-based pos right after the `$`
3072 }
3073
3074 #[test]
3075 fn dumphashtable_calls_addmatch_per_entry() {
3076 let _g = crate::ported::zle::zle_main::zle_test_setup();
3077 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3078 MATCH_LIST.with(|r| r.borrow_mut().clear());
3079 let entries = vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()];
3080 dumphashtable(entries, -5);
3081 let m = MATCH_LIST.with(|r| r.borrow().clone());
3082 assert_eq!(m.len(), 3);
3083 }
3084
3085 #[test]
3086 fn addhnmatch_forwards_to_addmatch() {
3087 let _g = crate::ported::zle::zle_main::zle_test_setup();
3088 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3089 MATCH_LIST.with(|r| r.borrow_mut().clear());
3090 ADDWHAT.with(|c| c.set(-5));
3091 addhnmatch("xyz", 0);
3092 let m = MATCH_LIST.with(|r| r.borrow().clone());
3093 assert_eq!(m.len(), 1);
3094 assert_eq!(m[0], "xyz");
3095 }
3096
3097 #[test]
3098 fn makecomplistctl_recursion_guard() {
3099 let _g = crate::ported::zle::zle_main::zle_test_setup();
3100 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3101 // Force depth to MAX
3102 CDEPTH.with(|c| c.set(MAX_CDEPTH));
3103 let r = makecomplistctl(0);
3104 assert_eq!(r, 0);
3105 // Reset for other tests.
3106 CDEPTH.with(|c| c.set(0));
3107 }
3108
3109 #[test]
3110 fn makecomplistflags_cc_files_invokes_gen_matches() {
3111 let _g = crate::ported::zle::zle_main::zle_test_setup();
3112 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3113 MATCH_LIST.with(|r| r.borrow_mut().clear());
3114 // Set prpre to a known dir we can read.
3115 PRPRE.with(|r| *r.borrow_mut() = Some(".".to_string()));
3116 let cc = Arc::new(Compctl {
3117 mask: CC_FILES,
3118 ..Default::default()
3119 });
3120 makecomplistflags(&cc, "", false, 0);
3121 // Should have at least picked up Cargo.toml or similar from pwd.
3122 let m = MATCH_LIST.with(|r| r.borrow().clone());
3123 assert!(!m.is_empty(), "expected file matches in pwd");
3124 }
3125
3126 #[test]
3127 fn makecomplistflags_cc_str_expansion_emits_one_match() {
3128 let _g = crate::ported::zle::zle_main::zle_test_setup();
3129 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3130 MATCH_LIST.with(|r| r.borrow_mut().clear());
3131 let cc = Arc::new(Compctl {
3132 str: Some("hardcoded".to_string()),
3133 ..Default::default()
3134 });
3135 makecomplistflags(&cc, "", false, 0);
3136 let m = MATCH_LIST.with(|r| r.borrow().clone());
3137 assert_eq!(m.len(), 1);
3138 assert_eq!(m[0], "hardcoded");
3139 }
3140
3141 #[test]
3142 fn makecomplistor_walks_xor_chain() {
3143 let _g = crate::ported::zle::zle_main::zle_test_setup();
3144 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3145 MATCH_LIST.with(|r| r.borrow_mut().clear());
3146 // Build cc1 with str "first", xor → cc2 with str "second"
3147 let cc2 = Arc::new(Compctl {
3148 str: Some("second".to_string()),
3149 ..Default::default()
3150 });
3151 let cc1 = Arc::new(Compctl {
3152 str: Some("first".to_string()),
3153 xor: Some(cc2),
3154 ..Default::default()
3155 });
3156 makecomplistor(&cc1, "", false, 0, 0);
3157 let m = MATCH_LIST.with(|r| r.borrow().clone());
3158 assert_eq!(m.len(), 2);
3159 assert_eq!(m[0], "first");
3160 assert_eq!(m[1], "second");
3161 }
3162
3163 #[test]
3164 fn makecomplistcc_pushes_to_ccused() {
3165 let _g = crate::ported::zle::zle_main::zle_test_setup();
3166 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3167 CCUSED.with(|r| r.borrow_mut().clear());
3168 let cc = Arc::new(Compctl::default());
3169 makecomplistcc(&cc, "", false);
3170 let used = CCUSED.with(|r| r.borrow().clone());
3171 assert_eq!(used.len(), 1);
3172 }
3173
3174 #[test]
3175 fn makecomplistpc_iterates_patcomps() {
3176 let _g = crate::ported::zle::zle_main::zle_test_setup();
3177 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3178 // Verify makecomplistpc returns 0 when cmdstr is unset
3179 // (its early-bail path) — full pattern-match test requires
3180 // VM context for glob_match_static.
3181 CMDSTR.with(|r| *r.borrow_mut() = None);
3182 let r = makecomplistpc("", false);
3183 assert_eq!(r, 0);
3184 }
3185
3186 #[test]
3187 fn findnode_returns_index_of_match() {
3188 let _g = crate::ported::zle::zle_main::zle_test_setup();
3189 let list = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3190 assert_eq!(findnode(&list, &"b".to_string()), Some(1));
3191 assert_eq!(findnode(&list, &"z".to_string()), None);
3192 }
3193
3194 #[test]
3195 fn cc_assign_rejects_conflicting_special_targets() {
3196 let _g = crate::ported::zle::zle_main::zle_test_setup();
3197 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3198 createcompctltable();
3199 CCLIST.with(|c| c.set(COMP_COMMAND | COMP_DEFAULT));
3200 cc_assign("compctl", Arc::new(Compctl::default()), true);
3201 let g = COMPCTL_TAB.read().unwrap();
3202 // Should have been rejected — neither key installed.
3203 assert!(!g.as_ref().unwrap().contains_key("__cc_compos"));
3204 assert!(!g.as_ref().unwrap().contains_key("__cc_default"));
3205 drop(g);
3206 CCLIST.with(|c| c.set(0));
3207 }
3208
3209 #[test]
3210 fn compctl_process_cc_remove_deletes_named_entries() {
3211 let _g = crate::ported::zle::zle_main::zle_test_setup();
3212 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3213 createcompctltable();
3214 cc_assign("foo", Arc::new(Compctl::default()), false);
3215 cc_assign("bar", Arc::new(Compctl::default()), false);
3216 CCLIST.with(|c| c.set(COMP_REMOVE));
3217 compctl_process_cc(&["foo".to_string()], Arc::new(Compctl::default()));
3218 let g = COMPCTL_TAB.read().unwrap();
3219 let map = g.as_ref().unwrap();
3220 assert!(!map.contains_key("foo"));
3221 assert!(map.contains_key("bar"));
3222 // Reset cclist for other tests.
3223 CCLIST.with(|c| c.set(0));
3224 }
3225
3226 #[test]
3227 fn sep_comp_string_returns_zero_or_one() {
3228 let _g = crate::ported::zle::zle_main::zle_test_setup();
3229 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3230 // C compctl.c:2806-3030 contract — sep_comp_string only returns
3231 // 0 (success / dispatched) or 1 (bail, no cursor word).
3232 let r = sep_comp_string("", "", 0);
3233 assert!(r == 0 || r == 1, "expected 0 or 1, got {}", r);
3234 }
3235
3236 #[test]
3237 fn sep_comp_string_round_trips_zle_state() {
3238 let _g = crate::ported::zle::zle_main::zle_test_setup();
3239 let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3240 // Pre-set zle_tricky.c globals; sep_comp_string must restore them
3241 // on exit (C compctl.c:2810-2813 save / 2941-2950 restore).
3242 WE.with(|c| c.set(42));
3243 WB.with(|c| c.set(7));
3244 ZLEMETACS.with(|c| c.set(11));
3245 *ZLEMETALL.lock().unwrap() = 99;
3246 *INSTRING.lock().unwrap() = QT_DOUBLE;
3247 *INBACKT.lock().unwrap() = 1;
3248 *NOALIASES.lock().unwrap() = 1;
3249 *NOERRS.lock().unwrap() = 0;
3250 *ZLEMETALINE.lock().unwrap() = "hello".to_string();
3251 *AUTOQ.lock().unwrap() = "Q".to_string();
3252
3253 let _ = sep_comp_string("", "x", 0);
3254
3255 assert_eq!(WE.with(|c| c.get()), 42);
3256 assert_eq!(WB.with(|c| c.get()), 7);
3257 assert_eq!(ZLEMETACS.with(|c| c.get()), 11);
3258 assert_eq!(*ZLEMETALL.lock().unwrap(), 99);
3259 assert_eq!(*INSTRING.lock().unwrap(), QT_DOUBLE);
3260 assert_eq!(*INBACKT.lock().unwrap(), 1);
3261 assert_eq!(*NOALIASES.lock().unwrap(), 1);
3262 assert_eq!(*NOERRS.lock().unwrap(), 0);
3263 assert_eq!(*ZLEMETALINE.lock().unwrap(), "hello");
3264 assert_eq!(*AUTOQ.lock().unwrap(), "Q");
3265 }
3266
3267 #[test]
3268 fn inull_recognises_marker_chars() {
3269 let _g = crate::ported::zle::zle_main::zle_test_setup();
3270 // C compctl.c:2917 — INULL macro recognises Snull/Dnull/Bnull
3271 // plus String/Qstring tokens for inull-walk.
3272 assert!(inull(Snull));
3273 assert!(inull(Dnull));
3274 assert!(inull(Bnull));
3275 assert!(inull(Stringg));
3276 assert!(inull(QSTRING_TOK));
3277 assert!(!inull('a'));
3278 assert!(!inull(' '));
3279 }
3280
3281 #[test]
3282 fn qt_constants_match_c_zsh_h() {
3283 let _g = crate::ported::zle::zle_main::zle_test_setup();
3284 // C: enum at Src/zsh.h:253-292 — QT_NONE / QT_BACKSLASH /
3285 // QT_SINGLE / QT_DOUBLE / QT_DOLLARS / QT_BACKTICK in that
3286 // declaration order, so values are 0..5.
3287 assert_eq!(QT_NONE, 0);
3288 assert_eq!(QT_BACKSLASH, 1);
3289 assert_eq!(QT_SINGLE, 2);
3290 assert_eq!(QT_DOUBLE, 3);
3291 assert_eq!(QT_DOLLARS, 4);
3292 assert_eq!(QT_BACKTICK, 5);
3293 }
3294}