Skip to main content

lattice_ui_tui/
keymap_insert.rs

1//! Crossterm-coupled test harness for the renderer-neutral
2//! `lattice_host::keymap_insert` 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_insert::*;
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, PushLayerKind};
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    /// Process-wide shared `ActionIds`. Built once on first
30    /// access. The IDs are stable for the duration of a test
31    /// run so cross-test handle-and-id comparisons hold.
32    /// Process-wide shared `(registry, actions)`, built once. The
33    /// registry is kept (not discarded) so the snippet mode-keymap
34    /// layer can be translated against the SAME registry that minted
35    /// `shared_actions()` -- `CommandId`s are only stable *within* one
36    /// registry instance, so a layer built from a second registry
37    /// would resolve `action:snippet-next-placeholder` to a different
38    /// id than the test asserts.
39    fn shared_init() -> &'static (lattice_grammar::CommandRegistry, ActionIds) {
40        use std::sync::OnceLock;
41        static INIT: OnceLock<(lattice_grammar::CommandRegistry, ActionIds)> = OnceLock::new();
42        INIT.get_or_init(|| {
43            let mut r = lattice_grammar::CommandRegistry::new();
44            let b = lattice_grammar::builtins::populate(&mut r);
45            let _ = lattice_grammar::ex_commands::populate(&mut r);
46            let a = crate::actions::populate(&mut r, &b);
47            (r, a)
48        })
49    }
50
51    fn shared_actions() -> &'static ActionIds {
52        &shared_init().1
53    }
54
55    fn shared_registry() -> &'static lattice_grammar::CommandRegistry {
56        &shared_init().0
57    }
58
59    fn populated_handle() -> KeymapHandle {
60        let h = KeymapHandle::new();
61        register_insert_bindings(&h, shared_actions());
62        h
63    }
64
65    fn populated_handle_with_popup() -> KeymapHandle {
66        let h = populated_handle();
67        h.push_layer(
68            PushLayerKind::MinorMode(completion_popup_mode_id()),
69            "completion-popup",
70            completion_popup_layer_bindings(shared_actions()),
71        );
72        h
73    }
74
75    /// Push the `active-snippet-mode` layer via the K.2.4 translation path
76    /// so the test uses the same mechanism as editor boot.
77    fn push_snippet_layer_via_k24(h: &KeymapHandle) {
78        // Translate the snippet mode keymap against the SHARED registry
79        // (not a fresh one) so its bindings resolve `action:snippet-*`
80        // to the same `CommandId`s `shared_actions()` exposes.
81        let mut mr = lattice_mode::ModeRegistry::new();
82        mr.register(lattice_snippet::modes::SnippetActiveMode)
83            .expect("register active-snippet-mode");
84        // SN.3c.1: `snippet-mode` owns the `<C-x><C-s>` expand chord
85        // (Insert) via `keymap()`. Register it here too so the
86        // translated layer carries the chord — mirrors boot, where
87        // `<C-x><C-s>` is no longer a Builtin binding.
88        mr.register(lattice_snippet::modes::SnippetMode::new())
89            .expect("register snippet-mode");
90        lattice_host::keymap_mode_contributions::translate_mode_keymaps(h, &mr, shared_registry());
91    }
92
93    fn populated_handle_with_snippet() -> KeymapHandle {
94        let h = populated_handle();
95        push_snippet_layer_via_k24(&h);
96        h
97    }
98
99    fn populated_handle_with_both() -> KeymapHandle {
100        // Order matters under the pre-K.1.c global pre-merge:
101        // snippet first, then popup, so popup's later
102        // installation wins on overlapping chords. After K.1.c
103        // the per-buffer active-mode order drives precedence
104        // — this fixture still exercises the lower-level
105        // registry merge ordering directly.
106        let h = populated_handle();
107        push_snippet_layer_via_k24(&h);
108        h.push_layer(
109            PushLayerKind::MinorMode(completion_popup_mode_id()),
110            "completion-popup",
111            completion_popup_layer_bindings(shared_actions()),
112        );
113        h
114    }
115
116    // SN.3c.2a: per-fixture active-minor-mode sets. Insert dispatch is
117    // now K.1.c-gated (`dispatch_insert` threads `active_minor_modes`),
118    // so each test passes the minor set its fixture pushed — base tests
119    // pass `&[]`, the popup/snippet fixtures pass the modes they
120    // activate. This is exactly the per-buffer gate production applies;
121    // before the gate, `handle.lookup` folded in every pushed layer
122    // unconditionally and these tests passed without naming the mode.
123    fn popup_minors() -> Vec<lattice_mode::ModeId> {
124        vec![completion_popup_mode_id()]
125    }
126    fn snippet_minors() -> Vec<lattice_mode::ModeId> {
127        vec![
128            lattice_snippet::modes::SnippetMode::mode_id(),
129            lattice_snippet::modes::SnippetActiveMode::mode_id(),
130        ]
131    }
132    fn both_minors() -> Vec<lattice_mode::ModeId> {
133        let mut v = snippet_minors();
134        v.push(completion_popup_mode_id());
135        v
136    }
137
138    // ---- Base Insert ----
139
140    #[test]
141    fn esc_in_base_insert_returns_to_normal() {
142        let h = populated_handle();
143        let a = shared_actions();
144        match dispatch_insert(
145            &h,
146            lattice_keymap::BindingMode::Insert,
147            &ev(KeyCode::Esc, KeyModifiers::NONE),
148            &[],
149            &[],
150        ) {
151            Action::Invoke(inv) => assert_eq!(inv.command, a.enter_mode_normal),
152            other => panic!("expected Invoke(enter_mode_normal), got {other:?}"),
153        }
154    }
155
156    #[test]
157    fn backspace_in_base_insert_deletes_char_backward() {
158        let h = populated_handle();
159        let a = shared_actions();
160        let r = dispatch_insert(
161            &h,
162            lattice_keymap::BindingMode::Insert,
163            &ev(KeyCode::Backspace, KeyModifiers::NONE),
164            &[],
165            &[],
166        );
167        match r {
168            Action::Invoke(inv) => assert_eq!(inv.command, a.delete_char_backward),
169            other => panic!("expected Invoke(delete_char_backward), got {other:?}"),
170        }
171    }
172
173    #[test]
174    fn enter_in_base_insert_inserts_newline() {
175        let h = populated_handle();
176        let a = shared_actions();
177        match dispatch_insert(
178            &h,
179            lattice_keymap::BindingMode::Insert,
180            &ev(KeyCode::Enter, KeyModifiers::NONE),
181            &[],
182            &[],
183        ) {
184            Action::Invoke(inv) => assert_eq!(inv.command, a.insert_newline),
185            other => panic!("expected Invoke(insert_newline), got {other:?}"),
186        }
187    }
188
189    #[test]
190    fn tab_in_base_insert_inserts_tab() {
191        let h = populated_handle();
192        let a = shared_actions();
193        match dispatch_insert(
194            &h,
195            lattice_keymap::BindingMode::Insert,
196            &ev(KeyCode::Tab, KeyModifiers::NONE),
197            &[],
198            &[],
199        ) {
200            Action::Invoke(inv) => assert_eq!(inv.command, a.insert_tab),
201            other => panic!("expected Invoke(insert_tab), got {other:?}"),
202        }
203    }
204
205    #[test]
206    fn printable_char_in_base_insert_falls_through_to_insert() {
207        let h = populated_handle();
208        for c in ['a', 'A', '1', '$', ' '] {
209            match dispatch_insert(
210                &h,
211                lattice_keymap::BindingMode::Insert,
212                &ev(KeyCode::Char(c), KeyModifiers::NONE),
213                &[],
214                &[],
215            ) {
216                Action::Insert(s) => assert_eq!(s, c.to_string()),
217                other => panic!("char {c:?}: expected Insert, got {other:?}"),
218            }
219        }
220    }
221
222    #[test]
223    fn ctrl_letter_unbound_in_base_insert_yields_none() {
224        let h = populated_handle();
225        // <C-y> isn't bound at base; legacy returned None.
226        let r = dispatch_insert(
227            &h,
228            lattice_keymap::BindingMode::Insert,
229            &ev(KeyCode::Char('y'), KeyModifiers::CONTROL),
230            &[],
231            &[],
232        );
233        assert!(matches!(r, Action::None));
234    }
235
236    #[test]
237    fn ctrl_space_in_base_insert_triggers_completion() {
238        let h = populated_handle();
239        let a = shared_actions();
240        let r = dispatch_insert(
241            &h,
242            lattice_keymap::BindingMode::Insert,
243            &ev(KeyCode::Char(' '), KeyModifiers::CONTROL),
244            &[],
245            &[],
246        );
247        match r {
248            Action::Invoke(inv) => assert_eq!(inv.command, a.completion_trigger),
249            other => panic!("expected Invoke(completion_trigger), got {other:?}"),
250        }
251    }
252
253    #[test]
254    fn ctrl_x_in_base_insert_absorbs_partial_chord() {
255        // Slice 8.i.4.b: pressing `<C-x>` returns
256        // `Action::AbsorbPartialChord(<C-x>)`. The trie's `Partial`
257        // result drives the App's `partial_chord` stack; the next
258        // keystroke runs through `dispatch_insert` with that prefix
259        // and resolves `<C-x><C-s>`.
260        //
261        // SN.3c.1: `<C-x>` is a partial prefix ONLY because
262        // `snippet-mode`'s layer provides the `<C-x><C-s>` terminal —
263        // it's no longer Builtin. Use the snippet-layer handle so the
264        // merged trie sees the prefix (matches boot, where the layer
265        // is pushed).
266        let h = populated_handle_with_snippet();
267        let r = dispatch_insert(
268            &h,
269            lattice_keymap::BindingMode::Insert,
270            &ev(KeyCode::Char('x'), KeyModifiers::CONTROL),
271            &[],
272            &snippet_minors(),
273        );
274        match r {
275            Action::AbsorbPartialChord(c) => assert_eq!(c, KeyChord::ctrl('x')),
276            other => panic!("expected AbsorbPartialChord(<C-x>), got {other:?}"),
277        }
278    }
279
280    #[test]
281    fn ctrl_x_then_ctrl_o_no_longer_binds() {
282        // CSM.K1: `<C-x><C-o>` (vim omni-completion alias)
283        // retired. The chord falls through to the dispatcher
284        // with no Invoke; `<C-Space>` is the sole popup trigger.
285        let h = populated_handle();
286        let r = dispatch_insert(
287            &h,
288            lattice_keymap::BindingMode::Insert,
289            &ev(KeyCode::Char('o'), KeyModifiers::CONTROL),
290            &[KeyChord::ctrl('x')],
291            &[],
292        );
293        assert!(
294            !matches!(r, Action::Invoke(_)),
295            "<C-x><C-o> should no longer resolve to an Invoke; got {r:?}",
296        );
297    }
298
299    #[test]
300    fn ctrl_x_then_ctrl_s_resolves_to_snippet_expand() {
301        // SN.3c.1: `<C-x><C-s>` is contributed by `snippet-mode`'s
302        // layer (not Builtin), so resolve against the snippet-layer
303        // handle — same merged trie the editor sees once the layer is
304        // boot-pushed.
305        let h = populated_handle_with_snippet();
306        let a = shared_actions();
307        let r = dispatch_insert(
308            &h,
309            lattice_keymap::BindingMode::Insert,
310            &ev(KeyCode::Char('s'), KeyModifiers::CONTROL),
311            &[KeyChord::ctrl('x')],
312            &snippet_minors(),
313        );
314        match r {
315            Action::Invoke(inv) => assert_eq!(inv.command, a.snippet_expand),
316            other => panic!("expected Invoke(snippet_expand), got {other:?}"),
317        }
318    }
319
320    #[test]
321    fn ctrl_x_then_unrecognized_drops_partial_chord() {
322        // Slice 8.i.4.b: an unrecognised second key after
323        // `<C-x>` returns `Action::None`. `App::apply`'s
324        // non-`AbsorbPartialChord(_)` clear-rule resets
325        // `partial_chord` automatically -- no explicit
326        // `SetPending(None)` round-trip needed.
327        let h = populated_handle();
328        let r = dispatch_insert(
329            &h,
330            lattice_keymap::BindingMode::Insert,
331            &ev(KeyCode::Char('q'), KeyModifiers::CONTROL),
332            &[KeyChord::ctrl('x')],
333            &[],
334        );
335        assert!(matches!(r, Action::None));
336    }
337
338    // ---- Completion popup overlay ----
339
340    #[test]
341    fn popup_ctrl_n_navigates_next() {
342        let h = populated_handle_with_popup();
343        let a = shared_actions();
344        let r = dispatch_insert(
345            &h,
346            lattice_keymap::BindingMode::Insert,
347            &ev(KeyCode::Char('n'), KeyModifiers::CONTROL),
348            &[],
349            &popup_minors(),
350        );
351        match r {
352            Action::Invoke(inv) => assert_eq!(inv.command, a.completion_next),
353            other => panic!("expected Invoke(completion_next), got {other:?}"),
354        }
355    }
356
357    #[test]
358    fn popup_down_arrow_navigates_next() {
359        let h = populated_handle_with_popup();
360        let a = shared_actions();
361        let r = dispatch_insert(
362            &h,
363            lattice_keymap::BindingMode::Insert,
364            &ev(KeyCode::Down, KeyModifiers::NONE),
365            &[],
366            &popup_minors(),
367        );
368        match r {
369            Action::Invoke(inv) => assert_eq!(inv.command, a.completion_next),
370            other => panic!("expected Invoke(completion_next), got {other:?}"),
371        }
372    }
373
374    #[test]
375    fn popup_tab_accepts() {
376        let h = populated_handle_with_popup();
377        let a = shared_actions();
378        let r = dispatch_insert(
379            &h,
380            lattice_keymap::BindingMode::Insert,
381            &ev(KeyCode::Tab, KeyModifiers::NONE),
382            &[],
383            &popup_minors(),
384        );
385        match r {
386            Action::Invoke(inv) => assert_eq!(inv.command, a.completion_accept),
387            other => panic!("expected Invoke(completion_accept), got {other:?}"),
388        }
389    }
390
391    #[test]
392    fn popup_enter_accepts() {
393        let h = populated_handle_with_popup();
394        let a = shared_actions();
395        let r = dispatch_insert(
396            &h,
397            lattice_keymap::BindingMode::Insert,
398            &ev(KeyCode::Enter, KeyModifiers::NONE),
399            &[],
400            &popup_minors(),
401        );
402        match r {
403            Action::Invoke(inv) => assert_eq!(inv.command, a.completion_accept),
404            other => panic!("expected Invoke(completion_accept), got {other:?}"),
405        }
406    }
407
408    #[test]
409    fn popup_esc_cancels_and_exits_insert() {
410        let h = populated_handle_with_popup();
411        let a = shared_actions();
412        let r = dispatch_insert(
413            &h,
414            lattice_keymap::BindingMode::Insert,
415            &ev(KeyCode::Esc, KeyModifiers::NONE),
416            &[],
417            &popup_minors(),
418        );
419        match r {
420            Action::Invoke(inv) => {
421                assert_eq!(inv.command, a.completion_cancel_and_exit_insert)
422            }
423            other => panic!("expected Invoke(completion_cancel_and_exit_insert), got {other:?}"),
424        }
425    }
426
427    #[test]
428    fn popup_ctrl_e_cancels_only() {
429        let h = populated_handle_with_popup();
430        let a = shared_actions();
431        let r = dispatch_insert(
432            &h,
433            lattice_keymap::BindingMode::Insert,
434            &ev(KeyCode::Char('e'), KeyModifiers::CONTROL),
435            &[],
436            &popup_minors(),
437        );
438        match r {
439            Action::Invoke(inv) => assert_eq!(inv.command, a.completion_cancel),
440            other => panic!("expected Invoke(completion_cancel), got {other:?}"),
441        }
442    }
443
444    #[test]
445    fn popup_bare_char_routes_through_accept_then_insert() {
446        use lattice_grammar::args::Args;
447        let h = populated_handle_with_popup();
448        let a = shared_actions();
449        let r = dispatch_insert(
450            &h,
451            lattice_keymap::BindingMode::Insert,
452            &ev(KeyCode::Char('a'), KeyModifiers::NONE),
453            &[],
454            &popup_minors(),
455        );
456        match r {
457            Action::Invoke(inv) => {
458                assert_eq!(inv.command, a.completion_accept_then_insert);
459                assert!(matches!(inv.args, Args::Char('a')));
460            }
461            other => {
462                panic!("expected Invoke(completion_accept_then_insert, Char('a')), got {other:?}")
463            }
464        }
465    }
466
467    /// CSM.K2: `<C-b>` inside the popup filters to
468    /// `gen:buffer-words` (Args::String payload). The previous
469    /// docs-scroll-up binding has been moved to `PageUp`.
470    #[test]
471    fn popup_ctrl_b_filters_to_buffer_words() {
472        use lattice_grammar::args::Args;
473        let h = populated_handle_with_popup();
474        let a = shared_actions();
475        let r = dispatch_insert(
476            &h,
477            lattice_keymap::BindingMode::Insert,
478            &ev(KeyCode::Char('b'), KeyModifiers::CONTROL),
479            &[],
480            &popup_minors(),
481        );
482        match r {
483            Action::Invoke(inv) => {
484                assert_eq!(inv.command, a.completion_filter_to_source);
485                match inv.args {
486                    Args::String(s) => {
487                        assert_eq!(s, lattice_completion::insert::BufferWordsSource::ID)
488                    }
489                    other => panic!("expected Args::String, got {other:?}"),
490                }
491            }
492            other => panic!(
493                "expected Invoke(completion_filter_to_source, \"gen:buffer-words\"), got {other:?}"
494            ),
495        }
496    }
497
498    /// CSM.K2: `<C-o>` filters to LSP.
499    #[test]
500    fn popup_ctrl_o_filters_to_lsp() {
501        use lattice_grammar::args::Args;
502        let h = populated_handle_with_popup();
503        let a = shared_actions();
504        let r = dispatch_insert(
505            &h,
506            lattice_keymap::BindingMode::Insert,
507            &ev(KeyCode::Char('o'), KeyModifiers::CONTROL),
508            &[],
509            &popup_minors(),
510        );
511        match r {
512            Action::Invoke(inv) => {
513                assert_eq!(inv.command, a.completion_filter_to_source);
514                match inv.args {
515                    Args::String(s) => {
516                        assert_eq!(s, lattice_completion::insert::LSP_COMPLETION_SOURCE_ID)
517                    }
518                    other => panic!("expected Args::String, got {other:?}"),
519                }
520            }
521            other => panic!("expected Invoke(completion_filter_to_source, lsp), got {other:?}"),
522        }
523    }
524
525    /// CSM.K2: `<C-Space>` inside the popup clears the active
526    /// source filter (replaces the legacy
527    /// `completion_trigger` binding on this layer).
528    #[test]
529    fn popup_ctrl_space_clears_filter() {
530        let h = populated_handle_with_popup();
531        let a = shared_actions();
532        let r = dispatch_insert(
533            &h,
534            lattice_keymap::BindingMode::Insert,
535            &ev(KeyCode::Char(' '), KeyModifiers::CONTROL),
536            &[],
537            &popup_minors(),
538        );
539        match r {
540            Action::Invoke(inv) => {
541                assert_eq!(inv.command, a.completion_filter_clear);
542            }
543            other => panic!("expected Invoke(completion_filter_clear), got {other:?}"),
544        }
545    }
546
547    /// CSM.K2: docs-scroll moved off `<C-f>`/`<C-b>` (now filter
548    /// chords) to `PageDown` / `PageUp`.
549    #[test]
550    fn popup_page_down_scrolls_docs() {
551        let h = populated_handle_with_popup();
552        let a = shared_actions();
553        let r = dispatch_insert(
554            &h,
555            lattice_keymap::BindingMode::Insert,
556            &ev(KeyCode::PageDown, KeyModifiers::NONE),
557            &[],
558            &popup_minors(),
559        );
560        match r {
561            Action::Invoke(inv) => {
562                assert_eq!(inv.command, a.completion_docs_scroll_down)
563            }
564            other => panic!("expected Invoke(completion_docs_scroll_down), got {other:?}"),
565        }
566    }
567
568    #[test]
569    fn popup_ctrl_x_falls_through_to_base_insert_partial_chord() {
570        // Slice 8.i.4.b: the popup layer doesn't bind <C-x>; lookup
571        // falls through to the `<C-x>` partial node, returning
572        // `AbsorbPartialChord(<C-x>)`.
573        //
574        // SN.3c.1: that partial node is no longer Builtin — it's
575        // contributed by `snippet-mode`'s `<C-x><C-s>` layer. In
576        // production both layers coexist (snippet-mode is Global on
577        // every document buffer, popup-mode is active while the popup
578        // is open), so the fixture pushes both. The assertion still
579        // protects the same property: the popup layer must not shadow
580        // the `<C-x>` partial.
581        let h = populated_handle_with_both();
582        let r = dispatch_insert(
583            &h,
584            lattice_keymap::BindingMode::Insert,
585            &ev(KeyCode::Char('x'), KeyModifiers::CONTROL),
586            &[],
587            &both_minors(),
588        );
589        match r {
590            Action::AbsorbPartialChord(c) => assert_eq!(c, KeyChord::ctrl('x')),
591            other => panic!("expected AbsorbPartialChord(<C-x>), got {other:?}"),
592        }
593    }
594
595    // ---- Active snippet overlay ----
596
597    #[test]
598    fn snippet_tab_jumps_to_next_placeholder() {
599        let h = populated_handle_with_snippet();
600        let a = shared_actions();
601        let r = dispatch_insert(
602            &h,
603            lattice_keymap::BindingMode::Insert,
604            &ev(KeyCode::Tab, KeyModifiers::NONE),
605            &[],
606            &snippet_minors(),
607        );
608        match r {
609            Action::Invoke(inv) => {
610                assert_eq!(inv.command, a.snippet_next_placeholder)
611            }
612            other => panic!("expected Invoke(snippet_next_placeholder), got {other:?}"),
613        }
614    }
615
616    #[test]
617    fn snippet_back_tab_jumps_to_prev_placeholder() {
618        let h = populated_handle_with_snippet();
619        let a = shared_actions();
620        let r = dispatch_insert(
621            &h,
622            lattice_keymap::BindingMode::Insert,
623            &ev(KeyCode::BackTab, KeyModifiers::NONE),
624            &[],
625            &snippet_minors(),
626        );
627        match r {
628            Action::Invoke(inv) => {
629                assert_eq!(inv.command, a.snippet_prev_placeholder)
630            }
631            other => panic!("expected Invoke(snippet_prev_placeholder), got {other:?}"),
632        }
633    }
634
635    #[test]
636    fn snippet_shift_tab_jumps_to_prev_placeholder() {
637        let h = populated_handle_with_snippet();
638        let a = shared_actions();
639        let r = dispatch_insert(
640            &h,
641            lattice_keymap::BindingMode::Insert,
642            &ev(KeyCode::Tab, KeyModifiers::SHIFT),
643            &[],
644            &snippet_minors(),
645        );
646        match r {
647            Action::Invoke(inv) => {
648                assert_eq!(inv.command, a.snippet_prev_placeholder)
649            }
650            other => panic!("expected Invoke(snippet_prev_placeholder), got {other:?}"),
651        }
652    }
653
654    #[test]
655    fn snippet_esc_leaves_snippet() {
656        let h = populated_handle_with_snippet();
657        let a = shared_actions();
658        let r = dispatch_insert(
659            &h,
660            lattice_keymap::BindingMode::Insert,
661            &ev(KeyCode::Esc, KeyModifiers::NONE),
662            &[],
663            &snippet_minors(),
664        );
665        // SN.3c.2b: `<Esc>` is `fall_through` — leave the snippet, then
666        // continue to the native `<Esc>` (enter-normal). Resolves to a
667        // Chain of the mode action followed by the native action.
668        match r {
669            Action::Chain(v) => match (v.first(), v.get(1)) {
670                (Some(Action::Invoke(leave)), Some(Action::Invoke(native))) => {
671                    assert_eq!(leave.command, a.snippet_leave);
672                    assert_eq!(native.command, a.enter_mode_normal);
673                    assert_eq!(v.len(), 2);
674                }
675                _ => panic!("expected Chain([snippet_leave, enter_mode_normal]), got {v:?}"),
676            },
677            other => panic!("expected Chain, got {other:?}"),
678        }
679    }
680
681    #[test]
682    fn snippet_unrelated_key_falls_through_to_base_insert() {
683        let h = populated_handle_with_snippet();
684        let r = dispatch_insert(
685            &h,
686            lattice_keymap::BindingMode::Insert,
687            &ev(KeyCode::Char('z'), KeyModifiers::NONE),
688            &[],
689            &snippet_minors(),
690        );
691        match r {
692            Action::Insert(s) => assert_eq!(s, "z"),
693            other => panic!("expected Insert(z), got {other:?}"),
694        }
695    }
696
697    // ---- Combined overlays: popup wins ----
698
699    #[test]
700    fn popup_wins_over_snippet_for_tab() {
701        let h = populated_handle_with_both();
702        let a = shared_actions();
703        let r = dispatch_insert(
704            &h,
705            lattice_keymap::BindingMode::Insert,
706            &ev(KeyCode::Tab, KeyModifiers::NONE),
707            &[],
708            &both_minors(),
709        );
710        // Popup binds <Tab> -> CompletionAccept; snippet binds
711        // <Tab> -> SnippetNextPlaceholder. Popup pushed last
712        // (higher LayerId) so popup wins.
713        match r {
714            Action::Invoke(inv) => assert_eq!(inv.command, a.completion_accept),
715            other => panic!("expected Invoke(completion_accept), got {other:?}"),
716        }
717    }
718
719    #[test]
720    fn popup_wins_over_snippet_for_esc() {
721        let h = populated_handle_with_both();
722        let a = shared_actions();
723        let r = dispatch_insert(
724            &h,
725            lattice_keymap::BindingMode::Insert,
726            &ev(KeyCode::Esc, KeyModifiers::NONE),
727            &[],
728            &both_minors(),
729        );
730        match r {
731            Action::Invoke(inv) => {
732                assert_eq!(inv.command, a.completion_cancel_and_exit_insert)
733            }
734            other => panic!("expected Invoke(completion_cancel_and_exit_insert), got {other:?}"),
735        }
736    }
737
738    #[test]
739    fn shift_tab_with_both_overlays_resolves_via_snippet_layer() {
740        // <S-Tab> is unique to the snippet layer; popup doesn't
741        // bind it. Falls through to snippet -> SnippetPrevPlaceholder.
742        let h = populated_handle_with_both();
743        let a = shared_actions();
744        let r = dispatch_insert(
745            &h,
746            lattice_keymap::BindingMode::Insert,
747            &ev(KeyCode::BackTab, KeyModifiers::NONE),
748            &[],
749            &both_minors(),
750        );
751        match r {
752            Action::Invoke(inv) => {
753                assert_eq!(inv.command, a.snippet_prev_placeholder)
754            }
755            other => panic!("expected Invoke(snippet_prev_placeholder), got {other:?}"),
756        }
757    }
758}