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