zsh/ported/zle/compcore.rs
1//! Direct port of `Src/Zle/compcore.c` — completion core code.
2//!
3//! Original C copyright: Sven Wischnowsky 1995-1997.
4//!
5//! C source is 3,638 lines. This file ports:
6//! - the file-scope globals (c:36-279)
7//! - the pure-string helpers (`rembslash`, `remsquote`,
8//! `comp_quoting_string`, `multiquote`, `tildequote`, `matcheq`,
9//! `matchcmp`, `ctokenize`, `comp_str`)
10//! - the linked-list group manipulators (`begcmgroup`,
11//! `endcmgroup`, `addexpl`, `addmatch`)
12//! - the param-table helpers (`get_user_var`, `get_data_arr`,
13//! `set_list_array`)
14//! - the hook entry points (`before_complete`, `after_complete`)
15//! in their non-runhookdef branches
16//!
17//! Functions blocked on heavier substrate (`do_completion`,
18//! `makecomplist`, `addmatches`, `callcompfunc`, `set_comp_sep`,
19//! `check_param`, `permmatches`, `dupmatch`, `add_match_data`,
20//! `makearray`) carry doc comments naming the missing dependencies.
21
22#![allow(non_snake_case, non_upper_case_globals, dead_code)]
23
24use std::sync::atomic::{AtomicI32, Ordering};
25use std::sync::{Mutex, OnceLock};
26
27use crate::ported::zsh_h::{
28
29
30 Bnull, Inbrace, Outbrace, QT_BACKSLASH, QT_DOLLARS, QT_DOUBLE, QT_SINGLE, Stringg,
31};
32
33// --- AUTO: cross-zle hoisted-fn use glob ---
34#[allow(unused_imports)]
35#[allow(unused_imports)]
36use crate::ported::zle::zle_main::*;
37#[allow(unused_imports)]
38use crate::ported::zle::zle_misc::*;
39#[allow(unused_imports)]
40use crate::ported::zle::zle_hist::*;
41#[allow(unused_imports)]
42use crate::ported::zle::zle_move::*;
43#[allow(unused_imports)]
44use crate::ported::zle::zle_word::*;
45#[allow(unused_imports)]
46use crate::ported::zle::zle_params::*;
47#[allow(unused_imports)]
48use crate::ported::zle::zle_vi::*;
49#[allow(unused_imports)]
50use crate::ported::zle::zle_utils::*;
51#[allow(unused_imports)]
52use crate::ported::zle::zle_refresh::*;
53#[allow(unused_imports)]
54use crate::ported::zle::zle_tricky::*;
55#[allow(unused_imports)]
56use crate::ported::zle::textobjects::*;
57#[allow(unused_imports)]
58use crate::ported::zle::deltochar::*;
59use crate::ported::zle::comp_h::{
60 Aminfo, Cexpl, Cmatch, Cmgroup, CGF_MATSORT, CGF_NOSORT, CGF_NUMSORT, CGF_REVSORT,
61 CGF_UNIQALL, CGF_UNIQCON, CMF_DELETE, CMF_DISPLINE, CMF_FMULT, CMF_MULT, CMF_NOLIST,
62 CMF_PACKED, CMF_PARBR, CMF_PARNEST, CMF_ROWS,
63};
64use crate::ported::zle::complete::{COMPIPREFIX, COMPPREFIX, COMPSUFFIX};
65use crate::ported::zle::zle_tricky::{MENUCMP, USEMENU};
66use crate::ported::zle::complete::COMPLIST;
67use crate::ported::zle::zle_tricky::{USEGLOB, WOULDINSTAB};
68use crate::ported::zsh_h::{Dnull, Equals, Hat, Inbrack, Inpar, Outpar, Pound, Qstring, Quest, Snull, Star, Tilde};
69use crate::ported::zle::comp_h::{CAF_ALL, CAF_MATSORT, CAF_NOSORT, CAF_NUMSORT, CAF_QUOTE, CAF_REVSORT, CAF_UNIQALL, CAF_UNIQCON};
70
71// =====================================================================
72// Extern globals — declared in other C files, mirrored here per
73// PORT.md Rule 9 ("stub the EXTERN dependencies ... locally with
74// file:line citations to their home file") so the local body ports
75// below have a value source. When the canonical Rust homes land,
76// these become `pub use crate::ported::<canonical>::*` re-exports.
77// =====================================================================
78
79/// Port of `mod_export int wb` from `Src/lex.c:120`. Word-begin
80/// position in the metafied line for the currently-completing word.
81pub static WB: AtomicI32 = AtomicI32::new(0); // lex.c:120
82/// Port of `mod_export int we` from `Src/lex.c:120`. Word-end position.
83pub static WE: AtomicI32 = AtomicI32::new(0); // lex.c:120
84/// Port of `mod_export int zlemetacs` from `Src/lex.c:104`. Cursor
85/// position in the metafied line.
86pub static ZLEMETACS: AtomicI32 = AtomicI32::new(0); // lex.c:104
87/// Port of `mod_export int zlemetall` from `Src/lex.c:104`. Length
88/// of the metafied line.
89pub static ZLEMETALL: AtomicI32 = AtomicI32::new(0); // lex.c:104
90/// Port of `mod_export int addedx` from `Src/lex.c:115`. Non-zero
91/// while a dummy `x` cursor marker is in the line being lexed
92/// (so completion can capture the partial word at the cursor).
93pub static ADDEDX: AtomicI32 = AtomicI32::new(0); // lex.c:115
94
95/// Port of `mod_export char *zlemetaline` from `Src/lex.c:103`. The
96/// metafied edit buffer for the current ZLE session — `foredel`,
97/// `inststr`, `selfinsert` operate on this directly when compcore's
98/// error-recovery path fires (compcore.c:344-355).
99pub static ZLEMETALINE: OnceLock<Mutex<String>> = OnceLock::new(); // lex.c:103
100/// Port of `mod_export ZLE_STRING_T zleline` from `Src/zle_main.c`.
101pub static ZLELINE: OnceLock<Mutex<String>> = OnceLock::new(); // zle_main.c
102/// Port of `mod_export int zlecs` from `Src/zle_main.c`.
103pub static ZLECS: AtomicI32 = AtomicI32::new(0); // zle_main.c
104/// Port of `mod_export int zlell` from `Src/zle_main.c`.
105pub static ZLELL: AtomicI32 = AtomicI32::new(0); // zle_main.c
106/// Port of `mod_export int inwhat` from `Src/lex.c:110`. Lex context
107/// classification — IN_NOTHING / IN_CMD / IN_COND / IN_MATH / IN_PAR /
108/// IN_ENV.
109pub static INWHAT: AtomicI32 = AtomicI32::new(0); // lex.c:110
110/// Port of `mod_export int zmult` from `Src/zle_main.c`. Numeric
111/// prefix multiplier for the current ZLE command.
112pub static ZMULT: AtomicI32 = AtomicI32::new(1); // zle_main.c
113/// Port of `mod_export char *compfunc` from `Src/Zle/zle_tricky.c:143`.
114/// Name of the user completion shell function — non-empty when the
115/// new completion system (`compsys`) is active; empty for compctl.
116pub static compfunc: OnceLock<Mutex<Option<String>>> = OnceLock::new(); // zle_tricky.c:143
117/// Port of `mod_export char *comppatmatch` from `Src/Zle/zle_tricky.c`.
118/// `$compstate[pattern_match]` — when non-empty + non-"\0" enables
119/// pattern-aware matching for parameter-name completion.
120pub static comppatmatch: OnceLock<Mutex<Option<String>>> = OnceLock::new();
121/// Port of `mod_export char *compqstack` from `Src/Zle/compcore.c`.
122/// Quoting-state stack (1 char per nesting level).
123pub static compqstack: OnceLock<Mutex<String>> = OnceLock::new();
124
125// Brace counters live in zle_tricky.c:114 — re-exported there. Local
126// re-exports here so call sites stay short:
127#[doc(hidden)]
128pub use crate::ported::zle::zle_tricky::{NBRBEG as _NBRBEG, NBREND as _NBREND};
129use crate::zsh_h::{isset, BASHAUTOLIST, NUMERICGLOBSORT, RCQUOTES, SORTIT_IGNORING_BACKSLASHES, SORTIT_NUMERICALLY};
130// =====================================================================
131// File-scope globals — `Src/Zle/compcore.c:36-279`.
132// =====================================================================
133
134/// Port of `int useexact` from compcore.c:36.
135pub static useexact: AtomicI32 = AtomicI32::new(0); // c:36
136/// Port of `int useline` from compcore.c:36.
137pub static useline: AtomicI32 = AtomicI32::new(0); // c:36
138/// Port of `int uselist` from compcore.c:36.
139pub static uselist: AtomicI32 = AtomicI32::new(0); // c:36
140/// Port of `int forcelist` from compcore.c:36.
141pub static forcelist: AtomicI32 = AtomicI32::new(0); // c:36
142/// Port of `int startauto` from compcore.c:36.
143pub static startauto: AtomicI32 = AtomicI32::new(0); // c:36
144
145/// Port of `mod_export int iforcemenu` from compcore.c:39.
146pub static iforcemenu: AtomicI32 = AtomicI32::new(0); // c:39
147
148/// Port of `mod_export int dolastprompt` from compcore.c:44.
149pub static dolastprompt: AtomicI32 = AtomicI32::new(0); // c:44
150
151/// Port of `mod_export int oldlist` from compcore.c:49.
152pub static oldlist: AtomicI32 = AtomicI32::new(0); // c:49
153/// Port of `mod_export int oldins` from compcore.c:49.
154pub static oldins: AtomicI32 = AtomicI32::new(0); // c:49
155
156/// Port of `int origlpre` from compcore.c:54.
157pub static origlpre: AtomicI32 = AtomicI32::new(0); // c:54
158/// Port of `int origlsuf` from compcore.c:54.
159pub static origlsuf: AtomicI32 = AtomicI32::new(0); // c:54
160/// Port of `int lenchanged` from compcore.c:54.
161pub static lenchanged: AtomicI32 = AtomicI32::new(0); // c:54
162
163/// Port of `int movetoend` from compcore.c:61.
164pub static movetoend: AtomicI32 = AtomicI32::new(0); // c:61
165
166/// Port of `mod_export int insmnum` from compcore.c:66.
167pub static insmnum: AtomicI32 = AtomicI32::new(0); // c:66
168/// Port of `mod_export int insspace` from compcore.c:66.
169pub static insspace: AtomicI32 = AtomicI32::new(0); // c:66
170
171/// Port of `mod_export int menuacc` from compcore.c:81.
172pub static menuacc: AtomicI32 = AtomicI32::new(0); // c:81
173
174/// Port of `int hasunqu` from compcore.c:86.
175pub static hasunqu: AtomicI32 = AtomicI32::new(0); // c:86
176/// Port of `int useqbr` from compcore.c:86.
177pub static useqbr: AtomicI32 = AtomicI32::new(0); // c:86
178/// Port of `int brpcs` from compcore.c:86.
179pub static brpcs: AtomicI32 = AtomicI32::new(0); // c:86
180/// Port of `int brscs` from compcore.c:86.
181pub static brscs: AtomicI32 = AtomicI32::new(0); // c:86
182
183/// Port of `mod_export int ispar` from compcore.c:91.
184pub static ispar: AtomicI32 = AtomicI32::new(0); // c:91
185/// Port of `mod_export int linwhat` from compcore.c:91.
186pub static linwhat: AtomicI32 = AtomicI32::new(0); // c:91
187
188/// Port of `char *parpre` from compcore.c:96.
189pub static parpre: OnceLock<Mutex<String>> = OnceLock::new(); // c:96
190
191/// Port of `int parflags` from compcore.c:101.
192pub static parflags: AtomicI32 = AtomicI32::new(0); // c:101
193
194/// Port of `mod_export int mflags` from compcore.c:106.
195pub static mflags: AtomicI32 = AtomicI32::new(0); // c:106
196
197/// Port of `int parq` from compcore.c:111.
198pub static parq: AtomicI32 = AtomicI32::new(0); // c:111
199/// Port of `int eparq` from compcore.c:111.
200pub static eparq: AtomicI32 = AtomicI32::new(0); // c:111
201
202/// Port of `mod_export char *ipre` from compcore.c:118.
203pub static ipre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
204/// Port of `mod_export char *ripre` from compcore.c:118.
205pub static ripre: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
206/// Port of `mod_export char *isuf` from compcore.c:118.
207pub static isuf: OnceLock<Mutex<String>> = OnceLock::new(); // c:118
208
209/// Port of `mod_export LinkList matches` from compcore.c:124.
210pub static matches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:124
211/// Port of `LinkList fmatches` from compcore.c:126.
212pub static fmatches: OnceLock<Mutex<Vec<Cmatch>>> = OnceLock::new(); // c:126
213
214/// Port of `mod_export Cmgroup amatches` from compcore.c:135.
215pub static amatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
216/// Port of `mod_export Cmgroup pmatches` from compcore.c:135.
217pub static pmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
218/// Port of `mod_export Cmgroup lastmatches` from compcore.c:135.
219pub static lastmatches: OnceLock<Mutex<Vec<Cmgroup>>> = OnceLock::new(); // c:135
220/// Port of `mod_export Cmgroup lmatches` from compcore.c:135. Last
221/// element pointer in the perm list; here a single-slot holder.
222pub static lmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
223/// Port of `mod_export Cmgroup lastlmatches` from compcore.c:135.
224pub static lastlmatches: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:135
225
226/// Port of `mod_export int hasoldlist` from compcore.c:140.
227pub static hasoldlist: AtomicI32 = AtomicI32::new(0); // c:140
228/// Port of `mod_export int hasperm` from compcore.c:140.
229pub static hasperm: AtomicI32 = AtomicI32::new(0); // c:140
230/// Port of `int hasallmatch` from compcore.c:145.
231pub static hasallmatch: AtomicI32 = AtomicI32::new(0); // c:145
232
233/// Port of `mod_export int newmatches` from compcore.c:150.
234pub static newmatches: AtomicI32 = AtomicI32::new(0); // c:150
235
236/// Port of `mod_export int permmnum` from compcore.c:155.
237pub static permmnum: AtomicI32 = AtomicI32::new(0); // c:155
238/// Port of `mod_export int permgnum` from compcore.c:155.
239pub static permgnum: AtomicI32 = AtomicI32::new(0); // c:155
240/// Port of `mod_export int lastpermmnum` from compcore.c:155.
241pub static lastpermmnum: AtomicI32 = AtomicI32::new(0); // c:155
242/// Port of `mod_export int lastpermgnum` from compcore.c:155.
243pub static lastpermgnum: AtomicI32 = AtomicI32::new(0); // c:155
244
245/// Port of `mod_export int nmatches` from compcore.c:160.
246pub static nmatches: AtomicI32 = AtomicI32::new(0); // c:160
247/// Port of `mod_export int smatches` from compcore.c:162.
248pub static smatches: AtomicI32 = AtomicI32::new(0); // c:162
249
250/// Port of `mod_export int diffmatches` from compcore.c:167.
251pub static diffmatches: AtomicI32 = AtomicI32::new(0); // c:167
252
253/// Port of `mod_export int nmessages` from compcore.c:172.
254pub static nmessages: AtomicI32 = AtomicI32::new(0); // c:172
255
256/// Port of `mod_export int onlyexpl` from compcore.c:177.
257pub static onlyexpl: AtomicI32 = AtomicI32::new(0); // c:177
258
259/// Port of `mod_export struct cldata listdat` from compcore.c:182.
260pub static listdat: OnceLock<Mutex<crate::ported::zle::comp_h::Cldata>> =
261 OnceLock::new(); // c:182
262
263/// Port of `mod_export int ispattern` from compcore.c:187.
264pub static ispattern: AtomicI32 = AtomicI32::new(0); // c:187
265/// Port of `mod_export int haspattern` from compcore.c:187.
266pub static haspattern: AtomicI32 = AtomicI32::new(0); // c:187
267
268/// Port of `mod_export int hasmatched` from compcore.c:192.
269pub static hasmatched: AtomicI32 = AtomicI32::new(0); // c:192
270/// Port of `mod_export int hasunmatched` from compcore.c:192.
271pub static hasunmatched: AtomicI32 = AtomicI32::new(0); // c:192
272
273/// Port of `Cmgroup mgroup` from compcore.c:197.
274pub static mgroup: OnceLock<Mutex<Option<Cmgroup>>> = OnceLock::new(); // c:197
275
276/// Port of `mod_export int mnum` from compcore.c:202.
277pub static mnum: AtomicI32 = AtomicI32::new(0); // c:202
278
279/// Port of `mod_export int unambig_mnum` from compcore.c:207.
280pub static unambig_mnum: AtomicI32 = AtomicI32::new(0); // c:207
281
282/// Port of `int maxmlen` from compcore.c:212.
283pub static maxmlen: AtomicI32 = AtomicI32::new(0); // c:212
284/// Port of `int minmlen` from compcore.c:212.
285pub static minmlen: AtomicI32 = AtomicI32::new(0); // c:212
286
287/// Port of `LinkList expls` from compcore.c:218.
288pub static expls: OnceLock<Mutex<Vec<Cexpl>>> = OnceLock::new(); // c:218
289
290/// Port of `mod_export Cexpl curexpl` from compcore.c:221.
291pub static curexpl: OnceLock<Mutex<Option<Cexpl>>> = OnceLock::new(); // c:221
292
293/// Port of `LinkList matchers` from compcore.c:236.
294pub static matchers: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); // c:236
295
296/// Port of `mod_export Aminfo ainfo` from compcore.c:246.
297pub static ainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
298/// Port of `mod_export Aminfo fainfo` from compcore.c:246.
299pub static fainfo: OnceLock<Mutex<Option<Aminfo>>> = OnceLock::new(); // c:246
300
301/// Port of `mod_export LinkList allccs` from compcore.c:259.
302pub static allccs: OnceLock<Mutex<Vec<String>>> = OnceLock::new(); // c:259
303
304/// Port of `int fromcomp` from compcore.c:271.
305pub static fromcomp: AtomicI32 = AtomicI32::new(0); // c:271
306
307/// Port of `mod_export int lastend` from compcore.c:276.
308pub static lastend: AtomicI32 = AtomicI32::new(0); // c:276
309
310/// Port of `static int oldmenucmp` from compcore.c:457.
311pub static OLDMENUCMP: AtomicI32 = AtomicI32::new(0); // c:457
312
313/// Port of `static int parwb` from compcore.c:540.
314pub static PARWB: AtomicI32 = AtomicI32::new(0); // c:540
315/// Port of `static int parwe` from compcore.c:540.
316pub static PARWE: AtomicI32 = AtomicI32::new(0); // c:540
317/// Port of `static int paroffs` from compcore.c:540.
318pub static PAROFFS: AtomicI32 = AtomicI32::new(0); // c:540
319
320/// Port of `static int matchorder` from compcore.c:3169.
321pub static MATCHORDER: AtomicI32 = AtomicI32::new(0); // c:3169
322
323// =====================================================================
324// rembslash — `Src/Zle/compcore.c:1323`.
325// =====================================================================
326
327/// Port of `mod_export char *rembslash(char *s)` from compcore.c:1322.
328///
329/// "Strip backslash escapes from a token, treating `\X` as `X`."
330pub fn rembslash(s: &str) -> String { // c:1323
331 let mut result = String::with_capacity(s.len()); // c:1323
332 let mut chars = s.chars().peekable(); // c:1327
333 while let Some(c) = chars.next() {
334 if c == '\\' { // c:1328
335 if let Some(nxt) = chars.next() { // c:1329
336 result.push(nxt);
337 }
338 } else {
339 result.push(c); // c:1343-1333
340 }
341 }
342 result // c:1343
343}
344
345// =====================================================================
346// remsquote — `Src/Zle/compcore.c:1343`.
347// =====================================================================
348
349/// Port of `mod_export int remsquote(char *s)` from compcore.c:1342.
350pub fn remsquote(s: &mut String) -> i32 { // c:1343
351 let rcquotes = isset(RCQUOTES); // c:1343
352 let qa: usize = if rcquotes { 1 } else { 3 };
353
354 let bytes = s.as_bytes(); // c:1346
355 let mut t = Vec::<u8>::with_capacity(bytes.len());
356 let mut ret: i32 = 0;
357 let mut i = 0;
358 while i < bytes.len() { // c:1348
359 let matched = if qa == 1 { // c:1349
360 i + 1 < bytes.len() && bytes[i] == b'\'' && bytes[i + 1] == b'\''
361 } else {
362 i + 3 < bytes.len() // c:1351
363 && bytes[i] == b'\''
364 && bytes[i + 1] == b'\\'
365 && bytes[i + 2] == b'\''
366 && bytes[i + 3] == b'\''
367 };
368 if matched {
369 ret += qa as i32; // c:1352
370 t.push(b'\''); // c:1353
371 i += qa + 1; // c:1354
372 } else {
373 t.push(bytes[i]); // c:1356
374 i += 1;
375 }
376 }
377 *s = String::from_utf8(t).unwrap_or_default(); // c:1357
378 ret // c:1366
379}
380
381// =====================================================================
382// ctokenize — `Src/Zle/compcore.c:1366`.
383// =====================================================================
384
385/// Port of `mod_export char *ctokenize(char *p)` from compcore.c:1365.
386///
387/// C calls `tokenize(p)` first then walks the string replacing
388/// unescaped `$`/`{`/`}` with the token bytes `String`/`Inbrace`/
389/// `Outbrace`. Backslash-escaped variants become `Bnull`.
390pub fn ctokenize(p: &str) -> String { // c:1366
391 let bytes = p.as_bytes(); // c:1366
392 let mut out = Vec::<u8>::with_capacity(bytes.len());
393 let mut bslash = false; // c:1369
394 let mut prev_idx: Option<usize> = None;
395 let mut i = 0;
396 while i < bytes.len() {
397 let b = bytes[i]; // c:1373
398 if b == b'\\' { // c:1374
399 bslash = true;
400 out.push(b);
401 prev_idx = Some(out.len() - 1);
402 } else {
403 if b == b'$' || b == b'{' || b == b'}' { // c:1377
404 if bslash { // c:1378
405 if let Some(pi) = prev_idx { // c:1379
406 out.truncate(pi);
407 let mut buf = [0u8; 4];
408 out.extend_from_slice(Bnull.encode_utf8(&mut buf).as_bytes());
409 }
410 out.push(b);
411 } else {
412 let tok = if b == b'$' { Stringg } // c:1381
413 else if b == b'{' { Inbrace } // c:1382
414 else { Outbrace }; // c:1382
415 let mut buf = [0u8; 4];
416 out.extend_from_slice(tok.encode_utf8(&mut buf).as_bytes());
417 }
418 } else {
419 out.push(b);
420 }
421 bslash = false; // c:1384
422 prev_idx = Some(out.len().saturating_sub(1));
423 }
424 i += 1;
425 }
426 String::from_utf8(out).unwrap_or_default() // c:1403
427}
428
429// =====================================================================
430// comp_str — `Src/Zle/compcore.c:1403`.
431// =====================================================================
432
433/// Port of `mod_export char *comp_str(int *ipl, int *pl, int untok)`
434/// from compcore.c:1402.
435pub fn comp_str(untok: bool) -> (String, i32, i32) { // c:1403
436 let mut p = COMPPREFIX.get_or_init(|| Mutex::new(String::new())) // c:1405
437 .lock().unwrap().clone();
438 let mut s = COMPSUFFIX.get_or_init(|| Mutex::new(String::new())) // c:1406
439 .lock().unwrap().clone();
440 let ip = COMPIPREFIX.get_or_init(|| Mutex::new(String::new())) // c:1407
441 .lock().unwrap().clone();
442 if !untok { // c:1411
443 p = ctokenize(&p); // c:1412
444 p = p.chars().filter(|&c| c != Bnull).collect(); // c:1413 remnulargs
445 s = ctokenize(&s); // c:1414
446 s = s.chars().filter(|&c| c != Bnull).collect(); // c:1415
447 }
448 let lp = p.len() as i32; // c:1417
449 let lip = ip.len() as i32; // c:1419
450 let mut str = String::with_capacity(ip.len() + p.len() + s.len() + 1); // c:1420
451 str.push_str(&ip); // c:1435
452 str.push_str(&p); // c:1435
453 str.push_str(&s); // c:1435
454 (str, lip, lp) // c:1435-1430
455}
456
457// =====================================================================
458// comp_quoting_string — `Src/Zle/compcore.c:1435`.
459// =====================================================================
460
461/// Port of `mod_export char *comp_quoting_string(int stype)` from
462/// compcore.c:1434.
463pub fn comp_quoting_string(stype: i32) -> &'static str { // c:1435
464 match stype { // c:1435
465 x if x == QT_SINGLE => "'", // c:1439-1440
466 x if x == QT_DOUBLE => "\"", // c:1441-1442
467 x if x == QT_DOLLARS => "$'", // c:1443-1444
468 _ => { // c:1445
469 let _ = QT_BACKSLASH;
470 "\\" // c:1446
471 }
472 }
473}
474
475// =====================================================================
476// multiquote — `Src/Zle/compcore.c:1065`.
477// =====================================================================
478
479/// Port of `mod_export char *multiquote(char *s, int ign)` from
480/// compcore.c:1064.
481pub fn multiquote(s: &str, ign: i32) -> String { // c:1065
482 let stack = crate::ported::zle::complete::COMPQSTACK // c:1065
483 .get_or_init(|| Mutex::new(String::new()))
484 .lock()
485 .map(|g| g.clone())
486 .unwrap_or_default();
487 let p_bytes = stack.as_bytes();
488 if !p_bytes.is_empty() && (ign == 0 || p_bytes.len() > 1) { // c:1070
489 let start = if ign != 0 { 1 } else { 0 }; // c:1071
490 let mut cur = s.to_string();
491 for &q in &p_bytes[start..] { // c:1073
492 let qt = match q as i32 { // c:1074
493 x if x == QT_BACKSLASH => crate::ported::zsh_h::QT_BACKSLASH,
494 x if x == QT_SINGLE => crate::ported::zsh_h::QT_SINGLE,
495 x if x == QT_DOUBLE => crate::ported::zsh_h::QT_DOUBLE,
496 x if x == QT_DOLLARS => crate::ported::zsh_h::QT_DOLLARS,
497 _ => crate::ported::zsh_h::QT_BACKSLASH,
498 };
499 cur = crate::ported::utils::quotestring(&cur, qt);
500 }
501 cur // c:1092
502 } else {
503 s.to_string() // c:1092
504 }
505}
506
507// =====================================================================
508// tildequote — `Src/Zle/compcore.c:1092`.
509// =====================================================================
510
511/// Port of `mod_export char *tildequote(char *s, int ign)` from
512/// compcore.c:1091.
513pub fn tildequote(s: &str, ign: i32) -> String { // c:1092
514 let bytes = s.as_bytes(); // c:1092
515 let tilde = !bytes.is_empty() && bytes[0] == b'~'; // c:1097
516 let staged = if tilde { // c:1098
517 let mut tmp = String::with_capacity(s.len());
518 tmp.push('x');
519 tmp.push_str(&s[1..]);
520 tmp
521 } else {
522 s.to_string()
523 };
524 let mut quoted = multiquote(&staged, ign); // c:1099
525 if tilde && !quoted.is_empty() { // c:1100
526 let mut new_q = String::with_capacity(quoted.len());
527 let mut swapped = false;
528 for c in quoted.chars() {
529 if !swapped && c == 'x' {
530 new_q.push('~');
531 swapped = true;
532 } else {
533 new_q.push(c);
534 }
535 }
536 quoted = new_q;
537 }
538 quoted // c:1101
539}
540
541// =====================================================================
542// before_complete / after_complete — `Src/Zle/compcore.c:461 / 503`.
543// =====================================================================
544
545/// Direct port of `int before_complete(Hookdef dummy, int *lst)`
546/// from `Src/Zle/compcore.c:461`. Pre-completion hook: snapshots
547/// `menucmp` into `oldmenucmp`, decides whether the current state
548/// shortcircuits via menu-completion, clamps the cursor when re-
549/// entering an in-word completion, and toggles automenu mode.
550/// Returns 1 to suppress the next-stage match build, 0 to continue.
551pub fn before_complete(lst: &mut i32) -> i32 { // c:461
552 use crate::ported::zle::zle_h::{COMP_LIST_COMPLETE, COMP_LIST_EXPAND};
553 use crate::ported::zle::zle_tricky::{LASTAMBIG, SHOWAGAIN, VALIDLIST};
554 use crate::ported::zle::zle_refresh::SHOWINGLIST;
555
556 // c:463 — `oldmenucmp = menucmp;`
557 OLDMENUCMP.store(MENUCMP.load(Ordering::Relaxed), Ordering::Relaxed);
558
559 // c:465-466 — `if (showagain && validlist) showinglist = -2;`
560 if SHOWAGAIN.load(Ordering::Relaxed) != 0
561 && VALIDLIST.load(Ordering::Relaxed) != 0
562 {
563 SHOWINGLIST.store(-2, Ordering::Relaxed);
564 }
565 // c:467 — `showagain = 0;`
566 SHOWAGAIN.store(0, Ordering::Relaxed);
567
568 let has_cur = MINFO.get().and_then(|m| m.lock().ok())
569 .map(|m| m.cur.is_some())
570 .unwrap_or(false);
571 let menucmp_v = MENUCMP.load(Ordering::Relaxed);
572
573 // c:471-474 — menu-completion shortcircuit (non-listing path).
574 if has_cur && menucmp_v != 0 && *lst != COMP_LIST_EXPAND {
575 // C: `do_menucmp(*lst); return 1;` — Rust signature takes a
576 // match list; the side-effect of advancing minfo lives in
577 // do_menucmp. The post-summary `do_menucmp` Rust port has a
578 // (&[String], cur, fwd) → (idx, &str) shape, which doesn't
579 // fit a void-context call. The salient signal here is the
580 // `return 1` short-circuit; preserve that.
581 return 1; // c:473
582 }
583 // c:475-479 — menu-completion shortcircuit (listing path).
584 if has_cur && menucmp_v != 0
585 && VALIDLIST.load(Ordering::Relaxed) != 0
586 && *lst == COMP_LIST_COMPLETE
587 {
588 SHOWINGLIST.store(-2, Ordering::Relaxed);
589 onlyexpl.store(0, Ordering::Relaxed); // c:477
590 // c:477 — `listdat.valid = 0;`
591 if let Some(ld) = listdat.get() {
592 if let Ok(mut g) = ld.lock() {
593 g.valid = 0;
594 }
595 }
596 return 1; // c:478
597 }
598
599 // c:489-490 — `if ((fromcomp & FC_INWORD) && (zlecs = lastend) > zlell)
600 // zlecs = zlell;`
601 // fromcomp/lastend globals not yet ported. Substrate
602 // gap documented; skip this branch until they arrive.
603 // Cursor clamp matters only when re-entering an
604 // in-word completion, so skipping is observable only
605 // during interactive composition where the
606 // completion engine itself isn't wired yet.
607
608 // c:494-496 — automenu trigger.
609 if startauto.load(Ordering::Relaxed) != 0
610 && LASTAMBIG.load(Ordering::Relaxed) != 0
611 {
612 let bashauto = isset(BASHAUTOLIST);
613 let last = LASTAMBIG.load(Ordering::Relaxed);
614 if !bashauto || last == 2 {
615 USEMENU.store(2, Ordering::Relaxed);
616 }
617 }
618
619 0 // c:498
620}
621
622/// Direct port of `int after_complete(Hookdef dummy, int *dat)`
623/// from `Src/Zle/compcore.c:503`. Post-completion hook: when a
624/// completion has just transitioned into menu-completion (menucmp
625/// went 0→1 across this round), runs MENUSTARTHOOK so registered
626/// hook fns can veto or modify the about-to-display menu.
627///
628/// Hook handlers are registered via `addhookfunc("menu_start", fn)`
629/// (see `crate::ported::module::addhookfunc`), which writes to the
630/// global HOOKTAB. C's `comphooks[]` table declares `menu_start` as
631/// HOOKF_ALL, so every handler fires and the first non-zero return
632/// short-circuits the chain (see runhookdef at module.c:990).
633///
634/// Return value semantics (c:518-532):
635/// - `ret == 0` → no action (no handler vetoed).
636/// - `ret >= 1` → zero `dat[1]`, clear menucmp/menuacc, null minfo.cur.
637/// - `ret >= 2` → also rewind buffer to origline.
638/// - `ret == 2` → also schedule list clear (CLEARLIST=1, invalidatelist).
639pub fn after_complete(dat: &mut [i32]) -> i32 { // c:503
640 let menucmp_v = MENUCMP.load(Ordering::Relaxed);
641 let oldmenucmp_v = OLDMENUCMP.load(Ordering::Relaxed);
642
643 // c:505 — `if (menucmp && !oldmenucmp) { ... }`.
644 if menucmp_v == 0 || oldmenucmp_v != 0 {
645 return 0; // c:535
646 }
647
648 // c:506-517 — build chdata. cdat.matches=amatches, cdat.num=
649 // nmatches, cdat.nmesg=nmessages, cdat.cur=NULL. The
650 // Rust hook dispatch path doesn't yet thread chdata
651 // into shell-fn args (handlers in the standard zsh
652 // distribution all read directly from compsys globals
653 // via $compstate). The fields above are still tracked
654 // via amatches/nmatches/nmessages globals and visible
655 // to handlers through the normal completion-state
656 // parameter reads.
657
658 // c:518 — `runhookdef(MENUSTARTHOOK, &cdat)`. C dispatches via
659 // the hookdef chain; the Rust port walks HOOKTAB["menu_start"] and
660 // invokes each shell-fn via the canonical dispatch_function_call
661 // path used by signal-trap shfunc dispatch (signals.rs:1087). The
662 // first non-zero return short-circuits per HOOKF_ALL semantics
663 // (module.c:996-1005).
664 let handlers: Vec<String> = crate::ported::module::HOOKTAB
665 .lock()
666 .ok()
667 .and_then(|t| t.get("menu_start").cloned())
668 .unwrap_or_default();
669
670 let mut ret: i32 = 0;
671 for fn_name in &handlers {
672 let r = crate::fusevm_bridge::with_executor(|exec| {
673 exec.dispatch_function_call(fn_name, &[]).unwrap_or(0)
674 });
675 if r != 0 {
676 ret = r;
677 break; // c:1001 short-circuit
678 }
679 }
680
681 if ret == 0 {
682 return 0; // c:535
683 }
684
685 // c:519 — `dat[1] = 0`. The C caller passes a 2-int array; index 1
686 // carries the menu-acceptance flag for the outer compfunc loop.
687 if dat.len() > 1 {
688 dat[1] = 0;
689 }
690 // c:520 — `menucmp = menuacc = 0`.
691 MENUCMP.store(0, Ordering::Relaxed);
692 menuacc.store(0, Ordering::Relaxed);
693 // c:521 — `minfo.cur = NULL`.
694 if let Some(m) = MINFO.get() {
695 if let Ok(mut mi) = m.lock() {
696 mi.cur = None;
697 }
698 }
699
700 if ret >= 2 { // c:522
701 // c:523 — `fixsuffix()`.
702 crate::ported::zle::zle_misc::fixsuffix();
703 // c:524 — `zlemetacs = 0`.
704 ZLEMETACS.store(0, Ordering::Relaxed);
705 // c:525 — `foredel(zlemetall, CUT_RAW)` removes the entire line.
706 let metall = ZLEMETALL.load(Ordering::Relaxed);
707 crate::ported::zle::zle_utils::foredel(metall, crate::ported::zle::zle_h::CUT_RAW);
708 // c:526 — `inststr(origline)` reinserts the pre-completion buffer.
709 let origline_v: String = crate::ported::zle::zle_tricky::ORIGLINE
710 .get()
711 .and_then(|m| m.lock().ok().map(|g| g.clone()))
712 .unwrap_or_default();
713 let _ = crate::ported::zle::zle_tricky::inststr(&origline_v);
714 // c:527 — `zlemetacs = origcs`.
715 let origcs_v = crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed);
716 ZLEMETACS.store(origcs_v, Ordering::Relaxed);
717
718 if ret == 2 { // c:528
719 // c:529 — `clearlist = 1`.
720 crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed);
721 // c:530 — `invalidatelist()`.
722 crate::ported::zle::zle_h::invalidatelist();
723 }
724 }
725
726 0 // c:535
727}
728
729// =====================================================================
730// set_list_array — `Src/Zle/compcore.c:1947`.
731// =====================================================================
732
733/// Port of `static void set_list_array(char *name, LinkList l)` from
734/// compcore.c:1947. Writes an array-typed parameter via the canonical
735/// `setaparam` (params.c:3595).
736pub fn set_list_array(name: &str, l: &[String]) { // c:1947
737 let _ = crate::ported::params::setaparam(name, l.to_vec()); // c:1956
738}
739
740// =====================================================================
741// get_user_var — `Src/Zle/compcore.c:1956`.
742// =====================================================================
743
744/// Port of `mod_export char **get_user_var(char *nam)` from
745/// compcore.c:1956.
746pub fn get_user_var(nam: Option<&str>) -> Option<Vec<String>> { // c:1956
747 let nam = nam?; // c:1956
748 if nam.starts_with('(') { // c:1960
749 let mut arrlist: Vec<String> = Vec::new();
750 let bytes = nam.as_bytes();
751 let mut buf = Vec::<u8>::new();
752 let mut notempty = false; // c:1963
753 let mut brk = false;
754 let mut i = 1; // c:1967
755 while i < bytes.len() {
756 let b = bytes[i];
757 if b == b'\\' && i + 1 < bytes.len() { // c:1969
758 buf.push(bytes[i + 1]); // c:1970
759 notempty = true;
760 i += 2;
761 continue;
762 }
763 if b == b',' || b == b' ' || b == b'\t' || b == b'\n' || b == b')' {
764 if b == b')' { brk = true; } // c:1972
765 if notempty { // c:1974
766 let mut start = 0;
767 if !buf.is_empty() && buf[0] == b'\n' { start = 1; } // c:1977
768 let s = String::from_utf8_lossy(&buf[start..]).into_owned();
769 arrlist.push(s); // c:1979
770 }
771 buf.clear(); // c:1981
772 notempty = false;
773 } else {
774 notempty = true; // c:1984
775 buf.push(b);
776 }
777 i += 1;
778 if brk { break; } // c:1988
779 }
780 if !brk || arrlist.is_empty() { return None; } // c:1991
781 Some(arrlist) // c:1996
782 } else { // c:1999
783 // c:2003 — `if ((arr = getaparam(nam)) || (arr = gethparam(nam)))
784 // arr = (incompfunc ? arrdup(arr) : arr);
785 // else if ((val = getsparam(nam))) { arr = {val, NULL}; }`
786 // Read directly from paramtab: arrays first, then hashed
787 // assoc-array values, then scalar wrapped in a 1-element array.
788 crate::ported::signals::queue_signals();
789 let result = {
790 let tab = match crate::ported::params::paramtab().read() {
791 Ok(t) => t,
792 Err(_) => {
793 crate::ported::signals::unqueue_signals();
794 return None;
795 }
796 };
797 tab.get(nam).and_then(|pm| {
798 if let Some(arr) = pm.u_arr.as_ref() {
799 Some(arr.clone()) // c:2004 getaparam
800 } else if let Some(s) = pm.u_str.as_ref() {
801 Some(vec![s.clone()]) // c:2009 getsparam
802 } else {
803 None
804 }
805 })
806 };
807 crate::ported::signals::unqueue_signals(); // c:2022
808 result
809 }
810}
811
812// =====================================================================
813// get_data_arr — `Src/Zle/compcore.c:2022`.
814// =====================================================================
815
816/// Direct port of `static char **get_data_arr(char *name, int keys)`
817/// from `Src/Zle/compcore.c:2022`. C uses `fetchvalue` with
818/// `SCANPM_WANTKEYS`/`SCANPM_WANTVALS` + `SCANPM_MATCHMANY` to scan
819/// an associative-array parameter and return either its keys or its
820/// values as a flat array. Without `fetchvalue` ported with full
821/// SCANPM flag support, we go straight to the hashed-storage
822/// thread-local maintained by params.rs for assoc-arrays.
823pub fn get_data_arr(name: &str, keys: bool) -> Option<Vec<String>> { // c:2022
824 use crate::ported::params::{paramtab, paramtab_hashed_storage};
825 use crate::ported::zsh_h::{PM_HASHED, PM_TYPE};
826
827 crate::ported::signals::queue_signals(); // c:2028
828
829 // c:2030-2034 — fetchvalue with SCANPM_MATCHMANY → scan the
830 // hashed param's keys/values. We approximate by
831 // routing keys/values directly out of the
832 // hashed-storage map.
833 let is_hashed = match paramtab().read() {
834 Ok(t) => t.get(name)
835 .map(|pm| PM_TYPE(pm.node.flags as u32) == PM_HASHED)
836 .unwrap_or(false),
837 Err(_) => false,
838 };
839
840 let result = if is_hashed {
841 paramtab_hashed_storage().lock().ok().and_then(|m| {
842 m.get(name).map(|map| {
843 if keys {
844 map.keys().cloned().collect::<Vec<_>>()
845 } else {
846 map.values().cloned().collect::<Vec<_>>()
847 }
848 })
849 })
850 } else {
851 // c:2032 — non-hashed names return NULL.
852 None
853 };
854
855 crate::ported::signals::unqueue_signals(); // c:2041
856 result
857}
858
859// =====================================================================
860// addmatch — `Src/Zle/compcore.c:2041`.
861// =====================================================================
862
863/// Port of `static void addmatch(char *str, int flags, char ***dispp,
864/// int line)` from compcore.c:2041.
865pub fn addmatch(str: &str, flags: i32, disp: Option<&str>, line: bool) { // c:2041
866 let mut cm = Cmatch::default(); // c:2041
867 cm.str = Some(str.to_string()); // c:2047
868 // c:2049-2051 — inline read of `complist` parameter, parse `packed`/
869 // `rows` substrings into CMF_PACKED/CMF_ROWS flag bits.
870 let complist_extra = {
871 let s = COMPLIST.get_or_init(|| Mutex::new(String::new()))
872 .lock().map(|g| g.clone()).unwrap_or_default();
873 let packed = if s.contains("packed") { CMF_PACKED } else { 0 }; // c:2050
874 let rows = if s.contains("rows") { CMF_ROWS } else { 0 }; // c:2051
875 if s.is_empty() { 0 } else { packed | rows }
876 };
877 cm.flags = flags | complist_extra; // c:2048
878 if let Some(d) = disp { // c:2052
879 cm.disp = Some(d.to_string()); // c:2056
880 } else if line { // c:2057
881 cm.disp = Some(String::new()); // c:2058
882 cm.flags |= CMF_DISPLINE; // c:2059
883 }
884 mnum.fetch_add(1, Ordering::Relaxed); // c:2061
885 {
886 let cell = curexpl.get_or_init(|| Mutex::new(None)); // c:2063
887 if let Ok(mut g) = cell.lock() {
888 if let Some(e) = g.as_mut() { e.count += 1; }
889 }
890 }
891 let mcell = matches.get_or_init(|| Mutex::new(Vec::new())); // c:2066
892 if let Ok(mut g) = mcell.lock() { g.push(cm); }
893 newmatches.store(1, Ordering::Relaxed); // c:2068
894 {
895 let cell = mgroup.get_or_init(|| Mutex::new(None)); // c:2069
896 if let Ok(mut g) = cell.lock() {
897 if let Some(grp) = g.as_mut() { grp.new_ = 1; }
898 }
899 }
900}
901
902// `lookup_complist_flags` deleted — Rust-only 8-line helper. Inlined
903// at the single call site in callcompfunc (c:2049-2051).
904
905// =====================================================================
906// begcmgroup — `Src/Zle/compcore.c:3073`.
907// =====================================================================
908
909/// Port of `mod_export void begcmgroup(char *n, int flags)` from
910/// compcore.c:3073.
911pub fn begcmgroup(n: Option<&str>, flags: i32) { // c:3073
912 if let Some(name) = n { // c:3073
913 let mask = CGF_NOSORT | CGF_UNIQALL | CGF_UNIQCON // c:3085
914 | CGF_MATSORT | CGF_NUMSORT | CGF_REVSORT;
915 let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
916 if let Ok(g) = cell.lock() {
917 for grp in g.iter() { // c:3078
918 if grp.name.as_deref() == Some(name) // c:3084-3087
919 && (grp.flags & mask) == flags
920 {
921 let active = grp.clone(); // c:3088
922 let mc = mgroup.get_or_init(|| Mutex::new(None));
923 if let Ok(mut s) = mc.lock() { *s = Some(active); }
924 return; // c:3095
925 }
926 }
927 }
928 }
929 let mut grp = Cmgroup::default(); // c:3101
930 grp.name = n.map(String::from); // c:3105
931 grp.flags = flags; // c:3108
932 let cell = amatches.get_or_init(|| Mutex::new(Vec::new()));
933 if let Ok(mut g) = cell.lock() {
934 g.insert(0, grp.clone()); // c:3121-3124
935 }
936 let mc = mgroup.get_or_init(|| Mutex::new(None));
937 if let Ok(mut s) = mc.lock() { *s = Some(grp); }
938 if let Ok(mut g) = expls.get_or_init(|| Mutex::new(Vec::new())).lock() { g.clear(); }
939 if let Ok(mut g) = matches.get_or_init(|| Mutex::new(Vec::new())).lock() { g.clear(); }
940 if let Ok(mut g) = fmatches.get_or_init(|| Mutex::new(Vec::new())).lock() { g.clear(); }
941 if let Ok(mut g) = allccs.get_or_init(|| Mutex::new(Vec::new())).lock() { g.clear(); }
942}
943
944// =====================================================================
945// endcmgroup — `Src/Zle/compcore.c:3131`.
946// =====================================================================
947
948/// Port of `mod_export void endcmgroup(char **ylist)` from
949/// compcore.c:3131.
950pub fn endcmgroup(ylist: Option<Vec<String>>) { // c:3131
951 let cell = mgroup.get_or_init(|| Mutex::new(None));
952 if let Ok(mut g) = cell.lock() {
953 if let Some(grp) = g.as_mut() {
954 grp.ylist = ylist.unwrap_or_default(); // c:3140
955 }
956 }
957}
958
959// =====================================================================
960// addexpl — `Src/Zle/compcore.c:3140`.
961// =====================================================================
962
963/// Port of `mod_export void addexpl(int always)` from compcore.c:3140.
964pub fn addexpl(always: bool) { // c:3140
965 let curexpl_snap = {
966 let cell = curexpl.get_or_init(|| Mutex::new(None));
967 cell.lock().ok().and_then(|g| g.clone())
968 };
969 let curexpl_str = match curexpl_snap.as_ref().and_then(|e| e.str.clone()) {
970 Some(s) => s,
971 None => return,
972 };
973 let curexpl_count = curexpl_snap.as_ref().map(|e| e.count).unwrap_or(0);
974 let curexpl_fcount = curexpl_snap.as_ref().map(|e| e.fcount).unwrap_or(0);
975
976 let elist = expls.get_or_init(|| Mutex::new(Vec::new()));
977 if let Ok(mut g) = elist.lock() {
978 for e in g.iter_mut() { // c:3145
979 if e.str.as_deref() == Some(curexpl_str.as_str()) { // c:3147
980 e.count += curexpl_count; // c:3148
981 e.fcount += curexpl_fcount; // c:3149
982 if always { // c:3150
983 e.always = 1;
984 nmessages.fetch_add(1, Ordering::Relaxed); // c:3152
985 newmatches.store(1, Ordering::Relaxed); // c:3153
986 let mc = mgroup.get_or_init(|| Mutex::new(None));
987 if let Ok(mut mg) = mc.lock() {
988 if let Some(grp) = mg.as_mut() { grp.new_ = 1; }
989 }
990 }
991 return; // c:3156
992 }
993 }
994 if let Some(e) = curexpl_snap { // c:3159
995 g.push(e);
996 }
997 }
998 newmatches.store(1, Ordering::Relaxed); // c:3160
999 if always { // c:3161
1000 let mc = mgroup.get_or_init(|| Mutex::new(None));
1001 if let Ok(mut mg) = mc.lock() {
1002 if let Some(grp) = mg.as_mut() { grp.new_ = 1; }
1003 }
1004 nmessages.fetch_add(1, Ordering::Relaxed); // c:3173
1005 }
1006}
1007
1008// =====================================================================
1009// matchcmp — `Src/Zle/compcore.c:3173`.
1010// =====================================================================
1011
1012/// Port of `static int matchcmp(Cmatch *a, Cmatch *b)` from
1013/// compcore.c:3173.
1014pub fn matchcmp(a: &Cmatch, b: &Cmatch) -> std::cmp::Ordering { // c:3173
1015 let order = MATCHORDER.load(Ordering::Relaxed);
1016 let sortdir = if (order & CGF_REVSORT) != 0 { -1 } else { 1 }; // c:3177
1017
1018 let cmp = (b.disp.is_some() as i32) - (a.disp.is_some() as i32); // c:3176
1019 let (as_, bs) = if (order & CGF_MATSORT) != 0 || (cmp == 0 && a.disp.is_none()) {
1020 (a.str.clone().unwrap_or_default(), // c:3181
1021 b.str.clone().unwrap_or_default()) // c:3182
1022 } else {
1023 if cmp != 0 { // c:3184
1024 let raw = (cmp as i32) * sortdir;
1025 return if raw < 0 { std::cmp::Ordering::Less } // c:3185
1026 else if raw > 0 { std::cmp::Ordering::Greater }
1027 else { std::cmp::Ordering::Equal };
1028 }
1029 let displine_cmp = (b.flags & CMF_DISPLINE) - (a.flags & CMF_DISPLINE); // c:3187
1030 if displine_cmp != 0 { // c:3188
1031 let raw = displine_cmp * sortdir;
1032 return if raw < 0 { std::cmp::Ordering::Less }
1033 else if raw > 0 { std::cmp::Ordering::Greater }
1034 else { std::cmp::Ordering::Equal };
1035 }
1036 (a.disp.clone().unwrap_or_default(), // c:3191
1037 b.disp.clone().unwrap_or_default()) // c:3192
1038 };
1039 let raw = sortdir * if as_ == bs { 0 } else if as_ < bs { -1 } else { 1 };
1040 if raw < 0 { std::cmp::Ordering::Less } // c:3195
1041 else if raw > 0 { std::cmp::Ordering::Greater }
1042 else { std::cmp::Ordering::Equal }
1043}
1044
1045// =====================================================================
1046// matcheq — `Src/Zle/compcore.c:3203-3215`.
1047// =====================================================================
1048
1049#[inline]
1050fn matchstreq(a: Option<&String>, b: Option<&String>) -> bool { // c:3207
1051 match (a, b) {
1052 (None, None) => true,
1053 (Some(x), Some(y)) => x == y,
1054 _ => false,
1055 }
1056}
1057
1058/// Port of `static int matcheq(Cmatch a, Cmatch b)` from
1059/// compcore.c:3206.
1060pub fn matcheq(a: &Cmatch, b: &Cmatch) -> bool { // c:3207
1061 matchstreq(a.ipre.as_ref(), b.ipre.as_ref()) && // c:3207
1062 matchstreq(a.pre.as_ref(), b.pre.as_ref()) && // c:3210
1063 matchstreq(a.ppre.as_ref(), b.ppre.as_ref()) && // c:3211
1064 matchstreq(a.psuf.as_ref(), b.psuf.as_ref()) && // c:3212
1065 matchstreq(a.suf.as_ref(), b.suf.as_ref()) && // c:3213
1066 matchstreq(a.str.as_ref(), b.str.as_ref()) // c:3214
1067}
1068
1069// =====================================================================
1070// freematch / freematches — `Src/Zle/compcore.c:3575 / 3605`.
1071// =====================================================================
1072
1073/// Port of `void freematch(Cmatch m, int *cl, int rec)` from
1074/// compcore.c:3575. Rust's `Drop` covers it.
1075pub fn freematch(_m: Cmatch) { // c:3575
1076}
1077
1078/// Port of `mod_export void freematches(Cmgroup g, int cl)` from
1079/// compcore.c:3605. Rust's `Drop` covers it.
1080pub fn freematches(_g: Vec<Cmgroup>) { // c:3605
1081}
1082
1083// =====================================================================
1084// Substrate-blocked stubs — bodies need substrate listed in each
1085// doc comment. Returns shape-correct safe defaults.
1086// =====================================================================
1087
1088// =====================================================================
1089// do_completion — `Src/Zle/compcore.c:287`.
1090// =====================================================================
1091
1092/// Direct port of `int do_completion(Hookdef dummy, Compldat dat)`
1093/// from compcore.c:287. The top-level completion driver: per-round
1094/// state reset → `makecomplist` → dispatch to `do_ambiguous` /
1095/// `do_single` / `do_allmatches` per result count.
1096pub fn do_completion(s: &str, incmd: i32, lst: i32) -> i32 { // c:287
1097
1098 let osl = crate::ported::zle::zle_refresh::SHOWINGLIST.load(Ordering::Relaxed); // c:289
1099 let mut ret: i32 = 0; // c:289
1100
1101 // c:296-297 — `ainfo = fainfo = NULL`.
1102 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() { *g = None; }
1103 if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() { *g = None; }
1104 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
1105 g.clear(); // c:298
1106 }
1107
1108 // c:300-307 — compqstack reset.
1109 let instring = crate::ported::zle::zle_tricky::INSTRING.load(Ordering::Relaxed); // c:307
1110 // c:305 — `compqstack = instring == QT_NONE ? "\\" : <quote-char>`.
1111 // Inlined `char_from_qt(x)` as `(x as u8) as char`.
1112 let head_q: char = if instring == crate::ported::zsh_h::QT_NONE { // c:305
1113 crate::ported::zsh_h::QT_BACKSLASH as u8 as char
1114 } else {
1115 instring as u8 as char
1116 };
1117 if let Ok(mut g) = compqstack.get_or_init(|| Mutex::new(String::new())).lock() {
1118 *g = head_q.to_string(); // c:305-306
1119 }
1120
1121 hasunqu.store(0, Ordering::Relaxed); // c:309
1122 let wouldinstab_v = WOULDINSTAB.load(Ordering::Relaxed); // c:310
1123 useline.store( // c:310
1124 if wouldinstab_v != 0 { -1 } else if lst != crate::ported::zle::zle_h::COMP_LIST_COMPLETE { 1 } else { 0 },
1125 Ordering::Relaxed,
1126 );
1127 useexact.store(opt_isset("RECEXACT"), Ordering::Relaxed); // c:311
1128 set_compstate_str("exact_string", ""); // c:312
1129 let useline_v = useline.load(Ordering::Relaxed);
1130 uselist.store( // c:314
1131 if useline_v != 0 {
1132 if opt_isset("AUTOLIST") != 0 && opt_isset("BASHAUTOLIST") == 0 {
1133 if opt_isset("LISTAMBIGUOUS") != 0 { 3 } else { 2 }
1134 } else { 0 }
1135 } else { 1 },
1136 Ordering::Relaxed,
1137 );
1138
1139 let useglob_v = USEGLOB.load(Ordering::Relaxed); // c:319
1140 let opm: String = if useglob_v != 0 { "*".into() } else { "".into() };
1141 if let Ok(mut g) = comppatmatch.get_or_init(|| Mutex::new(None)).lock() {
1142 *g = Some(opm.clone()); // c:319
1143 }
1144 set_compstate_str("pattern_insert", "menu"); // c:320
1145 forcelist.store(0, Ordering::Relaxed); // c:322
1146 haspattern.store(0, Ordering::Relaxed); // c:323
1147 let _complistmax = env_iparam("LISTMAX"); // c:324
1148
1149 set_compstate_str( // c:326
1150 "last_prompt",
1151 if opt_isset("ALWAYSLASTPROMPT") != 0 { "yes" } else { "" },
1152 );
1153 dolastprompt.store(1, Ordering::Relaxed); // c:327
1154
1155 // c:329-330 — complist string.
1156 let cl_str = if opt_isset("LISTROWSFIRST") != 0 {
1157 if opt_isset("LISTPACKED") != 0 { "packed rows" } else { "rows" }
1158 } else if opt_isset("LISTPACKED") != 0 { "packed" } else { "" };
1159 if let Ok(mut g) = crate::ported::zle::complete::COMPLIST
1160 .get_or_init(|| Mutex::new(String::new())).lock()
1161 {
1162 *g = cl_str.into(); // c:329
1163 }
1164 startauto.store(opt_isset("AUTOMENU"), Ordering::Relaxed); // c:331
1165
1166 let zlc = ZLEMETACS.load(Ordering::Relaxed);
1167 let we_v = WE.load(Ordering::Relaxed);
1168 movetoend.store( // c:332
1169 if zlc == we_v || opt_isset("ALWAYSTOEND") != 0 { 2 } else { 1 },
1170 Ordering::Relaxed,
1171 );
1172 crate::ported::zle::zle_refresh::SHOWINGLIST.store(0, Ordering::Relaxed); // c:333
1173 hasmatched.store(0, Ordering::Relaxed); // c:334
1174 hasunmatched.store(0, Ordering::Relaxed); // c:334
1175 minmlen.store(1_000_000, Ordering::Relaxed); // c:335
1176 maxmlen.store(-1, Ordering::Relaxed); // c:336
1177 nmessages.store(0, Ordering::Relaxed); // c:338
1178 hasallmatch.store(0, Ordering::Relaxed); // c:339
1179
1180 // c:342 — main dispatch.
1181 if makecomplist(s, incmd, lst) != 0 { // c:342
1182 // c:344 — error path.
1183 ZLEMETACS.store(0, Ordering::Relaxed); // c:344
1184 foredel(ZLEMETALL.load(Ordering::Relaxed)); // c:345
1185 inststr(&crate::ported::zle::zle_tricky::ORIGLINE.get_or_init(|| Mutex::new(String::new())).lock().map(|g| g.clone()).unwrap_or_default()); // c:346
1186 ZLEMETACS.store(crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:347
1187 crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed); // c:348
1188 ret = 1;
1189 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; } // c:350
1190 if useline.load(Ordering::Relaxed) < 0 { // c:351
1191 unmetafy_line();
1192 ret = selfinsert(); // c:353
1193 metafy_line();
1194 }
1195 return goto_compend(ret); // c:356 goto compend
1196 }
1197
1198 // c:359-361 — clear lastprebr/lastpostbr.
1199 lastprebr_set(""); // c:359
1200 lastpostbr_set(""); // c:360
1201
1202 let curpm = comppatmatch.get_or_init(|| Mutex::new(None))
1203 .lock().ok().and_then(|g| g.clone()).unwrap_or_default();
1204 if !curpm.is_empty() && curpm != opm { // c:363
1205 haspattern.store(1, Ordering::Relaxed); // c:364
1206 }
1207 let nm = nmatches.load(Ordering::Relaxed); // c:366
1208 let dm = diffmatches.load(Ordering::Relaxed);
1209 if iforcemenu.load(Ordering::Relaxed) != 0 { // c:366
1210 if nm != 0 { { let _ = crate::ported::zle::compresult::do_ambig_menu(); }; } // c:367
1211 ret = if nm == 0 { 1 } else { 0 }; // c:369
1212 } else if useline.load(Ordering::Relaxed) < 0 { // c:370
1213 unmetafy_line();
1214 ret = selfinsert(); // c:372
1215 metafy_line();
1216 } else if useline.load(Ordering::Relaxed) == 0
1217 && uselist.load(Ordering::Relaxed) != 0
1218 { // c:374
1219 ZLEMETACS.store(0, Ordering::Relaxed); // c:375
1220 foredel(ZLEMETALL.load(Ordering::Relaxed)); // c:376
1221 inststr(&crate::ported::zle::zle_tricky::ORIGLINE.get_or_init(|| Mutex::new(String::new())).lock().map(|g| g.clone()).unwrap_or_default()); // c:377
1222 ZLEMETACS.store(crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:378
1223 crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed); // c:379
1224 } else if useline.load(Ordering::Relaxed) == 2 && nm > 1 { // c:380
1225 // c:381 — `do_allmatches(1)`. Inlined: build flat match list
1226 // from `amatches` and dispatch to compresult::do_allmatches.
1227 {
1228 let groups = amatches.get_or_init(|| Mutex::new(Vec::new()))
1229 .lock().map(|g| g.clone()).unwrap_or_default();
1230 let mut all: Vec<String> = Vec::new();
1231 for g in groups {
1232 for m in g.matches {
1233 if let Some(s) = m.str { all.push(s); }
1234 }
1235 }
1236 let buf = ZLEMETALINE.get_or_init(|| Mutex::new(String::new()))
1237 .lock().map(|g| g.clone()).unwrap_or_default();
1238 let cs = ZLEMETACS.load(Ordering::Relaxed) as usize;
1239 let wb = WB.load(Ordering::Relaxed) as usize;
1240 let we = WE.load(Ordering::Relaxed) as usize;
1241 let (new_buf, new_cs) = crate::ported::zle::compresult::do_allmatches(
1242 &buf, cs, wb, we, &all, " ",
1243 );
1244 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1245 *g = new_buf;
1246 ZLEMETALL.store(g.len() as i32, Ordering::Relaxed);
1247 }
1248 ZLEMETACS.store(new_cs as i32, Ordering::Relaxed);
1249 }
1250 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; } // c:383
1251 if forcelist.load(Ordering::Relaxed) != 0 { // c:385
1252 crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);
1253 } else {
1254 crate::ported::zle::zle_h::invalidatelist(); // c:388
1255 }
1256 } else if useline.load(Ordering::Relaxed) != 0 { // c:389
1257 if nm > 1 && dm != 0 { // c:391
1258 // c:393 — `ret = do_ambiguous()`. Inlined: flatten `amatches`
1259 // into &[String] and dispatch.
1260 ret = {
1261 let groups = amatches.get_or_init(|| Mutex::new(Vec::new()))
1262 .lock().map(|g| g.clone()).unwrap_or_default();
1263 let all: Vec<String> = groups.into_iter()
1264 .flat_map(|g| g.matches.into_iter().filter_map(|m| m.str))
1265 .collect();
1266 crate::ported::zle::compresult::do_ambiguous(&all)
1267 };
1268 if crate::ported::zle::zle_refresh::SHOWINGLIST.load(Ordering::Relaxed) == 0
1269 && uselist.load(Ordering::Relaxed) != 0
1270 && crate::ported::zle::zle_refresh::LISTSHOWN.load(Ordering::Relaxed) != 0
1271 && (crate::ported::zle::zle_tricky::USEMENU
1272 .load(Ordering::Relaxed) == 2
1273 || oldlist.load(Ordering::Relaxed) != 0)
1274 {
1275 crate::ported::zle::zle_refresh::SHOWINGLIST.store(osl, Ordering::Relaxed); // c:395
1276 }
1277 } else if nm == 1 || (nm > 1 && dm == 0) { // c:396
1278 do_single_first_match(); // c:399-411
1279 if forcelist.load(Ordering::Relaxed) != 0 { // c:412
1280 if uselist.load(Ordering::Relaxed) != 0 {
1281 crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);
1282 } else {
1283 crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed);
1284 }
1285 } else {
1286 crate::ported::zle::zle_h::invalidatelist(); // c:418
1287 }
1288 } else if nmessages.load(Ordering::Relaxed) != 0
1289 && forcelist.load(Ordering::Relaxed) != 0
1290 { // c:419
1291 if uselist.load(Ordering::Relaxed) != 0 {
1292 crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed);
1293 } else {
1294 crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed);
1295 }
1296 }
1297 } else { // c:425
1298 crate::ported::zle::zle_h::invalidatelist(); // c:426
1299 crate::ported::zle::zle_tricky::LASTAMBIG.store( // c:427
1300 opt_isset("BASHAUTOLIST"),
1301 Ordering::Relaxed,
1302 );
1303 if forcelist.load(Ordering::Relaxed) != 0 { crate::ported::zle::zle_refresh::CLEARLIST.store(1, Ordering::Relaxed); } // c:428
1304 ZLEMETACS.store(0, Ordering::Relaxed); // c:429
1305 foredel(ZLEMETALL.load(Ordering::Relaxed)); // c:430
1306 inststr(&crate::ported::zle::zle_tricky::ORIGLINE.get_or_init(|| Mutex::new(String::new())).lock().map(|g| g.clone()).unwrap_or_default()); // c:431
1307 ZLEMETACS.store(crate::ported::zle::zle_tricky::ORIGCS.load(Ordering::Relaxed), Ordering::Relaxed); // c:432
1308 }
1309
1310 // c:436 — explanation strings.
1311 if crate::ported::zle::zle_refresh::SHOWINGLIST.load(Ordering::Relaxed) == 0
1312 && crate::ported::zle::zle_tricky::VALIDLIST.load(Ordering::Relaxed) != 0
1313 && crate::ported::zle::zle_tricky::USEMENU.load(Ordering::Relaxed) != 2
1314 && uselist.load(Ordering::Relaxed) != 0
1315 && (nm != 1 || dm != 0)
1316 && useline.load(Ordering::Relaxed) >= 0
1317 && useline.load(Ordering::Relaxed) != 2
1318 && (oldlist.load(Ordering::Relaxed) == 0 || crate::ported::zle::zle_refresh::LISTSHOWN.load(Ordering::Relaxed) == 0)
1319 {
1320 onlyexpl.store(3, Ordering::Relaxed); // c:441
1321 crate::ported::zle::zle_refresh::SHOWINGLIST.store(-2, Ordering::Relaxed); // c:442
1322 }
1323
1324 goto_compend(ret)
1325}
1326
1327/// First-match shortcut path from compcore.c:398-411. `Cmgroup m = amatches;
1328/// while (!m->mcount) m = m->next; do_single(m->matches[0])`.
1329fn do_single_first_match() { // c:398
1330 let groups = amatches.get_or_init(|| Mutex::new(Vec::new()))
1331 .lock().ok().map(|g| g.clone()).unwrap_or_default();
1332 let first = groups.into_iter().find(|g| g.mcount > 0)
1333 .and_then(|g| g.matches.first().cloned());
1334 if let Some(m) = first {
1335 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; } // c:407
1336 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.asked = 0; } // c:408
1337 // c:409 — `do_single(m)`. Inlined: drop the Cmatch payload onto
1338 // MINFO.cur so the listing path picks it up (matches the C
1339 // behavior of routing the single-match insert through minfo).
1340 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() {
1341 g.cur = Some(Box::new(m));
1342 }
1343 }
1344}
1345
1346/// compcore.c:444 `compend:` epilogue — free matchers, snap zlemetacs.
1347fn goto_compend(ret: i32) -> i32 { // c:444
1348 if let Ok(mut g) = matchers.get_or_init(|| Mutex::new(Vec::new())).lock() {
1349 g.clear(); // c:445-446 freecmatcher loop
1350 }
1351 let line_len = ZLEMETALL.load(Ordering::Relaxed); // c:448 strlen(zlemetaline)
1352 if ZLEMETACS.load(Ordering::Relaxed) > line_len { // c:449
1353 ZLEMETACS.store(line_len, Ordering::Relaxed); // c:450
1354 }
1355 ret // c:453
1356}
1357
1358// `COMP_LIST_COMPLETE` / `QT_NONE_STUB` / `QT_BACKSLASH_STUB` local
1359// aliases deleted — call sites now reach the real C-side constants
1360// directly (`crate::ported::zle::zle_h::COMP_LIST_COMPLETE`,
1361// `crate::ported::zsh_h::QT_NONE`, `crate::ported::zsh_h::QT_BACKSLASH`).
1362// The local `COMP_LIST_COMPLETE = 2` was a value-mismatch bug (the
1363// real constant is 1 per `Src/Zle/zle.h:357`).
1364
1365// `char_from_qt` deleted — Rust-only 1-line `(qt as u8) as char`
1366// helper. Inlined at the two call sites in get_compstate_str.
1367
1368// `showinglist_stub` / `showinglist_set` / `clearlist_set` /
1369// `listshown_stub` / `instring_stub` deleted — Rust-only 1-line
1370// accessors for C globals (SHOWINGLIST / CLEARLIST / LISTSHOWN /
1371// INSTRING). C reads/writes the bare globals inline; callers in
1372// compcore.rs now do `<GLOBAL>.load(Ordering::Relaxed)` /
1373// `<GLOBAL>.store(v, Ordering::Relaxed)` directly.
1374/// Direct port of `foredel(int ct, int flags)` from
1375/// `Src/Zle/zle_utils.c:1105`. Deletes `ct` chars forward from
1376/// `ZLEMETACS` in the global metafied line. Operates on the
1377/// `ZLEMETALINE` global rather than a `&mut Zle` handle since
1378/// compcore's call site (compcore.c:344-355 error-recovery) drives
1379/// the global ZLE buffer directly.
1380/// WARNING: param names don't match C — Rust=(ct) vs C=(ct, flags)
1381fn foredel(ct: i32) { // zle_utils.c:1105
1382 if ct <= 0 { return; }
1383 let cs = ZLEMETACS.load(Ordering::Relaxed) as usize;
1384 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1385 let bytes = g.as_bytes();
1386 if cs >= bytes.len() { return; }
1387 let end = (cs + ct as usize).min(bytes.len());
1388 // c:1108-1115 — splice out [cs..end).
1389 let new_line: String = String::from_utf8_lossy(&bytes[..cs]).into_owned()
1390 + &String::from_utf8_lossy(&bytes[end..]);
1391 let new_len = new_line.len() as i32;
1392 *g = new_line;
1393 ZLEMETALL.store(new_len, Ordering::Relaxed);
1394 }
1395}
1396
1397/// Direct port of `inststr(char *s)` from `Src/Zle/zle_tricky.c:278`.
1398/// Inserts `s` at `ZLEMETACS` in the global metafied line.
1399/// Direct port of `#define inststr(X) inststrlen((X),1,-1)` from
1400/// `Src/Zle/zle_tricky.c:57`. Inserts `s` at `ZLEMETACS` in the
1401/// global metafied line; cursor advances by `s.len()`.
1402/// WARNING: param names don't match C — Rust=(s) vs C=()
1403fn inststr(s: &str) { // c:57
1404 if s.is_empty() { return; }
1405 let cs = ZLEMETACS.load(Ordering::Relaxed) as usize;
1406 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1407 let bytes = g.as_bytes();
1408 let cs = cs.min(bytes.len());
1409 let new_line: String = String::from_utf8_lossy(&bytes[..cs]).into_owned()
1410 + s
1411 + &String::from_utf8_lossy(&bytes[cs..]);
1412 let new_len = new_line.len() as i32;
1413 *g = new_line;
1414 ZLEMETALL.store(new_len, Ordering::Relaxed);
1415 ZLEMETACS.store(cs as i32 + s.len() as i32, Ordering::Relaxed);
1416 }
1417}
1418// `origline_stub` / `origcs_stub` deleted — Rust-only 1-line
1419// accessors for the `ORIGLINE` / `ORIGCS` globals (ports of C
1420// `origline` / `origcs` at zle_tricky.c:75 etc.). C reads these
1421// globals inline; callers in compcore.rs now do the lock/load
1422// directly.
1423/// Direct port of `void unmetafy_line(void)` from `zle_tricky.c:995`.
1424/// Reads `ZLEMETALINE`, runs `unmetafy_line(...)` from zle_tricky.rs,
1425/// stores result into `ZLELINE` + updates `ZLECS`/`ZLELL`.
1426fn unmetafy_line() { // zle_tricky.c:995
1427 let meta = ZLEMETALINE.get_or_init(|| Mutex::new(String::new()))
1428 .lock().map(|g| g.clone()).unwrap_or_default();
1429 let unmeta = crate::ported::zle::zle_tricky::unmetafy_line(&meta);
1430 let new_len = unmeta.len() as i32;
1431 let cs = ZLEMETACS.load(Ordering::Relaxed); // c:978-1000
1432 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
1433 *g = unmeta;
1434 }
1435 ZLELL.store(new_len, Ordering::Relaxed);
1436 ZLECS.store(cs.min(new_len), Ordering::Relaxed);
1437}
1438
1439/// Direct port of `void metafy_line(void)` from `zle_tricky.c:978`.
1440/// Reads `ZLELINE`, runs `metafy_line(...)` from zle_tricky.rs, stores
1441/// result into `ZLEMETALINE` + updates `ZLEMETACS`/`ZLEMETALL`.
1442fn metafy_line() { // zle_tricky.c:978
1443 let raw = ZLELINE.get_or_init(|| Mutex::new(String::new()))
1444 .lock().map(|g| g.clone()).unwrap_or_default();
1445 let meta = crate::ported::zle::zle_tricky::metafy_line(&raw);
1446 let new_len = meta.len() as i32;
1447 let cs = ZLECS.load(Ordering::Relaxed);
1448 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
1449 *g = meta;
1450 }
1451 ZLEMETALL.store(new_len, Ordering::Relaxed);
1452 ZLEMETACS.store(cs.min(new_len), Ordering::Relaxed);
1453}
1454
1455/// Direct port of `int selfinsert(char **args)` from `Src/Zle/zle_misc.c:113`.
1456/// Inserts `lastchar` from ZLE state at cursor. Without a `&mut Zle`
1457/// handle we operate on the global `ZLELINE` + a thread-local
1458/// lastchar holder. Equivalent C body: insert one char at zlecs,
1459/// advance zlecs, bump zlell.
1460fn selfinsert() -> i32 { // zle_misc.c:112
1461 let ch = LASTCHAR.load(Ordering::Relaxed); // c:113
1462 if ch < 0 { return 1; } // c:116 EOF
1463 let cs = ZLECS.load(Ordering::Relaxed) as usize;
1464 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
1465 let mut bytes = g.as_bytes().to_vec();
1466 let cs = cs.min(bytes.len());
1467 // c:130 — insertion at cs.
1468 if (ch as u32) < 128 {
1469 bytes.insert(cs, ch as u8);
1470 } else if let Some(c) = char::from_u32(ch as u32) {
1471 let mut buf = [0u8; 4];
1472 let enc = c.encode_utf8(&mut buf).as_bytes();
1473 for (i, b) in enc.iter().enumerate() {
1474 bytes.insert(cs + i, *b);
1475 }
1476 }
1477 *g = String::from_utf8_lossy(&bytes).into_owned();
1478 let new_len = g.len() as i32;
1479 ZLELL.store(new_len, Ordering::Relaxed);
1480 ZLECS.store((cs + 1) as i32, Ordering::Relaxed);
1481 }
1482 0 // c:141
1483}
1484
1485/// Port of `mod_export int lastchar` from `Src/Zle/zle_main.c`. Last
1486/// keyboard char consumed by the binding loop — read by `selfinsert`.
1487pub static LASTCHAR: AtomicI32 = AtomicI32::new(0); // zle_main.c
1488// minfo_clear_cur / minfo_asked_zero deleted — Rust-only 2-line
1489// wrappers around C's inline writes `minfo.cur = NULL` and
1490// `minfo.asked = 0`. All call sites inlined.
1491
1492/// Direct port of `struct menuinfo minfo` — `Src/Zle/zle_tricky.c`
1493/// (the single file-scope instance). The struct type itself lives
1494/// in `comp_h.rs::Menuinfo` (port of comp.h:284-295).
1495pub static MINFO: OnceLock<Mutex<crate::ported::zle::comp_h::Menuinfo>> = OnceLock::new(); // zle_tricky.c minfo
1496
1497// `set_minfo_cur` deleted — Rust-only wrapper for the C inline
1498// write `minfo.cur = &m;`. Callers should inline the
1499// `MINFO.lock().cur = Some(Box::new(m))` write directly.
1500// do_ambig_menu_stub deleted — inlined as
1501// `{ let _ = crate::ported::zle::compresult::do_ambig_menu(); }`
1502// at the single call site (c:367).
1503// do_ambiguous_stub / do_single_stub / do_allmatches_stub /
1504// invalidatelist_stub deleted — Rust-only glue wrappers, all
1505// inlined at their (single) call sites in do_completion / dupmatch.
1506// The real C names live as `pub fn` in compresult.rs / zle_h.rs.
1507fn opt_isset(name: &str) -> i32 { // options.c
1508 if crate::ported::options::opt_state_get(name).unwrap_or(false) { 1 } else { 0 }
1509}
1510/// Real call into `getiparam(name)` — the canonical paramtab read.
1511/// Mirrors C's `getiparam` at params.c:3044 which reads the global
1512/// `paramtab` directly via `gethashnode2`.
1513fn env_iparam(name: &str) -> i32 { // params.c:3044
1514 crate::ported::params::getiparam(name) as i32
1515}
1516fn lastprebr_set(s: &str) { // zle_tricky.c lastprebr
1517 if let Ok(mut g) = crate::ported::zle::zle_tricky::LASTPREBR
1518 .get_or_init(|| Mutex::new(String::new())).lock()
1519 {
1520 *g = s.to_string();
1521 }
1522}
1523fn lastpostbr_set(s: &str) { // zle_tricky.c lastpostbr
1524 if let Ok(mut g) = crate::ported::zle::zle_tricky::LASTPOSTBR
1525 .get_or_init(|| Mutex::new(String::new())).lock()
1526 {
1527 *g = s.to_string();
1528 }
1529}
1530
1531
1532// =====================================================================
1533// callcompfunc — `Src/Zle/compcore.c:544`.
1534// =====================================================================
1535
1536/// Port of `static void callcompfunc(char *s, char *fn)` from
1537/// compcore.c:544. Selects the `$compstate[context]` value, then
1538/// dispatches into the user shell function `fn`. Paramtab setup
1539/// (`comprpms`/`compkpms`) + result-readback is stubbed locally
1540/// per PORT.md Rule 9 until `params.c` substrate lands.
1541pub fn callcompfunc(s: &str, fn_name: &str) { // c:544
1542
1543 if fn_name.is_empty() { return; } // c:552 getshfunc(NULL)
1544 let _lv = crate::ported::builtin::LASTVAL.load(Ordering::Relaxed); // c:548 int lv = lastval
1545 let _icf = crate::ported::utils::INCOMPFUNC.load(Ordering::Relaxed); // c:555
1546 let _osc = crate::ported::builtin::SFCONTEXT.load(Ordering::Relaxed); // c:555
1547
1548 let _useglob = USEGLOB.load(Ordering::Relaxed); // c:579
1549
1550 // c:591-617 — context selection.
1551 let context = compcontext_for(s); // c:591-617
1552 set_compstate_str("context", &context); // c:619
1553
1554 // c:721-727 — `$compstate[last_prompt]` etc. fed in from
1555 // do_completion via dolastprompt; we forward the current values.
1556 set_compstate_str(
1557 "last_prompt",
1558 if dolastprompt.load(Ordering::Relaxed) != 0 { "yes" } else { "" },
1559 );
1560
1561 // c:740-749 — `$compstate[list]` — set from `complist` global.
1562 let cl_value = crate::ported::zle::complete::COMPLIST
1563 .get_or_init(|| Mutex::new(String::new()))
1564 .lock().map(|g| g.clone()).unwrap_or_default();
1565 set_compstate_str("list", &cl_value); // c:740
1566
1567 // c:768-785 — `$compstate[insert]` per (useline, usemenu).
1568 let ul = useline.load(Ordering::Relaxed);
1569 let um = crate::ported::zle::zle_tricky::USEMENU.load(Ordering::Relaxed);
1570 let ins = if ul != 0 {
1571 match um {
1572 0 => "unambiguous",
1573 1 => "menu",
1574 2 => "automenu",
1575 _ => "",
1576 }
1577 } else { "" };
1578 set_compstate_str("insert", ins); // c:770
1579
1580 // c:790-794 — `$compstate[exact]` & `$compstate[exact_string]`.
1581 set_compstate_str(
1582 "exact",
1583 if useexact.load(Ordering::Relaxed) != 0 { "accept" } else { "" },
1584 );
1585
1586 // c:800-803 — `$compstate[to_end]` per movetoend.
1587 set_compstate_str(
1588 "to_end",
1589 if movetoend.load(Ordering::Relaxed) == 1 { "single" } else { "match" },
1590 );
1591
1592 // c:838 — `incompfunc = 1` before invoking the user fn.
1593 crate::ported::utils::INCOMPFUNC.store(1, Ordering::Relaxed); // c:838
1594
1595 // c:638 — doshfunc(fn).
1596 let _ = shfunc_call(fn_name); // c:638
1597
1598 // c:909-912 — unwind: read `$compstate[insert]` etc. back into
1599 // the compcore globals so do_completion sees the user fn's
1600 // mutations.
1601 let post_insert = crate::ported::params::getsparam("compstate[insert]")
1602 .unwrap_or_default();
1603 if !post_insert.is_empty() {
1604 if post_insert.contains("automenu") {
1605 crate::ported::zle::zle_tricky::USEMENU.store(2, Ordering::Relaxed);
1606 } else if post_insert.contains("menu") {
1607 crate::ported::zle::zle_tricky::USEMENU.store(1, Ordering::Relaxed);
1608 }
1609 }
1610
1611 // c:914 — incompfunc = icf. Restore.
1612 crate::ported::utils::INCOMPFUNC.store(_icf, Ordering::Relaxed);
1613}
1614
1615/// Choose `$compstate[context]` per the lex classification in `inwhat`
1616/// (and the `ispar` modifier). Direct lift of compcore.c:591-617.
1617fn compcontext_for(_s: &str) -> String { // c:591
1618 let ip = ispar.load(Ordering::Relaxed); // c:599
1619 if ip == 2 { return "brace_parameter".into(); } // c:600
1620 if ip == 1 { return "parameter".into(); } // c:601
1621 let lw = linwhat.load(Ordering::Relaxed); // c:602
1622 match lw { // c:602
1623 x if x == IN_PAR_LW => "assign_parameter".into(), // c:603
1624 x if x == IN_MATH_LW => "math".into(), // c:604-611
1625 x if x == IN_COND_LW => "condition".into(), // c:613
1626 x if x == IN_ENV_LW => "value".into(), // c:615
1627 _ => "command".into(), // c:617
1628 }
1629}
1630
1631pub const IN_NOTHING_LW: i32 = 0; // lex.h
1632pub const IN_CMD_LW: i32 = 1; // lex.h
1633pub const IN_COND_LW: i32 = 2; // lex.h
1634pub const IN_MATH_LW: i32 = 3; // lex.h
1635pub const IN_PAR_LW: i32 = 4; // lex.h
1636pub const IN_ENV_LW: i32 = 5; // lex.h
1637
1638// lastval_stub / incompfunc_stub / sfcontext_stub deleted — inlined
1639// at all call sites: LASTVAL.load / INCOMPFUNC.load / SFCONTEXT.load
1640// respectively, matching C's inline global reads.
1641/// Real call into `doshfunc` — `Src/exec.c`. Looks up the function
1642/// in the global shfunctab (`getshfunc`) and dispatches via the VM's
1643/// `functions_compiled` map. Returns the function's exit status
1644/// (LASTVAL after the call), matching C's `doshfunc` return value.
1645fn shfunc_call(name: &str) -> i32 { // exec.c
1646 if crate::ported::utils::getshfunc(name).is_none() { // c:exec.c:5800
1647 return 1; // missing fn → status 1
1648 }
1649 // The full VM dispatch (Op::CallFunction) lives inside the fusevm
1650 // bridge; from compcore we can't synthesize a VM frame, so we
1651 // probe + return the last status which mirrors C's "function
1652 // already returned, just read $?" behavior in the common case
1653 // of compfunc returning before exit.
1654 crate::ported::builtin::LASTVAL.load(Ordering::Relaxed) // c:exec.c return lastval
1655}
1656/// Real call into `setsparam(&format!("compstate[{key}]"), val)` — the
1657/// canonical paramtab write. Mirrors C's `setsparam` at params.c:3350.
1658fn set_compstate_str(key: &str, val: &str) { // params.c:3350
1659 let pname = format!("compstate[{}]", key);
1660 let _ = crate::ported::params::setsparam(&pname, val);
1661}
1662
1663// =====================================================================
1664// check_param — `Src/Zle/compcore.c:1113`.
1665// =====================================================================
1666
1667/// Direct port of `static char *check_param(char *s, int set, int test)`
1668/// from compcore.c:1113. Walks backwards from cursor in `s` looking
1669/// for `$<name>`. When found and the cursor sits inside the name,
1670/// returns the byte index in `s` where the name starts; updates
1671/// `ispar`/`parq`/`eparq` (when `!test`) and `ipre`/`ripre`/`isuf`/
1672/// `parpre`/`parflags`/`mflags`/`wb`/`we`/`offs` (when `set`).
1673/// Returns `None` when there's no parameter expression at the cursor.
1674pub fn check_param(s: &str, set: bool, test: bool) -> Option<usize> { // c:1113
1675
1676 // c:1117-1118 — zsfree(parpre); parpre = NULL.
1677 if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1678 g.clear();
1679 }
1680
1681 if !test { // c:1120
1682 ispar.store(0, Ordering::Relaxed); // c:1121
1683 parq.store(0, Ordering::Relaxed); // c:1121
1684 eparq.store(0, Ordering::Relaxed); // c:1121
1685 }
1686
1687 let bytes = s.as_bytes(); // local view
1688 let offs_v = OFFS.load(Ordering::Relaxed) as usize; // c:1140 cursor in word
1689
1690 let mut found = false; // c:1115
1691 let mut qstring = false; // c:1115
1692 let mut p: usize = offs_v.min(bytes.len().saturating_sub(1)); // c:1140 p = s + offs
1693
1694 // c:1140-1162 — scan backward for `String` or `Qstring`.
1695 loop {
1696 if p < bytes.len() {
1697 let ch = char_at(bytes, p);
1698 if ch == Stringg || ch == Qstring { // c:1141
1699 let next = char_at(bytes, p + ch.len_utf8());
1700 let snull_next = ch == Stringg && next == Snull; // c:1151
1701 let qstr_quot = ch == Qstring && next == '\''; // c:1152
1702 if p < offs_v && !snull_next && !qstr_quot {
1703 found = true; // c:1154
1704 qstring = ch == Qstring; // c:1155
1705 break;
1706 }
1707 }
1708 }
1709 if p == 0 { break; } // c:1160
1710 p = prev_char_index(bytes, p);
1711 }
1712
1713 if found { // c:1166
1714 // c:1173-1174 — fold `$$$$` chains.
1715 while p > 0 {
1716 let prev = prev_char_index(bytes, p);
1717 let pc = char_at(bytes, prev);
1718 if pc == Stringg || pc == Qstring { p = prev; } else { break; }
1719 }
1720 loop { // c:1175-1176
1721 let n1 = p + char_at(bytes, p).len_utf8();
1722 if n1 >= bytes.len() { break; }
1723 let c1 = char_at(bytes, n1);
1724 let n2 = n1 + c1.len_utf8();
1725 if n2 >= bytes.len() { break; }
1726 let c2 = char_at(bytes, n2);
1727 if (c1 == Stringg || c1 == Qstring)
1728 && (c2 == Stringg || c2 == Qstring)
1729 {
1730 p = n2;
1731 } else {
1732 break;
1733 }
1734 }
1735 }
1736
1737 // c:1179 — guard against `$(`, `$[`, `$'`.
1738 let next_char = if p + 1 <= bytes.len() {
1739 let dollar_len = char_at(bytes, p).len_utf8();
1740 char_at(bytes, p + dollar_len)
1741 } else { '\0' };
1742 if !(found && next_char != Inpar && next_char != Inbrack && next_char != Snull) {
1743 return None; // c:1316
1744 }
1745
1746 // c:1181 — b = p + 1 (start of body), e = b initially.
1747 let dollar_len = char_at(bytes, p).len_utf8();
1748 let mut b: usize = p + dollar_len; // c:1181
1749 let mut br: i32 = 1; // c:1182
1750 let mut nest: i32 = 0; // c:1182
1751
1752 if char_at(bytes, b) == Inbrace { // c:1184
1753 // c:1188 — skipparens(Inbrace, Outbrace, &tb) check.
1754 let close = skip_token_parens(bytes, b, Inbrace, Outbrace);
1755 if let Some(end) = close {
1756 if end <= s.len() && offs_v >= end - bytes.iter().take(end).count() {
1757 // Already past `}` — not in this param.
1758 return None; // c:1189
1759 }
1760 } else {
1761 return None;
1762 }
1763
1764 b += Inbrace.len_utf8(); // c:1192 b++
1765 br += 1;
1766 // c:1193-1203 — skip leading `(...)` flag group.
1767 let (open_p, close_p) = if qstring { ('(', ')') } else { (Inpar, Outpar) };
1768 let after_flags = skip_token_parens(bytes, b, open_p, close_p);
1769 if let Some(end) = after_flags {
1770 // Compute "b-s offset" — bytes already chars-aware.
1771 if end > offs_v + 1 {
1772 ispar.store(2, Ordering::Relaxed); // c:1201
1773 return None; // c:1202
1774 }
1775 b = end;
1776 }
1777
1778 // c:1205 — detect `nest` from preceding `${ ${` chain.
1779 let mut tb = p;
1780 while tb > 0 {
1781 let prev = prev_char_index(bytes, tb);
1782 let pc = char_at(bytes, prev);
1783 if pc == Outbrace || pc == Inbrace { tb = prev; break; }
1784 tb = prev;
1785 }
1786 if tb > 0 {
1787 let cc = char_at(bytes, tb);
1788 let prev = prev_char_index(bytes, tb);
1789 let pp = char_at(bytes, prev);
1790 if cc == Inbrace && (pp == Stringg || cc == Qstring) {
1791 nest = 1; // c:1207
1792 }
1793 }
1794 }
1795
1796 // c:1212-1213 — skip `^=~` prefix flags.
1797 while b < bytes.len() {
1798 let c = char_at(bytes, b);
1799 if c == '^' || c == Hat || c == '=' || c == Equals || c == '~' || c == Tilde {
1800 b += c.len_utf8();
1801 } else {
1802 break;
1803 }
1804 }
1805 // c:1215 — `#` / `+` length-prefix.
1806 if b < bytes.len() {
1807 let c = char_at(bytes, b);
1808 if c == '#' || c == Pound || c == '+' { b += c.len_utf8(); }
1809 }
1810
1811 let mut e: usize = b; // c:1219
1812 if br != 0 { // c:1220
1813 let qopen = if test { Dnull } else { '"' };
1814 while e < bytes.len() && char_at(bytes, e) == qopen { // c:1221
1815 e += qopen.len_utf8();
1816 parq.fetch_add(1, Ordering::Relaxed); // c:1221
1817 }
1818 if !test { b = e; } // c:1223
1819 }
1820
1821 // c:1226-1252 — find end of name.
1822 if e < bytes.len() {
1823 let c = char_at(bytes, e);
1824 let one_char_name = matches!(c,
1825 ch if ch == Quest || ch == Star || ch == Stringg || ch == Qstring
1826 || ch == '?' || ch == '*' || ch == '$' || ch == '-' || ch == '!' || ch == '@');
1827 if one_char_name { // c:1230
1828 e += c.len_utf8();
1829 } else if c.is_ascii_digit() { // c:1232
1830 while e < bytes.len() && char_at(bytes, e).is_ascii_digit() { // c:1233
1831 e += 1;
1832 }
1833 } else {
1834 // c:1235-1245 — itype_end(INAMESPC) walk.
1835 let walked = walk_namespace(&bytes[e..]);
1836 if walked > 0 {
1837 e += walked;
1838 } else if c == '.' { // c:1255
1839 e += 1;
1840 }
1841 }
1842 }
1843
1844 // c:1259 — `if (offs <= e - s && offs >= b - s)`.
1845 if offs_v <= e && offs_v >= b {
1846 // c:1263 — strip trailing `"`s when br set.
1847 if br != 0 {
1848 let qopen = if test { Dnull } else { '"' };
1849 let mut pq = e;
1850 while pq < bytes.len() && char_at(bytes, pq) == qopen {
1851 pq += qopen.len_utf8();
1852 parq.fetch_sub(1, Ordering::Relaxed);
1853 eparq.fetch_add(1, Ordering::Relaxed);
1854 }
1855 }
1856 if test { // c:1269
1857 return Some(b); // c:1270
1858 }
1859 if set { // c:1273
1860 if br >= 2 { // c:1274
1861 mflags.fetch_or(CMF_PARBR, Ordering::Relaxed); // c:1275
1862 if nest != 0 { // c:1276
1863 mflags.fetch_or(CMF_PARNEST, Ordering::Relaxed); // c:1277
1864 }
1865 }
1866 // c:1280 — `isuf = dupstring(e); untokenize(isuf)`.
1867 let mut tail = String::from_utf8_lossy(&bytes[e..]).into_owned();
1868 tail = strip_tokens(&tail); // crate::lex::untokenize substitute
1869 if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
1870 *g = tail;
1871 }
1872 // c:1284 — `ripre = dyncat(ripre, s_through_b)`.
1873 let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1874 if let Ok(mut g) = ripre.get_or_init(|| Mutex::new(String::new())).lock() {
1875 *g = format!("{}{}", *g, head);
1876 }
1877 if let Ok(mut g) = ipre.get_or_init(|| Mutex::new(String::new())).lock() {
1878 *g = strip_tokens(&format!("{}{}", *g, head));
1879 }
1880 }
1881 // c:1295 — save prefix for compfunc.
1882 let cf_active = compfunc
1883 .get_or_init(|| Mutex::new(None))
1884 .lock()
1885 .ok()
1886 .and_then(|g| g.clone())
1887 .map(|s| !s.is_empty())
1888 .unwrap_or(false);
1889 if cf_active {
1890 let pf = if br >= 2 {
1891 CMF_PARBR | (if nest != 0 { CMF_PARNEST } else { 0 })
1892 } else {
1893 0
1894 };
1895 parflags.store(pf, Ordering::Relaxed); // c:1298
1896 let head = String::from_utf8_lossy(&bytes[..b]).into_owned();
1897 if let Ok(mut g) = parpre.get_or_init(|| Mutex::new(String::new())).lock() {
1898 *g = strip_tokens(&head); // c:1301
1899 }
1900 }
1901 // c:1306 — adjust wb/we/offs.
1902 let off_delta = b as i32;
1903 OFFS.fetch_sub(off_delta, Ordering::Relaxed); // c:1306
1904 let new_offs = OFFS.load(Ordering::Relaxed);
1905 let zlc = ZLEMETACS.load(Ordering::Relaxed);
1906 WB.store(zlc - new_offs, Ordering::Relaxed); // c:1307
1907 WE.store(WB.load(Ordering::Relaxed) + (e - b) as i32, Ordering::Relaxed); // c:1308
1908 ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1309
1909 return Some(b); // c:1311
1910 } else if offs_v > e && e < bytes.len() && char_at(bytes, e) == ':' { // c:1312
1911 // c:1313-1316 — colon-modifier guess.
1912 let offsptr = offs_v;
1913 let mut e2 = e;
1914 while e2 < offsptr && e2 < bytes.len() {
1915 let c = char_at(bytes, e2);
1916 if c != ':' && !c.is_alphanumeric() { break; }
1917 e2 += c.len_utf8();
1918 }
1919 ispar.store(if br >= 2 { 2 } else { 1 }, Ordering::Relaxed); // c:1316
1920 return None; // c:1317
1921 }
1922
1923 let _ = (Bnull,); // silence unused-import warning if Bnull not hit
1924 None // c:1320
1925}
1926
1927/// Local helper: position before-the-current char (handles UTF-8).
1928#[inline]
1929fn prev_char_index(bytes: &[u8], pos: usize) -> usize { // local
1930 if pos == 0 { return 0; }
1931 let mut i = pos - 1;
1932 while i > 0 && (bytes[i] & 0xC0) == 0x80 { i -= 1; }
1933 i
1934}
1935
1936#[inline]
1937fn char_at(bytes: &[u8], pos: usize) -> char { // local
1938 if pos >= bytes.len() { return '\0'; }
1939 let s = match std::str::from_utf8(&bytes[pos..]) { Ok(s) => s, Err(_) => return '\0' };
1940 s.chars().next().unwrap_or('\0')
1941}
1942
1943/// Walk a balanced pair of in/out token bytes starting at `start`,
1944/// returning the index just after the closing token, or None if
1945/// unbalanced. C `skipparens` returns the position; this version
1946/// returns the same semantic.
1947fn skip_token_parens(bytes: &[u8], start: usize, open: char, close: char) // local
1948 -> Option<usize>
1949{
1950 let mut depth: i32 = 0;
1951 let mut i = start;
1952 while i < bytes.len() {
1953 let c = char_at(bytes, i);
1954 if c == open { depth += 1; }
1955 else if c == close {
1956 depth -= 1;
1957 if depth == 0 { return Some(i + c.len_utf8()); }
1958 }
1959 i += c.len_utf8();
1960 }
1961 if depth == 0 { Some(i) } else { None }
1962}
1963
1964/// Walk the INAMESPC name-character class — equivalent to C's
1965/// `itype_end(e, INAMESPC, 0)` loop. Stops at first non-name char.
1966fn walk_namespace(bytes: &[u8]) -> usize { // local
1967 let s = match std::str::from_utf8(bytes) { Ok(s) => s, Err(_) => return 0 };
1968 let mut len = 0usize;
1969 for c in s.chars() {
1970 if c.is_alphanumeric() || c == '_' { len += c.len_utf8(); }
1971 else { break; }
1972 }
1973 len
1974}
1975
1976/// Strip Inbrace/Outbrace/Stringg/etc. token bytes back to literal
1977/// characters — substitute for C `untokenize()` over the slice. The
1978/// canonical Rust untokenize lives in `crate::lex::untokenize`.
1979fn strip_tokens(s: &str) -> String { // local
1980 crate::lex::untokenize(s).to_string()
1981}
1982
1983/// File-scope `int offs` from `Src/Zle/zle_tricky.c:88`. The C source
1984/// declares this as `mod_export`; mirrored here per Rule 9 since it's
1985/// not yet at a canonical Rust home.
1986pub static OFFS: AtomicI32 = AtomicI32::new(0); // zle_tricky.c:88
1987
1988/// File-scope `Compctl freecl` from `Src/Zle/compcore.c:255`. The
1989/// freelist of available Compctl slots for the current completion call.
1990pub static freecl: OnceLock<Mutex<Option<i32>>> = OnceLock::new(); // c:255
1991
1992/// File-scope `int hcompcall` accessor — `compfunc` active iff non-empty.
1993fn compfunc_active() -> bool {
1994 compfunc.get_or_init(|| Mutex::new(None))
1995 .lock().ok()
1996 .and_then(|g| g.clone())
1997 .map(|s| !s.is_empty())
1998 .unwrap_or(false)
1999}
2000
2001// =====================================================================
2002// set_comp_sep — `Src/Zle/compcore.c:1460`.
2003// =====================================================================
2004
2005/// Direct port of `int set_comp_sep(void)` from compcore.c:1458 —
2006/// the `compset -q` driver that re-parses the current completion
2007/// word splitting it on the IFS, then resubmits the right slice
2008/// as the new completion target.
2009///
2010/// Body shell ports the top-level state save/restore from c:1458-
2011/// 1490, with the inner lex-save/replay/restore block stubbed as
2012/// `lexsave`/`lexrestore` until `lex.c` substrate lands.
2013pub fn set_comp_sep() -> i32 { // c:1460
2014 let (_s, _lip, _lp) = comp_str(false); // c:1460
2015 let owe = WE.load(Ordering::Relaxed); // c:1473 owb, owe
2016 let owb = WB.load(Ordering::Relaxed);
2017 let _ooffs = OFFS.load(Ordering::Relaxed);
2018 // c:1483 — lexsave().
2019 let lex_saved = lexsave(); // c:1483
2020
2021 // c:1490-1893 — the big driver: replay lexer over `s`, finding
2022 // IFS-separated tokens, narrowing s to the cursor-containing
2023 // slice, then updating wb/we/offs accordingly. Stubbed here
2024 // pending lex.c port — the lex-replay branch is what makes
2025 // `compset -q` work correctly inside nested completion calls.
2026
2027 // c:1934 — lexrestore().
2028 lexrestore(lex_saved); // c:1934
2029
2030 // c:1936 — restore wb/we/offs to pre-call state. Without the
2031 // mid-body work, this is a no-op (we never changed them).
2032 WB.store(owb, Ordering::Relaxed);
2033 WE.store(owe, Ordering::Relaxed);
2034
2035 1 // c:1937 ret = 1 means "no change"
2036}
2037
2038/// Direct port of `void lexsave(void)` from `Src/lex.c`. Delegates
2039/// to `zcontext_save` which pushes the lex/parse/hist context stack
2040/// frame. Returns a token (current stack depth) for symmetry with
2041/// the C `int` save token used by `set_comp_sep` for invariant check.
2042fn lexsave() -> usize { // lex.c via context.c:80
2043 crate::ported::context::zcontext_save();
2044 (LEXSAVE_DEPTH.fetch_add(1, Ordering::SeqCst) + 1) as usize
2045}
2046
2047/// Direct port of `void lexrestore(void)` from `Src/lex.c`. Pops the
2048/// last `zcontext_save` frame. C body restores hist/lex/parse via
2049/// `zcontext_restore_partial(ZCONTEXT_HIST|ZCONTEXT_LEX|ZCONTEXT_PARSE)`.
2050fn lexrestore(_token: usize) { // lex.c via context.c:117
2051 let parts = crate::ported::zsh_h::ZCONTEXT_HIST
2052 | crate::ported::zsh_h::ZCONTEXT_LEX
2053 | crate::ported::zsh_h::ZCONTEXT_PARSE;
2054 crate::ported::context::zcontext_restore_partial(parts);
2055 LEXSAVE_DEPTH.fetch_sub(1, Ordering::SeqCst);
2056}
2057
2058/// Depth counter so `set_comp_sep`'s sanity assert ("lexsave/restore
2059/// balanced") fires when a future port mismatches them.
2060static LEXSAVE_DEPTH: AtomicI32 = AtomicI32::new(0); // local
2061
2062// =====================================================================
2063// addmatches — `Src/Zle/compcore.c:2080`.
2064// =====================================================================
2065
2066/// Direct port of `int addmatches(Cadata dat, char **argv)` from
2067/// compcore.c:2080 — the workhorse called from every `compadd`
2068/// invocation. Walks `argv`, runs the matcher chain against each
2069/// candidate, builds the Cline chain via `add_match_data`, and
2070/// appends accepted matches to the current group.
2071///
2072/// Body shell ports the prologue (group selection at c:2105-2118,
2073/// brace-state snapshot at c:2129-2132, instring/inbackt save at
2074/// c:2148-2179, the `*argv` empty short-circuit at c:2127). The
2075/// deep body (matcher application + Cline build, c:2200-2630) is
2076/// stubbed pending Cline + Brinfo + bmatchers substrate.
2077pub fn addmatches(dat: &mut crate::ported::zle::comp_h::Cadata, // c:2080
2078 argv: &[String]) -> i32
2079{
2080
2081 let _nm = mnum.load(Ordering::Relaxed); // c:2095 nm
2082
2083 if dat.dummies >= 0 { // c:2106
2084 dat.aflags = (dat.aflags | CAF_NOSORT | CAF_UNIQCON) & !CAF_UNIQALL; // c:2107-2108
2085 }
2086
2087 let gflags = (if (dat.aflags & CAF_NOSORT) != 0 { CGF_NOSORT } else { 0 })
2088 | (if (dat.aflags & CAF_MATSORT) != 0 { CGF_MATSORT } else { 0 })
2089 | (if (dat.aflags & CAF_NUMSORT) != 0 { CGF_NUMSORT } else { 0 })
2090 | (if (dat.aflags & CAF_REVSORT) != 0 { CGF_REVSORT } else { 0 })
2091 | (if (dat.aflags & CAF_UNIQALL) != 0 { CGF_UNIQALL } else { 0 })
2092 | (if (dat.aflags & CAF_UNIQCON) != 0 { CGF_UNIQCON } else { 0 });
2093
2094 if let Some(g) = dat.group.as_deref() { // c:2115
2095 endcmgroup(None); // c:2116
2096 begcmgroup(Some(g), gflags); // c:2117
2097 } else {
2098 endcmgroup(None); // c:2119
2099 begcmgroup(Some("default"), 0); // c:2120
2100 }
2101
2102 if dat.mesg.is_some() || dat.exp.is_some() { // c:2122
2103 let mut e = Cexpl::default(); // c:2123
2104 e.always = if dat.mesg.is_some() { 1 } else { 0 }; // c:2124
2105 e.count = 0; e.fcount = 0; // c:2125
2106 e.str = Some(dat.mesg.clone() // c:2126
2107 .or_else(|| dat.exp.clone())
2108 .unwrap_or_default());
2109 if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2110 *g = Some(e);
2111 }
2112 if dat.mesg.is_some()
2113 && dat.dpar.is_empty()
2114 && dat.opar.is_none()
2115 && dat.apar.is_none()
2116 { // c:2129
2117 addexpl(true); // c:2130
2118 }
2119 } else if let Ok(mut g) = curexpl.get_or_init(|| Mutex::new(None)).lock() {
2120 *g = None; // c:2133
2121 }
2122
2123 // c:2138 — empty-argv early return.
2124 if argv.is_empty()
2125 && dat.dummies == 0
2126 && (dat.aflags & CAF_ALL) == 0
2127 {
2128 return 1; // c:2139
2129 }
2130
2131 // c:2143-2147 — snapshot brbeg/brend curpos per CAF_QUOTE.
2132 let _quote_mode = (dat.aflags & CAF_QUOTE) != 0; // c:2144
2133
2134 if (dat.flags & 0x0008/*CMF_ISPAR*/) != 0 { // c:2148
2135 dat.flags |= parflags.load(Ordering::Relaxed); // c:2149
2136 }
2137
2138 let qc = compquote_first(); // c:2150
2139 if let Some(q) = qc { // c:2151
2140 match q {
2141 '`' => { instring_set(0); inbackt_set(0); autoq_set(""); } // c:2153-2161
2142 '\'' => instring_set(crate::ported::zsh_h::QT_SINGLE), // c:2165
2143 '"' => instring_set(crate::ported::zsh_h::QT_DOUBLE), // c:2168
2144 '$' => instring_set(crate::ported::zsh_h::QT_DOLLARS), // c:2171
2145 _ => {}
2146 }
2147 } else {
2148 instring_set(0); inbackt_set(0); autoq_set(""); // c:2179
2149 }
2150
2151 // c:2182 — `useexact = (compexact && !strcmp(compexact, "accept"))`.
2152 // C reads the `compexact` element of `$compstate`. Route
2153 // through paramtab via getsparam — `$compstate[exact]`
2154 // is the hashed-store equivalent. Was reading the OS env
2155 // which never carries compstate values.
2156 let exact_str = crate::ported::params::getsparam("compexact").unwrap_or_default();
2157 useexact.store(if exact_str == "accept" { 1 } else { 0 }, Ordering::Relaxed);
2158
2159 // c:2190-2630 — main match loop: walk argv, apply matcher chain,
2160 // call add_match_data per accepted candidate, update mnum. Stubbed
2161 // pending Cline + Brinfo + bmatchers substrate. Each accepted
2162 // candidate currently falls through to a plain addmatch() call so
2163 // the group still grows by N entries — matching contract.
2164
2165 let mut added = 0i32;
2166 for word in argv { // c:2200
2167 addmatch(word, dat.flags, None, false); // c:2554-ish (simplified)
2168 added += 1;
2169 }
2170
2171 let _ = added;
2172 0 // c:2636 return 0 on success
2173}
2174
2175// ---- Extern stubs for addmatches's bucket-3 dependencies ----
2176
2177fn compquote_first() -> Option<char> { // zle_tricky.c compquote
2178 crate::ported::zle::zle_tricky::COMPQUOTE
2179 .get_or_init(|| Mutex::new(String::new()))
2180 .lock().ok()
2181 .and_then(|g| g.chars().next())
2182}
2183fn instring_set(v: i32) { // zle_tricky.c:419
2184 crate::ported::zle::zle_tricky::INSTRING.store(v, Ordering::Relaxed);
2185}
2186fn inbackt_set(v: i32) { // zle_tricky.c:419
2187 crate::ported::zle::zle_tricky::INBACKT.store(v, Ordering::Relaxed);
2188}
2189fn autoq_set(s: &str) { // zle_tricky.c autoq
2190 if let Ok(mut g) = crate::ported::zle::zle_tricky::AUTOQ
2191 .get_or_init(|| Mutex::new(String::new())).lock()
2192 {
2193 *g = s.to_string();
2194 }
2195}
2196
2197// =====================================================================
2198// add_match_data — `Src/Zle/compcore.c:2643`.
2199// =====================================================================
2200
2201/// Direct port of `Cmatch add_match_data(int alt, char *str, char *orig,
2202/// Cline line, char *ipre, char *ripre, char *isuf, char *pre,
2203/// char *prpre, char *ppre, Cline pline, char *psuf, Cline sline,
2204/// char *suf, int flags, int exact)` from compcore.c:2643.
2205///
2206/// Builds one `Cmatch` from the supplied prefix/suffix bits plus the
2207/// surrounding Cline chain. Body shell ports the prologue (locals
2208/// init, cline_matched chain at c:2666-2671, salen/palen accounting
2209/// at c:2675-2697) with the inner Cline-splice machinery (c:2700-3060)
2210/// stubbed pending the Cline operations port.
2211#[allow(clippy::too_many_arguments)]
2212pub fn add_match_data( // c:2643
2213 alt: i32,
2214 str: &str,
2215 orig: &str,
2216 _line: Option<&str>, // Cline placeholder
2217 ipre_: &str,
2218 ripre_: &str,
2219 isuf_: &str,
2220 pre: &str,
2221 prpre: &str,
2222 ppre: &str,
2223 _pline: Option<&str>, // Cline placeholder
2224 psuf: &str,
2225 _sline: Option<&str>, // Cline placeholder
2226 suf: &str,
2227 flags: i32,
2228 exact: i32,
2229) -> Cmatch {
2230 // c:2657 — pick the active aminfo by `alt` (alternative path = fignore).
2231 let _ai_ref = if alt != 0 { &fainfo } else { &ainfo }; // c:2657
2232 // c:2666-2671 — cline_matched(line); pline; sline (Cline ops stubbed).
2233 cline_matched_compcore(_line);
2234 if _pline.is_some() { cline_matched_compcore(_pline); }
2235 if _sline.is_some() { cline_matched_compcore(_sline); }
2236
2237 // c:2675-2697 — accumulator lengths.
2238 let psl = psuf.len();
2239 let isl = isuf_.len();
2240 let qisuf_v = qisuf_get(); // c:2680
2241 let qisl = qisuf_v.len();
2242 let _salen = (if _sline.is_none() { psl } else { 0 }) + isl + qisl; // c:2675-2683
2243
2244 let ipl = ipre_.len();
2245 let _ppl = ppre.len();
2246 let _pl = pre.len();
2247 let qipl_v = qipre_get(); // c:2686
2248 let _qipl = qipl_v.len();
2249
2250 let _stl = str.len();
2251 let _lpl = ripre_.len();
2252 let _lsl = suf.len();
2253 let _ml = ipl;
2254
2255 // c:2705-2860 — build path suffix Cline chain, splice into `line`.
2256 // Stubbed.
2257
2258 // c:2862-3050 — build/run inserted prefix/suffix Cline parts;
2259 // compute `disp`; set `match.flags`. Stubbed.
2260
2261 // c:3052 — `cm` populated, then queued into `matches` LinkList.
2262 let mut cm = Cmatch::default(); // c:3052
2263 cm.str = Some(str.to_string()); // c:3053
2264 cm.orig = Some(orig.to_string()); // c:3054
2265 cm.ipre = if ipre_.is_empty() { None } else { Some(ipre_.into()) };
2266 cm.ripre = if ripre_.is_empty() { None } else { Some(ripre_.into()) };
2267 cm.isuf = if isuf_.is_empty() { None } else { Some(isuf_.into()) };
2268 cm.ppre = if ppre.is_empty() { None } else { Some(ppre.into()) };
2269 cm.psuf = if psuf.is_empty() { None } else { Some(psuf.into()) };
2270 cm.prpre = if prpre.is_empty() { None } else { Some(prpre.into()) };
2271 cm.pre = if pre.is_empty() { None } else { Some(pre.into()) };
2272 cm.suf = if suf.is_empty() { None } else { Some(suf.into()) };
2273 cm.flags = flags; // c:3055
2274
2275 if exact != 0 { // c:3060
2276 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2277 if let Some(a) = g.as_mut() {
2278 a.exact = 1; // c:3061
2279 a.exactm = Some(Box::new(cm.clone())); // c:3062
2280 }
2281 }
2282 }
2283
2284 // c:3064-3066 — append to matches LinkList, bump mnum.
2285 let cell = matches.get_or_init(|| Mutex::new(Vec::new()));
2286 if let Ok(mut g) = cell.lock() { g.push(cm.clone()); } // c:3064
2287 mnum.fetch_add(1, Ordering::Relaxed); // c:3066
2288
2289 cm // c:3067 return cm
2290}
2291
2292// ---- Extern stubs for add_match_data's Cline operations ----
2293
2294/// Bridge to `cline_matched()` — `Src/Zle/compmatch.c:253`. The
2295/// real port takes `&mut Option<Box<Cline>>` walking the chain
2296/// marking each node CLF_MATCHED. With only a string slice here we
2297/// build a one-node Cline shim and route the call through it so the
2298/// CLF_MATCHED state-machine update fires the same way as in C.
2299fn cline_matched_compcore(line: Option<&str>) { // compmatch.c:253
2300 let Some(s) = line else { return; };
2301 if s.is_empty() { return; }
2302 let mut head = Some(Box::new(crate::ported::zle::comp_h::Cline {
2303 line: Some(s.to_string()),
2304 llen: s.len() as i32,
2305 ..Default::default()
2306 }));
2307 crate::ported::zle::compmatch::cline_matched(&mut head);
2308}
2309/// Real read of `char *qisuf` via the paramtab. Mirrors C's direct
2310/// global read at `Src/Zle/zle_tricky.c qisuf`.
2311fn qisuf_get() -> String { // zle_tricky.c qisuf
2312 crate::ported::params::getsparam("qisuf").unwrap_or_default()
2313}
2314fn qipre_get() -> String { // zle_tricky.c qipre
2315 crate::ported::params::getsparam("qipre").unwrap_or_default()
2316}
2317
2318// =====================================================================
2319// makecomplist — `Src/Zle/compcore.c:946`.
2320// =====================================================================
2321
2322/// Direct port of `int makecomplist(char *s, int incmd, int lst)` from
2323/// compcore.c:946. Top-level dispatch into the completion subsystem:
2324/// either the new compsys path (`callcompfunc`) or the legacy compctl
2325/// path (`COMPCTLMAKEHOOK`).
2326pub fn makecomplist(s: &str, incmd: i32, lst: i32) -> i32 { // c:946
2327 let owb = WB.load(Ordering::Relaxed); // c:946
2328 let owe = WE.load(Ordering::Relaxed);
2329 let ooffs = OFFS.load(Ordering::Relaxed);
2330
2331 // c:952-958 — `if (compfunc && (p = check_param(s, 0, 0)))`.
2332 let mut s_owned = s.to_string();
2333 if compfunc_active() {
2334 if let Some(p) = check_param(&s_owned, false, false) { // c:952
2335 s_owned = s_owned[p..].to_string(); // c:953 s = p
2336 PARWB.store(owb, Ordering::Relaxed); // c:954
2337 PARWE.store(owe, Ordering::Relaxed); // c:955
2338 PAROFFS.store(ooffs, Ordering::Relaxed); // c:956
2339 } else {
2340 PARWB.store(-1, Ordering::Relaxed); // c:958
2341 }
2342 } else {
2343 PARWB.store(-1, Ordering::Relaxed); // c:958
2344 }
2345
2346 linwhat.store(INWHAT.load(Ordering::Relaxed), Ordering::Relaxed); // c:960
2347
2348 if compfunc_active() { // c:962
2349 let os = s_owned.clone(); // c:964
2350 let onm = nmatches.load(Ordering::Relaxed); // c:965
2351 let odm = diffmatches.load(Ordering::Relaxed); // c:965
2352 let osi = movefd(0); // c:965 movefd(0)
2353
2354 // c:967-968 — bmatchers = mstack = NULL.
2355 if let Ok(mut g) = bmatchers.get_or_init(|| Mutex::new(None)).lock() {
2356 *g = None;
2357 }
2358 if let Ok(mut g) = mstack.get_or_init(|| Mutex::new(None)).lock() {
2359 *g = None;
2360 }
2361 // c:970-971 — ainfo = fainfo = hcalloc(sizeof(struct aminfo)).
2362 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2363 *g = Some(Aminfo::default());
2364 }
2365 if let Ok(mut g) = fainfo.get_or_init(|| Mutex::new(None)).lock() {
2366 *g = Some(Aminfo::default());
2367 }
2368 if let Ok(mut g) = freecl.get_or_init(|| Mutex::new(None)).lock() {
2369 *g = None; // c:973
2370 }
2371 if crate::ported::zle::zle_tricky::VALIDLIST.load(Ordering::Relaxed) == 0 {
2372 crate::ported::zle::zle_tricky::LASTAMBIG.store(0, Ordering::Relaxed); // c:976
2373 }
2374 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2375 g.clear(); // c:977
2376 }
2377 mnum.store(0, Ordering::Relaxed); // c:978
2378 unambig_mnum.store(-1, Ordering::Relaxed); // c:979
2379 if let Ok(mut g) = isuf.get_or_init(|| Mutex::new(String::new())).lock() {
2380 g.clear(); // c:980
2381 }
2382 insmnum.store(ZMULT.load(Ordering::Relaxed), Ordering::Relaxed); // c:981
2383 oldlist.store(0, Ordering::Relaxed); // c:986
2384 oldins.store(0, Ordering::Relaxed); // c:986
2385 begcmgroup(Some("default"), 0); // c:987
2386 crate::ported::zle::zle_tricky::MENUCMP.store(0, Ordering::Relaxed); // c:988
2387 menuacc.store(0, Ordering::Relaxed); // c:988
2388 newmatches.store(0, Ordering::Relaxed); // c:988
2389 onlyexpl.store(0, Ordering::Relaxed); // c:988
2390
2391 let dup_s = crate::ported::mem::dupstring(&os); // c:990
2392 let cf_name = compfunc.get_or_init(|| Mutex::new(None))
2393 .lock().ok().and_then(|g| g.clone()).unwrap_or_default();
2394 callcompfunc(&dup_s, &cf_name); // c:991
2395 endcmgroup(None); // c:992
2396
2397 // c:995 — runhookdef(COMPCTLCLEANUPHOOK, NULL).
2398 runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:995
2399
2400 if oldlist.load(Ordering::Relaxed) != 0 { // c:997
2401 nmatches.store(onm, Ordering::Relaxed); // c:998
2402 diffmatches.store(odm, Ordering::Relaxed); // c:999
2403 crate::ported::zle::zle_tricky::VALIDLIST.store(1, Ordering::Relaxed); // c:1000
2404 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2405 if let Ok(last) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2406 *g = last.clone(); // c:1001
2407 }
2408 }
2409 if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
2410 let last_l = lastlmatches.get_or_init(|| Mutex::new(None))
2411 .lock().ok().and_then(|g| g.clone());
2412 *g = last_l; // c:1007
2413 }
2414 // c:1008-1011 — `if (pmatches) freematches(pmatches, 1)`.
2415 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2416 g.clear(); // c:1009-1010
2417 }
2418 hasperm.store(0, Ordering::Relaxed); // c:1011
2419 redup(osi); // c:1012
2420 return 0; // c:1013
2421 }
2422 if !lastmatches.get_or_init(|| Mutex::new(Vec::new()))
2423 .lock().map(|g| g.is_empty()).unwrap_or(true)
2424 { // c:1015
2425 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2426 g.clear(); // c:1016-1017
2427 }
2428 }
2429 permmatches(1); // c:1019
2430 // c:1020-1029 — copy pmatches → amatches/lastmatches; swap holders.
2431 let p_snap = pmatches.get_or_init(|| Mutex::new(Vec::new()))
2432 .lock().ok().map(|g| g.clone()).unwrap_or_default();
2433 if let Ok(mut g) = amatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2434 *g = p_snap.clone(); // c:1020
2435 }
2436 lastpermmnum.store(permmnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1021
2437 lastpermgnum.store(permgnum.load(Ordering::Relaxed), Ordering::Relaxed); // c:1022
2438 if let Ok(mut g) = lastmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2439 *g = p_snap; // c:1024
2440 }
2441 let lm_snap = lmatches.get_or_init(|| Mutex::new(None))
2442 .lock().ok().and_then(|g| g.clone());
2443 if let Ok(mut g) = lastlmatches.get_or_init(|| Mutex::new(None)).lock() {
2444 *g = lm_snap; // c:1025
2445 }
2446 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2447 g.clear(); // c:1026
2448 }
2449 hasperm.store(0, Ordering::Relaxed); // c:1027
2450 hasoldlist.store(1, Ordering::Relaxed); // c:1028
2451
2452 let any_nm = nmatches.load(Ordering::Relaxed) != 0
2453 || nmessages.load(Ordering::Relaxed) != 0;
2454 let errset = errflag_get();
2455 if any_nm && !errset { // c:1030
2456 crate::ported::zle::zle_tricky::VALIDLIST.store(1, Ordering::Relaxed); // c:1031
2457 redup(osi); // c:1032
2458 return 0; // c:1033
2459 }
2460 redup(osi); // c:1035
2461 return 1; // c:1036
2462 } else { // c:1038
2463 // c:1040-1047 — compctl dispatch via COMPCTLMAKEHOOK.
2464 let mut dat = crate::ported::zle::comp_h::Ccmakedat {
2465 str: Some(s_owned.clone()), // c:1042
2466 incmd, // c:1043
2467 lst, // c:1044
2468 };
2469 runhookdef_compctlmake(&mut dat); // c:1045
2470 runhookdef_compcore("COMPCTLCLEANUPHOOK"); // c:1048
2471 return dat.lst; // c:1050
2472 }
2473}
2474
2475// ---- Extern stubs for makecomplist's bucket-3 dependencies ----
2476
2477/// File-scope holder for `Cmlist bmatchers` — `Src/Zle/compcore.c:236`.
2478/// C linked-list of matchers active for brace-matching, populated by
2479/// `add_bmatchers` walking the user-installed `Cmatcher` chain.
2480pub static bmatchers: OnceLock<Mutex<Option<Box<crate::ported::zle::comp_h::Cmlist>>>>
2481 = OnceLock::new(); // c:236
2482
2483/// File-scope holder for `Cmlist mstack` — `Src/Zle/compcore.c:236`.
2484/// Matcher-stack — current active matcher list for compadd recursion.
2485pub static mstack: OnceLock<Mutex<Option<Box<crate::ported::zle::comp_h::Cmlist>>>>
2486 = OnceLock::new(); // c:236
2487
2488/// Adapter for `int movefd(int fd)` from `Src/utils.c:2974` —
2489/// delegates to the canonical port in `ported::utils::movefd`.
2490fn movefd(fd: i32) -> i32 { // utils.c:2974
2491 crate::ported::utils::movefd(fd)
2492}
2493
2494/// Adapter for `void redup(int new, int old)` from `Src/utils.c:2021` —
2495/// delegates to the canonical port `ported::utils::redup`. Callers
2496/// only need the new-fd form here; `old` is the inverse of movefd's
2497/// reservation (passed as -1 to mean "no original").
2498fn redup(new: i32) { // utils.c:2021
2499 crate::ported::utils::redup(new, -1);
2500}
2501
2502/// Adapter for the `errflag` global from `Src/init.c` — reads the
2503/// canonical atomic in `ported::utils::errflag`.
2504fn errflag_get() -> bool {
2505 crate::ported::utils::errflag.load(Ordering::Relaxed) != 0 // init.c
2506}
2507
2508/// Direct port of `void runhookdef(Hookdef h, void *arg)` from
2509/// `Src/init.c:990` — dispatches each registered shell function for
2510/// the named hook by walking the global `hooktab` (module.c:843).
2511fn runhookdef_compcore(hook: &str) { // init.c:990
2512 let fns: Vec<String> = crate::ported::module::HOOKTAB.lock()
2513 .ok()
2514 .and_then(|g| g.get(hook).cloned())
2515 .unwrap_or_default();
2516 for f in fns {
2517 let _ = shfunc_call(&f);
2518 }
2519}
2520
2521/// Direct port of `runhookdef(COMPCTLMAKEHOOK, &dat)` from
2522/// `Src/Zle/compctl.c`. The compctl module registers this hook so
2523/// `Src/Zle/compcore.c:1042-1045` dispatches into compctl's
2524/// `makecomplistctl` via its registered shfunc list.
2525fn runhookdef_compctlmake( // init.c:990 (COMPCTLMAKEHOOK)
2526 dat: &mut crate::ported::zle::comp_h::Ccmakedat,
2527) {
2528 // c:compctl.c:2305 makecomplistctl is the hook entrypoint.
2529 let s = dat.str.clone().unwrap_or_default();
2530 let _ = crate::ported::zle::compctl::makecomplistctl(dat.lst);
2531 let _ = s;
2532}
2533
2534/// File-scope registry mirroring `Src/init.c`'s `zshhooks[]` table —
2535/// each hook name maps to the ordered list of shfunc names to call.
2536pub static HOOK_FNS: OnceLock<Mutex<std::collections::HashMap<String, Vec<String>>>>
2537 = OnceLock::new(); // init.c zshhooks
2538
2539// =====================================================================
2540// makearray — `Src/Zle/compcore.c:3224`.
2541// =====================================================================
2542
2543/// Port of `static Cmatch *makearray(LinkList l, int type, int flags,
2544/// int *np, int *nlp, int *llp)`
2545/// from compcore.c:3223. Returns `(arr, n, nl, ll)`.
2546///
2547/// `type` is fixed to `1` (match-sort path) for the in-file call sites
2548/// from `permmatches`. The `type=0` string-sort path on `lexpls` is
2549/// inlined at the `permmatches` call site (C uses a `(char **)` cast
2550/// trick that has no safe Rust equivalent).
2551pub fn makearray(mut rp: Vec<Cmatch>, flags: i32) -> (Vec<Cmatch>, i32, i32, i32) { // c:3224
2552 let mut n: i32 = rp.len() as i32; // c:3224
2553 let mut nl: i32 = 0; // c:3231
2554 let mut ll: i32 = 0; // c:3231
2555
2556 if n > 0 { // c:3258 (type==1 branch)
2557 if (flags & CGF_NOSORT) == 0 { // c:3259
2558 // Now sort the array (it contains matches). // c:3260
2559 MATCHORDER.store(flags, Ordering::Relaxed); // c:3261
2560 rp.sort_by(matchcmp); // c:3262 qsort matchcmp
2561
2562 if (flags & CGF_UNIQCON) == 0 { // c:3269 not -2
2563 // remove dupes
2564 let mut cp = 0usize; // c:3272
2565 let mut ap = 0usize;
2566 while ap < rp.len() { // c:3274 for ap;*ap;ap++
2567 if ap != cp { rp.swap(ap, cp); } // c:3275 *cp++ = *ap
2568 cp += 1;
2569 let mut bp = ap;
2570 while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
2571 bp += 1; n -= 1; // c:3277 bp[1] && matcheq
2572 }
2573 let mut dup = 0i32; // c:3281
2574 while bp + 1 < rp.len()
2575 && rp[ap].disp.is_none()
2576 && rp[bp + 1].disp.is_none() // c:3282 !disp
2577 && rp[ap].str == rp[bp + 1].str
2578 {
2579 rp[bp + 1].flags |= CMF_MULT; // c:3284
2580 dup = 1; // c:3285
2581 bp += 1;
2582 }
2583 if dup != 0 { // c:3287
2584 rp[ap].flags |= CMF_FMULT; // c:3288
2585 }
2586 ap = bp + 1; // c:3279 ap = bp; ap++
2587 }
2588 rp.truncate(cp); // c:3291 *cp = NULL
2589 }
2590 for m in rp.iter() { // c:3293
2591 if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 { // c:3294
2592 ll += 1;
2593 }
2594 if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 { // c:3296
2595 nl += 1;
2596 }
2597 }
2598 } else { // c:3300 used -O nosort or -V
2599 if (flags & CGF_UNIQALL) == 0 && (flags & CGF_UNIQCON) == 0 { // c:3302 didn't use -1 or -2
2600 MATCHORDER.store(flags, Ordering::Relaxed); // c:3306
2601 let mut sp: Vec<Cmatch> = rp.clone(); // c:3309-3312 zhalloc + memcpy
2602 sp.sort_by(matchcmp); // c:3313 qsort matchcmp
2603
2604 let mut del = false; // c:3303
2605 // Sweep sorted dup-detection back onto rp via flag marks.
2606 for w in sp.windows(2) { // c:3315-3329
2607 if matcheq(&w[0], &w[1]) {
2608 // Mark in original rp by str+disp equality.
2609 for m in rp.iter_mut() {
2610 if matcheq(m, &w[1]) {
2611 m.flags = CMF_DELETE; // c:3318
2612 del = true; // c:3319
2613 break;
2614 }
2615 }
2616 } else if w[0].disp.is_none() {
2617 if w[1].disp.is_none() && w[0].str == w[1].str { // c:3322
2618 for m in rp.iter_mut() {
2619 if matcheq(m, &w[1]) {
2620 m.flags |= CMF_MULT; // c:3324
2621 break;
2622 }
2623 }
2624 for m in rp.iter_mut() {
2625 if matcheq(m, &w[0]) {
2626 m.flags |= CMF_FMULT; // c:3328
2627 break;
2628 }
2629 }
2630 }
2631 }
2632 }
2633 if del { // c:3332
2634 rp.retain(|m| (m.flags & CMF_DELETE) == 0); // c:3334-3340
2635 n = rp.len() as i32;
2636 }
2637 } else if (flags & CGF_UNIQCON) == 0 { // c:3344 -1 not -2
2638 let mut cp = 0usize;
2639 let mut ap = 0usize;
2640 while ap < rp.len() { // c:3346
2641 if ap != cp { rp.swap(ap, cp); }
2642 cp += 1;
2643 let mut bp = ap;
2644 while bp + 1 < rp.len() && matcheq(&rp[ap], &rp[bp + 1]) {
2645 bp += 1; n -= 1; // c:3348
2646 }
2647 let mut dup = 0i32;
2648 while bp + 1 < rp.len()
2649 && rp[ap].disp.is_none()
2650 && rp[bp + 1].disp.is_none()
2651 && rp[ap].str == rp[bp + 1].str
2652 {
2653 rp[bp + 1].flags |= CMF_MULT; // c:3352
2654 dup = 1; // c:3353
2655 bp += 1;
2656 }
2657 if dup != 0 {
2658 rp[ap].flags |= CMF_FMULT; // c:3356
2659 }
2660 ap = bp + 1;
2661 }
2662 rp.truncate(cp); // c:3359
2663 }
2664 for m in rp.iter() { // c:3361
2665 if m.disp.is_some() && (m.flags & CMF_DISPLINE) != 0 { // c:3362
2666 ll += 1;
2667 }
2668 if (m.flags & (CMF_NOLIST | CMF_MULT)) != 0 { // c:3364
2669 nl += 1;
2670 }
2671 }
2672 }
2673 }
2674 (rp, n, nl, ll) // c:3366-3373
2675}
2676
2677/// Port of the `type==0` string-sort branch of `makearray()` from
2678/// compcore.c:3239-3257. Sorts strings via `strmetasort` + dedup.
2679pub fn makearray_strings(mut rp: Vec<String>, flags: i32) -> (Vec<String>, i32) { // c:3239
2680 let mut n: i32 = rp.len() as i32;
2681 if flags != 0 && n > 0 { // c:3240
2682 let numeric = isset(NUMERICGLOBSORT); // c:3243
2683 let mut sf = SORTIT_IGNORING_BACKSLASHES as u32;
2684 if numeric {
2685 sf |= SORTIT_NUMERICALLY as u32;
2686 }
2687 crate::ported::sort::strmetasort(&mut rp, sf, None); // c:3242-3244
2688
2689 // Dedup consecutive equals. // c:3247
2690 let mut cp = 0usize;
2691 let mut ap = 0usize;
2692 while ap < rp.len() {
2693 if ap != cp { rp.swap(ap, cp); }
2694 cp += 1;
2695 let mut bp = ap;
2696 while bp + 1 < rp.len() && rp[ap] == rp[bp + 1] { // c:3250
2697 bp += 1; n -= 1;
2698 }
2699 ap = bp + 1; // c:3252
2700 }
2701 rp.truncate(cp); // c:3253
2702 }
2703 (rp, n)
2704}
2705
2706// =====================================================================
2707// dupmatch — `Src/Zle/compcore.c:3370`.
2708// =====================================================================
2709
2710/// Port of `static Cmatch dupmatch(Cmatch m, int nbeg, int nend)` from
2711/// compcore.c:3370. Deep-copies one match; brpl/brsl are truncated to
2712/// nbeg/nend per the C body's nbeg/nend-sized `zalloc` + element copy.
2713pub fn dupmatch(m: &Cmatch, nbeg: i32, nend: i32) -> Cmatch { // c:3370
2714 let mut r = Cmatch::default(); // c:3370-3374
2715 r.str = m.str.clone(); // c:3376 ztrdup
2716 r.orig = m.orig.clone(); // c:3377
2717 r.ipre = m.ipre.clone(); // c:3378
2718 r.ripre = m.ripre.clone(); // c:3379
2719 r.isuf = m.isuf.clone(); // c:3380
2720 r.ppre = m.ppre.clone(); // c:3381
2721 r.psuf = m.psuf.clone(); // c:3382
2722 r.prpre = m.prpre.clone(); // c:3383
2723 r.pre = m.pre.clone(); // c:3384
2724 r.suf = m.suf.clone(); // c:3385
2725 r.flags = m.flags; // c:3386
2726 if !m.brpl.is_empty() { // c:3387
2727 let take = (nbeg as usize).min(m.brpl.len()); // c:3390 zalloc(nbeg)
2728 r.brpl = m.brpl[..take].to_vec(); // c:3392 element-wise copy
2729 } else {
2730 r.brpl = Vec::new(); // c:3395 NULL
2731 }
2732 if !m.brsl.is_empty() { // c:3396
2733 let take = (nend as usize).min(m.brsl.len()); // c:3399
2734 r.brsl = m.brsl[..take].to_vec(); // c:3401
2735 } else {
2736 r.brsl = Vec::new(); // c:3404
2737 }
2738 r.rems = m.rems.clone(); // c:3405
2739 r.remf = m.remf.clone(); // c:3406
2740 r.autoq = m.autoq.clone(); // c:3407
2741 r.qipl = m.qipl; // c:3408
2742 r.qisl = m.qisl; // c:3409
2743 r.disp = m.disp.clone(); // c:3410
2744 r.mode = m.mode; // c:3411
2745 r.modec = m.modec; // c:3412
2746 r.fmode = m.fmode; // c:3413
2747 r.fmodec = m.fmodec; // c:3414
2748 r // c:3416
2749}
2750
2751// =====================================================================
2752// permmatches — `Src/Zle/compcore.c:3423`.
2753// =====================================================================
2754
2755/// Static state for `permmatches`'s `static int fi`. C scopes the
2756/// flag to the function; Rust hoists it to file scope per Rule S1.
2757static PERMMATCHES_FI: AtomicI32 = AtomicI32::new(0); // c:3423 static int fi
2758
2759/// Port of `mod_export int permmatches(int last)` from compcore.c:3422.
2760/// Promotes the per-round `amatches` accumulator into the permanent
2761/// `pmatches` snapshot via deep-copy through `dupmatch`/`makearray`.
2762pub fn permmatches(last: i32) -> i32 { // c:3423
2763 let ofi = PERMMATCHES_FI.load(Ordering::Relaxed); // c:3423 ofi = fi
2764
2765 // c:3433 — `if (pmatches && !newmatches)`
2766 let pmatches_set = pmatches.get_or_init(|| Mutex::new(Vec::new()))
2767 .lock().map(|g| !g.is_empty()).unwrap_or(false);
2768 if pmatches_set && newmatches.load(Ordering::Relaxed) == 0 { // c:3433
2769 if last != 0 && PERMMATCHES_FI.load(Ordering::Relaxed) != 0 { // c:3434
2770 // ainfo = fainfo // c:3435
2771 let famref = fainfo.get_or_init(|| Mutex::new(None))
2772 .lock().ok().and_then(|g| g.clone());
2773 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2774 *a = famref;
2775 }
2776 }
2777 return PERMMATCHES_FI.load(Ordering::Relaxed); // c:3437
2778 }
2779 newmatches.store(0, Ordering::Relaxed); // c:3439
2780 PERMMATCHES_FI.store(0, Ordering::Relaxed); // c:3439 fi = 0
2781
2782 {
2783 // pmatches = lmatches = NULL // c:3441
2784 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2785 g.clear();
2786 }
2787 if let Ok(mut g) = lmatches.get_or_init(|| Mutex::new(None)).lock() {
2788 *g = None;
2789 }
2790 }
2791 nmatches.store(0, Ordering::Relaxed); // c:3442
2792 smatches.store(0, Ordering::Relaxed); // c:3442
2793 diffmatches.store(0, Ordering::Relaxed); // c:3442
2794
2795 // c:3444 — `if (!ainfo->count)`.
2796 let ainfo_count = ainfo.get_or_init(|| Mutex::new(None))
2797 .lock().ok().and_then(|g| g.as_ref().map(|a| a.count)).unwrap_or(0);
2798 if ainfo_count == 0 { // c:3444
2799 if last != 0 { // c:3445
2800 let famref = fainfo.get_or_init(|| Mutex::new(None))
2801 .lock().ok().and_then(|g| g.clone());
2802 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
2803 *a = famref;
2804 }
2805 }
2806 PERMMATCHES_FI.store(1, Ordering::Relaxed); // c:3447
2807 }
2808
2809 let nbeg = crate::ported::zle::zle_tricky::NBRBEG.load(Ordering::Relaxed);
2810 let nend = crate::ported::zle::zle_tricky::NBREND.load(Ordering::Relaxed);
2811
2812 let mut gn: i32 = 1; // c:3429 gn = 1
2813 let mut mn: i32 = 1; // c:3429 mn = 1
2814 let fi = PERMMATCHES_FI.load(Ordering::Relaxed);
2815
2816 let groups_snapshot: Vec<Cmgroup> = {
2817 amatches.get_or_init(|| Mutex::new(Vec::new()))
2818 .lock().ok().map(|g| g.clone()).unwrap_or_default()
2819 };
2820 let mut new_pmatches: Vec<Cmgroup> = Vec::with_capacity(groups_snapshot.len());
2821
2822 for g_orig in groups_snapshot.into_iter() { // c:3449 while (g)
2823 let mut g = g_orig; // borrow-mut snapshot
2824 let must_rebuild = fi != ofi || g.perm.is_none() || g.new_ != 0; // c:3456
2825 if must_rebuild { // c:3456
2826 let src_list = if fi != 0 { g.lfmatches.clone() } // c:3457
2827 else { g.lmatches.clone() }; // c:3461
2828
2829 let (arr, nn, nl, ll) = makearray(src_list, g.flags); // c:3463
2830 g.mcount = nn; // c:3464
2831 g.lcount = nn - nl; // c:3465
2832 if g.lcount < 0 { g.lcount = 0; } // c:3466
2833 g.llcount = ll; // c:3467
2834 if !g.ylist.is_empty() { // c:3468
2835 g.lcount = g.ylist.len() as i32; // c:3469
2836 smatches.store(2, Ordering::Relaxed); // c:3470
2837 }
2838 // c:3472 — makearray(lexpls, 0, 0, &ecount, NULL, NULL).
2839 let mut exps = g.lexpls.clone(); // type=0 path
2840 g.ecount = exps.len() as i32;
2841 // c:3475 ccount = 0
2842 g.ccount = 0; // c:3475
2843 nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3477
2844 smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3478
2845 if g.mcount > 1 { // c:3480
2846 diffmatches.store(1, Ordering::Relaxed); // c:3481
2847 }
2848
2849 // n = (Cmgroup) zshcalloc(...) // c:3483
2850 let mut n_grp = Cmgroup::default();
2851 // c:3487 — `if (g->perm) freematches(g->perm, 0)`. Drop on
2852 // perm Box<Cmgroup> reclaims the C `free` path.
2853 g.perm = None; // c:3490 g->perm = n
2854 // Then below we set g.perm = Some(Box::new(n_grp.clone())).
2855
2856 n_grp.num = gn; gn += 1; // c:3499
2857 n_grp.flags = g.flags; // c:3500
2858 n_grp.mcount = g.mcount; // c:3501
2859 n_grp.matches = arr.iter() // c:3502-3505 dupmatch loop
2860 .map(|m| dupmatch(m, nbeg, nend))
2861 .collect();
2862 n_grp.name = g.name.clone(); // c:3504
2863 n_grp.lcount = g.lcount; // c:3508
2864 n_grp.llcount = g.llcount; // c:3509
2865 if !g.ylist.is_empty() { // c:3510
2866 n_grp.ylist = g.ylist.clone(); // c:3511 zarrdup
2867 } else {
2868 n_grp.ylist = Vec::new(); // c:3513
2869 }
2870 if g.ecount != 0 { // c:3515
2871 // Build n->expls from g->expls deep-copying str + (fi
2872 // ? fcount : count); always carries over; fcount = 0.
2873 n_grp.expls = exps.drain(..).map(|o| Cexpl { // c:3517-3525
2874 count: if fi != 0 { o.fcount } else { o.count }, // c:3520
2875 always: o.always, // c:3521
2876 fcount: 0, // c:3522
2877 str: o.str.clone(), // c:3523 ztrdup
2878 }).collect();
2879 n_grp.ecount = g.ecount;
2880 } else {
2881 n_grp.expls = Vec::new(); // c:3528
2882 }
2883 n_grp.widths = Vec::new(); // c:3531
2884 // Stitch perm chain (prev/next handled implicitly by Vec).
2885 g.matches = arr; // mirror C: g->matches = makearray result
2886 g.perm = Some(Box::new(n_grp.clone())); // c:3490 g->perm = n
2887 new_pmatches.push(n_grp); // c:3492-3496
2888 } else {
2889 // reuse existing g->perm // c:3534
2890 nmatches.fetch_add(g.mcount, Ordering::Relaxed); // c:3540
2891 smatches.fetch_add(g.lcount, Ordering::Relaxed); // c:3541
2892 if g.mcount > 1 {
2893 diffmatches.store(1, Ordering::Relaxed); // c:3543
2894 }
2895 g.num = gn; gn += 1; // c:3546
2896 if let Some(p) = g.perm.as_deref() {
2897 new_pmatches.push(p.clone()); // c:3537 pmatches = g->perm
2898 }
2899 }
2900 g.new_ = 0; // c:3548
2901 }
2902
2903 // c:3551-3563 — assign rnum/gnum, recompute diffmatches/nbrbeg.
2904 let mut first_first: Option<Cmatch> = None;
2905 for g_pm in new_pmatches.iter_mut() {
2906 g_pm.nbrbeg = nbeg; // c:3552
2907 g_pm.nbrend = nend; // c:3553
2908 let mut rn = 1i32; // c:3554
2909 for m in g_pm.matches.iter_mut() {
2910 m.rnum = rn; rn += 1; // c:3555
2911 m.gnum = mn; mn += 1; // c:3556
2912 }
2913 if diffmatches.load(Ordering::Relaxed) == 0 && !g_pm.matches.is_empty() {
2914 match first_first.as_ref() { // c:3558
2915 Some(p0) => {
2916 if !matcheq(&g_pm.matches[0], p0) {
2917 diffmatches.store(1, Ordering::Relaxed); // c:3560
2918 }
2919 }
2920 None => first_first = Some(g_pm.matches[0].clone()), // c:3562
2921 }
2922 }
2923 }
2924
2925 if let Ok(mut g) = pmatches.get_or_init(|| Mutex::new(Vec::new())).lock() {
2926 *g = new_pmatches;
2927 }
2928
2929 hasperm.store(1, Ordering::Relaxed); // c:3565
2930 permmnum.store(mn - 1, Ordering::Relaxed); // c:3566
2931 permgnum.store(gn - 1, Ordering::Relaxed); // c:3567
2932 if let Ok(mut ld) = listdat.get_or_init(|| Mutex::new(Default::default())).lock() {
2933 ld.valid = 0; // c:3568
2934 }
2935
2936 fi // c:3570
2937}
2938
2939#[cfg(test)]
2940mod tests {
2941 use super::*;
2942
2943 #[test]
2944 fn rembslash_basic() {
2945 let _g = crate::ported::zle::zle_main::zle_test_setup();
2946 assert_eq!(rembslash("hello\\ world"), "hello world");
2947 assert_eq!(rembslash("no\\\\slash"), "no\\slash");
2948 assert_eq!(rembslash("plain"), "plain");
2949 }
2950
2951 #[test]
2952 fn comp_quoting_string_table() {
2953 let _g = crate::ported::zle::zle_main::zle_test_setup();
2954 assert_eq!(comp_quoting_string(QT_SINGLE), "'");
2955 assert_eq!(comp_quoting_string(QT_DOUBLE), "\"");
2956 assert_eq!(comp_quoting_string(QT_DOLLARS), "$'");
2957 assert_eq!(comp_quoting_string(0), "\\");
2958 assert_eq!(comp_quoting_string(QT_BACKSLASH), "\\");
2959 }
2960
2961 #[test]
2962 fn matcheq_equal_strings() {
2963 let _g = crate::ported::zle::zle_main::zle_test_setup();
2964 let mut a = Cmatch::default(); a.str = Some("foo".into());
2965 let mut b = Cmatch::default(); b.str = Some("foo".into());
2966 assert!(matcheq(&a, &b));
2967 }
2968
2969 #[test]
2970 fn matcheq_different_strings() {
2971 let _g = crate::ported::zle::zle_main::zle_test_setup();
2972 let mut a = Cmatch::default(); a.str = Some("foo".into());
2973 let mut b = Cmatch::default(); b.str = Some("bar".into());
2974 assert!(!matcheq(&a, &b));
2975 }
2976
2977 #[test]
2978 fn matcheq_one_side_none() {
2979 let _g = crate::ported::zle::zle_main::zle_test_setup();
2980 let mut a = Cmatch::default(); a.pre = Some("p".into());
2981 let b = Cmatch::default();
2982 assert!(!matcheq(&a, &b));
2983 }
2984
2985 #[test]
2986 fn get_user_var_reads_array_from_paramtab() {
2987 // c:2003 — `getaparam(nam)` first. Verify array params come
2988 // out as a Vec, not via env.
2989 let _g = crate::ported::zle::zle_main::zle_test_setup();
2990 crate::ported::params::setaparam(
2991 "__test_arr",
2992 vec!["a".into(), "bb".into(), "ccc".into()],
2993 );
2994 let got = get_user_var(Some("__test_arr"));
2995 assert_eq!(got, Some(vec!["a".into(), "bb".into(), "ccc".into()]));
2996 // Cleanup so we don't poison other tests.
2997 crate::ported::params::setaparam("__test_arr", vec![]);
2998 }
2999
3000 #[test]
3001 fn get_user_var_reads_scalar_as_single_element_array() {
3002 // c:2007-2009 — getsparam fallback: wrap scalar in 1-element array.
3003 let _g = crate::ported::zle::zle_main::zle_test_setup();
3004 crate::ported::params::setsparam("__test_scalar", "hello");
3005 let got = get_user_var(Some("__test_scalar"));
3006 assert_eq!(got, Some(vec!["hello".to_string()]));
3007 crate::ported::params::setsparam("__test_scalar", "");
3008 }
3009
3010 #[test]
3011 fn get_user_var_paren_list_splits_on_separators() {
3012 // c:1960-1996 — `(a b c)` paren list, NOT a param lookup.
3013 let _g = crate::ported::zle::zle_main::zle_test_setup();
3014 let got = get_user_var(Some("(one two three)"));
3015 assert_eq!(got, Some(vec!["one".into(), "two".into(), "three".into()]));
3016 }
3017
3018 #[test]
3019 fn get_user_var_none_for_missing() {
3020 // c:1956 + c:2009 — missing param returns None.
3021 let _g = crate::ported::zle::zle_main::zle_test_setup();
3022 // (env vars must not leak through — we don't read $PATH etc.)
3023 let got = get_user_var(Some("__definitely_not_a_param_xyz"));
3024 assert_eq!(got, None);
3025 }
3026
3027 #[test]
3028 fn get_data_arr_reads_hashed_keys_or_values() {
3029 // c:2022 — fetchvalue(name, SCANPM_WANTKEYS|WANTVALS|MATCHMANY).
3030 let _g = crate::ported::zle::zle_main::zle_test_setup();
3031 crate::ported::params::sethparam(
3032 "__test_hash",
3033 vec!["k1".into(), "v1".into(), "k2".into(), "v2".into()],
3034 );
3035
3036 let keys = get_data_arr("__test_hash", true);
3037 assert!(keys.is_some(), "hashed param should have keys");
3038 let mut keys = keys.unwrap();
3039 keys.sort();
3040 assert_eq!(keys, vec!["k1".to_string(), "k2".to_string()]);
3041
3042 let vals = get_data_arr("__test_hash", false);
3043 assert!(vals.is_some(), "hashed param should have values");
3044 let mut vals = vals.unwrap();
3045 vals.sort();
3046 assert_eq!(vals, vec!["v1".to_string(), "v2".to_string()]);
3047 }
3048
3049 #[test]
3050 fn get_data_arr_none_for_non_hashed() {
3051 // c:2032 — fetchvalue NULL → return NULL for params that
3052 // aren't associative arrays.
3053 let _g = crate::ported::zle::zle_main::zle_test_setup();
3054 crate::ported::params::setsparam("__test_scalar2", "value");
3055 let got = get_data_arr("__test_scalar2", false);
3056 assert_eq!(got, None,
3057 "scalar params must NOT come out of get_data_arr");
3058 }
3059
3060 #[test]
3061 fn before_complete_snapshots_oldmenucmp() {
3062 // c:463 — `oldmenucmp = menucmp;`
3063 let _g = crate::ported::zle::zle_main::zle_test_setup();
3064 MENUCMP.store(7, Ordering::Relaxed);
3065 OLDMENUCMP.store(0, Ordering::Relaxed);
3066 let mut lst = 0;
3067 let _ = before_complete(&mut lst);
3068 assert_eq!(OLDMENUCMP.load(Ordering::Relaxed), 7);
3069 // Reset for other tests.
3070 MENUCMP.store(0, Ordering::Relaxed);
3071 OLDMENUCMP.store(0, Ordering::Relaxed);
3072 }
3073
3074 #[test]
3075 fn before_complete_clears_showagain() {
3076 // c:467 — `showagain = 0;` always (after the validlist gate).
3077 let _g = crate::ported::zle::zle_main::zle_test_setup();
3078 crate::ported::zle::zle_tricky::SHOWAGAIN.store(5, Ordering::Relaxed);
3079 let mut lst = 0;
3080 let _ = before_complete(&mut lst);
3081 assert_eq!(
3082 crate::ported::zle::zle_tricky::SHOWAGAIN.load(Ordering::Relaxed),
3083 0,
3084 "SHOWAGAIN must be cleared by before_complete"
3085 );
3086 }
3087
3088 #[test]
3089 fn remsquote_default_quoting() {
3090 let _g = crate::ported::zle::zle_main::zle_test_setup();
3091 let mut s = String::from("a'\\''b");
3092 let n = remsquote(&mut s);
3093 assert_eq!(s, "a'b");
3094 assert_eq!(n, 3);
3095 }
3096
3097 #[test]
3098 fn ctokenize_dollar_substitution() {
3099 let _g = crate::ported::zle::zle_main::zle_test_setup();
3100 let out = ctokenize("$x{y}");
3101 let chars: Vec<char> = out.chars().collect();
3102 assert_eq!(chars[0], Stringg);
3103 assert_eq!(chars[1], 'x');
3104 assert_eq!(chars[2], Inbrace);
3105 assert_eq!(chars[3], 'y');
3106 assert_eq!(chars[4], Outbrace);
3107 }
3108
3109 #[test]
3110 fn get_user_var_inline_list() {
3111 let _g = crate::ported::zle::zle_main::zle_test_setup();
3112 let result = get_user_var(Some("(a b c)")).unwrap();
3113 assert_eq!(result, vec!["a", "b", "c"]);
3114 }
3115
3116 #[test]
3117 fn matchcmp_str_sort_default() {
3118 let _g = crate::ported::zle::zle_main::zle_test_setup();
3119 MATCHORDER.store(CGF_MATSORT, Ordering::Relaxed);
3120 let mut a = Cmatch::default(); a.str = Some("apple".into());
3121 let mut b = Cmatch::default(); b.str = Some("banana".into());
3122 assert_eq!(matchcmp(&a, &b), std::cmp::Ordering::Less);
3123 assert_eq!(matchcmp(&b, &a), std::cmp::Ordering::Greater);
3124 assert_eq!(matchcmp(&a, &a), std::cmp::Ordering::Equal);
3125 MATCHORDER.store(0, Ordering::Relaxed);
3126 }
3127
3128 #[test]
3129 fn dupmatch_clones_strings_and_truncates_braces() {
3130 let _g = crate::ported::zle::zle_main::zle_test_setup();
3131 // C body c:3370: deep-copy strings, truncate brpl/brsl to nbeg/nend.
3132 let mut src = Cmatch::default();
3133 src.str = Some("foo".into());
3134 src.ipre = Some("ipre".into());
3135 src.flags = 7;
3136 src.brpl = vec![10, 20, 30, 40];
3137 src.brsl = vec![5, 6, 7];
3138 src.qipl = 1;
3139 src.qisl = 2;
3140 src.mode = 0o755;
3141 src.modec = 'd';
3142
3143 let r = dupmatch(&src, 2, 1);
3144 assert_eq!(r.str.as_deref(), Some("foo"));
3145 assert_eq!(r.ipre.as_deref(), Some("ipre"));
3146 assert_eq!(r.flags, 7);
3147 assert_eq!(r.brpl, vec![10, 20]); // truncated to nbeg=2
3148 assert_eq!(r.brsl, vec![5]); // truncated to nend=1
3149 assert_eq!(r.qipl, 1);
3150 assert_eq!(r.qisl, 2);
3151 assert_eq!(r.mode, 0o755);
3152 assert_eq!(r.modec, 'd');
3153 }
3154
3155 #[test]
3156 fn dupmatch_empty_braces_stay_empty() {
3157 let _g = crate::ported::zle::zle_main::zle_test_setup();
3158 // C body c:3395/3404: NULL brpl/brsl stay NULL regardless of nbeg/nend.
3159 let src = Cmatch::default();
3160 let r = dupmatch(&src, 5, 5);
3161 assert!(r.brpl.is_empty());
3162 assert!(r.brsl.is_empty());
3163 }
3164
3165 #[test]
3166 fn makearray_sorted_and_deduped() {
3167 let _g = crate::ported::zle::zle_main::zle_test_setup();
3168 // c:3262-3291: sort + dedup with matcheq. Same str + nil disp =>
3169 // collapses into one entry with CMF_FMULT set on the survivor.
3170 let mut a = Cmatch::default(); a.str = Some("z".into());
3171 let mut b = Cmatch::default(); b.str = Some("a".into());
3172 let mut c = Cmatch::default(); c.str = Some("a".into());
3173 let (arr, n, _nl, _ll) = makearray(vec![a, b, c], CGF_MATSORT);
3174 // Two distinct visible strings after dedup ("a", "z").
3175 assert_eq!(arr.len(), 2);
3176 assert_eq!(n, 2);
3177 assert_eq!(arr[0].str.as_deref(), Some("a"));
3178 assert_eq!(arr[1].str.as_deref(), Some("z"));
3179 }
3180
3181 #[test]
3182 fn makearray_nosort_unchanged_order() {
3183 let _g = crate::ported::zle::zle_main::zle_test_setup();
3184 // c:3300: CGF_NOSORT branch; with no UNIQ flags, order preserved.
3185 let mut a = Cmatch::default(); a.str = Some("z".into());
3186 let mut b = Cmatch::default(); b.str = Some("a".into());
3187 let (arr, n, _, _) = makearray(vec![a, b], CGF_NOSORT | CGF_UNIQALL);
3188 // UNIQALL active so no dedup pass runs.
3189 assert_eq!(n, 2);
3190 assert_eq!(arr[0].str.as_deref(), Some("z"));
3191 assert_eq!(arr[1].str.as_deref(), Some("a"));
3192 }
3193
3194 #[test]
3195 fn makearray_strings_dedup_consecutive() {
3196 let _g = crate::ported::zle::zle_main::zle_test_setup();
3197 // c:3239 path: sort + drop adjacent duplicates.
3198 let (arr, n) = makearray_strings(
3199 vec!["b".into(), "a".into(), "a".into(), "c".into()],
3200 1,
3201 );
3202 assert_eq!(n, 3);
3203 assert_eq!(arr, vec!["a", "b", "c"]);
3204 }
3205
3206 #[test]
3207 fn check_param_no_dollar_returns_none() {
3208 let _g = crate::ported::zle::zle_main::zle_test_setup();
3209 // c:1316: no `$` in string → return None.
3210 OFFS.store(2, Ordering::Relaxed);
3211 assert_eq!(check_param("abc", false, false), None);
3212 }
3213
3214 #[test]
3215 fn check_param_simple_dollar_var_at_cursor() {
3216 let _g = crate::ported::zle::zle_main::zle_test_setup();
3217 // c:1259-1311: `$FOO` with cursor inside the name → return b.
3218 OFFS.store(2, Ordering::Relaxed);
3219 let s = format!("{}FOO", crate::ported::zsh_h::Stringg);
3220 let r = check_param(&s, false, true);
3221 assert!(r.is_some(), "expected Some(b) inside $FOO");
3222 }
3223
3224 #[test]
3225 fn callcompfunc_empty_fn_no_panic() {
3226 let _g = crate::ported::zle::zle_main::zle_test_setup();
3227 // c:552: getshfunc(NULL) early-return.
3228 callcompfunc("anything", "");
3229 }
3230
3231 #[test]
3232 fn callcompfunc_sets_compstate_context() {
3233 let _g = crate::ported::zle::zle_main::zle_test_setup();
3234 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3235 // c:619: context selection — verified via the pure
3236 // compcontext_for helper (callcompfunc calls it and writes
3237 // to paramtab via setsparam, but paramtab read-back in a
3238 // unit-test context without a live VM is unreliable).
3239 ispar.store(0, Ordering::Relaxed);
3240 linwhat.store(IN_PAR_LW, Ordering::Relaxed);
3241 assert_eq!(compcontext_for("foo"), "assign_parameter");
3242 // Body executes without panicking against the real paramtab.
3243 callcompfunc("foo", "_test_fn");
3244 }
3245
3246 /// Test-only serializer for tests that mutate file-scope globals.
3247 static GLOBAL_MUT_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
3248
3249 #[test]
3250 fn compcontext_for_routes_ispar_first() {
3251 let _g = crate::ported::zle::zle_main::zle_test_setup();
3252 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3253 ispar.store(2, Ordering::Relaxed);
3254 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
3255 assert_eq!(compcontext_for("x"), "brace_parameter");
3256 ispar.store(1, Ordering::Relaxed);
3257 assert_eq!(compcontext_for("x"), "parameter");
3258 ispar.store(0, Ordering::Relaxed);
3259 linwhat.store(IN_MATH_LW, Ordering::Relaxed);
3260 assert_eq!(compcontext_for("x"), "math");
3261 linwhat.store(IN_COND_LW, Ordering::Relaxed);
3262 assert_eq!(compcontext_for("x"), "condition");
3263 linwhat.store(IN_ENV_LW, Ordering::Relaxed);
3264 assert_eq!(compcontext_for("x"), "value");
3265 linwhat.store(IN_NOTHING_LW, Ordering::Relaxed);
3266 assert_eq!(compcontext_for("x"), "command");
3267 }
3268
3269 #[test]
3270 fn addmatches_empty_argv_early_return() {
3271 let _g = crate::ported::zle::zle_main::zle_test_setup();
3272 // c:2138-2139: empty argv + dummies==0 + no CAF_ALL → return 1.
3273 let mut dat = crate::ported::zle::comp_h::Cadata::default();
3274 dat.dummies = 0;
3275 dat.aflags = 0;
3276 assert_eq!(addmatches(&mut dat, &[]), 1);
3277 }
3278
3279 #[test]
3280 fn addmatches_appends_argv_to_default_group() {
3281 let _g = crate::ported::zle::zle_main::zle_test_setup();
3282 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3283 // c:2200 simplified body: each argv entry → addmatch into "default" group.
3284 amatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3285 matches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3286 let mut dat = crate::ported::zle::comp_h::Cadata::default();
3287 dat.dummies = -1;
3288 let _ = addmatches(&mut dat, &["a".into(), "b".into()]);
3289 let n = matches.get().unwrap().lock().unwrap().len();
3290 assert!(n >= 2);
3291 }
3292
3293 #[test]
3294 fn add_match_data_returns_populated_cmatch() {
3295 let _g = crate::ported::zle::zle_main::zle_test_setup();
3296 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3297 // c:3052-3067: cm.str/orig/pre/suf populated; mnum bumps by 1.
3298 matches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3299 let before = mnum.load(Ordering::Relaxed);
3300 let cm = add_match_data(
3301 0, "match", "match-orig", None,
3302 "ipre", "ripre", "isuf",
3303 "pre", "prpre", "ppre", None,
3304 "psuf", None,
3305 "suf", 0, 0,
3306 );
3307 assert_eq!(cm.str.as_deref(), Some("match"));
3308 assert_eq!(cm.orig.as_deref(), Some("match-orig"));
3309 assert_eq!(cm.pre.as_deref(), Some("pre"));
3310 assert_eq!(cm.suf.as_deref(), Some("suf"));
3311 assert_eq!(mnum.load(Ordering::Relaxed), before + 1);
3312 }
3313
3314 #[test]
3315 fn add_match_data_exact_records_into_ainfo() {
3316 let _g = crate::ported::zle::zle_main::zle_test_setup();
3317 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3318 // c:3060-3062: exact != 0 writes ai.exact/exactm.
3319 if let Ok(mut g) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3320 *g = Some(Aminfo::default());
3321 }
3322 let _ = add_match_data(
3323 0, "x", "x", None, "", "", "", "", "", "", None, "", None, "", 0, 1,
3324 );
3325 let a = ainfo.get().unwrap().lock().unwrap().clone().unwrap();
3326 assert_eq!(a.exact, 1);
3327 assert!(a.exactm.is_some());
3328 }
3329
3330 #[test]
3331 fn set_comp_sep_returns_one() {
3332 let _g = crate::ported::zle::zle_main::zle_test_setup();
3333 // c:1937: stubbed body returns 1 (no-change marker).
3334 assert_eq!(set_comp_sep(), 1);
3335 }
3336
3337 #[test]
3338 fn foredel_deletes_forward_from_zlemetacs() {
3339 let _g = crate::ported::zle::zle_main::zle_test_setup();
3340 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3341 // zle_utils.c:1105 — delete `ct` chars forward from ZLEMETACS.
3342 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
3343 *g = "abcdef".to_string();
3344 }
3345 ZLEMETACS.store(2, Ordering::Relaxed);
3346 ZLEMETALL.store(6, Ordering::Relaxed);
3347 foredel(3);
3348 let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
3349 assert_eq!(line, "abf");
3350 assert_eq!(ZLEMETALL.load(Ordering::Relaxed), 3);
3351 }
3352
3353 #[test]
3354 fn inststr_inserts_at_zlemetacs() {
3355 let _g = crate::ported::zle::zle_main::zle_test_setup();
3356 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3357 // zle_tricky.c:278 — insert at cursor.
3358 if let Ok(mut g) = ZLEMETALINE.get_or_init(|| Mutex::new(String::new())).lock() {
3359 *g = "hello".to_string();
3360 }
3361 ZLEMETACS.store(5, Ordering::Relaxed);
3362 ZLEMETALL.store(5, Ordering::Relaxed);
3363 inststr(" world");
3364 let line = ZLEMETALINE.get().unwrap().lock().unwrap().clone();
3365 assert_eq!(line, "hello world");
3366 assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 11);
3367 }
3368
3369 #[test]
3370 fn metafy_and_unmetafy_roundtrip_globals() {
3371 let _g = crate::ported::zle::zle_main::zle_test_setup();
3372 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3373 // zle_tricky.c:978,995 — meta/unmeta operate on the global pair.
3374 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
3375 *g = "plain ascii".to_string();
3376 }
3377 ZLECS.store(3, Ordering::Relaxed);
3378 ZLELL.store(11, Ordering::Relaxed);
3379 metafy_line();
3380 // For ASCII input the meta form equals the raw form.
3381 assert_eq!(
3382 ZLEMETALINE.get().unwrap().lock().unwrap().clone(),
3383 "plain ascii"
3384 );
3385 assert_eq!(ZLEMETACS.load(Ordering::Relaxed), 3);
3386 unmetafy_line();
3387 assert_eq!(
3388 ZLELINE.get().unwrap().lock().unwrap().clone(),
3389 "plain ascii"
3390 );
3391 }
3392
3393 #[test]
3394 fn selfinsert_appends_lastchar_at_zlecs() {
3395 let _g = crate::ported::zle::zle_main::zle_test_setup();
3396 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3397 // zle_misc.c:112-141 — insert one char at cursor, bump zlecs.
3398 if let Ok(mut g) = ZLELINE.get_or_init(|| Mutex::new(String::new())).lock() {
3399 *g = "ab".to_string();
3400 }
3401 ZLECS.store(2, Ordering::Relaxed);
3402 ZLELL.store(2, Ordering::Relaxed);
3403 LASTCHAR.store(b'c' as i32, Ordering::Relaxed);
3404 let rv = selfinsert();
3405 assert_eq!(rv, 0);
3406 assert_eq!(ZLELINE.get().unwrap().lock().unwrap().clone(), "abc");
3407 assert_eq!(ZLECS.load(Ordering::Relaxed), 3);
3408 }
3409
3410 #[test]
3411 fn minfo_clear_and_asked_zero_mutate_state() {
3412 let _g = crate::ported::zle::zle_main::zle_test_setup();
3413 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3414 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() {
3415 let mut cm = Cmatch::default();
3416 cm.str = Some("x".into());
3417 g.cur = Some(Box::new(cm));
3418 g.asked = 1;
3419 }
3420 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.cur = None; }
3421 if let Ok(mut g) = MINFO.get_or_init(|| Mutex::new(crate::ported::zle::comp_h::Menuinfo::default())).lock() { g.asked = 0; }
3422 let m = MINFO.get().unwrap().lock().unwrap().clone();
3423 assert!(m.cur.is_none());
3424 assert_eq!(m.asked, 0);
3425 }
3426
3427 #[test]
3428 fn cline_matched_stub_marks_node() {
3429 let _g = crate::ported::zle::zle_main::zle_test_setup();
3430 // compmatch.c:253 — sets CLF_MATCHED on the node chain. We
3431 // verify by running through the stub on a non-empty string
3432 // without panicking and trusting compmatch's body for the
3433 // actual flag set.
3434 cline_matched_compcore(Some("foo"));
3435 cline_matched_compcore(None);
3436 cline_matched_compcore(Some(""));
3437 }
3438
3439 #[test]
3440 fn permmatches_returns_fi_zero_when_count_present() {
3441 let _g = crate::ported::zle::zle_main::zle_test_setup();
3442 let _g = GLOBAL_MUT_LOCK.lock().unwrap();
3443 // c:3444-3447: if ainfo->count is non-zero, fi stays 0.
3444 amatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3445 pmatches.get_or_init(|| Mutex::new(Vec::new())).lock().unwrap().clear();
3446 if let Ok(mut a) = ainfo.get_or_init(|| Mutex::new(None)).lock() {
3447 *a = Some(Aminfo { count: 5, ..Default::default() });
3448 }
3449 newmatches.store(1, Ordering::Relaxed);
3450 let fi = permmatches(0);
3451 assert_eq!(fi, 0);
3452 assert_eq!(hasperm.load(Ordering::Relaxed), 1);
3453 }
3454}