1use crossterm::event::KeyEvent;
20
21use crate::app::Action;
22
23pub use lattice_host::input::TranslateContext;
24
25pub fn translate(ctx: TranslateContext<'_>, event: KeyEvent) -> Action {
26 let Some(chord) = crate::chord::from_event(&event) else {
27 return Action::None;
28 };
29 lattice_host::input::translate(ctx, chord)
30}
31
32#[cfg(test)]
33mod tests {
34 #![allow(clippy::unwrap_used, clippy::panic, unused_imports)]
35 use super::*;
36 use crate::buffers::BufferKind;
37 use crate::chord::{KeyChord, KeyKind, KeyMods, SpecialKey};
38 use crate::keymap_registry::KeymapHandle;
39 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
40 use lattice_grammar::CommandRegistry;
41 use lattice_grammar::ModalState;
42 use lattice_grammar::SearchDirection;
43 use lattice_grammar::Target;
44 use lattice_grammar::VisualKind;
45 use lattice_grammar::builtins::{Builtins, populate};
46
47 fn key(code: KeyCode) -> KeyEvent {
48 KeyEvent::new(code, KeyModifiers::NONE)
49 }
50
51 fn ctrl(code: KeyCode) -> KeyEvent {
52 KeyEvent::new(code, KeyModifiers::CONTROL)
53 }
54
55 fn shared_init() -> &'static (
64 CommandRegistry,
65 Builtins,
66 crate::actions::ActionIds,
67 lattice_syntax::SyntaxTextObjectIds,
68 lattice_syntax::SyntaxMotionIds,
69 ) {
70 use std::sync::OnceLock;
71 static INIT: OnceLock<(
72 CommandRegistry,
73 Builtins,
74 crate::actions::ActionIds,
75 lattice_syntax::SyntaxTextObjectIds,
76 lattice_syntax::SyntaxMotionIds,
77 )> = OnceLock::new();
78 INIT.get_or_init(|| {
79 let mut r = CommandRegistry::new();
80 let b = populate(&mut r);
81 let _ex = lattice_grammar::ex_commands::populate(&mut r);
82 let a = crate::actions::populate(&mut r, &b);
83 let so = lattice_syntax::register_syntax_text_objects(&mut r);
84 let sm = lattice_syntax::register_syntax_motions(&mut r);
85 (r, b, a, so, sm)
89 })
90 }
91
92 fn shared_registry() -> &'static CommandRegistry {
93 &shared_init().0
94 }
95
96 fn shared_builtins() -> &'static Builtins {
97 &shared_init().1
98 }
99
100 fn shared_actions() -> &'static crate::actions::ActionIds {
101 &shared_init().2
102 }
103
104 fn shared_syntax_textobjects() -> &'static lattice_syntax::SyntaxTextObjectIds {
105 &shared_init().3
106 }
107
108 fn shared_syntax_motions() -> &'static lattice_syntax::SyntaxMotionIds {
109 &shared_init().4
110 }
111
112 fn build_base_keymap() -> KeymapHandle {
117 let h = KeymapHandle::new();
118 h.set_command_registry(std::sync::Arc::new(arc_swap::ArcSwap::from_pointee(
123 shared_registry().clone(),
124 )));
125 let b = shared_builtins();
126 let a = shared_actions();
127 let so = shared_syntax_textobjects();
128 let sm = shared_syntax_motions();
129 crate::keymap_replace::register_replace_bindings(&h, a);
130 crate::keymap_visual::register_visual_bindings(&h, b, a, so);
131 crate::keymap_insert::register_insert_bindings(&h, a);
132 crate::keymap_normal::register_normal_bindings(&h, b, a, so, sm);
133 crate::keymap_normal::expand_grammar_rows(
136 &h,
137 shared_registry(),
138 b,
139 lattice_host::keymap_trie::KeymapLayer::Builtin,
140 );
141 h
142 }
143
144 fn shared_keymap_base() -> &'static KeymapHandle {
147 use std::sync::OnceLock;
148 static H: OnceLock<KeymapHandle> = OnceLock::new();
149 H.get_or_init(build_base_keymap)
150 }
151
152 fn shared_keymap_base_with_snippet_mode() -> &'static KeymapHandle {
162 use std::sync::OnceLock;
163 static H: OnceLock<KeymapHandle> = OnceLock::new();
164 H.get_or_init(|| {
165 let h = build_base_keymap();
166 let mut mr = lattice_mode::ModeRegistry::new();
167 mr.register(lattice_snippet::modes::SnippetMode::new())
168 .expect("register snippet-mode");
169 lattice_host::keymap_mode_contributions::translate_mode_keymaps(
170 &h,
171 &mr,
172 shared_registry(),
173 );
174 h
175 })
176 }
177
178 fn shared_keymap_with_command_line_mode() -> &'static KeymapHandle {
185 use std::sync::OnceLock;
186 static H: OnceLock<KeymapHandle> = OnceLock::new();
187 H.get_or_init(|| {
188 let h = build_base_keymap();
189 let mut mr = lattice_mode::ModeRegistry::new();
190 mr.register(lattice_host::command_line_mode::CommandLineMode)
191 .expect("register command-line-mode");
192 lattice_host::keymap_mode_contributions::translate_mode_keymaps(
193 &h,
194 &mr,
195 shared_registry(),
196 );
197 h
198 })
199 }
200 fn command_line_mode_active() -> &'static [lattice_mode::ModeId] {
204 use std::sync::OnceLock;
205 static M: OnceLock<Vec<lattice_mode::ModeId>> = OnceLock::new();
206 M.get_or_init(|| vec![lattice_host::command_line_mode::CommandLineMode::mode_id()])
207 }
208
209 fn popup_minor_modes() -> &'static [lattice_mode::ModeId] {
216 use std::sync::OnceLock;
217 static M: OnceLock<Vec<lattice_mode::ModeId>> = OnceLock::new();
218 M.get_or_init(|| vec![crate::keymap_insert::completion_popup_mode_id()])
219 }
220 fn snippet_mode_only() -> &'static [lattice_mode::ModeId] {
223 use std::sync::OnceLock;
224 static M: OnceLock<Vec<lattice_mode::ModeId>> = OnceLock::new();
225 M.get_or_init(|| vec![lattice_snippet::modes::SnippetMode::mode_id()])
226 }
227 fn snippet_minor_modes() -> &'static [lattice_mode::ModeId] {
229 use std::sync::OnceLock;
230 static M: OnceLock<Vec<lattice_mode::ModeId>> = OnceLock::new();
231 M.get_or_init(|| {
232 vec![
233 lattice_snippet::modes::SnippetMode::mode_id(),
234 lattice_snippet::modes::SnippetActiveMode::mode_id(),
235 ]
236 })
237 }
238 fn both_minor_modes() -> &'static [lattice_mode::ModeId] {
240 use std::sync::OnceLock;
241 static M: OnceLock<Vec<lattice_mode::ModeId>> = OnceLock::new();
242 M.get_or_init(|| {
243 vec![
244 lattice_snippet::modes::SnippetMode::mode_id(),
245 lattice_snippet::modes::SnippetActiveMode::mode_id(),
246 crate::keymap_insert::completion_popup_mode_id(),
247 ]
248 })
249 }
250
251 fn shared_keymap_with_popup() -> &'static KeymapHandle {
253 use std::sync::OnceLock;
254 static H: OnceLock<KeymapHandle> = OnceLock::new();
255 H.get_or_init(|| {
256 let h = build_base_keymap();
257 h.push_layer(
258 crate::keymap_registry::PushLayerKind::MinorMode(
259 crate::keymap_insert::completion_popup_mode_id(),
260 ),
261 "completion-popup",
262 crate::keymap_insert::completion_popup_layer_bindings(shared_actions()),
263 );
264 h
265 })
266 }
267
268 fn push_snippet_layer_via_k24(h: &KeymapHandle) {
270 let mut mr = lattice_mode::ModeRegistry::new();
274 mr.register(lattice_snippet::modes::SnippetActiveMode)
275 .expect("register active-snippet-mode");
276 mr.register(lattice_snippet::modes::SnippetMode::new())
280 .expect("register snippet-mode");
281 lattice_host::keymap_mode_contributions::translate_mode_keymaps(h, &mr, shared_registry());
282 }
283
284 fn shared_keymap_with_snippet() -> &'static KeymapHandle {
286 use std::sync::OnceLock;
287 static H: OnceLock<KeymapHandle> = OnceLock::new();
288 H.get_or_init(|| {
289 let h = build_base_keymap();
290 push_snippet_layer_via_k24(&h);
291 h
292 })
293 }
294
295 fn shared_keymap_with_both_overlays() -> &'static KeymapHandle {
299 use std::sync::OnceLock;
300 static H: OnceLock<KeymapHandle> = OnceLock::new();
301 H.get_or_init(|| {
302 let h = build_base_keymap();
303 push_snippet_layer_via_k24(&h);
304 h.push_layer(
305 crate::keymap_registry::PushLayerKind::MinorMode(
306 crate::keymap_insert::completion_popup_mode_id(),
307 ),
308 "completion-popup",
309 crate::keymap_insert::completion_popup_layer_bindings(shared_actions()),
310 );
311 h
312 })
313 }
314
315 fn fixture() -> (CommandRegistry, Builtins) {
316 let r = CommandRegistry::new();
321 (r, *shared_builtins())
322 }
323
324 fn test_keymap() -> &'static KeymapHandle {
325 shared_keymap_base()
326 }
327
328 fn ctx<'a>(modal: ModalState, b: &'a Builtins) -> TranslateContext<'a> {
329 TranslateContext {
330 modal,
331 builtins: b,
332 pending_count: 0,
333 op_count: 0,
334 recording_macro: false,
335 active_buffer: BufferKind::Document,
336 completion_open: false,
337 chord_capture: false,
338 picker_open: false,
339 insert_completion_open: false,
340 snippet_active: false,
341 keymap: test_keymap(),
342 terminal_insert_active: false,
343 terminal_esc_exits: false,
344 terminal_app_cursor_keys: false,
345 terminal_insert_exit_pending: false,
346 terminal_visual_active: false,
347 partial_chord: &[],
348 active_minor_modes: &[],
349 }
350 }
351
352 fn ctx_partial<'a>(
362 modal: ModalState,
363 partial: &'a [crate::chord::KeyChord],
364 b: &'a Builtins,
365 ) -> TranslateContext<'a> {
366 TranslateContext {
367 modal,
368 builtins: b,
369 pending_count: 0,
370 op_count: 0,
371 recording_macro: false,
372 active_buffer: BufferKind::Document,
373 completion_open: false,
374 chord_capture: false,
375 picker_open: false,
376 insert_completion_open: false,
377 snippet_active: false,
378 keymap: test_keymap(),
379 terminal_insert_active: false,
380 terminal_esc_exits: false,
381 terminal_app_cursor_keys: false,
382 terminal_insert_exit_pending: false,
383 terminal_visual_active: false,
384 partial_chord: partial,
385 active_minor_modes: &[],
386 }
387 }
388
389 fn ctx_with_count<'a>(
390 modal: ModalState,
391 b: &'a Builtins,
392 pending_count: u32,
393 ) -> TranslateContext<'a> {
394 TranslateContext {
395 modal,
396 builtins: b,
397 pending_count,
398 op_count: 0,
399 recording_macro: false,
400 active_buffer: BufferKind::Document,
401 completion_open: false,
402 chord_capture: false,
403 picker_open: false,
404 insert_completion_open: false,
405 snippet_active: false,
406 keymap: test_keymap(),
407 terminal_insert_active: false,
408 terminal_esc_exits: false,
409 terminal_app_cursor_keys: false,
410 terminal_insert_exit_pending: false,
411 terminal_visual_active: false,
412 partial_chord: &[],
413 active_minor_modes: &[],
414 }
415 }
416
417 #[allow(dead_code)]
423 fn ctx_with_op_count<'a>(
424 modal: ModalState,
425 b: &'a Builtins,
426 pending_count: u32,
427 op_count: u32,
428 ) -> TranslateContext<'a> {
429 TranslateContext {
430 modal,
431 builtins: b,
432 pending_count,
433 op_count,
434 recording_macro: false,
435 active_buffer: BufferKind::Document,
436 completion_open: false,
437 chord_capture: false,
438 picker_open: false,
439 insert_completion_open: false,
440 snippet_active: false,
441 keymap: test_keymap(),
442 terminal_insert_active: false,
443 terminal_esc_exits: false,
444 terminal_app_cursor_keys: false,
445 terminal_insert_exit_pending: false,
446 terminal_visual_active: false,
447 partial_chord: &[],
448 active_minor_modes: &[],
449 }
450 }
451
452 fn ctx_recording<'a>(modal: ModalState, b: &'a Builtins) -> TranslateContext<'a> {
453 TranslateContext {
454 modal,
455 builtins: b,
456 pending_count: 0,
457 op_count: 0,
458 recording_macro: true,
459 active_buffer: BufferKind::Document,
460 completion_open: false,
461 chord_capture: false,
462 picker_open: false,
463 insert_completion_open: false,
464 snippet_active: false,
465 keymap: test_keymap(),
466 terminal_insert_active: false,
467 terminal_esc_exits: false,
468 terminal_app_cursor_keys: false,
469 terminal_insert_exit_pending: false,
470 terminal_visual_active: false,
471 partial_chord: &[],
472 active_minor_modes: &[],
473 }
474 }
475
476 fn ctx_partial_with_op_count<'a>(
482 modal: ModalState,
483 partial: &'a [crate::chord::KeyChord],
484 b: &'a Builtins,
485 pending_count: u32,
486 op_count: u32,
487 ) -> TranslateContext<'a> {
488 TranslateContext {
489 modal,
490 builtins: b,
491 pending_count,
492 op_count,
493 recording_macro: false,
494 active_buffer: BufferKind::Document,
495 completion_open: false,
496 chord_capture: false,
497 picker_open: false,
498 insert_completion_open: false,
499 snippet_active: false,
500 keymap: test_keymap(),
501 terminal_insert_active: false,
502 terminal_esc_exits: false,
503 terminal_app_cursor_keys: false,
504 terminal_insert_exit_pending: false,
505 terminal_visual_active: false,
506 partial_chord: partial,
507 active_minor_modes: &[],
508 }
509 }
510
511 fn ctx_chord_capture<'a>(b: &'a Builtins) -> TranslateContext<'a> {
512 TranslateContext {
513 modal: ModalState::Command,
514 builtins: b,
515 pending_count: 0,
516 op_count: 0,
517 recording_macro: false,
518 active_buffer: BufferKind::Document,
519 completion_open: false,
520 chord_capture: true,
521 picker_open: false,
522 insert_completion_open: false,
523 snippet_active: false,
524 keymap: test_keymap(),
525 terminal_insert_active: false,
526 terminal_esc_exits: false,
527 terminal_app_cursor_keys: false,
528 terminal_insert_exit_pending: false,
529 terminal_visual_active: false,
530 partial_chord: &[],
531 active_minor_modes: &[],
532 }
533 }
534
535 fn invocation_command(action: &Action) -> Option<lattice_protocol::ids::CommandId> {
536 if let Action::Invoke(inv) = action {
537 Some(inv.command)
538 } else {
539 None
540 }
541 }
542
543 #[test]
546 fn ctrl_c_in_normal_mode_is_unbound() {
547 let (_, b) = fixture();
550 let action = translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('c')));
551 assert!(
552 matches!(action, Action::None),
553 "Normal-mode <C-c> must be unbound at Builtin layer; got {action:?}"
554 );
555 }
556
557 #[test]
558 fn ctrl_c_in_insert_mode_does_not_quit() {
559 let (_, b) = fixture();
560 let action = translate(ctx(ModalState::Insert, &b), ctrl(KeyCode::Char('c')));
561 assert!(
562 !matches!(action, Action::Quit),
563 "Insert-mode <C-c> must not quit — no universal hatch"
564 );
565 }
566
567 #[test]
568 fn ctrl_c_in_command_mode_does_not_quit() {
569 let (_, b) = fixture();
572 let action = translate(ctx(ModalState::Command, &b), ctrl(KeyCode::Char('c')));
573 assert!(
574 !matches!(action, Action::Quit),
575 "Command-mode <C-c> must not quit — no universal hatch"
576 );
577 }
578
579 #[test]
582 fn hjkl_invoke_corresponding_motions() {
583 let (_, b) = fixture();
584 let cases = [
585 (KeyCode::Char('h'), b.char_left.0),
586 (KeyCode::Char('j'), b.line_down.0),
587 (KeyCode::Char('k'), b.line_up.0),
588 (KeyCode::Char('l'), b.char_right.0),
589 ];
590 for (code, expected) in cases {
591 let action = translate(ctx(ModalState::Normal, &b), key(code));
592 assert_eq!(invocation_command(&action), Some(expected));
593 }
594 }
595
596 #[test]
597 fn arrows_alias_hjkl() {
598 let (_, b) = fixture();
599 let cases = [
600 (KeyCode::Left, b.char_left.0),
601 (KeyCode::Down, b.line_down.0),
602 (KeyCode::Up, b.line_up.0),
603 (KeyCode::Right, b.char_right.0),
604 ];
605 for (code, expected) in cases {
606 let action = translate(ctx(ModalState::Normal, &b), key(code));
607 assert_eq!(invocation_command(&action), Some(expected));
608 }
609 }
610
611 #[test]
612 fn zero_and_dollar_invoke_line_start_and_end() {
613 let (_, b) = fixture();
614 assert_eq!(
615 invocation_command(&translate(
616 ctx(ModalState::Normal, &b),
617 key(KeyCode::Char('0'))
618 )),
619 Some(b.line_start.0)
620 );
621 assert_eq!(
622 invocation_command(&translate(
623 ctx(ModalState::Normal, &b),
624 key(KeyCode::Char('$'))
625 )),
626 Some(b.line_end.0)
627 );
628 }
629
630 #[test]
631 fn capital_g_invokes_goto_last_line() {
632 let (_, b) = fixture();
633 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('G')));
634 assert_eq!(invocation_command(&action), Some(b.goto_last_line.0));
635 }
636
637 #[test]
638 fn first_g_absorbs_partial_chord() {
639 let (_, b) = fixture();
645 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('g')));
646 assert!(matches!(
647 action,
648 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('g')
649 ));
650 }
651
652 #[test]
653 fn second_g_with_pending_resolves_to_goto_first_line() {
654 let (_, b) = fixture();
655 let action = translate(
656 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
657 key(KeyCode::Char('g')),
658 );
659 assert_eq!(invocation_command(&action), Some(b.goto_first_line.0));
660 }
661
662 #[test]
663 fn unrelated_key_after_pending_g_clears_pending() {
664 let (_, b) = fixture();
665 assert!(matches!(
666 translate(
667 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
668 key(KeyCode::Char('z'))
669 ),
670 Action::None
671 ));
672 }
673
674 #[test]
677 fn i_enters_insert_mode() {
678 let (_, b) = fixture();
679 let a = shared_actions();
680 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('i'))) {
681 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_mode_insert),
682 other => panic!("expected Invoke(enter_mode_insert), got {other:?}"),
683 }
684 }
685
686 #[test]
687 fn a_enters_append_mode() {
688 let (_, b) = fixture();
689 let a = shared_actions();
690 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('a'))) {
691 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_append),
692 other => panic!("expected Invoke(enter_append), got {other:?}"),
693 }
694 }
695
696 #[test]
697 fn o_opens_line_below() {
698 let (_, b) = fixture();
699 let a = shared_actions();
700 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('o'))) {
705 Action::Invoke(inv) => assert_eq!(inv.command, a.open_line_below),
706 other => panic!("expected Invoke(open_line_below), got {other:?}"),
707 }
708 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('O'))) {
709 Action::Invoke(inv) => assert_eq!(inv.command, a.open_line_above),
710 other => panic!("expected Invoke(open_line_above), got {other:?}"),
711 }
712 }
713
714 #[test]
717 fn d_invokes_absorb_operator_delete() {
718 let (_, b) = fixture();
726 let a = shared_actions();
727 let _ = b;
728 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('d')));
729 match action {
730 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_delete),
731 _ => panic!("expected Invoke(absorb_operator_delete)"),
732 }
733 }
734
735 #[test]
736 fn dw_resolves_to_delete_with_word_forward_target() {
737 let (_, b) = fixture();
738 let action = translate(
739 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
740 key(KeyCode::Char('w')),
741 );
742 match action {
743 Action::Invoke(inv) => {
744 assert_eq!(inv.command, b.delete.0);
745 match inv.target {
746 Some(Target::Motion(id, _)) => assert_eq!(id, b.word_forward),
747 other => panic!("expected motion target, got {other:?}"),
748 }
749 }
750 _ => panic!("expected Invoke"),
751 }
752 }
753
754 #[test]
755 fn dd_resolves_to_delete_current_line() {
756 let (_, b) = fixture();
757 let action = translate(
758 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
759 key(KeyCode::Char('d')),
760 );
761 match action {
762 Action::Invoke(inv) => {
763 assert_eq!(inv.command, b.delete.0);
764 assert_eq!(inv.range, Some(lattice_grammar::Range::CurrentLine));
765 }
766 _ => panic!("expected Invoke"),
767 }
768 }
769
770 #[test]
771 fn esc_after_operator_cancels_pending() {
772 let (_, b) = fixture();
773 assert!(matches!(
774 translate(
775 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
776 key(KeyCode::Esc)
777 ),
778 Action::None
779 ));
780 }
781
782 #[test]
783 fn x_resolves_directly_to_delete_char_right() {
784 let (_, b) = fixture();
785 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('x')));
786 match action {
787 Action::Invoke(inv) => {
788 assert_eq!(inv.command, b.delete.0);
789 match inv.target {
790 Some(Target::Motion(id, _)) => assert_eq!(id, b.char_right),
791 other => panic!("expected motion target, got {other:?}"),
792 }
793 }
794 _ => panic!("expected Invoke"),
795 }
796 }
797
798 #[test]
801 fn esc_in_insert_returns_to_normal() {
802 let (_, b) = fixture();
803 let a = shared_actions();
804 match translate(ctx(ModalState::Insert, &b), key(KeyCode::Esc)) {
805 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_mode_normal),
806 other => panic!("expected Invoke(enter_mode_normal), got {other:?}"),
807 }
808 }
809
810 #[test]
811 fn printable_char_in_insert_inserts_text() {
812 let (_, b) = fixture();
813 match translate(ctx(ModalState::Insert, &b), key(KeyCode::Char('h'))) {
814 Action::Insert(s) => assert_eq!(s, "h"),
815 _ => panic!("expected Insert"),
816 }
817 }
818
819 #[test]
820 fn enter_in_insert_inserts_newline() {
821 let (_, b) = fixture();
822 let a = shared_actions();
823 match translate(ctx(ModalState::Insert, &b), key(KeyCode::Enter)) {
824 Action::Invoke(inv) => assert_eq!(inv.command, a.insert_newline),
825 other => panic!("expected Invoke(insert_newline), got {other:?}"),
826 }
827 }
828
829 #[test]
830 fn backspace_in_insert_deletes_char_backward() {
831 let (_, b) = fixture();
832 let a = shared_actions();
833 match translate(ctx(ModalState::Insert, &b), key(KeyCode::Backspace)) {
834 Action::Invoke(inv) => assert_eq!(inv.command, a.delete_char_backward),
835 other => panic!("expected Invoke(delete_char_backward), got {other:?}"),
836 }
837 }
838
839 #[test]
842 fn u_in_normal_undoes() {
843 let (_, b) = fixture();
844 let a = shared_actions();
845 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('u'))) {
846 Action::Invoke(inv) => assert_eq!(inv.command, a.undo),
847 other => panic!("expected Invoke(undo), got {other:?}"),
848 }
849 }
850
851 #[test]
852 fn ctrl_r_in_normal_redoes() {
853 let (_, b) = fixture();
854 let a = shared_actions();
855 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('r'))) {
856 Action::Invoke(inv) => assert_eq!(inv.command, a.redo),
857 other => panic!("expected Invoke(redo), got {other:?}"),
858 }
859 }
860
861 #[test]
864 fn colon_in_normal_enters_command_line() {
865 let (_, b) = fixture();
866 let a = shared_actions();
867 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char(':'))) {
868 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_command_line),
869 other => panic!("expected Invoke(enter_command_line), got {other:?}"),
870 }
871 }
872
873 #[test]
874 fn printable_char_in_command_routes_through_insert_dispatcher() {
875 let (_, b) = fixture();
880 match translate(ctx(ModalState::Command, &b), key(KeyCode::Char('w'))) {
881 Action::Insert(s) => assert_eq!(s, "w"),
882 other => panic!("expected Insert(\"w\"), got {other:?}"),
883 }
884 }
885
886 #[test]
896 fn ctrl_c_in_command_mode_is_regular_key() {
897 let (_, b) = fixture();
899 let action = translate(ctx(ModalState::Command, &b), ctrl(KeyCode::Char('c')));
900 assert!(!matches!(action, Action::Quit));
901 }
902
903 #[test]
906 fn slash_in_normal_enters_forward_search() {
907 let (_, b) = fixture();
908 let a = shared_actions();
909 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('/'))) {
910 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_search_forward),
911 other => panic!("expected Invoke(enter_search_forward), got {other:?}"),
912 }
913 }
914
915 #[test]
916 fn question_in_normal_enters_backward_search() {
917 let (_, b) = fixture();
918 let a = shared_actions();
919 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('?'))) {
920 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_search_backward),
921 other => panic!("expected Invoke(enter_search_backward), got {other:?}"),
922 }
923 }
924
925 #[test]
926 fn n_in_normal_repeats_search_forward() {
927 let (_, b) = fixture();
929 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('n'))) {
930 Action::Invoke(inv) => assert_eq!(inv.command, b.search_next.0),
931 other => panic!("expected Invoke(search_next motion), got {other:?}"),
932 }
933 }
934
935 #[test]
936 fn capital_n_in_normal_repeats_search_reverse() {
937 let (_, b) = fixture();
939 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('N'))) {
940 Action::Invoke(inv) => assert_eq!(inv.command, b.search_prev.0),
941 other => panic!("expected Invoke(search_prev motion), got {other:?}"),
942 }
943 }
944
945 #[test]
946 fn printable_char_in_search_appends_to_pattern() {
947 let (_, b) = fixture();
948 let modal = ModalState::Search(SearchDirection::Forward);
949 match translate(ctx(modal, &b), key(KeyCode::Char('f'))) {
953 Action::Insert(s) => assert_eq!(s, "f"),
954 other => panic!("expected Insert(\"f\"), got {other:?}"),
955 }
956 }
957
958 #[test]
959 fn enter_in_search_submits() {
960 let (_, b) = fixture();
961 let modal = ModalState::Search(SearchDirection::Forward);
962 assert!(matches!(
971 translate(ctx(modal, &b), key(KeyCode::Enter)),
972 Action::Invoke(_)
973 ));
974 }
975
976 #[test]
977 fn esc_in_search_cancels() {
978 let (_, b) = fixture();
979 let modal = ModalState::Search(SearchDirection::Backward);
980 assert!(matches!(
983 translate(ctx(modal, &b), key(KeyCode::Esc)),
984 Action::Invoke(_)
985 ));
986 }
987
988 #[test]
989 fn backspace_in_search_pops_pattern() {
990 let (_, b) = fixture();
991 let modal = ModalState::Search(SearchDirection::Forward);
992 assert!(matches!(
997 translate(ctx(modal, &b), key(KeyCode::Backspace)),
998 Action::Invoke(_)
999 ));
1000 }
1001
1002 #[test]
1003 fn ctrl_c_in_search_does_not_quit() {
1004 let (_, b) = fixture();
1006 let modal = ModalState::Search(SearchDirection::Forward);
1007 let action = translate(ctx(modal, &b), ctrl(KeyCode::Char('c')));
1008 assert!(!matches!(action, Action::Quit));
1009 }
1010
1011 #[test]
1014 fn capital_w_invokes_big_word_forward() {
1015 let (_, b) = fixture();
1016 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('W')));
1017 assert_eq!(invocation_command(&action), Some(b.big_word_forward.0));
1018 }
1019
1020 #[test]
1021 fn capital_b_invokes_big_word_backward() {
1022 let (_, b) = fixture();
1023 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('B')));
1024 assert_eq!(invocation_command(&action), Some(b.big_word_backward.0));
1025 }
1026
1027 #[test]
1028 fn capital_e_invokes_big_word_end() {
1029 let (_, b) = fixture();
1030 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('E')));
1031 assert_eq!(invocation_command(&action), Some(b.big_word_end.0));
1032 }
1033
1034 #[test]
1035 fn capital_d_invokes_delete_to_line_end() {
1036 let (_, b) = fixture();
1037 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('D')));
1038 match action {
1039 Action::Invoke(inv) => {
1040 assert_eq!(inv.command, b.delete.0);
1041 match inv.target {
1042 Some(Target::Motion(id, _)) => assert_eq!(id, b.line_end),
1043 other => panic!("expected line_end target, got {other:?}"),
1044 }
1045 }
1046 other => panic!("expected Invoke, got {other:?}"),
1047 }
1048 }
1049
1050 #[test]
1051 fn capital_c_invokes_change_to_line_end() {
1052 let (_, b) = fixture();
1053 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('C')));
1054 match action {
1055 Action::Invoke(inv) => {
1056 assert_eq!(inv.command, b.change.0);
1057 match inv.target {
1058 Some(Target::Motion(id, _)) => assert_eq!(id, b.line_end),
1059 other => panic!("expected line_end target, got {other:?}"),
1060 }
1061 }
1062 other => panic!("expected Invoke, got {other:?}"),
1063 }
1064 }
1065
1066 #[test]
1067 fn capital_s_invokes_change_current_line() {
1068 let (_, b) = fixture();
1069 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('S')));
1070 match action {
1071 Action::Invoke(inv) => {
1072 assert_eq!(inv.command, b.change.0);
1073 assert_eq!(inv.range, Some(lattice_grammar::Range::CurrentLine));
1074 }
1075 other => panic!("expected Invoke, got {other:?}"),
1076 }
1077 }
1078
1079 #[test]
1080 fn capital_j_emits_join_with_space() {
1081 let (_, b) = fixture();
1082 let a = shared_actions();
1083 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('J'))) {
1084 Action::Invoke(inv) => assert_eq!(inv.command, a.join_lines_with_space),
1085 other => panic!("expected Invoke(join_lines_with_space), got {other:?}"),
1086 }
1087 }
1088
1089 #[test]
1090 fn gj_after_g_emits_join_without_space() {
1091 let (_, b) = fixture();
1092 let a = shared_actions();
1093 match translate(
1094 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
1095 key(KeyCode::Char('J')),
1096 ) {
1097 Action::Invoke(inv) => assert_eq!(inv.command, a.join_lines_bare),
1098 other => panic!("expected Invoke(join_lines_bare), got {other:?}"),
1099 }
1100 }
1101
1102 #[test]
1108 fn semicolon_emits_the_find_repeat_motion() {
1109 let (_, b) = fixture();
1110 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char(';'))) {
1111 Action::Invoke(inv) => assert_eq!(inv.command, b.find_repeat.0),
1112 other => panic!("expected Invoke(find_repeat), got {other:?}"),
1113 }
1114 }
1115
1116 #[test]
1119 fn comma_emits_the_find_repeat_reverse_motion() {
1120 let (_, b) = fixture();
1121 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char(','))) {
1122 Action::Invoke(inv) => assert_eq!(inv.command, b.find_repeat_reverse.0),
1123 other => panic!("expected Invoke(find_repeat_reverse), got {other:?}"),
1124 }
1125 }
1126
1127 #[test]
1128 fn d_capital_w_resolves_to_delete_big_word_forward() {
1129 let (_, b) = fixture();
1130 let action = translate(
1131 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
1132 key(KeyCode::Char('W')),
1133 );
1134 match action {
1135 Action::Invoke(inv) => match inv.target {
1136 Some(Target::Motion(id, _)) => assert_eq!(id, b.big_word_forward),
1137 other => panic!("expected motion target, got {other:?}"),
1138 },
1139 _ => panic!("expected Invoke"),
1140 }
1141 }
1142
1143 #[test]
1146 fn q_in_normal_when_not_recording_absorbs_partial_chord() {
1147 let (_, b) = fixture();
1149 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('q')));
1150 assert!(matches!(
1151 action,
1152 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('q')
1153 ));
1154 }
1155
1156 #[test]
1157 fn q_in_normal_while_recording_stops() {
1158 let (_, b) = fixture();
1159 assert!(matches!(
1160 translate(
1161 ctx_recording(ModalState::Normal, &b),
1162 key(KeyCode::Char('q'))
1163 ),
1164 Action::StopMacroRecord
1165 ));
1166 }
1167
1168 #[test]
1169 fn at_in_normal_absorbs_partial_chord() {
1170 let (_, b) = fixture();
1172 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('@')));
1173 assert!(matches!(
1174 action,
1175 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('@')
1176 ));
1177 }
1178
1179 #[test]
1180 fn letter_after_q_starts_recording() {
1181 let (_, b) = fixture();
1182 let a = shared_actions();
1183 match translate(
1184 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('q')], &b),
1185 key(KeyCode::Char('a')),
1186 ) {
1187 Action::Invoke(inv) => {
1188 assert_eq!(inv.command, a.start_macro_record);
1189 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
1190 }
1191 other => panic!("expected Invoke(start_macro_record, Char('a')), got {other:?}"),
1192 }
1193 }
1194
1195 #[test]
1196 fn letter_after_at_plays_macro() {
1197 let (_, b) = fixture();
1198 let a = shared_actions();
1199 match translate(
1200 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('@')], &b),
1201 key(KeyCode::Char('q')),
1202 ) {
1203 Action::Invoke(inv) => {
1204 assert_eq!(inv.command, a.play_macro);
1205 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('q')));
1206 }
1207 other => panic!("expected Invoke(play_macro, Char('q')), got {other:?}"),
1208 }
1209 }
1210
1211 #[test]
1212 fn at_at_plays_last_macro() {
1213 let (_, b) = fixture();
1216 let a = shared_actions();
1217 match translate(
1218 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('@')], &b),
1219 key(KeyCode::Char('@')),
1220 ) {
1221 Action::Invoke(inv) => {
1222 assert_eq!(inv.command, a.play_macro);
1223 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('@')));
1224 }
1225 other => panic!("expected Invoke(play_macro, Char('@')), got {other:?}"),
1226 }
1227 }
1228
1229 #[test]
1230 fn esc_after_macro_pending_clears() {
1231 let (_, b) = fixture();
1232 assert!(matches!(
1233 translate(
1234 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('q')], &b),
1235 key(KeyCode::Esc)
1236 ),
1237 Action::None
1238 ));
1239 assert!(matches!(
1240 translate(
1241 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('@')], &b),
1242 key(KeyCode::Esc)
1243 ),
1244 Action::None
1245 ));
1246 }
1247
1248 #[test]
1255 fn zf_after_z_is_the_fold_operator_prefix() {
1256 let (_, b) = fixture();
1257 match translate(
1258 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
1259 key(KeyCode::Char('f')),
1260 ) {
1261 Action::AbsorbPartialChord(c) => assert_eq!(c, crate::chord::KeyChord::char('f')),
1262 other => panic!("expected `f` absorbed as the `zf` operator prefix, got {other:?}"),
1263 }
1264 match translate(
1265 ctx_partial(
1266 ModalState::Normal,
1267 &[
1268 crate::chord::KeyChord::char('z'),
1269 crate::chord::KeyChord::char('f'),
1270 ],
1271 &b,
1272 ),
1273 key(KeyCode::Char('j')),
1274 ) {
1275 Action::Invoke(inv) => {
1276 assert_eq!(
1277 inv.command, b.create_fold.0,
1278 "`zfj` invokes the fold operator"
1279 );
1280 assert!(inv.target.is_some(), "with `j` as its motion target");
1281 }
1282 other => panic!("expected Invoke(operator:create-fold), got {other:?}"),
1283 }
1284 }
1285
1286 #[test]
1287 fn zo_after_z_emits_open_fold() {
1288 let (_, b) = fixture();
1289 let a = shared_actions();
1290 match translate(
1291 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
1292 key(KeyCode::Char('o')),
1293 ) {
1294 Action::Invoke(inv) => assert_eq!(inv.command, a.open_fold_at_cursor),
1295 other => panic!("expected Invoke(open_fold_at_cursor), got {other:?}"),
1296 }
1297 }
1298
1299 #[test]
1300 fn zc_after_z_emits_close_fold() {
1301 let (_, b) = fixture();
1302 let a = shared_actions();
1303 match translate(
1304 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
1305 key(KeyCode::Char('c')),
1306 ) {
1307 Action::Invoke(inv) => assert_eq!(inv.command, a.close_fold_at_cursor),
1308 other => panic!("expected Invoke(close_fold_at_cursor), got {other:?}"),
1309 }
1310 }
1311
1312 #[test]
1313 fn za_after_z_emits_toggle_fold() {
1314 let (_, b) = fixture();
1315 let a = shared_actions();
1316 match translate(
1317 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
1318 key(KeyCode::Char('a')),
1319 ) {
1320 Action::Invoke(inv) => assert_eq!(inv.command, a.toggle_fold_at_cursor),
1321 other => panic!("expected Invoke(toggle_fold_at_cursor), got {other:?}"),
1322 }
1323 }
1324
1325 #[test]
1326 fn capital_z_r_after_z_opens_all() {
1327 let (_, b) = fixture();
1328 let a = shared_actions();
1329 match translate(
1330 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
1331 key(KeyCode::Char('R')),
1332 ) {
1333 Action::Invoke(inv) => assert_eq!(inv.command, a.open_all_folds),
1334 other => panic!("expected Invoke(open_all_folds), got {other:?}"),
1335 }
1336 }
1337
1338 #[test]
1339 fn capital_z_m_after_z_closes_all() {
1340 let (_, b) = fixture();
1341 let a = shared_actions();
1342 match translate(
1343 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
1344 key(KeyCode::Char('M')),
1345 ) {
1346 Action::Invoke(inv) => assert_eq!(inv.command, a.close_all_folds),
1347 other => panic!("expected Invoke(close_all_folds), got {other:?}"),
1348 }
1349 }
1350
1351 #[test]
1352 fn zd_after_z_deletes_fold() {
1353 let (_, b) = fixture();
1354 let a = shared_actions();
1355 match translate(
1356 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
1357 key(KeyCode::Char('d')),
1358 ) {
1359 Action::Invoke(inv) => assert_eq!(inv.command, a.delete_fold_at_cursor),
1360 other => panic!("expected Invoke(delete_fold_at_cursor), got {other:?}"),
1361 }
1362 }
1363
1364 #[test]
1367 fn ctrl_v_enters_blockwise_visual() {
1368 let (_, b) = fixture();
1369 let a = shared_actions();
1370 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('v'))) {
1371 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_visual_blockwise),
1372 other => panic!("expected Invoke(enter_visual_blockwise), got {other:?}"),
1373 }
1374 }
1375
1376 #[test]
1377 fn ctrl_q_is_alternate_blockwise_visual() {
1378 let (_, b) = fixture();
1383 let a = shared_actions();
1384 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('q'))) {
1385 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_visual_blockwise),
1386 other => panic!("expected Invoke(enter_visual_blockwise), got {other:?}"),
1387 }
1388 }
1389
1390 #[test]
1391 fn lowercase_q_without_ctrl_still_absorbs_macro_record_prefix() {
1392 let (_, b) = fixture();
1396 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('q'))) {
1397 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('q') => {}
1398 other => panic!("expected AbsorbPartialChord(q), got {other:?}"),
1399 }
1400 }
1401
1402 fn ctx_help_active<'a>(modal: ModalState, b: &'a Builtins) -> TranslateContext<'a> {
1412 TranslateContext {
1413 modal,
1414 builtins: b,
1415 pending_count: 0,
1416 op_count: 0,
1417 recording_macro: false,
1418 active_buffer: BufferKind::Help,
1419 completion_open: false,
1420 chord_capture: false,
1421 picker_open: false,
1422 insert_completion_open: false,
1423 snippet_active: false,
1424 keymap: test_keymap(),
1425 terminal_insert_active: false,
1426 terminal_esc_exits: false,
1427 terminal_app_cursor_keys: false,
1428 terminal_insert_exit_pending: false,
1429 terminal_visual_active: false,
1430 partial_chord: &[],
1431 active_minor_modes: &[],
1432 }
1433 }
1434
1435 fn ctx_help_active_partial<'a>(
1437 modal: ModalState,
1438 partial: &'a [crate::chord::KeyChord],
1439 b: &'a Builtins,
1440 ) -> TranslateContext<'a> {
1441 TranslateContext {
1442 modal,
1443 builtins: b,
1444 pending_count: 0,
1445 op_count: 0,
1446 recording_macro: false,
1447 active_buffer: BufferKind::Help,
1448 completion_open: false,
1449 chord_capture: false,
1450 picker_open: false,
1451 insert_completion_open: false,
1452 snippet_active: false,
1453 keymap: test_keymap(),
1454 terminal_insert_active: false,
1455 terminal_esc_exits: false,
1456 terminal_app_cursor_keys: false,
1457 terminal_insert_exit_pending: false,
1458 terminal_visual_active: false,
1459 partial_chord: partial,
1460 active_minor_modes: &[],
1461 }
1462 }
1463
1464 #[test]
1465 fn help_active_does_not_intercept_q_to_dismiss() {
1466 let (_, b) = fixture();
1467 assert!(!matches!(
1476 translate(
1477 ctx_help_active(ModalState::Normal, &b),
1478 key(KeyCode::Char('q'))
1479 ),
1480 Action::HelpDismiss
1481 ));
1482 }
1483
1484 #[test]
1485 fn help_esc_is_mode_owned_not_intercepted_by_translate() {
1486 let (_, b) = fixture();
1493 assert!(!matches!(
1494 translate(ctx_help_active(ModalState::Normal, &b), key(KeyCode::Esc)),
1495 Action::HelpDismiss
1496 ));
1497 }
1498
1499 #[test]
1500 fn help_active_routes_enter_to_follow_link() {
1501 let (_, b) = fixture();
1502 assert!(matches!(
1503 translate(ctx_help_active(ModalState::Normal, &b), key(KeyCode::Enter)),
1504 Action::FollowLink
1505 ));
1506 }
1507
1508 fn ctx_dashboard_active<'a>(modal: ModalState, b: &'a Builtins) -> TranslateContext<'a> {
1509 TranslateContext {
1510 active_buffer: BufferKind::Dashboard,
1511 ..ctx_help_active(modal, b)
1512 }
1513 }
1514
1515 #[test]
1516 fn dashboard_active_routes_enter_to_follow_link() {
1517 let (_, b) = fixture();
1521 assert!(matches!(
1522 translate(
1523 ctx_dashboard_active(ModalState::Normal, &b),
1524 key(KeyCode::Enter)
1525 ),
1526 Action::FollowLink
1527 ));
1528 }
1529
1530 #[test]
1531 fn dashboard_active_does_not_map_dash_to_oil_navigate_up() {
1532 let (_, b) = fixture();
1536 assert!(!matches!(
1537 translate(
1538 ctx_dashboard_active(ModalState::Normal, &b),
1539 key(KeyCode::Char('-'))
1540 ),
1541 Action::OilNavigateUp
1542 ));
1543 }
1544
1545 #[test]
1546 fn help_active_routes_jk_through_normal_motions() {
1547 let (_, b) = fixture();
1551 let action = translate(
1552 ctx_help_active(ModalState::Normal, &b),
1553 key(KeyCode::Char('j')),
1554 );
1555 assert_eq!(invocation_command(&action), Some(b.line_down.0));
1556 }
1557
1558 #[test]
1559 fn help_active_routes_gg_through_chord_grammar() {
1560 let (_, b) = fixture();
1567 let first = translate(
1568 ctx_help_active(ModalState::Normal, &b),
1569 key(KeyCode::Char('g')),
1570 );
1571 assert!(matches!(
1572 first,
1573 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('g')
1574 ));
1575 let second = translate(
1576 ctx_help_active_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
1577 key(KeyCode::Char('g')),
1578 );
1579 assert_eq!(invocation_command(&second), Some(b.goto_first_line.0));
1580 }
1581
1582 #[test]
1583 fn help_active_routes_capital_g_to_goto_last_line() {
1584 let (_, b) = fixture();
1585 let action = translate(
1586 ctx_help_active(ModalState::Normal, &b),
1587 key(KeyCode::Char('G')),
1588 );
1589 assert_eq!(invocation_command(&action), Some(b.goto_last_line.0));
1590 }
1591
1592 #[test]
1593 fn help_active_routes_ctrl_o_to_jump_history_back() {
1594 let (_, b) = fixture();
1598 let a = shared_actions();
1599 match translate(
1600 ctx_help_active(ModalState::Normal, &b),
1601 ctrl(KeyCode::Char('o')),
1602 ) {
1603 Action::Invoke(inv) => assert_eq!(inv.command, a.jump_history_back),
1604 other => panic!("expected Invoke(jump_history_back), got {other:?}"),
1605 }
1606 }
1607
1608 #[test]
1611 fn ctrl_w_absorbs_partial_chord() {
1612 let (_, b) = fixture();
1614 let action = translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('w')));
1615 assert!(matches!(
1616 action,
1617 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::ctrl('w')
1618 ));
1619 }
1620
1621 #[test]
1622 fn ctrl_w_l_navigates_right() {
1623 let (_, b) = fixture();
1624 let a = shared_actions();
1625 match translate(
1626 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::ctrl('w')], &b),
1627 key(KeyCode::Char('l')),
1628 ) {
1629 Action::Invoke(inv) => assert_eq!(inv.command, a.navigate_pane_right),
1630 other => panic!("expected Invoke(navigate_pane_right), got {other:?}"),
1631 }
1632 }
1633
1634 #[test]
1635 fn ctrl_w_ctrl_l_also_navigates_right() {
1636 let (_, b) = fixture();
1639 let a = shared_actions();
1640 match translate(
1641 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::ctrl('w')], &b),
1642 ctrl(KeyCode::Char('l')),
1643 ) {
1644 Action::Invoke(inv) => assert_eq!(inv.command, a.navigate_pane_right),
1645 other => panic!("expected Invoke(navigate_pane_right), got {other:?}"),
1646 }
1647 }
1648
1649 #[test]
1650 fn ctrl_w_ctrl_j_navigates_down() {
1651 let (_, b) = fixture();
1652 let a = shared_actions();
1653 match translate(
1654 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::ctrl('w')], &b),
1655 ctrl(KeyCode::Char('j')),
1656 ) {
1657 Action::Invoke(inv) => assert_eq!(inv.command, a.navigate_pane_down),
1658 other => panic!("expected Invoke(navigate_pane_down), got {other:?}"),
1659 }
1660 }
1661
1662 #[test]
1663 fn ctrl_w_w_cycles_to_next_pane() {
1664 let (_, b) = fixture();
1665 let a = shared_actions();
1666 match translate(
1667 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::ctrl('w')], &b),
1668 key(KeyCode::Char('w')),
1669 ) {
1670 Action::Invoke(inv) => assert_eq!(inv.command, a.next_pane),
1671 other => panic!("expected Invoke(next_pane), got {other:?}"),
1672 }
1673 }
1674
1675 #[test]
1676 fn ctrl_w_capital_w_cycles_to_prev_pane() {
1677 let (_, b) = fixture();
1678 let a = shared_actions();
1679 match translate(
1680 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::ctrl('w')], &b),
1681 key(KeyCode::Char('W')),
1682 ) {
1683 Action::Invoke(inv) => assert_eq!(inv.command, a.prev_pane),
1684 other => panic!("expected Invoke(prev_pane), got {other:?}"),
1685 }
1686 }
1687
1688 #[test]
1689 fn ctrl_c_in_normal_mode_under_help_is_unbound() {
1690 let (_, b) = fixture();
1692 let action = translate(
1693 ctx_help_active(ModalState::Normal, &b),
1694 ctrl(KeyCode::Char('c')),
1695 );
1696 assert!(matches!(action, Action::None));
1697 }
1698
1699 fn parse_chord_for_test(chord: &str) -> Vec<KeyEvent> {
1709 if chord.chars().count() == 1 {
1714 let c = chord.chars().next().expect("len == 1");
1715 return vec![KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)];
1716 }
1717 let mut out = Vec::new();
1718 let mut chars = chord.chars().peekable();
1719 while let Some(c) = chars.next() {
1720 if c != '<' {
1721 out.push(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
1722 continue;
1723 }
1724 let mut body = String::new();
1725 for n in chars.by_ref() {
1726 if n == '>' {
1727 break;
1728 }
1729 body.push(n);
1730 }
1731 let evt = match body.as_str() {
1732 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
1733 "CR" | "Enter" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
1734 "Tab" => KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE),
1735 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
1736 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
1737 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
1738 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
1739 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
1740 "Home" => KeyEvent::new(KeyCode::Home, KeyModifiers::NONE),
1741 "End" => KeyEvent::new(KeyCode::End, KeyModifiers::NONE),
1742 "PageUp" => KeyEvent::new(KeyCode::PageUp, KeyModifiers::NONE),
1743 "PageDown" => KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE),
1744 other => {
1745 if let Some(rest) = other.strip_prefix("S-") {
1746 let evt = match rest {
1750 "Tab" => KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE),
1751 _ => match rest.chars().next() {
1752 Some(c) => KeyEvent::new(KeyCode::Char(c), KeyModifiers::SHIFT),
1753 None => continue,
1754 },
1755 };
1756 out.push(evt);
1757 continue;
1758 }
1759 if let Some(rest) = other.strip_prefix("C-") {
1760 match rest {
1766 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::CONTROL),
1767 "PageUp" => KeyEvent::new(KeyCode::PageUp, KeyModifiers::CONTROL),
1770 "PageDown" => KeyEvent::new(KeyCode::PageDown, KeyModifiers::CONTROL),
1771 _ => match rest.chars().next() {
1772 Some(c) => KeyEvent::new(KeyCode::Char(c), KeyModifiers::CONTROL),
1773 None => continue,
1774 },
1775 }
1776 } else {
1777 continue;
1781 }
1782 }
1783 };
1784 out.push(evt);
1785 }
1786 out
1787 }
1788
1789 fn simulate_chord(
1793 chord: &str,
1794 mode: crate::keymap::BindingMode,
1795 builtins: &Builtins,
1796 ) -> Action {
1797 use crate::keymap::BindingMode;
1798 let modal = match mode {
1799 BindingMode::Visual => ModalState::Visual(lattice_grammar::VisualKind::Charwise),
1800 BindingMode::Insert | BindingMode::CompletionPopup | BindingMode::AfterCtrlX => {
1802 ModalState::Insert
1803 }
1804 BindingMode::Replace => ModalState::Replace,
1805 BindingMode::Command => ModalState::Command,
1806 BindingMode::Search => ModalState::Search(lattice_grammar::SearchDirection::Forward),
1807 _ => ModalState::Normal,
1812 };
1813 let active_buffer = if matches!(mode, BindingMode::Help) {
1814 BufferKind::Help
1815 } else {
1816 BufferKind::Document
1817 };
1818 let insert_completion_open = matches!(mode, BindingMode::CompletionPopup);
1825 let snippet_active = matches!(mode, BindingMode::Snippet);
1826 let modal = if snippet_active {
1828 ModalState::Insert
1829 } else {
1830 modal
1831 };
1832 let (keymap_for_mode, active_for_mode): (
1842 &'static KeymapHandle,
1843 &'static [lattice_mode::ModeId],
1844 ) = if insert_completion_open {
1845 (shared_keymap_with_popup(), popup_minor_modes())
1846 } else if snippet_active {
1847 (shared_keymap_with_snippet(), snippet_minor_modes())
1848 } else if matches!(mode, BindingMode::Command) {
1849 (
1850 shared_keymap_with_command_line_mode(),
1851 command_line_mode_active(),
1852 )
1853 } else {
1854 (shared_keymap_base_with_snippet_mode(), snippet_mode_only())
1860 };
1861 let mut partial_chord: Vec<crate::chord::KeyChord> = Vec::new();
1862 let mut last = Action::None;
1863 for event in parse_chord_for_test(chord) {
1864 let ctx = TranslateContext {
1865 modal,
1866 builtins,
1867 pending_count: 0,
1868 op_count: 0,
1869 recording_macro: false,
1870 active_buffer,
1871 completion_open: false,
1872 chord_capture: false,
1873 picker_open: false,
1874 insert_completion_open,
1875 snippet_active,
1876 keymap: keymap_for_mode,
1877 terminal_insert_active: false,
1878 terminal_esc_exits: false,
1879 terminal_app_cursor_keys: false,
1880 terminal_insert_exit_pending: false,
1881 terminal_visual_active: false,
1882 partial_chord: &partial_chord,
1883 active_minor_modes: active_for_mode,
1884 };
1885 last = translate(ctx, event);
1886 match &last {
1890 Action::AbsorbPartialChord(c) => partial_chord.push(*c),
1891 _ => partial_chord.clear(),
1892 }
1893 }
1894 last
1895 }
1896
1897 #[test]
1898 fn keymap_descriptors_dont_drift_from_translate() {
1899 let (_, b) = fixture();
1915 for entry in crate::keymap::default_keymap() {
1916 for &mode in entry.modes {
1918 let action = simulate_chord(entry.chord, mode, &b);
1919 assert!(
1920 !matches!(action, Action::None),
1921 "keymap descriptor `{}` ({}) doc=`{}` produced Action::None -- \
1922 binding may have been removed or moved",
1923 entry.chord,
1924 mode.label(),
1925 entry.doc,
1926 );
1927 }
1928 }
1929 }
1930
1931 #[test]
1932 fn every_catalog_command_resolves_in_registry() {
1933 let mut r = CommandRegistry::new();
1947 let b = populate(&mut r);
1948 let _ex = lattice_grammar::ex_commands::populate(&mut r);
1949 let _a = crate::actions::populate(&mut r, &b);
1950 for entry in crate::keymap::default_keymap() {
1951 let Some(name) = entry.command else {
1952 continue;
1953 };
1954 assert!(
1955 r.id_by_name(name).is_some(),
1956 "keymap descriptor `{}` ({}) names command `{}` -- \
1957 not found in CommandRegistry. Possible rename or \
1958 typo; catalog is out of sync with the registry.",
1959 entry.chord,
1960 entry.modes_label(),
1961 name,
1962 );
1963 }
1964 }
1965
1966 #[test]
1969 fn g_semicolon_after_g_walks_mark_history_back() {
1970 let (_, b) = fixture();
1971 let a = shared_actions();
1972 match translate(
1973 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
1974 key(KeyCode::Char(';')),
1975 ) {
1976 Action::Invoke(inv) => assert_eq!(inv.command, a.walk_mark_history_back),
1977 other => panic!("expected Invoke(walk_mark_history_back), got {other:?}"),
1978 }
1979 }
1980
1981 #[test]
1982 fn g_comma_after_g_walks_mark_history_forward() {
1983 let (_, b) = fixture();
1984 let a = shared_actions();
1985 match translate(
1986 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
1987 key(KeyCode::Char(',')),
1988 ) {
1989 Action::Invoke(inv) => assert_eq!(inv.command, a.walk_mark_history_forward),
1990 other => panic!("expected Invoke(walk_mark_history_forward), got {other:?}"),
1991 }
1992 }
1993
1994 #[test]
1999 fn gd_without_lsp_mode_is_unresolved() {
2000 let (_, b) = fixture();
2001 assert!(matches!(
2002 translate(
2003 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
2004 key(KeyCode::Char('d')),
2005 ),
2006 Action::None
2007 ));
2008 }
2009
2010 #[test]
2011 fn g_d_without_lsp_mode_is_unresolved() {
2012 let (_, b) = fixture();
2013 assert!(matches!(
2014 translate(
2015 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
2016 key(KeyCode::Char('D')),
2017 ),
2018 Action::None
2019 ));
2020 }
2021
2022 #[test]
2023 fn gy_without_lsp_mode_is_unresolved() {
2024 let (_, b) = fixture();
2025 assert!(matches!(
2026 translate(
2027 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
2028 key(KeyCode::Char('y')),
2029 ),
2030 Action::None
2031 ));
2032 }
2033
2034 #[test]
2035 fn g_i_without_lsp_mode_is_unresolved() {
2036 let (_, b) = fixture();
2037 assert!(matches!(
2038 translate(
2039 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
2040 key(KeyCode::Char('I')),
2041 ),
2042 Action::None
2043 ));
2044 }
2045
2046 #[test]
2047 fn gr_without_lsp_mode_is_unresolved() {
2048 let (_, b) = fixture();
2049 assert!(matches!(
2050 translate(
2051 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
2052 key(KeyCode::Char('r')),
2053 ),
2054 Action::None
2055 ));
2056 }
2057
2058 fn ctx_insert_completion<'a>(b: &'a Builtins) -> TranslateContext<'a> {
2061 TranslateContext {
2062 modal: ModalState::Insert,
2063 builtins: b,
2064 pending_count: 0,
2065 op_count: 0,
2066 recording_macro: false,
2067 active_buffer: BufferKind::Document,
2068 completion_open: false,
2069 chord_capture: false,
2070 picker_open: false,
2071 insert_completion_open: true,
2072 snippet_active: false,
2073 keymap: shared_keymap_with_popup(),
2079 terminal_insert_active: false,
2080 terminal_esc_exits: false,
2081 terminal_app_cursor_keys: false,
2082 terminal_insert_exit_pending: false,
2083 terminal_visual_active: false,
2084 partial_chord: &[],
2085 active_minor_modes: popup_minor_modes(),
2087 }
2088 }
2089
2090 #[test]
2091 fn ctrl_space_in_insert_triggers_completion() {
2092 let (_, b) = fixture();
2093 let a = shared_actions();
2094 match translate(ctx(ModalState::Insert, &b), ctrl(KeyCode::Char(' '))) {
2095 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_trigger),
2096 other => panic!("expected Invoke(completion_trigger), got {other:?}"),
2097 }
2098 }
2099
2100 #[test]
2101 fn ctrl_x_in_insert_absorbs_partial_chord() {
2102 let (_, b) = fixture();
2108 let mut cx = ctx(ModalState::Insert, &b);
2109 cx.keymap = shared_keymap_with_snippet();
2110 cx.active_minor_modes = snippet_minor_modes();
2111 let action = translate(cx, ctrl(KeyCode::Char('x')));
2112 assert!(matches!(
2113 action,
2114 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::ctrl('x')
2115 ));
2116 }
2117
2118 #[test]
2119 fn ctrl_x_ctrl_o_no_longer_invokes() {
2120 let (_, b) = fixture();
2125 let r = translate(
2126 ctx_partial(ModalState::Insert, &[crate::chord::KeyChord::ctrl('x')], &b),
2127 ctrl(KeyCode::Char('o')),
2128 );
2129 assert!(
2130 !matches!(r, Action::Invoke(_)),
2131 "<C-x><C-o> should no longer resolve to an Invoke; got {r:?}",
2132 );
2133 }
2134
2135 #[test]
2136 fn ctrl_x_followed_by_unrecognised_clears_partial_chord() {
2137 let (_, b) = fixture();
2142 assert!(matches!(
2143 translate(
2144 ctx_partial(ModalState::Insert, &[crate::chord::KeyChord::ctrl('x')], &b,),
2145 ctrl(KeyCode::Char('z'))
2146 ),
2147 Action::None
2148 ));
2149 }
2150
2151 #[test]
2152 fn popup_open_ctrl_n_navigates_next() {
2153 let (_, b) = fixture();
2154 let a = shared_actions();
2155 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char('n')));
2156 match r {
2157 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_next),
2158 other => panic!("expected Invoke(completion_next), got {other:?}"),
2159 }
2160 }
2161
2162 #[test]
2163 fn popup_open_ctrl_p_navigates_prev() {
2164 let (_, b) = fixture();
2165 let a = shared_actions();
2166 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char('p')));
2167 match r {
2168 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_prev),
2169 other => panic!("expected Invoke(completion_prev), got {other:?}"),
2170 }
2171 }
2172
2173 #[test]
2174 fn popup_open_tab_accepts() {
2175 let (_, b) = fixture();
2176 let a = shared_actions();
2177 let r = translate(ctx_insert_completion(&b), key(KeyCode::Tab));
2178 match r {
2179 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_accept),
2180 other => panic!("expected Invoke(completion_accept), got {other:?}"),
2181 }
2182 }
2183
2184 #[test]
2185 fn popup_open_ctrl_y_accepts() {
2186 let (_, b) = fixture();
2187 let a = shared_actions();
2188 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char('y')));
2189 match r {
2190 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_accept),
2191 other => panic!("expected Invoke(completion_accept), got {other:?}"),
2192 }
2193 }
2194
2195 #[test]
2196 fn popup_open_enter_accepts() {
2197 let (_, b) = fixture();
2198 let a = shared_actions();
2199 let r = translate(ctx_insert_completion(&b), key(KeyCode::Enter));
2200 match r {
2201 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_accept),
2202 other => panic!("expected Invoke(completion_accept), got {other:?}"),
2203 }
2204 }
2205
2206 #[test]
2207 fn popup_open_ctrl_e_cancels_keeps_insert() {
2208 let (_, b) = fixture();
2209 let a = shared_actions();
2210 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char('e')));
2211 match r {
2212 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_cancel),
2213 other => panic!("expected Invoke(completion_cancel), got {other:?}"),
2214 }
2215 }
2216
2217 #[test]
2218 fn popup_open_esc_cancels_and_exits_insert() {
2219 let (_, b) = fixture();
2220 let a = shared_actions();
2221 let r = translate(ctx_insert_completion(&b), key(KeyCode::Esc));
2222 match r {
2223 Action::Invoke(inv) => {
2224 assert_eq!(inv.command, a.completion_cancel_and_exit_insert)
2225 }
2226 other => panic!("expected Invoke(completion_cancel_and_exit_insert), got {other:?}"),
2227 }
2228 }
2229
2230 #[test]
2231 fn popup_open_ctrl_d_toggles_docs_only_inside_minor_mode() {
2232 let (_, b) = fixture();
2233 let a = shared_actions();
2234 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char('d')));
2236 match r {
2237 Action::Invoke(inv) => {
2238 assert_eq!(inv.command, a.completion_toggle_docs)
2239 }
2240 other => panic!("expected Invoke(completion_toggle_docs), got {other:?}"),
2241 }
2242 let half_down = translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('d')));
2247 if let Action::Invoke(inv) = half_down {
2248 assert_ne!(inv.command, a.completion_toggle_docs);
2249 }
2250 }
2251
2252 #[test]
2256 fn popup_open_ctrl_f_filters_to_path() {
2257 use lattice_grammar::args::Args;
2258 let (_, b) = fixture();
2259 let a = shared_actions();
2260 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char('f')));
2261 match r {
2262 Action::Invoke(inv) => {
2263 assert_eq!(inv.command, a.completion_filter_to_source);
2264 match inv.args {
2265 Args::String(s) => {
2266 assert_eq!(s, lattice_completion::insert::PATH_SOURCE_ID)
2267 }
2268 other => panic!("expected Args::String, got {other:?}"),
2269 }
2270 }
2271 other => {
2272 panic!("expected Invoke(completion_filter_to_source, \"gen:path\"), got {other:?}")
2273 }
2274 }
2275 }
2276
2277 #[test]
2280 fn popup_open_ctrl_b_filters_to_buffer_words() {
2281 use lattice_grammar::args::Args;
2282 let (_, b) = fixture();
2283 let a = shared_actions();
2284 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char('b')));
2285 match r {
2286 Action::Invoke(inv) => {
2287 assert_eq!(inv.command, a.completion_filter_to_source);
2288 match inv.args {
2289 Args::String(s) => {
2290 assert_eq!(s, lattice_completion::insert::BufferWordsSource::ID)
2291 }
2292 other => panic!("expected Args::String, got {other:?}"),
2293 }
2294 }
2295 other => panic!(
2296 "expected Invoke(completion_filter_to_source, \"gen:buffer-words\"), got {other:?}"
2297 ),
2298 }
2299 }
2300
2301 #[test]
2305 fn popup_open_ctrl_space_clears_filter() {
2306 let (_, b) = fixture();
2307 let a = shared_actions();
2308 let r = translate(ctx_insert_completion(&b), ctrl(KeyCode::Char(' ')));
2309 match r {
2310 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_filter_clear),
2311 other => panic!("expected Invoke(completion_filter_clear), got {other:?}"),
2312 }
2313 }
2314
2315 fn ctx_snippet_active<'a>(b: &'a Builtins) -> TranslateContext<'a> {
2318 TranslateContext {
2319 modal: ModalState::Insert,
2320 builtins: b,
2321 pending_count: 0,
2322 op_count: 0,
2323 recording_macro: false,
2324 active_buffer: BufferKind::Document,
2325 completion_open: false,
2326 chord_capture: false,
2327 picker_open: false,
2328 insert_completion_open: false,
2329 snippet_active: true,
2330 keymap: shared_keymap_with_snippet(),
2334 terminal_insert_active: false,
2335 terminal_esc_exits: false,
2336 terminal_app_cursor_keys: false,
2337 terminal_insert_exit_pending: false,
2338 terminal_visual_active: false,
2339 partial_chord: &[],
2340 active_minor_modes: snippet_minor_modes(),
2342 }
2343 }
2344
2345 #[test]
2346 fn snippet_active_tab_jumps_to_next_placeholder() {
2347 let (_, b) = fixture();
2348 let a = shared_actions();
2349 let r = translate(ctx_snippet_active(&b), key(KeyCode::Tab));
2350 match r {
2351 Action::Invoke(inv) => {
2352 assert_eq!(inv.command, a.snippet_next_placeholder)
2353 }
2354 other => panic!("expected Invoke(snippet_next_placeholder), got {other:?}"),
2355 }
2356 }
2357
2358 #[test]
2359 fn snippet_active_back_tab_jumps_to_prev_placeholder() {
2360 let (_, b) = fixture();
2361 let a = shared_actions();
2362 let r = translate(ctx_snippet_active(&b), key(KeyCode::BackTab));
2363 match r {
2364 Action::Invoke(inv) => {
2365 assert_eq!(inv.command, a.snippet_prev_placeholder)
2366 }
2367 other => panic!("expected Invoke(snippet_prev_placeholder), got {other:?}"),
2368 }
2369 }
2370
2371 #[test]
2372 fn snippet_active_esc_leaves_snippet_then_falls_through() {
2373 let (_, b) = fixture();
2377 let a = shared_actions();
2378 let r = translate(ctx_snippet_active(&b), key(KeyCode::Esc));
2379 match r {
2380 Action::Chain(v) => match (v.first(), v.get(1)) {
2381 (Some(Action::Invoke(leave)), Some(Action::Invoke(native))) => {
2382 assert_eq!(leave.command, a.snippet_leave);
2383 assert_eq!(native.command, a.enter_mode_normal);
2384 assert_eq!(v.len(), 2);
2385 }
2386 _ => panic!("expected Chain([snippet_leave, enter_mode_normal]), got {v:?}"),
2387 },
2388 other => panic!("expected Chain, got {other:?}"),
2389 }
2390 }
2391
2392 #[test]
2393 fn snippet_active_other_keys_fall_through_to_insert() {
2394 let (_, b) = fixture();
2395 let action = translate(ctx_snippet_active(&b), key(KeyCode::Char('x')));
2399 assert!(matches!(action, Action::Insert(s) if s == "x"));
2400 }
2401
2402 #[test]
2403 fn snippet_active_yields_to_completion_popup() {
2404 let (_, b) = fixture();
2414 let a = shared_actions();
2415 let mut ctx = ctx_snippet_active(&b);
2416 ctx.insert_completion_open = true;
2417 ctx.keymap = shared_keymap_with_both_overlays();
2418 ctx.active_minor_modes = both_minor_modes();
2419 let action = translate(ctx, key(KeyCode::Tab));
2420 match action {
2421 Action::Invoke(inv) => assert_eq!(inv.command, a.completion_accept),
2422 other => panic!("expected Invoke(completion_accept), got {other:?}"),
2423 }
2424 }
2425
2426 #[test]
2427 fn snippet_active_only_in_insert_mode() {
2428 let (_, b) = fixture();
2433 let a = shared_actions();
2434 let mut ctx = ctx_snippet_active(&b);
2435 ctx.modal = ModalState::Normal;
2436 let action = translate(ctx, key(KeyCode::Tab));
2437 if let Action::Invoke(inv) = action {
2440 assert_ne!(inv.command, a.snippet_next_placeholder);
2441 }
2442 }
2443
2444 #[test]
2445 fn ctrl_x_ctrl_s_resolves_to_snippet_expand() {
2446 let (_, b) = fixture();
2450 let a = shared_actions();
2451 let partial = [crate::chord::KeyChord::ctrl('x')];
2452 let mut cx = ctx_partial(ModalState::Insert, &partial, &b);
2453 cx.keymap = shared_keymap_with_snippet();
2454 cx.active_minor_modes = snippet_minor_modes();
2455 match translate(cx, ctrl(KeyCode::Char('s'))) {
2456 Action::Invoke(inv) => assert_eq!(inv.command, a.snippet_expand),
2457 other => panic!("expected Invoke(snippet_expand), got {other:?}"),
2458 }
2459 }
2460
2461 #[test]
2464 fn ctrl_o_emits_jump_history_back() {
2465 let (_, b) = fixture();
2466 let a = shared_actions();
2467 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('o'))) {
2468 Action::Invoke(inv) => assert_eq!(inv.command, a.jump_history_back),
2469 other => panic!("expected Invoke(jump_history_back), got {other:?}"),
2470 }
2471 }
2472
2473 #[test]
2474 fn ctrl_i_emits_jump_history_forward() {
2475 let (_, b) = fixture();
2476 let a = shared_actions();
2477 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('i'))) {
2478 Action::Invoke(inv) => assert_eq!(inv.command, a.jump_history_forward),
2479 other => panic!("expected Invoke(jump_history_forward), got {other:?}"),
2480 }
2481 }
2482
2483 #[test]
2484 fn ctrl_l_emits_redraw_screen() {
2485 let (_, b) = fixture();
2486 let a = shared_actions();
2487 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('l'))) {
2488 Action::Invoke(inv) => assert_eq!(inv.command, a.redraw_screen),
2489 other => panic!("expected Invoke(redraw_screen), got {other:?}"),
2490 }
2491 }
2492
2493 #[test]
2494 fn tab_in_normal_emits_jump_history_forward() {
2495 let (_, b) = fixture();
2496 let a = shared_actions();
2497 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Tab)) {
2498 Action::Invoke(inv) => assert_eq!(inv.command, a.jump_history_forward),
2499 other => panic!("expected Invoke(jump_history_forward), got {other:?}"),
2500 }
2501 }
2502
2503 #[test]
2506 fn quote_in_normal_absorbs_partial_chord() {
2507 let (_, b) = fixture();
2509 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('"')));
2510 assert!(matches!(
2511 action,
2512 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('"')
2513 ));
2514 }
2515
2516 #[test]
2517 fn lowercase_letter_after_quote_selects_named_register() {
2518 let (_, b) = fixture();
2519 let a = shared_actions();
2520 let action = translate(
2521 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('"')], &b),
2522 key(KeyCode::Char('a')),
2523 );
2524 match action {
2525 Action::Invoke(inv) => {
2526 assert_eq!(inv.command, a.select_register);
2527 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
2528 }
2529 other => panic!("expected Invoke(select_register, Char('a')), got {other:?}"),
2530 }
2531 }
2532
2533 #[test]
2534 fn digit_after_quote_selects_numbered_register() {
2535 let (_, b) = fixture();
2536 let a = shared_actions();
2537 let action = translate(
2538 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('"')], &b),
2539 key(KeyCode::Char('0')),
2540 );
2541 match action {
2542 Action::Invoke(inv) => {
2543 assert_eq!(inv.command, a.select_register);
2544 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('0')));
2545 }
2546 other => panic!("expected Invoke(select_register, Char('0')), got {other:?}"),
2547 }
2548 }
2549
2550 #[test]
2551 fn underscore_after_quote_selects_black_hole() {
2552 let (_, b) = fixture();
2553 let a = shared_actions();
2554 let action = translate(
2555 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('"')], &b),
2556 key(KeyCode::Char('_')),
2557 );
2558 match action {
2559 Action::Invoke(inv) => {
2560 assert_eq!(inv.command, a.select_register);
2561 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('_')));
2562 }
2563 other => panic!("expected Invoke(select_register, Char('_')), got {other:?}"),
2564 }
2565 }
2566
2567 #[test]
2568 fn plus_after_quote_selects_system() {
2569 let (_, b) = fixture();
2570 let a = shared_actions();
2571 let action = translate(
2572 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('"')], &b),
2573 key(KeyCode::Char('+')),
2574 );
2575 match action {
2576 Action::Invoke(inv) => {
2577 assert_eq!(inv.command, a.select_register);
2578 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('+')));
2579 }
2580 other => panic!("expected Invoke(select_register, Char('+')), got {other:?}"),
2581 }
2582 }
2583
2584 #[test]
2585 fn invalid_char_after_quote_passes_to_actionspec() {
2586 let (_, b) = fixture();
2592 let a = shared_actions();
2593 match translate(
2594 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('"')], &b),
2595 key(KeyCode::Char('@')),
2596 ) {
2597 Action::Invoke(inv) => {
2598 assert_eq!(inv.command, a.select_register);
2599 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('@')));
2600 }
2601 other => panic!("expected Invoke(select_register, Char('@')), got {other:?}"),
2602 }
2603 }
2604
2605 #[test]
2608 fn tilde_emits_toggle_case_at_cursor() {
2609 let (_, b) = fixture();
2610 let a = shared_actions();
2611 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('~'))) {
2612 Action::Invoke(inv) => assert_eq!(inv.command, a.toggle_case_at_cursor),
2613 other => panic!("expected Invoke(toggle_case_at_cursor), got {other:?}"),
2614 }
2615 }
2616
2617 #[test]
2620 fn star_emits_search_word_forward() {
2621 let (_, b) = fixture();
2623 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('*'))) {
2624 Action::Invoke(inv) => assert_eq!(inv.command, b.search_word_forward.0),
2625 other => panic!("expected Invoke(search_word_forward motion), got {other:?}"),
2626 }
2627 }
2628
2629 #[test]
2630 fn hash_emits_search_word_backward() {
2631 let (_, b) = fixture();
2633 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('#'))) {
2634 Action::Invoke(inv) => assert_eq!(inv.command, b.search_word_backward.0),
2635 other => panic!("expected Invoke(search_word_backward motion), got {other:?}"),
2636 }
2637 }
2638
2639 #[test]
2646 fn percent_emits_the_match_pair_motion() {
2647 let (_, b) = fixture();
2648 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('%'))) {
2649 Action::Invoke(inv) => assert_eq!(inv.command, b.match_pair.0),
2650 other => panic!("expected Invoke(match_pair), got {other:?}"),
2651 }
2652 }
2653
2654 #[test]
2657 fn capital_h_emits_the_viewport_top_motion() {
2658 let (_, b) = fixture();
2660 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('H')));
2661 match action {
2662 Action::Invoke(inv) => assert_eq!(inv.command, b.viewport_top.0),
2663 other => panic!("expected Invoke(viewport_top motion), got {other:?}"),
2664 }
2665 }
2666
2667 #[test]
2668 fn capital_m_emits_the_viewport_middle_motion() {
2669 let (_, b) = fixture();
2671 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('M')));
2672 match action {
2673 Action::Invoke(inv) => assert_eq!(inv.command, b.viewport_middle.0),
2674 other => panic!("expected Invoke(viewport_middle motion), got {other:?}"),
2675 }
2676 }
2677
2678 #[test]
2679 fn capital_l_emits_the_viewport_bottom_motion() {
2680 let (_, b) = fixture();
2682 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('L')));
2683 match action {
2684 Action::Invoke(inv) => assert_eq!(inv.command, b.viewport_bottom.0),
2685 other => panic!("expected Invoke(viewport_bottom motion), got {other:?}"),
2686 }
2687 }
2688
2689 #[test]
2690 fn z_absorbs_partial_chord() {
2691 let (_, b) = fixture();
2693 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('z')));
2694 assert!(matches!(
2695 action,
2696 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('z')
2697 ));
2698 }
2699
2700 #[test]
2701 fn zz_emits_scroll_cursor_center() {
2702 let (_, b) = fixture();
2703 let a = shared_actions();
2704 let action = translate(
2705 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
2706 key(KeyCode::Char('z')),
2707 );
2708 match action {
2709 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_cursor_to_center),
2710 other => panic!("expected Invoke(scroll_cursor_to_center), got {other:?}"),
2711 }
2712 }
2713
2714 #[test]
2715 fn zt_emits_scroll_cursor_top() {
2716 let (_, b) = fixture();
2717 let a = shared_actions();
2718 let action = translate(
2719 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
2720 key(KeyCode::Char('t')),
2721 );
2722 match action {
2723 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_cursor_to_top),
2724 other => panic!("expected Invoke(scroll_cursor_to_top), got {other:?}"),
2725 }
2726 }
2727
2728 #[test]
2729 fn zb_emits_scroll_cursor_bottom() {
2730 let (_, b) = fixture();
2731 let a = shared_actions();
2732 let action = translate(
2733 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
2734 key(KeyCode::Char('b')),
2735 );
2736 match action {
2737 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_cursor_to_bottom),
2738 other => panic!("expected Invoke(scroll_cursor_to_bottom), got {other:?}"),
2739 }
2740 }
2741
2742 #[test]
2743 fn ctrl_f_emits_page_down() {
2744 let (_, b) = fixture();
2745 let a = shared_actions();
2746 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('f'))) {
2747 Action::Invoke(inv) => assert_eq!(inv.command, a.page_down),
2748 other => panic!("expected Invoke(page_down), got {other:?}"),
2749 }
2750 }
2751
2752 #[test]
2753 fn ctrl_b_emits_page_up() {
2754 let (_, b) = fixture();
2755 let a = shared_actions();
2756 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('b'))) {
2757 Action::Invoke(inv) => assert_eq!(inv.command, a.page_up),
2758 other => panic!("expected Invoke(page_up), got {other:?}"),
2759 }
2760 }
2761
2762 #[test]
2763 fn ctrl_e_emits_scroll_line_down() {
2764 let (_, b) = fixture();
2765 let a = shared_actions();
2766 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('e'))) {
2767 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_line_down),
2768 other => panic!("expected Invoke(scroll_line_down), got {other:?}"),
2769 }
2770 }
2771
2772 #[test]
2773 fn ctrl_y_emits_scroll_line_up() {
2774 let (_, b) = fixture();
2775 let a = shared_actions();
2776 match translate(ctx(ModalState::Normal, &b), ctrl(KeyCode::Char('y'))) {
2777 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_line_up),
2778 other => panic!("expected Invoke(scroll_line_up), got {other:?}"),
2779 }
2780 }
2781
2782 #[test]
2783 fn esc_after_z_pending_clears() {
2784 let (_, b) = fixture();
2785 assert!(matches!(
2786 translate(
2787 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('z')], &b),
2788 key(KeyCode::Esc)
2789 ),
2790 Action::None
2791 ));
2792 }
2793
2794 #[test]
2797 fn capital_r_enters_replace_mode() {
2798 let (_, b) = fixture();
2799 let a = shared_actions();
2800 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('R'))) {
2801 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_mode_replace),
2802 other => panic!("expected Invoke(enter_mode_replace), got {other:?}"),
2803 }
2804 }
2805
2806 #[test]
2807 fn char_in_replace_emits_overwrite() {
2808 let (_, b) = fixture();
2809 let a = shared_actions();
2810 match translate(ctx(ModalState::Replace, &b), key(KeyCode::Char('z'))) {
2811 Action::Invoke(inv) => {
2812 assert_eq!(inv.command, a.overwrite_char);
2813 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('z')));
2814 }
2815 other => panic!("expected Invoke(overwrite_char, Char('z')), got {other:?}"),
2816 }
2817 }
2818
2819 #[test]
2820 fn esc_in_replace_returns_to_normal() {
2821 let (_, b) = fixture();
2822 let a = shared_actions();
2823 match translate(ctx(ModalState::Replace, &b), key(KeyCode::Esc)) {
2824 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_mode_normal),
2825 other => panic!("expected Invoke(enter_mode_normal), got {other:?}"),
2826 }
2827 }
2828
2829 #[test]
2830 fn backspace_in_replace_emits_replace_undo_last() {
2831 let (_, b) = fixture();
2832 let a = shared_actions();
2833 match translate(ctx(ModalState::Replace, &b), key(KeyCode::Backspace)) {
2834 Action::Invoke(inv) => assert_eq!(inv.command, a.replace_undo_last),
2835 other => panic!("expected Invoke(replace_undo_last), got {other:?}"),
2836 }
2837 }
2838
2839 #[test]
2840 fn enter_in_replace_inserts_newline() {
2841 let (_, b) = fixture();
2842 let a = shared_actions();
2843 match translate(ctx(ModalState::Replace, &b), key(KeyCode::Enter)) {
2844 Action::Invoke(inv) => assert_eq!(inv.command, a.insert_newline),
2845 other => panic!("expected Invoke(insert_newline), got {other:?}"),
2846 }
2847 }
2848
2849 #[test]
2855 fn replace_dispatch_reads_from_handle_not_baked_in() {
2856 let (_, b) = fixture();
2857 let empty = KeymapHandle::new();
2858 let mut c = ctx(ModalState::Replace, &b);
2859 c.keymap = ∅
2860 match translate(c, key(KeyCode::Char('z'))) {
2861 Action::None => {}
2862 other => panic!("empty handle must yield None for Replace dispatch, got {other:?}"),
2863 }
2864 }
2865
2866 #[test]
2873 fn alt_x_in_replace_overwrites_with_x() {
2874 let (_, b) = fixture();
2875 let a = shared_actions();
2876 let mut event = key(KeyCode::Char('x'));
2877 event.modifiers = KeyModifiers::ALT;
2878 match translate(ctx(ModalState::Replace, &b), event) {
2879 Action::Invoke(inv) => {
2880 assert_eq!(inv.command, a.overwrite_char);
2881 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('x')));
2882 }
2883 other => panic!("expected Invoke(overwrite_char, Char('x')), got {other:?}"),
2884 }
2885 }
2886
2887 #[test]
2890 fn m_in_normal_absorbs_partial_chord() {
2891 let (_, b) = fixture();
2893 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('m')));
2894 assert!(matches!(
2895 action,
2896 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('m')
2897 ));
2898 }
2899
2900 #[test]
2901 fn apostrophe_in_normal_absorbs_partial_chord() {
2902 let (_, b) = fixture();
2904 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('\'')));
2905 assert!(matches!(
2906 action,
2907 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('\'')
2908 ));
2909 }
2910
2911 #[test]
2912 fn backtick_in_normal_absorbs_partial_chord() {
2913 let (_, b) = fixture();
2915 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('`')));
2916 assert!(matches!(
2917 action,
2918 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('`')
2919 ));
2920 }
2921
2922 #[test]
2923 fn ma_after_m_emits_set_mark() {
2924 let (_, b) = fixture();
2925 let a = shared_actions();
2926 let action = translate(
2927 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('m')], &b),
2928 key(KeyCode::Char('a')),
2929 );
2930 match action {
2931 Action::Invoke(inv) => {
2932 assert_eq!(inv.command, a.set_mark);
2933 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
2934 }
2935 other => panic!("expected Invoke(set_mark, Char('a')), got {other:?}"),
2936 }
2937 }
2938
2939 #[test]
2940 fn jump_mark_line_routes_correctly() {
2941 let (_, b) = fixture();
2943 let action = translate(
2944 ctx_partial(
2945 ModalState::Normal,
2946 &[crate::chord::KeyChord::char('\'')],
2947 &b,
2948 ),
2949 key(KeyCode::Char('z')),
2950 );
2951 match action {
2952 Action::Invoke(inv) => {
2953 assert_eq!(inv.command, b.mark_line.0);
2954 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('z')));
2955 }
2956 other => panic!("expected Invoke(mark_line, Char('z')), got {other:?}"),
2957 }
2958 }
2959
2960 #[test]
2961 fn jump_mark_exact_routes_correctly() {
2962 let (_, b) = fixture();
2964 let action = translate(
2965 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('`')], &b),
2966 key(KeyCode::Char('A')),
2967 );
2968 match action {
2969 Action::Invoke(inv) => {
2970 assert_eq!(inv.command, b.mark_exact.0);
2971 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('A')));
2972 }
2973 other => panic!("expected Invoke(mark_exact, Char('A')), got {other:?}"),
2974 }
2975 }
2976
2977 #[test]
2978 fn esc_cancels_set_mark_pending() {
2979 let (_, b) = fixture();
2980 assert!(matches!(
2981 translate(
2982 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('m')], &b),
2983 key(KeyCode::Esc)
2984 ),
2985 Action::None
2986 ));
2987 }
2988
2989 #[test]
2990 fn non_alpha_after_set_mark_passes_char_to_actionspec() {
2991 let (_, b) = fixture();
2997 let a = shared_actions();
2998 match translate(
2999 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('m')], &b),
3000 key(KeyCode::Char(' ')),
3001 ) {
3002 Action::Invoke(inv) => {
3003 assert_eq!(inv.command, a.set_mark);
3004 assert!(matches!(inv.args, lattice_grammar::args::Args::Char(' ')));
3005 }
3006 other => panic!("expected Invoke(set_mark, Char(' ')), got {other:?}"),
3007 }
3008 }
3009
3010 #[test]
3013 fn gv_after_g_emits_reselect_visual() {
3014 let (_, b) = fixture();
3015 let a = shared_actions();
3016 let action = translate(
3017 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
3018 key(KeyCode::Char('v')),
3019 );
3020 match action {
3021 Action::Invoke(inv) => assert_eq!(inv.command, a.reselect_last_visual),
3022 other => panic!("expected Invoke(reselect_last_visual), got {other:?}"),
3023 }
3024 }
3025
3026 #[test]
3029 fn gt_invokes_absorb_operator_indent_right() {
3030 let (_, b) = fixture();
3032 let a = shared_actions();
3033 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('>')));
3034 match action {
3035 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_indent_right),
3036 other => panic!("expected Invoke(absorb_operator_indent_right), got {other:?}"),
3037 }
3038 }
3039
3040 #[test]
3041 fn lt_invokes_absorb_operator_indent_left() {
3042 let (_, b) = fixture();
3043 let a = shared_actions();
3044 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('<')));
3045 match action {
3046 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_indent_left),
3047 other => panic!("expected Invoke(absorb_operator_indent_left), got {other:?}"),
3048 }
3049 }
3050
3051 #[test]
3052 fn double_gt_resolves_to_indent_right_current_line() {
3053 let (_, b) = fixture();
3054 let action = translate(
3055 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('>')], &b),
3056 key(KeyCode::Char('>')),
3057 );
3058 match action {
3059 Action::Invoke(inv) => {
3060 assert_eq!(inv.command, b.indent_right.0);
3061 assert_eq!(inv.range, Some(lattice_grammar::Range::CurrentLine));
3062 }
3063 _ => panic!("expected Invoke"),
3064 }
3065 }
3066
3067 #[test]
3068 fn gu_after_g_invokes_absorb_operator_lower() {
3069 let (_, b) = fixture();
3070 let a = shared_actions();
3071 let action = translate(
3072 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
3073 key(KeyCode::Char('u')),
3074 );
3075 match action {
3076 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_lower),
3077 other => panic!("expected Invoke(absorb_operator_lower), got {other:?}"),
3078 }
3079 }
3080
3081 #[test]
3082 fn capital_g_then_capital_u_invokes_absorb_operator_upper() {
3083 let (_, b) = fixture();
3084 let a = shared_actions();
3085 let action = translate(
3086 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
3087 key(KeyCode::Char('U')),
3088 );
3089 match action {
3090 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_upper),
3091 other => panic!("expected Invoke(absorb_operator_upper), got {other:?}"),
3092 }
3093 }
3094
3095 #[test]
3096 fn g_tilde_after_g_invokes_absorb_operator_toggle_case() {
3097 let (_, b) = fixture();
3098 let a = shared_actions();
3099 let action = translate(
3100 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('g')], &b),
3101 key(KeyCode::Char('~')),
3102 );
3103 match action {
3104 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_toggle_case),
3105 other => panic!("expected Invoke(absorb_operator_toggle_case), got {other:?}"),
3106 }
3107 }
3108
3109 #[test]
3110 fn guu_resolves_to_lower_current_line() {
3111 let (_, b) = fixture();
3112 let action = translate(
3114 ctx_partial(
3115 ModalState::Normal,
3116 &[
3117 crate::chord::KeyChord::char('g'),
3118 crate::chord::KeyChord::char('u'),
3119 ],
3120 &b,
3121 ),
3122 key(KeyCode::Char('u')),
3123 );
3124 match action {
3125 Action::Invoke(inv) => {
3126 assert_eq!(inv.command, b.lower.0);
3127 assert_eq!(inv.range, Some(lattice_grammar::Range::CurrentLine));
3128 }
3129 _ => panic!("expected Invoke"),
3130 }
3131 }
3132
3133 #[test]
3134 fn g_capital_u_w_resolves_to_upper_word_forward() {
3135 let (_, b) = fixture();
3136 let action = translate(
3138 ctx_partial(
3139 ModalState::Normal,
3140 &[
3141 crate::chord::KeyChord::char('g'),
3142 crate::chord::KeyChord::char('U'),
3143 ],
3144 &b,
3145 ),
3146 key(KeyCode::Char('w')),
3147 );
3148 match action {
3149 Action::Invoke(inv) => {
3150 assert_eq!(inv.command, b.upper.0);
3151 match inv.target {
3152 Some(Target::Motion(id, _)) => assert_eq!(id, b.word_forward),
3153 other => panic!("expected motion target, got {other:?}"),
3154 }
3155 }
3156 _ => panic!("expected Invoke"),
3157 }
3158 }
3159
3160 #[test]
3163 fn i_in_operator_pending_absorbs_partial_chord() {
3164 let (_, b) = fixture();
3169 let action = translate(
3170 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
3171 key(KeyCode::Char('i')),
3172 );
3173 assert!(matches!(
3174 action,
3175 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('i')
3176 ));
3177 }
3178
3179 #[test]
3180 fn a_in_operator_pending_absorbs_partial_chord() {
3181 let (_, b) = fixture();
3182 let action = translate(
3183 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
3184 key(KeyCode::Char('a')),
3185 );
3186 assert!(matches!(
3187 action,
3188 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('a')
3189 ));
3190 }
3191
3192 #[test]
3193 fn diw_resolves_to_delete_inner_word() {
3194 let (_, b) = fixture();
3195 let action = translate(
3196 ctx_partial(
3197 ModalState::Normal,
3198 &[
3199 crate::chord::KeyChord::char('d'),
3200 crate::chord::KeyChord::char('i'),
3201 ],
3202 &b,
3203 ),
3204 key(KeyCode::Char('w')),
3205 );
3206 match action {
3207 Action::Invoke(inv) => {
3208 assert_eq!(inv.command, b.delete.0);
3209 match inv.target {
3210 Some(Target::TextObject(id, _)) => assert_eq!(id, b.inner_word),
3211 other => panic!("expected text-object target, got {other:?}"),
3212 }
3213 }
3214 _ => panic!("expected Invoke"),
3215 }
3216 }
3217
3218 #[test]
3219 fn da_quote_resolves_to_delete_around_double_quote() {
3220 let (_, b) = fixture();
3221 let action = translate(
3222 ctx_partial(
3223 ModalState::Normal,
3224 &[
3225 crate::chord::KeyChord::char('d'),
3226 crate::chord::KeyChord::char('a'),
3227 ],
3228 &b,
3229 ),
3230 key(KeyCode::Char('"')),
3231 );
3232 match action {
3233 Action::Invoke(inv) => match inv.target {
3234 Some(Target::TextObject(id, _)) => assert_eq!(id, b.around_quote_double),
3235 other => panic!("expected text-object target, got {other:?}"),
3236 },
3237 _ => panic!("expected Invoke"),
3238 }
3239 }
3240
3241 #[test]
3242 fn ci_paren_resolves_to_change_inner_paren() {
3243 let (_, b) = fixture();
3244 let action = translate(
3245 ctx_partial(
3246 ModalState::Normal,
3247 &[
3248 crate::chord::KeyChord::char('c'),
3249 crate::chord::KeyChord::char('i'),
3250 ],
3251 &b,
3252 ),
3253 key(KeyCode::Char('(')),
3254 );
3255 match action {
3256 Action::Invoke(inv) => {
3257 assert_eq!(inv.command, b.change.0);
3258 match inv.target {
3259 Some(Target::TextObject(id, _)) => assert_eq!(id, b.inner_paren),
3260 other => panic!("expected text-object target, got {other:?}"),
3261 }
3262 }
3263 _ => panic!("expected Invoke"),
3264 }
3265 }
3266
3267 #[test]
3268 #[allow(non_snake_case)]
3269 fn diW_resolves_to_delete_inner_big_word() {
3270 let (_, b) = fixture();
3271 let action = translate(
3272 ctx_partial(
3273 ModalState::Normal,
3274 &[
3275 crate::chord::KeyChord::char('d'),
3276 crate::chord::KeyChord::char('i'),
3277 ],
3278 &b,
3279 ),
3280 key(KeyCode::Char('W')),
3281 );
3282 match action {
3283 Action::Invoke(inv) => {
3284 assert_eq!(inv.command, b.delete.0);
3285 match inv.target {
3286 Some(Target::TextObject(id, _)) => assert_eq!(id, b.inner_big_word),
3287 other => panic!("expected text-object target, got {other:?}"),
3288 }
3289 }
3290 _ => panic!("expected Invoke"),
3291 }
3292 }
3293
3294 #[test]
3295 #[allow(non_snake_case)]
3296 fn daW_resolves_to_delete_around_big_word() {
3297 let (_, b) = fixture();
3298 let action = translate(
3299 ctx_partial(
3300 ModalState::Normal,
3301 &[
3302 crate::chord::KeyChord::char('d'),
3303 crate::chord::KeyChord::char('a'),
3304 ],
3305 &b,
3306 ),
3307 key(KeyCode::Char('W')),
3308 );
3309 match action {
3310 Action::Invoke(inv) => match inv.target {
3311 Some(Target::TextObject(id, _)) => assert_eq!(id, b.around_big_word),
3312 other => panic!("expected text-object target, got {other:?}"),
3313 },
3314 _ => panic!("expected Invoke"),
3315 }
3316 }
3317
3318 #[test]
3319 fn ci_angle_resolves_to_change_inner_angle() {
3320 let (_, b) = fixture();
3321 let action = translate(
3322 ctx_partial(
3323 ModalState::Normal,
3324 &[
3325 crate::chord::KeyChord::char('c'),
3326 crate::chord::KeyChord::char('i'),
3327 ],
3328 &b,
3329 ),
3330 key(KeyCode::Char('<')),
3331 );
3332 match action {
3333 Action::Invoke(inv) => match inv.target {
3334 Some(Target::TextObject(id, _)) => assert_eq!(id, b.inner_angle),
3335 other => panic!("expected text-object target, got {other:?}"),
3336 },
3337 _ => panic!("expected Invoke"),
3338 }
3339 }
3340
3341 #[test]
3342 fn da_angle_via_closer_resolves_to_delete_around_angle() {
3343 let (_, b) = fixture();
3345 let action = translate(
3346 ctx_partial(
3347 ModalState::Normal,
3348 &[
3349 crate::chord::KeyChord::char('d'),
3350 crate::chord::KeyChord::char('a'),
3351 ],
3352 &b,
3353 ),
3354 key(KeyCode::Char('>')),
3355 );
3356 match action {
3357 Action::Invoke(inv) => match inv.target {
3358 Some(Target::TextObject(id, _)) => assert_eq!(id, b.around_angle),
3359 other => panic!("expected text-object target, got {other:?}"),
3360 },
3361 _ => panic!("expected Invoke"),
3362 }
3363 }
3364
3365 #[test]
3366 fn esc_after_text_object_partial_chord_clears() {
3367 let (_, b) = fixture();
3374 assert!(matches!(
3375 translate(
3376 ctx_partial(
3377 ModalState::Normal,
3378 &[
3379 crate::chord::KeyChord::char('d'),
3380 crate::chord::KeyChord::char('i'),
3381 ],
3382 &b,
3383 ),
3384 key(KeyCode::Esc),
3385 ),
3386 Action::None
3387 ));
3388 }
3389
3390 #[test]
3393 fn dot_in_normal_emits_repeat_last_change() {
3394 let (_, b) = fixture();
3395 let a = shared_actions();
3396 match translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('.'))) {
3397 Action::Invoke(inv) => assert_eq!(inv.command, a.repeat_last_change),
3398 other => panic!("expected Invoke(repeat_last_change), got {other:?}"),
3399 }
3400 }
3401
3402 #[test]
3405 fn v_in_normal_enters_charwise_visual() {
3406 let (_, b) = fixture();
3407 let a = shared_actions();
3408 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('v')));
3409 match action {
3410 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_visual_charwise),
3411 other => panic!("expected Invoke(enter_visual_charwise), got {other:?}"),
3412 }
3413 }
3414
3415 #[test]
3416 fn capital_v_in_normal_enters_linewise_visual() {
3417 let (_, b) = fixture();
3418 let a = shared_actions();
3419 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('V')));
3420 match action {
3421 Action::Invoke(inv) => assert_eq!(inv.command, a.enter_visual_linewise),
3422 other => panic!("expected Invoke(enter_visual_linewise), got {other:?}"),
3423 }
3424 }
3425
3426 #[test]
3427 fn esc_in_visual_exits_to_normal() {
3428 let (_, b) = fixture();
3429 let a = shared_actions();
3430 let action = translate(
3431 ctx(ModalState::Visual(VisualKind::Charwise), &b),
3432 key(KeyCode::Esc),
3433 );
3434 match action {
3435 Action::Invoke(inv) => assert_eq!(inv.command, a.exit_visual),
3436 other => panic!("expected Invoke(exit_visual), got {other:?}"),
3437 }
3438 }
3439
3440 #[test]
3441 fn v_in_visual_toggles_off() {
3442 let (_, b) = fixture();
3443 let a = shared_actions();
3444 let action = translate(
3445 ctx(ModalState::Visual(VisualKind::Charwise), &b),
3446 key(KeyCode::Char('v')),
3447 );
3448 match action {
3449 Action::Invoke(inv) => assert_eq!(inv.command, a.exit_visual),
3450 other => panic!("expected Invoke(exit_visual), got {other:?}"),
3451 }
3452 }
3453
3454 #[test]
3455 fn motion_in_visual_returns_invocation() {
3456 let (_, b) = fixture();
3457 let action = translate(
3458 ctx(ModalState::Visual(VisualKind::Charwise), &b),
3459 key(KeyCode::Char('w')),
3460 );
3461 assert_eq!(invocation_command(&action), Some(b.word_forward.0));
3462 }
3463
3464 #[test]
3465 fn d_in_visual_invokes_delete_with_selection_range() {
3466 let (_, b) = fixture();
3467 let action = translate(
3468 ctx(ModalState::Visual(VisualKind::Charwise), &b),
3469 key(KeyCode::Char('d')),
3470 );
3471 match action {
3472 Action::Invoke(inv) => {
3473 assert_eq!(inv.command, b.delete.0);
3474 assert_eq!(inv.range, Some(lattice_grammar::Range::Selection));
3475 }
3476 other => panic!("expected Invoke(delete, Selection), got {other:?}"),
3477 }
3478 }
3479
3480 #[test]
3481 fn y_in_visual_invokes_yank_with_selection_range() {
3482 let (_, b) = fixture();
3483 let action = translate(
3484 ctx(ModalState::Visual(VisualKind::Charwise), &b),
3485 key(KeyCode::Char('y')),
3486 );
3487 match action {
3488 Action::Invoke(inv) => {
3489 assert_eq!(inv.command, b.yank.0);
3490 assert_eq!(inv.range, Some(lattice_grammar::Range::Selection));
3491 }
3492 other => panic!("expected Invoke(yank, Selection), got {other:?}"),
3493 }
3494 }
3495
3496 #[test]
3497 fn c_in_visual_invokes_change_with_selection_range() {
3498 let (_, b) = fixture();
3499 let action = translate(
3500 ctx(ModalState::Visual(VisualKind::Charwise), &b),
3501 key(KeyCode::Char('c')),
3502 );
3503 match action {
3504 Action::Invoke(inv) => {
3505 assert_eq!(inv.command, b.change.0);
3506 assert_eq!(inv.range, Some(lattice_grammar::Range::Selection));
3507 }
3508 other => panic!("expected Invoke(change, Selection), got {other:?}"),
3509 }
3510 }
3511
3512 #[test]
3513 fn gt_in_visual_invokes_indent_right_with_selection_range() {
3514 let (_, b) = fixture();
3515 for kind in [
3516 VisualKind::Charwise,
3517 VisualKind::Linewise,
3518 VisualKind::Blockwise,
3519 ] {
3520 let action = translate(ctx(ModalState::Visual(kind), &b), key(KeyCode::Char('>')));
3521 match action {
3522 Action::Invoke(inv) => {
3523 assert_eq!(inv.command, b.indent_right.0, "kind = {kind:?}");
3524 assert_eq!(inv.range, Some(lattice_grammar::Range::Selection));
3525 }
3526 other => panic!(
3527 "kind = {kind:?}, expected Invoke(indent_right, Selection), got {other:?}"
3528 ),
3529 }
3530 }
3531 }
3532
3533 #[test]
3534 fn lt_in_visual_invokes_indent_left_with_selection_range() {
3535 let (_, b) = fixture();
3536 for kind in [
3537 VisualKind::Charwise,
3538 VisualKind::Linewise,
3539 VisualKind::Blockwise,
3540 ] {
3541 let action = translate(ctx(ModalState::Visual(kind), &b), key(KeyCode::Char('<')));
3542 match action {
3543 Action::Invoke(inv) => {
3544 assert_eq!(inv.command, b.indent_left.0, "kind = {kind:?}");
3545 assert_eq!(inv.range, Some(lattice_grammar::Range::Selection));
3546 }
3547 other => panic!(
3548 "kind = {kind:?}, expected Invoke(indent_left, Selection), got {other:?}"
3549 ),
3550 }
3551 }
3552 }
3553
3554 #[test]
3557 fn digit_1_to_9_emits_push_digit_in_normal_mode() {
3558 let (_, b) = fixture();
3559 for digit in 1u8..=9 {
3560 let c = char::from_digit(digit as u32, 10).unwrap();
3561 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char(c)));
3562 assert!(matches!(action, Action::PushDigit(d) if d == digit));
3563 }
3564 }
3565
3566 #[test]
3577 fn ctrl_digit_is_a_chord_not_a_count() {
3578 let (_, b) = fixture();
3579 for digit in 0u8..=9 {
3580 let c = char::from_digit(digit as u32, 10).unwrap();
3581 let action = translate(
3582 ctx(ModalState::Normal, &b),
3583 crossterm::event::KeyEvent::new(
3584 KeyCode::Char(c),
3585 crossterm::event::KeyModifiers::CONTROL,
3586 ),
3587 );
3588 assert!(
3589 !matches!(action, Action::PushDigit(_)),
3590 "<C-{c}> must not be swallowed as a count digit, got {action:?}",
3591 );
3592 }
3593 }
3594
3595 #[test]
3599 fn plain_digit_is_still_a_count() {
3600 let (_, b) = fixture();
3601 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('6')));
3602 assert!(matches!(action, Action::PushDigit(6)));
3603 }
3604
3605 #[test]
3606 fn zero_with_no_count_invokes_line_start() {
3607 let (_, b) = fixture();
3608 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('0')));
3609 assert_eq!(invocation_command(&action), Some(b.line_start.0));
3610 }
3611
3612 #[test]
3613 fn zero_with_count_in_progress_extends_count() {
3614 let (_, b) = fixture();
3615 let action = translate(
3617 ctx_with_count(ModalState::Normal, &b, 1),
3618 key(KeyCode::Char('0')),
3619 );
3620 assert!(matches!(action, Action::PushDigit(0)));
3621 }
3622
3623 #[test]
3624 fn digit_after_count_extends_count() {
3625 let (_, b) = fixture();
3626 let action = translate(
3627 ctx_with_count(ModalState::Normal, &b, 12),
3628 key(KeyCode::Char('3')),
3629 );
3630 assert!(matches!(action, Action::PushDigit(3)));
3632 }
3633
3634 #[test]
3635 fn motion_after_count_dispatches_motion() {
3636 let (_, b) = fixture();
3637 let action = translate(
3638 ctx_with_count(ModalState::Normal, &b, 3),
3639 key(KeyCode::Char('w')),
3640 );
3641 match action {
3645 Action::Invoke(inv) => {
3646 assert_eq!(inv.command, b.word_forward.0);
3647 assert_eq!(inv.count, Some(lattice_grammar::command::Count(3)));
3648 }
3649 other => panic!("expected Invoke(word_forward, count=3), got {other:?}"),
3650 }
3651 }
3652
3653 #[test]
3662 fn op_count_times_motion_count_attaches_at_translate() {
3663 let (_, b) = fixture();
3664 let action = translate(
3665 ctx_partial_with_op_count(
3666 ModalState::Normal,
3667 &[crate::chord::KeyChord::char('d')],
3668 &b,
3669 3,
3670 2,
3671 ),
3672 key(KeyCode::Char('w')),
3673 );
3674 match action {
3675 Action::Invoke(inv) => {
3676 assert_eq!(inv.command, b.delete.0);
3677 assert!(matches!(
3678 inv.target,
3679 Some(Target::Motion(m, _)) if m == b.word_forward
3680 ));
3681 assert_eq!(inv.count, Some(lattice_grammar::command::Count(6)));
3682 }
3683 other => panic!("expected Invoke(delete, word_forward, count=6), got {other:?}"),
3684 }
3685 }
3686
3687 #[test]
3690 fn f_absorbs_partial_chord() {
3691 let (_, b) = fixture();
3696 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('f')));
3697 assert!(matches!(
3698 action,
3699 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('f')
3700 ));
3701 }
3702
3703 #[test]
3704 fn capital_f_absorbs_partial_chord() {
3705 let (_, b) = fixture();
3706 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('F')));
3707 assert!(matches!(
3708 action,
3709 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('F')
3710 ));
3711 }
3712
3713 #[test]
3714 fn t_absorbs_partial_chord() {
3715 let (_, b) = fixture();
3716 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('t')));
3717 assert!(matches!(
3718 action,
3719 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('t')
3720 ));
3721 }
3722
3723 #[test]
3724 fn capital_t_absorbs_partial_chord() {
3725 let (_, b) = fixture();
3726 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('T')));
3727 assert!(matches!(
3728 action,
3729 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('T')
3730 ));
3731 }
3732
3733 #[test]
3734 fn f_then_char_resolves_to_motion_with_args_char() {
3735 let (_, b) = fixture();
3739 let action = translate(
3740 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('f')], &b),
3741 key(KeyCode::Char('z')),
3742 );
3743 match action {
3744 Action::Invoke(inv) => {
3745 assert_eq!(inv.command, b.find_char_forward.0);
3746 assert_eq!(inv.args, lattice_grammar::Args::Char('z'));
3747 }
3748 other => panic!("expected Invoke(find_char_forward), got {other:?}"),
3749 }
3750 }
3751
3752 #[test]
3753 fn df_then_char_composes_delete_with_find_target() {
3754 let (_, b) = fixture();
3759 let after_d = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('d')));
3763 match after_d {
3764 Action::Invoke(_) => {}
3765 other => panic!("expected Invoke(absorb_operator_delete), got {other:?}"),
3766 };
3767 let after_df = translate(
3769 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
3770 key(KeyCode::Char('f')),
3771 );
3772 assert!(matches!(
3773 after_df,
3774 Action::AbsorbPartialChord(c) if c == crate::chord::KeyChord::char('f')
3775 ));
3776 let after_dfx = translate(
3779 ctx_partial(
3780 ModalState::Normal,
3781 &[
3782 crate::chord::KeyChord::char('d'),
3783 crate::chord::KeyChord::char('f'),
3784 ],
3785 &b,
3786 ),
3787 key(KeyCode::Char('x')),
3788 );
3789 match after_dfx {
3790 Action::Invoke(inv) => {
3791 assert_eq!(inv.command, b.delete.0);
3792 match inv.target {
3793 Some(Target::Motion(id, args)) => {
3794 assert_eq!(id, b.find_char_forward);
3795 assert_eq!(args, lattice_grammar::Args::Char('x'));
3796 }
3797 other => panic!("expected motion target, got {other:?}"),
3798 }
3799 }
3800 other => panic!("expected Invoke(delete, find_target), got {other:?}"),
3801 }
3802 }
3803
3804 #[test]
3805 fn esc_after_find_partial_clears_partial_chord() {
3806 let (_, b) = fixture();
3811 let action = translate(
3812 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('f')], &b),
3813 key(KeyCode::Esc),
3814 );
3815 assert!(matches!(action, Action::None));
3816 }
3817
3818 #[test]
3821 fn b_invokes_word_backward() {
3822 let (_, b) = fixture();
3823 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('b')));
3824 assert_eq!(invocation_command(&action), Some(b.word_backward.0));
3825 }
3826
3827 #[test]
3828 fn e_invokes_word_end() {
3829 let (_, b) = fixture();
3830 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('e')));
3831 assert_eq!(invocation_command(&action), Some(b.word_end.0));
3832 }
3833
3834 #[test]
3835 fn caret_invokes_first_non_blank() {
3836 let (_, b) = fixture();
3837 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('^')));
3838 assert_eq!(invocation_command(&action), Some(b.first_non_blank.0));
3839 }
3840
3841 #[test]
3842 fn db_resolves_to_delete_word_backward() {
3843 let (_, b) = fixture();
3844 let action = translate(
3845 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
3846 key(KeyCode::Char('b')),
3847 );
3848 match action {
3849 Action::Invoke(inv) => {
3850 assert_eq!(inv.command, b.delete.0);
3851 match inv.target {
3852 Some(Target::Motion(id, _)) => assert_eq!(id, b.word_backward),
3853 other => panic!("expected motion target, got {other:?}"),
3854 }
3855 }
3856 _ => panic!("expected Invoke"),
3857 }
3858 }
3859
3860 #[test]
3861 fn de_resolves_to_delete_word_end() {
3862 let (_, b) = fixture();
3863 let action = translate(
3864 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
3865 key(KeyCode::Char('e')),
3866 );
3867 match action {
3868 Action::Invoke(inv) => match inv.target {
3869 Some(Target::Motion(id, _)) => assert_eq!(id, b.word_end),
3870 other => panic!("expected motion target, got {other:?}"),
3871 },
3872 _ => panic!("expected Invoke"),
3873 }
3874 }
3875
3876 #[test]
3879 fn c_invokes_absorb_operator_change() {
3880 let (_, b) = fixture();
3881 let a = shared_actions();
3882 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('c')));
3883 match action {
3884 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_change),
3885 other => panic!("expected Invoke(absorb_operator_change), got {other:?}"),
3886 }
3887 }
3888
3889 #[test]
3890 fn cw_resolves_to_change_with_word_forward_target() {
3891 let (_, b) = fixture();
3892 let action = translate(
3893 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('c')], &b),
3894 key(KeyCode::Char('w')),
3895 );
3896 match action {
3897 Action::Invoke(inv) => {
3898 assert_eq!(inv.command, b.change.0);
3899 match inv.target {
3900 Some(Target::Motion(id, _)) => assert_eq!(id, b.word_forward),
3901 other => panic!("expected motion target, got {other:?}"),
3902 }
3903 }
3904 _ => panic!("expected Invoke"),
3905 }
3906 }
3907
3908 #[test]
3909 fn cc_resolves_to_change_with_current_line_range() {
3910 let (_, b) = fixture();
3911 let action = translate(
3912 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('c')], &b),
3913 key(KeyCode::Char('c')),
3914 );
3915 match action {
3916 Action::Invoke(inv) => {
3917 assert_eq!(inv.command, b.change.0);
3918 assert_eq!(inv.range, Some(lattice_grammar::Range::CurrentLine));
3919 }
3920 _ => panic!("expected Invoke"),
3921 }
3922 }
3923
3924 #[test]
3927 fn y_invokes_absorb_operator_yank() {
3928 let (_, b) = fixture();
3929 let a = shared_actions();
3930 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('y')));
3931 match action {
3932 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_yank),
3933 other => panic!("expected Invoke(absorb_operator_yank), got {other:?}"),
3934 }
3935 }
3936
3937 #[test]
3938 fn yw_resolves_to_yank_word_forward() {
3939 let (_, b) = fixture();
3940 let action = translate(
3941 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('y')], &b),
3942 key(KeyCode::Char('w')),
3943 );
3944 match action {
3945 Action::Invoke(inv) => {
3946 assert_eq!(inv.command, b.yank.0);
3947 match inv.target {
3948 Some(Target::Motion(id, _)) => assert_eq!(id, b.word_forward),
3949 other => panic!("expected motion target, got {other:?}"),
3950 }
3951 }
3952 _ => panic!("expected Invoke"),
3953 }
3954 }
3955
3956 #[test]
3957 fn yy_resolves_to_yank_current_line() {
3958 let (_, b) = fixture();
3959 let action = translate(
3960 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('y')], &b),
3961 key(KeyCode::Char('y')),
3962 );
3963 match action {
3964 Action::Invoke(inv) => {
3965 assert_eq!(inv.command, b.yank.0);
3966 assert_eq!(inv.range, Some(lattice_grammar::Range::CurrentLine));
3967 }
3968 _ => panic!("expected Invoke"),
3969 }
3970 }
3971
3972 #[test]
3973 fn capital_y_aliases_to_yank_current_line() {
3974 let (_, b) = fixture();
3975 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('Y')));
3976 match action {
3977 Action::Invoke(inv) => {
3978 assert_eq!(inv.command, b.yank.0);
3979 assert_eq!(inv.range, Some(lattice_grammar::Range::CurrentLine));
3980 }
3981 _ => panic!("expected Invoke"),
3982 }
3983 }
3984
3985 #[test]
3986 fn p_lowercase_is_paste_after() {
3987 let (_, b) = fixture();
3988 let a = shared_actions();
3989 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('p')));
3990 match action {
3991 Action::Invoke(inv) => assert_eq!(inv.command, a.paste_after),
3992 other => panic!("expected Invoke(paste_after), got {other:?}"),
3993 }
3994 }
3995
3996 #[test]
3997 fn p_uppercase_is_paste_before() {
3998 let (_, b) = fixture();
3999 let a = shared_actions();
4000 let action = translate(ctx(ModalState::Normal, &b), key(KeyCode::Char('P')));
4001 match action {
4002 Action::Invoke(inv) => assert_eq!(inv.command, a.paste_before),
4003 other => panic!("expected Invoke(paste_before), got {other:?}"),
4004 }
4005 }
4006
4007 #[test]
4008 fn dd_is_not_treated_as_change_current_line() {
4009 let (_, b) = fixture();
4011 let action = translate(
4012 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
4013 key(KeyCode::Char('c')),
4014 );
4015 assert!(matches!(action, Action::None));
4017 }
4018
4019 #[test]
4020 fn d_caret_resolves_to_delete_first_non_blank() {
4021 let (_, b) = fixture();
4022 let action = translate(
4023 ctx_partial(ModalState::Normal, &[crate::chord::KeyChord::char('d')], &b),
4024 key(KeyCode::Char('^')),
4025 );
4026 match action {
4027 Action::Invoke(inv) => match inv.target {
4028 Some(Target::Motion(id, _)) => assert_eq!(id, b.first_non_blank),
4029 other => panic!("expected motion target, got {other:?}"),
4030 },
4031 _ => panic!("expected Invoke"),
4032 }
4033 }
4034
4035 #[test]
4038 fn chord_capture_translates_ctrl_letter_to_chord_token() {
4039 let (_, b) = fixture();
4040 let action = translate(ctx_chord_capture(&b), ctrl(KeyCode::Char('c')));
4041 match action {
4042 Action::CommandLineAppendChord(s) => assert_eq!(s, "<C-c>"),
4043 other => panic!("expected CommandLineAppendChord, got {other:?}"),
4044 }
4045 }
4046
4047 #[test]
4048 fn chord_capture_translates_plain_letter_unwrapped() {
4049 let (_, b) = fixture();
4050 let action = translate(ctx_chord_capture(&b), key(KeyCode::Char('g')));
4051 match action {
4052 Action::CommandLineAppendChord(s) => assert_eq!(s, "g"),
4053 other => panic!("expected CommandLineAppendChord, got {other:?}"),
4054 }
4055 }
4056
4057 #[test]
4063 fn chord_capture_reserves_nothing() {
4064 let (_, b) = fixture();
4065 for (code, expected) in [
4066 (KeyCode::Enter, "<CR>"),
4067 (KeyCode::Esc, "<Esc>"),
4068 (KeyCode::Backspace, "<BS>"),
4069 ] {
4070 match translate(ctx_chord_capture(&b), key(code)) {
4071 Action::CommandLineAppendChord(s) => assert_eq!(
4072 s, expected,
4073 "{code:?} must be captured as a chord, not obeyed as a control"
4074 ),
4075 other => panic!("{code:?} was reserved: {other:?}"),
4076 }
4077 }
4078 }
4079
4080 #[test]
4081 fn chord_capture_translates_special_keys_with_angles() {
4082 let (_, b) = fixture();
4083 let action = translate(ctx_chord_capture(&b), key(KeyCode::Up));
4085 match action {
4086 Action::CommandLineAppendChord(s) => assert_eq!(s, "<Up>"),
4087 other => panic!("expected CommandLineAppendChord, got {other:?}"),
4088 }
4089 }
4090
4091 #[test]
4097 fn terminal_normal_a_upper_i_upper_a_also_enter_terminal_insert() {
4098 let (_, b) = fixture();
4099 for c in ['a', 'I', 'A'] {
4100 let ctx = TranslateContext {
4101 modal: ModalState::Normal,
4102 builtins: &b,
4103 pending_count: 0,
4104 op_count: 0,
4105 recording_macro: false,
4106 active_buffer: BufferKind::Terminal,
4107 completion_open: false,
4108 chord_capture: false,
4109 picker_open: false,
4110 insert_completion_open: false,
4111 snippet_active: false,
4112 terminal_insert_active: false,
4113 terminal_esc_exits: false,
4114 terminal_app_cursor_keys: false,
4115 terminal_insert_exit_pending: false,
4116 terminal_visual_active: false,
4117 keymap: test_keymap(),
4118 partial_chord: &[],
4119 active_minor_modes: &[],
4120 };
4121 let action = translate(ctx, key(KeyCode::Char(c)));
4122 assert!(
4123 matches!(action, Action::EnterTerminalInsert),
4124 "expected EnterTerminalInsert for `{c}`, got {action:?}",
4125 );
4126 }
4127 }
4128
4129 #[test]
4133 fn terminal_normal_i_enters_terminal_insert() {
4134 let (_, b) = fixture();
4135 let ctx = TranslateContext {
4136 modal: ModalState::Normal,
4137 builtins: &b,
4138 pending_count: 0,
4139 op_count: 0,
4140 recording_macro: false,
4141 active_buffer: BufferKind::Terminal,
4142 completion_open: false,
4143 chord_capture: false,
4144 picker_open: false,
4145 insert_completion_open: false,
4146 snippet_active: false,
4147 terminal_insert_active: false,
4148 terminal_esc_exits: false,
4149 terminal_app_cursor_keys: false,
4150 terminal_insert_exit_pending: false,
4151 terminal_visual_active: false,
4152 keymap: test_keymap(),
4153 partial_chord: &[],
4154 active_minor_modes: &[],
4155 };
4156 assert!(matches!(
4157 translate(ctx, key(KeyCode::Char('i'))),
4158 Action::EnterTerminalInsert,
4159 ));
4160 }
4161
4162 #[test]
4168 fn terminal_insert_encodes_chars_to_terminal_input() {
4169 let (_, b) = fixture();
4170 let mk_ctx = || TranslateContext {
4171 modal: ModalState::Normal,
4172 builtins: &b,
4173 pending_count: 0,
4174 op_count: 0,
4175 recording_macro: false,
4176 active_buffer: BufferKind::Terminal,
4177 completion_open: false,
4178 chord_capture: false,
4179 picker_open: false,
4180 insert_completion_open: false,
4181 snippet_active: false,
4182 terminal_insert_active: true,
4183 terminal_esc_exits: false,
4184 terminal_app_cursor_keys: false,
4185 terminal_insert_exit_pending: false,
4186 terminal_visual_active: false,
4187 keymap: test_keymap(),
4188 partial_chord: &[],
4189 active_minor_modes: &[],
4190 };
4191 match translate(mk_ctx(), key(KeyCode::Char('l'))) {
4192 Action::TerminalInput(bytes) => assert_eq!(bytes, b"l".to_vec()),
4193 other => panic!("expected TerminalInput, got {other:?}"),
4194 }
4195 match translate(mk_ctx(), key(KeyCode::Enter)) {
4196 Action::TerminalInput(bytes) => assert_eq!(bytes, vec![b'\r']),
4197 other => panic!("expected TerminalInput, got {other:?}"),
4198 }
4199 }
4200
4201 #[test]
4207 fn terminal_insert_ctrl_c_sends_sigint_byte_not_quit() {
4208 let (_, b) = fixture();
4209 let ctx = TranslateContext {
4210 modal: ModalState::Normal,
4211 builtins: &b,
4212 pending_count: 0,
4213 op_count: 0,
4214 recording_macro: false,
4215 active_buffer: BufferKind::Terminal,
4216 completion_open: false,
4217 chord_capture: false,
4218 picker_open: false,
4219 insert_completion_open: false,
4220 snippet_active: false,
4221 terminal_insert_active: true,
4222 terminal_esc_exits: false,
4223 terminal_app_cursor_keys: false,
4224 terminal_insert_exit_pending: false,
4225 terminal_visual_active: false,
4226 keymap: test_keymap(),
4227 partial_chord: &[],
4228 active_minor_modes: &[],
4229 };
4230 match translate(ctx, ctrl(KeyCode::Char('c'))) {
4231 Action::TerminalInput(bytes) => assert_eq!(bytes, vec![0x03]),
4232 other => panic!("expected TerminalInput([0x03]), got {other:?}"),
4233 }
4234 }
4235
4236 #[test]
4240 fn terminal_insert_ctrl_backslash_arms_exit_chord() {
4241 let (_, b) = fixture();
4242 let ctx = TranslateContext {
4243 modal: ModalState::Normal,
4244 builtins: &b,
4245 pending_count: 0,
4246 op_count: 0,
4247 recording_macro: false,
4248 active_buffer: BufferKind::Terminal,
4249 completion_open: false,
4250 chord_capture: false,
4251 picker_open: false,
4252 insert_completion_open: false,
4253 snippet_active: false,
4254 terminal_insert_active: true,
4255 terminal_esc_exits: false,
4256 terminal_app_cursor_keys: false,
4257 terminal_insert_exit_pending: false,
4258 terminal_visual_active: false,
4259 keymap: test_keymap(),
4260 partial_chord: &[],
4261 active_minor_modes: &[],
4262 };
4263 assert!(matches!(
4264 translate(ctx, ctrl(KeyCode::Char('\\'))),
4265 Action::TerminalArmExitChord,
4266 ));
4267 }
4268
4269 #[test]
4272 fn terminal_insert_armed_then_ctrl_n_exits() {
4273 let (_, b) = fixture();
4274 let ctx = TranslateContext {
4275 modal: ModalState::Normal,
4276 builtins: &b,
4277 pending_count: 0,
4278 op_count: 0,
4279 recording_macro: false,
4280 active_buffer: BufferKind::Terminal,
4281 completion_open: false,
4282 chord_capture: false,
4283 picker_open: false,
4284 insert_completion_open: false,
4285 snippet_active: false,
4286 terminal_insert_active: true,
4287 terminal_esc_exits: false,
4288 terminal_app_cursor_keys: false,
4289 terminal_insert_exit_pending: true,
4290 terminal_visual_active: false,
4291 keymap: test_keymap(),
4292 partial_chord: &[],
4293 active_minor_modes: &[],
4294 };
4295 assert!(matches!(
4296 translate(ctx, ctrl(KeyCode::Char('n'))),
4297 Action::ExitTerminalInsert,
4298 ));
4299 }
4300
4301 #[test]
4304 fn terminal_insert_armed_then_other_key_emits_lost_prefix_plus_byte() {
4305 let (_, b) = fixture();
4306 let ctx = TranslateContext {
4307 modal: ModalState::Normal,
4308 builtins: &b,
4309 pending_count: 0,
4310 op_count: 0,
4311 recording_macro: false,
4312 active_buffer: BufferKind::Terminal,
4313 completion_open: false,
4314 chord_capture: false,
4315 picker_open: false,
4316 insert_completion_open: false,
4317 snippet_active: false,
4318 terminal_insert_active: true,
4319 terminal_esc_exits: false,
4320 terminal_app_cursor_keys: false,
4321 terminal_insert_exit_pending: true,
4322 terminal_visual_active: false,
4323 keymap: test_keymap(),
4324 partial_chord: &[],
4325 active_minor_modes: &[],
4326 };
4327 match translate(ctx, key(KeyCode::Char('a'))) {
4328 Action::TerminalInput(bytes) => assert_eq!(bytes, vec![0x1c, b'a']),
4329 other => panic!("expected TerminalInput([0x1c, 'a']), got {other:?}"),
4330 }
4331 }
4332
4333 #[test]
4335 fn terminal_insert_arrow_uses_ss3_when_decckm_set() {
4336 let (_, b) = fixture();
4337 let ctx = TranslateContext {
4338 modal: ModalState::Normal,
4339 builtins: &b,
4340 pending_count: 0,
4341 op_count: 0,
4342 recording_macro: false,
4343 active_buffer: BufferKind::Terminal,
4344 completion_open: false,
4345 chord_capture: false,
4346 picker_open: false,
4347 insert_completion_open: false,
4348 snippet_active: false,
4349 terminal_insert_active: true,
4350 terminal_esc_exits: false,
4351 terminal_app_cursor_keys: true,
4352 terminal_insert_exit_pending: false,
4353 terminal_visual_active: false,
4354 keymap: test_keymap(),
4355 partial_chord: &[],
4356 active_minor_modes: &[],
4357 };
4358 match translate(ctx, key(KeyCode::Up)) {
4359 Action::TerminalInput(bytes) => assert_eq!(bytes, b"\x1bOA".to_vec()),
4360 other => panic!("expected SS3 ESC O A, got {other:?}"),
4361 }
4362 }
4363
4364 #[test]
4369 fn terminal_insert_esc_exits_when_option_set() {
4370 let (_, b) = fixture();
4371 let ctx = TranslateContext {
4372 modal: ModalState::Normal,
4373 builtins: &b,
4374 pending_count: 0,
4375 op_count: 0,
4376 recording_macro: false,
4377 active_buffer: BufferKind::Terminal,
4378 completion_open: false,
4379 chord_capture: false,
4380 picker_open: false,
4381 insert_completion_open: false,
4382 snippet_active: false,
4383 terminal_insert_active: true,
4384 terminal_esc_exits: true,
4385 terminal_app_cursor_keys: false,
4386 terminal_insert_exit_pending: false,
4387 terminal_visual_active: false,
4388 keymap: test_keymap(),
4389 partial_chord: &[],
4390 active_minor_modes: &[],
4391 };
4392 assert!(matches!(
4393 translate(ctx, key(KeyCode::Esc)),
4394 Action::ExitTerminalInsert,
4395 ));
4396 }
4397
4398 #[test]
4404 fn terminal_insert_esc_encodes_when_option_unset() {
4405 let (_, b) = fixture();
4406 let ctx = TranslateContext {
4407 modal: ModalState::Normal,
4408 builtins: &b,
4409 pending_count: 0,
4410 op_count: 0,
4411 recording_macro: false,
4412 active_buffer: BufferKind::Terminal,
4413 completion_open: false,
4414 chord_capture: false,
4415 picker_open: false,
4416 insert_completion_open: false,
4417 snippet_active: false,
4418 terminal_insert_active: true,
4419 terminal_esc_exits: false,
4420 terminal_app_cursor_keys: false,
4421 terminal_insert_exit_pending: false,
4422 terminal_visual_active: false,
4423 keymap: test_keymap(),
4424 partial_chord: &[],
4425 active_minor_modes: &[],
4426 };
4427 match translate(ctx, key(KeyCode::Esc)) {
4428 Action::TerminalInput(bytes) => assert_eq!(bytes, vec![0x1b]),
4429 other => panic!("expected TerminalInput([0x1b]), got {other:?}"),
4430 }
4431 }
4432}