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