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#![allow(non_snake_case, non_upper_case_globals, dead_code)]
18
19use crate::ported::context::zcontext_restore_partial;
20use crate::ported::module::{gethookdef, runhookdef};
21use crate::ported::params::{getsparam, paramtab, paramtab_hashed_storage, setaparam, setsparam};
22use crate::ported::signals::{queue_signals, unqueue_signals};
23use crate::ported::zle::comp_h::{
24 Aminfo, Brinfo, Cadata, Ccmakedat, Cexpl, Cline, Cmatch, Cmgroup, Cmlist, Menuinfo, CAF_ALL,
25 CAF_ARRAYS, CAF_KEYS, CAF_MATCH, CAF_MATSORT, CAF_NOSORT, CAF_NUMSORT, CAF_QUOTE, CAF_REVSORT,
26 CAF_UNIQALL, CAF_UNIQCON, CGF_MATSORT, CGF_NOSORT, CGF_NUMSORT, CGF_REVSORT, CGF_UNIQALL,
27 CGF_UNIQCON, CMF_DELETE, CMF_DISPLINE, CMF_FMULT, CMF_MULT, CMF_NOLIST, CMF_PACKED, CMF_PARBR,
28 CMF_PARNEST, CMF_ROWS,
29};
30use crate::ported::zle::complete::{
31 COMPIPREFIX, COMPLIST, COMPPREFIX, COMPQSTACK, COMPSUFFIX, INCOMPFUNC,
32};
33use crate::ported::zle::compmatch::{bld_parts, cline_matched};
34use crate::ported::zle::compresult::{do_ambig_menu, ztat};
35use crate::ported::zle::zle_h::{invalidatelist, COMP_LIST_COMPLETE, COMP_LIST_EXPAND, CUT_RAW};
36use crate::ported::zle::zle_refresh::{CLEARLIST, SHOWINGLIST};
37use crate::ported::zle::zle_tricky::{
38 inststr, MENUCMP, ORIGCS, ORIGLINE, USEGLOB, USEMENU, VALIDLIST, WOULDINSTAB,
39};
40use crate::ported::zle::zle_utils::foredel;
41#[allow(unused_imports)]
42use crate::ported::zle::{
43 deltochar::*, textobjects::*, zle_h::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
44 zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
45};
46use crate::ported::zsh_h::{
47 isset, Bnull, Dnull, Equals, Hat, Inbrace, Inbrack, Inpar, Outbrace, Outpar, Pound, Qstring,
48 Quest, Snull, Star, Stringg, Tilde, BASHAUTOLIST, NUMERICGLOBSORT, PM_HASHED, PM_TYPE,
49 QT_BACKSLASH, QT_DOLLARS, QT_DOUBLE, QT_NONE, QT_SINGLE, RCQUOTES, SORTIT_IGNORING_BACKSLASHES,
50 SORTIT_NUMERICALLY, ZCONTEXT_HIST, ZCONTEXT_LEX, ZCONTEXT_PARSE,
51};
52use crate::DPUTS;
53use std::sync::atomic::{AtomicI32, Ordering};
54use std::sync::{Mutex, OnceLock};
55
56// =====================================================================
57// do_completion — `Src/Zle/compcore.c:287`.
58// =====================================================================
59
60/// Direct port of `int do_completion(Hookdef dummy, Compldat dat)`
61/// from compcore.c:287. The top-level completion driver: per-round
62/// state reset → `makecomplist` → dispatch to `do_ambiguous` /
63/// `do_single` / `do_allmatches` per result count.
64pub fn do_completion(s: &str, incmd: i32, lst: i32) -> i32 {
65 // c:287
66
67 // !!! WARNING: RUST-ONLY — NO C COUNTERPART !!!
68 // Native (Rust) plugins that registered completions via
69 // `register_completion` (src/extensions/plugin_host.rs) have their
70 // compsys `compdef` wiring deferred to here — the completion pipeline
71 // is a safe point to eval the glue (compsys itself evals here),
72 // whereas evaling during `zmodload -R` plugin-init hangs the VM.
73 // Idempotent + cheap: no-ops once the pending queue is drained.
74 crate::plugin_host::flush_pending_completions();
75
76 let osl = SHOWINGLIST.load(Ordering::Relaxed); // c:289
77 let mut ret: i32 = 0; // c:289
78
79 // c:296-297 — `ainfo = fainfo = NULL`.
80 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
81 *g = None;
82 }
83 if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
84 *g = None;
85 }
86 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
87 g.clear(); // c:298
88 }
89
90 // c:300-307 — compqstack reset.
91 let instring = INSTRING.load(Ordering::Relaxed); // c:307
92 // c:305 — `compqstack = instring == QT_NONE ? "\\" : <quote-char>`.
93 // Inlined `char_from_qt(x)` as `(x as u8) as char`.
94 let head_q: char = if instring == QT_NONE {
95 // c:305
96 QT_BACKSLASH as u8 as char
97 } else {
98 instring as u8 as char
99 };
100 if let Ok(mut g) = COMPQSTACK.get_or_init(|| Mutex::new(String::new())).lock() {
101 *g = head_q.to_string(); // c:305-306
102 }
103 // !!! RUST-ONLY LINE — NO C COUNTERPART !!!
104 // In C, `$compstate[all_quotes]` has NO storage of its own: its
105 // `compkparams` row is `{ "all_quotes", PM_SCALAR | PM_READONLY, NULL,
106 // GSU(compqstack_gsu) }` (complete.c:1299) and `compqstack_gsu`
107 // (complete.c:1242-1243) routes every read through `get_compqstack`
108 // (complete.c:1479) against the live `compqstack` global — so the
109 // c:305-306 assignment IS the parameter update. zshrs splits the two: a
110 // single-key `${compstate[KEY]}` read comes straight out of
111 // `paramtab_hashed_storage` (`src/ported/subst.rs:7034-7044`), which
112 // special-cases only `nmatches`, so nothing ever published `all_quotes`
113 // and it read EMPTY where zsh gives `\`, `"`, `'` (`_cmdambivalent`
114 // sh:47 and the documented `compquote` idiom both read it). Run the
115 // getter and store its result at each `compqstack` write.
116 set_compstate_str(
117 "all_quotes",
118 &crate::ported::zle::complete::get_compqstack(std::ptr::null_mut()),
119 );
120
121 hasunqu.store(0, Ordering::Relaxed); // c:309
122 let wouldinstab_v = WOULDINSTAB.load(Ordering::Relaxed); // c:310
123 useline.store(
124 // c:310
125 if wouldinstab_v != 0 {
126 -1
127 } else if lst != COMP_LIST_COMPLETE {
128 1
129 } else {
130 0
131 },
132 Ordering::Relaxed,
133 );
134 useexact.store(opt_isset("RECEXACT"), Ordering::Relaxed); // c:311
135 set_compstate_str("exact_string", ""); // c:312
136 let useline_v = useline.load(Ordering::Relaxed);
137 uselist.store(
138 // c:314
139 if useline_v != 0 {
140 if opt_isset("AUTOLIST") != 0 && opt_isset("BASHAUTOLIST") == 0 {
141 if opt_isset("LISTAMBIGUOUS") != 0 {
142 3
143 } else {
144 2
145 }
146 } else {
147 0
148 }
149 } else {
150 1
151 },
152 Ordering::Relaxed,
153 );
154
155 let useglob_v = USEGLOB.load(Ordering::Relaxed); // c:319
156 let opm: String = if useglob_v != 0 {
157 "*".into()
158 } else {
159 "".into()
160 };
161 if let Ok(mut g) = comppatmatch.get_or_init(|| Mutex::new(None)).lock() {
162 *g = Some(opm.clone()); // c:319
163 }
164 // c:320-321 — `zsfree(comppatinsert); comppatinsert = ztrdup("menu");`
165 // `comppatinsert` is a plain module global (`complete.c:69`) that the
166 // `$compstate[pattern_insert]` entry is only a `VAL()` VIEW onto
167 // (`complete.c:1281`), so it OUTLIVES the completion widget's parameter
168 // scope. Writing only the parameter — as this port did — meant
169 // `do_ambiguous`'s GLOB_COMPLETE test (`compresult.c:764`) read it back
170 // absent after `endparamscope()` deleted `$compstate`, so that whole
171 // branch was dead. Keep the global and the parameter in step here, and
172 // re-sync the global from the parameter after the completion function
173 // returns (the c:843-925 unwind below).
174 if let Ok(mut g) = crate::ported::zle::complete::COMPPATINSERT
175 .get_or_init(|| Mutex::new(String::new()))
176 .lock()
177 {
178 *g = "menu".into(); // c:321
179 }
180 set_compstate_str("pattern_insert", "menu"); // c:320
181 forcelist.store(0, Ordering::Relaxed); // c:322
182 haspattern.store(0, Ordering::Relaxed); // c:323
183 // c:324 — complistmax mirrors the LISTMAX parameter for every
184 // completion; asklist reads it to decide when to prompt "do you wish
185 // to see all N possibilities?". Leaving the static at 0 made large
186 // command lists (`l<Tab>`, 230 matches) dump without asking.
187 crate::ported::zle::complete::COMPLISTMAX
188 .store(env_iparam("LISTMAX") as i64, Ordering::Relaxed); // c:324
189
190 // c:325 — `complastprompt = ztrdup(isset(ALWAYSLASTPROMPT) ? "yes" : "")`.
191 // The global outlives `$compstate`, which is gone once the completion
192 // function returns; complistmatches reads it at c:2033.
193 let lastprompt_v = if opt_isset("ALWAYSLASTPROMPT") != 0 { "yes" } else { "" };
194 if let Ok(mut g) = crate::ported::zle::complete::COMPLASTPROMPT
195 .get_or_init(|| Mutex::new(String::new()))
196 .lock()
197 {
198 *g = lastprompt_v.into();
199 }
200 set_compstate_str(
201 // c:326
202 "last_prompt",
203 if opt_isset("ALWAYSLASTPROMPT") != 0 {
204 "yes"
205 } else {
206 ""
207 },
208 );
209 dolastprompt.store(1, Ordering::Relaxed); // c:327
210
211 // c:329-330 — complist string.
212 let cl_str = if opt_isset("LISTROWSFIRST") != 0 {
213 if opt_isset("LISTPACKED") != 0 {
214 "packed rows"
215 } else {
216 "rows"
217 }
218 } else if opt_isset("LISTPACKED") != 0 {
219 "packed"
220 } else {
221 ""
222 };
223 if let Ok(mut g) = COMPLIST.get_or_init(|| Mutex::new(String::new())).lock() {
224 *g = cl_str.into(); // c:329
225 }
226 startauto.store(opt_isset("AUTOMENU"), Ordering::Relaxed); // c:331
227
228 let zlc = ZLEMETACS.load(Ordering::Relaxed);
229 let we_v = WE.load(Ordering::Relaxed);
230 movetoend.store(
231 // c:332
232 if zlc == we_v || opt_isset("ALWAYSTOEND") != 0 {
233 2
234 } else {
235 1
236 },
237 Ordering::Relaxed,
238 );
239 SHOWINGLIST.store(0, Ordering::Relaxed); // c:333
240 hasmatched.store(0, Ordering::Relaxed); // c:334
241 hasunmatched.store(0, Ordering::Relaxed); // c:334
242 minmlen.store(1_000_000, Ordering::Relaxed); // c:335
243 maxmlen.store(-1, Ordering::Relaxed); // c:336
244 // c:337 — `compignored = 0`. This line was absent, so the counter
245 // behind `$compstate[ignored]` (complete.c:41, exported through the
246 // compstate table at complete.c:1300) accumulated across every
247 // completion of the session instead of counting only THIS round's
248 // ignored-pattern rejections. `_ignored` gates its whole body on
249 // `[[ … || $compstate[ignored] -eq 0 ]] && return 1`, so a stale
250 // non-zero made it run on rounds that ignored nothing.
251 crate::ported::zle::complete::COMPIGNORED.store(0, Ordering::Relaxed); // c:337
252 nmessages.store(0, Ordering::Relaxed); // c:338
253 hasallmatch.store(0, Ordering::Relaxed); // c:339
254
255 // c:342 — main dispatch.
256 if makecomplist(s, incmd, lst) != 0 {
257 // c:342
258 // c:344 — error path.
259 ZLEMETACS.store(0, Ordering::Relaxed); // c:344
260 foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:345 — `foredel(zlemetall, CUT_RAW)`
261 let _ = inststr(
262 &ORIGLINE
263 .get_or_init(|| Mutex::new(String::new()))
264 .lock()
265 .map(|g| g.clone())
266 .unwrap_or_default(),
267 ); // c:346 — `inststr(origline)`
268 ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:347
269 CLEARLIST.store(1, Ordering::Relaxed); // c:348
270 ret = 1;
271 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
272 g.cur = None;
273 } // c:350
274 if useline.load(Ordering::Relaxed) < 0 {
275 // c:352
276 unmetafy_line(); // c:354
277 // zshrs bridge, no C counterpart: C has ONE line buffer —
278 // `zleline`/`zlecs`/`zlell` (zle_main.c:43,48) — which `doinsert`
279 // (zle_misc.c:37,51) writes and `metafy_line`/`unmetafy_line`
280 // (zle_tricky.c:978,995) convert, so C's sandwich round-trips
281 // through the same characters. This port splits it into the editor
282 // buffer (`zle_main::ZLELINE`, the Vec<char> `selfinsert` writes —
283 // zle_misc.rs:205) and the completion staging buffer
284 // (`compcore::ZLELINE`, the only one the two conversions read), so
285 // the two copies below hand the unmetafied line to the widget and
286 // take the edited line back. Without them the inserted character
287 // landed in a buffer `metafy_line` never reads and vanished: TAB on
288 // an all-blank line inserted nothing where zsh inserts a literal
289 // TAB (the `wouldinstab` path, zle_tricky.c:183-197 → c:311 →
290 // c:782 → `_main_complete`'s insert-tab early return → c:860).
291 // Same idiom as docomplete's entry bridge (zle_tricky.rs:857).
292 {
293 let comp_line: Vec<char> = ZLELINE
294 .get_or_init(|| Mutex::new(String::new()))
295 .lock()
296 .map(|g| g.chars().collect())
297 .unwrap_or_default();
298 let comp_ll = comp_line.len();
299 let comp_cs = ZLECS.load(Ordering::SeqCst).clamp(0, comp_ll as i32) as usize;
300 if let Ok(mut g) = crate::ported::zle::zle_main::ZLELINE.lock() {
301 *g = comp_line;
302 }
303 crate::ported::zle::zle_main::ZLECS.store(comp_cs, Ordering::SeqCst);
304 crate::ported::zle::zle_main::ZLELL.store(comp_ll, Ordering::SeqCst);
305 }
306 ret = selfinsert(&[]); // c:355
307 {
308 let ed_line: String = crate::ported::zle::zle_main::ZLELINE
309 .lock()
310 .map(|g| g.iter().collect())
311 .unwrap_or_default();
312 let ed_ll = ed_line.chars().count() as i32;
313 let ed_cs = crate::ported::zle::zle_main::ZLECS.load(Ordering::SeqCst) as i32;
314 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
315 *g = ed_line;
316 }
317 ZLECS.store(ed_cs.clamp(0, ed_ll), Ordering::SeqCst);
318 ZLELL.store(ed_ll, Ordering::SeqCst);
319 }
320 metafy_line(); // c:356
321 }
322 return goto_compend(ret); // c:358 goto compend
323 }
324
325 // c:360-362 — clear lastprebr/lastpostbr. Runs AFTER `makecomplist`,
326 // so the values `get_comp_string` recorded are still live for the
327 // c:2999 brace filter in `add_match_data`.
328 lastprebr_set(None); // c:360
329 lastpostbr_set(None); // c:361
330
331 let curpm = comppatmatch
332 .get_or_init(|| Mutex::new(None))
333 .lock()
334 .ok()
335 .and_then(|g| g.clone())
336 .unwrap_or_default();
337 if !curpm.is_empty() && curpm != opm {
338 // c:363
339 haspattern.store(1, Ordering::Relaxed); // c:364
340 }
341 let nm = nmatches.load(Ordering::Relaxed); // c:366
342 let dm = diffmatches.load(Ordering::Relaxed);
343 tracing::debug!(
344 target: "compsys_args",
345 nm,
346 dm,
347 useline = useline.load(Ordering::Relaxed),
348 uselist = uselist.load(Ordering::Relaxed),
349 iforcemenu = iforcemenu.load(Ordering::Relaxed),
350 "do_completion branch point"
351 );
352 if iforcemenu.load(Ordering::Relaxed) != 0 {
353 // c:366
354 if nm != 0 {
355 {
356 let _ = do_ambig_menu();
357 };
358 } // c:367
359 ret = if nm == 0 { 1 } else { 0 }; // c:369
360 } else if useline.load(Ordering::Relaxed) < 0 {
361 // c:370
362 unmetafy_line(); // c:372
363 // zshrs bridge, no C counterpart — same split-line-buffer copies as the
364 // c:352-357 arm above; see the comment there.
365 {
366 let comp_line: Vec<char> = ZLELINE
367 .get_or_init(|| Mutex::new(String::new()))
368 .lock()
369 .map(|g| g.chars().collect())
370 .unwrap_or_default();
371 let comp_ll = comp_line.len();
372 let comp_cs = ZLECS.load(Ordering::SeqCst).clamp(0, comp_ll as i32) as usize;
373 if let Ok(mut g) = crate::ported::zle::zle_main::ZLELINE.lock() {
374 *g = comp_line;
375 }
376 crate::ported::zle::zle_main::ZLECS.store(comp_cs, Ordering::SeqCst);
377 crate::ported::zle::zle_main::ZLELL.store(comp_ll, Ordering::SeqCst);
378 }
379 ret = selfinsert(&[]); // c:373
380 {
381 let ed_line: String = crate::ported::zle::zle_main::ZLELINE
382 .lock()
383 .map(|g| g.iter().collect())
384 .unwrap_or_default();
385 let ed_ll = ed_line.chars().count() as i32;
386 let ed_cs = crate::ported::zle::zle_main::ZLECS.load(Ordering::SeqCst) as i32;
387 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
388 *g = ed_line;
389 }
390 ZLECS.store(ed_cs.clamp(0, ed_ll), Ordering::SeqCst);
391 ZLELL.store(ed_ll, Ordering::SeqCst);
392 }
393 metafy_line(); // c:374
394 } else if useline.load(Ordering::Relaxed) == 0 && uselist.load(Ordering::Relaxed) != 0 {
395 // c:374
396 ZLEMETACS.store(0, Ordering::Relaxed); // c:375
397 foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:376 — `foredel(zlemetall, CUT_RAW)`
398 let _ = inststr(
399 &ORIGLINE
400 .get_or_init(|| Mutex::new(String::new()))
401 .lock()
402 .map(|g| g.clone())
403 .unwrap_or_default(),
404 ); // c:377 — `inststr(origline)`
405 ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:378
406 SHOWINGLIST.store(-2, Ordering::Relaxed);
407 // c:379
408 } else if useline.load(Ordering::Relaxed) == 2 && nm > 1 {
409 // c:380
410 // c:381 — `do_allmatches(1)`. Faithful minfo-driven insertion:
411 // iterates `amatches`, chaining do_single/accept_last against
412 // the shared ZLEMETALINE buffer (see compresult::do_allmatches).
413 crate::ported::zle::compresult::do_allmatches(1);
414 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
415 g.cur = None;
416 } // c:383
417 if forcelist.load(Ordering::Relaxed) != 0 {
418 // c:385
419 SHOWINGLIST.store(-2, Ordering::Relaxed);
420 } else {
421 invalidatelist(); // c:388
422 }
423 } else if useline.load(Ordering::Relaxed) != 0 {
424 // c:389
425 if nm > 1 && dm != 0 {
426 // c:391
427 // c:393 — `ret = do_ambiguous()`. Inlined: flatten `amatches`
428 // into &[String] and dispatch.
429 ret = {
430 let groups = amatches
431 .get_or_init(|| Mutex::new(Vec::new()))
432 .lock()
433 .map(|g| g.clone())
434 .unwrap_or_default();
435 let all: Vec<String> = groups
436 .into_iter()
437 .flat_map(|g| g.matches.into_iter().filter_map(|m| m.str))
438 .collect();
439 crate::ported::zle::compresult::do_ambiguous(&all)
440 };
441 if SHOWINGLIST.load(Ordering::Relaxed) == 0
442 && uselist.load(Ordering::Relaxed) != 0
443 && LISTSHOWN.load(Ordering::Relaxed) != 0
444 && (USEMENU.load(Ordering::Relaxed) == 2 || oldlist.load(Ordering::Relaxed) != 0)
445 {
446 SHOWINGLIST.store(osl, Ordering::Relaxed);
447 // c:395
448 }
449 } else if nm == 1 || (nm > 1 && dm == 0) {
450 // c:396
451 do_single_first_match(); // c:399-411
452 if forcelist.load(Ordering::Relaxed) != 0 {
453 // c:412
454 if uselist.load(Ordering::Relaxed) != 0 {
455 SHOWINGLIST.store(-2, Ordering::Relaxed);
456 } else {
457 CLEARLIST.store(1, Ordering::Relaxed);
458 }
459 } else {
460 invalidatelist(); // c:418
461 }
462 } else if nmessages.load(Ordering::Relaxed) != 0 && forcelist.load(Ordering::Relaxed) != 0 {
463 // c:419
464 if uselist.load(Ordering::Relaxed) != 0 {
465 SHOWINGLIST.store(-2, Ordering::Relaxed);
466 } else {
467 CLEARLIST.store(1, Ordering::Relaxed);
468 }
469 }
470 } else {
471 // c:425
472 invalidatelist(); // c:426
473 LASTAMBIG.store(
474 // c:427
475 opt_isset("BASHAUTOLIST"),
476 Ordering::Relaxed,
477 );
478 if forcelist.load(Ordering::Relaxed) != 0 {
479 CLEARLIST.store(1, Ordering::Relaxed);
480 } // c:428
481 ZLEMETACS.store(0, Ordering::Relaxed); // c:429
482 foredel(ZLEMETALL.load(Ordering::Relaxed), CUT_RAW); // c:430 — `foredel(zlemetall, CUT_RAW)`
483 let _ = inststr(
484 &ORIGLINE
485 .get_or_init(|| Mutex::new(String::new()))
486 .lock()
487 .map(|g| g.clone())
488 .unwrap_or_default(),
489 ); // c:431 — `inststr(origline)`
490 ZLEMETACS.store(ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:432
491 }
492
493 // c:436 — explanation strings.
494 if SHOWINGLIST.load(Ordering::Relaxed) == 0
495 && VALIDLIST.load(Ordering::Relaxed) != 0
496 && USEMENU.load(Ordering::Relaxed) != 2
497 && uselist.load(Ordering::Relaxed) != 0
498 && (nm != 1 || dm != 0)
499 && useline.load(Ordering::Relaxed) >= 0
500 && useline.load(Ordering::Relaxed) != 2
501 && (oldlist.load(Ordering::Relaxed) == 0 || LISTSHOWN.load(Ordering::Relaxed) == 0)
502 {
503 onlyexpl.store(3, Ordering::Relaxed); // c:441
504 SHOWINGLIST.store(-2, Ordering::Relaxed);
505 // c:442
506 }
507
508 goto_compend(ret)
509}
510
511// =====================================================================
512// before_complete / after_complete — `Src/Zle/compcore.c:461 / 503`.
513// =====================================================================
514
515/// Direct port of `int before_complete(Hookdef dummy, int *lst)`
516/// from `Src/Zle/compcore.c:461`. Pre-completion hook: snapshots
517/// `menucmp` into `oldmenucmp`, decides whether the current state
518/// shortcircuits via menu-completion, clamps the cursor when re-
519/// entering an in-word completion, and toggles automenu mode.
520/// Returns 1 to suppress the next-stage match build, 0 to continue.
521pub fn before_complete(lst: &mut i32) -> i32 {
522 // c:461
523
524 // c:463 — `oldmenucmp = menucmp;`
525 OLDMENUCMP.store(MENUCMP.load(Ordering::Relaxed), Ordering::Relaxed);
526
527 // c:465-466 — `if (showagain && validlist) showinglist = -2;`
528 if SHOWAGAIN.load(Ordering::Relaxed) != 0 && VALIDLIST.load(Ordering::Relaxed) != 0 {
529 SHOWINGLIST.store(-2, Ordering::Relaxed);
530 }
531 // c:467 — `showagain = 0;`
532 SHOWAGAIN.store(0, Ordering::Relaxed);
533
534 let has_cur = MINFO
535 .get()
536 .and_then(|m| m.lock().ok())
537 .map(|m| m.cur.is_some())
538 .unwrap_or(false);
539 let menucmp_v = MENUCMP.load(Ordering::Relaxed);
540
541 // c:471-474 — menu-completion shortcircuit (non-listing path).
542 // C: `do_menucmp(*lst); return 1;`. An active menu (minfo.cur set,
543 // menucmp on) means this Tab should step the menu cursor and insert the
544 // next match, NOT restart completion.
545 if has_cur && menucmp_v != 0 && *lst != COMP_LIST_EXPAND {
546 if *lst == COMP_LIST_COMPLETE {
547 // do_menucmp c:1258-1260 — just (re)show the list.
548 SHOWINGLIST.store(-2, Ordering::Relaxed);
549 } else {
550 // do_menucmp c:1263-1268 — `if (zlemetaline == NULL) metafy_line()`.
551 // before_complete runs before docomplete metafies the buffer, so
552 // do_single would otherwise edit a stale/unmetafied line and drop
553 // the command prefix (`cat alpha.txt` → `alpine.md`). Metafy the
554 // completion buffer (still holding the previous match's line) so
555 // do_single replaces only the word region tracked by minfo.
556 if ZLEMETALL.load(Ordering::Relaxed) == 0 {
557 metafy_line();
558 }
559 // do_menucmp c:1270-1276 —
560 // while (zmult) { minfo.cur = valid_match(minfo.cur, 1);
561 // zmult -= sign; }
562 // do_single(*minfo.cur);
563 // valid_match advances the menu cursor one valid match in the
564 // ZMULT direction, updating minfo.group_idx/cur_idx; do_single
565 // then replaces the previously-inserted match (tracked via
566 // minfo.pos/len/end) with the new one.
567 let mult = crate::ported::zle::zle_main::ZMOD
568 .lock()
569 .map(|g| g.mult)
570 .unwrap_or(1);
571 ZMULT.store(mult, Ordering::Relaxed);
572 let steps = mult.abs().max(1);
573 let mut mc = None;
574 for _ in 0..steps {
575 let cur_idx = MINFO
576 .get()
577 .and_then(|m| m.lock().ok())
578 .map(|m| m.cur_idx)
579 .unwrap_or(0);
580 mc = crate::ported::zle::compresult::valid_match(cur_idx, 1);
581 }
582 // c:1272 — minfo.cur = valid_match(...); set before do_single so
583 // the insertion state stays consistent with the advanced cursor.
584 if let Ok(mut mst) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
585 mst.cur = mc.clone().map(Box::new);
586 }
587 if let Some(ref m) = mc {
588 crate::ported::zle::compresult::do_single(m); // c:1276
589 }
590 }
591 return 1; // c:473
592 }
593 // c:475-479 — menu-completion shortcircuit (listing path).
594 if has_cur
595 && menucmp_v != 0
596 && VALIDLIST.load(Ordering::Relaxed) != 0
597 && *lst == COMP_LIST_COMPLETE
598 {
599 SHOWINGLIST.store(-2, Ordering::Relaxed);
600 onlyexpl.store(0, Ordering::Relaxed); // c:477
601 // c:477 — `listdat.valid = 0;`
602 if let Some(ld) = listdat.get() {
603 if let Ok(mut g) = ld.lock() {
604 g.valid = 0;
605 }
606 }
607 return 1; // c:478
608 }
609
610 // c:488-489 — `if ((fromcomp & FC_INWORD) && (zlecs = lastend) > zlell)
611 // zlecs = zlell;` — re-entering an in-word completion
612 // moves the cursor back to `lastend` (clamped to `zlell`).
613 //
614 // c:483-487: this hook "runs before metafication", so C writes the EDITOR
615 // cursor. In this port that is `zle_main::ZLECS`, which docomplete copies
616 // into the completion buffer right after the hook returns
617 // (zle_tricky.rs, the block before `metafy_line()`). C stores the
618 // metafied `lastend` into the unmetafied `zlecs` as is; so does this.
619 // The store used to land on `ZLEMETACS`, which that metafy_line()
620 // recomputes, so the move never happened: after `tst aB<TAB>` left the
621 // cursor at `a|BC`, the next TAB completed `a` + `BC` where zsh
622 // completes `aBC`.
623 if (fromcomp.load(Ordering::Relaxed) & crate::ported::zle::comp_h::FC_INWORD) != 0 {
624 let le = lastend.load(Ordering::Relaxed).max(0) as usize; // c:488 `zlecs = lastend`
625 let ll = crate::ported::zle::zle_main::ZLELL.load(Ordering::SeqCst);
626 crate::ported::zle::zle_main::ZLECS.store(le.min(ll), Ordering::SeqCst); // c:489 `zlecs = zlell`
627 }
628
629 // c:494-496 — automenu trigger.
630 if startauto.load(Ordering::Relaxed) != 0 && LASTAMBIG.load(Ordering::Relaxed) != 0 {
631 let bashauto = isset(BASHAUTOLIST);
632 let last = LASTAMBIG.load(Ordering::Relaxed);
633 if !bashauto || last == 2 {
634 USEMENU.store(2, Ordering::Relaxed);
635 }
636 }
637
638 0 // c:498
639}
640
641/// Direct port of `int after_complete(Hookdef dummy, int *dat)`
642/// from `Src/Zle/compcore.c:503`. Post-completion hook: when a
643/// completion has just transitioned into menu-completion (menucmp
644/// went 0→1 across this round), runs MENUSTARTHOOK so registered
645/// hook ported can veto or modify the about-to-display menu.
646///
647/// Hook handlers are registered via `addhookfunc("menu_start", fn)`
648/// (see `crate::ported::module::addhookfunc`), which writes to the
649/// global HOOKTAB. C's `comphooks[]` table declares `menu_start` as
650/// HOOKF_ALL, so every handler fires and the first non-zero return
651/// short-circuits the chain (see runhookdef at module.c:990).
652///
653/// Return value semantics (c:518-532):
654/// - `ret == 0` → no action (no handler vetoed).
655/// - `ret >= 1` → zero `dat[1]`, clear menucmp/menuacc, null minfo.cur.
656/// - `ret >= 2` → also rewind buffer to origline.
657/// - `ret == 2` → also schedule list clear (CLEARLIST=1, invalidatelist).
658pub fn after_complete(dat: &mut [i32]) -> i32 {
659 // c:503
660 let menucmp_v = MENUCMP.load(Ordering::Relaxed);
661 let oldmenucmp_v = OLDMENUCMP.load(Ordering::Relaxed);
662
663 // c:505 — `if (menucmp && !oldmenucmp) { ... }`.
664 //
665 // `iforcemenu == -1` marks a completion driven from INSIDE
666 // `domenuselect`'s interactive filter loop (complist.rs:2776-2779 —
667 // C sets it at c:2773). C never reaches the restore below on that
668 // path because its `oldmenucmp` is still 1 from the outer menu:
669 // c:517's `runhookdef(MENUSTARTHOOK, …)` runs the whole interactive
670 // loop BEFORE c:520 clears `menucmp`, so every nested
671 // `before_complete` (c:462) snapshots a 1.
672 //
673 // In this port `menucmp` reads 0 at the nested `before_complete`
674 // (measured: menucmp=1 oldmenucmp=0 ifm=-1 at the gate) and
675 // `do_ambig_menu` only re-raises it later, so the gate opened and
676 // the `ret >= 2` arm below ran `foredel` + `inststr(origline)` —
677 // throwing away the match the same completion had just inserted.
678 // That is why `menu select interactive` reported the typed
679 // characters instead of the completion: the status line read the
680 // line AFTER it had been reverted (`interactive: /s[]` where zsh
681 // shows `interactive: /sbin[]`).
682 //
683 // The same `iforcemenu != -1` test is what C uses everywhere else to
684 // mean "not driven by the interactive menu-select widget" (c:763,
685 // c:832, c:1381, c:1437), so state it directly here.
686 if menucmp_v == 0 || oldmenucmp_v != 0 || iforcemenu.load(Ordering::Relaxed) == -1 {
687 return 0; // c:535
688 }
689
690 // c:506-517 — build chdata. cdat.matches=amatches, cdat.num=
691 // nmatches, cdat.nmesg=nmessages, cdat.cur=NULL. The
692 // Rust hook dispatch path doesn't yet thread chdata
693 // into shell-fn args (handlers in the standard zsh
694 // distribution all read directly from compsys globals
695 // via $compstate). The fields above are still tracked
696 // via amatches/nmatches/nmessages globals and visible
697 // to handlers through the normal completion-state
698 // parameter reads.
699
700 // c:518 — `runhookdef(MENUSTARTHOOK, &cdat)`. Canonical dispatch
701 // via `gethookdef("menu_start") + runhookdef(h, &cdat)`. Returns
702 // 0 when no Hookfn is registered (matches c:993-995: empty funcs
703 // and h->def NULL → return 0).
704 let mut ret: i32 = 0;
705 let h_menu_start = gethookdef("menu_start");
706 if !h_menu_start.is_null() {
707 ret = runhookdef(h_menu_start, std::ptr::null_mut());
708 }
709
710 if ret == 0 {
711 return 0; // c:535
712 }
713
714 // c:519 — `dat[1] = 0`. The C caller passes a 2-int array; index 1
715 // carries the menu-acceptance flag for the outer compfunc loop.
716 if dat.len() > 1 {
717 dat[1] = 0;
718 }
719 // c:520 — `menucmp = menuacc = 0`.
720 MENUCMP.store(0, Ordering::Relaxed);
721 menuacc.store(0, Ordering::Relaxed);
722 // c:521 — `minfo.cur = NULL`.
723 if let Some(m) = MINFO.get() {
724 if let Ok(mut mi) = m.lock() {
725 mi.cur = None;
726 }
727 }
728
729 if ret >= 2 {
730 // c:522
731 // c:523 — `fixsuffix()`.
732 fixsuffix();
733 // c:524 — `zlemetacs = 0`.
734 ZLEMETACS.store(0, Ordering::Relaxed);
735 // c:525 — `foredel(zlemetall, CUT_RAW)` removes the entire line.
736 let metall = ZLEMETALL.load(Ordering::Relaxed);
737 foredel(metall, CUT_RAW);
738 // c:526 — `inststr(origline)` reinserts the pre-completion buffer.
739 let origline_v: String = ORIGLINE
740 .get()
741 .and_then(|m| m.lock().ok().map(|g| g.clone()))
742 .unwrap_or_default();
743 let _ = inststr(&origline_v);
744 // c:527 — `zlemetacs = origcs`.
745 let origcs_v = ORIGCS.load(Ordering::Relaxed);
746 ZLEMETACS.store(origcs_v, Ordering::Relaxed);
747
748 if ret == 2 {
749 // c:528
750 // c:529 — `clearlist = 1`.
751 CLEARLIST.store(1, Ordering::Relaxed);
752 // c:530 — `invalidatelist()`.
753 invalidatelist();
754 }
755 }
756
757 0 // c:535
758}
759
760// =====================================================================
761// callcompfunc — `Src/Zle/compcore.c:544`.
762// =====================================================================
763
764/// Port of `static void callcompfunc(char *s, char *fn)` from
765/// compcore.c:544. Selects the `$compstate[context]` value, then
766/// dispatches into the user shell function `fn`. The `comprpms`/
767/// `compkpms` slots are modelled by the `$compstate` hash storage
768/// (a key's PM_UNSET bit is spelled as removing the entry).
769pub fn callcompfunc(s: &str, fn_name: &str) {
770 tracing::debug!(target: "compsys_args", %s, %fn_name, "callcompfunc ENTER");
771 // c:544
772
773 if fn_name.is_empty() {
774 return;
775 } // c:552 getshfunc(NULL)
776 // Re-assert the `$module_path` compiled default at completion entry IF it
777 // has gone empty. `MODULE_PATH` is PM_DONTIMPORT (no env var seeds it), and
778 // its array half is not re-derived when the completion widget scope
779 // re-establishes the tied colon-arrays the way PATH/FPATH are from the
780 // environment — so `$module_path` reads empty inside completers, breaking
781 // every `_files -W module_path` (e.g. `zmodload <tab>`). Only restore when
782 // empty so a user's `module_path+=(…)` customization is preserved.
783 // module_path_init is idempotent (OnceLock-cached MODULE_DIR).
784 if crate::ported::params::getaparam("module_path")
785 .map(|a| a.is_empty())
786 .unwrap_or(true)
787 {
788 crate::ported::init::module_path_init();
789 }
790 let _lv = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed); // c:548 int lv = lastval
791 let _icf = INCOMPFUNC.load(Ordering::Relaxed); // c:555
792 let _osc = crate::ported::builtin::SFCONTEXT.load(Ordering::Relaxed); // c:555
793
794 let _useglob = USEGLOB.load(Ordering::Relaxed); // c:579
795
796 // c:561-563 — `kset = CP_ALLKEYS & ~(CP_PARAMETER | CP_REDIRECT |
797 // CP_QUOTE | CP_QUOTING | CP_EXACTSTR | CP_OLDLIST | CP_OLDINS | …)`,
798 // handed to `comp_setunset(…, kset, ~kset & CP_ALLKEYS)` at c:818.
799 // Every cleared bit raises PM_UNSET on that key's `compkpms` slot
800 // (complete.c:1557-1558), and a PM_UNSET param is skipped by every
801 // hash scan — so `${(@kv)compstate}`, and therefore `_lastcomp`
802 // (`_main_complete` sh:407), carries no entry for it at all.
803 //
804 // zshrs's assoc backing is a flat name→map with no per-key flag bits,
805 // so the equivalent of raising PM_UNSET is removing the entry. The
806 // publishes below used to write "" for these keys instead, which is a
807 // different observable state: present, with an empty value.
808 let kunset = |key: &str| {
809 // c:complete.c:1558 — `(*p)->node.flags |= PM_UNSET`.
810 if let Ok(mut tab) = paramtab_hashed_storage().lock() {
811 if let Some(hash) = tab.get_mut(crate::ported::zle::complete::COMPSTATENAME) {
812 hash.remove(key);
813 }
814 }
815 crate::ported::params::unsetparam(&format!("compstate[{}]", key));
816 };
817 // c:562 — `CP_EXACTSTR` is one of the bits cleared out of `kset`, so
818 // `$compstate[exact_string]` starts the round UNSET; only a later
819 // exact match publishes it (c:3046-3055, mirrored at the
820 // `set_compstate_str("exact_string", …)` site below). do_completion's
821 // c:312 `compexactstr = ""` resets the GLOBAL, not the param's set
822 // bit — the port's matching publish left the key present-and-empty,
823 // so `_lastcomp` carried an `exact_string` entry zsh does not have
824 // whenever the round found no exact match.
825 kunset("exact_string"); // c:562
826
827 // c:667-693 — `compquote` / `compquoting` from the quote state
828 // `get_comp_string` recorded. These ARE `$compstate[quote]` and
829 // `$compstate[quoting]` (complete.c:1276-1277). Neither was ported, so
830 // both read empty for every completion — `_main_complete`'s
831 // `[[ -n $compstate[quote] ]]` branches, `_path_files`'s quoting
832 // decisions and `addmatches`'s own c:2139 quote block all behaved as
833 // if nothing were ever quoted.
834 {
835 use crate::ported::zle::complete::{COMPQUOTE, COMPQUOTING};
836 let instring = INSTRING.load(Ordering::Relaxed);
837 let (cq, cqg): (&str, &str) = if instring > QT_BACKSLASH {
838 // c:669
839 match instring {
840 QT_SINGLE => ("'", "single"), // c:671-674
841 QT_DOUBLE => ("\"", "double"), // c:676-679
842 QT_DOLLARS => ("$'", "dollars"), // c:681-684
843 _ => ("", ""),
844 }
845 } else if INBACKT.load(Ordering::Relaxed) != 0 {
846 ("`", "backtick") // c:687-689
847 } else {
848 ("", "") // c:691-693
849 };
850 for (global, v) in [(&COMPQUOTE, cq), (&COMPQUOTING, cqg)] {
851 if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
852 *g = v.to_string();
853 }
854 }
855 // The `$compstate` entries are gsu VIEWS onto those globals in C;
856 // this port has to publish them explicitly.
857 //
858 // c:561-563 — `CP_QUOTE | CP_QUOTING` start cleared in `kset`;
859 // only the two quoted arms (c:686, c:690) raise them. The
860 // unquoted arm at c:691-693 leaves the globals empty AND the
861 // params unset, so the keys must disappear rather than appear
862 // with an empty value.
863 if cq.is_empty() && cqg.is_empty() {
864 kunset("quote"); // c:562
865 kunset("quoting"); // c:562
866 } else {
867 set_compstate_str("quote", cq); // complete.c:1276, c:686/690
868 set_compstate_str("quoting", cqg); // complete.c:1277, c:686/690
869 }
870 }
871
872 // Publish the completion word split at the cursor into the
873 // `$PREFIX` / `$SUFFIX` params (+ empty ignored-prefix/suffix). In C
874 // these are gsu-bound to `compprefix`/`compsuffix`; the Rust ports
875 // have no gsu binding, so without this every completer reads
876 // `$PREFIX=''` — `_main_complete`'s `compset -P 1 '='` then matches
877 // the empty prefix and wrongly forces `$compstate[context]=equal`,
878 // and `_path_files` has no prefix to glob. The word is split the way
879 // c:699-718 splits it — whole-word under `unset(COMPLETEINWORD)`, else
880 // at `OFFS` (zlemetacs - wb), the cursor offset within the word.
881 {
882 // c:699-718 — the compprefix/compsuffix split. C branches on
883 // `unset(COMPLETEINWORD)` FIRST: with the option OFF (the default)
884 // the WHOLE word is the prefix and the suffix is EMPTY — completion
885 // runs from the end of the word whatever column the cursor sits in;
886 // only with completeinword SET is the word split at `offs`:
887 // if (unset(COMPLETEINWORD)) {
888 // tmp = (linwhat == IN_MATH ? dupstring(s) : multiquote(s, 0));
889 // untokenize(tmp);
890 // compprefix = ztrdup(tmp);
891 // compsuffix = ztrdup("");
892 // } else { … split at s + offs … }
893 // The port applied the split unconditionally, i.e. completeinword
894 // semantics for everyone, so a mid-word TAB completed against the
895 // text left of the cursor instead of the whole word. The sibling
896 // site (`set_comp_sep`, c:1892-1906) already carries this branch —
897 // compcore.rs:2861-2868.
898 //
899 // c:700/711/715 wrap the word in `multiquote(s, 0)` before publishing
900 // it, and c:701/712/716 `untokenize` the result. That pair is NOT a
901 // no-op: `do_completion` seeds `$compqstack` with one element on every
902 // completion (c:301-308, compcore.rs:100-112) — the quote the word sits
903 // inside, or `QT_BACKSLASH` when it sits inside none — so every
904 // character the user QUOTED gets its quoting put back before the
905 // completion function sees `$PREFIX`.
906 //
907 // It only comes out right on a TOKENIZED word: `quotestring` passes a
908 // parser token straight through (utils.c:6392), so a live glob `*` (the
909 // token `Star`) stays bare while a `*` the user escaped — a plain `*`
910 // in the word by then — gets its backslash back. Handing it the
911 // UNTOKENIZED word instead escapes the LIVE metacharacters and
912 // publishes `\*` for `ls *`, `\*\(` for `ls *(`, `\$\{\(` for
913 // `echo ${(`.
914 //
915 // This port untokenizes at `get_comp_string`'s return, so the tokenized
916 // twin travels separately in `crate::comp_word_tok` (published by
917 // `makecomplist`, narrowed the same way `s` is). Every accessor there
918 // is fallible: `None` means no usable twin and the untokenized word is
919 // published as before.
920 //
921 // Without this, `echo "$PATH <TAB>` published `$PATH ` where zsh
922 // publishes `$PATH\ `. The word holds a real token (the `$`), so
923 // `get_comp_string` leaves `instring` at `QT_NONE`
924 // (zle_tricky.c:1729-1731 — `has_real_token(s + 1)` fails the
925 // quote-form test), the qstack head is therefore `QT_BACKSLASH`, and
926 // the space inside the unterminated `"` is a literal that has to be
927 // re-quoted with a backslash. `_expand` rebuilt its expansion from the
928 // unescaped word and produced none, so the second TAB listed
929 // "No Matches" where zsh lists the expansions.
930 let word_tok = crate::comp_word_tok::get();
931 let in_math = linwhat.load(Ordering::Relaxed) == IN_MATH_LW;
932 // c:698 — `makebangspecial(0)`. Clears ISPECIAL on `bangchar` for the
933 // duration of the `multiquote` calls below, restored at c:719. The
934 // completion word is not history-expanded, so a `!` in it must not be
935 // re-quoted as `\!` when it is published into `$PREFIX`/`$SUFFIX` —
936 // even though the SAME `quotestring` must escape it everywhere else in
937 // an interactive shell.
938 //
939 // This is a no-op unless something actually reads the bit;
940 // `quotestring`'s `ispecial` only started doing so in a103e66b60,
941 // which is why wiring it now matters and did not before.
942 crate::ported::utils::makebangspecial(false);
943 let (pre, suf) = if !isset(crate::ported::zsh_h::COMPLETEINWORD) {
944 // c:699
945 // c:700-703 — `tmp = (linwhat == IN_MATH ? dupstring(s)
946 // : multiquote(s, 0));`
947 let pre = if in_math {
948 s.to_string()
949 } else {
950 // The twin is usable only when it IS `s` tokenized. The
951 // completeinword arm below gets that check from `span`; this
952 // arm took the twin unchecked, so a twin left by an earlier
953 // completion published that completion's word as `$PREFIX`.
954 let whole = word_tok
955 .as_deref()
956 .filter(|t| crate::ported::lex::untokenize(t) == s);
957 crate::comp_word_tok::multiquoted(whole)
958 .unwrap_or_else(|| s.to_string())
959 };
960 (pre, String::new())
961 } else {
962 // c:704
963 let scs: Vec<char> = s.chars().collect();
964 let off = (OFFS.load(Ordering::Relaxed).max(0) as usize).min(scs.len()); // c:707
965 let hd: String = scs[..off].iter().collect(); // c:709-713
966 let tl: String = scs[off..].iter().collect(); // c:714-717
967 if in_math {
968 // c:711 `dupstring_wlen(s, offs)` / c:715 `dupstring(ss)`.
969 (hd, tl)
970 } else {
971 // c:711/715 — the same split, each half through `multiquote`.
972 let (htok, ttok) = match word_tok.as_deref() {
973 Some(t) => (
974 crate::comp_word_tok::span(t, 0, hd.len()),
975 crate::comp_word_tok::span(t, hd.len(), tl.len()),
976 ),
977 None => (None, None),
978 };
979 (
980 crate::comp_word_tok::multiquoted(htok.as_deref()).unwrap_or(hd),
981 crate::comp_word_tok::multiquoted(ttok.as_deref()).unwrap_or(tl),
982 )
983 }
984 };
985 // c:719 — `makebangspecial(1)`. Restores the bit, but only if
986 // `inittyptab` had stored `ZTF_BANGCHAR` (utils.c:4291); in a
987 // non-interactive shell it correctly stays clear.
988 crate::ported::utils::makebangspecial(true);
989 let _ = crate::ported::params::setsparam("PREFIX", &pre);
990 let _ = crate::ported::params::setsparam("SUFFIX", &suf);
991 // c:724-741 — `$IPREFIX` / `$ISUFFIX`.
992 //
993 // zsfree(compiprefix); zsfree(compisuffix);
994 // if (parwb < 0) { compiprefix = ztrdup(""); compisuffix = ztrdup(""); }
995 // else {
996 // compiprefix = zalloc((l = wb - parwb) + 1);
997 // memcpy(compiprefix, zlemetaline + parwb, l);
998 // compisuffix = zalloc((l = parwe - we) + 1);
999 // memcpy(compisuffix, zlemetaline + we, l);
1000 // wb = parwb; we = parwe; offs = paroffs;
1001 // }
1002 //
1003 // `makecomplist` (c:952-957) parks the word's ORIGINAL boundaries in
1004 // `parwb`/`parwe`/`paroffs` before `check_param` narrows `wb`/`we`/
1005 // `offs` down to the parameter NAME, so the two spans differ by
1006 // exactly the sigil (`$`, `${`, `${(flags)`) and by the `}` plus any
1007 // modifiers on the other side. That difference IS `$IPREFIX` /
1008 // `$ISUFFIX`.
1009 //
1010 // Both were hardcoded to the empty string here, which dropped the
1011 // whole block: `echo $PATH<TAB>` published `IPREFIX=''` where zsh
1012 // publishes `IPREFIX='$'`. Anything that rebuilds the word from
1013 // `$IPREFIX$PREFIX$SUFFIX$ISUFFIX` then lost the sigil — `_expand`
1014 // (Completion/Base/Completer/_expand:22) saw the word as `PATH`, its
1015 // substitution step produced `PATH` again, and sh:128's
1016 // "expansion equals the word" test returned 1, so the completer
1017 // emitted no expansions at all.
1018 let (ipre_v, isuf_v) = if PARWB.load(Ordering::Relaxed) < 0 {
1019 (String::new(), String::new()) // c:727-728
1020 } else {
1021 let line = ZLEMETALINE
1022 .get_or_init(|| Mutex::new(String::new()))
1023 .lock()
1024 .map(|g| g.clone())
1025 .unwrap_or_default();
1026 let parwb = PARWB.load(Ordering::Relaxed).max(0) as usize;
1027 let parwe = PARWE.load(Ordering::Relaxed).max(0) as usize;
1028 let wb = WB.load(Ordering::Relaxed).max(0) as usize;
1029 let we = WE.load(Ordering::Relaxed).max(0) as usize;
1030 // `get` returns None on an out-of-range or non-char-boundary
1031 // span; C's memcpy cannot fail because it copies bytes out of a
1032 // buffer it already sized, so the fallback here is the empty
1033 // string — the same value the `parwb < 0` arm uses.
1034 let ip = line.get(parwb..wb).unwrap_or("").to_string(); // c:732-734
1035 let is = line.get(we..parwe).unwrap_or("").to_string(); // c:735-737
1036 WB.store(PARWB.load(Ordering::Relaxed), Ordering::Relaxed); // c:739
1037 WE.store(PARWE.load(Ordering::Relaxed), Ordering::Relaxed); // c:740
1038 OFFS.store(PAROFFS.load(Ordering::Relaxed), Ordering::Relaxed); // c:741
1039 (ip, is)
1040 };
1041 let _ = crate::ported::params::setsparam("IPREFIX", &ipre_v);
1042 let _ = crate::ported::params::setsparam("ISUFFIX", &isuf_v);
1043 // c:742-745 — `compqiprefix = ztrdup(qipre ? qipre : "");
1044 // compqisuffix = ztrdup(qisuf ? qisuf : "");`
1045 // `compqiprefix`/`compqisuffix` ARE `$QIPREFIX`/`$QISUFFIX`
1046 // (complete.c:1266-1267), and `qipre`/`qisuf` are what
1047 // `get_comp_string` (zle_tricky.c:1753-1766) filled in with the
1048 // opening/closing quote of the word being completed. The port
1049 // hardcoded both to "" here, so `$QIPREFIX` was permanently empty:
1050 // completing inside `"…"` / `'…'` / `$'…'` dropped the opening
1051 // quote off the command line and every `$QIPREFIX`-testing
1052 // completer took its unquoted branch.
1053 crate::vm_helper::set_readonly_special(
1054 "QIPREFIX",
1055 &crate::ported::zle::zle_tricky::qipre_get(),
1056 ); // c:743
1057 crate::vm_helper::set_readonly_special(
1058 "QISUFFIX",
1059 &crate::ported::zle::zle_tricky::qisuf_get(),
1060 ); // c:745
1061 // c:complete.c:1235-1295 — in C these params ARE `compprefix`/
1062 // `compsuffix`/`compiprefix`/`compisuffix` (gsu-bound, one
1063 // storage), so the publish above resets the globals too. The Rust
1064 // compparams have no gsu binding, so the globals kept the PREVIOUS
1065 // call's values — and `expand-or-complete` calls this twice per
1066 // TAB. Mirror the reset onto the globals. (Same block as
1067 // addmatches below and bin_compfiles -p/-P in computil.rs.)
1068 for (param, global) in [
1069 ("PREFIX", &COMPPREFIX),
1070 ("SUFFIX", &COMPSUFFIX),
1071 ("IPREFIX", &COMPIPREFIX),
1072 ("ISUFFIX", &crate::ported::zle::complete::COMPISUFFIX),
1073 ] {
1074 if let Some(v) = crate::ported::params::getsparam(param) {
1075 if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
1076 *g = v;
1077 }
1078 }
1079 }
1080
1081 // c:720-723 — `zsfree(complastprefix); zsfree(complastsuffix);
1082 // complastprefix = ztrdup(compprefix);
1083 // complastsuffix = ztrdup(compsuffix);`.
1084 // Assigned from the compprefix/compsuffix GLOBALS the block above
1085 // just published — which is literally what C copies — rather than
1086 // from a second, locally recomputed split that can only agree with
1087 // them by accident. Taken here (before the completion fn runs and
1088 // before any `compset -P/-S` strips $PREFIX/$SUFFIX) so
1089 // `domenuselect`'s interactive status line renders the search
1090 // buffer as `interactive: <prefix>[]<suffix>` (setmstatus,
1091 // complist.c:2234-2235).
1092 for (src, dst) in [
1093 (&COMPPREFIX, &crate::ported::zle::complete::COMPLASTPREFIX),
1094 (&COMPSUFFIX, &crate::ported::zle::complete::COMPLASTSUFFIX),
1095 ] {
1096 let v = src
1097 .get_or_init(|| Mutex::new(String::new()))
1098 .lock()
1099 .map(|g| g.clone())
1100 .unwrap_or_default();
1101 if let Ok(mut g) = dst.get_or_init(|| Mutex::new(String::new())).lock() {
1102 *g = v; // c:722-723
1103 }
1104 }
1105
1106 // c:743-751 — `compqiprefix = qipre; compqisuffix = qisuf;
1107 // origlpre = strlen(compqiprefix)+strlen(compiprefix)+strlen(compprefix);
1108 // origlsuf = strlen(compqisuffix)+strlen(compisuffix)+strlen(compsuffix);
1109 // lenchanged = 0;`
1110 // `origlpre`/`origlsuf` record how long the prefix/suffix were when the
1111 // completion widget was entered; `addmatches` (c:2252-2254) compares the
1112 // CURRENT lengths against them and sets `lenchanged` when a completer has
1113 // moved the PREFIX/SUFFIX split (`compset -P/-S`, `_approximate`,
1114 // `_prefix`). `do_ambiguous` (compresult.c:794) reads that flag: with the
1115 // split moved it must NOT put the old word back when the unambiguous
1116 // string comes out short. All three were declared but never assigned, so
1117 // the flag was permanently 0 and `ls **/<TAB><TAB>s` restored `**/s` where
1118 // zsh leaves the word deleted (`interactive: []`).
1119 //
1120 // `compqiprefix`/`compqisuffix` ARE `$QIPREFIX`/`$QISUFFIX` in C
1121 // (gsu-bound, complete.c) — read back the values the publish above set
1122 // rather than a second, independently derived copy.
1123 {
1124 use crate::ported::zle::complete::{COMPISUFFIX, COMPQIPREFIX, COMPQISUFFIX};
1125 let qip = crate::ported::params::getsparam("QIPREFIX").unwrap_or_default(); // c:744
1126 let qis = crate::ported::params::getsparam("QISUFFIX").unwrap_or_default(); // c:746
1127 for (global, v) in [(&COMPQIPREFIX, &qip), (&COMPQISUFFIX, &qis)] {
1128 if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
1129 *g = v.clone();
1130 }
1131 }
1132 let glen = |g: &std::sync::OnceLock<Mutex<String>>| -> usize {
1133 g.get_or_init(|| Mutex::new(String::new()))
1134 .lock()
1135 .map(|s| s.len())
1136 .unwrap_or(0)
1137 };
1138 origlpre.store(
1139 (qip.len() + glen(&COMPIPREFIX) + glen(&COMPPREFIX)) as i32,
1140 Ordering::Relaxed,
1141 ); // c:747-748
1142 origlsuf.store(
1143 (qis.len() + glen(&COMPISUFFIX) + glen(&COMPSUFFIX)) as i32,
1144 Ordering::Relaxed,
1145 ); // c:749-750
1146 lenchanged.store(0, Ordering::Relaxed); // c:751
1147 }
1148 }
1149
1150 // c:591-617 — context selection.
1151 let context = compcontext_for(s); // c:591-617
1152 tracing::debug!(
1153 target: "compsys_args",
1154 %context,
1155 linwhat = linwhat.load(Ordering::Relaxed),
1156 ispar = ispar.load(Ordering::Relaxed),
1157 "callcompfunc context"
1158 );
1159 set_compstate_str("context", &context); // c:619
1160
1161 // c:577 — `compparameter = compredirect = ""`, then c:586 (subscript),
1162 // c:607 (IN_ENV value / array_value) overwrite it with `varname`, the
1163 // parameter name `get_comp_string` split off the line. This publish was
1164 // missing entirely, so `$compstate[parameter]` kept whatever a previous
1165 // completion left: `_value` dispatched `-value-,,-default-` instead of
1166 // `-value-,PATH,-default-` and never reached the per-parameter completer.
1167 let varname = || {
1168 crate::ported::zle::zle_tricky::VARNAME
1169 .get_or_init(|| Mutex::new(None))
1170 .lock()
1171 .ok()
1172 .and_then(|g| g.clone())
1173 .unwrap_or_default()
1174 };
1175 let compparameter = match context.as_str() {
1176 "subscript" if linwhat.load(Ordering::Relaxed) == IN_MATH_LW => varname(), // c:585-588
1177 "value" | "array_value" if linwhat.load(Ordering::Relaxed) == IN_ENV_LW => varname(), // c:607
1178 // c:628-629 — the default (no-command-word) `value` arm names
1179 // the parameter from the FIRST word on the line, not `varname`.
1180 "value" => crate::ported::zle::zle_tricky::CLWORDS
1181 .lock()
1182 .ok()
1183 .and_then(|g| g.first().cloned())
1184 .unwrap_or_default(),
1185 _ => String::new(), // c:577
1186 };
1187 // c:561-563 — `kset = CP_ALLKEYS & ~(CP_PARAMETER | …)`: the key
1188 // starts UNSET and only c:586 / c:594 / c:607 / c:626 raise
1189 // `kset |= CP_PARAMETER`, which is exactly the set of arms that give
1190 // `compparameter` a name. Publishing "" instead left the key present
1191 // in `${(@kv)compstate}` (and so in `_lastcomp`) where zsh has no
1192 // entry at all.
1193 if compparameter.is_empty() {
1194 kunset("parameter"); // c:562
1195 } else {
1196 set_compstate_str("parameter", &compparameter);
1197 }
1198
1199 // c:598-602 — `compcontext = "redirect"; if (rdstr) compredirect =
1200 // rdstr;`. `compredirect` is `$compstate[redirect]` (complete.c:1265)
1201 // and was never written by this port, so `_redirect` had nothing to
1202 // dispatch on and `_expand`'s multios branch (sh:236) saw an empty
1203 // operator. Reset to "" (c:577) in every other context.
1204 let compredirect = if context == "redirect" {
1205 crate::ported::zle::zle_tricky::RDSTR
1206 .lock()
1207 .ok()
1208 .and_then(|g| g.clone())
1209 .unwrap_or_default() // c:600-601
1210 } else {
1211 String::new() // c:577
1212 };
1213 // c:561-563 / c:601 — `CP_REDIRECT` likewise starts cleared and is
1214 // raised only by the `redirect` context arm.
1215 if compredirect.is_empty() {
1216 kunset("redirect"); // c:562
1217 } else {
1218 set_compstate_str("redirect", &compredirect); // c:601
1219 }
1220 // C binds `compredirect` to `$compstate[redirect]` through one gsu
1221 // storage; this port keeps the global and the param separate, so
1222 // mirror the write (same pattern as PREFIX/COMPPREFIX above).
1223 if let Ok(mut g) = crate::ported::zle::complete::COMPREDIRECT
1224 .get_or_init(|| Mutex::new(String::new()))
1225 .lock()
1226 {
1227 *g = compredirect;
1228 }
1229
1230 // c:648-653 — `compredirs = zlinklist2array(rdstrs, 1)`, published as
1231 // the `redirections` real-param (complete.c:1250). One entry per
1232 // COMPLETED redirection on the line, each `<op>:<target>`.
1233 setaparam(
1234 "redirections",
1235 crate::ported::zle::zle_tricky::RDSTRS
1236 .lock()
1237 .map(|g| g.clone())
1238 .unwrap_or_default(),
1239 );
1240
1241 // c:634-645 — `if (compwords) freearray(compwords); if (usea && …)
1242 // { compwords = copy of clwords } else compwords = empty`. C rebuilds
1243 // `$words` from the parsed line on EVERY call, which is what makes the
1244 // SECOND completion pass of `expand-or-complete` (zle_tricky.c:851)
1245 // see the full command line again — `get_comp_string` runs only once
1246 // per TAB. This port was missing the rebuild, so a first pass that
1247 // restricted `$words` (any `_arguments` spec with a `*::`/`*:::` rest
1248 // argument calls `comparguments -W` → restrict_range) left the second
1249 // pass with an empty word array: no command word, no completer
1250 // dispatch, and every match from the first pass discarded.
1251 //
1252 // `usea` (c:590) is 0 only in the math context; C's `aadd` sub-case
1253 // (parameter-subscript, c:626-630) needs `varname`, which this port
1254 // does not compute yet — it is treated as 0 here, exactly as before.
1255 {
1256 use crate::ported::zle::complete::{COMPCURRENT, COMPWORDS};
1257 let usea = linwhat.load(Ordering::Relaxed) != IN_MATH_LW;
1258 let ws: Vec<String> = if usea {
1259 crate::ported::zle::zle_tricky::CLWORDS
1260 .lock()
1261 .map(|g| g.clone())
1262 .unwrap_or_default() // c:639-643
1263 } else {
1264 Vec::new() // c:645
1265 };
1266 let n = ws.len() as i32;
1267 // c:751 — `compcurrent = (usea ? (clwpos + 1 - aadd) : 0)`. Like
1268 // `compwords`, this is RECOMPUTED per call, never carried over: the
1269 // first pass' `comparguments -W` shifts it down to the restricted
1270 // range, and reusing that value left the second pass pointing at
1271 // the command word. `clwpos < 0` means the cursor sits past the
1272 // last word (fresh trailing word) — same guard as the publish site
1273 // in get_comp_string.
1274 let clwpos = crate::ported::zle::zle_tricky::CLWPOS.load(Ordering::Relaxed);
1275 let cur = if !usea {
1276 0 // c:751 — math context: no words, no current
1277 } else if clwpos < 0 {
1278 n + 1
1279 } else {
1280 (clwpos + 1).max(1)
1281 };
1282 if let Ok(mut g) = COMPWORDS.get_or_init(|| Mutex::new(Vec::new())).lock() {
1283 *g = ws.clone();
1284 }
1285 COMPCURRENT.store(cur, Ordering::Relaxed);
1286 // zshrs bridge: `$words`/`$CURRENT` are plain paramtab copies here
1287 // (C binds them to the globals via gsu), so the rebuild has to
1288 // reach the params too — see get_comp_string's publish site.
1289 setaparam("words", ws);
1290 let _ = crate::ported::params::setiparam("CURRENT", cur as i64);
1291 }
1292
1293 // c:571-572 —
1294 // ```c
1295 // if (!*complastprompt)
1296 // kset &= ~CP_LASTPROMPT;
1297 // ```
1298 // C only READS `complastprompt` here (to drop CP_LASTPROMPT from the
1299 // "keys the completion function may set" mask); it never writes it.
1300 // The single writer is do_completion at c:325,
1301 // `complastprompt = ztrdup(isset(ALWAYSLASTPROMPT) ? "yes" : "")`,
1302 // ported at compcore.rs:200-208.
1303 //
1304 // This site used to WRITE `$compstate[last_prompt]` back from
1305 // `dolastprompt` (which do_completion has just set to 1 at c:326),
1306 // which stomped the "" that NO_ALWAYS_LAST_PROMPT had just stored.
1307 // addmatch's `if (!complastprompt || !*complastprompt) dolastprompt = 0`
1308 // (c:3014-3015) then never fired, so `dolastprompt` stayed 1,
1309 // `clearflag` came out 1 in asklist (c:1925) / compprintlist (c:2061),
1310 // and zrefresh's reset frame took the `if (clearflag)` branch at
1311 // c:1168-1172 (`\r` + `moveto(0, lpromptw)`) instead of the
1312 // `!clearflag` branch at c:1146-1167 (TCCLEAREOD + `zputs(lpromptbuf)`)
1313 // — so after a completion listing the prompt was never repainted.
1314 // `kset` is not materialised in this port (it is only used
1315 // descriptively, see c:561-563 above), so the C statement has no
1316 // representable effect beyond the read.
1317 let _complastprompt_isset = !get_compstate_str("last_prompt")
1318 .unwrap_or_default()
1319 .is_empty(); // c:571
1320
1321 // c:753-765 — `$compstate[list]` is REBUILT here from `uselist`, it is
1322 // not the value do_completion left in `complist` at c:327-330:
1323 //
1324 // switch (uselist) { case 0: ""; 1: "list"; 2: "autolist";
1325 // 3: "ambiguous"; }
1326 // if (isset(LISTPACKED)) complist = dyncat(complist, " packed");
1327 // if (isset(LISTROWSFIRST)) complist = dyncat(complist, " rows");
1328 //
1329 // The port published only the do_completion half ("packed"/"rows"), so
1330 // the leading state word was never there: `_main_complete` and friends
1331 // test `$compstate[list]` for `list`/`autolist`/`ambiguous` and always
1332 // read them as absent. Write the rebuilt value to BOTH the param and the
1333 // `complist` global, which is one storage in C (gsu-bound) and is what
1334 // `addmatch` reads at c:2048-2050 for CMF_PACKED/CMF_ROWS.
1335 let mut cl_value = match uselist.load(Ordering::Relaxed) {
1336 0 => String::new(), // c:755
1337 1 => "list".to_string(), // c:756
1338 2 => "autolist".to_string(), // c:757
1339 3 => "ambiguous".to_string(), // c:758
1340 _ => String::new(),
1341 };
1342 if opt_isset("LISTPACKED") != 0 {
1343 cl_value.push_str(" packed"); // c:761
1344 }
1345 if opt_isset("LISTROWSFIRST") != 0 {
1346 cl_value.push_str(" rows"); // c:763
1347 }
1348 if let Ok(mut g) = COMPLIST.get_or_init(|| Mutex::new(String::new())).lock() {
1349 *g = cl_value.clone(); // c:765
1350 }
1351 set_compstate_str("list", &cl_value); // c:765
1352
1353 // c:767-782 — `$compstate[insert]` per (useline, usemenu).
1354 let ul = useline.load(Ordering::Relaxed);
1355 let um = USEMENU.load(Ordering::Relaxed);
1356 let ins = if ul != 0 {
1357 // c:768-776
1358 match um {
1359 // c:769-772 — `compinsert = (isset(AUTOMENU) ?
1360 // "automenu-unambiguous" : "unambiguous");`
1361 //
1362 // AUTO_MENU is on in every emulation by default (options.c:90
1363 // lists it as `OPT_ALL`), so this arm — the one taken by an
1364 // ordinary first TAB — normally yields "automenu-unambiguous",
1365 // NOT the bare "unambiguous" this port hardcoded.
1366 //
1367 // The distinction is not cosmetic: it is the only way the shell
1368 // function layer learns that the next TAB is allowed to start
1369 // menu completion. Completion/Base/Core/_main_complete:302 gates
1370 // the whole MENUSELECT/MENUMODE block on
1371 // `[[ "$compstate[insert]" = *menu* ]]`, which
1372 // "automenu-unambiguous" satisfies and "unambiguous" does not
1373 // (ported at _main_complete.rs:929-931), and
1374 // Base/Completer/_match:53 tests for the value verbatim.
1375 0 => {
1376 if opt_isset("AUTOMENU") != 0 {
1377 "automenu-unambiguous"
1378 } else {
1379 "unambiguous"
1380 }
1381 }
1382 1 => "menu",
1383 2 => "automenu",
1384 _ => "",
1385 }
1386 } else {
1387 // c:777-780 — `compinsert = ""; kset &= ~CP_INSERT;`
1388 ""
1389 };
1390 // c:781 — `compinsert = (useline < 0 ? tricat("tab ", "", compinsert)
1391 // : ztrdup(compinsert));`
1392 //
1393 // `useline < 0` is set at c:310 from `wouldinstab`, i.e. TAB was
1394 // pressed with nothing but blanks to its left AND a completion
1395 // widget is installed (zle_tricky.c:192-196). The "tab " prefix is
1396 // the ONLY signal `_main_complete` has for that case: sh:70-79 of
1397 // Completion/Base/Core/_main_complete tests `compstate[insert] =
1398 // tab*` and, with the default `insert-tab yes`, returns 0 before
1399 // any completer runs, so the widget falls through to inserting a
1400 // literal TAB. Dropping the prefix made zshrs run the full
1401 // completer chain on an empty command line, which surfaced every
1402 // diagnostic those completers emit (e.g. a user `_describe` over an
1403 // unset array printing "compdescribe: invalid argument") onto a
1404 // prompt where zsh prints nothing at all.
1405 let ins = if ul < 0 {
1406 format!("tab {}", ins) // c:781
1407 } else {
1408 ins.to_string()
1409 };
1410 set_compstate_str("insert", &ins); // c:781
1411
1412 // c:785-790 — `$compstate[exact]`:
1413 // if (useexact) compexact = ztrdup("accept");
1414 // else { compexact = ztrdup(""); kset &= ~CP_EXACT; }
1415 // Outside `kset` the key is PM_UNSET (c:818 comp_setunset), so it is
1416 // not in `${(k)compstate}`. The store has no per-key unset bit, so the
1417 // unset key is represented by its absence; the c:912 read-back treats
1418 // an absent key as the "" published here (useexact stays 0).
1419 if useexact.load(Ordering::Relaxed) != 0 {
1420 set_compstate_str("exact", "accept"); // c:786
1421 } else if let Ok(mut tab) = paramtab_hashed_storage().lock() {
1422 if let Some(h) = tab.get_mut("compstate") {
1423 h.shift_remove("exact"); // c:788-789
1424 }
1425 }
1426
1427 // c:791-794 — `$compstate[to_end]` per movetoend.
1428 set_compstate_str(
1429 "to_end",
1430 if movetoend.load(Ordering::Relaxed) == 1 {
1431 "single"
1432 } else {
1433 "match"
1434 },
1435 );
1436
1437 // c:797-812 — `$compstate[old_list]` / `$compstate[old_insert]`:
1438 //
1439 // if (hasoldlist && lastpermmnum) {
1440 // compoldlist = listshown ? "shown" : "yes";
1441 // if (minfo.cur) { sprintf(buf,"%d",(*minfo.cur)->gnum);
1442 // compoldins = buf; }
1443 // else compoldins = "";
1444 // } else compoldlist = compoldins = "";
1445 //
1446 // Both publishes were missing, so a completer could never tell that a
1447 // previous list is still around. `_menu` and `_history_complete_word`
1448 // read `$compstate[old_list]` and write back "keep" to reuse it; with
1449 // the entry value absent (and the c:923-925 readback below equally
1450 // absent) the keep round-trip could not work at all.
1451 let hasoldlist_v = hasoldlist.load(Ordering::Relaxed);
1452 let lastpermmnum_v = lastpermmnum.load(Ordering::Relaxed);
1453 if hasoldlist_v != 0 && lastpermmnum_v != 0 {
1454 // c:797
1455 set_compstate_str(
1456 "old_list",
1457 if LISTSHOWN.load(Ordering::Relaxed) != 0 {
1458 "shown" // c:799
1459 } else {
1460 "yes" // c:801
1461 },
1462 );
1463 // c:803-808 — `compoldins = minfo.cur ? (*minfo.cur)->gnum : ""`.
1464 let cur_gnum: Option<i32> = MINFO
1465 .get()
1466 .and_then(|m| m.lock().ok())
1467 .and_then(|m| m.cur.as_ref().map(|c| c.gnum));
1468 match cur_gnum {
1469 // c:806 — `kset |= CP_OLDINS` only in the minfo.cur arm.
1470 Some(g) => set_compstate_str("old_insert", &g.to_string()), // c:804-805
1471 None => kunset("old_insert"), // c:808
1472 }
1473 } else {
1474 // c:810 — `compoldlist = compoldins = ""` with CP_OLDLIST /
1475 // CP_OLDINS still cleared from c:562, i.e. both params stay
1476 // PM_UNSET and neither key appears in `${(@kv)compstate}`.
1477 kunset("old_list");
1478 kunset("old_insert");
1479 }
1480
1481 // c:838 — `incompfunc = 1` before invoking the user fn.
1482 INCOMPFUNC.store(1, Ordering::Relaxed); // c:838
1483
1484 // c:828-832 — `largs = newlinklist(); addlinknode(largs,
1485 // dupstring(fn)); while (*cfargs) addlinknode(largs,
1486 // dupstring(*p++));`. argv[0] = function name, then the
1487 // wrapper-widget args stored in `cfargs` by `completecall`.
1488 let largs: Vec<String> = {
1489 let mut v = vec![fn_name.to_string()];
1490 if let Ok(cf) = crate::ported::zle::zle_tricky::cfargs.lock() {
1491 v.extend(cf.iter().cloned());
1492 }
1493 v
1494 };
1495
1496 // c:833-834 — `int oxt = isset(XTRACE); opts[XTRACE] = 0;`. Mute
1497 // xtrace during the body so PS4 noise doesn't appear from every
1498 // compsys helper line.
1499 let oxt = crate::ported::zsh_h::isset(crate::ported::zsh_h::XTRACE) as i32;
1500 crate::ported::options::opt_state_set(
1501 &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
1502 false,
1503 );
1504 let _ = oxt; // c:833 saved for restore at c:836
1505
1506 // c:835 — `cfret = doshfunc(shfunc, largs, 1)`. The body runner
1507 // closure resolves the actual implementation:
1508 // - If a Rust compsys port is registered for `fn_name` and
1509 // `backend = "rust"`, run that.
1510 // - Else autoload + run the upstream shfunc body via the
1511 // standard dispatch path. dispatch_function_call already
1512 // wraps the fusevm Chunk in its own doshfunc scope; we
1513 // intentionally call only the body half here so the C-faithful
1514 // prologue/epilogue runs exactly once around the body.
1515 let largs_for_body = largs.clone();
1516 let fn_name_owned = fn_name.to_string();
1517
1518 // c:843-925 reads `complist`/`compinsert`/`compexact`/`comptoend`/
1519 // `compoldlist`/`compoldins` AFTER `endparamscope()` (c:838). It can do
1520 // that because in C those are plain globals (complete.c:36-44) and the
1521 // `$compstate` entries are only gsu VIEWS onto them (complete.c:1280-1300
1522 // `VAL(compinsert)` …) — tearing down the parameter cannot touch the
1523 // value.
1524 //
1525 // This port has no gsu binding: the values live in the `compstate`
1526 // parameter itself, which `callcompfunc` stamps PM_SPECIAL|PM_REMOVABLE
1527 // at `locallevel + 1` (the c:816-817 block above), so `doshfunc`'s
1528 // `endparamscope()` DELETES it. Measured with a tracing probe on the
1529 // `cd /<TAB>` round: every key read back `None` after the call — `list`,
1530 // `insert`, `exact` and `to_end` alike.
1531 //
1532 // So snapshot the hash at the END OF THE BODY — still inside the
1533 // function scope, and after the completion function's last write, which
1534 // is exactly the state C's globals hold when it reads them at c:843.
1535 let compstate_end: std::sync::Arc<Mutex<Option<indexmap::IndexMap<String, String>>>> =
1536 std::sync::Arc::new(Mutex::new(None));
1537 let compstate_end_body = std::sync::Arc::clone(&compstate_end);
1538
1539 let body_runner = move || -> i32 {
1540 // c:6042 — `runshfunc(prog, wrappers, name)`. zshrs runs the
1541 // body via either the Rust compsys port (direct fn call) or
1542 // the fusevm Chunk dispatch (via exec accessors).
1543 // BODY ONLY: the enclosing `doshfunc` below is c:835's single
1544 // frame. `dispatch_function_call` wraps a second doshfunc, which
1545 // pushed the completion function onto $funcstack twice —
1546 // `compdef -K _fk …` saw `(_fk2 _fk _fk)` where zsh has
1547 // `(_fk2 _fk)`. run_function_body keeps the Rust-port and autoload
1548 // short-circuits. C convention: largs[0] = fn name, [1..] = argv.
1549 let rc = crate::ported::exec::run_function_body(&fn_name_owned, &largs_for_body[1..])
1550 .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed));
1551 // Capture `$compstate` before the enclosing doshfunc scope ends.
1552 if let Ok(tab) = paramtab_hashed_storage().lock() {
1553 if let Some(h) = tab.get("compstate") {
1554 if let Ok(mut g) = compstate_end_body.lock() {
1555 *g = Some(h.clone());
1556 }
1557 }
1558 }
1559 rc
1560 };
1561
1562 // Look up the real shfunc; if missing we still want doshfunc's
1563 // scope around the Rust port (synth_shf carries just the name).
1564 //
1565 // c:552 / c:835 — C hands `doshfunc` the node `getshfunc(fn)` returned,
1566 // flags included. PM_UNDEFINED is what keeps `doshfunc` from setting
1567 // `scriptname = name` (exec.c:5835-5836) before the autoload lookup, so a
1568 // corrupt `.zwc` met while loading `_main_complete` is reported under the
1569 // CALLER's name (`zsh:2: invalid zwc file:`); hardcoded 0 printed
1570 // `_main_complete: invalid zwc file:`. PM_LOADDIR is dropped for the same
1571 // reason `vm_helper::dispatch_function_call` drops it: it describes a
1572 // `filename` field this synthesized node does not carry.
1573 let real_flags = crate::ported::utils::getshfunc(fn_name)
1574 .map(|f| f.node.flags & !(crate::ported::zsh_h::PM_LOADDIR as i32))
1575 .unwrap_or(0);
1576 let mut synth_shf = crate::ported::zsh_h::shfunc {
1577 node: crate::ported::zsh_h::hashnode {
1578 next: None,
1579 nam: fn_name.to_string(),
1580 flags: real_flags,
1581 },
1582 filename: None,
1583 lineno: 0,
1584 funcdef: None,
1585 redir: None,
1586 sticky: None,
1587 body: None,
1588 redir_text: None,
1589 };
1590 // c:817-819 — `makecompparams(); comp_setunset(rset, …, kset,
1591 // ~kset & CP_ALLKEYS);`. makecompparams creates every compkparams row
1592 // (complete.c:1261-1290) inside `$compstate`; comp_setunset then marks
1593 // the rows outside `kset` PM_UNSET. The rows below are in `kset` on
1594 // every call — the c:562-564 initial mask never excludes them and no
1595 // later c:565-812 statement clears them — so `${(k)compstate}` always
1596 // lists them. Their values come from getters or C globals
1597 // (get_compstate_str), never from a set_compstate_str publish, so
1598 // without this the keys were missing from `${(k)compstate}` and from
1599 // `_lastcomp`. The value stored is only a placeholder: assoc scans
1600 // recompute it (params.rs, the get_compstate_str refresh).
1601 for key in [
1602 "nmatches", // c:1262 GSU(nmatches_gsu)
1603 "unambiguous", // c:1275 GSU(unambig_gsu)
1604 "unambiguous_cursor", // c:1276 GSU(unambig_curs_gsu)
1605 "unambiguous_positions", // c:1278 GSU(unambig_pos_gsu)
1606 "insert_positions", // c:1280 GSU(insert_pos_gsu)
1607 "list_max", // c:1282 VAL(complistmax)
1608 "vared", // c:1287 VAL(compvared); c:569 only adds the bit
1609 "list_lines", // c:1288 GSU(listlines_gsu)
1610 "all_quotes", // c:1289 GSU(compqstack_gsu)
1611 "ignored", // c:1290 VAL(compignored)
1612 ] {
1613 let present = paramtab_hashed_storage()
1614 .lock()
1615 .map(|t| t.get("compstate").is_some_and(|h| h.contains_key(key)))
1616 .unwrap_or(true);
1617 if !present {
1618 set_compstate_str(key, &get_compstate_str(key).unwrap_or_default());
1619 }
1620 }
1621
1622 // c:816-817 — `startparamscope(); makecompparams();`.
1623 //
1624 // C creates `$words` / `$CURRENT` / `$PREFIX` / `$SUFFIX` /
1625 // `$IPREFIX` / `$ISUFFIX` / `$QIPREFIX` / `$QISUFFIX` /
1626 // `$compstate` here with `createparam(name, type|PM_SPECIAL|
1627 // PM_REMOVABLE|PM_LOCAL)` and then `pm->level = locallevel + 1`
1628 // (addcompparams c:1300-1307, makecompparams c:1348). Their VALUES
1629 // live in C globals reached through a gsu vtable, so creating them
1630 // empty here costs nothing.
1631 //
1632 // zshrs has no gsu binding: the values live in the params, and the
1633 // block above has already written every one of them with
1634 // `setsparam`/`setaparam`/`setiparam` — i.e. at level 0, flagged as
1635 // ordinary scalars. Re-running `createparam` would shadow those
1636 // values with empty ones, so only the scope half of c:817 is
1637 // applied: stamp the level and the special/removable bits onto the
1638 // params that are already in place.
1639 //
1640 // Without this, `${(t)PREFIX}` read `scalar` instead of
1641 // `scalar-local-special` and the names outlived the completion.
1642 // `_parameters` filters candidates with
1643 // `${(@k)parameters[(R)…~*local*]}`, so every one of these was
1644 // offered as a completion for `unset <TAB>`.
1645 //
1646 // `PM_READONLY` (on QIPREFIX/QISUFFIX in the c:1256-1259 table) is
1647 // stamped from each row's own type, the way c:1301 or's `cp->type`
1648 // in. The bit used to be dropped here because a sticky one failed
1649 // the next completion's publish; the publishes that run after this
1650 // stamp now go through `vm_helper::set_readonly_special`, the
1651 // gsu-setfn equivalent C uses (c:1308-1324), which is not subject to
1652 // the assignment gate.
1653 {
1654 use crate::ported::zsh_h::{PM_READONLY, PM_REMOVABLE, PM_SPECIAL};
1655 // c:1307 / c:1348 — `pm->level = locallevel + 1`.
1656 let level = crate::ported::params::locallevel.load(Ordering::Relaxed) + 1;
1657 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
1658 for (name, ty) in crate::ported::zle::complete::COMPRPARAMS
1659 .iter()
1660 .map(|cp| (cp.name, cp.r#type))
1661 .chain([("compstate", 0)])
1662 {
1663 if let Some(pm) = tab.get_mut(name) {
1664 pm.level = level;
1665 // c:1301 — the row's own type bits, which is where
1666 // QIPREFIX/QISUFFIX's PM_READONLY comes from.
1667 pm.node.flags |= ty & PM_READONLY as i32;
1668 // c:complete.c:1313-1315 — every comprparams integer
1669 // row is a `VAL(...)` row (`CURRENT`), so it gets
1670 // `pm->gsu.i = &compvarinteger_gsu; pm->base = 10;`,
1671 // which is why `typeset -p CURRENT` prints `-i10`.
1672 if crate::ported::zsh_h::PM_TYPE(ty as u32)
1673 == crate::ported::zsh_h::PM_INTEGER
1674 {
1675 pm.base = 10;
1676 }
1677 // c:1301 — `cp->type | PM_SPECIAL | PM_REMOVABLE |
1678 // PM_LOCAL`. PM_REMOVABLE is load-bearing:
1679 // `scanendscope` (params.c:5905) only takes the
1680 // "restore the shadowed value" branch for
1681 // `(flags & (PM_SPECIAL|PM_REMOVABLE)) == PM_SPECIAL`.
1682 // These params have no shadow to restore — they must
1683 // be deleted, which is the PM_REMOVABLE path.
1684 pm.node.flags |= (PM_SPECIAL | PM_REMOVABLE) as i32;
1685 }
1686 }
1687 }
1688 }
1689
1690 // c:820 — `makezleparams(1);`.
1691 //
1692 // This line was missing entirely, so EVERY completion function ran
1693 // with no ZLE parameters at all: `$BUFFER`, `$LBUFFER`, `$RBUFFER`,
1694 // `$CURSOR`, `$HISTNO`, `$WIDGET`, `$KEYS`, `$BUFFERLINES`,
1695 // `$PENDING` all read as the empty string. `_fc` computes
1696 // `(( num = num - HISTNO ))` to turn history event numbers into the
1697 // negative offsets it completes, so `fc -<TAB>` offered nothing;
1698 // more broadly any completer that inspects the line it is completing
1699 // (`$BUFFER`/`$CURSOR`) saw an empty line.
1700 crate::ported::zle::zle_params::makezleparams(1); // c:820
1701 {
1702 // c:839 — `endparamscope()` is what tears these down again in C:
1703 // `makezleparams` creates each one PM_SPECIAL|PM_REMOVABLE|
1704 // PM_LOCAL at `locallevel + 1` (zle_params.c:200-206), so leaving
1705 // the completion scope unsets them and they never reach the
1706 // interactive shell.
1707 //
1708 // The Rust `makezleparams` publishes through
1709 // `setsparam`/`setiparam`/`setaparam` (the values live in the
1710 // params, not behind a gsu vtable), which leaves them at the
1711 // enclosing level as ordinary params. Stamp the same level and
1712 // flags the comp params get above so `doshfunc`'s `endparamscope`
1713 // removes them on the way out. Without this the names leaked into
1714 // the interactive shell after the first TAB — `$BUFFER` and
1715 // friends stayed set at the prompt, and `_parameters` offered
1716 // them (its candidate filter is
1717 // `${(@k)parameters[(R)…~*local*]}`).
1718 //
1719 // As with QIPREFIX above, `PM_READONLY` (c:201, `ro` is 1 here)
1720 // is deliberately not stamped: C re-creates the params from
1721 // scratch on every call, so the bit never blocks the next one,
1722 // whereas a bit that outlived the teardown would fail the next
1723 // widget's `makezleparams(0)` publish.
1724 use crate::ported::zsh_h::{PM_REMOVABLE, PM_SPECIAL};
1725 let level = crate::ported::params::locallevel.load(Ordering::Relaxed) + 1;
1726 if let Ok(mut tab) = crate::ported::params::paramtab().write() {
1727 for name in crate::ported::zle::zle_params::ZLEPARAM_NAMES {
1728 if let Some(pm) = tab.get_mut(*name) {
1729 pm.level = level; // c:206 `pm->level = locallevel + 1`
1730 pm.node.flags |= (PM_SPECIAL | PM_REMOVABLE) as i32; // c:200
1731 }
1732 }
1733 }
1734 }
1735
1736 let cfret_val = crate::ported::exec::doshfunc(&mut synth_shf, largs, true, body_runner);
1737 crate::ported::zle::zle_tricky::cfret.store(cfret_val, Ordering::Relaxed);
1738
1739 // c:836 — `opts[XTRACE] = oxt;` restore xtrace state.
1740 crate::ported::options::opt_state_set(
1741 &crate::ported::zsh_h::opt_name(crate::ported::zsh_h::XTRACE),
1742 oxt != 0,
1743 );
1744
1745 // c:839-841 — `lastcmd = 0; incompfunc = icf; startauto = 0;`.
1746 // `startauto` is cleared BEFORE the c:908 recompute below; without the
1747 // clear the AUTO_MENU value do_completion stored at c:331 survived any
1748 // completer that emptied `$compstate[insert]`.
1749 startauto.store(0, Ordering::Relaxed); // c:841
1750
1751 // c:843-925 — unwind: read the compstate values the completion function
1752 // may have rewritten back into the compcore globals. In C these ARE the
1753 // globals (the compstate entries are gsu-bound to `complist`,
1754 // `compinsert`, `compexact`, `comptoend`, `compoldlist`, `compoldins`),
1755 // so this is a plain read of a mutated variable; here it is a read of
1756 // `$compstate[…]`.
1757 //
1758 // Only the `usemenu` third of the c:857-907 arm existed before. Every
1759 // other assignment in the block — `uselist`, `forcelist`, `onlyexpl`,
1760 // `useline`, `insmnum`, `insspace`, `startauto`, `useexact`,
1761 // `movetoend`, `oldlist`, `oldins` — was simply absent, so a completion
1762 // function could not influence any of them: `compstate[list]=...force`
1763 // never forced a list, `_menu`'s `compstate[old_list]=keep` never kept
1764 // one, `compstate[insert]=2` never selected the 2nd match, and
1765 // `compstate[insert]=''` never suppressed insertion.
1766 //
1767 // Read `$compstate[…]` via the compstate hash (the canonical home), NOT
1768 // the flat `compstate[KEY]` bracketed param: the latter reads empty here
1769 // because the completion fn's write lands in the hash storage while the
1770 // flat param is scoped to the fn.
1771
1772 // Read one `$compstate` entry as C reads its backing global at c:843+:
1773 // the end-of-body snapshot first, then whatever is still live.
1774 //
1775 // `None` means the port HAS NO VALUE for this entry — a state C cannot
1776 // be in, because `callcompfunc` itself assigned every one of these
1777 // globals before the call (c:753-812). Applying C's "NULL" arm to it
1778 // would be a mistranslation of "absent" as "empty", and that is exactly
1779 // what regressed `menu select`: the `insert`/`list` entries read back
1780 // absent, so `useline` and `uselist` were both driven to 0 and
1781 // `do_completion` took its c:425 else-branch (revert the line, show
1782 // nothing) instead of listing. On `None` the globals keep the values
1783 // this function published, which is the round-trip C would have seen.
1784 let post = |key: &str| -> Option<String> {
1785 if let Some(v) = compstate_end
1786 .lock()
1787 .ok()
1788 .and_then(|g| g.as_ref().and_then(|h| h.get(key).cloned()))
1789 {
1790 return Some(v);
1791 }
1792 get_compstate_str(key)
1793 };
1794
1795 // c:843-855 — uselist / forcelist / onlyexpl from `complist`.
1796 if let Some(post_list) = post("list") {
1797 uselist.store(
1798 if post_list.starts_with("list") {
1799 1 // c:846
1800 } else if post_list.starts_with("auto") {
1801 2 // c:848
1802 } else if post_list.starts_with("ambig") {
1803 3 // c:850
1804 } else {
1805 0 // c:844 / c:852
1806 },
1807 Ordering::Relaxed,
1808 );
1809 forcelist.store(
1810 if post_list.contains("force") { 1 } else { 0 }, // c:853
1811 Ordering::Relaxed,
1812 );
1813 onlyexpl.store(
1814 (if post_list.contains("expl") { 1 } else { 0 }) // c:854
1815 | (if post_list.contains("messages") { 2 } else { 0 }), // c:855
1816 Ordering::Relaxed,
1817 );
1818 // Keep the `complist` global in step with the param — one storage in C.
1819 if let Ok(mut g) = COMPLIST.get_or_init(|| Mutex::new(String::new())).lock() {
1820 *g = post_list.clone();
1821 }
1822 }
1823
1824 // c:857-907 — useline / usemenu / insmnum / insspace from `compinsert`.
1825 let post_insert = match post("insert") {
1826 Some(v) => v,
1827 // Absent: leave useline/usemenu/insmnum/insspace as published.
1828 None => String::new(),
1829 };
1830 let have_insert = post("insert").is_some();
1831 // c:857-858 — `if (!compinsert) useline = 0;`. C's test is on the
1832 // POINTER: it fires only when `$compstate[insert]` has no value at all.
1833 // An EMPTY STRING is a perfectly ordinary `char *` and falls all the way
1834 // through to the c:883-896 else, whose own comment spells out that the
1835 // empty case is the one it exists for:
1836 //
1837 // } else {
1838 // if (strpfx("menu", compinsert)) useline = 1, usemenu = 1;
1839 // else if (strpfx("auto", compinsert)) useline = 1, usemenu = 2;
1840 // else {
1841 // useline = usemenu = 0;
1842 // /* if compstate[insert] was emptied, no unambiguous prefix
1843 // * ever gets inserted so allow the next tab to already start
1844 // * menu completion */
1845 // startauto = lastambig = isset(AUTOMENU);
1846 // }
1847 //
1848 // The port used to short-circuit `post_insert.is_empty()` into the NULL
1849 // arm, so it set `useline = 0` and stopped — never clearing `usemenu`
1850 // and, far more visibly, never arming `startauto`/`lastambig`. That
1851 // arming is the ONLY thing that lets the SECOND Tab start menu
1852 // completion after a round that inserted nothing: `before_complete`
1853 // (c:493-495, `startauto && lastambig` → `usemenu = 2`) has no other
1854 // source. Measured on `bindkey -` under
1855 // scripts/parity_combos/full.zsh (`menu 'select=0' interactive`):
1856 // `$compstate[insert]` reads back EMPTY in both shells there (sampled
1857 // from `comppostfuncs`, the last hook `_main_complete` runs before it
1858 // snapshots `_lastcomp`), so c:895 is the only arming zsh gets — and
1859 // zsh's second Tab entered interactive menu-select and printed
1860 // `interactive: -[]` above the list where this port's did not. The
1861 // follow-on keystrokes then went to the line instead of the menu
1862 // filter: typing `s` narrowed zsh's list to `-s` and self-inserted here.
1863 if !have_insert {
1864 // no-op: keep the c:767-782 values
1865 } else if post_insert.contains("tab") {
1866 useline.store(-1, Ordering::Relaxed); // c:860
1867 } else if post_insert == "unambig"
1868 || post_insert == "unambiguous"
1869 || post_insert == "automenu-unambiguous"
1870 {
1871 // c:861-864 — C compares these three EXACTLY, and does so *before*
1872 // the `strpfx("menu", …)` / `strpfx("auto", …)` arms at c:885-888.
1873 // The ordering is load-bearing now that the entry value grew an
1874 // "automenu" prefix (c:770): a prefix test would see "auto" and pick
1875 // usemenu = 2, starting menu completion on the very FIRST TAB
1876 // instead of inserting the unambiguous prefix and only arming the
1877 // next TAB — the arming is carried by `startauto` (c:908 below).
1878 useline.store(1, Ordering::Relaxed); // c:864
1879 USEMENU.store(0, Ordering::Relaxed); // c:864
1880 } else if post_insert == "all" {
1881 useline.store(2, Ordering::Relaxed); // c:866
1882 USEMENU.store(0, Ordering::Relaxed); // c:866
1883 } else if post_insert
1884 .as_bytes()
1885 .first()
1886 .is_some_and(u8::is_ascii_digit)
1887 {
1888 // c:867 — `idigit(*compinsert)`: insert the Nth match directly.
1889 // `first()` rather than `[0]`: an empty `$compstate[insert]` now
1890 // reaches this arm (see the c:857-858 note above), and C's
1891 // `idigit(*compinsert)` reads the NUL terminator there — false —
1892 // where an unguarded index would panic.
1893 useline.store(1, Ordering::Relaxed); // c:872
1894 USEMENU.store(3, Ordering::Relaxed); // c:872
1895 // c:873 — `insmnum = atoi(compinsert)`; inlined (leading digits).
1896 insmnum.store(
1897 post_insert
1898 .chars()
1899 .take_while(|c| c.is_ascii_digit())
1900 .collect::<String>()
1901 .parse::<i32>()
1902 .unwrap_or(0),
1903 Ordering::Relaxed,
1904 ); // c:873
1905 insspace.store(
1906 if post_insert.ends_with(' ') { 1 } else { 0 }, // c:881
1907 Ordering::Relaxed,
1908 );
1909 } else {
1910 if post_insert.starts_with("menu") {
1911 // c:885
1912 useline.store(1, Ordering::Relaxed); // c:886
1913 USEMENU.store(1, Ordering::Relaxed); // c:886
1914 } else if post_insert.starts_with("auto") {
1915 // c:887
1916 useline.store(1, Ordering::Relaxed); // c:888
1917 USEMENU.store(2, Ordering::Relaxed); // c:888
1918 } else {
1919 useline.store(0, Ordering::Relaxed); // c:890
1920 USEMENU.store(0, Ordering::Relaxed); // c:890
1921 // c:891-894 — "if compstate[insert] was emptied, no unambiguous
1922 // prefix ever gets inserted so allow the next tab to already
1923 // start menu completion".
1924 let am = opt_isset("AUTOMENU");
1925 startauto.store(am, Ordering::Relaxed); // c:894
1926 LASTAMBIG.store(am, Ordering::Relaxed); // c:894
1927 }
1928 // c:897-898 — `if (useline && (p = strchr(compinsert, ':')))
1929 // insmnum = atoi(++p);`
1930 if useline.load(Ordering::Relaxed) != 0 {
1931 if let Some(colon) = post_insert.find(':') {
1932 // c:898 — `insmnum = atoi(++p)`; inlined (leading digits).
1933 insmnum.store(
1934 post_insert[colon + 1..]
1935 .chars()
1936 .take_while(|c| c.is_ascii_digit())
1937 .collect::<String>()
1938 .parse::<i32>()
1939 .unwrap_or(0),
1940 Ordering::Relaxed,
1941 ); // c:898
1942 }
1943 }
1944 }
1945 // c:908-911 — `startauto = startauto ||
1946 // compinsert == "automenu-unambiguous" ||
1947 // (bashlistfirst && isset(AUTOMENU) && !*compinsert);`
1948 if have_insert
1949 && startauto.load(Ordering::Relaxed) == 0
1950 && (post_insert == "automenu-unambiguous"
1951 || (crate::ported::zle::zle_tricky::BASHLISTFIRST.load(Ordering::Relaxed) != 0
1952 && opt_isset("AUTOMENU") != 0
1953 && post_insert.is_empty()))
1954 {
1955 startauto.store(1, Ordering::Relaxed); // c:908
1956 }
1957
1958 // c:912 — `useexact = (compexact && !strcmp(compexact, "accept"));`
1959 if let Some(post_exact) = post("exact") {
1960 useexact.store(
1961 if post_exact == "accept" { 1 } else { 0 },
1962 Ordering::Relaxed,
1963 );
1964 }
1965
1966 // `comppatinsert` (complete.c:69) is `VAL()`-bound to
1967 // `$compstate[pattern_insert]` (complete.c:1281), so a completer's
1968 // `compstate[pattern_insert]=unambiguous` (`_expand:_expand:…`,
1969 // `_approximate:91`) lands straight in the C global and C needs no
1970 // read-back here. This port keeps the two storages separate, so mirror
1971 // the parameter into the global now — same treatment `complist` gets at
1972 // c:846-853 above. Absent (`None`) keeps the c:321 value, which is the
1973 // state C would be in when no completer touched it.
1974 if let Some(post_patins) = post("pattern_insert") {
1975 if let Ok(mut g) = crate::ported::zle::complete::COMPPATINSERT
1976 .get_or_init(|| Mutex::new(String::new()))
1977 .lock()
1978 {
1979 *g = post_patins;
1980 }
1981 }
1982
1983 // `complastprompt` (complete.c:57) is `VAL()`-bound to
1984 // `$compstate[last_prompt]` (complete.c:1293); mirror a completer's
1985 // write into the global the same way.
1986 if let Some(post_lastprompt) = post("last_prompt") {
1987 if let Ok(mut g) = crate::ported::zle::complete::COMPLASTPROMPT
1988 .get_or_init(|| Mutex::new(String::new()))
1989 .lock()
1990 {
1991 *g = post_lastprompt;
1992 }
1993 }
1994
1995 // c:914-921 — movetoend from `comptoend`.
1996 if let Some(post_toend) = post("to_end") {
1997 movetoend.store(
1998 if post_toend.is_empty() {
1999 0 // c:915
2000 } else if post_toend == "single" {
2001 1 // c:917
2002 } else if post_toend == "always" {
2003 3 // c:919
2004 } else {
2005 2 // c:921
2006 },
2007 Ordering::Relaxed,
2008 );
2009 }
2010
2011 // c:923-925 — `oldlist = (hasoldlist && compoldlist &&
2012 // !strcmp(compoldlist, "keep"));
2013 // oldins = (hasoldlist && minfo.cur &&
2014 // compoldins && !strcmp(compoldins, "keep"));`
2015 let hasoldlist_v = hasoldlist.load(Ordering::Relaxed);
2016 oldlist.store(
2017 if hasoldlist_v != 0 && post("old_list").as_deref() == Some("keep") {
2018 1
2019 } else {
2020 0
2021 },
2022 Ordering::Relaxed,
2023 ); // c:923
2024 let has_cur = MINFO
2025 .get()
2026 .and_then(|m| m.lock().ok())
2027 .map(|m| m.cur.is_some())
2028 .unwrap_or(false);
2029 oldins.store(
2030 if hasoldlist_v != 0 && has_cur && post("old_insert").as_deref() == Some("keep") {
2031 1
2032 } else {
2033 0
2034 },
2035 Ordering::Relaxed,
2036 ); // c:924-925
2037
2038 // c:932 — `lastval = lv`: the completion function's exit status must not
2039 // leak into the interactive shell's `$?`.
2040 crate::ported::builtin::LASTVAL.store(_lv, Ordering::Relaxed); // c:932
2041
2042 // c:840 — incompfunc = icf. Restore.
2043 INCOMPFUNC.store(_icf, Ordering::Relaxed);
2044}
2045
2046// =====================================================================
2047// makecomplist — `Src/Zle/compcore.c:946`.
2048// =====================================================================
2049
2050/// Guards the first-use hook attachment in `makecomplist` (see there).
2051static COMPCTL_HOOKS: std::sync::Once = std::sync::Once::new();
2052
2053/// Direct port of `int makecomplist(char *s, int incmd, int lst)` from
2054/// compcore.c:946. Top-level dispatch into the completion subsystem:
2055/// either the new compsys path (`callcompfunc`) or the legacy compctl
2056/// path (`COMPCTLMAKEHOOK`).
2057pub fn makecomplist(s: &str, incmd: i32, lst: i32) -> i32 {
2058 // c:946
2059 let owb = WB.load(Ordering::Relaxed); // c:946
2060 let owe = WE.load(Ordering::Relaxed);
2061 let ooffs = OFFS.load(Ordering::Relaxed);
2062
2063 // c:952-958 — `if (compfunc && (p = check_param(s, 0, 0)))`.
2064 let mut s_owned = s.to_string();
2065 // The TOKENIZED twin of `s`, for `callcompfunc`'s `multiquote(s, 0)`
2066 // (c:700/711/715) — see `crate::comp_word_tok`. C has no equivalent
2067 // bookkeeping: its `s` is tokenized from `get_comp_string`
2068 // (zle_tricky.c:2221) right through to `callcompfunc`, while this port
2069 // untokenizes at the return and stashes the tokenized form in
2070 // `COMP_STRING_TOK`. The twin is only usable when it still untokenizes
2071 // back to the word actually being completed; a completion that did not
2072 // come through `get_comp_string` leaves a stale value behind.
2073 let mut s_tok = crate::ported::zle::zle_tricky::COMP_STRING_TOK
2074 .get()
2075 .and_then(|m| m.lock().ok())
2076 .map(|g| g.clone())
2077 .filter(|t| crate::ported::lex::untokenize(t) == s_owned);
2078 // c:952 — C hands `check_param` the TOKENIZED `s`, and the test at
2079 // c:1141 is written in parser tokens precisely so an escaped `\$` (a
2080 // plain 0x24 by then, its `Bnull` chucked at zle_tricky.c:1920) is NOT a
2081 // parameter context. Feed the twin, with `offs` remapped into its byte
2082 // space; when there is no usable twin fall back to the untokenized word,
2083 // which cannot tell `\$PATH` from `$PATH`.
2084 let probe: Option<(String, usize)> = s_tok.as_ref().and_then(|t| {
2085 crate::comp_word_tok::tok_index(t, ooffs.max(0) as usize).map(|ti| (t.clone(), ti))
2086 });
2087 if compfunc_active() {
2088 let found = match &probe {
2089 Some((t, ti)) => {
2090 OFFS.store(*ti as i32, Ordering::Relaxed);
2091 let r = check_param(t, false, false, false);
2092 if r.is_none() {
2093 OFFS.store(ooffs, Ordering::Relaxed);
2094 }
2095 r
2096 }
2097 None => check_param(&s_owned, false, false, true),
2098 };
2099 if let Some(p) = found {
2100 // c:951
2101 // c:952 — `s = p`, where C's `p` points at the parameter NAME and
2102 // check_param has already NUL-terminated it at the end of the
2103 // name (`b[we-wb] = '\0'`, c:1298). Taking `s[p..]` alone kept
2104 // everything that followed the name (the `}` of a `${…}`, any
2105 // `:modifiers`, the ignored suffix), so `callcompfunc` received
2106 // e.g. `PA}` instead of `PA` and published that as `$PREFIX`.
2107 // `we - wb` is exactly `e - b`, set by check_param at c:1295-1296.
2108 let namelen = (WE.load(Ordering::Relaxed) - WB.load(Ordering::Relaxed)).max(0) as usize;
2109 match &probe {
2110 // `p` and `namelen` are byte counts in the TWIN. Convert the
2111 // whole result back to the untokenized byte space the rest of
2112 // this port indexes `wb`/`we`/`offs` in — the line the user
2113 // typed, which the c:1788-1926 cleanup loop has already
2114 // stripped of its quote characters.
2115 Some((t, _)) => {
2116 let cut = namelen.min(t.len().saturating_sub(p));
2117 let name_tok = t.get(p..p + cut).unwrap_or("").to_string();
2118 s_owned = crate::ported::lex::untokenize(&name_tok); // c:952 + c:1298
2119 let pre_u = crate::comp_word_tok::untok_index(t, p).unwrap_or(0);
2120 OFFS.store(ooffs - pre_u as i32, Ordering::Relaxed); // c:1294
2121 let wb = ZLEMETACS.load(Ordering::Relaxed) - OFFS.load(Ordering::Relaxed);
2122 WB.store(wb, Ordering::Relaxed); // c:1295
2123 WE.store(wb + s_owned.len() as i32, Ordering::Relaxed); // c:1296
2124 s_tok = Some(name_tok);
2125 }
2126 None => {
2127 let tail = &s_owned[p..];
2128 let cut = namelen.min(tail.len());
2129 s_tok = s_tok.and_then(|t| crate::comp_word_tok::span(&t, p, cut));
2130 s_owned = tail[..cut].to_string(); // c:952 + c:1298
2131 }
2132 }
2133 PARWB.store(owb, Ordering::Relaxed); // c:953
2134 PARWE.store(owe, Ordering::Relaxed); // c:955
2135 PAROFFS.store(ooffs, Ordering::Relaxed); // c:956
2136 } else {
2137 PARWB.store(-1, Ordering::Relaxed); // c:958
2138 }
2139 } else {
2140 PARWB.store(-1, Ordering::Relaxed); // c:958
2141 }
2142
2143 // Publish the narrowed twin for `callcompfunc` (c:700/711/715).
2144 crate::comp_word_tok::set(s_tok);
2145
2146 linwhat.store(INWHAT.load(Ordering::Relaxed), Ordering::Relaxed); // c:960
2147
2148 if compfunc_active() {
2149 // c:962
2150 let os = s_owned.clone(); // c:964
2151 let onm = nmatches.load(Ordering::Relaxed); // c:965
2152 let odm = diffmatches.load(Ordering::Relaxed); // c:965
2153 let osi = movefd(0); // c:965 movefd(0)
2154 // c:965 moves the shell's stdin off fd 0 and c:1013/1035/1039 `redup(osi, 0)`
2155 // puts it back, so the completion function runs with fd 0 FREE — and
2156 // the very next `open()`/`opendir()` in that window is handed
2157 // descriptor 0. zsh parks /dev/null there for exactly the same window
2158 // (the idiom, with its rationale, is spelled out at
2159 // Src/Zle/zle_main.c:1521-1526: "Many commands don't like having a
2160 // closed stdin, open on /dev/null instead"); measured against zsh 5.9,
2161 // `[[ /dev/fd/0 -ef /dev/null ]]` is TRUE inside a completion
2162 // function, while zshrs left fd 0 closed.
2163 //
2164 // A closed fd 0 is not merely untidy here: `_path_files -W /dev -g
2165 // '*(-/)'` calls `opendir("/dev")`, which lands on descriptor 0, so
2166 // `/dev/fd/0` — the target of the `/dev/stdin` symlink — resolves to
2167 // the very directory being scanned. The `-/` qualifier then stats a
2168 // directory and admits `/dev/stdin`, and `mount /dev/<TAB>` listed a
2169 // bogus `stdin@` next to `fd/` and `monotonic/`.
2170 if osi > 0 {
2171 unsafe {
2172 let devnull = std::ffi::CString::new("/dev/null").unwrap();
2173 let _ = libc::open(devnull.as_ptr(), libc::O_RDWR | libc::O_NOCTTY);
2174 }
2175 }
2176
2177 // c:967-968 — bmatchers = mstack = NULL.
2178 if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
2179 *g = None;
2180 }
2181 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
2182 *g = None;
2183 }
2184 // c:970-971 — ainfo = fainfo = hcalloc(sizeof(struct aminfo)).
2185 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2186 *g = Some(Aminfo::default());
2187 }
2188 if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
2189 *g = Some(Aminfo::default());
2190 }
2191 if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
2192 *g = None; // c:973
2193 }
2194 if VALIDLIST.load(Ordering::Relaxed) == 0 {
2195 LASTAMBIG.store(0, Ordering::Relaxed);
2196 // c:976
2197 }
2198 // c:976 — `amatches = NULL;`. In C that drops a POINTER: after c:1022
2199 // and c:1026 (`amatches = pmatches; ... lastmatches = pmatches;`)
2200 // the two name the SAME groups, and stay aliased until the next
2201 // completion (`invalidatelist`, c:Src/Zle/compresult.c:2331-2345,
2202 // ends both at once and clears `hasoldlist`). So every write made to
2203 // `amatches` in between — above all `calclist`'s per-group geometry
2204 // (`dcount`, `cols`, `lins`, `widths`) — is ALSO in `lastmatches`,
2205 // and that is what c:1001 `amatches = lastmatches` hands back for
2206 // `compstate[old_list]=keep`. zshrs holds two cloned Vecs, so those
2207 // writes stayed in `amatches` alone and the kept list came back with
2208 // `lcount` set but no rows or columns: `printlist` drew nothing, and
2209 // `_history-complete-older`'s second TAB dropped the listing zsh
2210 // keeps (spec-fuzz 9510/case0028, the 9401/0022 cell). While
2211 // `hasoldlist` says the alias is live, carry `amatches` into
2212 // `lastmatches` before letting go of it.
2213 let prev = amatches
2214 .get_or_init(|| Mutex::new(Vec::new()))
2215 .lock()
2216 .map(|mut g| std::mem::take(&mut *g))
2217 .unwrap_or_default(); // c:977
2218 if hasoldlist.load(Ordering::Relaxed) != 0 && !prev.is_empty() {
2219 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2220 *g = prev;
2221 }
2222 }
2223 mnum.store(0, Ordering::Relaxed); // c:978
2224 unambig_mnum.store(-1, Ordering::Relaxed); // c:979
2225 if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
2226 g.clear(); // c:980
2227 }
2228 // c:981 — `insmnum = zmult;` where `zmult` is `zmod.mult` (c:Src/Zle/zle.h:267).
2229 // Read ZMOD, not the ZMULT copy: nothing syncs ZMULT before a fresh
2230 // completion, so `reverse-menu-complete`'s negation (zle_tricky.c:347)
2231 // never reached do_ambig_menu and the menu started at the FIRST match.
2232 insmnum.store(
2233 crate::ported::zle::zle_main::ZMOD.lock().map(|g| g.mult).unwrap_or(1),
2234 Ordering::Relaxed,
2235 );
2236 oldlist.store(0, Ordering::Relaxed); // c:986
2237 oldins.store(0, Ordering::Relaxed); // c:986
2238 begcmgroup(Some("default"), 0); // c:987
2239 MENUCMP.store(0, Ordering::Relaxed); // c:988
2240 menuacc.store(0, Ordering::Relaxed); // c:988
2241 newmatches.store(0, Ordering::Relaxed); // c:988
2242 onlyexpl.store(0, Ordering::Relaxed); // c:988
2243
2244 let dup_s = crate::ported::mem::dupstring(&os); // c:990
2245 let cf_name = compfunc
2246 .get_or_init(|| Mutex::new(None))
2247 .lock()
2248 .ok()
2249 .and_then(|g| g.clone())
2250 .unwrap_or_default();
2251 callcompfunc(&dup_s, &cf_name); // c:991
2252 endcmgroup(None); // c:992
2253
2254 // !!! RUST-ONLY — the comphooks hookdefs (complete.c:1712 `comphooks`) and
2255 // compctl.c:4049 `boot_` (which addhookfunc()s ccmakehookfn /
2256 // cccleanuphookfn on them) are never reached by zshrs's static module
2257 // registration outside a running ZLE, so attach both on first use.
2258 COMPCTL_HOOKS.call_once(|| {
2259 // Only the two compctl hookdefs (complete.c:1712-1716): registering
2260 // the whole zle_main::boot_ set would also define "complete", and a
2261 // defined-but-empty "complete" hook stops docompletion from falling
2262 // back to do_completion.
2263 for name in ["compctl_make", "compctl_cleanup"] {
2264 if gethookdef(name).is_null() {
2265 let h = Box::into_raw(Box::new(crate::ported::zsh_h::hookdef {
2266 next: std::ptr::null_mut(),
2267 name: name.to_string(),
2268 def: None,
2269 flags: 0,
2270 funcs: std::ptr::null_mut(),
2271 }));
2272 if crate::ported::module::addhookdef(h) != 0 {
2273 unsafe { drop(Box::from_raw(h)) };
2274 }
2275 }
2276 }
2277 crate::ported::zle::compctl::boot_();
2278 });
2279 // c:995 — runhookdef(COMPCTLCLEANUPHOOK, NULL).
2280 // c:995 — `runhookdef(COMPCTLCLEANUPHOOK, NULL)`; COMPCTLCLEANUPHOOK is
2281 // `comphooks + 3` (comp.h:450) = the "compctl_cleanup" hookdef.
2282 runhookdef(gethookdef("compctl_cleanup"), std::ptr::null_mut()); // c:995
2283
2284 if oldlist.load(Ordering::Relaxed) != 0 {
2285 // c:997
2286 nmatches.store(onm, Ordering::Relaxed); // c:998
2287 diffmatches.store(odm, Ordering::Relaxed); // c:999
2288 VALIDLIST.store(1, Ordering::Relaxed); // c:1000
2289 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2290 if let Ok(last) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2291 *g = last.clone(); // c:1001
2292 }
2293 }
2294 if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
2295 let last_l = lastlmatches
2296 .get_or_init(|| Mutex::new(None))
2297 .lock()
2298 .ok()
2299 .and_then(|g| g.clone());
2300 *g = last_l; // c:1007
2301 }
2302 // c:1008-1011 — `if (pmatches) freematches(pmatches, 1)`.
2303 let drained = pmatches
2304 .get_or_init(|| Mutex::new(Vec::new()))
2305 .lock()
2306 .map(|mut g| std::mem::take(&mut *g))
2307 .unwrap_or_default();
2308 freematches(drained, 1); // c:1009-1010
2309 hasperm.store(0, Ordering::Relaxed); // c:1011
2310 redup(osi); // c:1012
2311 return 0; // c:1013
2312 }
2313 if !lastmatches
2314 .get_or_init(|| Mutex::new(Vec::new()))
2315 .lock()
2316 .map(|g| g.is_empty())
2317 .unwrap_or(true)
2318 {
2319 // c:1015
2320 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2321 g.clear(); // c:1016-1017
2322 }
2323 }
2324 permmatches(1); // c:1019
2325 // c:1020-1029 — copy pmatches → amatches/lastmatches; swap holders.
2326 let p_snap = pmatches
2327 .get_or_init(|| Mutex::new(Vec::new()))
2328 .lock()
2329 .ok()
2330 .map(|g| g.clone())
2331 .unwrap_or_default();
2332 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2333 *g = p_snap.clone(); // c:1020
2334 }
2335 lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1021
2336 lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1022
2337 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2338 *g = p_snap; // c:1024
2339 }
2340 let lm_snap = lmatches
2341 .get_or_init(|| Mutex::new(None))
2342 .lock()
2343 .ok()
2344 .and_then(|g| g.clone());
2345 if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
2346 *g = lm_snap; // c:1025
2347 }
2348 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2349 g.clear(); // c:1026
2350 }
2351 hasperm.store(0, Ordering::Relaxed); // c:1027
2352 hasoldlist.store(1, Ordering::Relaxed); // c:1028
2353
2354 let any_nm =
2355 nmatches.load(Ordering::Relaxed) != 0 || nmessages.load(Ordering::Relaxed) != 0;
2356 let errset = errflag_get();
2357 tracing::debug!(
2358 target: "compsys_args",
2359 nm = nmatches.load(Ordering::Relaxed),
2360 nmsg = nmessages.load(Ordering::Relaxed),
2361 errset,
2362 "makecomplist RETURN"
2363 );
2364 if any_nm && !errset {
2365 // c:1030
2366 VALIDLIST.store(1, Ordering::Relaxed); // c:1031
2367 redup(osi); // c:1032
2368 return 0; // c:1033
2369 }
2370 redup(osi); // c:1035
2371 return 1; // c:1036
2372 } else {
2373 // c:1038
2374 // c:1040-1047 — compctl dispatch via COMPCTLMAKEHOOK.
2375 let mut dat = Ccmakedat {
2376 str: Some(s_owned.clone()), // c:1042
2377 incmd, // c:1043
2378 lst, // c:1044
2379 };
2380 // !!! RUST-ONLY — the comphooks hookdefs (complete.c:1712 `comphooks`) and
2381 // compctl.c:4049 `boot_` (which addhookfunc()s ccmakehookfn /
2382 // cccleanuphookfn on them) are never reached by zshrs's static module
2383 // registration outside a running ZLE, so attach both on first use.
2384 COMPCTL_HOOKS.call_once(|| {
2385 // Only the two compctl hookdefs (complete.c:1712-1716): registering
2386 // the whole zle_main::boot_ set would also define "complete", and a
2387 // defined-but-empty "complete" hook stops docompletion from falling
2388 // back to do_completion.
2389 for name in ["compctl_make", "compctl_cleanup"] {
2390 if gethookdef(name).is_null() {
2391 let h = Box::into_raw(Box::new(crate::ported::zsh_h::hookdef {
2392 next: std::ptr::null_mut(),
2393 name: name.to_string(),
2394 def: None,
2395 flags: 0,
2396 funcs: std::ptr::null_mut(),
2397 }));
2398 if crate::ported::module::addhookdef(h) != 0 {
2399 unsafe { drop(Box::from_raw(h)) };
2400 }
2401 }
2402 }
2403 crate::ported::zle::compctl::boot_();
2404 });
2405 // c:1046 — `runhookdef(COMPCTLMAKEHOOK, (void *) &dat)`; COMPCTLMAKEHOOK is
2406 // `comphooks + 2` (comp.h:449) = the "compctl_make" hookdef.
2407 runhookdef(
2408 gethookdef("compctl_make"),
2409 &mut dat as *mut Ccmakedat as *mut std::ffi::c_void,
2410 ); // c:1046
2411 runhookdef(gethookdef("compctl_cleanup"), std::ptr::null_mut()); // c:1049
2412 return dat.lst; // c:1050
2413 }
2414}
2415
2416// =====================================================================
2417// multiquote — `Src/Zle/compcore.c:1065`.
2418// =====================================================================
2419
2420/// Port of `mod_export char *multiquote(char *s, int ign)` from
2421/// compcore.c:1064.
2422pub fn multiquote(s: &str, ign: i32) -> String {
2423 // c:1065
2424 // c:1067 — `char *p = compqstack;`. C takes the global POINTER and
2425 // walks it; nothing is copied. `compqstack` holds one byte per open
2426 // quoting level (c:301-308 allocates 2 bytes; c:1853-1859 pushes one
2427 // more per nesting level), so it is a handful of bytes at most.
2428 // Copying it into a stack buffer keeps the read allocation-free —
2429 // `multiquote` runs twice per completion match (compmatch.c:1160 and
2430 // c:1172 via `comp_match`), so a 46765-match `compadd -k functions`
2431 // was doing ~94k heap clones of a one-byte String.
2432 let mut qbuf = [0u8; 32];
2433 let mut qspill = String::new();
2434 let qlen = match COMPQSTACK.get_or_init(|| Mutex::new(String::new())).lock() {
2435 Ok(g) => {
2436 let b = g.as_bytes();
2437 if b.len() <= qbuf.len() {
2438 qbuf[..b.len()].copy_from_slice(b);
2439 b.len()
2440 } else {
2441 // Deeper nesting than the buffer holds: fall back to an
2442 // owned copy so no level is ever dropped.
2443 qspill = g.clone();
2444 usize::MAX
2445 }
2446 }
2447 Err(_) => 0,
2448 };
2449 let p_bytes: &[u8] = if qlen == usize::MAX {
2450 qspill.as_bytes()
2451 } else {
2452 &qbuf[..qlen]
2453 };
2454 if !p_bytes.is_empty() && (ign == 0 || p_bytes.len() > 1) {
2455 // c:1070
2456 let start = if ign != 0 { 1 } else { 0 }; // c:1071
2457 let mut cur = s.to_string();
2458 for &q in &p_bytes[start..] {
2459 // c:1073
2460 let qt = match q as i32 {
2461 // c:1074
2462 x if x == QT_BACKSLASH => QT_BACKSLASH,
2463 x if x == QT_SINGLE => QT_SINGLE,
2464 x if x == QT_DOUBLE => QT_DOUBLE,
2465 x if x == QT_DOLLARS => QT_DOLLARS,
2466 _ => QT_BACKSLASH,
2467 };
2468 cur = crate::ported::utils::quotestring(&cur, qt);
2469 }
2470 cur // c:1092
2471 } else {
2472 s.to_string() // c:1092
2473 }
2474}
2475
2476// =====================================================================
2477// tildequote — `Src/Zle/compcore.c:1092`.
2478// =====================================================================
2479
2480/// Port of `mod_export char *tildequote(char *s, int ign)` from
2481/// compcore.c:1091.
2482pub fn tildequote(s: &str, ign: i32) -> String {
2483 // c:1092
2484 let bytes = s.as_bytes(); // c:1092
2485 let tilde = !bytes.is_empty() && bytes[0] == b'~'; // c:1097
2486 let staged = if tilde {
2487 // c:1098
2488 let mut tmp = String::with_capacity(s.len());
2489 tmp.push('x');
2490 tmp.push_str(&s[1..]);
2491 tmp
2492 } else {
2493 s.to_string()
2494 };
2495 let mut quoted = multiquote(&staged, ign); // c:1099
2496 if tilde && !quoted.is_empty() {
2497 // c:1100
2498 let mut new_q = String::with_capacity(quoted.len());
2499 let mut swapped = false;
2500 for c in quoted.chars() {
2501 if !swapped && c == 'x' {
2502 new_q.push('~');
2503 swapped = true;
2504 } else {
2505 new_q.push(c);
2506 }
2507 }
2508 quoted = new_q;
2509 }
2510 quoted // c:1101
2511}
2512
2513// =====================================================================
2514// check_param — `Src/Zle/compcore.c:1113`.
2515// =====================================================================
2516
2517/// Direct port of `static char *check_param(char *s, int set, int test)`
2518/// from compcore.c:1113. Walks backwards from cursor in `s` looking
2519/// for `$<name>`. When found and the cursor sits inside the name,
2520/// returns the byte index in `s` where the name starts; updates
2521/// `ispar`/`parq`/`eparq` (when `!test`) and `ipre`/`ripre`/`isuf`/
2522/// `parpre`/`parflags`/`mflags`/`wb`/`we`/`offs` (when `set`).
2523/// Returns `None` when there's no parameter expression at the cursor.
2524///
2525/// `untok` is the one parameter C does not have. Every C caller — compcore.c
2526/// c:952 and c:1722, compctl.c:2889 and compctl.c:3161 — passes a TOKENIZED
2527/// word, and the distinction is load-bearing: an escaped `\$` reaches c:1141
2528/// as a plain 0x24 — never the `String` token, its `Bnull` having been
2529/// chucked by the line-cleanup loop at zle_tricky.c:1881/1920 — so
2530/// C reads `echo \$PATH` as no parameter context and `echo $PATH` as one.
2531/// Pass `untok = false` (faithful) whenever the tokenized word is in hand.
2532/// The legacy compctl caller has only the untokenized word and passes
2533/// `true`, which lets the `$`/`{`/`(` literals stand in for their tokens at
2534/// the cost of that one path not seeing the escape.
2535pub fn check_param(s: &str, set: bool, test: bool, untok: bool) -> Option<usize> {
2536 // c:1113
2537
2538 // c:1117-1118 — zsfree(parpre); parpre = NULL.
2539 if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
2540 g.clear();
2541 }
2542
2543 if !test {
2544 // c:1120
2545 ispar.store(0, Ordering::Relaxed); // c:1121
2546 parq.store(0, Ordering::Relaxed); // c:1121
2547 eparq.store(0, Ordering::Relaxed); // c:1121
2548 }
2549
2550 let bytes = s.as_bytes(); // local view
2551 let offs_v = OFFS.load(Ordering::Relaxed) as usize; // c:1140 cursor in word
2552
2553 let mut found = false; // c:1115
2554 let mut qstring = false; // c:1115
2555 let mut p: usize = offs_v.min(bytes.len().saturating_sub(1)); // c:1140 p = s + offs
2556
2557 // c:1141 — `if (*p == String || *p == Qstring)`. A plain 0x24 is NOT a
2558 // hit: the lexer emits `String` for a live `$` and leaves an escaped
2559 // `\$` as `Bnull` + 0x24, whose `Bnull` the line-cleanup loop chucks
2560 // (zle_tricky.c:1881/1920) — so the bare byte reaching here means the
2561 // user quoted it. Only `untok = true` widens the test.
2562 let is_str = |c: char| c == Stringg || (untok && c == '$');
2563
2564 // c:1140-1162 — scan backward for `String` or `Qstring`.
2565 loop {
2566 if p < bytes.len() {
2567 let ch = char_at(bytes, p);
2568 if is_str(ch) || ch == Qstring {
2569 // c:1141
2570 let next = char_at(bytes, p + ch.len_utf8());
2571 let snull_next = is_str(ch) && next == Snull; // c:1151
2572 let qstr_quot = ch == Qstring && next == '\''; // c:1152
2573 if p < offs_v && !snull_next && !qstr_quot {
2574 found = true; // c:1154
2575 qstring = ch == Qstring; // c:1155
2576 break;
2577 }
2578 }
2579 }
2580 if p == 0 {
2581 break;
2582 } // c:1160
2583 p = prev_char_index(bytes, p);
2584 }
2585
2586 if found {
2587 // c:1166
2588 // c:1173-1174 — fold `$$$$` chains.
2589 while p > 0 {
2590 let prev = prev_char_index(bytes, p);
2591 let pc = char_at(bytes, prev);
2592 if is_str(pc) || pc == Qstring {
2593 p = prev;
2594 } else {
2595 break;
2596 }
2597 }
2598 loop {
2599 // c:1175-1176
2600 let n1 = p + char_at(bytes, p).len_utf8();
2601 if n1 >= bytes.len() {
2602 break;
2603 }
2604 let c1 = char_at(bytes, n1);
2605 let n2 = n1 + c1.len_utf8();
2606 if n2 >= bytes.len() {
2607 break;
2608 }
2609 let c2 = char_at(bytes, n2);
2610 if (is_str(c1) || c1 == Qstring) && (is_str(c2) || c2 == Qstring) {
2611 p = n2;
2612 } else {
2613 break;
2614 }
2615 }
2616 }
2617
2618 // c:1179 — guard against `$(`, `$[`, `$'`.
2619 let next_char = if p + 1 <= bytes.len() {
2620 let dollar_len = char_at(bytes, p).len_utf8();
2621 char_at(bytes, p + dollar_len)
2622 } else {
2623 '\0'
2624 };
2625 if !(found && next_char != Inpar && next_char != Inbrack && next_char != Snull) {
2626 return None; // c:1316
2627 }
2628
2629 // c:1181 — b = p + 1 (start of body), e = b initially.
2630 let dollar_len = char_at(bytes, p).len_utf8();
2631 let mut b: usize = p + dollar_len; // c:1181
2632 let mut br: i32 = 1; // c:1182
2633 let mut nest: i32 = 0; // c:1182
2634
2635 // c:1182 — `if (*b == Inbrace)`. The `untok = true` caller also takes the
2636 // literal `{`, which is how `${…}` reaches it.
2637 let brace_ch = char_at(bytes, b);
2638 if brace_ch == Inbrace || (untok && brace_ch == '{') {
2639 let (ib, ob) = if brace_ch == '{' {
2640 ('{', '}')
2641 } else {
2642 (Inbrace, Outbrace)
2643 };
2644 // c:1184
2645 // c:1188 — `if (!skipparens(Inbrace, Outbrace, &tb) && tb - s <= offs) return NULL;`
2646 let mut tb: &str = &s[b..];
2647 let bal = crate::ported::utils::skipparens(ib, ob, &mut tb);
2648 let tb_after = s.len() - tb.len();
2649 if bal == 0 && tb_after <= offs_v {
2650 return None; // c:1189
2651 }
2652
2653 b += brace_ch.len_utf8(); // c:1192 b++
2654 br += 1;
2655 // c:1193-1203 — skip leading `(...)` flag group. C has a
2656 // ternary `qstring ? skipparens('(',')',&b) : skipparens(Inpar,Outpar,&b)`
2657 // — two source-level skipparens calls. Mirror that explicitly
2658 // so the call-coverage metric matches C.
2659 let mut b_str: &str = &s[b..];
2660 // The `untok = true` caller sees the `(`/`)` of a `${(flags)name}`
2661 // group as literals in the non-qstring case too — the same
2662 // accommodation the `Inbrace`/`{` test above makes. Without it
2663 // `skipparens` was handed `Inpar` against a literal `(`, returned -1
2664 // (its "wrong opening char" code) instead of 0, `b` never advanced
2665 // past the flags, the name scan then found `(` where a name should be
2666 // and `check_param` bailed with `ispar == 0`: `${(k)<TAB>` never
2667 // reached the `-brace-parameter-` context at all.
2668 let flag_ret: i32 = if qstring || (untok && char_at(bytes, b) == '(') {
2669 crate::ported::utils::skipparens('(', ')', &mut b_str)
2670 } else {
2671 crate::ported::utils::skipparens(Inpar, Outpar, &mut b_str)
2672 };
2673 let after_flags_pos = s.len() - b_str.len();
2674 if flag_ret > 0 || after_flags_pos > offs_v {
2675 ispar.store(2, Ordering::Relaxed); // c:1201
2676 return None; // c:1202
2677 }
2678 b = after_flags_pos;
2679
2680 // c:1205 — detect `nest` from preceding `${ ${` chain.
2681 let mut tb = p;
2682 while tb > 0 {
2683 let prev = prev_char_index(bytes, tb);
2684 let pc = char_at(bytes, prev);
2685 if pc == Outbrace || pc == Inbrace {
2686 tb = prev;
2687 break;
2688 }
2689 tb = prev;
2690 }
2691 if tb > 0 {
2692 let cc = char_at(bytes, tb);
2693 let prev = prev_char_index(bytes, tb);
2694 let pp = char_at(bytes, prev);
2695 if cc == Inbrace && (pp == Stringg || cc == Qstring) {
2696 nest = 1; // c:1207
2697 }
2698 }
2699 }
2700
2701 // c:1212-1213 — skip `^=~` prefix flags.
2702 while b < bytes.len() {
2703 let c = char_at(bytes, b);
2704 if c == '^' || c == Hat || c == '=' || c == Equals || c == '~' || c == Tilde {
2705 b += c.len_utf8();
2706 } else {
2707 break;
2708 }
2709 }
2710 // c:1215 — `#` / `+` length-prefix.
2711 if b < bytes.len() {
2712 let c = char_at(bytes, b);
2713 if c == '#' || c == Pound || c == '+' {
2714 b += c.len_utf8();
2715 }
2716 }
2717
2718 let mut e: usize = b; // c:1219
2719 if br != 0 {
2720 // c:1220
2721 let qopen = if test { Dnull } else { '"' };
2722 while e < bytes.len() && char_at(bytes, e) == qopen {
2723 // c:1221
2724 e += qopen.len_utf8();
2725 parq.fetch_add(1, Ordering::Relaxed); // c:1221
2726 }
2727 if !test {
2728 b = e;
2729 } // c:1223
2730 }
2731
2732 // c:1226-1252 — find end of name.
2733 if e < bytes.len() {
2734 let c = char_at(bytes, e);
2735 let one_char_name = matches!(c,
2736 ch if ch == Quest || ch == Star || ch == Stringg || ch == Qstring
2737 || ch == '?' || ch == '*' || ch == '$' || ch == '-' || ch == '!' || ch == '@');
2738 if one_char_name {
2739 // c:1230
2740 e += c.len_utf8();
2741 } else if c.is_ascii_digit() {
2742 // c:1232
2743 while e < bytes.len() && char_at(bytes, e).is_ascii_digit() {
2744 // c:1233
2745 e += 1;
2746 }
2747 } else {
2748 // c:1232-1241 — the `itype_end(e, INAMESPC, 0)` do/while:
2749 //
2750 // do { e = ie;
2751 // if (comppatmatch && *comppatmatch &&
2752 // (*e == Star || *e == Quest)) ie = e + 1;
2753 // else ie = itype_end(e, …);
2754 // } while (ie != e);
2755 //
2756 // The loop, not just its first iteration, is what lets a glob
2757 // metacharacter sit INSIDE the parameter name when
2758 // `$compstate[pattern_match]` is on: `$fo*ba<TAB>` must treat
2759 // `fo*ba` as one name. The port ran a single `walk_namespace`
2760 // and stopped at the `*`, so pattern parameter-name completion
2761 // saw the name end early and completed the wrong span.
2762 let patmatch_on = comppatmatch
2763 .get_or_init(|| Mutex::new(None))
2764 .lock()
2765 .ok()
2766 .and_then(|g| g.clone())
2767 .map(|v| !v.is_empty())
2768 .unwrap_or(false); // c:1235
2769 let mut ie = e + walk_namespace(&bytes[e..]); // c:1232
2770 loop {
2771 e = ie; // c:1234
2772 if e >= bytes.len() {
2773 break;
2774 }
2775 let ec = char_at(bytes, e);
2776 if patmatch_on && (ec == Star || ec == Quest) {
2777 ie = e + ec.len_utf8(); // c:1237
2778 } else {
2779 ie = e + walk_namespace(&bytes[e..]); // c:1239
2780 }
2781 if ie == e {
2782 break; // c:1240
2783 }
2784 }
2785 if e == b && c == '.' {
2786 // c:1242-1250 — a lone `.` counts as an incomplete name.
2787 e += 1;
2788 }
2789 }
2790 }
2791
2792 // c:1259 — `if (offs <= e - s && offs >= b - s)`.
2793 if offs_v <= e && offs_v >= b {
2794 // c:1263 — strip trailing `"`s when br set.
2795 if br != 0 {
2796 let qopen = if test { Dnull } else { '"' };
2797 let mut pq = e;
2798 while pq < bytes.len() && char_at(bytes, pq) == qopen {
2799 pq += qopen.len_utf8();
2800 parq.fetch_sub(1, Ordering::Relaxed);
2801 eparq.fetch_add(1, Ordering::Relaxed);
2802 }
2803 }
2804 if test {
2805 // c:1269
2806 return Some(b); // c:1270
2807 }
2808 if set {
2809 // c:1273
2810 if br >= 2 {
2811 // c:1274
2812 mflags.fetch_or(CMF_PARBR, Ordering::Relaxed); // c:1275
2813 if nest != 0 {
2814 // c:1276
2815 mflags.fetch_or(CMF_PARNEST, Ordering::Relaxed); // c:1277
2816 }
2817 }
2818 // c:1280 — `isuf = dupstring(e); untokenize(isuf)`.
2819 // `s` is the word lifted off the METAFIED line, so its content
2820 // bytes are metafied (`Meta` = 0x83 followed by `c ^ 32`) and are
2821 // not valid UTF-8. `dupstring` copies bytes; `from_utf8_lossy`
2822 // rewrote each escape as a 3-byte U+FFFD, which both destroyed
2823 // the character and shifted every later offset. `b`/`e` are byte
2824 // offsets into these same bytes, so the split stays byte-exact.
2825 let mut tail = unsafe { String::from_utf8_unchecked(bytes[e..].to_vec()) };
2826 tail = strip_tokens(&tail); // crate::lex::untokenize substitute
2827 if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
2828 *g = tail;
2829 }
2830 // c:1284 — `ripre = dyncat(ripre, s_through_b)` (the spec tree
2831 // e73499b372 has it at c:1279). Metafied bytes, byte-exact (see
2832 // the `isuf` split above).
2833 let head = unsafe { String::from_utf8_unchecked(bytes[..b].to_vec()) };
2834 if let Ok(mut g) = ripre.get_or_init(|| Mutex::new(String::new())).lock() {
2835 *g = format!("{}{}", *g, head);
2836 }
2837 if let Ok(mut g) = ipre.get_or_init(|| Mutex::new(String::new())).lock() {
2838 *g = strip_tokens(&format!("{}{}", *g, head));
2839 }
2840 }
2841 // c:1285-1286 — save prefix for compfunc.
2842 let cf_active = compfunc
2843 .get_or_init(|| Mutex::new(None))
2844 .lock()
2845 .ok()
2846 .and_then(|g| g.clone())
2847 .map(|s| !s.is_empty())
2848 .unwrap_or(false);
2849 if cf_active {
2850 let pf = if br >= 2 {
2851 CMF_PARBR | (if nest != 0 { CMF_PARNEST } else { 0 })
2852 } else {
2853 0
2854 };
2855 parflags.store(pf, Ordering::Relaxed); // c:1287
2856 // c:1290 `untokenize(parpre = ztrdup(s))` — same metafied
2857 // prefix as the `ripre` copy above, byte-exact.
2858 let head = unsafe { String::from_utf8_unchecked(bytes[..b].to_vec()) };
2859 if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
2860 *g = strip_tokens(&head); // c:1290
2861 }
2862 }
2863 // c:1293 — adjust wb/we/offs.
2864 let off_delta = b as i32;
2865 OFFS.fetch_sub(off_delta, Ordering::Relaxed); // c:1294
2866 let new_offs = OFFS.load(Ordering::Relaxed);
2867 let zlc = ZLEMETACS.load(Ordering::Relaxed);
2868 WB.store(zlc - new_offs, Ordering::Relaxed); // c:1295
2869 WE.store(
2870 WB.load(Ordering::Relaxed) + (e - b) as i32,
2871 Ordering::Relaxed,
2872 ); // c:1296
2873 ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1297
2874 return Some(b); // c:1299
2875 } else if offs_v > e && e < bytes.len() && char_at(bytes, e) == ':' {
2876 // c:1312
2877 // c:1313-1316 — colon-modifier guess.
2878 let offsptr = offs_v;
2879 let mut e2 = e;
2880 while e2 < offsptr && e2 < bytes.len() {
2881 let c = char_at(bytes, e2);
2882 if c != ':' && !c.is_alphanumeric() {
2883 break;
2884 }
2885 e2 += c.len_utf8();
2886 }
2887 ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1316
2888 return None; // c:1317
2889 }
2890
2891 let _ = (Bnull,); // silence unused-import warning if Bnull not hit
2892 None // c:1320
2893}
2894
2895// =====================================================================
2896// rembslash — `Src/Zle/compcore.c:1323`.
2897// =====================================================================
2898
2899/// Port of `mod_export char *rembslash(char *s)` from compcore.c:1322.
2900///
2901/// "Strip backslash escapes from a token, treating `\X` as `X`."
2902pub fn rembslash(s: &str) -> String {
2903 // c:1323
2904 let mut result = String::with_capacity(s.len()); // c:1323
2905 let mut chars = s.chars().peekable(); // c:1327
2906 while let Some(c) = chars.next() {
2907 if c == '\\' {
2908 // c:1328
2909 if let Some(nxt) = chars.next() {
2910 // c:1329
2911 result.push(nxt);
2912 }
2913 } else {
2914 result.push(c); // c:1343-1333
2915 }
2916 }
2917 result // c:1343
2918}
2919
2920// =====================================================================
2921// remsquote — `Src/Zle/compcore.c:1343`.
2922// =====================================================================
2923
2924/// Port of `mod_export int remsquote(char *s)` from compcore.c:1342.
2925pub fn remsquote(s: &mut String) -> i32 {
2926 // c:1343
2927 let rcquotes = isset(RCQUOTES); // c:1343
2928 let qa: usize = if rcquotes { 1 } else { 3 };
2929
2930 let bytes = s.as_bytes(); // c:1346
2931 let mut t = Vec::<u8>::with_capacity(bytes.len());
2932 let mut ret: i32 = 0;
2933 let mut i = 0;
2934 while i < bytes.len() {
2935 // c:1348
2936 let matched = if qa == 1 {
2937 // c:1349
2938 i + 1 < bytes.len() && bytes[i] == b'\'' && bytes[i + 1] == b'\''
2939 } else {
2940 i + 3 < bytes.len() // c:1351
2941 && bytes[i] == b'\''
2942 && bytes[i + 1] == b'\\'
2943 && bytes[i + 2] == b'\''
2944 && bytes[i + 3] == b'\''
2945 };
2946 if matched {
2947 ret += qa as i32; // c:1352
2948 t.push(b'\''); // c:1353
2949 i += qa + 1; // c:1354
2950 } else {
2951 t.push(bytes[i]); // c:1356
2952 i += 1;
2953 }
2954 }
2955 *s = String::from_utf8(t).unwrap_or_default(); // c:1357
2956 ret // c:1366
2957}
2958
2959// =====================================================================
2960// ctokenize — `Src/Zle/compcore.c:1366`.
2961// =====================================================================
2962
2963/// Port of `mod_export char *ctokenize(char *p)` from compcore.c:1365.
2964///
2965/// C calls `tokenize(p)` first then walks the string replacing
2966/// unescaped `$`/`{`/`}` with the token bytes `String`/`Inbrace`/
2967/// `Outbrace`. Backslash-escaped variants become `Bnull`.
2968pub fn ctokenize(p: &str) -> String {
2969 // c:1366
2970 // c:1370 — `tokenize(p);` ran FIRST in C and was missing here entirely,
2971 // so the glob metacharacters (`*`, `?`, `[`, `]`, `~`, `^`, `#`, …) that
2972 // `tokenize` turns into their high-bit token chars stayed literal. Every
2973 // consumer of `comp_str(untok=0)` (c:1411-1414) therefore received a
2974 // string that `haswilds`/`patcompile` read as plain text, so a typed
2975 // pattern in $PREFIX/$SUFFIX was never seen as a pattern. The loop below
2976 // is only the extra `$`/`{`/`}` pass C runs after tokenize.
2977 let mut tokenized = p.to_string();
2978 crate::ported::glob::tokenize(&mut tokenized); // c:1370
2979 let p: &str = &tokenized;
2980 let bytes = p.as_bytes(); // c:1372
2981 let mut out = Vec::<u8>::with_capacity(bytes.len());
2982 let mut bslash = false; // c:1369
2983 let mut prev_idx: Option<usize> = None;
2984 let mut i = 0;
2985 while i < bytes.len() {
2986 let b = bytes[i]; // c:1373
2987 if b == b'\\' {
2988 // c:1374
2989 bslash = true;
2990 out.push(b);
2991 prev_idx = Some(out.len() - 1);
2992 } else {
2993 if b == b'$' || b == b'{' || b == b'}' {
2994 // c:1377
2995 if bslash {
2996 // c:1378
2997 if let Some(pi) = prev_idx {
2998 // c:1379
2999 out.truncate(pi);
3000 let mut buf = [0u8; 4];
3001 out.extend_from_slice(Bnull.encode_utf8(&mut buf).as_bytes());
3002 }
3003 out.push(b);
3004 } else {
3005 let tok = if b == b'$' {
3006 Stringg
3007 }
3008 // c:1381
3009 else if b == b'{' {
3010 Inbrace
3011 }
3012 // c:1382
3013 else {
3014 Outbrace
3015 }; // c:1382
3016 let mut buf = [0u8; 4];
3017 out.extend_from_slice(tok.encode_utf8(&mut buf).as_bytes());
3018 }
3019 } else {
3020 out.push(b);
3021 }
3022 bslash = false; // c:1384
3023 prev_idx = Some(out.len().saturating_sub(1));
3024 }
3025 i += 1;
3026 }
3027 String::from_utf8(out).unwrap_or_default() // c:1403
3028}
3029
3030// =====================================================================
3031// comp_str — `Src/Zle/compcore.c:1403`.
3032// =====================================================================
3033
3034/// Port of `mod_export char *comp_str(int *ipl, int *pl, int untok)`
3035/// from compcore.c:1402.
3036pub fn comp_str(untok: bool) -> (String, i32, i32) {
3037 // c:1403
3038 let mut p = COMPPREFIX
3039 .get_or_init(|| Mutex::new(String::new())) // c:1405
3040 .lock()
3041 .unwrap()
3042 .clone();
3043 let mut s = COMPSUFFIX
3044 .get_or_init(|| Mutex::new(String::new())) // c:1406
3045 .lock()
3046 .unwrap()
3047 .clone();
3048 let ip = COMPIPREFIX
3049 .get_or_init(|| Mutex::new(String::new())) // c:1407
3050 .lock()
3051 .unwrap()
3052 .clone();
3053 if !untok {
3054 // c:1411
3055 p = ctokenize(&p); // c:1412
3056 p = p.chars().filter(|&c| c != Bnull).collect(); // c:1413 remnulargs
3057 s = ctokenize(&s); // c:1414
3058 s = s.chars().filter(|&c| c != Bnull).collect(); // c:1415
3059 }
3060 let lp = p.len() as i32; // c:1417
3061 let lip = ip.len() as i32; // c:1419
3062 let mut str = String::with_capacity(ip.len() + p.len() + s.len() + 1); // c:1420
3063 str.push_str(&ip); // c:1435
3064 str.push_str(&p); // c:1435
3065 str.push_str(&s); // c:1435
3066 (str, lip, lp) // c:1435-1430
3067}
3068
3069// =====================================================================
3070// comp_quoting_string — `Src/Zle/compcore.c:1435`.
3071// =====================================================================
3072
3073/// Port of `mod_export char *comp_quoting_string(int stype)` from
3074/// compcore.c:1434.
3075pub fn comp_quoting_string(stype: i32) -> &'static str {
3076 // c:1435
3077 match stype {
3078 // c:1435
3079 x if x == QT_SINGLE => "'", // c:1439-1440
3080 x if x == QT_DOUBLE => "\"", // c:1441-1442
3081 x if x == QT_DOLLARS => "$'", // c:1443-1444
3082 _ => {
3083 // c:1445
3084 let _ = QT_BACKSLASH;
3085 "\\" // c:1446
3086 }
3087 }
3088}
3089
3090// =====================================================================
3091// set_comp_sep — `Src/Zle/compcore.c:1460`.
3092// =====================================================================
3093
3094/// Direct port of `int set_comp_sep(void)` from compcore.c:1458 —
3095/// the `compset -q` driver that re-parses the current completion
3096/// word splitting it on the IFS, then resubmits the right slice
3097/// as the new completion target.
3098///
3099/// Inputs are now published/correct: `wb`/`we`/`offs` are written by
3100/// `get_comp_string` (WB/WE/OFFS shared statics) and `compqstack` by
3101/// `callcompfunc`'s c:305 reset (deduped to `complete::COMPQSTACK`).
3102///
3103/// Byte model: all of C's single-metafied-byte index arithmetic (the
3104/// `inull` walk c:1774-1804, the `chuck` removals, the `s[swb-1-sqq+dq]`
3105/// indexing c:1830, the `p[soffs]` chuck c:1739) is performed on local
3106/// single-byte-metafied `Vec<u8>` buffers built by `to_sb` (each
3107/// token-null marker `Snull`/`Dnull`/`Bnull` maps to ONE byte
3108/// 0x9d/0x9e/0x9f; `Meta`-escapes stay two bytes) and converted back
3109/// with `from_sb` (byte -> `char`, the char-per-metafied-byte form the
3110/// downstream comp* globals expect). `wb`/`we`/`zlemetacs` are consumed
3111/// as byte offsets. For ASCII completion words — command names, paths,
3112/// options, the dominant `compset -q` case — byte == char, so the
3113/// offsets are exact and the algorithm is a verifiable translation of C.
3114///
3115/// Known limitation (INHERITED from zshrs's input/lexer model, not a
3116/// defect of this port): for non-ASCII quoted words the shared cursor
3117/// model conflates byte and char units — `ingetc` steps the input by
3118/// char while `inbufct`/`zlemetall` are byte lengths (input.rs:355/540,
3119/// lex.rs:3013-3024) — so the incoming `wb`/`we`/`zlemetacs` diverge
3120/// from single-byte offsets. Tracked separately with the
3121/// `get_comp_string` quote-form tail; see that function's note.
3122///
3123/// The `QT_DOLLARS` (`$'...'`) arm (c:1613-1622) is fully wired: the
3124/// `getkeystring_with` decode applies `GETKEY_UPDATE_OFFSET`, so
3125/// both the `dolq` byte-count delta and the `css += zlemetacs - j`
3126/// cursor micro-adjustment are computed (inheriting the same non-ASCII
3127/// byte/char caveat noted above).
3128pub fn set_comp_sep() -> i32 {
3129 use crate::ported::lex::{
3130 ctxtlex, noaliases, set_noaliases, set_tok, set_tokstr, tok, tokstr, untokenize,
3131 LEX_LEXFLAGS,
3132 };
3133 use crate::ported::string::{dupstring_wlen, tricat};
3134 use crate::ported::utils::getkeystring_with;
3135 use crate::ported::zle::comp_h::{CP_QUOTE, CP_QUOTING};
3136 // COMPPREFIX/COMPSUFFIX/COMPIPREFIX/COMPQSTACK are already imported at
3137 // the module top; only the remaining comp* globals are pulled in here.
3138 use crate::ported::zle::complete::{
3139 COMPCURRENT, COMPISUFFIX, COMPQIPREFIX, COMPQISUFFIX, COMPQUOTE, COMPQUOTING, COMPWORDS,
3140 };
3141 use crate::ported::zle::zle_utils::{zle_restore_positions, zle_save_positions};
3142 use crate::ported::zsh_h::{
3143 Meta, COMPLETEINWORD, ENDINPUT, GETKEYS_DOLLARS_QUOTE, GETKEY_UPDATE_OFFSET, LEXERR,
3144 LEXFLAGS_ZLE, STRING_LEX,
3145 };
3146
3147 // ── single-byte-metafied <-> char-per-byte String conversions ──
3148 // Each metafied byte is one `char` in the port's string world, so
3149 // `c as u8` for c < 0x100 reconstructs the C single-byte buffer
3150 // (Meta-escapes = 0x83 + xor). A token char (crate::token_char) is its
3151 // one C token byte; real U+0084..=U+00A2 text is metafied the way C
3152 // holds data in that range, so it cannot come back as a token.
3153 let to_sb = |s: &str| -> Vec<u8> {
3154 let mut v = Vec::with_capacity(s.len());
3155 for c in s.chars() {
3156 let cp = c as u32;
3157 if let Some(t) = crate::token_char::token_byte(c) {
3158 v.push(t);
3159 } else if (0x84..=0xa2).contains(&cp) {
3160 v.push(Meta);
3161 v.push(cp as u8 ^ 32);
3162 } else if cp < 0x100 {
3163 v.push(cp as u8);
3164 } else {
3165 let mut b = [0u8; 4];
3166 v.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
3167 }
3168 }
3169 v
3170 };
3171 let from_sb = |b: &[u8]| -> String {
3172 let mut out = String::with_capacity(b.len());
3173 let mut i = 0;
3174 while i < b.len() {
3175 let x = b[i];
3176 if x == Meta && i + 1 < b.len() && (0x84..=0xa2).contains(&(b[i + 1] ^ 32)) {
3177 out.push((b[i + 1] ^ 32) as char); // data char to_sb metafied
3178 i += 2;
3179 continue;
3180 }
3181 out.push(crate::token_char::token_char_from_byte(x).unwrap_or(x as char));
3182 i += 1;
3183 }
3184 out
3185 };
3186 let snap = |g: &'static OnceLock<Mutex<String>>| -> String {
3187 g.get_or_init(|| Mutex::new(String::new()))
3188 .lock()
3189 .unwrap()
3190 .clone()
3191 };
3192 let put = |g: &'static OnceLock<Mutex<String>>, v: String| {
3193 *g.get_or_init(|| Mutex::new(String::new())).lock().unwrap() = v;
3194 };
3195
3196 // marker byte values (Snull/Dnull/Bnull/Stringg/Qstring are `char`).
3197 let snull_b = Snull as u32 as u8; // 0x9d
3198 let dnull_b = Dnull as u32 as u8; // 0x9e
3199 let bnull_b = Bnull as u32 as u8; // 0x9f
3200 let stringg_b = Stringg as u32 as u8; // 0x85 ($)
3201 let qstring_b = Qstring as u32 as u8; // 0x8c ("$)
3202 let meta_b: u8 = Meta; // 0x83
3203
3204 // c:1460 — s = comp_str(&lip, &lp, 1) with untok = 1.
3205 let (s_full, lip, lp) = comp_str(true);
3206 // c:1473 — int owe = we, owb = wb.
3207 let owe = WE.load(Ordering::Relaxed);
3208 let owb = WB.load(Ordering::Relaxed);
3209
3210 let mut foo: Vec<String> = Vec::new(); // c:1462 newlinklist()
3211
3212 // c:1478-1500 — locals.
3213 let mut swb: i32 = 0; // c:1490
3214 let mut swe: i32 = 0;
3215 let scs: i32; // cursor (fixed once set below)
3216 let mut soffs: i32 = 0;
3217 let ne = *crate::ported::utils::noerrs_lock().lock().unwrap(); // c:1479
3218 let mut got = false;
3219 let mut i: i32 = 0;
3220 let mut cur: i32 = -1;
3221 let mut css: i32 = 0;
3222 let mut remq = false;
3223 let mut dq: i32 = 0;
3224 let mut sq: i32 = 0;
3225 let qttype: i32;
3226 let mut sqq: i32 = 0;
3227 let mut lsq: i32 = 0;
3228 let mut qa: i32 = 0;
3229 let mut dolq: i32 = 0;
3230 let ois = INSTRING.load(Ordering::Relaxed); // c:1471
3231 let oib = INBACKT.load(Ordering::Relaxed);
3232 let noffs = lp; // active-prefix length
3233 let ona = noaliases();
3234
3235 // c:1476 — s += lip; wb += lip; untokenize(s).
3236 let s_after: String = if (lip as usize) <= s_full.len() {
3237 s_full[lip as usize..].to_string()
3238 } else {
3239 String::new()
3240 };
3241 WB.store(owb + lip, Ordering::Relaxed);
3242 let s = untokenize(&s_after);
3243 let s_b_full = to_sb(&s); // reconstructed arg, single-byte
3244
3245 // c:1483-1488 — zle_save_positions / addedx / noerrs / zcontext / lexflags.
3246 zle_save_positions();
3247 let ol = snap(&ZLEMETALINE);
3248 ADDEDX.store(1, Ordering::Relaxed);
3249 *crate::ported::utils::noerrs_lock().lock().unwrap() = 1;
3250 let lex_saved = lexsave(); // zcontext_save()
3251 LEX_LEXFLAGS.set(LEXFLAGS_ZLE);
3252
3253 // c:1494-1499 — tl = strlen(s)+2; tmp = " " + s[..noffs] + 'x' + s[noffs..].
3254 let noffs_u = (noffs.max(0) as usize).min(s_b_full.len());
3255 let tl0 = s_b_full.len() as i32 + 2; // strlen(s) + 2
3256 let mut tmp_b: Vec<u8> = Vec::with_capacity(s_b_full.len() + 3);
3257 tmp_b.push(b' ');
3258 tmp_b.extend_from_slice(&s_b_full[..noffs_u]);
3259 scs = 1 + noffs;
3260 ZLEMETACS.store(scs, Ordering::Relaxed);
3261 tmp_b.push(b'x');
3262 tmp_b.extend_from_slice(&s_b_full[noffs_u..]);
3263 let mut tmp = from_sb(&tmp_b);
3264
3265 // c:1501-1640 — quote-stack head processing.
3266 let compqstack_s = snap(&COMPQSTACK);
3267 qttype = compqstack_s
3268 .chars()
3269 .next()
3270 .map(|c| c as i32)
3271 .unwrap_or(QT_NONE);
3272 let qstack2 = compqstack_s
3273 .chars()
3274 .nth(1)
3275 .map(|c| c as u32 != 0)
3276 .unwrap_or(false); // compqstack[1]
3277 if qttype == QT_BACKSLASH {
3278 // c:1503-1506
3279 remq = true;
3280 tmp = rembslash(&tmp);
3281 } else if qttype == QT_SINGLE {
3282 // c:1508-1514
3283 qa = if isset(RCQUOTES) { 1 } else { 3 };
3284 let mut t = tmp.clone();
3285 sq = remsquote(&mut t);
3286 tmp = t;
3287 } else if qttype == QT_DOUBLE {
3288 // c:1516-1543 — strip \\ and \" pairs, tracking zlemetacs/css/dq.
3289 let mut v = to_sb(&tmp);
3290 let mut j: i32 = 0;
3291 let mut pi = 0usize;
3292 let mut zcs = ZLEMETACS.load(Ordering::Relaxed);
3293 while pi < v.len() {
3294 let c = v[pi];
3295 let nxt = v.get(pi + 1).copied();
3296 if c == b'\\' && (nxt == Some(b'\\') || nxt == Some(b'"')) {
3297 dq += 1;
3298 v.remove(pi); // chuck(p): drop the backslash
3299 match v.get(pi).copied() {
3300 Some(b'"') => zcs -= 1,
3301 _ => {
3302 if j > zcs {
3303 zcs += 1;
3304 css += 1;
3305 }
3306 }
3307 }
3308 if pi >= v.len() {
3309 break; // if (!*p) break
3310 }
3311 }
3312 pi += 1;
3313 j += 1;
3314 }
3315 ZLEMETACS.store(zcs, Ordering::Relaxed);
3316 tmp = from_sb(&v);
3317 } else if qttype == QT_DOLLARS {
3318 // c:1613-1622 — string decode + dolq, with the GETKEY_UPDATE_OFFSET
3319 // cursor micro-adjustment. `j = zlemetacs` (c:1614); the decode
3320 // updates zlemetacs in place as pre-cursor escapes collapse; then
3321 // `css += zlemetacs - j` (c:1621) folds the delta into the word
3322 // offset. GETKEYS_DOLLARS_QUOTE carries the port's decode flags;
3323 // GETKEY_UPDATE_OFFSET enables the offset bookkeeping in
3324 // getkeystring_with.
3325 let j = ZLEMETACS.load(Ordering::Relaxed); // c:1614 — j = zlemetacs
3326 let mut zcs = j;
3327 let (dec, _consumed) = getkeystring_with(
3328 &tmp,
3329 (GETKEYS_DOLLARS_QUOTE | GETKEY_UPDATE_OFFSET) as u32,
3330 Some(&mut zcs),
3331 );
3332 ZLEMETACS.store(zcs, Ordering::Relaxed);
3333 let sl_new = to_sb(&dec).len() as i32;
3334 // c:1619 — dolq = tl - sl (bytes removed by $' quoting).
3335 dolq = tl0 - sl_new;
3336 // c:1621 — css += zlemetacs - j.
3337 css += zcs - j;
3338 tmp = dec;
3339 }
3340 let odq = dq; // c:1642
3341
3342 // c:1643-1647 — push into lexer, set the working metaline.
3343 crate::ported::input::inpush(&dupstrspace(&tmp), 0, None);
3344 put(&ZLEMETALINE, tmp.clone());
3345 ZLEMETALL.store(tl0 - 1, Ordering::Relaxed); // tl - addedx
3346 crate::ported::hist::strinbeg(0);
3347 set_noaliases(true);
3348
3349 // c:1650-1755 — the ctxtlex token loop.
3350 let mut ns_b: Vec<u8> = Vec::new();
3351 loop {
3352 ctxtlex();
3353 let mut tokv = tok();
3354 let mut ts_opt = tokstr();
3355 if tokv == LEXERR {
3356 // c:1654-1668 — odd active-quote count means unterminated
3357 // string; treat as STRING and drop a trailing space.
3358 match &ts_opt {
3359 None => break,
3360 Some(ts) => {
3361 let j = ts.chars().filter(|&c| c == Snull || c == Dnull).count();
3362 if j & 1 == 1 {
3363 tokv = STRING_LEX;
3364 set_tok(STRING_LEX);
3365 if ts.ends_with(' ') {
3366 let mut t = ts.clone();
3367 t.pop();
3368 set_tokstr(Some(t.clone()));
3369 ts_opt = Some(t);
3370 }
3371 }
3372 }
3373 }
3374 }
3375 if tokv == ENDINPUT {
3376 break; // c:1670
3377 }
3378 let mut last_p: Option<usize> = None;
3379 if let Some(ts) = ts_opt.as_ref() {
3380 if !ts.is_empty() {
3381 // c:1673-1680 — Bnull accounting against dq.
3382 if dq != 0 {
3383 let cs: Vec<char> = ts.chars().collect();
3384 let mut k = 0usize;
3385 while dq != 0 && k < cs.len() {
3386 if cs[k] == Bnull {
3387 dq -= 1;
3388 if cs.get(k + 1) == Some(&'\\') {
3389 dq -= 1;
3390 }
3391 }
3392 k += 1;
3393 }
3394 }
3395 // c:1681-1690 — single-quote lsq accounting.
3396 if qttype == QT_SINGLE {
3397 lsq = 0;
3398 for c in ts.chars() {
3399 if sq != 0 && c == Snull {
3400 sq -= qa;
3401 }
3402 if c == '\'' {
3403 sq -= qa;
3404 lsq += qa;
3405 }
3406 }
3407 } else {
3408 lsq = 0;
3409 }
3410 foo.push(ts.clone()); // addlinknode(foo, p = ztrdup(tokstr))
3411 last_p = Some(foo.len() - 1);
3412 }
3413 }
3414 // c:1694-1705 — capture the cursor word once lexflags cleared.
3415 if !got && LEX_LEXFLAGS.get() == 0 {
3416 if let Some(cur_idx) = last_p {
3417 got = true;
3418 cur = cur_idx as i32;
3419 swb = WB.load(Ordering::Relaxed) - dq - sq - dolq;
3420 swe = WE.load(Ordering::Relaxed) - dq - sq - dolq;
3421 sqq = lsq;
3422 soffs = ZLEMETACS.load(Ordering::Relaxed) - swb - css;
3423 // chuck(p + soffs): drop the injected 'x' from the node.
3424 let mut wb_bytes = to_sb(&foo[cur_idx]);
3425 if soffs >= 0 && (soffs as usize) < wb_bytes.len() {
3426 wb_bytes.remove(soffs as usize);
3427 }
3428 foo[cur_idx] = from_sb(&wb_bytes);
3429 ns_b = wb_bytes; // ns = dupstring(p)
3430 }
3431 }
3432 i += 1;
3433 if tokv == ENDINPUT || tokv == LEXERR {
3434 break; // c:1707 do-while
3435 }
3436 }
3437
3438 // c:1709-1719 — tear down lexer state, restore positions.
3439 set_noaliases(ona);
3440 crate::ported::hist::strinend();
3441 crate::ported::input::inpop();
3442 crate::ported::utils::errflag
3443 .fetch_and(!crate::ported::utils::ERRFLAG_ERROR, Ordering::Relaxed);
3444 *crate::ported::utils::noerrs_lock().lock().unwrap() = ne;
3445 lexrestore(lex_saved); // zcontext_restore()
3446 WB.store(owb, Ordering::Relaxed);
3447 WE.store(owe, Ordering::Relaxed);
3448 put(&ZLEMETALINE, ol);
3449 zle_restore_positions();
3450 if cur < 0 || i < 1 {
3451 return 1; // c:1721
3452 }
3453
3454 // c:1723-1733 — check_param dispatch with offs temporarily = soffs.
3455 let o_offs = OFFS.load(Ordering::Relaxed);
3456 OFFS.store(soffs, Ordering::Relaxed);
3457 if check_param(&from_sb(&ns_b), false, true, false).is_some() {
3458 for b in ns_b.iter_mut() {
3459 if *b == dnull_b {
3460 *b = b'"';
3461 } else if *b == snull_b {
3462 *b = b'\'';
3463 }
3464 }
3465 }
3466 OFFS.store(o_offs, Ordering::Relaxed);
3467
3468 // c:1735 — ts = untokenize(dupstring(ns)).
3469 let ts_str = untokenize(&from_sb(&ns_b));
3470 let ts_b = to_sb(&ts_str);
3471
3472 // c:1737-1772 — quote-form detection: instring / inbackt / autoq.
3473 let ns0 = ns_b.first().copied();
3474 let ns1 = ns_b.get(1).copied();
3475 let quote_open = ns0 == Some(snull_b)
3476 || ns0 == Some(dnull_b)
3477 || ((ns0 == Some(stringg_b) || ns0 == Some(qstring_b)) && ns1 == Some(snull_b));
3478 let mut ts_off = 0usize;
3479 if quote_open {
3480 let mut nsptr = 0usize; // C's nsptr offset into ns
3481 match ns0 {
3482 x if x == Some(snull_b) => INSTRING.store(QT_SINGLE, Ordering::Relaxed),
3483 x if x == Some(dnull_b) => INSTRING.store(QT_DOUBLE, Ordering::Relaxed),
3484 _ => {
3485 INSTRING.store(QT_DOLLARS, Ordering::Relaxed);
3486 nsptr += 1;
3487 ts_off += 1;
3488 swb += 1;
3489 }
3490 }
3491 INBACKT.store(0, Ordering::Relaxed);
3492 swb += 1;
3493 // c:1747 — if (nsptr[strlen(nsptr)-1] == *nsptr && nsptr[1]) swe--
3494 let ns_slice = &ns_b[nsptr.min(ns_b.len())..];
3495 if ns_slice.len() >= 2 && *ns_slice.last().unwrap() == ns_slice[0] {
3496 swe -= 1;
3497 }
3498 // c:1749-1753 — autoq from ts prefix.
3499 ts_off += 1; // ++tsptr
3500 let ts_prefix = from_sb(&ts_b[..ts_off.min(ts_b.len())]);
3501 let autoq_v = if qstack2 {
3502 String::new()
3503 } else {
3504 multiquote(&ts_prefix, 1)
3505 };
3506 put(&AUTOQ, autoq_v);
3507 } else {
3508 INSTRING.store(QT_NONE, Ordering::Relaxed);
3509 put(&AUTOQ, String::new());
3510 }
3511
3512 // c:1774-1804 — the inull walk: drop null markers from ns, adjusting
3513 // swb/scs/soffs. `scs` is copied to a mutable walker `scs_w`.
3514 let mut scs_w = scs;
3515 {
3516 let mut pi = 0usize;
3517 let mut wi = swb;
3518 while pi < ns_b.len() {
3519 let c = ns_b[pi];
3520 if crate::ported::ztype_h::inull(c) {
3521 let next = ns_b.get(pi + 1).copied();
3522 let next_truthy = next.is_some();
3523 if wi < scs_w && c == bnull_b {
3524 if next_truthy && remq {
3525 swb -= 2;
3526 }
3527 if odq != 0 {
3528 swb -= 1;
3529 if next == Some(b'\\') {
3530 swb -= 1;
3531 }
3532 }
3533 }
3534 if next_truthy || c != bnull_b {
3535 if c == bnull_b {
3536 if scs_w == wi + 1 {
3537 scs_w += 1;
3538 soffs += 1;
3539 }
3540 } else {
3541 let cond = scs_w > wi;
3542 wi -= 1; // C's post-decrement in `scs > i--`
3543 if cond {
3544 scs_w -= 1;
3545 }
3546 }
3547 } else if scs_w == swe {
3548 scs_w -= 1;
3549 }
3550 ns_b.remove(pi); // chuck(p--); loop p++ revisits index pi
3551 wi += 1;
3552 } else {
3553 pi += 1;
3554 wi += 1;
3555 }
3556 }
3557 }
3558
3559 // c:1806 — ns = ts (the untokenized copy, advanced past open quote).
3560 let mut ns_final_b = ts_b[ts_off.min(ts_b.len())..].to_vec();
3561
3562 // c:1808-1813 — backslash-quoting length fixup.
3563 let instr = INSTRING.load(Ordering::Relaxed);
3564 let qstack_has_bs = compqstack_s.chars().any(|c| c as i32 == QT_BACKSLASH);
3565 if instr != QT_NONE && qstack_has_bs {
3566 let ns_now = from_sb(&ns_final_b);
3567 let rl = ns_final_b.len() as i32;
3568 let ql = to_sb(&multiquote(&ns_now, if qstack2 { 1 } else { 0 })).len() as i32;
3569 if ql > rl {
3570 swb -= ql - rl;
3571 }
3572 }
3573
3574 // c:1826-1855 — split the reconstructed s into qp (prefix) / qs (suffix)
3575 // around the word, with the empirical swb-1-sqq+dq / swe-- offsets.
3576 let idx = swb - 1 - sqq + dq;
3577 let iu = idx.clamp(0, s_b_full.len() as i32) as usize;
3578 let s_prefix = from_sb(&s_b_full[..iu]);
3579 let mut qp = if qttype == QT_SINGLE {
3580 dupstring_wlen(&s_prefix, iu)
3581 } else {
3582 rembslash(&s_prefix)
3583 };
3584 if swe < swb {
3585 swe = swb;
3586 }
3587 swe -= 1;
3588 let sl_s = s_b_full.len() as i32;
3589 if swe > sl_s {
3590 swe = sl_s;
3591 if ns_final_b.len() as i32 > swe - swb + 1 {
3592 let newlen = (swe - swb + 1).max(0) as usize;
3593 ns_final_b.truncate(newlen);
3594 }
3595 }
3596 let swe_u = swe.clamp(0, s_b_full.len() as i32) as usize;
3597 let s_suffix = from_sb(&s_b_full[swe_u..]);
3598 let mut qs = if qttype == QT_SINGLE {
3599 s_suffix.clone()
3600 } else {
3601 rembslash(&s_suffix)
3602 };
3603 let sl_ns = ns_final_b.len() as i32;
3604 if soffs > sl_ns {
3605 soffs = sl_ns;
3606 }
3607 if qttype == QT_SINGLE {
3608 let mut a = qp;
3609 remsquote(&mut a);
3610 qp = a;
3611 let mut b = qs;
3612 remsquote(&mut b);
3613 qs = b;
3614 }
3615
3616 // c:1857-1935 — publish the results.
3617 // c:1861-1868 — prepend the active quote char to compqstack.
3618 {
3619 let head = if instr == QT_NONE {
3620 QT_BACKSLASH
3621 } else {
3622 instr
3623 };
3624 let mut new_qstack = String::new();
3625 if let Some(hc) = char::from_u32(head as u32) {
3626 new_qstack.push(hc);
3627 }
3628 new_qstack.push_str(&compqstack_s);
3629 put(&COMPQSTACK, new_qstack);
3630 // !!! RUST-ONLY LINE — NO C COUNTERPART !!!
3631 // Same reason as the c:305-306 site above: in C the c:1854-1860
3632 // `compqstack = p` IS the `$compstate[all_quotes]` update, because
3633 // `compqstack_gsu` (complete.c:1299) has no storage of its own.
3634 set_compstate_str(
3635 "all_quotes",
3636 &crate::ported::zle::complete::get_compqstack(std::ptr::null_mut()),
3637 );
3638 }
3639
3640 // c:1870-1892 — compquote / compquoting + comp_setunset.
3641 let mut set = (CP_QUOTE | CP_QUOTING) as i32;
3642 let mut unset = 0i32;
3643 let (cq, cqg) = if instr == QT_DOUBLE {
3644 ("\"", "double")
3645 } else if instr == QT_SINGLE {
3646 ("'", "single")
3647 } else if instr == QT_DOLLARS {
3648 ("$'", "dollars")
3649 } else {
3650 unset = set;
3651 set = 0;
3652 ("", "")
3653 };
3654 put(&COMPQUOTE, cq.to_string());
3655 put(&COMPQUOTING, cqg.to_string());
3656 crate::ported::zle::complete::comp_setunset(0, 0, set, unset);
3657
3658 // c:1894-1907 — compprefix / compsuffix from ns around soffs.
3659 if !isset(COMPLETEINWORD) {
3660 put(&COMPPREFIX, untokenize(&from_sb(&ns_final_b)));
3661 put(&COMPSUFFIX, String::new());
3662 } else {
3663 let so = (soffs.max(0) as usize).min(ns_final_b.len());
3664 put(&COMPPREFIX, untokenize(&from_sb(&ns_final_b[..so])));
3665 put(&COMPSUFFIX, untokenize(&from_sb(&ns_final_b[so..])));
3666 }
3667 // c:1908-1910 — drop a dangling final backslash from compprefix.
3668 {
3669 let cp = snap(&COMPPREFIX);
3670 let cpb = cp.as_bytes();
3671 let n = cpb.len();
3672 if n > 1 && cpb[n - 1] == b'\\' && cpb[n - 2] != b'\\' && cpb[n - 2] != meta_b {
3673 let mut t = cp;
3674 t.pop();
3675 put(&COMPPREFIX, t);
3676 }
3677 }
3678
3679 // c:1912-1925 — fold qp/qs into the quoted ignored prefix/suffix.
3680 let cqip = tricat(
3681 &snap(&COMPQIPREFIX),
3682 &snap(&COMPIPREFIX),
3683 &multiquote(&qp, 1),
3684 );
3685 put(&COMPQIPREFIX, cqip);
3686 let cqis = tricat(
3687 &multiquote(&qs, 1),
3688 &snap(&COMPISUFFIX),
3689 &snap(&COMPQISUFFIX),
3690 );
3691 put(&COMPQISUFFIX, cqis);
3692 put(&COMPIPREFIX, String::new());
3693 put(&COMPISUFFIX, String::new());
3694
3695 // c:1926-1934 — rebuild compwords / compcurrent from foo.
3696 {
3697 let words: Vec<String> = foo.iter().map(|w| untokenize(w)).collect();
3698 let cnt = words.len() as i32;
3699 *COMPWORDS
3700 .get_or_init(|| Mutex::new(Vec::new()))
3701 .lock()
3702 .unwrap() = words;
3703 let mut compcur = cur + 1;
3704 if compcur > cnt {
3705 compcur = cnt;
3706 }
3707 COMPCURRENT.store(compcur, Ordering::Relaxed);
3708 }
3709
3710 // zshrs bridge: in C every comp* global written above IS the shell
3711 // parameter (gsu-bound at complete.c:1235-1295 — one storage), so a
3712 // completer sees the re-split word list the instant `compset -q`
3713 // returns. zshrs's `$words` / `$CURRENT` / `$PREFIX` / … are plain
3714 // paramtab copies published once per `callcompfunc`, so without this
3715 // mirror they still describe the PRE-split line. `_trap` is
3716 // if [[ CURRENT -eq 2 ]]; then compset -q; _normal; else …
3717 // and `_normal` re-reads `$words[1]` — which stayed `trap`, so it
3718 // dispatched `_trap` again: unbounded recursion until FUNCNEST, with
3719 // the error text landing in the user's buffer. Same mirror
3720 // `restrict_range` (complete.rs:1301-1307) already does for its own
3721 // COMPWORDS/COMPCURRENT edit.
3722 {
3723 let words = COMPWORDS
3724 .get_or_init(|| Mutex::new(Vec::new()))
3725 .lock()
3726 .map(|g| g.clone())
3727 .unwrap_or_default();
3728 setaparam("words", words);
3729 let _ =
3730 crate::ported::params::setiparam("CURRENT", COMPCURRENT.load(Ordering::Relaxed) as i64);
3731 for (param, global) in [
3732 ("PREFIX", &COMPPREFIX),
3733 ("SUFFIX", &COMPSUFFIX),
3734 ("IPREFIX", &COMPIPREFIX),
3735 ("ISUFFIX", &COMPISUFFIX),
3736 ("QIPREFIX", &COMPQIPREFIX),
3737 ("QISUFFIX", &COMPQISUFFIX),
3738 // NOTE: the list ENDS here, matching `comprparams[]`
3739 // (Src/Zle/complete.c:1248-1258), whose last entry is QISUFFIX.
3740 //
3741 // `QUOTE` and `QUOTING` were previously published here as
3742 // top-level parameters. They are NOT in comprparams — `compquote`
3743 // and `compquoting` live in `compkparams`
3744 // (Src/Zle/complete.c:1266-1267) and are therefore
3745 // `$compstate[quote]` / `$compstate[quoting]` KEYS ONLY, never
3746 // shell parameters. zsh's `callcompfunc` (c:1585-1610) restores
3747 // the C globals, not any `$QUOTE`/`$QUOTING`.
3748 //
3749 // Publishing them created two real scalars that zsh does not have,
3750 // which the user's `_parameters` then offered as completion
3751 // matches — a stable +2 on any cell reaching this path.
3752 // The compstate side is published correctly and separately at
3753 // compcore.rs:886-890.
3754 ] {
3755 // Same gsu-setfn bypass as the restore in complete.rs:
3756 // QIPREFIX/QISUFFIX carry PM_READONLY (c:1256-1257).
3757 crate::vm_helper::set_readonly_special(param, &snap(global));
3758 }
3759 }
3760
3761 // c:1935-1937 — restore instring / inbackt, ret = 0.
3762 INSTRING.store(ois, Ordering::Relaxed);
3763 INBACKT.store(oib, Ordering::Relaxed);
3764 0
3765}
3766
3767// Brace counters live in zle_tricky.c:114 — re-exported there. Local
3768// re-exports here so call sites stay short:
3769#[doc(hidden)]
3770// =====================================================================
3771// set_list_array — `Src/Zle/compcore.c:1947`.
3772// =====================================================================
3773
3774/// Port of `static void set_list_array(char *name, LinkList l)` from
3775/// compcore.c:1947. Writes an array-typed parameter via the canonical
3776/// `setaparam` (params.c:3595).
3777pub fn set_list_array(name: &str, l: &[String]) {
3778 // c:1947
3779 let _ = setaparam(name, l.to_vec()); // c:1956
3780}
3781
3782// =====================================================================
3783// get_user_var — `Src/Zle/compcore.c:1956`.
3784// =====================================================================
3785
3786/// Port of `mod_export char **get_user_var(char *nam)` from
3787/// compcore.c:1956.
3788pub fn get_user_var(nam: Option<&str>) -> Option<Vec<String>> {
3789 // c:1956
3790 let nam = nam?; // c:1956
3791 if nam.starts_with('(') {
3792 // c:1960
3793 let mut arrlist: Vec<String> = Vec::new();
3794 let bytes = nam.as_bytes();
3795 let mut buf = Vec::<u8>::new();
3796 let mut notempty = false; // c:1963
3797 let mut brk = false;
3798 let mut i = 1; // c:1967
3799 while i < bytes.len() {
3800 let b = bytes[i];
3801 if b == b'\\' && i + 1 < bytes.len() {
3802 // c:1969
3803 buf.push(bytes[i + 1]); // c:1970
3804 notempty = true;
3805 i += 2;
3806 continue;
3807 }
3808 if b == b',' || b == b' ' || b == b'\t' || b == b'\n' || b == b')' {
3809 if b == b')' {
3810 brk = true;
3811 } // c:1972
3812 if notempty {
3813 // c:1974
3814 let mut start = 0;
3815 if !buf.is_empty() && buf[0] == b'\n' {
3816 start = 1;
3817 } // c:1977
3818 // c:1979 `addlinknode(arrlist, s)` (c:1977-1978 is the
3819 // leading-newline skip this `start` reproduces). `buf`
3820 // is a byte-exact subsequence of the METAFIED `nam` (the
3821 // `Meta` skip below keeps each escape pair intact), so
3822 // it is not valid UTF-8 and `from_utf8_lossy` would have
3823 // replaced every escape with U+FFFD. Splits only ever
3824 // happen at the ASCII delimiters above, which can never
3825 // be a UTF-8 continuation byte, so the copy is exact.
3826 let s = unsafe { String::from_utf8_unchecked(buf[start..].to_vec()) };
3827 arrlist.push(s); // c:1979
3828 }
3829 buf.clear(); // c:1981
3830 notempty = false;
3831 } else {
3832 notempty = true; // c:1983
3833 buf.push(b);
3834 // c:1984-1985 — `if (*ptr == Meta) ptr++;`. The byte AFTER a
3835 // Meta escape is the real character XOR 0x20 and must never
3836 // be examined as a delimiter: a metafied `)` is `Meta 0x09`,
3837 // whose second byte is TAB, so without this skip `inblank`
3838 // split the list in the middle of a quoted element.
3839 if b == crate::ported::zsh_h::Meta as u8 && i + 1 < bytes.len() {
3840 i += 1;
3841 buf.push(bytes[i]);
3842 }
3843 }
3844 i += 1;
3845 if brk {
3846 break;
3847 } // c:1988
3848 }
3849 if !brk || arrlist.is_empty() {
3850 return None;
3851 } // c:1991
3852 Some(arrlist) // c:1996
3853 } else {
3854 // c:1999
3855 // c:2003 — `if ((arr = getaparam(nam)) || (arr = gethparam(nam)))
3856 // arr = (incompfunc ? arrdup(arr) : arr);
3857 // else if ((val = getsparam(nam))) { arr = {val, NULL}; }`
3858 //
3859 // Route through the canonical accessors, exactly the three C calls in
3860 // that order. The previous port read `pm.u_arr` / `pm.u_str` straight
3861 // out of the paramtab node, which is NOT where every parameter keeps
3862 // its value:
3863 //
3864 // * assoc arrays live in `paramtab_hashed_storage`, so `gethparam`
3865 // was effectively unimplemented here — the whole middle arm of the
3866 // C condition was missing;
3867 // * a plain array whose node carries its value anywhere other than a
3868 // populated `u_arr` (special/tied/magic params, and arrays
3869 // materialised by a Rust compsys port rather than by a shell
3870 // assignment) read back as None.
3871 //
3872 // A None here is reported by `cd_init` as `compdescribe: invalid
3873 // argument: <name>` (computil.rs:1052, c:516) and aborts the whole
3874 // description, so `_describe -t global-aliases 'global alias' ARR`
3875 // printed that error onto the command line and then a second
3876 // `compdescribe: no parsed state` from the following `-g` call.
3877 // Only reachable when `list-grouped` is FALSE: the grouped path in
3878 // `_describe` (sh:82) first copies each array into a local via
3879 // `eval local _a_N=( "${ARR[@]}" )`, and those locals DO land in
3880 // `u_arr`, which is why the same completion worked with the style on.
3881 queue_signals();
3882 let result = crate::ported::params::getaparam(nam)
3883 .or_else(|| crate::ported::params::gethparam(nam)) // c:2003
3884 .or_else(|| crate::ported::params::getsparam(nam).map(|v| vec![v])); // c:2005-2008
3885 unqueue_signals(); // c:2022
3886 result
3887 }
3888}
3889
3890// =====================================================================
3891// get_data_arr — `Src/Zle/compcore.c:2022`.
3892// =====================================================================
3893
3894/// Direct port of `static char **get_data_arr(char *name, int keys)`
3895/// from `Src/Zle/compcore.c:2022`:
3896/// ```c
3897/// queue_signals();
3898/// if (!(v = fetchvalue(&vbuf, &name, 1,
3899/// (keys ? SCANPM_WANTKEYS : SCANPM_WANTVALS) |
3900/// SCANPM_MATCHMANY)))
3901/// ret = NULL;
3902/// else
3903/// ret = getarrvalue(v);
3904/// unqueue_signals();
3905/// ```
3906/// A SUBSCRIPTED name goes straight through that call: `fetchvalue`
3907/// consumes the identifier (`Src/params.c:2196`) and hands the `[…]`
3908/// remainder to `getindex` (c:2289), which sets the SCANPM_MATCH* bits and
3909/// runs `getvaluearr` → `paramvalarr` → `scanparamvals`.
3910///
3911/// A BARE name keeps this port's accessor path (`getaparam` / `gethparam` /
3912/// `gethkparam`) rather than `fetchvalue`, because those are where the
3913/// zsh/parameter magic hashes and arrays (`commands`, `builtins`,
3914/// `reswords`, …) resolve their module scanfn/getfn — the paramtab node
3915/// holds no value for them, so a `fetchvalue` + `arrgetfn` read would come
3916/// back empty and `compadd -k commands` would add the literal word
3917/// "commands".
3918pub fn get_data_arr(name: &str, keys: bool) -> Option<Vec<String>> {
3919 // c:2022
3920
3921 queue_signals(); // c:2028
3922
3923 if name.contains('[') {
3924 // c:2030-2033 — `fetchvalue(&vbuf, &name, 1, (keys ?
3925 // SCANPM_WANTKEYS : SCANPM_WANTVALS) | SCANPM_MATCHMANY)`.
3926 //
3927 // Both scan flags are FIXED at this call site: WANTKEYS vs WANTVALS
3928 // comes from `keys`, and SCANPM_MATCHMANY is ALWAYS set — so
3929 // `scanparamvals`' single-match early-out (`Src/params.c:648-650`)
3930 // never fires and EVERY match is returned, whatever the case of the
3931 // subscript's search letter. `Src/params.c:1719-1722` picks the
3932 // match TARGET (`i`/`I` → SCANPM_MATCHKEY, `k`/`K` →
3933 // SCANPM_KEYMATCH, else SCANPM_MATCHVAL) while
3934 // `scanparamvals` (c:665-681) picks the OUTPUT purely from
3935 // WANTKEYS/WANTVALS. That is why `compadd -k 'styles[(R)…]'`
3936 // (Zsh/Command/_zstyle:363) yields the KEYS of the entries whose
3937 // VALUE matched.
3938 let sf = (if keys {
3939 crate::ported::zsh_h::SCANPM_WANTKEYS
3940 } else {
3941 crate::ported::zsh_h::SCANPM_WANTVALS
3942 }) | crate::ported::zsh_h::SCANPM_MATCHMANY;
3943 let mut vbuf = crate::ported::zsh_h::value {
3944 pm: None,
3945 arr: Vec::new(),
3946 scanflags: 0,
3947 valflags: 0,
3948 start: 0,
3949 end: -1,
3950 };
3951 let mut cursor: &str = name;
3952 let result =
3953 match crate::ported::params::fetchvalue(Some(&mut vbuf), &mut cursor, 1, sf as i32) {
3954 None => None, // c:2034-2035
3955 Some(v) => {
3956 // c:2037 — `ret = getarrvalue(v)`. zshrs's `getarrvalue`
3957 // takes the resolved array plus the slice bounds rather
3958 // than the Value, so spell out the C body's steps here.
3959 //
3960 // c:2554-2555 — `else if (IS_UNSET_VALUE(v)) return
3961 // arrdup(&nular[1]);` (empty). IS_UNSET_VALUE (c:472-474)
3962 // is `!pm || (pm->flags & PM_UNSET) || !*pm->nam`, which is
3963 // how a missing assoc KEY reports nothing: getindex rebinds
3964 // `v->pm` to a `PM_SCALAR|PM_UNSET` element on a miss
3965 // (c:1588).
3966 let unset = v.pm.as_ref().map_or(true, |p| {
3967 (p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET) != 0
3968 || p.node.nam.is_empty()
3969 });
3970 if unset {
3971 Some(Vec::new()) // c:2555
3972 } else {
3973 let arr = crate::ported::params::getvaluearr(Some(&mut *v)); // c:2564
3974 if v.start == 0 && v.end == -1 {
3975 // c:2565-2566 — whole-value read: hand the array
3976 // straight back. A PRESENT assoc key lands here
3977 // with an empty `arr` (getvaluearr has no
3978 // PM_SCALAR arm, c:719), which is C's "a scalar
3979 // element is not a match list" answer.
3980 Some(arr)
3981 } else {
3982 // !!! RUST-ONLY: `params::getarrvalue` takes a
3983 // 1-based start; a non-negative `v.start` is
3984 // getindex's 0-based offset (c:2145), a negative
3985 // one already counts from the end.
3986 let start = v.start as i64;
3987 Some(crate::ported::params::getarrvalue(
3988 &arr,
3989 if start >= 0 { start + 1 } else { start },
3990 v.end as i64,
3991 ))
3992 }
3993 }
3994 }
3995 };
3996 unqueue_signals(); // c:2039
3997 return result;
3998 }
3999
4000 // Route through the same param accessors `${(k)name}` / `${(v)name}`
4001 // / `${name}` use so SPECIAL magic hashes (`commands`, `builtins`,
4002 // `functions`, `aliases`, `reswords`, …) resolve via their module
4003 // scanfns — the raw `paramtab_hashed_storage` map is empty for those,
4004 // which is why `compadd -k commands` previously added the literal
4005 // word "commands" instead of every command name.
4006 let result = if keys {
4007 // c:2028-2033 — `fetchvalue(…, SCANPM_WANTKEYS | SCANPM_MATCHMANY)`
4008 // then `getarrvalue` (c:2556) → `getvaluearr` → for a PM_HASHED param
4009 // `paramvalarr(v->pm->gsu.h->getfn(v->pm), v->scanflags)`
4010 // (`Src/params.c:735-736`). `paramvalarr` emits through
4011 // `scanhashtable` (`Src/params.c:718`, `Src/hashtable.c:426`), so C
4012 // hands back an assoc's keys in HASH-BUCKET order — the very order
4013 // `${(k)assoc}` produces, since `${(k)}` reaches that same
4014 // `paramvalarr`.
4015 //
4016 // That agreement is load-bearing for `compadd -k NAME -d DISP`: the
4017 // caller builds DISP from `${(kv)NAME}` (e.g.
4018 // `Completion/Unix/Command/_git`'s __git_diff_filters), so the two
4019 // arrays are paired element-for-element ONLY while `-k` enumerates in
4020 // the same order `${(kv)}` did.
4021 //
4022 // `gethkparam` reads `paramtab_hashed_storage` directly
4023 // (`params.rs:6801`), an INSERTION-ordered `IndexMap`: for
4024 // `filters=(A added C copied b "pairing broken")` it yields `A C b`
4025 // where zsh scans `b A C`. Every key then drew the WRONG display
4026 // string, and since `matchcmp` sorts by `disp` when present
4027 // (c:3179-3191) two otherwise-`matcheq` matches with mismatched `disp`
4028 // stopped being adjacent, so the consecutive-run dedup at c:3271 —
4029 // which only collapses ADJACENT equals — left the duplicate in the
4030 // list (`git diff --diff-filter=<TAB>` listed `b -- pairing broken`
4031 // twice).
4032 //
4033 // `subst::assoc_keys` is the keys-only read `${(k)NAME}` itself uses:
4034 // it rebuilds zsh's bucket layout to recover the scan order, and for
4035 // a magic hash runs the scanfn with SCANPM_WANTKEYS alone (c:2029).
4036 // Reading through `assoc_get` asked for values too, so
4037 // `compadd -k functions` deparsed every function body only to drop
4038 // it (parameter.c:484-486 skips the body for a keys-only scan).
4039 crate::ported::subst::assoc_keys(name)
4040 .filter(|k| !k.is_empty())
4041 // A magic hash (`commands`, `builtins`, `functions`, …) has no
4042 // `paramtab_hashed_storage` row at all, so `assoc_get` yields
4043 // nothing and the module scanfn behind `gethkparam` is the only
4044 // backing. gethkparam also answers Some(empty) for an assoc that
4045 // exists but holds nothing, which is C's "exists, no entries".
4046 .or_else(|| crate::ported::params::gethkparam(name))
4047 // But `compadd -k` on a REGULAR array (e.g. `_setopt`'s
4048 // `local -a onopts`) must add its ELEMENTS: C's
4049 // `fetchvalue(SCANPM_WANTKEYS)` ignores WANTKEYS for a non-hash and
4050 // returns the value array. gethkparam returns None for a non-hash,
4051 // so fall back to the elements — without this, `setopt`/`unsetopt
4052 // <tab>` (and any `compadd -k <plain-array>`) produced ZERO matches.
4053 .or_else(|| crate::ported::params::getaparam(name))
4054 } else {
4055 // SCANPM_WANTVALS — plain-array elements, else assoc values.
4056 crate::ported::params::getaparam(name)
4057 .filter(|v| !v.is_empty())
4058 .or_else(|| crate::ported::params::gethparam(name))
4059 };
4060
4061 unqueue_signals(); // c:2041
4062 result
4063}
4064
4065// =====================================================================
4066// addmatch — `Src/Zle/compcore.c:2041`.
4067// =====================================================================
4068
4069/// Port of `static void addmatch(char *str, int flags, char ***dispp,
4070/// int line)` from compcore.c:2041.
4071pub fn addmatch(str: &str, flags: i32, disp: Option<&str>, line: bool) {
4072 // c:2041
4073 let mut cm = Cmatch::default(); // c:2041
4074 cm.str = Some(str.to_string()); // c:2047
4075 // c:2049-2051 — inline read of `complist` parameter, parse `packed`/
4076 // `rows` substrings into CMF_PACKED/CMF_ROWS flag bits.
4077 let complist_extra = {
4078 let s = COMPLIST
4079 .get_or_init(|| Mutex::new(String::new()))
4080 .lock()
4081 .map(|g| g.clone())
4082 .unwrap_or_default();
4083 let packed = if s.contains("packed") { CMF_PACKED } else { 0 }; // c:2050
4084 let rows = if s.contains("rows") { CMF_ROWS } else { 0 }; // c:2051
4085 if s.is_empty() {
4086 0
4087 } else {
4088 packed | rows
4089 }
4090 };
4091 cm.flags = flags | complist_extra; // c:2048
4092 if let Some(d) = disp {
4093 // c:2052
4094 cm.disp = Some(d.to_string()); // c:2056
4095 } else if line {
4096 // c:2057
4097 cm.disp = Some(String::new()); // c:2058
4098 cm.flags |= CMF_DISPLINE; // c:2059
4099 }
4100 mnum.fetch_add(1, Ordering::Relaxed); // c:2060
4101 // c:2061 — `ainfo->count++`. Missing here while the sibling
4102 // increment in `add_match_data` (c:3006) was present, so every
4103 // match added through THIS path (`compadd -x`/`-X` dummies, the
4104 // CAF_ALL `<all>` placeholder) was invisible to `ainfo.count` —
4105 // the counter `do_ambiguous`/`permmatches` use to decide whether
4106 // any match exists in the group.
4107 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
4108 if let Some(a) = g.as_mut() {
4109 a.count += 1; // c:2061
4110 }
4111 }
4112 {
4113 let cell = curexpl.get_or_init(|| Mutex::new(None)); // c:2062
4114 if let Ok(mut g) = cell.lock() {
4115 if let Some(e) = g.as_mut() {
4116 e.count += 1;
4117 }
4118 }
4119 }
4120 let mcell = crate::comp_match_handles::matches_arc(); // c:2066
4121 if let Ok(mut g) = mcell.lock() {
4122 g.push(cm);
4123 }
4124 newmatches.store(1, Ordering::Relaxed); // c:2068
4125 {
4126 let cell = mgroup.get_or_init(|| Mutex::new(None)); // c:2069
4127 if let Ok(g) = cell.lock() {
4128 if let Some(grp) = g.as_ref() {
4129 // c:2068 `mgroup->new = 1` — `new_` is the shared flag the
4130 // `amatches` entry sees too (comp_h.rs Cmgroup::new_).
4131 grp.new_.store(1, Ordering::Relaxed);
4132 }
4133 }
4134 }
4135}
4136
4137// =====================================================================
4138// addmatches — `Src/Zle/compcore.c:2080`.
4139// =====================================================================
4140
4141/// Direct port of `int addmatches(Cadata dat, char **argv)` from
4142/// compcore.c:2080 — the workhorse called from every `compadd`
4143/// invocation. Walks `argv`, runs the matcher chain against each
4144/// candidate, builds the Cline chain via `add_match_data`, and
4145/// appends accepted matches to the current group.
4146///
4147/// Real-bodied across all major phases: prologue (group selection
4148/// c:2105-2118, brace-state snapshot c:2129-2132, instring/inbackt
4149/// save c:2148-2179, `*argv` empty short-circuit c:2127), mstack
4150/// push c:2210-2222, aign/pign suffix-ignore + Patprog filters
4151/// c:2223-2246, disp array c:2247-2250, lipre/lisuf/lpre/lsuf
4152/// assembly c:2253-2300, per-candidate match loop with comp_match
4153/// dispatch + add_match_data emit + apar/opar writeback
4154/// c:2482-2601, apar/opar setaparam c:2602-2605, exp addexpl
4155/// c:2610, hasallmatch CAF_ALL placeholder c:2612-2614, dummy
4156/// entries c:2616-2617.
4157pub fn addmatches(
4158 dat: &mut Cadata, // c:2080
4159 argv: &[String],
4160) -> i32 {
4161 let _nm = mnum.load(Ordering::Relaxed); // c:2089 nm
4162 // c:2089-2090 — `ohp = haspattern`, `ois = instring`, `oib = inbackt`,
4163 // and c:2084 `oaq = autoq`. All four are restored at c:2624-2633; the
4164 // port saved none of them.
4165 let ohp = haspattern.load(Ordering::Relaxed); // c:2089
4166 let ois = INSTRING.load(Ordering::Relaxed); // c:2090
4167 let oib = INBACKT.load(Ordering::Relaxed); // c:2090
4168 let oaq = AUTOQ
4169 .get_or_init(|| Mutex::new(String::new()))
4170 .lock()
4171 .map(|g| g.clone())
4172 .unwrap_or_default(); // c:2084
4173 // c:2085 — `char *oqp = qipre, *oqs = qisuf`, restored at c:2629-2630.
4174 let oqp = crate::ported::zle::zle_tricky::qipre_get(); // c:2085
4175 let oqs = crate::ported::zle::zle_tricky::qisuf_get(); // c:2085
4176
4177 // c:2093 — `Cmlist oms = mstack;`, restored at c:2622 `mstack = oms;`.
4178 // The `-M` matcher a compadd carries is pushed onto `mstack` (c:2212) for
4179 // the duration of THAT call only. This port pushed but never restored, so
4180 // the matcher leaked into every later compadd of the same completion: once
4181 // `_describe` added an option list with `-M 'r:|[_-]=* r:|=*'`, a following
4182 // bare `compadd -k commands` matched the line prefix `-` against every
4183 // command name. `hash -<TAB>` offered 1152 candidates where zsh offers the
4184 // 6 options; the same leak inflates any completion that mixes a matcher-
4185 // carrying compadd with a later plain one.
4186 let oms_saved: Option<std::sync::Arc<Cmlist>> = mstack
4187 .get_or_init(|| Mutex::new(None))
4188 .lock()
4189 .ok()
4190 .and_then(|mut g| g.take());
4191 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4192 *g = oms_saved.clone();
4193 }
4194 // Restores `mstack` on EVERY exit from this function (C restores at the
4195 // single c:2622 return; the port has several early returns).
4196 struct MstackRestore(Option<std::sync::Arc<Cmlist>>);
4197 impl Drop for MstackRestore {
4198 fn drop(&mut self) {
4199 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4200 *g = self.0.take();
4201 }
4202 }
4203 }
4204 let _mstack_guard = MstackRestore(oms_saved);
4205
4206 // c:2049 — C's `complist` global is GSU-backed by `$compstate[list]`, so a
4207 // completer's `compstate[list]="... packed"` write (e.g. `_describe`
4208 // setting the grouped/packed layout) is visible to `addmatch` via `complist`.
4209 // The Rust port keeps COMPLIST as a separate global set only from the global
4210 // at completion init (c:740), so param writes never reached it — matches
4211 // added by grouped `_describe`/`_arguments` never got CMF_PACKED, so their
4212 // group lost CGF_PACKED and calclist skipped the per-column-width pass,
4213 // collapsing the name/description columns into one uniform column. Sync the
4214 // param → global here so `packed`/`rows` reach CMF_PACKED/CMF_ROWS.
4215 if let Some(cl) = get_compstate_str("list") {
4216 if let Ok(mut g) = COMPLIST.get_or_init(|| Mutex::new(String::new())).lock() {
4217 *g = cl;
4218 }
4219 }
4220
4221 if dat.dummies >= 0 {
4222 // c:2106
4223 dat.aflags = (dat.aflags | CAF_NOSORT | CAF_UNIQCON) & !CAF_UNIQALL; // c:2107-2108
4224 }
4225
4226 let gflags = (if (dat.aflags & CAF_NOSORT) != 0 {
4227 CGF_NOSORT
4228 } else {
4229 0
4230 }) | (if (dat.aflags & CAF_MATSORT) != 0 {
4231 CGF_MATSORT
4232 } else {
4233 0
4234 }) | (if (dat.aflags & CAF_NUMSORT) != 0 {
4235 CGF_NUMSORT
4236 } else {
4237 0
4238 }) | (if (dat.aflags & CAF_REVSORT) != 0 {
4239 CGF_REVSORT
4240 } else {
4241 0
4242 }) | (if (dat.aflags & CAF_UNIQALL) != 0 {
4243 CGF_UNIQALL
4244 } else {
4245 0
4246 }) | (if (dat.aflags & CAF_UNIQCON) != 0 {
4247 CGF_UNIQCON
4248 } else {
4249 0
4250 });
4251
4252 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");
4253 if let Some(g) = dat.group.as_deref() {
4254 // c:2115
4255 endcmgroup(None); // c:2116
4256 begcmgroup(Some(g), gflags); // c:2117
4257 } else {
4258 endcmgroup(None); // c:2119
4259 begcmgroup(Some("default"), 0); // c:2120
4260 }
4261
4262 if dat.mesg.is_some() || dat.exp.is_some() {
4263 // c:2122
4264 let mut e = Cexpl::default(); // c:2123
4265 e.always = if dat.mesg.is_some() { 1 } else { 0 }; // c:2124
4266 e.count = 0;
4267 e.fcount = 0; // c:2125
4268 e.str = Some(
4269 dat.mesg
4270 .clone() // c:2126
4271 .or_else(|| dat.exp.clone())
4272 .unwrap_or_default(),
4273 );
4274 if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
4275 *g = Some(e);
4276 }
4277 if dat.mesg.is_some() && dat.dpar.is_empty() && dat.opar.is_none() && dat.apar.is_none() {
4278 // c:2129
4279 addexpl(true); // c:2130
4280 }
4281 } else if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
4282 *g = None; // c:2133
4283 }
4284
4285 // c:2138 — empty-argv early return.
4286 if argv.is_empty() && dat.dummies == 0 && (dat.aflags & CAF_ALL) == 0 {
4287 return 1; // c:2139
4288 }
4289
4290 // c:2132-2135 — `for (bp = brbeg; bp; bp = bp->next)
4291 // bp->curpos = ((dat->aflags & CAF_QUOTE) ? bp->pos
4292 // : bp->qpos);`
4293 // and the same for `brend`. Only the CAF_QUOTE test existed here (as an
4294 // unused local); the snapshot itself was never performed, so `curpos`
4295 // stayed 0 for every brace and `add_match_data`'s brpl/brsl (c:2979) had
4296 // nothing to read.
4297 {
4298 let quote_mode = (dat.aflags & CAF_QUOTE) != 0; // c:2133
4299 for chain in [&BRBEG, &BREND] {
4300 if let Ok(mut g) = chain.get_or_init(|| Mutex::new(None)).lock() {
4301 let mut cur = g.as_deref_mut();
4302 while let Some(n) = cur {
4303 n.curpos = if quote_mode { n.pos } else { n.qpos }; // c:2133/2135
4304 cur = n.next.as_deref_mut();
4305 }
4306 }
4307 }
4308 }
4309
4310 if (dat.flags & 0x0008/*CMF_ISPAR*/) != 0 {
4311 // c:2148
4312 dat.flags |= parflags.load(Ordering::Relaxed); // c:2149
4313 }
4314
4315 let qc = compquote_first(); // c:2139
4316 if let Some(q) = qc {
4317 // c:2139 — `if (compquote && (qc = *compquote))`
4318 match q {
4319 '`' => {
4320 instring_set(QT_NONE);
4321 inbackt_set(0);
4322 autoq_set(""); // c:2140-2146
4323 }
4324 _ => {
4325 match q {
4326 '\'' => instring_set(QT_SINGLE), // c:2149-2151
4327 '"' => instring_set(QT_DOUBLE), // c:2153-2155
4328 '$' => instring_set(QT_DOLLARS), // c:2157-2159
4329 _ => {}
4330 }
4331 // c:2162-2163 — `inbackt = 0;
4332 // autoq = multiquote(*compquote == '$' ? compquote+1 : compquote, 1);`
4333 // Both lines were missing: `autoq` kept the PREVIOUS
4334 // completion's quote, which is what `do_single`
4335 // (compresult.c) re-closes the inserted word with.
4336 inbackt_set(0); // c:2162
4337 let cq = crate::ported::zle::complete::COMPQUOTE
4338 .get_or_init(|| Mutex::new(String::new()))
4339 .lock()
4340 .map(|g| g.clone())
4341 .unwrap_or_default();
4342 let cq = if cq.starts_with('$') {
4343 &cq[1..]
4344 } else {
4345 &cq[..]
4346 };
4347 autoq_set(&multiquote(cq, 1)); // c:2163
4348 }
4349 }
4350 } else {
4351 instring_set(QT_NONE);
4352 inbackt_set(0);
4353 autoq_set(""); // c:2166-2168
4354 }
4355 // c:2170-2171 — `qipre = ztrdup(compqiprefix ? compqiprefix : "");
4356 // qisuf = ztrdup(compqisuffix ? compqisuffix : "");`
4357 // `compqiprefix`/`compqisuffix` ARE `$QIPREFIX`/`$QISUFFIX` (complete.c:
4358 // 1266-1267), so a completer that ran `compset -q` (or `compset -P`)
4359 // hands its edited value back to the match builder here. Without this,
4360 // `add_match_data` saw whatever `get_comp_string` had left.
4361 for (global, param) in [
4362 (&crate::ported::zle::zle_tricky::QIPRE, "QIPREFIX"), // c:2170
4363 (&crate::ported::zle::zle_tricky::QISUF, "QISUFFIX"), // c:2171
4364 ] {
4365 let v = crate::ported::params::getsparam(param).unwrap_or_default();
4366 if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
4367 *g = v;
4368 }
4369 }
4370
4371 // c:2173 — `useexact = (compexact && !strcmp(compexact, "accept"))`.
4372 //
4373 // C's `compexact` global IS `$compstate[exact]` (gsu-bound at
4374 // complete.c:1281). The port read `getsparam("compexact")` — a shell
4375 // parameter of that literal name, which nothing ever sets — so this line
4376 // unconditionally CLEARED `useexact` on every compadd, defeating
4377 // REC_EXACT and any completer that set `compstate[exact]=accept`.
4378 let exact_str = get_compstate_str("exact").unwrap_or_default();
4379 useexact.store(if exact_str == "accept" { 1 } else { 0 }, Ordering::Relaxed);
4380
4381 // c:2170-2175 —
4382 // if ((doadd = (!dat->apar && !dat->opar && !dat->dpar))) {
4383 // if (dat->aflags & CAF_MATCH)
4384 // hasmatched = 1;
4385 // else
4386 // hasunmatched = 1;
4387 // }
4388 // `doadd` says this compadd puts matches on the completion list rather
4389 // than only filling `-A`/`-O`/`-D` arrays; only those calls tell the
4390 // result stage anything about the line. CAF_MATCH is `compadd` WITHOUT
4391 // `-U`, i.e. the candidate was matched against `$PREFIX`/`$SUFFIX`, so
4392 // the cline parts carry meaningful line/word anchors — that is exactly
4393 // the condition `cut_cline` (compresult.c:57) tests before it is willing
4394 // to trim the unambiguous string, and the condition `do_ambiguous`
4395 // (compresult.c:794) tests before restoring the typed word.
4396 //
4397 // The port set `hasmatched` only from the compctl path (compctl.c:3292),
4398 // never from compadd, so every compsys completion left both flags 0:
4399 // `cut_cline` took the `!hasmatched` "keep everything" branch forever and
4400 // `do_ambiguous`'s `!hasunmatched` guard was always satisfied. Measured on
4401 // the parity corpus under the `full` zstyle fixture: `cut_cline` reads
4402 // `hasmatched=0` without these lines and `hasmatched=1` with them, on 22
4403 // of the 31 corpus buffers that reach it (`df -` also reaches it with
4404 // `hasunmatched=1`, so both arms of the `if/else` fire).
4405 //
4406 // Placement follows C: this is c:2169-2175, ahead of the mstack push
4407 // (c:2200) and well ahead of the `*argv = NULL` prefix-mismatch bail at
4408 // c:2335. The port hoists parts of c:2280-2454 above the `doadd` binding
4409 // at the candidate loop and returns early on that bail, so setting the
4410 // flags there would silently skip the calls C still counts.
4411 let doadd = dat.apar.is_none() && dat.opar.is_none() && dat.dpar.is_empty(); // c:2170
4412 if doadd {
4413 if (dat.aflags & CAF_MATCH) != 0 {
4414 hasmatched.store(1, Ordering::Relaxed); // c:2172
4415 } else {
4416 hasunmatched.store(1, Ordering::Relaxed); // c:2174
4417 }
4418 }
4419
4420 // c:2210-2222 — push dat.match onto mstack (the matcher chain
4421 // queried by match_str during candidate evaluation).
4422 //
4423 // c:2094 `Cmlist oms = mstack` / c:2623 `mstack = oms` — the push is
4424 // scoped to THIS addmatches call: C stack-allocates the link (`struct
4425 // cmlist mst`) and restores the saved head on the way out. The port
4426 // pushed but never restored, so a `compadd -M` matcher stayed live for
4427 // every later compadd of the same completion. `ssh -<TAB>` showed it:
4428 // _arguments adds its options with `-M 'r:|[_-]=* r:|=*'`, that spec
4429 // survived onto _hosts' compadd, and match_str (c:593 walks the whole
4430 // mstack) then let the typed `-` match any host CONTAINING a `-` —
4431 // `ec2-23-20-9-23.compute-1.amazonaws.com` was offered where zsh
4432 // offers options only. `MstackPop` restores on every exit path.
4433 struct MstackPop(bool);
4434 impl Drop for MstackPop {
4435 fn drop(&mut self) {
4436 if !self.0 {
4437 return;
4438 }
4439 if let Ok(mut mst) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4440 // C restores the saved head; since this frame pushed exactly
4441 // one link, dropping it is the same list.
4442 *mst = mst.take().and_then(|link| link.next.clone());
4443 }
4444 }
4445 }
4446 let _mstack_pop = MstackPop(dat.match_.is_some());
4447 if let Some(ref m) = dat.match_ {
4448 // c:2210
4449 if let Ok(mut mst) = mstack.get_or_init(|| Mutex::new(None)).lock() {
4450 // C: mst.next = mstack; mst.matcher = dat->match; mstack = &mst.
4451 let new_link = std::sync::Arc::new(Cmlist {
4452 next: mst.take(),
4453 matcher: m.clone(),
4454 str: String::new(),
4455 });
4456 *mst = Some(new_link);
4457 }
4458 // c:2215 — add_bmatchers(dat->match).
4459 crate::ported::zle::compmatch::add_bmatchers(Some(m));
4460 // c:2217 — addlinknode(matchers, dat->match).
4461 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
4462 g.push(m.clone());
4463 }
4464 }
4465 // c:2220-2221 — `if (mnum && (mstack || bmatchers)) update_bmatchers();`
4466 // prunes bmatchers of any matcher no longer on mstack; without it the
4467 // restored stack and the bmatchers list drift apart.
4468 if mnum.load(Ordering::Relaxed) != 0 {
4469 let live = mstack
4470 .get_or_init(|| Mutex::new(None))
4471 .lock()
4472 .map(|g| g.is_some())
4473 .unwrap_or(false)
4474 || bmatchers
4475 .get_or_init(|| Mutex::new(None))
4476 .lock()
4477 .map(|g| g.is_some())
4478 .unwrap_or(false);
4479 if live {
4480 crate::ported::zle::compmatch::update_bmatchers();
4481 }
4482 }
4483
4484 // c:2223-2246 — get suffixes to ignore from dat.ign param.
4485 let (aign, pign) = if let Some(ign_name) = dat.ign.as_deref() {
4486 // c:2224
4487 let aign_raw = get_user_var(Some(ign_name)).unwrap_or_default();
4488 let mut literal_suffixes: Vec<String> = Vec::new();
4489 let mut pat_progs: Vec<crate::ported::pattern::Patprog> = Vec::new();
4490 for entry in aign_raw {
4491 // c:2231-2232 — `tokenize(tmp); remnulargs(tmp);` — fignore
4492 // entries are param values (untokenized text); C tokenizes
4493 // each BEFORE the token checks below and the patcompile.
4494 let mut tmp = entry.clone();
4495 crate::ported::glob::tokenize(&mut tmp); // c:2231
4496 crate::ported::glob::remnulargs(&mut tmp); // c:2232
4497 // c:2233-2236 — `(tmp[0] == Quest && tmp[1] == Star) ||
4498 // (tmp[1] == Quest && tmp[0] == Star)` token short-circuit:
4499 // trailing literal suffix.
4500 let tch: Vec<char> = tmp.chars().collect();
4501 let suffix: String = tch.iter().skip(2).collect();
4502 let star_prefix = tch.len() >= 3
4503 && ((tch[0] == crate::ported::zsh_h::Quest
4504 && tch[1] == crate::ported::zsh_h::Star)
4505 || (tch[1] == crate::ported::zsh_h::Quest
4506 && tch[0] == crate::ported::zsh_h::Star))
4507 && !crate::ported::pattern::haswilds(&suffix);
4508 if star_prefix {
4509 // c:2236 — `untokenize(*sp++ = tmp + 2);`
4510 literal_suffixes.push(crate::ported::lex::untokenize(&suffix));
4511 } else if let Some(prog) =
4512 crate::ported::pattern::patcompile(&tmp, 0, None::<&mut String>)
4513 {
4514 pat_progs.push(prog);
4515 }
4516 }
4517 (literal_suffixes, pat_progs)
4518 } else {
4519 (Vec::new(), Vec::new())
4520 };
4521
4522 // c:2247-2250 — get display strings.
4523 let disp_arr: Vec<String> = if let Some(ref d) = dat.disp {
4524 get_user_var(Some(d.as_str())).unwrap_or_default()
4525 } else {
4526 Vec::new()
4527 };
4528
4529 // zshrs bridge: in C the `compprefix`/`compsuffix`/`compiprefix`/
4530 // `compisuffix` globals ARE `$PREFIX`/`$SUFFIX`/`$IPREFIX`/`$ISUFFIX`
4531 // (the compparams are gsu-bound to them). The Rust compparams carry
4532 // no gsu binding (`complete.rs` `gsu: 0`), so the compsys completers'
4533 // writes to `$PREFIX` land in the param table while `compadd` reads
4534 // the globals — leaving `lpre` empty and every candidate matching.
4535 // During a live completion (`INCOMPFUNC`), refresh the globals from
4536 // the params so `comp_match` filters against the prefix the completer
4537 // actually set. Gated on INCOMPFUNC so direct-call unit tests that
4538 // seed the globals aren't clobbered.
4539 if INCOMPFUNC.load(Ordering::Relaxed) != 0 {
4540 for (param, global) in [
4541 ("PREFIX", &COMPPREFIX),
4542 ("SUFFIX", &COMPSUFFIX),
4543 ("IPREFIX", &COMPIPREFIX),
4544 ("ISUFFIX", &crate::ported::zle::complete::COMPISUFFIX),
4545 ] {
4546 if let Some(v) = getsparam(param) {
4547 if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
4548 *g = v;
4549 }
4550 }
4551 }
4552 }
4553
4554 // c:2253-2300 — CAF_MATCH lipre/lisuf/lpre/lsuf assembly.
4555 let compiprefix_s = COMPIPREFIX
4556 .get_or_init(|| Mutex::new(String::new()))
4557 .lock()
4558 .map(|g| g.clone())
4559 .unwrap_or_default();
4560 let compisuffix_s = crate::ported::zle::complete::COMPISUFFIX
4561 .get_or_init(|| Mutex::new(String::new()))
4562 .lock()
4563 .map(|g| g.clone())
4564 .unwrap_or_default();
4565 let compprefix_s = COMPPREFIX
4566 .get_or_init(|| Mutex::new(String::new()))
4567 .lock()
4568 .map(|g| g.clone())
4569 .unwrap_or_default();
4570 let compsuffix_s = COMPSUFFIX
4571 .get_or_init(|| Mutex::new(String::new()))
4572 .lock()
4573 .map(|g| g.clone())
4574 .unwrap_or_default();
4575 // c:2252-2253 — `if (dat->aflags & CAF_MATCH) { lipre = dupstring(compiprefix);
4576 // lisuf = dupstring(compisuffix); … }`. `lipre`/`lisuf` start out NULL
4577 // (c:2083) and are ONLY filled in under CAF_MATCH, because they exist to
4578 // feed the per-candidate match. `compadd -U` clears CAF_MATCH, so with `-U`
4579 // the C match carries no ignored prefix at all and the word on the line is
4580 // replaced whole. Reading them unconditionally handed every `-U` match the
4581 // live `$IPREFIX`, which c:2278-2282 below then prepends to `dat->ipre`:
4582 // `ls ~zzqaa<TAB>` under `completer _expand` with `hash -d zzqaa=/usr/share/zsh`
4583 // has IPREFIX=`~`, so `_expand`'s `compadd -UQ` re-inserted the tilde in
4584 // front of the expansion and the line became `ls ~/usr/share/zsh/`.
4585 // `lpre`/`lsuf` are left read unconditionally: every consumer of them below
4586 // is itself CAF_MATCH-gated or reached only through `comp_match` (c:2535).
4587 let lipre = if (dat.aflags & CAF_MATCH) != 0 {
4588 compiprefix_s.clone()
4589 } else {
4590 String::new()
4591 };
4592 let lisuf = if (dat.aflags & CAF_MATCH) != 0 {
4593 compisuffix_s.clone()
4594 } else {
4595 String::new()
4596 };
4597 let mut lpre = compprefix_s.clone();
4598 let mut lsuf = compsuffix_s.clone();
4599
4600 // c:2252-2254 — `if (llpl + strlen(compqiprefix) + strlen(lipre) != origlpre
4601 // || llsl + strlen(compqisuffix) + strlen(lisuf) != origlsuf)
4602 // lenchanged = 1;`
4603 // The completer moved the PREFIX/SUFFIX split since the widget was entered
4604 // (`compset -P/-S`, `_approximate`, `_prefix`). `do_ambiguous`
4605 // (compresult.c:794) uses this to decide NOT to put the old word back when
4606 // the unambiguous string comes out shorter than the word on the line.
4607 // Never assigned before, so the flag was stuck at 0.
4608 if (dat.aflags & CAF_MATCH) != 0 {
4609 let qip = getsparam("QIPREFIX").unwrap_or_default();
4610 let qis = getsparam("QISUFFIX").unwrap_or_default();
4611 if lpre.len() + qip.len() + lipre.len() != origlpre.load(Ordering::Relaxed) as usize
4612 || lsuf.len() + qis.len() + lisuf.len() != origlsuf.load(Ordering::Relaxed) as usize
4613 {
4614 lenchanged.store(1, Ordering::Relaxed); // c:2254
4615 }
4616 }
4617
4618 // c:2266-2276 — "Test if there is an existing -P prefix":
4619 // if (dat->pre && *dat->pre) {
4620 // int prefix_length = pfxlen(dat->pre, lpre);
4621 // if (dat->pre[prefix_length] == '\0' ||
4622 // lpre[prefix_length] == '\0') {
4623 // /* $compadd_args[-P] is a prefix of ${PREFIX}, or
4624 // * vice-versa. */
4625 // llpl -= prefix_length;
4626 // lpre += prefix_length;
4627 // }
4628 // }
4629 // `compadd -P PRE` puts PRE on the line AHEAD of every candidate, so —
4630 // exactly like the `-p PPRE` strip below — it has to come off `lpre`
4631 // before comp_match compares the candidates against it. `dat.pre` stays
4632 // in the add_match_data call further down; that is what re-inserts it.
4633 // This step was missing entirely, so `_zstyle`'s
4634 // compadd -P : -qS : chpwd completion vcs_info zftp zle
4635 // matched NOTHING the moment $PREFIX was ":" — comp_match(":", "",
4636 // "chpwd") rejects every candidate — which is the whole `zstyle <TAB>`
4637 // divergence. C compares C strings byte-wise, so the shared-prefix
4638 // length is a BYTE count here (zle_tricky::pfxlen counts chars, which
4639 // would mis-slice a multibyte prefix).
4640 if (dat.aflags & CAF_MATCH) != 0 {
4641 // c:2267
4642 if let Some(pre) = dat.pre.as_deref() {
4643 if !pre.is_empty() {
4644 // c:2268 — `pfxlen(dat->pre, lpre)`, in bytes.
4645 let prefix_length: usize = pre
4646 .chars()
4647 .zip(lpre.chars())
4648 .take_while(|(a, b)| a == b)
4649 .map(|(a, _)| a.len_utf8())
4650 .sum();
4651 // c:2269-2270 — one of the two ran out at the split point,
4652 // i.e. one is a prefix of the other.
4653 if prefix_length == pre.len() || prefix_length == lpre.len() {
4654 lpre = lpre[prefix_length..].to_string(); // c:2273-2274
4655 }
4656 }
4657 }
4658 }
4659
4660 // c:2084 — `char *oppre = dat->ppre`: the UNQUOTED path prefix as it
4661 // arrived, captured before the c:2288 quoting rewrites `dat.ppre`. Used
4662 // as the `prpre` default at c:2436.
4663 let oppre: Option<String> = dat.ppre.clone(); // c:2084
4664
4665 // c:2314-2340 — strip the path-prefix (`compadd -p PPRE`) from `lpre`
4666 // before matching each candidate. PPRE is already on the command line
4667 // ahead of the match, so comp_match must compare the candidate against
4668 // `lpre` with PPRE removed (candidate `sub` vs `su`, not vs the full
4669 // `/tmp/ptree/su`). Try the active matcher first (`match_str`, part=1),
4670 // else strip literally. Without this every `cmd /a/b/pre<TAB>` produced
4671 // no matches — the resolved candidate never matched the full-path lpre.
4672 let mut bcp: i32 = 0;
4673 let mut bcs: i32 = 0;
4674 // c:2339 / c:2366 — either the ppre or the psuf probe can conclude that
4675 // NO candidate can match (`*argv = NULL`), which skips the whole add.
4676 let mut ppre_nomatch = false;
4677 if (dat.aflags & CAF_MATCH) != 0 {
4678 // ppre is quoted below (c:2288) in C before this block; do the same
4679 // ordering here so lengths line up with the on-line text.
4680 let ppre_q: Option<String> = dat.ppre.as_deref().map(|existing| {
4681 if (dat.flags & 0x0001/*CMF_FILE*/) != 0 {
4682 tildequote(existing, if (dat.aflags & CAF_QUOTE) != 0 { 0 } else { 1 })
4683 } else {
4684 multiquote(existing, if (dat.aflags & CAF_QUOTE) != 0 { 0 } else { 1 })
4685 }
4686 });
4687 if let Some(ppre) = ppre_q.as_deref() {
4688 if !ppre.is_empty() {
4689 // NOTE: `dat.ppre` is left UNquoted here — the canonical
4690 // quoting happens in the c:2288 block below. `ppre_q` is a
4691 // local, same-quoting copy used only for the length math.
4692 let lpl = ppre.len();
4693 // c:2314 — `ml = match_str(lpre, ppre, …, part=1)`. Reset the
4694 // match accumulators first so this probe doesn't leak into the
4695 // per-candidate comp_match (the Rust threads the match Cline
4696 // through comp_match's own output, and instmatch re-inserts
4697 // dat.ppre from the Cmatch, so the probe's `pline` is unused).
4698 crate::ported::zle::compmatch::start_match();
4699 let ml = crate::ported::zle::compmatch::match_str(
4700 lpre.as_bytes(),
4701 ppre.as_bytes(),
4702 None,
4703 0,
4704 None,
4705 0,
4706 0,
4707 1,
4708 );
4709 if ml >= 0 {
4710 // c:2318-2325 — matcher matched the prefix.
4711 let cut = (ml as usize).min(lpre.len());
4712 lpre = lpre[cut..].to_string();
4713 bcp = ml;
4714 } else if lpre.len() <= lpl && ppre.starts_with(lpre.as_str()) {
4715 // c:2335 — lpre is a prefix of ppre → nothing left.
4716 lpre.clear();
4717 } else if lpre.len() > lpl && lpre.starts_with(ppre) {
4718 // c:2337 — ppre is a literal prefix of lpre → strip it.
4719 lpre = lpre[lpl..].to_string();
4720 } else {
4721 // c:2339 — `*argv = NULL`: no candidate can match.
4722 ppre_nomatch = true;
4723 bcp = lpl as i32; // c:2341
4724 }
4725 crate::ported::zle::compmatch::start_match();
4726 }
4727 }
4728
4729 // c:2344-2368 — the SUFFIX twin of the block above, and it was
4730 // missing outright. `compadd -s PSUF` puts PSUF on the command line
4731 // AFTER the match, so `lsuf` (a copy of `$SUFFIX`) has to have PSUF
4732 // taken off it before per-candidate matching, exactly as `lpre` has
4733 // PPRE taken off above. Without it comp_match was handed the FULL
4734 // `$SUFFIX` and matched it against a word tail that no longer
4735 // contained PSUF, so every candidate was rejected: `_path_files`
4736 // sh:698-702 fires `compadd -Qf -p "$linepath$testpath" -s
4737 // "/${tmp3#*/}"` for an ambiguous INTERMEDIATE path component, which
4738 // made `ls /u/l/b<TAB>`, `ls /u/s/b<TAB>` and `ls /u/l/<TAB>` add
4739 // ZERO matches where zsh adds two or three.
4740 let psuf_q: Option<String> = dat.psuf.as_deref().map(|existing| {
4741 multiquote(existing, if (dat.aflags & CAF_QUOTE) != 0 { 1 } else { 0 })
4742 });
4743 if let Some(psuf) = psuf_q.as_deref() {
4744 if !psuf.is_empty() {
4745 let lsl = psuf.len();
4746 // c:2345 — `ml = match_str(lsuf, s, &bsl, 0, NULL, 1, 0, 1)`
4747 // — sfx=1, test=1. Bracket it with start_match() for the same
4748 // reason as the ppre probe above: this is a PROBE, its match
4749 // accumulators must not leak into the per-candidate
4750 // comp_match that follows.
4751 crate::ported::zle::compmatch::start_match();
4752 let ml = crate::ported::zle::compmatch::match_str(
4753 lsuf.as_bytes(),
4754 psuf.as_bytes(),
4755 None,
4756 0,
4757 None,
4758 1,
4759 0,
4760 1,
4761 );
4762 if ml >= 0 {
4763 // c:2357-2359 — `lsuf[llsl - ml] = '\0'; llsl -= ml;
4764 // bcs = ml;`. `ml` is a byte count, so cut
4765 // the bytes rather than the &str.
4766 let keep = lsuf.len().saturating_sub(ml.max(0) as usize);
4767 let lb = lsuf.as_bytes();
4768 lsuf = String::from_utf8_lossy(lb.get(..keep).unwrap_or(lb)).into_owned();
4769 bcs = ml;
4770 } else {
4771 if lsuf.len() <= lsl && psuf.ends_with(lsuf.as_str()) {
4772 // c:2362 — lsuf is a suffix of psuf → nothing left.
4773 lsuf.clear();
4774 } else if lsuf.len() > lsl && lsuf.ends_with(psuf) {
4775 // c:2364 — psuf is a literal suffix of lsuf → strip it.
4776 let keep = lsuf.len() - lsl;
4777 let lb = lsuf.as_bytes();
4778 lsuf = String::from_utf8_lossy(lb.get(..keep).unwrap_or(lb)).into_owned();
4779 } else {
4780 // c:2366 — `*argv = NULL`: no candidate can match.
4781 ppre_nomatch = true;
4782 }
4783 bcs = lsl as i32; // c:2368
4784 }
4785 crate::ported::zle::compmatch::start_match();
4786 }
4787 }
4788 }
4789 // c:2339 / c:2366 — `*argv = NULL` only EMPTIES the word list. Everything
4790 // after it still runs: the c:2370-2420 pattern probe, the `-A`/`-O`/`-D`
4791 // arrays (c:2596-2603), `addexpl` (c:2604-2605), the `-C` all-match
4792 // (c:2606-2609), the `-E` dummies (c:2610-2611) and the c:2617-2627
4793 // restores. Returning here instead made `compadd -p , -C` on the word
4794 // `a` add NOTHING and fail, where zsh adds `<all>` and returns 0.
4795 let argv: &[String] = if ppre_nomatch { &[] } else { argv };
4796
4797 // c:2360-2389 — when `$compstate[pattern_match]` is set, compile the
4798 // line prefix+suffix (with the completion point as a `*` placeholder)
4799 // into a Patprog so candidates are matched as GLOBS instead of literal
4800 // prefixes. This is what makes `_approximate` work: it turns on
4801 // pattern_match and injects a leading `(#a$n)` approximate-glob into
4802 // PREFIX (via the compadd prefix injector), so e.g. `(#a1)aple*` matches
4803 // `apple`. The Rust port previously always passed `cp = None` to
4804 // comp_match, so pattern_match / approximate completion did nothing.
4805 // C uses an `x` placeholder at the completion point precisely so the
4806 // point wildcard alone never enables pattern matching; mirror that by
4807 // probing haswilds() on lpre+lsuf WITHOUT the placeholder — only real
4808 // wildcards in the typed prefix/suffix (`(#a1)`, `*`, `[...]`) count.
4809 let cp: Option<crate::ported::pattern::Patprog> = {
4810 let cpm = get_compstate_str("pattern_match").unwrap_or_default();
4811 if !cpm.is_empty() {
4812 let is = cpm.starts_with('*'); // c:2361 is = (*comppatmatch == '*')
4813 let mut probe = format!("{}{}", lpre, lsuf);
4814 crate::ported::glob::tokenize(&mut probe); // c:2371
4815 if crate::ported::pattern::haswilds(&probe) {
4816 // c:2372 — has real wildcards.
4817 let mut pat = String::new();
4818 pat.push_str(&lpre); // c:2367 lpre
4819 if is {
4820 // c:2374 — `tmp[llpl] = Star` (completion-point wildcard).
4821 pat.push('*');
4822 }
4823 pat.push_str(&lsuf); // c:2369 lsuf
4824 crate::ported::glob::tokenize(&mut pat);
4825 crate::ported::glob::remnulargs(&mut pat); // c:2376
4826 let prog = crate::ported::pattern::patcompile(&pat, 0, None); // c:2377
4827 if prog.is_some() {
4828 haspattern.store(1, Ordering::Relaxed); // c:2378
4829 }
4830 prog
4831 } else {
4832 None
4833 }
4834 } else {
4835 None
4836 }
4837 };
4838
4839 // c:2278-2300 — dat.ipre/isuf/ppre/psuf duplication with lipre/lisuf.
4840 if let Some(ref existing) = dat.ipre.clone() {
4841 dat.ipre = Some(if !lipre.is_empty() {
4842 format!("{}{}", lipre, existing)
4843 } else {
4844 existing.clone()
4845 });
4846 } else if !lipre.is_empty() {
4847 dat.ipre = Some(lipre.clone());
4848 }
4849 if let Some(ref existing) = dat.isuf.clone() {
4850 dat.isuf = Some(if !lisuf.is_empty() {
4851 format!("{}{}", lisuf, existing)
4852 } else {
4853 existing.clone()
4854 });
4855 } else if !lisuf.is_empty() {
4856 dat.isuf = Some(lisuf.clone());
4857 }
4858 let quote_flag = if (dat.aflags & CAF_QUOTE) != 0 { 1 } else { 0 };
4859 if let Some(ref existing) = dat.ppre.clone() {
4860 let quoted = if (dat.flags & 0x0001/*CMF_FILE*/) != 0 {
4861 tildequote(existing, quote_flag)
4862 } else {
4863 multiquote(existing, quote_flag)
4864 };
4865 dat.ppre = Some(quoted);
4866 }
4867 if let Some(ref existing) = dat.psuf.clone() {
4868 dat.psuf = Some(multiquote(existing, quote_flag));
4869 }
4870 let ppl = dat.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
4871 let psl = dat.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
4872
4873 // c:2431-2455 — the whole `if (*argv) { … }` block was missing.
4874 if !argv.is_empty() {
4875 // c:2436-2440 — `if (!dat->prpre && (dat->prpre = dupstring(oppre)))
4876 // { singsub(&(dat->prpre)); untokenize(dat->prpre); }`
4877 // `oppre` is the UNQUOTED `dat->ppre` saved at c:2084, before the
4878 // c:2288 quoting above. `prpre` is the path used for the real
4879 // filesystem probes (ztat/opendir in add_match_data c:2735+), so it
4880 // has to be the substituted, untokenized form — not the quoted one.
4881 // Without this default, `compadd -p <dir>/` with no explicit `-W`
4882 // left `prpre` empty and every file test ran against the bare match
4883 // name in the CWD.
4884 if dat.prpre.is_none() {
4885 if let Some(op) = oppre.clone() {
4886 let subbed = crate::ported::subst::singsub(&op); // c:2437
4887 dat.prpre = Some(crate::ported::lex::untokenize(&subbed).to_string());
4888 // c:2438
4889 }
4890 }
4891 // c:2443-2447 — `-r`/`-R` are mutually exclusive: a remove-func
4892 // (`remf`) wins and CLEARS `rems`. The port passed both through to
4893 // the match, so a compadd carrying both applied the char-based
4894 // suffix removal as well as the widget.
4895 if dat.remf.is_some() {
4896 dat.rems = None; // c:2445
4897 }
4898 // c:2449-2454 — quote the line prefix/suffix used for matching with
4899 // the OUTER quoting level (`ign = 1`), so comp_match compares the
4900 // candidate against text quoted the same way the line is.
4901 lpre = if (dat.aflags & CAF_QUOTE) == 0
4902 && dat.ppre.is_none()
4903 && (dat.flags & 0x0001/*CMF_FILE*/) != 0
4904 {
4905 tildequote(&lpre, 1) // c:2452
4906 } else {
4907 multiquote(&lpre, 1) // c:2452
4908 };
4909 lsuf = multiquote(&lsuf, 1); // c:2454
4910 }
4911
4912 // c:2170 — `doadd` was computed (and set hasmatched/hasunmatched) above,
4913 // at C's position for the assignment.
4914 tracing::debug!(
4915 target: "compsys_args",
4916 %lpre,
4917 %lsuf,
4918 doadd,
4919 caf_match = (dat.aflags & CAF_MATCH) != 0,
4920 dpar = ?dat.dpar,
4921 "addmatches candidate-loop setup"
4922 );
4923 let mut apar_list: Vec<String> = Vec::new();
4924 let mut opar_list: Vec<String> = Vec::new();
4925
4926 // c:2189-2207 — `-D par…` setup. Each `dpar` name holds an array
4927 // PARALLEL to the candidate words; as each word is added, the current
4928 // element of every dpar array is consumed into an output list, and at
4929 // the end the output lists are written back (so each dpar param ends
4930 // up holding just the entries for the words that matched). Names whose
4931 // array is empty/missing are dropped (C swaps them out). Bug #657 —
4932 // the previous port collected apar/opar but never handled dpar.
4933 let mut dpar_names: Vec<String> = Vec::new();
4934 let mut dparr: Vec<Vec<String>> = Vec::new();
4935 let mut dpar_idx: Vec<usize> = Vec::new();
4936 let mut dparl: Vec<Vec<String>> = Vec::new();
4937 for name in &dat.dpar {
4938 match crate::ported::params::getaparam(name) {
4939 Some(arr) if !arr.is_empty() => {
4940 dpar_names.push(name.clone()); // c:2197 getaparam non-empty
4941 dparr.push(arr);
4942 dpar_idx.push(0);
4943 dparl.push(Vec::new());
4944 }
4945 _ => {} // c:2200 — drop names with an empty/missing array
4946 }
4947 }
4948
4949 // c:2460-2476 + c:2582-2600 — CAF_ARRAYS expansion. `compadd -a
4950 // NAME…` / `-k NAME…` pass PARAMETER names, not literal matches:
4951 // the candidates are the values of the named arrays (or, with
4952 // CAF_KEYS from `-k`, the keys of the named associative arrays).
4953 // C weaves array-switching through the candidate loop via the
4954 // `next_array` label; because every non-empty array's elements are
4955 // consumed in order, the resulting candidate set is just the
4956 // concatenation of `get_data_arr` over the names — so expand up
4957 // front. Without this, `_command_names`' `compadd -k commands`
4958 // adds the literal word "commands" instead of every command name.
4959 let expanded_argv: Vec<String>;
4960 let argv: &[String] = if (dat.aflags & CAF_ARRAYS) != 0 {
4961 let keys = (dat.aflags & CAF_KEYS) != 0; // c:2468 CAF_KEYS
4962 let mut acc: Vec<String> = Vec::new();
4963 for name in argv {
4964 if let Some(vals) = get_data_arr(name, keys) {
4965 acc.extend(vals);
4966 }
4967 }
4968 expanded_argv = acc;
4969 &expanded_argv
4970 } else {
4971 argv
4972 };
4973
4974 // c:2482-2601 — main candidate loop.
4975 let mut added = 0i32;
4976 let mut disp_idx = 0usize;
4977 let mut compignored_local = 0i32;
4978 // c:2520-2522 / c:2540-2542 — `-D` parallel-array bookkeeping: the dpar
4979 // arrays advance in LOCKSTEP with the candidate words. When a word is
4980 // skipped (ignored, or comp_match fails) its dpar element must be dropped
4981 // — i.e. `dpar_idx` advances WITHOUT the corresponding value being kept —
4982 // so that surviving words stay aligned with their array elements. The
4983 // port only advanced `dpar_idx` on the KEEP path (else branch below), so
4984 // after any non-matching word every survivor grabbed the wrong element
4985 // (e.g. `compadd -D` for path completion kept `/Applications` instead of
4986 // `/tmp`), breaking `cmd /path/<TAB>` entirely.
4987 macro_rules! dpar_skip_word {
4988 () => {
4989 for i in 0..dpar_idx.len() {
4990 if dpar_idx[i] < dparr[i].len() {
4991 dpar_idx[i] += 1;
4992 }
4993 }
4994 };
4995 }
4996 'cand: for word in argv {
4997 // c:2482
4998 // c:2486-2489 — advance disp index.
4999 let cur_disp = if !disp_arr.is_empty() && disp_idx < disp_arr.len() {
5000 let d = disp_arr[disp_idx].clone();
5001 disp_idx += 1;
5002 Some(d)
5003 } else {
5004 None
5005 };
5006
5007 // c:2491-2527 — aign/pign suffix-test + Patprog test.
5008 if !aign.is_empty() || !pign.is_empty() {
5009 let full = format!(
5010 "{}{}{}",
5011 dat.ppre.as_deref().unwrap_or(""),
5012 word,
5013 dat.psuf.as_deref().unwrap_or("")
5014 );
5015 // c:2508-2510 — literal-suffix check. C requires the ignored
5016 // suffix to be STRICTLY shorter than the whole string
5017 // (`filell < il`), so an fignore entry equal to the candidate
5018 // does NOT ignore it; the port's `>=` dropped those candidates.
5019 for suf in &aign {
5020 if full.len() > suf.len() && full.ends_with(suf.as_str()) {
5021 // c:2519 — `compignored++` on the GLOBAL, which is
5022 // `$compstate[ignored]` (complete.c:1300). The port
5023 // counted into a local that was discarded at the end of
5024 // the function, so `_ignored` (which returns 1 unless
5025 // `$compstate[ignored]` is non-zero) could never fire and
5026 // ignored-patterns had no fallback completer.
5027 crate::ported::zle::complete::COMPIGNORED.fetch_add(1, Ordering::Relaxed);
5028 compignored_local += 1;
5029 dpar_skip_word!(); // c:2520
5030 continue 'cand;
5031 }
5032 }
5033 // c:2512-2517 — Patprog check.
5034 for prog in &pign {
5035 if crate::ported::pattern::pattry(prog, &full) {
5036 crate::ported::zle::complete::COMPIGNORED.fetch_add(1, Ordering::Relaxed); // c:2519
5037 compignored_local += 1;
5038 dpar_skip_word!(); // c:2520
5039 continue 'cand;
5040 }
5041 }
5042 }
5043
5044 // c:2528 — CAF_MATCH dispatch: when CAF_MATCH is set, run
5045 // comp_match with the active matcher chain (else branch); else
5046 // emit the (multi)quoted word directly with a single-anchor
5047 // Cline (no-matcher path c:2530-2533).
5048 let ms: String;
5049 let _lc;
5050 let isexact;
5051 if (dat.aflags & CAF_MATCH) == 0 {
5052 // c:2528-2534 — non-match mode: just (multi)quote the word.
5053 ms = if (dat.aflags & CAF_QUOTE) != 0 {
5054 word.clone()
5055 } else {
5056 multiquote(word, 0)
5057 };
5058 let sl = ms.len() as i32;
5059 _lc = bld_parts(&ms, sl, -1, None, None);
5060 isexact = 0;
5061 } else {
5062 // c:2535
5063 // c:2535-2546 — matcher-driven mode via comp_match.
5064 let qu = if (dat.aflags & CAF_QUOTE) != 0 {
5065 0
5066 } else if dat.ppre.is_some() || (dat.flags & 0x0001/*CMF_FILE*/) == 0 {
5067 1
5068 } else {
5069 2
5070 };
5071 let mut lc_out: Option<Box<Cline>> = None;
5072 let mut isexact_out = 0i32;
5073 // c:2535 — comp_match(lpre, lsuf, s, cp, &lc, qu, &bpl, bcp,
5074 // &bsl, bcs, &isexact).
5075 match crate::ported::zle::compmatch::comp_match(
5076 &lpre,
5077 &lsuf,
5078 word,
5079 cp.as_ref(),
5080 Some(&mut lc_out),
5081 qu,
5082 None,
5083 bcp, // c:2535 — brace-count base advanced by the ppre strip
5084 None,
5085 bcs, // c:2535 — and by the psuf strip (c:2359/c:2368)
5086 &mut isexact_out,
5087 ) {
5088 Some(matched) => {
5089 ms = matched;
5090 _lc = lc_out;
5091 isexact = isexact_out;
5092 }
5093 None => {
5094 dpar_skip_word!(); // c:2540 — drop this word's dpar element
5095 continue 'cand; // c:2541-2545 reject
5096 }
5097 }
5098 }
5099
5100 if doadd {
5101 // c:2547
5102 // c:2556 — add_match_data.
5103 let cm = add_match_data(
5104 0,
5105 &ms,
5106 word,
5107 _lc.clone(), // line — real Cline from comp_match
5108 dat.ipre.as_deref().unwrap_or(""),
5109 "", // ripre
5110 dat.isuf.as_deref().unwrap_or(""),
5111 dat.pre.as_deref(),
5112 dat.prpre.as_deref().unwrap_or(""),
5113 dat.ppre.as_deref().unwrap_or(""),
5114 None, // pline (path-prefix Cline; unused on this path)
5115 dat.psuf.as_deref().unwrap_or(""),
5116 None, // sline (path-suffix Cline; unused on this path)
5117 dat.suf.as_deref(),
5118 dat.flags,
5119 isexact,
5120 );
5121 // c:2557-2564 — `cm->disp = dparr ? *dparr++ : NULL` (and the
5122 // CMF_DISPLINE flag when `-l`/CAF_...): C patches the STORED
5123 // match through the returned pointer; the port's return-by-value
5124 // clone can't, so patch the just-pushed copy in the live list.
5125 // Without this every `compadd -d` display string (the
5126 // description lines _describe/_arguments build) was silently
5127 // dropped and lists rendered bare match names.
5128 // c:2562-2563 — `if (disp) cm->disp = dupstring(*disp)`. C
5129 // patches the STORED match through the returned pointer; the
5130 // port's add_match_data returns a VALUE clone while the stored
5131 // copy lives in the `matches` list, so patch that copy in place
5132 // (inline — no C-counterpart fn). Without this every
5133 // `compadd -d` display string (the description lines
5134 // _describe/_arguments build) was silently dropped and lists
5135 // rendered bare match names.
5136 // c:2560-2561 — `cm->rems = dat->rems; cm->remf = dat->remf`. These
5137 // were previously SKIPPED here (only `disp` was patched), so a
5138 // `compadd -r <chars>` / `-R <widget>` custom suffix-removal spec
5139 // never reached the match: makesuffixstr got `s=None` and the
5140 // char-based auto-remove-suffix was inert. Patch them on the just-
5141 // pushed match copy (the port's add_match_data returns a value).
5142 //
5143 // c:2560 — the whole patch is inside `if ((cm = add_match_data(…)))`,
5144 // so a match REJECTED by the c:2999 brace filter must not have the
5145 // previous match's rems/remf/disp written over it.
5146 if cm.is_some() {
5147 if let Ok(mut g) = crate::comp_match_handles::matches_arc().lock() {
5148 if let Some(last) = g.last_mut() {
5149 last.rems = dat.rems.clone(); // c:2560
5150 last.remf = dat.remf.clone(); // c:2561
5151 if cur_disp.is_some() {
5152 last.disp = cur_disp.clone(); // c:2562-2563
5153 }
5154 }
5155 }
5156 added += 1;
5157 }
5158 } else {
5159 // c:2566
5160 if dat.apar.is_some() {
5161 // c:2567
5162 apar_list.push(ms.clone());
5163 }
5164 if dat.opar.is_some() {
5165 // c:2569
5166 opar_list.push(word.clone());
5167 }
5168 // c:2571-2578 — consume one element from each live dpar array
5169 // in lockstep with this added word.
5170 for i in 0..dparl.len() {
5171 if dpar_idx[i] < dparr[i].len() {
5172 dparl[i].push(dparr[i][dpar_idx[i]].clone());
5173 dpar_idx[i] += 1;
5174 }
5175 }
5176 }
5177 }
5178
5179 // c:2602-2608 — apar/opar/dpar writeback.
5180 if let Some(ref name) = dat.apar {
5181 setaparam(name, apar_list);
5182 }
5183 if let Some(ref name) = dat.opar {
5184 setaparam(name, opar_list);
5185 }
5186 // c:2606-2607 — set_list_array(dpar[i], dparl[i]).
5187 for (i, name) in dpar_names.iter().enumerate() {
5188 setaparam(name, std::mem::take(&mut dparl[i]));
5189 }
5190
5191 // c:2610 — explanation emit.
5192 if dat.exp.is_some() {
5193 // c:2610
5194 addexpl(false);
5195 }
5196
5197 // c:2612-2614 — `<all>` placeholder when CAF_ALL set. C passes the
5198 // SHARED disp cursor (`addmatch("<all>", …, &disp, 1)`, c:2612), exactly
5199 // as the dummy loop below does, so with `-d` the placeholder CONSUMES the
5200 // next display string. Passing None left its disp empty, and
5201 // `bld_all_str` (c:2198-2230) then filled it by concatenating the match
5202 // `str`s — so `compadd -C -d arr -- a b` listed "a b" where zsh lists the
5203 // next entry of `arr`.
5204 let hasall = hasallmatch.load(Ordering::Relaxed);
5205 if hasall == 0 && (dat.aflags & CAF_ALL) != 0 {
5206 let d: Option<&str> = if disp_idx < disp_arr.len() {
5207 let d = Some(disp_arr[disp_idx].as_str());
5208 disp_idx += 1;
5209 d
5210 } else {
5211 None
5212 };
5213 addmatch(
5214 "<all>",
5215 dat.flags | crate::ported::zle::comp_h::CMF_ALL,
5216 d,
5217 true,
5218 );
5219 hasallmatch.store(1, Ordering::Relaxed);
5220 }
5221
5222 // c:2616-2617 — dummy entries. C's addmatch advances the SHARED disp
5223 // cursor (`&disp`, c:2054-2058), so each dummy carries the next
5224 // remaining display string — this is how the description-only lines
5225 // (compdescribe's CRT_EXPL `-E n` run: "opt -- description") reach
5226 // the list. Passing None here dropped every description line.
5227 while dat.dummies > 0 {
5228 let d: Option<&str> = if disp_idx < disp_arr.len() {
5229 let d = Some(disp_arr[disp_idx].as_str());
5230 disp_idx += 1;
5231 d
5232 } else {
5233 None
5234 };
5235 addmatch(
5236 "",
5237 dat.flags | crate::ported::zle::comp_h::CMF_DUMMY,
5238 d,
5239 false,
5240 );
5241 dat.dummies -= 1;
5242 }
5243
5244 tracing::debug!(
5245 target: "compsys_args",
5246 added,
5247 mnum = mnum.load(Ordering::Relaxed),
5248 doadd,
5249 "addmatches candidate-loop done"
5250 );
5251 let _ = (ppl, psl, compignored_local, added);
5252
5253 // c:2624-2626 — `instring = ois; inbackt = oib; autoq = oaq;`. The
5254 // c:2139-2168 block above rewrites all three from `$compstate[quote]`
5255 // for the duration of THIS compadd only; the port set them and never
5256 // put them back, so a single `compadd` inside a quoted word left
5257 // `instring`/`inbackt` clobbered for the rest of the completion (and for
5258 // `get_comp_string`'s next round, which reads them).
5259 instring_set(ois); // c:2624
5260 inbackt_set(oib); // c:2625
5261 autoq_set(&oaq); // c:2626
5262 // c:2627-2630 — `zsfree(qipre); zsfree(qisuf); qipre = oqp; qisuf = oqs;`
5263 for (global, v) in [
5264 (&crate::ported::zle::zle_tricky::QIPRE, oqp), // c:2629
5265 (&crate::ported::zle::zle_tricky::QISUF, oqs), // c:2630
5266 ] {
5267 if let Ok(mut g) = global.get_or_init(|| Mutex::new(String::new())).lock() {
5268 *g = v;
5269 }
5270 }
5271
5272 // c:2632-2633 — `if (mnum == nm) haspattern = ohp;`. A compadd that
5273 // added NOTHING must not leave `haspattern` raised by its own pattern
5274 // compile at c:2387/c:2428 — `do_ambiguous` keys menu-vs-insert
5275 // behaviour off it (compresult.c:764).
5276 if mnum.load(Ordering::Relaxed) == _nm {
5277 haspattern.store(ohp, Ordering::Relaxed); // c:2633
5278 }
5279
5280 // c:2635 — `return (mnum == nm)`: non-zero (1) when NO new matches were
5281 // added this call, 0 when at least one landed. Was hardcoded `0`, so
5282 // every `compadd` reported success even against zero matches — the
5283 // `completer` chain (_main_complete) then stopped after `_complete`
5284 // and never advanced to `_approximate` / `_ignored` / etc., and any
5285 // `compadd … && ret=0` idiom in a completer wrongly took the match arm.
5286 if mnum.load(Ordering::Relaxed) == _nm {
5287 1
5288 } else {
5289 0
5290 }
5291}
5292
5293// =====================================================================
5294// add_match_data — `Src/Zle/compcore.c:2643`.
5295// =====================================================================
5296
5297/// Direct port of `Cmatch add_match_data(int alt, char *str, char *orig,
5298/// Cline line, char *ipre, char *ripre, char *isuf, char *pre,
5299/// char *prpre, char *ppre, Cline pline, char *psuf, Cline sline,
5300/// char *suf, int flags, int exact)` from compcore.c:2643.
5301///
5302/// Builds one `Cmatch` from the supplied prefix/suffix bits plus the
5303/// surrounding Cline chain. Threads `line`/`pline`/`sline` through
5304/// `cline_matched` so the CLF_MATCHED state-machine update fires the
5305/// same way as C, then performs path-prefix/suffix splicing via
5306/// `bld_parts` to extend the Cline chain at the appropriate anchor.
5307///
5308/// Returns `None` — C's NULL at c:3000 — when the match is rejected by the
5309/// unfinished-brace filter (`lastprebr`/`lastpostbr` + `hasbrpsfx`). Nothing
5310/// is registered in that case: `mnum`, `ai->count` and the match list are all
5311/// left untouched.
5312#[allow(clippy::too_many_arguments)]
5313pub fn add_match_data(
5314 // c:2643
5315 alt: i32,
5316 str: &str,
5317 orig: &str,
5318 mut line: Option<Box<Cline>>,
5319 ipre_: &str,
5320 ripre_: &str,
5321 isuf_: &str,
5322 pre: Option<&str>,
5323 prpre: &str,
5324 ppre: &str,
5325 mut pline: Option<Box<Cline>>,
5326 psuf: &str,
5327 mut sline: Option<Box<Cline>>,
5328 suf: Option<&str>,
5329 flags: i32,
5330 exact: i32,
5331) -> Option<Cmatch> {
5332 // c:2663 — DPUTS(!line, "BUG: add_match_data() without cline")
5333 DPUTS!(line.is_none(), "BUG: add_match_data() without cline"); // c:2663
5334 // c:2656 — `Aminfo ai = (alt ? fainfo : ainfo);` — EVERY `ai->…` below
5335 // (c:3002 join_clines, c:3005 count++, c:3023 firstm, c:3036-3063 exact)
5336 // goes through this selection. The port bound it and then used the plain
5337 // `ainfo` at all five sites, so every alternate-set match (`alt != 0` —
5338 // the fignore/ignored-suffix path) accumulated into the MAIN aminfo:
5339 // `ainfo.count` counted matches that were never offered, and the
5340 // unambiguous cline built from `ainfo.line` was polluted by strings the
5341 // user asked to ignore.
5342 let ai_ref: &OnceLock<Mutex<Option<Aminfo>>> = if alt != 0 { &fainfo } else { &ainfo }; // c:2656
5343 // c:2666-2671 — cline_matched(line); pline; sline.
5344 cline_matched(&mut line);
5345 if pline.is_some() {
5346 cline_matched(&mut pline);
5347 }
5348 if sline.is_some() {
5349 cline_matched(&mut sline);
5350 }
5351
5352 // c:2675-2697 — accumulator lengths.
5353 let psl = psuf.len();
5354 let isl = isuf_.len();
5355 let qisuf_v = qisuf_get(); // c:2680
5356 let qisl = qisuf_v.len();
5357 let _salen = (if sline.is_none() { psl } else { 0 }) + isl + qisl; // c:2675-2683
5358
5359 let ipl = ipre_.len();
5360 // c:2769-2770 — `if (pre) palen += (pl = strlen(pre));`: C leaves `pl` at 0
5361 // for a NULL `pre`, so every later use (bld_parts at c:2809, the memcpy at
5362 // c:2837) copies nothing. A zero-length &str is exactly that behaviour, so
5363 // the arithmetic below keeps using a &str view; only the STORED cm.pre /
5364 // cm.suf keep the Option, which is the distinction c:2943-2944 preserves.
5365 let pre_s: &str = pre.unwrap_or("");
5366 let suf_s: &str = suf.unwrap_or("");
5367 let _ppl = ppre.len();
5368 let _pl = pre_s.len();
5369 let qipre_v = qipre_get(); // c:2686
5370 let qipl_v = qipre_v.clone();
5371 let _qipl = qipl_v.len();
5372
5373 let _stl = str.len();
5374 let _lpl = ripre_.len();
5375 let _lsl = suf_s.len();
5376 let _ml = ipl;
5377
5378 // c:2671-2762 — path-suffix Cline splicing. salen accumulates psl
5379 // (psuf when no sline), isl (isuf), qisl (qisuf). When salen > 0
5380 // and line is non-empty, we walk to the tail and append the
5381 // bld_parts-built Cline for each contributing string.
5382 let psl_local = if sline.is_none() && !psuf.is_empty() {
5383 psuf.len() as i32
5384 } else {
5385 0
5386 };
5387 let isl_local = isuf_.len() as i32;
5388 let qisl_local = qisuf_v.len() as i32;
5389 let salen = psl_local + isl_local + qisl_local;
5390 if salen > 0 && line.is_some() {
5391 // Walk to the tail of line via .next.
5392 unsafe {
5393 let mut tail: *mut Option<Box<Cline>> = &mut line;
5394 while let Some(ref n) = *tail {
5395 if n.next.is_none() {
5396 break;
5397 }
5398 tail = &mut (*tail).as_mut().unwrap().next;
5399 }
5400 // For each contributing string, build a Cline chain via
5401 // bld_parts and attach to the tail node's .next.
5402 if psl_local > 0 {
5403 let s = bld_parts(psuf, psl_local, psl_local, None, None);
5404 if let Some(node) = (*tail).as_mut() {
5405 node.next = s;
5406 while let Some(ref nn) = node.next {
5407 if nn.next.is_none() {
5408 break;
5409 }
5410 // already linked correctly; loop to advance tail
5411 break;
5412 }
5413 }
5414 // Walk to the new tail.
5415 while let Some(ref n) = *tail {
5416 if n.next.is_none() {
5417 break;
5418 }
5419 tail = &mut (*tail).as_mut().unwrap().next;
5420 }
5421 }
5422 if isl_local > 0 {
5423 let s = bld_parts(isuf_, isl_local, isl_local, None, None);
5424 if let Some(node) = (*tail).as_mut() {
5425 node.next = s;
5426 }
5427 while let Some(ref n) = *tail {
5428 if n.next.is_none() {
5429 break;
5430 }
5431 tail = &mut (*tail).as_mut().unwrap().next;
5432 }
5433 }
5434 if qisl_local > 0 {
5435 let mut s = bld_parts(&qisuf_v, qisl_local, qisl_local, None, None);
5436 // c:2741 — qsl->flags |= CLF_SUF; qsl->suffix = qsl->prefix.
5437 if let Some(qsl) = s.as_mut() {
5438 qsl.flags |= crate::ported::zle::comp_h::CLF_SUF;
5439 qsl.suffix = qsl.prefix.take();
5440 }
5441 if let Some(node) = (*tail).as_mut() {
5442 node.next = s;
5443 }
5444 }
5445 }
5446 }
5447
5448 // c:2766-2873 — path-prefix Cline splicing. palen accumulates qipl,
5449 // ipl, pl, ppl (when no pline). Each contributing string gets a
5450 // bld_parts Cline prepended to `line`.
5451 let qipl_local = qipre_v.len() as i32;
5452 let ipl_local = ipre_.len() as i32;
5453 let pl_local = pre_s.len() as i32;
5454 let ppl_local = if pline.is_none() && !ppre.is_empty() {
5455 ppre.len() as i32
5456 } else {
5457 0
5458 };
5459 if pl_local > 0 {
5460 if ppl_local > 0 {
5461 let p = bld_parts(ppre, ppl_local, ppl_local, None, None);
5462 // Walk p to its tail, link its tail's next to line.
5463 if p.is_some() {
5464 let mut p_chain = p;
5465 let mut tail: *mut Option<Box<Cline>> = &mut p_chain;
5466 unsafe {
5467 while let Some(ref n) = *tail {
5468 if n.next.is_none() {
5469 break;
5470 }
5471 tail = &mut (*tail).as_mut().unwrap().next;
5472 }
5473 if let Some(t) = (*tail).as_mut() {
5474 t.next = line.take();
5475 }
5476 }
5477 line = p_chain;
5478 }
5479 }
5480 let p = bld_parts(pre_s, pl_local, pl_local, None, None);
5481 if let Some(mut head) = p {
5482 let mut t: *mut Option<Box<Cline>> = &mut head.next;
5483 unsafe {
5484 while (*t).is_some() {
5485 if (*t).as_deref().unwrap().next.is_none() {
5486 break;
5487 }
5488 t = &mut (*t).as_mut().unwrap().next;
5489 }
5490 *t = line.take();
5491 }
5492 line = Some(head);
5493 }
5494 if ipl_local > 0 {
5495 let p = bld_parts(ipre_, ipl_local, ipl_local, None, None);
5496 if let Some(mut head) = p {
5497 let mut t: *mut Option<Box<Cline>> = &mut head.next;
5498 unsafe {
5499 // Walk to the empty slot past the tail node, then attach
5500 // `line` there. The previous code stopped AT the last node
5501 // and did `*t = line`, OVERWRITING it (dropped the final
5502 // bld_parts segment — e.g. the last dir component `zdt3/`
5503 // of `/private/tmp/zdt3/` — so ambiguous path completion
5504 // corrupted the line). c:2812/2818/2824/2841 `lp->next =
5505 // line` appends; it never replaces lp.
5506 while (*t).is_some() {
5507 t = &mut (*t).as_mut().unwrap().next;
5508 }
5509 *t = line.take();
5510 }
5511 line = Some(head);
5512 }
5513 }
5514 if qipl_local > 0 {
5515 let p = bld_parts(&qipre_v, qipl_local, qipl_local, None, None);
5516 if let Some(mut head) = p {
5517 let mut t: *mut Option<Box<Cline>> = &mut head.next;
5518 unsafe {
5519 // Walk to the empty slot past the tail node, then attach
5520 // `line` there. The previous code stopped AT the last node
5521 // and did `*t = line`, OVERWRITING it (dropped the final
5522 // bld_parts segment — e.g. the last dir component `zdt3/`
5523 // of `/private/tmp/zdt3/` — so ambiguous path completion
5524 // corrupted the line). c:2812/2818/2824/2841 `lp->next =
5525 // line` appends; it never replaces lp.
5526 while (*t).is_some() {
5527 t = &mut (*t).as_mut().unwrap().next;
5528 }
5529 *t = line.take();
5530 }
5531 line = Some(head);
5532 }
5533 }
5534 } else if qipl_local + ipl_local + pl_local + ppl_local > 0 || pline.is_some() {
5535 // c:2827-2842 — consolidated apre buffer.
5536 let apre = format!(
5537 "{}{}{}{}",
5538 qipre_v.as_str(),
5539 ipre_,
5540 pre_s,
5541 if pline.is_none() { ppre } else { "" }
5542 );
5543 let apre_len = apre.len() as i32;
5544 if apre_len > 0 {
5545 let p = bld_parts(&apre, apre_len, apre_len, None, None);
5546 if let Some(mut head) = p {
5547 let mut t: *mut Option<Box<Cline>> = &mut head.next;
5548 unsafe {
5549 // Walk to the empty slot past the tail node, then attach
5550 // `line` there. The previous code stopped AT the last node
5551 // and did `*t = line`, OVERWRITING it (dropped the final
5552 // bld_parts segment — e.g. the last dir component `zdt3/`
5553 // of `/private/tmp/zdt3/` — so ambiguous path completion
5554 // corrupted the line). c:2812/2818/2824/2841 `lp->next =
5555 // line` appends; it never replaces lp.
5556 while (*t).is_some() {
5557 t = &mut (*t).as_mut().unwrap().next;
5558 }
5559 *t = line.take();
5560 }
5561 line = Some(head);
5562 }
5563 }
5564 }
5565
5566 // c:2869 — `stl = strlen(str);`
5567 // c:2870-2920 — MULTIBYTE_SUPPORT.
5568 // c:2871 — "If "str" contains a character that won't convert into a wide
5569 // c:2872 — character, change it into a $'\123' sequence."
5570 // `mbrtowc` is the locale gate: under UTF-8 a stray 0x9b is MB_INVALID and
5571 // becomes `$'\233'` in cm->str — which is then what the listing prints and
5572 // what do_single inserts — while under LC_ALL=C every byte converts and the
5573 // string is kept as is. Without this the raw byte reached the listing (as
5574 // mb_niceformat's `\M-^[`) and the line.
5575 //
5576 // C walks the byte-metafied string and decodes each `Meta, c^32` pair
5577 // inline (c:2876-2877, c:2897-2898). `str` here is the char-level
5578 // metafied form (`unmetafy_str`), so the pairs are decoded up front and
5579 // the loop below runs on the same raw bytes C's `curchar` sees. Copied
5580 // runs (c:2911-2912) go back out in that form: a run that is valid UTF-8
5581 // stays as its characters, any other byte >= 0x80 is re-metafied as
5582 // `Meta, b^32` (utils.c:4880-4886 `if (imeta(c)) { *p++ = Meta; *p++ = c ^ 32; }`).
5583 let mut new_str: Option<String> = None;
5584 {
5585 use crate::ported::utils::{
5586 mbrtowc, unmetafy_str, MbStateBuf, MBSTATE_ZERO, MB_INCOMPLETE, MB_INVALID,
5587 };
5588 let src = unmetafy_str(str);
5589 let fe = src.len();
5590 let copy_run = |out: &mut String, run: &[u8]| match std::str::from_utf8(run) {
5591 Ok(s) => out.push_str(s),
5592 Err(_) => {
5593 for &b in run {
5594 if b < 0x80 {
5595 out.push(b as char);
5596 } else {
5597 out.push(crate::ported::zsh_h::Meta as char);
5598 out.push(char::from(b ^ 32));
5599 }
5600 }
5601 }
5602 };
5603 // c:2874 — `memset(&mbs, '\0', sizeof mbs);`
5604 let mut mbs: MbStateBuf = MBSTATE_ZERO;
5605 // c:2875 — `for (t = f = fs = str, fe = f + stl; fs < fe; )`. `t` (the
5606 // output end) is `fs` until the first invalid byte allocates new_str.
5607 let (mut t, mut f, mut fs) = (0usize, 0usize, 0usize);
5608 while fs < fe {
5609 // c:2876-2877 — `curchar = *f++` (Meta pair already decoded).
5610 let curchar = src[f];
5611 f += 1;
5612 // c:2878 — `cnt = mbrtowc(&wc, &curchar, 1, &mbs);`
5613 let mut wc: libc::wchar_t = 0;
5614 let cnt = unsafe {
5615 mbrtowc(
5616 &mut wc,
5617 &curchar as *const u8 as *const libc::c_char,
5618 1,
5619 &mut mbs as *mut MbStateBuf as *mut libc::c_void,
5620 )
5621 };
5622 match cnt {
5623 // c:2880-2882 — `case MB_INCOMPLETE: if (f < fe) continue;`
5624 MB_INCOMPLETE if f < fe => continue,
5625 // c:2883-2884 — FALL THROUGH into `case MB_INVALID:`
5626 MB_INCOMPLETE | MB_INVALID => {
5627 // c:2885-2886 — "Get mbs out of its undefined state."
5628 mbs = MBSTATE_ZERO;
5629 // c:2887-2892 — `if (!new_str) { new_str = zhalloc(...);
5630 // memcpy(new_str, str, t - str); ... }`
5631 let out = new_str.get_or_insert_with(|| {
5632 let mut s = String::new();
5633 copy_run(&mut s, &src[..t]);
5634 s
5635 });
5636 // c:2893-2895 — "Output one byte from the start of this
5637 // invalid multibyte sequence unless we got MB_INCOMPLETE at
5638 // the end of the string, in which case we output all the
5639 // incomplete bytes."
5640 loop {
5641 // c:2897-2898
5642 let c = src[fs];
5643 fs += 1;
5644 // c:2899-2905 — `$'\NNN'`
5645 out.push_str("$'\\");
5646 out.push((b'0' + ((c >> 6) & 7)) as char);
5647 out.push((b'0' + ((c >> 3) & 7)) as char);
5648 out.push((b'0' + (c & 7)) as char);
5649 out.push('\'');
5650 // c:2906 — `} while (cnt == MB_INCOMPLETE && fs < fe);`
5651 if !(cnt == MB_INCOMPLETE && fs < fe) {
5652 break;
5653 }
5654 }
5655 // c:2907-2908 — "Scanning restarts from the spot after the
5656 // char we skipped."
5657 f = fs;
5658 }
5659 // c:2910-2913 — `default: while (fs < f) *t++ = *fs++;`
5660 _ => {
5661 match new_str.as_mut() {
5662 Some(out) => copy_run(out, &src[fs..f]),
5663 None => t = f,
5664 }
5665 fs = f;
5666 }
5667 }
5668 }
5669 }
5670 // c:2916-2920 — `if (new_str) { *t = '\0'; str = new_str; stl = t - str; }`
5671 let str: &str = new_str.as_deref().unwrap_or(str);
5672 let stl = str.len();
5673
5674 // c:2929-2932 — Cmatch allocation + str/orig/ppre/psuf.
5675 let mut cm = Cmatch::default(); // c:2929
5676 cm.str = Some(str.to_string()); // c:2930
5677 cm.orig = Some(orig.to_string()); // c:2931
5678 cm.ppre = if ppre.is_empty() {
5679 None
5680 } else {
5681 Some(ppre.into())
5682 }; // c:2932
5683 cm.psuf = if psuf.is_empty() {
5684 None
5685 } else {
5686 Some(psuf.into())
5687 }; // c:2933
5688
5689 // c:2934 — prpre only when CMF_FILE.
5690 cm.prpre = if (flags & CMF_FILE) != 0 && !prpre.is_empty() {
5691 Some(prpre.into())
5692 } else {
5693 None
5694 };
5695
5696 // c:2935-2938 — ipre = qipre + ipre (concat when qipre non-empty).
5697 // qipre_v already computed above.
5698 cm.ipre = if !qipre_v.is_empty() {
5699 if !ipre_.is_empty() {
5700 Some(format!("{}{}", qipre_v, ipre_))
5701 } else {
5702 Some(qipre_v.clone())
5703 }
5704 } else if !ipre_.is_empty() {
5705 Some(ipre_.into())
5706 } else {
5707 None
5708 };
5709
5710 cm.ripre = if ripre_.is_empty() {
5711 None
5712 } else {
5713 Some(ripre_.into())
5714 }; // c:2939
5715
5716 // c:2940-2943 — isuf = isuf + qisuf (concat when qisuf non-empty).
5717 cm.isuf = if !qisuf_v.is_empty() {
5718 if !isuf_.is_empty() {
5719 Some(format!("{}{}", isuf_, qisuf_v))
5720 } else {
5721 Some(qisuf_v.clone())
5722 }
5723 } else if !isuf_.is_empty() {
5724 Some(isuf_.into())
5725 } else {
5726 None
5727 };
5728
5729 // c:2943-2944 — `cm->pre = pre; cm->suf = suf;` are UNCONDITIONAL. The six
5730 // neighbours above them (ppre/psuf/prpre/ipre/ripre/isuf, c:2931-2942) each
5731 // collapse empty-to-NULL with `x && *x ? x : NULL`, and C pointedly does NOT
5732 // do that for these two: an explicit `compadd -S ''` is a REAL empty suffix
5733 // and must stay distinguishable from "no -S given" (`dat.suf` starts NULL at
5734 // complete.c:625). This port collapsed both, so `-S ''` was indistinguishable
5735 // from omitting -S.
5736 cm.pre = pre.map(|s| s.to_string()); // c:2943
5737 cm.suf = suf.map(|s| s.to_string()); // c:2944
5738
5739 // c:2946 — flags + CMF_PACKED/CMF_ROWS from complist.
5740 let complist_s = COMPLIST
5741 .get()
5742 .and_then(|m| m.lock().ok().map(|g| g.clone()))
5743 .unwrap_or_default();
5744 let extra_flags = (if complist_s.contains("packed") {
5745 CMF_PACKED
5746 } else {
5747 0
5748 }) | (if complist_s.contains("rows") {
5749 CMF_ROWS
5750 } else {
5751 0
5752 });
5753 cm.flags = flags | extra_flags;
5754
5755 // c:2950-2951 — mode/fmode init to 0.
5756 cm.mode = 0;
5757 cm.fmode = 0;
5758 cm.modec = '\0';
5759 cm.fmodec = '\0';
5760
5761 // c:2952-2970 — CMF_FILE: stat the path for mode + modec.
5762 use crate::ported::zle::comp_h::CMF_FILE;
5763 if (flags & CMF_FILE) != 0 && !orig.is_empty() && !orig.ends_with('/') {
5764 let pb = format!("{}{}", cm.prpre.as_deref().unwrap_or("./"), orig);
5765 // c:2960-2963 — `ztat(pb, &buf, 1); cm->mode = buf.st_mode;
5766 // if ((cm->modec = file_type(buf.st_mode)) == ' ') cm->modec = 0;`
5767 // The `1` is ztat's ls flag → lstat, so modec is the type of the
5768 // link itself (`@` for a symlink). This is the marker printlist
5769 // shows, so it must come from lstat, not stat.
5770 if let Some(meta) = ztat(&pb, true) {
5771 use std::os::unix::fs::MetadataExt;
5772 cm.mode = meta.mode();
5773 let c = crate::ported::glob::file_type(cm.mode); // c:2962
5774 cm.modec = if c == ' ' { '\0' } else { c }; // c:2963
5775 }
5776 // c:2965-2968 — `ztat(pb, &buf, 0)` → stat, so fmode/fmodec is the
5777 // type of the symlink target (the marker used when following links).
5778 if let Some(meta) = ztat(&pb, false) {
5779 use std::os::unix::fs::MetadataExt;
5780 cm.fmode = meta.mode();
5781 let c = crate::ported::glob::file_type(cm.fmode); // c:2967
5782 cm.fmodec = if c == ' ' { '\0' } else { c }; // c:2968
5783 }
5784 }
5785
5786 // c:2970-2972 — `if ((*compqstack == QT_BACKSLASH && compqstack[1]) ||
5787 // (autoq && *compqstack && compqstack[1] == QT_BACKSLASH))
5788 // cm->flags |= CMF_NOSPACE;`
5789 //
5790 // Missing entirely. A match produced inside a nested quoting context
5791 // whose outer level is backslash-quoting must not get the automatic
5792 // trailing space (it would be swallowed by the enclosing quotes);
5793 // without the flag every such completion inserted a stray space.
5794 {
5795 let qstack = COMPQSTACK
5796 .get_or_init(|| Mutex::new(String::new()))
5797 .lock()
5798 .map(|g| g.clone())
5799 .unwrap_or_default();
5800 let qb = qstack.as_bytes();
5801 let autoq_set_now = AUTOQ
5802 .get()
5803 .and_then(|m| m.lock().ok().map(|g| !g.is_empty()))
5804 .unwrap_or(false);
5805 let q0_bslash = !qb.is_empty() && qb[0] as i32 == QT_BACKSLASH;
5806 let q1_bslash = qb.len() > 1 && qb[1] as i32 == QT_BACKSLASH;
5807 if (q0_bslash && qb.len() > 1) || (autoq_set_now && !qb.is_empty() && q1_bslash) {
5808 cm.flags |= crate::ported::zle::comp_h::CMF_NOSPACE; // c:2972
5809 }
5810 }
5811
5812 // c:2973-2992 — brpl/brsl brace position arrays. Walk BRBEG/BREND
5813 // (the global Brinfo chains from `Src/Zle/zle_tricky.c`), reading
5814 // `curpos` for each entry — the per-match position addmatches snapshots
5815 // at c:2132-2135 from `pos`/`qpos` according to CAF_QUOTE. The port read
5816 // the raw `qpos`, which is the UNQUOTED-case value only, so a
5817 // `compadd -Q` brace expansion got the wrong insertion offsets.
5818 cm.brpl = BRBEG
5819 .get_or_init(|| Mutex::new(None))
5820 .lock()
5821 .ok()
5822 .and_then(|g| {
5823 g.as_ref().map(|head| {
5824 let mut out: Vec<i32> = Vec::new();
5825 let mut cur = Some(head.as_ref());
5826 while let Some(n) = cur {
5827 out.push(n.curpos);
5828 cur = n.next.as_deref();
5829 }
5830 out
5831 })
5832 })
5833 .unwrap_or_default();
5834 cm.brsl = BREND
5835 .get_or_init(|| Mutex::new(None))
5836 .lock()
5837 .ok()
5838 .and_then(|g| {
5839 g.as_ref().map(|head| {
5840 let mut out: Vec<i32> = Vec::new();
5841 let mut cur = Some(head.as_ref());
5842 while let Some(n) = cur {
5843 out.push(n.curpos);
5844 cur = n.next.as_deref();
5845 }
5846 out
5847 })
5848 })
5849 .unwrap_or_default();
5850
5851 cm.qipl = qipre_v.len() as i32; // c:2994
5852 cm.qisl = qisuf_v.len() as i32; // c:2995
5853 // c:2996 — autoq read.
5854 let autoq_v = AUTOQ
5855 .get()
5856 .and_then(|m| m.lock().ok().map(|g| g.clone()))
5857 .unwrap_or_default();
5858 cm.autoq = if !autoq_v.is_empty() {
5859 Some(autoq_v)
5860 } else if INBACKT.load(Ordering::Relaxed) != 0 {
5861 Some("`".into())
5862 } else {
5863 None
5864 };
5865
5866 cm.rems = None;
5867 cm.remf = None;
5868 cm.disp = None; // c:2997
5869
5870 // c:2999-3000 — `if ((lastprebr || lastpostbr) &&
5871 // !hasbrpsfx(cm, lastprebr, lastpostbr)) return NULL;`
5872 //
5873 // The brace filter, absent from the port entirely. When the word being
5874 // completed sits in an UNFINISHED brace list with a comma before the
5875 // cursor (`echo a{b,<TAB>`), `get_comp_string` (zle_tricky.c:2203-2213)
5876 // records the text ahead of the `{` in `lastprebr` and the text after
5877 // the last `}` in `lastpostbr`. Every candidate is then trial-inserted
5878 // by `hasbrpsfx` and DROPPED unless it reproduces exactly that
5879 // surrounding text — which is what makes the brace element the only
5880 // thing being completed. Without the filter every candidate survived,
5881 // so `echo a{b,<TAB>` listed the whole fasd/file corpus where zsh
5882 // reports "No Matches": the stripped word `a` was still the match
5883 // prefix, but nothing checked that inserting a candidate leaves `a`
5884 // in front of the brace.
5885 //
5886 // Both operands are NULL-vs-set tests in C, so this must not be
5887 // written as an emptiness test: `cmd {b,<TAB>` records an EMPTY but
5888 // non-NULL `lastprebr` and still has to run the filter.
5889 {
5890 let (lpb, lsb) = (
5891 crate::ported::zle::zle_tricky::LASTPREBR
5892 .get()
5893 .and_then(|m| m.lock().ok())
5894 .and_then(|g| g.clone()),
5895 crate::ported::zle::zle_tricky::LASTPOSTBR
5896 .get()
5897 .and_then(|m| m.lock().ok())
5898 .and_then(|g| g.clone()),
5899 );
5900 if (lpb.is_some() || lsb.is_some())
5901 && !crate::ported::zle::compresult::hasbrpsfx(&cm, lpb.as_deref(), lsb.as_deref())
5902 {
5903 return None; // c:3000
5904 }
5905 }
5906
5907 // c:3002 — ai->line = join_clines(ai->line, line).
5908 if let Ok(mut g) = ai_ref.get_or_init(|| Mutex::new(None)).lock() {
5909 if let Some(a) = g.as_mut() {
5910 let old_line = a.line.take();
5911 a.line = crate::ported::zle::compmatch::join_clines(old_line, line);
5912 }
5913 }
5914
5915 // c:3004 — mnum++.
5916 mnum.fetch_add(1, Ordering::Relaxed);
5917
5918 // c:3005 — ai->count++.
5919 if let Ok(mut g) = ai_ref.get_or_init(|| Mutex::new(None)).lock() {
5920 if let Some(a) = g.as_mut() {
5921 a.count += 1;
5922 }
5923 }
5924
5925 // c:3008 — addlinknode((alt ? fmatches : matches), cm). Already
5926 // wired below via matches Vec push.
5927
5928 // c:3009-3010 — newmatches = 1; mgroup->new = 1. Only the first half was
5929 // ported; without `mgroup->new` the group holding this match was not
5930 // marked dirty, so `permmatches` could serve its cached permanent copy
5931 // and the match never appeared (the sibling `addmatch` at c:2068 sets it).
5932 newmatches.store(1, Ordering::Relaxed); // c:3009
5933 if let Ok(mg) = mgroup.get_or_init(|| Mutex::new(None)).lock() {
5934 if let Some(grp) = mg.as_ref() {
5935 grp.new_.store(1, Ordering::Relaxed); // c:3010
5936 }
5937 }
5938
5939 // c:3011-3012 — compignored++ when alt.
5940 if alt != 0 {
5941 crate::ported::zle::complete::COMPIGNORED.fetch_add(1, Ordering::Relaxed);
5942 }
5943
5944 // c:3015-3016 — `if (!*complastprompt) dolastprompt = 0;`. Read the
5945 // `complastprompt` value (the "last_prompt" compstate, set at c:326 to
5946 // "yes"/"" from ALWAYS_LAST_PROMPT) — NOT `complastprefix`, a different
5947 // variable the previous port read by mistake. With that bug dolastprompt
5948 // was cleared on every completion, so `clearflag` stayed 0 and `trashzle`
5949 // never emitted TCCLEAREOD: an on-screen completion list was left
5950 // stranded when the command was accepted.
5951 let complastprompt_v = get_compstate_str("last_prompt").unwrap_or_default();
5952 if complastprompt_v.is_empty() {
5953 dolastprompt.store(0, Ordering::Relaxed);
5954 }
5955
5956 // c:3018-3023 — curexpl.count/fcount increment.
5957 if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
5958 if let Some(e) = g.as_mut() {
5959 if alt != 0 {
5960 e.fcount += 1;
5961 } else {
5962 e.count += 1;
5963 }
5964 }
5965 }
5966
5967 // c:3023-3024 — ai->firstm = cm.
5968 if let Ok(mut g) = ai_ref.get_or_init(|| Mutex::new(None)).lock() {
5969 if let Some(a) = g.as_mut() {
5970 if a.firstm.is_none() {
5971 a.firstm = Some(Box::new(cm.clone()));
5972 }
5973 }
5974 }
5975
5976 // c:3027-3034 — minmlen/maxmlen tracking.
5977 let lpl = cm.ppre.as_deref().map(|s| s.len()).unwrap_or(0);
5978 let lsl = cm.psuf.as_deref().map(|s| s.len()).unwrap_or(0);
5979 let ml = (stl + lpl + lsl) as i32;
5980 let cur_min = minmlen.load(Ordering::Relaxed);
5981 let cur_max = maxmlen.load(Ordering::Relaxed);
5982 if ml < cur_min {
5983 minmlen.store(ml, Ordering::Relaxed);
5984 }
5985 if ml > cur_max {
5986 maxmlen.store(ml, Ordering::Relaxed);
5987 }
5988
5989 // c:3036-3064 — exact-match tracking on ai.
5990 if exact != 0 {
5991 // c:3036
5992 // c:3039-3056 — `if (incompfunc && !*compexactstr) compexactstr =
5993 // ppre + str + psuf;` — publishing `$compstate[exact_string]`, which
5994 // `_main_complete` reads to decide whether to accept the exact match
5995 // outright. The publish was missing, so the parameter stayed at the
5996 // "" that do_completion writes at c:312 and the exact match was never
5997 // recognisable to the shell-function layer. Computed BEFORE taking
5998 // the ai lock (set_compstate_str takes the paramtab lock).
5999 let publish_exact = INCOMPFUNC.load(Ordering::Relaxed) != 0
6000 && get_compstate_str("exact_string")
6001 .map(|s| s.is_empty())
6002 .unwrap_or(true);
6003 let exact_str = format!(
6004 "{}{}{}",
6005 cm.ppre.as_deref().unwrap_or(""), // c:3047-3049
6006 str, // c:3051
6007 cm.psuf.as_deref().unwrap_or("") // c:3053
6008 );
6009 let mut do_publish = false;
6010 if let Ok(mut g) = ai_ref.get_or_init(|| Mutex::new(None)).lock() {
6011 if let Some(a) = g.as_mut() {
6012 if a.exact == 0 {
6013 // c:3037
6014 a.exact = useexact.load(Ordering::Relaxed); // c:3038
6015 do_publish = publish_exact;
6016 a.exactm = Some(Box::new(cm.clone())); // c:3057
6017 } else if useexact.load(Ordering::Relaxed) != 0
6018 // c:3058 — C also requires that this exact match DIFFERS
6019 // from the one already recorded (`!ai->exactm ||
6020 // !matcheq(cm, ai->exactm)`). The port dropped that half,
6021 // so a second compadd of the SAME string (routine when
6022 // several completers offer it) escalated `exact` to 2 —
6023 // "ambiguous exact" — and the accept-exact path was lost.
6024 && a.exactm
6025 .as_deref()
6026 .map(|em| !matcheq(&cm, em))
6027 .unwrap_or(true)
6028 {
6029 // c:3059-3060 — ambiguous exact: set to 2, clear exactm.
6030 a.exact = 2;
6031 a.exactm = None;
6032 }
6033 }
6034 }
6035 if do_publish {
6036 set_compstate_str("exact_string", &exact_str); // c:3046-3055
6037 }
6038 }
6039
6040 // c:3064 — push cm into matches/fmatches LinkList.
6041 let cell = if alt != 0 {
6042 crate::comp_match_handles::fmatches_arc()
6043 } else {
6044 crate::comp_match_handles::matches_arc()
6045 };
6046 if let Ok(mut g) = cell.lock() {
6047 g.push(cm.clone());
6048 }
6049
6050 Some(cm) // c:3067 return cm
6051}
6052
6053// `lookup_complist_flags` deleted — Rust-only 8-line helper. Inlined
6054// at the single call site in callcompfunc (c:2049-2051).
6055
6056// =====================================================================
6057// begcmgroup — `Src/Zle/compcore.c:3073`.
6058// =====================================================================
6059
6060/// Port of `mod_export void begcmgroup(char *n, int flags)` from
6061/// compcore.c:3073.
6062pub fn begcmgroup(n: Option<&str>, flags: i32) {
6063 // c:3073
6064 if let Some(name) = n {
6065 // c:3073
6066 let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON // c:3085
6067 | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
6068 // c:3078-3094 — reuse an existing group with the same name+flags.
6069 let reused = {
6070 let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
6071 cell.lock().ok().and_then(|mut g| {
6072 g.iter_mut()
6073 .find(|grp| grp.name.as_deref() == Some(name) && (grp.flags & mask) == flags)
6074 .map(|grp| {
6075 // c:3088 `mgroup = p` is a pointer alias. `mgroup`
6076 // is only ever read for name/flags/`new`/`ylist` and
6077 // the shared `l*` lists, never for `matches` or
6078 // `perm` — the sorted array and the permanent copy
6079 // `permmatches` keeps on the group. Leave those two
6080 // out of the copy: cloning them duplicated every
6081 // match of the group twice on each `compadd` into
6082 // it (2 x 47058 per call on `arch <TAB>`).
6083 let kept_matches = std::mem::take(&mut grp.matches);
6084 let kept_perm = grp.perm.take();
6085 let active = grp.clone();
6086 grp.matches = kept_matches;
6087 grp.perm = kept_perm;
6088 active
6089 })
6090 })
6091 };
6092 if let Some(active) = reused {
6093 // c:3090-3093 — `expls = p->lexpls; matches = p->lmatches;
6094 // fmatches = p->lfmatches; allccs = p->lallccs;`. These are
6095 // pointer aliases into the reused group so appends keep flowing
6096 // into it. `active` was `.cloned()` from `amatches`, and the
6097 // `l*` fields are `Arc<Mutex<…>>`, so this clone SHARES the same
6098 // allocations as the `amatches` original — rebinding the handles
6099 // to them makes every subsequent append land in both, no copy.
6100 crate::comp_match_handles::rebind_current(
6101 &active.lmatches,
6102 &active.lfmatches,
6103 &active.lexpls,
6104 &active.lallccs,
6105 );
6106 let mc = mgroup.get_or_init(|| Mutex::new(None));
6107 if let Ok(mut s) = mc.lock() {
6108 *s = Some(active);
6109 }
6110 return; // c:3095
6111 }
6112 }
6113 let mut grp = Cmgroup::default(); // c:3101
6114 grp.name = n.map(String::from); // c:3105
6115 grp.flags = flags; // c:3108
6116 let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
6117 if let Ok(mut g) = cell.lock() {
6118 g.insert(0, grp.clone()); // c:3121-3124 — shares grp's Arc l* fields
6119 }
6120 // c:3110-3118 — alias the file-scope handles to the fresh group's (empty)
6121 // `l*` Arcs. Same allocation as the `amatches` clone above, so appends
6122 // land in both. No clearing needed: the new group's accumulators start
6123 // empty. Rebind BEFORE moving `grp` into `mgroup`.
6124 crate::comp_match_handles::rebind_current(
6125 &grp.lmatches,
6126 &grp.lfmatches,
6127 &grp.lexpls,
6128 &grp.lallccs,
6129 );
6130 let mc = mgroup.get_or_init(|| Mutex::new(None));
6131 if let Ok(mut s) = mc.lock() {
6132 *s = Some(grp);
6133 }
6134}
6135
6136// =====================================================================
6137// endcmgroup — `Src/Zle/compcore.c:3131`.
6138// =====================================================================
6139
6140/// Port of `mod_export void endcmgroup(char **ylist)` from
6141/// compcore.c:3131.
6142pub fn endcmgroup(ylist: Option<Vec<String>>) {
6143 // c:3131 — C is a one-liner (`mgroup->ylist = ylist`) because the
6144 // file-scope `matches`/`fmatches`/`expls`/`allccs` LinkLists ARE this
6145 // group's `l*` lists (aliased by begcmgroup). The Rust port keeps
6146 // those as separate Mutex globals, so on close we flush them into the
6147 // matching group inside `amatches` (identified by name+flags exactly
6148 // as begcmgroup dedups). Without this the matches added between
6149 // begcmgroup/endcmgroup never reach permmatches and `nmatches`
6150 // stays 0.
6151 // c:3133 — `mgroup->ylist = ylist`. The NULL is stored AS a NULL: an
6152 // absent ylist and an empty-but-present one take different branches in
6153 // `calclist` (compresult.c:1526/1673/1726 test the pointer), so
6154 // `unwrap_or_default()` here would erase the distinction before it
6155 // ever reaches the listing code.
6156 let yl = ylist;
6157
6158 // c:3131 — in C this is a one-liner (`mgroup->ylist = ylist`): `matches`
6159 // etc. already ARE `mgroup->lmatches` (aliased in begcmgroup), so nothing
6160 // to flush. The port now mirrors that — the group's `l*` are shared
6161 // `Arc`s the file-scope handles point at, so `compadd`'s appends are
6162 // already in the group. `new_` is shared the same way now
6163 // (comp_h.rs `Cmgroup::new_`), so the only per-clone SCALAR field left
6164 // to copy is `ylist`.
6165
6166 // Identify the current group and record ylist on the mgroup holder.
6167 let (name, flags) = {
6168 let mc = mgroup.get_or_init(|| Mutex::new(None));
6169 match mc.lock() {
6170 Ok(mut g) => match g.as_mut() {
6171 Some(grp) => {
6172 grp.ylist = yl.clone(); // c:3140
6173 (grp.name.clone(), grp.flags)
6174 }
6175 None => return,
6176 },
6177 Err(_) => return,
6178 }
6179 };
6180
6181 // Does the closing group carry any live content? Read the SHARED `l*`
6182 // Arcs (via handles that drop their guard before we lock the inner Vec,
6183 // so no lock is held into the `amatches` block below → no deadlock).
6184 // `newmatches` must be marked whenever a group holds matches OR an
6185 // explanation-only message (`_message -e`), else permmatches early-returns
6186 // on its stale cache and the group never displays.
6187 let flushed_any = !crate::comp_match_handles::matches_arc()
6188 .lock()
6189 .unwrap()
6190 .is_empty()
6191 || !crate::comp_match_handles::fmatches_arc()
6192 .lock()
6193 .unwrap()
6194 .is_empty()
6195 || !crate::comp_match_handles::expls_arc()
6196 .lock()
6197 .unwrap()
6198 .is_empty();
6199
6200 let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
6201 // Copy ONLY the scalar field to the amatches entry; its `l*` and `new_`
6202 // are the same allocations as this group's, already carrying the
6203 // appended matches and the dirty flag.
6204 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
6205 if let Some(grp) = g
6206 .iter_mut()
6207 .find(|grp| grp.name == name && (grp.flags & mask) == (flags & mask))
6208 {
6209 grp.ylist = yl;
6210 }
6211 }
6212 if flushed_any {
6213 newmatches.store(1, Ordering::Relaxed);
6214 }
6215}
6216
6217// =====================================================================
6218// addexpl — `Src/Zle/compcore.c:3140`.
6219// =====================================================================
6220
6221/// Port of `mod_export void addexpl(int always)` from compcore.c:3140.
6222pub fn addexpl(always: bool) {
6223 // c:3140
6224 let curexpl_snap = {
6225 let cell = curexpl.get_or_init(|| Mutex::new(None));
6226 cell.lock().ok().and_then(|g| g.clone())
6227 };
6228 let curexpl_str = match curexpl_snap.as_ref().and_then(|e| e.str.clone()) {
6229 Some(s) => s,
6230 None => return,
6231 };
6232 let curexpl_count = curexpl_snap.as_ref().map(|e| e.count).unwrap_or(0);
6233 let curexpl_fcount = curexpl_snap.as_ref().map(|e| e.fcount).unwrap_or(0);
6234
6235 let elist = crate::comp_match_handles::expls_arc();
6236 if let Ok(mut g) = elist.lock() {
6237 for e in g.iter_mut() {
6238 // c:3145
6239 if e.str.as_deref() == Some(curexpl_str.as_str()) {
6240 // c:3147
6241 e.count += curexpl_count; // c:3148
6242 e.fcount += curexpl_fcount; // c:3149
6243 if always {
6244 // c:3150
6245 e.always = 1;
6246 nmessages.fetch_add(1, Ordering::Relaxed); // c:3152
6247 newmatches.store(1, Ordering::Relaxed); // c:3153
6248 let mc = mgroup.get_or_init(|| Mutex::new(None));
6249 if let Ok(mg) = mc.lock() {
6250 if let Some(grp) = mg.as_ref() {
6251 grp.new_.store(1, Ordering::Relaxed); // c:3154
6252 }
6253 }
6254 }
6255 return; // c:3156
6256 }
6257 }
6258 if let Some(e) = curexpl_snap {
6259 // c:3159
6260 g.push(e);
6261 }
6262 }
6263 newmatches.store(1, Ordering::Relaxed); // c:3160
6264 if always {
6265 // c:3161
6266 let mc = mgroup.get_or_init(|| Mutex::new(None));
6267 if let Ok(mg) = mc.lock() {
6268 if let Some(grp) = mg.as_ref() {
6269 grp.new_.store(1, Ordering::Relaxed); // c:3162
6270 }
6271 }
6272 nmessages.fetch_add(1, Ordering::Relaxed); // c:3173
6273 }
6274}
6275
6276// =====================================================================
6277// matchcmp — `Src/Zle/compcore.c:3173`.
6278// =====================================================================
6279
6280/// Port of `static int matchcmp(Cmatch *a, Cmatch *b)` from
6281/// compcore.c:3173.
6282///
6283/// WARNING: signature differs from C — Rust=(a, ak, b, bk) vs C=(a, b).
6284/// `ak`/`bk` are the `zstrcmp` operands of `a`/`b` prepared once per sort
6285/// (`crate::tolerant_sort::MatchSortKey`). C reads `(*a)->str` /
6286/// `(*a)->disp` as metafied, NUL-terminated `char *` and the comparison
6287/// costs nothing but the collation; the port stores matches unmetafied, and
6288/// deriving the operands inside the comparator re-did a metafy check, a NUL
6289/// scan, a backslash scan and a NUL-terminating copy per operand on every
6290/// one of qsort's O(n log n) calls. `arch <TAB>` (47058 command names,
6291/// sorted four times by four `$compstate[nmatches]` reads) spent 61% of the
6292/// completion there. Callers build keys with `MatchSortKey::new(m)`.
6293pub fn matchcmp(
6294 a: &Cmatch,
6295 ak: &crate::tolerant_sort::MatchSortKey,
6296 b: &Cmatch,
6297 bk: &crate::tolerant_sort::MatchSortKey,
6298) -> std::cmp::Ordering {
6299 // c:3173
6300 let order = MATCHORDER.load(Ordering::Relaxed);
6301 let sortdir = if (order & CGF_REVSORT) != 0 { -1 } else { 1 }; // c:3177
6302
6303 let cmp = (b.disp.is_some() as i32) - (a.disp.is_some() as i32); // c:3176
6304 // c:3175 — `const char *as, *bs;`. C assigns POINTERS here
6305 // (c:3181-3182 / c:3191-3192); the comparator allocates nothing, and
6306 // neither does this: the operands are borrowed from the prepared keys.
6307 let (as_, bs) = if (order & CGF_MATSORT) != 0 || (cmp == 0 && a.disp.is_none()) {
6308 (&ak.str, &bk.str) // c:3181-3182
6309 } else {
6310 // c:3183-3184 / c:3186-3188 — C returns these two orderings RAW
6311 // (`return cmp;`); `sortdir` is applied only to the final `zstrcmp`
6312 // at c:3194. The port multiplied both early returns by `sortdir`, so
6313 // under CGF_REVSORT (`compadd -R`/reverse sort) the structural
6314 // groupings inverted too: matches WITHOUT display strings sorted
6315 // ahead of matches with them, and one-per-line display strings sank
6316 // to the bottom of the list instead of heading it.
6317 if cmp != 0 {
6318 return if cmp < 0 {
6319 std::cmp::Ordering::Less // c:3184
6320 } else {
6321 std::cmp::Ordering::Greater
6322 };
6323 }
6324 let displine_cmp = (b.flags & CMF_DISPLINE) - (a.flags & CMF_DISPLINE); // c:3186
6325 if displine_cmp != 0 {
6326 return if displine_cmp < 0 {
6327 std::cmp::Ordering::Less // c:3188
6328 } else {
6329 std::cmp::Ordering::Greater
6330 };
6331 }
6332 // c:3191-3192 — cmp == 0 and `a` has a disp, so both do.
6333 match (ak.disp.as_ref(), bk.disp.as_ref()) {
6334 (Some(ad), Some(bd)) => (ad, bd),
6335 _ => (&ak.str, &bk.str),
6336 }
6337 };
6338 // c:3195-3197 — `sortdir * zstrcmp(as, bs, SORTIT_IGNORING_BACKSLASHES |
6339 // ((isset(NUMERICGLOBSORT) || matchorder & CGF_NUMSORT) ?
6340 // SORTIT_NUMERICALLY : 0))`. zstrcmp routes through strcoll, so the
6341 // ordering is the locale's collation (case-insensitive on the usual
6342 // macOS/Linux locales) — matching zsh. The previous byte comparison
6343 // sorted ASCII, putting every uppercase name (`README.md`) ahead of
6344 // lowercase ones (`alpha.txt`), which diverged from zsh's completion
6345 // order.
6346 let numeric = crate::ported::zsh_h::isset(crate::ported::zsh_h::NUMERICGLOBSORT)
6347 || (order & CGF_NUMSORT) != 0;
6348 let flags = crate::ported::zsh_h::SORTIT_IGNORING_BACKSLASHES as u32
6349 | if numeric {
6350 crate::ported::zsh_h::SORTIT_NUMERICALLY as u32
6351 } else {
6352 0
6353 };
6354 // c:3181-3182/3191-3192 — the operands are `(*a)->str` / `(*a)->disp`,
6355 // i.e. METAFIED match strings. Nothing on the path from `compadd` to the
6356 // `qsort` at c:3259 unmetafies them, and `zstrcmp` does not either — only
6357 // `strmetasort` does (c:299-315), and that is a different C function on a
6358 // different path. So C's `strcoll` here sees metafied bytes, which for
6359 // most non-ASCII text is not a valid multibyte string, and the collation
6360 // degrades to a byte-wise result. zshrs stores match strings unmetafied,
6361 // so it was handing `strcoll` well-formed UTF-8 and getting the locale's
6362 // real collation instead: under `LC_ALL=en_US.UTF-8` on macOS every pair
6363 // of CJK/Kana/Hangul strings collates EQUAL, so `compadd -- 日本語 中文字
6364 // 한국어 ascii あかさたなはまやらわ` listed them in an arbitrary
6365 // qsort-tie order while zsh listed them deterministically.
6366 //
6367 // The keys hold the metafied bytes, so the same bytes reach the same
6368 // `strcoll` C uses. Metafication is the IDENTITY on ASCII, so the ASCII
6369 // ordering — including the case-insensitive collation noted above — is
6370 // untouched.
6371 let base = crate::metafied_key::zstrcmp_operands(as_, bs, flags);
6372 if sortdir < 0 {
6373 base.reverse()
6374 } else {
6375 base
6376 }
6377}
6378
6379/// Port of `static int matcheq(Cmatch a, Cmatch b)` from
6380/// compcore.c:3206.
6381pub fn matcheq(a: &Cmatch, b: &Cmatch) -> bool {
6382 // c:3207
6383 matchstreq(a.ipre.as_ref(), b.ipre.as_ref()) && // c:3207
6384 matchstreq(a.pre.as_ref(), b.pre.as_ref()) && // c:3210
6385 matchstreq(a.ppre.as_ref(), b.ppre.as_ref()) && // c:3211
6386 matchstreq(a.psuf.as_ref(), b.psuf.as_ref()) && // c:3212
6387 matchstreq(a.suf.as_ref(), b.suf.as_ref()) && // c:3213
6388 matchstreq(a.str.as_ref(), b.str.as_ref()) // c:3214
6389}
6390
6391// =====================================================================
6392// makearray — `Src/Zle/compcore.c:3224`.
6393// =====================================================================
6394
6395/// Port of `static Cmatch *makearray(LinkList l, int type, int flags,
6396/// int *np, int *nlp, int *llp)`
6397/// from compcore.c:3223. Returns `(arr, n, nl, ll)`.
6398///
6399/// `type` is fixed to `1` (match-sort path) for the in-file call sites
6400/// from `permmatches`. The `type=0` string-sort path on `lexpls` is
6401/// inlined at the `permmatches` call site (C uses a `(char **)` cast
6402/// trick that has no safe Rust equivalent).
6403///
6404/// c:3230-3236 — `rp = hcalloc(...); for (nod = firstnode(l); nod; incnode(nod))
6405/// *ap++ = (Cmatch) getdata(nod);`. The array holds the SAME `Cmatch`
6406/// POINTERS the LinkList holds, so every `->flags |= CMF_MULT` /
6407/// `|= CMF_FMULT` / `= CMF_DELETE` below writes THROUGH to the live match
6408/// still sitting on `g->lmatches` — and C never empties that list, so the
6409/// marks are still there on the NEXT `permmatches` call. `Cmatch` is a
6410/// value struct in the port (`comp_h.rs:517`), so taking the list by value
6411/// meant every one of those writes landed on a detached copy and was
6412/// discarded; a second `permmatches` in the same completion (every
6413/// `_description`/`_wanted` block reads `$compstate[nmatches]`, which is
6414/// `permmatches(0)` — `Src/Zle/complete.c:1411-1413`) restarted from
6415/// unmarked matches.
6416///
6417/// `src` is therefore `&mut` the LIVE list, and the "array" is an INDEX
6418/// PERMUTATION `ord` into it — the exact analogue of C's array of aliasing
6419/// pointers. Sorting, compacting (`*cp++ = *ap`) and truncating (`*cp =
6420/// NULL`) move only indices, exactly as C moves only pointers; every flag
6421/// write goes to `src[..]`, i.e. to the live match.
6422pub fn makearray(src: &mut Vec<Cmatch>, flags: i32) -> (Vec<Cmatch>, i32, i32, i32) {
6423 // c:3224
6424 let mut ord: Vec<usize> = (0..src.len()).collect(); // c:3230-3236
6425 let mut n: i32 = ord.len() as i32; // c:3224
6426 let mut nl: i32 = 0; // c:3231
6427 let mut ll: i32 = 0; // c:3231
6428
6429 if n > 0 {
6430 // c:3258 (type==1 branch)
6431 if (flags & CGF_NOSORT) == 0 {
6432 // c:3259
6433 // Now sort the array (it contains matches). // c:3260
6434 MATCHORDER.store(flags, Ordering::Relaxed); // c:3261
6435 // c:3262 — C `qsort(rp, n, sizeof(Cmatch), matchcmp)`. Must use the
6436 // qsort-tolerant sort: matchcmp→zstrcmp is not a strict weak order
6437 // (numeric/natural sort), which makes Rust's sort_by PANIC.
6438 // Sorting the permutation is C's "qsort the POINTER array": the
6439 // matches themselves never move.
6440 crate::tolerant_sort::qsort_matches(&mut ord, src);
6441
6442 if (flags & CGF_UNIQCON) == 0 {
6443 // c:3269 not -2
6444 // remove dupes
6445 let mut cp = 0usize; // c:3272
6446 let mut ap = 0usize;
6447 while ap < ord.len() {
6448 // c:3274 for ap;*ap;ap++
6449 // Scan the run of duplicates FIRST, using the element at
6450 // `ap` (C keeps `*ap` stable — `*cp++ = *ap` copies, it
6451 // does not move). Doing `ord.swap(ap, cp)` before this scan
6452 // (the old code) overwrote `rp[ap]` with a stale slot once
6453 // any earlier removal made `cp < ap`, so `rp[ap].str ==
6454 // rp[bp+1].str` compared the wrong element and same-string
6455 // duplicates (e.g. `libpng-config` from several $path dirs)
6456 // were never collapsed.
6457 // c:3271 — collapse the run of matcheq duplicates.
6458 let mut bp = ap;
6459 while bp + 1 < ord.len() && matcheq(&src[ord[ap]], &src[ord[bp + 1]]) {
6460 bp += 1;
6461 n -= 1; // c:3271 bp[1] && matcheq
6462 }
6463 // c:3272 — `ap = bp`: from here C compares against the
6464 // LAST element of the collapsed run.
6465 let run_end = bp;
6466 // c:3274-3278 — mark (do NOT remove) the following
6467 // elements that are not matcheq but would DISPLAY the
6468 // same string. C advances only `bp` here; `n` is not
6469 // decremented and the outer loop resumes at `run_end + 1`,
6470 // so every CMF_MULT element stays in the array and in
6471 // `mcount` — it is excluded from the *listing* later via
6472 // `nl` (c:3287-3288 → `lcount = nn - nl`). The port
6473 // decremented `n` and skipped past them, deleting them
6474 // outright: `mcount` under-counted, so a word with two
6475 // same-string matches from different path prefixes looked
6476 // like a single unambiguous match and was inserted
6477 // without ever offering the choice.
6478 let mut mark = run_end;
6479 let mut dup = 0i32; // c:3274
6480 while mark + 1 < ord.len()
6481 && src[ord[run_end]].disp.is_none()
6482 && src[ord[mark + 1]].disp.is_none() // c:3274 !disp
6483 && src[ord[run_end]].str == src[ord[mark + 1]].str
6484 {
6485 src[ord[mark + 1]].flags |= CMF_MULT; // c:3276
6486 dup = 1; // c:3277
6487 mark += 1;
6488 }
6489 if dup != 0 {
6490 // c:3279
6491 src[ord[run_end]].flags |= CMF_FMULT; // c:3280
6492 }
6493 // c:3270 `*cp++ = *ap` — keep the first of the run at `cp`.
6494 if ap != cp {
6495 ord.swap(ap, cp);
6496 }
6497 cp += 1;
6498 ap = run_end + 1; // c:3272 ap = bp; then outer ap++
6499 }
6500 ord.truncate(cp); // c:3282 *cp = NULL
6501 }
6502 for &i in ord.iter() {
6503 // c:3293
6504 let m = &src[i];
6505 if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
6506 // c:3294
6507 ll += 1;
6508 }
6509 if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
6510 // c:3296
6511 nl += 1;
6512 }
6513 }
6514 } else {
6515 // c:3300 used -O nosort or -V
6516 if (flags & CGF_UNIQALL) == 0 && (flags & CGF_UNIQCON) == 0 {
6517 // c:3302 didn't use -1 or -2
6518 MATCHORDER.store(flags, Ordering::Relaxed); // c:3297
6519 // c:3298-3300 — `sp = zhalloc(...); memcpy(sp, rp, ...)` copies the
6520 // POINTER array, so each `sp` slot ALIASES the same match as the
6521 // `rp` slot it came from and the flag writes below ARE writes into
6522 // `rp`'s matches. A cloned index permutation reproduces that
6523 // aliasing exactly. The old `rp.clone()` produced detached copies
6524 // and had to hunt the original back down with `matcheq`, which
6525 // always found the FIRST equal element: a run of three identical
6526 // matches marked one element CMF_DELETE twice instead of two
6527 // elements once, so only one of the two dupes was ever dropped.
6528 // !!! MEASURED DIVERGENCE (see `crate::tolerant_sort`): the
6529 // pair walk below kills the LATER member of each equal pair in
6530 // THIS sorted copy, and the survivor keeps its slot in `ord`,
6531 // i.e. in the displayed order. `matchcmp` returns 0 for two
6532 // copies of one match, so the C answer is the platform
6533 // `qsort(3)`'s undefined tie order while `qsort_tolerant` is
6534 // stable and always keeps the first-added copy. Measured on
6535 // `git show <TAB><TAB>`, where `_git` compadds the same 20
6536 // commits twice into `-J -default-`: zsh/Darwin lists them in
6537 // two ascending runs, zshrs in insertion order. Same set, same
6538 // descriptions. Not portably fixable — zsh's own order here is
6539 // whatever its libc's quicksort left behind.
6540 let mut sord: Vec<usize> = ord.clone();
6541 // c:3301-3302 — qsort matchcmp; tolerant sort (non-total-order cmp).
6542 crate::tolerant_sort::qsort_matches(&mut sord, src);
6543
6544 let mut del = false; // c:3303
6545 // c:3303-3318 — walk the sorted permutation in pairs. C's inner
6546 // `for (dup = 0; ...; bp = ++sp)` run-walk is NOT replicated (it
6547 // advances the array BASE, not `bp` — an upstream oddity); the
6548 // pairwise form is equivalent for CMF_MULT/CMF_FMULT because the
6549 // array is sorted, so every element whose predecessor shares its
6550 // `str` is exactly the set the run-walk marks.
6551 for k in 1..sord.len() {
6552 let (ai, bi) = (sord[k - 1], sord[k]); // c:3304
6553 if matcheq(&src[ai], &src[bi]) {
6554 // c:3305
6555 src[bi].flags = CMF_DELETE; // c:3306
6556 del = true; // c:3307
6557 } else if src[ai].disp.is_none() {
6558 // c:3308
6559 if src[bi].disp.is_none() && src[ai].str == src[bi].str {
6560 // c:3310-3311
6561 src[bi].flags |= CMF_MULT; // c:3312
6562 src[ai].flags |= CMF_FMULT; // c:3316
6563 }
6564 }
6565 }
6566 if del {
6567 // c:3319
6568 ord.retain(|&i| (src[i].flags & CMF_DELETE) == 0); // c:3320-3328
6569 n = ord.len() as i32;
6570 }
6571 } else if (flags & CGF_UNIQCON) == 0 {
6572 // c:3344 -1 not -2
6573 let mut cp = 0usize;
6574 let mut ap = 0usize;
6575 while ap < ord.len() {
6576 // c:3334
6577 // Scan the runs BEFORE the compaction swap: once an
6578 // earlier removal makes `cp < ap`, `ord.swap(ap, cp)`
6579 // overwrites slot `ap` with a stale index and every
6580 // comparison below reads the wrong element. C copies
6581 // (`*cp++ = *ap`), it does not swap. Same fix as the
6582 // sorted branch above.
6583 let mut bp = ap;
6584 while bp + 1 < ord.len() && matcheq(&src[ord[ap]], &src[ord[bp + 1]]) {
6585 bp += 1;
6586 n -= 1; // c:3336
6587 }
6588 let run_end = bp; // c:3337 `ap = bp`
6589 // c:3338-3342 — mark, do NOT remove: `n` is untouched and
6590 // the outer loop resumes at `run_end + 1`, so CMF_MULT
6591 // elements stay in the array (they are dropped from the
6592 // LISTING via `nl` at c:3351-3352, not from `mcount`).
6593 let mut mark = run_end;
6594 let mut dup = 0i32;
6595 while mark + 1 < ord.len()
6596 && src[ord[run_end]].disp.is_none()
6597 && src[ord[mark + 1]].disp.is_none()
6598 && src[ord[run_end]].str == src[ord[mark + 1]].str
6599 {
6600 src[ord[mark + 1]].flags |= CMF_MULT; // c:3340
6601 dup = 1; // c:3341
6602 mark += 1;
6603 }
6604 if dup != 0 {
6605 src[ord[run_end]].flags |= CMF_FMULT; // c:3344
6606 }
6607 if ap != cp {
6608 ord.swap(ap, cp); // c:3335 `*cp++ = *ap`
6609 }
6610 cp += 1;
6611 ap = run_end + 1;
6612 }
6613 ord.truncate(cp); // c:3346
6614 }
6615 for &i in ord.iter() {
6616 // c:3361
6617 let m = &src[i];
6618 if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 {
6619 // c:3362
6620 ll += 1;
6621 }
6622 if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 {
6623 // c:3364
6624 nl += 1;
6625 }
6626 }
6627 }
6628 }
6629 // c:3362 — C returns `rp` itself (the compacted pointer array).
6630 // Materialise the permutation for the caller; the flag writes above have
6631 // already landed on the live matches in `src`.
6632 let arr: Vec<Cmatch> = ord.iter().map(|&i| src[i].clone()).collect();
6633 (arr, n, nl, ll) // c:3366-3373
6634}
6635
6636// =====================================================================
6637// dupmatch — `Src/Zle/compcore.c:3370`.
6638// =====================================================================
6639
6640/// Port of `static Cmatch dupmatch(Cmatch m, int nbeg, int nend)` from
6641/// compcore.c:3370. Deep-copies one match; brpl/brsl are truncated to
6642/// nbeg/nend per the C body's nbeg/nend-sized `zalloc` + element copy.
6643pub fn dupmatch(m: &Cmatch, nbeg: i32, nend: i32) -> Cmatch {
6644 // c:3370
6645 let mut r = Cmatch::default(); // c:3370-3374
6646 r.str = m.str.clone(); // c:3376 ztrdup
6647 r.orig = m.orig.clone(); // c:3377
6648 r.ipre = m.ipre.clone(); // c:3378
6649 r.ripre = m.ripre.clone(); // c:3379
6650 r.isuf = m.isuf.clone(); // c:3380
6651 r.ppre = m.ppre.clone(); // c:3381
6652 r.psuf = m.psuf.clone(); // c:3382
6653 r.prpre = m.prpre.clone(); // c:3383
6654 r.pre = m.pre.clone(); // c:3384
6655 r.suf = m.suf.clone(); // c:3385
6656 r.flags = m.flags; // c:3386
6657 if !m.brpl.is_empty() {
6658 // c:3387
6659 let take = (nbeg as usize).min(m.brpl.len()); // c:3390 zalloc(nbeg)
6660 r.brpl = m.brpl[..take].to_vec(); // c:3392 element-wise copy
6661 } else {
6662 r.brpl = Vec::new(); // c:3395 NULL
6663 }
6664 if !m.brsl.is_empty() {
6665 // c:3396
6666 let take = (nend as usize).min(m.brsl.len()); // c:3399
6667 r.brsl = m.brsl[..take].to_vec(); // c:3401
6668 } else {
6669 r.brsl = Vec::new(); // c:3404
6670 }
6671 r.rems = m.rems.clone(); // c:3405
6672 r.remf = m.remf.clone(); // c:3406
6673 r.autoq = m.autoq.clone(); // c:3407
6674 r.qipl = m.qipl; // c:3408
6675 r.qisl = m.qisl; // c:3409
6676 r.disp = m.disp.clone(); // c:3410
6677 r.mode = m.mode; // c:3411
6678 r.modec = m.modec; // c:3412
6679 r.fmode = m.fmode; // c:3413
6680 r.fmodec = m.fmodec; // c:3414
6681 r // c:3416
6682}
6683
6684/// Port of `mod_export int permmatches(int last)` from compcore.c:3422.
6685/// Promotes the per-round `amatches` accumulator into the permanent
6686/// `pmatches` snapshot via deep-copy through `dupmatch`/`makearray`.
6687pub fn permmatches(last: i32) -> i32 {
6688 // c:3423
6689 let ofi = PERMMATCHES_FI.load(Ordering::Relaxed); // c:3423 ofi = fi
6690
6691 // c:3433 — `if (pmatches && !newmatches)`
6692 let pmatches_set = pmatches
6693 .get_or_init(|| Mutex::new(Vec::new()))
6694 .lock()
6695 .map(|g| !g.is_empty())
6696 .unwrap_or(false);
6697 if pmatches_set && newmatches.load(Ordering::Relaxed) == 0 {
6698 // c:3433
6699 if last != 0 && PERMMATCHES_FI.load(Ordering::Relaxed) != 0 {
6700 // c:3434
6701 // ainfo = fainfo // c:3435
6702 let famref = fainfo
6703 .get_or_init(|| Mutex::new(None))
6704 .lock()
6705 .ok()
6706 .and_then(|g| g.clone());
6707 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
6708 *a = famref;
6709 }
6710 }
6711 return PERMMATCHES_FI.load(Ordering::Relaxed); // c:3437
6712 }
6713 newmatches.store(0, Ordering::Relaxed); // c:3439
6714 PERMMATCHES_FI.store(0, Ordering::Relaxed); // c:3439 fi = 0
6715
6716 {
6717 // pmatches = lmatches = NULL // c:3441
6718 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
6719 g.clear();
6720 }
6721 if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
6722 *g = None;
6723 }
6724 }
6725 nmatches.store(0, Ordering::Relaxed); // c:3442
6726 smatches.store(0, Ordering::Relaxed); // c:3442
6727 diffmatches.store(0, Ordering::Relaxed); // c:3442
6728
6729 // c:3444 — `if (!ainfo->count)`.
6730 let ainfo_count = ainfo
6731 .get_or_init(|| Mutex::new(None))
6732 .lock()
6733 .ok()
6734 .and_then(|g| g.as_ref().map(|a| a.count))
6735 .unwrap_or(0);
6736 if ainfo_count == 0 {
6737 // c:3444
6738 if last != 0 {
6739 // c:3445
6740 let famref = fainfo
6741 .get_or_init(|| Mutex::new(None))
6742 .lock()
6743 .ok()
6744 .and_then(|g| g.clone());
6745 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
6746 *a = famref;
6747 }
6748 }
6749 PERMMATCHES_FI.store(1, Ordering::Relaxed); // c:3447
6750 }
6751
6752 let nbeg = NBRBEG.load(Ordering::Relaxed);
6753 let nend = NBREND.load(Ordering::Relaxed);
6754
6755 let mut gn: i32 = 1; // c:3429 gn = 1
6756 let mut mn: i32 = 1; // c:3429 mn = 1
6757 let fi = PERMMATCHES_FI.load(Ordering::Relaxed);
6758
6759 // c:3449 `Cmgroup g = amatches` — C walks the live chain in place. The
6760 // groups are TAKEN out of `amatches` (and every one is stored back after
6761 // the loop, c:3488/3542/3544 write-back below) rather than cloned: a
6762 // clone deep-copied each group's `matches` and its `perm` copy on every
6763 // `permmatches` call, i.e. on every `$compstate[nmatches]` read that
6764 // finds new matches — `arch <TAB>` copied 2 x 47058 matches four times
6765 // for nothing. Nothing the loop calls (`makearray`, `dupmatch`) reads
6766 // `amatches`.
6767 let groups_snapshot: Vec<Cmgroup> = {
6768 amatches
6769 .get_or_init(|| Mutex::new(Vec::new()))
6770 .lock()
6771 .ok()
6772 .map(|mut g| std::mem::take(&mut *g))
6773 .unwrap_or_default()
6774 };
6775 let mut new_pmatches: Vec<Cmgroup> = Vec::with_capacity(groups_snapshot.len());
6776 // c:3488 `g->perm = n`, c:3460-3467 `g->mcount/lcount/llcount`, c:3471
6777 // `g->ccount = 0`, c:3542 `g->num = gn++` and c:3544 `g->new = 0` are all
6778 // writes to the LIVE group in `amatches`. The port mutated a detached
6779 // clone and threw it away, so `g->new` never cleared and `g->perm` was
6780 // never recorded: the c:3452 cache test (`!g->perm || g->new`) could
6781 // never take its reuse branch and every `permmatches` call rebuilt and
6782 // re-`dupmatch`ed every group from scratch. Collect the mutated groups
6783 // and store them back after the loop.
6784 let mut updated_groups: Vec<Cmgroup> = Vec::with_capacity(new_pmatches.capacity());
6785 // c:3490-3491 / c:3528-3529 — `if (!lmatches) lmatches = n;` (resp.
6786 // `g->perm`). `lmatches` is the head C keeps for `makecomplist`'s
6787 // old-list reuse (c:1006 `lmatches = lastlmatches`); the port never
6788 // assigned it, so `compstate[old_list]=keep` had no list to restore.
6789 let mut lmatches_head: Option<Cmgroup> = None;
6790
6791 for g_orig in groups_snapshot.into_iter() {
6792 // c:3449 while (g)
6793 let mut g = g_orig; // borrow-mut snapshot
6794 let must_rebuild = fi != ofi || g.perm.is_none() || g.new_.load(Ordering::Relaxed) != 0; // c:3456
6795 if must_rebuild {
6796 // c:3456
6797 // c:3455-3457 — `mlist = g->lfmatches` / `mlist = g->lmatches`.
6798 // C hands `makearray` the LIVE LinkList: the CMF_MULT/CMF_FMULT/
6799 // CMF_DELETE marks it writes go through to the matches that stay
6800 // on the group, and persist for the next `permmatches` call.
6801 // Cloning the `Vec` here detached every one of those writes.
6802 // Clone the `Arc` instead (shared storage, comp_h.rs:433) and
6803 // hand `makearray` a `&mut` into it.
6804 let src_arc = if fi != 0 {
6805 g.lfmatches.clone() // c:3455
6806 } else {
6807 g.lmatches.clone() // c:3457
6808 };
6809
6810 // The guard is scoped to the call. `makearray` locks nothing
6811 // (`matchcmp`/`matcheq` read only MATCHORDER), and these two are
6812 // the ONLY `lmatches`/`lfmatches` lock sites in the tree, so no
6813 // re-entrant lock and no deadlock; the guard is dropped before
6814 // the next loop iteration.
6815 let (arr, nn, nl, ll) = {
6816 let mut src_list = src_arc.lock().unwrap();
6817 makearray(&mut src_list, g.flags) // c:3459
6818 };
6819 g.mcount = nn; // c:3464
6820 g.lcount = nn - nl; // c:3465
6821 if g.lcount < 0 {
6822 g.lcount = 0;
6823 } // c:3466
6824 g.llcount = ll; // c:3467
6825 // c:3468 — pointer test, so an EMPTY ylist still forces
6826 // `lcount = 0` and `smatches = 2` rather than keeping the
6827 // match-derived count.
6828 if let Some(ylen) = g.ylist.as_ref().map(|y| y.len() as i32) {
6829 g.lcount = ylen; // c:3469
6830 smatches.store(2, Ordering::Relaxed); // c:3470
6831 }
6832 // c:3472 — makearray(lexpls, 0, 0, &ecount, NULL, NULL).
6833 let mut exps = g.lexpls.lock().unwrap().clone(); // type=0 path
6834 g.ecount = exps.len() as i32;
6835 // c:3475 ccount = 0
6836 g.ccount = 0; // c:3475
6837 tracing::debug!(
6838 target: "compsys_args",
6839 mcount = g.mcount,
6840 lcount = g.lcount,
6841 nn,
6842 nl,
6843 ll,
6844 name = ?g.name,
6845 "permmatches group"
6846 );
6847 nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3477
6848 smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3478
6849 if g.mcount > 1 {
6850 // c:3480
6851 diffmatches.store(1, Ordering::Relaxed); // c:3481
6852 }
6853
6854 // n = (Cmgroup) zshcalloc(...) // c:3483
6855 let mut n_grp = Cmgroup::default();
6856 // c:3487 — `if (g->perm) freematches(g->perm, 0)`. Drop on
6857 // perm Box<Cmgroup> reclaims the C `free` path.
6858 g.perm = None; // c:3490 g->perm = n
6859 // Then below we set g.perm = Some(Box::new(n_grp.clone())).
6860
6861 n_grp.num = gn;
6862 gn += 1; // c:3499
6863 n_grp.flags = g.flags; // c:3500
6864 n_grp.mcount = g.mcount; // c:3501
6865 n_grp.matches = arr
6866 .iter() // c:3502-3505 dupmatch loop
6867 .map(|m| dupmatch(m, nbeg, nend))
6868 .collect();
6869 n_grp.name = g.name.clone(); // c:3504
6870 n_grp.lcount = g.lcount; // c:3508
6871 n_grp.llcount = g.llcount; // c:3509
6872 // c:3509-3513 — `if (g->ylist) n->ylist = zarrdup(g->ylist);
6873 // else n->ylist = NULL;` — a plain clone of the Option carries
6874 // both the NULL and the empty-but-present case unchanged.
6875 n_grp.ylist = g.ylist.clone(); // c:3511 zarrdup / c:3513 NULL
6876 if g.ecount != 0 {
6877 // c:3515
6878 // Build n->expls from g->expls deep-copying str + (fi
6879 // ? fcount : count); always carries over; fcount = 0.
6880 n_grp.expls = exps
6881 .drain(..)
6882 .map(|o| Cexpl {
6883 // c:3517-3525
6884 count: if fi != 0 { o.fcount } else { o.count }, // c:3520
6885 always: o.always, // c:3521
6886 fcount: 0, // c:3522
6887 str: o.str.clone(), // c:3523 ztrdup
6888 })
6889 .collect();
6890 n_grp.ecount = g.ecount;
6891 } else {
6892 n_grp.expls = Vec::new(); // c:3528
6893 }
6894 n_grp.widths = Vec::new(); // c:3531
6895 // Stitch perm chain (prev/next handled implicitly by Vec).
6896 g.matches = arr; // mirror C: g->matches = makearray result
6897 g.perm = Some(Box::new(n_grp.clone())); // c:3488 g->perm = n
6898 if lmatches_head.is_none() {
6899 lmatches_head = Some(n_grp.clone()); // c:3490-3491
6900 }
6901 new_pmatches.push(n_grp); // c:3492-3495
6902 } else {
6903 // reuse existing g->perm // c:3534
6904 nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3540
6905 smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3541
6906 if g.mcount > 1 {
6907 diffmatches.store(1, Ordering::Relaxed); // c:3543
6908 }
6909 g.num = gn;
6910 gn += 1; // c:3542
6911 if let Some(p) = g.perm.as_deref() {
6912 if lmatches_head.is_none() {
6913 lmatches_head = Some(p.clone()); // c:3528-3529
6914 }
6915 new_pmatches.push(p.clone()); // c:3533 pmatches = g->perm
6916 }
6917 }
6918 g.new_.store(0, Ordering::Relaxed); // c:3544
6919 updated_groups.push(g);
6920 }
6921 // c:3488/3542/3544 write-back — see the note above the loop.
6922 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
6923 *g = updated_groups;
6924 }
6925 // c:3490 — `lmatches` is the head of the permanent chain.
6926 if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
6927 *g = lmatches_head;
6928 }
6929
6930 // c:3490-3538 — C threads each group onto `pmatches` via
6931 // `n->next = pmatches; pmatches = n` (a PREPEND), so pmatches ends up
6932 // in the REVERSE of amatches iteration order. The port accumulates into
6933 // a Vec with push() (append), so reverse once here to recover C's order.
6934 // Without this, `group-order`/`group-name` groups list in creation order
6935 // reversed (e.g. `group-order veggies fruits` showed fruits first), and
6936 // the gnum/rnum numbering below — which C runs over the reversed
6937 // pmatches — was assigned against the wrong sequence.
6938 new_pmatches.reverse();
6939
6940 // c:3551-3563 — assign rnum/gnum, recompute diffmatches/nbrbeg.
6941 let mut first_first: Option<Cmatch> = None;
6942 for g_pm in new_pmatches.iter_mut() {
6943 g_pm.nbrbeg = nbeg; // c:3552
6944 g_pm.nbrend = nend; // c:3553
6945 let mut rn = 1i32; // c:3554
6946 for m in g_pm.matches.iter_mut() {
6947 m.rnum = rn;
6948 rn += 1; // c:3555
6949 m.gnum = mn;
6950 mn += 1; // c:3556
6951 }
6952 if diffmatches.load(Ordering::Relaxed) == 0 && !g_pm.matches.is_empty() {
6953 match first_first.as_ref() {
6954 // c:3558
6955 Some(p0) => {
6956 if !matcheq(&g_pm.matches[0], p0) {
6957 diffmatches.store(1, Ordering::Relaxed); // c:3560
6958 }
6959 }
6960 None => first_first = Some(g_pm.matches[0].clone()), // c:3562
6961 }
6962 }
6963 }
6964
6965 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
6966 *g = new_pmatches;
6967 }
6968
6969 hasperm.store(1, Ordering::Relaxed); // c:3565
6970 permmnum.store(mn - 1, Ordering::Relaxed); // c:3566
6971 permgnum.store(gn - 1, Ordering::Relaxed); // c:3567
6972 if let Ok(mut ld) = listdat
6973 .get_or_init(|| Mutex::new(Default::default()))
6974 .lock()
6975 {
6976 ld.valid = 0; // c:3568
6977 }
6978
6979 fi // c:3570
6980}
6981
6982// =====================================================================
6983// freematch / freematches — `Src/Zle/compcore.c:3575 / 3605`.
6984// =====================================================================
6985
6986/// Port of `static void freematch(Cmatch m, int nbeg, int nend)` from
6987/// `Src/Zle/compcore.c:3575`. C frees each Cmatch field via `zsfree`
6988/// (str/orig/ipre/ripre/isuf/ppre/psuf/pre/suf/prpre/rems/remf/disp/
6989/// autoq) and `zfree(m->brpl, nbeg * sizeof(int))` /
6990/// `zfree(m->brsl, nend * sizeof(int))` — all collapse to Rust's
6991/// automatic Drop on the owned String / `Vec<i32>` fields. nbeg/nend
6992/// kept on the signature for C parity (consumed by C as `zfree` size
6993/// args; Rust Vec carries its own length).
6994pub fn freematch(m: Cmatch, _nbeg: i32, _nend: i32) {
6995 // c:3577 — `if (!m) return;` — Rust's owned `m` is always valid;
6996 // dropping it on return runs every field's destructor (c:3579-3596
6997 // zsfree / zfree calls collapsed).
6998 drop(m); // c:3598 zfree(m)
6999}
7000
7001/// Direct port of `mod_export void freematches(Cmgroup g, int cm)` from
7002/// `Src/Zle/compcore.c:3605`. The C path walks the cmgroup chain freeing
7003/// each Cmatch + ylist + expls + widths + name; in Rust those are
7004/// owned by `Vec`/`Box`/`String` so Drop covers the per-node free.
7005/// The `cm` arm at c:3636-3637 (`minfo.cur = NULL`) is the only
7006/// side-effect that doesn't fall out of Rust's ownership model — wire
7007/// it explicitly.
7008pub fn freematches(g: Vec<Cmgroup>, cm: i32) {
7009 // c:3605
7010 drop(g);
7011 if cm != 0 {
7012 // c:3636
7013 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
7014 g.cur = None; // c:3637
7015 }
7016 }
7017}
7018
7019// =====================================================================
7020// Extern globals — declared in other C files, mirrored here per
7021// PORT.md Rule 9 ("stub the EXTERN dependencies ... locally with
7022// file:line citations to their home file") so the local body ports
7023// below have a value source. When the canonical Rust homes land,
7024// these become `pub use crate::ported::<canonical>::*` re-exports.
7025// =====================================================================
7026
7027/// Port of `mod_export int lastend` from `Src/Zle/compcore.c:276`.
7028/// Byte position in the metafied line where the most-recent
7029/// completion insertion ended.
7030///
7031/// Re-export alias of [`lastend`] — C has ONE `lastend` (compcore.c:276).
7032/// Two atomics existed: `compresult` wrote the lowercase `lastend`, while
7033/// `complist`'s menu-list positioning read this uppercase `LASTEND`, which was
7034/// never stored (always 0) — so menu-selection column math used a stale zero.
7035pub use self::lastend as LASTEND;
7036
7037/// Port of `mod_export int wb` from `Src/lex.c:120`. Word-begin
7038/// position in the metafied line for the currently-completing word.
7039pub static WB: AtomicI32 = AtomicI32::new(0); // lex.c:120
7040/// Port of `mod_export int we` from `Src/lex.c:120`. Word-end position.
7041pub static WE: AtomicI32 = AtomicI32::new(0); // lex.c:120
7042/// Port of `mod_export int zlemetacs` from `Src/lex.c:104`. Cursor
7043/// position in the metafied line.
7044pub static ZLEMETACS: AtomicI32 = AtomicI32::new(0); // lex.c:104
7045/// Port of `mod_export int zlemetall` from `Src/lex.c:104`. Length
7046/// of the metafied line.
7047pub static ZLEMETALL: AtomicI32 = AtomicI32::new(0); // lex.c:104
7048/// Port of `mod_export int addedx` from `Src/lex.c:115`. Non-zero
7049/// while a dummy `x` cursor marker is in the line being lexed
7050/// (so completion can capture the partial word at the cursor).
7051pub static ADDEDX: AtomicI32 = AtomicI32::new(0); // lex.c:115
7052
7053/// Port of `mod_export char *zlemetaline` from `Src/lex.c:103`. The
7054/// metafied edit buffer for the current ZLE session — `foredel`,
7055/// `inststr`, `selfinsert` operate on this directly when compcore's
7056/// error-recovery path fires (compcore.c:344-355).
7057pub static ZLEMETALINE: OnceLock<Mutex<String>> = OnceLock::new(); // lex.c:103
7058/// Port of `mod_export ZLE_STRING_T zleline` from `Src/zle_main.c`.
7059pub static ZLELINE: OnceLock<Mutex<String>> = OnceLock::new(); // zle_main.c
7060/// Port of `mod_export int zlecs` from `Src/zle_main.c`.
7061pub static ZLECS: AtomicI32 = AtomicI32::new(0); // zle_main.c
7062/// Port of `mod_export int zlell` from `Src/zle_main.c`.
7063pub static ZLELL: AtomicI32 = AtomicI32::new(0); // zle_main.c
7064/// Port of `mod_export int inwhat` from `Src/lex.c:110`. Lex context
7065/// classification — IN_NOTHING / IN_CMD / IN_COND / IN_MATH / IN_PAR /
7066/// IN_ENV.
7067pub static INWHAT: AtomicI32 = AtomicI32::new(0); // lex.c:110
7068/// Port of `mod_export int zmult` from `Src/zle_main.c`. Numeric
7069/// prefix multiplier for the current ZLE command.
7070pub static ZMULT: AtomicI32 = AtomicI32::new(1); // zle_main.c
7071/// Port of `mod_export char *compfunc` from `Src/Zle/zle_tricky.c:143`.
7072/// Name of the user completion shell function — non-empty when the
7073/// new completion system (`compsys`) is active; empty for compctl.
7074pub static compfunc: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // zle_tricky.c:143
7075/// Port of `mod_export char *comppatmatch` from `Src/Zle/zle_tricky.c`.
7076/// `$compstate[pattern_match]` — when non-empty + non-"\0" enables
7077/// pattern-aware matching for parameter-name completion.
7078pub static comppatmatch: OnceLock<Mutex<Option<String>>> = OnceLock::new();
7079// `compqstack` (C: complete.c `mod_export char *compqstack`) is deduped
7080// to the single canonical `complete::COMPQSTACK`, imported at the top of
7081// this module. The former compcore-local `compqstack` static was an
7082// orphan: the c:305 reset wrote it while `multiquote` (c:1065) read the
7083// imported COMPQSTACK, so the reset never reached its reader — a real
7084// bug now fixed by pointing both at COMPQSTACK.
7085// =====================================================================
7086// File-scope globals — `Src/Zle/compcore.c:36-279`.
7087// =====================================================================
7088
7089/// Port of `int useexact` from compcore.c:36.
7090pub static useexact: AtomicI32 = AtomicI32::new(0); // c:36
7091/// Port of `int useline` from compcore.c:36.
7092pub static useline: AtomicI32 = AtomicI32::new(0); // c:36
7093/// Port of `int uselist` from compcore.c:36.
7094pub static uselist: AtomicI32 = AtomicI32::new(0); // c:36
7095/// Port of `int forcelist` from compcore.c:36.
7096pub static forcelist: AtomicI32 = AtomicI32::new(0); // c:36
7097/// Port of `int startauto` from compcore.c:36.
7098pub static startauto: AtomicI32 = AtomicI32::new(0); // c:36
7099
7100/// Port of `mod_export int iforcemenu` from compcore.c:39.
7101pub static iforcemenu: AtomicI32 = AtomicI32::new(0); // c:39
7102
7103/// Port of `mod_export int dolastprompt` from compcore.c:44.
7104pub static dolastprompt: AtomicI32 = AtomicI32::new(0); // c:44
7105
7106/// Port of `mod_export int oldlist` from compcore.c:49.
7107pub static oldlist: AtomicI32 = AtomicI32::new(0); // c:49
7108/// Port of `mod_export int oldins` from compcore.c:49.
7109pub static oldins: AtomicI32 = AtomicI32::new(0); // c:49
7110
7111/// Port of `int origlpre` from compcore.c:54.
7112pub static origlpre: AtomicI32 = AtomicI32::new(0); // c:54
7113/// Port of `int origlsuf` from compcore.c:54.
7114pub static origlsuf: AtomicI32 = AtomicI32::new(0); // c:54
7115/// Port of `int lenchanged` from compcore.c:54.
7116pub static lenchanged: AtomicI32 = AtomicI32::new(0); // c:54
7117
7118/// Port of `int movetoend` from compcore.c:61.
7119pub static movetoend: AtomicI32 = AtomicI32::new(0); // c:61
7120
7121/// Port of `mod_export int insmnum` from compcore.c:66.
7122pub static insmnum: AtomicI32 = AtomicI32::new(0); // c:66
7123/// Port of `mod_export int insspace` from compcore.c:66.
7124pub static insspace: AtomicI32 = AtomicI32::new(0); // c:66
7125
7126/// Port of `mod_export int menuacc` from compcore.c:81.
7127pub static menuacc: AtomicI32 = AtomicI32::new(0); // c:81
7128
7129/// Port of `int hasunqu` from compcore.c:86.
7130pub static hasunqu: AtomicI32 = AtomicI32::new(0); // c:86
7131/// Port of `int useqbr` from compcore.c:86.
7132pub static useqbr: AtomicI32 = AtomicI32::new(0); // c:86
7133/// Port of `int brpcs` from compcore.c:86.
7134pub static brpcs: AtomicI32 = AtomicI32::new(0); // c:86
7135/// Port of `int brscs` from compcore.c:86.
7136pub static brscs: AtomicI32 = AtomicI32::new(0); // c:86
7137
7138/// Port of `mod_export int ispar` from compcore.c:91.
7139pub static ispar: AtomicI32 = AtomicI32::new(0); // c:91
7140/// Port of `mod_export int linwhat` from compcore.c:91.
7141pub static linwhat: AtomicI32 = AtomicI32::new(0); // c:91
7142
7143/// Port of `char *parpre` from compcore.c:96.
7144pub static parpre: OnceLock<Mutex<String>> = OnceLock::new(); // c:96
7145
7146/// Port of `int parflags` from compcore.c:101.
7147pub static parflags: AtomicI32 = AtomicI32::new(0); // c:101
7148
7149/// Port of `mod_export int mflags` from compcore.c:106.
7150pub static mflags: AtomicI32 = AtomicI32::new(0); // c:106
7151
7152/// Port of `int parq` from compcore.c:111.
7153pub static parq: AtomicI32 = AtomicI32::new(0); // c:111
7154/// Port of `int eparq` from compcore.c:111.
7155pub static eparq: AtomicI32 = AtomicI32::new(0); // c:111
7156
7157/// Port of `mod_export char *ipre` from compcore.c:118.
7158pub static ipre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
7159/// Port of `mod_export char *ripre` from compcore.c:118.
7160pub static ripre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
7161/// Port of `mod_export char *isuf` from compcore.c:118.
7162pub static isuf: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
7163
7164/// Port of `mod_export LinkList matches` from compcore.c:124.
7165pub static matches: OnceLock<Mutex<std::sync::Arc<Mutex<Vec<Cmatch>>>>> = OnceLock::new(); // c:124 (Arc handle — see comp_match_handles)
7166/// Port of `LinkList fmatches` from compcore.c:126.
7167pub static fmatches: OnceLock<Mutex<std::sync::Arc<Mutex<Vec<Cmatch>>>>> = OnceLock::new(); // c:126 (Arc handle)
7168
7169/// Port of `mod_export Cmgroup amatches` from compcore.c:135.
7170pub static amatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
7171/// Port of `mod_export Cmgroup pmatches` from compcore.c:135.
7172pub static pmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
7173/// Port of `mod_export Cmgroup lastmatches` from compcore.c:135.
7174pub static lastmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
7175/// Port of `mod_export Cmgroup lmatches` from compcore.c:135. Last
7176/// element pointer in the perm list; here a single-slot holder.
7177pub static lmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
7178/// Port of `mod_export Cmgroup lastlmatches` from compcore.c:135.
7179pub static lastlmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
7180
7181/// Port of `mod_export int hasoldlist` from compcore.c:140.
7182pub static hasoldlist: AtomicI32 = AtomicI32::new(0); // c:140
7183/// Port of `mod_export int hasperm` from compcore.c:140.
7184pub static hasperm: AtomicI32 = AtomicI32::new(0); // c:140
7185/// Port of `int hasallmatch` from compcore.c:145.
7186pub static hasallmatch: AtomicI32 = AtomicI32::new(0); // c:145
7187
7188/// Port of `mod_export int newmatches` from compcore.c:150.
7189pub static newmatches: AtomicI32 = AtomicI32::new(0); // c:150
7190
7191/// Port of `mod_export int permmnum` from compcore.c:155.
7192pub static permmnum: AtomicI32 = AtomicI32::new(0); // c:155
7193/// Port of `mod_export int permgnum` from compcore.c:155.
7194pub static permgnum: AtomicI32 = AtomicI32::new(0); // c:155
7195/// Port of `mod_export int lastpermmnum` from compcore.c:155.
7196pub static lastpermmnum: AtomicI32 = AtomicI32::new(0); // c:155
7197/// Port of `mod_export int lastpermgnum` from compcore.c:155.
7198pub static lastpermgnum: AtomicI32 = AtomicI32::new(0); // c:155
7199
7200/// Port of `mod_export int nmatches` from compcore.c:160.
7201pub static nmatches: AtomicI32 = AtomicI32::new(0); // c:160
7202/// Port of `mod_export int smatches` from compcore.c:162.
7203pub static smatches: AtomicI32 = AtomicI32::new(0); // c:162
7204
7205/// Port of `mod_export int diffmatches` from compcore.c:167.
7206pub static diffmatches: AtomicI32 = AtomicI32::new(0); // c:167
7207
7208/// Port of `mod_export int nmessages` from compcore.c:172.
7209pub static nmessages: AtomicI32 = AtomicI32::new(0); // c:172
7210
7211/// Port of `mod_export int onlyexpl` from compcore.c:177.
7212pub static onlyexpl: AtomicI32 = AtomicI32::new(0); // c:177
7213
7214/// Port of `mod_export struct cldata listdat` from compcore.c:182.
7215pub static listdat: OnceLock<Mutex<crate::ported::zle::comp_h::Cldata>> = OnceLock::new(); // c:182
7216
7217/// Port of `mod_export int ispattern` from compcore.c:187.
7218pub static ispattern: AtomicI32 = AtomicI32::new(0); // c:187
7219/// Port of `mod_export int haspattern` from compcore.c:187.
7220pub static haspattern: AtomicI32 = AtomicI32::new(0); // c:187
7221
7222/// Port of `mod_export int hasmatched` from compcore.c:192.
7223pub static hasmatched: AtomicI32 = AtomicI32::new(0); // c:192
7224/// Port of `mod_export int hasunmatched` from compcore.c:192.
7225pub static hasunmatched: AtomicI32 = AtomicI32::new(0); // c:192
7226
7227/// Port of `Cmgroup mgroup` from compcore.c:197.
7228pub static mgroup: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:197
7229
7230/// Port of `mod_export int mnum` from compcore.c:202.
7231pub static mnum: AtomicI32 = AtomicI32::new(0); // c:202
7232
7233/// Port of `mod_export int unambig_mnum` from compcore.c:207.
7234pub static unambig_mnum: AtomicI32 = AtomicI32::new(0); // c:207
7235
7236/// Port of `int maxmlen` from compcore.c:212.
7237pub static maxmlen: AtomicI32 = AtomicI32::new(0); // c:212
7238/// Port of `int minmlen` from compcore.c:212.
7239pub static minmlen: AtomicI32 = AtomicI32::new(0); // c:212
7240
7241/// Port of `LinkList expls` from compcore.c:218.
7242pub static expls: OnceLock<Mutex<std::sync::Arc<Mutex<Vec<Cexpl>>>>> = OnceLock::new(); // c:218 (Arc handle)
7243
7244/// Port of `mod_export Cexpl curexpl` from compcore.c:221.
7245pub static curexpl: OnceLock<Mutex<Option<Cexpl>>> = OnceLock::new(); // c:221
7246
7247/// Port of `LinkList matchers` from compcore.c:236. The C list holds
7248/// `Cmatcher` pointers (the parsed match-spec chains pushed by
7249/// addmatches's `add_bmatchers` block). Rust port mirrors that with
7250/// owned `Box<Cmatcher>` entries.
7251pub static matchers: OnceLock<Mutex<Vec<Box<crate::ported::zle::comp_h::Cmatcher>>>> =
7252 OnceLock::new(); // c:236
7253
7254/// Port of `mod_export Aminfo ainfo` from compcore.c:246.
7255pub static ainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
7256/// Port of `mod_export Aminfo fainfo` from compcore.c:246.
7257pub static fainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
7258
7259/// Port of `mod_export LinkList allccs` from compcore.c:259.
7260pub static allccs: OnceLock<Mutex<std::sync::Arc<Mutex<Vec<String>>>>> = OnceLock::new(); // c:259 (Arc handle)
7261
7262/// Port of `int fromcomp` from compcore.c:271.
7263pub static fromcomp: AtomicI32 = AtomicI32::new(0); // c:271
7264
7265/// Port of `mod_export int lastend` from compcore.c:276.
7266pub static lastend: AtomicI32 = AtomicI32::new(0); // c:276
7267
7268/// Port of `mod_export Brinfo brbeg` from `Src/Zle/zle_tricky.c`.
7269/// Linked list of opening-brace positions in the word being completed.
7270pub static BRBEG: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brbeg
7271
7272/// Port of `mod_export Brinfo brend` from `Src/Zle/zle_tricky.c`.
7273/// Linked list of closing-brace positions in the word being completed.
7274pub static BREND: OnceLock<Mutex<Option<Box<Brinfo>>>> = OnceLock::new(); // zle_tricky.c brend
7275
7276/// Port of `static int oldmenucmp` from compcore.c:457.
7277pub static OLDMENUCMP: AtomicI32 = AtomicI32::new(0); // c:457
7278
7279/// Port of `static int parwb` from compcore.c:540.
7280pub static PARWB: AtomicI32 = AtomicI32::new(0); // c:540
7281/// Port of `static int parwe` from compcore.c:540.
7282pub static PARWE: AtomicI32 = AtomicI32::new(0); // c:540
7283/// Port of `static int paroffs` from compcore.c:540.
7284pub static PAROFFS: AtomicI32 = AtomicI32::new(0); // c:540
7285
7286/// Port of `static int matchorder` from compcore.c:3169.
7287pub static MATCHORDER: AtomicI32 = AtomicI32::new(0); // c:3169
7288
7289/// Port of `mod_export int lastchar` from `Src/Zle/zle_main.c`. Last
7290/// keyboard char consumed by the binding loop — read by `selfinsert`.
7291pub static LASTCHAR: AtomicI32 = AtomicI32::new(0); // zle_main.c
7292 // minfo_clear_cur / minfo_asked_zero deleted — Rust-only 2-line
7293 // wrappers around C's inline writes `minfo.cur = NULL` and
7294 // `minfo.asked = 0`. All call sites inlined.
7295
7296/// Direct port of `struct menuinfo minfo` — `Src/Zle/zle_tricky.c`
7297/// (the single file-scope instance). The struct type itself lives
7298/// in `comp_h.rs::Menuinfo` (port of comp.h:284-295).
7299pub static MINFO: OnceLock<Mutex<Menuinfo>> = OnceLock::new(); // zle_tricky.c minfo
7300
7301// =====================================================================
7302// matcheq — `Src/Zle/compcore.c:3203-3215`.
7303// =====================================================================
7304
7305#[inline]
7306fn matchstreq(a: Option<&String>, b: Option<&String>) -> bool {
7307 // c:3207
7308 match (a, b) {
7309 (None, None) => true,
7310 (Some(x), Some(y)) => x == y,
7311 _ => false,
7312 }
7313}
7314
7315/// First-match shortcut path from compcore.c:398-411. `Cmgroup m = amatches;
7316/// while (!m->mcount) m = m->next; do_single(m->matches[0])`.
7317fn do_single_first_match() {
7318 // c:398
7319 let groups = amatches
7320 .get_or_init(|| Mutex::new(Vec::new()))
7321 .lock()
7322 .ok()
7323 .map(|g| g.clone())
7324 .unwrap_or_default();
7325 let first = groups
7326 .into_iter()
7327 .find(|g| g.mcount > 0)
7328 .and_then(|g| g.matches.first().cloned());
7329 if let Some(m) = first {
7330 // c:407 — `minfo.cur = NULL`.
7331 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
7332 g.cur = None;
7333 g.asked = 0; // c:408
7334 }
7335 // c:409 — `do_single(m->matches[0])`. This is the actual
7336 // single-match insert: it deletes the word between wb/we (incl.
7337 // the addx placeholder) and inserts the completed string with
7338 // its suffix. Previously this call was omitted (the match was
7339 // merely stashed on minfo.cur), so a unique completion left the
7340 // typed prefix + 'x' on the line instead of the completed word.
7341 crate::ported::zle::compresult::do_single(&m);
7342 }
7343}
7344
7345/// compcore.c:444 `compend:` epilogue — free matchers, snap zlemetacs.
7346fn goto_compend(ret: i32) -> i32 {
7347 // c:444
7348 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
7349 g.clear(); // c:445-446 freecmatcher loop
7350 }
7351 let line_len = ZLEMETALL.load(Ordering::Relaxed); // c:448 strlen(zlemetaline)
7352 if ZLEMETACS.load(Ordering::Relaxed) > line_len {
7353 // c:449
7354 ZLEMETACS.store(line_len, Ordering::Relaxed); // c:450
7355 }
7356 ret // c:453
7357}
7358
7359// `COMP_LIST_COMPLETE` / `QT_NONE_STUB` / `QT_BACKSLASH_STUB` local
7360// aliases deleted — call sites now reach the real C-side constants
7361// directly (`crate::ported::zle::zle_h::COMP_LIST_COMPLETE`,
7362// `QT_NONE`, `QT_BACKSLASH`).
7363// The local `COMP_LIST_COMPLETE = 2` was a value-mismatch bug (the
7364// real constant is 1 per `Src/Zle/zle.h:357`).
7365
7366// `char_from_qt` deleted — Rust-only 1-line `(qt as u8) as char`
7367// helper. Inlined at the two call sites in get_compstate_str.
7368
7369// `showinglist_stub` / `showinglist_set` / `clearlist_set` /
7370// `listshown_stub` / `instring_stub` deleted — Rust-only 1-line
7371// accessors for C globals (SHOWINGLIST / CLEARLIST / LISTSHOWN /
7372// INSTRING). C reads/writes the bare globals inline; callers in
7373// compcore.rs now do `<GLOBAL>.load(Ordering::Relaxed)` /
7374// `<GLOBAL>.store(v, Ordering::Relaxed)` directly.
7375// `fn foredel` / `fn inststr` locals deleted — both duplicated
7376// canonical ports living in their proper home files:
7377// - foredel: `Src/Zle/zle_utils.c:1105`
7378// → `foredel`
7379// - inststr: macro `Src/Zle/zle_tricky.c:57` (inststr(X) →
7380// inststrlen(X,1,-1)) → `inststr`
7381// The duplicates here narrowed C signatures (foredel dropped `flags`,
7382// inststr dropped the i32 return + duplicated inststrlen's body) and
7383// violated Rule C (every decl in its mirroring C file). Callers in
7384// this module now route through the canonical ported.
7385/// `IN_NOTHING_LW` constant.
7386// These MUST match the raw IN_* enum in zsh_h (Src/zsh.h:2322-2332), which is
7387// the single encoding C uses for both `inwhat` and `linwhat`. `linwhat` is
7388// copied verbatim from `INWHAT` (makecomplist c:960), so a divergent ordering
7389// here made makecomplistglobal's `linwhat == IN_ENV_LW` never match after an
7390// assignment (`x=<Tab>`): INWHAT held IN_ENV=4 while IN_ENV_LW was 5, so
7391// value/assignment completion silently did nothing.
7392pub const IN_NOTHING_LW: i32 = 0; // = IN_NOTHING (zsh.h:2322)
7393/// `IN_CMD_LW` constant.
7394pub const IN_CMD_LW: i32 = 1; // = IN_CMD (zsh.h:2324)
7395/// `IN_MATH_LW` constant.
7396pub const IN_MATH_LW: i32 = 2; // = IN_MATH (zsh.h:2326)
7397/// `IN_COND_LW` constant.
7398pub const IN_COND_LW: i32 = 3; // = IN_COND (zsh.h:2328)
7399/// `IN_ENV_LW` constant.
7400pub const IN_ENV_LW: i32 = 4; // = IN_ENV (zsh.h:2330)
7401/// `IN_PAR_LW` constant.
7402pub const IN_PAR_LW: i32 = 5; // = IN_PAR (zsh.h:2332)
7403 // `origline_stub` / `origcs_stub` deleted — Rust-only 1-line
7404 // accessors for the `ORIGLINE` / `ORIGCS` globals (ports of C
7405 // `origline` / `origcs` at zle_tricky.c:75 etc.). C reads these
7406 // globals inline; callers in compcore.rs now do the lock/load
7407 // directly.
7408/// Port of `void unmetafy_line(void)` from `zle_tricky.c:995`.
7409///
7410/// C body:
7411/// `zlemetaline[zlemetall] = '\0';
7412/// zleline = stringaszleline(zlemetaline, zlemetacs, &zlell,
7413/// &linesz, &zlecs);
7414/// free(zlemetaline); zlemetaline = NULL;
7415/// CCRIGHT();`
7416///
7417/// Reads ZLEMETALINE, decodes via the canonical stringaszleline
7418/// (handles incs adjustment + unmetafy + UTF-8 decode), populates
7419/// ZLELINE / ZLELL / ZLECS, clears ZLEMETALINE/ZLEMETALL.
7420pub fn unmetafy_line() {
7421 // zle_tricky.c:995
7422 let meta = ZLEMETALINE
7423 .get_or_init(|| Mutex::new(String::new()))
7424 .lock()
7425 .map(|g| g.clone())
7426 .unwrap_or_default();
7427 let zlemetacs = ZLEMETACS.load(Ordering::Relaxed) as i32;
7428 let mut out_ll: i32 = 0;
7429 let mut out_cs: i32 = 0;
7430 // c:998 — `zleline = stringaszleline(zlemetaline, zlemetacs, &zlell, &linesz, &zlecs);`
7431 let line = crate::ported::zle::zle_utils::stringaszleline(
7432 &meta,
7433 zlemetacs,
7434 Some(&mut out_ll),
7435 None,
7436 Some(&mut out_cs),
7437 );
7438 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
7439 *g = line.iter().collect();
7440 }
7441 ZLELL.store(out_ll, Ordering::Relaxed);
7442 ZLECS.store(out_cs, Ordering::Relaxed);
7443 // c:1001-1002 — `free(zlemetaline); zlemetaline = NULL;`. Rust:
7444 // clear the buffer + zero the length to mark meta-mode inactive.
7445 if let Some(m) = ZLEMETALINE.get() {
7446 if let Ok(mut g) = m.lock() {
7447 g.clear();
7448 }
7449 }
7450 ZLEMETALL.store(0, Ordering::Relaxed);
7451 // c:1007 — CCRIGHT(): combining-char alignment fixup. No-op in
7452 // this Rust port (handled by stringaszleline's codepoint walk).
7453}
7454
7455/// Port of `void metafy_line(void)` from `zle_tricky.c:978`.
7456///
7457/// C body:
7458/// `zlemetaline = zlelineasstring(zleline, zlell, zlecs,
7459/// &zlemetall, &zlemetacs, 0);
7460/// metalinesz = zlemetall;
7461/// free(zleline); zleline = NULL;`
7462///
7463/// Reads ZLELINE, encodes via the canonical zlelineasstring (handles
7464/// wcrtomb + metafy expansion), populates ZLEMETALINE / ZLEMETALL /
7465/// ZLEMETACS, clears ZLELINE/ZLELL.
7466pub fn metafy_line() {
7467 // zle_tricky.c:978
7468 let raw_vec: Vec<char> = ZLELINE
7469 .get_or_init(|| Mutex::new(String::new()))
7470 .lock()
7471 .map(|g| g.chars().collect())
7472 .unwrap_or_default();
7473 let zlell = raw_vec.len();
7474 let zlecs = ZLECS.load(Ordering::Relaxed);
7475 let mut out_ll: i32 = 0;
7476 let mut out_cs: i32 = 0;
7477 // c:982 — `zlemetaline = zlelineasstring(zleline, zlell, zlecs, &zlemetall, &zlemetacs, 0);`
7478 let meta = crate::ported::zle::zle_utils::zlelineasstring(
7479 &raw_vec,
7480 zlell,
7481 zlecs,
7482 Some(&mut out_ll),
7483 Some(&mut out_cs),
7484 0,
7485 );
7486 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
7487 *g = meta;
7488 }
7489 ZLEMETALL.store(out_ll, Ordering::Relaxed);
7490 ZLEMETACS.store(out_cs, Ordering::Relaxed);
7491 // c:985 — `metalinesz = zlemetall;`. Rust String grows on demand;
7492 // no separate sizeline tracker.
7493 // c:989-990 — `free(zleline); zleline = NULL;`. Rust: clear the
7494 // buffer + zero ZLELL.
7495 if let Ok(mut g) = ZLELINE.get().unwrap().lock() {
7496 g.clear();
7497 }
7498 ZLELL.store(0, Ordering::Relaxed);
7499}
7500
7501fn opt_isset(name: &str) -> i32 {
7502 // options.c
7503 if crate::ported::options::opt_state_get(name).unwrap_or(false) {
7504 1
7505 } else {
7506 0
7507 }
7508}
7509/// Real call into `getiparam(name)` — the canonical paramtab read.
7510/// Mirrors C's `getiparam` at params.c:3044 which reads the global
7511/// `paramtab` directly via `gethashnode2`.
7512fn env_iparam(name: &str) -> i32 {
7513 // params.c:3044
7514 crate::ported::params::getiparam(name) as i32
7515}
7516fn lastprebr_set(s: Option<&str>) {
7517 // zle_tricky.c lastprebr
7518 if let Ok(mut g) = LASTPREBR.get_or_init(|| Mutex::new(None)).lock() {
7519 *g = s.map(str::to_string);
7520 }
7521}
7522fn lastpostbr_set(s: Option<&str>) {
7523 // zle_tricky.c lastpostbr
7524 if let Ok(mut g) = LASTPOSTBR.get_or_init(|| Mutex::new(None)).lock() {
7525 *g = s.map(str::to_string);
7526 }
7527}
7528
7529/// Choose `$compstate[context]` per the lex classification in `inwhat`
7530/// (and the `ispar` modifier). Direct lift of compcore.c:578-633.
7531///
7532/// The arm ORDER is load-bearing and follows C exactly: `ispar`,
7533/// `IN_PAR`, `IN_MATH`, **`lincmd`**, **`linredir`**, then the
7534/// `switch (linwhat)` for IN_ENV / IN_COND / default. An earlier
7535/// revision tested IN_COND/IN_ENV before `lincmd`, which is the
7536/// opposite of C — harmless in practice only because `get_comp_string`
7537/// forces `lincmd = 0` inside a `[[ … ]]` (c:1198-1202, the `!incond`
7538/// terms) and `docomplete` zeroes it for IN_ENV (zle_tricky.c:701-702).
7539fn compcontext_for(_s: &str) -> String {
7540 use crate::ported::zle::zle_tricky::{CMDSTR, LINARR, LINCMD, LINREDIR};
7541 let ip = ispar.load(Ordering::Relaxed); // c:578
7542 if ip != 0 {
7543 // c:579
7544 return if ip == 2 {
7545 "brace_parameter".into()
7546 } else {
7547 "parameter".into()
7548 };
7549 }
7550 let lw = linwhat.load(Ordering::Relaxed);
7551 let insubscr = crate::ported::zle::zle_tricky::INSUBSCR.load(Ordering::Relaxed); // c:583
7552 if lw == IN_PAR_LW {
7553 return "assign_parameter".into(); // c:580-581
7554 }
7555 if lw == IN_MATH_LW {
7556 // c:582-591 — inside an unclosed `[` the math context is a
7557 // SUBSCRIPT, and `$compstate[parameter]` names the array
7558 // (published by callcompfunc from `varname`). Without this
7559 // `echo $fpath[<TAB>` never reached `_subscript`.
7560 return if insubscr != 0 {
7561 "subscript".into() // c:584
7562 } else {
7563 "math".into() // c:590
7564 };
7565 }
7566 if LINCMD.load(Ordering::Relaxed) != 0 {
7567 // c:592-597 — `[` in COMMAND position is a subscript too.
7568 return if insubscr != 0 {
7569 "subscript".into() // c:594
7570 } else {
7571 "command".into() // c:597
7572 };
7573 }
7574 if LINREDIR.load(Ordering::Relaxed) != 0 {
7575 // c:598-602 — the cursor word is a redirection TARGET
7576 // (`echo x > /tm<TAB>`). `callcompfunc` publishes `rdstr` as
7577 // `$compstate[redirect]` right after this returns, which is what
7578 // `_redirect` (Completion/Zsh/Context/_redirect) dispatches on.
7579 return "redirect".into(); // c:599
7580 }
7581 match lw {
7582 // c:604
7583 // c:605-606 — an array assignment (`x=(a b <TAB>)`) is
7584 // `array_value`, a scalar one (`x=<TAB>`) plain `value`.
7585 x if x == IN_ENV_LW => if LINARR.load(Ordering::Relaxed) != 0 {
7586 "array_value"
7587 } else {
7588 "value"
7589 }
7590 .into(),
7591 x if x == IN_COND_LW => "condition".into(), // c:619-620
7592 // c:622-630 — no command word was parsed at all, so this is the
7593 // value of something rather than an argument to a command.
7594 _ => {
7595 let have_cmdstr = CMDSTR.lock().map(|g| g.is_some()).unwrap_or(false); // c:623
7596 if have_cmdstr {
7597 "command".into() // c:624
7598 } else {
7599 "value".into() // c:626
7600 }
7601 }
7602 }
7603}
7604
7605/// File-scope `int offs` from `Src/Zle/zle_tricky.c:88`. The C source
7606/// declares this as `mod_export`; mirrored here per Rule 9 since it's
7607/// not yet at a canonical Rust home.
7608pub static OFFS: AtomicI32 = AtomicI32::new(0); // zle_tricky.c:88
7609
7610/// File-scope `Compctl freecl` from `Src/Zle/compcore.c:255`. The
7611/// freelist of available Compctl slots for the current completion call.
7612pub static freecl: OnceLock<Mutex<Option<i32>>> = OnceLock::new(); // c:255
7613
7614/// Real call into `doshfunc` — `Src/exec.c`. Looks up the function
7615/// in the global shfunctab (`getshfunc`) and dispatches via the VM's
7616/// `functions_compiled` map. Returns the function's exit status
7617/// (LASTVAL after the call), matching C's `doshfunc` return value.
7618pub fn shfunc_call(name: &str) -> i32 {
7619 // exec.c
7620 if crate::ported::utils::getshfunc(name).is_none() {
7621 // c:exec.c:5800
7622 return 1; // missing fn → status 1
7623 }
7624 // Route through the canonical exec accessors dispatcher so the
7625 // function actually executes. Hook returns Option<i32>; None
7626 // means no executor context is set up yet (fall back to
7627 // LASTVAL read).
7628 crate::ported::exec::dispatch_function_call(name, &[])
7629 .unwrap_or_else(|| crate::ported::builtin::LASTVAL.load(Ordering::Relaxed))
7630}
7631/// Real call into `setsparam(&format!("compstate[{key}]"), val)` — the
7632/// canonical paramtab write. Mirrors C's `setsparam` at params.c:3350.
7633///
7634/// Now `pub` so compsys engine ports can write `$compstate[KEY]`
7635/// directly. Also dual-writes to `paramtab_hashed_storage()` under
7636/// the "compstate" key so subscript lookups via the hash-param
7637/// machinery see the same value — `$compstate` IS a PM_HASHED param
7638/// (created by `makecompparams` at `complete.rs:1499`), and shell
7639/// scripts read it as such.
7640pub fn set_compstate_str(key: &str, val: &str) {
7641 // !!! RUST-ONLY: in C these keys are globals behind compstate_gsu and
7642 // `$compstate` exists only between makecompparams (complete.c:1340)
7643 // and endparamscope (compcore.c:839). This bridge publishes them
7644 // through the param, so the association has to exist first.
7645 //
7646 // Hash-storage write FIRST: a `compstate` row in
7647 // `paramtab_hashed_storage()` is what makes the setsparam below
7648 // route `compstate[KEY]` as an association key rather than an
7649 // arithmetic subscript. assignsparam does not special-case the
7650 // name, because outside completion zsh has no `$compstate`.
7651 if let Ok(mut tab) = paramtab_hashed_storage().lock() {
7652 tab.entry("compstate".to_string())
7653 .or_default()
7654 .insert(key.to_string(), val.to_string());
7655 }
7656
7657 // params.c:3350 — flat bracketed-param write; creates the PM_HASHED
7658 // node on first use and keeps it in step with the store.
7659 let pname = format!("compstate[{}]", key);
7660 let _ = setsparam(&pname, val);
7661
7662 // The `VAL(...)` rows in `compkparams` (`Src/Zle/complete.c:1292`,
7663 // `:1297`, `:1300`) name a real variable rather than a getter, so in
7664 // C an assignment to `$compstate[KEY]` updates that variable and a
7665 // later read sees it. [`get_compstate_str`] serves those keys from
7666 // the backing global, so the write has to land there too or the
7667 // round-trip is lost. The getter-only rows are deliberately absent:
7668 // C recomputes them on every read and a stored value would be stale.
7669 match key {
7670 // c:1292 `VAL(complistmax)`.
7671 "list_max" => {
7672 if let Ok(n) = val.parse::<i64>() {
7673 crate::ported::zle::complete::COMPLISTMAX.store(n, Ordering::Relaxed);
7674 }
7675 }
7676 // c:1297 `VAL(compvared)`.
7677 "vared" => {
7678 if let Ok(mut s) = crate::ported::zle::complete::COMPVARED
7679 .get_or_init(|| Mutex::new(String::new()))
7680 .lock()
7681 {
7682 *s = val.to_string();
7683 }
7684 }
7685 // c:1300 `VAL(compignored)`.
7686 "ignored" => {
7687 if let Ok(n) = val.parse::<i64>() {
7688 crate::ported::zle::complete::COMPIGNORED.store(n, Ordering::Relaxed);
7689 }
7690 }
7691 _ => {}
7692 }
7693}
7694
7695/// The `$compstate` keys whose values C does not store: their
7696/// `compkparams` rows (`Src/Zle/complete.c:1261-1300`) carry a `gsu`
7697/// vtable instead of a `var` pointer, so every read runs the getter
7698/// against live completion state. Listed in `compkparams` order.
7699pub const LIVE_COMPSTATE_KEYS: &[&str] = &[
7700 "nmatches", // c:1262 nmatches_gsu
7701 "unambiguous", // c:1285 unambig_gsu
7702 "unambiguous_cursor", // c:1286 unambig_curs_gsu
7703 "unambiguous_positions", // c:1288 unambig_pos_gsu
7704 "insert_positions", // c:1290 insert_pos_gsu
7705 "list_max", // c:1292 VAL(complistmax)
7706 "vared", // c:1297 VAL(compvared)
7707 "list_lines", // c:1298 listlines_gsu
7708 "all_quotes", // c:1299 compqstack_gsu
7709 "ignored", // c:1300 VAL(compignored)
7710];
7711
7712/// Read `$compstate[KEY]`. Returns `None` when the key was never set.
7713///
7714/// The [`LIVE_COMPSTATE_KEYS`] arm below is the Rust stand-in for C's
7715/// per-key gsu getter firing on each read; everything else comes from
7716/// the hash-storage view (the canonical home for a PM_HASHED param),
7717/// falling back to the legacy flat `compstate[KEY]` bracketed param for
7718/// entries that some code wrote via raw `setsparam` without going
7719/// through [`set_compstate_str`].
7720pub fn get_compstate_str(key: &str) -> Option<String> {
7721 // c:complete.c:1236-1252 — the gsu-backed keys are recomputed on
7722 // every read; a stored value would be stale. Before this arm covered
7723 // more than `nmatches`, none of them existed anywhere in zshrs's
7724 // compstate storage, so `_lastcomp` (`_main_complete` sh:407) came
7725 // back missing nine entries — `_lastcomp[unambiguous]` and
7726 // `[unambiguous_cursor]` (read at sh:84-86 and by `_next_tags`
7727 // sh:105) among them.
7728 let nil = std::ptr::null_mut();
7729 match key {
7730 // c:complete.c:1401-1405 — `get_nmatches`: flush pending match
7731 // groups via `permmatches(0)`, then read the counter. A stored
7732 // read served a stale 0, so every completer's
7733 // `nm != $compstate[nmatches]` idiom (_describe, _arguments,
7734 // _alternative, …) concluded "nothing was added" and option
7735 // completion died even though addmatches had added hundreds.
7736 "nmatches" => {
7737 let v = if permmatches(0) != 0 {
7738 0
7739 } else {
7740 nmatches.load(Ordering::Relaxed)
7741 };
7742 return Some(v.to_string());
7743 }
7744 // c:1439-1442 — `unambig_data(NULL, NULL, NULL)`.
7745 "unambiguous" => return Some(crate::ported::zle::complete::get_unambig(nil)),
7746 // c:1446-1450 — `unambig_data(&c, NULL, NULL); return c`.
7747 "unambiguous_cursor" => {
7748 return Some(crate::ported::zle::complete::get_unambig_curs(nil).to_string())
7749 }
7750 // c:1447-1456 — `unambig_data(NULL, &p, NULL); return p`.
7751 "unambiguous_positions" => return Some(crate::ported::zle::complete::get_unambig_pos(nil)),
7752 // c:1458-1466 — `unambig_data(NULL, NULL, &p); return p`.
7753 "insert_positions" => return Some(crate::ported::zle::complete::get_insert_pos(nil)),
7754 // c:1292 `VAL(complistmax)`; seeded from $LISTMAX at c:323.
7755 "list_max" => {
7756 return Some(
7757 crate::ported::zle::complete::COMPLISTMAX
7758 .load(Ordering::Relaxed)
7759 .to_string(),
7760 )
7761 }
7762 // c:1297 `VAL(compvared)` — the parameter name `vared` is
7763 // editing, `""` outside `vared` (c:compcore.c:565-570). zshrs
7764 // does not track `varedarg` yet, so the global stays at the
7765 // `""` C publishes for every non-`vared` completion.
7766 "vared" => {
7767 return Some(
7768 crate::ported::zle::complete::COMPVARED
7769 .get_or_init(|| Mutex::new(String::new()))
7770 .lock()
7771 .map(|s| s.clone())
7772 .unwrap_or_default(),
7773 )
7774 }
7775 // c:1408-1420 — `get_listlines` → `list_lines()`.
7776 "list_lines" => return Some(crate::ported::zle::complete::get_listlines(nil).to_string()),
7777 // c:1469 — `get_compqstack`: one char per quoting level.
7778 "all_quotes" => return Some(crate::ported::zle::complete::get_compqstack(nil)),
7779 // c:1300 `VAL(compignored)` — matches dropped by `compadd -F`.
7780 "ignored" => {
7781 return Some(
7782 crate::ported::zle::complete::COMPIGNORED
7783 .load(Ordering::Relaxed)
7784 .to_string(),
7785 )
7786 }
7787 _ => {}
7788 }
7789 if let Ok(tab) = paramtab_hashed_storage().lock() {
7790 if let Some(hash) = tab.get("compstate") {
7791 if let Some(v) = hash.get(key) {
7792 return Some(v.clone());
7793 }
7794 }
7795 }
7796 // Fallback: pre-existing callers wrote via raw `setsparam` only.
7797 let pname = format!("compstate[{}]", key);
7798 getsparam(&pname)
7799}
7800
7801/// Local helper: position before-the-current char (handles UTF-8).
7802#[inline]
7803fn prev_char_index(bytes: &[u8], pos: usize) -> usize {
7804 // local
7805 if pos == 0 {
7806 return 0;
7807 }
7808 let mut i = pos - 1;
7809 while i > 0 && (bytes[i] & 0xC0) == 0x80 {
7810 i -= 1;
7811 }
7812 i
7813}
7814
7815#[inline]
7816fn char_at(bytes: &[u8], pos: usize) -> char {
7817 // local
7818 if pos >= bytes.len() {
7819 return '\0';
7820 }
7821 let s = match std::str::from_utf8(&bytes[pos..]) {
7822 Ok(s) => s,
7823 Err(_) => return '\0',
7824 };
7825 s.chars().next().unwrap_or('\0')
7826}
7827
7828/// Depth counter so `set_comp_sep`'s sanity assert ("lexsave/restore
7829/// balanced") fires when a future port mismatches them.
7830static LEXSAVE_DEPTH: AtomicI32 = AtomicI32::new(0); // local
7831
7832/// Walk a balanced pair of in/out token bytes starting at `start`,
7833/// returning the index just after the closing token, or None if
7834/// unbalanced. C `skipparens` returns the position; this version
7835/// returns the same semantic.
7836fn skip_token_parens(bytes: &[u8], start: usize, open: char, close: char) -> Option<usize> {
7837 let mut depth: i32 = 0;
7838 let mut i = start;
7839 while i < bytes.len() {
7840 let c = char_at(bytes, i);
7841 if c == open {
7842 depth += 1;
7843 } else if c == close {
7844 depth -= 1;
7845 if depth == 0 {
7846 return Some(i + c.len_utf8());
7847 }
7848 }
7849 i += c.len_utf8();
7850 }
7851 if depth == 0 {
7852 Some(i)
7853 } else {
7854 None
7855 }
7856}
7857
7858/// Walk the INAMESPC name-character class — equivalent to C's
7859/// `itype_end(e, INAMESPC, 0)` loop. Stops at first non-name char.
7860fn walk_namespace(bytes: &[u8]) -> usize {
7861 // local
7862 let s = match std::str::from_utf8(bytes) {
7863 Ok(s) => s,
7864 Err(_) => return 0,
7865 };
7866 let mut len = 0usize;
7867 for c in s.chars() {
7868 if c.is_alphanumeric() || c == '_' {
7869 len += c.len_utf8();
7870 } else {
7871 break;
7872 }
7873 }
7874 len
7875}
7876
7877/// Strip Inbrace/Outbrace/Stringg/etc. token bytes back to literal
7878/// characters — substitute for C `untokenize()` over the slice. The
7879/// canonical Rust untokenize lives in `crate::lex::untokenize`.
7880fn strip_tokens(s: &str) -> String {
7881 // local
7882 crate::lex::untokenize(s).to_string()
7883}
7884
7885/// File-scope `int hcompcall` accessor — `compfunc` active iff non-empty.
7886fn compfunc_active() -> bool {
7887 compfunc
7888 .get_or_init(|| Mutex::new(None))
7889 .lock()
7890 .ok()
7891 .and_then(|g| g.clone())
7892 .map(|s| !s.is_empty())
7893 .unwrap_or(false)
7894}
7895
7896/// Direct port of `void lexsave(void)` from `Src/lex.c`. Delegates
7897/// to `zcontext_save` which pushes the lex/parse/hist context stack
7898/// frame. Returns a token (current stack depth) for symmetry with
7899/// the C `int` save token used by `set_comp_sep` for invariant check.
7900fn lexsave() -> usize {
7901 // lex.c via context.c:80
7902 crate::ported::context::zcontext_save();
7903 (LEXSAVE_DEPTH.fetch_add(1, Ordering::SeqCst) + 1) as usize
7904}
7905
7906/// Direct port of `void lexrestore(void)` from `Src/lex.c`. Pops the
7907/// last `zcontext_save` frame. C body restores hist/lex/parse via
7908/// `zcontext_restore_partial(ZCONTEXT_HIST|ZCONTEXT_LEX|ZCONTEXT_PARSE)`.
7909fn lexrestore(_token: usize) {
7910 // lex.c via context.c:117
7911 let parts = ZCONTEXT_HIST | ZCONTEXT_LEX | ZCONTEXT_PARSE;
7912 zcontext_restore_partial(parts);
7913 LEXSAVE_DEPTH.fetch_sub(1, Ordering::SeqCst);
7914}
7915
7916// ---- Extern stubs for addmatches's bucket-3 dependencies ----
7917
7918/// Reads the first char of `char *compquote` — `Src/Zle/complete.c:54`,
7919/// gsu-bound to `$compstate[quote]` (complete.c:1276), i.e. C's
7920/// `(qc = *compquote)` at c:2139.
7921///
7922/// !!! WARNING: RUST-ONLY HELPER !!!
7923/// C dereferences the bare global; the port keeps it in an
7924/// `OnceLock<Mutex<String>>`, so the deref needs a function. It used to
7925/// read `zle_tricky::COMPQUOTE` — a Rust-only DUPLICATE of the global
7926/// that has no counterpart in `Src/Zle/zle_tricky.c` and that nothing
7927/// ever writes, so `addmatches`'s quote block (c:2139-2168) always took
7928/// the `else` arm and cleared `instring`/`autoq` on every compadd.
7929fn compquote_first() -> Option<char> {
7930 // complete.c:54
7931 crate::ported::zle::complete::COMPQUOTE
7932 .get_or_init(|| Mutex::new(String::new()))
7933 .lock()
7934 .ok()
7935 .and_then(|g| g.chars().next())
7936}
7937fn instring_set(v: i32) {
7938 // zle_tricky.c:419
7939 INSTRING.store(v, Ordering::Relaxed);
7940}
7941fn inbackt_set(v: i32) {
7942 // zle_tricky.c:419
7943 INBACKT.store(v, Ordering::Relaxed);
7944}
7945fn autoq_set(s: &str) {
7946 // zle_tricky.c autoq
7947 if let Ok(mut g) = AUTOQ.get_or_init(|| Mutex::new(String::new())).lock() {
7948 *g = s.to_string();
7949 }
7950}
7951
7952// ---- Extern stubs for makecomplist's bucket-3 dependencies ----
7953
7954/// File-scope holder for `Cmlist bmatchers` — `Src/Zle/compcore.c:236`.
7955/// C linked-list of matchers active for brace-matching, populated by
7956/// `add_bmatchers` walking the user-installed `Cmatcher` chain.
7957pub static bmatchers: OnceLock<Mutex<Option<std::sync::Arc<Cmlist>>>> = OnceLock::new(); // c:236
7958
7959/// File-scope holder for `Cmlist mstack` — `Src/Zle/compcore.c:236`.
7960/// Matcher-stack — current active matcher list for compadd recursion.
7961pub static mstack: OnceLock<Mutex<Option<std::sync::Arc<Cmlist>>>> = OnceLock::new(); // c:236
7962
7963// ---- Extern stubs for add_match_data's Cline operations ----
7964
7965/// Bridge to `cline_matched()` — `Src/Zle/compmatch.c:253`. The
7966/// real port takes `&mut Option<Box<Cline>>` walking the chain
7967/// marking each node CLF_MATCHED. With only a string slice here we
7968/// build a one-node Cline shim and route the call through it so the
7969/// CLF_MATCHED state-machine update fires the same way as in C.
7970fn cline_matched_compcore(line: Option<&str>) {
7971 // compmatch.c:253
7972 let Some(s) = line else {
7973 return;
7974 };
7975 if s.is_empty() {
7976 return;
7977 }
7978 let mut head = Some(Box::new(Cline {
7979 line: Some(s.to_string()),
7980 llen: s.len() as i32,
7981 ..Default::default()
7982 }));
7983 cline_matched(&mut head);
7984}
7985/// Reads `char *qisuf` — `Src/Zle/zle_tricky.c:137`.
7986///
7987/// This used to be `getsparam("qisuf")`, i.e. a lookup for a SHELL
7988/// PARAMETER spelled with the C variable's name. No such parameter
7989/// exists in zsh or in this port (the shell-visible names are
7990/// `$QIPREFIX` / `$QISUFFIX`, complete.c:1266-1267, and they are gsu
7991/// views onto `compqiprefix`/`compqisuffix`, not onto `qipre`/`qisuf`),
7992/// so the read missed on every match and `add_match_data` built every
7993/// `cm->ipre`/`cm->isuf` without the word's quotes.
7994fn qisuf_get() -> String {
7995 // zle_tricky.c:137
7996 crate::ported::zle::zle_tricky::qisuf_get()
7997}
7998/// Reads `char *qipre` — `Src/Zle/zle_tricky.c:137`. See [`qisuf_get`].
7999fn qipre_get() -> String {
8000 // zle_tricky.c:137
8001 crate::ported::zle::zle_tricky::qipre_get()
8002}
8003
8004/// Adapter for `int movefd(int fd)` from `Src/utils.c:2974` —
8005/// delegates to the canonical port in `ported::utils::movefd`.
8006fn movefd(fd: i32) -> i32 {
8007 // utils.c:2974
8008 crate::ported::utils::movefd(fd)
8009}
8010
8011/// Adapter for `int redup(int x, int y)` from `Src/utils.c:2021` —
8012/// delegates to the canonical port `ported::utils::redup`. Every caller
8013/// here is one of C's three `redup(osi, 0)` sites (compcore.c:1013/1035/
8014/// 1039), i.e. "put the descriptor `movefd(0)` parked at c:964 back on
8015/// fd 0", so the target is 0.
8016///
8017/// The adapter used to pass `-1` as the target. `redup(osi, -1)` takes
8018/// the `x != y` arm, `dup2(osi, -1)` fails with EBADF, and the saved
8019/// descriptor is closed anyway — so the shell's stdin was DESTROYED by
8020/// the first Tab and fd 0 stayed closed for the rest of the session
8021/// (`cat` at the prompt after a completion read a bad descriptor).
8022fn redup(new: i32) {
8023 // utils.c:2021
8024 crate::ported::utils::redup(new, 0);
8025}
8026
8027/// File-scope registry mirroring `Src/init.c`'s `zshhooks[]` table —
8028/// each hook name maps to the ordered list of shfunc names to call.
8029pub static HOOK_FNS: OnceLock<Mutex<std::collections::HashMap<String, Vec<String>>>> =
8030 OnceLock::new(); // init.c zshhooks
8031
8032/// Adapter for the `errflag` global from `Src/init.c` — reads the
8033/// canonical atomic in `ported::utils::errflag`.
8034fn errflag_get() -> bool {
8035 crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 // init.c
8036}
8037
8038// =====================================================================
8039// permmatches — `Src/Zle/compcore.c:3423`.
8040// =====================================================================
8041
8042/// Static state for `permmatches`'s `static int fi`. C scopes the
8043/// flag to the function; Rust hoists it to file scope per Rule S1.
8044static PERMMATCHES_FI: AtomicI32 = AtomicI32::new(0); // c:3423 static int fi
8045
8046/// Port of the `type==0` string-sort branch of `makearray()` from
8047/// compcore.c:3239-3257. Sorts strings via `strmetasort` + dedup.
8048pub fn makearray_strings(mut rp: Vec<String>, flags: i32) -> (Vec<String>, i32) {
8049 // c:3239
8050 let mut n: i32 = rp.len() as i32;
8051 if flags != 0 && n > 0 {
8052 // c:3240
8053 let numeric = isset(NUMERICGLOBSORT); // c:3243
8054 let mut sf = SORTIT_IGNORING_BACKSLASHES as u32;
8055 if numeric {
8056 sf |= SORTIT_NUMERICALLY as u32;
8057 }
8058 crate::ported::sort::strmetasort(&mut rp, sf, None); // c:3242-3244
8059
8060 // Dedup consecutive equals. // c:3247
8061 let mut cp = 0usize;
8062 let mut ap = 0usize;
8063 while ap < rp.len() {
8064 if ap != cp {
8065 rp.swap(ap, cp);
8066 }
8067 cp += 1;
8068 let mut bp = ap;
8069 while bp + 1 < rp.len() && rp[ap] == rp[bp + 1] {
8070 // c:3250
8071 bp += 1;
8072 n -= 1;
8073 }
8074 ap = bp + 1; // c:3252
8075 }
8076 rp.truncate(cp); // c:3253
8077 }
8078 (rp, n)
8079}
8080
8081#[cfg(test)]
8082mod tests {
8083 use super::*;
8084
8085 #[test]
8086 fn rembslash_basic() {
8087 let _g = crate::test_util::global_state_lock();
8088 let _g = zle_test_setup();
8089 assert_eq!(rembslash("hello\\ world"), "hello world");
8090 assert_eq!(rembslash("no\\\\slash"), "no\\slash");
8091 assert_eq!(rembslash("plain"), "plain");
8092 }
8093
8094 #[test]
8095 fn comp_quoting_string_table() {
8096 let _g = crate::test_util::global_state_lock();
8097 let _g = zle_test_setup();
8098 assert_eq!(comp_quoting_string(QT_SINGLE), "'");
8099 assert_eq!(comp_quoting_string(QT_DOUBLE), "\"");
8100 assert_eq!(comp_quoting_string(QT_DOLLARS), "$'");
8101 assert_eq!(comp_quoting_string(0), "\\");
8102 assert_eq!(comp_quoting_string(QT_BACKSLASH), "\\");
8103 }
8104
8105 #[test]
8106 fn matcheq_equal_strings() {
8107 let _g = crate::test_util::global_state_lock();
8108 let _g = zle_test_setup();
8109 let mut a = Cmatch::default();
8110 a.str = Some("foo".into());
8111 let mut b = Cmatch::default();
8112 b.str = Some("foo".into());
8113 assert!(matcheq(&a, &b));
8114 }
8115
8116 #[test]
8117 fn matcheq_different_strings() {
8118 let _g = crate::test_util::global_state_lock();
8119 let _g = zle_test_setup();
8120 let mut a = Cmatch::default();
8121 a.str = Some("foo".into());
8122 let mut b = Cmatch::default();
8123 b.str = Some("bar".into());
8124 assert!(!matcheq(&a, &b));
8125 }
8126
8127 #[test]
8128 fn matcheq_one_side_none() {
8129 let _g = crate::test_util::global_state_lock();
8130 let _g = zle_test_setup();
8131 let mut a = Cmatch::default();
8132 a.pre = Some("p".into());
8133 let b = Cmatch::default();
8134 assert!(!matcheq(&a, &b));
8135 }
8136
8137 #[test]
8138 fn get_user_var_reads_array_from_paramtab() {
8139 let _g = crate::test_util::global_state_lock();
8140 // c:2003 — `getaparam(nam)` first. Verify array params come
8141 // out as a Vec, not via env.
8142 let _g = zle_test_setup();
8143 setaparam("__test_arr", vec!["a".into(), "bb".into(), "ccc".into()]);
8144 let got = get_user_var(Some("__test_arr"));
8145 assert_eq!(got, Some(vec!["a".into(), "bb".into(), "ccc".into()]));
8146 // Cleanup so we don't poison other tests.
8147 setaparam("__test_arr", vec![]);
8148 }
8149
8150 #[test]
8151 fn get_user_var_reads_scalar_as_single_element_array() {
8152 let _g = crate::test_util::global_state_lock();
8153 // c:2007-2009 — getsparam fallback: wrap scalar in 1-element array.
8154 let _g = zle_test_setup();
8155 setsparam("__test_scalar", "hello");
8156 let got = get_user_var(Some("__test_scalar"));
8157 assert_eq!(got, Some(vec!["hello".to_string()]));
8158 setsparam("__test_scalar", "");
8159 }
8160
8161 #[test]
8162 fn get_user_var_paren_list_splits_on_separators() {
8163 let _g = crate::test_util::global_state_lock();
8164 // c:1960-1996 — `(a b c)` paren list, NOT a param lookup.
8165 let _g = zle_test_setup();
8166 let got = get_user_var(Some("(one two three)"));
8167 assert_eq!(got, Some(vec!["one".into(), "two".into(), "three".into()]));
8168 }
8169
8170 #[test]
8171 fn get_user_var_none_for_missing() {
8172 let _g = crate::test_util::global_state_lock();
8173 // c:1956 + c:2009 — missing param returns None.
8174 let _g = zle_test_setup();
8175 // (env vars must not leak through — we don't read $PATH etc.)
8176 let got = get_user_var(Some("__definitely_not_a_param_xyz"));
8177 assert_eq!(got, None);
8178 }
8179
8180 #[test]
8181 fn get_data_arr_reads_hashed_keys_or_values() {
8182 let _g = crate::test_util::global_state_lock();
8183 // c:2022 — fetchvalue(name, SCANPM_WANTKEYS|WANTVALS|MATCHMANY).
8184 let _g = zle_test_setup();
8185 crate::ported::params::sethparam(
8186 "__test_hash",
8187 vec!["k1".into(), "v1".into(), "k2".into(), "v2".into()],
8188 );
8189
8190 let keys = get_data_arr("__test_hash", true);
8191 assert!(keys.is_some(), "hashed param should have keys");
8192 let mut keys = keys.unwrap();
8193 keys.sort();
8194 assert_eq!(keys, vec!["k1".to_string(), "k2".to_string()]);
8195
8196 let vals = get_data_arr("__test_hash", false);
8197 assert!(vals.is_some(), "hashed param should have values");
8198 let mut vals = vals.unwrap();
8199 vals.sort();
8200 assert_eq!(vals, vec!["v1".to_string(), "v2".to_string()]);
8201 }
8202
8203 /// The `compadd -k <magic-hash>` path, pinned end-to-end from
8204 /// `get_data_arr` down.
8205 ///
8206 /// c:2022-2037 — `fetchvalue(…, SCANPM_WANTKEYS|SCANPM_MATCHMANY)`
8207 /// then `getarrvalue`, which for a PM_HASHED param is
8208 /// `paramvalarr(v->pm->gsu.h->getfn(v->pm), v->scanflags)`
8209 /// (Src/params.c:717). For a SPECIALPMDEF magic hash that `getfn`
8210 /// returns the fake HashTable whose `scantab` IS the module's
8211 /// `scanpm*` fn, so the SCAN is the backing.
8212 ///
8213 /// zshrs stores ordinary assoc contents in the name-keyed
8214 /// `paramtab_hashed_storage` map; an EMPTY row seeded there for a
8215 /// magic name used to answer first and shadow the scan. That is the
8216 /// exact route `_path_commands`' `compadd -k commands` (sh:103)
8217 /// takes, so command-name completion returned ZERO external
8218 /// commands in a fresh shell and only started working after some
8219 /// other `$commands` read had run the scan (which is what calls
8220 /// `fillcmdnamtable` under HASH_LIST_ALL,
8221 /// Src/Modules/parameter.c:253).
8222 ///
8223 /// Probed with `builtins` rather than `commands` so the assertion
8224 /// does not depend on `$PATH` or on `pathchecked` state another
8225 /// test may have consumed.
8226 #[test]
8227 fn get_data_arr_scans_a_magic_hash_past_an_empty_storage_row() {
8228 let _g = crate::test_util::global_state_lock();
8229 let _g = zle_test_setup();
8230 crate::vm_helper::seed_partab_param("builtins");
8231 // Exactly the row that used to answer instead of the scan.
8232 crate::ported::params::paramtab_hashed_storage()
8233 .lock()
8234 .unwrap()
8235 .insert("builtins".to_string(), Default::default());
8236
8237 let keys = get_data_arr("builtins", true)
8238 .expect("c:2034 — a PM_HASHED magic name must not fetch as None");
8239 assert!(
8240 keys.iter().any(|k| k == "print"),
8241 "c:2037 — `compadd -k builtins` must see the scanfn's keys, not the \
8242 empty hashed-storage row (got {} key(s))",
8243 keys.len()
8244 );
8245
8246 crate::ported::params::paramtab_hashed_storage()
8247 .lock()
8248 .unwrap()
8249 .remove("builtins");
8250 }
8251
8252 #[test]
8253 fn get_data_arr_none_for_non_hashed() {
8254 let _g = crate::test_util::global_state_lock();
8255 // c:2032 — fetchvalue NULL → return NULL for params that
8256 // aren't associative arrays.
8257 let _g = zle_test_setup();
8258 setsparam("__test_scalar2", "value");
8259 let got = get_data_arr("__test_scalar2", false);
8260 assert_eq!(got, None, "scalar params must NOT come out of get_data_arr");
8261 }
8262
8263 #[test]
8264 fn before_complete_snapshots_oldmenucmp() {
8265 let _g = crate::test_util::global_state_lock();
8266 // c:463 — `oldmenucmp = menucmp;`
8267 let _g = zle_test_setup();
8268 MENUCMP.store(7, Ordering::Relaxed);
8269 OLDMENUCMP.store(0, Ordering::Relaxed);
8270 let mut lst = 0;
8271 let _ = before_complete(&mut lst);
8272 assert_eq!(OLDMENUCMP.load(Ordering::Relaxed), 7);
8273 // Reset for other tests.
8274 MENUCMP.store(0, Ordering::Relaxed);
8275 OLDMENUCMP.store(0, Ordering::Relaxed);
8276 }
8277
8278 #[test]
8279 fn before_complete_clears_showagain() {
8280 let _g = crate::test_util::global_state_lock();
8281 // c:467 — `showagain = 0;` always (after the validlist gate).
8282 let _g = zle_test_setup();
8283 SHOWAGAIN.store(5, Ordering::Relaxed);
8284 let mut lst = 0;
8285 let _ = before_complete(&mut lst);
8286 assert_eq!(
8287 SHOWAGAIN.load(Ordering::Relaxed),
8288 0,
8289 "SHOWAGAIN must be cleared by before_complete"
8290 );
8291 }
8292
8293 #[test]
8294 fn remsquote_default_quoting() {
8295 let _g = crate::test_util::global_state_lock();
8296 let _g = zle_test_setup();
8297 let mut s = String::from("a'\\''b");
8298 let n = remsquote(&mut s);
8299 assert_eq!(s, "a'b");
8300 assert_eq!(n, 3);
8301 }
8302
8303 #[test]
8304 fn ctokenize_dollar_substitution() {
8305 let _g = crate::test_util::global_state_lock();
8306 let _g = zle_test_setup();
8307 let out = ctokenize("$x{y}");
8308 let chars: Vec<char> = out.chars().collect();
8309 assert_eq!(chars[0], Stringg);
8310 assert_eq!(chars[1], 'x');
8311 assert_eq!(chars[2], Inbrace);
8312 assert_eq!(chars[3], 'y');
8313 assert_eq!(chars[4], Outbrace);
8314 }
8315
8316 #[test]
8317 fn get_user_var_inline_list() {
8318 let _g = crate::test_util::global_state_lock();
8319 let _g = zle_test_setup();
8320 let result = get_user_var(Some("(a b c)")).unwrap();
8321 assert_eq!(result, vec!["a", "b", "c"]);
8322 }
8323
8324 /// `matchcmp` with freshly built operand keys.
8325 fn mc(a: &Cmatch, b: &Cmatch) -> std::cmp::Ordering {
8326 use crate::tolerant_sort::MatchSortKey;
8327 matchcmp(a, &MatchSortKey::new(a), b, &MatchSortKey::new(b))
8328 }
8329
8330 #[test]
8331 fn matchcmp_str_sort_default() {
8332 let _g = crate::test_util::global_state_lock();
8333 let _g = zle_test_setup();
8334 MATCHORDER.store(CGF_MATSORT, Ordering::Relaxed);
8335 let mut a = Cmatch::default();
8336 a.str = Some("apple".into());
8337 let mut b = Cmatch::default();
8338 b.str = Some("banana".into());
8339 assert_eq!(mc(&a, &b), std::cmp::Ordering::Less);
8340 assert_eq!(mc(&b, &a), std::cmp::Ordering::Greater);
8341 assert_eq!(mc(&a, &a), std::cmp::Ordering::Equal);
8342 MATCHORDER.store(0, Ordering::Relaxed);
8343 }
8344
8345 /// `matchcmp` must collate the METAFIED form of the match strings,
8346 /// because that is what `Src/Zle/compcore.c:3194` hands `zstrcmp`:
8347 /// `(*a)->str` is a metafied match string and nothing on the path to
8348 /// the `qsort` at c:3259 unmetafies it (only `strmetasort` does, at
8349 /// c:299-315, and that is a different path serving `${(o)}`).
8350 ///
8351 /// The failure this pins was measured under `LC_ALL=en_US.UTF-8` on
8352 /// macOS, where `strcoll` returns 0 for every pair of CJK / Kana /
8353 /// Hangul strings: comparing the UNmetafied text called all four
8354 /// distinct matches EQUAL, so their list order fell out of the
8355 /// qsort's tie handling and drifted from zsh's. Metafication escapes
8356 /// bytes in `[0x83, 0xa2]` (`Src/utils.c:4195-4201`), which are dense
8357 /// inside these UTF-8 sequences, so C's `strcoll` sees a non-multibyte
8358 /// byte string and produces a total order instead.
8359 ///
8360 /// The locale is set and restored inside the test because the
8361 /// divergence is invisible under `LC_ALL=C`, where `strcoll` is
8362 /// `strcmp` and both forms agree.
8363 #[test]
8364 fn matchcmp_collates_metafied_form_so_distinct_matches_never_tie() {
8365 let _g = crate::test_util::global_state_lock();
8366 let _g = zle_test_setup();
8367 MATCHORDER.store(CGF_MATSORT, Ordering::Relaxed);
8368
8369 let saved: Option<std::ffi::CString> = unsafe {
8370 let p = libc::setlocale(libc::LC_ALL, std::ptr::null());
8371 if p.is_null() {
8372 None
8373 } else {
8374 Some(std::ffi::CStr::from_ptr(p).to_owned())
8375 }
8376 };
8377 let have_utf8 =
8378 unsafe { !libc::setlocale(libc::LC_ALL, c"en_US.UTF-8".as_ptr()).is_null() };
8379
8380 let mk = |s: &str| {
8381 let mut m = Cmatch::default();
8382 m.str = Some(s.into());
8383 m
8384 };
8385 // The corpus from the bug report, in the order `compadd` received it.
8386 let words = [
8387 "日本語",
8388 "中文字",
8389 "한국어",
8390 "ascii",
8391 "あかさたなはまやらわ",
8392 ];
8393 let cms: Vec<Cmatch> = words.iter().map(|w| mk(w)).collect();
8394
8395 let mut failures: Vec<String> = Vec::new();
8396 for i in 0..cms.len() {
8397 for j in 0..cms.len() {
8398 if i == j {
8399 continue;
8400 }
8401 // Distinct matches must never compare equal: an Equal here
8402 // leaves the listing order to the sort's tie handling, which
8403 // is exactly the nondeterminism zsh does not have.
8404 if mc(&cms[i], &cms[j]) == std::cmp::Ordering::Equal {
8405 failures.push(format!("{} vs {}", words[i], words[j]));
8406 }
8407 // Antisymmetry, which a byte/metafied total order guarantees.
8408 assert_eq!(
8409 mc(&cms[i], &cms[j]),
8410 mc(&cms[j], &cms[i]).reverse(),
8411 "matchcmp not antisymmetric for {} vs {}",
8412 words[i],
8413 words[j]
8414 );
8415 }
8416 }
8417
8418 // ASCII is the overwhelmingly common case and metafication is the
8419 // identity on it (`imeta` covers only NUL and [0x83, 0xa2]), so the
8420 // locale-collated ordering that put `alpha.txt` before `README.md`
8421 // must survive untouched.
8422 assert_eq!(
8423 mc(&mk("alpha.txt"), &mk("README.md")),
8424 crate::ported::sort::zstrcmp(
8425 "alpha.txt",
8426 "README.md",
8427 crate::ported::zsh_h::SORTIT_IGNORING_BACKSLASHES as u32,
8428 ),
8429 "metafication must not change ASCII match ordering",
8430 );
8431
8432 unsafe {
8433 match saved {
8434 Some(ref s) => libc::setlocale(libc::LC_ALL, s.as_ptr()),
8435 None => libc::setlocale(libc::LC_ALL, c"C".as_ptr()),
8436 };
8437 }
8438 MATCHORDER.store(0, Ordering::Relaxed);
8439
8440 assert!(
8441 failures.is_empty(),
8442 "matchcmp reported distinct non-ASCII matches as EQUAL \
8443 (en_US.UTF-8 available: {have_utf8}): {failures:?}",
8444 );
8445 }
8446
8447 #[test]
8448 fn dupmatch_clones_strings_and_truncates_braces() {
8449 let _g = crate::test_util::global_state_lock();
8450 let _g = zle_test_setup();
8451 // C body c:3370: deep-copy strings, truncate brpl/brsl to nbeg/nend.
8452 let mut src = Cmatch::default();
8453 src.str = Some("foo".into());
8454 src.ipre = Some("ipre".into());
8455 src.flags = 7;
8456 src.brpl = vec![10, 20, 30, 40];
8457 src.brsl = vec![5, 6, 7];
8458 src.qipl = 1;
8459 src.qisl = 2;
8460 src.mode = 0o755;
8461 src.modec = 'd';
8462
8463 let r = dupmatch(&src, 2, 1);
8464 assert_eq!(r.str.as_deref(), Some("foo"));
8465 assert_eq!(r.ipre.as_deref(), Some("ipre"));
8466 assert_eq!(r.flags, 7);
8467 assert_eq!(r.brpl, vec![10, 20]); // truncated to nbeg=2
8468 assert_eq!(r.brsl, vec![5]); // truncated to nend=1
8469 assert_eq!(r.qipl, 1);
8470 assert_eq!(r.qisl, 2);
8471 assert_eq!(r.mode, 0o755);
8472 assert_eq!(r.modec, 'd');
8473 }
8474
8475 #[test]
8476 fn dupmatch_empty_braces_stay_empty() {
8477 let _g = crate::test_util::global_state_lock();
8478 let _g = zle_test_setup();
8479 // C body c:3395/3404: NULL brpl/brsl stay NULL regardless of nbeg/nend.
8480 let src = Cmatch::default();
8481 let r = dupmatch(&src, 5, 5);
8482 assert!(r.brpl.is_empty());
8483 assert!(r.brsl.is_empty());
8484 }
8485
8486 #[test]
8487 fn makearray_sorted_and_deduped() {
8488 let _g = crate::test_util::global_state_lock();
8489 let _g = zle_test_setup();
8490 // c:3262-3291: sort + dedup with matcheq. Same str + nil disp =>
8491 // collapses into one entry with CMF_FMULT set on the survivor.
8492 let mut a = Cmatch::default();
8493 a.str = Some("z".into());
8494 let mut b = Cmatch::default();
8495 b.str = Some("a".into());
8496 let mut c = Cmatch::default();
8497 c.str = Some("a".into());
8498 let (arr, n, _nl, _ll) = makearray(&mut vec![a, b, c], CGF_MATSORT);
8499 // Two distinct visible strings after dedup ("a", "z").
8500 assert_eq!(arr.len(), 2);
8501 assert_eq!(n, 2);
8502 assert_eq!(arr[0].str.as_deref(), Some("a"));
8503 assert_eq!(arr[1].str.as_deref(), Some("z"));
8504 }
8505
8506 #[test]
8507 fn makearray_nosort_unchanged_order() {
8508 let _g = crate::test_util::global_state_lock();
8509 let _g = zle_test_setup();
8510 // c:3300: CGF_NOSORT branch; with no UNIQ flags, order preserved.
8511 let mut a = Cmatch::default();
8512 a.str = Some("z".into());
8513 let mut b = Cmatch::default();
8514 b.str = Some("a".into());
8515 let (arr, n, _, _) = makearray(&mut vec![a, b], CGF_NOSORT | CGF_UNIQALL);
8516 // UNIQALL active so no dedup pass runs.
8517 assert_eq!(n, 2);
8518 assert_eq!(arr[0].str.as_deref(), Some("z"));
8519 assert_eq!(arr[1].str.as_deref(), Some("a"));
8520 }
8521
8522 #[test]
8523 fn makearray_flag_writes_reach_the_live_list() {
8524 let _g = crate::test_util::global_state_lock();
8525 let _g = zle_test_setup();
8526 // c:3230-3236 + c:3274-3280 — `rp` aliases the LinkList's matches, so
8527 // `(bp[1])->flags |= CMF_MULT` marks the LIVE match; C never empties
8528 // `g->lmatches`, so the mark is still there on the next
8529 // `permmatches`. Two matches with the same `str` but different `ppre`
8530 // are NOT matcheq (c:3210 compares ppre) yet DISPLAY the same string
8531 // (c:3274-3275 compares only `str`), so the first takes CMF_FMULT and
8532 // the second CMF_MULT — and neither is removed.
8533 let mut a = Cmatch::default();
8534 a.str = Some("cfg".into());
8535 a.ppre = Some("bin/".into());
8536 let mut b = Cmatch::default();
8537 b.str = Some("cfg".into());
8538 b.ppre = Some("sbin/".into());
8539 let mut live = vec![a, b];
8540
8541 let (arr, n, nl, _ll) = makearray(&mut live, CGF_MATSORT);
8542 assert_eq!(n, 2, "not matcheq (ppre differs), so both stay in mcount");
8543 assert_eq!(nl, 1, "the CMF_MULT one drops out of the LISTING only");
8544 assert_eq!(arr.len(), 2);
8545 // The writes landed on the live list, not on a detached copy.
8546 assert_ne!(live[0].flags & CMF_FMULT, 0, "c:3280 on the live match");
8547 assert_ne!(live[1].flags & CMF_MULT, 0, "c:3276 on the live match");
8548
8549 // A second permmatches pass over the SAME live list (every
8550 // `_description` block reads `$compstate[nmatches]`, which is
8551 // `permmatches(0)` — `Src/Zle/complete.c:1413`) sees the marks
8552 // already set and agrees with the first.
8553 let (arr2, n2, nl2, _) = makearray(&mut live, CGF_MATSORT);
8554 assert_eq!((arr2.len(), n2, nl2), (arr.len(), n, nl));
8555 }
8556
8557 #[test]
8558 fn makearray_nosort_deletes_every_dupe_of_a_run() {
8559 let _g = crate::test_util::global_state_lock();
8560 let _g = zle_test_setup();
8561 // c:3305-3307 — in the CGF_NOSORT dedup-all branch C marks
8562 // `bp[0]->flags = CMF_DELETE` on the SORTED-array element, which
8563 // aliases a distinct live match on each pair. The old port searched
8564 // the detached copy back to the first `matcheq` element, so a run of
8565 // three identical matches marked the same element twice and dropped
8566 // only one of the two dupes.
8567 let mk = |s: &str| {
8568 let mut m = Cmatch::default();
8569 m.str = Some(s.into());
8570 m
8571 };
8572 let mut live = vec![mk("dup"), mk("dup"), mk("dup"), mk("other")];
8573 let (arr, n, _nl, _ll) = makearray(&mut live, CGF_NOSORT);
8574 assert_eq!(n, 2, "both dupes removed, `dup` + `other` remain");
8575 assert_eq!(arr.len(), 2);
8576 assert_eq!(live[1].flags & CMF_DELETE, CMF_DELETE, "c:3306");
8577 assert_eq!(live[2].flags & CMF_DELETE, CMF_DELETE, "c:3306");
8578 }
8579
8580 #[test]
8581 fn makearray_strings_dedup_consecutive() {
8582 let _g = crate::test_util::global_state_lock();
8583 let _g = zle_test_setup();
8584 // c:3239 path: sort + drop adjacent duplicates.
8585 let (arr, n) = makearray_strings(vec!["b".into(), "a".into(), "a".into(), "c".into()], 1);
8586 assert_eq!(n, 3);
8587 assert_eq!(arr, vec!["a", "b", "c"]);
8588 }
8589
8590 #[test]
8591 fn check_param_no_dollar_returns_none() {
8592 let _g = crate::test_util::global_state_lock();
8593 let _g = zle_test_setup();
8594 // c:1316: no `$` in string → return None.
8595 OFFS.store(2, Ordering::Relaxed);
8596 assert_eq!(check_param("abc", false, false, false), None);
8597 }
8598
8599 #[test]
8600 fn check_param_simple_dollar_var_at_cursor() {
8601 let _g = crate::test_util::global_state_lock();
8602 let _g = zle_test_setup();
8603 // c:1259-1311: `$FOO` with cursor inside the name → return b.
8604 // OFFS is a byte offset into the UTF-8 word: one past the token.
8605 OFFS.store(Stringg.len_utf8() as i32 + 1, Ordering::Relaxed);
8606 let s = format!("{}FOO", Stringg);
8607 let r = check_param(&s, false, true, false);
8608 assert!(r.is_some(), "expected Some(b) inside $FOO");
8609 }
8610
8611 /// c:1141 — a live `$` is the `String` token; an escaped `\$` reaches
8612 /// `check_param` as a plain 0x24, its `Bnull` chucked by the line-cleanup
8613 /// loop (zle_tricky.c:1881/1920). So `echo \$PATH` has NO parameter
8614 /// context while `echo $PATH` has one. Accepting the literal made the two
8615 /// indistinguishable: `\$PATH` published `PREFIX=PATH` / `IPREFIX=\$`
8616 /// where zsh publishes the whole `\$PATH` as the prefix.
8617 #[test]
8618 fn check_param_escaped_dollar_is_not_a_parameter() {
8619 let _g = crate::test_util::global_state_lock();
8620 let _g = zle_test_setup();
8621
8622 // `echo \$PATH` — the word as check_param sees it: literal `$`.
8623 let escaped = "$PATH";
8624 OFFS.store(escaped.len() as i32, Ordering::Relaxed);
8625 assert_eq!(
8626 check_param(escaped, false, true, false),
8627 None,
8628 "a bare 0x24 means the user escaped it"
8629 );
8630
8631 // `echo $PATH` — the same five visible chars, but a `String` token.
8632 let live = format!("{Stringg}PATH");
8633 OFFS.store(live.len() as i32, Ordering::Relaxed);
8634 assert_eq!(
8635 check_param(&live, false, true, false),
8636 Some(Stringg.len_utf8()),
8637 "the String token opens a parameter name at b = p + 1"
8638 );
8639 }
8640
8641 /// c:1226-1229 — the end-of-name test lists the `Quest`/`Star` TOKENS but
8642 /// only the literal `-`/`!`. `$-` lexes to `String` + `Dash`, which
8643 /// matches neither, so `e` never leaves `b`, the c:1254 cursor-inside-name
8644 /// test fails and C reports no parameter context at all. Reading the
8645 /// untokenized `-` as a one-char name instead split the word and dropped
8646 /// the `$` from what `_expand` echoed back.
8647 #[test]
8648 fn check_param_dollar_dash_is_not_a_one_char_name() {
8649 let _g = crate::test_util::global_state_lock();
8650 let _g = zle_test_setup();
8651 let dash = crate::ported::zsh_h::Dash;
8652 let s = format!("{Stringg}{dash}");
8653 OFFS.store(s.len() as i32, Ordering::Relaxed);
8654 assert_eq!(check_param(&s, false, true, false), None);
8655 }
8656
8657 /// c:1189-1202 + c:1297 — `${(k)<cursor>` is a brace parameter: the
8658 /// `(k)` flag group is skipped and `ispar` ends at 2, which is what
8659 /// `compcontext_for` turns into `brace_parameter`.
8660 ///
8661 /// Exercises the `untok = true` arm — a caller holding only the
8662 /// untokenized word, where the flag group arrives as literal `(`/`)`.
8663 /// Handing those to `skipparens(Inpar, Outpar, …)` returns -1 ("wrong
8664 /// opening char"), `b` never moved past the flags, the name scan hit `(`
8665 /// and bailed — `ispar` stayed 0, the context stayed `command`, and
8666 /// `echo ${(k)<TAB>` produced nothing at all.
8667 #[test]
8668 fn check_param_brace_with_flag_group_sets_ispar_two() {
8669 let _g = crate::test_util::global_state_lock();
8670 let _g = zle_test_setup();
8671 ispar.store(0, Ordering::Relaxed);
8672 // `${(k)` with the cursor just past the `)`.
8673 let s = "${(k)";
8674 OFFS.store(s.len() as i32, Ordering::Relaxed);
8675 let r = check_param(s, false, false, true);
8676 assert!(r.is_some(), "cursor sits at the (empty) parameter name");
8677 assert_eq!(
8678 ispar.load(Ordering::Relaxed),
8679 2,
8680 "a flag group must still leave a brace-parameter context"
8681 );
8682 }
8683
8684 #[test]
8685 fn callcompfunc_empty_fn_no_panic() {
8686 let _g = crate::test_util::global_state_lock();
8687 let _g = zle_test_setup();
8688 // c:552: getshfunc(NULL) early-return.
8689 callcompfunc("anything", "");
8690 }
8691
8692 #[test]
8693 fn callcompfunc_sets_compstate_context() {
8694 let _g = crate::test_util::global_state_lock();
8695 let _g = zle_test_setup();
8696 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8697 // c:619: context selection — verified via the pure
8698 // compcontext_for helper (callcompfunc calls it and writes
8699 // to paramtab via setsparam, but paramtab read-back in a
8700 // unit-test context without a live VM is unreliable).
8701 ispar.store(0, Ordering::Relaxed);
8702 linwhat.store(IN_PAR_LW, Ordering::Relaxed);
8703 assert_eq!(compcontext_for("foo"), "assign_parameter");
8704 // Body executes without panicking against the real paramtab.
8705 callcompfunc("foo", "_test_fn");
8706 }
8707
8708 /// c:complete.c:1235-1295 — `$PREFIX`/`$SUFFIX`/`$IPREFIX`/`$ISUFFIX`
8709 /// and the `compprefix`/`compsuffix`/`compiprefix`/`compisuffix`
8710 /// globals are ONE storage in C, so `callcompfunc`'s per-call publish
8711 /// of the cursor word split resets BOTH.
8712 ///
8713 /// Regression for `PATH=/usr/bin:<TAB>`: `expand-or-complete` calls
8714 /// the completion function twice per TAB and `_path_files` leaves its
8715 /// last path component in `$PREFIX`. Because only the param was
8716 /// republished, the second pass's `compset -P '*:'` (which reads the
8717 /// GLOBAL) matched the stale `bin:` instead of the live `/usr/bin:` —
8718 /// `$IPREFIX` came out `bin:` and the rebuilt line lost `/usr/`.
8719 #[test]
8720 fn callcompfunc_republishes_word_split_onto_comp_globals() {
8721 let _g = crate::test_util::global_state_lock();
8722 let _g = zle_test_setup();
8723 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8724 // Leftovers from the previous completion pass.
8725 *COMPPREFIX
8726 .get_or_init(|| Mutex::new(String::new()))
8727 .lock()
8728 .unwrap() = "bin:".to_string();
8729 *COMPIPREFIX
8730 .get_or_init(|| Mutex::new(String::new()))
8731 .lock()
8732 .unwrap() = "PATH=/usr/".to_string();
8733
8734 // Cursor at the end of the word, as get_comp_string leaves it.
8735 OFFS.store("/usr/bin:".chars().count() as i32, Ordering::Relaxed);
8736 callcompfunc("/usr/bin:", "_test_fn");
8737
8738 assert_eq!(
8739 COMPPREFIX
8740 .get_or_init(|| Mutex::new(String::new()))
8741 .lock()
8742 .unwrap()
8743 .clone(),
8744 "/usr/bin:",
8745 "compprefix must track the freshly published $PREFIX"
8746 );
8747 assert_eq!(
8748 COMPIPREFIX
8749 .get_or_init(|| Mutex::new(String::new()))
8750 .lock()
8751 .unwrap()
8752 .clone(),
8753 "",
8754 "compiprefix must be reset with $IPREFIX, not carried over"
8755 );
8756 }
8757
8758 /// c:compcore.c:820 — `makezleparams(1)` publishes the ZLE special
8759 /// params for the completion function, and c:839's `endparamscope`
8760 /// takes them away again on the way out.
8761 ///
8762 /// The publish was missing entirely, so every completer saw
8763 /// `$BUFFER`/`$CURSOR`/`$HISTNO`/`$WIDGET`/`$KEYS`/`$LBUFFER`/
8764 /// `$RBUFFER`/`$BUFFERLINES`/`$PENDING` as the empty string.
8765 ///
8766 /// Both halves are pinned with ONE observable: seed `BUFFER` with a
8767 /// sentinel at the enclosing scope first.
8768 /// * publish missing → the sentinel survives (`Some(sentinel)`),
8769 /// * publish present but teardown missing → the live line survives,
8770 /// * both present → the name is gone entirely.
8771 /// Only the last is C behaviour.
8772 #[test]
8773 fn callcompfunc_publishes_and_tears_down_zle_params() {
8774 let _g = crate::test_util::global_state_lock();
8775 let _g = zle_test_setup();
8776 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8777
8778 // `$FUNCNEST` is unset in a bare unit-test paramtab, and
8779 // `getiparam` reports 0 for unset — which trips doshfunc's
8780 // c:6000 guard (`funcstacksz >= zsh_funcnest`) on the very first
8781 // frame and returns BEFORE c:839's endparamscope. Give it the
8782 // shell's real default so the scope actually unwinds.
8783 let funcnest_save = crate::ported::params::getiparam("FUNCNEST");
8784 let _ = crate::ported::params::setiparam("FUNCNEST", 500);
8785
8786 const SENTINEL: &str = "@@not-published@@";
8787 for name in ["BUFFER", "WIDGET", "LBUFFER"] {
8788 let _ = crate::ported::params::setsparam(name, SENTINEL);
8789 }
8790 // A non-empty editor line so the publish is distinguishable from
8791 // "published an empty string".
8792 *crate::ported::zle::zle_main::ZLELINE.lock().unwrap() = "fc -".chars().collect();
8793 crate::ported::zle::zle_main::ZLELL.store(4, Ordering::SeqCst);
8794 crate::ported::zle::zle_main::ZLECS.store(4, Ordering::SeqCst);
8795
8796 OFFS.store(1, Ordering::Relaxed);
8797 callcompfunc("-", "_test_fn");
8798 let _ = crate::ported::params::setiparam("FUNCNEST", funcnest_save);
8799
8800 for name in ["BUFFER", "WIDGET", "LBUFFER"] {
8801 let after = crate::ported::params::getsparam(name);
8802 assert_ne!(
8803 after.as_deref(),
8804 Some(SENTINEL),
8805 "${name} still holds the pre-call sentinel — c:820 makezleparams(1) \
8806 never ran, so the completion function saw no ZLE parameters"
8807 );
8808 assert_eq!(
8809 after, None,
8810 "${name} outlived the completion scope — c:839 endparamscope must \
8811 unset every PM_LOCAL zleparam, or the name leaks into the \
8812 interactive shell after the first TAB"
8813 );
8814 }
8815 }
8816
8817 /// Test-only serializer for tests that mutate file-scope globals.
8818 ///
8819 /// Every acquisition is poison-tolerant
8820 /// (`.unwrap_or_else(|e| e.into_inner())`, the same recovery
8821 /// `zle_test_setup()` and `test_util::global_state_lock()` use). A
8822 /// plain `.unwrap()` turned ONE failing test into sixteen: the first
8823 /// panic while the guard was held poisoned the mutex, and every later
8824 /// compcore test died on `PoisonError` before running a single
8825 /// assertion, hiding whether it would have passed. Recovery here does
8826 /// not weaken any test — each one still resets the globals it touches
8827 /// via `zle_test_setup()`.
8828 static GLOBAL_MUT_LOCK: Mutex<()> = Mutex::new(());
8829
8830 #[test]
8831 fn compcontext_for_routes_ispar_first() {
8832 let _g = crate::test_util::global_state_lock();
8833 let _g = zle_test_setup();
8834 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8835 // c:583/593 — the subscript arms key off `insubscr`; pin it off so
8836 // the non-subscript routing below is deterministic (`zle_reset`
8837 // does not clear it).
8838 crate::ported::zle::zle_tricky::INSUBSCR.store(0, Ordering::Relaxed);
8839 crate::ported::zle::zle_tricky::LINCMD.store(0, Ordering::Relaxed);
8840 // c:598/606/623 — the redirect / array_value / default-command
8841 // arms read three more file-scope globals; pin them so the
8842 // routing below is deterministic regardless of test order.
8843 crate::ported::zle::zle_tricky::LINREDIR.store(0, Ordering::Relaxed);
8844 crate::ported::zle::zle_tricky::LINARR.store(0, Ordering::Relaxed);
8845 *crate::ported::zle::zle_tricky::CMDSTR.lock().unwrap() = Some("ls".into());
8846 ispar.store(2, Ordering::Relaxed);
8847 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
8848 assert_eq!(compcontext_for("x"), "brace_parameter");
8849 ispar.store(1, Ordering::Relaxed);
8850 assert_eq!(compcontext_for("x"), "parameter");
8851 ispar.store(0, Ordering::Relaxed);
8852 linwhat.store(IN_MATH_LW, Ordering::Relaxed);
8853 assert_eq!(compcontext_for("x"), "math");
8854 linwhat.store(IN_COND_LW, Ordering::Relaxed);
8855 assert_eq!(compcontext_for("x"), "condition");
8856 linwhat.store(IN_ENV_LW, Ordering::Relaxed);
8857 assert_eq!(compcontext_for("x"), "value");
8858 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
8859 assert_eq!(compcontext_for("x"), "command");
8860 *crate::ported::zle::zle_tricky::CMDSTR.lock().unwrap() = None;
8861 }
8862
8863 /// c:598-602 / c:605-606 / c:622-630 — the three arms that were
8864 /// missing from this port entirely. `linredir` outranks the
8865 /// `switch (linwhat)` but is outranked by `lincmd`; `linarr`
8866 /// upgrades IN_ENV from `value` to `array_value`; and the default
8867 /// arm degrades to `value` when no command word was parsed.
8868 #[test]
8869 fn compcontext_for_redirect_array_value_and_cmdstr_arms() {
8870 let _g = crate::test_util::global_state_lock();
8871 let _g = zle_test_setup();
8872 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8873 use crate::ported::zle::zle_tricky::{CMDSTR, INSUBSCR, LINARR, LINCMD, LINREDIR};
8874 ispar.store(0, Ordering::Relaxed);
8875 INSUBSCR.store(0, Ordering::Relaxed);
8876 LINCMD.store(0, Ordering::Relaxed);
8877 LINARR.store(0, Ordering::Relaxed);
8878 *CMDSTR.lock().unwrap() = Some("echo".into());
8879
8880 // c:598-599 — `echo x > /tm<TAB>`.
8881 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
8882 LINREDIR.store(1, Ordering::Relaxed);
8883 assert_eq!(compcontext_for("x"), "redirect");
8884 // c:592 — but a command-position word still wins over it.
8885 LINCMD.store(1, Ordering::Relaxed);
8886 assert_eq!(compcontext_for("x"), "command");
8887 LINCMD.store(0, Ordering::Relaxed);
8888 LINREDIR.store(0, Ordering::Relaxed);
8889
8890 // c:605-606 — IN_ENV splits on `linarr`.
8891 linwhat.store(IN_ENV_LW, Ordering::Relaxed);
8892 assert_eq!(compcontext_for("x"), "value");
8893 LINARR.store(1, Ordering::Relaxed);
8894 assert_eq!(compcontext_for("x"), "array_value");
8895 LINARR.store(0, Ordering::Relaxed);
8896
8897 // c:623-626 — the default arm needs a command word to say
8898 // "command"; without one it is a `value`.
8899 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
8900 assert_eq!(compcontext_for("x"), "command");
8901 *CMDSTR.lock().unwrap() = None;
8902 assert_eq!(compcontext_for("x"), "value");
8903 }
8904
8905 /// c:582-597 — with `insubscr` set, BOTH the math arm and the command
8906 /// arm select the `subscript` context (so `_complete` dispatches
8907 /// `-subscript-`). Regression for `echo $fpath[<TAB>`, which routed to
8908 /// `math` and never reached `_subscript`.
8909 #[test]
8910 fn compcontext_for_routes_subscript_when_insubscr_set() {
8911 let _g = crate::test_util::global_state_lock();
8912 let _g = zle_test_setup();
8913 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8914 use crate::ported::zle::zle_tricky::{CMDSTR, INSUBSCR, LINCMD, LINREDIR};
8915 ispar.store(0, Ordering::Relaxed);
8916 LINCMD.store(0, Ordering::Relaxed);
8917 LINREDIR.store(0, Ordering::Relaxed);
8918 *CMDSTR.lock().unwrap() = Some("ls".into());
8919
8920 // c:584 — math context inside an unclosed subscript.
8921 linwhat.store(IN_MATH_LW, Ordering::Relaxed);
8922 INSUBSCR.store(1, Ordering::Relaxed);
8923 assert_eq!(compcontext_for("x"), "subscript");
8924 // c:590 — a real math expression stays `math`.
8925 INSUBSCR.store(0, Ordering::Relaxed);
8926 assert_eq!(compcontext_for("x"), "math");
8927
8928 // c:593-597 — `[` in command position is a subscript, not a command.
8929 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
8930 LINCMD.store(1, Ordering::Relaxed);
8931 INSUBSCR.store(1, Ordering::Relaxed);
8932 assert_eq!(compcontext_for("x"), "subscript");
8933 INSUBSCR.store(0, Ordering::Relaxed);
8934 assert_eq!(compcontext_for("x"), "command");
8935 LINCMD.store(0, Ordering::Relaxed);
8936 *CMDSTR.lock().unwrap() = None;
8937 }
8938
8939 #[test]
8940 fn addmatches_empty_argv_early_return() {
8941 let _g = crate::test_util::global_state_lock();
8942 let _g = zle_test_setup();
8943 // c:2138-2139: empty argv + dummies==0 + no CAF_ALL → return 1.
8944 let mut dat = Cadata::default();
8945 dat.dummies = 0;
8946 dat.aflags = 0;
8947 assert_eq!(addmatches(&mut dat, &[]), 1);
8948 }
8949
8950 /// c:2252-2253 — `lipre`/`lisuf` are read from `compiprefix`/`compisuffix`
8951 /// ONLY inside `if (dat->aflags & CAF_MATCH)`. `compadd -U` clears
8952 /// CAF_MATCH, so a `-U` match carries NO ignored prefix and replaces the
8953 /// word on the line whole; c:2278-2282 then has nothing to prepend.
8954 ///
8955 /// Reading them unconditionally gave every `-U` match the live `$IPREFIX`.
8956 /// Measured on a PTY (`scripts/comptab_screen.py`) with
8957 /// `fpath=(/opt/homebrew/Cellar/zsh/5.9.2/share/zsh/functions)`,
8958 /// `zstyle ':completion:*' completer _expand` and
8959 /// `hash -d zzqaa=/usr/share/zsh` — both shells report
8960 /// `IPREFIX=<~> PREFIX=<zzqaa>`, and the match set is identical:
8961 ///
8962 /// ```text
8963 /// buffer `ls ~zzqaa` + TAB
8964 /// zsh ls /usr/share/zsh/
8965 /// zshrs ls ~/usr/share/zsh/ <- before
8966 /// zshrs ls /usr/share/zsh/ <- after
8967 /// ```
8968 #[test]
8969 fn addmatches_u_flag_match_carries_no_ignored_prefix() {
8970 let _g = crate::test_util::global_state_lock();
8971 let _g = zle_test_setup();
8972 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
8973
8974 // The `~` `_expand` leaves in `$IPREFIX` when the word is `~zzqaa`.
8975 // Seeded on the GLOBAL, not the param: the param->global refresh above
8976 // is gated on INCOMPFUNC, which is 0 here.
8977 *COMPIPREFIX
8978 .get_or_init(|| Mutex::new(String::new()))
8979 .lock()
8980 .unwrap() = "~".to_string();
8981
8982 let ipre_of_last = |aflags: i32| -> Option<String> {
8983 crate::comp_match_handles::matches_arc().lock().unwrap().clear();
8984 let mut dat = Cadata::default();
8985 dat.dummies = -1;
8986 dat.aflags = aflags;
8987 let _ = addmatches(&mut dat, &["/usr/share/zsh".to_string()]);
8988 let arc = crate::comp_match_handles::matches_arc();
8989 let m = arc.lock().unwrap();
8990 m.last().and_then(|cm| cm.ipre.clone())
8991 };
8992
8993 // `compadd -U`: no CAF_MATCH, so c:2253 never runs and the match has
8994 // no ipre to re-insert in front of the expansion.
8995 assert_eq!(
8996 ipre_of_last(0),
8997 None,
8998 "c:2252 — a non-CAF_MATCH (`-U`) match must carry no ignored prefix"
8999 );
9000
9001 // A matching compadd still picks `$IPREFIX` up, which is what keeps
9002 // `compset -P`'d completers re-inserting the part they consumed.
9003 assert_eq!(
9004 ipre_of_last(CAF_MATCH),
9005 Some("~".to_string()),
9006 "c:2253 — under CAF_MATCH the ignored prefix still reaches the match"
9007 );
9008
9009 *COMPIPREFIX
9010 .get_or_init(|| Mutex::new(String::new()))
9011 .lock()
9012 .unwrap() = String::new();
9013 }
9014
9015 #[test]
9016 fn addmatches_appends_argv_to_default_group() {
9017 let _g = crate::test_util::global_state_lock();
9018 let _g = zle_test_setup();
9019 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9020 // c:2200 simplified body: each argv entry → addmatch into "default" group.
9021 amatches
9022 .get_or_init(|| Mutex::new(Vec::new()))
9023 .lock()
9024 .unwrap()
9025 .clear();
9026 crate::comp_match_handles::matches_arc()
9027 .lock()
9028 .unwrap()
9029 .clear();
9030 let mut dat = Cadata::default();
9031 dat.dummies = -1;
9032 let _ = addmatches(&mut dat, &["a".into(), "b".into()]);
9033 let n = crate::comp_match_handles::matches_arc()
9034 .lock()
9035 .unwrap()
9036 .len();
9037 assert!(n >= 2);
9038 }
9039
9040 /// c:2094 `Cmlist oms = mstack` / c:2623 `mstack = oms` — a `compadd -M`
9041 /// matcher lives only for the duration of that one call. When the port
9042 /// leaked it, `_arguments`' option matcher `r:|[_-]=* r:|=*` stayed live
9043 /// for the `_hosts` compadd that followed and a typed `-` matched every
9044 /// host CONTAINING a `-`, so `ssh -<TAB>` listed
9045 /// `ec2-…compute-1.amazonaws.com` among the options.
9046 #[test]
9047 fn addmatches_restores_mstack_after_the_call() {
9048 let _g = crate::test_util::global_state_lock();
9049 let _g = zle_test_setup();
9050 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9051 fn mstack_depth() -> usize {
9052 let g = mstack.get_or_init(|| Mutex::new(None)).lock().unwrap();
9053 let mut n = 0;
9054 let mut cur = g.as_deref();
9055 while let Some(link) = cur {
9056 n += 1;
9057 cur = link.next.as_deref();
9058 }
9059 n
9060 }
9061 let before = mstack_depth();
9062 let mut dat = Cadata::default();
9063 dat.dummies = -1;
9064 dat.match_ = crate::ported::zle::complete::parse_cmatcher("compadd", "r:|[_-]=* r:|=*");
9065 assert!(dat.match_.is_some(), "matcher spec must parse");
9066 let _ = addmatches(&mut dat, &["a-b".into()]);
9067 assert_eq!(mstack_depth(), before, "matcher leaked past its compadd");
9068 }
9069
9070 #[test]
9071 fn add_match_data_returns_populated_cmatch() {
9072 let _g = crate::test_util::global_state_lock();
9073 let _g = zle_test_setup();
9074 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9075 // c:3052-3067: cm.str/orig/pre/suf populated; mnum bumps by 1.
9076 crate::comp_match_handles::matches_arc()
9077 .lock()
9078 .unwrap()
9079 .clear();
9080 let before = mnum.load(Ordering::Relaxed);
9081 let cm = add_match_data(
9082 0,
9083 "match",
9084 "match-orig",
9085 None,
9086 "ipre",
9087 "ripre",
9088 "isuf",
9089 Some("pre"),
9090 "prpre",
9091 "ppre",
9092 None,
9093 "psuf",
9094 None,
9095 Some("suf"),
9096 0,
9097 0,
9098 );
9099 let cm = cm.expect("no unfinished brace is recorded, so c:2999 must not reject");
9100 assert_eq!(cm.str.as_deref(), Some("match"));
9101 assert_eq!(cm.orig.as_deref(), Some("match-orig"));
9102 assert_eq!(cm.pre.as_deref(), Some("pre"));
9103 assert_eq!(cm.suf.as_deref(), Some("suf"));
9104 assert_eq!(mnum.load(Ordering::Relaxed), before + 1);
9105 }
9106
9107 #[test]
9108 fn add_match_data_exact_records_into_ainfo() {
9109 let _g = crate::test_util::global_state_lock();
9110 let _g = zle_test_setup();
9111 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9112 // c:3037-3058: exact != 0 writes `ai->exact = useexact` and
9113 // `ai->exactm = cm`. The test sets useexact=1 to exercise the
9114 // accept-exact path.
9115 let saved_useexact = useexact.load(Ordering::Relaxed);
9116 useexact.store(1, Ordering::Relaxed);
9117 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
9118 *g = Some(Aminfo::default());
9119 }
9120 let _ = add_match_data(
9121 0,
9122 "x",
9123 "x",
9124 None,
9125 "",
9126 "",
9127 "",
9128 Some(""),
9129 "",
9130 "",
9131 None,
9132 "",
9133 None,
9134 Some(""),
9135 0,
9136 1,
9137 );
9138 let a = ainfo.get().unwrap().lock().unwrap().clone().unwrap();
9139 useexact.store(saved_useexact, Ordering::Relaxed);
9140 assert_eq!(a.exact, 1);
9141 assert!(a.exactm.is_some());
9142 }
9143
9144 /// Faithful end-to-end exercise of `set_comp_sep` (`compset -q`): an
9145 /// ASCII argument `a b c` with the cursor at the end of the middle
9146 /// word must be re-lexed into three words, narrowed to the cursor
9147 /// word `b`, and split into qp/qs ignored prefix/suffix around it.
9148 /// Drives the real lexer (via `global_state_lock`'s `inittyptab`) and
9149 /// asserts the word split, cursor index, compprefix, and the qp/qs
9150 /// reconstruction — covering the marker/offset arithmetic
9151 /// (`swb-1-sqq+dq`, `p[soffs]` chuck) for the byte-consistent case.
9152 #[test]
9153 fn set_comp_sep_splits_ascii_word() {
9154 use crate::ported::zle::complete::{
9155 COMPCURRENT, COMPISUFFIX, COMPQIPREFIX, COMPQISUFFIX, COMPQUOTE, COMPWORDS,
9156 };
9157 let _g = crate::test_util::global_state_lock();
9158 let _g = zle_test_setup();
9159
9160 let setg = |g: &'static OnceLock<Mutex<String>>, v: &str| {
9161 *g.get_or_init(|| Mutex::new(String::new())).lock().unwrap() = v.to_string();
9162 };
9163 let getg = |g: &'static OnceLock<Mutex<String>>| -> String {
9164 g.get_or_init(|| Mutex::new(String::new()))
9165 .lock()
9166 .unwrap()
9167 .clone()
9168 };
9169
9170 // Reconstructed arg "a b c"; cursor between 'b' and the following
9171 // space: compprefix="a b" (active-prefix len 3), compsuffix=" c".
9172 setg(&COMPPREFIX, "a b");
9173 setg(&COMPSUFFIX, " c");
9174 setg(&COMPIPREFIX, "");
9175 setg(&COMPISUFFIX, "");
9176 setg(&COMPQIPREFIX, "");
9177 setg(&COMPQISUFFIX, "");
9178 setg(&COMPQSTACK, ""); // empty => qttype = QT_NONE (unquoted)
9179 setg(&COMPQUOTE, "");
9180 OFFS.store(0, Ordering::Relaxed);
9181 WB.store(0, Ordering::Relaxed);
9182 WE.store(0, Ordering::Relaxed);
9183 ZLEMETACS.store(0, Ordering::Relaxed);
9184 INSTRING.store(0, Ordering::Relaxed);
9185 INBACKT.store(0, Ordering::Relaxed);
9186
9187 // c:1721 — a real split happened (cur >= 0), so ret == 0.
9188 assert_eq!(set_comp_sep(), 0, "compset -q must split the ASCII word");
9189
9190 // c:1926-1934 — three words, cursor word 'b' with its injected
9191 // 'x' chucked back out.
9192 let words = COMPWORDS
9193 .get_or_init(|| Mutex::new(Vec::new()))
9194 .lock()
9195 .unwrap()
9196 .clone();
9197 assert_eq!(
9198 words,
9199 vec!["a".to_string(), "b".to_string(), "c".to_string()],
9200 "arg must re-lex into three words"
9201 );
9202 // c:1930 — 0-offset cur=1 -> 1-based compcurrent=2.
9203 assert_eq!(COMPCURRENT.load(Ordering::Relaxed), 2);
9204
9205 // c:1894-1906 — prefix/suffix of the cursor word (COMPLETEINWORD off).
9206 assert_eq!(getg(&COMPPREFIX), "b");
9207 assert_eq!(getg(&COMPSUFFIX), "");
9208
9209 // c:1912-1919 — qp/qs fold into compqiprefix/compqisuffix. The
9210 // just-prepended 1-char compqstack makes multiquote(...,1) a
9211 // no-op, so the ignored prefix/suffix are verbatim slices of the
9212 // arg; qip + word + qis must reconstruct "a b c".
9213 let recon = format!(
9214 "{}{}{}",
9215 getg(&COMPQIPREFIX),
9216 getg(&COMPPREFIX),
9217 getg(&COMPQISUFFIX)
9218 );
9219 assert_eq!(recon, "a b c", "qip + word + qis must reconstruct the arg");
9220
9221 // c:1926-1934 + c:complete.c:1235-1295 — in C `$words` / `$CURRENT`
9222 // ARE `compwords` / `compcurrent` (gsu-bound, one storage), so the
9223 // re-split is visible to the calling completer the moment `compset -q`
9224 // returns. zshrs's params are separate paramtab copies, so the port
9225 // has to publish them explicitly. Without that publish `_trap`
9226 // if [[ CURRENT -eq 2 ]]; then compset -q; _normal; else …
9227 // left `$words` as `(trap -)` with `$CURRENT` still 2, `_normal`
9228 // re-dispatched the command word `trap`, and `_trap` recursed until
9229 // FUNCNEST — printing `maximum nested function level reached` into
9230 // the user's edit buffer on `trap -<TAB>`.
9231 assert_eq!(
9232 crate::ported::params::getaparam("words"),
9233 Some(vec!["a".to_string(), "b".to_string(), "c".to_string()]),
9234 "compset -q must republish $words for the calling completer"
9235 );
9236 assert_eq!(
9237 crate::ported::params::getsparam("CURRENT").as_deref(),
9238 Some("2"),
9239 "compset -q must republish $CURRENT for the calling completer"
9240 );
9241 assert_eq!(
9242 crate::ported::params::getsparam("PREFIX").as_deref(),
9243 Some("b"),
9244 "compset -q must republish $PREFIX for the calling completer"
9245 );
9246 }
9247
9248 #[test]
9249 fn foredel_deletes_forward_from_zlemetacs() {
9250 let _g = crate::test_util::global_state_lock();
9251 let _g = zle_test_setup();
9252 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9253 // zle_utils.c:1105 — delete `ct` chars forward from ZLEMETACS.
9254 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
9255 *g = "abcdef".to_string();
9256 }
9257 ZLEMETACS.store(2, Ordering::Relaxed);
9258 ZLEMETALL.store(6, Ordering::Relaxed);
9259 foredel(3, CUT_RAW);
9260 let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
9261 assert_eq!(line, "abf");
9262 assert_eq!(ZLEMETALL.load(Ordering::Relaxed), 3);
9263 }
9264
9265 #[test]
9266 fn inststr_inserts_at_zlemetacs() {
9267 let _g = crate::test_util::global_state_lock();
9268 let _g = zle_test_setup();
9269 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9270 // zle_tricky.c:278 — insert at cursor.
9271 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
9272 *g = "hello".to_string();
9273 }
9274 ZLEMETACS.store(5, Ordering::Relaxed);
9275 ZLEMETALL.store(5, Ordering::Relaxed);
9276 let _ = inststr(" world");
9277 let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
9278 assert_eq!(line, "hello world");
9279 assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 11);
9280 }
9281
9282 #[test]
9283 fn metafy_and_unmetafy_roundtrip_globals() {
9284 let _g = crate::test_util::global_state_lock();
9285 let _g = zle_test_setup();
9286 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9287 // zle_tricky.c:978,995 — meta/unmeta operate on the global pair.
9288 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
9289 *g = "plain ascii".to_string();
9290 }
9291 ZLECS.store(3, Ordering::Relaxed);
9292 ZLELL.store(11, Ordering::Relaxed);
9293 metafy_line();
9294 // For ASCII input the meta form equals the raw form.
9295 assert_eq!(
9296 ZLEMETALINE.get().unwrap().lock().unwrap().clone(),
9297 "plain ascii"
9298 );
9299 assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 3);
9300 unmetafy_line();
9301 assert_eq!(
9302 ZLELINE.get().unwrap().lock().unwrap().clone(),
9303 "plain ascii"
9304 );
9305 }
9306
9307 #[test]
9308 fn selfinsert_appends_lastchar_at_zlecs() {
9309 let _g = crate::test_util::global_state_lock();
9310 let _g = zle_test_setup();
9311 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9312 // zle_misc.c:112-141 — insert one char at cursor, bump zlecs.
9313 //
9314 // `selfinsert()` (zle_misc.rs:180) writes through
9315 // `self_insert(c)` which mutates the `zle_main::ZLELINE`
9316 // (`Mutex<Vec<char>>`) plus `zle_main::ZLECS`/`ZLELL` —
9317 // NOT the `compcore::ZLELINE` (`OnceLock<Mutex<String>>`)
9318 // used by the meta/unmeta tests above. The original test
9319 // seeded the wrong buffer, so the assert kept seeing "ab"
9320 // (the compcore buffer never received the insert) while
9321 // self_insert silently appended 'c' to the zle_main buffer.
9322 //
9323 // Set up the zle_main buffer for the insert, then read back
9324 // from there. `zle_test_setup` already clears the zle_main
9325 // statics, so we start from a known zero state.
9326 {
9327 let mut g = crate::ported::zle::zle_main::ZLELINE.lock().unwrap();
9328 *g = "ab".chars().collect();
9329 }
9330 crate::ported::zle::zle_main::ZLECS.store(2, Ordering::Relaxed);
9331 crate::ported::zle::zle_main::ZLELL.store(2, Ordering::Relaxed);
9332 LASTCHAR.store(b'c' as i32, Ordering::Relaxed);
9333 // Force the wide-char re-derive path (lastchar_wide_valid=0 →
9334 // selfinsert refills it from LASTCHAR per zle_misc.c:119-122).
9335 LASTCHAR_WIDE_VALID.store(0, Ordering::Relaxed);
9336 let rv = selfinsert(&[]);
9337 assert_eq!(rv, 0);
9338 let buf: String = crate::ported::zle::zle_main::ZLELINE
9339 .lock()
9340 .unwrap()
9341 .iter()
9342 .collect();
9343 assert_eq!(buf, "abc");
9344 assert_eq!(
9345 crate::ported::zle::zle_main::ZLECS.load(Ordering::Relaxed),
9346 3,
9347 );
9348 }
9349
9350 #[test]
9351 fn minfo_clear_and_asked_zero_mutate_state() {
9352 let _g = crate::test_util::global_state_lock();
9353 let _g = zle_test_setup();
9354 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9355 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
9356 let mut cm = Cmatch::default();
9357 cm.str = Some("x".into());
9358 g.cur = Some(Box::new(cm));
9359 g.asked = 1;
9360 }
9361 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
9362 g.cur = None;
9363 }
9364 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(Menuinfo::default())).lock() {
9365 g.asked = 0;
9366 }
9367 let m = MINFO.get().unwrap().lock().unwrap().clone();
9368 assert!(m.cur.is_none());
9369 assert_eq!(m.asked, 0);
9370 }
9371
9372 #[test]
9373 fn cline_matched_stub_marks_node() {
9374 let _g = crate::test_util::global_state_lock();
9375 let _g = zle_test_setup();
9376 // compmatch.c:253 — sets CLF_MATCHED on the node chain. We
9377 // verify by running through the stub on a non-empty string
9378 // without panicking and trusting compmatch's body for the
9379 // actual flag set.
9380 cline_matched_compcore(Some("foo"));
9381 cline_matched_compcore(None);
9382 cline_matched_compcore(Some(""));
9383 }
9384
9385 #[test]
9386 fn permmatches_returns_fi_zero_when_count_present() {
9387 let _g = crate::test_util::global_state_lock();
9388 let _g = zle_test_setup();
9389 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9390 // c:3444-3447: if ainfo->count is non-zero, fi stays 0.
9391 amatches
9392 .get_or_init(|| Mutex::new(Vec::new()))
9393 .lock()
9394 .unwrap()
9395 .clear();
9396 pmatches
9397 .get_or_init(|| Mutex::new(Vec::new()))
9398 .lock()
9399 .unwrap()
9400 .clear();
9401 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
9402 *a = Some(Aminfo {
9403 count: 5,
9404 ..Default::default()
9405 });
9406 }
9407 newmatches.store(1, Ordering::Relaxed);
9408 let fi = permmatches(0);
9409 assert_eq!(fi, 0);
9410 assert_eq!(hasperm.load(Ordering::Relaxed), 1);
9411 }
9412
9413 /// c:2068 / c:3010 / c:3154 / c:3162 — `mgroup->new = 1` MUST be visible
9414 /// through the `amatches` chain, because in C `mgroup` and the `amatches`
9415 /// entry are the same `struct cmgroup` (`begcmgroup` c:3087 on reuse,
9416 /// c:3100+c:3123 on create). `permmatches` reads the flag while walking
9417 /// `amatches` (c:3452 `if (fi != ofi || !g->perm || g->new)`) and clears it
9418 /// there (c:3544).
9419 ///
9420 /// With `new_` a per-clone `i32`, the write landed only on the `mgroup`
9421 /// copy, so an OPEN group's freshly added matches were invisible: the c:3452
9422 /// test took its reuse branch and c:3536 added the group's STALE `mcount` to
9423 /// `nmatches`. `$compstate[nmatches]` is live through `get_nmatches`
9424 /// (`Src/Zle/complete.c:1411-1413`), so every completer that returns
9425 /// `[[ nm -ne compstate[nmatches] ]]` — `Completion/Unix/Type/_path_files`
9426 /// sh:895 — reported "added nothing" and the whole completer chain ran past
9427 /// the completer that should have ended it.
9428 #[test]
9429 fn mgroup_new_flag_is_shared_with_the_amatches_entry() {
9430 let _g = crate::test_util::global_state_lock();
9431 let _g = zle_test_setup();
9432 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9433 amatches
9434 .get_or_init(|| Mutex::new(Vec::new()))
9435 .lock()
9436 .unwrap()
9437 .clear();
9438
9439 begcmgroup(Some("corrections"), 0); // c:3072
9440 // c:2068 — exactly the write `addmatch` performs, through `mgroup`.
9441 mgroup
9442 .get_or_init(|| Mutex::new(None))
9443 .lock()
9444 .unwrap()
9445 .as_ref()
9446 .expect("begcmgroup must leave a current group")
9447 .new_
9448 .store(1, Ordering::Relaxed);
9449
9450 let seen = amatches
9451 .get_or_init(|| Mutex::new(Vec::new()))
9452 .lock()
9453 .unwrap()
9454 .iter()
9455 .find(|g| g.name.as_deref() == Some("corrections"))
9456 .map(|g| g.new_.load(Ordering::Relaxed));
9457 assert_eq!(
9458 seen,
9459 Some(1),
9460 "c:3452 permmatches reads `g->new` off the amatches walk, so the \
9461 mgroup write must alias it"
9462 );
9463
9464 // c:3544 — and the clear on the amatches walk must reach `mgroup` too.
9465 amatches
9466 .get_or_init(|| Mutex::new(Vec::new()))
9467 .lock()
9468 .unwrap()
9469 .iter()
9470 .for_each(|g| g.new_.store(0, Ordering::Relaxed));
9471 assert_eq!(
9472 mgroup
9473 .get_or_init(|| Mutex::new(None))
9474 .lock()
9475 .unwrap()
9476 .as_ref()
9477 .unwrap()
9478 .new_
9479 .load(Ordering::Relaxed),
9480 0
9481 );
9482 }
9483
9484 /// c:3449-3544 / c:3087 — `permmatches` walks `amatches` in place and
9485 /// `begcmgroup` re-opening an existing group aliases it. The port takes
9486 /// the groups out of `amatches` for the walk and leaves the group's sorted
9487 /// `matches` and `perm` out of `mgroup`'s copy; both must leave the
9488 /// `amatches` entry whole: a re-opened group keeps its permanent copy (so
9489 /// an unchanged group is served from it, c:3534) and new matches added
9490 /// after the re-open are sorted in with the old ones on the next read.
9491 #[test]
9492 fn permmatches_and_group_reopen_keep_the_amatches_group_whole() {
9493 let _g = crate::test_util::global_state_lock();
9494 let _g = zle_test_setup();
9495 let _g = GLOBAL_MUT_LOCK.lock().unwrap_or_else(|e| e.into_inner());
9496 amatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
9497 pmatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
9498 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
9499 *a = Some(Aminfo { count: 1, ..Default::default() });
9500 }
9501 MATCHORDER.store(0, Ordering::Relaxed);
9502 let add = |words: &[&str]| {
9503 let cell = crate::comp_match_handles::matches_arc();
9504 let mut l = cell.lock().unwrap();
9505 for w in words {
9506 let mut m = Cmatch::default();
9507 m.str = Some(w.to_string());
9508 l.push(m);
9509 }
9510 drop(l);
9511 mgroup.get_or_init(|| Mutex::new(None)).lock().unwrap()
9512 .as_ref().unwrap().new_.store(1, Ordering::Relaxed);
9513 newmatches.store(1, Ordering::Relaxed);
9514 };
9515 let pm_strs = || -> Vec<String> {
9516 pmatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap()
9517 .iter().flat_map(|g| g.matches.iter().map(|m| m.str.clone().unwrap()))
9518 .collect()
9519 };
9520
9521 begcmgroup(Some("big"), 0);
9522 add(&["delta", "alpha", "charlie"]);
9523 permmatches(0);
9524 assert_eq!(pm_strs(), ["alpha", "charlie", "delta"]);
9525
9526 // Re-open the same group: `mgroup` aliases it, `amatches` keeps both
9527 // the sorted array and the permanent copy.
9528 begcmgroup(Some("big"), 0);
9529 {
9530 let am = amatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap();
9531 let g = am.iter().find(|g| g.name.as_deref() == Some("big")).unwrap();
9532 assert_eq!(g.matches.len(), 3, "re-open must not strip the sorted array");
9533 assert_eq!(g.perm.as_ref().map(|p| p.matches.len()), Some(3));
9534 }
9535 // c:3433 — nothing new: the cached permanent copy is served.
9536 newmatches.store(1, Ordering::Relaxed);
9537 permmatches(0);
9538 assert_eq!(pm_strs(), ["alpha", "charlie", "delta"]);
9539 assert_eq!(nmatches.load(Ordering::Relaxed), 3);
9540
9541 add(&["bravo"]);
9542 permmatches(0);
9543 assert_eq!(pm_strs(), ["alpha", "bravo", "charlie", "delta"]);
9544 assert_eq!(nmatches.load(Ordering::Relaxed), 4);
9545 assert_eq!(
9546 amatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().len(),
9547 1,
9548 "the walk must put every group back"
9549 );
9550 }
9551
9552 /// c:1323 — `rembslash` removes backslash escapes by walking the
9553 /// string and dropping every `\` while keeping its successor.
9554 /// `\\\\` (literal `\\`) → `\` (single backslash); `\a` → `a`.
9555 /// A regression that drops the successor too would silently strip
9556 /// real chars from `path/to/\$file`.
9557 #[test]
9558 fn rembslash_unescapes_canonical_pairs() {
9559 let _g = crate::test_util::global_state_lock();
9560 assert_eq!(rembslash(r"\a"), "a");
9561 assert_eq!(rembslash(r"\\"), r"\");
9562 assert_eq!(rembslash(r"\$foo"), "$foo");
9563 assert_eq!(rembslash("plain"), "plain");
9564 }
9565
9566 /// c:1323 — empty input → empty output (the loop never runs).
9567 /// Catches a regression that returns " " or "\0" for empty input.
9568 #[test]
9569 fn rembslash_empty_input_returns_empty() {
9570 let _g = crate::test_util::global_state_lock();
9571 assert_eq!(rembslash(""), "");
9572 }
9573
9574 /// c:1323 — trailing lone `\` MUST silently drop (no following
9575 /// char to keep). C's pattern `if (let Some) { push }` is
9576 /// equivalent. Regression that pushes the literal `\` would break
9577 /// shell paths with trailing backslashes (rare but legal).
9578 #[test]
9579 fn rembslash_trailing_lone_backslash_drops_silently() {
9580 let _g = crate::test_util::global_state_lock();
9581 assert_eq!(rembslash(r"foo\"), "foo");
9582 }
9583
9584 /// c:1366 — `ctokenize` is the inverse of `untokenize`: escapes
9585 /// shell metacharacters into their tokenised forms used by the
9586 /// completion machinery. Plain alphanumerics pass through.
9587 #[test]
9588 fn ctokenize_passes_alphanumerics_through() {
9589 let _g = crate::test_util::global_state_lock();
9590 assert_eq!(ctokenize("foo123"), "foo123");
9591 assert_eq!(ctokenize(""), "");
9592 assert_eq!(ctokenize("path/to"), "path/to");
9593 }
9594
9595 /// c:1435 — `comp_quoting_string` returns one of the canonical
9596 /// quote strings: `'`, `"`, `$'`, or "" depending on `stype`.
9597 /// Catches a regression where the dispatch returns the wrong
9598 /// quote — completion would generate `cmd 'arg"` (mismatched).
9599 #[test]
9600 fn comp_quoting_string_dispatches_known_styles() {
9601 let _g = crate::test_util::global_state_lock();
9602 // The exact stype values are private to the completion impl,
9603 // but the function MUST return a non-panicking string for
9604 // every reasonable input. Probe a few values.
9605 for stype in 0..=8 {
9606 let _ = comp_quoting_string(stype);
9607 }
9608 }
9609
9610 /// c:1065 — `multiquote` with empty COMPQSTACK is a no-op (returns
9611 /// input unchanged). The stack is the per-completion quoting
9612 /// context; outside completion it's empty. Regression that quotes
9613 /// regardless would corrupt every non-completion caller.
9614 #[test]
9615 fn multiquote_empty_stack_returns_input_unchanged() {
9616 let _g = crate::test_util::global_state_lock();
9617 // Reset COMPQSTACK to empty.
9618 if let Some(c) = COMPQSTACK.get() {
9619 if let Ok(mut g) = c.lock() {
9620 g.clear();
9621 }
9622 }
9623 assert_eq!(multiquote("hello", 0), "hello");
9624 assert_eq!(multiquote("", 0), "");
9625 }
9626
9627 /// c:1073 — `multiquote` ESCAPES for the active quoting level; it must
9628 /// never WRAP the candidate in quote characters. `comp_match`
9629 /// (compmatch.c:1172) runs every compadd candidate through it and then
9630 /// matches the result against `$PREFIX` (compmatch.c:1178), so a wrapped
9631 /// candidate can never share a prefix with the typed word.
9632 ///
9633 /// Regression pinned: `quotestring(QT_DOUBLE)` used to return `"abcdef"`
9634 /// (with the quote pair) instead of the body `abcdef`, against C's
9635 /// contract at utils.c:6131-6134. Inside a quoted word — `cmd "ab<TAB>`,
9636 /// where compqstack holds QT_DOUBLE — `comp_match` compared `ab` to
9637 /// `"abcdef"`, rejected it, and `compadd` produced ZERO matches, so
9638 /// completion silently did nothing for EVERY quoted word.
9639 #[test]
9640 fn multiquote_escapes_but_never_wraps_the_candidate() {
9641 let _g = crate::test_util::global_state_lock();
9642 for (qt, label) in [
9643 (QT_DOUBLE, "QT_DOUBLE"),
9644 (QT_SINGLE, "QT_SINGLE"),
9645 (QT_DOLLARS, "QT_DOLLARS"),
9646 (QT_BACKSLASH, "QT_BACKSLASH"),
9647 ] {
9648 if let Ok(mut g) = COMPQSTACK.get_or_init(|| Mutex::new(String::new())).lock() {
9649 *g = (qt as u8 as char).to_string(); // c:305-306
9650 }
9651 assert_eq!(
9652 multiquote("abcdef", 0),
9653 "abcdef",
9654 "{label}: a candidate with nothing to escape must pass through \
9655 unchanged so comp_match can prefix-match it"
9656 );
9657 }
9658 if let Some(c) = COMPQSTACK.get() {
9659 if let Ok(mut g) = c.lock() {
9660 g.clear();
9661 }
9662 }
9663 }
9664
9665 /// c:1092 — `tildequote("foo")` (no leading ~) MUST behave like
9666 /// multiquote — the tilde-special path is a no-op when there's no
9667 /// `~` to protect. Regression that always strips/restores would
9668 /// silently mangle non-tilde inputs.
9669 #[test]
9670 fn tildequote_non_tilde_input_unchanged() {
9671 let _g = crate::test_util::global_state_lock();
9672 // Empty COMPQSTACK + no ~ → input unchanged.
9673 if let Some(c) = COMPQSTACK.get() {
9674 if let Ok(mut g) = c.lock() {
9675 g.clear();
9676 }
9677 }
9678 assert_eq!(tildequote("foo/bar", 0), "foo/bar");
9679 }
9680
9681 /// c:1092 — empty input through tildequote is empty out.
9682 #[test]
9683 fn tildequote_empty_input_empty_output() {
9684 let _g = crate::test_util::global_state_lock();
9685 if let Some(c) = COMPQSTACK.get() {
9686 if let Ok(mut g) = c.lock() {
9687 g.clear();
9688 }
9689 }
9690 assert_eq!(tildequote("", 0), "");
9691 }
9692
9693 // ─── zsh-corpus pins for rembslash ─────────────────────────────
9694
9695 /// `rembslash("\\a\\b\\c")` strips backslashes → "abc".
9696 #[test]
9697 fn compcore_corpus_rembslash_strips_escapes() {
9698 let _g = crate::test_util::global_state_lock();
9699 assert_eq!(rembslash(r"\a\b\c"), "abc");
9700 }
9701
9702 /// `rembslash` with no backslashes is identity.
9703 #[test]
9704 fn compcore_corpus_rembslash_no_escapes_identity() {
9705 let _g = crate::test_util::global_state_lock();
9706 assert_eq!(rembslash("hello"), "hello");
9707 }
9708
9709 /// `rembslash("")` returns empty string.
9710 #[test]
9711 fn compcore_corpus_rembslash_empty_is_empty() {
9712 let _g = crate::test_util::global_state_lock();
9713 assert_eq!(rembslash(""), "");
9714 }
9715
9716 /// `rembslash("\\\\")` (escaped backslash) returns "\\".
9717 #[test]
9718 fn compcore_corpus_rembslash_escaped_backslash() {
9719 let _g = crate::test_util::global_state_lock();
9720 // r"\\" in Rust is the 2-char string "\\\\" → one literal backslash + one literal backslash
9721 assert_eq!(rembslash(r"\\"), r"\");
9722 }
9723
9724 /// `rembslash` mid-string escape preserves surroundings.
9725 #[test]
9726 fn compcore_corpus_rembslash_preserves_context() {
9727 let _g = crate::test_util::global_state_lock();
9728 assert_eq!(rembslash(r"hello\ world"), "hello world");
9729 }
9730
9731 /// Trailing single backslash is consumed (no char after).
9732 #[test]
9733 fn compcore_corpus_rembslash_trailing_backslash_consumed() {
9734 let _g = crate::test_util::global_state_lock();
9735 assert_eq!(rembslash(r"abc\"), "abc");
9736 }
9737
9738 // ═══════════════════════════════════════════════════════════════════
9739 // Additional C-parity tests for Src/Zle/compcore.c rembslash + remsquote
9740 // + ctokenize + multiquote / tildequote string transforms.
9741 // ═══════════════════════════════════════════════════════════════════
9742
9743 /// c:1323 — `rembslash("abc")` (no backslash) is identity.
9744 #[test]
9745 fn rembslash_no_backslash_is_identity() {
9746 let _g = crate::test_util::global_state_lock();
9747 assert_eq!(rembslash("abc"), "abc");
9748 assert_eq!(rembslash("hello world"), "hello world");
9749 }
9750
9751 /// c:1323 — `rembslash` of single backslash + char drops the backslash.
9752 #[test]
9753 fn rembslash_drops_backslash_keeps_next() {
9754 let _g = crate::test_util::global_state_lock();
9755 assert_eq!(rembslash(r"\a"), "a");
9756 assert_eq!(rembslash(r"\."), ".");
9757 assert_eq!(rembslash(r"\$"), "$");
9758 }
9759
9760 /// c:1323 — repeated escapes: every `\X` collapses to `X`.
9761 #[test]
9762 fn rembslash_multiple_escapes_chain() {
9763 let _g = crate::test_util::global_state_lock();
9764 assert_eq!(rembslash(r"\a\b\c"), "abc");
9765 }
9766
9767 /// c:1343 — `remsquote("")` returns 0 (no chars consumed).
9768 #[test]
9769 fn remsquote_empty_returns_zero() {
9770 let _g = crate::test_util::global_state_lock();
9771 let mut s = String::new();
9772 let r = remsquote(&mut s);
9773 assert_eq!(r, 0);
9774 assert_eq!(s, "");
9775 }
9776
9777 /// c:1343 — `remsquote("abc")` (no quote sequences) is identity,
9778 /// returns 0.
9779 #[test]
9780 fn remsquote_no_quotes_is_identity() {
9781 let _g = crate::test_util::global_state_lock();
9782 let mut s = String::from("hello world");
9783 let r = remsquote(&mut s);
9784 assert_eq!(r, 0, "no quote sequences → 0");
9785 assert_eq!(s, "hello world", "string unchanged");
9786 }
9787
9788 /// c:1366 — `ctokenize("")` returns empty string.
9789 #[test]
9790 fn ctokenize_empty_returns_empty() {
9791 let _g = crate::test_util::global_state_lock();
9792 assert_eq!(ctokenize(""), "");
9793 }
9794
9795 /// c:1366 — `ctokenize("abc")` (no special chars) preserves content.
9796 #[test]
9797 fn ctokenize_plain_ascii_preserved() {
9798 let _g = crate::test_util::global_state_lock();
9799 // No $, {, }, or backslash → byte-for-byte preservation.
9800 let r = ctokenize("abc");
9801 assert_eq!(r.as_bytes()[0], b'a');
9802 assert_eq!(r.as_bytes()[1], b'b');
9803 assert_eq!(r.as_bytes()[2], b'c');
9804 }
9805
9806 /// c:1505 — `comp_quoting_string(0)` returns a static str (no panic).
9807 #[test]
9808 fn comp_quoting_string_returns_static_str_for_all_stypes() {
9809 let _g = crate::test_util::global_state_lock();
9810 for stype in 0..10 {
9811 let _s = comp_quoting_string(stype);
9812 // No panic = pass; returned &'static str is well-defined.
9813 }
9814 }
9815
9816 /// c:954 — `multiquote("")` returns empty.
9817 #[test]
9818 fn multiquote_empty_returns_empty() {
9819 let _g = crate::test_util::global_state_lock();
9820 let _g2 = zle_test_setup();
9821 let r = multiquote("", 0);
9822 assert_eq!(r, "");
9823 }
9824
9825 /// c:980 — `tildequote("")` returns empty.
9826 #[test]
9827 fn tildequote_empty_returns_empty() {
9828 let _g = crate::test_util::global_state_lock();
9829 let _g2 = zle_test_setup();
9830 let r = tildequote("", 0);
9831 assert_eq!(r, "");
9832 }
9833
9834 /// c:980 — `tildequote("plain")` (no tilde) is identity.
9835 #[test]
9836 fn tildequote_no_tilde_is_identity() {
9837 let _g = crate::test_util::global_state_lock();
9838 let _g2 = zle_test_setup();
9839 let r = tildequote("plain", 0);
9840 assert_eq!(r, "plain", "no tilde → input unchanged");
9841 }
9842
9843 // ═══════════════════════════════════════════════════════════════════
9844 // Additional C-parity tests for Src/Zle/compcore.c
9845 // c:936 multiquote / c:972 tildequote / c:1014 check_param /
9846 // c:1315 rembslash / c:1338 remsquote / c:1381 ctokenize /
9847 // c:1478 comp_quoting_string / c:2809 matchcmp / c:2872 matcheq
9848 // ═══════════════════════════════════════════════════════════════════
9849
9850 /// c:1315 — `rembslash("")` empty returns empty.
9851 #[test]
9852 fn rembslash_empty_returns_empty() {
9853 assert_eq!(rembslash(""), "");
9854 }
9855
9856 /// c:1315 — `rembslash` is pure.
9857 #[test]
9858 fn rembslash_is_pure() {
9859 for s in ["", "abc", r"\a", r"\\\\"] {
9860 let first = rembslash(s);
9861 for _ in 0..3 {
9862 assert_eq!(rembslash(s), first, "rembslash({:?}) must be pure", s);
9863 }
9864 }
9865 }
9866
9867 /// c:1381 — `ctokenize` is deterministic.
9868 #[test]
9869 fn ctokenize_is_deterministic() {
9870 for s in ["", "abc", "a*b", "a?b"] {
9871 let first = ctokenize(s);
9872 for _ in 0..3 {
9873 assert_eq!(
9874 ctokenize(s),
9875 first,
9876 "ctokenize({:?}) must be deterministic",
9877 s
9878 );
9879 }
9880 }
9881 }
9882
9883 /// c:1478 — `comp_quoting_string(0)` returns non-empty static.
9884 #[test]
9885 fn comp_quoting_string_zero_returns_static_str() {
9886 let _: &'static str = comp_quoting_string(0);
9887 }
9888
9889 /// c:936 — `multiquote` is pure for arbitrary input.
9890 #[test]
9891 fn multiquote_is_pure() {
9892 let _g = crate::test_util::global_state_lock();
9893 let _g2 = zle_test_setup();
9894 for s in ["", "abc", "x y"] {
9895 let first = multiquote(s, 0);
9896 for _ in 0..3 {
9897 assert_eq!(
9898 multiquote(s, 0),
9899 first,
9900 "multiquote({:?}, 0) must be pure",
9901 s
9902 );
9903 }
9904 }
9905 }
9906
9907 /// c:972 — `tildequote` is pure for non-tilde input.
9908 #[test]
9909 fn tildequote_is_pure() {
9910 let _g = crate::test_util::global_state_lock();
9911 let _g2 = zle_test_setup();
9912 for s in ["", "abc", "no_tilde", "/path/to/file"] {
9913 let first = tildequote(s, 0);
9914 for _ in 0..3 {
9915 assert_eq!(
9916 tildequote(s, 0),
9917 first,
9918 "tildequote({:?}, 0) must be pure",
9919 s
9920 );
9921 }
9922 }
9923 }
9924
9925 /// c:1338 — `remsquote(&mut empty)` returns 0.
9926 #[test]
9927 fn remsquote_empty_returns_zero_pin() {
9928 let mut s = String::new();
9929 let r = remsquote(&mut s);
9930 assert_eq!(r, 0, "empty input → 0");
9931 }
9932
9933 /// c:1014 — `check_param("")` empty returns Option<usize>.
9934 #[test]
9935 fn check_param_empty_returns_option_type() {
9936 let _g = crate::test_util::global_state_lock();
9937 let _g2 = zle_test_setup();
9938 let _: Option<usize> = check_param("", false, false, false);
9939 }
9940
9941 /// c:1014 — `check_param` is deterministic for empty input.
9942 #[test]
9943 fn check_param_empty_is_deterministic() {
9944 let _g = crate::test_util::global_state_lock();
9945 let _g2 = zle_test_setup();
9946 let first = check_param("", false, false, false);
9947 for _ in 0..3 {
9948 assert_eq!(check_param("", false, false, false), first);
9949 }
9950 }
9951
9952 /// c:1439 — `comp_str(false)` returns (String, i32, i32) tuple.
9953 #[test]
9954 fn comp_str_returns_string_i32_i32_tuple() {
9955 let _g = crate::test_util::global_state_lock();
9956 let _g2 = zle_test_setup();
9957 let _: (String, i32, i32) = comp_str(false);
9958 }
9959
9960 // ═══════════════════════════════════════════════════════════════════
9961 // Additional C-parity tests for Src/Zle/compcore.c
9962 // c:1505 set_comp_sep / c:1544 set_list_array / c:1555 get_user_var /
9963 // c:1645 get_data_arr / c:2681 begcmgroup / c:2735 endcmgroup /
9964 // c:2749 addexpl / c:1478 comp_quoting_string
9965 // ═══════════════════════════════════════════════════════════════════
9966
9967 /// c:1505 — `set_comp_sep` returns i32 (compile-time type pin).
9968 #[test]
9969 fn set_comp_sep_returns_i32_type() {
9970 let _g = crate::test_util::global_state_lock();
9971 let _g2 = zle_test_setup();
9972 let _: i32 = set_comp_sep();
9973 }
9974
9975 /// c:1555 — `get_user_var(None)` returns Option<Vec<String>>.
9976 #[test]
9977 fn get_user_var_returns_option_vec_string_type() {
9978 let _g = crate::test_util::global_state_lock();
9979 let _g2 = zle_test_setup();
9980 let _: Option<Vec<String>> = get_user_var(None);
9981 }
9982
9983 /// c:1555 — `get_user_var(None)` is deterministic.
9984 #[test]
9985 fn get_user_var_none_is_deterministic() {
9986 let _g = crate::test_util::global_state_lock();
9987 let _g2 = zle_test_setup();
9988 let first = get_user_var(None);
9989 for _ in 0..3 {
9990 assert_eq!(
9991 get_user_var(None),
9992 first,
9993 "get_user_var(None) must be deterministic"
9994 );
9995 }
9996 }
9997
9998 /// c:1645 — `get_data_arr("", false)` returns Option<Vec<String>>.
9999 #[test]
10000 fn get_data_arr_returns_option_vec_string_type() {
10001 let _g = crate::test_util::global_state_lock();
10002 let _g2 = zle_test_setup();
10003 let _: Option<Vec<String>> = get_data_arr("", false);
10004 }
10005
10006 /// c:1645 — `get_data_arr("", _)` empty name returns None.
10007 #[test]
10008 fn get_data_arr_empty_name_returns_none() {
10009 let _g = crate::test_util::global_state_lock();
10010 let _g2 = zle_test_setup();
10011 assert!(get_data_arr("", false).is_none(), "empty name → None");
10012 assert!(
10013 get_data_arr("", true).is_none(),
10014 "empty name (keys=true) → None"
10015 );
10016 }
10017
10018 /// c:2681 — `begcmgroup(None, 0)` safe.
10019 #[test]
10020 fn begcmgroup_none_no_panic() {
10021 let _g = crate::test_util::global_state_lock();
10022 let _g2 = zle_test_setup();
10023 begcmgroup(None, 0);
10024 }
10025
10026 /// c:2735 — `endcmgroup(None)` safe.
10027 #[test]
10028 fn endcmgroup_none_no_panic() {
10029 let _g = crate::test_util::global_state_lock();
10030 let _g2 = zle_test_setup();
10031 endcmgroup(None);
10032 }
10033
10034 /// c:2681 + c:2735 — beg/end cmgroup round-trip safe.
10035 #[test]
10036 fn begcmgroup_endcmgroup_round_trip_safe() {
10037 let _g = crate::test_util::global_state_lock();
10038 let _g2 = zle_test_setup();
10039 for _ in 0..3 {
10040 begcmgroup(None, 0);
10041 endcmgroup(None);
10042 }
10043 }
10044
10045 /// c:2749 — `addexpl(false)` safe.
10046 #[test]
10047 fn addexpl_false_no_panic() {
10048 let _g = crate::test_util::global_state_lock();
10049 let _g2 = zle_test_setup();
10050 addexpl(false);
10051 }
10052
10053 /// c:1544 — `set_list_array("", &[])` empty inputs is safe.
10054 #[test]
10055 fn set_list_array_empty_no_panic() {
10056 let _g = crate::test_util::global_state_lock();
10057 let _g2 = zle_test_setup();
10058 set_list_array("", &[]);
10059 }
10060
10061 /// c:1478 — `comp_quoting_string(N)` returns &'static str (type pin).
10062 #[test]
10063 fn comp_quoting_string_returns_static_str_type() {
10064 let _: &'static str = comp_quoting_string(0);
10065 }
10066
10067 /// c:1478 — `comp_quoting_string` is pure across stypes.
10068 #[test]
10069 fn comp_quoting_string_pure_across_stypes() {
10070 for stype in 0..10 {
10071 let first = comp_quoting_string(stype);
10072 for _ in 0..3 {
10073 assert_eq!(
10074 comp_quoting_string(stype),
10075 first,
10076 "comp_quoting_string({}) must be pure",
10077 stype
10078 );
10079 }
10080 }
10081 }
10082
10083 /// c:Src/Zle/compcore.c:483-489 — `before_complete` runs before
10084 /// metafication and moves the EDITOR cursor to `lastend` when the last
10085 /// completion left it inside the word (`FC_INWORD`), clamped to the line
10086 /// length. docomplete copies the editor cursor into the completion buffer
10087 /// after the hook, so the store has to land on `zle_main::ZLECS`. It used to
10088 /// land on `ZLEMETACS`, which that copy overwrites: the second TAB of
10089 /// `tst aB` (cursor left at `a|BC`) completed PREFIX=a SUFFIX=BC where zsh
10090 /// completes PREFIX=aBC (Y02compmatch #27, #40).
10091 #[test]
10092 fn before_complete_in_word_moves_the_editor_cursor_to_lastend() {
10093 let _g = crate::test_util::global_state_lock();
10094 let _g = zle_test_setup();
10095 use crate::ported::zle::zle_main::{ZLECS as EDCS, ZLELL as EDLL};
10096 let s = Ordering::SeqCst;
10097 if let Some(m) = MINFO.get() {
10098 m.lock().unwrap().cur = None;
10099 }
10100 MENUCMP.store(0, s);
10101 startauto.store(0, s);
10102
10103 // `tst aBC`, cursor at `a|BC`, last completion ended at 7.
10104 EDLL.store(7, s);
10105 EDCS.store(5, s);
10106 lastend.store(7, s);
10107 fromcomp.store(crate::ported::zle::comp_h::FC_INWORD, s);
10108 let mut lst = COMP_COMPLETE;
10109 assert_eq!(before_complete(&mut lst), 0);
10110 assert_eq!(EDCS.load(s), 7, "c:488 — zlecs = lastend");
10111
10112 // c:489 — clamped to zlell.
10113 EDCS.store(5, s);
10114 lastend.store(9, s);
10115 before_complete(&mut lst);
10116 assert_eq!(EDCS.load(s), 7, "c:489 — zlecs = zlell when lastend > zlell");
10117
10118 // Without FC_INWORD the cursor stays where the user left it.
10119 EDCS.store(5, s);
10120 fromcomp.store(0, s);
10121 before_complete(&mut lst);
10122 assert_eq!(EDCS.load(s), 5, "no FC_INWORD, no move");
10123
10124 lastend.store(0, s);
10125 EDCS.store(0, s);
10126 EDLL.store(0, s);
10127 }
10128}