Skip to main content

lattice_ui_tui/
keymap_replace.rs

1//! Crossterm-coupled test harness for the renderer-neutral
2//! `lattice_host::keymap_replace` catalog. Production code
3//! moved to lattice-host in slice 5.4 / slice 4; the tests
4//! stay here because their `ev()` helper builds `KeyChord`
5//! values via `crate::chord::from_event(&KeyEvent { ... })`.
6
7pub use lattice_host::keymap_replace::*;
8
9#[cfg(test)]
10mod tests {
11    #![allow(clippy::unwrap_used, clippy::panic, unused_imports)]
12    use super::*;
13    use crate::actions::ActionIds;
14    use crate::app::Action;
15    use crate::chord::KeyChord;
16    use crate::keymap_registry::KeymapHandle;
17    use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
18
19    fn ev(code: KeyCode, mods: KeyModifiers) -> KeyChord {
20        let raw = KeyEvent {
21            code,
22            modifiers: mods,
23            kind: KeyEventKind::Press,
24            state: KeyEventState::NONE,
25        };
26        crate::chord::from_event(&raw).expect("test event converts to a chord")
27    }
28
29    fn shared_actions() -> &'static ActionIds {
30        use std::sync::OnceLock;
31        static A: OnceLock<ActionIds> = OnceLock::new();
32        A.get_or_init(|| {
33            let mut r = lattice_grammar::CommandRegistry::new();
34            let b = lattice_grammar::builtins::populate(&mut r);
35            let _ = lattice_grammar::ex_commands::populate(&mut r);
36            crate::actions::populate(&mut r, &b)
37        })
38    }
39
40    fn populated_handle() -> KeymapHandle {
41        let h = KeymapHandle::new();
42        register_replace_bindings(&h, shared_actions());
43        h
44    }
45
46    #[test]
47    fn esc_exits_to_normal() {
48        let h = populated_handle();
49        let a = shared_actions();
50        let r = dispatch_replace(&h, &ev(KeyCode::Esc, KeyModifiers::NONE));
51        match r {
52            Action::Invoke(inv) => assert_eq!(inv.command, a.enter_mode_normal),
53            other => panic!("expected Invoke(enter_mode_normal), got {other:?}"),
54        }
55    }
56
57    #[test]
58    fn backspace_undoes_last() {
59        let h = populated_handle();
60        let a = shared_actions();
61        let r = dispatch_replace(&h, &ev(KeyCode::Backspace, KeyModifiers::NONE));
62        match r {
63            Action::Invoke(inv) => assert_eq!(inv.command, a.replace_undo_last),
64            other => panic!("expected Invoke(replace_undo_last), got {other:?}"),
65        }
66    }
67
68    #[test]
69    fn enter_inserts_newline() {
70        let h = populated_handle();
71        let a = shared_actions();
72        let r = dispatch_replace(&h, &ev(KeyCode::Enter, KeyModifiers::NONE));
73        match r {
74            Action::Invoke(inv) => assert_eq!(inv.command, a.insert_newline),
75            other => panic!("expected Invoke(insert_newline), got {other:?}"),
76        }
77    }
78
79    #[test]
80    fn printable_char_overwrites_with_correct_char() {
81        let h = populated_handle();
82        let a = shared_actions();
83        for c in ['a', 'A', '$', '0', ' '] {
84            let r = dispatch_replace(&h, &ev(KeyCode::Char(c), KeyModifiers::NONE));
85            match r {
86                Action::Invoke(inv) => {
87                    assert_eq!(inv.command, a.overwrite_char);
88                    assert!(matches!(inv.args, lattice_grammar::args::Args::Char(got) if got == c));
89                }
90                other => panic!(
91                    "char {c}: expected Invoke(overwrite_char, args=Char({c:?})), got {other:?}"
92                ),
93            }
94        }
95    }
96
97    #[test]
98    fn ctrl_modifier_yields_none() {
99        let h = populated_handle();
100        let r = dispatch_replace(&h, &ev(KeyCode::Char('c'), KeyModifiers::CONTROL));
101        assert!(matches!(r, Action::None));
102    }
103
104    #[test]
105    fn unhandled_special_keys_yield_none() {
106        // `<Up>`, `<Tab>`, etc. aren't bound in Replace mode.
107        let h = populated_handle();
108        for code in [KeyCode::Up, KeyCode::Tab, KeyCode::F(1)] {
109            let r = dispatch_replace(&h, &ev(code, KeyModifiers::NONE));
110            assert!(
111                matches!(r, Action::None),
112                "code {code:?}: expected None, got {r:?}"
113            );
114        }
115    }
116
117    /// `<M-x>` falls through to `OverwriteChar('x')` -- legacy
118    /// `translate_replace` short-circuited only on `CONTROL`, so
119    /// any other modifier-bearing printable still hit the
120    /// `KeyCode::Char(c)` arm. The dispatcher strips the
121    /// non-CONTROL modifiers before lookup so the bare-char
122    /// wildcard absorbs them; this test pins that semantic.
123    #[test]
124    fn alt_modifier_falls_through_to_overwrite() {
125        let h = populated_handle();
126        let a = shared_actions();
127        let r = dispatch_replace(&h, &ev(KeyCode::Char('x'), KeyModifiers::ALT));
128        match r {
129            Action::Invoke(inv) => {
130                assert_eq!(inv.command, a.overwrite_char);
131                assert!(matches!(inv.args, lattice_grammar::args::Args::Char('x')));
132            }
133            other => panic!("expected Invoke(overwrite_char, Char('x')), got {other:?}"),
134        }
135    }
136
137    /// SHIFT alone with a printable char STILL counts as a
138    /// printable (the terminal already encoded shift in the
139    /// case). Drift case to verify.
140    #[test]
141    fn shift_only_printable_overwrites() {
142        let h = populated_handle();
143        let a = shared_actions();
144        let r = dispatch_replace(&h, &ev(KeyCode::Char('A'), KeyModifiers::SHIFT));
145        match r {
146            Action::Invoke(inv) => {
147                assert_eq!(inv.command, a.overwrite_char);
148                assert!(matches!(inv.args, lattice_grammar::args::Args::Char('A')));
149            }
150            other => panic!("expected Invoke(overwrite_char, Char('A')), got {other:?}"),
151        }
152    }
153
154    /// `KeyKind::Char` is `pub` so the catch-all in the
155    /// drift comparator can introspect; assert we don't have
156    /// stray uses that bypass the public API.
157    #[test]
158    fn keykind_remains_pub_visible() {
159        let _ = crate::chord::KeyKind::Char('a');
160    }
161}