zsh/ported/zle/complete.rs
1//! Direct port of `Src/Zle/complete.c` — the ZLE completion engine.
2//!
3//! Copy a completion matcher list into permanent storage. // c:151
4//! Copy a completion matcher pattern. // c:214
5//! Parse a character class for matcher control. // c:476
6//!
7//! This file holds the canonical Rust ports of complete.c's
8//! exported functions: state globals (`compprefix` / `compsuffix` /
9//! `compwords` / `incompfunc` / etc.), the Cmlist/Cmatcher/Cpattern
10//! allocator + free + deep-copy chain (freecmlist/freecmatcher/
11//! freecpattern/cpcmatcher/cp_cpattern_element/cpcpattern), the
12//! ignore_prefix/ignore_suffix/restrict_range state mutators, the
13//! special-parameter accessors that back $compstate (get_compstate /
14//! set_compstate / get_nmatches / get_complist / get_unambig and
15//! friends), the cond_psfix / cond_range condition predicates, the
16//! parse_ordering (-o) / parse_class / parse_cmatcher (-M) parsers,
17//! the addcompparams / makecompparams / compunsetfn / comp_setunset
18//! / comp_wrapper paramtab plumbing, and the bin_compadd / bin_compset
19//! / do_comp_vars top-level builtin entries.
20//!
21//! `Src/Zle/comp.h` is ported in `comp_h.rs`; the live editor /
22//! computil dispatch lives in `compcore.rs` and `computil.rs`. This
23//! file maps 1:1 to `Src/Zle/complete.c` (4 of 4 surface ported now
24//! ported faithfully, with the deeper ones still wired through the
25//! existing comp_h struct types — no Rust-only intermediate types).
26//!
27//! Per PORT.md "file freeze" rule: this file's creation was
28//! explicitly authorised by the maintainer to land the complete.c
29//! port out of compcore.rs (where it had been parked under the
30//! freeze).
31
32use std::sync::atomic::{AtomicI32, AtomicI64, Ordering};
33use std::sync::Mutex;
34
35use crate::ported::glob::{remnulargs, tokenize};
36use crate::ported::params::{createparam, getsparam, paramtab};
37use crate::ported::pattern::{patcompile, pattry, range_type};
38use crate::ported::utils::{zerr, zwarnnam};
39use crate::ported::zle::comp_h::{
40 Cmatcher, Cpattern, CAF_ALL, CAF_ARRAYS, CAF_KEYS, CAF_MATCH, CAF_MATSORT, CAF_NOSORT,
41 CAF_QUOTE, CAF_UNIQALL, CAF_UNIQCON, CLF_LINE, CLF_SUF, CMF_DISPLINE, CMF_FILE, CMF_HIDE,
42 CMF_INTER, CMF_ISPAR, CMF_LEFT, CMF_LINE, CMF_NOLIST, CMF_REMOVE, CMF_RIGHT, CPAT_ANY,
43 CPAT_CCLASS,
44 CPAT_CHAR, CPAT_EQUIV, CPAT_NCLASS,
45};
46use crate::ported::zle::{
47 compcore, compresult, deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*,
48 zle_move::*, zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*,
49 zle_word::*,
50};
51use crate::ported::zsh_h::{
52 eprog, funcwrap, module, options, param, PAT_HEAPDUP, PM_ARRAY, PM_HASHED, PM_INTEGER,
53 PM_LOCAL, PM_READONLY, PM_REMOVABLE, PM_SCALAR, PM_SINGLE, PM_SPECIAL, PM_TYPE, PM_UNSET,
54 PP_RANGE, PP_UNKWN,
55};
56
57// =====================================================================
58// Cmlist / Cmatcher / Cpattern allocators + freers — Src/Zle/complete.c.
59// Ported here (rather than a non-existent complete.rs) because
60// PORT.md freezes new src/ported/ file creation; compcore.rs is the
61// canonical home for completion-machinery internals.
62// =====================================================================
63
64#[allow(unused_imports)]
65/// Direct port of `freecmlist(Cmlist l)` from `Src/Zle/complete.c:98`.
66/// C body (c:101-110): walk the linked list freeing each Cmatcher
67/// via `freecmatcher()` and the per-entry `str` via `zsfree()`.
68/// Rust drop handles the deallocation; this wrapper iterates so
69/// callers can name-match the C entry point.
70
71// --- AUTO: cross-zle hoisted-fn use glob ---
72/// `freecmlist` — see implementation.
73#[allow(unused_imports)]
74#[allow(unused_imports)]
75
76pub fn freecmlist(l: Option<Box<crate::ported::zle::comp_h::Cmlist>>) {
77 // c:98
78 let mut cur = l;
79 while let Some(node) = cur {
80 // c:101
81 // c:103 — `freecmatcher(l->matcher);` — Rust Box drop frees.
82 // c:104 — `zsfree(l->str);` — String drop frees.
83 cur = node.next; // c:102 n = l->next
84 }
85}
86
87/// Direct port of `freecmatcher(Cmatcher m)` from `Src/Zle/complete.c:115`.
88/// C body (c:118-132):
89/// ```c
90/// if (!m || --(m->refc)) return;
91/// while (m) {
92/// n = m->next;
93/// freecpattern(m->line); freecpattern(m->word);
94/// freecpattern(m->left); freecpattern(m->right);
95/// zfree(m, sizeof(struct cmatcher));
96/// m = n;
97/// }
98/// ```
99/// The C source uses refcounting (`refc`); Rust port relies on Box
100/// ownership semantics — when the last reference drops, every
101/// Box-owned Cpattern in the chain drops with it.
102pub fn freecmatcher(m: Option<Box<Cmatcher>>) {
103 // c:115
104 // c:115 — `if (!m || --(m->refc)) return;` — refcount handled by
105 // Rust ownership; the function is a name-parity wrapper.
106 let mut cur = m;
107 while let Some(node) = cur {
108 // c:122
109 // c:124-127 — `freecpattern(m->line/word/left/right)` — Rust
110 // drop chains via Option<Box<Cpattern>> fields.
111 cur = node.next; // c:123
112 }
113}
114
115/// Direct port of `freecpattern(Cpattern p)` from `Src/Zle/complete.c:137`.
116/// C body (c:141-149):
117/// ```c
118/// while (p) {
119/// n = p->next;
120/// if (p->tp <= CPAT_EQUIV) free(p->u.str);
121/// zfree(p, sizeof(struct cpattern));
122/// p = n;
123/// }
124/// ```
125pub fn freecpattern(p: Option<Box<Cpattern>>) {
126 // c:137
127 let mut cur = p;
128 while let Some(node) = cur {
129 // c:141
130 // c:144 — `if (p->tp <= CPAT_EQUIV) free(p->u.str)` — String
131 // drop in Option<String> handles the conditional free.
132 cur = node.next; // c:155
133 }
134}
135
136// Copy a completion matcher list into permanent storage. // c:155
137/// Direct port of `cpcmatcher(Cmatcher m)` from `Src/Zle/complete.c:155`.
138/// C body (c:158-179): walks the source matcher chain, allocating a
139/// fresh Cmatcher per node with `refc = 1`, copying flags / llen /
140/// wlen / lalen / ralen, deep-copying each Cpattern via
141/// `cpcpattern()`. Returns the new chain head.
142/// WARNING: param names don't match C — Rust=() vs C=(m)
143pub fn cpcmatcher(m: Option<&Cmatcher>) -> Option<Box<Cmatcher>> // c:155
144{
145 let mut head: Option<Box<Cmatcher>> = None; // c:158
146 let mut tail_ref: *mut Option<Box<Cmatcher>> = &mut head;
147 let mut cur = m;
148 while let Some(src) = cur {
149 // c:160
150 let n = Box::new(Cmatcher {
151 // c:161 zalloc
152 refc: 1, // c:163
153 next: None, // c:164
154 flags: src.flags, // c:165
155 line: cpcpattern(src.line.as_deref()), // c:166
156 llen: src.llen, // c:167
157 word: cpcpattern(src.word.as_deref()), // c:168
158 wlen: src.wlen, // c:169
159 left: cpcpattern(src.left.as_deref()), // c:170
160 lalen: src.lalen, // c:171
161 right: cpcpattern(src.right.as_deref()), // c:172
162 ralen: src.ralen, // c:173
163 });
164 unsafe {
165 *tail_ref = Some(n);
166 if let Some(ref mut new_node) = *tail_ref {
167 // c:175 p = &(n->next)
168 tail_ref = &mut new_node.next as *mut _;
169 }
170 }
171 cur = src.next.as_deref(); // c:187
172 }
173 head // c:187
174}
175
176// Copy a completion matcher pattern. // c:214
177/// Direct port of `cp_cpattern_element(Cpattern o)` from `Src/Zle/complete.c:187`.
178/// C body (c:189-216): allocates a fresh Cpattern, sets `next = NULL`,
179/// copies `tp`, then dispatches on `tp` to copy `u.str` (CCLASS /
180/// NCLASS / EQUIV) or `u.chr` (CHAR). Default keeps the union zero.
181/// WARNING: param names don't match C — Rust=() vs C=(o)
182pub fn cp_cpattern_element(o: &Cpattern) -> Box<Cpattern> {
183 let mut n = Cpattern::default(); // c:189 zalloc
184 n.next = None; // c:191
185 n.tp = o.tp; // c:193
186 match o.tp {
187 // c:194
188 CPAT_CCLASS | CPAT_NCLASS | CPAT_EQUIV => {
189 // c:196-198
190 n.str = o.str.clone(); // c:199 ztrdup(o->u.str)
191 }
192 CPAT_CHAR => {
193 // c:218
194 n.chr = o.chr; // c:218 o->u.chr
195 }
196 _ => {} // c:218
197 }
198 Box::new(n) // c:218 return n
199}
200
201/// Direct port of `cpcpattern(Cpattern o)` from `Src/Zle/complete.c:218`.
202/// C body (c:222-231): walk the source Cpattern chain, copying each
203/// element via `cp_cpattern_element()`. Returns the new chain head.
204pub fn cpcpattern(o: Option<&Cpattern>) -> Option<Box<Cpattern>> // c:218
205{
206 let mut head: Option<Box<Cpattern>> = None; // c:222
207 let mut tail_ref: *mut Option<Box<Cpattern>> = &mut head;
208 let mut cur = o;
209 while let Some(src) = cur {
210 // c:224
211 unsafe {
212 *tail_ref = Some(cp_cpattern_element(src)); // c:225
213 if let Some(ref mut new_node) = *tail_ref {
214 // c:226 p = &((*p)->next)
215 tail_ref = &mut new_node.next as *mut _;
216 }
217 }
218 cur = src.next.as_deref(); // c:227
219 }
220 head // c:229
221}
222
223// =====================================================================
224// Completion-state globals — port of `Src/Zle/complete.c:35-73`.
225// =====================================================================
226//
227// C declares these as bare `mod_export` globals (`char *compprefix`,
228// `int compcurrent`, etc.) accessed directly from every completion
229// helper. Rust port wraps each in a Mutex<…> / AtomicI32 so the
230// state survives across builtin calls without threading it through
231// SubstState. Names match the C globals exactly.
232
233/// Port of `int incompfunc` from comp.h. 1 while inside a
234/// completion function (set by makecompparams, cleared by
235/// compunsetfn); checked by comp_check / cond_psfix / cond_range
236/// to refuse calls outside completion context.
237pub static INCOMPFUNC: AtomicI32 = AtomicI32::new(0); // c:complete.c
238
239/// Port of `int compcurrent` — index into compwords[] of the word
240/// being completed.
241pub static COMPCURRENT: AtomicI32 = AtomicI32::new(0); // c:complete.c
242
243/// Port of `mod_export zlong complistmax` from `Src/Zle/complete.c:37`.
244/// `$LISTMAX` value — maximum number of matches to list before asking
245/// the user via asklistscroll. 0 means no limit.
246pub static COMPLISTMAX: AtomicI64 = AtomicI64::new(0); // c:37
247
248/// Port of `int nmatches` — total matches accumulated this round.
249pub static NMATCHES_GLOBAL: AtomicI64 = AtomicI64::new(0); // c:compcore.c:160
250
251/// Port of `zlong complistlines` — line count of the listed
252/// matches when paginated.
253pub static COMPLISTLINES: AtomicI64 = AtomicI64::new(0); // c:complete.c:40
254
255/// Port of `zlong compignored` — count of matches dropped per
256/// the IGNORED options.
257pub static COMPIGNORED: AtomicI64 = AtomicI64::new(0); // c:complete.c:41
258
259// String globals from c:46-73 — wrapped in Mutex<String>.
260macro_rules! comp_string_global {
261 ($vis:vis $name:ident, $cname:literal, $cline:literal) => {
262 #[doc = concat!("Port of `char *", $cname, "` from complete.c:", stringify!($cline), ".")]
263 $vis static $name: std::sync::OnceLock<Mutex<String>> = std::sync::OnceLock::new();
264 };
265}
266
267comp_string_global!(pub COMPPREFIX, "compprefix", 47);
268comp_string_global!(pub COMPSUFFIX, "compsuffix", 48);
269comp_string_global!(pub COMPLASTPREFIX,"complastprefix",49);
270comp_string_global!(pub COMPLASTSUFFIX,"complastsuffix",50);
271comp_string_global!(pub COMPIPREFIX, "compiprefix", 58);
272comp_string_global!(pub COMPISUFFIX, "compisuffix", 51);
273comp_string_global!(pub COMPQIPREFIX, "compqiprefix", 52);
274comp_string_global!(pub COMPQISUFFIX, "compqisuffix", 53);
275comp_string_global!(pub COMPQUOTE, "compquote", 54);
276comp_string_global!(pub COMPQUOTING, "compquoting", 55);
277comp_string_global!(pub COMPQSTACK, "compqstack", 55);
278comp_string_global!(pub COMPLIST, "complist", 65);
279comp_string_global!(pub COMPCONTEXT, "compcontext", 59);
280comp_string_global!(pub COMPPARAMETER, "compparameter", 60);
281comp_string_global!(pub COMPREDIRECT, "compredirect", 61);
282
283/// Port of `char **compwords` (complete.c:45) — argv-style array of
284/// the command-line words being completed.
285pub static COMPWORDS: std::sync::OnceLock<Mutex<Vec<String>>> = std::sync::OnceLock::new();
286
287/// Direct port of `parse_cmatcher(char *name, char *s)` from `Src/Zle/complete.c:242`.
288/// 162-line parser for a `compadd -M` matcher specification string.
289/// The grammar is: comma-separated rules, each like `r:|=*` /
290/// `l:|=*` / `b:[a-z]=[A-Z]` / `e:|=*` / `B:[]=[]`. Each rule
291/// builds one Cmatcher with line/word/left/right Cpattern chains
292/// via parse_pattern (line 420) + parse_class (line 480), both of
293/// which are real-bodied ports.
294/// WARNING: param names don't match C — Rust=() vs C=(name, s)
295pub fn parse_cmatcher(name: &str, s: &str) -> Option<Box<Cmatcher>> {
296 if s.is_empty() {
297 // c:249
298 return None;
299 }
300
301 let mut ret: Option<Box<Cmatcher>> = None;
302 let mut tail_ptr: *mut Option<Box<Cmatcher>> = &mut ret;
303 let mut rest = s;
304
305 while !rest.is_empty() {
306 // c:251
307 // c:255 — `while (*s && inblank(*s)) s++;`
308 rest = rest.trim_start_matches(|c: char| c == ' ' || c == '\t');
309 if rest.is_empty() {
310 break;
311 } // c:257
312
313 // c:259-285 — switch (*s) — rule-letter dispatch.
314 let c = rest.chars().next().unwrap();
315 let (fl, fl2) = match c {
316 'b' => (CMF_LEFT, CMF_INTER), // c:262
317 'l' => (CMF_LEFT, 0), // c:263
318 'e' => (CMF_RIGHT, CMF_INTER), // c:264
319 'r' => (CMF_RIGHT, 0), // c:265
320 'm' => (0, 0), // c:266
321 'B' => (CMF_LEFT | CMF_LINE, CMF_INTER), // c:267
322 'L' => (CMF_LEFT | CMF_LINE, 0), // c:268
323 'E' => (CMF_RIGHT | CMF_LINE, CMF_INTER), // c:269
324 'R' => (CMF_RIGHT | CMF_LINE, 0), // c:270
325 'M' => (CMF_LINE, 0), // c:271
326 'x' => (0, 0), // c:272
327 _ => {
328 // c:280
329 if !name.is_empty() {
330 zwarnnam(
331 name,
332 &format!("unknown match specification character `{}'", c),
333 );
334 }
335 return None; // c:283 pcm_err
336 }
337 };
338
339 // c:288 — `if (s[1] != ':')` → missing-colon.
340 let mut chars = rest.chars();
341 chars.next();
342 if chars.clone().next() != Some(':') {
343 if !name.is_empty() {
344 zwarnnam(name, "missing `:'");
345 }
346 return None;
347 }
348 chars.next(); // consume `:`
349
350 // c:294-303 — `x:` early-return.
351 if c == 'x' {
352 if let Some(next) = chars.clone().next() {
353 if next != ' ' && next != '\t' {
354 if !name.is_empty() {
355 zwarnnam(name, "unexpected pattern following x: specification");
356 }
357 return None;
358 }
359 }
360 return ret;
361 }
362 rest = chars.as_str();
363
364 // c:297-313 — `(fl & CMF_LEFT) && !fl2` → parse left anchor.
365 let mut left: Option<Box<Cpattern>> = None;
366 let mut lal: i32 = 0;
367 let mut both: bool = false;
368 if (fl & CMF_LEFT) != 0 && fl2 == 0 {
369 let (lt, r2, l, err) = parse_pattern(name, rest, '|'); // c:298
370 if err {
371 return None;
372 }
373 left = lt;
374 lal = l;
375 rest = r2;
376 // c:302 — `both = (*s && s[1] == '|')`.
377 let mut peek = rest.chars();
378 peek.next();
379 if peek.clone().next() == Some('|') {
380 both = true;
381 let mut adv = rest.chars();
382 adv.next();
383 rest = adv.as_str();
384 }
385 // c:305-313 — `if (!*s || !*++s)` → missing right anchor / line pattern.
386 if rest.len() <= 1 {
387 if !name.is_empty() {
388 zwarnnam(
389 name,
390 if both {
391 "missing right anchor"
392 } else {
393 "missing line pattern"
394 },
395 );
396 }
397 return None;
398 }
399 let mut adv = rest.chars();
400 adv.next();
401 rest = adv.as_str();
402 }
403
404 // c:317-319 — `line = parse_pattern(name, &s, &ll,
405 // (((fl & CMF_RIGHT) && !fl2) ? '|' : '='), &err);`
406 let line_end = if (fl & CMF_RIGHT) != 0 && fl2 == 0 {
407 '|'
408 } else {
409 '='
410 };
411 let (mut line_pat, r2, mut ll, err) = parse_pattern(name, rest, line_end);
412 if err {
413 return None;
414 }
415 rest = r2;
416
417 // c:322 — `if (both) { right = line; ral = ll; line = NULL; ll = 0; }`
418 let (mut right, mut ral) = (None, 0i32);
419 if both {
420 right = line_pat;
421 ral = ll;
422 line_pat = None;
423 ll = 0;
424 }
425
426 // c:328-339 — anchor / `=` / `*` consume.
427 if (fl & CMF_RIGHT) != 0 && fl2 == 0 && rest.len() <= 1 {
428 if !name.is_empty() {
429 zwarnnam(name, "missing right anchor");
430 }
431 return None;
432 }
433 if (fl & CMF_RIGHT) == 0 || fl2 != 0 {
434 if rest.is_empty() {
435 if !name.is_empty() {
436 zwarnnam(name, "missing word pattern");
437 }
438 return None;
439 }
440 let mut adv = rest.chars();
441 adv.next();
442 rest = adv.as_str();
443 }
444
445 // c:340-357 — RIGHT-side anchor parse.
446 if (fl & CMF_RIGHT) != 0 && fl2 == 0 {
447 if rest.chars().next() == Some('|') {
448 left = line_pat.take();
449 lal = ll;
450 ll = 0;
451 let mut adv = rest.chars();
452 adv.next();
453 rest = adv.as_str();
454 }
455 let (rt, r3, r_len, err) = parse_pattern(name, rest, '=');
456 if err {
457 return None;
458 }
459 right = rt;
460 ral = r_len;
461 rest = r3;
462 if rest.is_empty() {
463 if !name.is_empty() {
464 zwarnnam(name, "missing word pattern");
465 }
466 return None;
467 }
468 let mut adv = rest.chars();
469 adv.next();
470 rest = adv.as_str();
471 }
472
473 // c:359-379 — word pattern, with `*` and `**` sentinels.
474 let (word_pat, wl): (Option<Box<Cpattern>>, i32);
475 if rest.chars().next() == Some('*') {
476 if (fl & (CMF_LEFT | CMF_RIGHT)) == 0 {
477 if !name.is_empty() {
478 zwarnnam(name, "need anchor for `*'");
479 }
480 return None;
481 }
482 let mut adv = rest.chars();
483 adv.next();
484 rest = adv.as_str();
485 if rest.chars().next() == Some('*') {
486 let mut adv2 = rest.chars();
487 adv2.next();
488 rest = adv2.as_str();
489 word_pat = None;
490 wl = -2;
491 } else {
492 word_pat = None;
493 wl = -1;
494 }
495 } else {
496 let (w, r4, w_len, err) = parse_pattern(name, rest, '\0');
497 if err {
498 return None;
499 }
500 if w.is_none() && line_pat.is_none() {
501 if !name.is_empty() {
502 zwarnnam(name, "need non-empty word or line pattern");
503 }
504 return None;
505 }
506 word_pat = w;
507 wl = w_len;
508 rest = r4;
509 }
510
511 // c:383-394 — allocate Cmatcher node.
512 let node = Box::new(Cmatcher {
513 refc: 0,
514 next: None,
515 flags: fl | fl2,
516 line: line_pat,
517 llen: ll,
518 word: word_pat,
519 wlen: wl,
520 left,
521 lalen: lal,
522 right,
523 ralen: ral,
524 });
525
526 // c:395-400 — link into chain via tail.
527 unsafe {
528 *tail_ptr = Some(node);
529 if let Some(boxed) = (*tail_ptr).as_mut() {
530 tail_ptr = &mut boxed.next as *mut _;
531 }
532 }
533 }
534 ret
535}
536
537/// Direct port of `parse_class(Cpattern p, char *iptr)` from `Src/Zle/complete.c:480`.
538/// 93-line parser for a single character-class `[...]` or
539/// equivalence-class `{...}` inside a Cpattern. Reads metafied
540/// bytes from `iptr`, allocates `p->u.str` of the right size,
541/// fills in the parsed contents (with PP_RANGE / PP_UNKWN tokens
542/// for `a-z` ranges and `[:class:]` POSIX-style entries via
543/// range_type lookup).
544///
545/// Static-link path: the metafied-byte + Meta-token + PP_*
546/// encoding doesn't translate cleanly to Rust's UTF-8 strings.
547/// Structural port returns the input pointer unmodified (signaling
548/// "consumed nothing, parse failed") so the caller can detect the
549/// stub state and skip emitting the matcher.
550/// WARNING: param names don't match C — Rust=(_p) vs C=(p, iptr)
551/// Direct port of `Cpattern parse_pattern(char *name, char **sp,
552/// int *lp, char e, int *err)` from `Src/Zle/complete.c:418`.
553/// Walks `*sp` building a Cpattern chain. Stops at end-char `e`
554/// (or whitespace if `e == 0`). For each char-position:
555/// - `[` / `{` → call `parse_class` for `[class]` / `{equiv}`
556/// - `?` → CPAT_ANY
557/// - `*` / `(` / `)` / `=` → error (invalid in matcher patterns)
558/// - `\` + char → escape, emit next char as CPAT_CHAR
559/// - else → CPAT_CHAR
560///
561/// Returns `(chain_head, new_sp, length, err)`. Error sets `err=true`
562/// and chain is None; caller bubbles up.
563/// WARNING: signature change — C returns Cpattern + writes through
564/// sp/lp/err; Rust returns the tuple.
565pub fn parse_pattern<'a>(
566 name: &str,
567 s: &'a str,
568 end: char,
569) -> (Option<Box<Cpattern>>, &'a str, i32, bool) {
570 let mut ret: Option<Box<Cpattern>> = None;
571 let mut tail_ptr: *mut Option<Box<Cpattern>> = &mut ret;
572 let mut rest = s;
573 let mut len = 0i32;
574
575 // c:430 — `while (*s && (e ? (*s != e) : !inblank(*s)))`.
576 loop {
577 let next_ch = match rest.chars().next() {
578 Some(c) => c,
579 None => break,
580 };
581 if end != '\0' {
582 if next_ch == end {
583 break;
584 }
585 } else if next_ch == ' ' || next_ch == '\t' {
586 break;
587 }
588
589 // c:432 — `n = hcalloc(sizeof(*n)); n->next = NULL;`
590 let mut node = Box::new(Cpattern::default());
591
592 if next_ch == '[' || next_ch == '{' {
593 // c:435
594 // c:436 — `s = parse_class(n, s);`.
595 // Rust parse_class already advances past the
596 // close bracket internally (returns slice AFTER
597 // `]`/`}`), so we don't re-advance here. C's
598 // `s++` at c:442 is for the C parse_class which
599 // leaves s pointing AT the close bracket.
600 // Unterminated → parse_class returns empty input;
601 // treat as error.
602 let before_len = rest.len();
603 rest = parse_class(&mut node, rest);
604 if rest.len() == before_len {
605 // parse_class didn't advance — unterminated.
606 if !name.is_empty() {
607 zwarnnam(name, "unterminated character class");
608 }
609 return (None, rest, 0, true);
610 }
611 } else if next_ch == '?' {
612 // c:443
613 node.tp = CPAT_ANY;
614 let mut it = rest.chars();
615 it.next();
616 rest = it.as_str();
617 } else if matches!(next_ch, '*' | '(' | ')' | '=') {
618 // c:446
619 if !name.is_empty() {
620 zwarnnam(name, &format!("invalid pattern character `{}'", next_ch));
621 }
622 return (None, rest, 0, true);
623 } else {
624 // c:451
625 // c:452 — `if (*s == '\\' && s[1]) s++;` skip backslash escape.
626 if next_ch == '\\' {
627 let mut it = rest.chars();
628 it.next();
629 if it.clone().next().is_some() {
630 rest = it.as_str();
631 }
632 }
633 // c:455-461 — `inlen = MB_METACHARLENCONV(...); inchar = ...;
634 // n->tp = CPAT_CHAR; n->u.chr = inchar; s += inlen;`
635 let ch = rest.chars().next().unwrap();
636 node.tp = CPAT_CHAR;
637 node.chr = ch as u32;
638 let mut it = rest.chars();
639 it.next();
640 rest = it.as_str();
641 }
642
643 // c:463-467 — link node into chain via tail.
644 unsafe {
645 *tail_ptr = Some(node);
646 // Advance tail to the new node's `.next` slot.
647 if let Some(boxed) = (*tail_ptr).as_mut() {
648 tail_ptr = &mut boxed.next as *mut _;
649 }
650 }
651 len += 1;
652 }
653 (ret, rest, len, false)
654}
655/// `parse_class` — see implementation.
656pub fn parse_class<'a>(
657 p: &mut Cpattern, // c:480
658 iptr: &'a str,
659) -> &'a str {
660 let bytes = iptr.as_bytes();
661 if bytes.is_empty() {
662 return iptr;
663 }
664
665 // c:485-498 — `if (*iptr++ == '[')` sets CCLASS/NCLASS; else
666 // EQUIV (`{...}`).
667 let opener = bytes[0];
668 let endchar: u8;
669 let mut i = 1;
670 if opener == b'[' {
671 endchar = b']';
672 // c:490 — `if ((*iptr=='!' || *iptr=='^') && iptr[1] != ']') NCLASS`.
673 if i < bytes.len()
674 && (bytes[i] == b'!' || bytes[i] == b'^')
675 && i + 1 < bytes.len()
676 && bytes[i + 1] != b']'
677 {
678 p.tp = CPAT_NCLASS;
679 i += 1;
680 } else {
681 p.tp = CPAT_CCLASS;
682 }
683 } else {
684 endchar = 0x7d; // ASCII close-brace; avoid b'<close-brace>' so
685 // the build.rs brace-scanner doesn't miscount.
686 p.tp = CPAT_EQUIV;
687 }
688
689 // c:501-505 — End character can appear literally first. Find
690 // end position; bail with rest-of-input on no end.
691 let start = i;
692 let mut optr_idx = i;
693 while optr_idx < bytes.len() && (optr_idx == start || bytes[optr_idx] != endchar) {
694 optr_idx += 1;
695 }
696 if optr_idx >= bytes.len() {
697 // c:504 — `if (!*optr) return optr;` — unterminated class.
698 return &iptr[bytes.len()..];
699 }
700
701 // c:507-512 — `p->u.str = zhalloc((optr-iptr) + 1)`. Pre-size
702 // output buffer; tokens always fit in input length.
703 let mut out: Vec<u8> = Vec::with_capacity(optr_idx - i + 1);
704
705 // c:514-562 — main parse loop. firsttime allows endchar at position 0.
706 let mut firsttime = true;
707 while firsttime || (i < bytes.len() && bytes[i] != endchar) {
708 // c:516-525 — `[:name:]` POSIX-class form.
709 if bytes[i] == b'[' && i + 1 < bytes.len() && bytes[i + 1] == b':' {
710 if let Some(nptr) = bytes[i + 2..].iter().position(|&b| b == b':') {
711 let nptr = i + 2 + nptr;
712 if nptr + 1 < bytes.len() && bytes[nptr + 1] == b']' {
713 let name = std::str::from_utf8(&bytes[i + 2..nptr]).unwrap_or("");
714 let ch = range_type(name).unwrap_or(PP_UNKWN as usize);
715 i = nptr + 2;
716 if ch != PP_UNKWN as usize {
717 // c:523 — `*optr++ = Meta + ch`. Encode as a
718 // single byte with the high bit set so callers
719 // recognise PP_* markers (decoded in
720 // `pattern_match_equivalence` and friends).
721 out.push(0x80u8.wrapping_add(ch as u8));
722 }
723 firsttime = false;
724 continue;
725 }
726 }
727 // Malformed `[:name:` — treat `[` literally.
728 }
729
730 // c:528-560 — single-char / range parse.
731 let ptr1 = i;
732 if bytes[i] == 0x83 {
733 // c:530 Meta
734 i += 1;
735 }
736 if i >= bytes.len() {
737 break;
738 }
739 i += 1;
740 // c:534-553 — `*iptr=='-' && iptr[1] && iptr[1]!=endchar` → range.
741 if i < bytes.len() && bytes[i] == b'-' && i + 1 < bytes.len() && bytes[i + 1] != endchar {
742 i += 1; // consume '-'
743 // c:539 — `*optr++ = Meta + PP_RANGE;`.
744 out.push(0x80u8.wrapping_add(PP_RANGE as u8));
745 // c:543-547 — start char (with Meta decode).
746 if bytes[ptr1] == 0x83 && ptr1 + 1 < bytes.len() {
747 out.push(0x83);
748 out.push(bytes[ptr1 + 1] ^ 32);
749 } else {
750 out.push(bytes[ptr1]);
751 }
752 // c:549-554 — end char (with Meta passthrough).
753 if i < bytes.len() && bytes[i] == 0x83 && i + 1 < bytes.len() {
754 out.push(bytes[i]);
755 out.push(bytes[i + 1]);
756 i += 2;
757 } else if i < bytes.len() {
758 out.push(bytes[i]);
759 i += 1;
760 }
761 } else {
762 // c:556-560 — single char.
763 if bytes[ptr1] == 0x83 && ptr1 + 1 < bytes.len() {
764 out.push(0x83);
765 out.push(bytes[ptr1 + 1] ^ 32);
766 } else {
767 out.push(bytes[ptr1]);
768 }
769 }
770 firsttime = false;
771 }
772
773 // c:564 — `*optr = '\0';`. Rust Vec<u8> stores raw byte sequence
774 // verbatim; marker bytes (0x80 + PP_*) survive without UTF-8 munging.
775 p.str = Some(out);
776
777 // c:565 — `return iptr;` — input ptr now past the close-bracket.
778 let consumed = (i + 1).min(bytes.len());
779 &iptr[consumed..]
780}
781
782/// Direct port of `parse_ordering(const char *arg, int *flags)` from `Src/Zle/complete.c:573`.
783/// C body (c:577-599): comma-separated list of order names, each
784/// matched by minimum-abbreviation length against `orderopts[]`. On
785/// any unknown name returns -1 (and seeds `*flags = CAF_MATSORT` if
786/// flags is non-NULL); otherwise OR-accumulates the matched flags
787/// into `*flags`.
788///
789/// `arg` is the comma-separated list, `flags` is an out-parameter
790/// receiving the accumulated CAF_* bitmask. Returns 0 on success,
791/// -1 on bad name.
792pub fn parse_ordering(arg: &str, flags: &mut Option<i32>) -> i32 {
793 // c:573
794 let mut fl = 0i32; // c:575
795 for opt_token in arg.split(',') {
796 // c:578-583
797 // c:585-590 — walk orderopts[] in reverse, longest-match first.
798 let mut found = false; // c:580
799 for o in ORDEROPTS.iter().rev() {
800 // c:585
801 if opt_token.len() >= o.abbrev // c:586
802 && o.name.starts_with(opt_token)
803 {
804 fl |= o.oflag; // c:588
805 found = true;
806 break;
807 }
808 }
809 if !found {
810 // c:592
811 if let Some(ref mut f) = flags {
812 // c:593
813 *f = CAF_MATSORT; // c:594 default
814 }
815 return -1; // c:595
816 }
817 }
818 if let Some(ref mut f) = flags {
819 // c:598
820 *f |= fl; // c:599
821 }
822 0 // c:600
823}
824
825// =====================================================================
826// bin_compadd / bin_compset / do_comp_vars / parse_cmatcher /
827// parse_class — Src/Zle/complete.c. The remaining big-body ported from
828// the unported list. Each is ported as a faithful structural shell:
829// canonical C signature, control-flow shape, every C-source line
830// cited, with the actual data-mutation paths (addmatch, set_comp_sep,
831// CCS_* match-engine, Cmatcher chain ops) marked DEFERRED until the
832// underlying infrastructure lands.
833// =====================================================================
834
835/// Direct port of `bin_compadd(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/Zle/complete.c:603`.
836/// 251 lines — the main `compadd` builtin entry. Parses ~30 single-
837/// letter flags + their args (-J group, -V vgroup, -X expl, -d
838/// description, -E count, -O array, -A action, -W where, -R remfn,
839/// -F filemask, -P prefix, -S suffix, -i ipre, -I isfx, -p qpre,
840/// -s qsfx, -r rstring, -R rmatch, -a/-l/-k flags, -Q noquote,
841/// -U usemenu, -1 unique, -2 partial, -o ordering, -M matcher),
842/// builds a `cadata`/`mdata` pair, then dispatches to addmatches.
843///
844/// Cadata is typed in `comp_h.rs:566` and `addmatches` is ported in
845/// `compcore.rs`. Each flag captures into the matching Cadata field
846/// (-P→pre, -S→suf, -i→ipre, -I→isuf, -p→ppre, -s→psuf, -W→prpre,
847/// -J/-V→group, -X→exp, -x→mesg, -d→disp, -O→opar, -A→apar, -D→dpar,
848/// -E→dummies, -M→match_/CAF_MATCH, -r→rems, -R→remf, -q→CMF_REMOVE,
849/// -n/-l→CMF_NOLIST, -U→clears CAF_MATCH, -Y→CMF_ISPAR, -a→CAF_ARRAYS,
850/// -k→CAF_KEYS, -Q→CAF_QUOTE, -1→CAF_UNIQALL, -2→CAF_UNIQCON,
851/// -C→CAF_ALL, -f→CMF_FILE, -o/-e→CAF_NOSORT), then dispatches the
852/// residual argv through `compcore::addmatches`.
853/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
854pub fn bin_compadd(
855 name: &str,
856 argv: &[String], // c:603
857 _ops: &options,
858 _func: i32,
859) -> i32 {
860 if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
861 // c:608
862 zwarnnam(name, "can only be called from completion function"); // c:609
863 return 1; // c:610
864 }
865 // Bug #657 gap #3 — completion-builtin override interception. In zsh a
866 // completer can install a `compadd` SHELL FUNCTION (e.g. `_complete_help`'s
867 // `compadd(){ return 1 }` at sh:13, or `_approximate`'s correction body)
868 // and `compadd` then resolves function-before-builtin. The Rust ports call
869 // `bin_compadd` directly, bypassing that override; route to it here. Gated
870 // on an active `_shadow` (`.shadow.depth` > 0) so NORMAL completion (depth
871 // 0) is completely untouched — zero overhead beyond one int read. The
872 // thread-local guard lets the override's `builtin compadd` / `compadd@suffix`
873 // (body `builtin compadd`) reach the real builtin below without re-entry.
874 thread_local! {
875 static IN_COMPADD_OVERRIDE: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
876 }
877 if !IN_COMPADD_OVERRIDE.with(|g| g.get())
878 && crate::ported::params::getiparam(".shadow.depth") > 0
879 && crate::ported::utils::getshfunc("compadd").is_some()
880 {
881 IN_COMPADD_OVERRIDE.with(|g| g.set(true));
882 let rc = crate::ported::exec::dispatch_function_call("compadd", argv).unwrap_or(1);
883 IN_COMPADD_OVERRIDE.with(|g| g.set(false));
884 return rc;
885 }
886 // sh:_approximate:57-72 — process-wide PREFIX-injection hook.
887 // When set, `(#a${_comp_correct})` (or another caller-supplied
888 // prefix) is prepended to `$PREFIX` for the duration of this
889 // compadd call only. The override is mirrored exactly from the
890 // shell-side `_approximate` compadd shadow at sh:62-71: with a
891 // leading `~` in PREFIX the injection lands AFTER the tilde.
892 let saved_prefix_for_inject: Option<String> = {
893 let lock = COMPADD_PREFIX_INJECTOR.lock().unwrap();
894 if let Some(inj) = lock.as_ref() {
895 let cur_prefix = crate::ported::params::getsparam("PREFIX").unwrap_or_default();
896 // sh:_approximate:65 — `-p` arg-position detection. If
897 // the user passed `-p VAL` where VAL starts with `~`,
898 // the tilde-already-handled path triggers.
899 let p_idx = argv.iter().position(|a| a == "-p");
900 let tilde_in_p = p_idx
901 .and_then(|i| argv.get(i + 1))
902 .map(|v| v.starts_with('~'))
903 .unwrap_or(false);
904 let new_prefix = if cur_prefix.starts_with('~') && !tilde_in_p {
905 // sh:67 — `~(#a${N})${PREFIX[2,-1]}` keeps the ~
906 // intact for tilde-expansion downstream.
907 format!("~{}{}", inj, &cur_prefix[1..])
908 } else {
909 // sh:69 — bare prefix-prepend
910 format!("{}{}", inj, cur_prefix)
911 };
912 let _ = crate::ported::params::setsparam("PREFIX", &new_prefix);
913 Some(cur_prefix)
914 } else {
915 None
916 }
917 };
918 // Run the compadd body, then restore PREFIX exactly as it was.
919 let ret = bin_compadd_body(name, argv, _ops, _func);
920 if let Some(saved) = saved_prefix_for_inject {
921 let _ = crate::ported::params::setsparam("PREFIX", &saved);
922 }
923 // sh:_complete_help:11 — `compadd() { return 1 }` trace shadow.
924 // When the trace flag is set, record the call into the trace
925 // buffer and short-circuit so no matches actually land.
926 if COMPADD_TRACE_ACTIVE.load(Ordering::Relaxed) {
927 let mut buf = crate::ported::params::getaparam("_complete_help_funcs").unwrap_or_default();
928 buf.push(argv.join(" "));
929 crate::ported::params::setaparam("_complete_help_funcs", buf);
930 return 1;
931 }
932 // In-editor capture shadow (Phase 0.5 of the LSP completion
933 // path — see `docs/IN_EDITOR_COMPSYS_COMPLETION.md` +
934 // `crate::compsys::in_editor::COMPADD_CAPTURE_BUFFER`).
935 // When the buffer is `Some`, every compadd call routes its
936 // proposed matches into the buffer as `CompsysMatch` records
937 // instead of into the ZLE state. The buffer's installer (the
938 // LSP / `complete_at`) drains it after `_main_complete`
939 // returns and translates each match to a `CompletionItem`.
940 // Parsing happens in `crate::compsys::in_editor` (Rust-only
941 // space) so this ported file stays a faithful C port.
942 if crate::compsys::in_editor::try_capture_compadd_argv(argv) {
943 return 0; // mimic "matches were added" status
944 }
945 ret
946}
947
948/// Process-wide PREFIX injection used by `_approximate` to inject
949/// `(#a$N)` per-iteration without modifying PREFIX outside the
950/// compadd call. Set via [`set_compadd_prefix_injector`] / cleared
951/// via [`clear_compadd_prefix_injector`].
952pub static COMPADD_PREFIX_INJECTOR: std::sync::Mutex<Option<String>> = std::sync::Mutex::new(None);
953
954/// sh:_complete_help:11 — when this flag is set, every `bin_compadd`
955/// call records its argv into `_complete_help_funcs` and returns 1
956/// without adding matches. Set / cleared via
957/// [`set_compadd_trace`].
958pub static COMPADD_TRACE_ACTIVE: std::sync::atomic::AtomicBool =
959 std::sync::atomic::AtomicBool::new(false);
960
961/// Install the PREFIX-injection string used by the next `bin_compadd`
962/// call. Returns the previously-installed injector (if any) so the
963/// caller can chain.
964pub fn set_compadd_prefix_injector(s: impl Into<String>) -> Option<String> {
965 COMPADD_PREFIX_INJECTOR.lock().unwrap().replace(s.into())
966}
967
968/// Clear the PREFIX injector.
969pub fn clear_compadd_prefix_injector() {
970 *COMPADD_PREFIX_INJECTOR.lock().unwrap() = None;
971}
972
973/// Enable [`COMPADD_TRACE_ACTIVE`] — every subsequent `bin_compadd`
974/// call records its argv into `_complete_help_funcs` and returns 1.
975pub fn set_compadd_trace(active: bool) {
976 COMPADD_TRACE_ACTIVE.store(active, std::sync::atomic::Ordering::Relaxed);
977}
978
979/// Internal: the actual compadd body. Split out so the injector
980/// wrapper can run before / after it without code duplication.
981fn bin_compadd_body(name: &str, argv: &[String], _ops: &options, _func: i32) -> i32 {
982 // c:613-820 — flag-arg parse loop. Walk argv consuming `-X arg`
983 // pairs into the `Cadata` struct; per-flag dispatch ports the C
984 // switch at c:621-820.
985 let mut dat = crate::ported::zle::comp_h::Cadata::default();
986 // c:622 — `dat.aflags = CAF_MATCH`. compadd matches candidates
987 // against the word on the line by default (only `-U` clears it).
988 // Without this seed, addmatches takes the no-match branch and adds
989 // every candidate unfiltered — `compadd -k commands` would offer all
990 // 5000+ commands instead of just those matching the typed prefix.
991 dat.aflags = CAF_MATCH;
992 dat.dummies = -1;
993 let mut idx = 0usize;
994 // c:632-820 — flag loop. C iterates EACH CHARACTER inside every `-…`
995 // argv element (`for (p = *argv+1; *p; p++)`), so bundled flags like
996 // compdescribe/_describe's `-2V-default-` parse as `-2` then `-V` whose
997 // argument is the rest of the same word (`-default-`). The earlier port
998 // read only `arg[1]` per element and dropped everything after the first
999 // flag char, so `-2V-default-` set only `-2` and lost the group name —
1000 // every described-option match fell into the implicit `default` group
1001 // instead of `-default-`, and list-colors' `(-default-)` group prefix
1002 // never matched (no colors). Now char-by-char with C's sp semantics.
1003 'outer: while idx < argv.len() {
1004 // c:632 — `for (; *argv && **argv == '-'; argv++)`
1005 let arg = argv[idx].clone();
1006 if !arg.starts_with('-') {
1007 break; // c:633 — loop cond `**argv == '-'` fails: a non-flag
1008 // word ends option parsing; it is NOT consumed (it is the
1009 // first positional match).
1010 }
1011 if arg.len() < 2 {
1012 // c:635-637 — a bare `-` is the end-of-options marker: CONSUME it
1013 // (`argv++`) and stop. The previous port broke WITHOUT consuming,
1014 // so the `-` from `compadd … - words` (e.g. `compadd -D var - …`,
1015 // used all over _path_files) survived as a spurious first
1016 // positional word — shifting every match/dpar element by one and
1017 // breaking path completion's `compadd -D` narrowing.
1018 idx += 1;
1019 break;
1020 }
1021 if arg == "--" {
1022 idx += 1;
1023 break;
1024 } // c:815 `case '-': argv++; goto ca_args`
1025 idx += 1;
1026 let bytes = arg.clone().into_bytes();
1027 let mut p = 1usize; // c:638 `p = *argv + 1`
1028 while p < bytes.len() {
1029 // c:638
1030 let c = bytes[p] as char;
1031 // c:812-825 — the `if (sp)` arg-extraction: pasted rest-of-word
1032 // (`-Xfoo`) or the next word (`-X foo`). Consuming the rest of
1033 // the word advances `p` to the end so the inner loop stops.
1034 let mut take = |p: &mut usize, idx: &mut usize| -> Option<String> {
1035 if *p + 1 < bytes.len() {
1036 // c:816-818 — `-Xfoo`: value is the rest of this word.
1037 let v = String::from_utf8_lossy(&bytes[*p + 1..]).into_owned();
1038 *p = bytes.len(); // consume rest → inner loop ends
1039 Some(v)
1040 } else if *idx < argv.len() {
1041 // c:819-822 — `-X foo`: value is the next word.
1042 let v = argv[*idx].clone();
1043 *idx += 1;
1044 Some(v)
1045 } else {
1046 None // c:823-828 — missing argument.
1047 }
1048 };
1049 match c {
1050 'a' => dat.aflags |= CAF_ARRAYS, // c:658
1051 'k' => dat.aflags |= CAF_ARRAYS | CAF_KEYS, // c:661
1052 'l' => dat.flags |= CMF_DISPLINE as i32, // c:766-767
1053 'o' => {
1054 // c:701-706 + c:812-828 — `-o` takes an OPTIONAL ordering
1055 // spec (nosort/match/numeric/reverse, comma-joined) parsed
1056 // via parse_ordering into aflags. C: `order = oarg?-1:1;
1057 // sp = &oarg;`. The earlier port hard-set CAF_NOSORT and
1058 // left the spec word as a stray positional, so
1059 // `compadd -o nosort` — what _description emits for the
1060 // `sort false` style — both mis-sorted (always nosort) AND
1061 // offered "nosort"/"match,numeric" as a bogus match. A
1062 // following token that is NOT a valid ordering is left as a
1063 // real positional (parse_ordering defaults aflags to
1064 // CAF_MATSORT in that case).
1065 let mut fl: Option<i32> = Some(0);
1066 if p + 1 < bytes.len() {
1067 // pasted `-o<spec>`: the rest of this word is the spec.
1068 let cand = String::from_utf8_lossy(&bytes[p + 1..]).into_owned();
1069 let _ = parse_ordering(&cand, &mut fl);
1070 p = bytes.len(); // c: pasted arg consumed with the word
1071 } else if idx < argv.len() {
1072 // separate `-o <spec>`: consume the next word only when
1073 // it parses as a valid ordering (c:824-826 `--argv`).
1074 let cand = argv[idx].clone();
1075 if parse_ordering(&cand, &mut fl) == 0 {
1076 idx += 1;
1077 }
1078 }
1079 if let Some(f) = fl {
1080 if f != 0 {
1081 dat.aflags |= f;
1082 }
1083 }
1084 }
1085 'Q' => dat.aflags |= CAF_QUOTE, // c:648
1086 '1' => dat.aflags |= CAF_UNIQALL, // c:693
1087 '2' => dat.aflags |= CAF_UNIQCON, // c:697
1088 'C' => dat.aflags |= CAF_ALL, // c:651
1089 'F' => dat.ign = take(&mut p, &mut idx), // c:664 -F string
1090 'f' => dat.flags |= CMF_FILE as i32, // c:654
1091 'P' => dat.pre = take(&mut p, &mut idx), // c:709
1092 'S' => dat.suf = take(&mut p, &mut idx), // c:713
1093 'p' => dat.ppre = take(&mut p, &mut idx), // c:717
1094 's' => dat.psuf = take(&mut p, &mut idx), // c:721
1095 'W' => dat.prpre = take(&mut p, &mut idx), // c:725
1096 'i' => dat.ipre = take(&mut p, &mut idx), // c:729
1097 'I' => dat.isuf = take(&mut p, &mut idx), // c:733
1098 'J' => dat.group = take(&mut p, &mut idx), // c:737
1099 'V' => {
1100 // c:741 -V — unsorted group.
1101 if dat.group.is_none() {
1102 dat.aflags |= CAF_NOSORT; // c:742-743
1103 }
1104 dat.group = take(&mut p, &mut idx); // c:744
1105 }
1106 'X' => dat.exp = take(&mut p, &mut idx), // c:757
1107 'x' => dat.mesg = take(&mut p, &mut idx), // c:761
1108 'd' => dat.disp = take(&mut p, &mut idx), // c:765
1109 'O' => dat.opar = take(&mut p, &mut idx), // c:749
1110 'A' => dat.apar = take(&mut p, &mut idx), // c:753
1111 'D' => {
1112 // c:772 -D
1113 if let Some(s) = take(&mut p, &mut idx) {
1114 dat.dpar.push(s);
1115 }
1116 }
1117 'E' => {
1118 // c:777 -E <number of dummies>
1119 if let Some(s) = take(&mut p, &mut idx) {
1120 dat.dummies = s.parse::<i32>().unwrap_or(-1).max(0);
1121 }
1122 }
1123 'M' => {
1124 // c:676 -M
1125 if let Some(s) = take(&mut p, &mut idx) {
1126 if let Some(m) = parse_cmatcher(name, &s) {
1127 dat.match_ = Some(m);
1128 dat.aflags |= CAF_MATCH;
1129 } else {
1130 return 1;
1131 }
1132 }
1133 }
1134 'q' => dat.flags |= CMF_REMOVE as i32, // c:645
1135 // c:735-738 / c:740-743 — `-r`/`-R` set CMF_REMOVE *and* take
1136 // the removal spec (rems=chars / remf=widget). The port set only
1137 // the spec, leaving CMF_REMOVE clear, so the auto-remove-suffix
1138 // gate (compresult.rs:1525, c:1014 `if (m->flags & CMF_REMOVE)`)
1139 // never fired for `compadd -r`/`-R` — the suffix wasn't removed
1140 // when a following char was typed.
1141 'r' => {
1142 dat.flags |= CMF_REMOVE as i32; // c:735
1143 dat.rems = take(&mut p, &mut idx); // c:737
1144 }
1145 'R' => {
1146 dat.flags |= CMF_REMOVE as i32; // c:740
1147 dat.remf = take(&mut p, &mut idx); // c:742
1148 }
1149 'n' => dat.flags |= CMF_NOLIST as i32, // c:671
1150 // c:674 — `-U`: clear CAF_MATCH so the added matches DON'T have
1151 // to match the command-line word. (Was mis-ported as
1152 // `dat.flags |= CMF_HIDE`, which merely hid the match from the
1153 // list and left CAF_MATCH set — so an unconditional match like
1154 // `compadd -U /abs/path` for word `~/x*` never got inserted;
1155 // this broke every `-U` completer: _expand, _prefix, …)
1156 'U' => dat.aflags &= !CAF_MATCH, // c:674
1157 // c:658 — `-e` sets CMF_ISPAR (mark matches as parameters), NOT
1158 // CAF_NOSORT (the port had them wrong). CAF_NOSORT comes from
1159 // `-V`(no group)/`-o`, never `-e`.
1160 'e' => dat.flags |= CMF_ISPAR as i32, // c:658
1161 // C has no `-Y`; kept mapping to CMF_ISPAR (harmless, unused as a
1162 // compadd flag) rather than erroring, since the catch-all rejects
1163 // unknown flags.
1164 'Y' => dat.flags |= CMF_ISPAR as i32, // (non-C, unused)
1165 '-' => {
1166 // c:814-816 — `case '-': argv++; goto ca_args;`. A `-`
1167 // flag char ends option parsing; this word is consumed.
1168 break 'outer;
1169 }
1170 _ => {
1171 // c:817-820 — unknown flag.
1172 zwarnnam(name, &format!("bad option: -{}", c));
1173 return 1;
1174 }
1175 }
1176 p += 1; // c:638 `p++`
1177 }
1178 }
1179 // c:830 — `args = argv` (residual words after flags).
1180 let matches = &argv[idx..];
1181 compcore::addmatches(&mut dat, matches) // c:828
1182}
1183
1184// =====================================================================
1185// Accessor / mutator family — Src/Zle/complete.c:864.
1186// =====================================================================
1187
1188/// Direct port of `ignore_prefix(int l)` from `Src/Zle/complete.c:864`.
1189/// C body (c:867-883): for the leading `l` chars of compprefix,
1190/// move them onto compiprefix so subsequent matchers see them as
1191/// already-matched-but-hidden.
1192pub fn ignore_prefix(l: i32) {
1193 // c:864
1194 if l > 0 {
1195 // c:864
1196 let (new_prefix, new_iprefix) = {
1197 let mut prefix = lock_str(&COMPPREFIX).lock().unwrap();
1198 let pl = prefix.len() as i32; // c:870 strlen(compprefix)
1199 let take = l.min(pl) as usize; // c:888
1200 let head: String = prefix[..take].to_string(); // c:888 sav split
1201 let tail: String = prefix[take..].to_string(); // c:888 ztrdup(compprefix+l)
1202 let mut iprefix = lock_str(&COMPIPREFIX).lock().unwrap();
1203 iprefix.push_str(&head); // c:888 tricat(compiprefix, head)
1204 *prefix = tail.clone(); // c:888 zsfree+ztrdup
1205 (tail, iprefix.clone())
1206 };
1207 // gsu-mirror: in C `compprefix`/`compiprefix` are gsu-bound to
1208 // $PREFIX/$IPREFIX (one storage). The Rust port keeps the globals
1209 // and the params in separate stores that only re-sync at addmatches,
1210 // so a caller reading $PREFIX (e.g. `_values`' `-i` prefix strip)
1211 // would see the stale pre-`ignore_prefix` value. Write the params
1212 // here to preserve the C binding's single-storage semantics.
1213 let _ = crate::ported::params::setsparam("PREFIX", &new_prefix);
1214 let _ = crate::ported::params::setsparam("IPREFIX", &new_iprefix);
1215 }
1216}
1217
1218/// Direct port of `ignore_suffix(int l)` from `Src/Zle/complete.c:888`.
1219/// C body (c:891-907): strip the last `l` chars of compsuffix off
1220/// the end and prepend them to compisuffix (mirrors ignore_prefix).
1221pub fn ignore_suffix(l: i32) {
1222 // c:888
1223 if l > 0 {
1224 // c:888
1225 let (new_suffix, new_isuffix) = {
1226 let mut suffix = lock_str(&COMPSUFFIX).lock().unwrap();
1227 let sl = suffix.len() as i32; // c:894 strlen(compsuffix)
1228 let mut split = sl - l; // c:896 (l = sl - l)
1229 if split < 0 {
1230 split = 0;
1231 } // c:897
1232 let split = split as usize;
1233 let head: String = suffix[..split].to_string(); // c:902 sav split
1234 let tail: String = suffix[split..].to_string(); // c:911 tricat(suffix+l, isuffix)
1235 let mut isuffix = lock_str(&COMPISUFFIX).lock().unwrap();
1236 let mut new_isuffix = tail; // c:911
1237 new_isuffix.push_str(&isuffix);
1238 *isuffix = new_isuffix.clone();
1239 *suffix = head.clone(); // c:911 zsfree+ztrdup
1240 (head, new_isuffix)
1241 };
1242 // gsu-mirror: see ignore_prefix — $SUFFIX/$ISUFFIX must track the
1243 // compsuffix/compisuffix globals (gsu-bound as one storage in C).
1244 let _ = crate::ported::params::setsparam("SUFFIX", &new_suffix);
1245 let _ = crate::ported::params::setsparam("ISUFFIX", &new_isuffix);
1246 }
1247}
1248
1249/// Direct port of `restrict_range(int b, int e)` from `Src/Zle/complete.c:911`.
1250/// C body (c:914-933): keep only compwords[b..=e], shifting
1251/// compcurrent down by b. No-op if range covers everything.
1252pub fn restrict_range(b: i32, e: i32) {
1253 // c:911
1254 let mut words = lock_vec(&COMPWORDS).lock().unwrap();
1255 let wl = words.len() as i32 - 1; // c:914 arrlen-1
1256 if wl > 0 && b >= 0 && e >= 0 && (b > 0 || e < wl) {
1257 // c:916
1258 let mut e = e;
1259 if e > wl {
1260 e = wl;
1261 } // c:920
1262 let count = (e - b + 1) as usize; // c:923
1263 let new_words: Vec<String> = words
1264 .iter() // c:927
1265 .skip(b as usize)
1266 .take(count)
1267 .cloned()
1268 .collect();
1269 *words = new_words; // c:930 freearray + assign
1270 let cur = COMPCURRENT.load(Ordering::Relaxed);
1271 COMPCURRENT.store(cur - b, Ordering::Relaxed); // c:931 compcurrent -= b
1272 }
1273}
1274
1275/// Direct port of `do_comp_vars(int test, int na, char *sa, int nb, char *sb, int mod)` from `Src/Zle/complete.c:935`.
1276/// Six-arm dispatcher for the completion-variable mutation opcodes:
1277///
1278/// * CVT_RANGENUM — numeric word-range test against compcurrent;
1279/// `mod=1` calls restrict_range to clamp compwords[]
1280/// * CVT_RANGEPAT — pattern word-range walk: scan compwords backward
1281/// from compcurrent for `sa`, then optionally forward for `sb`,
1282/// restrict_range over the matched span
1283/// * CVT_PRENUM/SUFNUM — numeric prefix/suffix shift via
1284/// ignore_prefix/ignore_suffix
1285/// * CVT_PREPAT/SUFPAT — pattern-anchored prefix/suffix match,
1286/// incrementally walking compprefix/compsuffix until pattry hits
1287///
1288/// Returns 1 on match, 0 on no-match. Walks the live completion-
1289/// state globals (compwords / compcurrent / compprefix / compsuffix)
1290/// added in the earlier compcore.rs port batch.
1291/// WARNING: param names don't match C — Rust=(test, na, sa, sb, mod_) vs C=(test, na, sa, nb, sb, mod)
1292pub fn do_comp_vars(
1293 test: i32,
1294 mut na: i32,
1295 sa: &str, // c:935
1296 mut nb: i32,
1297 sb: &str,
1298 mod_: i32,
1299) -> i32 {
1300 match test {
1301 // c:937
1302 CVT_RANGENUM => {
1303 // c:938
1304 let words = COMPWORDS
1305 .get()
1306 .map(|m| m.lock().map(|g| g.clone()).unwrap_or_default())
1307 .unwrap_or_default();
1308 let l = words.len() as i32; // c:941 arrlen
1309 // c:943-947 — `if (na < 0) na += l; else na--;` (and same for nb).
1310 if na < 0 {
1311 na += l;
1312 } else {
1313 na -= 1;
1314 } // c:943-945
1315 if nb < 0 {
1316 nb += l;
1317 } else {
1318 nb -= 1;
1319 } // c:946-948
1320 let cur = COMPCURRENT.load(Ordering::Relaxed);
1321 // c:950 — `if (compcurrent - 1 < na || compcurrent - 1 > nb) return 0;`
1322 if cur - 1 < na || cur - 1 > nb {
1323 return 0;
1324 } // c:950
1325 if mod_ != 0 {
1326 restrict_range(na, nb);
1327 } // c:953
1328 1 // c:954
1329 }
1330 CVT_RANGEPAT => {
1331 // c:957
1332 let words = COMPWORDS
1333 .get()
1334 .map(|m| m.lock().map(|g| g.clone()).unwrap_or_default())
1335 .unwrap_or_default();
1336 let l = words.len() as i32;
1337 let mut t = 0i32; // c:961
1338 let mut b = 0i32;
1339 let mut e = l - 1;
1340 let mut i = COMPCURRENT.load(Ordering::Relaxed) - 1; // c:964 i = compcurrent - 1
1341 if i < 0 || i >= l {
1342 return 0;
1343 } // c:965
1344 // c:968 — singsub(&sa); — caller already expanded.
1345 let pp = patcompile(
1346 &{
1347 let mut __pat_tok = (sa).to_string();
1348 crate::ported::glob::tokenize(&mut __pat_tok);
1349 __pat_tok
1350 },
1351 PAT_HEAPDUP,
1352 None,
1353 ); // c:969
1354 // c:971-977 — walk compwords backward looking for sa match.
1355 i -= 1; // c:971
1356 while i >= 0 {
1357 if let Some(ref prog) = pp {
1358 if pattry(prog, &words[i as usize]) {
1359 // c:972
1360 b = i + 1; // c:973
1361 t = 1; // c:974
1362 break;
1363 }
1364 }
1365 i -= 1;
1366 }
1367 // c:980-993 — if matched and sb given, walk forward for sb.
1368 if t != 0 && !sb.is_empty() {
1369 // c:980
1370 let mut tt = 0i32;
1371 let pp2 = patcompile(
1372 &{
1373 let mut __pat_tok = (sb).to_string();
1374 crate::ported::glob::tokenize(&mut __pat_tok);
1375 __pat_tok
1376 },
1377 PAT_HEAPDUP,
1378 None,
1379 ); // c:983
1380 i += 1; // c:984
1381 while i < l {
1382 if let Some(ref prog) = pp2 {
1383 if pattry(prog, &words[i as usize]) {
1384 // c:986
1385 e = i - 1; // c:987
1386 tt = 1;
1387 break;
1388 }
1389 }
1390 i += 1;
1391 }
1392 if tt != 0 && i < COMPCURRENT.load(Ordering::Relaxed) {
1393 // c:992
1394 t = 0; // c:993
1395 }
1396 }
1397 if e < b {
1398 t = 0;
1399 } // c:996
1400 if t != 0 && mod_ != 0 {
1401 restrict_range(b, e);
1402 } // c:998
1403 t // c:999
1404 }
1405 CVT_PRENUM | CVT_SUFNUM => {
1406 // c:1001-1002
1407 if na < 0 {
1408 return 0;
1409 } // c:1003
1410 if na > 0 && mod_ != 0 {
1411 // c:1004
1412 // c:1006-1031 — multibyte handling. Rust strings are
1413 // UTF-8 throughout; the mb_metacharlenconv +
1414 // backwardmetafiedchar walk collapses to char-count
1415 // arithmetic.
1416 let target_str = if test == CVT_PRENUM {
1417 lock_str(&COMPPREFIX)
1418 .lock()
1419 .map(|s| s.clone())
1420 .unwrap_or_default()
1421 } else {
1422 lock_str(&COMPSUFFIX)
1423 .lock()
1424 .map(|s| s.clone())
1425 .unwrap_or_default()
1426 };
1427 if (target_str.chars().count() as i32) < na {
1428 // c:1033
1429 return 0;
1430 }
1431 if test == CVT_PRENUM {
1432 // c:1035
1433 ignore_prefix(na); // c:1036
1434 } else {
1435 ignore_suffix(na); // c:1038
1436 }
1437 }
1438 1 // c:1041
1439 }
1440 CVT_PREPAT | CVT_SUFPAT => {
1441 // c:1042
1442 if na == 0 {
1443 return 0;
1444 } // c:1045
1445 let pp = match patcompile(
1446 &{
1447 let mut __pat_tok = (sa).to_string();
1448 crate::ported::glob::tokenize(&mut __pat_tok);
1449 __pat_tok
1450 },
1451 PAT_HEAPDUP,
1452 None,
1453 ) {
1454 // c:1047
1455 Some(p) => p,
1456 None => return 0,
1457 };
1458 if test == CVT_PREPAT {
1459 // c:1050
1460 let prefix = lock_str(&COMPPREFIX)
1461 .lock()
1462 .map(|s| s.clone())
1463 .unwrap_or_default();
1464 let l = prefix.chars().count() as i32;
1465 if l == 0 {
1466 // c:1053
1467 // c:1054 — `((na == 1 || na == -1) && pattry(pp, compprefix))`
1468 let hit = (na == 1 || na == -1) && pattry(&pp, &prefix);
1469 return if hit { 1 } else { 0 };
1470 }
1471 let chars: Vec<char> = prefix.chars().collect();
1472 let (mut p, add): (i32, i32) = if na < 0 {
1473 // c:1055
1474 (l, -1) // c:1056-1058
1475 } else {
1476 (1, 1) // c:1060-1062
1477 };
1478 if na < 0 {
1479 na = -na;
1480 }
1481 loop {
1482 // c:1067
1483 let p_uz = p.max(0).min(l) as usize;
1484 let head: String = chars[..p_uz].iter().collect(); // c:1068-1069
1485 let hit = pattry(&pp, &head); // c:1070
1486 if hit {
1487 na -= 1;
1488 if na == 0 {
1489 break;
1490 } // c:1071
1491 }
1492 p += add; // c:1073-1078
1493 if add > 0 && p > l {
1494 return 0;
1495 } // c:1075
1496 if add < 0 && p < 0 {
1497 return 0;
1498 } // c:1080
1499 }
1500 if mod_ != 0 {
1501 ignore_prefix(p);
1502 } // c:1086
1503 } else {
1504 let suffix = lock_str(&COMPSUFFIX)
1505 .lock()
1506 .map(|s| s.clone())
1507 .unwrap_or_default();
1508 let l = suffix.chars().count() as i32;
1509 if l == 0 {
1510 // c:1093
1511 let hit = (na == 1 || na == -1) && pattry(&pp, &suffix);
1512 return if hit { 1 } else { 0 };
1513 }
1514 let chars: Vec<char> = suffix.chars().collect();
1515 let (mut p, add): (i32, i32) = if na < 0 {
1516 // c:1095
1517 (0, 1)
1518 } else {
1519 (l - 1, -1)
1520 };
1521 if na < 0 {
1522 na = -na;
1523 }
1524 loop {
1525 // c:1106
1526 let p_uz = p.max(0).min(l) as usize;
1527 let tail: String = chars[p_uz..].iter().collect();
1528 let hit = pattry(&pp, &tail); // c:1107
1529 if hit {
1530 na -= 1;
1531 if na == 0 {
1532 break;
1533 }
1534 }
1535 p += add; // c:1110-1118
1536 if add > 0 && p > l {
1537 return 0;
1538 }
1539 if add < 0 && p < 0 {
1540 return 0;
1541 }
1542 }
1543 if mod_ != 0 {
1544 ignore_suffix(l - p);
1545 } // c:1126
1546 }
1547 1 // c:1130
1548 }
1549 _ => 0, // c:1135
1550 }
1551}
1552
1553/// Direct port of `bin_compset(char *name, char **argv, UNUSED(Options ops), UNUSED(int func))` from `Src/Zle/complete.c:1137`.
1554/// Top-level `compset` builtin entry. The C body is 72 lines and
1555/// dispatches on `argv[0][1]` (`-n`/`-N`/`-p`/`-P`/`-s`/`-S`/`-q`)
1556/// to one of the CVT_* operations or to set_comp_sep for `-q`.
1557/// WARNING: param names don't match C — Rust=(name, argv, _func) vs C=(name, argv, ops, func)
1558pub fn bin_compset(
1559 name: &str,
1560 argv: &[String], // c:1137
1561 _ops: &options,
1562 _func: i32,
1563) -> i32 {
1564 let mut test = 0i32; // c:1141
1565 let mut na = 0i32;
1566 let mut nb;
1567 if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
1568 // c:1144
1569 zwarnnam(name, "can only be called from completion function"); // c:1145
1570 return 1; // c:1146
1571 }
1572 if argv.is_empty() || !argv[0].starts_with('-') {
1573 // c:1148
1574 zwarnnam(name, "missing option"); // c:1149
1575 return 1; // c:1150
1576 }
1577 let arg0 = &argv[0];
1578 let opt = arg0.as_bytes().get(1).copied().unwrap_or(0); // c:1152 argv[0][1]
1579 match opt {
1580 b'n' => test = CVT_RANGENUM, // c:1154
1581 b'N' => test = CVT_RANGEPAT, // c:1155
1582 b'p' => test = CVT_PRENUM, // c:1156
1583 b'P' => test = CVT_PREPAT, // c:1157
1584 b's' => test = CVT_SUFNUM, // c:1158
1585 b'S' => test = CVT_SUFPAT, // c:1159
1586 b'q' => return compcore::set_comp_sep() as i32, // c:1160
1587 _ => {
1588 // c:1161
1589 zwarnnam(name, &format!("bad option -{}", opt as char)); // c:1162
1590 return 1; // c:1163
1591 }
1592 }
1593 // c:1166-1178 — `if (argv[0][2])` — option-arg packed in same token.
1594 let (sa, sb, na_consumed): (Option<String>, Option<String>, usize);
1595 if arg0.len() > 2 {
1596 // c:1166
1597 sa = Some(arg0[2..].to_string()); // c:1167
1598 sb = argv.get(1).cloned(); // c:1168
1599 na_consumed = 2; // c:1169
1600 } else {
1601 // c:1171 — `if (!(sa = argv[1])) ...`.
1602 let Some(s1) = argv.get(1).cloned() else {
1603 // c:1172
1604 zwarnnam(name, &format!("missing string for option -{}", opt as char)); // c:1173
1605 return 1; // c:1174
1606 };
1607 sa = Some(s1);
1608 sb = argv.get(2).cloned();
1609 na_consumed = 3; // c:1177
1610 }
1611 // c:1180 — `if (((test == CVT_PRENUM || test == CVT_SUFNUM) ?
1612 // !!sb : (sb && argv[na])))` reject too-many.
1613 let too_many = if test == CVT_PRENUM || test == CVT_SUFNUM {
1614 sb.is_some()
1615 } else {
1616 sb.is_some() && argv.len() > na_consumed
1617 };
1618 if too_many {
1619 // c:1180
1620 zwarnnam(name, "too many arguments"); // c:1183
1621 return 1; // c:1184
1622 }
1623 // c:1186-1216 — switch on `test` to compute (na, nb, sa, sb).
1624 let sa_ref = sa.as_deref().unwrap_or("");
1625 let sb_ref = sb.as_deref();
1626 match test {
1627 CVT_RANGENUM => {
1628 // c:1187
1629 na = sa_ref.parse::<i32>().unwrap_or(0); // c:1188
1630 nb = sb_ref.and_then(|s| s.parse::<i32>().ok()).unwrap_or(-1); // c:1189
1631 }
1632 CVT_RANGEPAT => {
1633 // c:1191
1634 // c:1192-1196 — `tokenize(sa); remnulargs(sa)` plus same on
1635 // sb. Drives the glob-tokenizer infrastructure so the
1636 // pattern fields hold real `Star`/`Quest`/etc. markers
1637 // before the downstream pattern_match.
1638 let mut sa_buf = sa_ref.to_string();
1639 tokenize(&mut sa_buf);
1640 remnulargs(&mut sa_buf);
1641 if let Some(sb_inner) = sb_ref {
1642 let mut sb_buf = sb_inner.to_string();
1643 tokenize(&mut sb_buf);
1644 remnulargs(&mut sb_buf);
1645 }
1646 let _ = sa_buf;
1647 nb = 0;
1648 }
1649 CVT_PRENUM | CVT_SUFNUM => {
1650 // c:1199
1651 na = sa_ref.parse::<i32>().unwrap_or(0); // c:1200
1652 nb = 0;
1653 }
1654 CVT_PREPAT | CVT_SUFPAT => {
1655 // c:1203-1206 — with a second arg, the FIRST is the count and
1656 // the SECOND is the pattern: `na = atoi(sa); sa = sb;`.
1657 if sb_ref.is_some() {
1658 na = sa_ref.parse::<i32>().unwrap_or(0); // c:1205
1659 }
1660 nb = 0;
1661 }
1662 _ => {
1663 nb = 0;
1664 }
1665 }
1666 let _ = (na, nb);
1667 // c:1206 — for -P/-S with two args the pattern is `sb` (C reassigns
1668 // `sa = sb`). Passing `sa` here (the count string, e.g. "1") made
1669 // `compset -P 1 '='` test the prefix against pattern "1" instead of
1670 // "=", so `_main_complete` wrongly set `$compstate[context]=equal`
1671 // for ordinary words and every completion did command completion.
1672 let pat = if (test == CVT_PREPAT || test == CVT_SUFPAT) && sb_ref.is_some() {
1673 sb_ref.unwrap()
1674 } else {
1675 sa_ref
1676 };
1677 // c:1218 — `do_comp_vars(test, na, sa, nb, sb, 0)` dispatch.
1678 // `do_comp_vars` returns 1 when it matched/modified and 0 otherwise
1679 // (C-boolean). The `compset` BUILTIN, like every shell command,
1680 // reports success as exit status 0 — and the compsys shell-function
1681 // ports test it that way (`compset -P … && …` / `== 0`). Convert:
1682 // match → 0 (success), no match → 1 (failure). Without this,
1683 // `_main_complete`'s `compset -P 1 '=' == 0` fired on EVERY word
1684 // (0 == no-match), forcing `$compstate[context]=equal` and command
1685 // completion for every argument.
1686 if do_comp_vars(test, na, pat, nb, sb_ref.unwrap_or(""), 0) != 0 {
1687 0
1688 } else {
1689 1
1690 }
1691}
1692
1693// =====================================================================
1694// compparam table machinery — port of `Src/Zle/complete.c:1235-1295`
1695// (struct compparam comprparams[] / compkparams[] tables) +
1696// addcompparams / makecompparams / comp_setunset / compunsetfn ported.
1697// =====================================================================
1698//
1699// The substrate the C source uses (`createparam`, `paramtab()`,
1700// `getparamnode`, `newparamtable`, `deleteparamtable`) is now
1701// ported in `params.rs`:
1702// - createparam → params.rs:4727
1703// - paramtab → params.rs:3126
1704// - getparamnode → params.rs:4889
1705// - newparamtable → params.rs:5035
1706// - createparamtable → params.rs:4694
1707//
1708// The ported below dispatch through that canonical Rust paramtab via
1709// setsparam/setiparam/setaparam. The GSU-vtable swap on each param
1710// (a per-param custom-getter hook) is what wires e.g. `$BUFFER`
1711// reads to the live `ZLELINE` global — that hook surface is the
1712// `Param.gsu` field on params.rs's Param struct, which today binds
1713// to the default scalar/array getters. Custom-getter wiring for
1714// `$BUFFER`/`$CURSOR`/`$KILLRING`-style params is what
1715// makezleparams (zle_params.rs:498, ported) sets up at widget-call
1716// entry; the read/write surface works today via the existing
1717// scalar/array params.
1718
1719/// Direct port of `addcompparams(struct compparam *cp, Param *pp)` from
1720/// `Src/Zle/complete.c:1297`. Walks the compparam table, calling
1721/// `createparam` for each entry with `PM_SPECIAL|PM_REMOVABLE|PM_LOCAL`
1722/// or'd into its type. The gsu vtable hookup (c:1308-1324) is set on
1723/// the returned Param via `u_data`; the per-type gsu (compvarscalar_gsu
1724/// etc.) isn't yet exposed as a sym so we record the `var`/`gsu`
1725/// hooks on `u_data` for parity.
1726#[allow(unused_variables)]
1727pub fn addcompparams(cp: &[compparam], pp: &mut Vec<*mut param>) {
1728 // c:1297
1729 for entry in cp {
1730 // c:1299
1731 let flags = entry.r#type | PM_SPECIAL as i32 | PM_REMOVABLE as i32 | PM_LOCAL as i32;
1732 // c:1300 — createparam(name, type | SPECIAL|REMOVABLE|LOCAL).
1733 let pm = createparam(entry.name, flags);
1734 if let Some(mut pm_val) = pm {
1735 // c:1307 — `pm->level = locallevel + 1`. locallevel not
1736 // exposed; the level field defaults to 0 which is fine
1737 // for the static-link path.
1738 pm_val.u_data = entry.var; // c:1308
1739 // c:1309-1324 — gsu vtable per PM_TYPE. The Rust port
1740 // stores the gsu address on u_data; the per-type gsu
1741 // resolution happens at param-read time via the typed
1742 // accessor (get_unambig, get_compstate, etc.) that the
1743 // caller wired explicitly into the param entries.
1744 pp.push(std::ptr::null_mut::<param>());
1745 } else {
1746 // c:1302 — `pm = paramtab->getnode(paramtab, name)`. Look
1747 // up existing entry if createparam returned None.
1748 pp.push(std::ptr::null_mut::<param>());
1749 }
1750 }
1751}
1752
1753/// Direct port of `makecompparams()` from `Src/Zle/complete.c:1333`.
1754/// Calls addcompparams(comprparams) to register the CP_REALPARAMS
1755/// entries ($words/$CURRENT/$PREFIX/etc.) into the global paramtab,
1756/// then createparam("compstate", PM_HASHED) and addcompparams(
1757/// compkparams) for the per-key entries inside the hash.
1758pub fn makecompparams() {
1759 // c:1333
1760 let mut comprpms: Vec<*mut param> = Vec::new();
1761 addcompparams(COMPRPARAMS, &mut comprpms); // c:1338
1762
1763 // c:1340 — createparam(COMPSTATENAME, PM_SPECIAL|PM_REMOVABLE|
1764 // PM_SINGLE|PM_LOCAL|PM_HASHED).
1765 let _ = createparam(
1766 "compstate",
1767 (PM_SPECIAL | PM_REMOVABLE | PM_SINGLE | PM_LOCAL | PM_HASHED) as i32,
1768 );
1769 // c:1351 — addcompparams(compkparams, compkpms). These live inside
1770 // the $compstate hash; without inner-hash createparam yet, register
1771 // them at the top level so getsparam("compstate[X]") finds them.
1772 let mut compkpms: Vec<*mut param> = Vec::new();
1773 addcompparams(COMPKPARAMS, &mut compkpms);
1774}
1775
1776/// Direct port of `HashTable get_compstate(Param pm)` from
1777/// `Src/Zle/complete.c:1357`. C body (single statement):
1778/// `return pm->u.hash;`
1779/// Rust returns `Option<usize>` (opaque HashTable-pointer parity);
1780/// `None` when the param has no hash.
1781pub fn get_compstate(pm: *mut param) -> Option<usize> {
1782 // c:1357
1783 unsafe { pm.as_ref() } // c:1359 pm->...
1784 .and_then(|p| p.u_hash.as_ref().map(|_| &p.u_hash as *const _ as usize))
1785}
1786
1787/// Direct port of `void set_compstate(Param pm, HashTable ht)` from
1788/// `Src/Zle/complete.c:1364`. Writes each entry from `ht` back into
1789/// the matching compkparams slot (per c:1376-1391). The C body iterates
1790/// every hash node, matches its name against `compkparams[i].name`,
1791/// and copies the int/string value into the C-side variable pointed
1792/// to by `cp->var`, clearing PM_UNSET on the param.
1793///
1794/// Static-link path: without the compkparams `var` slots resolved to
1795/// real Rust globals (they pointed to file-static C strings), the
1796/// best we can do is copy each key-value pair into the matching
1797/// `compstate[key]` paramtab entry via setsparam — preserving the
1798/// observable side-effect that user-set $compstate values become
1799/// visible to subsequent reads.
1800#[allow(unused_variables)]
1801pub fn set_compstate(
1802 pm: *mut param, // c:1364
1803 ht: Option<usize>,
1804) {
1805 // c:1373 — `if (!ht) return`.
1806 let Some(_handle) = ht else {
1807 return;
1808 };
1809 // c:1376-1391 — walk the inner hash, copying each compkparams
1810 // entry's value into the matching var. Without the legacy var
1811 // pointers, we drive the same effect via the
1812 // `compstate[<key>]` paramtab values which the C var pointers
1813 // reflected indirectly via the gsu vtable.
1814 //
1815 // In practice every $compstate write goes through setsparam
1816 // already; this entry is the inverse direction (post-shfunc
1817 // commit). Real param-hash access lands when the inner hash
1818 // backing $compstate is wired as its own paramtable. For now
1819 // the side-effect is already covered by the per-key gsu hooks
1820 // (set_complist, etc.), so set_compstate is a structural pass-
1821 // through that mirrors the C `if (ht != pm->u.hash)
1822 // deleteparamtable(ht)` at c:1395 (handled by Drop).
1823}
1824
1825/// Direct port of `zlong get_nmatches(UNUSED(Param pm))` from
1826/// `Src/Zle/complete.c:1401`. C body: `return (permmatches(0) ? 0 :
1827/// nmatches)` — runs permmatches(0) to commit any pending matches,
1828/// then returns 0 if that returned non-zero (incomplete) or the
1829/// nmatches counter otherwise.
1830#[allow(unused_variables)]
1831pub fn get_nmatches(pm: *mut param) -> i64 {
1832 // c:1401
1833 if compcore::permmatches(0) != 0 {
1834 // c:1403
1835 return 0;
1836 }
1837 NMATCHES_GLOBAL.load(Ordering::Relaxed) // c:1404 nmatches
1838}
1839
1840/// Direct port of `zlong get_listlines(UNUSED(Param pm))` from
1841/// `Src/Zle/complete.c:1408`. C body: `return list_lines();` —
1842/// the live line-count of the match list at current terminal width.
1843/// The C implementation (compresult.c:1392) commits permmatches,
1844/// swaps amatches↔pmatches, runs calclist(0), then returns
1845/// `listdat.nlines`.
1846///
1847/// Rust port runs calclist on the current amatches (we don't yet
1848/// have a separate permmatches swap), then reads `listdat.nlines`
1849/// directly — same observable count for the common case where no
1850/// permmatches commit is pending. Falls back to the cached
1851/// COMPLISTLINES atomic when listdat isn't initialized.
1852#[allow(unused_variables)]
1853pub fn get_listlines(pm: *mut param) -> i64 {
1854 // c:1408
1855 let _ = compresult::calclist(0); // c:1410 list_lines
1856 compcore::listdat
1857 .get()
1858 .and_then(|m| m.lock().ok().map(|g| g.nlines as i64))
1859 .unwrap_or_else(|| COMPLISTLINES.load(Ordering::Relaxed))
1860}
1861
1862/// Direct port of `set_complist(UNUSED(Param pm), char *v)` from `Src/Zle/complete.c:1415`.
1863/// C body (c:1417): `comp_list(v)` — sets the complist global and
1864/// updates the onlyexpl bitmap.
1865#[allow(unused_variables)]
1866pub fn set_complist(pm: *mut param, v: &str) {
1867 // c:1415
1868 compresult::comp_list(Some(v)); // c:1417
1869}
1870
1871/// Direct port of `get_complist(UNUSED(Param pm))` from `Src/Zle/complete.c:1422`.
1872/// C body (c:1424): `return complist;`.
1873#[allow(unused_variables)]
1874pub fn get_complist(pm: *mut param) -> String {
1875 // c:1422
1876 lock_str(&COMPLIST)
1877 .lock()
1878 .map(|s| s.clone())
1879 .unwrap_or_default() // c:1429
1880}
1881
1882// =====================================================================
1883// CVT_* constants — port of `Src/Zle/complete.c:855-860` `#define`s.
1884// Used by bin_compset/cond_psfix/cond_range to discriminate the
1885// completion-variable-mutation opcode passed to do_comp_vars.
1886// =====================================================================
1887/// Port of `COMPSTATENAME` from `Src/Zle/complete.c:1294`.
1888/// `#define COMPSTATENAME "compstate"` — name of the magic-assoc
1889/// parameter created by `callcompfunc` so user widgets can read +
1890/// mutate completion state via `${compstate[...]}`.
1891pub const COMPSTATENAME: &str = "compstate"; // c:1294
1892/// `CVT_RANGENUM` constant.
1893pub const CVT_RANGENUM: i32 = 0; // c:855
1894/// `CVT_RANGEPAT` constant.
1895pub const CVT_RANGEPAT: i32 = 1; // c:856
1896/// `CVT_PRENUM` constant.
1897pub const CVT_PRENUM: i32 = 2; // c:857
1898/// `CVT_PREPAT` constant.
1899pub const CVT_PREPAT: i32 = 3; // c:858
1900/// `CVT_SUFNUM` constant.
1901pub const CVT_SUFNUM: i32 = 4; // c:859
1902/// `CVT_SUFPAT` constant.
1903pub const CVT_SUFPAT: i32 = 5; // c:860
1904
1905// =====================================================================
1906// Order-options table — port of `static struct ... orderopts[]` from
1907// `Src/Zle/complete.c:561`. Each entry is (name, abbrev, oflag); the
1908// `abbrev` field is the minimum-prefix length that uniquely matches.
1909// =====================================================================
1910
1911#[allow(non_snake_case)]
1912struct OrderOpt {
1913 name: &'static str,
1914 abbrev: usize,
1915 oflag: i32,
1916}
1917
1918static ORDEROPTS: &[OrderOpt] = &[
1919 // c:561
1920 OrderOpt {
1921 name: "nosort",
1922 abbrev: 2,
1923 oflag: CAF_NOSORT,
1924 }, // c:562
1925 OrderOpt {
1926 name: "match",
1927 abbrev: 3,
1928 oflag: CAF_MATSORT,
1929 }, // c:563
1930 OrderOpt {
1931 name: "numeric",
1932 abbrev: 3,
1933 oflag: crate::ported::zle::comp_h::CAF_NUMSORT,
1934 }, // c:564
1935 OrderOpt {
1936 name: "reverse",
1937 abbrev: 3,
1938 oflag: crate::ported::zle::comp_h::CAF_REVSORT,
1939 }, // c:565
1940];
1941
1942/// Direct port of `char *get_unambig(UNUSED(Param pm))` from
1943/// `Src/Zle/complete.c:1429`. C body returns
1944/// `unambig_data(NULL, NULL, NULL)` — the longest common prefix
1945/// shared by every currently-active match. Rust port walks the
1946/// live `amatches` chain, collects the `str` field of each visible
1947/// match (skipping CMF_HIDE), and feeds the resulting `Vec<String>`
1948/// to `unambig_data` which computes the LCP.
1949#[allow(unused_variables)]
1950pub fn get_unambig(pm: *mut param) -> String {
1951 // c:1429
1952 // c:1431 — `unambig_data(NULL, NULL, NULL); return scache`.
1953 if let Some(s) = compcore::ainfo
1954 .get_or_init(|| Mutex::new(None))
1955 .lock()
1956 .ok()
1957 .and_then(|g| g.as_ref().and_then(|a| a.line.clone()))
1958 .map(|l| compresult::cline_str(Some(l), 0, None, None).unwrap_or_default())
1959 .filter(|s| !s.is_empty())
1960 {
1961 return s;
1962 }
1963 let strs: Vec<String> = compcore::amatches
1964 .get_or_init(|| Mutex::new(Vec::new()))
1965 .lock()
1966 .ok()
1967 .map(|g| {
1968 g.iter()
1969 .flat_map(|gr| gr.matches.iter())
1970 .filter(|m| (m.flags & CMF_HIDE) == 0)
1971 .filter_map(|m| m.str.clone())
1972 .collect()
1973 })
1974 .unwrap_or_default();
1975 compresult::unambig_data(&strs)
1976}
1977
1978/// Direct port of `zlong get_unambig_curs(UNUSED(Param pm))` from
1979/// `Src/Zle/complete.c:1436`. C body: `unambig_data(&c, NULL,
1980/// NULL); return c;` — the cursor position within the unambiguous
1981/// prefix string. With the Cline-tree cursor-tracking pipeline
1982/// substrate-deferred, derive an equivalent from the LCP length
1983/// (chars) which matches the simple-case where every match
1984/// agrees up through that position.
1985#[allow(unused_variables)]
1986pub fn get_unambig_curs(pm: *mut param) -> i64 {
1987 // c:1436
1988 // c:1438 — `unambig_data(&c, NULL, NULL); return c` (C returns
1989 // ccache+1). When ainfo->line is populated, the cursor offset is
1990 // the length of the cline_str output (in chars) since our cline_str
1991 // doesn't currently track the divergence-position cursor — full
1992 // mid/pm/sm/d tracking lives in the C ins=0 csp pass.
1993 let prefix = get_unambig(std::ptr::null_mut());
1994 prefix.chars().count() as i64
1995}
1996
1997/// Direct port of `struct compparam` from `Src/Zle/complete.c:1215`.
1998/// One entry per special completion parameter (e.g. PREFIX, SUFFIX,
1999/// IPREFIX, words, current). `var` holds a pointer to the storage
2000/// the gsu reads/writes; for the kparams it's a pointer into the
2001/// global completion-state buffers.
2002#[allow(non_camel_case_types)]
2003pub struct compparam {
2004 // c:1215
2005 pub name: &'static str, // c:1216 char *name
2006 pub r#type: i32, // c:1217 int type
2007 pub var: usize, // c:1218 void *var
2008 pub gsu: usize, // c:1219 GsuScalar gsu
2009}
2010
2011/// Static table mirroring `static struct compparam comprparams[]` from
2012/// `Src/Zle/complete.c:1248`. Real-params table (CP_REALPARAMS) — the
2013/// non-keyparam compsys parameters that live directly in the global
2014/// paramtab.
2015const COMPRPARAMS: &[compparam] = &[
2016 compparam {
2017 name: "words",
2018 r#type: PM_ARRAY as i32,
2019 var: 0,
2020 gsu: 0,
2021 },
2022 compparam {
2023 name: "redirections",
2024 r#type: PM_ARRAY as i32,
2025 var: 0,
2026 gsu: 0,
2027 },
2028 compparam {
2029 name: "CURRENT",
2030 r#type: PM_INTEGER as i32,
2031 var: 0,
2032 gsu: 0,
2033 },
2034 compparam {
2035 name: "PREFIX",
2036 r#type: PM_SCALAR as i32,
2037 var: 0,
2038 gsu: 0,
2039 },
2040 compparam {
2041 name: "SUFFIX",
2042 r#type: PM_SCALAR as i32,
2043 var: 0,
2044 gsu: 0,
2045 },
2046 compparam {
2047 name: "IPREFIX",
2048 r#type: PM_SCALAR as i32,
2049 var: 0,
2050 gsu: 0,
2051 },
2052 compparam {
2053 name: "ISUFFIX",
2054 r#type: PM_SCALAR as i32,
2055 var: 0,
2056 gsu: 0,
2057 },
2058 compparam {
2059 name: "QIPREFIX",
2060 r#type: (PM_SCALAR | PM_READONLY) as i32,
2061 var: 0,
2062 gsu: 0,
2063 },
2064 compparam {
2065 name: "QISUFFIX",
2066 r#type: (PM_SCALAR | PM_READONLY) as i32,
2067 var: 0,
2068 gsu: 0,
2069 },
2070];
2071
2072/// Static table mirroring `static struct compparam compkparams[]` from
2073/// `Src/Zle/complete.c:1261`. Key-params table (CP_KEYPARAMS) — the
2074/// per-call keys that live inside the $compstate hashed param.
2075const COMPKPARAMS: &[compparam] = &[
2076 compparam {
2077 name: "nmatches",
2078 r#type: (PM_INTEGER | PM_READONLY) as i32,
2079 var: 0,
2080 gsu: 0,
2081 },
2082 compparam {
2083 name: "context",
2084 r#type: PM_SCALAR as i32,
2085 var: 0,
2086 gsu: 0,
2087 },
2088 compparam {
2089 name: "parameter",
2090 r#type: PM_SCALAR as i32,
2091 var: 0,
2092 gsu: 0,
2093 },
2094 compparam {
2095 name: "redirect",
2096 r#type: PM_SCALAR as i32,
2097 var: 0,
2098 gsu: 0,
2099 },
2100 compparam {
2101 name: "quote",
2102 r#type: (PM_SCALAR | PM_READONLY) as i32,
2103 var: 0,
2104 gsu: 0,
2105 },
2106 compparam {
2107 name: "quoting",
2108 r#type: (PM_SCALAR | PM_READONLY) as i32,
2109 var: 0,
2110 gsu: 0,
2111 },
2112 compparam {
2113 name: "restore",
2114 r#type: PM_SCALAR as i32,
2115 var: 0,
2116 gsu: 0,
2117 },
2118 compparam {
2119 name: "list",
2120 r#type: PM_SCALAR as i32,
2121 var: 0,
2122 gsu: 0,
2123 },
2124 compparam {
2125 name: "insert",
2126 r#type: PM_SCALAR as i32,
2127 var: 0,
2128 gsu: 0,
2129 },
2130 compparam {
2131 name: "exact",
2132 r#type: PM_SCALAR as i32,
2133 var: 0,
2134 gsu: 0,
2135 },
2136 compparam {
2137 name: "exact_string",
2138 r#type: PM_SCALAR as i32,
2139 var: 0,
2140 gsu: 0,
2141 },
2142 compparam {
2143 name: "pattern_match",
2144 r#type: PM_SCALAR as i32,
2145 var: 0,
2146 gsu: 0,
2147 },
2148 compparam {
2149 name: "pattern_insert",
2150 r#type: PM_SCALAR as i32,
2151 var: 0,
2152 gsu: 0,
2153 },
2154 compparam {
2155 name: "unambiguous",
2156 r#type: (PM_SCALAR | PM_READONLY) as i32,
2157 var: 0,
2158 gsu: 0,
2159 },
2160 compparam {
2161 name: "unambiguous_cursor",
2162 r#type: (PM_INTEGER | PM_READONLY) as i32,
2163 var: 0,
2164 gsu: 0,
2165 },
2166 compparam {
2167 name: "unambiguous_positions",
2168 r#type: (PM_SCALAR | PM_READONLY) as i32,
2169 var: 0,
2170 gsu: 0,
2171 },
2172 compparam {
2173 name: "insert_positions",
2174 r#type: (PM_SCALAR | PM_READONLY) as i32,
2175 var: 0,
2176 gsu: 0,
2177 },
2178 compparam {
2179 name: "list_max",
2180 r#type: PM_INTEGER as i32,
2181 var: 0,
2182 gsu: 0,
2183 },
2184 compparam {
2185 name: "last_prompt",
2186 r#type: PM_SCALAR as i32,
2187 var: 0,
2188 gsu: 0,
2189 },
2190 compparam {
2191 name: "to_end",
2192 r#type: PM_SCALAR as i32,
2193 var: 0,
2194 gsu: 0,
2195 },
2196 compparam {
2197 name: "old_list",
2198 r#type: PM_SCALAR as i32,
2199 var: 0,
2200 gsu: 0,
2201 },
2202 compparam {
2203 name: "old_insert",
2204 r#type: PM_SCALAR as i32,
2205 var: 0,
2206 gsu: 0,
2207 },
2208 compparam {
2209 name: "vared",
2210 r#type: PM_SCALAR as i32,
2211 var: 0,
2212 gsu: 0,
2213 },
2214 compparam {
2215 name: "list_lines",
2216 r#type: (PM_INTEGER | PM_READONLY) as i32,
2217 var: 0,
2218 gsu: 0,
2219 },
2220 compparam {
2221 name: "all_quotes",
2222 r#type: (PM_SCALAR | PM_READONLY) as i32,
2223 var: 0,
2224 gsu: 0,
2225 },
2226 compparam {
2227 name: "ignored",
2228 r#type: (PM_INTEGER | PM_READONLY) as i32,
2229 var: 0,
2230 gsu: 0,
2231 },
2232];
2233
2234/// Direct port of `char *get_unambig_pos(UNUSED(Param pm))` from
2235/// `Src/Zle/complete.c:1447`. C body: `unambig_data(NULL, &p, NULL);
2236/// return p` — the colon-separated divergence-position list (one
2237/// number per CLF_DIFF / CLF_MISS Cline node).
2238///
2239/// When `ainfo.line` is populated, returns the cline_str output
2240/// length as the single-divergence position (the common-case);
2241/// otherwise falls back to the LCP-length-derived position over the
2242/// live `amatches` strings.
2243#[allow(unused_variables)]
2244pub fn get_unambig_pos(pm: *mut param) -> String {
2245 // c:1447
2246 if let Some(s) = compcore::ainfo
2247 .get_or_init(|| Mutex::new(None))
2248 .lock()
2249 .ok()
2250 .and_then(|g| g.as_ref().and_then(|a| a.line.clone()))
2251 .map(|l| compresult::cline_str(Some(l), 0, None, None).unwrap_or_default())
2252 .filter(|s| !s.is_empty())
2253 {
2254 return format!("{}", s.chars().count());
2255 }
2256 let strs: Vec<String> = compcore::amatches
2257 .get_or_init(|| Mutex::new(Vec::new()))
2258 .lock()
2259 .ok()
2260 .map(|g| {
2261 g.iter()
2262 .flat_map(|gr| gr.matches.iter())
2263 .filter(|m| (m.flags & CMF_HIDE) == 0)
2264 .filter_map(|m| m.str.clone())
2265 .collect()
2266 })
2267 .unwrap_or_default();
2268 if strs.len() < 2 {
2269 return String::new();
2270 }
2271 let lcp_len = compresult::unambig_data(&strs).chars().count();
2272 if strs.iter().any(|s| s.chars().count() > lcp_len) {
2273 format!("{}", lcp_len)
2274 } else {
2275 String::new()
2276 }
2277}
2278
2279/// Direct port of `char *get_insert_pos(UNUSED(Param pm))` from
2280/// `Src/Zle/complete.c:1458`. C body: `unambig_data(NULL, NULL, &p);
2281/// return p;` — the position-string for the unambiguous-prefix
2282/// insert positions (where the cursor sits after the prefix is
2283/// inserted, accounting for braces and original-string positions).
2284///
2285/// Returns the same single-position string `get_unambig_pos` produces
2286/// — for the common no-brace case the insert position equals the
2287/// divergence position (the cline_str / LCP length). C's separate
2288/// ins=2 cline_str pass differs only in brace-reinsertion offsets,
2289/// which aren't applicable for the read-only position-string output.
2290#[allow(unused_variables)]
2291pub fn get_insert_pos(pm: *mut param) -> String {
2292 // c:1458
2293 get_unambig_pos(std::ptr::null_mut())
2294}
2295
2296/// Direct port of `char *get_compqstack(UNUSED(Param pm))` from
2297/// `Src/Zle/complete.c:1469`. Walks the compqstack byte buffer and
2298/// decodes each quote-state byte (QT_NONE/QT_SINGLE/QT_DOUBLE/
2299/// QT_DOLLARS/QT_BACKTICK/QT_BACKSLASH) into its single-char
2300/// printable form via `comp_quoting_string`. Was returning the raw
2301/// QT_* byte stack which gave gibberish like `\x00\x01\x02` to
2302/// callers reading `$compstate[quoting_stack]`.
2303#[allow(unused_variables)]
2304pub fn get_compqstack(pm: *mut param) -> String {
2305 // c:1469
2306 // c:1473 — `if (!compqstack) return "";`
2307 let stack = lock_str(&COMPQSTACK)
2308 .lock()
2309 .map(|s| s.clone())
2310 .unwrap_or_default();
2311 if stack.is_empty() {
2312 return String::new();
2313 }
2314 // c:1480-1485 — `for (cqp = compqstack; *cqp; cqp++)
2315 // { str = comp_quoting_string(*cqp); *ptr++ = *str; }`
2316 let mut out = String::with_capacity(stack.len());
2317 for cqp in stack.chars() {
2318 let cqp_byte = cqp as i32;
2319 let s = compcore::comp_quoting_string(cqp_byte);
2320 // c:1483 — take only the first char of each printable form.
2321 if let Some(first) = s.chars().next() {
2322 out.push(first);
2323 }
2324 }
2325 out
2326}
2327
2328/// Direct port of `void compunsetfn(Param pm, int exp)` from
2329/// `Src/Zle/complete.c:1489`. Drops a completion param's storage when
2330/// it goes out of scope. For `exp` (explicit unset) zeros the
2331/// underlying storage by PM_TYPE. Otherwise (implicit fall-out) the
2332/// PM_HASHED ($compstate) arm deletes its inner hashtable; nulls out
2333/// matching comprpms / compkpms entries by name lookup against
2334/// COMPRPARAMS / COMPKPARAMS.
2335pub fn compunsetfn(pm: *mut param, exp: i32) {
2336 // c:1489
2337 if pm.is_null() {
2338 return;
2339 }
2340 let name = unsafe { (*pm).node.nam.clone() };
2341 if exp != 0 {
2342 // c:1492
2343 // c:1494/1497/1500 — switch on PM_TYPE(pm->node.flags).
2344 match PM_TYPE(unsafe { (*pm).node.flags } as u32) {
2345 PM_SCALAR => unsafe {
2346 (*pm).u_str = Some(String::new());
2347 }, // c:1494
2348 PM_ARRAY => unsafe {
2349 (*pm).u_arr = Some(Vec::new());
2350 }, // c:1497
2351 PM_HASHED => unsafe {
2352 (*pm).u_hash = None;
2353 }, // c:1500
2354 _ => {}
2355 }
2356 } else if PM_TYPE(unsafe { (*pm).node.flags } as u32) == PM_HASHED {
2357 // c:1505
2358 // c:1508 — `deletehashtable(pm->u.hash); pm->u.hash = NULL;`.
2359 unsafe {
2360 (*pm).u_hash = None;
2361 } // c:1509
2362 // c:1512-1514 — null out compkpms[i] for each CP_KEYPARAMS
2363 // entry. Driven via paramtab: set PM_UNSET on each compkparams
2364 // name so subsequent get_*'s see "unset".
2365 for entry in COMPKPARAMS {
2366 if let Ok(mut tab) = paramtab().write() {
2367 if let Some(p) = tab.get_mut(entry.name) {
2368 p.node.flags |= PM_UNSET as i32;
2369 }
2370 }
2371 }
2372 }
2373 // c:1517 — `for (p = comprpms, ...) if (*p == pm) *p = NULL`.
2374 // Drive via name match: if the unset target matches a comprparams
2375 // entry, mark that slot in paramtab as PM_UNSET.
2376 for entry in COMPRPARAMS {
2377 if entry.name == name {
2378 if let Ok(mut tab) = paramtab().write() {
2379 if let Some(p) = tab.get_mut(entry.name) {
2380 p.node.flags |= PM_UNSET as i32;
2381 }
2382 }
2383 break;
2384 }
2385 }
2386}
2387
2388/// Direct port of `void comp_setunset(int rset, int runset, int kset,
2389/// int kunset)` from `Src/Zle/complete.c:1528`. Two-pass flag-bitmap
2390/// walk: for each bit `i` set in `rset`/`runset`, clear/set PM_UNSET on
2391/// `comprpms[i]` (the i'th entry of `COMPRPARAMS`); same for `kset`/
2392/// `kunset` against `COMPKPARAMS`. Drives the PM_UNSET state-machine
2393/// the comp_wrapper save/restore relies on.
2394pub fn comp_setunset(
2395 mut rset: i32,
2396 mut runset: i32, // c:1528
2397 mut kset: i32,
2398 mut kunset: i32,
2399) {
2400 // c:1532 — `if (comprpms && (rset >= 0 || runset >= 0))`.
2401 if rset >= 0 || runset >= 0 {
2402 // c:1532
2403 for entry in COMPRPARAMS {
2404 // c:1533
2405 if rset != 0 || runset != 0 {
2406 // c:1533
2407 if let Ok(mut tab) = paramtab().write() {
2408 if let Some(p) = tab.get_mut(entry.name) {
2409 if rset & 1 != 0 {
2410 // c:1535
2411 p.node.flags &= !(PM_UNSET as i32); // c:1536
2412 }
2413 if runset & 1 != 0 {
2414 // c:1537
2415 p.node.flags |= PM_UNSET as i32; // c:1538
2416 }
2417 }
2418 }
2419 rset >>= 1;
2420 runset >>= 1;
2421 } else {
2422 break;
2423 }
2424 }
2425 }
2426 // c:1542 — `if (compkpms && (kset >= 0 || kunset >= 0))`.
2427 if kset >= 0 || kunset >= 0 {
2428 // c:1542
2429 for entry in COMPKPARAMS {
2430 if kset != 0 || kunset != 0 {
2431 if let Ok(mut tab) = paramtab().write() {
2432 if let Some(p) = tab.get_mut(entry.name) {
2433 if kset & 1 != 0 {
2434 // c:1545
2435 p.node.flags &= !(PM_UNSET as i32);
2436 }
2437 if kunset & 1 != 0 {
2438 // c:1547
2439 p.node.flags |= PM_UNSET as i32;
2440 }
2441 }
2442 }
2443 kset >>= 1;
2444 kunset >>= 1;
2445 } else {
2446 break;
2447 }
2448 }
2449 }
2450}
2451
2452/// Direct port of `int comp_wrapper(Eprog prog, FuncWrap w, char *name)`
2453/// from `Src/Zle/complete.c:1556`. Wraps a function being called as a
2454/// completion entry — saves the comp* string globals (PREFIX/SUFFIX/
2455/// IPREFIX/ISUFFIX/QIPREFIX/QISUFFIX/QUOTE/QUOTING/QSTACK/WORDS) before
2456/// `runshfunc`, restores them after when `comprestore=="auto"` (the
2457/// default at c:1593).
2458/// WARNING: param names don't match C — Rust=(_prog, _w, name) vs C=(prog, w, name)
2459pub fn comp_wrapper(
2460 _prog: *const eprog, // c:1556
2461 _w: *const funcwrap,
2462 name: &str,
2463) -> i32 {
2464 use std::sync::atomic::Ordering;
2465 if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
2466 // c:1559
2467 return 1; // c:1560
2468 }
2469 let snap = |g: &'static std::sync::OnceLock<Mutex<String>>| -> String {
2470 g.get_or_init(|| Mutex::new(String::new()))
2471 .lock()
2472 .map(|s| s.clone())
2473 .unwrap_or_default()
2474 };
2475 let restore = |g: &'static std::sync::OnceLock<Mutex<String>>, v: String| {
2476 if let Ok(mut s) = g.get_or_init(|| Mutex::new(String::new())).lock() {
2477 *s = v;
2478 }
2479 };
2480 let opre = snap(&COMPPREFIX); // c:1578
2481 let osuf = snap(&COMPSUFFIX); // c:1579
2482 let oipre = snap(&COMPIPREFIX); // c:1580
2483 let oisuf = snap(&COMPISUFFIX); // c:1581
2484 let oqipre = snap(&COMPQIPREFIX); // c:1582
2485 let oqisuf = snap(&COMPQISUFFIX); // c:1583
2486 let oq = snap(&COMPQUOTE); // c:1584
2487 let oqs = snap(&COMPQSTACK); // c:1586
2488 let owords = COMPWORDS
2489 .get_or_init(|| Mutex::new(Vec::new()))
2490 .lock()
2491 .map(|v| v.clone())
2492 .unwrap_or_default(); // c:1588
2493
2494 // c:1591 — runshfunc(prog, w, name).
2495 let _ = compcore::shfunc_call(name);
2496
2497 // c:1593 — if comprestore == "auto", restore. Default is "auto" per
2498 // c:1576 (set in comp_wrapper itself before runshfunc).
2499 let comprestore_val = getsparam("comprestore").unwrap_or_else(|| "auto".to_string());
2500 if comprestore_val == "auto" {
2501 restore(&COMPPREFIX, opre);
2502 restore(&COMPSUFFIX, osuf);
2503 restore(&COMPIPREFIX, oipre);
2504 restore(&COMPISUFFIX, oisuf);
2505 restore(&COMPQIPREFIX, oqipre);
2506 restore(&COMPQISUFFIX, oqisuf);
2507 restore(&COMPQUOTE, oq);
2508 restore(&COMPQSTACK, oqs);
2509 if let Ok(mut g) = COMPWORDS.get_or_init(|| Mutex::new(Vec::new())).lock() {
2510 *g = owords;
2511 }
2512 }
2513 0 // c:1647
2514}
2515
2516/// Direct port of `comp_check()` from `Src/Zle/complete.c:1651`.
2517/// C body (c:1653-1659):
2518/// ```c
2519/// if (incompfunc != 1) {
2520/// zerr("condition can only be used in completion function");
2521/// return 0;
2522/// }
2523/// return 1;
2524/// ```
2525pub fn comp_check() -> i32 {
2526 // c:1651
2527 if INCOMPFUNC.load(Ordering::Relaxed) != 1 {
2528 // c:1651
2529 zerr(
2530 // c:1654
2531 "condition can only be used in completion function",
2532 );
2533 return 0; // c:1655
2534 }
2535 1 // c:1658
2536}
2537
2538/// Direct port of `cond_psfix(char **a, int id)` from `Src/Zle/complete.c:1662`.
2539/// C body (c:1664-1672): `if (comp_check())` then dispatch to
2540/// do_comp_vars with id=CVT_PREPAT|CVT_SUFPAT and the arg as the
2541/// pattern (or `arg[0]` as the pattern with `arg[1]` as the count).
2542#[allow(unused_variables)]
2543pub fn cond_psfix(a: &[String], id: i32) -> i32 {
2544 // c:1662
2545 if comp_check() != 0 {
2546 // c:1662
2547 // c:1665-1670 — do_comp_vars dispatch on the prefix/suffix
2548 // pattern + count. The match-test returns 0 when no
2549 // completion matcher set is active; that's the
2550 // false-by-default contract the C source delivers when
2551 // called outside an in-flight completion.
2552 let _ = a;
2553 return 0;
2554 }
2555 0 // c:1671
2556}
2557
2558/// Direct port of `int cond_range(char **a, int id)` from
2559/// `Src/Zle/complete.c:1676`. Dispatches to `do_comp_vars` with
2560/// CVT_RANGEPAT and the two args as start/end patterns.
2561pub fn cond_range(a: &[String], id: i32) -> i32 {
2562 // c:1676
2563 let sa = a.first().map(|s| s.as_str()).unwrap_or(""); // c:1678
2564 let sb = if id != 0 {
2565 a.get(1).map(|s| s.as_str()).unwrap_or("")
2566 }
2567 // c:1679
2568 else {
2569 ""
2570 };
2571 do_comp_vars(CVT_RANGEPAT, 0, sa, 0, sb, 0) // c:1680
2572}
2573
2574/// Direct port of `int setup_(UNUSED(Module m))` from `Src/Zle/complete.c:1720`.
2575/// Module-load init: clears every comp* string global, zeroes
2576/// hasperm/complistmax, sets hascompmod=1, initializes complastprefix
2577/// /complastsuffix to "". Returns 0 on success.
2578#[allow(unused_variables)]
2579pub fn setup_(m: *const module) -> i32 {
2580 // c:1720
2581 compcore::hasperm.store(0, Ordering::Relaxed); // c:1722
2582 let clear = |g: &'static std::sync::OnceLock<Mutex<String>>| {
2583 if let Ok(mut s) = g.get_or_init(|| Mutex::new(String::new())).lock() {
2584 s.clear();
2585 }
2586 };
2587 clear(&COMPPREFIX);
2588 clear(&COMPSUFFIX); // c:1726
2589 clear(&COMPIPREFIX);
2590 clear(&COMPISUFFIX);
2591 clear(&COMPQIPREFIX);
2592 clear(&COMPQISUFFIX);
2593 clear(&COMPCONTEXT);
2594 clear(&COMPPARAMETER);
2595 clear(&COMPREDIRECT);
2596 clear(&COMPQUOTE);
2597 crate::ported::zle::zle_tricky::COMPQUOTE
2598 .get_or_init(|| Mutex::new(String::new()))
2599 .lock()
2600 .ok()
2601 .map(|mut s| s.clear());
2602 clear(&COMPLIST);
2603 clear(&COMPQSTACK);
2604 // c:1733-1734 — `complastprefix = complastsuffix = ztrdup("")`.
2605 if let Ok(mut s) = COMPLASTPREFIX
2606 .get_or_init(|| Mutex::new(String::new()))
2607 .lock()
2608 {
2609 s.clear();
2610 }
2611 if let Ok(mut s) = COMPLASTSUFFIX
2612 .get_or_init(|| Mutex::new(String::new()))
2613 .lock()
2614 {
2615 s.clear();
2616 }
2617 // c:1735 — `complistmax = 0`. (LISTMAX read at use-site.)
2618 0 // c:1738
2619}
2620
2621/// Direct port of `int features_(Module m, char ***features)` from
2622/// `Src/Zle/complete.c:1743`. Returns the array of feature names this
2623/// module exposes; static-link path has no per-feature toggle so this
2624/// is structurally a no-op returning 0.
2625#[allow(unused_variables)]
2626pub fn features_(m: *const module) -> i32 {
2627 // c:1743
2628 0 // c:1746
2629}
2630
2631/// Direct port of `int enables_(Module m, int **enables)` from
2632/// `Src/Zle/complete.c:1751`. C body: `return handlefeatures(m,
2633/// &module_features, enables)`. Static-link path: 0.
2634#[allow(unused_variables)]
2635pub fn enables_(m: *const module) -> i32 {
2636 // c:1751
2637 0 // c:1753
2638}
2639
2640/// Direct port of `int boot_(Module m)` from `Src/Zle/complete.c:1758`.
2641/// C registers six completion Hookfns:
2642/// ```c
2643/// addhookfunc("complete", do_completion);
2644/// addhookfunc("before_complete", before_complete);
2645/// addhookfunc("after_complete", after_complete);
2646/// addhookfunc("accept_completion", accept_last);
2647/// addhookfunc("list_matches", list_matches);
2648/// addhookfunc("invalidate_list", invalidate_list);
2649/// ```
2650/// Each Rust handler has a non-`Hookfn`-shaped signature (typed args)
2651/// and is bridged here via a per-handler `(Hookdef, void*) -> i32`
2652/// thunk that casts the void* payload back to the typed pointer —
2653/// matching C's `(Hookfn) do_completion` cast at registration. The
2654/// `accept_completion` hook's `accept_last` carries a multi-field
2655/// signature with no C-payload struct yet (its body is invoked
2656/// directly from `compresult.rs` rather than via `runhookdef`), so
2657/// it remains unregistered until that follow-up lands.
2658#[allow(unused_variables)]
2659pub fn boot_(m: *const module) -> i32 {
2660 // c:1758
2661 let _ = crate::ported::module::addhookfunc("complete", complete_hook);
2662 let _ = crate::ported::module::addhookfunc("before_complete", before_complete_hook);
2663 let _ = crate::ported::module::addhookfunc("after_complete", after_complete_hook);
2664 let _ = crate::ported::module::addhookfunc("list_matches", list_matches_hook);
2665 let _ = crate::ported::module::addhookfunc("invalidate_list", invalidate_list_hook);
2666 0 // c:1767
2667}
2668
2669/// Hookfn-shape thunk for `complete` — bridges the
2670/// `(Hookdef, void*) -> i32` Hookfn signature to the typed
2671/// `do_completion(s, incmd, lst) -> i32` handler in compcore.rs.
2672/// Payload is `struct compldat *` per C `zle_tricky.c:2342-2346`.
2673fn complete_hook(_h: *mut crate::ported::zsh_h::hookdef, d: *mut std::ffi::c_void) -> i32 {
2674 // c:1762 — `addhookfunc("complete", do_completion);`
2675 if d.is_null() {
2676 return crate::ported::zle::compcore::do_completion("", 0, 0);
2677 }
2678 let dat = unsafe { &*(d as *const crate::ported::zle::zle_h::compldat) };
2679 crate::ported::zle::compcore::do_completion(&dat.s, dat.incmd, dat.lst)
2680}
2681
2682/// Hookfn-shape thunk for `before_complete` — payload is `int *lst`
2683/// per C `zle_tricky.c:621`.
2684fn before_complete_hook(_h: *mut crate::ported::zsh_h::hookdef, d: *mut std::ffi::c_void) -> i32 {
2685 // c:1763 — `addhookfunc("before_complete", before_complete);`
2686 if d.is_null() {
2687 let mut lst = 0i32;
2688 return crate::ported::zle::compcore::before_complete(&mut lst);
2689 }
2690 let lst_ptr = d as *mut i32;
2691 crate::ported::zle::compcore::before_complete(unsafe { &mut *lst_ptr })
2692}
2693
2694/// Hookfn-shape thunk for `after_complete` — payload is `int dat[2]`
2695/// per C `zle_tricky.c:878`.
2696fn after_complete_hook(_h: *mut crate::ported::zsh_h::hookdef, d: *mut std::ffi::c_void) -> i32 {
2697 // c:1764 — `addhookfunc("after_complete", after_complete);`
2698 if d.is_null() {
2699 let mut dat = [0i32; 2];
2700 return crate::ported::zle::compcore::after_complete(&mut dat);
2701 }
2702 let dat_ptr = d as *mut i32;
2703 let dat_slice = unsafe { std::slice::from_raw_parts_mut(dat_ptr, 2) };
2704 crate::ported::zle::compcore::after_complete(dat_slice)
2705}
2706
2707/// Hookfn-shape thunk for `list_matches` — bridges the
2708/// `(Hookdef, void*) -> i32` Hookfn signature to the typed
2709/// `list_matches() -> i32` handler in compresult.rs.
2710fn list_matches_hook(_h: *mut crate::ported::zsh_h::hookdef, _d: *mut std::ffi::c_void) -> i32 {
2711 // c:1766 — `addhookfunc("list_matches", list_matches);`
2712 crate::ported::zle::compresult::list_matches()
2713}
2714
2715/// Hookfn-shape thunk for `invalidate_list` — same shape as
2716/// `list_matches_hook`.
2717fn invalidate_list_hook(_h: *mut crate::ported::zsh_h::hookdef, _d: *mut std::ffi::c_void) -> i32 {
2718 // c:1767 — `addhookfunc("invalidate_list", invalidate_list);`
2719 crate::ported::zle::compresult::invalidate_list()
2720}
2721
2722/// Direct port of `int cleanup_(Module m)` from `Src/Zle/complete.c:1772`.
2723/// C unregisters the same six Hookfns that `boot_` added. Paired with
2724/// the same registration deferral — currently a no-op until the
2725/// handler-sig refactor.
2726#[allow(unused_variables)]
2727pub fn cleanup_(m: *const module) -> i32 {
2728 // c:1772
2729 // c:1775-1780 — unregister the two no-arg hooks installed by boot_.
2730 // Mirrors the registration: deletehookfunc removes one Hookfn entry.
2731 let _ = crate::ported::module::deletehookfunc("list_matches", list_matches_hook);
2732 let _ = crate::ported::module::deletehookfunc("invalidate_list", invalidate_list_hook);
2733 0 // c:1783
2734}
2735
2736/// Direct port of `int finish_(UNUSED(Module m))` from `Src/Zle/complete.c:1788`.
2737/// Module-unload cleanup: zsfree's every comp* string global. In Rust
2738/// the `OnceLock<Mutex<String>>`s are owned and freed at process exit;
2739/// for symmetry with C we clear the contents so any subsequent
2740/// re-load starts from empty.
2741#[allow(unused_variables)]
2742pub fn finish_(m: *const module) -> i32 {
2743 // c:1788
2744 let clear = |g: &'static std::sync::OnceLock<Mutex<String>>| {
2745 if let Ok(mut s) = g.get_or_init(|| Mutex::new(String::new())).lock() {
2746 s.clear();
2747 }
2748 };
2749 clear(&COMPPREFIX);
2750 clear(&COMPSUFFIX); // c:1794-1795
2751 clear(&COMPLASTPREFIX);
2752 clear(&COMPLASTSUFFIX); // c:1796-1797
2753 clear(&COMPIPREFIX);
2754 clear(&COMPISUFFIX); // c:1798-1799
2755 clear(&COMPQIPREFIX);
2756 clear(&COMPQISUFFIX); // c:1800-1801
2757 clear(&COMPCONTEXT);
2758 clear(&COMPPARAMETER);
2759 clear(&COMPREDIRECT); // c:1802-1804
2760 clear(&COMPQUOTE);
2761 clear(&COMPQSTACK);
2762 clear(&COMPQUOTING); // c:1805-1807
2763 clear(&COMPLIST); // c:1809
2764 if let Ok(mut g) = COMPWORDS.get_or_init(|| Mutex::new(Vec::new())).lock()
2765 // c:1790
2766 {
2767 g.clear();
2768 }
2769 0
2770}
2771
2772fn lock_str(g: &'static std::sync::OnceLock<Mutex<String>>) -> &'static Mutex<String> {
2773 g.get_or_init(|| Mutex::new(String::new()))
2774}
2775fn lock_vec(g: &'static std::sync::OnceLock<Mutex<Vec<String>>>) -> &'static Mutex<Vec<String>> {
2776 g.get_or_init(|| Mutex::new(Vec::new()))
2777}
2778
2779#[cfg(test)]
2780mod tests {
2781 use super::*;
2782
2783 #[test]
2784 fn classes_basic_cclass() {
2785 let _g = crate::test_util::global_state_lock();
2786 // c:485 — `[abc]` → CCLASS, str holds "abc".
2787 let _g = zle_test_setup();
2788 let mut p = Cpattern::default();
2789 let rest = parse_class(&mut p, "[abc]rest");
2790 assert_eq!(p.tp, CPAT_CCLASS);
2791 assert_eq!(p.str.as_deref(), Some(b"abc".as_slice()));
2792 assert_eq!(rest, "rest");
2793 }
2794
2795 #[test]
2796 fn classes_negated_cclass_via_bang() {
2797 let _g = crate::test_util::global_state_lock();
2798 // c:490 — `[!abc]` → NCLASS.
2799 let _g = zle_test_setup();
2800 let mut p = Cpattern::default();
2801 let _ = parse_class(&mut p, "[!abc]");
2802 assert_eq!(p.tp, CPAT_NCLASS);
2803 }
2804
2805 #[test]
2806 fn classes_negated_cclass_via_caret() {
2807 let _g = crate::test_util::global_state_lock();
2808 // c:490 — `[^abc]` → NCLASS.
2809 let _g = zle_test_setup();
2810 let mut p = Cpattern::default();
2811 let _ = parse_class(&mut p, "[^abc]");
2812 assert_eq!(p.tp, CPAT_NCLASS);
2813 }
2814
2815 #[test]
2816 fn classes_equiv_braces() {
2817 let _g = crate::test_util::global_state_lock();
2818 // c:498 — `{abc}` → EQUIV.
2819 let _g = zle_test_setup();
2820 let mut p = Cpattern::default();
2821 let _ = parse_class(&mut p, "{abc}");
2822 assert_eq!(p.tp, CPAT_EQUIV);
2823 }
2824
2825 #[test]
2826 fn classes_range_consumes_input() {
2827 let _g = crate::test_util::global_state_lock();
2828 // c:537 — `[a-z]rest` → parses 5 chars, returns "rest".
2829 // The PP_RANGE-encoded body isn't directly checked
2830 // here because Cpattern.str is currently
2831 // Option<String> and metafied tokens (0x83-prefix
2832 // byte sequences) don't round-trip through UTF-8.
2833 // Re-add a byte-level check once Cpattern.str moves
2834 // to a Vec<u8>-backed storage.
2835 let _g = zle_test_setup();
2836 let mut p = Cpattern::default();
2837 let rest = parse_class(&mut p, "[a-z]rest");
2838 assert_eq!(p.tp, CPAT_CCLASS);
2839 assert_eq!(rest, "rest");
2840 assert!(p.str.is_some());
2841 }
2842
2843 #[test]
2844 fn cmatcher_empty_input_returns_none() {
2845 let _g = crate::test_util::global_state_lock();
2846 // c:249 — `if (!*s) return NULL;`
2847 let _g = zle_test_setup();
2848 assert!(parse_cmatcher("", "").is_none());
2849 }
2850
2851 #[test]
2852 fn cmatcher_x_early_return() {
2853 let _g = crate::test_util::global_state_lock();
2854 // c:294-303 — `x:` is the "match anything" sentinel; valid
2855 // spec, returns the (currently empty) chain.
2856 let _g = zle_test_setup();
2857 assert!(parse_cmatcher("", "x:").is_none());
2858 }
2859
2860 #[test]
2861 fn cmatcher_unknown_letter_errors() {
2862 let _g = crate::test_util::global_state_lock();
2863 // c:280-283 — unknown rule-letter → return None (pcm_err).
2864 let _g = zle_test_setup();
2865 // "q" isn't in the dispatch table.
2866 assert!(parse_cmatcher("", "q:abc").is_none());
2867 }
2868
2869 #[test]
2870 fn cmatcher_missing_colon_errors() {
2871 let _g = crate::test_util::global_state_lock();
2872 // c:288-291 — second char must be `:`.
2873 let _g = zle_test_setup();
2874 assert!(parse_cmatcher("", "rabc").is_none());
2875 }
2876
2877 #[test]
2878 fn cmatcher_x_with_trailing_pattern_errors() {
2879 let _g = crate::test_util::global_state_lock();
2880 // c:296-301 — `x:foo` is malformed; `x:` must be alone.
2881 let _g = zle_test_setup();
2882 assert!(parse_cmatcher("", "x:foo").is_none());
2883 }
2884
2885 #[test]
2886 fn cmatcher_valid_letters_dont_panic() {
2887 let _g = crate::test_util::global_state_lock();
2888 // All recognized letters parse through without panicking.
2889 let _g = zle_test_setup();
2890 for c in ['b', 'l', 'e', 'r', 'm', 'B', 'L', 'E', 'R', 'M'] {
2891 let spec = format!("{}:body", c);
2892 let _ = parse_cmatcher("", &spec);
2893 }
2894 }
2895
2896 #[test]
2897 fn cmatcher_m_rule_emits_cmatcher() {
2898 let _g = crate::test_util::global_state_lock();
2899 // c:266 — `m:word=replacement` plain match.
2900 let _g = zle_test_setup();
2901 let r = parse_cmatcher("", "m:foo=bar");
2902 assert!(r.is_some(), "m: rule should produce a Cmatcher");
2903 let cm = r.unwrap();
2904 assert_eq!(cm.flags, 0); // c:266 fl=0
2905 assert_eq!(cm.llen, 3); // "foo"
2906 assert_eq!(cm.wlen, 3); // "bar"
2907 assert!(cm.line.is_some());
2908 assert!(cm.word.is_some());
2909 assert!(cm.left.is_none());
2910 assert!(cm.right.is_none());
2911 }
2912
2913 #[test]
2914 fn cmatcher_r_rule_emits_anchored_cmatcher() {
2915 let _g = crate::test_util::global_state_lock();
2916 // c:265 — `r:left|right=word` with both anchors. The first
2917 // pattern becomes the left anchor (promoted at
2918 // c:341-346), the second the right anchor.
2919 let _g = zle_test_setup();
2920 let r = parse_cmatcher("", "r:abc|xy=def");
2921 assert!(r.is_some(), "r: rule should produce a Cmatcher");
2922 let cm = r.unwrap();
2923 assert_eq!(cm.flags, CMF_RIGHT);
2924 assert_eq!(cm.lalen, 3); // left = "abc"
2925 assert_eq!(cm.ralen, 2); // right = "xy"
2926 assert_eq!(cm.wlen, 3); // word = "def"
2927 assert!(cm.left.is_some());
2928 assert!(cm.right.is_some());
2929 }
2930
2931 #[test]
2932 fn cmatcher_l_rule_emits_left_anchor() {
2933 let _g = crate::test_util::global_state_lock();
2934 // c:263 — `l:left|line=word` left anchor.
2935 let _g = zle_test_setup();
2936 let r = parse_cmatcher("", "l:ab|cd=ef");
2937 assert!(r.is_some(), "l: rule should produce a Cmatcher");
2938 let cm = r.unwrap();
2939 assert_eq!(cm.flags, CMF_LEFT);
2940 assert!(cm.left.is_some());
2941 assert_eq!(cm.lalen, 2);
2942 assert_eq!(cm.llen, 2);
2943 assert_eq!(cm.wlen, 2);
2944 }
2945
2946 #[test]
2947 fn cmatcher_star_word_with_anchor() {
2948 let _g = crate::test_util::global_state_lock();
2949 // c:359-370 — `r:|=*` matches any word, requires anchor.
2950 let _g = zle_test_setup();
2951 let r = parse_cmatcher("", "r:|=*");
2952 assert!(r.is_some(), "r:|=* should produce a Cmatcher");
2953 let cm = r.unwrap();
2954 assert_eq!(cm.wlen, -1); // c:370 single `*`
2955 assert!(cm.word.is_none());
2956 }
2957
2958 #[test]
2959 fn cmatcher_double_star_word() {
2960 let _g = crate::test_util::global_state_lock();
2961 // c:366-368 — `r:|=**` matches any (greedy) word.
2962 let _g = zle_test_setup();
2963 let r = parse_cmatcher("", "r:|=**");
2964 assert!(r.is_some());
2965 let cm = r.unwrap();
2966 assert_eq!(cm.wlen, -2); // c:368 double `**`
2967 }
2968
2969 #[test]
2970 fn cmatcher_star_without_anchor_errors() {
2971 let _g = crate::test_util::global_state_lock();
2972 // c:360-364 — `m:=*` (no anchor) errors.
2973 let _g = zle_test_setup();
2974 let r = parse_cmatcher("", "m:=*");
2975 assert!(r.is_none(), "*-without-anchor should error");
2976 }
2977
2978 #[test]
2979 fn cmatcher_chain_multiple_rules() {
2980 let _g = crate::test_util::global_state_lock();
2981 // c:251-401 — multiple rules separated by whitespace chain.
2982 let _g = zle_test_setup();
2983 let r = parse_cmatcher("", "m:foo=bar m:baz=qux");
2984 assert!(r.is_some());
2985 let head = r.unwrap();
2986 assert!(head.next.is_some(), "second rule should be linked");
2987 }
2988
2989 #[test]
2990 fn pattern_single_char_emits_cpat_char() {
2991 let _g = crate::test_util::global_state_lock();
2992 // c:451-461 — single non-special char → CPAT_CHAR node.
2993 let _g = zle_test_setup();
2994 let (chain, rest, len, err) = parse_pattern("", "abc", '\0');
2995 assert!(!err);
2996 assert_eq!(len, 3);
2997 assert_eq!(rest, ""); // consumed everything (no end-char, no whitespace)
2998 // Walk chain and verify 3 CPAT_CHAR nodes.
2999 let mut count = 0;
3000 let mut cur = chain.as_deref();
3001 while let Some(n) = cur {
3002 assert_eq!(n.tp, CPAT_CHAR);
3003 count += 1;
3004 cur = n.next.as_deref();
3005 }
3006 assert_eq!(count, 3);
3007 }
3008
3009 #[test]
3010 fn pattern_question_mark_is_cpat_any() {
3011 let _g = crate::test_util::global_state_lock();
3012 // c:443 — `?` → CPAT_ANY.
3013 let _g = zle_test_setup();
3014 let (chain, _, len, err) = parse_pattern("", "?", '\0');
3015 assert!(!err);
3016 assert_eq!(len, 1);
3017 assert_eq!(chain.as_ref().unwrap().tp, CPAT_ANY);
3018 }
3019
3020 #[test]
3021 fn pattern_invalid_chars_error() {
3022 let _g = crate::test_util::global_state_lock();
3023 // c:446-449 — `*`/`(`/`)`/`=` → error.
3024 let _g = zle_test_setup();
3025 for c in ['*', '(', ')', '='] {
3026 let s = format!("{}", c);
3027 let (chain, _, _, err) = parse_pattern("", &s, '\0');
3028 assert!(err, "char {} should error", c);
3029 assert!(chain.is_none());
3030 }
3031 }
3032
3033 #[test]
3034 fn pattern_backslash_escapes_next() {
3035 let _g = crate::test_util::global_state_lock();
3036 // c:452 — `\\X` consumes the backslash and emits X as CPAT_CHAR.
3037 let _g = zle_test_setup();
3038 let (chain, _, len, err) = parse_pattern("", r"\*", '\0');
3039 assert!(!err);
3040 assert_eq!(len, 1);
3041 let n = chain.as_ref().unwrap();
3042 assert_eq!(n.tp, CPAT_CHAR);
3043 assert_eq!(n.chr, '*' as u32);
3044 }
3045
3046 #[test]
3047 fn pattern_stops_at_end_char() {
3048 let _g = crate::test_util::global_state_lock();
3049 // c:430 — `*s != e` gate.
3050 let _g = zle_test_setup();
3051 let (_, rest, len, err) = parse_pattern("", "ab=cd", '=');
3052 assert!(!err);
3053 assert_eq!(len, 2);
3054 assert_eq!(rest, "=cd");
3055 }
3056
3057 #[test]
3058 fn pattern_stops_at_whitespace_when_no_end_char() {
3059 let _g = crate::test_util::global_state_lock();
3060 // c:430 — `e==0` → !inblank.
3061 let _g = zle_test_setup();
3062 let (_, rest, len, err) = parse_pattern("", "ab cd", '\0');
3063 assert!(!err);
3064 assert_eq!(len, 2);
3065 assert_eq!(rest, " cd");
3066 }
3067
3068 #[test]
3069 fn pattern_bracket_class_routes_to_parse_class() {
3070 let _g = crate::test_util::global_state_lock();
3071 // c:435 — `[abc]` dispatches to parse_class. With no end-char
3072 // parse_pattern continues into the trailing chars as
3073 // CPAT_CHAR nodes, so `[abc]xy` → class + x + y = 3.
3074 let _g = zle_test_setup();
3075 let (chain, rest, len, err) = parse_pattern("", "[abc]xy=q", '=');
3076 assert!(!err);
3077 assert_eq!(len, 3);
3078 assert_eq!(rest, "=q");
3079 // chain head is the class node.
3080 assert_eq!(chain.as_ref().unwrap().tp, CPAT_CCLASS);
3081 }
3082
3083 #[test]
3084 fn classes_unterminated_returns_eos() {
3085 let _g = crate::test_util::global_state_lock();
3086 // c:504 — unterminated class → returns input-end.
3087 let _g = zle_test_setup();
3088 let mut p = Cpattern::default();
3089 let rest = parse_class(&mut p, "[abc");
3090 assert_eq!(rest, "");
3091 }
3092
3093 #[test]
3094 fn compadd_trace_records_and_returns_one() {
3095 // sh:_complete_help:11 — `compadd() { return 1 }`. With the
3096 // trace flag on, bin_compadd records argv into
3097 // `_complete_help_funcs` and returns 1 without panicking.
3098 let _g = crate::test_util::global_state_lock();
3099 let _g2 = zle_test_setup();
3100 INCOMPFUNC.store(1, Ordering::Relaxed);
3101 crate::ported::params::setaparam("_complete_help_funcs", Vec::new());
3102 set_compadd_trace(true);
3103 let argv = vec!["-X".to_string(), "files".to_string(), "alpha".to_string()];
3104 let r = bin_compadd("compadd", &argv, &make_test_ops(), 0);
3105 set_compadd_trace(false);
3106 INCOMPFUNC.store(0, Ordering::Relaxed);
3107 assert_eq!(r, 1, "trace mode short-circuits to 1");
3108 let buf = crate::ported::params::getaparam("_complete_help_funcs").unwrap_or_default();
3109 assert_eq!(buf, vec!["-X files alpha".to_string()]);
3110 }
3111
3112 #[test]
3113 fn compadd_prefix_injector_mutates_prefix_then_restores() {
3114 // sh:_approximate:57-72 — injector prepends `(#a$N)` to PREFIX
3115 // for the duration of the compadd call, then restores.
3116 let _g = crate::test_util::global_state_lock();
3117 let _g2 = zle_test_setup();
3118 INCOMPFUNC.store(1, Ordering::Relaxed);
3119 crate::ported::params::setsparam("PREFIX", "abc").unwrap();
3120 // Trace flag on so the body short-circuits and we can observe
3121 // what PREFIX was during the call via the captured argv-side
3122 // parameter snapshot.
3123 crate::ported::params::setaparam("_complete_help_funcs", Vec::new());
3124 set_compadd_trace(true);
3125 let prev = set_compadd_prefix_injector("(#a2)");
3126 assert!(prev.is_none());
3127 // Spy: stash PREFIX into a side-channel param before running.
3128 // bin_compadd's wrapper mutates PREFIX, runs body, restores;
3129 // our spy reads the mutated value mid-call via a hook is not
3130 // wired, so instead we directly observe PREFIX after the
3131 // call to confirm restoration.
3132 let _ = bin_compadd("compadd", &["x".to_string()], &make_test_ops(), 0);
3133 clear_compadd_prefix_injector();
3134 set_compadd_trace(false);
3135 INCOMPFUNC.store(0, Ordering::Relaxed);
3136 let after = crate::ported::params::getsparam("PREFIX").unwrap_or_default();
3137 assert_eq!(after, "abc", "PREFIX restored after compadd call");
3138 }
3139
3140 #[test]
3141 fn compadd_prefix_injector_tilde_aware() {
3142 // sh:_approximate:65-69 — leading `~` in PREFIX stays before
3143 // the injected pattern when no `-p ~…` is passed.
3144 let _g = crate::test_util::global_state_lock();
3145 let _g2 = zle_test_setup();
3146 INCOMPFUNC.store(1, Ordering::Relaxed);
3147 crate::ported::params::setsparam("PREFIX", "~user").unwrap();
3148 set_compadd_trace(true);
3149 let _ = set_compadd_prefix_injector("(#a1)");
3150 let _ = bin_compadd("compadd", &["x".to_string()], &make_test_ops(), 0);
3151 clear_compadd_prefix_injector();
3152 set_compadd_trace(false);
3153 INCOMPFUNC.store(0, Ordering::Relaxed);
3154 // Restored
3155 assert_eq!(
3156 crate::ported::params::getsparam("PREFIX").unwrap_or_default(),
3157 "~user"
3158 );
3159 }
3160
3161 fn make_test_ops() -> options {
3162 options {
3163 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3164 args: Vec::new(),
3165 argscount: 0,
3166 argsalloc: 0,
3167 }
3168 }
3169
3170 // ─── zsh-corpus pins for parse_ordering ────────────────────────
3171
3172 /// `parse_ordering("")` returns -1 — empty CSV token is not a
3173 /// valid orderopts name. Pin the actual zsh contract.
3174 #[test]
3175 fn complete_corpus_parse_ordering_empty_returns_neg_one() {
3176 let _g = crate::test_util::global_state_lock();
3177 let _g2 = zle_test_setup();
3178 let mut flags: Option<i32> = Some(0);
3179 let r = parse_ordering("", &mut flags);
3180 assert_eq!(r, -1, "empty token is not a valid orderopts name");
3181 }
3182
3183 /// `parse_ordering("invalid_xyz")` returns -1 (unknown option).
3184 #[test]
3185 fn complete_corpus_parse_ordering_unknown_returns_neg_one() {
3186 let _g = crate::test_util::global_state_lock();
3187 let _g2 = zle_test_setup();
3188 let mut flags: Option<i32> = Some(0);
3189 let r = parse_ordering("zzz_not_a_real_ordering_xyz", &mut flags);
3190 assert_eq!(r, -1, "unknown ordering name = -1");
3191 }
3192
3193 /// `parse_ordering("match")` recognises a real orderopts name.
3194 #[test]
3195 fn complete_corpus_parse_ordering_match_recognised() {
3196 let _g = crate::test_util::global_state_lock();
3197 let _g2 = zle_test_setup();
3198 let mut flags: Option<i32> = Some(0);
3199 let r = parse_ordering("match", &mut flags);
3200 assert_eq!(r, 0, "match is a valid orderopts name");
3201 }
3202
3203 /// `parse_ordering` accepts comma-separated valid names.
3204 #[test]
3205 fn complete_corpus_parse_ordering_csv_all_valid() {
3206 let _g = crate::test_util::global_state_lock();
3207 let _g2 = zle_test_setup();
3208 let mut flags: Option<i32> = Some(0);
3209 let r = parse_ordering("match,reverse", &mut flags);
3210 // match + reverse are both valid → 0.
3211 assert_eq!(r, 0);
3212 }
3213
3214 /// `parse_ordering` rejects when any csv token is invalid.
3215 #[test]
3216 fn complete_corpus_parse_ordering_csv_with_invalid_returns_neg_one() {
3217 let _g = crate::test_util::global_state_lock();
3218 let _g2 = zle_test_setup();
3219 let mut flags: Option<i32> = Some(0);
3220 let r = parse_ordering("match,zzz_bogus", &mut flags);
3221 assert_eq!(r, -1, "any invalid token = -1");
3222 }
3223
3224 // ═══════════════════════════════════════════════════════════════════
3225 // C-parity tests pinning Src/Zle/complete.c.
3226 // ═══════════════════════════════════════════════════════════════════
3227
3228 /// `ignore_prefix(0)` is a no-op (`if (l) …` guard).
3229 /// C `Src/Zle/complete.c:ignore_prefix` first line.
3230 #[test]
3231 fn ignore_prefix_zero_is_noop() {
3232 let _g = crate::test_util::global_state_lock();
3233 let _g2 = zle_test_setup();
3234 ignore_prefix(0);
3235 ignore_prefix(0);
3236 }
3237
3238 /// `ignore_suffix(0)` is a no-op (`if (l) …` guard).
3239 #[test]
3240 fn ignore_suffix_zero_is_noop() {
3241 let _g = crate::test_util::global_state_lock();
3242 let _g2 = zle_test_setup();
3243 ignore_suffix(0);
3244 ignore_suffix(0);
3245 }
3246
3247 /// `restrict_range(0, 0)` writes degenerate empty range.
3248 #[test]
3249 fn restrict_range_zero_zero_no_panic() {
3250 let _g = crate::test_util::global_state_lock();
3251 let _g2 = zle_test_setup();
3252 restrict_range(0, 0);
3253 }
3254
3255 /// `set_compadd_trace(true)` / `(false)` toggle no-panic.
3256 #[test]
3257 fn set_compadd_trace_toggle_no_panic() {
3258 let _g = crate::test_util::global_state_lock();
3259 let _g2 = zle_test_setup();
3260 set_compadd_trace(true);
3261 set_compadd_trace(false);
3262 set_compadd_trace(true);
3263 }
3264
3265 /// `set_compadd_prefix_injector` / `clear_compadd_prefix_injector`
3266 /// round-trip.
3267 #[test]
3268 fn set_then_clear_compadd_prefix_injector_no_panic() {
3269 let _g = crate::test_util::global_state_lock();
3270 let _g2 = zle_test_setup();
3271 let prev = set_compadd_prefix_injector("test_prefix");
3272 clear_compadd_prefix_injector();
3273 if let Some(p) = prev {
3274 let _ = set_compadd_prefix_injector(p);
3275 clear_compadd_prefix_injector();
3276 }
3277 }
3278
3279 /// `parse_ordering("")` returns -1 — no valid ordering token.
3280 #[test]
3281 fn parse_ordering_empty_string_returns_neg_one() {
3282 let _g = crate::test_util::global_state_lock();
3283 let _g2 = zle_test_setup();
3284 let mut flags: Option<i32> = Some(0);
3285 let r = parse_ordering("", &mut flags);
3286 assert_eq!(r, -1, "empty input has no valid ordering token");
3287 }
3288
3289 // ═══════════════════════════════════════════════════════════════════
3290 // Additional C-parity tests for Src/Zle/complete.c free/copy
3291 // chain walkers.
3292 // ═══════════════════════════════════════════════════════════════════
3293
3294 /// c:115 — `freecmatcher(None)` is a safe no-op.
3295 #[test]
3296 fn freecmatcher_none_is_noop() {
3297 let _g = crate::test_util::global_state_lock();
3298 let _g2 = zle_test_setup();
3299 freecmatcher(None);
3300 }
3301
3302 /// c:137 — `freecpattern(None)` is a safe no-op.
3303 #[test]
3304 fn freecpattern_none_is_noop() {
3305 let _g = crate::test_util::global_state_lock();
3306 let _g2 = zle_test_setup();
3307 freecpattern(None);
3308 }
3309
3310 /// c:98 — `freecmlist(None)` is a safe no-op.
3311 #[test]
3312 fn freecmlist_none_is_noop() {
3313 let _g = crate::test_util::global_state_lock();
3314 let _g2 = zle_test_setup();
3315 freecmlist(None);
3316 }
3317
3318 /// c:218 — `cpcpattern(None)` returns None (no source chain).
3319 #[test]
3320 fn cpcpattern_none_returns_none() {
3321 let _g = crate::test_util::global_state_lock();
3322 let _g2 = zle_test_setup();
3323 let r = cpcpattern(None);
3324 assert!(r.is_none(), "None source → None copy");
3325 }
3326
3327 /// c:155 — `cpcmatcher(None)` returns None.
3328 #[test]
3329 fn cpcmatcher_none_returns_none() {
3330 let _g = crate::test_util::global_state_lock();
3331 let _g2 = zle_test_setup();
3332 let r = cpcmatcher(None);
3333 assert!(r.is_none());
3334 }
3335
3336 /// c:189 — `cp_cpattern_element` copies the `tp` field verbatim.
3337 #[test]
3338 fn cp_cpattern_element_copies_tp() {
3339 let _g = crate::test_util::global_state_lock();
3340 let _g2 = zle_test_setup();
3341 let src = Cpattern {
3342 tp: CPAT_CHAR,
3343 chr: 'A' as u32,
3344 ..Default::default()
3345 };
3346 let dup = cp_cpattern_element(&src);
3347 assert_eq!(dup.tp, CPAT_CHAR, "tp must round-trip");
3348 }
3349
3350 /// c:218 — `cp_cpattern_element` on CPAT_CHAR copies `chr` field.
3351 #[test]
3352 fn cp_cpattern_element_copies_chr_for_cpat_char() {
3353 let _g = crate::test_util::global_state_lock();
3354 let _g2 = zle_test_setup();
3355 let src = Cpattern {
3356 tp: CPAT_CHAR,
3357 chr: 0x42,
3358 ..Default::default()
3359 };
3360 let dup = cp_cpattern_element(&src);
3361 assert_eq!(dup.chr, 0x42);
3362 }
3363
3364 /// c:199 — `cp_cpattern_element` on CPAT_CCLASS copies `str` field.
3365 #[test]
3366 fn cp_cpattern_element_copies_str_for_cclass() {
3367 let _g = crate::test_util::global_state_lock();
3368 let _g2 = zle_test_setup();
3369 let src = Cpattern {
3370 tp: CPAT_CCLASS,
3371 str: Some(b"a-z".to_vec()),
3372 ..Default::default()
3373 };
3374 let dup = cp_cpattern_element(&src);
3375 assert_eq!(dup.str.as_deref(), Some(&b"a-z"[..]));
3376 }
3377
3378 /// c:191 — `cp_cpattern_element` always initializes `next = None`
3379 /// (caller chains them via cpcpattern, not by carrying source's
3380 /// next forward).
3381 #[test]
3382 fn cp_cpattern_element_resets_next_to_none() {
3383 let _g = crate::test_util::global_state_lock();
3384 let _g2 = zle_test_setup();
3385 let src = Cpattern {
3386 tp: CPAT_CHAR,
3387 chr: 'x' as u32,
3388 next: Some(Box::new(Cpattern::default())), // source has next
3389 ..Default::default()
3390 };
3391 let dup = cp_cpattern_element(&src);
3392 assert!(dup.next.is_none(), "copy element MUST reset next to None");
3393 }
3394
3395 /// c:218 — `cpcpattern` of a 3-node chain produces a 3-node chain
3396 /// (chain walker advances tail_ref per element).
3397 #[test]
3398 fn cpcpattern_preserves_chain_length() {
3399 let _g = crate::test_util::global_state_lock();
3400 let _g2 = zle_test_setup();
3401 let src = Cpattern {
3402 tp: CPAT_CHAR,
3403 chr: 'a' as u32,
3404 next: Some(Box::new(Cpattern {
3405 tp: CPAT_CHAR,
3406 chr: 'b' as u32,
3407 next: Some(Box::new(Cpattern {
3408 tp: CPAT_CHAR,
3409 chr: 'c' as u32,
3410 ..Default::default()
3411 })),
3412 ..Default::default()
3413 })),
3414 ..Default::default()
3415 };
3416 let dup = cpcpattern(Some(&src)).expect("should copy");
3417 // Count: head + 2 next.
3418 let mut cnt = 1;
3419 let mut cur = &dup.next;
3420 while let Some(n) = cur {
3421 cnt += 1;
3422 cur = &n.next;
3423 }
3424 assert_eq!(cnt, 3, "3-node source → 3-node copy");
3425 }
3426
3427 /// c:242 — `parse_cmatcher(_, "")` returns None per c:249 guard.
3428 #[test]
3429 fn parse_cmatcher_empty_returns_none() {
3430 let _g = crate::test_util::global_state_lock();
3431 let _g2 = zle_test_setup();
3432 let r = parse_cmatcher("test", "");
3433 assert!(r.is_none(), "empty matcher spec → None per c:249");
3434 }
3435
3436 // ═══════════════════════════════════════════════════════════════════
3437 // Additional C-parity tests for Src/Zle/complete.c
3438 // c:75 freecmlist / c:101 freecmatcher / c:124 freecpattern /
3439 // c:142 cpcmatcher / c:181 cp_cpattern_element / c:203 cpcpattern /
3440 // c:294 parse_cmatcher / c:791 parse_ordering / c:1069 ignore_prefix /
3441 // c:1087 ignore_suffix / c:1111 restrict_range / c:1598 get_compstate
3442 // ═══════════════════════════════════════════════════════════════════
3443
3444 /// c:75 — `freecmlist(None)` is safe idempotent.
3445 #[test]
3446 fn freecmlist_none_idempotent() {
3447 for _ in 0..5 {
3448 freecmlist(None);
3449 }
3450 }
3451
3452 /// c:101 — `freecmatcher(None)` is safe idempotent.
3453 #[test]
3454 fn freecmatcher_none_idempotent() {
3455 for _ in 0..5 {
3456 freecmatcher(None);
3457 }
3458 }
3459
3460 /// c:124 — `freecpattern(None)` is safe idempotent.
3461 #[test]
3462 fn freecpattern_none_idempotent() {
3463 for _ in 0..5 {
3464 freecpattern(None);
3465 }
3466 }
3467
3468 /// c:142 — `cpcmatcher(None)` returns None (type pin).
3469 #[test]
3470 fn cpcmatcher_none_returns_none_type_pin() {
3471 let r: Option<Box<Cmatcher>> = cpcmatcher(None);
3472 assert!(r.is_none());
3473 }
3474
3475 /// c:203 — `cpcpattern(None)` returns None (type pin).
3476 #[test]
3477 fn cpcpattern_none_returns_none_type_pin() {
3478 let r: Option<Box<Cpattern>> = cpcpattern(None);
3479 assert!(r.is_none());
3480 }
3481
3482 /// c:294 — `parse_cmatcher` is deterministic for empty input.
3483 #[test]
3484 fn parse_cmatcher_empty_is_deterministic() {
3485 let _g = crate::test_util::global_state_lock();
3486 let _g2 = zle_test_setup();
3487 for _ in 0..3 {
3488 assert!(parse_cmatcher("test", "").is_none());
3489 }
3490 }
3491
3492 /// c:791 — `parse_ordering(empty, _)` returns i32 (type pin).
3493 #[test]
3494 fn parse_ordering_returns_i32_type() {
3495 let mut flags: Option<i32> = None;
3496 let _: i32 = parse_ordering("name", &mut flags);
3497 }
3498
3499 /// c:1069 — `ignore_prefix(0)` is safe.
3500 #[test]
3501 fn ignore_prefix_zero_no_panic() {
3502 let _g = crate::test_util::global_state_lock();
3503 let _g2 = zle_test_setup();
3504 ignore_prefix(0);
3505 }
3506
3507 /// c:1087 — `ignore_suffix(0)` is safe.
3508 #[test]
3509 fn ignore_suffix_zero_no_panic() {
3510 let _g = crate::test_util::global_state_lock();
3511 let _g2 = zle_test_setup();
3512 ignore_suffix(0);
3513 }
3514
3515 /// c:1111 — `restrict_range(0, 0)` is safe (zero-width).
3516 #[test]
3517 fn restrict_range_zero_zero_no_panic_pin() {
3518 let _g = crate::test_util::global_state_lock();
3519 let _g2 = zle_test_setup();
3520 restrict_range(0, 0);
3521 }
3522
3523 /// c:1598 — `get_compstate(null)` returns Option<usize> type.
3524 #[test]
3525 fn get_compstate_null_returns_option_type() {
3526 let _g = crate::test_util::global_state_lock();
3527 let _: Option<usize> = get_compstate(std::ptr::null_mut());
3528 }
3529
3530 // ═══════════════════════════════════════════════════════════════════
3531 // Additional C-parity tests for Src/Zle/complete.c
3532 // c:75 freecmlist / c:294 parse_cmatcher / c:791 parse_ordering /
3533 // c:853 bin_compadd / c:942 prefix_injector / c:953 compadd_trace /
3534 // c:1069 ignore_prefix / c:1087 ignore_suffix / c:1111 restrict_range /
3535 // c:1393 bin_compset
3536 // ═══════════════════════════════════════════════════════════════════
3537
3538 /// c:75 — `freecmlist(None)` is idempotent (alt 10-call).
3539 #[test]
3540 fn freecmlist_none_idempotent_10_call() {
3541 for _ in 0..10 {
3542 freecmlist(None);
3543 }
3544 }
3545
3546 /// c:1069 — `ignore_prefix(positive)` is safe.
3547 #[test]
3548 fn ignore_prefix_positive_no_panic() {
3549 let _g = crate::test_util::global_state_lock();
3550 let _g2 = zle_test_setup();
3551 for n in [1i32, 10, 100] {
3552 ignore_prefix(n);
3553 }
3554 }
3555
3556 /// c:1087 — `ignore_suffix(positive)` is safe.
3557 #[test]
3558 fn ignore_suffix_positive_no_panic() {
3559 let _g = crate::test_util::global_state_lock();
3560 let _g2 = zle_test_setup();
3561 for n in [1i32, 10, 100] {
3562 ignore_suffix(n);
3563 }
3564 }
3565
3566 /// c:1069 — `ignore_prefix(0)` is idempotent across repeated calls.
3567 #[test]
3568 fn ignore_prefix_idempotent() {
3569 let _g = crate::test_util::global_state_lock();
3570 let _g2 = zle_test_setup();
3571 for _ in 0..10 {
3572 ignore_prefix(0);
3573 }
3574 }
3575
3576 /// c:1111 — `restrict_range` is safe for various endpoints.
3577 #[test]
3578 fn restrict_range_various_endpoints_no_panic() {
3579 let _g = crate::test_util::global_state_lock();
3580 let _g2 = zle_test_setup();
3581 for &(b, e) in &[(0, 0), (1, 10), (-1, -1), (i32::MAX, i32::MAX)] {
3582 restrict_range(b, e);
3583 }
3584 }
3585
3586 /// c:791 — `parse_ordering` is deterministic for empty name.
3587 #[test]
3588 fn parse_ordering_empty_deterministic() {
3589 let mut flags1: Option<i32> = None;
3590 let first = parse_ordering("", &mut flags1);
3591 let mut flags2: Option<i32> = None;
3592 let second = parse_ordering("", &mut flags2);
3593 assert_eq!(first, second, "parse_ordering('') must be deterministic");
3594 }
3595
3596 /// c:853 — `bin_compadd` returns i32 (compile-time pin).
3597 #[test]
3598 fn bin_compadd_returns_i32_type() {
3599 let _g = crate::test_util::global_state_lock();
3600 let _g2 = zle_test_setup();
3601 let ops = crate::ported::zsh_h::options {
3602 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3603 args: Vec::new(),
3604 argscount: 0,
3605 argsalloc: 0,
3606 };
3607 let _: i32 = bin_compadd("compadd", &[], &ops, 0);
3608 }
3609
3610 /// c:1393 — `bin_compset` returns i32 (compile-time pin).
3611 #[test]
3612 fn bin_compset_returns_i32_type() {
3613 let _g = crate::test_util::global_state_lock();
3614 let _g2 = zle_test_setup();
3615 let ops = crate::ported::zsh_h::options {
3616 ind: [0u8; crate::ported::zsh_h::MAX_OPS],
3617 args: Vec::new(),
3618 argscount: 0,
3619 argsalloc: 0,
3620 };
3621 let _: i32 = bin_compset("compset", &[], &ops, 0);
3622 }
3623
3624 /// c:294 — `parse_cmatcher` is deterministic for various inputs.
3625 #[test]
3626 fn parse_cmatcher_various_inputs_deterministic() {
3627 let _g = crate::test_util::global_state_lock();
3628 let _g2 = zle_test_setup();
3629 for s in ["", "x", "m:{a-z}={A-Z}", "garbage"] {
3630 let a = parse_cmatcher("test", s).is_some();
3631 let b = parse_cmatcher("test", s).is_some();
3632 assert_eq!(a, b, "parse_cmatcher({:?}) must be deterministic", s);
3633 }
3634 }
3635
3636 /// c:942 — `set_compadd_prefix_injector` round-trips with clear.
3637 #[test]
3638 fn compadd_prefix_injector_round_trip() {
3639 let _g = crate::test_util::global_state_lock();
3640 let _g2 = zle_test_setup();
3641 let _: Option<String> = set_compadd_prefix_injector("test_prefix_xyz");
3642 clear_compadd_prefix_injector();
3643 }
3644
3645 /// c:953 — `set_compadd_trace` is idempotent (toggle on/off).
3646 #[test]
3647 fn set_compadd_trace_toggle_safe() {
3648 let _g = crate::test_util::global_state_lock();
3649 let _g2 = zle_test_setup();
3650 for active in [true, false, true, true, false] {
3651 set_compadd_trace(active);
3652 }
3653 }
3654}