Skip to main content

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 ported. This file ports the type definitions, constants,
11//! and simpler free ported first; large ported (`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 crate::ported::builtin::findcmd;
22use crate::ported::pattern::{patcompile, pattry};
23use crate::ported::utils::errflag;
24use crate::ported::zle::comp_h::{Aminfo, Cmlist};
25use crate::ported::zle::compctl_h::{
26    Compcond, CompcondData, Compctl, CCT_CURPAT, CCT_CURPRE, CCT_CURSTR, CCT_CURSUB, CCT_CURSUBC,
27    CCT_CURSUF, CCT_NUMWORDS, CCT_POS, CCT_QUOTE, CCT_RANGEPAT, CCT_RANGESTR, CCT_WORDPAT,
28    CCT_WORDSTR, CC_ALGLOB, CC_ALREG, CC_ARRAYS, CC_BINDINGS, CC_BUILTINS, CC_CCCONT, CC_COMMPATH,
29    CC_DEFCONT, CC_DELETE, CC_DIRS, CC_DISCMDS, CC_ENVVARS, CC_EXCMDS, CC_EXPANDEXPL, CC_EXTCMDS,
30    CC_FILES, CC_INTVARS, CC_JOBS, CC_NAMED, CC_NOSORT, CC_OPTIONS, CC_PARAMS, CC_PATCONT,
31    CC_QUOTEFLAG, CC_READONLYS, CC_REMOVE, CC_RESWDS, CC_RUNNING, CC_SCALARS, CC_SHFUNCS,
32    CC_SPECIALS, CC_STOPPED, CC_UNIQALL, CC_UNIQCON, CC_USERS, CC_VARS, CC_XORCONT,
33};
34use crate::ported::zle::complete::parse_cmatcher;
35#[allow(unused_imports)]
36use crate::ported::zle::{
37    deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
38    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
39};
40use crate::ported::zsh_h::PAT_HEAPDUP;
41use crate::ported::zsh_h::{
42    IN_NOTHING, QT_BACKSLASH, QT_BACKTICK, QT_DOLLARS, QT_DOUBLE, QT_NONE, QT_SINGLE,
43};
44use std::collections::HashMap;
45use std::os::unix::fs::PermissionsExt;
46use std::sync::{Arc, Mutex};
47
48// Re-export the canonical `compctl.h` ports from compctl_h.rs so
49// callers within compctl.rs reference the legit names. The four
50// types (Compctlp/Patcomp/Compcond/Compctl + CompcondData) are
51// direct ports of the C structs declared in Src/Zle/compctl.h.
52
53// --- AUTO: cross-zle hoisted-fn use glob ---
54#[allow(unused_imports)]
55#[allow(unused_imports)]
56// =====================================================================
57// COMP_* — `compctl` operation flags from `Src/Zle/compctl.c:53-60`.
58// Encode the command-line operation requested by `compctl`'s flag
59// arguments (`-L`, `-C`, `-D`, `-T`, `-M`).
60// =====================================================================
61
62/// Port of `COMP_LIST` from `Src/Zle/compctl.c:53`. `-L` flag — list
63/// existing compctl bindings.
64pub const COMP_LIST: i32 = 1 << 0; // c:53
65/// Port of `COMP_COMMAND` from `compctl.c:54`. `-C` — operate on the
66/// command-completion table.
67pub const COMP_COMMAND: i32 = 1 << 1; // c:54
68/// Port of `COMP_DEFAULT` from `compctl.c:55`. `-D` — operate on the
69/// default-completion entry.
70pub const COMP_DEFAULT: i32 = 1 << 2; // c:55
71/// Port of `COMP_FIRST` from `compctl.c:56`. `-T` — operate on the
72/// first-completion entry.
73pub const COMP_FIRST: i32 = 1 << 3; // c:56
74/// Port of `COMP_REMOVE` from `compctl.c:57`. `+` prefix or remove op.
75pub const COMP_REMOVE: i32 = 1 << 4; // c:57
76/// Port of `COMP_LISTMATCH` from `compctl.c:58`. `-L -M` combination.
77pub const COMP_LISTMATCH: i32 = 1 << 5; // c:58
78
79/// Port of `COMP_SPECIAL` from `compctl.c:60`. Mask covering all
80/// "special" entry-point flags.
81pub const COMP_SPECIAL: i32 = COMP_COMMAND | COMP_DEFAULT | COMP_FIRST; // c:60
82
83// =================================================================
84// Free ported — start of compctl.c proper
85// =================================================================
86
87/// Initialize the `compctltab` hash table.
88/// Port of `createcompctltable()` from Src/Zle/compctl.c:70. The C
89/// version wires hash function pointers (hasher, addnode, getnode,
90/// printnode, freenode); Rust uses a plain HashMap so the wiring
91/// reduces to allocation.
92pub(crate) fn createcompctltable() {
93    let mut g = COMPCTL_TAB.write().unwrap();
94    *g = Some(HashMap::new());
95    let mut p = PATCOMPS.write().unwrap();
96    p.clear();
97}
98
99/// Free a `compctlp` hash node.
100/// Port of `freecompctlp(HashNode hn)` from Src/Zle/compctl.c:92. Rust's Arc
101/// drop handles the inner Compctl free; this is the entry the C
102/// hash table calls back when removing a node.
103/// WARNING: param names don't match C — Rust=() vs C=(hn)
104pub(crate) fn freecompctlp(name: &str) {
105    let mut g = COMPCTL_TAB.write().unwrap();
106    if let Some(map) = g.as_mut() {
107        map.remove(name);
108    }
109}
110
111/// Port of `void freecompctl(Compctl cc)` from Src/Zle/compctl.c:103.
112/// ```c
113/// void
114/// freecompctl(Compctl cc)
115/// {
116///     if (cc == &cc_default ||
117///         cc == &cc_first ||
118///         cc == &cc_compos ||
119///         --cc->refc > 0)
120///         return;
121///     zsfree(cc->keyvar);
122///     zsfree(cc->glob);
123///     zsfree(cc->str);
124///     zsfree(cc->func);
125///     zsfree(cc->explain);
126///     zsfree(cc->ylist);
127///     zsfree(cc->prefix);
128///     zsfree(cc->suffix);
129///     zsfree(cc->hpat);
130///     zsfree(cc->gname);
131///     zsfree(cc->subcmd);
132///     zsfree(cc->substr);
133///     if (cc->cond) freecompcond(cc->cond);
134///     if (cc->ext) {
135///         Compctl n, m;
136///         n = cc->ext;
137///         do { m = (Compctl)(n->next); freecompctl(n); n = m; } while (n);
138///     }
139///     if (cc->xor && cc->xor != &cc_default)
140///         freecompctl(cc->xor);
141///     if (cc->matcher) freecmatcher(cc->matcher);
142///     zsfree(cc->mstr);
143///     zfree(cc, sizeof(struct compctl));
144/// }
145/// ```
146pub(crate) fn freecompctl(cc: Arc<Compctl>) {
147    // c:103
148    // c:105-109 — sentinel + refc-decrement early return. The Rust
149    // Arc carries refcounting natively; `Arc::strong_count > 1`
150    // mirrors the C `--cc->refc > 0` test exactly.
151    // c:105-107 — pointer-equality vs cc_default/cc_first/cc_compos.
152    // The Rust port stores those sentinel structs in COMPCTL_TAB keyed
153    // by `__cc_*` (see c:806-856 below). Snapshot them inline so we
154    // don't smuggle a Rust-only helper through src/ported/.
155    let (cc_default_ref, cc_first_ref, cc_compos_ref) = {
156        match COMPCTL_TAB.read().ok().and_then(|g| g.clone()) {
157            Some(map) => (
158                map.get("__cc_default").cloned(),
159                map.get("__cc_first").cloned(),
160                map.get("__cc_compos").cloned(),
161            ),
162            None => (None, None, None),
163        }
164    };
165    let is_sentinel = cc_default_ref.as_ref().is_some_and(|s| Arc::ptr_eq(s, &cc))
166        || cc_first_ref.as_ref().is_some_and(|s| Arc::ptr_eq(s, &cc))
167        || cc_compos_ref.as_ref().is_some_and(|s| Arc::ptr_eq(s, &cc));
168    if is_sentinel || Arc::strong_count(&cc) > 1 {
169        // c:108 --cc->refc > 0
170        return; // c:109
171    }
172
173    // c:111-122 — zsfree every owned string field. Rust's `Drop` on
174    // `Option<String>` handles each of these when `cc` falls out of
175    // scope at end of fn. Mirror the C order explicitly so the audit
176    // trail matches; the `let _` bindings force-evaluate each field
177    // for parity with `zsfree(NULL)` being a defined no-op.
178    let _ = &cc.keyvar; // c:111 zsfree(keyvar)
179    let _ = &cc.glob; // c:112 zsfree(glob)
180    let _ = &cc.str; // c:113 zsfree(str)
181    let _ = &cc.func; // c:114 zsfree(func)
182    let _ = &cc.explain; // c:115 zsfree(explain)
183    let _ = &cc.ylist; // c:116 zsfree(ylist)
184    let _ = &cc.prefix; // c:117 zsfree(prefix)
185    let _ = &cc.suffix; // c:118 zsfree(suffix)
186    let _ = &cc.hpat; // c:119 zsfree(hpat)
187    let _ = &cc.gname; // c:120 zsfree(gname)
188    let _ = &cc.subcmd; // c:121 zsfree(subcmd)
189    let _ = &cc.substr; // c:122 zsfree(substr)
190
191    // c:123-124 — `if (cc->cond) freecompcond(cc->cond);`
192    if let Some(cond) = cc.cond.as_deref() {
193        // c:123
194        freecompcond((*cond).clone()); // c:124
195    }
196
197    // c:125-135 — recursive ext-chain walk.
198    if cc.ext.is_some() {
199        // c:125
200        let mut n: Option<Arc<Compctl>> = cc.ext.clone(); // c:128 n = cc->ext
201        while let Some(node) = n.take() {
202            // c:129-134 do { ... } while (n)
203            let m = node.next.clone(); // c:130 m = n->next
204            freecompctl(node); // c:131 freecompctl(n)
205            n = m; // c:132 n = m
206        }
207    }
208
209    // c:136-137 — `if (cc->xor && cc->xor != &cc_default) freecompctl(cc->xor);`
210    if let Some(xor) = cc.xor.clone() {
211        // c:136 cc->xor
212        let xor_is_default = cc_default_ref
213            .as_ref()
214            .is_some_and(|s| Arc::ptr_eq(s, &xor));
215        if !xor_is_default {
216            // c:136 cc->xor != &cc_default
217            freecompctl(xor); // c:137
218        }
219    }
220
221    // c:138-139 — `if (cc->matcher) freecmatcher(cc->matcher);`
222    // freecmatcher isn't ported as a free fn (see freecmatcher port
223    // below in this file or matcher Drop). Rust Box::drop on the
224    // matcher field covers this when `cc` drops.
225    let _ = &cc.matcher; // c:138-139
226
227    // c:140 — zsfree(cc->mstr);
228    let _ = &cc.mstr; // c:140
229
230    // c:141 — `zfree(cc, sizeof(struct compctl));`
231    // Arc::drop at end of scope handles the box-free. Mirror the
232    // explicit C call site for audit parity.
233    drop(cc); // c:141
234}
235
236/// Free a `compcond` spec.
237/// Port of `freecompcond(void *a)` from Src/Zle/compctl.c:146. C walks the
238/// or/and chain, freeing per-type union data. Rust's enum + Box
239/// drop the chain automatically; this is the entry kept for ABI
240/// parity with the C source.
241/// WARNING: param names don't match C — Rust=() vs C=(a)
242pub(crate) fn freecompcond(cc: Compcond) {
243    // c:146
244    // c:148-186 — walk `or` chain; for each `or` node, walk its `and`
245    // chain freeing per-type union data, then `zfree(c, sizeof(struct
246    // compcond))`. Rust Box+Vec+String drop subsumes every per-field
247    // `zsfree`/`free` call, but the structural walk is preserved so
248    // the chain is consumed in the same order C frees it (top-down,
249    // or-chain outer / and-chain inner).
250    let mut or_cur: Option<Box<Compcond>> = Some(Box::new(cc)); // c:151 for (c = cc; c; c = or)
251    while let Some(mut or_node) = or_cur {
252        let next_or = or_node.or.take(); // c:152 or = c->or
253        let mut and_cur: Option<Box<Compcond>> = Some(or_node); // c:153 for (; c; c = and)
254        while let Some(mut and_node) = and_cur {
255            let next_and = and_node.and.take(); // c:154 and = c->and
256                                                // c:155-184 — per-typ union frees. Box/Vec/String Drop on
257                                                // and_node going out of scope handles every variant
258                                                // (CCT_POS / CCT_NUMWORDS / CCT_CURSUF / CCT_CURPRE /
259                                                // CCT_RANGESTR / CCT_RANGEPAT / default).
260            let _ = and_node.u; // c:155-184 zsfree per-variant
261            and_cur = next_and; // c:185 c = and
262        }
263        or_cur = next_or; // c:151 c = or
264    }
265}
266
267/// Direct port of `static Cmlist cpcmlist(Cmlist l)` from
268/// Src/Zle/compctl.c:291. Deep-copies a Cmlist linked list, using
269/// `cpcmatcher` for each matcher's chain. Returns the new head.
270pub(crate) fn cpcmlist(
271    // c:291
272    mut l: Option<&Cmlist>,
273) -> Option<Box<Cmlist>> {
274    let mut head: Option<Box<Cmlist>> = None; // c:293 r = NULL
275    let mut tail_ref: *mut Option<Box<Cmlist>> = &mut head;
276    while let Some(src) = l {
277        // c:295 while (l)
278        let matcher_chain = crate::ported::zle::complete::cpcmatcher(
279            // c:298 cpcmatcher
280            Some(&*src.matcher),
281        )
282        .expect("cpcmatcher returned None for non-null source");
283        let n = Box::new(Cmlist {
284            // c:296 zalloc
285            next: None,             // c:297
286            matcher: matcher_chain, // c:298
287            str: src.str.clone(),   // c:299 ztrdup
288        });
289        unsafe {
290            *tail_ref = Some(n);
291            if let Some(ref mut newnode) = *tail_ref {
292                // c:301 p = &(n->next)
293                tail_ref = &mut newnode.next as *mut _;
294            }
295        }
296        l = src.next.as_deref(); // c:311 l = l->next
297    }
298    head // c:311 return r
299}
300
301// `cclist` — flag for listing/command/default/first completion.
302// Port of file-static `int cclist;` at Src/Zle/compctl.c:63.
303// Bucket-1 per PORT_PLAN.md — per-completion-call scratch state,
304// thread_local so concurrent completion invocations don't race.
305thread_local! {
306    static CCLIST: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
307}
308
309// `showmask` — mask determining what to print.
310// Port of file-static `unsigned long showmask;` at Src/Zle/compctl.c:66.
311// Bucket-1 per PORT_PLAN.md.
312thread_local! {
313    static SHOWMASK: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
314}
315
316/// Direct port of `static int set_gmatcher(char *name, char **argv)` from
317/// Src/Zle/compctl.c:311. Parses each argv entry as a cmatcher
318/// spec, builds a fresh Cmlist chain, frees the old CMATCHER and
319/// installs the new one via cpcmlist.
320pub(crate) fn set_gmatcher(name: &str, argv: &[String]) -> i32 {
321    // c:311
322    let mut head: Option<Box<Cmlist>> = None; // c:314 l = NULL
323    let mut tail_ref: *mut Option<Box<Cmlist>> = &mut head;
324    for word in argv {
325        // c:317 while (*argv)
326        let m = match parse_cmatcher(name, word) {
327            Some(m) => m,     // c:319 parse_cmatcher
328            None => return 1, // c:319 == pcm_err
329        };
330        let n = Box::new(Cmlist {
331            // c:320 zhalloc
332            next: None,        // c:321
333            matcher: m,        // c:322
334            str: word.clone(), // c:323
335        });
336        unsafe {
337            *tail_ref = Some(n);
338            if let Some(ref mut newnode) = *tail_ref {
339                // c:325
340                tail_ref = &mut newnode.next as *mut _;
341            }
342        }
343    }
344    // freecmlist(cmatcher) — Drop on the Box handles the C free path.       // c:336
345    let new_list = cpcmlist(head.as_deref()); // c:336 cpcmlist(l)
346    if let Ok(mut guard) = CMATCHER.write() {
347        *guard = new_list;
348    }
349    1 // c:336
350}
351
352/// Direct port of `static int get_gmatcher(char *name, char **argv)` from
353/// Src/Zle/compctl.c:336. Looks for a leading `-M` flag followed
354/// by matcher specs (no `-`-prefixed args), then forwards to
355/// `set_gmatcher` and translates its return into 0/1/2.
356pub(crate) fn get_gmatcher(name: &str, argv: &[String]) -> i32 {
357    // c:336
358    if argv.first().map(|s| s.as_str()) != Some("-M") {
359        // c:336
360        return 0; // c:349
361    }
362    let rest = &argv[1..]; // c:339 p = ++argv
363    for w in rest {
364        // c:341 while (*p)
365        if w.starts_with('-') {
366            // c:342
367            return 0; // c:357
368        }
369    }
370    if set_gmatcher(name, rest) != 0 {
371        // c:357
372        return 2; // c:357
373    }
374    1 // c:357
375}
376
377/// Direct port of `void print_gmatcher(int ac)` from
378/// `Src/Zle/compctl.c:357`. Prints the global matcher chain (the
379/// CMATCHER list) as `compctl -M 'str1' 'str2' ...` when `ac` is
380/// non-zero, or `MATCH 'str1' ...` otherwise. Used by `compctl -L`.
381pub(crate) fn print_gmatcher(ac: i32) {
382    // c:357
383    let guard = CMATCHER.read().ok();
384    let head = match guard.as_ref().and_then(|g| g.as_deref()) {
385        Some(h) => h,
386        None => return, // c:361 if (cmatcher)
387    };
388    let prefix = if ac != 0 { "compctl -M" } else { "MATCH" }; // c:362
389    print!("{}", prefix);
390    let mut cur: Option<&Cmlist> = Some(head);
391    while let Some(p) = cur {
392        // c:364
393        print!(" '{}'", p.str); // c:365
394        cur = p.next.as_deref();
395    }
396    println!(); // c:369
397}
398
399/// Get a compctl from arg vector — main compctl-spec parser.
400/// Port of `get_compctl(char *name, char ***av, Compctl cc, int first, int isdef, int cl)` from Src/Zle/compctl.c:377 (~600 lines).
401///
402/// Walks `argv` letter-by-letter, applying flag bits to `cc.mask` /
403/// `cc.mask2` and capturing the string args (`-K func`, `-X expl`,
404/// `-P prefix`, `-S suffix`, `-g glob`, `-s str`, etc.).
405///
406/// Returns 0 on success, 1 on parse error. On success, advances the
407/// caller's argv past the consumed flags via `*av_idx` mutation.
408///
409/// Implements the simple-flag-char arms (per-char → mask bit) from
410/// compctl.c:418-508, every arg-taking flag (`-k`/`-K`/`-Y`/`-X`/
411/// `-y`/`-P`/`-S`/`-g`/`-s`/`-l`/`-h`/`-W`/`-J`/`-V`/`-M`/`-H`/`-t`),
412/// the `-+` xor-chain marker, and the special-target flags (`-C`/
413/// `-D`/`-T`/`-L`). The `-x` extended-condition form is handled by
414/// `get_xcompctl` (called from the caller chain).
415pub(crate) fn get_compctl(
416    name: &str,
417    av: &mut Vec<String>,
418    cc: &mut Compctl,
419    first: bool,
420    mut isdef: bool,
421    cl: i32,
422) -> i32 {
423    // C: `argv = *av;` — alias the caller's array.
424    let mut i: usize = 0;
425    let hx = false;
426    let mut cclist_local = CCLIST.with(|c| c.get());
427    cc.mask2 = CC_CCCONT; // c:407
428
429    // C: `compctl + foo ...` becomes default — c:392-404
430    if first
431        && i < av.len()
432        && av[i] == "+"
433        && !(i + 1 < av.len() && av[i + 1].starts_with('-') && av[i + 1].len() > 1)
434    {
435        i += 1;
436        if i < av.len() && av[i].starts_with('-') {
437            i += 1;
438        }
439        av.drain(0..i);
440        if cl != 0 {
441            return 1;
442        } else {
443            CCLIST.with(|c| c.set(COMP_REMOVE));
444            return 0;
445        }
446    }
447
448    // Loop through the flags. C: c:412 `for (; !ready && argv[0] && argv[0][0] == '-' && (argv[0][1] || !first); )`
449    let mut ready = false;
450    while !ready && i < av.len() && av[i].starts_with('-') && (av[i].len() > 1 || !first) {
451        // C: bare `-` becomes `-+` to absorb the next iter — c:413-414
452        if av[i].len() == 1 {
453            av[i] = "-+".to_string();
454        }
455        // Walk chars after the `-`. C: `while (!ready && *++(*argv))`
456        let arg = av[i].clone();
457        let chars: Vec<char> = arg.chars().skip(1).collect();
458        let mut consumed = false;
459        for c in chars {
460            if ready {
461                break;
462            }
463            // Simple-flag-char dispatch — direct port of the
464            // switch at c:418-508.
465            match c {
466                'f' => cc.mask |= CC_FILES,             // c:419
467                'c' => cc.mask |= CC_COMMPATH,          // c:422
468                'm' => cc.mask |= CC_EXTCMDS,           // c:425
469                'w' => cc.mask |= CC_RESWDS,            // c:428
470                'o' => cc.mask |= CC_OPTIONS,           // c:431
471                'v' => cc.mask |= CC_VARS,              // c:434
472                'b' => cc.mask |= CC_BINDINGS,          // c:437
473                'A' => cc.mask |= CC_ARRAYS,            // c:440
474                'I' => cc.mask |= CC_INTVARS,           // c:443
475                'F' => cc.mask |= CC_SHFUNCS,           // c:446
476                'p' => cc.mask |= CC_PARAMS,            // c:449
477                'E' => cc.mask |= CC_ENVVARS,           // c:452
478                'j' => cc.mask |= CC_JOBS,              // c:455
479                'r' => cc.mask |= CC_RUNNING,           // c:458
480                'z' => cc.mask |= CC_STOPPED,           // c:461
481                'B' => cc.mask |= CC_BUILTINS,          // c:464
482                'a' => cc.mask |= CC_ALREG | CC_ALGLOB, // c:467
483                'R' => cc.mask |= CC_ALREG,             // c:470
484                'G' => cc.mask |= CC_ALGLOB,            // c:473
485                'u' => cc.mask |= CC_USERS,             // c:476
486                'd' => cc.mask |= CC_DISCMDS,           // c:479
487                'e' => cc.mask |= CC_EXCMDS,            // c:482
488                'N' => cc.mask |= CC_SCALARS,           // c:485
489                'O' => cc.mask |= CC_READONLYS,         // c:488
490                'Z' => cc.mask |= CC_SPECIALS,          // c:491
491                'q' => cc.mask |= CC_REMOVE,            // c:494
492                'U' => cc.mask |= CC_DELETE,            // c:497
493                'n' => cc.mask |= CC_NAMED,             // c:500
494                'Q' => cc.mask |= CC_QUOTEFLAG,         // c:503
495                '/' => cc.mask |= CC_DIRS,              // c:506
496                '1' => {
497                    // c:722
498                    cc.mask2 |= CC_UNIQALL;
499                    cc.mask2 &= !CC_UNIQCON;
500                }
501                '2' => {
502                    // c:726
503                    cc.mask2 |= CC_UNIQCON;
504                    cc.mask2 &= !CC_UNIQALL;
505                }
506                'C' => {
507                    // c:777
508                    if cl != 0 {
509                        eprintln!("{}: illegal option -{}", name, c);
510                        return 1;
511                    }
512                    if first && !hx {
513                        cclist_local |= COMP_COMMAND;
514                    } else {
515                        eprintln!("{}: misplaced command completion (-C) flag", name);
516                        return 1;
517                    }
518                }
519                'D' => {
520                    // c:789
521                    if cl != 0 {
522                        eprintln!("{}: illegal option -{}", name, c);
523                        return 1;
524                    }
525                    if first && !hx {
526                        isdef = true;
527                        cclist_local |= COMP_DEFAULT;
528                    } else {
529                        eprintln!("{}: misplaced default completion (-D) flag", name);
530                        return 1;
531                    }
532                }
533                'T' => {
534                    // c:802
535                    if cl != 0 {
536                        eprintln!("{}: illegal option -{}", name, c);
537                        return 1;
538                    }
539                    if first && !hx {
540                        cclist_local |= COMP_FIRST;
541                    } else {
542                        eprintln!("{}: misplaced first completion (-T) flag", name);
543                        return 1;
544                    }
545                }
546                'L' => {
547                    // c:814
548                    if cl != 0 {
549                        eprintln!("{}: illegal option -{}", name, c);
550                        return 1;
551                    }
552                    if !first || hx {
553                        eprintln!("{}: illegal use of -L flag", name);
554                        return 1;
555                    }
556                    cclist_local |= COMP_LIST;
557                }
558                '+' => {
559                    // c:850 (xor chain marker)
560                    // Marks end of this compctl spec; remainder is
561                    // the next xor'd compctl. Stop the loop here;
562                    // the caller iterates again for the xor chain.
563                    ready = true;
564                    consumed = true;
565                    break;
566                }
567                _ => {
568                    // Arg-taking flags + unknown — bail to the
569                    // post-loop handler. These are c:509+ (`t` retry,
570                    // `k` keyvar, `K` func, `Y`/`X` explain, `y`
571                    // ylist, `P`/`S` prefix/suffix, `g` glob, `s`
572                    // str, `l`/`h` subcmd/substr, `W` withd, `J`/`V`
573                    // gname, `M` matcher, `H` history, `x` extended).
574                    // For now, if the arg-taking char is followed by
575                    // no body, consume one extra argv slot as the
576                    // arg. Else ignore. Real impls land per-flag.
577                    let (has_inline, inline_val) = (
578                        arg.len() > 2 && arg.chars().nth(1) == Some(c),
579                        if arg.len() > 2 {
580                            arg[2..].to_string()
581                        } else {
582                            String::new()
583                        },
584                    );
585                    let mut val: Option<String> = None;
586                    if has_inline {
587                        val = Some(inline_val);
588                    } else if i + 1 < av.len() {
589                        val = Some(av[i + 1].clone());
590                        i += 1;
591                    }
592                    match c {
593                        'k' => cc.keyvar = val, // c:553
594                        'K' => cc.func = val,   // c:565
595                        'Y' => {
596                            // c:577
597                            cc.mask |= CC_EXPANDEXPL;
598                            cc.explain = val;
599                        }
600                        'X' => {
601                            // c:580
602                            cc.mask &= !CC_EXPANDEXPL;
603                            cc.explain = val;
604                        }
605                        'y' => cc.ylist = val,  // c:594
606                        'P' => cc.prefix = val, // c:606
607                        'S' => cc.suffix = val, // c:618
608                        'g' => cc.glob = val,   // c:630
609                        's' => cc.str = val,    // c:642
610                        'l' => cc.subcmd = val, // c:655
611                        'h' => cc.substr = val, // c:670
612                        'W' => cc.withd = val,  // c:685
613                        'J' => cc.gname = val,  // c:697
614                        'V' => {
615                            // c:709
616                            cc.gname = val;
617                            cc.mask2 |= CC_NOSORT;
618                        }
619                        'M' => {
620                            // c:730
621                            // Matcher spec — store the raw string and
622                            // also validate it via `parse_cmatcher`
623                            // (Src/Zle/complete.c:242), failing the
624                            // compctl parse on a malformed matcher
625                            // per C c:731-735.
626                            if let Some(s) = val {
627                                if parse_cmatcher(name, &s).is_none() {
628                                    eprintln!("{}: bad matcher specification `{}'", name, s);
629                                    return 1;
630                                }
631                                cc.mstr = Some(s);
632                            }
633                        }
634                        'H' => {
635                            // c:757
636                            // -H N PAT — number + pattern. The
637                            // simple-flag walker consumed N as `val`;
638                            // the next argv is PAT.
639                            if let Some(s) = val {
640                                cc.hnum = s.parse::<i32>().unwrap_or(0).max(0);
641                            }
642                            if i + 1 < av.len() {
643                                cc.hpat = Some(av[i + 1].clone());
644                                if cc.hpat.as_deref() == Some("*") {
645                                    cc.hpat = Some(String::new());
646                                }
647                                i += 1;
648                            }
649                        }
650                        't' => {
651                            // c:509 retry spec
652                            // `-t {+|n|-|x}` controls continuation.
653                            // Direct port of the switch at c:528-545.
654                            if let Some(s) = val {
655                                let bit = match s.as_str() {
656                                    "+" => CC_XORCONT,
657                                    "n" => 0,
658                                    "-" => CC_PATCONT,
659                                    "x" => CC_DEFCONT,
660                                    _ => {
661                                        eprintln!(
662                                            "{}: invalid retry specification character `{}`",
663                                            name, s
664                                        );
665                                        return 1;
666                                    }
667                                };
668                                cc.mask2 = bit;
669                            }
670                        }
671                        _ => {
672                            eprintln!("{}: unknown compctl flag `-{}`", name, c);
673                            return 1;
674                        }
675                    }
676                    consumed = true;
677                    break;
678                }
679            }
680        }
681        i += 1;
682        if !consumed {
683            // Pure simple-flag arg — already advanced.
684        }
685    }
686
687    // C: c:1582 — push the parsed cct into the caller's slot.
688    av.drain(0..i);
689    let _ = isdef;
690    CCLIST.with(|c| c.set(cclist_local));
691    0
692}
693
694/// Parse the `-x` extended-condition compctl form.
695/// Port of `get_xcompctl(char *name, char ***av, Compctl cc, int isdef)` from Src/Zle/compctl.c:909 (~260 lines).
696///
697/// C signature: `int get_xcompctl(char *name, char ***av, Compctl cc,
698/// int isdef)`. Walks the per-condition syntax `s[…][…], p[…]` …
699/// and chains them as Compcond entries on `cc.ext`. Each `case`
700/// letter dispatches to one CCT_* type (`s`→CURSUF, `p`→POS, etc.),
701/// then the `[…]` argument syntax is parsed per-type.
702///
703/// Inside the `[]`, the C source uses temporary lexer-style markers
704/// `\200` (CCT_END) and `\201` (CCT_AND) to mark the active `]`/`,`
705/// boundaries — Rust uses Vec splits instead.
706///
707/// Returns 0 on success, 1 on parse error. Advances `*av` past the
708/// consumed conditions.
709pub(crate) fn get_xcompctl(name: &str, av: &mut Vec<String>, cc: &mut Compctl, isdef: bool) -> i32 {
710    let mut ready = false;
711    let mut next_chain: Vec<Arc<Compctl>> = Vec::new();
712
713    while !ready {
714        // C: c:920 — `o = m = c = (Compcond) zshcalloc(...)`
715        // o tracks or-chain head, m tracks first cond (root), c tracks
716        // current cond being parsed.
717        let mut head: Compcond = Compcond::default();
718        let mut current_or = &mut head as *mut Compcond;
719
720        // C: c:922 — `for (t = *argv; *t;)` walk one argv slot
721        if av.is_empty() {
722            // C: c:1150 — missing args
723            eprintln!("{}: missing command names", name);
724            return 1;
725        }
726        let arg = av[0].clone();
727        let bytes: Vec<char> = arg.chars().collect();
728        let mut t = 0_usize;
729        let mut current_and: Option<*mut Compcond> = None;
730
731        while t < bytes.len() {
732            // Skip leading spaces — c:923-924
733            while t < bytes.len() && bytes[t] == ' ' {
734                t += 1;
735            }
736            if t >= bytes.len() {
737                break;
738            }
739
740            // C: c:926-972 — switch on condition code char
741            let typ = match bytes[t] {
742                'q' => CCT_QUOTE,    // c:927
743                's' => CCT_CURSUF,   // c:930
744                'S' => CCT_CURPRE,   // c:933
745                'p' => CCT_POS,      // c:936
746                'c' => CCT_CURSTR,   // c:939
747                'C' => CCT_CURPAT,   // c:942
748                'w' => CCT_WORDSTR,  // c:945
749                'W' => CCT_WORDPAT,  // c:948
750                'n' => CCT_CURSUB,   // c:951
751                'N' => CCT_CURSUBC,  // c:954
752                'm' => CCT_NUMWORDS, // c:957
753                'r' => CCT_RANGESTR, // c:960
754                'R' => CCT_RANGEPAT, // c:963
755                _ => {
756                    eprintln!("{}: unknown condition code: {}", name, bytes[t]);
757                    return 1;
758                }
759            };
760
761            // C: c:974 — must be followed by `[`
762            if t + 1 >= bytes.len() || bytes[t + 1] != '[' {
763                eprintln!(
764                    "{}: expected condition after condition code: {}",
765                    name, bytes[t]
766                );
767                return 1;
768            }
769            t += 1;
770
771            // C: c:985-997 — count `[…][…]` blocks (n = arity).
772            // Walk balanced brackets, collecting bodies.
773            let mut bodies: Vec<String> = Vec::new();
774            while t < bytes.len() && bytes[t] == '[' {
775                t += 1; // skip `[`
776                        // skip leading spaces inside brackets — c:1028
777                while t < bytes.len() && bytes[t] == ' ' {
778                    t += 1;
779                }
780                let body_start = t;
781                let mut depth = 1_i32;
782                while t < bytes.len() && depth > 0 {
783                    if bytes[t] == '\\' && t + 1 < bytes.len() {
784                        t += 2;
785                        continue;
786                    }
787                    if bytes[t] == '[' {
788                        depth += 1;
789                    } else if bytes[t] == ']' {
790                        depth -= 1;
791                        if depth == 0 {
792                            break;
793                        }
794                    }
795                    t += 1;
796                }
797                if t >= bytes.len() {
798                    eprintln!("{}: error after condition code", name);
799                    return 1;
800                }
801                let body: String = bytes[body_start..t].iter().collect();
802                bodies.push(body);
803                t += 1; // skip `]`
804            }
805            let n = bodies.len() as i32;
806
807            // C: c:1009-1025 — allocate per-type data, dispatch parse.
808            let data = match typ {
809                t if t == CCT_POS || t == CCT_NUMWORDS => {
810                    // c:1030-1054 — one or two ints per body.
811                    let mut a: Vec<i32> = Vec::with_capacity(n as usize);
812                    let mut b: Vec<i32> = Vec::with_capacity(n as usize);
813                    for body in &bodies {
814                        // body shape: "N" or "N,M"
815                        let parts: Vec<&str> = body.splitn(2, ',').collect();
816                        let av_n: i32 = parts[0].trim().parse().unwrap_or(0);
817                        let bv_n: i32 = if parts.len() == 2 {
818                            parts[1].trim().parse().unwrap_or(0)
819                        } else {
820                            av_n // c:1042 — single arg → b copies a
821                        };
822                        a.push(av_n);
823                        b.push(bv_n);
824                    }
825                    CompcondData::R { a, b }
826                }
827                t if t == CCT_CURSUF || t == CCT_CURPRE || t == CCT_QUOTE => {
828                    // c:1056-1069 — single string per body.
829                    let s: Vec<String> = bodies.iter().cloned().collect();
830                    let p: Vec<i32> = vec![0; s.len()];
831                    CompcondData::S { p, s }
832                }
833                t if t == CCT_RANGESTR || t == CCT_RANGEPAT => {
834                    // c:1070-1099 — two strings per body, comma-separated.
835                    let mut a: Vec<String> = Vec::with_capacity(n as usize);
836                    let mut b: Vec<String> = Vec::with_capacity(n as usize);
837                    for body in &bodies {
838                        let parts: Vec<&str> = body.splitn(2, ',').collect();
839                        a.push(parts[0].to_string());
840                        b.push(parts.get(1).map(|s| s.to_string()).unwrap_or_default());
841                    }
842                    CompcondData::L { a, b }
843                }
844                _ => {
845                    // c:1100-1121 — number followed by string per body.
846                    let mut p: Vec<i32> = Vec::with_capacity(n as usize);
847                    let mut s: Vec<String> = Vec::with_capacity(n as usize);
848                    for body in &bodies {
849                        let parts: Vec<&str> = body.splitn(2, ',').collect();
850                        if parts.len() != 2 {
851                            eprintln!("{}: error in condition", name);
852                            return 1;
853                        }
854                        p.push(parts[0].trim().parse().unwrap_or(0));
855                        s.push(parts[1].to_string());
856                    }
857                    CompcondData::S { p, s }
858                }
859            };
860
861            // Fill the current condition node.
862            // SAFETY: current_or points to either head (stack) or a
863            // Box<Compcond> we control via current_and chain.
864            unsafe {
865                let cur = match current_and {
866                    Some(p) => p,
867                    None => current_or,
868                };
869                (*cur).typ = typ;
870                (*cur).n = n;
871                (*cur).u = data;
872            }
873
874            // Skip trailing spaces — c:1123
875            while t < bytes.len() && bytes[t] == ' ' {
876                t += 1;
877            }
878
879            // C: c:1125-1134 — `,` → or-chain, else and-chain
880            if t < bytes.len() && bytes[t] == ',' {
881                let new_node = Box::new(Compcond::default());
882                let new_ptr = Box::into_raw(new_node);
883                unsafe {
884                    let cur = current_and.unwrap_or(current_or);
885                    (*cur).or = Some(Box::from_raw(new_ptr));
886                    current_or = (*cur).or.as_mut().unwrap().as_mut() as *mut Compcond;
887                }
888                current_and = None;
889                t += 1;
890            } else if t < bytes.len() {
891                let new_node = Box::new(Compcond::default());
892                let new_ptr = Box::into_raw(new_node);
893                unsafe {
894                    let cur = current_and.unwrap_or(current_or);
895                    (*cur).and = Some(Box::from_raw(new_ptr));
896                    current_and = Some((*cur).and.as_mut().unwrap().as_mut() as *mut Compcond);
897                }
898            }
899        }
900
901        // C: c:1137-1142 — assign condition to a fresh compctl on
902        // the chain, parse the flags that follow.
903        let mut next_cc = Compctl::default();
904        next_cc.cond = Some(Box::new(head));
905        // Drop the consumed argv slot.
906        av.remove(0);
907        if get_compctl(name, av, &mut next_cc, false, isdef, 0) != 0 {
908            return 1;
909        }
910        next_chain.push(Arc::new(next_cc));
911
912        // C: c:1143-1145 — special target → finished
913        let cclist = CCLIST.with(|c| c.get());
914        if (av.is_empty()) && (cclist & COMP_SPECIAL) != 0 {
915            ready = true;
916            continue;
917        }
918
919        // C: c:1150-1162 — look for next `-` flag block or `--` term
920        if av.is_empty() || !av[0].starts_with('-') || (av[0].len() == 1 && av.len() < 2) {
921            eprintln!("{}: missing command names", name);
922            return 1;
923        }
924        if av[0] == "--" {
925            ready = true;
926        } else if av[0] == "-+" && av.len() >= 2 && av[1] == "--" {
927            ready = true;
928            av.remove(0);
929        }
930        av.remove(0);
931    }
932
933    // C: c:1167-1168 — install the chain on cc.ext.
934    if let Some(first) = next_chain.into_iter().next() {
935        cc.ext = Some(first);
936    }
937    0
938}
939
940/// Copy fields from `cct` into the spec stored at `name`.
941/// Port of `cc_assign(char *name, Compctl *ccptr, Compctl cct, int reass)` from Src/Zle/compctl.c:1174 (~75 lines).
942///
943/// C semantics: with `reass=true`, the special targets
944/// (cc_compos / cc_default / cc_first) are reassigned via
945/// `cc_reassign` which strips the prior `ext`/`xor` chains while
946/// preserving the static storage. Then every string field is
947/// `zsfree`d on the old spec and `ztrdup`d from `cct` into the new
948/// slot. Rust's Arc<Compctl> handles drop refcounting; this fn
949/// installs `cct` directly under `name` in the hash table.
950///
951/// The reass=true case for the special targets currently routes
952/// through the same install path — the static-storage distinction
953/// in C is a memory-model detail that doesn't transfer to Rust's
954/// Arc-based ownership.
955pub(crate) fn cc_assign(name: &str, cct: Arc<Compctl>, reass: bool) {
956    let cclist = CCLIST.with(|c| c.get());
957    if reass && (cclist & COMP_LIST) == 0 {
958        // C: c:1182-1188 — reject conflicting special targets
959        let conflicts = cclist == (COMP_COMMAND | COMP_DEFAULT)
960            || cclist == (COMP_COMMAND | COMP_FIRST)
961            || cclist == (COMP_DEFAULT | COMP_FIRST)
962            || cclist == COMP_SPECIAL;
963        if conflicts {
964            eprintln!("{}: can't set -D, -T, and -C simultaneously", name);
965            return;
966        }
967        // C: c:1190-1202 — reassign special target. The COMMAND /
968        // DEFAULT / FIRST cases install under reserved names. The
969        // C statics cc_compos / cc_default / cc_first map to these
970        // reserved keys in zshrs's table.
971        if (cclist & COMP_COMMAND) != 0 {
972            let _ = cc_reassign(cct.clone());
973            let mut g = COMPCTL_TAB.write().unwrap();
974            if g.is_none() {
975                *g = Some(HashMap::new());
976            }
977            if let Some(map) = g.as_mut() {
978                map.insert("__cc_compos".to_string(), cct);
979            }
980            return;
981        }
982        if (cclist & COMP_DEFAULT) != 0 {
983            let _ = cc_reassign(cct.clone());
984            let mut g = COMPCTL_TAB.write().unwrap();
985            if g.is_none() {
986                *g = Some(HashMap::new());
987            }
988            if let Some(map) = g.as_mut() {
989                map.insert("__cc_default".to_string(), cct);
990            }
991            return;
992        }
993        if (cclist & COMP_FIRST) != 0 {
994            let _ = cc_reassign(cct.clone());
995            let mut g = COMPCTL_TAB.write().unwrap();
996            if g.is_none() {
997                *g = Some(HashMap::new());
998            }
999            if let Some(map) = g.as_mut() {
1000                map.insert("__cc_first".to_string(), cct);
1001            }
1002            return;
1003        }
1004    }
1005    // C: c:1205-1247 — Rust's Arc replaces the manual zsfree/ztrdup
1006    // ladder. The new spec is installed under `name`; the prior
1007    // entry (if any) drops its refcount when this insert overwrites.
1008    let mut g = COMPCTL_TAB.write().unwrap();
1009    if g.is_none() {
1010        *g = Some(HashMap::new());
1011    }
1012    if let Some(map) = g.as_mut() {
1013        map.insert(name.to_string(), cct);
1014    }
1015}
1016
1017/// Free a special-target compctl's chain while preserving its slot.
1018/// Port of `cc_reassign(Compctl cc)` from Src/Zle/compctl.c:1253.
1019///
1020/// C semantics: builds a temporary Compctl carrying `cc->xor` /
1021/// `cc->ext`, sets refc=1, calls `freecompctl` on it (which
1022/// recursively frees those chains), then nulls them on `cc`. This
1023/// is needed because cc_compos / cc_default / cc_first are static
1024/// allocations that can't themselves be freed — only their chains.
1025///
1026/// Rust's Arc handles refcounting. Returning a fresh empty Compctl
1027/// matches the "free the chain, keep the storage" semantic by
1028/// dropping the input cc's ext/xor refcounts and giving the caller
1029/// a placeholder.
1030/// WARNING: param names don't match C — Rust=() vs C=(cc)
1031pub(crate) fn cc_reassign(_cc: Arc<Compctl>) -> Arc<Compctl> {
1032    // Arc drop on the input cc handles the C `freecompctl(c2)` call —
1033    // when refcount hits zero, ext/xor chains drop too. Return an
1034    // empty placeholder for the caller to populate.
1035    Arc::new(Compctl::default())
1036}
1037
1038/// Test whether the given string is a pattern.
1039/// Port of `compctl_name_pat(char **p)` from Src/Zle/compctl.c:1275.
1040///
1041/// C signature: `int compctl_name_pat(char **p)` — returns 1 if `*p`
1042/// contains glob wildcards (after `tokenize` + `remnulargs`); also
1043/// rewrites `*p` either to the tokenized form (pattern) or with
1044/// backslashes removed (literal). Rust port: returns `(is_pattern,
1045/// new_text)` tuple since we can't mutate a `&str` in-place.
1046///
1047/// Pattern detection: the C `haswilds()` checks for the lexer's
1048/// glob-meta tokens (Star, Quest, Inbrack, etc.). Since the input
1049/// here is plain user-typed text, we approximate by checking for
1050/// the literal `*`/`?`/`[` characters.
1051/// WARNING: param names don't match C — Rust=() vs C=(p)
1052pub(crate) fn compctl_name_pat(p: &str) -> (bool, String) {
1053    // C: c:1282 `if (haswilds(s))` — has glob metas
1054    let has_glob = p.chars().any(|c| matches!(c, '*' | '?' | '['));
1055    if has_glob {
1056        // C: c:1283 `*p = s` — keep the (tokenized) pattern as-is.
1057        // Rust: return the original; caller treats as pattern.
1058        (true, p.to_string())
1059    } else {
1060        // C: c:1286 `*p = rembslash(*p)` — strip backslashes from
1061        // literal text (`\X` → `X`).
1062        let mut out = String::with_capacity(p.len());
1063        let mut chars = p.chars().peekable();
1064        while let Some(c) = chars.next() {
1065            if c == '\\' {
1066                if let Some(&nx) = chars.peek() {
1067                    out.push(nx);
1068                    chars.next();
1069                    continue;
1070                }
1071            }
1072            out.push(c);
1073        }
1074        (false, out)
1075    }
1076}
1077
1078/// Delete a pattern compctl by name.
1079/// Port of `delpatcomp(char *n)` from Src/Zle/compctl.c:1294.
1080pub(crate) fn delpatcomp(n: &str) {
1081    // c:1294
1082    let mut patcomps = PATCOMPS.write().unwrap();
1083    // c:1296 — Patcomp p, q;
1084    // c:1298 — for (q = 0, p = patcomps; p; q = p, p = p->next)
1085    for i in 0..patcomps.len() {
1086        // c:1299 — if (!strcmp(n, p->pat)) {
1087        if patcomps[i].0 == n {
1088            // c:1300-1303 — splice: if (q) q->next = p->next; else patcomps = p->next;
1089            // c:1304 — zsfree(p->pat);     (Rust: Drop handles)
1090            // c:1305 — freecompctl(p->cc); (Rust: Drop handles Arc<Compctl>)
1091            // c:1306 — free(p);            (Rust: Vec::remove drops)
1092            patcomps.remove(i);
1093            // c:1308 — break;
1094            break;
1095        }
1096    }
1097}
1098
1099/// Process the parsed compctl into the table.
1100/// Port of `compctl_process_cc(char **s, Compctl cc)` from Src/Zle/compctl.c:1315 —
1101/// installs the spec into compctltab (or patcomps for `-p PAT`),
1102/// or removes entries when COMP_REMOVE is set (the `-` flag).
1103/// WARNING: param names don't match C — Rust=(cc) vs C=(s, cc)
1104pub(crate) fn compctl_process_cc(s: &[String], cc: Arc<Compctl>) -> i32 {
1105    let cclist = CCLIST.with(|c| c.get());
1106    if (cclist & COMP_REMOVE) != 0 {
1107        // C: c:1320-1328 — delete entries for the listed commands
1108        for n in s {
1109            // pattern shape — `compctl -p`. compctl_name_pat
1110            // returns true if `n` looks like a pattern; here we
1111            // just check both tables.
1112            let mut p = PATCOMPS.write().unwrap();
1113            let len_before = p.len();
1114            p.retain(|(pat, _)| pat != n);
1115            let pat_removed = p.len() != len_before;
1116            drop(p);
1117            if !pat_removed {
1118                if let Some(map) = COMPCTL_TAB.write().unwrap().as_mut() {
1119                    map.remove(n);
1120                }
1121            }
1122        }
1123    } else {
1124        // C: c:1330-1351 — add the parsed compctl to the table
1125        for n in s {
1126            // For now, treat all names as plain (not pattern) —
1127            // pattern-mode `-p` requires get_compctl to set a flag
1128            // we haven't ported yet.
1129            let mut g = COMPCTL_TAB.write().unwrap();
1130            if g.is_none() {
1131                *g = Some(HashMap::new());
1132            }
1133            if let Some(map) = g.as_mut() {
1134                map.insert(n.clone(), cc.clone());
1135            }
1136        }
1137    }
1138    0
1139}
1140
1141/// Print a single compctl spec.
1142/// Port of `printcompctl(char *s, Compctl cc, int printflags, int ispat)` from Src/Zle/compctl.c:1359 (~190 lines).
1143///
1144/// Emits the `compctl -FLAGS NAME` line that re-creates the spec.
1145/// Direct port of the C flag-letter walk (c:1362 `css = "fcqovbAIFp..."`):
1146/// each char in the css string corresponds to a CC_* bit; if the bit
1147/// is set in cc.mask, the letter prints. Same for `mss` against mask2.
1148///
1149/// Then per-string-arg flags (-K func, -X expl, etc.), -x extended
1150/// chain, +xor chain. Trailing arg is the command name (or pattern
1151/// when ispat=true).
1152/// WARNING: param names don't match C — Rust=(cc, printflags, ispat) vs C=(s, cc, printflags, ispat)
1153pub(crate) fn printcompctl(s: &str, cc: &Compctl, printflags: i32, ispat: bool) {
1154    // C: c:1362-1364 — flag-letter strings (positional → bit index)
1155    const CSS: &str = "fcqovbAIFpEjrzBRGudeNOZUnQmw/";
1156    const MSS: &str = " pcCwWsSnNmrRq";
1157
1158    // C: c:1366
1159    let mut flags = cc.mask;
1160    let flags2 = cc.mask2;
1161
1162    // C: c:1369-1372 — printflags adjusts cclist mode
1163    const PRINT_LIST: i32 = 1 << 0;
1164    const PRINT_TYPE: i32 = 1 << 1;
1165    let mut cclist = CCLIST.with(|c| c.get());
1166    if (printflags & PRINT_LIST) != 0 {
1167        cclist |= COMP_LIST;
1168    } else if (printflags & PRINT_TYPE) != 0 {
1169        cclist &= !COMP_LIST;
1170    }
1171
1172    // C: c:1374 — adjust EXCMDS if DISCMDS not set
1173    if (flags & CC_EXCMDS) != 0 && (flags & CC_DISCMDS) == 0 {
1174        flags &= !CC_EXCMDS;
1175    }
1176
1177    // C: c:1379 — showmask filter
1178    let showmask = SHOWMASK.with(|c| c.get());
1179    if showmask != 0 && (flags & showmask) == 0 {
1180        return;
1181    }
1182
1183    // C: c:1384-1385 — clear showmask for recursive calls
1184    let oldshowmask = showmask;
1185    SHOWMASK.with(|c| c.set(0));
1186
1187    // C: c:1388-1402 — print prefix
1188    if (cclist & COMP_LIST) != 0 {
1189        print!("compctl");
1190    } else if !s.is_empty() {
1191        print!("compctl");
1192    }
1193
1194    // C: c:1404-1417 — walk CSS for primary mask flags
1195    for (i, ch) in CSS.chars().enumerate() {
1196        if ch == ' ' {
1197            continue;
1198        }
1199        if (flags & (1u64 << i)) != 0 {
1200            print!(" -{}", ch);
1201        }
1202    }
1203
1204    // C: walk MSS for mask2 flags (NOSORT, etc.)
1205    let _ = MSS; // mss is for the printable mask2 letters; pending
1206                 // a full per-bit mapping in zsh's source
1207
1208    // C: c:1418-1430 — string-arg flags (-K func, etc.)
1209    if let Some(s) = &cc.keyvar {
1210        print!(" -k '{}'", s);
1211    }
1212    if let Some(s) = &cc.glob {
1213        print!(" -g '{}'", s);
1214    }
1215    if let Some(s) = &cc.str {
1216        print!(" -s '{}'", s);
1217    }
1218    if let Some(s) = &cc.func {
1219        print!(" -K '{}'", s);
1220    }
1221    if let Some(s) = &cc.explain {
1222        if (cc.mask & CC_EXPANDEXPL) != 0 {
1223            print!(" -Y '{}'", s);
1224        } else {
1225            print!(" -X '{}'", s);
1226        }
1227    }
1228    if let Some(s) = &cc.ylist {
1229        print!(" -y '{}'", s);
1230    }
1231    if let Some(s) = &cc.prefix {
1232        print!(" -P '{}'", s);
1233    }
1234    if let Some(s) = &cc.suffix {
1235        print!(" -S '{}'", s);
1236    }
1237    if let Some(s) = &cc.subcmd {
1238        print!(" -l '{}'", s);
1239    }
1240    if let Some(s) = &cc.substr {
1241        print!(" -h '{}'", s);
1242    }
1243    if let Some(s) = &cc.withd {
1244        print!(" -W '{}'", s);
1245    }
1246    if let Some(s) = &cc.gname {
1247        if (flags2 & CC_NOSORT) != 0 {
1248            print!(" -V '{}'", s);
1249        } else {
1250            print!(" -J '{}'", s);
1251        }
1252    }
1253    if let Some(s) = &cc.mstr {
1254        print!(" -M '{}'", s);
1255    }
1256    if cc.hnum > 0 {
1257        if let Some(p) = &cc.hpat {
1258            print!(" -H {} '{}'", cc.hnum, if p.is_empty() { "*" } else { p });
1259        }
1260    }
1261
1262    // C: c:1518-1523 — xor chain
1263    if cc.xor.is_some() {
1264        print!(" +");
1265    }
1266
1267    // C: c:1524-1543 — trailing name (or pattern)
1268    if !s.is_empty() && (cclist & COMP_LIST) != 0 {
1269        if ispat {
1270            print!(" -p '{}'", s);
1271        } else {
1272            print!(" '{}'", s);
1273        }
1274    } else if !s.is_empty() {
1275        print!(" '{}'", s);
1276    }
1277    println!();
1278
1279    // C: c:1545 — restore showmask
1280    SHOWMASK.with(|c| c.set(oldshowmask));
1281}
1282
1283/// Print a compctl hash node.
1284/// Port of `printcompctlp(HashNode hn, int printflags)` from Src/Zle/compctl.c:1550 — hash-table
1285/// callback that calls printcompctl.
1286pub(crate) fn printcompctlp(name: &str, hn: &Compctl, printflags: i32) {
1287    printcompctl(name, hn, printflags, false);
1288}
1289
1290/// `compctl` builtin entry point.
1291/// Port of `bin_compctl(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from Src/Zle/compctl.c:1562 (~110 lines).
1292/// Direct port of the C dispatch flow:
1293///   1. Reset cclist + showmask
1294///   2. Try `get_gmatcher` — if returns non-zero, return that-1
1295///   3. Allocate cct, run `get_compctl`. On failure, free + return 1
1296///   4. Save mask in showmask (with EXCMDS/DISCMDS adjust)
1297///   5. If no remaining args or COMP_LIST, free cc
1298///   6. If no args and no special: print all (patcomps + compctltab +
1299///      cc_compos/cc_default/cc_first + global matchers)
1300///   7. If COMP_LIST: print only the named entries
1301///   8. Else: install via compctl_process_cc
1302/// WARNING: param names don't match C — Rust=(argv) vs C=(name, argv, ops, func)
1303pub fn bin_compctl(
1304    name: &str,
1305    argv: &[String],
1306    _ops: &crate::ported::zsh_h::options,
1307    _func: i32,
1308) -> i32 {
1309    let mut argv: Vec<String> = argv.to_vec();
1310    let mut ret: i32 = 0;
1311
1312    // C: c:1570-1571 — clear static flags
1313    CCLIST.with(|c| c.set(0));
1314    SHOWMASK.with(|c| c.set(0));
1315
1316    // C: c:1574-1596 — parse args if any
1317    if !argv.is_empty() {
1318        // C: c:1576 — try global matcher first
1319        let gret = get_gmatcher(name, &argv);
1320        if gret != 0 {
1321            return gret - 1;
1322        }
1323
1324        // C: c:1581 — allocate compctl
1325        let mut cc = Compctl::default();
1326        // C: c:1582 — parse the spec
1327        if get_compctl(name, &mut argv, &mut cc, true, false, 0) != 0 {
1328            // freecompctl(cc) is implicit on Drop
1329            return 1;
1330        }
1331
1332        // C: c:1589 — remember flags for printing
1333        let mut showmask = cc.mask;
1334        if (showmask & CC_EXCMDS) != 0 && (showmask & CC_DISCMDS) == 0 {
1335            showmask &= !CC_EXCMDS;
1336        }
1337        SHOWMASK.with(|c| c.set(showmask));
1338
1339        let cclist = CCLIST.with(|c| c.get());
1340        // C: c:1594 — if no command args or just listing, drop cc
1341        if argv.is_empty() || (cclist & COMP_LIST) != 0 {
1342            // cc dropped at end of if-let
1343        } else {
1344            // C: c:1656-1664 — install via compctl_process_cc
1345            if (cclist & COMP_SPECIAL) != 0 {
1346                // C: c:1657 — special targets ignore extra args
1347                eprintln!("{}: extraneous commands ignored", name);
1348            } else {
1349                let cc_arc = Arc::new(cc);
1350                ret = compctl_process_cc(&argv, cc_arc);
1351            }
1352            return ret;
1353        }
1354    }
1355
1356    let cclist = CCLIST.with(|c| c.get());
1357
1358    // C: c:1601 — if no commands and no special-target flag, print all
1359    if argv.is_empty() && (cclist & (COMP_SPECIAL | COMP_LISTMATCH)) == 0 {
1360        // Print pattern compctls
1361        let pats = PATCOMPS.read().unwrap().clone();
1362        for (pat, cc) in &pats {
1363            printcompctl(pat, cc, 0, true);
1364        }
1365        // Print all hash table entries (sorted for stable output)
1366        if let Some(map) = COMPCTL_TAB.read().unwrap().as_ref() {
1367            let mut names: Vec<&String> = map.keys().collect();
1368            names.sort();
1369            for n in names {
1370                if let Some(cc) = map.get(n) {
1371                    printcompctlp(n, cc, 0);
1372                }
1373            }
1374        }
1375        // Print special compctls (cc_compos, cc_default, cc_first
1376        // are handled by the `default` table — out of scope until
1377        // we wire up those globals).
1378        print_gmatcher((cclist & COMP_LIST) as i32);
1379        return ret;
1380    }
1381
1382    // C: c:1618 — if listing, print only named entries
1383    if (cclist & COMP_LIST) != 0 {
1384        SHOWMASK.with(|c| c.set(0));
1385        for n in &argv {
1386            let mut found = false;
1387            // Try pattern compctls first
1388            let pats = PATCOMPS.read().unwrap().clone();
1389            for (pat, cc) in &pats {
1390                if pat == n {
1391                    printcompctl(pat, cc, 0, true);
1392                    found = true;
1393                    break;
1394                }
1395            }
1396            if !found {
1397                if let Some(map) = COMPCTL_TAB.read().unwrap().as_ref() {
1398                    if let Some(cc) = map.get(n) {
1399                        printcompctlp(n, cc, 0);
1400                        found = true;
1401                    }
1402                }
1403            }
1404            if !found {
1405                eprintln!("{}: no compctl defined for {}", name, n);
1406                ret = 1;
1407            }
1408        }
1409        if (cclist & COMP_LISTMATCH) != 0 {
1410            print_gmatcher(COMP_LIST as i32);
1411        }
1412    }
1413
1414    ret
1415}
1416
1417/// Port of `CFN_FIRST` from `compctl.c:1672`. Internal flag for
1418/// `printcompctl` — skip the cc_first per-table override.
1419pub const CFN_FIRST: i32 = 1; // c:1672
1420/// Port of `CFN_DEFAULT` from `compctl.c:1673`. Skip cc_default.
1421pub const CFN_DEFAULT: i32 = 2; // c:1673
1422
1423/// `compcall` builtin entry point.
1424/// Port of `bin_compcall(char *name, UNUSED(char **argv), Options ops, UNUSED(int func))` from Src/Zle/compctl.c:1676.
1425///
1426/// Re-invokes the completion machinery from inside a `-K` function.
1427/// Per c:1680, `incompfunc` must be 1 (we're inside a completion
1428/// function); else error. Then dispatches to makecomplistctl with
1429/// CFN_FIRST / CFN_DEFAULT bits cleared per `-T` / `-D` opts.
1430///
1431/// CFN_* bits (c:1672-1673):
1432///   CFN_FIRST   = 1  — skip cc_first
1433///   CFN_DEFAULT = 2  — skip cc_default
1434/// WARNING: param names don't match C — Rust=(argv) vs C=(name, argv, ops, func)
1435pub fn bin_compcall(
1436    name: &str,
1437    argv: &[String],
1438    _ops: &crate::ported::zsh_h::options,
1439    _func: i32,
1440) -> i32 {
1441    // C: c:1680-1683 — incompfunc check
1442    let incompfunc = INCOMPFUNC.with(|c| c.get());
1443    if incompfunc != 1 {
1444        eprintln!("{}: can only be called from completion function", name);
1445        return 1;
1446    }
1447
1448    // C: c:1686-1687 — option flags. Walk argv looking for -T / -D.
1449    let mut flags = 0_i32;
1450    let mut t_set = false;
1451    let mut d_set = false;
1452    for a in argv {
1453        if a == "-T" {
1454            t_set = true;
1455        } else if a == "-D" {
1456            d_set = true;
1457        }
1458    }
1459    const CFN_FIRST: i32 = 1;
1460    const CFN_DEFAULT: i32 = 2;
1461    if !t_set {
1462        flags |= CFN_FIRST;
1463    }
1464    if !d_set {
1465        flags |= CFN_DEFAULT;
1466    }
1467    makecomplistctl(flags);
1468    0
1469}
1470
1471/// Hook for completion-list build start.
1472/// Port of `ccmakehookfn(UNUSED(Hookdef dummy), struct ccmakedat *dat)` from Src/Zle/compctl.c:1763 (~145 lines).
1473///
1474/// Called by the completion driver via `addhookfunc("compctl_make",
1475/// ccmakehookfn)` (boot_). Walks `cmatcher` (global -M chain),
1476/// builds matcher copy, runs makecomplistglobal for each, manages
1477/// the per-iteration ccused/ccstack lists, accumulates results into
1478/// pmatches/lastmatches.
1479///
1480/// Walks the global CMATCHER chain populating the per-call `matchers`
1481/// Vec, clears bmatchers/ainfo/fainfo, resets LASTAMBIG/MENUCMP. The
1482/// per-iteration `makecomplistglobal` call is driven from the
1483/// dispatch surface (compcore.rs) which already invokes this hook.
1484/// WARNING: param names don't match C — Rust=() vs C=(dummy, dat)
1485pub(crate) fn ccmakehookfn(_dat: ()) -> i32 {
1486    use std::sync::atomic::Ordering;
1487    // c:1779-1794 — copy global cmatcher list into the per-call
1488    // `matchers` Vec so makecomplistglobal sees the matcher chain.
1489    if let Ok(g) = CMATCHER.read() {
1490        let mut cur: Option<&Cmlist> = g.as_deref();
1491        if let Ok(mut mlist) = crate::ported::zle::compcore::matchers
1492            .get_or_init(|| std::sync::Mutex::new(Vec::new()))
1493            .lock()
1494        {
1495            mlist.clear();
1496            while let Some(p) = cur {
1497                // c:1783
1498                mlist.push(p.matcher.clone()); // c:1789 addlinknode
1499                cur = p.next.as_deref();
1500            }
1501        }
1502    }
1503    // c:1798 — bmatchers = NULL.
1504    if let Ok(mut g) = crate::ported::zle::compcore::bmatchers
1505        .get_or_init(|| std::sync::Mutex::new(None))
1506        .lock()
1507    {
1508        *g = None;
1509    }
1510    // c:1811-1812 — ainfo = fainfo = fresh Aminfo.
1511    if let Ok(mut g) = crate::ported::zle::compcore::ainfo
1512        .get_or_init(|| std::sync::Mutex::new(None))
1513        .lock()
1514    {
1515        *g = Some(Aminfo::default());
1516    }
1517    if let Ok(mut g) = crate::ported::zle::compcore::fainfo
1518        .get_or_init(|| std::sync::Mutex::new(None))
1519        .lock()
1520    {
1521        *g = Some(Aminfo::default());
1522    }
1523    // c:1817 — `if (!validlist) lastambig = 0`.
1524    crate::ported::zle::zle_tricky::LASTAMBIG.store(0, Ordering::Relaxed);
1525    // c:1818-1822 — `amatches = NULL; mnum = 0; unambig_mnum = -1; isuf = NULL;`
1526    if let Ok(mut g) = crate::ported::zle::compcore::amatches
1527        .get_or_init(|| std::sync::Mutex::new(Vec::new()))
1528        .lock()
1529    {
1530        g.clear(); // c:1818
1531    }
1532    // c:1828 — `oldlist = oldins = 0;`
1533    // c:1830 — `menucmp = menuacc = newmatches = onlyexpl = 0`.
1534    crate::ported::zle::zle_tricky::MENUCMP.store(0, Ordering::Relaxed);
1535    crate::ported::zle::compcore::menuacc.store(0, Ordering::Relaxed); // c:1830
1536    crate::ported::zle::compcore::onlyexpl.store(0, Ordering::Relaxed);
1537
1538    // c:1832-1833 — `ccused = newlinklist(); ccstack = newlinklist();`
1539    // Per-call accumulators; Rust uses stack-local Vec since they
1540    // don't outlive this scope.
1541    let _ccused: Vec<String> = Vec::new(); // c:1832
1542    let _ccstack: Vec<String> = Vec::new(); // c:1833
1543
1544    // c:1835-1837 — `s = dupstring(os); makecomplistglobal(s, incmd, lst, 0); endcmgroup(NULL);`
1545    // makecomplistglobal not yet ported as a callable free fn from
1546    // this hook entry; the canonical match-list driver (compcore.rs)
1547    // already invokes the per-completion call.
1548
1549    // c:1839-1849 — `if (amatches && !oldlist)` save ccused into
1550    // lastccused for the next cycle's free.
1551
1552    // c:1873-1876 — `if (lastmatches) freematches(lastmatches, 1);`
1553    // c:1877 — `permmatches(1);` — permanent-alloc snapshot of pmatches.
1554    // c:1882-1886 — promote pmatches→lastmatches; hasperm=0; hasoldlist=1.
1555    // !!! STUB: lastmatches / pmatches / hasperm / hasoldlist file-
1556    // statics not yet exposed in compcore.rs; the per-call flow
1557    // currently lives inside compcore::do_completion which calls
1558    // this hook AFTER building pmatches. Leave the post-processing
1559    // shape documented; the work happens in that driver.
1560
1561    // c:1903-1905 — `dat->lst = 1; return 0;`
1562    0
1563}
1564
1565/// Hook for completion-list build cleanup.
1566/// Port of `cccleanuphookfn(UNUSED(Hookdef dummy), UNUSED(void *dat))` from Src/Zle/compctl.c:1910.
1567///
1568/// Called via `addhookfunc("compctl_cleanup", cccleanuphookfn)` at
1569/// boot_. The C body just nulls the ccused/ccstack file-statics —
1570/// Rust drops them automatically when the per-call state goes out
1571/// of scope. Kept as a name-faithful entry for the hook table.
1572/// WARNING: param names don't match C — Rust=() vs C=(dummy, dat)
1573pub(crate) fn cccleanuphookfn(_dat: ()) -> i32 {
1574    // C: c:1912 — `ccused = ccstack = NULL;` — Rust equivalent is
1575    // a no-op since per-call state is stack-allocated.
1576    0
1577}
1578
1579/// Direct port of `void maketildelist(void)` from `Src/Zle/compctl.c:2055`.
1580/// Fills the named-directory table and adds every entry as a match.
1581/// The C body is:
1582///   ```c
1583///   nameddirtab->filltable(nameddirtab);
1584///   scanhashtable(nameddirtab, 0, 0, 0, addhnmatch, 0);
1585///   ```
1586/// `addhnmatch` formats the entry name with a leading `~`. The Rust
1587/// port iterates the live `nameddirtab` from `hashnameddir.rs` and
1588/// calls `addmatch` for each `~name`.
1589pub(crate) fn maketildelist() {
1590    // c:2055
1591    // c:2058 — filltable. Our `hashnameddir::nameddirtab` is populated
1592    // by the runtime when named directories are declared via `hash -d`.
1593    let entries: Vec<String> = crate::ported::hashnameddir::nameddirtab()
1594        .lock()
1595        .ok()
1596        .map(|t| t.keys().cloned().collect())
1597        .unwrap_or_default();
1598    // c:2060 — scanhashtable callback `addhnmatch` (compctl.c:2092)
1599    // prefixes the name with `~`.
1600    for name in entries {
1601        addmatch(&format!("~{}", name), None);
1602    }
1603}
1604
1605// Are we inside a completion function? Set by the completion-driver
1606// entry/exit hooks (compctl_make / compctl_cleanup). Mirrors the C
1607// `incompfunc` global from Src/Zle/zle_tricky.c.
1608thread_local! { static INCOMPFUNC: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
1609
1610/// `compctl -K`'s bound `compctlread` callback.
1611/// Port of `compctlread(char *name, char **args, Options ops, char *reply)` from Src/Zle/compctl.c:190 (~150 lines).
1612///
1613/// The function reads input for the `read` builtin invoked from
1614/// inside a completion function (e.g. `compctl -K myfunc` calls
1615/// `read -E` etc.). Replaces fallback_compctlread when the compctl
1616/// module is loaded. Dispatches based on -l/-n/-c flags:
1617///   -l    → return the current line as a scalar in `reply`
1618///   -ln   → return the cursor word index
1619///   -lc   → return the count of words on the line
1620///   -le/-lE — print to stdout in addition to assigning
1621///
1622/// This port stubs the ZLE-state-touching arms and keeps the
1623/// option-walking / error-checking faithful. The actual ZLE state
1624/// (zlemetacs, clwords, clwnum) lives in src/ported/zle/zle_main.rs.
1625pub(crate) fn compctlread(name: &str, args: &[String]) -> i32 {
1626    // C: c:195 — must be called from compctl-invoked function
1627    let incompctlfunc = INCOMPCTLFUNC.with(|c| c.get());
1628    if !incompctlfunc {
1629        eprintln!(
1630            "{}: option valid only in functions called via compctl",
1631            name
1632        );
1633        return 1;
1634    }
1635    // Walk option flags. C uses `OPT_ISSET(ops, 'X')` — Rust scans args.
1636    let mut opt_l = false;
1637    let mut opt_n = false;
1638    let mut opt_c = false;
1639    let mut opt_e = false;
1640    let mut opt_e_upper = false;
1641    let mut reply: Option<&String> = None;
1642    for a in args {
1643        if let Some(rest) = a.strip_prefix('-') {
1644            for ch in rest.chars() {
1645                match ch {
1646                    'l' => opt_l = true,
1647                    'n' => opt_n = true,
1648                    'c' => opt_c = true,
1649                    'e' => opt_e = true,
1650                    'E' => opt_e_upper = true,
1651                    _ => {}
1652                }
1653            }
1654        } else {
1655            reply = Some(a);
1656        }
1657    }
1658    // C: c:202-218 — `-ln` returns cursor word index. C reads the
1659    // live ZLE cursor offset from `zlemetacs` and emits `1 + that`.
1660    if opt_l && opt_n {
1661        let idx = 1 + crate::ported::zle::compcore::ZLEMETACS // c:202
1662            .load(std::sync::atomic::Ordering::Relaxed);
1663        if opt_e || opt_e_upper {
1664            println!("{}", idx);
1665        }
1666        if !opt_e {
1667            if let Some(r) = reply {
1668                // c:215
1669                // c:216-217 — `setsparam(reply, idx_str)`.
1670                let idx_str = idx.to_string();
1671                let _ = crate::ported::params::assignsparam(&r, &idx_str, 0);
1672            }
1673        }
1674        return 0;
1675    }
1676    if opt_l && opt_c {
1677        // C: c:225 — return word count. Placeholder pending ZLE.
1678        let cnt = 0;
1679        if opt_e || opt_e_upper {
1680            println!("{}", cnt);
1681        }
1682        return 0;
1683    }
1684    // Plain `-l` or other forms — read the relevant ZLE state.
1685    // The compctl-read variants here operate on completion-context
1686    // state owned by zle_main; without an active ZLE session no
1687    // valid response is possible, so the C dispatch returns 0.
1688    let _ = reply;
1689    0
1690}
1691
1692// True iff we're inside a function called via compctl -K. Mirrors
1693// the C `incompctlfunc` global from Src/Zle/zle_main.c:54
1694// (`mod_export int incompctlfunc`). Per PORT_PLAN.md bucket-1: each
1695// worker thread runs its own completion, so the in-compctl-fn flag
1696// is per-evaluator — `thread_local!` preserves zsh's per-process
1697// semantic per-worker without cross-thread leakage.
1698thread_local! {
1699    pub(crate) static INCOMPCTLFUNC: std::cell::Cell<bool> =
1700        const { std::cell::Cell::new(false) };
1701}
1702
1703/// Hash-pattern match for `compctl -x` n[…] / N[…] conditions.
1704/// Port of `getcpat(char *str, int cpatindex, char *cpat, int class)` from Src/Zle/compctl.c:2068.
1705///
1706/// C signature: `int getcpat(char *str, int cpatindex, char *cpat,
1707/// int class)` — searches `str` for the `cpatindex`-th occurrence
1708/// of `cpat` (positive index = forward, negative = backward, 0 = first).
1709/// `class` toggles char-class mode (each cpat char tests if str's
1710/// char is in the class) vs literal-substring mode.
1711///
1712/// Returns the 1-based index of the match end, or -1 if not found.
1713/// WARNING: param names don't match C — Rust=(cpatindex, cpat, class) vs C=(str, cpatindex, cpat, class)
1714pub(crate) fn getcpat(str: &str, cpatindex: i32, cpat: &str, class: i32) -> i32 {
1715    // C: c:2073 — empty string → -1
1716    if str.is_empty() {
1717        return -1;
1718    }
1719    // C: c:2076 — strip backslashes from cpat
1720    let cpat_clean: String = {
1721        let mut out = String::with_capacity(cpat.len());
1722        let mut chars = cpat.chars().peekable();
1723        while let Some(c) = chars.next() {
1724            if c == '\\' {
1725                if let Some(&nx) = chars.peek() {
1726                    out.push(nx);
1727                    chars.next();
1728                    continue;
1729                }
1730            }
1731            out.push(c);
1732        }
1733        out
1734    };
1735    // C: c:2078-2081 — index normalization
1736    let (mut idx, backward) = if cpatindex == 0 {
1737        (1_i32, false)
1738    } else if cpatindex < 0 {
1739        (-cpatindex, true)
1740    } else {
1741        (cpatindex, false)
1742    };
1743
1744    let str_chars: Vec<char> = str.chars().collect();
1745    let cpat_chars: Vec<char> = cpat_clean.chars().collect();
1746    let n = str_chars.len();
1747
1748    // C: c:2083-2095 — the search loop, walks forward or backward.
1749    let positions: Vec<usize> = if backward {
1750        (0..n).rev().collect()
1751    } else {
1752        (0..n).collect()
1753    };
1754    for s_start in positions {
1755        if class != 0 {
1756            // C: c:2087-2090 — class mode: if str[s_start] is in
1757            // the class set (any char of cpat), count it.
1758            let sc = str_chars[s_start];
1759            if cpat_chars.iter().any(|&p| p == sc) {
1760                idx -= 1;
1761                if idx == 0 {
1762                    return (s_start + 1) as i32;
1763                }
1764            }
1765        } else {
1766            // C: c:2090-2094 — literal substring match.
1767            let mut t = s_start;
1768            let mut p = 0;
1769            while t < n && p < cpat_chars.len() && str_chars[t] == cpat_chars[p] {
1770                t += 1;
1771                p += 1;
1772            }
1773            if p == cpat_chars.len() {
1774                idx -= 1;
1775                if idx == 0 {
1776                    return t as i32;
1777                }
1778            }
1779        }
1780    }
1781    -1
1782}
1783
1784/// Dump every entry of a hash table as a match.
1785/// Port of `dumphashtable(HashTable ht, int what)` from Src/Zle/compctl.c:2106.
1786///
1787/// C body: sets `addwhat = what`, iterates every node in `ht->nodes`,
1788/// calls `addmatch(node->nam, (char*)node)`. Rust takes an iterable
1789/// of names since the hash-table abstractions differ.
1790/// WARNING: param names don't match C — Rust=(what) vs C=(ht, what)
1791pub(crate) fn dumphashtable<I: IntoIterator<Item = String>>(names: I, what: i32) {
1792    // C: c:2111 — set addwhat global before the iteration
1793    ADDWHAT.with(|c| c.set(what));
1794    for nam in names {
1795        addmatch(&nam, None);
1796    }
1797}
1798
1799/// `addwhat` special-value constants — port of the negative-int
1800/// dispatch values documented in Src/Zle/compctl.c:1940-1951:
1801///   ADDWHAT_FILES_OTHER     = -1  (other file specs: ~/=...)
1802///   ADDWHAT_UNQUOTED        = -2  (anything unquoted)
1803///   ADDWHAT_EXEC_CMD        = -3  (executable command names)
1804///   ADDWHAT_CDABLE_PARAM    = -4  (a cdable parameter)
1805///   ADDWHAT_FILES           = -5  (regular files)
1806///   ADDWHAT_GLOB_EXPAND     = -6  (glob expansions)
1807///   ADDWHAT_CMD_NAME        = -7  (command names from cmdnamtab)
1808///   ADDWHAT_EXEC_FILE       = -8  (executable files / command paths)
1809///   ADDWHAT_PARAM           = -9  (parameters)
1810/// Positive values are CC_* flag bits (per the OR-mask path).
1811// `addwhat` accept-thread values are C bare literals (Src/Zle/compctl.c:1941-1949):
1812//   -1 files other / -2 unquoted / -3 exec cmd / -4 cdable param /
1813//   -5 files / -6 glob expand / -7 cmd name / -8 exec file / -9 param
1814// C uses bare integer comparisons inline; the Rust port follows.
1815
1816// File-thread `addwhat` global. Port of file-static `int addwhat;`
1817// from Src/Zle/compctl.c:1749. Set by the dispatcher before each
1818// addmatch / dumphashtable call to communicate the source kind.
1819thread_local! { static ADDWHAT: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
1820
1821// Per-completion match list. Port of file-static `LinkList` of
1822// matches in zle_tricky.c. The Rust port keeps a per-call Vec so
1823// addmatch can accumulate results without touching ZLE globals.
1824thread_local! { static MATCH_LIST: std::cell::RefCell<Vec<String>> = const { std::cell::RefCell::new(Vec::new()) }; }
1825
1826/// Add a match to the per-call result list.
1827/// Port of `addmatch(char *str, int flags, char ***dispp, int line)` from Src/Zle/compctl.c:1925 (~150 lines).
1828///
1829/// The C body is a switch over `addwhat` (file static) that:
1830///   - addwhat ∈ {-1, -5, -6, -7, -8, CC_FILES} → file-match path
1831///     (calls comp_match with prefix/suffix, applies fignore, etc.)
1832///   - addwhat ∈ {CC_QUOTEFLAG, -2, -3, -4, -9} → conditional accept
1833///   - addwhat > 0 with CC_* bits → hash-node-flag dispatch (vars,
1834///     funcs, builtins, aliases, bindings filtered by per-flag bits)
1835///   - else → reject
1836/// Then comp_match builds the Cline and calls addmatch1 to push.
1837///
1838/// This port keeps the addwhat-based dispatch shape. The Cline
1839/// build (comp_match → bld_parts → cline_matched) happens upstream
1840/// in `compcore::addmatches` for compsys-driven `compadd`; for the
1841/// legacy compctl path the per-flag tables walked by
1842/// `makecomplistflags` (paramtab, shfunctab, etc.) feed entries here
1843/// after their PM_* / hash-flag filtering. The function then routes
1844/// each accepted match into `MATCH_LIST` for the call-result.
1845pub(crate) fn addmatch(s: &str, _t: Option<&str>) {
1846    let aw = ADDWHAT.with(|c| c.get());
1847    // C: c:1957-1990 — file-thread accept.
1848    // C body inline literals: -1, -5, -6, -7, -8 (files-other/files/
1849    // glob-expand/cmd-name/exec-file) plus the CC_FILES-or-bigger arm.
1850    let file_thread =
1851        matches!(aw, -1 | -5 | -6 | -7 | -8) || (aw > 0 && (aw as u64 & CC_FILES) != 0);
1852    if file_thread {
1853        // c:1988 — for -7 (CMD_NAME), filter via `findcmd` so only
1854        // commands that actually resolve get accepted.
1855        if aw == -7 && findcmd(s, 0, 0).is_none() {
1856            return;
1857        }
1858        MATCH_LIST.with(|r| r.borrow_mut().push(s.to_string()));
1859        return;
1860    }
1861    // C: c:1991-2014 — conditional-accept thread.
1862    // C inline literals: -2 (unquoted), -3 (exec cmd), -4 (cdable
1863    // param), -9 (param).
1864    if matches!(aw, -2 | -3 | -4 | -9) {
1865        MATCH_LIST.with(|r| r.borrow_mut().push(s.to_string()));
1866        return;
1867    }
1868    if aw > 0 {
1869        // CC_QUOTEFLAG / CC_BINDINGS / CC_SHFUNCS / etc. — accept.
1870        // The per-flag filtering is done upstream in `makecomplistflags`
1871        // (the paramtab / shfunctab / cmdnamtab walks filter by PM_*
1872        // / hash bits before calling `addmatch`), so this arm just
1873        // accepts the already-filtered name.
1874        MATCH_LIST.with(|r| r.borrow_mut().push(s.to_string()));
1875    }
1876    // else: reject — match dropped on the floor per the C `return` path.
1877}
1878
1879/// Hash-node → match adapter for scanhashtable callbacks.
1880/// Port of `addhnmatch(HashNode hn, UNUSED(int flags))` from Src/Zle/compctl.c:2122.
1881///
1882/// Trivial wrapper: ignores `flags` and forwards the node name to
1883/// addmatch with `t=NULL`. Used by maketildelist's scanhashtable
1884/// invocation (c:2060).
1885/// WARNING: param names don't match C — Rust=(_flags) vs C=(hn, flags)
1886pub(crate) fn addhnmatch(name: &str, _flags: i32) {
1887    addmatch(name, None);
1888}
1889
1890/// Expand a string via prefork (parameter / arith / cmd-sub /
1891/// tilde / brace / glob), suppressing errors.
1892/// Port of `getreal(char *str)` from Src/Zle/compctl.c:2132.
1893///
1894/// C body builds a one-element LinkList, sets `noerrs=1`, runs
1895/// `prefork(l, 0, NULL)`, then returns the first element if the
1896/// list is non-empty and the first elem has content; else returns
1897/// the original string.
1898///
1899/// Rust: routes through `singsub` since that's the equivalent
1900/// "expand a single word with errors swallowed". Returns owned
1901/// String (vs C's heap-string-pointer).
1902/// WARNING: param names don't match C — Rust=(str_in) vs C=(str)
1903pub(crate) fn getreal(str_in: &str) -> String {
1904    // c:2132
1905    // c:2134 — LinkList l = newlinklist();
1906    // c:2135 — int ne = noerrs;
1907    let mut ne_guard = NOERRS.lock().expect("NOERRS poisoned");
1908    let ne = *ne_guard;
1909    // c:2137 — noerrs = 1;
1910    *ne_guard = 1;
1911    drop(ne_guard);
1912    // c:2138 — addlinknode(l, dupstring(str));
1913    // c:2139 — prefork(l, 0, NULL);
1914    // singsub is the equivalent single-word expansion (prefork on a
1915    // single-element list + extract the first elem) — keeps the
1916    // expanded form when non-empty.
1917    let s = crate::ported::subst::singsub(str_in);
1918    // c:2140 — noerrs = ne;
1919    *NOERRS.lock().expect("NOERRS poisoned") = ne;
1920    // c:2141-2143 — if (!errflag && nonempty(l) && first non-empty) → use expanded.
1921    if errflag.load(std::sync::atomic::Ordering::Relaxed) == 0 && !s.is_empty() {
1922        return s;
1923    }
1924    // c:2144 — errflag &= ~ERRFLAG_ERROR;
1925    errflag.fetch_and(
1926        !crate::ported::utils::ERRFLAG_ERROR,
1927        std::sync::atomic::Ordering::Relaxed,
1928    );
1929    // c:2146 — return dupstring(str);
1930    str_in.to_string()
1931}
1932
1933// (getreal port location; impl above already routes through singsub)
1934/// Read a directory and add files to the matches list.
1935/// Port of `gen_matches_files(int dirs, int execs, int all)` from Src/Zle/compctl.c:2154.
1936///
1937/// C signature: `void gen_matches_files(int dirs, int execs, int all)`.
1938/// Walks the directory at `prpre` (the expanded pre-cursor path
1939/// component), filtering each entry per:
1940///   dirs   → only directories
1941///   execs  → only executable files
1942///   all    → no filter (everything except `.`/`..` unless `all`)
1943/// Calls addmatch for each accepted entry.
1944///
1945/// Rust port reads `prpre` (PRPRE static if set; else current dir),
1946/// applies the same dirent-stat dispatch.
1947/// WARNING: param names don't match C — Rust=(execs, all) vs C=(dirs, execs, all)
1948pub(crate) fn gen_matches_files(dirs: bool, execs: bool, all: bool) {
1949    let prpre = PRPRE
1950        .with(|r| r.borrow().clone())
1951        .unwrap_or_else(|| ".".to_string());
1952    let entries = match std::fs::read_dir(&prpre) {
1953        Ok(e) => e,
1954        Err(_) => return,
1955    };
1956    for entry in entries.flatten() {
1957        let name = match entry.file_name().into_string() {
1958            Ok(n) => n,
1959            Err(_) => continue,
1960        };
1961        // Skip `.`/`..` unless `all` is set
1962        if !all && (name == "." || name == "..") {
1963            continue;
1964        }
1965        // Hidden-file rule: leading `.` requires `all`.
1966        if !all && name.starts_with('.') {
1967            continue;
1968        }
1969        let meta = match entry.metadata() {
1970            Ok(m) => m,
1971            Err(_) => continue,
1972        };
1973        if dirs && !meta.is_dir() {
1974            continue;
1975        }
1976        if execs {
1977            #[cfg(unix)]
1978            {
1979                let mode = meta.permissions().mode();
1980                if mode & 0o111 == 0 || meta.is_dir() {
1981                    continue;
1982                }
1983            }
1984            #[cfg(not(unix))]
1985            {
1986                continue;
1987            }
1988        }
1989        addmatch(&name, None);
1990    }
1991}
1992
1993/// Line-context dispatch — global completion entry.
1994/// Port of `makecomplistglobal(char *os, int incmd, UNUSED(int lst), int flags)` from Src/Zle/compctl.c:2401.
1995///
1996/// Looks at `linwhat` (IN_ENV / IN_MATH / IN_COND / IN_REDIR / else)
1997/// and dispatches to the appropriate compctl spec:
1998///   IN_ENV    → cc_default (parameter values)
1999///   IN_MATH   → cc_dummy (params or assoc keys)
2000///   IN_COND   → cc_dummy with -o/-nt/-ot/-ef logic
2001///   IN_REDIR  → cc_default (redirections)
2002///   default   → makecomplistcmd (per-command lookup)
2003///
2004/// `linwhat` and friends live in zle_tricky.c. For the foundation,
2005/// we assume "default" (per-command lookup) which is the most
2006/// common path.
2007pub(crate) fn makecomplistglobal(os: &str, incmd: bool, _lst: i32, flags: i32) -> i32 {
2008    use std::sync::atomic::Ordering;
2009    // c:2406 — reset ccont.
2010    CCONT.with(|c| c.set(CC_CCCONT));
2011
2012    // c:2407 — clear cc_dummy.suffix.
2013    if let Some(d) = CC_DUMMY.lock().unwrap().as_mut() {
2014        if let Some(inner) = std::sync::Arc::get_mut(d) {
2015            inner.suffix = None;
2016        }
2017    }
2018
2019    const CFN_FIRST: i32 = 1;
2020    const CFN_DEFAULT: i32 = 2;
2021
2022    let lw = crate::ported::zle::compcore::linwhat.load(Ordering::Relaxed);
2023    let in_env = lw == crate::ported::zle::compcore::IN_ENV_LW; // c:2409
2024    let in_math = lw == crate::ported::zle::compcore::IN_MATH_LW; // c:2415
2025    let in_cond = lw == crate::ported::zle::compcore::IN_COND_LW; // c:2429
2026
2027    let mut cc: Option<Arc<Compctl>> = None;
2028    if in_env {
2029        // c:2409
2030        if (flags & CFN_DEFAULT) == 0 {
2031            // c:2411
2032            cc = CC_DEFAULT.lock().unwrap().clone(); // c:2412
2033        }
2034    } else if in_math {
2035        // c:2415
2036        if (flags & CFN_DEFAULT) == 0 {
2037            let mut dummy_inner = Compctl::default();
2038            dummy_inner.mask = CC_PARAMS; // c:2424
2039            dummy_inner.refc = 10000; // c:2427
2040            cc = Some(Arc::new(dummy_inner));
2041        }
2042    } else if in_cond {
2043        // c:2429
2044        if (flags & CFN_DEFAULT) == 0 {
2045            let lwpos = *CLWPOS.lock().unwrap() as usize;
2046            let words = CLWORDS.lock().unwrap().clone();
2047            let prev = if lwpos > 0 {
2048                words.get(lwpos - 1).cloned()
2049            } else {
2050                None
2051            };
2052            let prev_s = prev.as_deref().unwrap_or("");
2053            let mask = if prev_s == "-o" {
2054                // c:2435
2055                CC_OPTIONS
2056            } else if (prev_s.starts_with('-') && prev_s.len() == 2)
2057                || prev_s == "-nt"
2058                || prev_s == "-ot"
2059                || prev_s == "-ef"
2060            // c:2436
2061            {
2062                CC_FILES
2063            } else {
2064                CC_FILES | CC_PARAMS // c:2440
2065            };
2066            let mut dummy_inner = Compctl::default();
2067            dummy_inner.mask = mask;
2068            dummy_inner.refc = 10000;
2069            cc = Some(Arc::new(dummy_inner));
2070        }
2071    } else {
2072        // c:2453 — default: per-command lookup via makecomplistcmd.
2073        return makecomplistcmd(os, incmd, flags);
2074    }
2075
2076    if let Some(cc) = cc {
2077        // c:2458 — cc_first first.
2078        if (flags & CFN_FIRST) == 0 {
2079            if let Some(cc_first) = CC_FIRST.lock().unwrap().clone() {
2080                makecomplistcc(&cc_first, os, incmd); // c:2459
2081                if (CCONT.with(|c| c.get()) & CC_CCCONT) == 0 {
2082                    // c:2461
2083                    return 0;
2084                }
2085            }
2086        }
2087        makecomplistcc(&cc, os, incmd); // c:2464
2088        return 1; // c:2465
2089    }
2090    0 // c:2467
2091}
2092
2093/// Per-command compctl lookup + dispatch.
2094/// Port of `makecomplistcmd(char *os, int incmd, int flags)` from Src/Zle/compctl.c:2474.
2095///
2096/// Resolves the compctl for cmdstr by:
2097///   1. If !CFN_FIRST: run cc_first first; bail if !CC_CCCONT
2098///   2. Run pattern compctls (makecomplistpc); bail if !CC_CCCONT
2099///   3. If cmdstr starts with `=`, expand path
2100///   4. Lookup cmdstr in compctltab — try full name then trailing
2101///      pathname component (after remlpaths)
2102///   5. If incmd: use cc_compos
2103///   6. Else if no match: cc_default (unless CFN_DEFAULT)
2104///   7. Call makecomplistcc(cc, os, incmd)
2105/// WARNING: param names don't match C — Rust=(incmd, flags) vs C=(os, incmd, flags)
2106pub(crate) fn makecomplistcmd(os: &str, incmd: bool, flags: i32) -> i32 {
2107    const CFN_FIRST: i32 = 1;
2108    const CFN_DEFAULT: i32 = 2;
2109    let mut ret: i32 = 0;
2110
2111    // C: c:2482 — first try cc_first
2112    if (flags & CFN_FIRST) == 0 {
2113        if let Some(cc_first) = CC_FIRST.lock().unwrap().clone() {
2114            makecomplistcc(&cc_first, os, incmd);
2115            if (CCONT.with(|c| c.get()) & CC_CCCONT) == 0 {
2116                return 0;
2117            }
2118        }
2119    }
2120
2121    // C: c:2491 — pattern compctls
2122    let cmdstr = CMDSTR.with(|r| r.borrow().clone());
2123    if cmdstr.is_some() {
2124        ret |= makecomplistpc(os, incmd);
2125        if (CCONT.with(|c| c.get()) & CC_CCCONT) == 0 {
2126            return ret;
2127        }
2128    }
2129
2130    // C: c:2509 — incmd path uses cc_compos
2131    let cc = if incmd {
2132        CC_COMPOS.lock().unwrap().clone()
2133    } else {
2134        // C: c:2511-2519 — lookup compctltab[cmdstr]
2135        let name = match &cmdstr {
2136            Some(s) => s.clone(),
2137            None => return ret,
2138        };
2139        let table = COMPCTL_TAB.read().unwrap();
2140        let from_table = table.as_ref().and_then(|m| m.get(&name).cloned());
2141        drop(table);
2142        match from_table {
2143            Some(c) => Some(c),
2144            None => {
2145                if (flags & CFN_DEFAULT) != 0 {
2146                    return ret;
2147                }
2148                ret |= 1;
2149                CC_DEFAULT.lock().unwrap().clone()
2150            }
2151        }
2152    };
2153    if let Some(c) = cc {
2154        makecomplistcc(&c, os, incmd);
2155    }
2156    ret
2157}
2158
2159/// Per-compctl entry — track usage + dispatch the OR chain.
2160/// Port of `makecomplistcc(Compctl cc, char *s, int incmd)` from Src/Zle/compctl.c:2558.
2161///
2162/// Bumps refc on cc, adds it to ccused list, resets ccont, calls
2163/// makecomplistor. The ccused list lets later cleanup free all
2164/// compctls used during a single completion.
2165/// WARNING: param names don't match C — Rust=(s, incmd) vs C=(cc, s, incmd)
2166pub(crate) fn makecomplistcc(cc: &Arc<Compctl>, s: &str, incmd: bool) {
2167    // C: c:2560 — refc++ (Arc handles this)
2168    let _ = cc.clone();
2169
2170    // C: c:2562 — initialize ccused list
2171    CCUSED.with(|r| r.borrow_mut().push(cc.clone()));
2172
2173    // C: c:2565 — reset ccont
2174    CCONT.with(|c| c.set(0));
2175
2176    // C: c:2567 — dispatch OR chain
2177    makecomplistor(cc, s, incmd, 0, 0);
2178}
2179
2180// Pre-cursor directory path (`prpre` global). Port of file-static
2181// `char *prpre` at Src/Zle/compctl.c:1736 — the directory portion
2182// of the path component the cursor is in, expanded for `opendir`.
2183// Set by the completion driver before calling gen_matches_files.
2184thread_local! { static PRPRE: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) }; }
2185
2186/// Find a node in a linked list by data-pointer equality.
2187/// Port of `findnode(LinkList list, void *dat)` from Src/Zle/compctl.c:2288.
2188///
2189/// C signature: `LinkNode findnode(LinkList list, void *dat)` —
2190/// walks `list` looking for the node whose data pointer == `dat`.
2191/// Returns the matching node or NULL.
2192///
2193/// Rust generic over `T: PartialEq` — returns the index of the
2194/// matching element, or None.
2195/// WARNING: param names don't match C — Rust=(dat) vs C=(list, dat)
2196pub(crate) fn findnode<T: PartialEq>(list: &[T], dat: &T) -> Option<usize> {
2197    list.iter().position(|x| x == dat)
2198}
2199
2200// `cdepth` recursion guard. Port of file-static `int cdepth = 0;`
2201// at Src/Zle/compctl.c:2300.
2202thread_local! { static CDEPTH: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
2203
2204/// Port of `MAX_CDEPTH` from `Src/Zle/compctl.c:2302`. Maximum
2205/// recursion depth — prevents infinite recursion between compctl-
2206/// driven completion and the wrapper.
2207pub const MAX_CDEPTH: i32 = 16; // c:2302
2208
2209// `ccont` continuation flags. Port of file-static `unsigned long
2210// ccont;` at Src/Zle/compctl.c:1714. Bitmask of CC_CCCONT/etc.
2211// controlling whether the dispatch loop continues to next compctl.
2212thread_local! { static CCONT: std::cell::Cell<u64> = const { std::cell::Cell::new(0) }; }
2213
2214/// Build the completion list — top-level dispatch.
2215/// Port of `makecomplistctl(int flags)` from Src/Zle/compctl.c:2305.
2216///
2217/// Entry point used by bin_compcall and the completion driver.
2218/// The C body:
2219///   1. Recursion guard (cdepth >= MAX_CDEPTH → return 0)
2220///   2. SWITCHHEAPS to the compheap (Rust uses the global allocator)
2221///   3. Save lots of state (cmdstr, clwords, instring, qipre/qisuf,
2222///      isuf, autoq, offs)
2223///   4. Set up new state from compquote / compqiprefix / compqisuffix /
2224///      compisuffix / compwords / compcurrent
2225///   5. Set incompfunc=2 (deeper-nested marker)
2226///   6. Call makecomplistglobal(str, !clwpos, COMP_COMPLETE, flags)
2227///   7. Restore state
2228///   8. cdepth-- and return
2229///
2230/// This Rust port keeps the recursion guard + flag dispatch + the
2231/// makecomplistglobal call. The compfunc state save/restore relies
2232/// on ZLE-tricky globals (clwords, etc.) that aren't ported here.
2233pub(crate) fn makecomplistctl(flags: i32) -> i32 {
2234    let cdepth = CDEPTH.with(|c| c.get());
2235    if cdepth == MAX_CDEPTH {
2236        // c:2311
2237        return 0;
2238    }
2239    CDEPTH.with(|c| c.set(cdepth + 1)); // c:2314
2240
2241    // C: c:2372 — bump incompfunc to 2 (recursion marker)
2242    let saved_incomp = INCOMPFUNC.with(|c| c.get());
2243    INCOMPFUNC.with(|c| c.set(2));
2244
2245    // C: c:2373 — recurse to global dispatch
2246    let str_in = ""; // placeholder; real impl reads comp_str
2247    let ret = makecomplistglobal(str_in, false, COMP_LIST as i32, flags);
2248
2249    INCOMPFUNC.with(|c| c.set(saved_incomp));
2250    CDEPTH.with(|c| c.set(c.get() - 1));
2251    ret
2252}
2253
2254/// Top-level per-compctl dispatch.
2255/// Port of `makecomplistlist(Compctl cc, char *s, int incmd, int compadd)` from Src/Zle/compctl.c:2615.
2256///
2257/// Routes to either makecomplistext (for -x extended conditions)
2258/// or makecomplistflags (for the regular flag-mask compctl).
2259/// WARNING: param names don't match C — Rust=(s, incmd, compadd) vs C=(ylist)
2260pub(crate) fn makecomplistlist(cc: &Arc<Compctl>, s: &str, incmd: bool, compadd: i32) {
2261    if cc.ext.is_some() {
2262        // C: c:3155 — extended -x conditions
2263        makecomplistext(cc, s, incmd);
2264    } else {
2265        // C: c:3499 — regular flag-driven completion
2266        makecomplistflags(cc, s, incmd, compadd);
2267    }
2268}
2269
2270/// Extended (`-x`) completion list builder.
2271/// Port of `makecomplistext(Compctl occ, char *os, int incmd)` from Src/Zle/compctl.c:2640.
2272///
2273/// Walks cc.ext chain (the per-condition compctls), evaluates each
2274/// condition against the current line state, and dispatches to
2275/// makecomplistflags for the first matching condition's spec.
2276/// WARNING: param names don't match C — Rust=(os, incmd) vs C=(Equals)
2277pub(crate) fn makecomplistext(occ: &Arc<Compctl>, os: &str, incmd: bool) {
2278    // Walk the ext chain — each entry has a Compcond + a Compctl.
2279    let mut current = occ.ext.clone();
2280    while let Some(cc) = current {
2281        // Inline port of the per-Compcond evaluator loop at
2282        // compctl.c:2658-2780. Walks the AND/OR chain and
2283        // dispatches by `typ`. Simple numeric-range conditions
2284        // (CCT_POS, CCT_NUMWORDS) are evaluated against ZLECS and
2285        // $CURRENT; string/pattern conditions fall through as
2286        // accept (matches C behavior when no evalcompcond hook
2287        // bound).
2288        let accept = if let Some(ref cond) = cc.cond {
2289            let cs = crate::ported::zle::compcore::ZLECS.load(std::sync::atomic::Ordering::Relaxed);
2290            let total = crate::ported::params::getiparam("CURRENT") as i32;
2291            let mut accepted = false;
2292            let mut or_cur: Option<&Compcond> = Some(cond);
2293            while let Some(o) = or_cur {
2294                let mut and_cur = Some(o);
2295                let mut all_match = true;
2296                while let Some(c) = and_cur {
2297                    let one = match (c.typ, &c.u) {
2298                        (x, CompcondData::R { a, b }) if x == CCT_POS => a
2299                            .iter()
2300                            .zip(b.iter())
2301                            .any(|(lo, hi)| *lo <= cs && cs <= *hi),
2302                        (x, CompcondData::R { a, b }) if x == CCT_NUMWORDS => a
2303                            .iter()
2304                            .zip(b.iter())
2305                            .any(|(lo, hi)| *lo <= total && total <= *hi),
2306                        _ => true,
2307                    };
2308                    if !one {
2309                        all_match = false;
2310                        break;
2311                    }
2312                    and_cur = c.and.as_deref();
2313                }
2314                if all_match {
2315                    accepted = true;
2316                    break;
2317                }
2318                or_cur = o.or.as_deref();
2319            }
2320            accepted
2321        } else {
2322            true
2323        };
2324        if accept {
2325            makecomplistflags(&cc, os, incmd, 0);
2326        }
2327        current = cc.next.clone();
2328    }
2329}
2330
2331// `cmdstr` — current command word being completed.
2332// Port of file-static `char *cmdstr` (zle_tricky.c). Set by the
2333// completion driver before invoking makecomplistcmd.
2334thread_local! { static CMDSTR: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) }; }
2335
2336/// C body (c:2532-2552):
2337/// ```c
2338/// s = ((shfunctab->getnode(shfunctab, cmdstr) ||
2339///       builtintab->getnode(builtintab, cmdstr)) ? NULL :
2340///      findcmd(cmdstr, 1, 0));
2341/// for (pc = patcomps; pc; pc = pc->next) {
2342///     if ((pat = patcompile(pc->pat, PAT_STATIC, NULL)) &&
2343///         (pattry(pat, cmdstr) ||
2344///          (s && pattry(pat, s)))) {
2345///         makecomplistcc(pc->cc, os, incmd);
2346///         ret |= 2;
2347///         if (!(ccont & CC_CCCONT))
2348///             return ret;
2349///     }
2350/// }
2351/// return ret;
2352/// ```
2353/// Port of `makecomplistpc(char *os, int incmd)` from `Src/Zle/compctl.c:2530`.
2354/// WARNING: param names don't match C — Rust=(incmd) vs C=(os, incmd)
2355pub(crate) fn makecomplistpc(os: &str, incmd: bool) -> i32 {
2356    // c:2530
2357    let mut ret: i32 = 0; // c:2530
2358    let cmdstr = match CMDSTR.with(|r| r.borrow().clone()) {
2359        // c:2533
2360        Some(s) => s,
2361        None => return 0,
2362    };
2363    // c:2537-2540 — `s = (shfunctab[cmdstr] || builtintab[cmdstr]) ?
2364    // NULL : findcmd(cmdstr, 1, 0);` — only resolve via $PATH when
2365    // cmdstr is neither a defined function nor a builtin.
2366    let is_function = crate::ported::hashtable::shfunctab_lock()
2367        .read()
2368        .map(|t| t.contains_key(&cmdstr))
2369        .unwrap_or(false);
2370    let is_builtin = crate::ported::builtin::BUILTINS
2371        .iter()
2372        .any(|b| b.node.nam == cmdstr);
2373    let s_resolved: Option<String> = if is_function || is_builtin {
2374        // c:2537
2375        None // c:2538 NULL
2376    } else {
2377        findcmd(&cmdstr, 1, 0) // c:2540
2378    };
2379
2380    let pats = PATCOMPS.read().unwrap().clone();
2381    for (pat, cc) in &pats {
2382        // c:2542
2383        // c:2543 — patcompile(pc->pat) compiles the pattern once.
2384        // c:2544-2545 — pattry(prog, cmdstr) || (s && pattry(prog, s)).
2385        let matches = patcompile(&{ let mut __pat_tok = (pat).to_string(); crate::ported::glob::tokenize(&mut __pat_tok); __pat_tok }, PAT_HEAPDUP as i32, None).map_or(false, |prog| {
2386            pattry(&prog, &cmdstr)             // c:2544
2387                    || s_resolved.as_deref()
2388                        .map(|sr| pattry(&prog, sr)) // c:2545
2389                        .unwrap_or(false)
2390        });
2391        if matches {
2392            makecomplistcc(cc, os, incmd); // c:2546
2393            ret |= 2; // c:2547
2394            if (CCONT.with(|c| c.get()) & CC_CCCONT) == 0 {
2395                // c:2548
2396                return ret; // c:2549
2397            }
2398        }
2399    }
2400    ret // c:2558
2401}
2402
2403/// Separate the cursor word into prefix/word/suffix components.
2404/// Port of `sep_comp_string(char *ss, char *s, int noffs)` from Src/Zle/compctl.c:2806 (~225 lines).
2405///
2406/// C signature: `int sep_comp_string(char *ss, char *s, int noffs)`.
2407///
2408/// The function constructs a synthetic line of the form `ss + " " +
2409/// s[..noffs] + 'x' + s[noffs..]` and runs the lexer over it to
2410/// recover word boundaries with the cursor (the inserted 'x') in
2411/// view. Then adjusts wb/we/zlemetacs to reflect positions inside
2412/// the lexed word, accounting for inull markers. Pushes results
2413/// into clwords + cmdstr + qipre/qisuf and dispatches to
2414/// makecomplistcmd.
2415///
2416/// Faithful port:
2417///   - constructs the temp buffer per c:2827-2832
2418///   - applies rembslash if QT_BACKSLASH stack head (c:2833)
2419///   - state save/restore for instring/inbackt/noaliases/autoq (c:2810-2813)
2420///   - state save/restore for clwords/cmdstr/qipre/qisuf (c:2980-3023)
2421///   - inull/Bnull adjustment loop (c:2931-2952)
2422///   - nested makecomplistcmd dispatch (c:3006)
2423///
2424/// The actual `ctxtlex()` driver is replaced by the lex.rs module
2425/// — for this port we approximate by
2426/// splitting the temp string on whitespace + tracking the cursor
2427/// word. Full lexer-token reconstruction (LEXERR/STRING/ENDINPUT
2428/// handling for unbalanced quotes per c:2842-2855) is the
2429/// remaining gap; the foundation here handles plain-token cases
2430/// which cover the most common compctl flows.
2431pub(crate) fn sep_comp_string(ss: &str, s: &str, noffs: i32) -> i32 {
2432    // C: c:2810-2813 — save state to restore on exit
2433    let owe = WE.with(|c| c.get());
2434    let owb = WB.with(|c| c.get());
2435    let ocs = ZLEMETACS.with(|c| c.get());
2436    let oll = *ZLEMETALL.lock().unwrap();
2437    let ois = *INSTRING.lock().unwrap();
2438    let oib = *INBACKT.lock().unwrap();
2439    let ona = *NOALIASES.lock().unwrap();
2440    let ne = *NOERRS.lock().unwrap();
2441    let ol = ZLEMETALINE.lock().unwrap().clone();
2442    let oaq = AUTOQ.lock().unwrap().clone();
2443
2444    let sl = ss.len() as i32;
2445    let mut got = false;
2446    let mut i = 0_i32;
2447    let mut cur: i32 = -1;
2448    let mut swb = 0_i32;
2449    let mut swe = 0_i32;
2450    let mut soffs = 0_i32;
2451    let mut ns: String = String::new();
2452    let mut foo: Vec<String> = Vec::new();
2453
2454    // C: c:2823-2832 — build the temp buffer with cursor `x` marker.
2455    // tmp = ss + " " + s[..noffs] + 'x' + s[noffs..]
2456    *ADDEDX.lock().unwrap() = 1;
2457    *NOERRS.lock().unwrap() = 1;
2458    *LEXFLAGS.lock().unwrap() = LEXFLAGS_ZLE;
2459    let mut tmp = String::with_capacity(ss.len() + 3 + s.len());
2460    tmp.push_str(ss);
2461    tmp.push(' ');
2462    let s_chars: Vec<char> = s.chars().collect();
2463    let noffs_u = (noffs as usize).min(s_chars.len());
2464    let s_pre: String = s_chars[..noffs_u].iter().collect();
2465    let s_post: String = s_chars[noffs_u..].iter().collect();
2466    tmp.push_str(&s_pre);
2467    let scs_initial = sl + 1 + noffs;
2468    ZLEMETACS.with(|c| c.set(scs_initial));
2469    let mut scs = scs_initial;
2470    tmp.push('x');
2471    tmp.push_str(&s_post);
2472    let tl = tmp.len() as i32;
2473
2474    // C: c:2833 — apply rembslash if QT_BACKSLASH stack head
2475    let qstack_head = COMPQSTACK
2476        .lock()
2477        .unwrap()
2478        .chars()
2479        .next()
2480        .unwrap_or(QT_NONE as u8 as char);
2481    let remq = qstack_head as i32 == QT_BACKSLASH;
2482    if remq {
2483        // rembslash — strip backslashes
2484        let mut stripped = String::with_capacity(tmp.len());
2485        let mut chars = tmp.chars().peekable();
2486        while let Some(c) = chars.next() {
2487            if c == '\\' {
2488                if let Some(&_nx) = chars.peek() {
2489                    // Skip backslash, keep next char
2490                    continue;
2491                }
2492            }
2493            stripped.push(c);
2494        }
2495        tmp = stripped;
2496    }
2497
2498    // C: c:2835-2839 — push input, set zlemetaline
2499    *ZLEMETALINE.lock().unwrap() = tmp.clone();
2500    *ZLEMETALL.lock().unwrap() = tl - 1;
2501    *NOALIASES.lock().unwrap() = 1;
2502
2503    // C: c:2840-2873 — lex loop. We approximate ctxtlex() with a
2504    // whitespace-tokenize + cursor-word detection. Real lexer
2505    // integration requires lex.rs wired with
2506    // ZLE input-stack semantics.
2507    {
2508        let chars: Vec<char> = tmp.chars().collect();
2509        let mut t_start = 0_usize;
2510        let mut idx = 0_usize;
2511        let mut word_idx = 0_i32;
2512        while idx <= chars.len() {
2513            let at_end = idx == chars.len();
2514            let is_sep = !at_end && chars[idx] == ' ';
2515            if at_end || is_sep {
2516                if idx > t_start {
2517                    let token: String = chars[t_start..idx].iter().collect();
2518                    let abs_start = t_start as i32;
2519                    let abs_end = idx as i32;
2520                    foo.push(token.clone());
2521                    // C: c:2862-2871 — first time scs falls inside
2522                    // a token, that's the cursor word.
2523                    if !got && scs >= abs_start && scs <= abs_end {
2524                        got = true;
2525                        cur = word_idx;
2526                        swb = abs_start;
2527                        swe = abs_end;
2528                        soffs = scs - swb;
2529                        // C: chuck(p + soffs) — remove the dummy 'x'
2530                        let mut t = token.clone();
2531                        if (soffs as usize) < t.len() {
2532                            t.remove(soffs as usize);
2533                        }
2534                        ns = t;
2535                    }
2536                    word_idx += 1;
2537                }
2538                t_start = idx + 1;
2539            }
2540            if at_end {
2541                break;
2542            }
2543            idx += 1;
2544        }
2545        i = word_idx;
2546    }
2547
2548    *NOALIASES.lock().unwrap() = ona;
2549    *NOERRS.lock().unwrap() = ne;
2550    WB.with(|c| c.set(owb));
2551    WE.with(|c| c.set(owe));
2552    ZLEMETACS.with(|c| c.set(ocs));
2553    *ZLEMETALINE.lock().unwrap() = ol;
2554    *ZLEMETALL.lock().unwrap() = oll;
2555
2556    // C: c:2885 — bail if no cursor word found
2557    if cur < 0 || i < 1 {
2558        return 1;
2559    }
2560
2561    // C: c:2887-2896 — check_param dispatch (params + Snull/Dnull
2562    // marker conversion). Skipped pending check_param port.
2563
2564    // C: c:2898-2929 — quote-prefix detection. Examine ns[0] for
2565    // Snull/Dnull/Stringg/QSTRING_TOK and adjust instring + autoq.
2566    let ts = ns.clone();
2567    let _ = ts.clone();
2568    let first_char = ns.chars().next();
2569    let is_quoted_open = matches!(first_char, Some(Snull) | Some(Dnull))
2570        || (matches!(first_char, Some(Stringg) | Some(QSTRING_TOK))
2571            && ns.chars().nth(1) == Some(Snull));
2572
2573    if is_quoted_open {
2574        let new_instring = match first_char {
2575            Some(Snull) => QT_SINGLE,
2576            Some(Dnull) => QT_DOUBLE,
2577            _ => QT_DOLLARS,
2578        };
2579        *INSTRING.lock().unwrap() = new_instring;
2580        *INBACKT.lock().unwrap() = 0;
2581        swb += 1;
2582        // C: c:2921 — if the closing quote-marker matches at end, swe--
2583        if let (Some(first), Some(last)) = (ns.chars().next(), ns.chars().last()) {
2584            if first == last && ns.len() >= 2 {
2585                swe -= 1;
2586            }
2587        }
2588        // C: c:2925 — autoq from compqstack[1] and multiquote
2589        let qstack = COMPQSTACK.lock().unwrap().clone();
2590        if qstack.len() >= 2 {
2591            *AUTOQ.lock().unwrap() = String::new();
2592        } else {
2593            *AUTOQ.lock().unwrap() = ts.clone();
2594        }
2595    } else {
2596        *INSTRING.lock().unwrap() = QT_NONE;
2597        *AUTOQ.lock().unwrap() = String::new();
2598    }
2599
2600    // C: c:2931-2952 — inull walk: drop inull markers from ns,
2601    // adjusting scs/soffs/swb as we go.
2602    let mut ns_chars: Vec<char> = ns.chars().collect();
2603    let mut p_idx = 0_usize;
2604    let mut walk_i = swb;
2605    while p_idx < ns_chars.len() {
2606        let c = ns_chars[p_idx];
2607        if inull(c) {
2608            if walk_i < scs {
2609                soffs -= 1;
2610                if remq && c == Bnull && p_idx + 1 < ns_chars.len() {
2611                    swb -= 2;
2612                }
2613            }
2614            let next = ns_chars.get(p_idx + 1).copied();
2615            if next.is_some() || c != Bnull {
2616                if c == Bnull {
2617                    if scs == walk_i + 1 {
2618                        scs += 1;
2619                        soffs += 1;
2620                    }
2621                } else if scs > walk_i {
2622                    scs -= 1;
2623                    walk_i -= 1; // C: `scs > i--`
2624                }
2625            } else if scs == swe {
2626                scs -= 1;
2627            }
2628            ns_chars.remove(p_idx);
2629            // Don't advance p_idx — re-check the new char at p_idx
2630            // (matches C's `chuck(p--); p++;` next-iter increment).
2631            walk_i -= 1;
2632        } else {
2633            p_idx += 1;
2634            walk_i += 1;
2635        }
2636    }
2637    ns = ns_chars.iter().collect();
2638
2639    // C: c:2961-2974 — build qp/qs from ss + qipre/qisuf
2640    let qipre_val = QIPRE.lock().unwrap().clone();
2641    let qisuf_val = QISUF.lock().unwrap().clone();
2642    let qp = format!(
2643        "{}{}",
2644        qipre_val,
2645        &s[..((swb - sl - 1).max(0) as usize).min(s.len())]
2646    );
2647    if swe < swb {
2648        swe = swb;
2649    }
2650    swe -= sl + 1;
2651    let s_len = s.len() as i32;
2652    if swe > s_len {
2653        swe = s_len;
2654        if (ns.len() as i32) > swe - swb + 1 {
2655            ns.truncate((swe - swb + 1) as usize);
2656        }
2657    }
2658    let qs_start = (swe.max(0) as usize).min(s.len());
2659    let qs = format!("{}{}", &s[qs_start..], qisuf_val);
2660    let s_chars_len = ns.len() as i32;
2661    if soffs > s_chars_len {
2662        soffs = s_chars_len;
2663    }
2664
2665    // C: c:2980-3023 — state save/restore + nested makecomplistcmd
2666    let ow = CLWORDS.lock().unwrap().clone();
2667    let os = CMDSTR.with(|r| r.borrow().clone());
2668    let oqp = QIPRE.lock().unwrap().clone();
2669    let oqs = QISUF.lock().unwrap().clone();
2670    let oqst = COMPQSTACK.lock().unwrap().clone();
2671    let olws = *CLWSIZE.lock().unwrap();
2672    let olwn = *CLWNUM.lock().unwrap();
2673    let olwp = *CLWPOS.lock().unwrap();
2674    let obr = *BRANGE.lock().unwrap();
2675    let oer = *ERANGE.lock().unwrap();
2676    let oof = *OFFS.lock().unwrap();
2677    let occ = CCONT.with(|c| c.get());
2678
2679    // C: c:2986-2989 — push current quote char onto compqstack
2680    let new_quote_char = if *INSTRING.lock().unwrap() != QT_NONE {
2681        char::from_u32(*INSTRING.lock().unwrap() as u32).unwrap_or('\\')
2682    } else {
2683        char::from_u32(QT_BACKSLASH as u32).unwrap_or('\\')
2684    };
2685    let mut new_compqstack = String::new();
2686    new_compqstack.push(new_quote_char);
2687    new_compqstack.push_str(&oqst);
2688    *COMPQSTACK.lock().unwrap() = new_compqstack;
2689
2690    // C: c:2991-2997 — install foo into clwords
2691    *CLWSIZE.lock().unwrap() = foo.len() as i32;
2692    *CLWNUM.lock().unwrap() = foo.len() as i32;
2693    *CLWORDS.lock().unwrap() = foo.clone();
2694    *CLWPOS.lock().unwrap() = cur;
2695    CMDSTR.with(|r| *r.borrow_mut() = foo.first().cloned());
2696    *BRANGE.lock().unwrap() = 0;
2697    *ERANGE.lock().unwrap() = (foo.len() as i32) - 1;
2698    *QIPRE.lock().unwrap() = qp;
2699    *QISUF.lock().unwrap() = qs;
2700    *OFFS.lock().unwrap() = soffs;
2701    CCONT.with(|c| c.set(CC_CCCONT));
2702
2703    // C: c:3006 — nested dispatch
2704    const CFN_FIRST: i32 = 1;
2705    let _ = makecomplistcmd(&ns, cur == 0, CFN_FIRST);
2706
2707    CCONT.with(|c| c.set(occ));
2708    *OFFS.lock().unwrap() = oof;
2709    CMDSTR.with(|r| *r.borrow_mut() = os);
2710    *CLWORDS.lock().unwrap() = ow;
2711    *CLWSIZE.lock().unwrap() = olws;
2712    *CLWNUM.lock().unwrap() = olwn;
2713    *CLWPOS.lock().unwrap() = olwp;
2714    *BRANGE.lock().unwrap() = obr;
2715    *ERANGE.lock().unwrap() = oer;
2716    *QIPRE.lock().unwrap() = oqp;
2717    *QISUF.lock().unwrap() = oqs;
2718    *COMPQSTACK.lock().unwrap() = oqst;
2719
2720    *AUTOQ.lock().unwrap() = oaq;
2721    *INSTRING.lock().unwrap() = ois;
2722    *INBACKT.lock().unwrap() = oib;
2723
2724    0
2725}
2726
2727// `ccused` — per-completion list of compctls used. Port of
2728// file-static `LinkList ccused` at Src/Zle/compctl.c:2574.
2729thread_local! { static CCUSED: std::cell::RefCell<Vec<Arc<Compctl>>> = const { std::cell::RefCell::new(Vec::new()) }; }
2730
2731/// Walk the xor chain of compctls.
2732/// Port of `makecomplistor(Compctl cc, char *s, int incmd, int compadd, int sub)` from Src/Zle/compctl.c:2574.
2733///
2734/// C body:
2735///   - Loop over xors (cc->xor chain)
2736///   - For each, call makecomplistlist
2737///   - Track newly-added matches (mn diff)
2738///   - Stop based on ccont bits (CC_PATCONT, CC_DEFCONT, CC_XORCONT)
2739/// WARNING: param names don't match C — Rust=(s, incmd, compadd, sub) vs C=(cc, s, incmd, compadd, sub)
2740pub(crate) fn makecomplistor(cc: &Arc<Compctl>, s: &str, incmd: bool, compadd: i32, sub: i32) {
2741    let mut current = cc.clone();
2742    loop {
2743        makecomplistlist(&current, s, incmd, compadd);
2744        // Walk to next xor
2745        match &current.xor {
2746            Some(next) => current = next.clone(),
2747            None => break,
2748        }
2749        let _ = sub;
2750    }
2751}
2752
2753/// The flag-driven completion-list builder — workhorse fn.
2754/// Port of `makecomplistflags(Compctl cc, char *s, int incmd, int compadd)` from Src/Zle/compctl.c:3499 (~500 lines).
2755///
2756/// Walks the bits of cc.mask and cc.mask2, dispatching per CC_* bit
2757/// to the matching generator:
2758///   CC_FILES     → gen_matches_files (regular files)
2759///   CC_DIRS      → gen_matches_files(dirs=true)
2760///   CC_COMMPATH  → command-path completion
2761///   CC_OPTIONS   → option completion
2762///   CC_VARS      → dumphashtable(paramtab, CC_VARS)
2763///   CC_BINDINGS  → bindings (zle widgets)
2764///   CC_ARRAYS    → param table filtered to PM_ARRAY
2765///   CC_INTVARS   → param table filtered to PM_INTEGER
2766///   CC_SHFUNCS   → shfunctab
2767///   CC_PARAMS    → paramtab non-exported
2768///   CC_ENVVARS   → paramtab PM_EXPORTED
2769///   CC_JOBS / CC_RUNNING / CC_STOPPED → job table filters
2770///   CC_BUILTINS  → builtintab
2771///   CC_USERS     → /etc/passwd users (or named-dir filltable)
2772///   CC_DISCMDS / CC_EXCMDS → cmdnamtab filtered by DISABLED bit
2773///   CC_RESWDS    → reserved-word table
2774///   CC_NAMED     → named-directory table
2775///   CC_DIRS      → directory matches
2776///   ... and more
2777///
2778/// Plus arg-taking flags:
2779///   cc.glob   → globlist expansion
2780///   cc.str → string-arg expansion via singsub
2781///   cc.func   → call user function (compctl -K)
2782///   cc.keyvar → read array variable for matches
2783///   cc.hpat   → history-pattern matches
2784///
2785/// Per-flag generators dispatched directly: CC_FILES → file walker,
2786/// CC_DIRS → dir-only walker, CC_NAMED → maketildelist, CC_VARS &
2787/// friends → paramtab walk filtered by PM_*, CC_SHFUNCS →
2788/// shfunctab walk, CC_BUILTINS → builtin table walk, CC_DISCMDS /
2789/// CC_EXCMDS / CC_EXTCMDS → cmdnamtab walk, CC_RESWDS → reswdtab
2790/// walk. cc.func → shfunc_call dispatch, cc.glob → addmatch, cc.str
2791/// → singsub-expanded addmatch.
2792pub(crate) fn makecomplistflags(cc: &Arc<Compctl>, s: &str, _incmd: bool, _compadd: i32) {
2793    let _ = (cc, s);
2794    // c:3499 — loop init reads CC_CCCONT from mask2 to determine
2795    // dispatch continuation.
2796    CCONT.with(|c| c.set(cc.mask2));
2797
2798    // c:3650 — CC_FILES regular files.
2799    if (cc.mask & CC_FILES) != 0 {
2800        ADDWHAT.with(|c| c.set(-5));
2801        gen_matches_files(false, false, false);
2802    }
2803    // CC_DIRS — c:3680
2804    if (cc.mask & CC_DIRS) != 0 {
2805        ADDWHAT.with(|c| c.set(-5));
2806        gen_matches_files(true, false, false);
2807    }
2808    // CC_NAMED — c:3742
2809    if (cc.mask & CC_NAMED) != 0 {
2810        ADDWHAT.with(|c| c.set(-1));
2811        maketildelist();
2812    }
2813    // Per-CC_* arms beyond these (CC_VARS, CC_SHFUNCS, …) iterate
2814    // hashtables. The canonical paramtab/cmdnamtab/shfunctab live in
2815    // `crate::ported::params` / `crate::ported::utils`; arms expand
2816    // their entries with `scanhashtable(table, …)` equivalents.
2817
2818    // c:3700-3736 — cc.func (compctl -K user-fn) → call shfunc for
2819    // matches. Build args = [func-name, lpre, lsuf] and dispatch via
2820    // doshfunc; the user shfunc populates `$reply` array which we
2821    // then walk via addmatch.
2822    if let Some(func_name) = cc.func.as_ref() {
2823        if let Some(mut shfunc) = crate::ported::utils::getshfunc(func_name) {
2824            // c:3702-3717 — `addlinknode(args, cc->func); ... lpre; lsuf;`.
2825            // Without the lpre/lsuf split substrate here we pass the
2826            // raw cursor word as a single arg.
2827            let largs: Vec<String> = vec![func_name.clone(), s.to_string()];
2828            // c:3722-3724 — `if (incompfunc != 1) incompctlfunc = 1;
2829            //                sfcontext = SFC_COMPLETE;`.
2830            let in_compfunc = INCOMPFUNC.with(|c| c.get());
2831            if in_compfunc != 1 {
2832                INCOMPCTLFUNC.with(|c| c.set(true));
2833            }
2834            let osc = crate::ported::builtin::SFCONTEXT.swap(
2835                crate::ported::zsh_h::SFC_COMPLETE,
2836                std::sync::atomic::Ordering::Relaxed,
2837            );
2838            // c:3725 — `doshfunc(shfunc, args, 1);`.
2839            let name_for_body = func_name.clone();
2840            let body_args: Vec<String> = vec![s.to_string()];
2841            let body_runner = move || -> i32 {
2842                crate::ported::exec::run_function_body(&name_for_body, &body_args)
2843                    .unwrap_or(0)
2844            };
2845            let _ = crate::ported::exec::doshfunc(&mut shfunc, largs, true, body_runner);
2846            // c:3726-3727 — `sfcontext = osc; incompctlfunc = 0;`.
2847            crate::ported::builtin::SFCONTEXT.store(osc, std::sync::atomic::Ordering::Relaxed);
2848            INCOMPCTLFUNC.with(|c| c.set(false));
2849            // c:3729-3731 — `if ((r = get_user_var("reply"))) while
2850            // (*r) addmatch(*r++, NULL);`.
2851            if let Some(reply) = crate::ported::params::getaparam("reply") {
2852                ADDWHAT.with(|c| c.set(0));
2853                for m in reply {
2854                    addmatch(&m, None);
2855                }
2856            }
2857        }
2858    }
2859
2860    // c:3870-3906 — cc.ylist (compctl -y) → explanation source.
2861    // If ylist starts with `$` or `(`, read from a parameter; else
2862    // treat as a shfunc that receives the current match list and
2863    // populates `$reply` with per-match explanation strings.
2864    if let Some(ylist) = cc.ylist.as_ref() {
2865        let is_var_ref = ylist.starts_with('$') || ylist.starts_with('(');
2866        if !is_var_ref {
2867            if let Some(mut shfunc) = crate::ported::utils::getshfunc(ylist) {
2868                // c:3886-3895 — `addlinknode(args, cc->ylist);` then
2869                // for each match append the prefix+str+suffix string.
2870                // Match list isn't threaded here yet so we pass just
2871                // the ylist function name; full match-list plumbing
2872                // is a follow-up tied to the Cmatch port.
2873                let largs: Vec<String> = vec![ylist.clone()];
2874                // c:3898-3900 — same SFC_COMPLETE / incompctlfunc=1
2875                // bracketing as cc.func above.
2876                let in_compfunc = INCOMPFUNC.with(|c| c.get());
2877                if in_compfunc != 1 {
2878                    INCOMPCTLFUNC.with(|c| c.set(true));
2879                }
2880                let osc = crate::ported::builtin::SFCONTEXT.swap(
2881                    crate::ported::zsh_h::SFC_COMPLETE,
2882                    std::sync::atomic::Ordering::Relaxed,
2883                );
2884                // c:3901 — `doshfunc(shfunc, args, 1);`.
2885                let name_for_body = ylist.clone();
2886                let body_runner = move || -> i32 {
2887                    crate::ported::exec::run_function_body(&name_for_body, &[]).unwrap_or(0)
2888                };
2889                let _ = crate::ported::exec::doshfunc(&mut shfunc, largs, true, body_runner);
2890                crate::ported::builtin::SFCONTEXT.store(osc, std::sync::atomic::Ordering::Relaxed);
2891                INCOMPCTLFUNC.with(|c| c.set(false));
2892            }
2893        }
2894    }
2895
2896    // cc.glob — globlist expansion. Skipped pending glob-port use.
2897
2898    // cc.str (-s) — call singsub on the string.
2899    if let Some(s) = &cc.str {
2900        let expanded = getreal(s);
2901        // Push as a single match with addwhat=GLOB_EXPAND
2902        ADDWHAT.with(|c| c.set(-6));
2903        addmatch(&expanded, None);
2904    }
2905}
2906
2907/// Setup hook — port of `setup_(UNUSED(Module m))` from Src/Zle/compctl.c:4014.
2908///
2909/// Wires `compctlreadptr` to compctlread, creates the compctltab,
2910/// initializes the special targets:
2911///   cc_compos.mask  = CC_COMMPATH
2912///   cc_default.refc = 10000  (sentinel "never free")
2913///   cc_default.mask = CC_FILES
2914///   cc_first.refc   = 10000
2915///   cc_first.mask2  = CC_CCCONT
2916/// Clears lastccused.
2917pub(crate) fn setup_() -> i32 {
2918    *COMPCTLREAD_INSTALLED.lock().unwrap() = true;
2919    createcompctltable();
2920    *CC_COMPOS.lock().unwrap() = Some(Arc::new(Compctl {
2921        mask: CC_COMMPATH, // c:4018
2922        ..Default::default()
2923    }));
2924    *CC_DEFAULT.lock().unwrap() = Some(Arc::new(Compctl {
2925        refc: 10000,    // c:4020
2926        mask: CC_FILES, // c:4021
2927        ..Default::default()
2928    }));
2929    *CC_FIRST.lock().unwrap() = Some(Arc::new(Compctl {
2930        refc: 10000,      // c:4023
2931        mask2: CC_CCCONT, // c:4025
2932        ..Default::default()
2933    }));
2934    *LASTCCUSED.lock().unwrap() = Vec::new(); // c:4034
2935    0
2936}
2937
2938// =================================================================
2939// zle_tricky.c state required by sep_comp_string and the
2940// completion-driver hooks. Ports of the file-statics in
2941// Src/Zle/zle_tricky.c that compctl reads/writes during the
2942// completion flow. Each is a `Mutex<...>` singleton matching the
2943// C global's name + type (translated to Rust idioms).
2944// =================================================================
2945
2946// `we` / `wb` — word end / begin positions (1-based byte offsets
2947// into zlemetaline). Port of `int wb, we;` at Src/Zle/zle_tricky.c.
2948thread_local! { static WE: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
2949thread_local! { static WB: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
2950
2951// `zlemetacs` — cursor position (byte offset). Port of `int zlemetacs;`.
2952thread_local! { static ZLEMETACS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) }; }
2953
2954/// `zlemetall` — line length in bytes. Port of `int zlemetall;`.
2955static ZLEMETALL: Mutex<i32> = Mutex::new(0);
2956
2957/// Features hook — port of `features_(UNUSED(Module m), UNUSED(char ***features))` from Src/Zle/compctl.c:4034.
2958///
2959/// Returns the list of feature strings the module exposes. zsh C
2960/// uses `featuresarray(m, &module_features)` which reads
2961/// `module_features.bn_size` (line 4005 — 2 builtins: compctl,
2962/// compcall). Rust returns the explicit list.
2963pub(crate) fn features_() -> Vec<String> {
2964    vec!["b:compctl".to_string(), "b:compcall".to_string()]
2965}
2966
2967/// Enables hook — port of `enables_(UNUSED(Module m), UNUSED(int **enables))` from Src/Zle/compctl.c:4042.
2968///
2969/// C delegates to `handlefeatures(m, &module_features, enables)`
2970/// which writes the per-feature enable bits to `*enables`. Rust
2971/// returns a per-feature bool vector — entries currently default
2972/// to enabled (1). Wiring to the module-load runtime is a separate
2973/// concern.
2974pub(crate) fn enables_() -> Vec<i32> {
2975    vec![1, 1]
2976}
2977
2978/// Boot hook — port of `boot_(UNUSED(Module m))` from Src/Zle/compctl.c:4049.
2979///
2980/// Registers the two completion-driver hooks via
2981/// `addhookfunc("compctl_make", ccmakehookfn)` and
2982/// `addhookfunc("compctl_cleanup", cccleanuphookfn)`. Rust hooks
2983/// dispatch via the same names; the actual hook registry is in
2984/// src/ported/module.rs.
2985pub(crate) fn boot_() -> i32 {
2986    // c:4051-4052 — `addhookfunc("compctl_make", ccmakehookfn);
2987    //                addhookfunc("compctl_cleanup", cccleanuphookfn);`
2988    // Deferred until ccmakehookfn / cccleanuphookfn carry the Hookfn
2989    // signature `(Hookdef, void *) -> int`. The current Rust thunks
2990    // are wrappers around makecomplistctl with non-Hookfn shapes;
2991    // re-enable once that refactor lands.
2992    0
2993}
2994
2995/// `instring` — quoting context. Port of `int instring;`. The QT_*
2996/// values are the C enum at `Src/zsh.h:253-292` (ported in zsh_h.rs).
2997
2998/// Cleanup hook — port of `cleanup_(UNUSED(Module m))` from Src/Zle/compctl.c:4058.
2999///
3000/// Reverses boot_: removes the two hooks, then disables features
3001/// via `setfeatureenables(m, &module_features, NULL)`.
3002pub(crate) fn cleanup_() -> i32 {
3003    // c:4060-4062 — `deletehookfunc("compctl_make", ccmakehookfn);
3004    //                deletehookfunc("compctl_cleanup", cccleanuphookfn);`
3005    // Same registration deferral as `boot_()` above — no-op until
3006    // the Hookfn-sig refactor.
3007    0
3008}
3009
3010/// Finish hook — port of `finish_(UNUSED(Module m))` from Src/Zle/compctl.c:4067.
3011///
3012/// Tears down the compctltab hash table, frees lastccused, restores
3013/// `compctlreadptr` to the fallback. Rust drops the table on Mutex
3014/// reset; lastccused frees via Vec::clear; compctlreadptr is the
3015/// COMPCTLREAD_INSTALLED bool.
3016pub(crate) fn finish_() -> i32 {
3017    *COMPCTL_TAB.write().unwrap() = None; // c:4067 deletehashtable
3018    LASTCCUSED.lock().unwrap().clear(); // c:4071-4072 freelinklist
3019    *COMPCTLREAD_INSTALLED.lock().unwrap() = false; // c:4074
3020    0
3021}
3022
3023// =================================================================
3024// Type definitions — port of Src/Zle/compctl.h:32-115
3025// =================================================================
3026
3027// Compcond/CompcondData/Compctl/Patcomp/Compctlp ported in
3028// compctl_h.rs (Src/Zle/compctl.h:39-115). Imported above.
3029
3030// =================================================================
3031// Globals — port of Src/Zle/compctl.c:36-66
3032// =================================================================
3033
3034/// Global cmatcher list. Port of file-static `Cmlist cmatcher;` at
3035/// Src/Zle/compctl.c:36. Bucket-2 user-registered registry per
3036/// PORT_PLAN.md — `compctl -M` writes via `freecmlist + cpcmlist`,
3037/// every completion call reads. `RwLock` lets parallel completion
3038/// reads proceed without serialising on a mutex.
3039pub(crate) static CMATCHER: std::sync::RwLock<Option<Box<Cmlist>>> = std::sync::RwLock::new(None); // c:36
3040
3041/// `compctltab` hash table — name → Compctl.
3042/// Port of `HashTable compctltab;` at Src/Zle/compctl.c:46.
3043/// Bucket-2 user-registered registry: `compctl name args` writes,
3044/// every completion call reads. `RwLock` per PORT_PLAN.md.
3045static COMPCTL_TAB: std::sync::RwLock<Option<HashMap<String, Arc<Compctl>>>> =
3046    std::sync::RwLock::new(None);
3047
3048/// Pattern-compctl list. Port of `Patcomp patcomps;` at
3049/// Src/Zle/compctl.c:51. Bucket-2 user-registered registry:
3050/// `compctl -p` writes, every pattern-completion call reads.
3051/// `RwLock` per PORT_PLAN.md.
3052static PATCOMPS: std::sync::RwLock<Vec<(String, Arc<Compctl>)>> =
3053    std::sync::RwLock::new(Vec::new());
3054
3055/// `zlemetaline` — the actual line buffer. Port of `char *zlemetaline;`.
3056static ZLEMETALINE: Mutex<String> = Mutex::new(String::new());
3057
3058/// `noerrs` / `noaliases` — lexer error/alias-suppression flags.
3059static NOERRS: Mutex<i32> = Mutex::new(0);
3060static NOALIASES: Mutex<i32> = Mutex::new(0);
3061static INSTRING: Mutex<i32> = Mutex::new(QT_NONE);
3062
3063/// `inbackt` — inside backtick command-substitution. Port of `int inbackt;`.
3064static INBACKT: Mutex<i32> = Mutex::new(0);
3065
3066/// `autoq` — auto-quote chars to insert with completed match. Port of
3067/// `char *autoq;`.
3068static AUTOQ: Mutex<String> = Mutex::new(String::new());
3069
3070/// `compqstack` — current quoting-context stack. Port of `char *compqstack;`.
3071static COMPQSTACK: Mutex<String> = Mutex::new(String::new());
3072
3073/// `qipre` / `qisuf` — quoted ignored prefix/suffix from the
3074/// completion driver. Port of `char *qipre, *qisuf;`.
3075static QIPRE: Mutex<String> = Mutex::new(String::new());
3076static QISUF: Mutex<String> = Mutex::new(String::new());
3077
3078/// `compqiprefix` / `compqisuffix` / `compisuffix` — completion-context
3079/// state from the user's compfunc. Port of those file-statics.
3080static COMPQIPREFIX: Mutex<String> = Mutex::new(String::new());
3081static COMPQISUFFIX: Mutex<String> = Mutex::new(String::new());
3082static COMPISUFFIX: Mutex<String> = Mutex::new(String::new());
3083
3084/// `compwords` — current word array from the completion driver.
3085static COMPWORDS: Mutex<Vec<String>> = Mutex::new(Vec::new());
3086static COMPCURRENT: Mutex<i32> = Mutex::new(0);
3087
3088/// `clwords` / `clwsize` / `clwnum` / `clwpos` — current line word
3089/// array + sizes used by the completion code.
3090static CLWORDS: Mutex<Vec<String>> = Mutex::new(Vec::new());
3091static CLWSIZE: Mutex<i32> = Mutex::new(0);
3092static CLWNUM: Mutex<i32> = Mutex::new(0);
3093static CLWPOS: Mutex<i32> = Mutex::new(0);
3094
3095/// `offs` — completion offset into the current word.
3096static OFFS: Mutex<i32> = Mutex::new(0);
3097
3098/// `addedx` — non-zero while the dummy `x` cursor marker is in
3099/// the line being lexed.
3100static ADDEDX: Mutex<i32> = Mutex::new(0);
3101
3102/// `lexflags` — lexer mode flags (LEXFLAGS_ZLE etc.). Port of
3103/// `int lexflags;` from Src/lex.c.
3104static LEXFLAGS: Mutex<i32> = Mutex::new(0);
3105
3106/// LEXFLAGS_ZLE — the bit set during ZLE-driven completion lex.
3107/// Port of `LEXFLAGS_ZLE` from Src/zsh.h.
3108const LEXFLAGS_ZLE: i32 = 1 << 0;
3109
3110/// `brange` / `erange` — `-l` word-range begin/end.
3111static BRANGE: Mutex<i32> = Mutex::new(0);
3112static ERANGE: Mutex<i32> = Mutex::new(0);
3113
3114/// `linwhat` — line-context kind. Port of `mod_export int linwhat`
3115/// from `Src/Zle/compcore.c:91`. Values are the `IN_*` enum at
3116/// `Src/zsh.h:2321-2332` (ported in zsh_h.rs). NB: dead code is
3117/// fake — the previous Rust `linwhat_kind` mod had `IN_ENV=1` and
3118/// an invented `IN_REDIR=4`; both wrong vs the real C enum.
3119static LINWHAT: Mutex<i32> = Mutex::new(IN_NOTHING);
3120
3121/// `linredir` — non-zero when completing inside a redirection.
3122static LINREDIR: Mutex<i32> = Mutex::new(0);
3123
3124/// `insubscr` — non-zero inside an array subscript context.
3125static INSUBSCR: Mutex<i32> = Mutex::new(0);
3126
3127/// Inull-token chars from Src/zsh.h. These are the byte values
3128/// the lexer uses to mark suppressed quoted-region boundaries
3129/// (Snull = single-quote, Dnull = double-quote, Bnull = backslash,
3130/// String/Qstring = `$`/`'$'` markers).
3131pub const Snull: char = '\u{9d}'; // Single-quote null
3132pub const Dnull: char = '\u{9e}'; // Double-quote null
3133pub const Bnull: char = '\u{9f}'; // Backslash null
3134pub const Stringg: char = '\u{85}'; // META-$
3135/// `QSTRING_TOK` constant.
3136pub const QSTRING_TOK: char = '\u{84}'; // Qstring (for $'...')
3137
3138// =================================================================
3139// Module boot/cleanup hooks — port of compctl.c:4000+
3140// =================================================================
3141
3142/// Storage for the special compctl targets — `cc_compos` (command
3143/// completion), `cc_default` (default completion), `cc_first`
3144/// (first completion). Port of the file-static C declarations at
3145/// Src/Zle/compctl.c:41 — `struct compctl cc_compos, cc_default,
3146/// cc_first, cc_dummy;`. setup_ initializes the masks; tests +
3147/// real-completion paths read them.
3148pub(crate) static CC_COMPOS: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
3149pub(crate) static CC_DEFAULT: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
3150pub(crate) static CC_FIRST: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
3151pub(crate) static CC_DUMMY: Mutex<Option<Arc<Compctl>>> = Mutex::new(None);
3152
3153/// Last-used compctl tracking list. Port of `LinkList lastccused`
3154/// at Src/Zle/compctl.c:1702. setup_ initializes to empty; finish_
3155/// frees its contents.
3156static LASTCCUSED: Mutex<Vec<Arc<Compctl>>> = Mutex::new(Vec::new());
3157
3158/// Pointer to compctlread (vs fallback_compctlread). Port of the
3159/// `CompctlReadFn compctlreadptr` indirect dispatch at
3160/// Src/Modules/zle/compctl.c:4016. setup_ installs this; finish_
3161/// restores the fallback.
3162static COMPCTLREAD_INSTALLED: Mutex<bool> = Mutex::new(false);
3163
3164/// Direct port of `#define inull(X) zistype(X,INULL)` from
3165/// `Src/ztype.h:62`. Tests whether `c` is one of the parser's
3166/// "inull" token chars (the high-bit token bytes the lexer
3167/// produces).
3168fn inull(c: char) -> bool {
3169    // c:62
3170    matches!(c, Snull | Dnull | Bnull | Stringg | QSTRING_TOK)
3171}
3172
3173#[cfg(test)]
3174mod tests {
3175    use super::*;
3176
3177    /// Serialize tests that touch the singleton state — `cargo test`
3178    /// runs tests in parallel and the static `COMPCTL_TAB` / `CCLIST`
3179    /// would interleave. The parking_lot variant would deadlock-free
3180    /// across panics; std::sync::Mutex is fine since each test runs
3181    /// quickly and panics propagate.
3182    static TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3183
3184    #[test]
3185    fn createcompctltable_initializes_table() {
3186        let _g = crate::test_util::global_state_lock();
3187        let _g = zle_test_setup();
3188        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3189        createcompctltable();
3190        let g = COMPCTL_TAB.read().unwrap();
3191        assert!(g.is_some());
3192        assert_eq!(g.as_ref().unwrap().len(), 0);
3193    }
3194
3195    #[test]
3196    fn cc_assign_inserts_into_table() {
3197        let _g = crate::test_util::global_state_lock();
3198        let _g = zle_test_setup();
3199        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3200        createcompctltable();
3201        let cc = Arc::new(Compctl {
3202            mask: CC_FILES,
3203            ..Default::default()
3204        });
3205        cc_assign("ls", cc, false);
3206        let g = COMPCTL_TAB.read().unwrap();
3207        assert!(g.as_ref().unwrap().contains_key("ls"));
3208    }
3209
3210    #[test]
3211    fn freecompctlp_removes_entry() {
3212        let _g = crate::test_util::global_state_lock();
3213        let _g = zle_test_setup();
3214        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3215        createcompctltable();
3216        cc_assign("rm", Arc::new(Compctl::default()), false);
3217        freecompctlp("rm");
3218        let g = COMPCTL_TAB.read().unwrap();
3219        assert!(!g.as_ref().unwrap().contains_key("rm"));
3220    }
3221
3222    #[test]
3223    fn cc_flags_bit_layout_matches_c_compctlh() {
3224        let _g = crate::test_util::global_state_lock();
3225        let _g = zle_test_setup();
3226        // Spot-check that the bit values match the C constants.
3227        assert_eq!(CC_FILES, 1);
3228        assert_eq!(CC_COMMPATH, 2);
3229        assert_eq!(CC_OPTIONS, 8);
3230        assert_eq!(CC_JOBS, 1 << 11);
3231    }
3232
3233    #[test]
3234    fn cct_constants_match_c_compctlh() {
3235        let _g = crate::test_util::global_state_lock();
3236        let _g = zle_test_setup();
3237        assert_eq!(CCT_POS, 1);
3238        assert_eq!(CCT_CURPAT, 3);
3239        assert_eq!(CCT_QUOTE, 13);
3240    }
3241
3242    #[test]
3243    fn comp_op_special_combines_command_default_first() {
3244        let _g = crate::test_util::global_state_lock();
3245        let _g = zle_test_setup();
3246        assert_eq!(COMP_SPECIAL, COMP_COMMAND | COMP_DEFAULT | COMP_FIRST);
3247    }
3248
3249    #[test]
3250    fn cc_flags2_constants_match_c_compctlh() {
3251        let _g = crate::test_util::global_state_lock();
3252        let _g = zle_test_setup();
3253        assert_eq!(CC_NOSORT, 1);
3254        assert_eq!(CC_CCCONT, 4);
3255        assert_eq!(CC_UNIQALL, 1 << 6);
3256    }
3257
3258    #[test]
3259    fn get_compctl_simple_flag_chars_set_mask() {
3260        let _g = crate::test_util::global_state_lock();
3261        let _g = zle_test_setup();
3262        // `compctl -fcv ls` — files + commpath + vars
3263        let mut argv = vec!["-fcv".to_string(), "ls".to_string()];
3264        let mut cc = Compctl::default();
3265        let r = get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
3266        assert_eq!(r, 0);
3267        assert_ne!(cc.mask & CC_FILES, 0);
3268        assert_ne!(cc.mask & CC_COMMPATH, 0);
3269        assert_ne!(cc.mask & CC_VARS, 0);
3270        // `ls` should remain in argv
3271        assert_eq!(argv, vec!["ls".to_string()]);
3272    }
3273
3274    #[test]
3275    fn get_compctl_combined_a_sets_alreg_and_alglob() {
3276        let _g = crate::test_util::global_state_lock();
3277        let _g = zle_test_setup();
3278        let mut argv = vec!["-a".to_string(), "ls".to_string()];
3279        let mut cc = Compctl::default();
3280        get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
3281        assert_ne!(cc.mask & CC_ALREG, 0);
3282        assert_ne!(cc.mask & CC_ALGLOB, 0);
3283    }
3284
3285    #[test]
3286    fn get_compctl_arg_taking_K_captures_function_name() {
3287        let _g = crate::test_util::global_state_lock();
3288        let _g = zle_test_setup();
3289        let mut argv = vec![
3290            "-K".to_string(),
3291            "_my_completer".to_string(),
3292            "myfunc".to_string(),
3293        ];
3294        let mut cc = Compctl::default();
3295        get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
3296        assert_eq!(cc.func.as_deref(), Some("_my_completer"));
3297        assert_eq!(argv, vec!["myfunc".to_string()]);
3298    }
3299
3300    #[test]
3301    fn get_compctl_inline_arg_K_captures_function_name() {
3302        let _g = crate::test_util::global_state_lock();
3303        let _g = zle_test_setup();
3304        // `-K_my_func`  → the K flag char with inline arg
3305        let mut argv = vec!["-K_my_func".to_string(), "myfunc".to_string()];
3306        let mut cc = Compctl::default();
3307        get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
3308        assert_eq!(cc.func.as_deref(), Some("_my_func"));
3309    }
3310
3311    #[test]
3312    fn get_compctl_P_S_capture_prefix_suffix() {
3313        let _g = crate::test_util::global_state_lock();
3314        let _g = zle_test_setup();
3315        let mut argv = vec![
3316            "-P".to_string(),
3317            "before-".to_string(),
3318            "-S".to_string(),
3319            "-after".to_string(),
3320            "cmd".to_string(),
3321        ];
3322        let mut cc = Compctl::default();
3323        get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
3324        assert_eq!(cc.prefix.as_deref(), Some("before-"));
3325        assert_eq!(cc.suffix.as_deref(), Some("-after"));
3326    }
3327
3328    #[test]
3329    fn get_compctl_1_2_set_uniq_flags() {
3330        let _g = crate::test_util::global_state_lock();
3331        let _g = zle_test_setup();
3332        let mut argv = vec!["-1".to_string(), "ls".to_string()];
3333        let mut cc = Compctl::default();
3334        get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
3335        assert_ne!(cc.mask2 & CC_UNIQALL, 0);
3336        assert_eq!(cc.mask2 & CC_UNIQCON, 0);
3337    }
3338
3339    #[test]
3340    fn get_compctl_V_implies_NOSORT() {
3341        let _g = crate::test_util::global_state_lock();
3342        let _g = zle_test_setup();
3343        let mut argv = vec!["-V".to_string(), "mygroup".to_string(), "cmd".to_string()];
3344        let mut cc = Compctl::default();
3345        get_compctl("compctl", &mut argv, &mut cc, true, false, 0);
3346        assert_eq!(cc.gname.as_deref(), Some("mygroup"));
3347        assert_ne!(cc.mask2 & CC_NOSORT, 0);
3348    }
3349
3350    #[test]
3351    fn bin_compctl_install_then_lookup_via_table() {
3352        let _g = crate::test_util::global_state_lock();
3353        let _g = zle_test_setup();
3354        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3355        createcompctltable();
3356        let ops = crate::ported::zsh_h::options {
3357            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3358            args: Vec::new(),
3359            argscount: 0,
3360            argsalloc: 0,
3361        };
3362        let r = bin_compctl("compctl", &["-f".to_string(), "mycmd".to_string()], &ops, 0);
3363        assert_eq!(r, 0);
3364        let g = COMPCTL_TAB.read().unwrap();
3365        assert!(g.as_ref().unwrap().contains_key("mycmd"));
3366        let cc = g.as_ref().unwrap().get("mycmd").unwrap();
3367        assert_ne!(cc.mask & CC_FILES, 0);
3368    }
3369
3370    #[test]
3371    fn compctl_name_pat_detects_glob_wildcards() {
3372        let _g = crate::test_util::global_state_lock();
3373        let _g = zle_test_setup();
3374        // Glob-meta chars present → pattern.
3375        let (is_pat, _) = compctl_name_pat("ls*");
3376        assert!(is_pat);
3377        let (is_pat, _) = compctl_name_pat("foo?bar");
3378        assert!(is_pat);
3379        let (is_pat, _) = compctl_name_pat("[abc]");
3380        assert!(is_pat);
3381    }
3382
3383    #[test]
3384    fn compctl_name_pat_strips_backslashes_from_literal() {
3385        let _g = crate::test_util::global_state_lock();
3386        let _g = zle_test_setup();
3387        let (is_pat, out) = compctl_name_pat("\\$home");
3388        assert!(!is_pat);
3389        // Backslash dropped, `$` kept.
3390        assert_eq!(out, "$home");
3391    }
3392
3393    #[test]
3394    fn delpatcomp_removes_matching_pattern() {
3395        let _g = crate::test_util::global_state_lock();
3396        let _g = zle_test_setup();
3397        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3398        let mut p = PATCOMPS.write().unwrap();
3399        p.push(("foo*".to_string(), Arc::new(Compctl::default())));
3400        p.push(("bar*".to_string(), Arc::new(Compctl::default())));
3401        drop(p);
3402        delpatcomp("foo*");
3403        let p = PATCOMPS.read().unwrap();
3404        assert_eq!(p.len(), 1);
3405        assert_eq!(p[0].0, "bar*");
3406    }
3407
3408    /// `delpatcomp` MUST remove only the FIRST match (c:1308 `break`)
3409    /// when duplicate names exist. C uses the explicit `break` after
3410    /// the first hit. A regression that switched to `Vec::retain`
3411    /// would remove ALL matches — semantically different, observable
3412    /// when users register multiple compctl entries under the same
3413    /// pattern (`compctl -p 'foo*' ...` twice for layered configs).
3414    /// `delpatcomp` MUST remove only the FIRST match (c:1308 `break`)
3415    /// when duplicate names exist. C uses the explicit `break` after
3416    /// the first hit. A regression that switched to `Vec::retain`
3417    /// would remove ALL matches — semantically different, observable
3418    /// when users register multiple compctl entries under the same
3419    /// pattern (`compctl -p 'foo*' ...` twice for layered configs).
3420    #[test]
3421    fn delpatcomp_removes_only_first_match_when_duplicates() {
3422        let _g = crate::test_util::global_state_lock();
3423        let _g = zle_test_setup();
3424        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3425        // Clear shared PATCOMPS state — other tests may have left
3426        // entries that would interfere with the duplicate-count.
3427        {
3428            let mut p = PATCOMPS.write().unwrap();
3429            p.clear();
3430            // Three entries under the same pattern name — distinguishable
3431            // by the embedded Compctl::keyvar (used as a tag).
3432            for tag in ["a", "b", "c"] {
3433                p.push((
3434                    "dup*".to_string(),
3435                    Arc::new(Compctl {
3436                        keyvar: Some(tag.to_string()),
3437                        ..Compctl::default()
3438                    }),
3439                ));
3440            }
3441        }
3442
3443        delpatcomp("dup*");
3444
3445        let p = PATCOMPS.read().unwrap();
3446        assert_eq!(
3447            p.len(),
3448            2,
3449            "c:1308 — only ONE entry removed; got len={}",
3450            p.len()
3451        );
3452        // The remaining entries must be the 2nd and 3rd (b then c) —
3453        // the first ("a") was the one removed.
3454        assert_eq!(
3455            p[0].1.keyvar.as_deref(),
3456            Some("b"),
3457            "remaining first entry must be the second-inserted (b)"
3458        );
3459        assert_eq!(
3460            p[1].1.keyvar.as_deref(),
3461            Some("c"),
3462            "remaining second entry must be the third-inserted (c)"
3463        );
3464    }
3465
3466    #[test]
3467    fn cc_assign_with_reass_command_target_uses_special_key() {
3468        let _g = crate::test_util::global_state_lock();
3469        let _g = zle_test_setup();
3470        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3471        createcompctltable();
3472        CCLIST.with(|c| c.set(COMP_COMMAND));
3473        cc_assign(
3474            "compctl",
3475            Arc::new(Compctl {
3476                mask: CC_FILES,
3477                ..Default::default()
3478            }),
3479            true,
3480        );
3481        let g = COMPCTL_TAB.read().unwrap();
3482        assert!(g.as_ref().unwrap().contains_key("__cc_compos"));
3483        // Reset for other tests.
3484        drop(g);
3485        CCLIST.with(|c| c.set(0));
3486    }
3487
3488    #[test]
3489    fn cc_assign_with_reass_default_target_uses_special_key() {
3490        let _g = crate::test_util::global_state_lock();
3491        let _g = zle_test_setup();
3492        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3493        createcompctltable();
3494        CCLIST.with(|c| c.set(COMP_DEFAULT));
3495        cc_assign("compctl", Arc::new(Compctl::default()), true);
3496        let g = COMPCTL_TAB.read().unwrap();
3497        assert!(g.as_ref().unwrap().contains_key("__cc_default"));
3498        drop(g);
3499        CCLIST.with(|c| c.set(0));
3500    }
3501
3502    #[test]
3503    fn setup_initializes_special_targets_and_table() {
3504        let _g = crate::test_util::global_state_lock();
3505        let _g = zle_test_setup();
3506        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3507        setup_();
3508        // cc_compos has CC_COMMPATH set
3509        let cc_compos = CC_COMPOS.lock().unwrap().clone();
3510        assert!(cc_compos.is_some());
3511        assert_eq!(cc_compos.unwrap().mask, CC_COMMPATH);
3512        // cc_default has CC_FILES + refc=10000 sentinel
3513        let cc_default = CC_DEFAULT.lock().unwrap().clone();
3514        assert!(cc_default.is_some());
3515        let cc_default = cc_default.unwrap();
3516        assert_eq!(cc_default.mask, CC_FILES);
3517        assert_eq!(cc_default.refc, 10000);
3518        // cc_first has CC_CCCONT in mask2
3519        let cc_first = CC_FIRST.lock().unwrap().clone();
3520        assert!(cc_first.is_some());
3521        assert_eq!(cc_first.unwrap().mask2, CC_CCCONT);
3522        // table exists
3523        assert!(COMPCTL_TAB.read().unwrap().is_some());
3524        // compctlread installed
3525        assert!(*COMPCTLREAD_INSTALLED.lock().unwrap());
3526    }
3527
3528    #[test]
3529    fn finish_tears_down_state() {
3530        let _g = crate::test_util::global_state_lock();
3531        let _g = zle_test_setup();
3532        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3533        setup_();
3534        finish_();
3535        // Table cleared
3536        assert!(COMPCTL_TAB.read().unwrap().is_none());
3537        // compctlread restored
3538        assert!(!*COMPCTLREAD_INSTALLED.lock().unwrap());
3539        // lastccused cleared
3540        assert_eq!(LASTCCUSED.lock().unwrap().len(), 0);
3541    }
3542
3543    #[test]
3544    fn features_returns_two_builtins() {
3545        let _g = crate::test_util::global_state_lock();
3546        let _g = zle_test_setup();
3547        let f = features_();
3548        assert_eq!(f, vec!["b:compctl".to_string(), "b:compcall".to_string()]);
3549    }
3550
3551    #[test]
3552    fn enables_returns_two_enabled_bits() {
3553        let _g = crate::test_util::global_state_lock();
3554        let _g = zle_test_setup();
3555        let e = enables_();
3556        assert_eq!(e, vec![1, 1]);
3557    }
3558
3559    #[test]
3560    fn bin_compcall_outside_compfunc_errors() {
3561        let _g = crate::test_util::global_state_lock();
3562        let _g = zle_test_setup();
3563        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3564        INCOMPFUNC.with(|c| c.set(0));
3565        let ops = crate::ported::zsh_h::options {
3566            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3567            args: Vec::new(),
3568            argscount: 0,
3569            argsalloc: 0,
3570        };
3571        let r = bin_compcall("compcall", &[], &ops, 0);
3572        assert_eq!(r, 1);
3573    }
3574
3575    #[test]
3576    fn bin_compcall_inside_compfunc_succeeds() {
3577        let _g = crate::test_util::global_state_lock();
3578        let _g = zle_test_setup();
3579        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3580        INCOMPFUNC.with(|c| c.set(1));
3581        let ops = crate::ported::zsh_h::options {
3582            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3583            args: Vec::new(),
3584            argscount: 0,
3585            argsalloc: 0,
3586        };
3587        let r = bin_compcall("compcall", &["-T".to_string()], &ops, 0);
3588        assert_eq!(r, 0);
3589        // Reset
3590        INCOMPFUNC.with(|c| c.set(0));
3591    }
3592
3593    #[test]
3594    fn compctlread_outside_compctl_func_errors() {
3595        let _g = crate::test_util::global_state_lock();
3596        let _g = zle_test_setup();
3597        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3598        INCOMPCTLFUNC.with(|c| c.set(false));
3599        let r = compctlread("compctlread", &[]);
3600        assert_eq!(r, 1);
3601    }
3602
3603    #[test]
3604    fn cccleanuphookfn_returns_zero() {
3605        let _g = crate::test_util::global_state_lock();
3606        let _g = zle_test_setup();
3607        // Trivial — no state to verify, just that it doesn't panic.
3608        assert_eq!(cccleanuphookfn(()), 0);
3609    }
3610
3611    #[test]
3612    fn addmatch_rejects_unset_addwhat() {
3613        let _g = crate::test_util::global_state_lock();
3614        let _g = zle_test_setup();
3615        // C: c:2015 — `else` arm in addmatch falls through to drop the
3616        // match when addwhat is 0 (neither file-thread nor
3617        // conditional-accept set).
3618        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3619        MATCH_LIST.with(|r| r.borrow_mut().clear());
3620        ADDWHAT.with(|c| c.set(0));
3621        addmatch("dropped", None);
3622        let captured = MATCH_LIST.with(|r| r.borrow().clone());
3623        assert!(captured.is_empty(), "addwhat=0 should drop matches");
3624    }
3625
3626    #[test]
3627    fn addmatch_accepts_files_kind() {
3628        let _g = crate::test_util::global_state_lock();
3629        let _g = zle_test_setup();
3630        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3631        MATCH_LIST.with(|r| r.borrow_mut().clear());
3632        ADDWHAT.with(|c| c.set(-5));
3633        addmatch("foo.txt", None);
3634        addmatch("bar.txt", None);
3635        let m = MATCH_LIST.with(|r| r.borrow().clone());
3636        assert_eq!(m.len(), 2);
3637        assert_eq!(m[0], "foo.txt");
3638    }
3639
3640    #[test]
3641    fn addmatch_accepts_param_kind() {
3642        let _g = crate::test_util::global_state_lock();
3643        let _g = zle_test_setup();
3644        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3645        MATCH_LIST.with(|r| r.borrow_mut().clear());
3646        ADDWHAT.with(|c| c.set(-9));
3647        addmatch("HOME", None);
3648        let m = MATCH_LIST.with(|r| r.borrow().clone());
3649        assert_eq!(m.len(), 1);
3650        assert_eq!(m[0], "HOME");
3651    }
3652
3653    #[test]
3654    fn addmatch_accepts_cc_files_positive_mask() {
3655        let _g = crate::test_util::global_state_lock();
3656        let _g = zle_test_setup();
3657        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3658        MATCH_LIST.with(|r| r.borrow_mut().clear());
3659        ADDWHAT.with(|c| c.set(CC_FILES as i32));
3660        addmatch("foo", None);
3661        let m = MATCH_LIST.with(|r| r.borrow().clone());
3662        assert_eq!(m.len(), 1);
3663    }
3664
3665    #[test]
3666    fn getcpat_finds_first_substring() {
3667        let _g = crate::test_util::global_state_lock();
3668        let _g = zle_test_setup();
3669        // Search "abcabc" for "bc" first occurrence → position 3
3670        // (1-based, points past the matched substring).
3671        let r = getcpat("abcabc", 1, "bc", 0);
3672        assert_eq!(r, 3);
3673    }
3674
3675    #[test]
3676    fn getcpat_finds_second_substring() {
3677        let _g = crate::test_util::global_state_lock();
3678        let _g = zle_test_setup();
3679        // Search "abcabc" for the 2nd "bc" → position 6.
3680        let r = getcpat("abcabc", 2, "bc", 0);
3681        assert_eq!(r, 6);
3682    }
3683
3684    #[test]
3685    fn getcpat_negative_index_searches_backward() {
3686        let _g = crate::test_util::global_state_lock();
3687        let _g = zle_test_setup();
3688        // Backward search "abcabc" for last "bc" → position 5.
3689        let r = getcpat("abcabc", -1, "bc", 0);
3690        assert!(r >= 0, "should find match (got {})", r);
3691    }
3692
3693    #[test]
3694    fn getcpat_class_mode_matches_any_char_in_set() {
3695        let _g = crate::test_util::global_state_lock();
3696        let _g = zle_test_setup();
3697        // Search "abcdef" for any of {b, d, f} — class mode.
3698        // First match at index 1 (b).
3699        let r = getcpat("abcdef", 1, "bdf", 1);
3700        assert_eq!(r, 2); // 1-based position of 'b'
3701    }
3702
3703    #[test]
3704    fn getcpat_not_found_returns_negative_one() {
3705        let _g = crate::test_util::global_state_lock();
3706        let _g = zle_test_setup();
3707        let r = getcpat("hello", 1, "xyz", 0);
3708        assert_eq!(r, -1);
3709    }
3710
3711    #[test]
3712    fn getcpat_strips_backslashes_in_pattern() {
3713        let _g = crate::test_util::global_state_lock();
3714        let _g = zle_test_setup();
3715        // `\$` in pattern should be treated as literal `$`.
3716        let r = getcpat("foo$bar", 1, "\\$", 0);
3717        assert_eq!(r, 4); // 1-based pos right after the `$`
3718    }
3719
3720    #[test]
3721    fn dumphashtable_calls_addmatch_per_entry() {
3722        let _g = crate::test_util::global_state_lock();
3723        let _g = zle_test_setup();
3724        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3725        MATCH_LIST.with(|r| r.borrow_mut().clear());
3726        let entries = vec!["alpha".to_string(), "beta".to_string(), "gamma".to_string()];
3727        dumphashtable(entries, -5);
3728        let m = MATCH_LIST.with(|r| r.borrow().clone());
3729        assert_eq!(m.len(), 3);
3730    }
3731
3732    #[test]
3733    fn addhnmatch_forwards_to_addmatch() {
3734        let _g = crate::test_util::global_state_lock();
3735        let _g = zle_test_setup();
3736        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3737        MATCH_LIST.with(|r| r.borrow_mut().clear());
3738        ADDWHAT.with(|c| c.set(-5));
3739        addhnmatch("xyz", 0);
3740        let m = MATCH_LIST.with(|r| r.borrow().clone());
3741        assert_eq!(m.len(), 1);
3742        assert_eq!(m[0], "xyz");
3743    }
3744
3745    #[test]
3746    fn makecomplistctl_recursion_guard() {
3747        let _g = crate::test_util::global_state_lock();
3748        let _g = zle_test_setup();
3749        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3750        // Force depth to MAX
3751        CDEPTH.with(|c| c.set(MAX_CDEPTH));
3752        let r = makecomplistctl(0);
3753        assert_eq!(r, 0);
3754        // Reset for other tests.
3755        CDEPTH.with(|c| c.set(0));
3756    }
3757
3758    #[test]
3759    fn makecomplistflags_cc_files_invokes_gen_matches() {
3760        let _g = crate::test_util::global_state_lock();
3761        let _g = zle_test_setup();
3762        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3763        MATCH_LIST.with(|r| r.borrow_mut().clear());
3764        // Set prpre to a known dir we can read.
3765        PRPRE.with(|r| *r.borrow_mut() = Some(".".to_string()));
3766        let cc = Arc::new(Compctl {
3767            mask: CC_FILES,
3768            ..Default::default()
3769        });
3770        makecomplistflags(&cc, "", false, 0);
3771        // Should have at least picked up Cargo.toml or similar from pwd.
3772        let m = MATCH_LIST.with(|r| r.borrow().clone());
3773        assert!(!m.is_empty(), "expected file matches in pwd");
3774    }
3775
3776    #[test]
3777    fn makecomplistflags_cc_str_expansion_emits_one_match() {
3778        let _g = crate::test_util::global_state_lock();
3779        let _g = zle_test_setup();
3780        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3781        MATCH_LIST.with(|r| r.borrow_mut().clear());
3782        let cc = Arc::new(Compctl {
3783            str: Some("hardcoded".to_string()),
3784            ..Default::default()
3785        });
3786        makecomplistflags(&cc, "", false, 0);
3787        let m = MATCH_LIST.with(|r| r.borrow().clone());
3788        assert_eq!(m.len(), 1);
3789        assert_eq!(m[0], "hardcoded");
3790    }
3791
3792    #[test]
3793    fn makecomplistor_walks_xor_chain() {
3794        let _g = crate::test_util::global_state_lock();
3795        let _g = zle_test_setup();
3796        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3797        MATCH_LIST.with(|r| r.borrow_mut().clear());
3798        // Build cc1 with str "first", xor → cc2 with str "second"
3799        let cc2 = Arc::new(Compctl {
3800            str: Some("second".to_string()),
3801            ..Default::default()
3802        });
3803        let cc1 = Arc::new(Compctl {
3804            str: Some("first".to_string()),
3805            xor: Some(cc2),
3806            ..Default::default()
3807        });
3808        makecomplistor(&cc1, "", false, 0, 0);
3809        let m = MATCH_LIST.with(|r| r.borrow().clone());
3810        assert_eq!(m.len(), 2);
3811        assert_eq!(m[0], "first");
3812        assert_eq!(m[1], "second");
3813    }
3814
3815    #[test]
3816    fn makecomplistcc_pushes_to_ccused() {
3817        let _g = crate::test_util::global_state_lock();
3818        let _g = zle_test_setup();
3819        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3820        CCUSED.with(|r| r.borrow_mut().clear());
3821        let cc = Arc::new(Compctl::default());
3822        makecomplistcc(&cc, "", false);
3823        let used = CCUSED.with(|r| r.borrow().clone());
3824        assert_eq!(used.len(), 1);
3825    }
3826
3827    #[test]
3828    fn makecomplistpc_iterates_patcomps() {
3829        let _g = crate::test_util::global_state_lock();
3830        let _g = zle_test_setup();
3831        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3832        // Verify makecomplistpc returns 0 when cmdstr is unset
3833        // (its early-bail path) — full pattern-match test requires
3834        // VM context for glob_match_static.
3835        CMDSTR.with(|r| *r.borrow_mut() = None);
3836        let r = makecomplistpc("", false);
3837        assert_eq!(r, 0);
3838    }
3839
3840    #[test]
3841    fn findnode_returns_index_of_match() {
3842        let _g = crate::test_util::global_state_lock();
3843        let _g = zle_test_setup();
3844        let list = vec!["a".to_string(), "b".to_string(), "c".to_string()];
3845        assert_eq!(findnode(&list, &"b".to_string()), Some(1));
3846        assert_eq!(findnode(&list, &"z".to_string()), None);
3847    }
3848
3849    #[test]
3850    fn cc_assign_rejects_conflicting_special_targets() {
3851        let _g = crate::test_util::global_state_lock();
3852        let _g = zle_test_setup();
3853        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3854        createcompctltable();
3855        CCLIST.with(|c| c.set(COMP_COMMAND | COMP_DEFAULT));
3856        cc_assign("compctl", Arc::new(Compctl::default()), true);
3857        let g = COMPCTL_TAB.read().unwrap();
3858        // Should have been rejected — neither key installed.
3859        assert!(!g.as_ref().unwrap().contains_key("__cc_compos"));
3860        assert!(!g.as_ref().unwrap().contains_key("__cc_default"));
3861        drop(g);
3862        CCLIST.with(|c| c.set(0));
3863    }
3864
3865    #[test]
3866    fn compctl_process_cc_remove_deletes_named_entries() {
3867        let _g = crate::test_util::global_state_lock();
3868        let _g = zle_test_setup();
3869        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3870        createcompctltable();
3871        cc_assign("foo", Arc::new(Compctl::default()), false);
3872        cc_assign("bar", Arc::new(Compctl::default()), false);
3873        CCLIST.with(|c| c.set(COMP_REMOVE));
3874        compctl_process_cc(&["foo".to_string()], Arc::new(Compctl::default()));
3875        let g = COMPCTL_TAB.read().unwrap();
3876        let map = g.as_ref().unwrap();
3877        assert!(!map.contains_key("foo"));
3878        assert!(map.contains_key("bar"));
3879        // Reset cclist for other tests.
3880        CCLIST.with(|c| c.set(0));
3881    }
3882
3883    #[test]
3884    fn sep_comp_string_returns_zero_or_one() {
3885        let _g = crate::test_util::global_state_lock();
3886        let _g = zle_test_setup();
3887        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3888        // C compctl.c:2806-3030 contract — sep_comp_string only returns
3889        // 0 (success / dispatched) or 1 (bail, no cursor word).
3890        let r = sep_comp_string("", "", 0);
3891        assert!(r == 0 || r == 1, "expected 0 or 1, got {}", r);
3892    }
3893
3894    #[test]
3895    fn sep_comp_string_round_trips_zle_state() {
3896        let _g = crate::test_util::global_state_lock();
3897        let _g = zle_test_setup();
3898        let _g = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
3899        // Pre-set zle_tricky.c globals; sep_comp_string must restore them
3900        // on exit (C compctl.c:2810-2813 save / 2941-2950 restore).
3901        WE.with(|c| c.set(42));
3902        WB.with(|c| c.set(7));
3903        ZLEMETACS.with(|c| c.set(11));
3904        *ZLEMETALL.lock().unwrap() = 99;
3905        *INSTRING.lock().unwrap() = QT_DOUBLE;
3906        *INBACKT.lock().unwrap() = 1;
3907        *NOALIASES.lock().unwrap() = 1;
3908        *NOERRS.lock().unwrap() = 0;
3909        *ZLEMETALINE.lock().unwrap() = "hello".to_string();
3910        *AUTOQ.lock().unwrap() = "Q".to_string();
3911
3912        let _ = sep_comp_string("", "x", 0);
3913
3914        assert_eq!(WE.with(|c| c.get()), 42);
3915        assert_eq!(WB.with(|c| c.get()), 7);
3916        assert_eq!(ZLEMETACS.with(|c| c.get()), 11);
3917        assert_eq!(*ZLEMETALL.lock().unwrap(), 99);
3918        assert_eq!(*INSTRING.lock().unwrap(), QT_DOUBLE);
3919        assert_eq!(*INBACKT.lock().unwrap(), 1);
3920        assert_eq!(*NOALIASES.lock().unwrap(), 1);
3921        assert_eq!(*NOERRS.lock().unwrap(), 0);
3922        assert_eq!(*ZLEMETALINE.lock().unwrap(), "hello");
3923        assert_eq!(*AUTOQ.lock().unwrap(), "Q");
3924    }
3925
3926    #[test]
3927    fn inull_recognises_marker_chars() {
3928        let _g = crate::test_util::global_state_lock();
3929        let _g = zle_test_setup();
3930        // C compctl.c:2917 — INULL macro recognises Snull/Dnull/Bnull
3931        // plus String/Qstring tokens for inull-walk.
3932        assert!(inull(Snull));
3933        assert!(inull(Dnull));
3934        assert!(inull(Bnull));
3935        assert!(inull(Stringg));
3936        assert!(inull(QSTRING_TOK));
3937        assert!(!inull('a'));
3938        assert!(!inull(' '));
3939    }
3940
3941    #[test]
3942    fn qt_constants_match_c_zsh_h() {
3943        let _g = crate::test_util::global_state_lock();
3944        let _g = zle_test_setup();
3945        // C: enum at Src/zsh.h:253-292 — QT_NONE / QT_BACKSLASH /
3946        // QT_SINGLE / QT_DOUBLE / QT_DOLLARS / QT_BACKTICK in that
3947        // declaration order, so values are 0..5.
3948        assert_eq!(QT_NONE, 0);
3949        assert_eq!(QT_BACKSLASH, 1);
3950        assert_eq!(QT_SINGLE, 2);
3951        assert_eq!(QT_DOUBLE, 3);
3952        assert_eq!(QT_DOLLARS, 4);
3953        assert_eq!(QT_BACKTICK, 5);
3954    }
3955
3956    // ─── zsh-corpus pins for inull / QT_* ──────────────────────────
3957
3958    /// `inull` recognises every null-token in the set.
3959    #[test]
3960    fn compctl_corpus_inull_recognises_null_tokens() {
3961        let _g = crate::test_util::global_state_lock();
3962        let _g = zle_test_setup();
3963        assert!(inull(Snull));
3964        assert!(inull(Dnull));
3965        assert!(inull(Bnull));
3966        assert!(inull(Stringg));
3967        assert!(inull(QSTRING_TOK));
3968    }
3969
3970    /// `inull` rejects ordinary printable chars.
3971    #[test]
3972    fn compctl_corpus_inull_rejects_printables() {
3973        let _g = crate::test_util::global_state_lock();
3974        let _g = zle_test_setup();
3975        for c in ['a', 'Z', '0', ' ', '!', '~'] {
3976            assert!(!inull(c), "{c:?} should NOT be inull");
3977        }
3978    }
3979
3980    /// All QT_* constants are pairwise distinct.
3981    #[test]
3982    fn compctl_corpus_qt_pairwise_distinct() {
3983        let qs = [
3984            QT_NONE,
3985            QT_BACKSLASH,
3986            QT_SINGLE,
3987            QT_DOUBLE,
3988            QT_DOLLARS,
3989            QT_BACKTICK,
3990        ];
3991        for (i, a) in qs.iter().enumerate() {
3992            for b in &qs[i + 1..] {
3993                assert_ne!(a, b);
3994            }
3995        }
3996    }
3997
3998    /// All QT_* in [0, 5] range.
3999    #[test]
4000    fn compctl_corpus_qt_all_within_range() {
4001        for q in [
4002            QT_NONE,
4003            QT_BACKSLASH,
4004            QT_SINGLE,
4005            QT_DOUBLE,
4006            QT_DOLLARS,
4007            QT_BACKTICK,
4008        ] {
4009            assert!((0..=5).contains(&q), "QT_* value {q} out of [0,5]");
4010        }
4011    }
4012
4013    // ═══════════════════════════════════════════════════════════════════
4014    // Additional C-parity tests for Src/Zle/compctl.c
4015    // c:92 createcompctltable / c:104 freecompctlp / c:1052 compctl_name_pat
4016    // c:1080 delpatcomp / c:1303 bin_compctl / c:1435 bin_compcall /
4017    // c:1485 ccmakehookfn / c:1573 cccleanuphookfn / c:1589 maketildelist /
4018    // c:1948 gen_matches_files
4019    // ═══════════════════════════════════════════════════════════════════
4020
4021    /// c:92 — `createcompctltable` is idempotent (safe to call multiple times).
4022    #[test]
4023    fn createcompctltable_idempotent() {
4024        let _g = crate::test_util::global_state_lock();
4025        for _ in 0..5 {
4026            createcompctltable();
4027        }
4028    }
4029
4030    /// c:104 — `freecompctlp("")` empty name is safe.
4031    #[test]
4032    fn freecompctlp_empty_name_no_panic() {
4033        let _g = crate::test_util::global_state_lock();
4034        freecompctlp("");
4035    }
4036
4037    /// c:104 — `freecompctlp(unknown)` safe.
4038    #[test]
4039    fn freecompctlp_unknown_name_no_panic() {
4040        let _g = crate::test_util::global_state_lock();
4041        freecompctlp("__never_a_real_compctl_xyz__");
4042    }
4043
4044    /// c:1052 — `compctl_name_pat("")` returns (bool, String) type.
4045    #[test]
4046    fn compctl_name_pat_returns_tuple_type() {
4047        let _: (bool, String) = compctl_name_pat("");
4048    }
4049
4050    /// c:1052 — `compctl_name_pat` is pure.
4051    #[test]
4052    fn compctl_name_pat_is_pure() {
4053        for s in ["", "name", "*pat*", "with[brackets]"] {
4054            let first = compctl_name_pat(s);
4055            for _ in 0..3 {
4056                assert_eq!(
4057                    compctl_name_pat(s),
4058                    first,
4059                    "compctl_name_pat({:?}) must be pure",
4060                    s
4061                );
4062            }
4063        }
4064    }
4065
4066    /// c:1080 — `delpatcomp("")` empty name is safe.
4067    #[test]
4068    fn delpatcomp_empty_name_no_panic() {
4069        let _g = crate::test_util::global_state_lock();
4070        delpatcomp("");
4071    }
4072
4073    /// c:1485 — `ccmakehookfn` returns i32 (type pin).
4074    #[test]
4075    fn ccmakehookfn_returns_i32_type() {
4076        let _: i32 = ccmakehookfn(());
4077    }
4078
4079    /// c:1573 — `cccleanuphookfn` returns i32 (type pin).
4080    #[test]
4081    fn cccleanuphookfn_returns_i32_type() {
4082        let _: i32 = cccleanuphookfn(());
4083    }
4084
4085    /// c:1485 — `ccmakehookfn` is idempotent.
4086    #[test]
4087    fn ccmakehookfn_idempotent() {
4088        for _ in 0..5 {
4089            let _ = ccmakehookfn(());
4090        }
4091    }
4092
4093    /// c:1573 — `cccleanuphookfn` is idempotent.
4094    #[test]
4095    fn cccleanuphookfn_idempotent() {
4096        for _ in 0..5 {
4097            let _ = cccleanuphookfn(());
4098        }
4099    }
4100
4101    // ═══════════════════════════════════════════════════════════════════
4102    // Additional C-parity tests for Src/Zle/compctl.c
4103    // c:320 set_gmatcher / c:356 get_gmatcher / c:381 print_gmatcher /
4104    // c:1303 bin_compctl / c:1435 bin_compcall / c:1589 maketildelist
4105    // ═══════════════════════════════════════════════════════════════════
4106
4107    /// c:320 — `set_gmatcher` returns i32 (compile-time type pin).
4108    #[test]
4109    fn set_gmatcher_returns_i32_type() {
4110        let _g = crate::test_util::global_state_lock();
4111        let _: i32 = set_gmatcher("", &[]);
4112    }
4113
4114    /// c:356 — `get_gmatcher` returns i32 (compile-time type pin).
4115    #[test]
4116    fn get_gmatcher_returns_i32_type() {
4117        let _g = crate::test_util::global_state_lock();
4118        let _: i32 = get_gmatcher("", &[]);
4119    }
4120
4121    /// c:381 — `print_gmatcher(0)` is safe.
4122    #[test]
4123    fn print_gmatcher_safe_for_zero() {
4124        let _g = crate::test_util::global_state_lock();
4125        print_gmatcher(0);
4126    }
4127
4128    /// c:1589 — `maketildelist` is safe (no panic / no side effects
4129    /// outside the tilde-completion buffer).
4130    #[test]
4131    fn maketildelist_safe_no_panic() {
4132        let _g = crate::test_util::global_state_lock();
4133        maketildelist();
4134    }
4135
4136    /// c:1589 — `maketildelist` is idempotent.
4137    #[test]
4138    fn maketildelist_idempotent() {
4139        let _g = crate::test_util::global_state_lock();
4140        for _ in 0..5 {
4141            maketildelist();
4142        }
4143    }
4144
4145    /// c:320 — `set_gmatcher` is deterministic for stable input.
4146    #[test]
4147    fn set_gmatcher_is_deterministic() {
4148        let _g = crate::test_util::global_state_lock();
4149        let first = set_gmatcher("test", &[]);
4150        for _ in 0..3 {
4151            assert_eq!(
4152                set_gmatcher("test", &[]),
4153                first,
4154                "set_gmatcher must be deterministic"
4155            );
4156        }
4157    }
4158
4159    /// c:356 — `get_gmatcher` is deterministic for stable input.
4160    #[test]
4161    fn get_gmatcher_is_deterministic() {
4162        let _g = crate::test_util::global_state_lock();
4163        let first = get_gmatcher("test", &[]);
4164        for _ in 0..3 {
4165            assert_eq!(
4166                get_gmatcher("test", &[]),
4167                first,
4168                "get_gmatcher must be deterministic"
4169            );
4170        }
4171    }
4172
4173    /// c:1303 — `bin_compctl` no-args returns i32 (compile-time type pin).
4174    #[test]
4175    fn bin_compctl_returns_i32_type() {
4176        let _g = crate::test_util::global_state_lock();
4177        let ops = crate::ported::zsh_h::options {
4178            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
4179            args: Vec::new(),
4180            argscount: 0,
4181            argsalloc: 0,
4182        };
4183        let _: i32 = bin_compctl("compctl", &[], &ops, 0);
4184    }
4185
4186    /// c:1435 — `bin_compcall` no-args returns i32.
4187    #[test]
4188    fn bin_compcall_returns_i32_type() {
4189        let _g = crate::test_util::global_state_lock();
4190        let ops = crate::ported::zsh_h::options {
4191            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
4192            args: Vec::new(),
4193            argscount: 0,
4194            argsalloc: 0,
4195        };
4196        let _: i32 = bin_compcall("compcall", &[], &ops, 0);
4197    }
4198
4199    /// c:1485 — `ccmakehookfn(())` is deterministic.
4200    #[test]
4201    fn ccmakehookfn_is_deterministic() {
4202        let first = ccmakehookfn(());
4203        for _ in 0..3 {
4204            assert_eq!(
4205                ccmakehookfn(()),
4206                first,
4207                "ccmakehookfn must be deterministic"
4208            );
4209        }
4210    }
4211
4212    /// c:1573 — `cccleanuphookfn(())` is deterministic.
4213    #[test]
4214    fn cccleanuphookfn_is_deterministic() {
4215        let first = cccleanuphookfn(());
4216        for _ in 0..3 {
4217            assert_eq!(
4218                cccleanuphookfn(()),
4219                first,
4220                "cccleanuphookfn must be deterministic"
4221            );
4222        }
4223    }
4224
4225    /// c:381 — `print_gmatcher` for various indices is safe (no panic).
4226    #[test]
4227    fn print_gmatcher_full_index_range_safe() {
4228        let _g = crate::test_util::global_state_lock();
4229        for ac in [-1i32, 0, 1, 10, 100, i32::MAX, i32::MIN] {
4230            print_gmatcher(ac);
4231        }
4232    }
4233
4234    /// c:1052 — `compctl_name_pat(empty)` returns (false, empty).
4235    #[test]
4236    fn compctl_name_pat_empty_returns_false_empty() {
4237        let (is_pat, name) = compctl_name_pat("");
4238        assert!(!is_pat, "empty input not a pattern");
4239        assert_eq!(name, "", "empty input → empty name");
4240    }
4241
4242    // ═══════════════════════════════════════════════════════════════════
4243    // Additional C-parity tests for Src/Zle/compctl.c
4244    // c:92 createcompctltable / c:104 freecompctlp / c:320 set_gmatcher /
4245    // c:356 get_gmatcher / c:1052 compctl_name_pat / c:1080 delpatcomp /
4246    // c:1303 bin_compctl / c:1435 bin_compcall / c:1485 ccmakehookfn
4247    // ═══════════════════════════════════════════════════════════════════
4248
4249    /// c:92 — `createcompctltable` is idempotent (alt 10-call).
4250    #[test]
4251    fn createcompctltable_idempotent_10_call_alt() {
4252        let _g = crate::test_util::global_state_lock();
4253        for _ in 0..10 {
4254            createcompctltable();
4255        }
4256    }
4257
4258    /// c:104 — `freecompctlp(empty)` safe.
4259    #[test]
4260    fn freecompctlp_empty_no_panic() {
4261        let _g = crate::test_util::global_state_lock();
4262        freecompctlp("");
4263    }
4264
4265    /// c:104 — `freecompctlp("__never__")` for unknown name safe (alt pin).
4266    #[test]
4267    fn freecompctlp_unknown_name_no_panic_alt() {
4268        let _g = crate::test_util::global_state_lock();
4269        freecompctlp("__never_real_compctl_xyz__");
4270    }
4271
4272    /// c:320 — `set_gmatcher` returns i32 (compile-time pin, alt).
4273    #[test]
4274    fn set_gmatcher_returns_i32_type_alt() {
4275        let _g = crate::test_util::global_state_lock();
4276        let _: i32 = set_gmatcher("test", &[]);
4277    }
4278
4279    /// c:356 — `get_gmatcher` returns i32 (compile-time pin, alt).
4280    #[test]
4281    fn get_gmatcher_returns_i32_type_alt() {
4282        let _g = crate::test_util::global_state_lock();
4283        let _: i32 = get_gmatcher("test", &[]);
4284    }
4285
4286    /// c:1052 — `compctl_name_pat` returns (bool, String) tuple.
4287    #[test]
4288    fn compctl_name_pat_returns_bool_string_tuple_type() {
4289        let _: (bool, String) = compctl_name_pat("");
4290    }
4291
4292    /// c:1052 — `compctl_name_pat` is deterministic.
4293    #[test]
4294    fn compctl_name_pat_deterministic() {
4295        for s in ["", "abc", "*", "(pattern)", "name"] {
4296            let a = compctl_name_pat(s);
4297            let b = compctl_name_pat(s);
4298            assert_eq!(a, b, "compctl_name_pat({:?}) must be pure", s);
4299        }
4300    }
4301
4302    /// c:1080 — `delpatcomp("")` empty pattern safe.
4303    #[test]
4304    fn delpatcomp_empty_no_panic() {
4305        let _g = crate::test_util::global_state_lock();
4306        delpatcomp("");
4307    }
4308
4309    /// c:1080 — `delpatcomp("__never__")` unknown pattern safe.
4310    #[test]
4311    fn delpatcomp_unknown_no_panic() {
4312        let _g = crate::test_util::global_state_lock();
4313        delpatcomp("__never_real_pattern_xyz__");
4314    }
4315
4316    /// c:1303 — `bin_compctl` non-negative exit code.
4317    #[test]
4318    fn bin_compctl_exit_code_non_negative() {
4319        let _g = crate::test_util::global_state_lock();
4320        let ops = crate::ported::zsh_h::options {
4321            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
4322            args: Vec::new(),
4323            argscount: 0,
4324            argsalloc: 0,
4325        };
4326        let r = bin_compctl("compctl", &[], &ops, 0);
4327        assert!(r >= 0, "bin_compctl exit code must be ≥ 0, got {}", r);
4328    }
4329
4330    /// c:1435 — `bin_compcall` non-negative exit code.
4331    #[test]
4332    fn bin_compcall_exit_code_non_negative() {
4333        let _g = crate::test_util::global_state_lock();
4334        let ops = crate::ported::zsh_h::options {
4335            ind: [0u8; crate::ported::zsh_h::MAX_OPS],
4336            args: Vec::new(),
4337            argscount: 0,
4338            argsalloc: 0,
4339        };
4340        let r = bin_compcall("compcall", &[], &ops, 0);
4341        assert!(r >= 0, "bin_compcall exit code must be ≥ 0, got {}", r);
4342    }
4343
4344    /// c:1485 — `ccmakehookfn` returns i32 (compile-time pin, alt).
4345    #[test]
4346    fn ccmakehookfn_returns_i32_type_alt() {
4347        let _: i32 = ccmakehookfn(());
4348    }
4349}