1pub use lattice_host::keymap_normal::*;
13
14#[cfg(test)]
15mod tests {
16 #![allow(clippy::unwrap_used, clippy::panic, unused_imports)]
17 use super::*;
18 use crate::actions::ActionIds;
19 use crate::app::{Action, FindKind};
20 use crate::chord::{KeyChord, KeyKind, KeyMods, SpecialKey};
21 use crate::keymap::BindingMode;
22 use crate::keymap_registry::KeymapHandle;
23 use crate::keymap_trie::{BoundCommand, ChordPattern, KeymapLayer, LookupResult};
24 use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
25 use lattice_grammar::CommandRegistry;
26 use lattice_grammar::Target;
27 use lattice_grammar::builtins::Builtins;
28 use lattice_grammar::command::CommandInvocation;
29
30 fn ev(code: KeyCode, mods: KeyModifiers) -> KeyChord {
37 let raw = KeyEvent {
38 code,
39 modifiers: mods,
40 kind: KeyEventKind::Press,
41 state: KeyEventState::NONE,
42 };
43 crate::chord::from_event(&raw).expect("test event converts to a chord")
44 }
45
46 fn lookup_normal(handle: &KeymapHandle, chord: &KeyChord) -> Option<Action> {
55 lattice_host::keymap_normal::lookup_normal(handle, chord, &[])
56 }
57
58 fn lookup_normal_with_prefix(
59 handle: &KeymapHandle,
60 prefix: &[KeyChord],
61 chord: &KeyChord,
62 ) -> Action {
63 lattice_host::keymap_normal::lookup_normal_with_prefix(handle, prefix, chord, &[])
64 }
65
66 fn fixture() -> (CommandRegistry, Builtins, ActionIds) {
67 let mut r = CommandRegistry::new();
68 let b = lattice_grammar::builtins::populate(&mut r);
69 let a = crate::actions::populate(&mut r, &b);
70 (r, b, a)
71 }
72
73 fn populated_handle() -> (KeymapHandle, Builtins, ActionIds) {
74 let (mut r, b, a) = fixture();
75 let so = lattice_syntax::register_syntax_text_objects(&mut r);
76 let sm = lattice_syntax::register_syntax_motions(&mut r);
77 let h = KeymapHandle::new();
78 register_normal_bindings(&h, &b, &a, &so, &sm);
79 (h, b, a)
80 }
81
82 #[test]
83 fn motion_h_invokes_char_left() {
84 let (h, b, _) = populated_handle();
85 let r = lookup_normal(&h, &ev(KeyCode::Char('h'), KeyModifiers::NONE));
86 match r {
87 Some(Action::Invoke(inv)) => assert_eq!(inv.command, b.char_left.0),
88 other => panic!("expected Invoke(char_left), got {other:?}"),
89 }
90 }
91
92 #[test]
93 fn arrow_left_aliases_char_left() {
94 let (h, b, _) = populated_handle();
95 let r = lookup_normal(&h, &ev(KeyCode::Left, KeyModifiers::NONE));
96 match r {
97 Some(Action::Invoke(inv)) => assert_eq!(inv.command, b.char_left.0),
98 other => panic!("expected Invoke(char_left), got {other:?}"),
99 }
100 }
101
102 #[test]
103 fn upper_g_invokes_goto_last_line() {
104 let (h, b, _) = populated_handle();
105 let r = lookup_normal(&h, &ev(KeyCode::Char('G'), KeyModifiers::NONE));
106 match r {
107 Some(Action::Invoke(inv)) => assert_eq!(inv.command, b.goto_last_line.0),
108 other => panic!("expected Invoke(goto_last_line), got {other:?}"),
109 }
110 }
111
112 #[test]
113 fn pseudo_operator_x_carries_char_right_target() {
114 let (h, b, _) = populated_handle();
115 let r = lookup_normal(&h, &ev(KeyCode::Char('x'), KeyModifiers::NONE));
116 match r {
117 Some(Action::Invoke(inv)) => {
118 assert_eq!(inv.command, b.delete.0);
119 assert!(matches!(inv.target, Some(Target::Motion(m, _)) if m == b.char_right));
120 }
121 other => panic!("expected Invoke(delete, char_right), got {other:?}"),
122 }
123 }
124
125 #[test]
126 fn pseudo_operator_d_carries_line_end_target() {
127 let (h, b, _) = populated_handle();
128 let r = lookup_normal(&h, &ev(KeyCode::Char('D'), KeyModifiers::NONE));
129 match r {
130 Some(Action::Invoke(inv)) => {
131 assert_eq!(inv.command, b.delete.0);
132 assert!(matches!(inv.target, Some(Target::Motion(m, _)) if m == b.line_end));
133 }
134 other => panic!("expected Invoke(delete, line_end), got {other:?}"),
135 }
136 }
137
138 #[test]
139 fn pseudo_operator_y_capital_uses_current_line_range() {
140 let (h, b, _) = populated_handle();
141 let r = lookup_normal(&h, &ev(KeyCode::Char('Y'), KeyModifiers::NONE));
142 match r {
143 Some(Action::Invoke(inv)) => {
144 assert_eq!(inv.command, b.yank.0);
145 assert!(matches!(
146 inv.range,
147 Some(lattice_grammar::Range::CurrentLine)
148 ));
149 }
150 other => panic!("expected Invoke(yank, CurrentLine), got {other:?}"),
151 }
152 }
153
154 #[test]
155 fn viewport_h_resolves_to_the_viewport_top_motion() {
156 let (h, b, _) = populated_handle();
158 let r = lookup_normal(&h, &ev(KeyCode::Char('H'), KeyModifiers::NONE));
159 match r {
160 Some(Action::Invoke(inv)) => assert_eq!(inv.command, b.viewport_top.0),
161 other => panic!("expected Invoke(viewport_top motion), got {other:?}"),
162 }
163 }
164
165 #[test]
166 fn mode_entry_v_enters_charwise_visual() {
167 let (h, _, a) = populated_handle();
168 let r = lookup_normal(&h, &ev(KeyCode::Char('v'), KeyModifiers::NONE));
169 match r {
170 Some(Action::Invoke(inv)) => assert_eq!(inv.command, a.enter_visual_charwise),
171 other => panic!("expected Invoke(enter_visual_charwise), got {other:?}"),
172 }
173 }
174
175 #[test]
176 fn paste_p_lower_pastes_after() {
177 let (h, _, a) = populated_handle();
178 let r = lookup_normal(&h, &ev(KeyCode::Char('p'), KeyModifiers::NONE));
179 match r {
180 Some(Action::Invoke(inv)) => assert_eq!(inv.command, a.paste_after),
181 other => panic!("expected Invoke(paste_after), got {other:?}"),
182 }
183 }
184
185 #[test]
186 fn search_slash_enters_forward_search() {
187 let (h, _, a) = populated_handle();
188 let r = lookup_normal(&h, &ev(KeyCode::Char('/'), KeyModifiers::NONE));
189 match r {
190 Some(Action::Invoke(inv)) => assert_eq!(inv.command, a.enter_search_forward),
191 other => panic!("expected Invoke(enter_search_forward), got {other:?}"),
192 }
193 }
194
195 #[test]
196 fn tab_jumps_history_forward() {
197 let (h, _, a) = populated_handle();
198 let r = lookup_normal(&h, &ev(KeyCode::Tab, KeyModifiers::NONE));
199 match r {
200 Some(Action::Invoke(inv)) => assert_eq!(inv.command, a.jump_history_forward),
201 other => panic!("expected Invoke(jump_history_forward), got {other:?}"),
202 }
203 }
204
205 #[test]
206 fn page_down_invokes_line_down_with_count_ten() {
207 let (h, b, _) = populated_handle();
208 let r = lookup_normal(&h, &ev(KeyCode::PageDown, KeyModifiers::NONE));
209 match r {
210 Some(Action::Invoke(inv)) => {
211 assert_eq!(inv.command, b.line_down.0);
212 assert_eq!(inv.count, Some(lattice_grammar::command::Count(10)));
213 }
214 other => panic!("expected Invoke(line_down, count=10), got {other:?}"),
215 }
216 }
217
218 #[test]
224 fn d_invokes_absorb_operator_delete() {
225 let (h, _, a) = populated_handle();
231 let r = lookup_normal(&h, &ev(KeyCode::Char('d'), KeyModifiers::NONE));
232 match r {
233 Some(Action::Invoke(inv)) => {
234 assert_eq!(inv.command, a.absorb_operator_delete);
235 }
236 other => panic!("expected Invoke(absorb_operator_delete), got {other:?}"),
237 }
238 }
239
240 #[test]
241 fn dw_resolves_to_delete_with_word_forward_target() {
242 let (h, b, _) = populated_handle();
243 let r = lookup_normal_with_prefix(
244 &h,
245 &[KeyChord::char('d')],
246 &ev(KeyCode::Char('w'), KeyModifiers::NONE),
247 );
248 match r {
249 Action::Invoke(inv) => {
250 assert_eq!(inv.command, b.delete.0);
251 assert!(matches!(
252 inv.target,
253 Some(Target::Motion(m, _)) if m == b.word_forward
254 ));
255 }
256 other => panic!("expected Invoke(delete, word_forward), got {other:?}"),
257 }
258 }
259
260 #[test]
265 fn d_uppercase_g_resolves_to_delete_with_goto_last_line_target() {
266 let (h, b, _) = populated_handle();
267 let r = lookup_normal_with_prefix(
268 &h,
269 &[KeyChord::char('d')],
270 &ev(KeyCode::Char('G'), KeyModifiers::NONE),
271 );
272 match r {
273 Action::Invoke(inv) => {
274 assert_eq!(inv.command, b.delete.0);
275 assert!(matches!(
276 inv.target,
277 Some(Target::Motion(m, _)) if m == b.goto_last_line
278 ));
279 }
280 other => panic!("expected Invoke(delete, goto_last_line), got {other:?}"),
281 }
282 }
283
284 #[test]
285 fn dd_resolves_to_delete_current_line() {
286 let (h, b, _) = populated_handle();
287 let r = lookup_normal_with_prefix(
288 &h,
289 &[KeyChord::char('d')],
290 &ev(KeyCode::Char('d'), KeyModifiers::NONE),
291 );
292 match r {
293 Action::Invoke(inv) => {
294 assert_eq!(inv.command, b.delete.0);
295 assert!(matches!(
296 inv.range,
297 Some(lattice_grammar::Range::CurrentLine)
298 ));
299 }
300 other => panic!("expected Invoke(delete, CurrentLine), got {other:?}"),
301 }
302 }
303
304 #[test]
305 fn yy_resolves_to_yank_current_line() {
306 let (h, b, _) = populated_handle();
307 let r = lookup_normal_with_prefix(
308 &h,
309 &[KeyChord::char('y')],
310 &ev(KeyCode::Char('y'), KeyModifiers::NONE),
311 );
312 match r {
313 Action::Invoke(inv) => {
314 assert_eq!(inv.command, b.yank.0);
315 assert!(matches!(
316 inv.range,
317 Some(lattice_grammar::Range::CurrentLine)
318 ));
319 }
320 other => panic!("expected Invoke(yank, CurrentLine), got {other:?}"),
321 }
322 }
323
324 #[test]
325 fn cc_resolves_to_change_current_line() {
326 let (h, b, _) = populated_handle();
327 let r = lookup_normal_with_prefix(
328 &h,
329 &[KeyChord::char('c')],
330 &ev(KeyCode::Char('c'), KeyModifiers::NONE),
331 );
332 match r {
333 Action::Invoke(inv) => {
334 assert_eq!(inv.command, b.change.0);
335 assert!(matches!(
336 inv.range,
337 Some(lattice_grammar::Range::CurrentLine)
338 ));
339 }
340 other => panic!("expected Invoke(change, CurrentLine), got {other:?}"),
341 }
342 }
343
344 #[test]
345 fn di_absorbs_partial_chord() {
346 let (h, _, _a) = populated_handle();
351 let r = lookup_normal_with_prefix(
352 &h,
353 &[KeyChord::char('d')],
354 &ev(KeyCode::Char('i'), KeyModifiers::NONE),
355 );
356 assert!(matches!(
357 r,
358 Action::AbsorbPartialChord(c) if c == KeyChord::char('i')
359 ));
360 }
361
362 #[test]
363 fn diw_resolves_to_delete_inner_word() {
364 let (h, b, _) = populated_handle();
365 let r = lookup_normal_with_prefix(
366 &h,
367 &[KeyChord::char('d'), KeyChord::char('i')],
368 &ev(KeyCode::Char('w'), KeyModifiers::NONE),
369 );
370 match r {
371 Action::Invoke(inv) => {
372 assert_eq!(inv.command, b.delete.0);
373 assert!(matches!(
374 inv.target,
375 Some(Target::TextObject(id, _)) if id == b.inner_word
376 ));
377 }
378 other => panic!("expected Invoke(delete, inner_word), got {other:?}"),
379 }
380 }
381
382 #[test]
383 fn dab_resolves_to_delete_around_paren() {
384 let (h, b_, _) = populated_handle();
386 let r = lookup_normal_with_prefix(
387 &h,
388 &[KeyChord::char('d'), KeyChord::char('a')],
389 &ev(KeyCode::Char('b'), KeyModifiers::NONE),
390 );
391 match r {
392 Action::Invoke(inv) => {
393 assert_eq!(inv.command, b_.delete.0);
394 assert!(matches!(
395 inv.target,
396 Some(Target::TextObject(id, _)) if id == b_.around_paren
397 ));
398 }
399 other => panic!("expected Invoke(delete, around_paren), got {other:?}"),
400 }
401 }
402
403 #[test]
404 fn df_absorbs_partial_chord() {
405 let (h, _, _a) = populated_handle();
409 let r = lookup_normal_with_prefix(
410 &h,
411 &[KeyChord::char('d')],
412 &ev(KeyCode::Char('f'), KeyModifiers::NONE),
413 );
414 assert!(matches!(
415 r,
416 Action::AbsorbPartialChord(c) if c == KeyChord::char('f')
417 ));
418 }
419
420 #[test]
421 fn d_unrecognised_drops_pending() {
422 let (h, _, _a) = populated_handle();
423 let r = lookup_normal_with_prefix(
424 &h,
425 &[KeyChord::char('d')],
426 &ev(KeyCode::Char('Q'), KeyModifiers::NONE),
427 );
428 assert!(matches!(r, Action::None));
429 }
430
431 #[test]
434 fn g_uu_resolves_to_upper_current_line() {
435 let (h, b, _) = populated_handle();
436 let r = lookup_normal_with_prefix(
437 &h,
438 &[KeyChord::char('g'), KeyChord::char('U')],
439 &ev(KeyCode::Char('U'), KeyModifiers::NONE),
440 );
441 match r {
442 Action::Invoke(inv) => {
443 assert_eq!(inv.command, b.upper.0);
444 assert!(matches!(
445 inv.range,
446 Some(lattice_grammar::Range::CurrentLine)
447 ));
448 }
449 other => panic!("expected Invoke(upper, CurrentLine), got {other:?}"),
450 }
451 }
452
453 #[test]
455 fn g_uw_resolves_to_upper_with_word_forward() {
456 let (h, b, _) = populated_handle();
457 let r = lookup_normal_with_prefix(
458 &h,
459 &[KeyChord::char('g'), KeyChord::char('U')],
460 &ev(KeyCode::Char('w'), KeyModifiers::NONE),
461 );
462 match r {
463 Action::Invoke(inv) => {
464 assert_eq!(inv.command, b.upper.0);
465 assert!(matches!(
466 inv.target,
467 Some(Target::Motion(m, _)) if m == b.word_forward
468 ));
469 }
470 other => panic!("expected Invoke(upper, word_forward), got {other:?}"),
471 }
472 }
473
474 #[test]
479 fn g_absorbs_partial_chord() {
480 let (h, _, _a) = populated_handle();
482 let r = lookup_normal(&h, &ev(KeyCode::Char('g'), KeyModifiers::NONE));
483 assert!(matches!(
484 r,
485 Some(Action::AbsorbPartialChord(c)) if c == KeyChord::char('g')
486 ));
487 }
488
489 #[test]
490 fn z_absorbs_partial_chord() {
491 let (h, _, _a) = populated_handle();
493 let r = lookup_normal(&h, &ev(KeyCode::Char('z'), KeyModifiers::NONE));
494 assert!(matches!(
495 r,
496 Some(Action::AbsorbPartialChord(c)) if c == KeyChord::char('z')
497 ));
498 }
499
500 #[test]
501 fn gg_resolves_to_goto_first_line() {
502 let (h, b, _) = populated_handle();
503 let r = lookup_normal_with_prefix(
504 &h,
505 &[KeyChord::char('g')],
506 &ev(KeyCode::Char('g'), KeyModifiers::NONE),
507 );
508 match r {
509 Action::Invoke(inv) => assert_eq!(inv.command, b.goto_first_line.0),
510 other => panic!("expected Invoke(goto_first_line), got {other:?}"),
511 }
512 }
513
514 #[test]
515 fn gd_without_lsp_mode_is_unresolved_at_builtin_layer() {
516 let (h, _, _) = populated_handle();
519 let r = lookup_normal_with_prefix(
520 &h,
521 &[KeyChord::char('g')],
522 &ev(KeyCode::Char('d'), KeyModifiers::NONE),
523 );
524 assert!(
525 matches!(r, Action::None),
526 "expected Action::None, got {r:?}"
527 );
528 }
529
530 #[test]
531 fn gu_invokes_absorb_operator_lower() {
532 let (h, _, a) = populated_handle();
535 let r = lookup_normal_with_prefix(
536 &h,
537 &[KeyChord::char('g')],
538 &ev(KeyCode::Char('u'), KeyModifiers::NONE),
539 );
540 match r {
541 Action::Invoke(inv) => assert_eq!(inv.command, a.absorb_operator_lower),
542 other => panic!("expected Invoke(absorb_operator_lower), got {other:?}"),
543 }
544 }
545
546 #[test]
547 fn g_capital_j_resolves_to_join_lines_without_space() {
548 let (h, _, a) = populated_handle();
549 let r = lookup_normal_with_prefix(
550 &h,
551 &[KeyChord::char('g')],
552 &ev(KeyCode::Char('J'), KeyModifiers::NONE),
553 );
554 match r {
555 Action::Invoke(inv) => assert_eq!(inv.command, a.join_lines_bare),
556 other => panic!("expected Invoke(join_lines_bare), got {other:?}"),
557 }
558 }
559
560 #[test]
561 fn zz_centers_cursor() {
562 let (h, _, a) = populated_handle();
563 let r = lookup_normal_with_prefix(
564 &h,
565 &[KeyChord::char('z')],
566 &ev(KeyCode::Char('z'), KeyModifiers::NONE),
567 );
568 match r {
569 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_cursor_to_center),
570 other => panic!("expected Invoke(scroll_cursor_to_center), got {other:?}"),
571 }
572 }
573
574 #[test]
575 fn z_dot_aliases_zz() {
576 let (h, _, a) = populated_handle();
577 let r = lookup_normal_with_prefix(
578 &h,
579 &[KeyChord::char('z')],
580 &ev(KeyCode::Char('.'), KeyModifiers::NONE),
581 );
582 match r {
583 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_cursor_to_center),
584 other => panic!("expected Invoke(scroll_cursor_to_center), got {other:?}"),
585 }
586 }
587
588 #[test]
589 fn z_enter_aliases_zt() {
590 let (h, _, a) = populated_handle();
591 let r = lookup_normal_with_prefix(
592 &h,
593 &[KeyChord::char('z')],
594 &ev(KeyCode::Enter, KeyModifiers::NONE),
595 );
596 match r {
597 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_cursor_to_top),
598 other => panic!("expected Invoke(scroll_cursor_to_top), got {other:?}"),
599 }
600 }
601
602 #[test]
603 fn z_dash_aliases_zb() {
604 let (h, _, a) = populated_handle();
605 let r = lookup_normal_with_prefix(
606 &h,
607 &[KeyChord::char('z')],
608 &ev(KeyCode::Char('-'), KeyModifiers::NONE),
609 );
610 match r {
611 Action::Invoke(inv) => assert_eq!(inv.command, a.scroll_cursor_to_bottom),
612 other => panic!("expected Invoke(scroll_cursor_to_bottom), got {other:?}"),
613 }
614 }
615
616 #[test]
617 fn za_toggles_fold_at_cursor() {
618 let (h, _, a) = populated_handle();
619 let r = lookup_normal_with_prefix(
620 &h,
621 &[KeyChord::char('z')],
622 &ev(KeyCode::Char('a'), KeyModifiers::NONE),
623 );
624 match r {
625 Action::Invoke(inv) => assert_eq!(inv.command, a.toggle_fold_at_cursor),
626 other => panic!("expected Invoke(toggle_fold_at_cursor), got {other:?}"),
627 }
628 }
629
630 #[test]
631 fn z_unrecognized_drops_pending() {
632 let (h, _, _a) = populated_handle();
633 let r = lookup_normal_with_prefix(
634 &h,
635 &[KeyChord::char('z')],
636 &ev(KeyCode::Char('X'), KeyModifiers::NONE),
637 );
638 assert!(matches!(r, Action::None));
639 }
640
641 #[test]
642 fn z_esc_drops_pending() {
643 let (h, _, _a) = populated_handle();
644 let r = lookup_normal_with_prefix(
645 &h,
646 &[KeyChord::char('z')],
647 &ev(KeyCode::Esc, KeyModifiers::NONE),
648 );
649 assert!(matches!(r, Action::None));
650 }
651
652 #[test]
658 fn q_absorbs_partial_chord() {
659 let (h, _, _a) = populated_handle();
661 let r = lookup_normal(&h, &ev(KeyCode::Char('q'), KeyModifiers::NONE));
662 assert!(matches!(
663 r,
664 Some(Action::AbsorbPartialChord(c)) if c == KeyChord::char('q')
665 ));
666 }
667
668 #[test]
671 fn ma_resolves_to_set_mark_a() {
672 let (h, _, a) = populated_handle();
673 let r = lookup_normal_with_prefix(
674 &h,
675 &[KeyChord::char('m')],
676 &ev(KeyCode::Char('a'), KeyModifiers::NONE),
677 );
678 match r {
679 Action::Invoke(inv) => {
680 assert_eq!(inv.command, a.set_mark);
681 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
682 }
683 other => panic!("expected Invoke(set_mark, Char('a')), got {other:?}"),
684 }
685 }
686
687 #[test]
688 fn m_invalid_passes_char_to_actionspec() {
689 let (h, _, a) = populated_handle();
696 let r = lookup_normal_with_prefix(
697 &h,
698 &[KeyChord::char('m')],
699 &ev(KeyCode::Char('!'), KeyModifiers::NONE),
700 );
701 match r {
702 Action::Invoke(inv) => {
703 assert_eq!(inv.command, a.set_mark);
704 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('!')));
705 }
706 other => panic!("expected Invoke(set_mark, Char('!')), got {other:?}"),
707 }
708 }
709
710 #[test]
711 fn apostrophe_a_resolves_to_the_mark_line_motion() {
712 let (h, b, _) = populated_handle();
714 let r = lookup_normal_with_prefix(
715 &h,
716 &[KeyChord::char('\'')],
717 &ev(KeyCode::Char('a'), KeyModifiers::NONE),
718 );
719 match r {
720 Action::Invoke(inv) => {
721 assert_eq!(inv.command, b.mark_line.0);
722 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
723 }
724 other => panic!("expected Invoke(mark_line, Char('a')), got {other:?}"),
725 }
726 }
727
728 #[test]
729 fn backtick_a_resolves_to_the_mark_exact_motion() {
730 let (h, b, _) = populated_handle();
732 let r = lookup_normal_with_prefix(
733 &h,
734 &[KeyChord::char('`')],
735 &ev(KeyCode::Char('a'), KeyModifiers::NONE),
736 );
737 match r {
738 Action::Invoke(inv) => {
739 assert_eq!(inv.command, b.mark_exact.0);
740 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
741 }
742 other => panic!("expected Invoke(mark_exact, Char('a')), got {other:?}"),
743 }
744 }
745
746 #[test]
747 fn quote_a_selects_named_register_a() {
748 let (h, _, a) = populated_handle();
749 let r = lookup_normal_with_prefix(
750 &h,
751 &[KeyChord::char('"')],
752 &ev(KeyCode::Char('a'), KeyModifiers::NONE),
753 );
754 match r {
755 Action::Invoke(inv) => {
756 assert_eq!(inv.command, a.select_register);
757 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
758 }
759 other => panic!("expected Invoke(select_register, Char('a')), got {other:?}"),
760 }
761 }
762
763 #[test]
764 fn quote_plus_selects_system_register() {
765 let (h, _, a) = populated_handle();
766 let r = lookup_normal_with_prefix(
767 &h,
768 &[KeyChord::char('"')],
769 &ev(KeyCode::Char('+'), KeyModifiers::NONE),
770 );
771 match r {
772 Action::Invoke(inv) => {
773 assert_eq!(inv.command, a.select_register);
774 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('+')));
775 }
776 other => panic!("expected Invoke(select_register, Char('+')), got {other:?}"),
777 }
778 }
779
780 #[test]
781 fn quote_zero_selects_numbered_register_zero() {
782 let (h, _, a) = populated_handle();
783 let r = lookup_normal_with_prefix(
784 &h,
785 &[KeyChord::char('"')],
786 &ev(KeyCode::Char('0'), KeyModifiers::NONE),
787 );
788 match r {
789 Action::Invoke(inv) => {
790 assert_eq!(inv.command, a.select_register);
791 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('0')));
792 }
793 other => panic!("expected Invoke(select_register, Char('0')), got {other:?}"),
794 }
795 }
796
797 #[test]
798 fn quote_invalid_passes_char_to_actionspec() {
799 let (h, _, a) = populated_handle();
804 let r = lookup_normal_with_prefix(
805 &h,
806 &[KeyChord::char('"')],
807 &ev(KeyCode::Char('!'), KeyModifiers::NONE),
808 );
809 match r {
810 Action::Invoke(inv) => {
811 assert_eq!(inv.command, a.select_register);
812 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('!')));
813 }
814 other => panic!("expected Invoke(select_register, Char('!')), got {other:?}"),
815 }
816 }
817
818 #[test]
819 fn qa_starts_macro_record_a() {
820 let (h, _, a) = populated_handle();
821 let r = lookup_normal_with_prefix(
822 &h,
823 &[KeyChord::char('q')],
824 &ev(KeyCode::Char('a'), KeyModifiers::NONE),
825 );
826 match r {
827 Action::Invoke(inv) => {
828 assert_eq!(inv.command, a.start_macro_record);
829 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
830 }
831 other => panic!("expected Invoke(start_macro_record, Char('a')), got {other:?}"),
832 }
833 }
834
835 #[test]
836 fn at_at_plays_last_macro() {
837 let (h, _, a) = populated_handle();
843 let r = lookup_normal_with_prefix(
844 &h,
845 &[KeyChord::char('@')],
846 &ev(KeyCode::Char('@'), KeyModifiers::NONE),
847 );
848 match r {
849 Action::Invoke(inv) => {
850 assert_eq!(inv.command, a.play_macro);
851 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('@')));
852 }
853 other => panic!("expected Invoke(play_macro, Char('@')), got {other:?}"),
854 }
855 }
856
857 #[test]
858 fn at_a_plays_macro_a() {
859 let (h, _, a) = populated_handle();
860 let r = lookup_normal_with_prefix(
861 &h,
862 &[KeyChord::char('@')],
863 &ev(KeyCode::Char('a'), KeyModifiers::NONE),
864 );
865 match r {
866 Action::Invoke(inv) => {
867 assert_eq!(inv.command, a.play_macro);
868 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('a')));
869 }
870 other => panic!("expected Invoke(play_macro, Char('a')), got {other:?}"),
871 }
872 }
873
874 #[test]
875 fn f_x_resolves_to_find_char_forward_with_args() {
876 let (h, b, _) = populated_handle();
877 let r = lookup_normal_with_prefix(
878 &h,
879 &[KeyChord::char('f')],
880 &ev(KeyCode::Char('X'), KeyModifiers::NONE),
881 );
882 match r {
883 Action::Invoke(inv) => {
884 assert_eq!(inv.command, b.find_char_forward.0);
885 assert!(matches!(inv.args, lattice_grammar::args::Args::Char('X')));
886 }
887 other => panic!("expected Invoke(find_char_forward, Char('X')), got {other:?}"),
888 }
889 }
890
891 #[test]
892 fn r_x_resolves_to_replace_char_with_char_arg_on_operator() {
893 let (h, b, _) = populated_handle();
897 let r = lookup_normal_with_prefix(
898 &h,
899 &[KeyChord::char('r')],
900 &ev(KeyCode::Char('X'), KeyModifiers::NONE),
901 );
902 match r {
903 Action::Invoke(inv) => {
904 assert_eq!(inv.command, b.replace_char.0);
905 assert!(
906 matches!(inv.args, lattice_grammar::args::Args::Char('X')),
907 "replacement char rides on the operator args, got {:?}",
908 inv.args
909 );
910 assert!(
911 matches!(inv.target, Some(Target::Motion(m, _)) if m == b.char_right),
912 "range sized by char_right, got {:?}",
913 inv.target
914 );
915 }
916 other => panic!("expected Invoke(replace_char, Char('X')), got {other:?}"),
917 }
918 }
919
920 #[test]
921 fn r_alone_absorbs_partial_chord() {
922 let (h, _, _) = populated_handle();
924 let r = lookup_normal(&h, &ev(KeyCode::Char('r'), KeyModifiers::NONE));
925 assert!(
926 matches!(r, Some(Action::AbsorbPartialChord(c)) if c == KeyChord::char('r')),
927 "expected AbsorbPartialChord(r), got {r:?}"
928 );
929 }
930
931 #[test]
932 fn dfx_resolves_to_delete_with_find_char_target() {
933 let (h, b, _) = populated_handle();
935 let r = lookup_normal_with_prefix(
936 &h,
937 &[KeyChord::char('d'), KeyChord::char('f')],
938 &ev(KeyCode::Char('X'), KeyModifiers::NONE),
939 );
940 match r {
941 Action::Invoke(inv) => {
942 assert_eq!(inv.command, b.delete.0);
943 match inv.target {
944 Some(Target::Motion(m, args)) => {
945 assert_eq!(m, b.find_char_forward);
946 assert!(matches!(args, lattice_grammar::args::Args::Char('X')));
947 }
948 other => panic!("expected Motion(find_char_forward, Char('X')), got {other:?}"),
949 }
950 }
951 other => panic!("got {other:?}"),
952 }
953 }
954
955 #[test]
963 fn f_ctrl_x_falls_through_to_drop_pending() {
964 let (h, _, _a) = populated_handle();
965 let r = lookup_normal_with_prefix(
966 &h,
967 &[KeyChord::char('f')],
968 &ev(KeyCode::Char('x'), KeyModifiers::CONTROL),
969 );
970 assert!(matches!(r, Action::None));
971 }
972
973 #[test]
977 fn shift_h_resolves_via_lowercase_chord() {
978 let (h, b, _) = populated_handle();
979 let r = lookup_normal(&h, &ev(KeyCode::Char('h'), KeyModifiers::SHIFT));
980 match r {
981 Some(Action::Invoke(inv)) => assert_eq!(inv.command, b.char_left.0),
982 other => panic!("expected Invoke(char_left), got {other:?}"),
983 }
984 }
985
986 #[test]
992 fn alt_h_resolves_to_char_left() {
993 let (h, b, _) = populated_handle();
994 let r = lookup_normal(&h, &ev(KeyCode::Char('h'), KeyModifiers::ALT));
995 match r {
996 Some(Action::Invoke(inv)) => assert_eq!(inv.command, b.char_left.0),
997 other => panic!("expected Invoke(char_left), got {other:?}"),
998 }
999 }
1000
1001 fn invoke_no_count() -> Action {
1004 Action::Invoke(CommandInvocation::of(
1005 lattice_protocol::ids::CommandId::new(42),
1006 ))
1007 }
1008
1009 fn invoke_with_default_count(n: u32) -> Action {
1010 Action::Invoke(
1011 CommandInvocation::of(lattice_protocol::ids::CommandId::new(42))
1012 .with_count(lattice_grammar::command::Count(n)),
1013 )
1014 }
1015
1016 #[test]
1017 fn attach_count_pending_count_only() {
1018 let r = attach_count(invoke_no_count(), 5, 0);
1019 match r {
1020 Action::Invoke(inv) => {
1021 assert_eq!(inv.count, Some(lattice_grammar::command::Count(5)));
1022 }
1023 other => panic!("got {other:?}"),
1024 }
1025 }
1026
1027 #[test]
1028 fn attach_count_op_times_motion() {
1029 let r = attach_count(invoke_no_count(), 3, 2);
1030 match r {
1031 Action::Invoke(inv) => {
1032 assert_eq!(inv.count, Some(lattice_grammar::command::Count(6)));
1033 }
1034 other => panic!("got {other:?}"),
1035 }
1036 }
1037
1038 #[test]
1039 fn attach_count_op_only_uses_default_motion_count_one() {
1040 let r = attach_count(invoke_no_count(), 0, 4);
1043 match r {
1044 Action::Invoke(inv) => {
1045 assert_eq!(inv.count, Some(lattice_grammar::command::Count(4)));
1046 }
1047 other => panic!("got {other:?}"),
1048 }
1049 }
1050
1051 #[test]
1052 fn attach_count_default_count_baked_in_used_when_pending_zero() {
1053 let r = attach_count(invoke_with_default_count(10), 0, 0);
1057 match r {
1058 Action::Invoke(inv) => {
1059 assert_eq!(inv.count, Some(lattice_grammar::command::Count(10)));
1060 }
1061 other => panic!("got {other:?}"),
1062 }
1063 }
1064
1065 #[test]
1066 fn attach_count_pending_overrides_default_count() {
1067 let r = attach_count(invoke_with_default_count(10), 5, 0);
1070 match r {
1071 Action::Invoke(inv) => {
1072 assert_eq!(inv.count, Some(lattice_grammar::command::Count(5)));
1073 }
1074 other => panic!("got {other:?}"),
1075 }
1076 }
1077
1078 #[test]
1079 fn attach_count_no_attachment_when_final_is_one() {
1080 let r = attach_count(invoke_no_count(), 0, 0);
1084 match r {
1085 Action::Invoke(inv) => assert_eq!(inv.count, None),
1086 other => panic!("got {other:?}"),
1087 }
1088 }
1089
1090 #[test]
1091 fn attach_count_passes_through_non_invoke_actions() {
1092 let r = attach_count(Action::ExitVisual, 5, 0);
1093 assert!(matches!(r, Action::ExitVisual));
1094 let r = attach_count(Action::None, 0, 0);
1095 assert!(matches!(r, Action::None));
1096 let r = attach_count(Action::AbsorbPartialChord(KeyChord::char('g')), 5, 0);
1099 assert!(matches!(r, Action::AbsorbPartialChord(_)));
1100 }
1101
1102 #[test]
1103 fn attach_count_idempotent_when_re_applied() {
1104 let once = attach_count(invoke_no_count(), 3, 2);
1109 let once_clone = match &once {
1110 Action::Invoke(inv) => Action::Invoke(inv.clone()),
1111 _ => panic!(),
1112 };
1113 let twice = attach_count(once_clone, 3, 2);
1114 match (once, twice) {
1115 (Action::Invoke(a), Action::Invoke(b)) => {
1116 assert_eq!(a.count, b.count);
1117 assert_eq!(a.count, Some(lattice_grammar::command::Count(6)));
1118 }
1119 other => panic!("got {other:?}"),
1120 }
1121 }
1122
1123 #[test]
1130 fn ctrl_d_resolves_to_the_half_page_scroll() {
1131 let (h, _, a) = populated_handle();
1132 let r = lookup_normal(&h, &ev(KeyCode::Char('d'), KeyModifiers::CONTROL));
1133 match r {
1134 Some(Action::Invoke(inv)) => {
1135 assert_eq!(inv.command, a.half_page_down);
1136 assert_eq!(inv.count, None);
1137 }
1138 other => panic!("expected Invoke(half_page_down), got {other:?}"),
1139 }
1140 }
1141
1142 #[test]
1143 fn ctrl_o_resolves_to_jump_history_back() {
1144 let (h, _, a) = populated_handle();
1145 let r = lookup_normal(&h, &ev(KeyCode::Char('o'), KeyModifiers::CONTROL));
1146 match r {
1147 Some(Action::Invoke(inv)) => assert_eq!(inv.command, a.jump_history_back),
1148 other => panic!("expected Invoke(jump_history_back), got {other:?}"),
1149 }
1150 }
1151
1152 #[test]
1153 fn ctrl_v_enters_blockwise_visual() {
1154 let (h, _, a) = populated_handle();
1155 let r = lookup_normal(&h, &ev(KeyCode::Char('v'), KeyModifiers::CONTROL));
1156 match r {
1157 Some(Action::Invoke(inv)) => assert_eq!(inv.command, a.enter_visual_blockwise),
1158 other => panic!("expected Invoke(enter_visual_blockwise), got {other:?}"),
1159 }
1160 }
1161
1162 #[test]
1163 fn ctrl_w_absorbs_partial_chord() {
1164 let (h, _, _a) = populated_handle();
1166 let r = lookup_normal(&h, &ev(KeyCode::Char('w'), KeyModifiers::CONTROL));
1167 assert!(matches!(
1168 r,
1169 Some(Action::AbsorbPartialChord(c)) if c == KeyChord::ctrl('w')
1170 ));
1171 }
1172
1173 #[test]
1174 fn ctrl_w_then_l_navigates_pane_right() {
1175 let (h, _, a) = populated_handle();
1176 let r = lookup_normal_with_prefix(
1177 &h,
1178 &[KeyChord::ctrl('w')],
1179 &ev(KeyCode::Char('l'), KeyModifiers::NONE),
1180 );
1181 match r {
1182 Action::Invoke(inv) => assert_eq!(inv.command, a.navigate_pane_right),
1183 other => panic!("expected Invoke(navigate_pane_right), got {other:?}"),
1184 }
1185 }
1186
1187 #[test]
1188 fn ctrl_w_then_ctrl_l_also_navigates_pane_right() {
1189 let (h, _, a) = populated_handle();
1192 let r = lookup_normal_with_prefix(
1193 &h,
1194 &[KeyChord::ctrl('w')],
1195 &ev(KeyCode::Char('l'), KeyModifiers::CONTROL),
1196 );
1197 match r {
1198 Action::Invoke(inv) => assert_eq!(inv.command, a.navigate_pane_right),
1199 other => panic!("expected Invoke(navigate_pane_right), got {other:?}"),
1200 }
1201 }
1202
1203 #[test]
1204 fn ctrl_w_then_arrow_left_navigates_pane_left() {
1205 let (h, _, a) = populated_handle();
1206 let r = lookup_normal_with_prefix(
1207 &h,
1208 &[KeyChord::ctrl('w')],
1209 &ev(KeyCode::Left, KeyModifiers::NONE),
1210 );
1211 match r {
1212 Action::Invoke(inv) => assert_eq!(inv.command, a.navigate_pane_left),
1213 other => panic!("expected Invoke(navigate_pane_left), got {other:?}"),
1214 }
1215 }
1216
1217 #[test]
1218 fn ctrl_w_then_backspace_navigates_pane_left() {
1219 let (h, _, a) = populated_handle();
1222 let r = lookup_normal_with_prefix(
1223 &h,
1224 &[KeyChord::ctrl('w')],
1225 &ev(KeyCode::Backspace, KeyModifiers::NONE),
1226 );
1227 match r {
1228 Action::Invoke(inv) => assert_eq!(inv.command, a.navigate_pane_left),
1229 other => panic!("expected Invoke(navigate_pane_left), got {other:?}"),
1230 }
1231 }
1232
1233 #[test]
1234 fn ctrl_w_then_tab_cycles_to_next_pane() {
1235 let (h, _, a) = populated_handle();
1236 let r = lookup_normal_with_prefix(
1237 &h,
1238 &[KeyChord::ctrl('w')],
1239 &ev(KeyCode::Tab, KeyModifiers::NONE),
1240 );
1241 match r {
1242 Action::Invoke(inv) => assert_eq!(inv.command, a.next_pane),
1243 other => panic!("expected Invoke(next_pane), got {other:?}"),
1244 }
1245 }
1246
1247 #[test]
1248 fn ctrl_w_then_back_tab_cycles_to_prev_pane() {
1249 let (h, _, a) = populated_handle();
1253 let r = lookup_normal_with_prefix(
1254 &h,
1255 &[KeyChord::ctrl('w')],
1256 &ev(KeyCode::BackTab, KeyModifiers::NONE),
1257 );
1258 match r {
1259 Action::Invoke(inv) => assert_eq!(inv.command, a.prev_pane),
1260 other => panic!("expected Invoke(prev_pane), got {other:?}"),
1261 }
1262 }
1263
1264 #[test]
1265 fn ctrl_w_then_v_splits_vertical() {
1266 let (h, _, a) = populated_handle();
1267 let r = lookup_normal_with_prefix(
1268 &h,
1269 &[KeyChord::ctrl('w')],
1270 &ev(KeyCode::Char('v'), KeyModifiers::NONE),
1271 );
1272 match r {
1273 Action::Invoke(inv) => assert_eq!(inv.command, a.split_pane_vertical),
1274 other => panic!("expected Invoke(split_pane_vertical), got {other:?}"),
1275 }
1276 }
1277
1278 #[test]
1279 fn ctrl_w_then_capital_s_splits_horizontal() {
1280 let (h, _, a) = populated_handle();
1282 let r = lookup_normal_with_prefix(
1283 &h,
1284 &[KeyChord::ctrl('w')],
1285 &ev(KeyCode::Char('S'), KeyModifiers::NONE),
1286 );
1287 match r {
1288 Action::Invoke(inv) => assert_eq!(inv.command, a.split_pane_horizontal),
1289 other => panic!("expected Invoke(split_pane_horizontal), got {other:?}"),
1290 }
1291 }
1292
1293 #[test]
1294 fn ctrl_w_then_q_closes_pane() {
1295 let (h, _, a) = populated_handle();
1296 let r = lookup_normal_with_prefix(
1297 &h,
1298 &[KeyChord::ctrl('w')],
1299 &ev(KeyCode::Char('q'), KeyModifiers::NONE),
1300 );
1301 match r {
1302 Action::Invoke(inv) => assert_eq!(inv.command, a.close_pane),
1303 other => panic!("expected Invoke(close_pane), got {other:?}"),
1304 }
1305 }
1306
1307 #[test]
1308 fn ctrl_w_then_esc_drops_pending() {
1309 let (h, _, _a) = populated_handle();
1310 let r = lookup_normal_with_prefix(
1311 &h,
1312 &[KeyChord::ctrl('w')],
1313 &ev(KeyCode::Esc, KeyModifiers::NONE),
1314 );
1315 assert!(matches!(r, Action::None));
1316 }
1317}