zsh/ported/zle/compcore.rs
1//! Direct port of `Src/Zle/compcore.c` — completion core code.
2//!
3//! Original C copyright: Sven Wischnowsky 1995-1997.
4//!
5//! C source is 3,638 lines. This file ports:
6//! - the file-scope globals (c:36-279)
7//! - the pure-string helpers (`rembslash`, `remsquote`,
8//! `comp_quoting_string`, `multiquote`, `tildequote`, `matcheq`,
9//! `matchcmp`, `ctokenize`, `comp_str`)
10//! - the linked-list group manipulators (`begcmgroup`,
11//! `endcmgroup`, `addexpl`, `addmatch`)
12//! - the param-table helpers (`get_user_var`, `get_data_arr`,
13//! `set_list_array`)
14//! - the hook entry points (`before_complete`, `after_complete`)
15//! in their non-runhookdef branches
16//!
17//! Functions blocked on heavier substrate (`do_completion`,
18//! `makecomplist`, `addmatches`, `callcompfunc`, `set_comp_sep`,
19//! `check_param`, `permmatches`, `dupmatch`, `add_match_data`,
20//! `makearray`) carry doc comments naming the missing dependencies.
21
22#![allow(non_snake_case, non_upper_case_globals, dead_code)]
23
24use crate::ported::context::zcontext_restore_partial;
25use crate::ported::module::{gethookdef, runhookdef};
26use crate::ported::params::{getsparam, paramtab, paramtab_hashed_storage, setaparam, setsparam};
27use crate::ported::signals::{queue_signals, unqueue_signals};
28use crate::ported::zle::comp_h::{
29 Aminfo, Brinfo, Cadata, Ccmakedat, Cexpl, Cline, Cmatch, Cmgroup, Cmlist, Menuinfo, CAF_ALL,
30 CAF_ARRAYS, CAF_KEYS, CAF_MATCH, CAF_MATSORT, CAF_NOSORT, CAF_NUMSORT, CAF_QUOTE, CAF_REVSORT,
31 CAF_UNIQALL,
32 CAF_UNIQCON, CGF_MATSORT, CGF_NOSORT, CGF_NUMSORT, CGF_REVSORT, CGF_UNIQALL, CGF_UNIQCON,
33 CMF_DELETE, CMF_DISPLINE, CMF_FMULT, CMF_MULT, CMF_NOLIST, CMF_PACKED, CMF_PARBR, CMF_PARNEST,
34 CMF_ROWS,
35};
36use crate::ported::zle::complete::{
37 COMPIPREFIX, COMPLIST, COMPPREFIX, COMPQSTACK, COMPSUFFIX, INCOMPFUNC,
38};
39use crate::ported::zle::compmatch::{bld_parts, cline_matched};
40use crate::ported::zle::compresult::{do_ambig_menu, ztat};
41use crate::ported::zle::zle_h::{invalidatelist, COMP_LIST_COMPLETE, COMP_LIST_EXPAND, CUT_RAW};
42use crate::ported::zle::zle_refresh::{CLEARLIST, SHOWINGLIST};
43use crate::ported::zle::zle_tricky::{
44 inststr, MENUCMP, ORIGCS, ORIGLINE, USEGLOB, USEMENU, VALIDLIST, WOULDINSTAB,
45};
46use crate::ported::zle::zle_utils::foredel;
47#[allow(unused_imports)]
48use crate::ported::zle::{
49 deltochar::*, textobjects::*, zle_h::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
50 zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
51};
52use crate::ported::zsh_h::{
53 isset, Bnull, Dnull, Equals, Hat, Inbrace, Inbrack, Inpar, Outbrace, Outpar, Pound, Qstring,
54 Quest, Snull, Star, Stringg, Tilde, BASHAUTOLIST, NUMERICGLOBSORT, PM_HASHED, PM_TYPE,
55 QT_BACKSLASH, QT_DOLLARS, QT_DOUBLE, QT_NONE, QT_SINGLE, RCQUOTES, SORTIT_IGNORING_BACKSLASHES,
56 SORTIT_NUMERICALLY, ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE,
57};
58use crate::DPUTS;
59use std::sync::atomic::{AtomicI32, Ordering};
60use std::sync::{Mutex, OnceLock};
61
62// =====================================================================
63// Substrate-blocked stubs — bodies need substrate listed in each
64// doc comment. Returns shape-correct safe defaults.
65// =====================================================================
66
67// =====================================================================
68// do_completion — `Src/Zle/compcore.c:287`.
69// =====================================================================
70
71/// Direct port of `int do_completion(Hookdef dummy, Compldat dat)`
72/// from compcore.c:287. The top-level completion driver: per-round
73/// state reset → `makecomplist` → dispatch to `do_ambiguous` /
74/// `do_single` / `do_allmatches` per result count.
75pub fn do_completion(s: &str, incmd: i32, lst: i32) -> i32 {
76 // c:287
77
78 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
79 // Native (Rust) plugins that registered completions via
80 // `register_completion` (src/extensions/plugin_host.rs) have their
81 // compsys `compdef` wiring deferred to here — the completion pipeline
82 // is a safe point to eval the glue (compsys itself evals here),
83 // whereas evaling during `zmodload -R` plugin-init hangs the VM.
84 // Idempotent + cheap: no-ops once the pending queue is drained.
85 crate::plugin_host::flush_pending_completions();
86
87 let osl = SHOWINGLIST.load(Ordering::Relaxed); // c:289
88 let mut ret: i32 = 0; // c:289
89
90 // c:296-297 — `ainfo = fainfo = NULL`.
91 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
92 *g = None;
93 }
94 if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
95 *g = None;
96 }
97 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
98 g.clear(); // c:298
99 }
100
101 // c:300-307 — compqstack reset.
102 let instring = INSTRING.load(Ordering::Relaxed); // c:307
103 // c:305 — `compqstack = instring == QT_NONE ? "\\" : <quote-char>`.
104 // Inlined `char_from_qt(x)` as `(x as u8) as char`.
105 let head_q: char = if instring == QT_NONE {
106 // c:305
107 QT_BACKSLASH as u8 as char
108 } else {
109 instring as u8 as char
110 };
111 if let Ok(mut g) = COMPQSTACK.get_or_init(|| Mutex::new(String::new())).lock() {
112 *g = head_q.to_string(); // c:305-306
113 }
114
115 hasunqu.store(0, Ordering::Relaxed); // c:309
116 let wouldinstab_v = WOULDINSTAB.load(Ordering::Relaxed); // c:310
117 useline.store(
118 // c:310
119 if wouldinstab_v != 0 {
120 -1
121 } else if lst != COMP_LIST_COMPLETE {
122 1
123 } else {
124 0
125 },
126 Ordering::Relaxed,
127 );
128 useexact.store(opt_isset("RECEXACT"), Ordering::Relaxed); // c:311
129 set_compstate_str("exact_string", ""); // c:312
130 let useline_v = useline.load(Ordering::Relaxed);
131 uselist.store(
132 // c:314
133 if useline_v != 0 {
134 if opt_isset("AUTOLIST") != 0 && opt_isset("BASHAUTOLIST") == 0 {
135 if opt_isset("LISTAMBIGUOUS") != 0 {
136 3
137 } else {
138 2
139 }
140 } else {
141 0
142 }
143 } else {
144 1
145 },
146 Ordering::Relaxed,
147 );
148
149 let useglob_v = USEGLOB.load(Ordering::Relaxed); // c:319
150 let opm: String = if useglob_v != 0 {
151 "*".into()
152 } else {
153 "".into()
154 };
155 if let Ok(mut g) = comppatmatch.get_or_init(|| Mutex::new(None)).lock() {
156 *g = Some(opm.clone()); // c:319
157 }
158 set_compstate_str("pattern_insert", "menu"); // c:320
159 forcelist.store(0, Ordering::Relaxed); // c:322
160 haspattern.store(0, Ordering::Relaxed); // c:323
161 // c:324 — complistmax mirrors the LISTMAX parameter for every
162 // completion; asklist reads it to decide when to prompt "do you wish
163 // to see all N possibilities?". Leaving the static at 0 made large
164 // command lists (`l<Tab>`, 230 matches) dump without asking.
165 crate::ported::zle::complete::COMPLISTMAX
166 .store(env_iparam("LISTMAX") as i64, Ordering::Relaxed); // c:324
167
168 set_compstate_str(
169 // c:326
170 "last_prompt",
171 if opt_isset("ALWAYSLASTPROMPT") != 0 {
172 "yes"
173 } else {
174 ""
175 },
176 );
177 dolastprompt.store(1, Ordering::Relaxed); // c:327
178
179 // c:329-330 — complist string.
180 let cl_str = if opt_isset("LISTROWSFIRST") != 0 {
181 if opt_isset("LISTPACKED") != 0 {
182 "packed rows"
183 } else {
184 "rows"
185 }
186 } else if opt_isset("LISTPACKED") != 0 {
187 "packed"
188 } else {
189 ""
190 };
191 if let Ok(mut g) = COMPLIST.get_or_init(|| Mutex::new(String::new())).lock() {
192 *g = cl_str.into(); // c:329
193 }
194 startauto.store(opt_isset("AUTOMENU"), Ordering::Relaxed); // c:331
195
196 let zlc = ZLEMETACS.load(Ordering::Relaxed);
197 let we_v = WE.load(Ordering::Relaxed);
198 movetoend.store(
199 // c:332
200 if zlc == we_v || opt_isset("ALWAYSTOEND") != 0 {
201 2
202 } else {
203 1
204 },
205 Ordering::Relaxed,
206 );
207 SHOWINGLIST.store(0, Ordering::Relaxed); // c:333
208 hasmatched.store(0, Ordering::Relaxed); // c:334
209 hasunmatched.store(0, Ordering::Relaxed); // c:334
210 minmlen.store(1_000_000, Ordering::Relaxed); // c:335
211 maxmlen.store(-1, Ordering::Relaxed); // c:336
212 nmessages.store(0, Ordering::Relaxed); // c:338
213 hasallmatch.store(0, Ordering::Relaxed); // c:339
214
215 // c:342 — main dispatch.
216 if makecomplist(s, incmd, lst) != 0 {
217 // c:342
218 // c:344 — error path.
219 ZLEMETACS.store(0, Ordering::Relaxed); // c:344
220 foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:345 — `foredel(zlemetall, CUT_RAW)`
221 let _ = inststr(
222 &ORIGLINE
223 .get_or_init(|| Mutex::new(String::new()))
224 .lock()
225 .map(|g| g.clone())
226 .unwrap_or_default(),
227 ); // c:346 — `inststr(origline)`
228 ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:347
229 CLEARLIST.store(1, Ordering::Relaxed); // c:348
230 ret = 1;
231 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
232 g.cur = None;
233 } // c:350
234 if useline.load(Ordering::Relaxed) < 0 {
235 // c:351
236 unmetafy_line();
237 ret = selfinsert(&[]); // c:353
238 metafy_line();
239 }
240 return goto_compend(ret); // c:356 goto compend
241 }
242
243 // c:359-361 — clear lastprebr/lastpostbr.
244 lastprebr_set(""); // c:359
245 lastpostbr_set(""); // c:360
246
247 let curpm = comppatmatch
248 .get_or_init(|| Mutex::new(None))
249 .lock()
250 .ok()
251 .and_then(|g| g.clone())
252 .unwrap_or_default();
253 if !curpm.is_empty() && curpm != opm {
254 // c:363
255 haspattern.store(1, Ordering::Relaxed); // c:364
256 }
257 let nm = nmatches.load(Ordering::Relaxed); // c:366
258 let dm = diffmatches.load(Ordering::Relaxed);
259 tracing::debug!(
260 target: "compsys_args",
261 nm,
262 dm,
263 useline = useline.load(Ordering::Relaxed),
264 uselist = uselist.load(Ordering::Relaxed),
265 iforcemenu = iforcemenu.load(Ordering::Relaxed),
266 "do_completion branch point"
267 );
268 if iforcemenu.load(Ordering::Relaxed) != 0 {
269 // c:366
270 if nm != 0 {
271 {
272 let _ = do_ambig_menu();
273 };
274 } // c:367
275 ret = if nm == 0 { 1 } else { 0 }; // c:369
276 } else if useline.load(Ordering::Relaxed) < 0 {
277 // c:370
278 unmetafy_line();
279 ret = selfinsert(&[]); // c:372
280 metafy_line();
281 } else if useline.load(Ordering::Relaxed) == 0 && uselist.load(Ordering::Relaxed) != 0 {
282 // c:374
283 ZLEMETACS.store(0, Ordering::Relaxed); // c:375
284 foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:376 — `foredel(zlemetall, CUT_RAW)`
285 let _ = inststr(
286 &ORIGLINE
287 .get_or_init(|| Mutex::new(String::new()))
288 .lock()
289 .map(|g| g.clone())
290 .unwrap_or_default(),
291 ); // c:377 — `inststr(origline)`
292 ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:378
293 SHOWINGLIST.store(-2, Ordering::Relaxed);
294 // c:379
295 } else if useline.load(Ordering::Relaxed) == 2 && nm > 1 {
296 // c:380
297 // c:381 — `do_allmatches(1)`. Faithful minfo-driven insertion:
298 // iterates `amatches`, chaining do_single/accept_last against
299 // the shared ZLEMETALINE buffer (see compresult::do_allmatches).
300 crate::ported::zle::compresult::do_allmatches(1);
301 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
302 g.cur = None;
303 } // c:383
304 if forcelist.load(Ordering::Relaxed) != 0 {
305 // c:385
306 SHOWINGLIST.store(-2, Ordering::Relaxed);
307 } else {
308 invalidatelist(); // c:388
309 }
310 } else if useline.load(Ordering::Relaxed) != 0 {
311 // c:389
312 if nm > 1 && dm != 0 {
313 // c:391
314 // c:393 — `ret = do_ambiguous()`. Inlined: flatten `amatches`
315 // into &[String] and dispatch.
316 ret = {
317 let groups = amatches
318 .get_or_init(|| Mutex::new(Vec::new()))
319 .lock()
320 .map(|g| g.clone())
321 .unwrap_or_default();
322 let all: Vec<String> = groups
323 .into_iter()
324 .flat_map(|g| g.matches.into_iter().filter_map(|m| m.str))
325 .collect();
326 crate::ported::zle::compresult::do_ambiguous(&all)
327 };
328 if SHOWINGLIST.load(Ordering::Relaxed) == 0
329 && uselist.load(Ordering::Relaxed) != 0
330 && LISTSHOWN.load(Ordering::Relaxed) != 0
331 && (USEMENU.load(Ordering::Relaxed) == 2 || oldlist.load(Ordering::Relaxed) != 0)
332 {
333 SHOWINGLIST.store(osl, Ordering::Relaxed);
334 // c:395
335 }
336 } else if nm == 1 || (nm > 1 && dm == 0) {
337 // c:396
338 do_single_first_match(); // c:399-411
339 if forcelist.load(Ordering::Relaxed) != 0 {
340 // c:412
341 if uselist.load(Ordering::Relaxed) != 0 {
342 SHOWINGLIST.store(-2, Ordering::Relaxed);
343 } else {
344 CLEARLIST.store(1, Ordering::Relaxed);
345 }
346 } else {
347 invalidatelist(); // c:418
348 }
349 } else if nmessages.load(Ordering::Relaxed) != 0 && forcelist.load(Ordering::Relaxed) != 0 {
350 // c:419
351 if uselist.load(Ordering::Relaxed) != 0 {
352 SHOWINGLIST.store(-2, Ordering::Relaxed);
353 } else {
354 CLEARLIST.store(1, Ordering::Relaxed);
355 }
356 }
357 } else {
358 // c:425
359 invalidatelist(); // c:426
360 LASTAMBIG.store(
361 // c:427
362 opt_isset("BASHAUTOLIST"),
363 Ordering::Relaxed,
364 );
365 if forcelist.load(Ordering::Relaxed) != 0 {
366 CLEARLIST.store(1, Ordering::Relaxed);
367 } // c:428
368 ZLEMETACS.store(0, Ordering::Relaxed); // c:429
369 foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:430 — `foredel(zlemetall, CUT_RAW)`
370 let _ = inststr(
371 &ORIGLINE
372 .get_or_init(|| Mutex::new(String::new()))
373 .lock()
374 .map(|g| g.clone())
375 .unwrap_or_default(),
376 ); // c:431 — `inststr(origline)`
377 ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:432
378 }
379
380 // c:436 — explanation strings.
381 if SHOWINGLIST.load(Ordering::Relaxed) == 0
382 && VALIDLIST.load(Ordering::Relaxed) != 0
383 && USEMENU.load(Ordering::Relaxed) != 2
384 && uselist.load(Ordering::Relaxed) != 0
385 && (nm != 1 || dm != 0)
386 && useline.load(Ordering::Relaxed) >= 0
387 && useline.load(Ordering::Relaxed) != 2
388 && (oldlist.load(Ordering::Relaxed) == 0 || LISTSHOWN.load(Ordering::Relaxed) == 0)
389 {
390 onlyexpl.store(3, Ordering::Relaxed); // c:441
391 SHOWINGLIST.store(-2, Ordering::Relaxed);
392 // c:442
393 }
394
395 goto_compend(ret)
396}
397
398// =====================================================================
399// before_complete / after_complete — `Src/Zle/compcore.c:461 / 503`.
400// =====================================================================
401
402/// Direct port of `int before_complete(Hookdef dummy, int *lst)`
403/// from `Src/Zle/compcore.c:461`. Pre-completion hook: snapshots
404/// `menucmp` into `oldmenucmp`, decides whether the current state
405/// shortcircuits via menu-completion, clamps the cursor when re-
406/// entering an in-word completion, and toggles automenu mode.
407/// Returns 1 to suppress the next-stage match build, 0 to continue.
408pub fn before_complete(lst: &mut i32) -> i32 {
409 // c:461
410
411 // c:463 — `oldmenucmp = menucmp;`
412 OLDMENUCMP.store(MENUCMP.load(Ordering::Relaxed), Ordering::Relaxed);
413
414 // c:465-466 — `if (showagain && validlist) showinglist = -2;`
415 if SHOWAGAIN.load(Ordering::Relaxed) != 0 && VALIDLIST.load(Ordering::Relaxed) != 0 {
416 SHOWINGLIST.store(-2, Ordering::Relaxed);
417 }
418 // c:467 — `showagain = 0;`
419 SHOWAGAIN.store(0, Ordering::Relaxed);
420
421 let has_cur = MINFO
422 .get()
423 .and_then(|m| m.lock().ok())
424 .map(|m| m.cur.is_some())
425 .unwrap_or(false);
426 let menucmp_v = MENUCMP.load(Ordering::Relaxed);
427
428 // c:471-474 — menu-completion shortcircuit (non-listing path).
429 // C: `do_menucmp(*lst); return 1;`. An active menu (minfo.cur set,
430 // menucmp on) means this Tab should step the menu cursor and insert the
431 // next match, NOT restart completion.
432 if has_cur && menucmp_v != 0 && *lst != COMP_LIST_EXPAND {
433 if *lst == COMP_LIST_COMPLETE {
434 // do_menucmp c:1258-1260 — just (re)show the list.
435 SHOWINGLIST.store(-2, Ordering::Relaxed);
436 } else {
437 // do_menucmp c:1263-1268 — `if (zlemetaline == NULL) metafy_line()`.
438 // before_complete runs before docomplete metafies the buffer, so
439 // do_single would otherwise edit a stale/unmetafied line and drop
440 // the command prefix (`cat alpha.txt` → `alpine.md`). Metafy the
441 // completion buffer (still holding the previous match's line) so
442 // do_single replaces only the word region tracked by minfo.
443 if ZLEMETALL.load(Ordering::Relaxed) == 0 {
444 metafy_line();
445 }
446 // do_menucmp c:1270-1276 —
447 // while (zmult) { minfo.cur = valid_match(minfo.cur, 1);
448 // zmult -= sign; }
449 // do_single(*minfo.cur);
450 // valid_match advances the menu cursor one valid match in the
451 // ZMULT direction, updating minfo.group_idx/cur_idx; do_single
452 // then replaces the previously-inserted match (tracked via
453 // minfo.pos/len/end) with the new one.
454 let mult = crate::ported::zle::zle_main::ZMOD
455 .lock()
456 .map(|g| g.mult)
457 .unwrap_or(1);
458 ZMULT.store(mult, Ordering::Relaxed);
459 let steps = mult.abs().max(1);
460 let mut mc = None;
461 for _ in 0..steps {
462 let cur_idx = MINFO
463 .get()
464 .and_then(|m| m.lock().ok())
465 .map(|m| m.cur_idx)
466 .unwrap_or(0);
467 mc = crate::ported::zle::compresult::valid_match(cur_idx, 1);
468 }
469 // c:1272 — minfo.cur = valid_match(...); set before do_single so
470 // the insertion state stays consistent with the advanced cursor.
471 if let Ok(mut mst) = MINFO
472 .get_or_init(|| Mutex::new(Menuinfo::default()))
473 .lock()
474 {
475 mst.cur = mc.clone().map(Box::new);
476 }
477 if let Some(ref m) = mc {
478 crate::ported::zle::compresult::do_single(m); // c:1276
479 }
480 }
481 return 1; // c:473
482 }
483 // c:475-479 — menu-completion shortcircuit (listing path).
484 if has_cur
485 && menucmp_v != 0
486 && VALIDLIST.load(Ordering::Relaxed) != 0
487 && *lst == COMP_LIST_COMPLETE
488 {
489 SHOWINGLIST.store(-2, Ordering::Relaxed);
490 onlyexpl.store(0, Ordering::Relaxed); // c:477
491 // c:477 — `listdat.valid = 0;`
492 if let Some(ld) = listdat.get() {
493 if let Ok(mut g) = ld.lock() {
494 g.valid = 0;
495 }
496 }
497 return 1; // c:478
498 }
499
500 // c:489-490 — `if ((fromcomp & FC_INWORD) && (zlecs = lastend) > zlell)
501 // zlecs = zlell;` — re-entering an in-word completion
502 // restores cursor to lastend (clamped to zlell).
503 if (fromcomp.load(Ordering::Relaxed) & crate::ported::zle::comp_h::FC_INWORD) != 0 {
504 let le = lastend.load(Ordering::Relaxed);
505 let ll = ZLEMETALL.load(Ordering::Relaxed);
506 let new_cs = if le > ll { ll } else { le };
507 ZLEMETACS.store(new_cs, Ordering::Relaxed);
508 }
509
510 // c:494-496 — automenu trigger.
511 if startauto.load(Ordering::Relaxed) != 0 && LASTAMBIG.load(Ordering::Relaxed) != 0 {
512 let bashauto = isset(BASHAUTOLIST);
513 let last = LASTAMBIG.load(Ordering::Relaxed);
514 if !bashauto || last == 2 {
515 USEMENU.store(2, Ordering::Relaxed);
516 }
517 }
518
519 0 // c:498
520}
521
522/// Direct port of `int after_complete(Hookdef dummy, int *dat)`
523/// from `Src/Zle/compcore.c:503`. Post-completion hook: when a
524/// completion has just transitioned into menu-completion (menucmp
525/// went 0→1 across this round), runs MENUSTARTHOOK so registered
526/// hook ported can veto or modify the about-to-display menu.
527///
528/// Hook handlers are registered via `addhookfunc("menu_start", fn)`
529/// (see `crate::ported::module::addhookfunc`), which writes to the
530/// global HOOKTAB. C's `comphooks[]` table declares `menu_start` as
531/// HOOKF_ALL, so every handler fires and the first non-zero return
532/// short-circuits the chain (see runhookdef at module.c:990).
533///
534/// Return value semantics (c:518-532):
535/// - `ret == 0` → no action (no handler vetoed).
536/// - `ret >= 1` → zero `dat[1]`, clear menucmp/menuacc, null minfo.cur.
537/// - `ret >= 2` → also rewind buffer to origline.
538/// - `ret == 2` → also schedule list clear (CLEARLIST=1, invalidatelist).
539pub fn after_complete(dat: &mut [i32]) -> i32 {
540 // c:503
541 let menucmp_v = MENUCMP.load(Ordering::Relaxed);
542 let oldmenucmp_v = OLDMENUCMP.load(Ordering::Relaxed);
543
544 // c:505 — `if (menucmp && !oldmenucmp) { ... }`.
545 if menucmp_v == 0 || oldmenucmp_v != 0 {
546 return 0; // c:535
547 }
548
549 // c:506-517 — build chdata. cdat.matches=amatches, cdat.num=
550 // nmatches, cdat.nmesg=nmessages, cdat.cur=NULL. The
551 // Rust hook dispatch path doesn't yet thread chdata
552 // into shell-fn args (handlers in the standard zsh
553 // distribution all read directly from compsys globals
554 // via $compstate). The fields above are still tracked
555 // via amatches/nmatches/nmessages globals and visible
556 // to handlers through the normal completion-state
557 // parameter reads.
558
559 // c:518 — `runhookdef(MENUSTARTHOOK, &cdat)`. Canonical dispatch
560 // via `gethookdef("menu_start") + runhookdef(h, &cdat)`. Returns
561 // 0 when no Hookfn is registered (matches c:993-995: empty funcs
562 // and h->def NULL → return 0).
563 let mut ret: i32 = 0;
564 let h_menu_start = gethookdef("menu_start");
565 if !h_menu_start.is_null() {
566 ret = runhookdef(h_menu_start, std::ptr::null_mut());
567 }
568
569 if ret == 0 {
570 return 0; // c:535
571 }
572
573 // c:519 — `dat[1] = 0`. The C caller passes a 2-int array; index 1
574 // carries the menu-acceptance flag for the outer compfunc loop.
575 if dat.len() > 1 {
576 dat[1] = 0;
577 }
578 // c:520 — `menucmp = menuacc = 0`.
579 MENUCMP.store(0, Ordering::Relaxed);
580 menuacc.store(0, Ordering::Relaxed);
581 // c:521 — `minfo.cur = NULL`.
582 if let Some(m) = MINFO.get() {
583 if let Ok(mut mi) = m.lock() {
584 mi.cur = None;
585 }
586 }
587
588 if ret >= 2 {
589 // c:522
590 // c:523 — `fixsuffix()`.
591 fixsuffix();
592 // c:524 — `zlemetacs = 0`.
593 ZLEMETACS.store(0, Ordering::Relaxed);
594 // c:525 — `foredel(zlemetall, CUT_RAW)` removes the entire line.
595 let metall = ZLEMETALL.load(Ordering::Relaxed);
596 foredel(metall, CUT_RAW);
597 // c:526 — `inststr(origline)` reinserts the pre-completion buffer.
598 let origline_v: String = ORIGLINE
599 .get()
600 .and_then(|m| m.lock().ok().map(|g| g.clone()))
601 .unwrap_or_default();
602 let _ = inststr(&origline_v);
603 // c:527 — `zlemetacs = origcs`.
604 let origcs_v = ORIGCS.load(Ordering::Relaxed);
605 ZLEMETACS.store(origcs_v, Ordering::Relaxed);
606
607 if ret == 2 {
608 // c:528
609 // c:529 — `clearlist = 1`.
610 CLEARLIST.store(1, Ordering::Relaxed);
611 // c:530 — `invalidatelist()`.
612 invalidatelist();
613 }
614 }
615
616 0 // c:535
617}
618
619// =====================================================================
620// callcompfunc — `Src/Zle/compcore.c:544`.
621// =====================================================================
622
623/// Port of `static void callcompfunc(char *s, char *fn)` from
624/// compcore.c:544. Selects the `$compstate[context]` value, then
625/// dispatches into the user shell function `fn`. Paramtab setup
626/// (`comprpms`/`compkpms`) + result-readback is stubbed locally
627/// per PORT.md Rule 9 until `params.c` substrate lands.
628pub fn callcompfunc(s: &str, fn_name: &str) {
629 tracing::debug!(target: "compsys_args", %s, %fn_name, "callcompfunc ENTER");
630 // c:544
631
632 if fn_name.is_empty() {
633 return;
634 } // c:552 getshfunc(NULL)
635 let _lv = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed); // c:548 int lv = lastval
636 let _icf = INCOMPFUNC.load(Ordering::Relaxed); // c:555
637 let _osc = crate::ported::builtin::SFCONTEXT.load(Ordering::Relaxed); // c:555
638
639 let _useglob = USEGLOB.load(Ordering::Relaxed); // c:579
640
641 // Publish the completion word split at the cursor into the
642 // `$PREFIX` / `$SUFFIX` params (+ empty ignored-prefix/suffix). In C
643 // these are gsu-bound to `compprefix`/`compsuffix`; the Rust ports
644 // have no gsu binding, so without this every completer reads
645 // `$PREFIX=''` — `_main_complete`'s `compset -P 1 '='` then matches
646 // the empty prefix and wrongly forces `$compstate[context]=equal`,
647 // and `_path_files` has no prefix to glob. Split at `OFFS`
648 // (zlemetacs - wb), the cursor offset within the word.
649 {
650 let scs: Vec<char> = s.chars().collect();
651 let off = (OFFS.load(Ordering::Relaxed).max(0) as usize).min(scs.len());
652 let pre: String = scs[..off].iter().collect();
653 let suf: String = scs[off..].iter().collect();
654 let _ = crate::ported::params::setsparam("PREFIX", &pre);
655 let _ = crate::ported::params::setsparam("SUFFIX", &suf);
656 let _ = crate::ported::params::setsparam("IPREFIX", "");
657 let _ = crate::ported::params::setsparam("ISUFFIX", "");
658 let _ = crate::ported::params::setsparam("QIPREFIX", "");
659 let _ = crate::ported::params::setsparam("QISUFFIX", "");
660 }
661
662 // c:591-617 — context selection.
663 let context = compcontext_for(s); // c:591-617
664 set_compstate_str("context", &context); // c:619
665
666 // c:721-727 — `$compstate[last_prompt]` etc. fed in from
667 // do_completion via dolastprompt; we forward the current values.
668 set_compstate_str(
669 "last_prompt",
670 if dolastprompt.load(Ordering::Relaxed) != 0 {
671 "yes"
672 } else {
673 ""
674 },
675 );
676
677 // c:740-749 — `$compstate[list]` — set from `complist` global.
678 let cl_value = COMPLIST
679 .get_or_init(|| Mutex::new(String::new()))
680 .lock()
681 .map(|g| g.clone())
682 .unwrap_or_default();
683 set_compstate_str("list", &cl_value); // c:740
684
685 // c:768-785 — `$compstate[insert]` per (useline, usemenu).
686 let ul = useline.load(Ordering::Relaxed);
687 let um = USEMENU.load(Ordering::Relaxed);
688 let ins = if ul != 0 {
689 match um {
690 0 => "unambiguous",
691 1 => "menu",
692 2 => "automenu",
693 _ => "",
694 }
695 } else {
696 ""
697 };
698 set_compstate_str("insert", ins); // c:770
699
700 // c:790-794 — `$compstate[exact]` & `$compstate[exact_string]`.
701 set_compstate_str(
702 "exact",
703 if useexact.load(Ordering::Relaxed) != 0 {
704 "accept"
705 } else {
706 ""
707 },
708 );
709
710 // c:800-803 — `$compstate[to_end]` per movetoend.
711 set_compstate_str(
712 "to_end",
713 if movetoend.load(Ordering::Relaxed) == 1 {
714 "single"
715 } else {
716 "match"
717 },
718 );
719
720 // c:838 — `incompfunc = 1` before invoking the user fn.
721 INCOMPFUNC.store(1, Ordering::Relaxed); // c:838
722
723 // c:828-832 — `largs = newlinklist(); addlinknode(largs,
724 // dupstring(fn)); while (*cfargs) addlinknode(largs,
725 // dupstring(*p++));`. argv[0] = function name, then the
726 // wrapper-widget args stored in `cfargs` by `completecall`.
727 let largs: Vec<String> = {
728 let mut v = vec![fn_name.to_string()];
729 if let Ok(cf) = crate::ported::zle::zle_tricky::cfargs.lock() {
730 v.extend(cf.iter().cloned());
731 }
732 v
733 };
734
735 // c:833-834 — `int oxt = isset(XTRACE); opts[XTRACE] = 0;`. Mute
736 // xtrace during the body so PS4 noise doesn't appear from every
737 // compsys helper line.
738 let oxt = crate::ported::zsh_h::isset(crate::ported::zsh_h::XTRACE) as i32;
739 crate::ported::options::opt_state_set(
740 &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
741 false,
742 );
743 let _ = oxt; // c:833 saved for restore at c:836
744
745 // c:835 — `cfret = doshfunc(shfunc, largs, 1)`. The body runner
746 // closure resolves the actual implementation:
747 // - If a Rust compsys port is registered for `fn_name` and
748 // `backend = "rust"`, run that.
749 // - Else autoload + run the upstream shfunc body via the
750 // standard dispatch path. dispatch_function_call already
751 // wraps the fusevm Chunk in its own doshfunc scope; we
752 // intentionally call only the body half here so the C-faithful
753 // prologue/epilogue runs exactly once around the body.
754 let largs_for_body = largs.clone();
755 let fn_name_owned = fn_name.to_string();
756 let body_runner = move || -> i32 {
757 // c:6042 — `runshfunc(prog, wrappers, name)`. zshrs runs the
758 // body via either the Rust compsys port (direct fn call) or
759 // the fusevm Chunk dispatch (via exec accessors).
760 if let Some(rust_fn) = crate::compsys::router::try_rust_dispatch(&fn_name_owned) {
761 // C convention: largs[0] = fn name, [1..] = real argv.
762 return rust_fn(&largs_for_body[1..]);
763 }
764 crate::ported::exec::dispatch_function_call(&fn_name_owned, &largs_for_body[1..])
765 .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed))
766 };
767
768 // Look up the real shfunc; if missing we still want doshfunc's
769 // scope around the Rust port (synth_shf carries just the name).
770 let mut synth_shf = crate::ported::zsh_h::shfunc {
771 node: crate::ported::zsh_h::hashnode {
772 next: None,
773 nam: fn_name.to_string(),
774 flags: 0,
775 },
776 filename: None,
777 lineno: 0,
778 funcdef: None,
779 redir: None,
780 sticky: None,
781 body: None,
782 };
783 let cfret_val = crate::ported::exec::doshfunc(&mut synth_shf, largs, true, body_runner);
784 crate::ported::zle::zle_tricky::cfret.store(cfret_val, Ordering::Relaxed);
785
786 // c:836 — `opts[XTRACE] = oxt;` restore xtrace state.
787 crate::ported::options::opt_state_set(
788 &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
789 oxt != 0,
790 );
791
792 // c:909-912 — unwind: read `$compstate[insert]` etc. back into
793 // the compcore globals so do_completion sees the user fn's
794 // mutations.
795 // Read `$compstate[insert]` via the compstate hash (the canonical
796 // home), NOT the flat `compstate[insert]` bracketed param: the latter
797 // reads empty here because the completion fn's `compstate[insert]=menu`
798 // write lands in the hash storage while the flat param is scoped to the
799 // fn. Reading the flat param missed the menu decision entirely, so
800 // USEMENU never became 1 and menu-completion (→ menucmp → the
801 // menu_start hook → domenuselect) never started.
802 let post_insert = get_compstate_str("insert").unwrap_or_default();
803 if !post_insert.is_empty() {
804 if post_insert.contains("automenu") {
805 USEMENU.store(2, Ordering::Relaxed);
806 } else if post_insert.contains("menu") {
807 USEMENU.store(1, Ordering::Relaxed);
808 }
809 }
810
811 // c:914 — incompfunc = icf. Restore.
812 INCOMPFUNC.store(_icf, Ordering::Relaxed);
813}
814
815// =====================================================================
816// makecomplist — `Src/Zle/compcore.c:946`.
817// =====================================================================
818
819/// Direct port of `int makecomplist(char *s, int incmd, int lst)` from
820/// compcore.c:946. Top-level dispatch into the completion subsystem:
821/// either the new compsys path (`callcompfunc`) or the legacy compctl
822/// path (`COMPCTLMAKEHOOK`).
823pub fn makecomplist(s: &str, incmd: i32, lst: i32) -> i32 {
824 // c:946
825 let owb = WB.load(Ordering::Relaxed); // c:946
826 let owe = WE.load(Ordering::Relaxed);
827 let ooffs = OFFS.load(Ordering::Relaxed);
828
829 // c:952-958 — `if (compfunc && (p = check_param(s, 0, 0)))`.
830 let mut s_owned = s.to_string();
831 if compfunc_active() {
832 if let Some(p) = check_param(&s_owned, false, false) {
833 // c:952
834 s_owned = s_owned[p..].to_string(); // c:953 s = p
835 PARWB.store(owb, Ordering::Relaxed); // c:954
836 PARWE.store(owe, Ordering::Relaxed); // c:955
837 PAROFFS.store(ooffs, Ordering::Relaxed); // c:956
838 } else {
839 PARWB.store(-1, Ordering::Relaxed); // c:958
840 }
841 } else {
842 PARWB.store(-1, Ordering::Relaxed); // c:958
843 }
844
845 linwhat.store(INWHAT.load(Ordering::Relaxed), Ordering::Relaxed); // c:960
846
847 if compfunc_active() {
848 // c:962
849 let os = s_owned.clone(); // c:964
850 let onm = nmatches.load(Ordering::Relaxed); // c:965
851 let odm = diffmatches.load(Ordering::Relaxed); // c:965
852 let osi = movefd(0); // c:965 movefd(0)
853
854 // c:967-968 — bmatchers = mstack = NULL.
855 if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
856 *g = None;
857 }
858 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
859 *g = None;
860 }
861 // c:970-971 — ainfo = fainfo = hcalloc(sizeof(struct aminfo)).
862 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
863 *g = Some(Aminfo::default());
864 }
865 if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
866 *g = Some(Aminfo::default());
867 }
868 if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
869 *g = None; // c:973
870 }
871 if VALIDLIST.load(Ordering::Relaxed) == 0 {
872 LASTAMBIG.store(0, Ordering::Relaxed);
873 // c:976
874 }
875 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
876 g.clear(); // c:977
877 }
878 mnum.store(0, Ordering::Relaxed); // c:978
879 unambig_mnum.store(-1, Ordering::Relaxed); // c:979
880 if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
881 g.clear(); // c:980
882 }
883 insmnum.store(ZMULT.load(Ordering::Relaxed), Ordering::Relaxed); // c:981
884 oldlist.store(0, Ordering::Relaxed); // c:986
885 oldins.store(0, Ordering::Relaxed); // c:986
886 begcmgroup(Some("default"), 0); // c:987
887 MENUCMP.store(0, Ordering::Relaxed); // c:988
888 menuacc.store(0, Ordering::Relaxed); // c:988
889 newmatches.store(0, Ordering::Relaxed); // c:988
890 onlyexpl.store(0, Ordering::Relaxed); // c:988
891
892 let dup_s = crate::ported::mem::dupstring(&os); // c:990
893 let cf_name = compfunc
894 .get_or_init(|| Mutex::new(None))
895 .lock()
896 .ok()
897 .and_then(|g| g.clone())
898 .unwrap_or_default();
899 callcompfunc(&dup_s, &cf_name); // c:991
900 endcmgroup(None); // c:992
901
902 // c:995 — runhookdef(COMPCTLCLEANUPHOOK, NULL).
903 runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:995
904
905 if oldlist.load(Ordering::Relaxed) != 0 {
906 // c:997
907 nmatches.store(onm, Ordering::Relaxed); // c:998
908 diffmatches.store(odm, Ordering::Relaxed); // c:999
909 VALIDLIST.store(1, Ordering::Relaxed); // c:1000
910 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
911 if let Ok(last) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
912 *g = last.clone(); // c:1001
913 }
914 }
915 if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
916 let last_l = lastlmatches
917 .get_or_init(|| Mutex::new(None))
918 .lock()
919 .ok()
920 .and_then(|g| g.clone());
921 *g = last_l; // c:1007
922 }
923 // c:1008-1011 — `if (pmatches) freematches(pmatches, 1)`.
924 let drained = pmatches
925 .get_or_init(|| Mutex::new(Vec::new()))
926 .lock()
927 .map(|mut g| std::mem::take(&mut *g))
928 .unwrap_or_default();
929 freematches(drained, 1); // c:1009-1010
930 hasperm.store(0, Ordering::Relaxed); // c:1011
931 redup(osi); // c:1012
932 return 0; // c:1013
933 }
934 if !lastmatches
935 .get_or_init(|| Mutex::new(Vec::new()))
936 .lock()
937 .map(|g| g.is_empty())
938 .unwrap_or(true)
939 {
940 // c:1015
941 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
942 g.clear(); // c:1016-1017
943 }
944 }
945 permmatches(1); // c:1019
946 // c:1020-1029 — copy pmatches → amatches/lastmatches; swap holders.
947 let p_snap = pmatches
948 .get_or_init(|| Mutex::new(Vec::new()))
949 .lock()
950 .ok()
951 .map(|g| g.clone())
952 .unwrap_or_default();
953 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
954 *g = p_snap.clone(); // c:1020
955 }
956 lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1021
957 lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1022
958 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
959 *g = p_snap; // c:1024
960 }
961 let lm_snap = lmatches
962 .get_or_init(|| Mutex::new(None))
963 .lock()
964 .ok()
965 .and_then(|g| g.clone());
966 if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
967 *g = lm_snap; // c:1025
968 }
969 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
970 g.clear(); // c:1026
971 }
972 hasperm.store(0, Ordering::Relaxed); // c:1027
973 hasoldlist.store(1, Ordering::Relaxed); // c:1028
974
975 let any_nm =
976 nmatches.load(Ordering::Relaxed) != 0 || nmessages.load(Ordering::Relaxed) != 0;
977 let errset = errflag_get();
978 tracing::debug!(
979 target: "compsys_args",
980 nm = nmatches.load(Ordering::Relaxed),
981 nmsg = nmessages.load(Ordering::Relaxed),
982 errset,
983 "makecomplist RETURN"
984 );
985 if any_nm && !errset {
986 // c:1030
987 VALIDLIST.store(1, Ordering::Relaxed); // c:1031
988 redup(osi); // c:1032
989 return 0; // c:1033
990 }
991 redup(osi); // c:1035
992 return 1; // c:1036
993 } else {
994 // c:1038
995 // c:1040-1047 — compctl dispatch via COMPCTLMAKEHOOK.
996 let mut dat = Ccmakedat {
997 str: Some(s_owned.clone()), // c:1042
998 incmd, // c:1043
999 lst, // c:1044
1000 };
1001 runhookdef_compctlmake(&mut dat); // c:1045
1002 runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:1048
1003 return dat.lst; // c:1050
1004 }
1005}
1006
1007// =====================================================================
1008// multiquote — `Src/Zle/compcore.c:1065`.
1009// =====================================================================
1010
1011/// Port of `mod_export char *multiquote(char *s, int ign)` from
1012/// compcore.c:1064.
1013pub fn multiquote(s: &str, ign: i32) -> String {
1014 // c:1065
1015 let stack = COMPQSTACK // c:1065
1016 .get_or_init(|| Mutex::new(String::new()))
1017 .lock()
1018 .map(|g| g.clone())
1019 .unwrap_or_default();
1020 let p_bytes = stack.as_bytes();
1021 if !p_bytes.is_empty() && (ign == 0 || p_bytes.len() > 1) {
1022 // c:1070
1023 let start = if ign != 0 { 1 } else { 0 }; // c:1071
1024 let mut cur = s.to_string();
1025 for &q in &p_bytes[start..] {
1026 // c:1073
1027 let qt = match q as i32 {
1028 // c:1074
1029 x if x == QT_BACKSLASH => QT_BACKSLASH,
1030 x if x == QT_SINGLE => QT_SINGLE,
1031 x if x == QT_DOUBLE => QT_DOUBLE,
1032 x if x == QT_DOLLARS => QT_DOLLARS,
1033 _ => QT_BACKSLASH,
1034 };
1035 cur = crate::ported::utils::quotestring(&cur, qt);
1036 }
1037 cur // c:1092
1038 } else {
1039 s.to_string() // c:1092
1040 }
1041}
1042
1043// =====================================================================
1044// tildequote — `Src/Zle/compcore.c:1092`.
1045// =====================================================================
1046
1047/// Port of `mod_export char *tildequote(char *s, int ign)` from
1048/// compcore.c:1091.
1049pub fn tildequote(s: &str, ign: i32) -> String {
1050 // c:1092
1051 let bytes = s.as_bytes(); // c:1092
1052 let tilde = !bytes.is_empty() && bytes[0] == b'~'; // c:1097
1053 let staged = if tilde {
1054 // c:1098
1055 let mut tmp = String::with_capacity(s.len());
1056 tmp.push('x');
1057 tmp.push_str(&s[1..]);
1058 tmp
1059 } else {
1060 s.to_string()
1061 };
1062 let mut quoted = multiquote(&staged, ign); // c:1099
1063 if tilde && !quoted.is_empty() {
1064 // c:1100
1065 let mut new_q = String::with_capacity(quoted.len());
1066 let mut swapped = false;
1067 for c in quoted.chars() {
1068 if !swapped && c == 'x' {
1069 new_q.push('~');
1070 swapped = true;
1071 } else {
1072 new_q.push(c);
1073 }
1074 }
1075 quoted = new_q;
1076 }
1077 quoted // c:1101
1078}
1079
1080// =====================================================================
1081// check_param — `Src/Zle/compcore.c:1113`.
1082// =====================================================================
1083
1084/// Direct port of `static char *check_param(char *s, int set, int test)`
1085/// from compcore.c:1113. Walks backwards from cursor in `s` looking
1086/// for `$<name>`. When found and the cursor sits inside the name,
1087/// returns the byte index in `s` where the name starts; updates
1088/// `ispar`/`parq`/`eparq` (when `!test`) and `ipre`/`ripre`/`isuf`/
1089/// `parpre`/`parflags`/`mflags`/`wb`/`we`/`offs` (when `set`).
1090/// Returns `None` when there's no parameter expression at the cursor.
1091pub fn check_param(s: &str, set: bool, test: bool) -> Option<usize> {
1092 // c:1113
1093
1094 // c:1117-1118 — zsfree(parpre); parpre = NULL.
1095 if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1096 g.clear();
1097 }
1098
1099 if !test {
1100 // c:1120
1101 ispar.store(0, Ordering::Relaxed); // c:1121
1102 parq.store(0, Ordering::Relaxed); // c:1121
1103 eparq.store(0, Ordering::Relaxed); // c:1121
1104 }
1105
1106 let bytes = s.as_bytes(); // local view
1107 let offs_v = OFFS.load(Ordering::Relaxed) as usize; // c:1140 cursor in word
1108
1109 let mut found = false; // c:1115
1110 let mut qstring = false; // c:1115
1111 let mut p: usize = offs_v.min(bytes.len().saturating_sub(1)); // c:1140 p = s + offs
1112
1113 // get_comp_string returns the word untokenized, so the `$` sigil
1114 // arrives as a literal 0x24 rather than the String token C scans for.
1115 // Treat the literal `$` as equivalent to the String token here so
1116 // `$VAR<Tab>` parameter completion fires; the len_utf8-based cursor
1117 // math below already handles the 1-byte literal vs 2-byte token.
1118 let is_str = |c: char| c == Stringg || c == '$';
1119
1120 // c:1140-1162 — scan backward for `String` or `Qstring`.
1121 loop {
1122 if p < bytes.len() {
1123 let ch = char_at(bytes, p);
1124 if is_str(ch) || ch == Qstring {
1125 // c:1141
1126 let next = char_at(bytes, p + ch.len_utf8());
1127 let snull_next = is_str(ch) && next == Snull; // c:1151
1128 let qstr_quot = ch == Qstring && next == '\''; // c:1152
1129 if p < offs_v && !snull_next && !qstr_quot {
1130 found = true; // c:1154
1131 qstring = ch == Qstring; // c:1155
1132 break;
1133 }
1134 }
1135 }
1136 if p == 0 {
1137 break;
1138 } // c:1160
1139 p = prev_char_index(bytes, p);
1140 }
1141
1142 if found {
1143 // c:1166
1144 // c:1173-1174 — fold `$$$$` chains.
1145 while p > 0 {
1146 let prev = prev_char_index(bytes, p);
1147 let pc = char_at(bytes, prev);
1148 if is_str(pc) || pc == Qstring {
1149 p = prev;
1150 } else {
1151 break;
1152 }
1153 }
1154 loop {
1155 // c:1175-1176
1156 let n1 = p + char_at(bytes, p).len_utf8();
1157 if n1 >= bytes.len() {
1158 break;
1159 }
1160 let c1 = char_at(bytes, n1);
1161 let n2 = n1 + c1.len_utf8();
1162 if n2 >= bytes.len() {
1163 break;
1164 }
1165 let c2 = char_at(bytes, n2);
1166 if (is_str(c1) || c1 == Qstring) && (is_str(c2) || c2 == Qstring) {
1167 p = n2;
1168 } else {
1169 break;
1170 }
1171 }
1172 }
1173
1174 // c:1179 — guard against `$(`, `$[`, `$'`.
1175 let next_char = if p + 1 <= bytes.len() {
1176 let dollar_len = char_at(bytes, p).len_utf8();
1177 char_at(bytes, p + dollar_len)
1178 } else {
1179 '\0'
1180 };
1181 if !(found && next_char != Inpar && next_char != Inbrack && next_char != Snull) {
1182 return None; // c:1316
1183 }
1184
1185 // c:1181 — b = p + 1 (start of body), e = b initially.
1186 let dollar_len = char_at(bytes, p).len_utf8();
1187 let mut b: usize = p + dollar_len; // c:1181
1188 let mut br: i32 = 1; // c:1182
1189 let mut nest: i32 = 0; // c:1182
1190
1191 // get_comp_string returns the word untokenized, so `${…}` arrives with a
1192 // literal `{`/`}` rather than the Inbrace/Outbrace tokens C matches here;
1193 // accept either so `${PA<Tab>` is recognized as a braced parameter.
1194 let brace_ch = char_at(bytes, b);
1195 if brace_ch == Inbrace || brace_ch == '{' {
1196 let (ib, ob) = if brace_ch == '{' {
1197 ('{', '}')
1198 } else {
1199 (Inbrace, Outbrace)
1200 };
1201 // c:1184
1202 // c:1188 — `if (!skipparens(Inbrace, Outbrace, &tb) && tb - s <= offs) return NULL;`
1203 let mut tb: &str = &s[b..];
1204 let bal = crate::ported::utils::skipparens(ib, ob, &mut tb);
1205 let tb_after = s.len() - tb.len();
1206 if bal == 0 && tb_after <= offs_v {
1207 return None; // c:1189
1208 }
1209
1210 b += brace_ch.len_utf8(); // c:1192 b++
1211 br += 1;
1212 // c:1193-1203 — skip leading `(...)` flag group. C has a
1213 // ternary `qstring ? skipparens('(',')',&b) : skipparens(Inpar,Outpar,&b)`
1214 // — two source-level skipparens calls. Mirror that explicitly
1215 // so the call-coverage metric matches C.
1216 let mut b_str: &str = &s[b..];
1217 let flag_ret: i32 = if qstring {
1218 crate::ported::utils::skipparens('(', ')', &mut b_str)
1219 } else {
1220 crate::ported::utils::skipparens(Inpar, Outpar, &mut b_str)
1221 };
1222 let after_flags_pos = s.len() - b_str.len();
1223 if flag_ret > 0 || after_flags_pos > offs_v {
1224 ispar.store(2, Ordering::Relaxed); // c:1201
1225 return None; // c:1202
1226 }
1227 b = after_flags_pos;
1228
1229 // c:1205 — detect `nest` from preceding `${ ${` chain.
1230 let mut tb = p;
1231 while tb > 0 {
1232 let prev = prev_char_index(bytes, tb);
1233 let pc = char_at(bytes, prev);
1234 if pc == Outbrace || pc == Inbrace {
1235 tb = prev;
1236 break;
1237 }
1238 tb = prev;
1239 }
1240 if tb > 0 {
1241 let cc = char_at(bytes, tb);
1242 let prev = prev_char_index(bytes, tb);
1243 let pp = char_at(bytes, prev);
1244 if cc == Inbrace && (pp == Stringg || cc == Qstring) {
1245 nest = 1; // c:1207
1246 }
1247 }
1248 }
1249
1250 // c:1212-1213 — skip `^=~` prefix flags.
1251 while b < bytes.len() {
1252 let c = char_at(bytes, b);
1253 if c == '^' || c == Hat || c == '=' || c == Equals || c == '~' || c == Tilde {
1254 b += c.len_utf8();
1255 } else {
1256 break;
1257 }
1258 }
1259 // c:1215 — `#` / `+` length-prefix.
1260 if b < bytes.len() {
1261 let c = char_at(bytes, b);
1262 if c == '#' || c == Pound || c == '+' {
1263 b += c.len_utf8();
1264 }
1265 }
1266
1267 let mut e: usize = b; // c:1219
1268 if br != 0 {
1269 // c:1220
1270 let qopen = if test { Dnull } else { '"' };
1271 while e < bytes.len() && char_at(bytes, e) == qopen {
1272 // c:1221
1273 e += qopen.len_utf8();
1274 parq.fetch_add(1, Ordering::Relaxed); // c:1221
1275 }
1276 if !test {
1277 b = e;
1278 } // c:1223
1279 }
1280
1281 // c:1226-1252 — find end of name.
1282 if e < bytes.len() {
1283 let c = char_at(bytes, e);
1284 let one_char_name = matches!(c,
1285 ch if ch == Quest || ch == Star || ch == Stringg || ch == Qstring
1286 || ch == '?' || ch == '*' || ch == '$' || ch == '-' || ch == '!' || ch == '@');
1287 if one_char_name {
1288 // c:1230
1289 e += c.len_utf8();
1290 } else if c.is_ascii_digit() {
1291 // c:1232
1292 while e < bytes.len() && char_at(bytes, e).is_ascii_digit() {
1293 // c:1233
1294 e += 1;
1295 }
1296 } else {
1297 // c:1235-1245 — itype_end(INAMESPC) walk.
1298 let walked = walk_namespace(&bytes[e..]);
1299 if walked > 0 {
1300 e += walked;
1301 } else if c == '.' {
1302 // c:1255
1303 e += 1;
1304 }
1305 }
1306 }
1307
1308 // c:1259 — `if (offs <= e - s && offs >= b - s)`.
1309 if offs_v <= e && offs_v >= b {
1310 // c:1263 — strip trailing `"`s when br set.
1311 if br != 0 {
1312 let qopen = if test { Dnull } else { '"' };
1313 let mut pq = e;
1314 while pq < bytes.len() && char_at(bytes, pq) == qopen {
1315 pq += qopen.len_utf8();
1316 parq.fetch_sub(1, Ordering::Relaxed);
1317 eparq.fetch_add(1, Ordering::Relaxed);
1318 }
1319 }
1320 if test {
1321 // c:1269
1322 return Some(b); // c:1270
1323 }
1324 if set {
1325 // c:1273
1326 if br >= 2 {
1327 // c:1274
1328 mflags.fetch_or(CMF_PARBR, Ordering::Relaxed); // c:1275
1329 if nest != 0 {
1330 // c:1276
1331 mflags.fetch_or(CMF_PARNEST, Ordering::Relaxed); // c:1277
1332 }
1333 }
1334 // c:1280 — `isuf = dupstring(e); untokenize(isuf)`.
1335 let mut tail = String::from_utf8_lossy(&bytes[e..]).into_owned();
1336 tail = strip_tokens(&tail); // crate::lex::untokenize substitute
1337 if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
1338 *g = tail;
1339 }
1340 // c:1284 — `ripre = dyncat(ripre, s_through_b)`.
1341 let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1342 if let Ok(mut g) = ripre.get_or_init(|| Mutex::new(String::new())).lock() {
1343 *g = format!("{}{}", *g, head);
1344 }
1345 if let Ok(mut g) = ipre.get_or_init(|| Mutex::new(String::new())).lock() {
1346 *g = strip_tokens(&format!("{}{}", *g, head));
1347 }
1348 }
1349 // c:1295 — save prefix for compfunc.
1350 let cf_active = compfunc
1351 .get_or_init(|| Mutex::new(None))
1352 .lock()
1353 .ok()
1354 .and_then(|g| g.clone())
1355 .map(|s| !s.is_empty())
1356 .unwrap_or(false);
1357 if cf_active {
1358 let pf = if br >= 2 {
1359 CMF_PARBR | (if nest != 0 { CMF_PARNEST } else { 0 })
1360 } else {
1361 0
1362 };
1363 parflags.store(pf, Ordering::Relaxed); // c:1298
1364 let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1365 if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1366 *g = strip_tokens(&head); // c:1301
1367 }
1368 }
1369 // c:1306 — adjust wb/we/offs.
1370 let off_delta = b as i32;
1371 OFFS.fetch_sub(off_delta, Ordering::Relaxed); // c:1306
1372 let new_offs = OFFS.load(Ordering::Relaxed);
1373 let zlc = ZLEMETACS.load(Ordering::Relaxed);
1374 WB.store(zlc - new_offs, Ordering::Relaxed); // c:1307
1375 WE.store(
1376 WB.load(Ordering::Relaxed) + (e - b) as i32,
1377 Ordering::Relaxed,
1378 ); // c:1308
1379 ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1309
1380 return Some(b); // c:1311
1381 } else if offs_v > e && e < bytes.len() && char_at(bytes, e) == ':' {
1382 // c:1312
1383 // c:1313-1316 — colon-modifier guess.
1384 let offsptr = offs_v;
1385 let mut e2 = e;
1386 while e2 < offsptr && e2 < bytes.len() {
1387 let c = char_at(bytes, e2);
1388 if c != ':' && !c.is_alphanumeric() {
1389 break;
1390 }
1391 e2 += c.len_utf8();
1392 }
1393 ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1316
1394 return None; // c:1317
1395 }
1396
1397 let _ = (Bnull,); // silence unused-import warning if Bnull not hit
1398 None // c:1320
1399}
1400
1401// =====================================================================
1402// rembslash — `Src/Zle/compcore.c:1323`.
1403// =====================================================================
1404
1405/// Port of `mod_export char *rembslash(char *s)` from compcore.c:1322.
1406///
1407/// "Strip backslash escapes from a token, treating `\X` as `X`."
1408pub fn rembslash(s: &str) -> String {
1409 // c:1323
1410 let mut result = String::with_capacity(s.len()); // c:1323
1411 let mut chars = s.chars().peekable(); // c:1327
1412 while let Some(c) = chars.next() {
1413 if c == '\\' {
1414 // c:1328
1415 if let Some(nxt) = chars.next() {
1416 // c:1329
1417 result.push(nxt);
1418 }
1419 } else {
1420 result.push(c); // c:1343-1333
1421 }
1422 }
1423 result // c:1343
1424}
1425
1426// =====================================================================
1427// remsquote — `Src/Zle/compcore.c:1343`.
1428// =====================================================================
1429
1430/// Port of `mod_export int remsquote(char *s)` from compcore.c:1342.
1431pub fn remsquote(s: &mut String) -> i32 {
1432 // c:1343
1433 let rcquotes = isset(RCQUOTES); // c:1343
1434 let qa: usize = if rcquotes { 1 } else { 3 };
1435
1436 let bytes = s.as_bytes(); // c:1346
1437 let mut t = Vec::<u8>::with_capacity(bytes.len());
1438 let mut ret: i32 = 0;
1439 let mut i = 0;
1440 while i < bytes.len() {
1441 // c:1348
1442 let matched = if qa == 1 {
1443 // c:1349
1444 i + 1 < bytes.len() && bytes[i] == b'\'' && bytes[i + 1] == b'\''
1445 } else {
1446 i + 3 < bytes.len() // c:1351
1447 && bytes[i] == b'\''
1448 && bytes[i + 1] == b'\\'
1449 && bytes[i + 2] == b'\''
1450 && bytes[i + 3] == b'\''
1451 };
1452 if matched {
1453 ret += qa as i32; // c:1352
1454 t.push(b'\''); // c:1353
1455 i += qa + 1; // c:1354
1456 } else {
1457 t.push(bytes[i]); // c:1356
1458 i += 1;
1459 }
1460 }
1461 *s = String::from_utf8(t).unwrap_or_default(); // c:1357
1462 ret // c:1366
1463}
1464
1465// =====================================================================
1466// ctokenize — `Src/Zle/compcore.c:1366`.
1467// =====================================================================
1468
1469/// Port of `mod_export char *ctokenize(char *p)` from compcore.c:1365.
1470///
1471/// C calls `tokenize(p)` first then walks the string replacing
1472/// unescaped `$`/`{`/`}` with the token bytes `String`/`Inbrace`/
1473/// `Outbrace`. Backslash-escaped variants become `Bnull`.
1474pub fn ctokenize(p: &str) -> String {
1475 // c:1366
1476 let bytes = p.as_bytes(); // c:1366
1477 let mut out = Vec::<u8>::with_capacity(bytes.len());
1478 let mut bslash = false; // c:1369
1479 let mut prev_idx: Option<usize> = None;
1480 let mut i = 0;
1481 while i < bytes.len() {
1482 let b = bytes[i]; // c:1373
1483 if b == b'\\' {
1484 // c:1374
1485 bslash = true;
1486 out.push(b);
1487 prev_idx = Some(out.len() - 1);
1488 } else {
1489 if b == b'$' || b == b'{' || b == b'}' {
1490 // c:1377
1491 if bslash {
1492 // c:1378
1493 if let Some(pi) = prev_idx {
1494 // c:1379
1495 out.truncate(pi);
1496 let mut buf = [0u8; 4];
1497 out.extend_from_slice(Bnull.encode_utf8(&mut buf).as_bytes());
1498 }
1499 out.push(b);
1500 } else {
1501 let tok = if b == b'$' {
1502 Stringg
1503 }
1504 // c:1381
1505 else if b == b'{' {
1506 Inbrace
1507 }
1508 // c:1382
1509 else {
1510 Outbrace
1511 }; // c:1382
1512 let mut buf = [0u8; 4];
1513 out.extend_from_slice(tok.encode_utf8(&mut buf).as_bytes());
1514 }
1515 } else {
1516 out.push(b);
1517 }
1518 bslash = false; // c:1384
1519 prev_idx = Some(out.len().saturating_sub(1));
1520 }
1521 i += 1;
1522 }
1523 String::from_utf8(out).unwrap_or_default() // c:1403
1524}
1525
1526// =====================================================================
1527// comp_str — `Src/Zle/compcore.c:1403`.
1528// =====================================================================
1529
1530/// Port of `mod_export char *comp_str(int *ipl, int *pl, int untok)`
1531/// from compcore.c:1402.
1532pub fn comp_str(untok: bool) -> (String, i32, i32) {
1533 // c:1403
1534 let mut p = COMPPREFIX
1535 .get_or_init(|| Mutex::new(String::new())) // c:1405
1536 .lock()
1537 .unwrap()
1538 .clone();
1539 let mut s = COMPSUFFIX
1540 .get_or_init(|| Mutex::new(String::new())) // c:1406
1541 .lock()
1542 .unwrap()
1543 .clone();
1544 let ip = COMPIPREFIX
1545 .get_or_init(|| Mutex::new(String::new())) // c:1407
1546 .lock()
1547 .unwrap()
1548 .clone();
1549 if !untok {
1550 // c:1411
1551 p = ctokenize(&p); // c:1412
1552 p = p.chars().filter(|&c| c != Bnull).collect(); // c:1413 remnulargs
1553 s = ctokenize(&s); // c:1414
1554 s = s.chars().filter(|&c| c != Bnull).collect(); // c:1415
1555 }
1556 let lp = p.len() as i32; // c:1417
1557 let lip = ip.len() as i32; // c:1419
1558 let mut str = String::with_capacity(ip.len() + p.len() + s.len() + 1); // c:1420
1559 str.push_str(&ip); // c:1435
1560 str.push_str(&p); // c:1435
1561 str.push_str(&s); // c:1435
1562 (str, lip, lp) // c:1435-1430
1563}
1564
1565// =====================================================================
1566// comp_quoting_string — `Src/Zle/compcore.c:1435`.
1567// =====================================================================
1568
1569/// Port of `mod_export char *comp_quoting_string(int stype)` from
1570/// compcore.c:1434.
1571pub fn comp_quoting_string(stype: i32) -> &'static str {
1572 // c:1435
1573 match stype {
1574 // c:1435
1575 x if x == QT_SINGLE => "'", // c:1439-1440
1576 x if x == QT_DOUBLE => "\"", // c:1441-1442
1577 x if x == QT_DOLLARS => "$'", // c:1443-1444
1578 _ => {
1579 // c:1445
1580 let _ = QT_BACKSLASH;
1581 "\\" // c:1446
1582 }
1583 }
1584}
1585
1586// =====================================================================
1587// set_comp_sep — `Src/Zle/compcore.c:1460`.
1588// =====================================================================
1589
1590/// Direct port of `int set_comp_sep(void)` from compcore.c:1458 —
1591/// the `compset -q` driver that re-parses the current completion
1592/// word splitting it on the IFS, then resubmits the right slice
1593/// as the new completion target.
1594///
1595/// Inputs are now published/correct: `wb`/`we`/`offs` are written by
1596/// `get_comp_string` (WB/WE/OFFS shared statics) and `compqstack` by
1597/// `callcompfunc`'s c:305 reset (deduped to `complete::COMPQSTACK`).
1598///
1599/// Byte model: all of C's single-metafied-byte index arithmetic (the
1600/// `inull` walk c:1774-1804, the `chuck` removals, the `s[swb-1-sqq+dq]`
1601/// indexing c:1830, the `p[soffs]` chuck c:1739) is performed on local
1602/// single-byte-metafied `Vec<u8>` buffers built by `to_sb` (each
1603/// token-null marker `Snull`/`Dnull`/`Bnull` maps to ONE byte
1604/// 0x9d/0x9e/0x9f; `Meta`-escapes stay two bytes) and converted back
1605/// with `from_sb` (byte -> `char`, the char-per-metafied-byte form the
1606/// downstream comp* globals expect). `wb`/`we`/`zlemetacs` are consumed
1607/// as byte offsets. For ASCII completion words — command names, paths,
1608/// options, the dominant `compset -q` case — byte == char, so the
1609/// offsets are exact and the algorithm is a verifiable translation of C.
1610///
1611/// Known limitation (INHERITED from zshrs's input/lexer model, not a
1612/// defect of this port): for non-ASCII quoted words the shared cursor
1613/// model conflates byte and char units — `ingetc` steps the input by
1614/// char while `inbufct`/`zlemetall` are byte lengths (input.rs:355/540,
1615/// lex.rs:3013-3024) — so the incoming `wb`/`we`/`zlemetacs` diverge
1616/// from single-byte offsets. Tracked separately with the
1617/// `get_comp_string` quote-form tail; see that function's note.
1618///
1619/// The `QT_DOLLARS` (`$'...'`) arm (c:1613-1622) is fully wired: the
1620/// `getkeystring_with` decode applies `GETKEY_UPDATE_OFFSET`, so
1621/// both the `dolq` byte-count delta and the `css += zlemetacs - j`
1622/// cursor micro-adjustment are computed (inheriting the same non-ASCII
1623/// byte/char caveat noted above).
1624pub fn set_comp_sep() -> i32 {
1625 use crate::ported::lex::{
1626 ctxtlex, noaliases, set_noaliases, set_tok, set_tokstr, tok, tokstr, untokenize,
1627 LEX_LEXFLAGS,
1628 };
1629 use crate::ported::string::{dupstring_wlen, tricat};
1630 use crate::ported::utils::getkeystring_with;
1631 use crate::ported::zle::comp_h::{CP_QUOTE, CP_QUOTING};
1632 // COMPPREFIX/COMPSUFFIX/COMPIPREFIX/COMPQSTACK are already imported at
1633 // the module top; only the remaining comp* globals are pulled in here.
1634 use crate::ported::zle::complete::{
1635 COMPCURRENT, COMPISUFFIX, COMPQIPREFIX, COMPQISUFFIX, COMPQUOTE, COMPQUOTING, COMPWORDS,
1636 };
1637 use crate::ported::zle::zle_utils::{zle_restore_positions, zle_save_positions};
1638 use crate::ported::zsh_h::{
1639 COMPLETEINWORD, ENDINPUT, GETKEY_UPDATE_OFFSET, GETKEYS_DOLLARS_QUOTE, LEXERR, LEXFLAGS_ZLE,
1640 Meta, STRING_LEX,
1641 };
1642
1643 // ── single-byte-metafied <-> char-per-byte String conversions ──
1644 // Each metafied byte is one `char` in the port's string world, so
1645 // `c as u8` for c < 0x100 reconstructs the C single-byte buffer
1646 // (markers 0x9d/0x9e/0x9f = one byte, Meta-escapes = 0x83 + xor).
1647 let to_sb = |s: &str| -> Vec<u8> {
1648 let mut v = Vec::with_capacity(s.len());
1649 for c in s.chars() {
1650 let cp = c as u32;
1651 if cp < 0x100 {
1652 v.push(cp as u8);
1653 } else {
1654 let mut b = [0u8; 4];
1655 v.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
1656 }
1657 }
1658 v
1659 };
1660 let from_sb = |b: &[u8]| -> String { b.iter().map(|&x| x as char).collect() };
1661 let snap = |g: &'static OnceLock<Mutex<String>>| -> String {
1662 g.get_or_init(|| Mutex::new(String::new()))
1663 .lock()
1664 .unwrap()
1665 .clone()
1666 };
1667 let put = |g: &'static OnceLock<Mutex<String>>, v: String| {
1668 *g.get_or_init(|| Mutex::new(String::new())).lock().unwrap() = v;
1669 };
1670
1671 // marker byte values (Snull/Dnull/Bnull/Stringg/Qstring are `char`).
1672 let snull_b = Snull as u32 as u8; // 0x9d
1673 let dnull_b = Dnull as u32 as u8; // 0x9e
1674 let bnull_b = Bnull as u32 as u8; // 0x9f
1675 let stringg_b = Stringg as u32 as u8; // 0x85 ($)
1676 let qstring_b = Qstring as u32 as u8; // 0x8c ("$)
1677 let meta_b: u8 = Meta; // 0x83
1678
1679 // c:1460 — s = comp_str(&lip, &lp, 1) with untok = 1.
1680 let (s_full, lip, lp) = comp_str(true);
1681 // c:1473 — int owe = we, owb = wb.
1682 let owe = WE.load(Ordering::Relaxed);
1683 let owb = WB.load(Ordering::Relaxed);
1684
1685 let mut foo: Vec<String> = Vec::new(); // c:1462 newlinklist()
1686
1687 // c:1478-1500 — locals.
1688 let mut swb: i32 = 0; // c:1490
1689 let mut swe: i32 = 0;
1690 let scs: i32; // cursor (fixed once set below)
1691 let mut soffs: i32 = 0;
1692 let ne = crate::ported::exec::noerrs.load(Ordering::Relaxed); // c:1479
1693 let mut got = false;
1694 let mut i: i32 = 0;
1695 let mut cur: i32 = -1;
1696 let mut css: i32 = 0;
1697 let mut remq = false;
1698 let mut dq: i32 = 0;
1699 let mut sq: i32 = 0;
1700 let qttype: i32;
1701 let mut sqq: i32 = 0;
1702 let mut lsq: i32 = 0;
1703 let mut qa: i32 = 0;
1704 let mut dolq: i32 = 0;
1705 let ois = INSTRING.load(Ordering::Relaxed); // c:1471
1706 let oib = INBACKT.load(Ordering::Relaxed);
1707 let noffs = lp; // active-prefix length
1708 let ona = noaliases();
1709
1710 // c:1476 — s += lip; wb += lip; untokenize(s).
1711 let s_after: String = if (lip as usize) <= s_full.len() {
1712 s_full[lip as usize..].to_string()
1713 } else {
1714 String::new()
1715 };
1716 WB.store(owb + lip, Ordering::Relaxed);
1717 let s = untokenize(&s_after);
1718 let s_b_full = to_sb(&s); // reconstructed arg, single-byte
1719
1720 // c:1483-1488 — zle_save_positions / addedx / noerrs / zcontext / lexflags.
1721 zle_save_positions();
1722 let ol = snap(&ZLEMETALINE);
1723 ADDEDX.store(1, Ordering::Relaxed);
1724 crate::ported::exec::noerrs.store(1, Ordering::Relaxed);
1725 let lex_saved = lexsave(); // zcontext_save()
1726 LEX_LEXFLAGS.set(LEXFLAGS_ZLE);
1727
1728 // c:1494-1499 — tl = strlen(s)+2; tmp = " " + s[..noffs] + 'x' + s[noffs..].
1729 let noffs_u = (noffs.max(0) as usize).min(s_b_full.len());
1730 let tl0 = s_b_full.len() as i32 + 2; // strlen(s) + 2
1731 let mut tmp_b: Vec<u8> = Vec::with_capacity(s_b_full.len() + 3);
1732 tmp_b.push(b' ');
1733 tmp_b.extend_from_slice(&s_b_full[..noffs_u]);
1734 scs = 1 + noffs;
1735 ZLEMETACS.store(scs, Ordering::Relaxed);
1736 tmp_b.push(b'x');
1737 tmp_b.extend_from_slice(&s_b_full[noffs_u..]);
1738 let mut tmp = from_sb(&tmp_b);
1739
1740 // c:1501-1640 — quote-stack head processing.
1741 let compqstack_s = snap(&COMPQSTACK);
1742 qttype = compqstack_s
1743 .chars()
1744 .next()
1745 .map(|c| c as i32)
1746 .unwrap_or(QT_NONE);
1747 let qstack2 = compqstack_s
1748 .chars()
1749 .nth(1)
1750 .map(|c| c as u32 != 0)
1751 .unwrap_or(false); // compqstack[1]
1752 if qttype == QT_BACKSLASH {
1753 // c:1503-1506
1754 remq = true;
1755 tmp = rembslash(&tmp);
1756 } else if qttype == QT_SINGLE {
1757 // c:1508-1514
1758 qa = if isset(RCQUOTES) { 1 } else { 3 };
1759 let mut t = tmp.clone();
1760 sq = remsquote(&mut t);
1761 tmp = t;
1762 } else if qttype == QT_DOUBLE {
1763 // c:1516-1543 — strip \\ and \" pairs, tracking zlemetacs/css/dq.
1764 let mut v = to_sb(&tmp);
1765 let mut j: i32 = 0;
1766 let mut pi = 0usize;
1767 let mut zcs = ZLEMETACS.load(Ordering::Relaxed);
1768 while pi < v.len() {
1769 let c = v[pi];
1770 let nxt = v.get(pi + 1).copied();
1771 if c == b'\\' && (nxt == Some(b'\\') || nxt == Some(b'"')) {
1772 dq += 1;
1773 v.remove(pi); // chuck(p): drop the backslash
1774 match v.get(pi).copied() {
1775 Some(b'"') => zcs -= 1,
1776 _ => {
1777 if j > zcs {
1778 zcs += 1;
1779 css += 1;
1780 }
1781 }
1782 }
1783 if pi >= v.len() {
1784 break; // if (!*p) break
1785 }
1786 }
1787 pi += 1;
1788 j += 1;
1789 }
1790 ZLEMETACS.store(zcs, Ordering::Relaxed);
1791 tmp = from_sb(&v);
1792 } else if qttype == QT_DOLLARS {
1793 // c:1613-1622 — string decode + dolq, with the GETKEY_UPDATE_OFFSET
1794 // cursor micro-adjustment. `j = zlemetacs` (c:1614); the decode
1795 // updates zlemetacs in place as pre-cursor escapes collapse; then
1796 // `css += zlemetacs - j` (c:1621) folds the delta into the word
1797 // offset. GETKEYS_DOLLARS_QUOTE carries the port's decode flags;
1798 // GETKEY_UPDATE_OFFSET enables the offset bookkeeping in
1799 // getkeystring_with.
1800 let j = ZLEMETACS.load(Ordering::Relaxed); // c:1614 — j = zlemetacs
1801 let mut zcs = j;
1802 let (dec, _consumed) = getkeystring_with(
1803 &tmp,
1804 (GETKEYS_DOLLARS_QUOTE | GETKEY_UPDATE_OFFSET) as u32,
1805 Some(&mut zcs),
1806 );
1807 ZLEMETACS.store(zcs, Ordering::Relaxed);
1808 let sl_new = to_sb(&dec).len() as i32;
1809 // c:1619 — dolq = tl - sl (bytes removed by $' quoting).
1810 dolq = tl0 - sl_new;
1811 // c:1621 — css += zlemetacs - j.
1812 css += zcs - j;
1813 tmp = dec;
1814 }
1815 let odq = dq; // c:1642
1816
1817 // c:1643-1647 — push into lexer, set the working metaline.
1818 crate::ported::input::inpush(&dupstrspace(&tmp), 0, None);
1819 put(&ZLEMETALINE, tmp.clone());
1820 ZLEMETALL.store(tl0 - 1, Ordering::Relaxed); // tl - addedx
1821 crate::ported::hist::strinbeg(0);
1822 set_noaliases(true);
1823
1824 // c:1650-1755 — the ctxtlex token loop.
1825 let mut ns_b: Vec<u8> = Vec::new();
1826 loop {
1827 ctxtlex();
1828 let mut tokv = tok();
1829 let mut ts_opt = tokstr();
1830 if tokv == LEXERR {
1831 // c:1654-1668 — odd active-quote count means unterminated
1832 // string; treat as STRING and drop a trailing space.
1833 match &ts_opt {
1834 None => break,
1835 Some(ts) => {
1836 let j = ts.chars().filter(|&c| c == Snull || c == Dnull).count();
1837 if j & 1 == 1 {
1838 tokv = STRING_LEX;
1839 set_tok(STRING_LEX);
1840 if ts.ends_with(' ') {
1841 let mut t = ts.clone();
1842 t.pop();
1843 set_tokstr(Some(t.clone()));
1844 ts_opt = Some(t);
1845 }
1846 }
1847 }
1848 }
1849 }
1850 if tokv == ENDINPUT {
1851 break; // c:1670
1852 }
1853 let mut last_p: Option<usize> = None;
1854 if let Some(ts) = ts_opt.as_ref() {
1855 if !ts.is_empty() {
1856 // c:1673-1680 — Bnull accounting against dq.
1857 if dq != 0 {
1858 let cs: Vec<char> = ts.chars().collect();
1859 let mut k = 0usize;
1860 while dq != 0 && k < cs.len() {
1861 if cs[k] == Bnull {
1862 dq -= 1;
1863 if cs.get(k + 1) == Some(&'\\') {
1864 dq -= 1;
1865 }
1866 }
1867 k += 1;
1868 }
1869 }
1870 // c:1681-1690 — single-quote lsq accounting.
1871 if qttype == QT_SINGLE {
1872 lsq = 0;
1873 for c in ts.chars() {
1874 if sq != 0 && c == Snull {
1875 sq -= qa;
1876 }
1877 if c == '\'' {
1878 sq -= qa;
1879 lsq += qa;
1880 }
1881 }
1882 } else {
1883 lsq = 0;
1884 }
1885 foo.push(ts.clone()); // addlinknode(foo, p = ztrdup(tokstr))
1886 last_p = Some(foo.len() - 1);
1887 }
1888 }
1889 // c:1694-1705 — capture the cursor word once lexflags cleared.
1890 if !got && LEX_LEXFLAGS.get() == 0 {
1891 if let Some(cur_idx) = last_p {
1892 got = true;
1893 cur = cur_idx as i32;
1894 swb = WB.load(Ordering::Relaxed) - dq - sq - dolq;
1895 swe = WE.load(Ordering::Relaxed) - dq - sq - dolq;
1896 sqq = lsq;
1897 soffs = ZLEMETACS.load(Ordering::Relaxed) - swb - css;
1898 // chuck(p + soffs): drop the injected 'x' from the node.
1899 let mut wb_bytes = to_sb(&foo[cur_idx]);
1900 if soffs >= 0 && (soffs as usize) < wb_bytes.len() {
1901 wb_bytes.remove(soffs as usize);
1902 }
1903 foo[cur_idx] = from_sb(&wb_bytes);
1904 ns_b = wb_bytes; // ns = dupstring(p)
1905 }
1906 }
1907 i += 1;
1908 if tokv == ENDINPUT || tokv == LEXERR {
1909 break; // c:1707 do-while
1910 }
1911 }
1912
1913 // c:1709-1719 — tear down lexer state, restore positions.
1914 set_noaliases(ona);
1915 crate::ported::hist::strinend();
1916 crate::ported::input::inpop();
1917 crate::ported::utils::errflag
1918 .fetch_and(!crate::ported::utils::ERRFLAG_ERROR, Ordering::Relaxed);
1919 crate::ported::exec::noerrs.store(ne, Ordering::Relaxed);
1920 lexrestore(lex_saved); // zcontext_restore()
1921 WB.store(owb, Ordering::Relaxed);
1922 WE.store(owe, Ordering::Relaxed);
1923 put(&ZLEMETALINE, ol);
1924 zle_restore_positions();
1925 if cur < 0 || i < 1 {
1926 return 1; // c:1721
1927 }
1928
1929 // c:1723-1733 — check_param dispatch with offs temporarily = soffs.
1930 let o_offs = OFFS.load(Ordering::Relaxed);
1931 OFFS.store(soffs, Ordering::Relaxed);
1932 if check_param(&from_sb(&ns_b), false, true).is_some() {
1933 for b in ns_b.iter_mut() {
1934 if *b == dnull_b {
1935 *b = b'"';
1936 } else if *b == snull_b {
1937 *b = b'\'';
1938 }
1939 }
1940 }
1941 OFFS.store(o_offs, Ordering::Relaxed);
1942
1943 // c:1735 — ts = untokenize(dupstring(ns)).
1944 let ts_str = untokenize(&from_sb(&ns_b));
1945 let ts_b = to_sb(&ts_str);
1946
1947 // c:1737-1772 — quote-form detection: instring / inbackt / autoq.
1948 let ns0 = ns_b.first().copied();
1949 let ns1 = ns_b.get(1).copied();
1950 let quote_open = ns0 == Some(snull_b)
1951 || ns0 == Some(dnull_b)
1952 || ((ns0 == Some(stringg_b) || ns0 == Some(qstring_b)) && ns1 == Some(snull_b));
1953 let mut ts_off = 0usize;
1954 if quote_open {
1955 let mut nsptr = 0usize; // C's nsptr offset into ns
1956 match ns0 {
1957 x if x == Some(snull_b) => INSTRING.store(QT_SINGLE, Ordering::Relaxed),
1958 x if x == Some(dnull_b) => INSTRING.store(QT_DOUBLE, Ordering::Relaxed),
1959 _ => {
1960 INSTRING.store(QT_DOLLARS, Ordering::Relaxed);
1961 nsptr += 1;
1962 ts_off += 1;
1963 swb += 1;
1964 }
1965 }
1966 INBACKT.store(0, Ordering::Relaxed);
1967 swb += 1;
1968 // c:1747 — if (nsptr[strlen(nsptr)-1] == *nsptr && nsptr[1]) swe--
1969 let ns_slice = &ns_b[nsptr.min(ns_b.len())..];
1970 if ns_slice.len() >= 2 && *ns_slice.last().unwrap() == ns_slice[0] {
1971 swe -= 1;
1972 }
1973 // c:1749-1753 — autoq from ts prefix.
1974 ts_off += 1; // ++tsptr
1975 let ts_prefix = from_sb(&ts_b[..ts_off.min(ts_b.len())]);
1976 let autoq_v = if qstack2 {
1977 String::new()
1978 } else {
1979 multiquote(&ts_prefix, 1)
1980 };
1981 put(&AUTOQ, autoq_v);
1982 } else {
1983 INSTRING.store(QT_NONE, Ordering::Relaxed);
1984 put(&AUTOQ, String::new());
1985 }
1986
1987 // c:1774-1804 — the inull walk: drop null markers from ns, adjusting
1988 // swb/scs/soffs. `scs` is copied to a mutable walker `scs_w`.
1989 let mut scs_w = scs;
1990 {
1991 let mut pi = 0usize;
1992 let mut wi = swb;
1993 while pi < ns_b.len() {
1994 let c = ns_b[pi];
1995 if crate::ported::ztype_h::inull(c) {
1996 let next = ns_b.get(pi + 1).copied();
1997 let next_truthy = next.is_some();
1998 if wi < scs_w && c == bnull_b {
1999 if next_truthy && remq {
2000 swb -= 2;
2001 }
2002 if odq != 0 {
2003 swb -= 1;
2004 if next == Some(b'\\') {
2005 swb -= 1;
2006 }
2007 }
2008 }
2009 if next_truthy || c != bnull_b {
2010 if c == bnull_b {
2011 if scs_w == wi + 1 {
2012 scs_w += 1;
2013 soffs += 1;
2014 }
2015 } else {
2016 let cond = scs_w > wi;
2017 wi -= 1; // C's post-decrement in `scs > i--`
2018 if cond {
2019 scs_w -= 1;
2020 }
2021 }
2022 } else if scs_w == swe {
2023 scs_w -= 1;
2024 }
2025 ns_b.remove(pi); // chuck(p--); loop p++ revisits index pi
2026 wi += 1;
2027 } else {
2028 pi += 1;
2029 wi += 1;
2030 }
2031 }
2032 }
2033
2034 // c:1806 — ns = ts (the untokenized copy, advanced past open quote).
2035 let mut ns_final_b = ts_b[ts_off.min(ts_b.len())..].to_vec();
2036
2037 // c:1808-1813 — backslash-quoting length fixup.
2038 let instr = INSTRING.load(Ordering::Relaxed);
2039 let qstack_has_bs = compqstack_s.chars().any(|c| c as i32 == QT_BACKSLASH);
2040 if instr != QT_NONE && qstack_has_bs {
2041 let ns_now = from_sb(&ns_final_b);
2042 let rl = ns_final_b.len() as i32;
2043 let ql = to_sb(&multiquote(&ns_now, if qstack2 { 1 } else { 0 })).len() as i32;
2044 if ql > rl {
2045 swb -= ql - rl;
2046 }
2047 }
2048
2049 // c:1826-1855 — split the reconstructed s into qp (prefix) / qs (suffix)
2050 // around the word, with the empirical swb-1-sqq+dq / swe-- offsets.
2051 let idx = swb - 1 - sqq + dq;
2052 let iu = idx.clamp(0, s_b_full.len() as i32) as usize;
2053 let s_prefix = from_sb(&s_b_full[..iu]);
2054 let mut qp = if qttype == QT_SINGLE {
2055 dupstring_wlen(&s_prefix, iu)
2056 } else {
2057 rembslash(&s_prefix)
2058 };
2059 if swe < swb {
2060 swe = swb;
2061 }
2062 swe -= 1;
2063 let sl_s = s_b_full.len() as i32;
2064 if swe > sl_s {
2065 swe = sl_s;
2066 if ns_final_b.len() as i32 > swe - swb + 1 {
2067 let newlen = (swe - swb + 1).max(0) as usize;
2068 ns_final_b.truncate(newlen);
2069 }
2070 }
2071 let swe_u = swe.clamp(0, s_b_full.len() as i32) as usize;
2072 let s_suffix = from_sb(&s_b_full[swe_u..]);
2073 let mut qs = if qttype == QT_SINGLE {
2074 s_suffix.clone()
2075 } else {
2076 rembslash(&s_suffix)
2077 };
2078 let sl_ns = ns_final_b.len() as i32;
2079 if soffs > sl_ns {
2080 soffs = sl_ns;
2081 }
2082 if qttype == QT_SINGLE {
2083 let mut a = qp;
2084 remsquote(&mut a);
2085 qp = a;
2086 let mut b = qs;
2087 remsquote(&mut b);
2088 qs = b;
2089 }
2090
2091 // c:1857-1935 — publish the results.
2092 // c:1861-1868 — prepend the active quote char to compqstack.
2093 {
2094 let head = if instr == QT_NONE { QT_BACKSLASH } else { instr };
2095 let mut new_qstack = String::new();
2096 if let Some(hc) = char::from_u32(head as u32) {
2097 new_qstack.push(hc);
2098 }
2099 new_qstack.push_str(&compqstack_s);
2100 put(&COMPQSTACK, new_qstack);
2101 }
2102
2103 // c:1870-1892 — compquote / compquoting + comp_setunset.
2104 let mut set = (CP_QUOTE | CP_QUOTING) as i32;
2105 let mut unset = 0i32;
2106 let (cq, cqg) = if instr == QT_DOUBLE {
2107 ("\"", "double")
2108 } else if instr == QT_SINGLE {
2109 ("'", "single")
2110 } else if instr == QT_DOLLARS {
2111 ("$'", "dollars")
2112 } else {
2113 unset = set;
2114 set = 0;
2115 ("", "")
2116 };
2117 put(&COMPQUOTE, cq.to_string());
2118 put(&COMPQUOTING, cqg.to_string());
2119 crate::ported::zle::complete::comp_setunset(0, 0, set, unset);
2120
2121 // c:1894-1907 — compprefix / compsuffix from ns around soffs.
2122 if !isset(COMPLETEINWORD) {
2123 put(&COMPPREFIX, untokenize(&from_sb(&ns_final_b)));
2124 put(&COMPSUFFIX, String::new());
2125 } else {
2126 let so = (soffs.max(0) as usize).min(ns_final_b.len());
2127 put(&COMPPREFIX, untokenize(&from_sb(&ns_final_b[..so])));
2128 put(&COMPSUFFIX, untokenize(&from_sb(&ns_final_b[so..])));
2129 }
2130 // c:1908-1910 — drop a dangling final backslash from compprefix.
2131 {
2132 let cp = snap(&COMPPREFIX);
2133 let cpb = cp.as_bytes();
2134 let n = cpb.len();
2135 if n > 1 && cpb[n - 1] == b'\\' && cpb[n - 2] != b'\\' && cpb[n - 2] != meta_b {
2136 let mut t = cp;
2137 t.pop();
2138 put(&COMPPREFIX, t);
2139 }
2140 }
2141
2142 // c:1912-1925 — fold qp/qs into the quoted ignored prefix/suffix.
2143 let cqip = tricat(&snap(&COMPQIPREFIX), &snap(&COMPIPREFIX), &multiquote(&qp, 1));
2144 put(&COMPQIPREFIX, cqip);
2145 let cqis = tricat(&multiquote(&qs, 1), &snap(&COMPISUFFIX), &snap(&COMPQISUFFIX));
2146 put(&COMPQISUFFIX, cqis);
2147 put(&COMPIPREFIX, String::new());
2148 put(&COMPISUFFIX, String::new());
2149
2150 // c:1926-1934 — rebuild compwords / compcurrent from foo.
2151 {
2152 let words: Vec<String> = foo.iter().map(|w| untokenize(w)).collect();
2153 let cnt = words.len() as i32;
2154 *COMPWORDS
2155 .get_or_init(|| Mutex::new(Vec::new()))
2156 .lock()
2157 .unwrap() = words;
2158 let mut compcur = cur + 1;
2159 if compcur > cnt {
2160 compcur = cnt;
2161 }
2162 COMPCURRENT.store(compcur, Ordering::Relaxed);
2163 }
2164
2165 // c:1935-1937 — restore instring / inbackt, ret = 0.
2166 INSTRING.store(ois, Ordering::Relaxed);
2167 INBACKT.store(oib, Ordering::Relaxed);
2168 0
2169}
2170
2171// Brace counters live in zle_tricky.c:114 — re-exported there. Local
2172// re-exports here so call sites stay short:
2173#[doc(hidden)]
2174// =====================================================================
2175// set_list_array — `Src/Zle/compcore.c:1947`.
2176// =====================================================================
2177
2178/// Port of `static void set_list_array(char *name, LinkList l)` from
2179/// compcore.c:1947. Writes an array-typed parameter via the canonical
2180/// `setaparam` (params.c:3595).
2181pub fn set_list_array(name: &str, l: &[String]) {
2182 // c:1947
2183 let _ = setaparam(name, l.to_vec()); // c:1956
2184}
2185
2186// =====================================================================
2187// get_user_var — `Src/Zle/compcore.c:1956`.
2188// =====================================================================
2189
2190/// Port of `mod_export char **get_user_var(char *nam)` from
2191/// compcore.c:1956.
2192pub fn get_user_var(nam: Option<&str>) -> Option<Vec<String>> {
2193 // c:1956
2194 let nam = nam?; // c:1956
2195 if nam.starts_with('(') {
2196 // c:1960
2197 let mut arrlist: Vec<String> = Vec::new();
2198 let bytes = nam.as_bytes();
2199 let mut buf = Vec::<u8>::new();
2200 let mut notempty = false; // c:1963
2201 let mut brk = false;
2202 let mut i = 1; // c:1967
2203 while i < bytes.len() {
2204 let b = bytes[i];
2205 if b == b'\\' && i + 1 < bytes.len() {
2206 // c:1969
2207 buf.push(bytes[i + 1]); // c:1970
2208 notempty = true;
2209 i += 2;
2210 continue;
2211 }
2212 if b == b',' || b == b' ' || b == b'\t' || b == b'\n' || b == b')' {
2213 if b == b')' {
2214 brk = true;
2215 } // c:1972
2216 if notempty {
2217 // c:1974
2218 let mut start = 0;
2219 if !buf.is_empty() && buf[0] == b'\n' {
2220 start = 1;
2221 } // c:1977
2222 let s = String::from_utf8_lossy(&buf[start..]).into_owned();
2223 arrlist.push(s); // c:1979
2224 }
2225 buf.clear(); // c:1981
2226 notempty = false;
2227 } else {
2228 notempty = true; // c:1984
2229 buf.push(b);
2230 }
2231 i += 1;
2232 if brk {
2233 break;
2234 } // c:1988
2235 }
2236 if !brk || arrlist.is_empty() {
2237 return None;
2238 } // c:1991
2239 Some(arrlist) // c:1996
2240 } else {
2241 // c:1999
2242 // c:2003 — `if ((arr = getaparam(nam)) || (arr = gethparam(nam)))
2243 // arr = (incompfunc ? arrdup(arr) : arr);
2244 // else if ((val = getsparam(nam))) { arr = {val, NULL}; }`
2245 // Read directly from paramtab: arrays first, then hashed
2246 // assoc-array values, then scalar wrapped in a 1-element array.
2247 queue_signals();
2248 let result = {
2249 let tab = match paramtab().read() {
2250 Ok(t) => t,
2251 Err(_) => {
2252 unqueue_signals();
2253 return None;
2254 }
2255 };
2256 tab.get(nam).and_then(|pm| {
2257 if let Some(arr) = pm.u_arr.as_ref() {
2258 Some(arr.clone()) // c:2004 getaparam
2259 } else if let Some(s) = pm.u_str.as_ref() {
2260 Some(vec![s.clone()]) // c:2009 getsparam
2261 } else {
2262 None
2263 }
2264 })
2265 };
2266 unqueue_signals(); // c:2022
2267 result
2268 }
2269}
2270
2271// =====================================================================
2272// get_data_arr — `Src/Zle/compcore.c:2022`.
2273// =====================================================================
2274
2275/// Direct port of `static char **get_data_arr(char *name, int keys)`
2276/// from `Src/Zle/compcore.c:2022`. C uses `fetchvalue` with
2277/// `SCANPM_WANTKEYS`/`SCANPM_WANTVALS` + `SCANPM_MATCHMANY` to scan
2278/// an associative-array parameter and return either its keys or its
2279/// values as a flat array. Without `fetchvalue` ported with full
2280/// SCANPM flag support, we go straight to the hashed-storage
2281/// thread-local maintained by params.rs for assoc-arrays.
2282pub fn get_data_arr(name: &str, keys: bool) -> Option<Vec<String>> {
2283 // c:2022
2284
2285 queue_signals(); // c:2028
2286
2287 // c:2030-2034 — `fetchvalue(&vbuf, &name, 1, (keys ? SCANPM_WANTKEYS
2288 // : SCANPM_WANTVALS) | SCANPM_MATCHMANY)` then `getarrvalue(v)`.
2289 // Route through the same param accessors `${(k)name}` / `${(v)name}`
2290 // / `${name}` use so SPECIAL magic hashes (`commands`, `builtins`,
2291 // `functions`, `aliases`, `reswords`, …) resolve via their module
2292 // scanfns — the raw `paramtab_hashed_storage` map is empty for those,
2293 // which is why `compadd -k commands` previously added the literal
2294 // word "commands" instead of every command name.
2295 let result = if keys {
2296 // SCANPM_WANTKEYS — assoc keys (gethkparam handles special hashes).
2297 crate::ported::params::gethkparam(name)
2298 } else {
2299 // SCANPM_WANTVALS — plain-array elements, else assoc values.
2300 crate::ported::params::getaparam(name)
2301 .filter(|v| !v.is_empty())
2302 .or_else(|| crate::ported::params::gethparam(name))
2303 };
2304
2305 unqueue_signals(); // c:2041
2306 result
2307}
2308
2309// =====================================================================
2310// addmatch — `Src/Zle/compcore.c:2041`.
2311// =====================================================================
2312
2313/// Port of `static void addmatch(char *str, int flags, char ***dispp,
2314/// int line)` from compcore.c:2041.
2315pub fn addmatch(str: &str, flags: i32, disp: Option<&str>, line: bool) {
2316 // c:2041
2317 let mut cm = Cmatch::default(); // c:2041
2318 cm.str = Some(str.to_string()); // c:2047
2319 // c:2049-2051 — inline read of `complist` parameter, parse `packed`/
2320 // `rows` substrings into CMF_PACKED/CMF_ROWS flag bits.
2321 let complist_extra = {
2322 let s = COMPLIST
2323 .get_or_init(|| Mutex::new(String::new()))
2324 .lock()
2325 .map(|g| g.clone())
2326 .unwrap_or_default();
2327 let packed = if s.contains("packed") { CMF_PACKED } else { 0 }; // c:2050
2328 let rows = if s.contains("rows") { CMF_ROWS } else { 0 }; // c:2051
2329 if s.is_empty() {
2330 0
2331 } else {
2332 packed | rows
2333 }
2334 };
2335 cm.flags = flags | complist_extra; // c:2048
2336 if let Some(d) = disp {
2337 // c:2052
2338 cm.disp = Some(d.to_string()); // c:2056
2339 } else if line {
2340 // c:2057
2341 cm.disp = Some(String::new()); // c:2058
2342 cm.flags |= CMF_DISPLINE; // c:2059
2343 }
2344 mnum.fetch_add(1, Ordering::Relaxed); // c:2061
2345 {
2346 let cell = curexpl.get_or_init(|| Mutex::new(None)); // c:2063
2347 if let Ok(mut g) = cell.lock() {
2348 if let Some(e) = g.as_mut() {
2349 e.count += 1;
2350 }
2351 }
2352 }
2353 let mcell = matches.get_or_init(|| Mutex::new(Vec::new())); // c:2066
2354 if let Ok(mut g) = mcell.lock() {
2355 g.push(cm);
2356 }
2357 newmatches.store(1, Ordering::Relaxed); // c:2068
2358 {
2359 let cell = mgroup.get_or_init(|| Mutex::new(None)); // c:2069
2360 if let Ok(mut g) = cell.lock() {
2361 if let Some(grp) = g.as_mut() {
2362 grp.new_ = 1;
2363 }
2364 }
2365 }
2366}
2367
2368// =====================================================================
2369// addmatches — `Src/Zle/compcore.c:2080`.
2370// =====================================================================
2371
2372/// Direct port of `int addmatches(Cadata dat, char **argv)` from
2373/// compcore.c:2080 — the workhorse called from every `compadd`
2374/// invocation. Walks `argv`, runs the matcher chain against each
2375/// candidate, builds the Cline chain via `add_match_data`, and
2376/// appends accepted matches to the current group.
2377///
2378/// Real-bodied across all major phases: prologue (group selection
2379/// c:2105-2118, brace-state snapshot c:2129-2132, instring/inbackt
2380/// save c:2148-2179, `*argv` empty short-circuit c:2127), mstack
2381/// push c:2210-2222, aign/pign suffix-ignore + Patprog filters
2382/// c:2223-2246, disp array c:2247-2250, lipre/lisuf/lpre/lsuf
2383/// assembly c:2253-2300, per-candidate match loop with comp_match
2384/// dispatch + add_match_data emit + apar/opar writeback
2385/// c:2482-2601, apar/opar setaparam c:2602-2605, exp addexpl
2386/// c:2610, hasallmatch CAF_ALL placeholder c:2612-2614, dummy
2387/// entries c:2616-2617.
2388pub fn addmatches(
2389 dat: &mut Cadata, // c:2080
2390 argv: &[String],
2391) -> i32 {
2392 let _nm = mnum.load(Ordering::Relaxed); // c:2095 nm
2393
2394 if dat.dummies >= 0 {
2395 // c:2106
2396 dat.aflags = (dat.aflags | CAF_NOSORT | CAF_UNIQCON) & !CAF_UNIQALL; // c:2107-2108
2397 }
2398
2399 let gflags = (if (dat.aflags & CAF_NOSORT) != 0 {
2400 CGF_NOSORT
2401 } else {
2402 0
2403 }) | (if (dat.aflags & CAF_MATSORT) != 0 {
2404 CGF_MATSORT
2405 } else {
2406 0
2407 }) | (if (dat.aflags & CAF_NUMSORT) != 0 {
2408 CGF_NUMSORT
2409 } else {
2410 0
2411 }) | (if (dat.aflags & CAF_REVSORT) != 0 {
2412 CGF_REVSORT
2413 } else {
2414 0
2415 }) | (if (dat.aflags & CAF_UNIQALL) != 0 {
2416 CGF_UNIQALL
2417 } else {
2418 0
2419 }) | (if (dat.aflags & CAF_UNIQCON) != 0 {
2420 CGF_UNIQCON
2421 } else {
2422 0
2423 });
2424
2425 tracing::debug!(target: "compsys_args", group = dat.group.as_deref().unwrap_or("<none>"), exp = dat.exp.as_deref().unwrap_or("<none>"), nargs = argv.len(), doadd = dat.dpar.is_empty(), "addmatches group");
2426 if let Some(g) = dat.group.as_deref() {
2427 // c:2115
2428 endcmgroup(None); // c:2116
2429 begcmgroup(Some(g), gflags); // c:2117
2430 } else {
2431 endcmgroup(None); // c:2119
2432 begcmgroup(Some("default"), 0); // c:2120
2433 }
2434
2435 if dat.mesg.is_some() || dat.exp.is_some() {
2436 // c:2122
2437 let mut e = Cexpl::default(); // c:2123
2438 e.always = if dat.mesg.is_some() { 1 } else { 0 }; // c:2124
2439 e.count = 0;
2440 e.fcount = 0; // c:2125
2441 e.str = Some(
2442 dat.mesg
2443 .clone() // c:2126
2444 .or_else(|| dat.exp.clone())
2445 .unwrap_or_default(),
2446 );
2447 if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2448 *g = Some(e);
2449 }
2450 if dat.mesg.is_some() && dat.dpar.is_empty() && dat.opar.is_none() && dat.apar.is_none() {
2451 // c:2129
2452 addexpl(true); // c:2130
2453 }
2454 } else if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2455 *g = None; // c:2133
2456 }
2457
2458 // c:2138 — empty-argv early return.
2459 if argv.is_empty() && dat.dummies == 0 && (dat.aflags & CAF_ALL) == 0 {
2460 return 1; // c:2139
2461 }
2462
2463 // c:2143-2147 — snapshot brbeg/brend curpos per CAF_QUOTE.
2464 let _quote_mode = (dat.aflags & CAF_QUOTE) != 0; // c:2144
2465
2466 if (dat.flags & 0x0008/*CMF_ISPAR*/) != 0 {
2467 // c:2148
2468 dat.flags |= parflags.load(Ordering::Relaxed); // c:2149
2469 }
2470
2471 let qc = compquote_first(); // c:2150
2472 if let Some(q) = qc {
2473 // c:2151
2474 match q {
2475 '`' => {
2476 instring_set(0);
2477 inbackt_set(0);
2478 autoq_set("");
2479 } // c:2153-2161
2480 '\'' => instring_set(QT_SINGLE), // c:2165
2481 '"' => instring_set(QT_DOUBLE), // c:2168
2482 '$' => instring_set(QT_DOLLARS), // c:2171
2483 _ => {}
2484 }
2485 } else {
2486 instring_set(0);
2487 inbackt_set(0);
2488 autoq_set(""); // c:2179
2489 }
2490
2491 // c:2182 — `useexact = (compexact && !strcmp(compexact, "accept"))`.
2492 // C reads the `compexact` element of `$compstate`. Route
2493 // through paramtab via getsparam — `$compstate[exact]`
2494 // is the hashed-store equivalent. Was reading the OS env
2495 // which never carries compstate values.
2496 let exact_str = getsparam("compexact").unwrap_or_default();
2497 useexact.store(if exact_str == "accept" { 1 } else { 0 }, Ordering::Relaxed);
2498
2499 // c:2210-2222 — push dat.match onto mstack (the matcher chain
2500 // queried by match_str during candidate evaluation).
2501 if let Some(ref m) = dat.match_ {
2502 // c:2210
2503 if let Ok(mut mst) = mstack.get_or_init(|| Mutex::new(None)).lock() {
2504 // C: mst.next = mstack; mst.matcher = dat->match; mstack = &mst.
2505 let new_link = Box::new(Cmlist {
2506 next: mst.take(),
2507 matcher: m.clone(),
2508 str: String::new(),
2509 });
2510 *mst = Some(new_link);
2511 }
2512 // c:2215 — add_bmatchers(dat->match).
2513 crate::ported::zle::compmatch::add_bmatchers(Some(m));
2514 // c:2217 — addlinknode(matchers, dat->match).
2515 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
2516 g.push(m.clone());
2517 }
2518 }
2519
2520 // c:2223-2246 — get suffixes to ignore from dat.ign param.
2521 let (aign, pign) = if let Some(ign_name) = dat.ign.as_deref() {
2522 // c:2224
2523 let aign_raw = get_user_var(Some(ign_name)).unwrap_or_default();
2524 let mut literal_suffixes: Vec<String> = Vec::new();
2525 let mut pat_progs: Vec<crate::ported::pattern::Patprog> = Vec::new();
2526 for entry in aign_raw {
2527 // c:2231-2232 — `tokenize(tmp); remnulargs(tmp);` — fignore
2528 // entries are param values (untokenized text); C tokenizes
2529 // each BEFORE the token checks below and the patcompile.
2530 let mut tmp = entry.clone();
2531 crate::ported::glob::tokenize(&mut tmp); // c:2231
2532 crate::ported::glob::remnulargs(&mut tmp); // c:2232
2533 // c:2233-2236 — `(tmp[0] == Quest && tmp[1] == Star) ||
2534 // (tmp[1] == Quest && tmp[0] == Star)` token short-circuit:
2535 // trailing literal suffix.
2536 let tch: Vec<char> = tmp.chars().collect();
2537 let suffix: String = tch.iter().skip(2).collect();
2538 let star_prefix = tch.len() >= 3
2539 && ((tch[0] == crate::ported::zsh_h::Quest
2540 && tch[1] == crate::ported::zsh_h::Star)
2541 || (tch[1] == crate::ported::zsh_h::Quest
2542 && tch[0] == crate::ported::zsh_h::Star))
2543 && !crate::ported::pattern::haswilds(&suffix);
2544 if star_prefix {
2545 // c:2236 — `untokenize(*sp++ = tmp + 2);`
2546 literal_suffixes.push(crate::ported::lex::untokenize(&suffix));
2547 } else if let Some(prog) =
2548 crate::ported::pattern::patcompile(&tmp, 0, None::<&mut String>)
2549 {
2550 pat_progs.push(prog);
2551 }
2552 }
2553 (literal_suffixes, pat_progs)
2554 } else {
2555 (Vec::new(), Vec::new())
2556 };
2557
2558 // c:2247-2250 — get display strings.
2559 let disp_arr: Vec<String> = if let Some(ref d) = dat.disp {
2560 get_user_var(Some(d.as_str())).unwrap_or_default()
2561 } else {
2562 Vec::new()
2563 };
2564
2565 // zshrs bridge: in C the `compprefix`/`compsuffix`/`compiprefix`/
2566 // `compisuffix` globals ARE `$PREFIX`/`$SUFFIX`/`$IPREFIX`/`$ISUFFIX`
2567 // (the compparams are gsu-bound to them). The Rust compparams carry
2568 // no gsu binding (`complete.rs` `gsu: 0`), so the compsys completers'
2569 // writes to `$PREFIX` land in the param table while `compadd` reads
2570 // the globals — leaving `lpre` empty and every candidate matching.
2571 // During a live completion (`INCOMPFUNC`), refresh the globals from
2572 // the params so `comp_match` filters against the prefix the completer
2573 // actually set. Gated on INCOMPFUNC so direct-call unit tests that
2574 // seed the globals aren't clobbered.
2575 if INCOMPFUNC.load(Ordering::Relaxed) != 0 {
2576 for (param, global) in [
2577 ("PREFIX", &COMPPREFIX),
2578 ("SUFFIX", &COMPSUFFIX),
2579 ("IPREFIX", &COMPIPREFIX),
2580 ("ISUFFIX", &crate::ported::zle::complete::COMPISUFFIX),
2581 ] {
2582 if let Some(v) = getsparam(param) {
2583 if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
2584 *g = v;
2585 }
2586 }
2587 }
2588 }
2589
2590 // c:2253-2300 — CAF_MATCH lipre/lisuf/lpre/lsuf assembly.
2591 let compiprefix_s = COMPIPREFIX
2592 .get_or_init(|| Mutex::new(String::new()))
2593 .lock()
2594 .map(|g| g.clone())
2595 .unwrap_or_default();
2596 let compisuffix_s = crate::ported::zle::complete::COMPISUFFIX
2597 .get_or_init(|| Mutex::new(String::new()))
2598 .lock()
2599 .map(|g| g.clone())
2600 .unwrap_or_default();
2601 let compprefix_s = COMPPREFIX
2602 .get_or_init(|| Mutex::new(String::new()))
2603 .lock()
2604 .map(|g| g.clone())
2605 .unwrap_or_default();
2606 let compsuffix_s = COMPSUFFIX
2607 .get_or_init(|| Mutex::new(String::new()))
2608 .lock()
2609 .map(|g| g.clone())
2610 .unwrap_or_default();
2611 let lipre = compiprefix_s.clone();
2612 let lisuf = compisuffix_s.clone();
2613 let mut lpre = compprefix_s.clone();
2614 let lsuf = compsuffix_s.clone();
2615
2616 // c:2314-2340 — strip the path-prefix (`compadd -p PPRE`) from `lpre`
2617 // before matching each candidate. PPRE is already on the command line
2618 // ahead of the match, so comp_match must compare the candidate against
2619 // `lpre` with PPRE removed (candidate `sub` vs `su`, not vs the full
2620 // `/tmp/ptree/su`). Try the active matcher first (`match_str`, part=1),
2621 // else strip literally. Without this every `cmd /a/b/pre<TAB>` produced
2622 // no matches — the resolved candidate never matched the full-path lpre.
2623 let mut bcp: i32 = 0;
2624 let mut ppre_nomatch = false;
2625 if (dat.aflags & CAF_MATCH) != 0 {
2626 // ppre is quoted below (c:2288) in C before this block; do the same
2627 // ordering here so lengths line up with the on-line text.
2628 let ppre_q: Option<String> = dat.ppre.as_deref().map(|existing| {
2629 if (dat.flags & 0x0001/*CMF_FILE*/) != 0 {
2630 tildequote(existing, if (dat.aflags & CAF_QUOTE) != 0 { 0 } else { 1 })
2631 } else {
2632 multiquote(existing, if (dat.aflags & CAF_QUOTE) != 0 { 0 } else { 1 })
2633 }
2634 });
2635 if let Some(ppre) = ppre_q.as_deref() {
2636 if !ppre.is_empty() {
2637 // NOTE: `dat.ppre` is left UNquoted here — the canonical
2638 // quoting happens in the c:2288 block below. `ppre_q` is a
2639 // local, same-quoting copy used only for the length math.
2640 let lpl = ppre.len();
2641 // c:2314 — `ml = match_str(lpre, ppre, …, part=1)`. Reset the
2642 // match accumulators first so this probe doesn't leak into the
2643 // per-candidate comp_match (the Rust threads the match Cline
2644 // through comp_match's own output, and instmatch re-inserts
2645 // dat.ppre from the Cmatch, so the probe's `pline` is unused).
2646 crate::ported::zle::compmatch::start_match();
2647 let ml = crate::ported::zle::compmatch::match_str(
2648 &lpre, ppre, None, 0, None, 0, 0, 1,
2649 );
2650 if ml >= 0 {
2651 // c:2318-2325 — matcher matched the prefix.
2652 let cut = (ml as usize).min(lpre.len());
2653 lpre = lpre[cut..].to_string();
2654 bcp = ml;
2655 } else if lpre.len() <= lpl && ppre.starts_with(lpre.as_str()) {
2656 // c:2335 — lpre is a prefix of ppre → nothing left.
2657 lpre.clear();
2658 } else if lpre.len() > lpl && lpre.starts_with(ppre) {
2659 // c:2337 — ppre is a literal prefix of lpre → strip it.
2660 lpre = lpre[lpl..].to_string();
2661 } else {
2662 // c:2339 — `*argv = NULL`: no candidate can match.
2663 ppre_nomatch = true;
2664 }
2665 crate::ported::zle::compmatch::start_match();
2666 }
2667 }
2668 }
2669 if ppre_nomatch {
2670 return if mnum.load(Ordering::Relaxed) == _nm { 1 } else { 0 };
2671 }
2672
2673 // c:2360-2389 — when `$compstate[pattern_match]` is set, compile the
2674 // line prefix+suffix (with the completion point as a `*` placeholder)
2675 // into a Patprog so candidates are matched as GLOBS instead of literal
2676 // prefixes. This is what makes `_approximate` work: it turns on
2677 // pattern_match and injects a leading `(#a$n)` approximate-glob into
2678 // PREFIX (via the compadd prefix injector), so e.g. `(#a1)aple*` matches
2679 // `apple`. The Rust port previously always passed `cp = None` to
2680 // comp_match, so pattern_match / approximate completion did nothing.
2681 // C uses an `x` placeholder at the completion point precisely so the
2682 // point wildcard alone never enables pattern matching; mirror that by
2683 // probing haswilds() on lpre+lsuf WITHOUT the placeholder — only real
2684 // wildcards in the typed prefix/suffix (`(#a1)`, `*`, `[...]`) count.
2685 let cp: Option<crate::ported::pattern::Patprog> = {
2686 let cpm = get_compstate_str("pattern_match").unwrap_or_default();
2687 if !cpm.is_empty() {
2688 let is = cpm.starts_with('*'); // c:2361 is = (*comppatmatch == '*')
2689 let mut probe = format!("{}{}", lpre, lsuf);
2690 crate::ported::glob::tokenize(&mut probe); // c:2371
2691 if crate::ported::pattern::haswilds(&probe) {
2692 // c:2372 — has real wildcards.
2693 let mut pat = String::new();
2694 pat.push_str(&lpre); // c:2367 lpre
2695 if is {
2696 // c:2374 — `tmp[llpl] = Star` (completion-point wildcard).
2697 pat.push('*');
2698 }
2699 pat.push_str(&lsuf); // c:2369 lsuf
2700 crate::ported::glob::tokenize(&mut pat);
2701 crate::ported::glob::remnulargs(&mut pat); // c:2376
2702 let prog = crate::ported::pattern::patcompile(&pat, 0, None); // c:2377
2703 if prog.is_some() {
2704 haspattern.store(1, Ordering::Relaxed); // c:2378
2705 }
2706 prog
2707 } else {
2708 None
2709 }
2710 } else {
2711 None
2712 }
2713 };
2714
2715 // c:2278-2300 — dat.ipre/isuf/ppre/psuf duplication with lipre/lisuf.
2716 if let Some(ref existing) = dat.ipre.clone() {
2717 dat.ipre = Some(if !lipre.is_empty() {
2718 format!("{}{}", lipre, existing)
2719 } else {
2720 existing.clone()
2721 });
2722 } else if !lipre.is_empty() {
2723 dat.ipre = Some(lipre.clone());
2724 }
2725 if let Some(ref existing) = dat.isuf.clone() {
2726 dat.isuf = Some(if !lisuf.is_empty() {
2727 format!("{}{}", lisuf, existing)
2728 } else {
2729 existing.clone()
2730 });
2731 } else if !lisuf.is_empty() {
2732 dat.isuf = Some(lisuf.clone());
2733 }
2734 let quote_flag = if (dat.aflags & CAF_QUOTE) != 0 { 1 } else { 0 };
2735 if let Some(ref existing) = dat.ppre.clone() {
2736 let quoted = if (dat.flags & 0x0001/*CMF_FILE*/) != 0 {
2737 tildequote(existing, quote_flag)
2738 } else {
2739 multiquote(existing, quote_flag)
2740 };
2741 dat.ppre = Some(quoted);
2742 }
2743 if let Some(ref existing) = dat.psuf.clone() {
2744 dat.psuf = Some(multiquote(existing, quote_flag));
2745 }
2746 let ppl = dat.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
2747 let psl = dat.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
2748
2749 // c:2179-2184 — `doadd = !apar && !opar && !dpar`.
2750 let doadd = dat.apar.is_none() && dat.opar.is_none() && dat.dpar.is_empty();
2751 tracing::debug!(
2752 target: "compsys_args",
2753 %lpre,
2754 %lsuf,
2755 doadd,
2756 caf_match = (dat.aflags & CAF_MATCH) != 0,
2757 dpar = ?dat.dpar,
2758 "addmatches candidate-loop setup"
2759 );
2760 let mut apar_list: Vec<String> = Vec::new();
2761 let mut opar_list: Vec<String> = Vec::new();
2762
2763 // c:2189-2207 — `-D par…` setup. Each `dpar` name holds an array
2764 // PARALLEL to the candidate words; as each word is added, the current
2765 // element of every dpar array is consumed into an output list, and at
2766 // the end the output lists are written back (so each dpar param ends
2767 // up holding just the entries for the words that matched). Names whose
2768 // array is empty/missing are dropped (C swaps them out). Bug #657 —
2769 // the previous port collected apar/opar but never handled dpar.
2770 let mut dpar_names: Vec<String> = Vec::new();
2771 let mut dparr: Vec<Vec<String>> = Vec::new();
2772 let mut dpar_idx: Vec<usize> = Vec::new();
2773 let mut dparl: Vec<Vec<String>> = Vec::new();
2774 for name in &dat.dpar {
2775 match crate::ported::params::getaparam(name) {
2776 Some(arr) if !arr.is_empty() => {
2777 dpar_names.push(name.clone()); // c:2197 getaparam non-empty
2778 dparr.push(arr);
2779 dpar_idx.push(0);
2780 dparl.push(Vec::new());
2781 }
2782 _ => {} // c:2200 — drop names with an empty/missing array
2783 }
2784 }
2785
2786 // c:2460-2476 + c:2582-2600 — CAF_ARRAYS expansion. `compadd -a
2787 // NAME…` / `-k NAME…` pass PARAMETER names, not literal matches:
2788 // the candidates are the values of the named arrays (or, with
2789 // CAF_KEYS from `-k`, the keys of the named associative arrays).
2790 // C weaves array-switching through the candidate loop via the
2791 // `next_array` label; because every non-empty array's elements are
2792 // consumed in order, the resulting candidate set is just the
2793 // concatenation of `get_data_arr` over the names — so expand up
2794 // front. Without this, `_command_names`' `compadd -k commands`
2795 // adds the literal word "commands" instead of every command name.
2796 let expanded_argv: Vec<String>;
2797 let argv: &[String] = if (dat.aflags & CAF_ARRAYS) != 0 {
2798 let keys = (dat.aflags & CAF_KEYS) != 0; // c:2468 CAF_KEYS
2799 let mut acc: Vec<String> = Vec::new();
2800 for name in argv {
2801 if let Some(vals) = get_data_arr(name, keys) {
2802 acc.extend(vals);
2803 }
2804 }
2805 expanded_argv = acc;
2806 &expanded_argv
2807 } else {
2808 argv
2809 };
2810
2811 // c:2482-2601 — main candidate loop.
2812 let mut added = 0i32;
2813 let mut disp_idx = 0usize;
2814 let mut compignored_local = 0i32;
2815 // c:2520-2522 / c:2540-2542 — `-D` parallel-array bookkeeping: the dpar
2816 // arrays advance in LOCKSTEP with the candidate words. When a word is
2817 // skipped (ignored, or comp_match fails) its dpar element must be dropped
2818 // — i.e. `dpar_idx` advances WITHOUT the corresponding value being kept —
2819 // so that surviving words stay aligned with their array elements. The
2820 // port only advanced `dpar_idx` on the KEEP path (else branch below), so
2821 // after any non-matching word every survivor grabbed the wrong element
2822 // (e.g. `compadd -D` for path completion kept `/Applications` instead of
2823 // `/tmp`), breaking `cmd /path/<TAB>` entirely.
2824 macro_rules! dpar_skip_word {
2825 () => {
2826 for i in 0..dpar_idx.len() {
2827 if dpar_idx[i] < dparr[i].len() {
2828 dpar_idx[i] += 1;
2829 }
2830 }
2831 };
2832 }
2833 'cand: for word in argv {
2834 // c:2482
2835 // c:2486-2489 — advance disp index.
2836 let cur_disp = if !disp_arr.is_empty() && disp_idx < disp_arr.len() {
2837 let d = disp_arr[disp_idx].clone();
2838 disp_idx += 1;
2839 Some(d)
2840 } else {
2841 None
2842 };
2843
2844 // c:2491-2527 — aign/pign suffix-test + Patprog test.
2845 if !aign.is_empty() || !pign.is_empty() {
2846 let full = format!(
2847 "{}{}{}",
2848 dat.ppre.as_deref().unwrap_or(""),
2849 word,
2850 dat.psuf.as_deref().unwrap_or("")
2851 );
2852 // c:2509-2511 — literal-suffix check.
2853 for suf in &aign {
2854 if full.len() >= suf.len() && full.ends_with(suf.as_str()) {
2855 compignored_local += 1;
2856 dpar_skip_word!(); // c:2520
2857 continue 'cand;
2858 }
2859 }
2860 // c:2513-2518 — Patprog check.
2861 for prog in &pign {
2862 if crate::ported::pattern::pattry(prog, &full) {
2863 compignored_local += 1;
2864 dpar_skip_word!(); // c:2520
2865 continue 'cand;
2866 }
2867 }
2868 }
2869
2870 // c:2528 — CAF_MATCH dispatch: when CAF_MATCH is set, run
2871 // comp_match with the active matcher chain (else branch); else
2872 // emit the (multi)quoted word directly with a single-anchor
2873 // Cline (no-matcher path c:2530-2533).
2874 let ms: String;
2875 let _lc;
2876 let isexact;
2877 if (dat.aflags & CAF_MATCH) == 0 {
2878 // c:2528-2534 — non-match mode: just (multi)quote the word.
2879 ms = if (dat.aflags & CAF_QUOTE) != 0 {
2880 word.clone()
2881 } else {
2882 multiquote(word, 0)
2883 };
2884 let sl = ms.len() as i32;
2885 _lc = bld_parts(&ms, sl, -1, None, None);
2886 isexact = 0;
2887 } else {
2888 // c:2535
2889 // c:2535-2546 — matcher-driven mode via comp_match.
2890 let qu = if (dat.aflags & CAF_QUOTE) != 0 {
2891 0
2892 } else if dat.ppre.is_some() || (dat.flags & 0x0001/*CMF_FILE*/) == 0 {
2893 1
2894 } else {
2895 2
2896 };
2897 let mut lc_out: Option<Box<Cline>> = None;
2898 let mut isexact_out = 0i32;
2899 // c:2535 — comp_match(lpre, lsuf, s, cp, &lc, qu, &bpl, bcp,
2900 // &bsl, bcs, &isexact).
2901 match crate::ported::zle::compmatch::comp_match(
2902 &lpre,
2903 &lsuf,
2904 word,
2905 cp.as_ref(),
2906 Some(&mut lc_out),
2907 qu,
2908 None,
2909 bcp, // c:2535 — brace-count base advanced by the ppre strip
2910 None,
2911 0,
2912 &mut isexact_out,
2913 ) {
2914 Some(matched) => {
2915 ms = matched;
2916 _lc = lc_out;
2917 isexact = isexact_out;
2918 }
2919 None => {
2920 dpar_skip_word!(); // c:2540 — drop this word's dpar element
2921 continue 'cand; // c:2541-2545 reject
2922 }
2923 }
2924 }
2925
2926 if doadd {
2927 // c:2547
2928 // c:2556 — add_match_data.
2929 let cm = add_match_data(
2930 0,
2931 &ms,
2932 word,
2933 _lc.clone(), // line — real Cline from comp_match
2934 dat.ipre.as_deref().unwrap_or(""),
2935 "", // ripre
2936 dat.isuf.as_deref().unwrap_or(""),
2937 dat.pre.as_deref().unwrap_or(""),
2938 dat.prpre.as_deref().unwrap_or(""),
2939 dat.ppre.as_deref().unwrap_or(""),
2940 None, // pline (path-prefix Cline; unused on this path)
2941 dat.psuf.as_deref().unwrap_or(""),
2942 None, // sline (path-suffix Cline; unused on this path)
2943 dat.suf.as_deref().unwrap_or(""),
2944 dat.flags,
2945 isexact,
2946 );
2947 // c:2557-2564 — `cm->disp = dparr ? *dparr++ : NULL` (and the
2948 // CMF_DISPLINE flag when `-l`/CAF_...): C patches the STORED
2949 // match through the returned pointer; the port's return-by-value
2950 // clone can't, so patch the just-pushed copy in the live list.
2951 // Without this every `compadd -d` display string (the
2952 // description lines _describe/_arguments build) was silently
2953 // dropped and lists rendered bare match names.
2954 // c:2562-2563 — `if (disp) cm->disp = dupstring(*disp)`. C
2955 // patches the STORED match through the returned pointer; the
2956 // port's add_match_data returns a VALUE clone while the stored
2957 // copy lives in the `matches` list, so patch that copy in place
2958 // (inline — no C-counterpart fn). Without this every
2959 // `compadd -d` display string (the description lines
2960 // _describe/_arguments build) was silently dropped and lists
2961 // rendered bare match names.
2962 // c:2560-2561 — `cm->rems = dat->rems; cm->remf = dat->remf`. These
2963 // were previously SKIPPED here (only `disp` was patched), so a
2964 // `compadd -r <chars>` / `-R <widget>` custom suffix-removal spec
2965 // never reached the match: makesuffixstr got `s=None` and the
2966 // char-based auto-remove-suffix was inert. Patch them on the just-
2967 // pushed match copy (the port's add_match_data returns a value).
2968 {
2969 if let Ok(mut g) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2970 if let Some(last) = g.last_mut() {
2971 last.rems = dat.rems.clone(); // c:2560
2972 last.remf = dat.remf.clone(); // c:2561
2973 if cur_disp.is_some() {
2974 last.disp = cur_disp.clone(); // c:2562-2563
2975 }
2976 }
2977 }
2978 }
2979 let _ = cm;
2980 added += 1;
2981 } else {
2982 // c:2566
2983 if dat.apar.is_some() {
2984 // c:2567
2985 apar_list.push(ms.clone());
2986 }
2987 if dat.opar.is_some() {
2988 // c:2569
2989 opar_list.push(word.clone());
2990 }
2991 // c:2571-2578 — consume one element from each live dpar array
2992 // in lockstep with this added word.
2993 for i in 0..dparl.len() {
2994 if dpar_idx[i] < dparr[i].len() {
2995 dparl[i].push(dparr[i][dpar_idx[i]].clone());
2996 dpar_idx[i] += 1;
2997 }
2998 }
2999 }
3000 }
3001
3002 // c:2602-2608 — apar/opar/dpar writeback.
3003 if let Some(ref name) = dat.apar {
3004 setaparam(name, apar_list);
3005 }
3006 if let Some(ref name) = dat.opar {
3007 setaparam(name, opar_list);
3008 }
3009 // c:2606-2607 — set_list_array(dpar[i], dparl[i]).
3010 for (i, name) in dpar_names.iter().enumerate() {
3011 setaparam(name, std::mem::take(&mut dparl[i]));
3012 }
3013
3014 // c:2610 — explanation emit.
3015 if dat.exp.is_some() {
3016 // c:2610
3017 addexpl(false);
3018 }
3019
3020 // c:2612-2614 — `<all>` placeholder when CAF_ALL set.
3021 let hasall = hasallmatch.load(Ordering::Relaxed);
3022 if hasall == 0 && (dat.aflags & CAF_ALL) != 0 {
3023 addmatch(
3024 "<all>",
3025 dat.flags | crate::ported::zle::comp_h::CMF_ALL,
3026 None,
3027 true,
3028 );
3029 hasallmatch.store(1, Ordering::Relaxed);
3030 }
3031
3032 // c:2616-2617 — dummy entries. C's addmatch advances the SHARED disp
3033 // cursor (`&disp`, c:2054-2058), so each dummy carries the next
3034 // remaining display string — this is how the description-only lines
3035 // (compdescribe's CRT_EXPL `-E n` run: "opt -- description") reach
3036 // the list. Passing None here dropped every description line.
3037 while dat.dummies > 0 {
3038 let d: Option<&str> = if disp_idx < disp_arr.len() {
3039 let d = Some(disp_arr[disp_idx].as_str());
3040 disp_idx += 1;
3041 d
3042 } else {
3043 None
3044 };
3045 addmatch(
3046 "",
3047 dat.flags | crate::ported::zle::comp_h::CMF_DUMMY,
3048 d,
3049 false,
3050 );
3051 dat.dummies -= 1;
3052 }
3053
3054 tracing::debug!(
3055 target: "compsys_args",
3056 added,
3057 mnum = mnum.load(Ordering::Relaxed),
3058 doadd,
3059 "addmatches candidate-loop done"
3060 );
3061 let _ = (ppl, psl, compignored_local, added);
3062 // c:2636 — `return (mnum == nm)`: non-zero (1) when NO new matches were
3063 // added this call, 0 when at least one landed. Was hardcoded `0`, so
3064 // every `compadd` reported success even against zero matches — the
3065 // `completer` chain (_main_complete) then stopped after `_complete`
3066 // and never advanced to `_approximate` / `_ignored` / etc., and any
3067 // `compadd … && ret=0` idiom in a completer wrongly took the match arm.
3068 if mnum.load(Ordering::Relaxed) == _nm {
3069 1
3070 } else {
3071 0
3072 }
3073}
3074
3075// =====================================================================
3076// add_match_data — `Src/Zle/compcore.c:2643`.
3077// =====================================================================
3078
3079/// Direct port of `Cmatch add_match_data(int alt, char *str, char *orig,
3080/// Cline line, char *ipre, char *ripre, char *isuf, char *pre,
3081/// char *prpre, char *ppre, Cline pline, char *psuf, Cline sline,
3082/// char *suf, int flags, int exact)` from compcore.c:2643.
3083///
3084/// Builds one `Cmatch` from the supplied prefix/suffix bits plus the
3085/// surrounding Cline chain. Threads `line`/`pline`/`sline` through
3086/// `cline_matched` so the CLF_MATCHED state-machine update fires the
3087/// same way as C, then performs path-prefix/suffix splicing via
3088/// `bld_parts` to extend the Cline chain at the appropriate anchor.
3089#[allow(clippy::too_many_arguments)]
3090pub fn add_match_data(
3091 // c:2643
3092 alt: i32,
3093 str: &str,
3094 orig: &str,
3095 mut line: Option<Box<Cline>>,
3096 ipre_: &str,
3097 ripre_: &str,
3098 isuf_: &str,
3099 pre: &str,
3100 prpre: &str,
3101 ppre: &str,
3102 mut pline: Option<Box<Cline>>,
3103 psuf: &str,
3104 mut sline: Option<Box<Cline>>,
3105 suf: &str,
3106 flags: i32,
3107 exact: i32,
3108) -> Cmatch {
3109 // c:2663 — DPUTS(!line, "BUG: add_match_data() without cline")
3110 DPUTS!(line.is_none(), "BUG: add_match_data() without cline"); // c:2663
3111 // c:2657 — pick the active aminfo by `alt` (alternative path = fignore).
3112 let _ai_ref = if alt != 0 { &fainfo } else { &ainfo }; // c:2657
3113 // c:2666-2671 — cline_matched(line); pline; sline.
3114 cline_matched(&mut line);
3115 if pline.is_some() {
3116 cline_matched(&mut pline);
3117 }
3118 if sline.is_some() {
3119 cline_matched(&mut sline);
3120 }
3121
3122 // c:2675-2697 — accumulator lengths.
3123 let psl = psuf.len();
3124 let isl = isuf_.len();
3125 let qisuf_v = qisuf_get(); // c:2680
3126 let qisl = qisuf_v.len();
3127 let _salen = (if sline.is_none() { psl } else { 0 }) + isl + qisl; // c:2675-2683
3128
3129 let ipl = ipre_.len();
3130 let _ppl = ppre.len();
3131 let _pl = pre.len();
3132 let qipre_v = qipre_get(); // c:2686
3133 let qipl_v = qipre_v.clone();
3134 let _qipl = qipl_v.len();
3135
3136 let _stl = str.len();
3137 let _lpl = ripre_.len();
3138 let _lsl = suf.len();
3139 let _ml = ipl;
3140
3141 // c:2671-2762 — path-suffix Cline splicing. salen accumulates psl
3142 // (psuf when no sline), isl (isuf), qisl (qisuf). When salen > 0
3143 // and line is non-empty, we walk to the tail and append the
3144 // bld_parts-built Cline for each contributing string.
3145 let psl_local = if sline.is_none() && !psuf.is_empty() {
3146 psuf.len() as i32
3147 } else {
3148 0
3149 };
3150 let isl_local = isuf_.len() as i32;
3151 let qisl_local = qisuf_v.len() as i32;
3152 let salen = psl_local + isl_local + qisl_local;
3153 if salen > 0 && line.is_some() {
3154 // Walk to the tail of line via .next.
3155 unsafe {
3156 let mut tail: *mut Option<Box<Cline>> = &mut line;
3157 while let Some(ref n) = *tail {
3158 if n.next.is_none() {
3159 break;
3160 }
3161 tail = &mut (*tail).as_mut().unwrap().next;
3162 }
3163 // For each contributing string, build a Cline chain via
3164 // bld_parts and attach to the tail node's .next.
3165 if psl_local > 0 {
3166 let s = bld_parts(psuf, psl_local, psl_local, None, None);
3167 if let Some(node) = (*tail).as_mut() {
3168 node.next = s;
3169 while let Some(ref nn) = node.next {
3170 if nn.next.is_none() {
3171 break;
3172 }
3173 // already linked correctly; loop to advance tail
3174 break;
3175 }
3176 }
3177 // Walk to the new tail.
3178 while let Some(ref n) = *tail {
3179 if n.next.is_none() {
3180 break;
3181 }
3182 tail = &mut (*tail).as_mut().unwrap().next;
3183 }
3184 }
3185 if isl_local > 0 {
3186 let s = bld_parts(isuf_, isl_local, isl_local, None, None);
3187 if let Some(node) = (*tail).as_mut() {
3188 node.next = s;
3189 }
3190 while let Some(ref n) = *tail {
3191 if n.next.is_none() {
3192 break;
3193 }
3194 tail = &mut (*tail).as_mut().unwrap().next;
3195 }
3196 }
3197 if qisl_local > 0 {
3198 let mut s = bld_parts(&qisuf_v, qisl_local, qisl_local, None, None);
3199 // c:2741 — qsl->flags |= CLF_SUF; qsl->suffix = qsl->prefix.
3200 if let Some(qsl) = s.as_mut() {
3201 qsl.flags |= crate::ported::zle::comp_h::CLF_SUF;
3202 qsl.suffix = qsl.prefix.take();
3203 }
3204 if let Some(node) = (*tail).as_mut() {
3205 node.next = s;
3206 }
3207 }
3208 }
3209 }
3210
3211 // c:2766-2873 — path-prefix Cline splicing. palen accumulates qipl,
3212 // ipl, pl, ppl (when no pline). Each contributing string gets a
3213 // bld_parts Cline prepended to `line`.
3214 let qipl_local = qipre_v.len() as i32;
3215 let ipl_local = ipre_.len() as i32;
3216 let pl_local = pre.len() as i32;
3217 let ppl_local = if pline.is_none() && !ppre.is_empty() {
3218 ppre.len() as i32
3219 } else {
3220 0
3221 };
3222 if pl_local > 0 {
3223 if ppl_local > 0 {
3224 let p = bld_parts(ppre, ppl_local, ppl_local, None, None);
3225 // Walk p to its tail, link its tail's next to line.
3226 if p.is_some() {
3227 let mut p_chain = p;
3228 let mut tail: *mut Option<Box<Cline>> = &mut p_chain;
3229 unsafe {
3230 while let Some(ref n) = *tail {
3231 if n.next.is_none() {
3232 break;
3233 }
3234 tail = &mut (*tail).as_mut().unwrap().next;
3235 }
3236 if let Some(t) = (*tail).as_mut() {
3237 t.next = line.take();
3238 }
3239 }
3240 line = p_chain;
3241 }
3242 }
3243 let p = bld_parts(pre, pl_local, pl_local, None, None);
3244 if let Some(mut head) = p {
3245 let mut t: *mut Option<Box<Cline>> = &mut head.next;
3246 unsafe {
3247 while (*t).is_some() {
3248 if (*t).as_deref().unwrap().next.is_none() {
3249 break;
3250 }
3251 t = &mut (*t).as_mut().unwrap().next;
3252 }
3253 *t = line.take();
3254 }
3255 line = Some(head);
3256 }
3257 if ipl_local > 0 {
3258 let p = bld_parts(ipre_, ipl_local, ipl_local, None, None);
3259 if let Some(mut head) = p {
3260 let mut t: *mut Option<Box<Cline>> = &mut head.next;
3261 unsafe {
3262 // Walk to the empty slot past the tail node, then attach
3263 // `line` there. The previous code stopped AT the last node
3264 // and did `*t = line`, OVERWRITING it (dropped the final
3265 // bld_parts segment — e.g. the last dir component `zdt3/`
3266 // of `/private/tmp/zdt3/` — so ambiguous path completion
3267 // corrupted the line). c:2812/2818/2824/2841 `lp->next =
3268 // line` appends; it never replaces lp.
3269 while (*t).is_some() {
3270 t = &mut (*t).as_mut().unwrap().next;
3271 }
3272 *t = line.take();
3273 }
3274 line = Some(head);
3275 }
3276 }
3277 if qipl_local > 0 {
3278 let p = bld_parts(&qipre_v, qipl_local, qipl_local, None, None);
3279 if let Some(mut head) = p {
3280 let mut t: *mut Option<Box<Cline>> = &mut head.next;
3281 unsafe {
3282 // Walk to the empty slot past the tail node, then attach
3283 // `line` there. The previous code stopped AT the last node
3284 // and did `*t = line`, OVERWRITING it (dropped the final
3285 // bld_parts segment — e.g. the last dir component `zdt3/`
3286 // of `/private/tmp/zdt3/` — so ambiguous path completion
3287 // corrupted the line). c:2812/2818/2824/2841 `lp->next =
3288 // line` appends; it never replaces lp.
3289 while (*t).is_some() {
3290 t = &mut (*t).as_mut().unwrap().next;
3291 }
3292 *t = line.take();
3293 }
3294 line = Some(head);
3295 }
3296 }
3297 } else if qipl_local + ipl_local + pl_local + ppl_local > 0 || pline.is_some() {
3298 // c:2827-2842 — consolidated apre buffer.
3299 let apre = format!(
3300 "{}{}{}{}",
3301 qipre_v.as_str(),
3302 ipre_,
3303 pre,
3304 if pline.is_none() { ppre } else { "" }
3305 );
3306 let apre_len = apre.len() as i32;
3307 if apre_len > 0 {
3308 let p = bld_parts(&apre, apre_len, apre_len, None, None);
3309 if let Some(mut head) = p {
3310 let mut t: *mut Option<Box<Cline>> = &mut head.next;
3311 unsafe {
3312 // Walk to the empty slot past the tail node, then attach
3313 // `line` there. The previous code stopped AT the last node
3314 // and did `*t = line`, OVERWRITING it (dropped the final
3315 // bld_parts segment — e.g. the last dir component `zdt3/`
3316 // of `/private/tmp/zdt3/` — so ambiguous path completion
3317 // corrupted the line). c:2812/2818/2824/2841 `lp->next =
3318 // line` appends; it never replaces lp.
3319 while (*t).is_some() {
3320 t = &mut (*t).as_mut().unwrap().next;
3321 }
3322 *t = line.take();
3323 }
3324 line = Some(head);
3325 }
3326 }
3327 }
3328
3329 let stl = str.len();
3330
3331 // c:2929-2932 — Cmatch allocation + str/orig/ppre/psuf.
3332 let mut cm = Cmatch::default(); // c:2929
3333 cm.str = Some(str.to_string()); // c:2930
3334 cm.orig = Some(orig.to_string()); // c:2931
3335 cm.ppre = if ppre.is_empty() {
3336 None
3337 } else {
3338 Some(ppre.into())
3339 }; // c:2932
3340 cm.psuf = if psuf.is_empty() {
3341 None
3342 } else {
3343 Some(psuf.into())
3344 }; // c:2933
3345
3346 // c:2934 — prpre only when CMF_FILE.
3347 cm.prpre = if (flags & CMF_FILE) != 0 && !prpre.is_empty() {
3348 Some(prpre.into())
3349 } else {
3350 None
3351 };
3352
3353 // c:2935-2938 — ipre = qipre + ipre (concat when qipre non-empty).
3354 // qipre_v already computed above.
3355 cm.ipre = if !qipre_v.is_empty() {
3356 if !ipre_.is_empty() {
3357 Some(format!("{}{}", qipre_v, ipre_))
3358 } else {
3359 Some(qipre_v.clone())
3360 }
3361 } else if !ipre_.is_empty() {
3362 Some(ipre_.into())
3363 } else {
3364 None
3365 };
3366
3367 cm.ripre = if ripre_.is_empty() {
3368 None
3369 } else {
3370 Some(ripre_.into())
3371 }; // c:2939
3372
3373 // c:2940-2943 — isuf = isuf + qisuf (concat when qisuf non-empty).
3374 cm.isuf = if !qisuf_v.is_empty() {
3375 if !isuf_.is_empty() {
3376 Some(format!("{}{}", isuf_, qisuf_v))
3377 } else {
3378 Some(qisuf_v.clone())
3379 }
3380 } else if !isuf_.is_empty() {
3381 Some(isuf_.into())
3382 } else {
3383 None
3384 };
3385
3386 cm.pre = if pre.is_empty() {
3387 None
3388 } else {
3389 Some(pre.into())
3390 }; // c:2944
3391 cm.suf = if suf.is_empty() {
3392 None
3393 } else {
3394 Some(suf.into())
3395 }; // c:2945
3396
3397 // c:2946 — flags + CMF_PACKED/CMF_ROWS from complist.
3398 let complist_s = COMPLIST
3399 .get()
3400 .and_then(|m| m.lock().ok().map(|g| g.clone()))
3401 .unwrap_or_default();
3402 let extra_flags = (if complist_s.contains("packed") {
3403 CMF_PACKED
3404 } else {
3405 0
3406 }) | (if complist_s.contains("rows") {
3407 CMF_ROWS
3408 } else {
3409 0
3410 });
3411 cm.flags = flags | extra_flags;
3412
3413 // c:2950-2951 — mode/fmode init to 0.
3414 cm.mode = 0;
3415 cm.fmode = 0;
3416 cm.modec = '\0';
3417 cm.fmodec = '\0';
3418
3419 // c:2952-2970 — CMF_FILE: stat the path for mode + modec.
3420 use crate::ported::zle::comp_h::CMF_FILE;
3421 if (flags & CMF_FILE) != 0 && !orig.is_empty() && !orig.ends_with('/') {
3422 let pb = format!("{}{}", cm.prpre.as_deref().unwrap_or("./"), orig);
3423 // c:2960-2963 — `ztat(pb, &buf, 1); cm->mode = buf.st_mode;
3424 // if ((cm->modec = file_type(buf.st_mode)) == ' ') cm->modec = 0;`
3425 // The `1` is ztat's ls flag → lstat, so modec is the type of the
3426 // link itself (`@` for a symlink). This is the marker printlist
3427 // shows, so it must come from lstat, not stat.
3428 if let Some(meta) = ztat(&pb, true) {
3429 use std::os::unix::fs::MetadataExt;
3430 cm.mode = meta.mode();
3431 let c = crate::ported::glob::file_type(cm.mode); // c:2962
3432 cm.modec = if c == ' ' { '\0' } else { c }; // c:2963
3433 }
3434 // c:2965-2968 — `ztat(pb, &buf, 0)` → stat, so fmode/fmodec is the
3435 // type of the symlink target (the marker used when following links).
3436 if let Some(meta) = ztat(&pb, false) {
3437 use std::os::unix::fs::MetadataExt;
3438 cm.fmode = meta.mode();
3439 let c = crate::ported::glob::file_type(cm.fmode); // c:2967
3440 cm.fmodec = if c == ' ' { '\0' } else { c }; // c:2968
3441 }
3442 }
3443
3444 // c:2974-2993 — brpl/brsl brace position arrays. Walk BRBEG/BREND
3445 // (the global Brinfo chains from `Src/Zle/zle_tricky.c`), reading
3446 // `qpos` for each entry to derive the position offset within the
3447 // match string. With no brace chain populated (zero-brace common
3448 // case) brpl/brsl stay empty.
3449 cm.brpl = BRBEG
3450 .get_or_init(|| Mutex::new(None))
3451 .lock()
3452 .ok()
3453 .and_then(|g| {
3454 g.as_ref().map(|head| {
3455 let mut out: Vec<i32> = Vec::new();
3456 let mut cur = Some(head.as_ref());
3457 while let Some(n) = cur {
3458 out.push(n.qpos);
3459 cur = n.next.as_deref();
3460 }
3461 out
3462 })
3463 })
3464 .unwrap_or_default();
3465 cm.brsl = BREND
3466 .get_or_init(|| Mutex::new(None))
3467 .lock()
3468 .ok()
3469 .and_then(|g| {
3470 g.as_ref().map(|head| {
3471 let mut out: Vec<i32> = Vec::new();
3472 let mut cur = Some(head.as_ref());
3473 while let Some(n) = cur {
3474 out.push(n.qpos);
3475 cur = n.next.as_deref();
3476 }
3477 out
3478 })
3479 })
3480 .unwrap_or_default();
3481
3482 cm.qipl = qipre_v.len() as i32; // c:2994
3483 cm.qisl = qisuf_v.len() as i32; // c:2995
3484 // c:2996 — autoq read.
3485 let autoq_v = AUTOQ
3486 .get()
3487 .and_then(|m| m.lock().ok().map(|g| g.clone()))
3488 .unwrap_or_default();
3489 cm.autoq = if !autoq_v.is_empty() {
3490 Some(autoq_v)
3491 } else if INBACKT.load(Ordering::Relaxed) != 0 {
3492 Some("`".into())
3493 } else {
3494 None
3495 };
3496
3497 cm.rems = None;
3498 cm.remf = None;
3499 cm.disp = None; // c:2997
3500
3501 // c:3003 — ai->line = join_clines(ai->line, line).
3502 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3503 if let Some(a) = g.as_mut() {
3504 let old_line = a.line.take();
3505 a.line = crate::ported::zle::compmatch::join_clines(old_line, line);
3506 }
3507 }
3508
3509 // c:3005 — mnum++.
3510 mnum.fetch_add(1, Ordering::Relaxed);
3511
3512 // c:3006 — ai->count++.
3513 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3514 if let Some(a) = g.as_mut() {
3515 a.count += 1;
3516 }
3517 }
3518
3519 // c:3008 — addlinknode((alt ? fmatches : matches), cm). Already
3520 // wired below via matches Vec push.
3521
3522 // c:3010-3011 — newmatches = 1; mgroup->new = 1.
3523 newmatches.store(1, Ordering::Relaxed);
3524
3525 // c:3012-3013 — compignored++ when alt.
3526 if alt != 0 {
3527 crate::ported::zle::complete::COMPIGNORED.fetch_add(1, Ordering::Relaxed);
3528 }
3529
3530 // c:3015-3016 — `if (!*complastprompt) dolastprompt = 0;`. Read the
3531 // `complastprompt` value (the "last_prompt" compstate, set at c:326 to
3532 // "yes"/"" from ALWAYS_LAST_PROMPT) — NOT `complastprefix`, a different
3533 // variable the previous port read by mistake. With that bug dolastprompt
3534 // was cleared on every completion, so `clearflag` stayed 0 and `trashzle`
3535 // never emitted TCCLEAREOD: an on-screen completion list was left
3536 // stranded when the command was accepted.
3537 let complastprompt_v = get_compstate_str("last_prompt").unwrap_or_default();
3538 if complastprompt_v.is_empty() {
3539 dolastprompt.store(0, Ordering::Relaxed);
3540 }
3541
3542 // c:3018-3023 — curexpl.count/fcount increment.
3543 if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
3544 if let Some(e) = g.as_mut() {
3545 if alt != 0 {
3546 e.fcount += 1;
3547 } else {
3548 e.count += 1;
3549 }
3550 }
3551 }
3552
3553 // c:3024-3025 — ai->firstm = cm.
3554 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3555 if let Some(a) = g.as_mut() {
3556 if a.firstm.is_none() {
3557 a.firstm = Some(Box::new(cm.clone()));
3558 }
3559 }
3560 }
3561
3562 // c:3027-3034 — minmlen/maxmlen tracking.
3563 let lpl = cm.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
3564 let lsl = cm.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
3565 let ml = (stl + lpl + lsl) as i32;
3566 let cur_min = minmlen.load(Ordering::Relaxed);
3567 let cur_max = maxmlen.load(Ordering::Relaxed);
3568 if ml < cur_min {
3569 minmlen.store(ml, Ordering::Relaxed);
3570 }
3571 if ml > cur_max {
3572 maxmlen.store(ml, Ordering::Relaxed);
3573 }
3574
3575 // c:3037-3064 — exact-match tracking on ai.
3576 if exact != 0 {
3577 // c:3037
3578 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3579 if let Some(a) = g.as_mut() {
3580 if a.exact == 0 {
3581 // c:3038
3582 a.exact = useexact.load(Ordering::Relaxed);
3583 a.exactm = Some(Box::new(cm.clone())); // c:3058
3584 } else if useexact.load(Ordering::Relaxed) != 0 {
3585 // c:3059
3586 // c:3060-3061 — ambiguous exact: set to 2, clear exactm.
3587 a.exact = 2;
3588 a.exactm = None;
3589 }
3590 }
3591 }
3592 }
3593
3594 // c:3064 — push cm into matches/fmatches LinkList.
3595 let cell = if alt != 0 {
3596 fmatches.get_or_init(|| Mutex::new(Vec::new()))
3597 } else {
3598 matches.get_or_init(|| Mutex::new(Vec::new()))
3599 };
3600 if let Ok(mut g) = cell.lock() {
3601 g.push(cm.clone());
3602 }
3603
3604 cm // c:3067 return cm
3605}
3606
3607// `lookup_complist_flags` deleted — Rust-only 8-line helper. Inlined
3608// at the single call site in callcompfunc (c:2049-2051).
3609
3610// =====================================================================
3611// begcmgroup — `Src/Zle/compcore.c:3073`.
3612// =====================================================================
3613
3614/// Port of `mod_export void begcmgroup(char *n, int flags)` from
3615/// compcore.c:3073.
3616pub fn begcmgroup(n: Option<&str>, flags: i32) {
3617 // c:3073
3618 if let Some(name) = n {
3619 // c:3073
3620 let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON // c:3085
3621 | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
3622 // c:3078-3094 — reuse an existing group with the same name+flags.
3623 let reused = {
3624 let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
3625 cell.lock().ok().and_then(|g| {
3626 g.iter()
3627 .find(|grp| grp.name.as_deref() == Some(name) && (grp.flags & mask) == flags)
3628 .cloned() // c:3088
3629 })
3630 };
3631 if let Some(active) = reused {
3632 // c:3090-3093 — `expls = p->lexpls; matches = p->lmatches;
3633 // fmatches = p->lfmatches; allccs = p->lallccs;`. In C these
3634 // are pointer aliases into the reused group so appends keep
3635 // flowing into it. The Rust port keeps them as separate
3636 // Mutex globals, so restore their contents from the group;
3637 // `endcmgroup` flushes them back on close.
3638 if let Ok(mut m) = expls.get_or_init(|| Mutex::new(Vec::new())).lock() {
3639 *m = active.lexpls.clone();
3640 }
3641 if let Ok(mut m) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3642 *m = active.lmatches.clone();
3643 }
3644 if let Ok(mut m) = fmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3645 *m = active.lfmatches.clone();
3646 }
3647 if let Ok(mut m) = allccs.get_or_init(|| Mutex::new(Vec::new())).lock() {
3648 *m = active.lallccs.clone();
3649 }
3650 let mc = mgroup.get_or_init(|| Mutex::new(None));
3651 if let Ok(mut s) = mc.lock() {
3652 *s = Some(active);
3653 }
3654 return; // c:3095
3655 }
3656 }
3657 let mut grp = Cmgroup::default(); // c:3101
3658 grp.name = n.map(String::from); // c:3105
3659 grp.flags = flags; // c:3108
3660 let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
3661 if let Ok(mut g) = cell.lock() {
3662 g.insert(0, grp.clone()); // c:3121-3124
3663 }
3664 let mc = mgroup.get_or_init(|| Mutex::new(None));
3665 if let Ok(mut s) = mc.lock() {
3666 *s = Some(grp);
3667 }
3668 if let Ok(mut g) = expls.get_or_init(|| Mutex::new(Vec::new())).lock() {
3669 g.clear();
3670 }
3671 if let Ok(mut g) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3672 g.clear();
3673 }
3674 if let Ok(mut g) = fmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3675 g.clear();
3676 }
3677 if let Ok(mut g) = allccs.get_or_init(|| Mutex::new(Vec::new())).lock() {
3678 g.clear();
3679 }
3680}
3681
3682// =====================================================================
3683// endcmgroup — `Src/Zle/compcore.c:3131`.
3684// =====================================================================
3685
3686/// Port of `mod_export void endcmgroup(char **ylist)` from
3687/// compcore.c:3131.
3688pub fn endcmgroup(ylist: Option<Vec<String>>) {
3689 // c:3131 — C is a one-liner (`mgroup->ylist = ylist`) because the
3690 // file-scope `matches`/`fmatches`/`expls`/`allccs` LinkLists ARE this
3691 // group's `l*` lists (aliased by begcmgroup). The Rust port keeps
3692 // those as separate Mutex globals, so on close we flush them into the
3693 // matching group inside `amatches` (identified by name+flags exactly
3694 // as begcmgroup dedups). Without this the matches added between
3695 // begcmgroup/endcmgroup never reach permmatches and `nmatches`
3696 // stays 0.
3697 let yl = ylist.unwrap_or_default();
3698
3699 // Snapshot the file-scope accumulators before touching amatches
3700 // (distinct mutexes; snapshot-first keeps the lock scopes disjoint).
3701 let m_snap = matches
3702 .get_or_init(|| Mutex::new(Vec::new()))
3703 .lock()
3704 .map(|g| g.clone())
3705 .unwrap_or_default();
3706 let fm_snap = fmatches
3707 .get_or_init(|| Mutex::new(Vec::new()))
3708 .lock()
3709 .map(|g| g.clone())
3710 .unwrap_or_default();
3711 let ex_snap = expls
3712 .get_or_init(|| Mutex::new(Vec::new()))
3713 .lock()
3714 .map(|g| g.clone())
3715 .unwrap_or_default();
3716 let ac_snap = allccs
3717 .get_or_init(|| Mutex::new(Vec::new()))
3718 .lock()
3719 .map(|g| g.clone())
3720 .unwrap_or_default();
3721
3722 // Identify the current group and record ylist on the mgroup holder.
3723 let (name, flags, new_) = {
3724 let mc = mgroup.get_or_init(|| Mutex::new(None));
3725 match mc.lock() {
3726 Ok(mut g) => match g.as_mut() {
3727 Some(grp) => {
3728 grp.ylist = yl.clone(); // c:3140
3729 (grp.name.clone(), grp.flags, grp.new_)
3730 }
3731 None => return,
3732 },
3733 Err(_) => return,
3734 }
3735 };
3736
3737 let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
3738 // Rust-only correctness note (no C counterpart — C aliases `matches` to
3739 // the current group's mlist so permmatches always sees live matches):
3740 // the port keeps `matches` as a separate global flushed here, so a
3741 // `permmatches` triggered mid-completion (e.g. a completer's
3742 // `$compstate[nmatches]` read) can process a still-open group BEFORE its
3743 // matches are flushed, cache nmatches with that group empty, and — since
3744 // permmatches early-returns when `newmatches==0` — never re-count it
3745 // after this flush. Every `compadd -J g2` past the first vanished. Mark
3746 // `newmatches` when a flush actually moves matches into a group so the
3747 // next permmatches recomputes instead of returning the stale cache.
3748 let flushed_any = !m_snap.is_empty() || !fm_snap.is_empty();
3749 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
3750 if let Some(grp) = g
3751 .iter_mut()
3752 .find(|grp| grp.name == name && (grp.flags & mask) == (flags & mask))
3753 {
3754 grp.lmatches = m_snap;
3755 grp.lfmatches = fm_snap;
3756 grp.lexpls = ex_snap;
3757 grp.lallccs = ac_snap;
3758 grp.ylist = yl;
3759 grp.new_ = new_;
3760 }
3761 }
3762 if flushed_any {
3763 newmatches.store(1, Ordering::Relaxed);
3764 }
3765}
3766
3767// =====================================================================
3768// addexpl — `Src/Zle/compcore.c:3140`.
3769// =====================================================================
3770
3771/// Port of `mod_export void addexpl(int always)` from compcore.c:3140.
3772pub fn addexpl(always: bool) {
3773 // c:3140
3774 let curexpl_snap = {
3775 let cell = curexpl.get_or_init(|| Mutex::new(None));
3776 cell.lock().ok().and_then(|g| g.clone())
3777 };
3778 let curexpl_str = match curexpl_snap.as_ref().and_then(|e| e.str.clone()) {
3779 Some(s) => s,
3780 None => return,
3781 };
3782 let curexpl_count = curexpl_snap.as_ref().map(|e| e.count).unwrap_or(0);
3783 let curexpl_fcount = curexpl_snap.as_ref().map(|e| e.fcount).unwrap_or(0);
3784
3785 let elist = expls.get_or_init(|| Mutex::new(Vec::new()));
3786 if let Ok(mut g) = elist.lock() {
3787 for e in g.iter_mut() {
3788 // c:3145
3789 if e.str.as_deref() == Some(curexpl_str.as_str()) {
3790 // c:3147
3791 e.count += curexpl_count; // c:3148
3792 e.fcount += curexpl_fcount; // c:3149
3793 if always {
3794 // c:3150
3795 e.always = 1;
3796 nmessages.fetch_add(1, Ordering::Relaxed); // c:3152
3797 newmatches.store(1, Ordering::Relaxed); // c:3153
3798 let mc = mgroup.get_or_init(|| Mutex::new(None));
3799 if let Ok(mut mg) = mc.lock() {
3800 if let Some(grp) = mg.as_mut() {
3801 grp.new_ = 1;
3802 }
3803 }
3804 }
3805 return; // c:3156
3806 }
3807 }
3808 if let Some(e) = curexpl_snap {
3809 // c:3159
3810 g.push(e);
3811 }
3812 }
3813 newmatches.store(1, Ordering::Relaxed); // c:3160
3814 if always {
3815 // c:3161
3816 let mc = mgroup.get_or_init(|| Mutex::new(None));
3817 if let Ok(mut mg) = mc.lock() {
3818 if let Some(grp) = mg.as_mut() {
3819 grp.new_ = 1;
3820 }
3821 }
3822 nmessages.fetch_add(1, Ordering::Relaxed); // c:3173
3823 }
3824}
3825
3826// =====================================================================
3827// matchcmp — `Src/Zle/compcore.c:3173`.
3828// =====================================================================
3829
3830/// Port of `static int matchcmp(Cmatch *a, Cmatch *b)` from
3831/// compcore.c:3173.
3832pub fn matchcmp(a: &Cmatch, b: &Cmatch) -> std::cmp::Ordering {
3833 // c:3173
3834 let order = MATCHORDER.load(Ordering::Relaxed);
3835 let sortdir = if (order & CGF_REVSORT) != 0 { -1 } else { 1 }; // c:3177
3836
3837 let cmp = (b.disp.is_some() as i32) - (a.disp.is_some() as i32); // c:3176
3838 let (as_, bs) = if (order & CGF_MATSORT) != 0 || (cmp == 0 && a.disp.is_none()) {
3839 (
3840 a.str.clone().unwrap_or_default(), // c:3181
3841 b.str.clone().unwrap_or_default(),
3842 ) // c:3182
3843 } else {
3844 if cmp != 0 {
3845 // c:3184
3846 let raw = (cmp as i32) * sortdir;
3847 return if raw < 0 {
3848 std::cmp::Ordering::Less
3849 }
3850 // c:3185
3851 else if raw > 0 {
3852 std::cmp::Ordering::Greater
3853 } else {
3854 std::cmp::Ordering::Equal
3855 };
3856 }
3857 let displine_cmp = (b.flags & CMF_DISPLINE) - (a.flags & CMF_DISPLINE); // c:3187
3858 if displine_cmp != 0 {
3859 // c:3188
3860 let raw = displine_cmp * sortdir;
3861 return if raw < 0 {
3862 std::cmp::Ordering::Less
3863 } else if raw > 0 {
3864 std::cmp::Ordering::Greater
3865 } else {
3866 std::cmp::Ordering::Equal
3867 };
3868 }
3869 (
3870 a.disp.clone().unwrap_or_default(), // c:3191
3871 b.disp.clone().unwrap_or_default(),
3872 ) // c:3192
3873 };
3874 // c:3195-3197 — `sortdir * zstrcmp(as, bs, SORTIT_IGNORING_BACKSLASHES |
3875 // ((isset(NUMERICGLOBSORT) || matchorder & CGF_NUMSORT) ?
3876 // SORTIT_NUMERICALLY : 0))`. zstrcmp routes through strcoll, so the
3877 // ordering is the locale's collation (case-insensitive on the usual
3878 // macOS/Linux locales) — matching zsh. The previous byte comparison
3879 // sorted ASCII, putting every uppercase name (`README.md`) ahead of
3880 // lowercase ones (`alpha.txt`), which diverged from zsh's completion
3881 // order.
3882 let numeric = crate::ported::zsh_h::isset(crate::ported::zsh_h::NUMERICGLOBSORT)
3883 || (order & CGF_NUMSORT) != 0;
3884 let flags = crate::ported::zsh_h::SORTIT_IGNORING_BACKSLASHES as u32
3885 | if numeric {
3886 crate::ported::zsh_h::SORTIT_NUMERICALLY as u32
3887 } else {
3888 0
3889 };
3890 let base = crate::ported::sort::zstrcmp(&as_, &bs, flags);
3891 if sortdir < 0 {
3892 base.reverse()
3893 } else {
3894 base
3895 }
3896}
3897
3898/// Port of `static int matcheq(Cmatch a, Cmatch b)` from
3899/// compcore.c:3206.
3900pub fn matcheq(a: &Cmatch, b: &Cmatch) -> bool {
3901 // c:3207
3902 matchstreq(a.ipre.as_ref(), b.ipre.as_ref()) && // c:3207
3903 matchstreq(a.pre.as_ref(), b.pre.as_ref()) && // c:3210
3904 matchstreq(a.ppre.as_ref(), b.ppre.as_ref()) && // c:3211
3905 matchstreq(a.psuf.as_ref(), b.psuf.as_ref()) && // c:3212
3906 matchstreq(a.suf.as_ref(), b.suf.as_ref()) && // c:3213
3907 matchstreq(a.str.as_ref(), b.str.as_ref()) // c:3214
3908}
3909
3910// =====================================================================
3911// makearray — `Src/Zle/compcore.c:3224`.
3912// =====================================================================
3913
3914/// Port of `static Cmatch *makearray(LinkList l, int type, int flags,
3915/// int *np, int *nlp, int *llp)`
3916/// from compcore.c:3223. Returns `(arr, n, nl, ll)`.
3917///
3918/// `type` is fixed to `1` (match-sort path) for the in-file call sites
3919/// from `permmatches`. The `type=0` string-sort path on `lexpls` is
3920/// inlined at the `permmatches` call site (C uses a `(char **)` cast
3921/// trick that has no safe Rust equivalent).
3922pub fn makearray(mut rp: Vec<Cmatch>, flags: i32) -> (Vec<Cmatch>, i32, i32, i32) {
3923 // c:3224
3924 let mut n: i32 = rp.len() as i32; // c:3224
3925 let mut nl: i32 = 0; // c:3231
3926 let mut ll: i32 = 0; // c:3231
3927
3928 if n > 0 {
3929 // c:3258 (type==1 branch)
3930 if (flags & CGF_NOSORT) == 0 {
3931 // c:3259
3932 // Now sort the array (it contains matches). // c:3260
3933 MATCHORDER.store(flags, Ordering::Relaxed); // c:3261
3934 // c:3262 — C `qsort(rp, n, sizeof(Cmatch), matchcmp)`. Must use the
3935 // qsort-tolerant sort: matchcmp→zstrcmp is not a strict weak order
3936 // (numeric/natural sort), which makes Rust's sort_by PANIC.
3937 crate::tolerant_sort::qsort_tolerant(&mut rp, matchcmp);
3938
3939 if (flags & CGF_UNIQCON) == 0 {
3940 // c:3269 not -2
3941 // remove dupes
3942 let mut cp = 0usize; // c:3272
3943 let mut ap = 0usize;
3944 while ap < rp.len() {
3945 // c:3274 for ap;*ap;ap++
3946 // Scan the run of duplicates FIRST, using the element at
3947 // `ap` (C keeps `*ap` stable — `*cp++ = *ap` copies, it
3948 // does not move). Doing `rp.swap(ap, cp)` before this scan
3949 // (the old code) overwrote `rp[ap]` with a stale slot once
3950 // any earlier removal made `cp < ap`, so `rp[ap].str ==
3951 // rp[bp+1].str` compared the wrong element and same-string
3952 // duplicates (e.g. `libpng-config` from several $path dirs)
3953 // were never collapsed.
3954 let mut bp = ap;
3955 while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
3956 bp += 1;
3957 n -= 1; // c:3277 bp[1] && matcheq
3958 }
3959 let mut dup = 0i32; // c:3281
3960 while bp + 1 < rp.len()
3961 && rp[ap].disp.is_none()
3962 && rp[bp + 1].disp.is_none() // c:3282 !disp
3963 && rp[ap].str == rp[bp + 1].str
3964 {
3965 rp[bp + 1].flags |= CMF_MULT; // c:3284
3966 dup = 1; // c:3285
3967 bp += 1;
3968 n -= 1; // same-string duplicate is dropped too
3969 }
3970 if dup != 0 {
3971 // c:3287
3972 rp[ap].flags |= CMF_FMULT; // c:3288
3973 }
3974 // c:3275 `*cp++ = *ap` — keep the first of the run at `cp`.
3975 if ap != cp {
3976 rp.swap(ap, cp);
3977 }
3978 cp += 1;
3979 ap = bp + 1; // c:3279 ap = bp; ap++
3980 }
3981 rp.truncate(cp); // c:3291 *cp = NULL
3982 }
3983 for m in rp.iter() {
3984 // c:3293
3985 if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
3986 // c:3294
3987 ll += 1;
3988 }
3989 if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
3990 // c:3296
3991 nl += 1;
3992 }
3993 }
3994 } else {
3995 // c:3300 used -O nosort or -V
3996 if (flags & CGF_UNIQALL) == 0 && (flags & CGF_UNIQCON) == 0 {
3997 // c:3302 didn't use -1 or -2
3998 MATCHORDER.store(flags, Ordering::Relaxed); // c:3306
3999 let mut sp: Vec<Cmatch> = rp.clone(); // c:3309-3312 zhalloc + memcpy
4000 // c:3313 — qsort matchcmp; tolerant sort (non-total-order cmp).
4001 crate::tolerant_sort::qsort_tolerant(&mut sp, matchcmp);
4002
4003 let mut del = false; // c:3303
4004 // Sweep sorted dup-detection back onto rp via flag marks.
4005 for w in sp.windows(2) {
4006 // c:3315-3329
4007 if matcheq(&w[0], &w[1]) {
4008 // Mark in original rp by str+disp equality.
4009 for m in rp.iter_mut() {
4010 if matcheq(m, &w[1]) {
4011 m.flags = CMF_DELETE; // c:3318
4012 del = true; // c:3319
4013 break;
4014 }
4015 }
4016 } else if w[0].disp.is_none() {
4017 if w[1].disp.is_none() && w[0].str == w[1].str {
4018 // c:3322
4019 for m in rp.iter_mut() {
4020 if matcheq(m, &w[1]) {
4021 m.flags |= CMF_MULT; // c:3324
4022 break;
4023 }
4024 }
4025 for m in rp.iter_mut() {
4026 if matcheq(m, &w[0]) {
4027 m.flags |= CMF_FMULT; // c:3328
4028 break;
4029 }
4030 }
4031 }
4032 }
4033 }
4034 if del {
4035 // c:3332
4036 rp.retain(|m| (m.flags & CMF_DELETE) == 0); // c:3334-3340
4037 n = rp.len() as i32;
4038 }
4039 } else if (flags & CGF_UNIQCON) == 0 {
4040 // c:3344 -1 not -2
4041 let mut cp = 0usize;
4042 let mut ap = 0usize;
4043 while ap < rp.len() {
4044 // c:3346
4045 if ap != cp {
4046 rp.swap(ap, cp);
4047 }
4048 cp += 1;
4049 let mut bp = ap;
4050 while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
4051 bp += 1;
4052 n -= 1; // c:3348
4053 }
4054 let mut dup = 0i32;
4055 while bp + 1 < rp.len()
4056 && rp[ap].disp.is_none()
4057 && rp[bp + 1].disp.is_none()
4058 && rp[ap].str == rp[bp + 1].str
4059 {
4060 rp[bp + 1].flags |= CMF_MULT; // c:3352
4061 dup = 1; // c:3353
4062 bp += 1;
4063 }
4064 if dup != 0 {
4065 rp[ap].flags |= CMF_FMULT; // c:3356
4066 }
4067 ap = bp + 1;
4068 }
4069 rp.truncate(cp); // c:3359
4070 }
4071 for m in rp.iter() {
4072 // c:3361
4073 if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
4074 // c:3362
4075 ll += 1;
4076 }
4077 if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
4078 // c:3364
4079 nl += 1;
4080 }
4081 }
4082 }
4083 }
4084 (rp, n, nl, ll) // c:3366-3373
4085}
4086
4087// =====================================================================
4088// dupmatch — `Src/Zle/compcore.c:3370`.
4089// =====================================================================
4090
4091/// Port of `static Cmatch dupmatch(Cmatch m, int nbeg, int nend)` from
4092/// compcore.c:3370. Deep-copies one match; brpl/brsl are truncated to
4093/// nbeg/nend per the C body's nbeg/nend-sized `zalloc` + element copy.
4094pub fn dupmatch(m: &Cmatch, nbeg: i32, nend: i32) -> Cmatch {
4095 // c:3370
4096 let mut r = Cmatch::default(); // c:3370-3374
4097 r.str = m.str.clone(); // c:3376 ztrdup
4098 r.orig = m.orig.clone(); // c:3377
4099 r.ipre = m.ipre.clone(); // c:3378
4100 r.ripre = m.ripre.clone(); // c:3379
4101 r.isuf = m.isuf.clone(); // c:3380
4102 r.ppre = m.ppre.clone(); // c:3381
4103 r.psuf = m.psuf.clone(); // c:3382
4104 r.prpre = m.prpre.clone(); // c:3383
4105 r.pre = m.pre.clone(); // c:3384
4106 r.suf = m.suf.clone(); // c:3385
4107 r.flags = m.flags; // c:3386
4108 if !m.brpl.is_empty() {
4109 // c:3387
4110 let take = (nbeg as usize).min(m.brpl.len()); // c:3390 zalloc(nbeg)
4111 r.brpl = m.brpl[..take].to_vec(); // c:3392 element-wise copy
4112 } else {
4113 r.brpl = Vec::new(); // c:3395 NULL
4114 }
4115 if !m.brsl.is_empty() {
4116 // c:3396
4117 let take = (nend as usize).min(m.brsl.len()); // c:3399
4118 r.brsl = m.brsl[..take].to_vec(); // c:3401
4119 } else {
4120 r.brsl = Vec::new(); // c:3404
4121 }
4122 r.rems = m.rems.clone(); // c:3405
4123 r.remf = m.remf.clone(); // c:3406
4124 r.autoq = m.autoq.clone(); // c:3407
4125 r.qipl = m.qipl; // c:3408
4126 r.qisl = m.qisl; // c:3409
4127 r.disp = m.disp.clone(); // c:3410
4128 r.mode = m.mode; // c:3411
4129 r.modec = m.modec; // c:3412
4130 r.fmode = m.fmode; // c:3413
4131 r.fmodec = m.fmodec; // c:3414
4132 r // c:3416
4133}
4134
4135/// Port of `mod_export int permmatches(int last)` from compcore.c:3422.
4136/// Promotes the per-round `amatches` accumulator into the permanent
4137/// `pmatches` snapshot via deep-copy through `dupmatch`/`makearray`.
4138pub fn permmatches(last: i32) -> i32 {
4139 // c:3423
4140 let ofi = PERMMATCHES_FI.load(Ordering::Relaxed); // c:3423 ofi = fi
4141
4142 // c:3433 — `if (pmatches && !newmatches)`
4143 let pmatches_set = pmatches
4144 .get_or_init(|| Mutex::new(Vec::new()))
4145 .lock()
4146 .map(|g| !g.is_empty())
4147 .unwrap_or(false);
4148 if pmatches_set && newmatches.load(Ordering::Relaxed) == 0 {
4149 // c:3433
4150 if last != 0 && PERMMATCHES_FI.load(Ordering::Relaxed) != 0 {
4151 // c:3434
4152 // ainfo = fainfo // c:3435
4153 let famref = fainfo
4154 .get_or_init(|| Mutex::new(None))
4155 .lock()
4156 .ok()
4157 .and_then(|g| g.clone());
4158 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
4159 *a = famref;
4160 }
4161 }
4162 return PERMMATCHES_FI.load(Ordering::Relaxed); // c:3437
4163 }
4164 newmatches.store(0, Ordering::Relaxed); // c:3439
4165 PERMMATCHES_FI.store(0, Ordering::Relaxed); // c:3439 fi = 0
4166
4167 {
4168 // pmatches = lmatches = NULL // c:3441
4169 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
4170 g.clear();
4171 }
4172 if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
4173 *g = None;
4174 }
4175 }
4176 nmatches.store(0, Ordering::Relaxed); // c:3442
4177 smatches.store(0, Ordering::Relaxed); // c:3442
4178 diffmatches.store(0, Ordering::Relaxed); // c:3442
4179
4180 // c:3444 — `if (!ainfo->count)`.
4181 let ainfo_count = ainfo
4182 .get_or_init(|| Mutex::new(None))
4183 .lock()
4184 .ok()
4185 .and_then(|g| g.as_ref().map(|a| a.count))
4186 .unwrap_or(0);
4187 if ainfo_count == 0 {
4188 // c:3444
4189 if last != 0 {
4190 // c:3445
4191 let famref = fainfo
4192 .get_or_init(|| Mutex::new(None))
4193 .lock()
4194 .ok()
4195 .and_then(|g| g.clone());
4196 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
4197 *a = famref;
4198 }
4199 }
4200 PERMMATCHES_FI.store(1, Ordering::Relaxed); // c:3447
4201 }
4202
4203 let nbeg = NBRBEG.load(Ordering::Relaxed);
4204 let nend = NBREND.load(Ordering::Relaxed);
4205
4206 let mut gn: i32 = 1; // c:3429 gn = 1
4207 let mut mn: i32 = 1; // c:3429 mn = 1
4208 let fi = PERMMATCHES_FI.load(Ordering::Relaxed);
4209
4210 let groups_snapshot: Vec<Cmgroup> = {
4211 amatches
4212 .get_or_init(|| Mutex::new(Vec::new()))
4213 .lock()
4214 .ok()
4215 .map(|g| g.clone())
4216 .unwrap_or_default()
4217 };
4218 let mut new_pmatches: Vec<Cmgroup> = Vec::with_capacity(groups_snapshot.len());
4219
4220 for g_orig in groups_snapshot.into_iter() {
4221 // c:3449 while (g)
4222 let mut g = g_orig; // borrow-mut snapshot
4223 let must_rebuild = fi != ofi || g.perm.is_none() || g.new_ != 0; // c:3456
4224 if must_rebuild {
4225 // c:3456
4226 let src_list = if fi != 0 {
4227 g.lfmatches.clone()
4228 }
4229 // c:3457
4230 else {
4231 g.lmatches.clone()
4232 }; // c:3461
4233
4234 let (arr, nn, nl, ll) = makearray(src_list, g.flags); // c:3463
4235 g.mcount = nn; // c:3464
4236 g.lcount = nn - nl; // c:3465
4237 if g.lcount < 0 {
4238 g.lcount = 0;
4239 } // c:3466
4240 g.llcount = ll; // c:3467
4241 if !g.ylist.is_empty() {
4242 // c:3468
4243 g.lcount = g.ylist.len() as i32; // c:3469
4244 smatches.store(2, Ordering::Relaxed); // c:3470
4245 }
4246 // c:3472 — makearray(lexpls, 0, 0, &ecount, NULL, NULL).
4247 let mut exps = g.lexpls.clone(); // type=0 path
4248 g.ecount = exps.len() as i32;
4249 // c:3475 ccount = 0
4250 g.ccount = 0; // c:3475
4251 tracing::debug!(
4252 target: "compsys_args",
4253 mcount = g.mcount,
4254 lcount = g.lcount,
4255 nn,
4256 nl,
4257 ll,
4258 name = ?g.name,
4259 "permmatches group"
4260 );
4261 nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3477
4262 smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3478
4263 if g.mcount > 1 {
4264 // c:3480
4265 diffmatches.store(1, Ordering::Relaxed); // c:3481
4266 }
4267
4268 // n = (Cmgroup) zshcalloc(...) // c:3483
4269 let mut n_grp = Cmgroup::default();
4270 // c:3487 — `if (g->perm) freematches(g->perm, 0)`. Drop on
4271 // perm Box<Cmgroup> reclaims the C `free` path.
4272 g.perm = None; // c:3490 g->perm = n
4273 // Then below we set g.perm = Some(Box::new(n_grp.clone())).
4274
4275 n_grp.num = gn;
4276 gn += 1; // c:3499
4277 n_grp.flags = g.flags; // c:3500
4278 n_grp.mcount = g.mcount; // c:3501
4279 n_grp.matches = arr
4280 .iter() // c:3502-3505 dupmatch loop
4281 .map(|m| dupmatch(m, nbeg, nend))
4282 .collect();
4283 n_grp.name = g.name.clone(); // c:3504
4284 n_grp.lcount = g.lcount; // c:3508
4285 n_grp.llcount = g.llcount; // c:3509
4286 if !g.ylist.is_empty() {
4287 // c:3510
4288 n_grp.ylist = g.ylist.clone(); // c:3511 zarrdup
4289 } else {
4290 n_grp.ylist = Vec::new(); // c:3513
4291 }
4292 if g.ecount != 0 {
4293 // c:3515
4294 // Build n->expls from g->expls deep-copying str + (fi
4295 // ? fcount : count); always carries over; fcount = 0.
4296 n_grp.expls = exps
4297 .drain(..)
4298 .map(|o| Cexpl {
4299 // c:3517-3525
4300 count: if fi != 0 { o.fcount } else { o.count }, // c:3520
4301 always: o.always, // c:3521
4302 fcount: 0, // c:3522
4303 str: o.str.clone(), // c:3523 ztrdup
4304 })
4305 .collect();
4306 n_grp.ecount = g.ecount;
4307 } else {
4308 n_grp.expls = Vec::new(); // c:3528
4309 }
4310 n_grp.widths = Vec::new(); // c:3531
4311 // Stitch perm chain (prev/next handled implicitly by Vec).
4312 g.matches = arr; // mirror C: g->matches = makearray result
4313 g.perm = Some(Box::new(n_grp.clone())); // c:3490 g->perm = n
4314 new_pmatches.push(n_grp); // c:3492-3496
4315 } else {
4316 // reuse existing g->perm // c:3534
4317 nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3540
4318 smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3541
4319 if g.mcount > 1 {
4320 diffmatches.store(1, Ordering::Relaxed); // c:3543
4321 }
4322 g.num = gn;
4323 gn += 1; // c:3546
4324 if let Some(p) = g.perm.as_deref() {
4325 new_pmatches.push(p.clone()); // c:3537 pmatches = g->perm
4326 }
4327 }
4328 g.new_ = 0; // c:3548
4329 }
4330
4331 // c:3490-3538 — C threads each group onto `pmatches` via
4332 // `n->next = pmatches; pmatches = n` (a PREPEND), so pmatches ends up
4333 // in the REVERSE of amatches iteration order. The port accumulates into
4334 // a Vec with push() (append), so reverse once here to recover C's order.
4335 // Without this, `group-order`/`group-name` groups list in creation order
4336 // reversed (e.g. `group-order veggies fruits` showed fruits first), and
4337 // the gnum/rnum numbering below — which C runs over the reversed
4338 // pmatches — was assigned against the wrong sequence.
4339 new_pmatches.reverse();
4340
4341 // c:3551-3563 — assign rnum/gnum, recompute diffmatches/nbrbeg.
4342 let mut first_first: Option<Cmatch> = None;
4343 for g_pm in new_pmatches.iter_mut() {
4344 g_pm.nbrbeg = nbeg; // c:3552
4345 g_pm.nbrend = nend; // c:3553
4346 let mut rn = 1i32; // c:3554
4347 for m in g_pm.matches.iter_mut() {
4348 m.rnum = rn;
4349 rn += 1; // c:3555
4350 m.gnum = mn;
4351 mn += 1; // c:3556
4352 }
4353 if diffmatches.load(Ordering::Relaxed) == 0 && !g_pm.matches.is_empty() {
4354 match first_first.as_ref() {
4355 // c:3558
4356 Some(p0) => {
4357 if !matcheq(&g_pm.matches[0], p0) {
4358 diffmatches.store(1, Ordering::Relaxed); // c:3560
4359 }
4360 }
4361 None => first_first = Some(g_pm.matches[0].clone()), // c:3562
4362 }
4363 }
4364 }
4365
4366 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
4367 *g = new_pmatches;
4368 }
4369
4370 hasperm.store(1, Ordering::Relaxed); // c:3565
4371 permmnum.store(mn - 1, Ordering::Relaxed); // c:3566
4372 permgnum.store(gn - 1, Ordering::Relaxed); // c:3567
4373 if let Ok(mut ld) = listdat
4374 .get_or_init(|| Mutex::new(Default::default()))
4375 .lock()
4376 {
4377 ld.valid = 0; // c:3568
4378 }
4379
4380 fi // c:3570
4381}
4382
4383// =====================================================================
4384// freematch / freematches — `Src/Zle/compcore.c:3575 / 3605`.
4385// =====================================================================
4386
4387/// Port of `static void freematch(Cmatch m, int nbeg, int nend)` from
4388/// `Src/Zle/compcore.c:3575`. C frees each Cmatch field via `zsfree`
4389/// (str/orig/ipre/ripre/isuf/ppre/psuf/pre/suf/prpre/rems/remf/disp/
4390/// autoq) and `zfree(m->brpl, nbeg * sizeof(int))` /
4391/// `zfree(m->brsl, nend * sizeof(int))` — all collapse to Rust's
4392/// automatic Drop on the owned String / `Vec<i32>` fields. nbeg/nend
4393/// kept on the signature for C parity (consumed by C as `zfree` size
4394/// args; Rust Vec carries its own length).
4395pub fn freematch(m: Cmatch, _nbeg: i32, _nend: i32) {
4396 // c:3577 — `if (!m) return;` — Rust's owned `m` is always valid;
4397 // dropping it on return runs every field's destructor (c:3579-3596
4398 // zsfree / zfree calls collapsed).
4399 drop(m); // c:3598 zfree(m)
4400}
4401
4402/// Direct port of `mod_export void freematches(Cmgroup g, int cm)` from
4403/// `Src/Zle/compcore.c:3605`. The C path walks the cmgroup chain freeing
4404/// each Cmatch + ylist + expls + widths + name; in Rust those are
4405/// owned by `Vec`/`Box`/`String` so Drop covers the per-node free.
4406/// The `cm` arm at c:3636-3637 (`minfo.cur = NULL`) is the only
4407/// side-effect that doesn't fall out of Rust's ownership model — wire
4408/// it explicitly.
4409pub fn freematches(g: Vec<Cmgroup>, cm: i32) {
4410 // c:3605
4411 drop(g);
4412 if cm != 0 {
4413 // c:3636
4414 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
4415 g.cur = None; // c:3637
4416 }
4417 }
4418}
4419
4420// =====================================================================
4421// Extern globals — declared in other C files, mirrored here per
4422// PORT.md Rule 9 ("stub the EXTERN dependencies ... locally with
4423// file:line citations to their home file") so the local body ports
4424// below have a value source. When the canonical Rust homes land,
4425// these become `pub use crate::ported::<canonical>::*` re-exports.
4426// =====================================================================
4427
4428/// Port of `mod_export int lastend` from `Src/Zle/compcore.c:276`.
4429/// Byte position in the metafied line where the most-recent
4430/// completion insertion ended.
4431///
4432/// Re-export alias of [`lastend`] — C has ONE `lastend` (compcore.c:276).
4433/// Two atomics existed: `compresult` wrote the lowercase `lastend`, while
4434/// `complist`'s menu-list positioning read this uppercase `LASTEND`, which was
4435/// never stored (always 0) — so menu-selection column math used a stale zero.
4436pub use self::lastend as LASTEND;
4437
4438/// Port of `mod_export int wb` from `Src/lex.c:120`. Word-begin
4439/// position in the metafied line for the currently-completing word.
4440pub static WB: AtomicI32 = AtomicI32::new(0); // lex.c:120
4441/// Port of `mod_export int we` from `Src/lex.c:120`. Word-end position.
4442pub static WE: AtomicI32 = AtomicI32::new(0); // lex.c:120
4443/// Port of `mod_export int zlemetacs` from `Src/lex.c:104`. Cursor
4444/// position in the metafied line.
4445pub static ZLEMETACS: AtomicI32 = AtomicI32::new(0); // lex.c:104
4446/// Port of `mod_export int zlemetall` from `Src/lex.c:104`. Length
4447/// of the metafied line.
4448pub static ZLEMETALL: AtomicI32 = AtomicI32::new(0); // lex.c:104
4449/// Port of `mod_export int addedx` from `Src/lex.c:115`. Non-zero
4450/// while a dummy `x` cursor marker is in the line being lexed
4451/// (so completion can capture the partial word at the cursor).
4452pub static ADDEDX: AtomicI32 = AtomicI32::new(0); // lex.c:115
4453
4454/// Port of `mod_export char *zlemetaline` from `Src/lex.c:103`. The
4455/// metafied edit buffer for the current ZLE session — `foredel`,
4456/// `inststr`, `selfinsert` operate on this directly when compcore's
4457/// error-recovery path fires (compcore.c:344-355).
4458pub static ZLEMETALINE: OnceLock<Mutex<String>> = OnceLock::new(); // lex.c:103
4459/// Port of `mod_export ZLE_STRING_T zleline` from `Src/zle_main.c`.
4460pub static ZLELINE: OnceLock<Mutex<String>> = OnceLock::new(); // zle_main.c
4461/// Port of `mod_export int zlecs` from `Src/zle_main.c`.
4462pub static ZLECS: AtomicI32 = AtomicI32::new(0); // zle_main.c
4463/// Port of `mod_export int zlell` from `Src/zle_main.c`.
4464pub static ZLELL: AtomicI32 = AtomicI32::new(0); // zle_main.c
4465/// Port of `mod_export int inwhat` from `Src/lex.c:110`. Lex context
4466/// classification — IN_NOTHING / IN_CMD / IN_COND / IN_MATH / IN_PAR /
4467/// IN_ENV.
4468pub static INWHAT: AtomicI32 = AtomicI32::new(0); // lex.c:110
4469/// Port of `mod_export int zmult` from `Src/zle_main.c`. Numeric
4470/// prefix multiplier for the current ZLE command.
4471pub static ZMULT: AtomicI32 = AtomicI32::new(1); // zle_main.c
4472/// Port of `mod_export char *compfunc` from `Src/Zle/zle_tricky.c:143`.
4473/// Name of the user completion shell function — non-empty when the
4474/// new completion system (`compsys`) is active; empty for compctl.
4475pub static compfunc: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // zle_tricky.c:143
4476/// Port of `mod_export char *comppatmatch` from `Src/Zle/zle_tricky.c`.
4477/// `$compstate[pattern_match]` — when non-empty + non-"\0" enables
4478/// pattern-aware matching for parameter-name completion.
4479pub static comppatmatch: OnceLock<Mutex<Option<String>>> = OnceLock::new();
4480// `compqstack` (C: complete.c `mod_export char *compqstack`) is deduped
4481// to the single canonical `complete::COMPQSTACK`, imported at the top of
4482// this module. The former compcore-local `compqstack` static was an
4483// orphan: the c:305 reset wrote it while `multiquote` (c:1065) read the
4484// imported COMPQSTACK, so the reset never reached its reader — a real
4485// bug now fixed by pointing both at COMPQSTACK.
4486// =====================================================================
4487// File-scope globals — `Src/Zle/compcore.c:36-279`.
4488// =====================================================================
4489
4490/// Port of `int useexact` from compcore.c:36.
4491pub static useexact: AtomicI32 = AtomicI32::new(0); // c:36
4492/// Port of `int useline` from compcore.c:36.
4493pub static useline: AtomicI32 = AtomicI32::new(0); // c:36
4494/// Port of `int uselist` from compcore.c:36.
4495pub static uselist: AtomicI32 = AtomicI32::new(0); // c:36
4496/// Port of `int forcelist` from compcore.c:36.
4497pub static forcelist: AtomicI32 = AtomicI32::new(0); // c:36
4498/// Port of `int startauto` from compcore.c:36.
4499pub static startauto: AtomicI32 = AtomicI32::new(0); // c:36
4500
4501/// Port of `mod_export int iforcemenu` from compcore.c:39.
4502pub static iforcemenu: AtomicI32 = AtomicI32::new(0); // c:39
4503
4504/// Port of `mod_export int dolastprompt` from compcore.c:44.
4505pub static dolastprompt: AtomicI32 = AtomicI32::new(0); // c:44
4506
4507/// Port of `mod_export int oldlist` from compcore.c:49.
4508pub static oldlist: AtomicI32 = AtomicI32::new(0); // c:49
4509/// Port of `mod_export int oldins` from compcore.c:49.
4510pub static oldins: AtomicI32 = AtomicI32::new(0); // c:49
4511
4512/// Port of `int origlpre` from compcore.c:54.
4513pub static origlpre: AtomicI32 = AtomicI32::new(0); // c:54
4514/// Port of `int origlsuf` from compcore.c:54.
4515pub static origlsuf: AtomicI32 = AtomicI32::new(0); // c:54
4516/// Port of `int lenchanged` from compcore.c:54.
4517pub static lenchanged: AtomicI32 = AtomicI32::new(0); // c:54
4518
4519/// Port of `int movetoend` from compcore.c:61.
4520pub static movetoend: AtomicI32 = AtomicI32::new(0); // c:61
4521
4522/// Port of `mod_export int insmnum` from compcore.c:66.
4523pub static insmnum: AtomicI32 = AtomicI32::new(0); // c:66
4524/// Port of `mod_export int insspace` from compcore.c:66.
4525pub static insspace: AtomicI32 = AtomicI32::new(0); // c:66
4526
4527/// Port of `mod_export int menuacc` from compcore.c:81.
4528pub static menuacc: AtomicI32 = AtomicI32::new(0); // c:81
4529
4530/// Port of `int hasunqu` from compcore.c:86.
4531pub static hasunqu: AtomicI32 = AtomicI32::new(0); // c:86
4532/// Port of `int useqbr` from compcore.c:86.
4533pub static useqbr: AtomicI32 = AtomicI32::new(0); // c:86
4534/// Port of `int brpcs` from compcore.c:86.
4535pub static brpcs: AtomicI32 = AtomicI32::new(0); // c:86
4536/// Port of `int brscs` from compcore.c:86.
4537pub static brscs: AtomicI32 = AtomicI32::new(0); // c:86
4538
4539/// Port of `mod_export int ispar` from compcore.c:91.
4540pub static ispar: AtomicI32 = AtomicI32::new(0); // c:91
4541/// Port of `mod_export int linwhat` from compcore.c:91.
4542pub static linwhat: AtomicI32 = AtomicI32::new(0); // c:91
4543
4544/// Port of `char *parpre` from compcore.c:96.
4545pub static parpre: OnceLock<Mutex<String>> = OnceLock::new(); // c:96
4546
4547/// Port of `int parflags` from compcore.c:101.
4548pub static parflags: AtomicI32 = AtomicI32::new(0); // c:101
4549
4550/// Port of `mod_export int mflags` from compcore.c:106.
4551pub static mflags: AtomicI32 = AtomicI32::new(0); // c:106
4552
4553/// Port of `int parq` from compcore.c:111.
4554pub static parq: AtomicI32 = AtomicI32::new(0); // c:111
4555/// Port of `int eparq` from compcore.c:111.
4556pub static eparq: AtomicI32 = AtomicI32::new(0); // c:111
4557
4558/// Port of `mod_export char *ipre` from compcore.c:118.
4559pub static ipre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
4560/// Port of `mod_export char *ripre` from compcore.c:118.
4561pub static ripre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
4562/// Port of `mod_export char *isuf` from compcore.c:118.
4563pub static isuf: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
4564
4565/// Port of `mod_export LinkList matches` from compcore.c:124.
4566pub static matches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:124
4567/// Port of `LinkList fmatches` from compcore.c:126.
4568pub static fmatches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:126
4569
4570/// Port of `mod_export Cmgroup amatches` from compcore.c:135.
4571pub static amatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
4572/// Port of `mod_export Cmgroup pmatches` from compcore.c:135.
4573pub static pmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
4574/// Port of `mod_export Cmgroup lastmatches` from compcore.c:135.
4575pub static lastmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
4576/// Port of `mod_export Cmgroup lmatches` from compcore.c:135. Last
4577/// element pointer in the perm list; here a single-slot holder.
4578pub static lmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
4579/// Port of `mod_export Cmgroup lastlmatches` from compcore.c:135.
4580pub static lastlmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
4581
4582/// Port of `mod_export int hasoldlist` from compcore.c:140.
4583pub static hasoldlist: AtomicI32 = AtomicI32::new(0); // c:140
4584/// Port of `mod_export int hasperm` from compcore.c:140.
4585pub static hasperm: AtomicI32 = AtomicI32::new(0); // c:140
4586/// Port of `int hasallmatch` from compcore.c:145.
4587pub static hasallmatch: AtomicI32 = AtomicI32::new(0); // c:145
4588
4589/// Port of `mod_export int newmatches` from compcore.c:150.
4590pub static newmatches: AtomicI32 = AtomicI32::new(0); // c:150
4591
4592/// Port of `mod_export int permmnum` from compcore.c:155.
4593pub static permmnum: AtomicI32 = AtomicI32::new(0); // c:155
4594/// Port of `mod_export int permgnum` from compcore.c:155.
4595pub static permgnum: AtomicI32 = AtomicI32::new(0); // c:155
4596/// Port of `mod_export int lastpermmnum` from compcore.c:155.
4597pub static lastpermmnum: AtomicI32 = AtomicI32::new(0); // c:155
4598/// Port of `mod_export int lastpermgnum` from compcore.c:155.
4599pub static lastpermgnum: AtomicI32 = AtomicI32::new(0); // c:155
4600
4601/// Port of `mod_export int nmatches` from compcore.c:160.
4602pub static nmatches: AtomicI32 = AtomicI32::new(0); // c:160
4603/// Port of `mod_export int smatches` from compcore.c:162.
4604pub static smatches: AtomicI32 = AtomicI32::new(0); // c:162
4605
4606/// Port of `mod_export int diffmatches` from compcore.c:167.
4607pub static diffmatches: AtomicI32 = AtomicI32::new(0); // c:167
4608
4609/// Port of `mod_export int nmessages` from compcore.c:172.
4610pub static nmessages: AtomicI32 = AtomicI32::new(0); // c:172
4611
4612/// Port of `mod_export int onlyexpl` from compcore.c:177.
4613pub static onlyexpl: AtomicI32 = AtomicI32::new(0); // c:177
4614
4615/// Port of `mod_export struct cldata listdat` from compcore.c:182.
4616pub static listdat: OnceLock<Mutex<crate::ported::zle::comp_h::Cldata>> = OnceLock::new(); // c:182
4617
4618/// Port of `mod_export int ispattern` from compcore.c:187.
4619pub static ispattern: AtomicI32 = AtomicI32::new(0); // c:187
4620/// Port of `mod_export int haspattern` from compcore.c:187.
4621pub static haspattern: AtomicI32 = AtomicI32::new(0); // c:187
4622
4623/// Port of `mod_export int hasmatched` from compcore.c:192.
4624pub static hasmatched: AtomicI32 = AtomicI32::new(0); // c:192
4625/// Port of `mod_export int hasunmatched` from compcore.c:192.
4626pub static hasunmatched: AtomicI32 = AtomicI32::new(0); // c:192
4627
4628/// Port of `Cmgroup mgroup` from compcore.c:197.
4629pub static mgroup: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:197
4630
4631/// Port of `mod_export int mnum` from compcore.c:202.
4632pub static mnum: AtomicI32 = AtomicI32::new(0); // c:202
4633
4634/// Port of `mod_export int unambig_mnum` from compcore.c:207.
4635pub static unambig_mnum: AtomicI32 = AtomicI32::new(0); // c:207
4636
4637/// Port of `int maxmlen` from compcore.c:212.
4638pub static maxmlen: AtomicI32 = AtomicI32::new(0); // c:212
4639/// Port of `int minmlen` from compcore.c:212.
4640pub static minmlen: AtomicI32 = AtomicI32::new(0); // c:212
4641
4642/// Port of `LinkList expls` from compcore.c:218.
4643pub static expls: OnceLock<Mutex<Vec<Cexpl>>> = OnceLock::new(); // c:218
4644
4645/// Port of `mod_export Cexpl curexpl` from compcore.c:221.
4646pub static curexpl: OnceLock<Mutex<Option<Cexpl>>> = OnceLock::new(); // c:221
4647
4648/// Port of `LinkList matchers` from compcore.c:236. The C list holds
4649/// `Cmatcher` pointers (the parsed match-spec chains pushed by
4650/// addmatches's `add_bmatchers` block). Rust port mirrors that with
4651/// owned `Box<Cmatcher>` entries.
4652pub static matchers: OnceLock<Mutex<Vec<Box<crate::ported::zle::comp_h::Cmatcher>>>> =
4653 OnceLock::new(); // c:236
4654
4655/// Port of `mod_export Aminfo ainfo` from compcore.c:246.
4656pub static ainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
4657/// Port of `mod_export Aminfo fainfo` from compcore.c:246.
4658pub static fainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
4659
4660/// Port of `mod_export LinkList allccs` from compcore.c:259.
4661pub static allccs: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); // c:259
4662
4663/// Port of `int fromcomp` from compcore.c:271.
4664pub static fromcomp: AtomicI32 = AtomicI32::new(0); // c:271
4665
4666/// Port of `mod_export int lastend` from compcore.c:276.
4667pub static lastend: AtomicI32 = AtomicI32::new(0); // c:276
4668
4669/// Port of `mod_export Brinfo brbeg` from `Src/Zle/zle_tricky.c`.
4670/// Linked list of opening-brace positions in the word being completed.
4671pub static BRBEG: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brbeg
4672
4673/// Port of `mod_export Brinfo brend` from `Src/Zle/zle_tricky.c`.
4674/// Linked list of closing-brace positions in the word being completed.
4675pub static BREND: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brend
4676
4677/// Port of `static int oldmenucmp` from compcore.c:457.
4678pub static OLDMENUCMP: AtomicI32 = AtomicI32::new(0); // c:457
4679
4680/// Port of `static int parwb` from compcore.c:540.
4681pub static PARWB: AtomicI32 = AtomicI32::new(0); // c:540
4682/// Port of `static int parwe` from compcore.c:540.
4683pub static PARWE: AtomicI32 = AtomicI32::new(0); // c:540
4684/// Port of `static int paroffs` from compcore.c:540.
4685pub static PAROFFS: AtomicI32 = AtomicI32::new(0); // c:540
4686
4687/// Port of `static int matchorder` from compcore.c:3169.
4688pub static MATCHORDER: AtomicI32 = AtomicI32::new(0); // c:3169
4689
4690/// Port of `mod_export int lastchar` from `Src/Zle/zle_main.c`. Last
4691/// keyboard char consumed by the binding loop — read by `selfinsert`.
4692pub static LASTCHAR: AtomicI32 = AtomicI32::new(0); // zle_main.c
4693 // minfo_clear_cur / minfo_asked_zero deleted — Rust-only 2-line
4694 // wrappers around C's inline writes `minfo.cur = NULL` and
4695 // `minfo.asked = 0`. All call sites inlined.
4696
4697/// Direct port of `struct menuinfo minfo` — `Src/Zle/zle_tricky.c`
4698/// (the single file-scope instance). The struct type itself lives
4699/// in `comp_h.rs::Menuinfo` (port of comp.h:284-295).
4700pub static MINFO: OnceLock<Mutex<Menuinfo>> = OnceLock::new(); // zle_tricky.c minfo
4701
4702// =====================================================================
4703// matcheq — `Src/Zle/compcore.c:3203-3215`.
4704// =====================================================================
4705
4706#[inline]
4707fn matchstreq(a: Option<&String>, b: Option<&String>) -> bool {
4708 // c:3207
4709 match (a, b) {
4710 (None, None) => true,
4711 (Some(x), Some(y)) => x == y,
4712 _ => false,
4713 }
4714}
4715
4716/// First-match shortcut path from compcore.c:398-411. `Cmgroup m = amatches;
4717/// while (!m->mcount) m = m->next; do_single(m->matches[0])`.
4718fn do_single_first_match() {
4719 // c:398
4720 let groups = amatches
4721 .get_or_init(|| Mutex::new(Vec::new()))
4722 .lock()
4723 .ok()
4724 .map(|g| g.clone())
4725 .unwrap_or_default();
4726 let first = groups
4727 .into_iter()
4728 .find(|g| g.mcount > 0)
4729 .and_then(|g| g.matches.first().cloned());
4730 if let Some(m) = first {
4731 // c:407 — `minfo.cur = NULL`.
4732 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
4733 g.cur = None;
4734 g.asked = 0; // c:408
4735 }
4736 // c:409 — `do_single(m->matches[0])`. This is the actual
4737 // single-match insert: it deletes the word between wb/we (incl.
4738 // the addx placeholder) and inserts the completed string with
4739 // its suffix. Previously this call was omitted (the match was
4740 // merely stashed on minfo.cur), so a unique completion left the
4741 // typed prefix + 'x' on the line instead of the completed word.
4742 crate::ported::zle::compresult::do_single(&m);
4743 }
4744}
4745
4746/// compcore.c:444 `compend:` epilogue — free matchers, snap zlemetacs.
4747fn goto_compend(ret: i32) -> i32 {
4748 // c:444
4749 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
4750 g.clear(); // c:445-446 freecmatcher loop
4751 }
4752 let line_len = ZLEMETALL.load(Ordering::Relaxed); // c:448 strlen(zlemetaline)
4753 if ZLEMETACS.load(Ordering::Relaxed) > line_len {
4754 // c:449
4755 ZLEMETACS.store(line_len, Ordering::Relaxed); // c:450
4756 }
4757 ret // c:453
4758}
4759
4760// `COMP_LIST_COMPLETE` / `QT_NONE_STUB` / `QT_BACKSLASH_STUB` local
4761// aliases deleted — call sites now reach the real C-side constants
4762// directly (`crate::ported::zle::zle_h::COMP_LIST_COMPLETE`,
4763// `QT_NONE`, `QT_BACKSLASH`).
4764// The local `COMP_LIST_COMPLETE = 2` was a value-mismatch bug (the
4765// real constant is 1 per `Src/Zle/zle.h:357`).
4766
4767// `char_from_qt` deleted — Rust-only 1-line `(qt as u8) as char`
4768// helper. Inlined at the two call sites in get_compstate_str.
4769
4770// `showinglist_stub` / `showinglist_set` / `clearlist_set` /
4771// `listshown_stub` / `instring_stub` deleted — Rust-only 1-line
4772// accessors for C globals (SHOWINGLIST / CLEARLIST / LISTSHOWN /
4773// INSTRING). C reads/writes the bare globals inline; callers in
4774// compcore.rs now do `<GLOBAL>.load(Ordering::Relaxed)` /
4775// `<GLOBAL>.store(v, Ordering::Relaxed)` directly.
4776// `fn foredel` / `fn inststr` locals deleted — both duplicated
4777// canonical ports living in their proper home files:
4778// - foredel: `Src/Zle/zle_utils.c:1105`
4779// → `foredel`
4780// - inststr: macro `Src/Zle/zle_tricky.c:57` (inststr(X) →
4781// inststrlen(X,1,-1)) → `inststr`
4782// The duplicates here narrowed C signatures (foredel dropped `flags`,
4783// inststr dropped the i32 return + duplicated inststrlen's body) and
4784// violated Rule C (every decl in its mirroring C file). Callers in
4785// this module now route through the canonical ported.
4786/// `IN_NOTHING_LW` constant.
4787// These MUST match the raw IN_* enum in zsh_h (Src/zsh.h:2322-2332), which is
4788// the single encoding C uses for both `inwhat` and `linwhat`. `linwhat` is
4789// copied verbatim from `INWHAT` (makecomplist c:960), so a divergent ordering
4790// here made makecomplistglobal's `linwhat == IN_ENV_LW` never match after an
4791// assignment (`x=<Tab>`): INWHAT held IN_ENV=4 while IN_ENV_LW was 5, so
4792// value/assignment completion silently did nothing.
4793pub const IN_NOTHING_LW: i32 = 0; // = IN_NOTHING (zsh.h:2322)
4794/// `IN_CMD_LW` constant.
4795pub const IN_CMD_LW: i32 = 1; // = IN_CMD (zsh.h:2324)
4796/// `IN_MATH_LW` constant.
4797pub const IN_MATH_LW: i32 = 2; // = IN_MATH (zsh.h:2326)
4798/// `IN_COND_LW` constant.
4799pub const IN_COND_LW: i32 = 3; // = IN_COND (zsh.h:2328)
4800/// `IN_ENV_LW` constant.
4801pub const IN_ENV_LW: i32 = 4; // = IN_ENV (zsh.h:2330)
4802/// `IN_PAR_LW` constant.
4803pub const IN_PAR_LW: i32 = 5; // = IN_PAR (zsh.h:2332)
4804 // `origline_stub` / `origcs_stub` deleted — Rust-only 1-line
4805 // accessors for the `ORIGLINE` / `ORIGCS` globals (ports of C
4806 // `origline` / `origcs` at zle_tricky.c:75 etc.). C reads these
4807 // globals inline; callers in compcore.rs now do the lock/load
4808 // directly.
4809/// Port of `void unmetafy_line(void)` from `zle_tricky.c:995`.
4810///
4811/// C body:
4812/// `zlemetaline[zlemetall] = '\0';
4813/// zleline = stringaszleline(zlemetaline, zlemetacs, &zlell,
4814/// &linesz, &zlecs);
4815/// free(zlemetaline); zlemetaline = NULL;
4816/// CCRIGHT();`
4817///
4818/// Reads ZLEMETALINE, decodes via the canonical stringaszleline
4819/// (handles incs adjustment + unmetafy + UTF-8 decode), populates
4820/// ZLELINE / ZLELL / ZLECS, clears ZLEMETALINE/ZLEMETALL.
4821pub fn unmetafy_line() {
4822 // zle_tricky.c:995
4823 let meta = ZLEMETALINE
4824 .get_or_init(|| Mutex::new(String::new()))
4825 .lock()
4826 .map(|g| g.clone())
4827 .unwrap_or_default();
4828 let zlemetacs = ZLEMETACS.load(Ordering::Relaxed) as i32;
4829 let mut out_ll: i32 = 0;
4830 let mut out_cs: i32 = 0;
4831 // c:998 — `zleline = stringaszleline(zlemetaline, zlemetacs, &zlell, &linesz, &zlecs);`
4832 let line = crate::ported::zle::zle_utils::stringaszleline(
4833 &meta,
4834 zlemetacs,
4835 Some(&mut out_ll),
4836 None,
4837 Some(&mut out_cs),
4838 );
4839 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
4840 *g = line.iter().collect();
4841 }
4842 ZLELL.store(out_ll, Ordering::Relaxed);
4843 ZLECS.store(out_cs, Ordering::Relaxed);
4844 // c:1001-1002 — `free(zlemetaline); zlemetaline = NULL;`. Rust:
4845 // clear the buffer + zero the length to mark meta-mode inactive.
4846 if let Some(m) = ZLEMETALINE.get() {
4847 if let Ok(mut g) = m.lock() {
4848 g.clear();
4849 }
4850 }
4851 ZLEMETALL.store(0, Ordering::Relaxed);
4852 // c:1007 — CCRIGHT(): combining-char alignment fixup. No-op in
4853 // this Rust port (handled by stringaszleline's codepoint walk).
4854}
4855
4856/// Port of `void metafy_line(void)` from `zle_tricky.c:978`.
4857///
4858/// C body:
4859/// `zlemetaline = zlelineasstring(zleline, zlell, zlecs,
4860/// &zlemetall, &zlemetacs, 0);
4861/// metalinesz = zlemetall;
4862/// free(zleline); zleline = NULL;`
4863///
4864/// Reads ZLELINE, encodes via the canonical zlelineasstring (handles
4865/// wcrtomb + metafy expansion), populates ZLEMETALINE / ZLEMETALL /
4866/// ZLEMETACS, clears ZLELINE/ZLELL.
4867pub fn metafy_line() {
4868 // zle_tricky.c:978
4869 let raw_vec: Vec<char> = ZLELINE
4870 .get_or_init(|| Mutex::new(String::new()))
4871 .lock()
4872 .map(|g| g.chars().collect())
4873 .unwrap_or_default();
4874 let zlell = raw_vec.len();
4875 let zlecs = ZLECS.load(Ordering::Relaxed);
4876 let mut out_ll: i32 = 0;
4877 let mut out_cs: i32 = 0;
4878 // c:982 — `zlemetaline = zlelineasstring(zleline, zlell, zlecs, &zlemetall, &zlemetacs, 0);`
4879 let meta = crate::ported::zle::zle_utils::zlelineasstring(
4880 &raw_vec,
4881 zlell,
4882 zlecs,
4883 Some(&mut out_ll),
4884 Some(&mut out_cs),
4885 0,
4886 );
4887 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
4888 *g = meta;
4889 }
4890 ZLEMETALL.store(out_ll, Ordering::Relaxed);
4891 ZLEMETACS.store(out_cs, Ordering::Relaxed);
4892 // c:985 — `metalinesz = zlemetall;`. Rust String grows on demand;
4893 // no separate sizeline tracker.
4894 // c:989-990 — `free(zleline); zleline = NULL;`. Rust: clear the
4895 // buffer + zero ZLELL.
4896 if let Ok(mut g) = ZLELINE.get().unwrap().lock() {
4897 g.clear();
4898 }
4899 ZLELL.store(0, Ordering::Relaxed);
4900}
4901
4902fn opt_isset(name: &str) -> i32 {
4903 // options.c
4904 if crate::ported::options::opt_state_get(name).unwrap_or(false) {
4905 1
4906 } else {
4907 0
4908 }
4909}
4910/// Real call into `getiparam(name)` — the canonical paramtab read.
4911/// Mirrors C's `getiparam` at params.c:3044 which reads the global
4912/// `paramtab` directly via `gethashnode2`.
4913fn env_iparam(name: &str) -> i32 {
4914 // params.c:3044
4915 crate::ported::params::getiparam(name) as i32
4916}
4917fn lastprebr_set(s: &str) {
4918 // zle_tricky.c lastprebr
4919 if let Ok(mut g) = LASTPREBR.get_or_init(|| Mutex::new(String::new())).lock() {
4920 *g = s.to_string();
4921 }
4922}
4923fn lastpostbr_set(s: &str) {
4924 // zle_tricky.c lastpostbr
4925 if let Ok(mut g) = LASTPOSTBR.get_or_init(|| Mutex::new(String::new())).lock() {
4926 *g = s.to_string();
4927 }
4928}
4929
4930/// Choose `$compstate[context]` per the lex classification in `inwhat`
4931/// (and the `ispar` modifier). Direct lift of compcore.c:591-617.
4932fn compcontext_for(_s: &str) -> String {
4933 // c:591
4934 let ip = ispar.load(Ordering::Relaxed); // c:599
4935 if ip == 2 {
4936 return "brace_parameter".into();
4937 } // c:600
4938 if ip == 1 {
4939 return "parameter".into();
4940 } // c:601
4941 let lw = linwhat.load(Ordering::Relaxed); // c:602
4942 match lw {
4943 // c:602
4944 x if x == IN_PAR_LW => "assign_parameter".into(), // c:603
4945 x if x == IN_MATH_LW => "math".into(), // c:604-611
4946 x if x == IN_COND_LW => "condition".into(), // c:613
4947 x if x == IN_ENV_LW => "value".into(), // c:615
4948 _ => "command".into(), // c:617
4949 }
4950}
4951
4952/// File-scope `int offs` from `Src/Zle/zle_tricky.c:88`. The C source
4953/// declares this as `mod_export`; mirrored here per Rule 9 since it's
4954/// not yet at a canonical Rust home.
4955pub static OFFS: AtomicI32 = AtomicI32::new(0); // zle_tricky.c:88
4956
4957/// File-scope `Compctl freecl` from `Src/Zle/compcore.c:255`. The
4958/// freelist of available Compctl slots for the current completion call.
4959pub static freecl: OnceLock<Mutex<Option<i32>>> = OnceLock::new(); // c:255
4960
4961/// Real call into `doshfunc` — `Src/exec.c`. Looks up the function
4962/// in the global shfunctab (`getshfunc`) and dispatches via the VM's
4963/// `functions_compiled` map. Returns the function's exit status
4964/// (LASTVAL after the call), matching C's `doshfunc` return value.
4965pub fn shfunc_call(name: &str) -> i32 {
4966 // exec.c
4967 if crate::ported::utils::getshfunc(name).is_none() {
4968 // c:exec.c:5800
4969 return 1; // missing fn → status 1
4970 }
4971 // Route through the canonical exec accessors dispatcher so the
4972 // function actually executes. Hook returns Option<i32>; None
4973 // means no executor context is set up yet (fall back to
4974 // LASTVAL read).
4975 crate::ported::exec::dispatch_function_call(name, &[])
4976 .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed))
4977}
4978/// Real call into `setsparam(&format!("compstate[{key}]"), val)` — the
4979/// canonical paramtab write. Mirrors C's `setsparam` at params.c:3350.
4980///
4981/// Now `pub` so compsys engine ports can write `$compstate[KEY]`
4982/// directly. Also dual-writes to `paramtab_hashed_storage()` under
4983/// the "compstate" key so subscript lookups via the hash-param
4984/// machinery see the same value — `$compstate` IS a PM_HASHED param
4985/// (created by `makecompparams` at `complete.rs:1499`), and shell
4986/// scripts read it as such.
4987pub fn set_compstate_str(key: &str, val: &str) {
4988 // params.c:3350 — flat bracketed-param write (preserves the
4989 // pre-existing access path used by `set_compstate_str` callers).
4990 let pname = format!("compstate[{}]", key);
4991 let _ = setsparam(&pname, val);
4992
4993 // Hash-storage write: dual-store under the `compstate` hash so
4994 // `${compstate[KEY]}` shell reads (via the hashparam machinery)
4995 // and any direct `paramtab_hashed_storage()` consumer see the
4996 // same value.
4997 if let Ok(mut tab) = paramtab_hashed_storage().lock() {
4998 tab.entry("compstate".to_string())
4999 .or_default()
5000 .insert(key.to_string(), val.to_string());
5001 }
5002}
5003
5004/// Read `$compstate[KEY]`. Returns `None` when the key was never set.
5005///
5006/// Prefers the hash-storage view (the canonical home for a PM_HASHED
5007/// param); falls back to the legacy flat `compstate[KEY]` bracketed
5008/// param for entries that some code wrote via raw `setsparam` without
5009/// going through [`set_compstate_str`].
5010pub fn get_compstate_str(key: &str) -> Option<String> {
5011 // c:complete.c:1411-1414 — `compstate[nmatches]` is a LIVE GSU integer:
5012 // `get_nmatches` flushes pending match groups via `permmatches(0)` and
5013 // returns the running `nmatches` counter. The stored-hash read below
5014 // served a stale 0 for it, so every completer's `nm != $compstate[nmatches]`
5015 // idiom (_describe, _arguments, _alternative, …) concluded "nothing was
5016 // added" and option completion died even though addmatches had added
5017 // hundreds of matches.
5018 if key == "nmatches" {
5019 let v = if permmatches(0) != 0 {
5020 0
5021 } else {
5022 nmatches.load(Ordering::Relaxed)
5023 };
5024 return Some(v.to_string());
5025 }
5026 if let Ok(tab) = paramtab_hashed_storage().lock() {
5027 if let Some(hash) = tab.get("compstate") {
5028 if let Some(v) = hash.get(key) {
5029 return Some(v.clone());
5030 }
5031 }
5032 }
5033 // Fallback: pre-existing callers wrote via raw `setsparam` only.
5034 let pname = format!("compstate[{}]", key);
5035 getsparam(&pname)
5036}
5037
5038/// Local helper: position before-the-current char (handles UTF-8).
5039#[inline]
5040fn prev_char_index(bytes: &[u8], pos: usize) -> usize {
5041 // local
5042 if pos == 0 {
5043 return 0;
5044 }
5045 let mut i = pos - 1;
5046 while i > 0 && (bytes[i] & 0xC0) == 0x80 {
5047 i -= 1;
5048 }
5049 i
5050}
5051
5052#[inline]
5053fn char_at(bytes: &[u8], pos: usize) -> char {
5054 // local
5055 if pos >= bytes.len() {
5056 return '\0';
5057 }
5058 let s = match std::str::from_utf8(&bytes[pos..]) {
5059 Ok(s) => s,
5060 Err(_) => return '\0',
5061 };
5062 s.chars().next().unwrap_or('\0')
5063}
5064
5065/// Depth counter so `set_comp_sep`'s sanity assert ("lexsave/restore
5066/// balanced") fires when a future port mismatches them.
5067static LEXSAVE_DEPTH: AtomicI32 = AtomicI32::new(0); // local
5068
5069/// Walk a balanced pair of in/out token bytes starting at `start`,
5070/// returning the index just after the closing token, or None if
5071/// unbalanced. C `skipparens` returns the position; this version
5072/// returns the same semantic.
5073fn skip_token_parens(bytes: &[u8], start: usize, open: char, close: char) -> Option<usize> {
5074 let mut depth: i32 = 0;
5075 let mut i = start;
5076 while i < bytes.len() {
5077 let c = char_at(bytes, i);
5078 if c == open {
5079 depth += 1;
5080 } else if c == close {
5081 depth -= 1;
5082 if depth == 0 {
5083 return Some(i + c.len_utf8());
5084 }
5085 }
5086 i += c.len_utf8();
5087 }
5088 if depth == 0 {
5089 Some(i)
5090 } else {
5091 None
5092 }
5093}
5094
5095/// Walk the INAMESPC name-character class — equivalent to C's
5096/// `itype_end(e, INAMESPC, 0)` loop. Stops at first non-name char.
5097fn walk_namespace(bytes: &[u8]) -> usize {
5098 // local
5099 let s = match std::str::from_utf8(bytes) {
5100 Ok(s) => s,
5101 Err(_) => return 0,
5102 };
5103 let mut len = 0usize;
5104 for c in s.chars() {
5105 if c.is_alphanumeric() || c == '_' {
5106 len += c.len_utf8();
5107 } else {
5108 break;
5109 }
5110 }
5111 len
5112}
5113
5114/// Strip Inbrace/Outbrace/Stringg/etc. token bytes back to literal
5115/// characters — substitute for C `untokenize()` over the slice. The
5116/// canonical Rust untokenize lives in `crate::lex::untokenize`.
5117fn strip_tokens(s: &str) -> String {
5118 // local
5119 crate::lex::untokenize(s).to_string()
5120}
5121
5122/// File-scope `int hcompcall` accessor — `compfunc` active iff non-empty.
5123fn compfunc_active() -> bool {
5124 compfunc
5125 .get_or_init(|| Mutex::new(None))
5126 .lock()
5127 .ok()
5128 .and_then(|g| g.clone())
5129 .map(|s| !s.is_empty())
5130 .unwrap_or(false)
5131}
5132
5133/// Direct port of `void lexsave(void)` from `Src/lex.c`. Delegates
5134/// to `zcontext_save` which pushes the lex/parse/hist context stack
5135/// frame. Returns a token (current stack depth) for symmetry with
5136/// the C `int` save token used by `set_comp_sep` for invariant check.
5137fn lexsave() -> usize {
5138 // lex.c via context.c:80
5139 crate::ported::context::zcontext_save();
5140 (LEXSAVE_DEPTH.fetch_add(1, Ordering::SeqCst) + 1) as usize
5141}
5142
5143/// Direct port of `void lexrestore(void)` from `Src/lex.c`. Pops the
5144/// last `zcontext_save` frame. C body restores hist/lex/parse via
5145/// `zcontext_restore_partial(ZCONTEXT_HIST|ZCONTEXT_LEX|ZCONTEXT_PARSE)`.
5146fn lexrestore(_token: usize) {
5147 // lex.c via context.c:117
5148 let parts = ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE;
5149 zcontext_restore_partial(parts);
5150 LEXSAVE_DEPTH.fetch_sub(1, Ordering::SeqCst);
5151}
5152
5153// ---- Extern stubs for addmatches's bucket-3 dependencies ----
5154
5155fn compquote_first() -> Option<char> {
5156 // zle_tricky.c compquote
5157 COMPQUOTE
5158 .get_or_init(|| Mutex::new(String::new()))
5159 .lock()
5160 .ok()
5161 .and_then(|g| g.chars().next())
5162}
5163fn instring_set(v: i32) {
5164 // zle_tricky.c:419
5165 INSTRING.store(v, Ordering::Relaxed);
5166}
5167fn inbackt_set(v: i32) {
5168 // zle_tricky.c:419
5169 INBACKT.store(v, Ordering::Relaxed);
5170}
5171fn autoq_set(s: &str) {
5172 // zle_tricky.c autoq
5173 if let Ok(mut g) = AUTOQ.get_or_init(|| Mutex::new(String::new())).lock() {
5174 *g = s.to_string();
5175 }
5176}
5177
5178// ---- Extern stubs for makecomplist's bucket-3 dependencies ----
5179
5180/// File-scope holder for `Cmlist bmatchers` — `Src/Zle/compcore.c:236`.
5181/// C linked-list of matchers active for brace-matching, populated by
5182/// `add_bmatchers` walking the user-installed `Cmatcher` chain.
5183pub static bmatchers: OnceLock<Mutex<Option<Box<Cmlist>>>> = OnceLock::new(); // c:236
5184
5185/// File-scope holder for `Cmlist mstack` — `Src/Zle/compcore.c:236`.
5186/// Matcher-stack — current active matcher list for compadd recursion.
5187pub static mstack: OnceLock<Mutex<Option<Box<Cmlist>>>> = OnceLock::new(); // c:236
5188
5189// ---- Extern stubs for add_match_data's Cline operations ----
5190
5191/// Bridge to `cline_matched()` — `Src/Zle/compmatch.c:253`. The
5192/// real port takes `&mut Option<Box<Cline>>` walking the chain
5193/// marking each node CLF_MATCHED. With only a string slice here we
5194/// build a one-node Cline shim and route the call through it so the
5195/// CLF_MATCHED state-machine update fires the same way as in C.
5196fn cline_matched_compcore(line: Option<&str>) {
5197 // compmatch.c:253
5198 let Some(s) = line else {
5199 return;
5200 };
5201 if s.is_empty() {
5202 return;
5203 }
5204 let mut head = Some(Box::new(Cline {
5205 line: Some(s.to_string()),
5206 llen: s.len() as i32,
5207 ..Default::default()
5208 }));
5209 cline_matched(&mut head);
5210}
5211/// Real read of `char *qisuf` via the paramtab. Mirrors C's direct
5212/// global read at `Src/Zle/zle_tricky.c qisuf`.
5213fn qisuf_get() -> String {
5214 // zle_tricky.c qisuf
5215 getsparam("qisuf").unwrap_or_default()
5216}
5217fn qipre_get() -> String {
5218 // zle_tricky.c qipre
5219 getsparam("qipre").unwrap_or_default()
5220}
5221
5222/// Adapter for `int movefd(int fd)` from `Src/utils.c:2974` —
5223/// delegates to the canonical port in `ported::utils::movefd`.
5224fn movefd(fd: i32) -> i32 {
5225 // utils.c:2974
5226 crate::ported::utils::movefd(fd)
5227}
5228
5229/// Adapter for `void redup(int new, int old)` from `Src/utils.c:2021` —
5230/// delegates to the canonical port `ported::utils::redup`. Callers
5231/// only need the new-fd form here; `old` is the inverse of movefd's
5232/// reservation (passed as -1 to mean "no original").
5233fn redup(new: i32) {
5234 // utils.c:2021
5235 crate::ported::utils::redup(new, -1);
5236}
5237
5238/// File-scope registry mirroring `Src/init.c`'s `zshhooks[]` table —
5239/// each hook name maps to the ordered list of shfunc names to call.
5240pub static HOOK_FNS: OnceLock<Mutex<std::collections::HashMap<String, Vec<String>>>> =
5241 OnceLock::new(); // init.c zshhooks
5242
5243/// Adapter for the `errflag` global from `Src/init.c` — reads the
5244/// canonical atomic in `ported::utils::errflag`.
5245fn errflag_get() -> bool {
5246 crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 // init.c
5247}
5248
5249/// Local dispatcher used by compcore call sites for hook names that
5250/// don't yet have a typed-data argument. Delegates to the canonical
5251/// `module::runhookdef(gethookdef(name), NULL)` — no-op when no
5252/// Hookfn is registered (c:993-995). Returns the Hookfn return value
5253/// (or 0 when no handler fires).
5254fn runhookdef_compcore(hook: &str) -> i32 {
5255 // c:990
5256 let h = gethookdef(hook);
5257 if h.is_null() {
5258 return 0;
5259 }
5260 runhookdef(h, std::ptr::null_mut())
5261}
5262
5263/// Port of `static int ccmakehookfn(Hookdef, Ccmakedat dat)` from
5264/// `Src/Zle/compctl.c:1763` — the function the compctl module registers
5265/// on COMPCTLMAKEHOOK. This is the compctl-side analog of the compfunc
5266/// branch in `makecomplist`: it builds the match list via
5267/// `makecomplistglobal` (the default command/file completion the bare
5268/// `zsh -f` shell uses), finalizes it with `permmatches`, and reports
5269/// success through `dat.lst` (0 = matches, 1 = none). The previous stub
5270/// only called the unrelated `makecomplistctl` and never set `dat.lst`,
5271/// so `makecomplist` returned the raw list-type (nonzero) and
5272/// `do_completion` always took the feep/error path — `l<Tab>` produced
5273/// nothing.
5274///
5275/// The C source loops over the global matcher list (`cmatcher`); the
5276/// common case (and always under `-f`) has none, so this port runs the
5277/// single no-matcher pass, matching begcmgroup/makecomplistglobal/
5278/// endcmgroup/permmatches exactly as the compfunc branch does.
5279fn runhookdef_compctlmake(
5280 // c:1763
5281 dat: &mut Ccmakedat,
5282) {
5283 let os = dat.str.clone().unwrap_or_default();
5284 let incmd = dat.incmd;
5285 let lst = dat.lst;
5286
5287 // c:1794-1810 — no global matchers → mstack = NULL, bmatchers = NULL.
5288 if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
5289 *g = None;
5290 }
5291 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
5292 *g = None;
5293 }
5294 // c:1812-1813 — ainfo = fainfo = hcalloc.
5295 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
5296 *g = Some(Aminfo::default());
5297 }
5298 if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
5299 *g = Some(Aminfo::default());
5300 }
5301 if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
5302 *g = None; // c:1815
5303 }
5304 if VALIDLIST.load(Ordering::Relaxed) == 0 {
5305 LASTAMBIG.store(0, Ordering::Relaxed); // c:1817
5306 }
5307 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5308 g.clear(); // c:1818
5309 }
5310 mnum.store(0, Ordering::Relaxed); // c:1819
5311 unambig_mnum.store(-1, Ordering::Relaxed); // c:1820
5312 if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
5313 g.clear(); // c:1821
5314 }
5315 insmnum.store(ZMULT.load(Ordering::Relaxed), Ordering::Relaxed); // c:1822
5316 oldlist.store(0, Ordering::Relaxed); // c:1829
5317 oldins.store(0, Ordering::Relaxed); // c:1829
5318 begcmgroup(Some("default"), 0); // c:1830
5319 MENUCMP.store(0, Ordering::Relaxed); // c:1831
5320 menuacc.store(0, Ordering::Relaxed); // c:1831
5321 newmatches.store(0, Ordering::Relaxed); // c:1831
5322 onlyexpl.store(0, Ordering::Relaxed); // c:1831
5323
5324 // c:1836-1837 — makecomplistglobal(s, incmd, lst, 0) generates the
5325 // matches (per-command compctl or the default command/file logic).
5326 crate::ported::zle::compctl::makecomplistglobal(&os, incmd != 0, lst, 0);
5327 endcmgroup(None); // c:1838
5328
5329 // c:1879-1892 — permmatches(1); amatches = pmatches; swap holders.
5330 permmatches(1); // c:1879
5331 let p_snap = pmatches
5332 .get_or_init(|| Mutex::new(Vec::new()))
5333 .lock()
5334 .ok()
5335 .map(|g| g.clone())
5336 .unwrap_or_default();
5337 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5338 *g = p_snap.clone(); // c:1880
5339 }
5340 lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1881
5341 lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1882
5342 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5343 *g = p_snap; // c:1884
5344 }
5345 let lm_snap = lmatches
5346 .get_or_init(|| Mutex::new(None))
5347 .lock()
5348 .ok()
5349 .and_then(|g| g.clone());
5350 if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
5351 *g = lm_snap; // c:1885
5352 }
5353 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
5354 g.clear(); // c:1886
5355 }
5356 hasperm.store(0, Ordering::Relaxed); // c:1887
5357 hasoldlist.store(1, Ordering::Relaxed); // c:1888
5358
5359 // c:1890-1901 — success iff we produced matches and no error.
5360 if nmatches.load(Ordering::Relaxed) != 0 && !errflag_get() {
5361 VALIDLIST.store(1, Ordering::Relaxed); // c:1891
5362 dat.lst = 0; // c:1894
5363 } else {
5364 dat.lst = 1; // c:1908
5365 }
5366}
5367
5368// =====================================================================
5369// permmatches — `Src/Zle/compcore.c:3423`.
5370// =====================================================================
5371
5372/// Static state for `permmatches`'s `static int fi`. C scopes the
5373/// flag to the function; Rust hoists it to file scope per Rule S1.
5374static PERMMATCHES_FI: AtomicI32 = AtomicI32::new(0); // c:3423 static int fi
5375
5376/// Port of the `type==0` string-sort branch of `makearray()` from
5377/// compcore.c:3239-3257. Sorts strings via `strmetasort` + dedup.
5378pub fn makearray_strings(mut rp: Vec<String>, flags: i32) -> (Vec<String>, i32) {
5379 // c:3239
5380 let mut n: i32 = rp.len() as i32;
5381 if flags != 0 && n > 0 {
5382 // c:3240
5383 let numeric = isset(NUMERICGLOBSORT); // c:3243
5384 let mut sf = SORTIT_IGNORING_BACKSLASHES as u32;
5385 if numeric {
5386 sf |= SORTIT_NUMERICALLY as u32;
5387 }
5388 crate::ported::sort::strmetasort(&mut rp, sf, None); // c:3242-3244
5389
5390 // Dedup consecutive equals. // c:3247
5391 let mut cp = 0usize;
5392 let mut ap = 0usize;
5393 while ap < rp.len() {
5394 if ap != cp {
5395 rp.swap(ap, cp);
5396 }
5397 cp += 1;
5398 let mut bp = ap;
5399 while bp + 1 < rp.len() && rp[ap] == rp[bp + 1] {
5400 // c:3250
5401 bp += 1;
5402 n -= 1;
5403 }
5404 ap = bp + 1; // c:3252
5405 }
5406 rp.truncate(cp); // c:3253
5407 }
5408 (rp, n)
5409}
5410
5411#[cfg(test)]
5412mod tests {
5413 use super::*;
5414
5415 #[test]
5416 fn rembslash_basic() {
5417 let _g = crate::test_util::global_state_lock();
5418 let _g = zle_test_setup();
5419 assert_eq!(rembslash("hello\\ world"), "hello world");
5420 assert_eq!(rembslash("no\\\\slash"), "no\\slash");
5421 assert_eq!(rembslash("plain"), "plain");
5422 }
5423
5424 #[test]
5425 fn comp_quoting_string_table() {
5426 let _g = crate::test_util::global_state_lock();
5427 let _g = zle_test_setup();
5428 assert_eq!(comp_quoting_string(QT_SINGLE), "'");
5429 assert_eq!(comp_quoting_string(QT_DOUBLE), "\"");
5430 assert_eq!(comp_quoting_string(QT_DOLLARS), "$'");
5431 assert_eq!(comp_quoting_string(0), "\\");
5432 assert_eq!(comp_quoting_string(QT_BACKSLASH), "\\");
5433 }
5434
5435 #[test]
5436 fn matcheq_equal_strings() {
5437 let _g = crate::test_util::global_state_lock();
5438 let _g = zle_test_setup();
5439 let mut a = Cmatch::default();
5440 a.str = Some("foo".into());
5441 let mut b = Cmatch::default();
5442 b.str = Some("foo".into());
5443 assert!(matcheq(&a, &b));
5444 }
5445
5446 #[test]
5447 fn matcheq_different_strings() {
5448 let _g = crate::test_util::global_state_lock();
5449 let _g = zle_test_setup();
5450 let mut a = Cmatch::default();
5451 a.str = Some("foo".into());
5452 let mut b = Cmatch::default();
5453 b.str = Some("bar".into());
5454 assert!(!matcheq(&a, &b));
5455 }
5456
5457 #[test]
5458 fn matcheq_one_side_none() {
5459 let _g = crate::test_util::global_state_lock();
5460 let _g = zle_test_setup();
5461 let mut a = Cmatch::default();
5462 a.pre = Some("p".into());
5463 let b = Cmatch::default();
5464 assert!(!matcheq(&a, &b));
5465 }
5466
5467 #[test]
5468 fn get_user_var_reads_array_from_paramtab() {
5469 let _g = crate::test_util::global_state_lock();
5470 // c:2003 — `getaparam(nam)` first. Verify array params come
5471 // out as a Vec, not via env.
5472 let _g = zle_test_setup();
5473 setaparam("__test_arr", vec!["a".into(), "bb".into(), "ccc".into()]);
5474 let got = get_user_var(Some("__test_arr"));
5475 assert_eq!(got, Some(vec!["a".into(), "bb".into(), "ccc".into()]));
5476 // Cleanup so we don't poison other tests.
5477 setaparam("__test_arr", vec![]);
5478 }
5479
5480 #[test]
5481 fn get_user_var_reads_scalar_as_single_element_array() {
5482 let _g = crate::test_util::global_state_lock();
5483 // c:2007-2009 — getsparam fallback: wrap scalar in 1-element array.
5484 let _g = zle_test_setup();
5485 setsparam("__test_scalar", "hello");
5486 let got = get_user_var(Some("__test_scalar"));
5487 assert_eq!(got, Some(vec!["hello".to_string()]));
5488 setsparam("__test_scalar", "");
5489 }
5490
5491 #[test]
5492 fn get_user_var_paren_list_splits_on_separators() {
5493 let _g = crate::test_util::global_state_lock();
5494 // c:1960-1996 — `(a b c)` paren list, NOT a param lookup.
5495 let _g = zle_test_setup();
5496 let got = get_user_var(Some("(one two three)"));
5497 assert_eq!(got, Some(vec!["one".into(), "two".into(), "three".into()]));
5498 }
5499
5500 #[test]
5501 fn get_user_var_none_for_missing() {
5502 let _g = crate::test_util::global_state_lock();
5503 // c:1956 + c:2009 — missing param returns None.
5504 let _g = zle_test_setup();
5505 // (env vars must not leak through — we don't read $PATH etc.)
5506 let got = get_user_var(Some("__definitely_not_a_param_xyz"));
5507 assert_eq!(got, None);
5508 }
5509
5510 #[test]
5511 fn get_data_arr_reads_hashed_keys_or_values() {
5512 let _g = crate::test_util::global_state_lock();
5513 // c:2022 — fetchvalue(name, SCANPM_WANTKEYS|WANTVALS|MATCHMANY).
5514 let _g = zle_test_setup();
5515 crate::ported::params::sethparam(
5516 "__test_hash",
5517 vec!["k1".into(), "v1".into(), "k2".into(), "v2".into()],
5518 );
5519
5520 let keys = get_data_arr("__test_hash", true);
5521 assert!(keys.is_some(), "hashed param should have keys");
5522 let mut keys = keys.unwrap();
5523 keys.sort();
5524 assert_eq!(keys, vec!["k1".to_string(), "k2".to_string()]);
5525
5526 let vals = get_data_arr("__test_hash", false);
5527 assert!(vals.is_some(), "hashed param should have values");
5528 let mut vals = vals.unwrap();
5529 vals.sort();
5530 assert_eq!(vals, vec!["v1".to_string(), "v2".to_string()]);
5531 }
5532
5533 #[test]
5534 fn get_data_arr_none_for_non_hashed() {
5535 let _g = crate::test_util::global_state_lock();
5536 // c:2032 — fetchvalue NULL → return NULL for params that
5537 // aren't associative arrays.
5538 let _g = zle_test_setup();
5539 setsparam("__test_scalar2", "value");
5540 let got = get_data_arr("__test_scalar2", false);
5541 assert_eq!(got, None, "scalar params must NOT come out of get_data_arr");
5542 }
5543
5544 #[test]
5545 fn before_complete_snapshots_oldmenucmp() {
5546 let _g = crate::test_util::global_state_lock();
5547 // c:463 — `oldmenucmp = menucmp;`
5548 let _g = zle_test_setup();
5549 MENUCMP.store(7, Ordering::Relaxed);
5550 OLDMENUCMP.store(0, Ordering::Relaxed);
5551 let mut lst = 0;
5552 let _ = before_complete(&mut lst);
5553 assert_eq!(OLDMENUCMP.load(Ordering::Relaxed), 7);
5554 // Reset for other tests.
5555 MENUCMP.store(0, Ordering::Relaxed);
5556 OLDMENUCMP.store(0, Ordering::Relaxed);
5557 }
5558
5559 #[test]
5560 fn before_complete_clears_showagain() {
5561 let _g = crate::test_util::global_state_lock();
5562 // c:467 — `showagain = 0;` always (after the validlist gate).
5563 let _g = zle_test_setup();
5564 SHOWAGAIN.store(5, Ordering::Relaxed);
5565 let mut lst = 0;
5566 let _ = before_complete(&mut lst);
5567 assert_eq!(
5568 SHOWAGAIN.load(Ordering::Relaxed),
5569 0,
5570 "SHOWAGAIN must be cleared by before_complete"
5571 );
5572 }
5573
5574 #[test]
5575 fn remsquote_default_quoting() {
5576 let _g = crate::test_util::global_state_lock();
5577 let _g = zle_test_setup();
5578 let mut s = String::from("a'\\''b");
5579 let n = remsquote(&mut s);
5580 assert_eq!(s, "a'b");
5581 assert_eq!(n, 3);
5582 }
5583
5584 #[test]
5585 fn ctokenize_dollar_substitution() {
5586 let _g = crate::test_util::global_state_lock();
5587 let _g = zle_test_setup();
5588 let out = ctokenize("$x{y}");
5589 let chars: Vec<char> = out.chars().collect();
5590 assert_eq!(chars[0], Stringg);
5591 assert_eq!(chars[1], 'x');
5592 assert_eq!(chars[2], Inbrace);
5593 assert_eq!(chars[3], 'y');
5594 assert_eq!(chars[4], Outbrace);
5595 }
5596
5597 #[test]
5598 fn get_user_var_inline_list() {
5599 let _g = crate::test_util::global_state_lock();
5600 let _g = zle_test_setup();
5601 let result = get_user_var(Some("(a b c)")).unwrap();
5602 assert_eq!(result, vec!["a", "b", "c"]);
5603 }
5604
5605 #[test]
5606 fn matchcmp_str_sort_default() {
5607 let _g = crate::test_util::global_state_lock();
5608 let _g = zle_test_setup();
5609 MATCHORDER.store(CGF_MATSORT, Ordering::Relaxed);
5610 let mut a = Cmatch::default();
5611 a.str = Some("apple".into());
5612 let mut b = Cmatch::default();
5613 b.str = Some("banana".into());
5614 assert_eq!(matchcmp(&a, &b), std::cmp::Ordering::Less);
5615 assert_eq!(matchcmp(&b, &a), std::cmp::Ordering::Greater);
5616 assert_eq!(matchcmp(&a, &a), std::cmp::Ordering::Equal);
5617 MATCHORDER.store(0, Ordering::Relaxed);
5618 }
5619
5620 #[test]
5621 fn dupmatch_clones_strings_and_truncates_braces() {
5622 let _g = crate::test_util::global_state_lock();
5623 let _g = zle_test_setup();
5624 // C body c:3370: deep-copy strings, truncate brpl/brsl to nbeg/nend.
5625 let mut src = Cmatch::default();
5626 src.str = Some("foo".into());
5627 src.ipre = Some("ipre".into());
5628 src.flags = 7;
5629 src.brpl = vec![10, 20, 30, 40];
5630 src.brsl = vec![5, 6, 7];
5631 src.qipl = 1;
5632 src.qisl = 2;
5633 src.mode = 0o755;
5634 src.modec = 'd';
5635
5636 let r = dupmatch(&src, 2, 1);
5637 assert_eq!(r.str.as_deref(), Some("foo"));
5638 assert_eq!(r.ipre.as_deref(), Some("ipre"));
5639 assert_eq!(r.flags, 7);
5640 assert_eq!(r.brpl, vec![10, 20]); // truncated to nbeg=2
5641 assert_eq!(r.brsl, vec![5]); // truncated to nend=1
5642 assert_eq!(r.qipl, 1);
5643 assert_eq!(r.qisl, 2);
5644 assert_eq!(r.mode, 0o755);
5645 assert_eq!(r.modec, 'd');
5646 }
5647
5648 #[test]
5649 fn dupmatch_empty_braces_stay_empty() {
5650 let _g = crate::test_util::global_state_lock();
5651 let _g = zle_test_setup();
5652 // C body c:3395/3404: NULL brpl/brsl stay NULL regardless of nbeg/nend.
5653 let src = Cmatch::default();
5654 let r = dupmatch(&src, 5, 5);
5655 assert!(r.brpl.is_empty());
5656 assert!(r.brsl.is_empty());
5657 }
5658
5659 #[test]
5660 fn makearray_sorted_and_deduped() {
5661 let _g = crate::test_util::global_state_lock();
5662 let _g = zle_test_setup();
5663 // c:3262-3291: sort + dedup with matcheq. Same str + nil disp =>
5664 // collapses into one entry with CMF_FMULT set on the survivor.
5665 let mut a = Cmatch::default();
5666 a.str = Some("z".into());
5667 let mut b = Cmatch::default();
5668 b.str = Some("a".into());
5669 let mut c = Cmatch::default();
5670 c.str = Some("a".into());
5671 let (arr, n, _nl, _ll) = makearray(vec![a, b, c], CGF_MATSORT);
5672 // Two distinct visible strings after dedup ("a", "z").
5673 assert_eq!(arr.len(), 2);
5674 assert_eq!(n, 2);
5675 assert_eq!(arr[0].str.as_deref(), Some("a"));
5676 assert_eq!(arr[1].str.as_deref(), Some("z"));
5677 }
5678
5679 #[test]
5680 fn makearray_nosort_unchanged_order() {
5681 let _g = crate::test_util::global_state_lock();
5682 let _g = zle_test_setup();
5683 // c:3300: CGF_NOSORT branch; with no UNIQ flags, order preserved.
5684 let mut a = Cmatch::default();
5685 a.str = Some("z".into());
5686 let mut b = Cmatch::default();
5687 b.str = Some("a".into());
5688 let (arr, n, _, _) = makearray(vec![a, b], CGF_NOSORT | CGF_UNIQALL);
5689 // UNIQALL active so no dedup pass runs.
5690 assert_eq!(n, 2);
5691 assert_eq!(arr[0].str.as_deref(), Some("z"));
5692 assert_eq!(arr[1].str.as_deref(), Some("a"));
5693 }
5694
5695 #[test]
5696 fn makearray_strings_dedup_consecutive() {
5697 let _g = crate::test_util::global_state_lock();
5698 let _g = zle_test_setup();
5699 // c:3239 path: sort + drop adjacent duplicates.
5700 let (arr, n) = makearray_strings(vec!["b".into(), "a".into(), "a".into(), "c".into()], 1);
5701 assert_eq!(n, 3);
5702 assert_eq!(arr, vec!["a", "b", "c"]);
5703 }
5704
5705 #[test]
5706 fn check_param_no_dollar_returns_none() {
5707 let _g = crate::test_util::global_state_lock();
5708 let _g = zle_test_setup();
5709 // c:1316: no `$` in string → return None.
5710 OFFS.store(2, Ordering::Relaxed);
5711 assert_eq!(check_param("abc", false, false), None);
5712 }
5713
5714 #[test]
5715 fn check_param_simple_dollar_var_at_cursor() {
5716 let _g = crate::test_util::global_state_lock();
5717 let _g = zle_test_setup();
5718 // c:1259-1311: `$FOO` with cursor inside the name → return b.
5719 OFFS.store(2, Ordering::Relaxed);
5720 let s = format!("{}FOO", Stringg);
5721 let r = check_param(&s, false, true);
5722 assert!(r.is_some(), "expected Some(b) inside $FOO");
5723 }
5724
5725 #[test]
5726 fn callcompfunc_empty_fn_no_panic() {
5727 let _g = crate::test_util::global_state_lock();
5728 let _g = zle_test_setup();
5729 // c:552: getshfunc(NULL) early-return.
5730 callcompfunc("anything", "");
5731 }
5732
5733 #[test]
5734 fn callcompfunc_sets_compstate_context() {
5735 let _g = crate::test_util::global_state_lock();
5736 let _g = zle_test_setup();
5737 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5738 // c:619: context selection — verified via the pure
5739 // compcontext_for helper (callcompfunc calls it and writes
5740 // to paramtab via setsparam, but paramtab read-back in a
5741 // unit-test context without a live VM is unreliable).
5742 ispar.store(0, Ordering::Relaxed);
5743 linwhat.store(IN_PAR_LW, Ordering::Relaxed);
5744 assert_eq!(compcontext_for("foo"), "assign_parameter");
5745 // Body executes without panicking against the real paramtab.
5746 callcompfunc("foo", "_test_fn");
5747 }
5748
5749 /// Test-only serializer for tests that mutate file-scope globals.
5750 static GLOBAL_MUT_LOCK: Mutex<()> = Mutex::new(());
5751
5752 #[test]
5753 fn compcontext_for_routes_ispar_first() {
5754 let _g = crate::test_util::global_state_lock();
5755 let _g = zle_test_setup();
5756 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5757 ispar.store(2, Ordering::Relaxed);
5758 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
5759 assert_eq!(compcontext_for("x"), "brace_parameter");
5760 ispar.store(1, Ordering::Relaxed);
5761 assert_eq!(compcontext_for("x"), "parameter");
5762 ispar.store(0, Ordering::Relaxed);
5763 linwhat.store(IN_MATH_LW, Ordering::Relaxed);
5764 assert_eq!(compcontext_for("x"), "math");
5765 linwhat.store(IN_COND_LW, Ordering::Relaxed);
5766 assert_eq!(compcontext_for("x"), "condition");
5767 linwhat.store(IN_ENV_LW, Ordering::Relaxed);
5768 assert_eq!(compcontext_for("x"), "value");
5769 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
5770 assert_eq!(compcontext_for("x"), "command");
5771 }
5772
5773 #[test]
5774 fn addmatches_empty_argv_early_return() {
5775 let _g = crate::test_util::global_state_lock();
5776 let _g = zle_test_setup();
5777 // c:2138-2139: empty argv + dummies==0 + no CAF_ALL → return 1.
5778 let mut dat = Cadata::default();
5779 dat.dummies = 0;
5780 dat.aflags = 0;
5781 assert_eq!(addmatches(&mut dat, &[]), 1);
5782 }
5783
5784 #[test]
5785 fn addmatches_appends_argv_to_default_group() {
5786 let _g = crate::test_util::global_state_lock();
5787 let _g = zle_test_setup();
5788 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5789 // c:2200 simplified body: each argv entry → addmatch into "default" group.
5790 amatches
5791 .get_or_init(|| Mutex::new(Vec::new()))
5792 .lock()
5793 .unwrap()
5794 .clear();
5795 matches
5796 .get_or_init(|| Mutex::new(Vec::new()))
5797 .lock()
5798 .unwrap()
5799 .clear();
5800 let mut dat = Cadata::default();
5801 dat.dummies = -1;
5802 let _ = addmatches(&mut dat, &["a".into(), "b".into()]);
5803 let n = matches.get().unwrap().lock().unwrap().len();
5804 assert!(n >= 2);
5805 }
5806
5807 #[test]
5808 fn add_match_data_returns_populated_cmatch() {
5809 let _g = crate::test_util::global_state_lock();
5810 let _g = zle_test_setup();
5811 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5812 // c:3052-3067: cm.str/orig/pre/suf populated; mnum bumps by 1.
5813 matches
5814 .get_or_init(|| Mutex::new(Vec::new()))
5815 .lock()
5816 .unwrap()
5817 .clear();
5818 let before = mnum.load(Ordering::Relaxed);
5819 let cm = add_match_data(
5820 0,
5821 "match",
5822 "match-orig",
5823 None,
5824 "ipre",
5825 "ripre",
5826 "isuf",
5827 "pre",
5828 "prpre",
5829 "ppre",
5830 None,
5831 "psuf",
5832 None,
5833 "suf",
5834 0,
5835 0,
5836 );
5837 assert_eq!(cm.str.as_deref(), Some("match"));
5838 assert_eq!(cm.orig.as_deref(), Some("match-orig"));
5839 assert_eq!(cm.pre.as_deref(), Some("pre"));
5840 assert_eq!(cm.suf.as_deref(), Some("suf"));
5841 assert_eq!(mnum.load(Ordering::Relaxed), before + 1);
5842 }
5843
5844 #[test]
5845 fn add_match_data_exact_records_into_ainfo() {
5846 let _g = crate::test_util::global_state_lock();
5847 let _g = zle_test_setup();
5848 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5849 // c:3037-3058: exact != 0 writes `ai->exact = useexact` and
5850 // `ai->exactm = cm`. The test sets useexact=1 to exercise the
5851 // accept-exact path.
5852 let saved_useexact = useexact.load(Ordering::Relaxed);
5853 useexact.store(1, Ordering::Relaxed);
5854 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
5855 *g = Some(Aminfo::default());
5856 }
5857 let _ = add_match_data(
5858 0, "x", "x", None, "", "", "", "", "", "", None, "", None, "", 0, 1,
5859 );
5860 let a = ainfo.get().unwrap().lock().unwrap().clone().unwrap();
5861 useexact.store(saved_useexact, Ordering::Relaxed);
5862 assert_eq!(a.exact, 1);
5863 assert!(a.exactm.is_some());
5864 }
5865
5866 /// Faithful end-to-end exercise of `set_comp_sep` (`compset -q`): an
5867 /// ASCII argument `a b c` with the cursor at the end of the middle
5868 /// word must be re-lexed into three words, narrowed to the cursor
5869 /// word `b`, and split into qp/qs ignored prefix/suffix around it.
5870 /// Drives the real lexer (via `global_state_lock`'s `inittyptab`) and
5871 /// asserts the word split, cursor index, compprefix, and the qp/qs
5872 /// reconstruction — covering the marker/offset arithmetic
5873 /// (`swb-1-sqq+dq`, `p[soffs]` chuck) for the byte-consistent case.
5874 #[test]
5875 fn set_comp_sep_splits_ascii_word() {
5876 use crate::ported::zle::complete::{
5877 COMPCURRENT, COMPISUFFIX, COMPQIPREFIX, COMPQISUFFIX, COMPQUOTE, COMPWORDS,
5878 };
5879 let _g = crate::test_util::global_state_lock();
5880 let _g = zle_test_setup();
5881
5882 let setg = |g: &'static OnceLock<Mutex<String>>, v: &str| {
5883 *g.get_or_init(|| Mutex::new(String::new())).lock().unwrap() = v.to_string();
5884 };
5885 let getg = |g: &'static OnceLock<Mutex<String>>| -> String {
5886 g.get_or_init(|| Mutex::new(String::new()))
5887 .lock()
5888 .unwrap()
5889 .clone()
5890 };
5891
5892 // Reconstructed arg "a b c"; cursor between 'b' and the following
5893 // space: compprefix="a b" (active-prefix len 3), compsuffix=" c".
5894 setg(&COMPPREFIX, "a b");
5895 setg(&COMPSUFFIX, " c");
5896 setg(&COMPIPREFIX, "");
5897 setg(&COMPISUFFIX, "");
5898 setg(&COMPQIPREFIX, "");
5899 setg(&COMPQISUFFIX, "");
5900 setg(&COMPQSTACK, ""); // empty => qttype = QT_NONE (unquoted)
5901 setg(&COMPQUOTE, "");
5902 OFFS.store(0, Ordering::Relaxed);
5903 WB.store(0, Ordering::Relaxed);
5904 WE.store(0, Ordering::Relaxed);
5905 ZLEMETACS.store(0, Ordering::Relaxed);
5906 INSTRING.store(0, Ordering::Relaxed);
5907 INBACKT.store(0, Ordering::Relaxed);
5908
5909 // c:1721 — a real split happened (cur >= 0), so ret == 0.
5910 assert_eq!(set_comp_sep(), 0, "compset -q must split the ASCII word");
5911
5912 // c:1926-1934 — three words, cursor word 'b' with its injected
5913 // 'x' chucked back out.
5914 let words = COMPWORDS
5915 .get_or_init(|| Mutex::new(Vec::new()))
5916 .lock()
5917 .unwrap()
5918 .clone();
5919 assert_eq!(
5920 words,
5921 vec!["a".to_string(), "b".to_string(), "c".to_string()],
5922 "arg must re-lex into three words"
5923 );
5924 // c:1930 — 0-offset cur=1 -> 1-based compcurrent=2.
5925 assert_eq!(COMPCURRENT.load(Ordering::Relaxed), 2);
5926
5927 // c:1894-1906 — prefix/suffix of the cursor word (COMPLETEINWORD off).
5928 assert_eq!(getg(&COMPPREFIX), "b");
5929 assert_eq!(getg(&COMPSUFFIX), "");
5930
5931 // c:1912-1919 — qp/qs fold into compqiprefix/compqisuffix. The
5932 // just-prepended 1-char compqstack makes multiquote(...,1) a
5933 // no-op, so the ignored prefix/suffix are verbatim slices of the
5934 // arg; qip + word + qis must reconstruct "a b c".
5935 let recon = format!(
5936 "{}{}{}",
5937 getg(&COMPQIPREFIX),
5938 getg(&COMPPREFIX),
5939 getg(&COMPQISUFFIX)
5940 );
5941 assert_eq!(recon, "a b c", "qip + word + qis must reconstruct the arg");
5942 }
5943
5944 #[test]
5945 fn foredel_deletes_forward_from_zlemetacs() {
5946 let _g = crate::test_util::global_state_lock();
5947 let _g = zle_test_setup();
5948 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5949 // zle_utils.c:1105 — delete `ct` chars forward from ZLEMETACS.
5950 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
5951 *g = "abcdef".to_string();
5952 }
5953 ZLEMETACS.store(2, Ordering::Relaxed);
5954 ZLEMETALL.store(6, Ordering::Relaxed);
5955 foredel(3, CUT_RAW);
5956 let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
5957 assert_eq!(line, "abf");
5958 assert_eq!(ZLEMETALL.load(Ordering::Relaxed), 3);
5959 }
5960
5961 #[test]
5962 fn inststr_inserts_at_zlemetacs() {
5963 let _g = crate::test_util::global_state_lock();
5964 let _g = zle_test_setup();
5965 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5966 // zle_tricky.c:278 — insert at cursor.
5967 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
5968 *g = "hello".to_string();
5969 }
5970 ZLEMETACS.store(5, Ordering::Relaxed);
5971 ZLEMETALL.store(5, Ordering::Relaxed);
5972 let _ = inststr(" world");
5973 let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
5974 assert_eq!(line, "hello world");
5975 assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 11);
5976 }
5977
5978 #[test]
5979 fn metafy_and_unmetafy_roundtrip_globals() {
5980 let _g = crate::test_util::global_state_lock();
5981 let _g = zle_test_setup();
5982 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
5983 // zle_tricky.c:978,995 — meta/unmeta operate on the global pair.
5984 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
5985 *g = "plain ascii".to_string();
5986 }
5987 ZLECS.store(3, Ordering::Relaxed);
5988 ZLELL.store(11, Ordering::Relaxed);
5989 metafy_line();
5990 // For ASCII input the meta form equals the raw form.
5991 assert_eq!(
5992 ZLEMETALINE.get().unwrap().lock().unwrap().clone(),
5993 "plain ascii"
5994 );
5995 assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 3);
5996 unmetafy_line();
5997 assert_eq!(
5998 ZLELINE.get().unwrap().lock().unwrap().clone(),
5999 "plain ascii"
6000 );
6001 }
6002
6003 #[test]
6004 fn selfinsert_appends_lastchar_at_zlecs() {
6005 let _g = crate::test_util::global_state_lock();
6006 let _g = zle_test_setup();
6007 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
6008 // zle_misc.c:112-141 — insert one char at cursor, bump zlecs.
6009 //
6010 // `selfinsert()` (zle_misc.rs:180) writes through
6011 // `self_insert(c)` which mutates the `zle_main::ZLELINE`
6012 // (`Mutex<Vec<char>>`) plus `zle_main::ZLECS`/`ZLELL` —
6013 // NOT the `compcore::ZLELINE` (`OnceLock<Mutex<String>>`)
6014 // used by the meta/unmeta tests above. The original test
6015 // seeded the wrong buffer, so the assert kept seeing "ab"
6016 // (the compcore buffer never received the insert) while
6017 // self_insert silently appended 'c' to the zle_main buffer.
6018 //
6019 // Set up the zle_main buffer for the insert, then read back
6020 // from there. `zle_test_setup` already clears the zle_main
6021 // statics, so we start from a known zero state.
6022 {
6023 let mut g = crate::ported::zle::zle_main::ZLELINE.lock().unwrap();
6024 *g = "ab".chars().collect();
6025 }
6026 crate::ported::zle::zle_main::ZLECS.store(2, Ordering::Relaxed);
6027 crate::ported::zle::zle_main::ZLELL.store(2, Ordering::Relaxed);
6028 LASTCHAR.store(b'c' as i32, Ordering::Relaxed);
6029 // Force the wide-char re-derive path (lastchar_wide_valid=0 →
6030 // selfinsert refills it from LASTCHAR per zle_misc.c:119-122).
6031 LASTCHAR_WIDE_VALID.store(0, Ordering::Relaxed);
6032 let rv = selfinsert(&[]);
6033 assert_eq!(rv, 0);
6034 let buf: String = crate::ported::zle::zle_main::ZLELINE
6035 .lock()
6036 .unwrap()
6037 .iter()
6038 .collect();
6039 assert_eq!(buf, "abc");
6040 assert_eq!(
6041 crate::ported::zle::zle_main::ZLECS.load(Ordering::Relaxed),
6042 3,
6043 );
6044 }
6045
6046 #[test]
6047 fn minfo_clear_and_asked_zero_mutate_state() {
6048 let _g = crate::test_util::global_state_lock();
6049 let _g = zle_test_setup();
6050 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
6051 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
6052 let mut cm = Cmatch::default();
6053 cm.str = Some("x".into());
6054 g.cur = Some(Box::new(cm));
6055 g.asked = 1;
6056 }
6057 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
6058 g.cur = None;
6059 }
6060 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
6061 g.asked = 0;
6062 }
6063 let m = MINFO.get().unwrap().lock().unwrap().clone();
6064 assert!(m.cur.is_none());
6065 assert_eq!(m.asked, 0);
6066 }
6067
6068 #[test]
6069 fn cline_matched_stub_marks_node() {
6070 let _g = crate::test_util::global_state_lock();
6071 let _g = zle_test_setup();
6072 // compmatch.c:253 — sets CLF_MATCHED on the node chain. We
6073 // verify by running through the stub on a non-empty string
6074 // without panicking and trusting compmatch's body for the
6075 // actual flag set.
6076 cline_matched_compcore(Some("foo"));
6077 cline_matched_compcore(None);
6078 cline_matched_compcore(Some(""));
6079 }
6080
6081 #[test]
6082 fn permmatches_returns_fi_zero_when_count_present() {
6083 let _g = crate::test_util::global_state_lock();
6084 let _g = zle_test_setup();
6085 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
6086 // c:3444-3447: if ainfo->count is non-zero, fi stays 0.
6087 amatches
6088 .get_or_init(|| Mutex::new(Vec::new()))
6089 .lock()
6090 .unwrap()
6091 .clear();
6092 pmatches
6093 .get_or_init(|| Mutex::new(Vec::new()))
6094 .lock()
6095 .unwrap()
6096 .clear();
6097 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
6098 *a = Some(Aminfo {
6099 count: 5,
6100 ..Default::default()
6101 });
6102 }
6103 newmatches.store(1, Ordering::Relaxed);
6104 let fi = permmatches(0);
6105 assert_eq!(fi, 0);
6106 assert_eq!(hasperm.load(Ordering::Relaxed), 1);
6107 }
6108
6109 /// c:1323 — `rembslash` removes backslash escapes by walking the
6110 /// string and dropping every `\` while keeping its successor.
6111 /// `\\\\` (literal `\\`) → `\` (single backslash); `\a` → `a`.
6112 /// A regression that drops the successor too would silently strip
6113 /// real chars from `path/to/\$file`.
6114 #[test]
6115 fn rembslash_unescapes_canonical_pairs() {
6116 let _g = crate::test_util::global_state_lock();
6117 assert_eq!(rembslash(r"\a"), "a");
6118 assert_eq!(rembslash(r"\\"), r"\");
6119 assert_eq!(rembslash(r"\$foo"), "$foo");
6120 assert_eq!(rembslash("plain"), "plain");
6121 }
6122
6123 /// c:1323 — empty input → empty output (the loop never runs).
6124 /// Catches a regression that returns " " or "\0" for empty input.
6125 #[test]
6126 fn rembslash_empty_input_returns_empty() {
6127 let _g = crate::test_util::global_state_lock();
6128 assert_eq!(rembslash(""), "");
6129 }
6130
6131 /// c:1323 — trailing lone `\` MUST silently drop (no following
6132 /// char to keep). C's pattern `if (let Some) { push }` is
6133 /// equivalent. Regression that pushes the literal `\` would break
6134 /// shell paths with trailing backslashes (rare but legal).
6135 #[test]
6136 fn rembslash_trailing_lone_backslash_drops_silently() {
6137 let _g = crate::test_util::global_state_lock();
6138 assert_eq!(rembslash(r"foo\"), "foo");
6139 }
6140
6141 /// c:1366 — `ctokenize` is the inverse of `untokenize`: escapes
6142 /// shell metacharacters into their tokenised forms used by the
6143 /// completion machinery. Plain alphanumerics pass through.
6144 #[test]
6145 fn ctokenize_passes_alphanumerics_through() {
6146 let _g = crate::test_util::global_state_lock();
6147 assert_eq!(ctokenize("foo123"), "foo123");
6148 assert_eq!(ctokenize(""), "");
6149 assert_eq!(ctokenize("path/to"), "path/to");
6150 }
6151
6152 /// c:1435 — `comp_quoting_string` returns one of the canonical
6153 /// quote strings: `'`, `"`, `$'`, or "" depending on `stype`.
6154 /// Catches a regression where the dispatch returns the wrong
6155 /// quote — completion would generate `cmd 'arg"` (mismatched).
6156 #[test]
6157 fn comp_quoting_string_dispatches_known_styles() {
6158 let _g = crate::test_util::global_state_lock();
6159 // The exact stype values are private to the completion impl,
6160 // but the function MUST return a non-panicking string for
6161 // every reasonable input. Probe a few values.
6162 for stype in 0..=8 {
6163 let _ = comp_quoting_string(stype);
6164 }
6165 }
6166
6167 /// c:1065 — `multiquote` with empty COMPQSTACK is a no-op (returns
6168 /// input unchanged). The stack is the per-completion quoting
6169 /// context; outside completion it's empty. Regression that quotes
6170 /// regardless would corrupt every non-completion caller.
6171 #[test]
6172 fn multiquote_empty_stack_returns_input_unchanged() {
6173 let _g = crate::test_util::global_state_lock();
6174 // Reset COMPQSTACK to empty.
6175 if let Some(c) = COMPQSTACK.get() {
6176 if let Ok(mut g) = c.lock() {
6177 g.clear();
6178 }
6179 }
6180 assert_eq!(multiquote("hello", 0), "hello");
6181 assert_eq!(multiquote("", 0), "");
6182 }
6183
6184 /// c:1092 — `tildequote("foo")` (no leading ~) MUST behave like
6185 /// multiquote — the tilde-special path is a no-op when there's no
6186 /// `~` to protect. Regression that always strips/restores would
6187 /// silently mangle non-tilde inputs.
6188 #[test]
6189 fn tildequote_non_tilde_input_unchanged() {
6190 let _g = crate::test_util::global_state_lock();
6191 // Empty COMPQSTACK + no ~ → input unchanged.
6192 if let Some(c) = COMPQSTACK.get() {
6193 if let Ok(mut g) = c.lock() {
6194 g.clear();
6195 }
6196 }
6197 assert_eq!(tildequote("foo/bar", 0), "foo/bar");
6198 }
6199
6200 /// c:1092 — empty input through tildequote is empty out.
6201 #[test]
6202 fn tildequote_empty_input_empty_output() {
6203 let _g = crate::test_util::global_state_lock();
6204 if let Some(c) = COMPQSTACK.get() {
6205 if let Ok(mut g) = c.lock() {
6206 g.clear();
6207 }
6208 }
6209 assert_eq!(tildequote("", 0), "");
6210 }
6211
6212 // ─── zsh-corpus pins for rembslash ─────────────────────────────
6213
6214 /// `rembslash("\\a\\b\\c")` strips backslashes → "abc".
6215 #[test]
6216 fn compcore_corpus_rembslash_strips_escapes() {
6217 let _g = crate::test_util::global_state_lock();
6218 assert_eq!(rembslash(r"\a\b\c"), "abc");
6219 }
6220
6221 /// `rembslash` with no backslashes is identity.
6222 #[test]
6223 fn compcore_corpus_rembslash_no_escapes_identity() {
6224 let _g = crate::test_util::global_state_lock();
6225 assert_eq!(rembslash("hello"), "hello");
6226 }
6227
6228 /// `rembslash("")` returns empty string.
6229 #[test]
6230 fn compcore_corpus_rembslash_empty_is_empty() {
6231 let _g = crate::test_util::global_state_lock();
6232 assert_eq!(rembslash(""), "");
6233 }
6234
6235 /// `rembslash("\\\\")` (escaped backslash) returns "\\".
6236 #[test]
6237 fn compcore_corpus_rembslash_escaped_backslash() {
6238 let _g = crate::test_util::global_state_lock();
6239 // r"\\" in Rust is the 2-char string "\\\\" → one literal backslash + one literal backslash
6240 assert_eq!(rembslash(r"\\"), r"\");
6241 }
6242
6243 /// `rembslash` mid-string escape preserves surroundings.
6244 #[test]
6245 fn compcore_corpus_rembslash_preserves_context() {
6246 let _g = crate::test_util::global_state_lock();
6247 assert_eq!(rembslash(r"hello\ world"), "hello world");
6248 }
6249
6250 /// Trailing single backslash is consumed (no char after).
6251 #[test]
6252 fn compcore_corpus_rembslash_trailing_backslash_consumed() {
6253 let _g = crate::test_util::global_state_lock();
6254 assert_eq!(rembslash(r"abc\"), "abc");
6255 }
6256
6257 // ═══════════════════════════════════════════════════════════════════
6258 // Additional C-parity tests for Src/Zle/compcore.c rembslash + remsquote
6259 // + ctokenize + multiquote / tildequote string transforms.
6260 // ═══════════════════════════════════════════════════════════════════
6261
6262 /// c:1323 — `rembslash("abc")` (no backslash) is identity.
6263 #[test]
6264 fn rembslash_no_backslash_is_identity() {
6265 let _g = crate::test_util::global_state_lock();
6266 assert_eq!(rembslash("abc"), "abc");
6267 assert_eq!(rembslash("hello world"), "hello world");
6268 }
6269
6270 /// c:1323 — `rembslash` of single backslash + char drops the backslash.
6271 #[test]
6272 fn rembslash_drops_backslash_keeps_next() {
6273 let _g = crate::test_util::global_state_lock();
6274 assert_eq!(rembslash(r"\a"), "a");
6275 assert_eq!(rembslash(r"\."), ".");
6276 assert_eq!(rembslash(r"\$"), "$");
6277 }
6278
6279 /// c:1323 — repeated escapes: every `\X` collapses to `X`.
6280 #[test]
6281 fn rembslash_multiple_escapes_chain() {
6282 let _g = crate::test_util::global_state_lock();
6283 assert_eq!(rembslash(r"\a\b\c"), "abc");
6284 }
6285
6286 /// c:1343 — `remsquote("")` returns 0 (no chars consumed).
6287 #[test]
6288 fn remsquote_empty_returns_zero() {
6289 let _g = crate::test_util::global_state_lock();
6290 let mut s = String::new();
6291 let r = remsquote(&mut s);
6292 assert_eq!(r, 0);
6293 assert_eq!(s, "");
6294 }
6295
6296 /// c:1343 — `remsquote("abc")` (no quote sequences) is identity,
6297 /// returns 0.
6298 #[test]
6299 fn remsquote_no_quotes_is_identity() {
6300 let _g = crate::test_util::global_state_lock();
6301 let mut s = String::from("hello world");
6302 let r = remsquote(&mut s);
6303 assert_eq!(r, 0, "no quote sequences → 0");
6304 assert_eq!(s, "hello world", "string unchanged");
6305 }
6306
6307 /// c:1366 — `ctokenize("")` returns empty string.
6308 #[test]
6309 fn ctokenize_empty_returns_empty() {
6310 let _g = crate::test_util::global_state_lock();
6311 assert_eq!(ctokenize(""), "");
6312 }
6313
6314 /// c:1366 — `ctokenize("abc")` (no special chars) preserves content.
6315 #[test]
6316 fn ctokenize_plain_ascii_preserved() {
6317 let _g = crate::test_util::global_state_lock();
6318 // No $, {, }, or backslash → byte-for-byte preservation.
6319 let r = ctokenize("abc");
6320 assert_eq!(r.as_bytes()[0], b'a');
6321 assert_eq!(r.as_bytes()[1], b'b');
6322 assert_eq!(r.as_bytes()[2], b'c');
6323 }
6324
6325 /// c:1505 — `comp_quoting_string(0)` returns a static str (no panic).
6326 #[test]
6327 fn comp_quoting_string_returns_static_str_for_all_stypes() {
6328 let _g = crate::test_util::global_state_lock();
6329 for stype in 0..10 {
6330 let _s = comp_quoting_string(stype);
6331 // No panic = pass; returned &'static str is well-defined.
6332 }
6333 }
6334
6335 /// c:954 — `multiquote("")` returns empty.
6336 #[test]
6337 fn multiquote_empty_returns_empty() {
6338 let _g = crate::test_util::global_state_lock();
6339 let _g2 = zle_test_setup();
6340 let r = multiquote("", 0);
6341 assert_eq!(r, "");
6342 }
6343
6344 /// c:980 — `tildequote("")` returns empty.
6345 #[test]
6346 fn tildequote_empty_returns_empty() {
6347 let _g = crate::test_util::global_state_lock();
6348 let _g2 = zle_test_setup();
6349 let r = tildequote("", 0);
6350 assert_eq!(r, "");
6351 }
6352
6353 /// c:980 — `tildequote("plain")` (no tilde) is identity.
6354 #[test]
6355 fn tildequote_no_tilde_is_identity() {
6356 let _g = crate::test_util::global_state_lock();
6357 let _g2 = zle_test_setup();
6358 let r = tildequote("plain", 0);
6359 assert_eq!(r, "plain", "no tilde → input unchanged");
6360 }
6361
6362 // ═══════════════════════════════════════════════════════════════════
6363 // Additional C-parity tests for Src/Zle/compcore.c
6364 // c:936 multiquote / c:972 tildequote / c:1014 check_param /
6365 // c:1315 rembslash / c:1338 remsquote / c:1381 ctokenize /
6366 // c:1478 comp_quoting_string / c:2809 matchcmp / c:2872 matcheq
6367 // ═══════════════════════════════════════════════════════════════════
6368
6369 /// c:1315 — `rembslash("")` empty returns empty.
6370 #[test]
6371 fn rembslash_empty_returns_empty() {
6372 assert_eq!(rembslash(""), "");
6373 }
6374
6375 /// c:1315 — `rembslash` is pure.
6376 #[test]
6377 fn rembslash_is_pure() {
6378 for s in ["", "abc", r"\a", r"\\\\"] {
6379 let first = rembslash(s);
6380 for _ in 0..3 {
6381 assert_eq!(rembslash(s), first, "rembslash({:?}) must be pure", s);
6382 }
6383 }
6384 }
6385
6386 /// c:1381 — `ctokenize` is deterministic.
6387 #[test]
6388 fn ctokenize_is_deterministic() {
6389 for s in ["", "abc", "a*b", "a?b"] {
6390 let first = ctokenize(s);
6391 for _ in 0..3 {
6392 assert_eq!(
6393 ctokenize(s),
6394 first,
6395 "ctokenize({:?}) must be deterministic",
6396 s
6397 );
6398 }
6399 }
6400 }
6401
6402 /// c:1478 — `comp_quoting_string(0)` returns non-empty static.
6403 #[test]
6404 fn comp_quoting_string_zero_returns_static_str() {
6405 let _: &'static str = comp_quoting_string(0);
6406 }
6407
6408 /// c:936 — `multiquote` is pure for arbitrary input.
6409 #[test]
6410 fn multiquote_is_pure() {
6411 let _g = crate::test_util::global_state_lock();
6412 let _g2 = zle_test_setup();
6413 for s in ["", "abc", "x y"] {
6414 let first = multiquote(s, 0);
6415 for _ in 0..3 {
6416 assert_eq!(
6417 multiquote(s, 0),
6418 first,
6419 "multiquote({:?}, 0) must be pure",
6420 s
6421 );
6422 }
6423 }
6424 }
6425
6426 /// c:972 — `tildequote` is pure for non-tilde input.
6427 #[test]
6428 fn tildequote_is_pure() {
6429 let _g = crate::test_util::global_state_lock();
6430 let _g2 = zle_test_setup();
6431 for s in ["", "abc", "no_tilde", "/path/to/file"] {
6432 let first = tildequote(s, 0);
6433 for _ in 0..3 {
6434 assert_eq!(
6435 tildequote(s, 0),
6436 first,
6437 "tildequote({:?}, 0) must be pure",
6438 s
6439 );
6440 }
6441 }
6442 }
6443
6444 /// c:1338 — `remsquote(&mut empty)` returns 0.
6445 #[test]
6446 fn remsquote_empty_returns_zero_pin() {
6447 let mut s = String::new();
6448 let r = remsquote(&mut s);
6449 assert_eq!(r, 0, "empty input → 0");
6450 }
6451
6452 /// c:1014 — `check_param("")` empty returns Option<usize>.
6453 #[test]
6454 fn check_param_empty_returns_option_type() {
6455 let _g = crate::test_util::global_state_lock();
6456 let _g2 = zle_test_setup();
6457 let _: Option<usize> = check_param("", false, false);
6458 }
6459
6460 /// c:1014 — `check_param` is deterministic for empty input.
6461 #[test]
6462 fn check_param_empty_is_deterministic() {
6463 let _g = crate::test_util::global_state_lock();
6464 let _g2 = zle_test_setup();
6465 let first = check_param("", false, false);
6466 for _ in 0..3 {
6467 assert_eq!(check_param("", false, false), first);
6468 }
6469 }
6470
6471 /// c:1439 — `comp_str(false)` returns (String, i32, i32) tuple.
6472 #[test]
6473 fn comp_str_returns_string_i32_i32_tuple() {
6474 let _g = crate::test_util::global_state_lock();
6475 let _g2 = zle_test_setup();
6476 let _: (String, i32, i32) = comp_str(false);
6477 }
6478
6479 // ═══════════════════════════════════════════════════════════════════
6480 // Additional C-parity tests for Src/Zle/compcore.c
6481 // c:1505 set_comp_sep / c:1544 set_list_array / c:1555 get_user_var /
6482 // c:1645 get_data_arr / c:2681 begcmgroup / c:2735 endcmgroup /
6483 // c:2749 addexpl / c:1478 comp_quoting_string
6484 // ═══════════════════════════════════════════════════════════════════
6485
6486 /// c:1505 — `set_comp_sep` returns i32 (compile-time type pin).
6487 #[test]
6488 fn set_comp_sep_returns_i32_type() {
6489 let _g = crate::test_util::global_state_lock();
6490 let _g2 = zle_test_setup();
6491 let _: i32 = set_comp_sep();
6492 }
6493
6494 /// c:1555 — `get_user_var(None)` returns Option<Vec<String>>.
6495 #[test]
6496 fn get_user_var_returns_option_vec_string_type() {
6497 let _g = crate::test_util::global_state_lock();
6498 let _g2 = zle_test_setup();
6499 let _: Option<Vec<String>> = get_user_var(None);
6500 }
6501
6502 /// c:1555 — `get_user_var(None)` is deterministic.
6503 #[test]
6504 fn get_user_var_none_is_deterministic() {
6505 let _g = crate::test_util::global_state_lock();
6506 let _g2 = zle_test_setup();
6507 let first = get_user_var(None);
6508 for _ in 0..3 {
6509 assert_eq!(
6510 get_user_var(None),
6511 first,
6512 "get_user_var(None) must be deterministic"
6513 );
6514 }
6515 }
6516
6517 /// c:1645 — `get_data_arr("", false)` returns Option<Vec<String>>.
6518 #[test]
6519 fn get_data_arr_returns_option_vec_string_type() {
6520 let _g = crate::test_util::global_state_lock();
6521 let _g2 = zle_test_setup();
6522 let _: Option<Vec<String>> = get_data_arr("", false);
6523 }
6524
6525 /// c:1645 — `get_data_arr("", _)` empty name returns None.
6526 #[test]
6527 fn get_data_arr_empty_name_returns_none() {
6528 let _g = crate::test_util::global_state_lock();
6529 let _g2 = zle_test_setup();
6530 assert!(get_data_arr("", false).is_none(), "empty name → None");
6531 assert!(
6532 get_data_arr("", true).is_none(),
6533 "empty name (keys=true) → None"
6534 );
6535 }
6536
6537 /// c:2681 — `begcmgroup(None, 0)` safe.
6538 #[test]
6539 fn begcmgroup_none_no_panic() {
6540 let _g = crate::test_util::global_state_lock();
6541 let _g2 = zle_test_setup();
6542 begcmgroup(None, 0);
6543 }
6544
6545 /// c:2735 — `endcmgroup(None)` safe.
6546 #[test]
6547 fn endcmgroup_none_no_panic() {
6548 let _g = crate::test_util::global_state_lock();
6549 let _g2 = zle_test_setup();
6550 endcmgroup(None);
6551 }
6552
6553 /// c:2681 + c:2735 — beg/end cmgroup round-trip safe.
6554 #[test]
6555 fn begcmgroup_endcmgroup_round_trip_safe() {
6556 let _g = crate::test_util::global_state_lock();
6557 let _g2 = zle_test_setup();
6558 for _ in 0..3 {
6559 begcmgroup(None, 0);
6560 endcmgroup(None);
6561 }
6562 }
6563
6564 /// c:2749 — `addexpl(false)` safe.
6565 #[test]
6566 fn addexpl_false_no_panic() {
6567 let _g = crate::test_util::global_state_lock();
6568 let _g2 = zle_test_setup();
6569 addexpl(false);
6570 }
6571
6572 /// c:1544 — `set_list_array("", &[])` empty inputs is safe.
6573 #[test]
6574 fn set_list_array_empty_no_panic() {
6575 let _g = crate::test_util::global_state_lock();
6576 let _g2 = zle_test_setup();
6577 set_list_array("", &[]);
6578 }
6579
6580 /// c:1478 — `comp_quoting_string(N)` returns &'static str (type pin).
6581 #[test]
6582 fn comp_quoting_string_returns_static_str_type() {
6583 let _: &'static str = comp_quoting_string(0);
6584 }
6585
6586 /// c:1478 — `comp_quoting_string` is pure across stypes.
6587 #[test]
6588 fn comp_quoting_string_pure_across_stypes() {
6589 for stype in 0..10 {
6590 let first = comp_quoting_string(stype);
6591 for _ in 0..3 {
6592 assert_eq!(
6593 comp_quoting_string(stype),
6594 first,
6595 "comp_quoting_string({}) must be pure",
6596 stype
6597 );
6598 }
6599 }
6600 }
6601}