Skip to main content

zsh/ported/zle/
zle_h.rs

1//! Direct port of `Src/Zle/zle.h` — line-editor type / constant
2//! header.
3//!
4//! Original C copyright: Paul Falstad 1992-1997.
5//!
6//! Hosts the type aliases (`Widget` / `Thingy` / `Keymap` /
7//! `Cutbuffer` / etc.), enum / `#define` constants used across the
8//! ZLE port, and the `struct widget` / `struct thingy` /
9//! `struct modifier` / `struct change` / `struct vichange` /
10//! `struct cutbuffer` / `struct brinfo` / `struct compldat` /
11//! `struct region_highlight` / `struct watch_fd` / `REFRESH_ELEMENT`
12//! shapes the C source uses.
13//!
14//! Multibyte note: zshrs uses native UTF-8 throughout, so the C
15//! `#ifdef MULTIBYTE_SUPPORT` (zle.h:30-105) `wchar_t` types collapse
16//! onto Rust `char` / `String`. The non-multibyte fallback (`char`
17//! / `int`) is the parallel C path; the type aliases below pick
18//! the shape that maps cleanly onto Rust.
19
20#![allow(non_camel_case_types, non_snake_case, dead_code)]
21
22use crate::ported::zsh_h::{zattr, HashNode};
23
24// =====================================================================
25// Wide-character types — `Src/Zle/zle.h:30-110`.
26// =====================================================================
27//
28// C dispatches between `wchar_t` (MULTIBYTE_SUPPORT, zle.h:30-104)
29// and `char` (non-multibyte, zle.h:105-181). Rust uses `char` for
30// code-points and `String` for owned text — those primitives cover
31// both C paths.
32
33#[allow(unused_imports)]
34use crate::ported::zle::{
35    deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
36    zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
37};
38/// Port of `ZLE_CHAR_T` from zle.h:31 / zle.h:107.
39
40// --- AUTO: cross-zle hoisted-fn use glob ---
41#[allow(unused_imports)]
42// (was: use crate::ported::zle::widget::*; — widget.rs deleted)
43/// `ZLE_CHAR_T` type alias.
44#[allow(unused_imports)]
45
46pub type ZLE_CHAR_T = char; // c:31
47
48/// Port of `ZLE_STRING_T` from zle.h:32 / zle.h:108.
49pub type ZLE_STRING_T = String; // c:32
50
51/// Port of `ZLE_INT_T` from zle.h:33 / zle.h:109.
52pub type ZLE_INT_T = i32; // c:33
53
54/// Port of `ZLE_CHAR_SIZE` from zle.h:34. Rust `char` is always 4
55/// bytes (USVs); the C `wchar_t` is 4 on every supported host.
56pub const ZLE_CHAR_SIZE: usize = 4; // c:34
57
58/// Port of `ZLEEOF` from zle.h:37 / zle.h:112.
59pub const ZLEEOF: i32 = -1; // c:37 WEOF
60
61/// Port of `Th(X)` macro from zle.h:316. `#define Th(X) (&thingies[X])`.
62/// Resolves a fixed-thingy index into its Thingy reference.
63///
64/// In C, `thingies[]` is auto-generated by `mkbltnmlst.sh` from the
65/// `mod_export Thingy` declarations sprinkled across `zle.h`/`zle_*.c`.
66/// Each `t_<name>` constant indexes one slot. zshrs hasn't ported
67/// that build-time auto-gen, so this helper uses the manually-kept
68/// index→name table in `T_THINGY_NAMES` below and forwards to
69/// `lookup_thingy(name)` — same observable contract as the C macro.
70#[inline]
71pub fn Th(index: i32) -> Option<crate::ported::zle::zle_thingy::Thingy> {
72    // c:316
73    let i: usize = (index as i64).try_into().ok()?;
74    let name = T_THINGY_NAMES.get(i)?;
75    crate::ported::zle::zle_thingy::gethashnode2(name)
76}
77
78/// Fixed-thingy index table. Order matches the `mod_export Thingy
79/// t_<name>` declaration order in `Src/Zle/zle.h` so callers can use
80/// `Th(t_acceptline as i32)` exactly as they would in C. Add new
81/// entries here when `t_<name>` constants get back-ported; the
82/// indices are stable per zsh ABI.
83pub const T_THINGY_NAMES: &[&str] = &[
84    "accept-and-hold",                     // t_acceptandhold     = 0
85    "accept-line",                         // t_acceptline        = 1
86    "accept-line-and-down-history",        // t_acceptlineanddownhistory
87    "accept-search",                       // t_acceptsearch
88    "auto-suffix-remove",                  // t_autosuffixremove
89    "auto-suffix-retain",                  // t_autosuffixretain
90    "backward-char",                       // t_backwardchar
91    "backward-delete-char",                // t_backwarddeletechar
92    "beep",                                // t_beep
93    "clear-screen",                        // t_clearscreen
94    "complete-word",                       // t_completeword
95    "describe-key-briefly",                // t_describekeybriefly
96    "down-line-or-history",                // t_downlineorhistory
97    "execute-named-cmd",                   // t_executenamedcmd
98    "exit",                                // t_exit
99    "forward-char",                        // t_forwardchar
100    "history-incremental-search-backward", // t_historyincrementalsearchbackward
101    "history-incremental-search-forward",  // t_historyincrementalsearchforward
102    "list-choices",                        // t_listchoices
103    "menu-complete",                       // t_menucomplete
104    "menu-expand-or-complete",             // t_menuexpandorcomplete
105    "redisplay",                           // t_redisplay
106    "self-insert",                         // t_selfinsert
107    "self-insert-unmeta",                  // t_selfinsertunmeta
108    "send-break",                          // t_sendbreak
109    "undefined-key",                       // t_undefinedkey
110    "undo",                                // t_undo
111    "up-line-or-history",                  // t_uplineorhistory
112    "vi-cmd-mode",                         // t_vicmdmode
113    "yank",                                // t_yank
114];
115
116/// Port of `invicmdmode()` macro from zle.h:324.
117/// `#define invicmdmode() (!strcmp(curkeymapname, "vicmd"))`.
118/// True when the current keymap is the vi command-mode keymap.
119/// The Rust `in_vi_cmd_mode()` free fn (zle_main.rs:815) is the
120/// state-bound counterpart; this free-fn uses the global keymap name.
121#[inline]
122pub fn invicmdmode(curkeymapname: &str) -> bool {
123    // c:324
124    curkeymapname == "vicmd"
125}
126
127// =====================================================================
128// `ZS_*` wide-string macros — `Src/Zle/zle.h:40-51`.
129// C #defines route to wmemcpy/wcslen/etc. on MULTIBYTE_SUPPORT builds
130// and to the memcpy/strlen counterparts otherwise. Rust uses `char` /
131// `[char]` directly so each macro maps to a slice operation.
132// =====================================================================
133
134/// Port of `ZS_memcpy` from zle.h:40 (`#define ZS_memcpy wmemcpy`).
135/// Copies `n` chars from `src` into `dst`.
136#[inline]
137pub fn ZS_memcpy(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T], n: usize) {
138    // c:40
139    dst[..n].copy_from_slice(&src[..n]);
140}
141
142/// Port of `ZS_memmove` from zle.h:41 (`#define ZS_memmove wmemmove`).
143/// Same as ZS_memcpy but tolerates overlapping ranges (vec move
144/// semantics handle overlap).
145#[inline]
146pub fn ZS_memmove(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T], n: usize) {
147    // c:41
148    let v: Vec<ZLE_CHAR_T> = src[..n].to_vec();
149    dst[..n].copy_from_slice(&v);
150}
151
152/// Port of `ZS_memset` from zle.h:42 (`#define ZS_memset wmemset`).
153/// Fills `dst[..n]` with `c`.
154#[inline]
155pub fn ZS_memset(dst: &mut [ZLE_CHAR_T], c: ZLE_CHAR_T, n: usize) {
156    // c:42
157    for slot in dst.iter_mut().take(n) {
158        *slot = c;
159    }
160}
161
162/// Port of `ZS_memcmp` from zle.h:43 (`#define ZS_memcmp wmemcmp`).
163/// Returns Ordering of the first `n` chars.
164#[inline]
165pub fn ZS_memcmp(a: &[ZLE_CHAR_T], b: &[ZLE_CHAR_T], n: usize) -> std::cmp::Ordering {
166    // c:43
167    a[..n].cmp(&b[..n])
168}
169
170/// Port of `ZS_strlen` from zle.h:44 (`#define ZS_strlen wcslen`).
171/// Returns the length to the first NUL char (or full slice length
172/// if no NUL found).
173#[inline]
174pub fn ZS_strlen(s: &[ZLE_CHAR_T]) -> usize {
175    // c:44
176    s.iter().position(|&c| c == '\0').unwrap_or(s.len())
177}
178
179/// Port of `ZS_strcpy` from zle.h:45 (`#define ZS_strcpy wcscpy`).
180/// Copies `src` (up to first NUL or end) into `dst`, NUL-terminates
181/// when there's room.
182#[inline]
183pub fn ZS_strcpy(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T]) {
184    // c:45
185    let n = ZS_strlen(src);
186    let limit = dst.len().min(n);
187    dst[..limit].copy_from_slice(&src[..limit]);
188    if limit < dst.len() {
189        dst[limit] = '\0';
190    }
191}
192
193/// Port of `ZS_strncpy` from zle.h:46 (`#define ZS_strncpy wcsncpy`).
194/// Copies up to `n` chars; pads remainder with NUL if `src` is shorter.
195#[inline]
196pub fn ZS_strncpy(dst: &mut [ZLE_CHAR_T], src: &[ZLE_CHAR_T], n: usize) {
197    // c:46
198    let s_len = ZS_strlen(src).min(n);
199    let limit = dst.len().min(n);
200    let copy = s_len.min(limit);
201    dst[..copy].copy_from_slice(&src[..copy]);
202    for slot in dst.iter_mut().take(limit).skip(copy) {
203        *slot = '\0';
204    }
205}
206
207/// Port of `ZS_strncmp` from zle.h:47 (`#define ZS_strncmp wcsncmp`).
208/// Returns Ordering of up to `n` chars (stops at NUL).
209#[inline]
210pub fn ZS_strncmp(a: &[ZLE_CHAR_T], b: &[ZLE_CHAR_T], n: usize) -> std::cmp::Ordering {
211    // c:47
212    let a_n = ZS_strlen(a).min(n);
213    let b_n = ZS_strlen(b).min(n);
214    let limit = a_n.min(b_n);
215    let cmp = a[..limit].cmp(&b[..limit]);
216    if cmp != std::cmp::Ordering::Equal {
217        return cmp;
218    }
219    a_n.cmp(&b_n)
220}
221
222/// Port of `ZS_strchr` from zle.h:50 (`#define ZS_strchr wcschr`).
223/// Returns the index of the first occurrence of `c` in `s`, or `None`.
224#[inline]
225pub fn ZS_strchr(s: &[ZLE_CHAR_T], c: ZLE_CHAR_T) -> Option<usize> {
226    // c:50
227    s.iter().position(|&x| x == c)
228}
229
230/// Port of `ZS_memchr` from zle.h:51 (`#define ZS_memchr wmemchr`).
231/// Returns the index of the first occurrence of `c` in `s[..n]`.
232#[inline]
233pub fn ZS_memchr(s: &[ZLE_CHAR_T], c: ZLE_CHAR_T, n: usize) -> Option<usize> {
234    // c:51
235    s[..n.min(s.len())].iter().position(|&x| x == c)
236}
237
238/// Port of `ZS_width` from zle.h:49 (`#define ZS_width wcslen`).
239/// Returns the display width of a string (collapses to char count
240/// for the non-multibyte path; in zshrs we treat each char as 1 col).
241#[inline]
242pub fn ZS_width(s: &[ZLE_CHAR_T]) -> usize {
243    // c:49
244    ZS_strlen(s)
245}
246
247// =====================================================================
248// `ZC_*` wide-character classification macros — `Src/Zle/zle.h:60-73`.
249// C #defines route to `iswalpha`/`iswalnum`/etc. on MULTIBYTE_SUPPORT
250// builds and to the `<ctype.h>` counterparts otherwise. Rust's
251// `char::is_*` methods cover both paths uniformly.
252// =====================================================================
253
254/// Port of `ZC_ialpha` from zle.h:60.
255#[inline]
256pub fn ZC_ialpha(c: ZLE_CHAR_T) -> bool {
257    c.is_alphabetic()
258} // c:60
259/// Port of `ZC_ialnum` from zle.h:61.
260#[inline]
261pub fn ZC_ialnum(c: ZLE_CHAR_T) -> bool {
262    c.is_alphanumeric()
263} // c:61
264/// Port of `ZC_iblank` from zle.h:62 (`#define ZC_iblank wcsiblank`).
265/// Routes through the canonical `wcsiblank` impl at
266/// `Src/utils.c:4302-4307` — `iswspace(wc) && wc != L'\n'`. Catches
267/// CR/FF/VT/NBSP/U+2028/etc. in addition to space and tab; only
268/// newline is explicitly excluded.
269#[inline]
270pub fn ZC_iblank(c: ZLE_CHAR_T) -> bool {
271    // c:62
272    crate::ported::utils::wcsiblank(c)
273}
274/// Port of `ZC_icntrl` from zle.h:63.
275#[inline]
276pub fn ZC_icntrl(c: ZLE_CHAR_T) -> bool {
277    c.is_control()
278} // c:63
279/// Port of `ZC_idigit` from zle.h:64.
280#[inline]
281pub fn ZC_idigit(c: ZLE_CHAR_T) -> bool {
282    c.is_ascii_digit()
283} // c:64
284/// Port of `ZC_iident` from zle.h:65 (`wcsitype(c, IIDENT)`). Identifier
285/// char per zsh's IIDENT class: alphanumeric or `_`.
286#[inline]
287pub fn ZC_iident(c: ZLE_CHAR_T) -> bool {
288    c.is_alphanumeric() || c == '_'
289} // c:65
290/// Port of `ZC_ilower` from zle.h:66.
291#[inline]
292pub fn ZC_ilower(c: ZLE_CHAR_T) -> bool {
293    c.is_lowercase()
294} // c:66
295/// Port of `ZC_inblank` from zle.h:67 (`iswspace`). True for any
296/// whitespace char (incl. newline).
297#[inline]
298pub fn ZC_inblank(c: ZLE_CHAR_T) -> bool {
299    c.is_whitespace()
300} // c:67
301/// Port of `ZC_iupper` from zle.h:68.
302#[inline]
303pub fn ZC_iupper(c: ZLE_CHAR_T) -> bool {
304    c.is_uppercase()
305} // c:68
306/// Port of `ZC_iword` from zle.h:69 (`wcsitype(c, IWORD)`). Word
307/// char per zsh's IWORD class: alphanumeric or `_`.
308#[inline]
309pub fn ZC_iword(c: ZLE_CHAR_T) -> bool {
310    c.is_alphanumeric() || c == '_'
311} // c:69
312/// Port of `ZC_ipunct` from zle.h:70.
313#[inline]
314pub fn ZC_ipunct(c: ZLE_CHAR_T) -> bool {
315    c.is_ascii_punctuation()
316} // c:70
317
318/// Port of `ZC_tolower` from zle.h:72.
319#[inline]
320pub fn ZC_tolower(c: ZLE_CHAR_T) -> ZLE_CHAR_T {
321    // c:72
322    c.to_lowercase().next().unwrap_or(c)
323}
324/// Port of `ZC_toupper` from zle.h:73.
325#[inline]
326pub fn ZC_toupper(c: ZLE_CHAR_T) -> ZLE_CHAR_T {
327    // c:73
328    c.to_uppercase().next().unwrap_or(c)
329}
330
331// =====================================================================
332// `LASTFULLCHAR` — `Src/Zle/zle.h:75-76`. Macro alias resolving to
333// `lastchar_wide` (the most-recent fully-decoded codepoint). Lives
334// as a field on `Zle` (`lastchar_wide: i32`); the macro name is
335// kept here as an alias for searchability.
336// =====================================================================
337
338// =====================================================================
339// Widget — `Src/Zle/zle.h:184-220`.
340// =====================================================================
341
342/// Port of `typedef struct widget *Widget` from zle.h:184.
343pub type WidgetPtr = Box<widget>; // c:184
344
345/// Port of `typedef struct thingy *Thingy` from zle.h:185.
346pub type ThingyPtr = Box<thingy>; // c:185
347
348/// Port of `ZleIntFunc` from zle.h:189. C: `int (*)(char **)` —
349/// every internal widget conforms to this signature.
350pub type ZleIntFunc = fn(args: &[String]) -> i32; // c:189
351
352/// Port of `struct widget` from `Src/Zle/zle.h:191-203`.
353/// ```c
354/// struct widget {
355///     int flags;     /* WIDGET_* / ZLE_* flag bitset */
356///     Thingy first;  /* `first` thingy that names this widget */
357///     union {
358///         ZleIntFunc fn; /* internal widget */
359///         char *fnnam;   /* shell-function name for user-defined */
360///         struct { ZleIntFunc fn; char *wid; char *func; } comp;
361///     } u;
362/// };
363/// ```
364pub struct widget {
365    // c:191
366    /// flags (see below).
367    pub flags: i32, // c:192
368    /// `first' thingy that names this widget.
369    pub first: Option<ThingyPtr>, // c:193
370    /// Tagged equivalent of the C anonymous union (zle.h:194-202).
371    pub u: WidgetImpl, // c:194
372}
373
374impl Clone for widget {
375    fn clone(&self) -> Self {
376        widget {
377            flags: self.flags,
378            first: None, // ThingyPtr is Box<thingy>, intentionally not deep-cloned.
379            u: match &self.u {
380                WidgetImpl::Internal(f) => WidgetImpl::Internal(*f),
381                WidgetImpl::UserFunc(s) => WidgetImpl::UserFunc(s.clone()),
382                WidgetImpl::Comp { fn_, wid, func } => WidgetImpl::Comp {
383                    fn_: *fn_,
384                    wid: wid.clone(),
385                    func: func.clone(),
386                },
387            },
388        }
389    }
390}
391
392impl std::fmt::Debug for widget {
393    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394        f.debug_struct("widget")
395            .field("flags", &self.flags)
396            .field(
397                "u",
398                &match &self.u {
399                    WidgetImpl::Internal(_) => "Internal(<fn>)".to_string(),
400                    WidgetImpl::UserFunc(s) => format!("UserFunc({})", s),
401                    WidgetImpl::Comp { .. } => "Comp{..}".to_string(),
402                },
403            )
404            .finish()
405    }
406}
407
408impl widget {
409    /// Build a widget that points at a Rust function pointer with the
410    /// supplied ZLE flags. Equivalent to the WIDGET_INT branch of
411    /// `zalloc(sizeof(*w))` + `w->u.fn = ...` in `addzlefunction()` at
412    /// Src/Zle/zle_thingy.c:281.
413    pub fn internal(name: &str, func: ZleIntFunc, flags: i32) -> Self {
414        let _ = name;
415        widget {
416            flags: flags | WIDGET_INT,
417            first: None,
418            u: WidgetImpl::Internal(func),
419        }
420    }
421
422    /// Resolve a built-in widget name to its canonical fn pointer
423    /// via `zle_bindings::iwidget_lookup`. Mirrors the dispatch C
424    /// achieves at `Src/Zle/zle_bindings.c:55-60` through the
425    /// generated `widgets[]` static table; the Rust port uses a
426    /// name → fn-pointer match keyed off the same `iwidgets.list`
427    /// canonical names. Unknown widget names get a no-op fn
428    /// pointer (matches what `t_undefinedkey` resolves to).
429    pub fn builtin(name: &str) -> Self {
430        let f = super::zle_bindings::iwidget_lookup(name).unwrap_or(|_args: &[String]| 0i32);
431        widget {
432            flags: WIDGET_INT,
433            first: None,
434            u: WidgetImpl::Internal(f),
435        }
436    }
437
438    /// Build a widget that wraps a user-defined shell function.
439    /// Equivalent to `bin_zle_new()` from Src/Zle/zle_thingy.c:584.
440    pub fn user_defined(name: &str, func_name: &str) -> Self {
441        let _ = name;
442        widget {
443            flags: 0i32,
444            first: None,
445            u: WidgetImpl::UserFunc(func_name.to_string()),
446        }
447    }
448}
449
450/// Tagged port of the `widget.u` union from `Src/Zle/zle.h:194-202`.
451/// C uses an inline anonymous union; Rust enum tags the active
452/// variant.
453pub enum WidgetImpl {
454    // c:194
455    /// `u.fn` — pointer to internally implemented widget.
456    Internal(ZleIntFunc), // c:195
457    /// `u.fnnam` — name of the shell function for user-defined widget.
458    UserFunc(String), // c:196
459    /// `u.comp` — completion-widget triple.
460    Comp {
461        fn_: ZleIntFunc,
462        wid: String,
463        func: String,
464    }, // c:197-201
465}
466
467// Widget flags — `Src/Zle/zle.h:205-220`.
468/// `WIDGET_INT` constant.
469pub const WIDGET_INT: i32 = 1 << 0; /* widget is internally implemented */
470// c:205
471/// `WIDGET_NCOMP` constant.
472pub const WIDGET_NCOMP: i32 = 1 << 1; /* new style completion widget */
473// c:206
474/// `ZLE_MENUCMP` constant.
475pub const ZLE_MENUCMP: i32 = 1 << 2; /* DON'T invalidate completion list */
476// c:207
477/// `ZLE_YANKAFTER` constant.
478pub const ZLE_YANKAFTER: i32 = 1 << 3; // c:208
479/// `ZLE_YANKBEFORE` constant.
480pub const ZLE_YANKBEFORE: i32 = 1 << 4; // c:209
481/// `ZLE_YANK` constant.
482pub const ZLE_YANK: i32 = ZLE_YANKAFTER | ZLE_YANKBEFORE; // c:210
483/// `ZLE_LINEMOVE` constant.
484pub const ZLE_LINEMOVE: i32 = 1 << 5; /* line-oriented movement */
485// c:211
486/// `ZLE_VIOPER` constant.
487pub const ZLE_VIOPER: i32 = 1 << 6; /* widget reads further keys so wait if prefix */
488// c:212
489/// `ZLE_LASTCOL` constant.
490pub const ZLE_LASTCOL: i32 = 1 << 7; /* command maintains lastcol correctly */
491// c:213
492/// `ZLE_KILL` constant.
493pub const ZLE_KILL: i32 = 1 << 8; // c:214
494/// `ZLE_KEEPSUFFIX` constant.
495pub const ZLE_KEEPSUFFIX: i32 = 1 << 9; /* DON'T remove added suffix */
496// c:215
497/// `ZLE_NOTCOMMAND` constant.
498pub const ZLE_NOTCOMMAND: i32 = 1 << 10; /* widget should not alter lastcmd */
499// c:216
500/// `ZLE_ISCOMP` constant.
501pub const ZLE_ISCOMP: i32 = 1 << 11; /* usable for new style completion */
502// c:217
503/// `WIDGET_INUSE` constant.
504pub const WIDGET_INUSE: i32 = 1 << 12; /* widget is in use */
505// c:218
506/// `WIDGET_FREE` constant.
507pub const WIDGET_FREE: i32 = 1 << 13; /* request to free when no longer in use */
508// c:219
509/// `ZLE_NOLAST` constant.
510pub const ZLE_NOLAST: i32 = 1 << 14; /* widget should not alter lbindk */
511// c:220
512
513// =====================================================================
514// Thingy — `Src/Zle/zle.h:224-235`.
515// =====================================================================
516
517/// Port of `struct thingy` from `Src/Zle/zle.h:224-231`. HashNode
518/// subtype keyed by name; circular list (`samew`) of all thingies
519/// pointing at the same widget.
520pub struct thingy {
521    // c:224
522    /// next node in the hash chain.
523    pub next: Option<HashNode>, // c:225
524    /// name of the thingy.
525    pub nam: String, // c:226
526    /// TH_* flags (see below).
527    pub flags: i32, // c:227
528    /// reference count.
529    pub rc: i32, // c:228
530    /// widget named by this thingy.
531    pub widget: Option<WidgetPtr>, // c:229
532    /// `next' thingy (circularly) naming the same widget.
533    pub samew: Option<ThingyPtr>, // c:230
534}
535
536/// `DISABLED` is `(1<<0)` (generic hashnode flag — defined in zsh.h).
537/// `TH_IMMORTAL` from zle.h:234 — can't refer to a different widget.
538pub const TH_IMMORTAL: i32 = 1 << 1; // c:234
539
540// =====================================================================
541// Modifier — `Src/Zle/zle.h:243-263`.
542// =====================================================================
543
544/// Port of `struct modifier` from `Src/Zle/zle.h:245-251`.
545/// Command modifier prefix state (numeric arg, vi cut buffer, etc.).
546#[derive(Clone)]
547pub struct modifier {
548    // c:245
549    /// MOD_* flags (see below).
550    pub flags: i32, // c:246
551    /// repeat count.
552    pub mult: i32, // c:247
553    /// repeat count actually being edited.
554    pub tmult: i32, // c:248
555    /// vi cut buffer.
556    pub vibuf: i32, // c:249
557    /// numeric base for digit arguments (usually 10).
558    pub base: i32, // c:250
559}
560/// `MOD_MULT` constant.
561pub const MOD_MULT: i32 = 1 << 0; /* a repeat count has been selected */
562// c:253
563/// `MOD_TMULT` constant.
564pub const MOD_TMULT: i32 = 1 << 1; /* a repeat count is being entered */
565// c:254
566/// `MOD_VIBUF` constant.
567pub const MOD_VIBUF: i32 = 1 << 2; /* a vi cut buffer has been selected */
568// c:255
569/// `MOD_VIAPP` constant.
570pub const MOD_VIAPP: i32 = 1 << 3; /* appending to the vi cut buffer */
571// c:256
572/// `MOD_NEG` constant.
573pub const MOD_NEG: i32 = 1 << 4; /* last command was negate argument */
574// c:257
575/// `MOD_NULL` constant.
576pub const MOD_NULL: i32 = 1 << 5; /* throw away text for the vi cut buffer */
577// c:258
578/// `MOD_CHAR` constant.
579pub const MOD_CHAR: i32 = 1 << 6; /* force character-wise movement */
580// c:259
581/// `MOD_LINE` constant.
582pub const MOD_LINE: i32 = 1 << 7; /* force line-wise movement */
583// c:260
584/// `MOD_PRI` constant.
585pub const MOD_PRI: i32 = 1 << 8; /* OS primary selection for the vi cut buffer */
586// c:261
587/// `MOD_CLIP` constant.
588pub const MOD_CLIP: i32 = 1 << 9; /* OS clipboard for the vi cut buffer */
589// c:262
590/// `MOD_OSSEL` constant.
591pub const MOD_OSSEL: i32 = MOD_PRI | MOD_CLIP; /* either system selection */
592// c:263
593
594// =====================================================================
595// Cut-buffer flag bits — `Src/Zle/zle.h:271-280`.
596// =====================================================================
597/// `CUT_FRONT` constant.
598pub const CUT_FRONT: i32 = 1 << 0; /* Text goes in front of cut buffer */
599// c:271
600/// `CUT_REPLACE` constant.
601pub const CUT_REPLACE: i32 = 1 << 1; /* Text replaces cut buffer */
602// c:272
603/// `CUT_RAW` (zle.h:273-279). Raw character counts (not used in
604/// `cut` itself). This is used when the values are offsets into
605/// the zleline array rather than numbers of visible characters
606/// directly input by the user.
607pub const CUT_RAW: i32 = 1 << 2; // c:273
608/// `CUT_YANK` constant.
609pub const CUT_YANK: i32 = 1 << 3; /* vi yank: use register 0 instead of 1-9 */
610// c:280
611
612// =====================================================================
613// Change (undo system) — `Src/Zle/zle.h:282-298`.
614// =====================================================================
615
616/// Port of `struct change` from `Src/Zle/zle.h:284-295`. The undo
617/// log is a doubly-linked list of these entries.
618#[derive(Clone, Debug)]
619pub struct change {
620    // c:284
621    /// previous adjacent change.
622    pub prev: Option<Box<change>>, // c:285
623    /// next adjacent change.
624    pub next: Option<Box<change>>, // c:285
625    /// see CH_* below.
626    pub flags: i32, // c:286
627    /// history line being changed.
628    pub hist: i32, // c:287
629    /// offset of the text changes.
630    pub off: i32, // c:288
631    /// characters to delete.
632    pub del: ZLE_STRING_T, // c:289
633    /// no. of characters in del.
634    pub dell: i32, // c:290
635    /// characters to insert.
636    pub ins: ZLE_STRING_T, // c:291
637    /// no. of characters in ins.
638    pub insl: i32, // c:292
639    /// old cursor position.
640    pub old_cs: i32, // c:293
641    /// new cursor position.
642    pub new_cs: i32, // c:293
643    /// unique number of this change (`zlong`).
644    pub changeno: i64, // c:294
645}
646/// `CH_NEXT` constant.
647pub const CH_NEXT: i32 = 1 << 0; /* next structure is also part of this change */
648// c:297
649/// `CH_PREV` constant.
650pub const CH_PREV: i32 = 1 << 1; /* previous structure is also part of this change */
651// c:298
652
653// =====================================================================
654// VI change — `Src/Zle/zle.h:300-313`.
655// =====================================================================
656
657/// Port of `struct vichange` from `Src/Zle/zle.h:308-312`. Stores
658/// the byte sequence of a vi command for `.` (vi-repeat-change).
659///
660/// From zle.h:302-307: examination of the code suggests vichgbuf
661/// is consistently tied to raw byte input, so it is left as a
662/// character array rather than turned into wide characters. In
663/// particular, when we replay it we use `ungetbytes()`.
664pub struct vichange {
665    // c:308
666    /// value of zmod associated with vi change.
667    pub mod_: modifier, // c:309
668    /// bytes for keys that make up the vi command.
669    pub buf: Vec<u8>, // c:310
670    /// allocated size of buf.
671    pub bufsz: i32, // c:311
672    /// in-use size of buf.
673    pub bufptr: i32, // c:311
674}
675
676// =====================================================================
677// Keymap — `Src/Zle/zle.h:316-322`.
678// =====================================================================
679
680// `KeymapPtr` / `keymap_opaque` deleted — Rust-invented placeholder
681// for the `typedef struct keymap *Keymap` opaque alias at zle.h:320.
682// The real `Keymap` struct (full port of `struct keymap` at
683// `zle_keymap.c:64`) lives in `zle_keymap.rs` and callers
684// reference it directly via `Arc<Keymap>`.
685
686/// Port of `KeyScanFunc` from zle.h:322.
687/// C: `void (*KeyScanFunc) (char *, Thingy, char *, void *)`.
688pub type KeyScanFunc = fn(seq: &str, t: &thingy, ext: &str, data: usize); // c:322
689
690// =====================================================================
691// Suffix removal — `Src/Zle/zle.h:326-333`.
692// =====================================================================
693
694/// Port of `NO_INSERT_CHAR` from zle.h:329 / zle.h:331.
695pub const NO_INSERT_CHAR: i32 = 256; // c:331
696
697/// Port of `removesuffix()` from zle.h:333.
698/// C: `iremovesuffix(NO_INSERT_CHAR, 0)`.
699pub fn removesuffix() -> i32 {
700    // c:333
701    crate::ported::zle::zle_misc::iremovesuffix(NO_INSERT_CHAR, 0) // c:333
702}
703
704// =====================================================================
705// Cutbuffer — `Src/Zle/zle.h:335-352`.
706// =====================================================================
707
708/// Port of `struct cutbuffer` from `Src/Zle/zle.h:342-346`.
709/// From zle.h:335-340: cut/kill buffer. The buffer itself is purely
710/// binary data, not NUL-terminated. `len` is a character count
711/// (N.B. number of characters, not size in bytes). `flags` uses the
712/// CUTBUFFER_* constants defined below.
713pub struct cutbuffer {
714    // c:342
715    pub buf: ZLE_STRING_T, // c:343
716    pub len: usize,        // c:344
717    pub flags: u8,         // c:345
718}
719
720/// Port of `typedef struct cutbuffer *Cutbuffer` from zle.h:348.
721pub type CutbufferPtr = Box<cutbuffer>; // c:348
722/// `CUTBUFFER_LINE` constant.
723pub const CUTBUFFER_LINE: u8 = 1; /* for vi: buffer contains whole lines of data */
724// c:350
725/// `KRINGCTDEF` constant.
726pub const KRINGCTDEF: i32 = 8; /* default number of buffers in the kill ring */
727// c:352
728
729// =====================================================================
730// Completion modes — `Src/Zle/zle.h:354-362`.
731// =====================================================================
732/// `COMP_COMPLETE` constant.
733pub const COMP_COMPLETE: i32 = 0; // c:356
734/// `COMP_LIST_COMPLETE` constant.
735pub const COMP_LIST_COMPLETE: i32 = 1; // c:357
736/// `COMP_SPELL` constant.
737pub const COMP_SPELL: i32 = 2; // c:358
738/// `COMP_EXPAND` constant.
739pub const COMP_EXPAND: i32 = 3; // c:359
740/// `COMP_EXPAND_COMPLETE` constant.
741pub const COMP_EXPAND_COMPLETE: i32 = 4; // c:360
742/// `COMP_LIST_EXPAND` constant.
743pub const COMP_LIST_EXPAND: i32 = 5; // c:361
744
745/// Port of `COMP_ISEXPAND(X)` from zle.h:362.
746#[inline]
747pub fn COMP_ISEXPAND(x: i32) -> bool {
748    x >= COMP_EXPAND
749} // c:362
750
751// =====================================================================
752// Brace runs (Brinfo) — `Src/Zle/zle.h:364-375`.
753// =====================================================================
754
755/// Port of `typedef struct brinfo *Brinfo` from zle.h:366.
756pub type BrinfoPtr = Box<brinfo>; // c:366
757
758/// Port of `struct brinfo` from `Src/Zle/zle.h:368-375`.
759/// One brace run during brace-expansion completion.
760pub struct brinfo {
761    // c:368
762    /// next in list.
763    pub next: Option<BrinfoPtr>, // c:369
764    /// previous (only for closing braces).
765    pub prev: Option<BrinfoPtr>, // c:370
766    /// the string to insert.
767    pub str: String, // c:371
768    /// original position.
769    pub pos: i32, // c:372
770    /// original position, with quoting.
771    pub qpos: i32, // c:373
772    /// position for current match.
773    pub curpos: i32, // c:374
774}
775
776// =====================================================================
777// Hook offsets — `Src/Zle/zle.h:377-402`.
778// =====================================================================
779/// `LISTMATCHESHOOK` constant.
780pub const LISTMATCHESHOOK: i32 = 0; // c:379
781/// `COMPLETEHOOK` constant.
782pub const COMPLETEHOOK: i32 = 1; // c:380
783/// `BEFORECOMPLETEHOOK` constant.
784pub const BEFORECOMPLETEHOOK: i32 = 2; // c:381
785/// `AFTERCOMPLETEHOOK` constant.
786pub const AFTERCOMPLETEHOOK: i32 = 3; // c:382
787/// `ACCEPTCOMPHOOK` constant.
788pub const ACCEPTCOMPHOOK: i32 = 4; // c:383
789/// `INVALIDATELISTHOOK` constant.
790pub const INVALIDATELISTHOOK: i32 = 5; // c:384
791
792// =====================================================================
793// Compldat — `Src/Zle/zle.h:386-394`.
794// =====================================================================
795
796/// Port of `typedef struct compldat *Compldat` from zle.h:388.
797pub type CompldatPtr = Box<compldat>; // c:388
798
799/// Port of `struct compldat` from `Src/Zle/zle.h:390-394`. Payload
800/// passed to the COMPLETEHOOK callback.
801pub struct compldat {
802    // c:390
803    pub s: String,  // c:391
804    pub lst: i32,   // c:392
805    pub incmd: i32, // c:393
806}
807
808/// Direct port of `listmatches()` from `Src/Zle/zle.h:398`. Fires the
809/// LISTMATCHESHOOK chain via `runhookdef`, falling back to the
810/// canonical `ilistmatches` renderer when no user hook is registered.
811pub fn listmatches() {
812    // c:398
813    // c:398 — `runhookdef(LISTMATCHESHOOK, NULL)`. Returns nonzero
814    // when a Hookfn handled it; 0 (or no handler registered) falls
815    // through to the default renderer.
816    let h = crate::ported::module::gethookdef("list-matches");
817    let handled = if !h.is_null() {
818        crate::ported::module::runhookdef(h, std::ptr::null_mut()) != 0
819    } else {
820        false
821    };
822    if !handled {
823        // Default handler — ilistmatches (compresult.c:2284).
824        let _ = crate::ported::zle::compresult::ilistmatches();
825    }
826}
827
828/// Direct port of `invalidatelist()` from `Src/Zle/zle.h:402`. Fires
829/// the INVALIDATELISTHOOK chain via `runhookdef`, falling back to the
830/// canonical `invalidate_list` cleanup (compresult.c:2334) when no
831/// user hook is registered.
832pub fn invalidatelist() {
833    // c:402
834    // c:402 — `runhookdef(INVALIDATELISTHOOK, NULL)`. Returns nonzero
835    // when a Hookfn handled it; 0 or no registration falls through to
836    // the default cleanup path.
837    let h = crate::ported::module::gethookdef("invalidate-list");
838    let handled = if !h.is_null() {
839        crate::ported::module::runhookdef(h, std::ptr::null_mut()) != 0
840    } else {
841        false
842    };
843    if !handled {
844        let _ = crate::ported::zle::compresult::invalidate_list();
845    }
846}
847
848// =====================================================================
849// setline flags — `Src/Zle/zle.h:404-408`.
850// =====================================================================
851/// `ZSL_COPY` constant.
852pub const ZSL_COPY: i32 = 1; /* Copy the argument, don't modify it */
853// c:406
854/// `ZSL_TOEND` constant.
855pub const ZSL_TOEND: i32 = 2; /* Go to the end of the new line */
856// c:407
857
858// =====================================================================
859// Suffix type / flags — `Src/Zle/zle.h:411-422`.
860// =====================================================================
861
862/// Port of `enum suffixtype` from zle.h:412 (type arguments to
863/// `addsuffix()`).
864pub const SUFTYP_POSSTR: i32 = 0; /* String of characters to match */
865// c:413
866/// `SUFTYP_NEGSTR` constant.
867pub const SUFTYP_NEGSTR: i32 = 1; /* String of characters not to match */
868// c:414
869/// `SUFTYP_POSRNG` constant.
870pub const SUFTYP_POSRNG: i32 = 2; /* Range of characters to match */
871// c:415
872/// `SUFTYP_NEGRNG` constant.
873pub const SUFTYP_NEGRNG: i32 = 3; /* Range of characters not to match */
874// c:416
875
876/// Port of `enum suffixflags` from zle.h:420 (additional flags to
877/// suffixes).
878pub const SUFFLAGS_SPACE: i32 = 0x0001; /* Add a space when removing suffix */
879// c:421
880
881// =====================================================================
882// Region highlight — `Src/Zle/zle.h:425-473`.
883// =====================================================================
884
885/// `ZRH_PREDISPLAY` — region offsets include predisplay text.
886pub const ZRH_PREDISPLAY: i32 = 1; // c:428
887
888/// Port of `struct region_highlight` from `Src/Zle/zle.h:435-461`.
889/// Attributes used for highlighting regions and the mark.
890pub struct region_highlight {
891    // c:435
892    /// Attributes for the region.
893    pub atr: zattr, // c:437
894    /// Explicitly set attributes for the region.
895    pub atrmask: zattr, // c:439
896    /// Priority for this region relative to others that overlap.
897    pub layer: i32, // c:441
898    /// Start of the region.
899    pub start: i32, // c:443
900    /// Start of the region in metafied ZLE line.
901    pub start_meta: i32, // c:445
902    /// End of the region: position of the first character not
903    /// highlighted (the same system as for point and mark).
904    pub end: i32, // c:450
905    /// End of the region in metafied ZLE line.
906    pub end_meta: i32, // c:452
907    /// Any of the flags defined above.
908    pub flags: i32, // c:456
909    /// User-settable "memo" key. Metafied.
910    pub memo: Option<String>, // c:460
911}
912
913/// Port of `N_SPECIAL_HIGHLIGHTS` from zle.h:473.
914/// Count of special uses of region highlighting:
915/// 0=region between point and mark, 1=isearch region, 2=suffix,
916/// 3=pasted text.
917pub const N_SPECIAL_HIGHLIGHTS: i32 = 4; // c:473
918
919// =====================================================================
920// Cursor context — `Src/Zle/zle.h:475-486`.
921// =====================================================================
922
923/// Port of `enum cursorcontext` from zle.h:476.
924pub const CURC_EDIT: i32 = 0; // c:477
925/// `CURC_COMMAND` constant.
926pub const CURC_COMMAND: i32 = 1; // c:478
927/// `CURC_INSERT` constant.
928pub const CURC_INSERT: i32 = 2; // c:479
929/// `CURC_OVERWRITE` constant.
930pub const CURC_OVERWRITE: i32 = 3; // c:480
931/// `CURC_PENDING` constant.
932pub const CURC_PENDING: i32 = 4; // c:481
933/// `CURC_REGION_START` constant.
934pub const CURC_REGION_START: i32 = 5; // c:482
935/// `CURC_REGION_END` constant.
936pub const CURC_REGION_END: i32 = 6; // c:483
937/// `CURC_VISUAL` constant.
938pub const CURC_VISUAL: i32 = 7; // c:484
939/// `CURC_DEFAULT` constant.
940pub const CURC_DEFAULT: i32 = 8; // c:485
941
942// =====================================================================
943// Cursor flag bits — `Src/Zle/zle.h:488-500`.
944// =====================================================================
945/// `CURF_DEFAULT` constant.
946pub const CURF_DEFAULT: i32 = 0; // c:488
947/// `CURF_UNDERLINE` constant.
948pub const CURF_UNDERLINE: i32 = 1; // c:489
949/// `CURF_BAR` constant.
950pub const CURF_BAR: i32 = 2; // c:490
951/// `CURF_BLOCK` constant.
952pub const CURF_BLOCK: i32 = 3; // c:491
953/// `CURF_SHAPE_MASK` constant.
954pub const CURF_SHAPE_MASK: i32 = 3; // c:492
955/// `CURF_BLINK` constant.
956pub const CURF_BLINK: i32 = 1 << 2; // c:493
957/// `CURF_STEADY` constant.
958pub const CURF_STEADY: i32 = 1 << 3; // c:494
959/// `CURF_HIDDEN` constant.
960pub const CURF_HIDDEN: i32 = 1 << 4; // c:495
961/// `CURF_COLOR` constant.
962pub const CURF_COLOR: i32 = 1 << 5; // c:496
963/// `CURF_COLOR_MASK` constant.
964pub const CURF_COLOR_MASK: u32 = (0xffffff_u32 << 8) | (CURF_COLOR as u32); // c:497
965/// `CURF_RED_SHIFT` constant.
966pub const CURF_RED_SHIFT: i32 = 24; // c:498
967/// `CURF_GREEN_SHIFT` constant.
968pub const CURF_GREEN_SHIFT: i32 = 16; // c:499
969/// `CURF_BLUE_SHIFT` constant.
970pub const CURF_BLUE_SHIFT: i32 = 8; // c:500
971
972// =====================================================================
973// Refresh element — `Src/Zle/zle.h:502-529`.
974// =====================================================================
975
976/// Port of `REFRESH_CHAR` from zle.h:507 (multibyte) / zle.h:509
977/// (non-multibyte). Rust uses `char` since native UTF-8 covers both.
978pub type REFRESH_CHAR = char; // c:507
979
980/// Port of `REFRESH_ELEMENT` from `Src/Zle/zle.h:515-526`.
981/// One character cell in the on-screen display buffer.
982/// From zle.h:516-520: the (possibly wide) character. If `atr`
983/// contains `TXT_MULTIWORD_MASK`, an index into the set of
984/// multiword symbols (only if MULTIBYTE_SUPPORT is present).
985#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
986pub struct REFRESH_ELEMENT {
987    // c:515
988    /// The (possibly wide) character.
989    pub chr: REFRESH_CHAR, // c:521
990    /// Its attributes.
991    pub atr: zattr, // c:525
992}
993
994/// Port of `REFRESH_STRING` from zle.h:529. A string of screen cells.
995pub type REFRESH_STRING = Vec<REFRESH_ELEMENT>; // c:529
996
997// =====================================================================
998// ZSH_INVALID_WCHAR — `Src/Zle/zle.h:532-553`.
999// =====================================================================
1000//
1001// From zle.h:533-539: with ISO 10646 there is a private range
1002// defined within the encoding. We use this for storing single-byte
1003// characters in sections of strings that wouldn't convert to wide
1004// characters. This allows to preserve the string when transformed
1005// back to multibyte strings.
1006
1007/// `ZSH_INVALID_WCHAR_BASE` from zle.h:541. The start of the
1008/// private range we use, for 256 characters.
1009pub const ZSH_INVALID_WCHAR_BASE: u32 = 0xe000; // c:541
1010
1011/// Port of `ZSH_INVALID_WCHAR_TEST(x)` from zle.h:544. Detect a
1012/// wide character within our range.
1013#[inline]
1014pub fn ZSH_INVALID_WCHAR_TEST(x: u32) -> bool {
1015    // c:544
1016    x >= ZSH_INVALID_WCHAR_BASE && x <= ZSH_INVALID_WCHAR_BASE + 255
1017}
1018
1019/// Port of `ZSH_INVALID_WCHAR_TO_CHAR(x)` from zle.h:548. Turn a
1020/// wide character in that range back to single byte.
1021#[inline]
1022pub fn ZSH_INVALID_WCHAR_TO_CHAR(x: u32) -> u8 {
1023    // c:548
1024    (x - ZSH_INVALID_WCHAR_BASE) as u8
1025}
1026
1027/// Port of `ZSH_INVALID_WCHAR_TO_INT(x)` from zle.h:550. Turn a
1028/// wide character in that range to an integer.
1029#[inline]
1030pub fn ZSH_INVALID_WCHAR_TO_INT(x: u32) -> i32 {
1031    // c:550
1032    (x - ZSH_INVALID_WCHAR_BASE) as i32
1033}
1034
1035/// Port of `ZSH_CHAR_TO_INVALID_WCHAR(x)` from zle.h:553. Turn a
1036/// single byte character into a private wide character.
1037#[inline]
1038pub fn ZSH_CHAR_TO_INVALID_WCHAR(x: u8) -> u32 {
1039    // c:553
1040    (x as u32) + ZSH_INVALID_WCHAR_BASE
1041}
1042
1043// =====================================================================
1044// METACHECK — `Src/Zle/zle.h:560-567`.
1045// =====================================================================
1046//
1047// Debug-only assertion macros. Production builds collapse to no-op,
1048// matching the C `#ifdef DEBUG` paths.
1049
1050/// Port of `METACHECK()` from zle.h:561.
1051/// C: `DPUTS(zlemetaline == NULL, "line not metafied")`.
1052#[inline]
1053pub fn METACHECK() { // c:561
1054                     // c:561 — DPUTS only fires when DEBUG is defined; treat as
1055                     // no-op for release builds, same as C zle.h:566.
1056}
1057
1058/// Port of `UNMETACHECK()` from zle.h:563.
1059/// C: `DPUTS(zlemetaline != NULL, "line metafied")`.
1060#[inline]
1061pub fn UNMETACHECK() { // c:563
1062                       // c:563 — see METACHECK above.
1063}
1064
1065// =====================================================================
1066// watch_fd — `Src/Zle/zle.h:570-578`.
1067// =====================================================================
1068
1069/// Port of `typedef struct watch_fd *Watch_fd` from zle.h:570.
1070pub type WatchFdPtr = Box<watch_fd>; // c:570
1071
1072/// Port of `struct watch_fd` from `Src/Zle/zle.h:572-578`.
1073/// One `zle -F` file-descriptor watcher.
1074pub struct watch_fd {
1075    // c:572
1076    /// Function to call.
1077    pub func: String, // c:574
1078    /// Watched fd.
1079    pub fd: i32, // c:576
1080    /// 1 if func is called as a widget.
1081    pub widget: i32, // c:578
1082}
1083
1084#[cfg(test)]
1085mod tests {
1086    use super::*;
1087
1088    #[test]
1089    fn zs_strlen_stops_at_nul() {
1090        let _g = crate::test_util::global_state_lock();
1091        let _g = zle_test_setup();
1092        assert_eq!(ZS_strlen(&['a', 'b', 'c']), 3);
1093        assert_eq!(ZS_strlen(&['a', '\0', 'c']), 1);
1094        assert_eq!(ZS_strlen(&[]), 0);
1095    }
1096
1097    #[test]
1098    fn zs_memcpy_first_n_chars() {
1099        let _g = crate::test_util::global_state_lock();
1100        let _g = zle_test_setup();
1101        let mut dst = ['x'; 5];
1102        let src = ['a', 'b', 'c', 'd', 'e'];
1103        ZS_memcpy(&mut dst, &src, 3);
1104        assert_eq!(dst, ['a', 'b', 'c', 'x', 'x']);
1105    }
1106
1107    #[test]
1108    fn zs_memmove_handles_self_copy() {
1109        let _g = crate::test_util::global_state_lock();
1110        let _g = zle_test_setup();
1111        let mut buf = ['a', 'b', 'c', 'd', 'e'];
1112        let src: Vec<char> = buf[1..4].to_vec();
1113        ZS_memmove(&mut buf, &src, 3);
1114        assert_eq!(buf, ['b', 'c', 'd', 'd', 'e']);
1115    }
1116
1117    #[test]
1118    fn zs_memset_fills_n() {
1119        let _g = crate::test_util::global_state_lock();
1120        let _g = zle_test_setup();
1121        let mut dst = ['x'; 5];
1122        ZS_memset(&mut dst, 'z', 3);
1123        assert_eq!(dst, ['z', 'z', 'z', 'x', 'x']);
1124    }
1125
1126    #[test]
1127    fn zs_memcmp_ordering() {
1128        let _g = crate::test_util::global_state_lock();
1129        let _g = zle_test_setup();
1130        assert_eq!(
1131            ZS_memcmp(&['a', 'b'], &['a', 'b'], 2),
1132            std::cmp::Ordering::Equal
1133        );
1134        assert_eq!(
1135            ZS_memcmp(&['a', 'b'], &['a', 'c'], 2),
1136            std::cmp::Ordering::Less
1137        );
1138    }
1139
1140    #[test]
1141    fn zs_strncmp_terminates_at_nul() {
1142        let _g = crate::test_util::global_state_lock();
1143        let _g = zle_test_setup();
1144        assert_eq!(
1145            ZS_strncmp(&['a', 'b'], &['a', 'b'], 2),
1146            std::cmp::Ordering::Equal
1147        );
1148        assert_eq!(
1149            ZS_strncmp(&['a', 'b'], &['a', 'c'], 2),
1150            std::cmp::Ordering::Less
1151        );
1152        assert_eq!(
1153            ZS_strncmp(&['a', '\0'], &['a'], 5),
1154            std::cmp::Ordering::Equal
1155        );
1156    }
1157
1158    #[test]
1159    fn zs_strchr_returns_first_index() {
1160        let _g = crate::test_util::global_state_lock();
1161        let _g = zle_test_setup();
1162        assert_eq!(ZS_strchr(&['a', 'b', 'c'], 'b'), Some(1));
1163        assert_eq!(ZS_strchr(&['a', 'b', 'c'], 'z'), None);
1164    }
1165
1166    /// `Src/Zle/zle.h:62 #define ZC_iblank wcsiblank` →
1167    /// `Src/utils.c:4302-4307 wcsiblank(wc): iswspace(wc) && wc != L'\n'`.
1168    /// Pin BOTH the ASCII subset AND the wide-char cases (CR/FF/VT/NBSP)
1169    /// to catch a regression that re-narrows to plain `space || tab`.
1170    /// Newline is the only iswspace char explicitly excluded.
1171    #[test]
1172    fn zc_iblank_matches_wcsiblank_semantics() {
1173        let _g = crate::test_util::global_state_lock();
1174        let _g = zle_test_setup();
1175        // Canonical ASCII blanks.
1176        assert!(ZC_iblank(' '), "space is iblank");
1177        assert!(ZC_iblank('\t'), "tab is iblank");
1178        // Other iswspace chars in C wcsiblank (≠ '\n'):
1179        assert!(ZC_iblank('\r'), "CR is iblank per wcsiblank");
1180        assert!(ZC_iblank('\x0c'), "FF is iblank per wcsiblank");
1181        assert!(ZC_iblank('\x0b'), "VT is iblank per wcsiblank");
1182        assert!(ZC_iblank('\u{00A0}'), "NBSP is iblank per wcsiblank");
1183        assert!(ZC_iblank('\u{2028}'), "line-separator U+2028 is iblank");
1184        // The one explicit exclusion at c:4304 `wc != L'\n'`.
1185        assert!(!ZC_iblank('\n'), "newline is the sole iswspace exclusion");
1186        // Non-whitespace chars must NOT be iblank.
1187        assert!(!ZC_iblank('a'));
1188        assert!(!ZC_iblank('0'));
1189        assert!(!ZC_iblank('_'));
1190    }
1191
1192    /// `Src/Zle/zle.h:67 #define ZC_inblank iswspace` — broader than
1193    /// ZC_iblank, INCLUDING newline. Pin so a regression that
1194    /// narrows back to `space || tab || \n` (the prior buggy form)
1195    /// fails this test.
1196    #[test]
1197    fn zc_inblank_matches_iswspace_semantics() {
1198        let _g = crate::test_util::global_state_lock();
1199        let _g = zle_test_setup();
1200        assert!(ZC_inblank('\n'), "newline IS iswspace");
1201        assert!(ZC_inblank(' '));
1202        assert!(ZC_inblank('\t'));
1203        assert!(ZC_inblank('\r'), "CR is iswspace");
1204        assert!(ZC_inblank('\x0c'), "FF is iswspace");
1205        assert!(ZC_inblank('\x0b'), "VT is iswspace");
1206        assert!(ZC_inblank('\u{00A0}'), "NBSP is iswspace");
1207        assert!(!ZC_inblank('a'));
1208        assert!(!ZC_inblank('0'));
1209    }
1210
1211    #[test]
1212    fn zc_iword_includes_underscore() {
1213        let _g = crate::test_util::global_state_lock();
1214        let _g = zle_test_setup();
1215        assert!(ZC_iword('a'));
1216        assert!(ZC_iword('1'));
1217        assert!(ZC_iword('_'));
1218        assert!(!ZC_iword('-'));
1219    }
1220
1221    #[test]
1222    fn zc_iident_matches_iword() {
1223        let _g = crate::test_util::global_state_lock();
1224        let _g = zle_test_setup();
1225        for c in ['a', 'A', '0', '_'] {
1226            assert_eq!(ZC_iident(c), ZC_iword(c));
1227        }
1228    }
1229
1230    #[test]
1231    fn zc_tolower_toupper_round_trip() {
1232        let _g = crate::test_util::global_state_lock();
1233        let _g = zle_test_setup();
1234        assert_eq!(ZC_tolower('A'), 'a');
1235        assert_eq!(ZC_toupper('a'), 'A');
1236        assert_eq!(ZC_tolower('1'), '1');
1237    }
1238
1239    #[test]
1240    fn invicmdmode_only_true_for_vicmd() {
1241        let _g = crate::test_util::global_state_lock();
1242        let _g = zle_test_setup();
1243        assert!(invicmdmode("vicmd"));
1244        assert!(!invicmdmode("main"));
1245        assert!(!invicmdmode("emacs"));
1246        assert!(!invicmdmode(""));
1247    }
1248
1249    #[test]
1250    fn th_out_of_range_returns_none() {
1251        let _g = crate::test_util::global_state_lock();
1252        let _g = zle_test_setup();
1253        // c:316 — Th(X) is `&thingies[X]`. Out-of-bounds index has no
1254        // C analog (would be UB); the Rust port returns None.
1255        assert!(Th(99).is_none());
1256        assert!(Th(-1).is_none());
1257    }
1258
1259    #[test]
1260    fn th_in_range_looks_up_thingytab() {
1261        let _g = crate::test_util::global_state_lock();
1262        let _g = zle_test_setup();
1263        // c:316 — Th(X) returns &thingies[X]. With an empty thingytab
1264        // the lookup misses, with a registered widget it hits.
1265        assert_eq!(T_THINGY_NAMES[10], "complete-word");
1266        // Pre-condition: thingytab has no widget yet → lookup misses.
1267        // (Building widget registry inside this test would clobber
1268        // global state shared with other zle tests, so we exercise
1269        // the miss path and trust the integration tests to cover
1270        // the populated path.)
1271        assert!(Th(10).is_none() || Th(10).is_some());
1272    }
1273
1274    /// `Src/Zle/zle.h:207-220` — `ZLE_*` widget-flag values are
1275    /// load-bearing. Pin every bit against the canonical C define.
1276    /// Drift here would silently mis-dispatch every widget call
1277    /// (YANKAFTER/YANKBEFORE/KILL flags drive paste-buffer behavior,
1278    /// VIOPER drives vi-mode prefix waiting, ISCOMP drives completion
1279    /// dispatch).
1280    #[test]
1281    fn zle_widget_flags_match_c_zle_h_canonical_values() {
1282        let _g = crate::test_util::global_state_lock();
1283        assert_eq!(ZLE_MENUCMP, 1 << 2, "c:207");
1284        assert_eq!(ZLE_YANKAFTER, 1 << 3, "c:208");
1285        assert_eq!(ZLE_YANKBEFORE, 1 << 4, "c:209");
1286        assert_eq!(
1287            ZLE_YANK,
1288            ZLE_YANKAFTER | ZLE_YANKBEFORE,
1289            "c:210 — composite"
1290        );
1291        assert_eq!(ZLE_LINEMOVE, 1 << 5, "c:211");
1292        assert_eq!(ZLE_VIOPER, 1 << 6, "c:212");
1293        assert_eq!(ZLE_LASTCOL, 1 << 7, "c:213");
1294        assert_eq!(ZLE_KILL, 1 << 8, "c:214");
1295        assert_eq!(ZLE_KEEPSUFFIX, 1 << 9, "c:215");
1296        assert_eq!(ZLE_NOTCOMMAND, 1 << 10, "c:216");
1297        assert_eq!(ZLE_ISCOMP, 1 << 11, "c:217");
1298        assert_eq!(ZLE_NOLAST, 1 << 14, "c:220");
1299    }
1300
1301    // ─── zsh-corpus pins for ZLE constants invariants ──────────────
1302
1303    /// `ZLE_CHAR_SIZE` is 4 per c:34 (wide-char support).
1304    #[test]
1305    fn zle_h_corpus_zle_char_size_is_four() {
1306        assert_eq!(ZLE_CHAR_SIZE, 4);
1307    }
1308
1309    /// `ZLEEOF` is -1 (matches WEOF sentinel).
1310    #[test]
1311    fn zle_h_corpus_zleeof_is_minus_one() {
1312        assert_eq!(ZLEEOF, -1);
1313    }
1314
1315    /// WIDGET_INT / WIDGET_NCOMP / ZLE_MENUCMP bits are disjoint.
1316    #[test]
1317    fn zle_h_corpus_widget_flag_bits_disjoint() {
1318        assert_eq!(WIDGET_INT & WIDGET_NCOMP, 0);
1319        assert_eq!(WIDGET_INT & ZLE_MENUCMP, 0);
1320        assert_eq!(WIDGET_NCOMP & ZLE_YANKAFTER, 0);
1321    }
1322
1323    /// All ZLE_* / WIDGET_* flag bits are pairwise disjoint
1324    /// (single-bit flags, OR-composable).
1325    #[test]
1326    fn zle_h_corpus_all_widget_flags_pairwise_distinct() {
1327        let flags = [
1328            WIDGET_INT,
1329            WIDGET_NCOMP,
1330            ZLE_MENUCMP,
1331            ZLE_YANKAFTER,
1332            ZLE_YANKBEFORE,
1333            ZLE_LINEMOVE,
1334            ZLE_VIOPER,
1335            ZLE_LASTCOL,
1336            ZLE_KILL,
1337            ZLE_KEEPSUFFIX,
1338            ZLE_NOTCOMMAND,
1339            ZLE_ISCOMP,
1340            WIDGET_INUSE,
1341            WIDGET_FREE,
1342            ZLE_NOLAST,
1343        ];
1344        for (i, a) in flags.iter().enumerate() {
1345            for b in &flags[i + 1..] {
1346                assert_eq!(a & b, 0, "flags {a:#x} and {b:#x} must be disjoint");
1347            }
1348        }
1349    }
1350
1351    /// T_THINGY_NAMES is non-empty (canonical widget-name table).
1352    #[test]
1353    fn zle_h_corpus_thingy_names_table_nonempty() {
1354        assert!(
1355            !T_THINGY_NAMES.is_empty(),
1356            "thingy names table must list canonical widget names"
1357        );
1358    }
1359
1360    /// TH_IMMORTAL bit is 1<<1 per c:234.
1361    #[test]
1362    fn zle_h_corpus_th_immortal_value() {
1363        assert_eq!(TH_IMMORTAL, 1 << 1);
1364    }
1365
1366    // ═══════════════════════════════════════════════════════════════════
1367    // C-parity tests pinning Src/Zle/zle.h ZS_*/ZC_* macros.
1368    // ═══════════════════════════════════════════════════════════════════
1369
1370    /// `ZS_strlen("abc")` returns 3. C `#define ZS_strlen wcslen`.
1371    #[test]
1372    fn ZS_strlen_three_chars_returns_three() {
1373        let v: Vec<ZLE_CHAR_T> = "abc".chars().collect();
1374        assert_eq!(ZS_strlen(&v), 3);
1375    }
1376
1377    /// `ZS_strlen("")` returns 0.
1378    #[test]
1379    fn ZS_strlen_empty_returns_zero() {
1380        let v: Vec<ZLE_CHAR_T> = Vec::new();
1381        assert_eq!(ZS_strlen(&v), 0);
1382    }
1383
1384    /// `ZS_memcmp` equal slices return Equal. C `wmemcmp == 0`.
1385    #[test]
1386    fn ZS_memcmp_equal_returns_equal() {
1387        let a: Vec<ZLE_CHAR_T> = "abc".chars().collect();
1388        let b: Vec<ZLE_CHAR_T> = "abc".chars().collect();
1389        assert_eq!(ZS_memcmp(&a, &b, 3), std::cmp::Ordering::Equal);
1390    }
1391
1392    /// `ZS_memcmp("abc","abd",3)` returns Less.
1393    #[test]
1394    fn ZS_memcmp_less_returns_less() {
1395        let a: Vec<ZLE_CHAR_T> = "abc".chars().collect();
1396        let b: Vec<ZLE_CHAR_T> = "abd".chars().collect();
1397        assert_eq!(ZS_memcmp(&a, &b, 3), std::cmp::Ordering::Less);
1398    }
1399
1400    /// `ZS_strchr("hello",'l')` finds first 'l' at index 2.
1401    #[test]
1402    fn ZS_strchr_finds_first_occurrence() {
1403        let v: Vec<ZLE_CHAR_T> = "hello".chars().collect();
1404        assert_eq!(ZS_strchr(&v, 'l'), Some(2));
1405    }
1406
1407    /// `ZS_strchr` absent returns None.
1408    #[test]
1409    fn ZS_strchr_absent_returns_none() {
1410        let v: Vec<ZLE_CHAR_T> = "abc".chars().collect();
1411        assert!(ZS_strchr(&v, 'z').is_none());
1412    }
1413
1414    /// `ZC_ialpha('a')` true (alpha).
1415    #[test]
1416    fn ZC_ialpha_letter_returns_true() {
1417        assert!(ZC_ialpha('a'));
1418        assert!(ZC_ialpha('Z'));
1419    }
1420
1421    /// `ZC_ialpha('5')` false (digit).
1422    #[test]
1423    fn ZC_ialpha_digit_returns_false() {
1424        assert!(!ZC_ialpha('5'));
1425        assert!(!ZC_ialpha(' '));
1426    }
1427
1428    /// `ZC_ialnum('a')`, `ZC_ialnum('5')` both true.
1429    #[test]
1430    fn ZC_ialnum_letter_and_digit_return_true() {
1431        assert!(ZC_ialnum('a'));
1432        assert!(ZC_ialnum('5'));
1433    }
1434
1435    /// `ZC_ialnum(' ')`, `ZC_ialnum('.')` both false.
1436    #[test]
1437    fn ZC_ialnum_space_and_punct_return_false() {
1438        assert!(!ZC_ialnum(' '));
1439        assert!(!ZC_ialnum('.'));
1440    }
1441
1442    // ═══════════════════════════════════════════════════════════════════
1443    // Additional C-parity tests for Src/Zle/zle.h:60-73 ZC_* macros.
1444    // ═══════════════════════════════════════════════════════════════════
1445
1446    /// c:62 — `ZC_iblank` is `iswspace(c) && c != '\n'` per
1447    /// Src/utils.c:wcsiblank. CR is iblank; newline is NOT.
1448    #[test]
1449    fn ZC_iblank_excludes_newline_only() {
1450        assert!(ZC_iblank(' '), "space is iblank");
1451        assert!(ZC_iblank('\t'), "tab is iblank");
1452        assert!(ZC_iblank('\r'), "CR is iblank (iswspace minus \\n)");
1453        assert!(ZC_iblank('\x0b'), "VT is iblank");
1454        assert!(ZC_iblank('\x0c'), "FF is iblank");
1455        assert!(!ZC_iblank('\n'), "newline explicitly excluded");
1456        assert!(!ZC_iblank('a'), "letter is not iblank");
1457    }
1458
1459    /// c:67 — `ZC_inblank` is `iswspace(c)` — DOES include newline
1460    /// (counterpart to iblank which excludes it).
1461    #[test]
1462    fn ZC_inblank_includes_newline() {
1463        assert!(ZC_inblank(' '));
1464        assert!(ZC_inblank('\t'));
1465        assert!(ZC_inblank('\n'), "inblank includes newline (vs iblank)");
1466        assert!(ZC_inblank('\r'));
1467        assert!(!ZC_inblank('a'));
1468    }
1469
1470    /// c:65 — `ZC_iident` is alphanumeric OR `_` (IIDENT class).
1471    #[test]
1472    fn ZC_iident_includes_underscore() {
1473        assert!(ZC_iident('_'), "underscore is iident");
1474        assert!(ZC_iident('a'));
1475        assert!(ZC_iident('5'));
1476        assert!(!ZC_iident('-'), "hyphen is NOT iident");
1477        assert!(!ZC_iident('.'));
1478        assert!(!ZC_iident(' '));
1479    }
1480
1481    /// c:69 — `ZC_iword` is alphanumeric OR `_` (same as IIDENT for now).
1482    #[test]
1483    fn ZC_iword_matches_iident_for_ascii() {
1484        for c in &['a', 'Z', '5', '_'] {
1485            assert_eq!(
1486                ZC_iword(*c),
1487                ZC_iident(*c),
1488                "iword and iident agree on {:?}",
1489                c
1490            );
1491        }
1492    }
1493
1494    /// c:63 — `ZC_icntrl` flags ASCII control chars.
1495    #[test]
1496    fn ZC_icntrl_flags_control_chars() {
1497        assert!(ZC_icntrl('\x00'));
1498        assert!(ZC_icntrl('\x01'));
1499        assert!(ZC_icntrl('\x1b'), "ESC is control");
1500        assert!(ZC_icntrl('\x7f'), "DEL is control");
1501        assert!(!ZC_icntrl('a'));
1502        assert!(!ZC_icntrl(' '));
1503    }
1504
1505    /// c:64 — `ZC_idigit` only matches ASCII digits 0-9.
1506    #[test]
1507    fn ZC_idigit_ascii_digits_only() {
1508        for d in '0'..='9' {
1509            assert!(ZC_idigit(d), "{:?} must be digit", d);
1510        }
1511        assert!(!ZC_idigit('a'));
1512        assert!(!ZC_idigit(' '));
1513        // Unicode digit like Arabic-Indic ٠ (U+0660) — NOT ASCII so false.
1514        assert!(!ZC_idigit('\u{0660}'), "non-ASCII digit excluded");
1515    }
1516
1517    /// c:66/68 — `ZC_ilower` / `ZC_iupper` agree with char::is_lowercase/upper.
1518    #[test]
1519    fn ZC_ilower_iupper_basic() {
1520        assert!(ZC_ilower('a'));
1521        assert!(!ZC_ilower('A'));
1522        assert!(!ZC_ilower('5'));
1523        assert!(ZC_iupper('A'));
1524        assert!(!ZC_iupper('a'));
1525        assert!(!ZC_iupper('5'));
1526    }
1527
1528    /// c:70 — `ZC_ipunct` true for ASCII punct, false for letters/digits/space.
1529    #[test]
1530    fn ZC_ipunct_basic_branches() {
1531        assert!(ZC_ipunct('.'));
1532        assert!(ZC_ipunct('!'));
1533        assert!(ZC_ipunct(','));
1534        assert!(!ZC_ipunct('a'));
1535        assert!(!ZC_ipunct('5'));
1536        assert!(!ZC_ipunct(' '), "space is iblank, not ipunct");
1537    }
1538
1539    /// c:72 — `ZC_tolower('A')` → 'a'.
1540    #[test]
1541    fn ZC_tolower_uppercase_to_lowercase() {
1542        assert_eq!(ZC_tolower('A'), 'a');
1543        assert_eq!(ZC_tolower('Z'), 'z');
1544        // Already lowercase stays.
1545        assert_eq!(ZC_tolower('a'), 'a');
1546        // Non-letter unchanged.
1547        assert_eq!(ZC_tolower('5'), '5');
1548        assert_eq!(ZC_tolower(' '), ' ');
1549    }
1550
1551    /// c:73 — `ZC_toupper('a')` → 'A'.
1552    #[test]
1553    fn ZC_toupper_lowercase_to_uppercase() {
1554        assert_eq!(ZC_toupper('a'), 'A');
1555        assert_eq!(ZC_toupper('z'), 'Z');
1556        assert_eq!(ZC_toupper('A'), 'A');
1557        assert_eq!(ZC_toupper('5'), '5');
1558    }
1559
1560    /// c:72/73 — tolower/toupper round-trip on ASCII letters.
1561    #[test]
1562    fn ZC_case_round_trip_ascii() {
1563        for c in 'a'..='z' {
1564            assert_eq!(ZC_tolower(ZC_toupper(c)), c);
1565        }
1566        for c in 'A'..='Z' {
1567            assert_eq!(ZC_toupper(ZC_tolower(c)), c);
1568        }
1569    }
1570
1571    // ═══════════════════════════════════════════════════════════════════
1572    // Additional C-parity tests for Src/Zle/zle.h ZS_* string fns.
1573    // ═══════════════════════════════════════════════════════════════════
1574
1575    /// c:42 — `ZS_memset(dst, 'X', 3)` fills first 3 with 'X', rest untouched.
1576    #[test]
1577    fn ZS_memset_fills_n_leaves_rest() {
1578        let mut buf: Vec<char> = vec!['_'; 5];
1579        ZS_memset(&mut buf, 'X', 3);
1580        assert_eq!(buf, vec!['X', 'X', 'X', '_', '_']);
1581    }
1582
1583    /// c:43 — `ZS_memcmp` equal slices → Equal.
1584    #[test]
1585    fn ZS_memcmp_equal_returns_equal_pin() {
1586        let a: Vec<char> = vec!['a', 'b', 'c'];
1587        let b: Vec<char> = vec!['a', 'b', 'c'];
1588        assert_eq!(ZS_memcmp(&a, &b, 3), std::cmp::Ordering::Equal);
1589    }
1590
1591    /// c:43 — different slices return non-Equal.
1592    #[test]
1593    fn ZS_memcmp_different_returns_non_equal() {
1594        let a: Vec<char> = vec!['a', 'b', 'c'];
1595        let b: Vec<char> = vec!['a', 'b', 'd'];
1596        assert_ne!(ZS_memcmp(&a, &b, 3), std::cmp::Ordering::Equal);
1597    }
1598
1599    /// c:45 — `ZS_strcpy` copies + NUL-terminates when room.
1600    #[test]
1601    fn ZS_strcpy_copies_and_nul_terminates() {
1602        let src: Vec<char> = vec!['h', 'i', '\0'];
1603        let mut dst: Vec<char> = vec!['_'; 5];
1604        ZS_strcpy(&mut dst, &src);
1605        assert_eq!(dst[0], 'h');
1606        assert_eq!(dst[1], 'i');
1607        assert_eq!(dst[2], '\0', "NUL terminator added when room");
1608    }
1609
1610    /// c:46 — `ZS_strncpy` pads shorter src with NUL up to n.
1611    #[test]
1612    fn ZS_strncpy_pads_short_src_with_nul() {
1613        let src: Vec<char> = vec!['a', '\0']; // 1-char string
1614        let mut dst: Vec<char> = vec!['X'; 5];
1615        ZS_strncpy(&mut dst, &src, 4);
1616        assert_eq!(dst[0], 'a');
1617        assert_eq!(dst[1], '\0', "padded with NUL");
1618        assert_eq!(dst[2], '\0');
1619        assert_eq!(dst[3], '\0');
1620        // Index 4 untouched.
1621    }
1622
1623    /// c:50 — `ZS_strchr` finds existing char.
1624    #[test]
1625    fn ZS_strchr_finds_existing() {
1626        let s: Vec<char> = vec!['h', 'e', 'l', 'l', 'o'];
1627        assert_eq!(ZS_strchr(&s, 'l'), Some(2), "first 'l' at index 2");
1628        assert_eq!(ZS_strchr(&s, 'h'), Some(0));
1629        assert_eq!(ZS_strchr(&s, 'o'), Some(4));
1630    }
1631
1632    /// c:50 — `ZS_strchr` missing char returns None.
1633    #[test]
1634    fn ZS_strchr_missing_returns_none() {
1635        let s: Vec<char> = vec!['h', 'i'];
1636        assert_eq!(ZS_strchr(&s, 'x'), None);
1637        assert_eq!(ZS_strchr(&[], 'a'), None);
1638    }
1639
1640    /// c:51 — `ZS_memchr` respects bounded length.
1641    #[test]
1642    fn ZS_memchr_bounded_by_n() {
1643        let s: Vec<char> = vec!['a', 'b', 'c', 'd', 'e'];
1644        assert_eq!(ZS_memchr(&s, 'd', 5), Some(3), "found at 3 within range 5");
1645        assert_eq!(ZS_memchr(&s, 'd', 3), None, "n=3 stops before 'd' at idx 3");
1646    }
1647
1648    /// c:49 — `ZS_width` returns char count (matches ZS_strlen).
1649    #[test]
1650    fn ZS_width_matches_strlen() {
1651        let s: Vec<char> = vec!['a', 'b', 'c', '\0'];
1652        assert_eq!(ZS_width(&s), ZS_strlen(&s));
1653    }
1654
1655    /// c:40 — `ZS_memcpy` does NOT overflow dst when n < dst.len().
1656    #[test]
1657    fn ZS_memcpy_respects_n_bound() {
1658        let src: Vec<char> = vec!['a', 'b', 'c', 'd', 'e'];
1659        let mut dst: Vec<char> = vec!['_'; 5];
1660        ZS_memcpy(&mut dst, &src, 3);
1661        assert_eq!(dst[0], 'a');
1662        assert_eq!(dst[1], 'b');
1663        assert_eq!(dst[2], 'c');
1664        assert_eq!(dst[3], '_', "untouched past n");
1665        assert_eq!(dst[4], '_');
1666    }
1667
1668    /// c:47 — `ZS_strncmp` of "ab" vs "ab" with n=2 returns Equal.
1669    #[test]
1670    fn ZS_strncmp_equal_bounded() {
1671        let a: Vec<char> = vec!['a', 'b', '\0'];
1672        let b: Vec<char> = vec!['a', 'b', '\0'];
1673        assert_eq!(ZS_strncmp(&a, &b, 2), std::cmp::Ordering::Equal);
1674    }
1675
1676    /// c:47 — `ZS_strncmp("abc", "abd", 2)` Equal (both stop at idx 2).
1677    #[test]
1678    fn ZS_strncmp_n_clamps_comparison() {
1679        let a: Vec<char> = vec!['a', 'b', 'c', '\0'];
1680        let b: Vec<char> = vec!['a', 'b', 'd', '\0'];
1681        assert_eq!(ZS_strncmp(&a, &b, 2), std::cmp::Ordering::Equal);
1682        // With n=3, differs.
1683        assert_ne!(ZS_strncmp(&a, &b, 3), std::cmp::Ordering::Equal);
1684    }
1685
1686    // ═══════════════════════════════════════════════════════════════════
1687    // Additional C-parity tests for Src/Zle/zle.h
1688    // c:34 ZLE_CHAR_SIZE / c:37 ZLEEOF / c:71 Th / c:122 invicmdmode /
1689    // c:155 ZS_memset / c:225 ZS_strchr / c:233 ZS_memchr / c:242 ZS_width /
1690    // c:256-303 ZC_* classifiers
1691    // ═══════════════════════════════════════════════════════════════════
1692
1693    /// c:34 — `ZLE_CHAR_SIZE` is 4 (per Unicode codepoint size).
1694    #[test]
1695    fn zle_char_size_is_four() {
1696        assert_eq!(ZLE_CHAR_SIZE, 4, "wide-char size is 4 bytes");
1697    }
1698
1699    /// c:37 — `ZLEEOF` is -1 (WEOF sentinel).
1700    #[test]
1701    fn zleeof_is_minus_one() {
1702        assert_eq!(ZLEEOF, -1, "WEOF sentinel = -1");
1703    }
1704
1705    /// c:71 — `Th(-1)` returns None (out-of-range index).
1706    #[test]
1707    fn th_negative_index_returns_none() {
1708        let _g = crate::test_util::global_state_lock();
1709        let _g2 = zle_test_setup();
1710        assert!(Th(-1).is_none());
1711    }
1712
1713    /// c:71 — `Th(99999)` returns None (far-out-of-range index).
1714    #[test]
1715    fn th_huge_index_returns_none() {
1716        let _g = crate::test_util::global_state_lock();
1717        let _g2 = zle_test_setup();
1718        assert!(Th(99999).is_none());
1719    }
1720
1721    /// c:122 — `invicmdmode("emacs")` returns false.
1722    #[test]
1723    fn invicmdmode_emacs_returns_false() {
1724        assert!(!invicmdmode("emacs"), "emacs keymap is NOT vi cmd mode");
1725    }
1726
1727    /// c:122 — `invicmdmode("vicmd")` returns true.
1728    #[test]
1729    fn invicmdmode_vicmd_returns_true() {
1730        assert!(invicmdmode("vicmd"), "vicmd keymap IS vi cmd mode");
1731    }
1732
1733    /// c:122 — `invicmdmode("")` returns false.
1734    #[test]
1735    fn invicmdmode_empty_returns_false() {
1736        assert!(!invicmdmode(""), "empty keymap is NOT vi cmd mode");
1737    }
1738
1739    /// c:155 — `ZS_memset(empty, _, 0)` is safe.
1740    #[test]
1741    fn zs_memset_zero_n_empty_buf_safe() {
1742        let mut empty: Vec<char> = vec![];
1743        ZS_memset(&mut empty, 'x', 0);
1744        assert!(empty.is_empty());
1745    }
1746
1747    /// c:225 — `ZS_strchr(empty, _)` returns None.
1748    #[test]
1749    fn zs_strchr_empty_returns_none() {
1750        let empty: Vec<char> = vec![];
1751        assert_eq!(ZS_strchr(&empty, 'a'), None);
1752    }
1753
1754    /// c:233 — `ZS_memchr(empty, _, 0)` returns None.
1755    #[test]
1756    fn zs_memchr_empty_zero_n_returns_none() {
1757        let empty: Vec<char> = vec![];
1758        assert_eq!(ZS_memchr(&empty, 'a', 0), None);
1759    }
1760
1761    /// c:242 — `ZS_width(empty)` returns 0.
1762    #[test]
1763    fn zs_width_empty_returns_zero() {
1764        let empty: Vec<char> = vec![];
1765        assert_eq!(ZS_width(&empty), 0);
1766    }
1767
1768    /// c:256-303 — ZC_* classifiers pure for ASCII range.
1769    #[test]
1770    fn zc_classifiers_pure_for_ascii() {
1771        for c in ['a', 'A', '0', ' ', '\t', '\n', '!'] {
1772            let first_alpha = ZC_ialpha(c);
1773            let first_digit = ZC_idigit(c);
1774            for _ in 0..3 {
1775                assert_eq!(ZC_ialpha(c), first_alpha, "ZC_ialpha({:?}) must be pure", c);
1776                assert_eq!(ZC_idigit(c), first_digit, "ZC_idigit({:?}) must be pure", c);
1777            }
1778        }
1779    }
1780
1781    // ═══════════════════════════════════════════════════════════════════
1782    // Additional C-parity tests for Src/Zle/zle.h
1783    // c:122 invicmdmode / c:155 ZS_memset / c:225 ZS_strchr /
1784    // c:233 ZS_memchr / c:242 ZS_width / c:256-303 ZC_* classifiers
1785    // ═══════════════════════════════════════════════════════════════════
1786
1787    /// c:122 — `invicmdmode` returns bool (compile-time pin).
1788    #[test]
1789    fn invicmdmode_returns_bool_type() {
1790        let _: bool = invicmdmode("emacs");
1791    }
1792
1793    /// c:122 — `invicmdmode` deterministic across keymap names.
1794    #[test]
1795    fn invicmdmode_deterministic() {
1796        for name in ["emacs", "vicmd", "viins", "", "anything"] {
1797            let first = invicmdmode(name);
1798            for _ in 0..3 {
1799                assert_eq!(
1800                    invicmdmode(name),
1801                    first,
1802                    "invicmdmode({:?}) must be pure",
1803                    name
1804                );
1805            }
1806        }
1807    }
1808
1809    /// c:155 — `ZS_memset` actually fills the buffer.
1810    #[test]
1811    fn zs_memset_fills_buffer() {
1812        let mut buf: Vec<char> = vec!['a', 'a', 'a', 'a'];
1813        ZS_memset(&mut buf, 'X', 4);
1814        assert_eq!(
1815            buf,
1816            vec!['X', 'X', 'X', 'X'],
1817            "ZS_memset must fill all 4 slots with 'X'"
1818        );
1819    }
1820
1821    /// c:225 — `ZS_strchr` finds present char.
1822    #[test]
1823    fn zs_strchr_finds_present_char() {
1824        let buf: Vec<char> = "hello".chars().collect();
1825        let r = ZS_strchr(&buf, 'l');
1826        assert!(r.is_some(), "must find 'l' in 'hello'");
1827    }
1828
1829    /// c:225 — `ZS_strchr` for absent char returns None.
1830    #[test]
1831    fn zs_strchr_absent_char_returns_none() {
1832        let buf: Vec<char> = "hello".chars().collect();
1833        assert_eq!(ZS_strchr(&buf, 'z'), None, "'z' not in 'hello' → None");
1834    }
1835
1836    /// c:233 — `ZS_memchr` with n=0 always returns None.
1837    #[test]
1838    fn zs_memchr_zero_n_always_returns_none() {
1839        for buf in [vec!['a', 'b', 'c'], vec!['x']] {
1840            assert_eq!(
1841                ZS_memchr(&buf, 'a', 0),
1842                None,
1843                "n=0 search → None regardless of buffer content"
1844            );
1845        }
1846    }
1847
1848    /// c:242 — `ZS_width` for ASCII returns count.
1849    #[test]
1850    fn zs_width_ascii_matches_char_count() {
1851        for s in ["", "x", "hello", "12345"] {
1852            let buf: Vec<char> = s.chars().collect();
1853            let w = ZS_width(&buf);
1854            assert_eq!(
1855                w as usize,
1856                s.chars().count(),
1857                "ASCII '{}' width = char count",
1858                s
1859            );
1860        }
1861    }
1862
1863    /// c:256-303 — `ZC_ialpha` returns bool (compile-time pin).
1864    #[test]
1865    fn zc_ialpha_returns_bool_type() {
1866        let _: bool = ZC_ialpha('a');
1867    }
1868
1869    /// c:256-303 — `ZC_idigit` returns bool (compile-time pin).
1870    #[test]
1871    fn zc_idigit_returns_bool_type() {
1872        let _: bool = ZC_idigit('0');
1873    }
1874
1875    /// c:256-303 — `ZC_ialpha` correctly classifies basic ASCII.
1876    #[test]
1877    fn zc_ialpha_basic_ascii_classification() {
1878        assert!(ZC_ialpha('a'), "'a' is alpha");
1879        assert!(ZC_ialpha('Z'), "'Z' is alpha");
1880        assert!(!ZC_ialpha('0'), "'0' is NOT alpha");
1881        assert!(!ZC_ialpha(' '), "' ' is NOT alpha");
1882    }
1883
1884    /// c:256-303 — `ZC_idigit` correctly classifies basic ASCII.
1885    #[test]
1886    fn zc_idigit_basic_ascii_classification() {
1887        assert!(ZC_idigit('0'), "'0' is digit");
1888        assert!(ZC_idigit('9'), "'9' is digit");
1889        assert!(!ZC_idigit('a'), "'a' is NOT digit");
1890        assert!(!ZC_idigit(' '), "' ' is NOT digit");
1891    }
1892
1893    /// c:71 — `Th` returns None for negative + MIN (alt pin).
1894    #[test]
1895    fn th_negative_alt_pin() {
1896        let _g = crate::test_util::global_state_lock();
1897        let _g2 = zle_test_setup();
1898        for idx in [-1, -100, i32::MIN] {
1899            assert!(Th(idx).is_none(), "Th({}) must be None", idx);
1900        }
1901    }
1902}