1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum PendingState {
6 Replace {
7 count: usize,
8 },
9 Find {
13 count: usize,
14 forward: bool,
15 till: bool,
16 },
17 AfterG {
22 count: usize,
23 },
24 AfterZ {
29 count: usize,
30 },
31 AfterOp {
42 op: crate::operator::OperatorKind,
43 count1: usize,
44 inner_count: usize,
45 },
46 OpFind {
58 op: crate::operator::OperatorKind,
59 total_count: usize,
60 forward: bool,
61 till: bool,
62 },
63 OpTextObj {
68 op: crate::operator::OperatorKind,
69 total_count: usize,
70 inner: bool,
71 },
72 OpG {
77 op: crate::operator::OperatorKind,
78 total_count: usize,
79 },
80 OpMark {
84 op: crate::operator::OperatorKind,
85 linewise: bool,
86 },
87 SelectRegister,
91 SetMark,
95 GotoMarkLine,
99 GotoMarkChar,
102 RecordMacroTarget,
108 PlayMacroTarget {
115 count: usize,
116 },
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum Outcome {
122 Wait(PendingState),
124 Commit(crate::cmd::EngineCmd),
126 Cancel,
128 Forward,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
136pub enum Key {
137 Char(char),
138 Esc,
139 Enter,
140 Backspace,
141 Tab,
142 }
144
145pub fn step(state: PendingState, key: Key) -> Outcome {
146 match state {
147 PendingState::Replace { count } => match key {
148 Key::Esc => Outcome::Cancel,
149 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::ReplaceChar { ch, count }),
150 Key::Enter => Outcome::Commit(crate::cmd::EngineCmd::ReplaceChar { ch: '\n', count }),
151 _ => Outcome::Cancel,
152 },
153 PendingState::Find {
154 count,
155 forward,
156 till,
157 } => match key {
158 Key::Esc => Outcome::Cancel,
159 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::FindChar {
160 ch,
161 forward,
162 till,
163 count,
164 }),
165 _ => Outcome::Cancel,
167 },
168 PendingState::AfterG { count } => match key {
169 Key::Esc => Outcome::Cancel,
170 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::AfterGChord { ch, count }),
171 _ => Outcome::Cancel,
173 },
174 PendingState::AfterZ { count } => match key {
175 Key::Esc => Outcome::Cancel,
176 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::AfterZChord { ch, count }),
177 _ => Outcome::Cancel,
179 },
180 PendingState::AfterOp {
181 op,
182 count1,
183 inner_count,
184 } => match key {
185 Key::Esc => Outcome::Cancel,
186 Key::Char(d @ '0'..='9') => {
187 if d == '0' && inner_count == 0 {
189 let total = count1.max(1);
191 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpMotion {
192 op,
193 motion_key: '0',
194 total_count: total,
195 })
196 } else {
197 let new_inner = inner_count
198 .saturating_mul(10)
199 .saturating_add(d as usize - '0' as usize);
200 Outcome::Wait(PendingState::AfterOp {
201 op,
202 count1,
203 inner_count: new_inner,
204 })
205 }
206 }
207 Key::Char(ch) => {
208 let total = count1.max(1).saturating_mul(inner_count.max(1));
209 if ch == op.double_char() {
211 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpDouble {
212 op,
213 total_count: total,
214 })
215 } else if ch == 'i' {
219 Outcome::Wait(PendingState::OpTextObj {
220 op,
221 total_count: total,
222 inner: true,
223 })
224 } else if ch == 'a' {
225 Outcome::Wait(PendingState::OpTextObj {
226 op,
227 total_count: total,
228 inner: false,
229 })
230 } else if ch == 'g' {
235 Outcome::Wait(PendingState::OpG {
236 op,
237 total_count: total,
238 })
239 } else if ch == 'f' {
243 Outcome::Wait(PendingState::OpFind {
244 op,
245 total_count: total,
246 forward: true,
247 till: false,
248 })
249 } else if ch == 'F' {
250 Outcome::Wait(PendingState::OpFind {
251 op,
252 total_count: total,
253 forward: false,
254 till: false,
255 })
256 } else if ch == 't' {
257 Outcome::Wait(PendingState::OpFind {
258 op,
259 total_count: total,
260 forward: true,
261 till: true,
262 })
263 } else if ch == 'T' {
264 Outcome::Wait(PendingState::OpFind {
265 op,
266 total_count: total,
267 forward: false,
268 till: true,
269 })
270 } else if ch == '\'' || ch == '`' {
271 Outcome::Wait(PendingState::OpMark {
272 op,
273 linewise: ch == '\'',
274 })
275 } else {
276 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpMotion {
279 op,
280 motion_key: ch,
281 total_count: total,
282 })
283 }
284 }
285 _ => Outcome::Cancel,
287 },
288 PendingState::OpFind {
289 op,
290 total_count,
291 forward,
292 till,
293 } => match key {
294 Key::Esc => Outcome::Cancel,
295 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::ApplyOpFind {
296 op,
297 ch,
298 forward,
299 till,
300 total_count,
301 }),
302 _ => Outcome::Cancel,
304 },
305 PendingState::OpTextObj {
306 op,
307 total_count,
308 inner,
309 } => match key {
310 Key::Esc => Outcome::Cancel,
311 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::ApplyOpTextObj {
312 op,
313 ch,
314 inner,
315 total_count,
316 }),
317 _ => Outcome::Cancel,
319 },
320 PendingState::OpG { op, total_count } => match key {
321 Key::Esc => Outcome::Cancel,
322 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::ApplyOpG {
323 op,
324 ch,
325 total_count,
326 }),
327 _ => Outcome::Cancel,
330 },
331 PendingState::OpMark { op, linewise } => match key {
332 Key::Esc => Outcome::Cancel,
333 Key::Char(ch) => {
334 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpMark { op, ch, linewise })
335 }
336 _ => Outcome::Cancel,
338 },
339 PendingState::SelectRegister => match key {
340 Key::Esc => Outcome::Cancel,
341 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::SetPendingRegister { reg: ch }),
342 _ => Outcome::Cancel,
344 },
345 PendingState::SetMark => match key {
346 Key::Esc => Outcome::Cancel,
347 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::SetMark { ch }),
348 _ => Outcome::Cancel,
350 },
351 PendingState::GotoMarkLine => match key {
352 Key::Esc => Outcome::Cancel,
353 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::GotoMarkLine { ch }),
354 _ => Outcome::Cancel,
356 },
357 PendingState::GotoMarkChar => match key {
358 Key::Esc => Outcome::Cancel,
359 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::GotoMarkChar { ch }),
360 _ => Outcome::Cancel,
362 },
363 PendingState::RecordMacroTarget => match key {
364 Key::Esc => Outcome::Cancel,
365 Key::Char(ch) if ch.is_ascii_alphabetic() || ch.is_ascii_digit() => {
366 Outcome::Commit(crate::cmd::EngineCmd::StartMacroRecord { reg: ch })
367 }
368 _ => Outcome::Cancel,
370 },
371 PendingState::PlayMacroTarget { count } => match key {
372 Key::Esc => Outcome::Cancel,
373 Key::Char('@') => Outcome::Commit(crate::cmd::EngineCmd::PlayMacro { reg: '@', count }),
375 Key::Char(':') => Outcome::Commit(crate::cmd::EngineCmd::PlayMacro { reg: ':', count }),
377 Key::Char(ch) if ch.is_ascii_alphabetic() || ch.is_ascii_digit() => {
378 Outcome::Commit(crate::cmd::EngineCmd::PlayMacro { reg: ch, count })
379 }
380 _ => Outcome::Cancel,
382 },
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389 use crate::cmd::EngineCmd;
390 use crate::operator::OperatorKind;
391
392 #[test]
395 fn after_g_gg_commits() {
396 let state = PendingState::AfterG { count: 1 };
397 assert_eq!(
398 step(state, Key::Char('g')),
399 Outcome::Commit(EngineCmd::AfterGChord { ch: 'g', count: 1 })
400 );
401 }
402
403 #[test]
404 fn after_g_gv_commits() {
405 let state = PendingState::AfterG { count: 1 };
406 assert_eq!(
407 step(state, Key::Char('v')),
408 Outcome::Commit(EngineCmd::AfterGChord { ch: 'v', count: 1 })
409 );
410 }
411
412 #[test]
413 fn after_g_gu_operator_commits() {
414 let state = PendingState::AfterG { count: 1 };
416 assert_eq!(
417 step(state, Key::Char('U')),
418 Outcome::Commit(EngineCmd::AfterGChord { ch: 'U', count: 1 })
419 );
420 }
421
422 #[test]
423 fn after_g_gi_commits() {
424 let state = PendingState::AfterG { count: 1 };
425 assert_eq!(
426 step(state, Key::Char('i')),
427 Outcome::Commit(EngineCmd::AfterGChord { ch: 'i', count: 1 })
428 );
429 }
430
431 #[test]
432 fn after_g_esc_cancels() {
433 let state = PendingState::AfterG { count: 1 };
434 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
435 }
436
437 #[test]
438 fn after_g_count_carry_through() {
439 let state = PendingState::AfterG { count: 5 };
441 assert_eq!(
442 step(state, Key::Char('g')),
443 Outcome::Commit(EngineCmd::AfterGChord { ch: 'g', count: 5 })
444 );
445 }
446
447 #[test]
448 fn after_g_non_char_cancels() {
449 let state = PendingState::AfterG { count: 1 };
451 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
452 }
453
454 #[test]
455 fn g_ampersand_dispatches_via_g_chord() {
456 let state = PendingState::AfterG { count: 1 };
459 assert_eq!(
460 step(state, Key::Char('&')),
461 Outcome::Commit(EngineCmd::AfterGChord { ch: '&', count: 1 })
462 );
463 }
464
465 #[test]
468 fn after_z_zz_commits() {
469 let state = PendingState::AfterZ { count: 1 };
470 assert_eq!(
471 step(state, Key::Char('z')),
472 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 1 })
473 );
474 }
475
476 #[test]
477 fn after_z_zf_commits() {
478 let state = PendingState::AfterZ { count: 1 };
479 assert_eq!(
480 step(state, Key::Char('f')),
481 Outcome::Commit(EngineCmd::AfterZChord { ch: 'f', count: 1 })
482 );
483 }
484
485 #[test]
486 fn after_z_esc_cancels() {
487 let state = PendingState::AfterZ { count: 1 };
488 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
489 }
490
491 #[test]
492 fn after_z_count_carry_through() {
493 let state = PendingState::AfterZ { count: 3 };
495 assert_eq!(
496 step(state, Key::Char('z')),
497 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 3 })
498 );
499 }
500
501 #[test]
502 fn after_z_non_char_cancels() {
503 let state = PendingState::AfterZ { count: 1 };
505 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
506 }
507
508 fn after_op(op: OperatorKind, count1: usize) -> PendingState {
511 PendingState::AfterOp {
512 op,
513 count1,
514 inner_count: 0,
515 }
516 }
517
518 #[test]
519 fn op_d_then_w_commits_motion() {
520 let state = after_op(OperatorKind::Delete, 1);
521 assert_eq!(
522 step(state, Key::Char('w')),
523 Outcome::Commit(EngineCmd::ApplyOpMotion {
524 op: OperatorKind::Delete,
525 motion_key: 'w',
526 total_count: 1,
527 })
528 );
529 }
530
531 #[test]
532 fn op_d_then_d_commits_double() {
533 let state = after_op(OperatorKind::Delete, 1);
534 assert_eq!(
535 step(state, Key::Char('d')),
536 Outcome::Commit(EngineCmd::ApplyOpDouble {
537 op: OperatorKind::Delete,
538 total_count: 1,
539 })
540 );
541 }
542
543 #[test]
544 fn op_d_inner_count_d3w_commits_motion_with_count_3() {
545 let state = after_op(OperatorKind::Delete, 1);
547 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
549 panic!("expected Wait");
550 };
551 assert_eq!(
552 state2,
553 PendingState::AfterOp {
554 op: OperatorKind::Delete,
555 count1: 1,
556 inner_count: 3
557 }
558 );
559 assert_eq!(
561 step(state2, Key::Char('w')),
562 Outcome::Commit(EngineCmd::ApplyOpMotion {
563 op: OperatorKind::Delete,
564 motion_key: 'w',
565 total_count: 3,
566 })
567 );
568 }
569
570 #[test]
571 fn op_2d_d_commits_double_with_count_2() {
572 let state = after_op(OperatorKind::Delete, 2);
574 assert_eq!(
575 step(state, Key::Char('d')),
576 Outcome::Commit(EngineCmd::ApplyOpDouble {
577 op: OperatorKind::Delete,
578 total_count: 2,
579 })
580 );
581 }
582
583 #[test]
584 fn op_2d_3w_commits_motion_with_total_6() {
585 let state = after_op(OperatorKind::Delete, 2);
587 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
588 panic!("expected Wait");
589 };
590 assert_eq!(
591 step(state2, Key::Char('w')),
592 Outcome::Commit(EngineCmd::ApplyOpMotion {
593 op: OperatorKind::Delete,
594 motion_key: 'w',
595 total_count: 6,
596 })
597 );
598 }
599
600 #[test]
601 fn op_d_then_i_transitions_to_op_text_obj_inner() {
602 let state = after_op(OperatorKind::Delete, 1);
604 assert_eq!(
605 step(state, Key::Char('i')),
606 Outcome::Wait(PendingState::OpTextObj {
607 op: OperatorKind::Delete,
608 total_count: 1,
609 inner: true,
610 })
611 );
612 }
613
614 #[test]
615 fn op_d_then_a_transitions_to_op_text_obj_around() {
616 let state = after_op(OperatorKind::Delete, 1);
618 assert_eq!(
619 step(state, Key::Char('a')),
620 Outcome::Wait(PendingState::OpTextObj {
621 op: OperatorKind::Delete,
622 total_count: 1,
623 inner: false,
624 })
625 );
626 }
627
628 #[test]
629 fn op_d_then_g_transitions_to_op_g() {
630 let state = after_op(OperatorKind::Delete, 1);
631 assert_eq!(
632 step(state, Key::Char('g')),
633 Outcome::Wait(PendingState::OpG {
634 op: OperatorKind::Delete,
635 total_count: 1,
636 })
637 );
638 }
639
640 #[test]
641 fn op_d_then_f_transitions_to_op_find_forward_not_till() {
642 let state = after_op(OperatorKind::Delete, 1);
644 assert_eq!(
645 step(state, Key::Char('f')),
646 Outcome::Wait(PendingState::OpFind {
647 op: OperatorKind::Delete,
648 total_count: 1,
649 forward: true,
650 till: false,
651 })
652 );
653 }
654
655 #[test]
656 fn op_d_then_cap_f_transitions_to_op_find_backward_not_till() {
657 let state = after_op(OperatorKind::Delete, 1);
659 assert_eq!(
660 step(state, Key::Char('F')),
661 Outcome::Wait(PendingState::OpFind {
662 op: OperatorKind::Delete,
663 total_count: 1,
664 forward: false,
665 till: false,
666 })
667 );
668 }
669
670 #[test]
671 fn op_d_then_t_transitions_to_op_find_forward_till() {
672 let state = after_op(OperatorKind::Delete, 1);
674 assert_eq!(
675 step(state, Key::Char('t')),
676 Outcome::Wait(PendingState::OpFind {
677 op: OperatorKind::Delete,
678 total_count: 1,
679 forward: true,
680 till: true,
681 })
682 );
683 }
684
685 #[test]
686 fn op_d_then_cap_t_transitions_to_op_find_backward_till() {
687 let state = after_op(OperatorKind::Delete, 1);
689 assert_eq!(
690 step(state, Key::Char('T')),
691 Outcome::Wait(PendingState::OpFind {
692 op: OperatorKind::Delete,
693 total_count: 1,
694 forward: false,
695 till: true,
696 })
697 );
698 }
699
700 fn op_find(op: OperatorKind, total_count: usize, forward: bool, till: bool) -> PendingState {
703 PendingState::OpFind {
704 op,
705 total_count,
706 forward,
707 till,
708 }
709 }
710
711 #[test]
712 fn op_then_quote_or_backtick_then_mark_commits_apply_op_mark() {
713 let after_d = PendingState::AfterOp {
714 op: OperatorKind::Delete,
715 count1: 1,
716 inner_count: 0,
717 };
718 for (key, linewise) in [('\'', true), ('`', false)] {
719 let Outcome::Wait(state) = step(after_d, Key::Char(key)) else {
720 panic!("`d{key}` should wait for the mark name");
721 };
722 assert_eq!(
723 state,
724 PendingState::OpMark {
725 op: OperatorKind::Delete,
726 linewise,
727 }
728 );
729 assert_eq!(
730 step(state, Key::Char('a')),
731 Outcome::Commit(EngineCmd::ApplyOpMark {
732 op: OperatorKind::Delete,
733 ch: 'a',
734 linewise,
735 })
736 );
737 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
738 }
739 }
740
741 #[test]
742 fn op_d_then_f_then_x_commits_apply_op_find() {
743 let state = op_find(OperatorKind::Delete, 1, true, false);
745 assert_eq!(
746 step(state, Key::Char('x')),
747 Outcome::Commit(EngineCmd::ApplyOpFind {
748 op: OperatorKind::Delete,
749 ch: 'x',
750 forward: true,
751 till: false,
752 total_count: 1,
753 })
754 );
755 }
756
757 #[test]
758 fn op_d_then_cap_f_then_x_commits_apply_op_find_backward() {
759 let state = op_find(OperatorKind::Delete, 1, false, false);
761 assert_eq!(
762 step(state, Key::Char('x')),
763 Outcome::Commit(EngineCmd::ApplyOpFind {
764 op: OperatorKind::Delete,
765 ch: 'x',
766 forward: false,
767 till: false,
768 total_count: 1,
769 })
770 );
771 }
772
773 #[test]
774 fn op_d_then_t_then_x_commits_apply_op_find_till() {
775 let state = op_find(OperatorKind::Delete, 1, true, true);
777 assert_eq!(
778 step(state, Key::Char('x')),
779 Outcome::Commit(EngineCmd::ApplyOpFind {
780 op: OperatorKind::Delete,
781 ch: 'x',
782 forward: true,
783 till: true,
784 total_count: 1,
785 })
786 );
787 }
788
789 #[test]
790 fn op_d_then_cap_t_then_x_commits_apply_op_find_backward_till() {
791 let state = op_find(OperatorKind::Delete, 1, false, true);
793 assert_eq!(
794 step(state, Key::Char('x')),
795 Outcome::Commit(EngineCmd::ApplyOpFind {
796 op: OperatorKind::Delete,
797 ch: 'x',
798 forward: false,
799 till: true,
800 total_count: 1,
801 })
802 );
803 }
804
805 #[test]
806 fn op_2d_3f_x_commits_total_count_6() {
807 let state = PendingState::AfterOp {
810 op: OperatorKind::Delete,
811 count1: 2,
812 inner_count: 3,
813 };
814 let Outcome::Wait(op_find_state) = step(state, Key::Char('f')) else {
815 panic!("expected Wait(OpFind)");
816 };
817 assert_eq!(
818 op_find_state,
819 PendingState::OpFind {
820 op: OperatorKind::Delete,
821 total_count: 6,
822 forward: true,
823 till: false,
824 }
825 );
826 assert_eq!(
827 step(op_find_state, Key::Char('x')),
828 Outcome::Commit(EngineCmd::ApplyOpFind {
829 op: OperatorKind::Delete,
830 ch: 'x',
831 forward: true,
832 till: false,
833 total_count: 6,
834 })
835 );
836 }
837
838 #[test]
839 fn op_d_f_then_esc_cancels() {
840 let state = op_find(OperatorKind::Delete, 1, true, false);
842 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
843 }
844
845 #[test]
846 fn op_d_f_then_enter_cancels() {
847 let state = op_find(OperatorKind::Delete, 1, true, false);
849 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
850 }
851
852 #[test]
853 fn op_d_then_esc_cancels() {
854 let state = after_op(OperatorKind::Delete, 1);
855 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
856 }
857
858 #[test]
859 fn op_d_non_char_cancels() {
860 let state = after_op(OperatorKind::Delete, 1);
861 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
862 }
863
864 fn op_text_obj(op: OperatorKind, total_count: usize, inner: bool) -> PendingState {
867 PendingState::OpTextObj {
868 op,
869 total_count,
870 inner,
871 }
872 }
873
874 #[test]
875 fn op_d_then_i_then_w_commits_apply_op_text_obj_inner() {
876 let state = op_text_obj(OperatorKind::Delete, 1, true);
878 assert_eq!(
879 step(state, Key::Char('w')),
880 Outcome::Commit(EngineCmd::ApplyOpTextObj {
881 op: OperatorKind::Delete,
882 ch: 'w',
883 inner: true,
884 total_count: 1,
885 })
886 );
887 }
888
889 #[test]
890 fn op_d_then_a_then_w_commits_apply_op_text_obj_around() {
891 let state = op_text_obj(OperatorKind::Delete, 1, false);
893 assert_eq!(
894 step(state, Key::Char('w')),
895 Outcome::Commit(EngineCmd::ApplyOpTextObj {
896 op: OperatorKind::Delete,
897 ch: 'w',
898 inner: false,
899 total_count: 1,
900 })
901 );
902 }
903
904 #[test]
905 fn op_d_then_i_then_quote_commits_with_quote_char() {
906 let state = op_text_obj(OperatorKind::Delete, 1, true);
908 assert_eq!(
909 step(state, Key::Char('"')),
910 Outcome::Commit(EngineCmd::ApplyOpTextObj {
911 op: OperatorKind::Delete,
912 ch: '"',
913 inner: true,
914 total_count: 1,
915 })
916 );
917 }
918
919 #[test]
920 fn op_d_then_i_then_paren_commits_with_paren() {
921 let state = op_text_obj(OperatorKind::Delete, 1, true);
923 assert_eq!(
924 step(state, Key::Char('(')),
925 Outcome::Commit(EngineCmd::ApplyOpTextObj {
926 op: OperatorKind::Delete,
927 ch: '(',
928 inner: true,
929 total_count: 1,
930 })
931 );
932 }
933
934 #[test]
935 fn op_c_then_i_then_p_commits_change_paragraph_inner() {
936 let state = op_text_obj(OperatorKind::Change, 1, true);
938 assert_eq!(
939 step(state, Key::Char('p')),
940 Outcome::Commit(EngineCmd::ApplyOpTextObj {
941 op: OperatorKind::Change,
942 ch: 'p',
943 inner: true,
944 total_count: 1,
945 })
946 );
947 }
948
949 #[test]
950 fn op_d_i_then_esc_cancels() {
951 let state = op_text_obj(OperatorKind::Delete, 1, true);
953 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
954 }
955
956 #[test]
957 fn op_d_i_then_enter_cancels() {
958 let state = op_text_obj(OperatorKind::Delete, 1, true);
960 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
961 }
962
963 #[test]
964 fn op_2d_i_w_total_count_2_preserved() {
965 let state = PendingState::AfterOp {
968 op: OperatorKind::Delete,
969 count1: 2,
970 inner_count: 0,
971 };
972 let Outcome::Wait(obj_state) = step(state, Key::Char('i')) else {
973 panic!("expected Wait(OpTextObj)");
974 };
975 assert_eq!(
976 obj_state,
977 PendingState::OpTextObj {
978 op: OperatorKind::Delete,
979 total_count: 2,
980 inner: true,
981 }
982 );
983 assert_eq!(
984 step(obj_state, Key::Char('w')),
985 Outcome::Commit(EngineCmd::ApplyOpTextObj {
986 op: OperatorKind::Delete,
987 ch: 'w',
988 inner: true,
989 total_count: 2,
990 })
991 );
992 }
993
994 #[test]
995 fn op_d_bare_zero_is_line_start_motion() {
996 let state = after_op(OperatorKind::Delete, 1);
998 assert_eq!(
999 step(state, Key::Char('0')),
1000 Outcome::Commit(EngineCmd::ApplyOpMotion {
1001 op: OperatorKind::Delete,
1002 motion_key: '0',
1003 total_count: 1,
1004 })
1005 );
1006 }
1007
1008 #[test]
1009 fn op_d_zero_accumulates_when_inner_count_nonzero() {
1010 let state = after_op(OperatorKind::Delete, 1);
1012 let Outcome::Wait(s2) = step(state, Key::Char('1')) else {
1013 panic!("expected Wait");
1014 };
1015 let Outcome::Wait(s3) = step(s2, Key::Char('0')) else {
1016 panic!("expected Wait");
1017 };
1018 assert_eq!(
1019 s3,
1020 PendingState::AfterOp {
1021 op: OperatorKind::Delete,
1022 count1: 1,
1023 inner_count: 10,
1024 }
1025 );
1026 assert_eq!(
1027 step(s3, Key::Char('w')),
1028 Outcome::Commit(EngineCmd::ApplyOpMotion {
1029 op: OperatorKind::Delete,
1030 motion_key: 'w',
1031 total_count: 10,
1032 })
1033 );
1034 }
1035
1036 #[test]
1037 fn op_total_count_saturates_instead_of_overflowing() {
1038 let state = PendingState::AfterOp {
1041 op: OperatorKind::Delete,
1042 count1: usize::MAX,
1043 inner_count: 2,
1044 };
1045 assert_eq!(
1046 step(state, Key::Char('w')),
1047 Outcome::Commit(EngineCmd::ApplyOpMotion {
1048 op: OperatorKind::Delete,
1049 motion_key: 'w',
1050 total_count: usize::MAX,
1051 })
1052 );
1053 let Outcome::Wait(folded) = step(state, Key::Char('f')) else {
1055 panic!("expected Wait(OpFind)");
1056 };
1057 assert_eq!(
1058 folded,
1059 PendingState::OpFind {
1060 op: OperatorKind::Delete,
1061 total_count: usize::MAX,
1062 forward: true,
1063 till: false,
1064 }
1065 );
1066 }
1067
1068 #[test]
1071 fn op_yank_doubled() {
1072 let state = after_op(OperatorKind::Yank, 1);
1073 assert_eq!(
1074 step(state, Key::Char('y')),
1075 Outcome::Commit(EngineCmd::ApplyOpDouble {
1076 op: OperatorKind::Yank,
1077 total_count: 1,
1078 })
1079 );
1080 }
1081
1082 #[test]
1083 fn op_change_doubled() {
1084 let state = after_op(OperatorKind::Change, 1);
1085 assert_eq!(
1086 step(state, Key::Char('c')),
1087 Outcome::Commit(EngineCmd::ApplyOpDouble {
1088 op: OperatorKind::Change,
1089 total_count: 1,
1090 })
1091 );
1092 }
1093
1094 #[test]
1095 fn op_indent_doubled() {
1096 let state = after_op(OperatorKind::Indent, 1);
1097 assert_eq!(
1098 step(state, Key::Char('>')),
1099 Outcome::Commit(EngineCmd::ApplyOpDouble {
1100 op: OperatorKind::Indent,
1101 total_count: 1,
1102 })
1103 );
1104 }
1105
1106 #[test]
1107 fn op_outdent_doubled() {
1108 let state = after_op(OperatorKind::Outdent, 1);
1109 assert_eq!(
1110 step(state, Key::Char('<')),
1111 Outcome::Commit(EngineCmd::ApplyOpDouble {
1112 op: OperatorKind::Outdent,
1113 total_count: 1,
1114 })
1115 );
1116 }
1117
1118 #[test]
1121 fn op_uppercase_then_cap_u_commits_double() {
1122 let state = after_op(OperatorKind::Uppercase, 1);
1124 assert_eq!(
1125 step(state, Key::Char('U')),
1126 Outcome::Commit(EngineCmd::ApplyOpDouble {
1127 op: OperatorKind::Uppercase,
1128 total_count: 1,
1129 })
1130 );
1131 }
1132
1133 #[test]
1134 fn op_lowercase_then_u_commits_double() {
1135 let state = after_op(OperatorKind::Lowercase, 1);
1137 assert_eq!(
1138 step(state, Key::Char('u')),
1139 Outcome::Commit(EngineCmd::ApplyOpDouble {
1140 op: OperatorKind::Lowercase,
1141 total_count: 1,
1142 })
1143 );
1144 }
1145
1146 #[test]
1147 fn op_togglecase_then_tilde_commits_double() {
1148 let state = after_op(OperatorKind::ToggleCase, 1);
1150 assert_eq!(
1151 step(state, Key::Char('~')),
1152 Outcome::Commit(EngineCmd::ApplyOpDouble {
1153 op: OperatorKind::ToggleCase,
1154 total_count: 1,
1155 })
1156 );
1157 }
1158
1159 #[test]
1160 fn op_reflow_then_q_commits_double() {
1161 let state = after_op(OperatorKind::Reflow, 1);
1163 assert_eq!(
1164 step(state, Key::Char('q')),
1165 Outcome::Commit(EngineCmd::ApplyOpDouble {
1166 op: OperatorKind::Reflow,
1167 total_count: 1,
1168 })
1169 );
1170 }
1171
1172 #[test]
1173 fn op_uppercase_then_w_commits_motion() {
1174 let state = after_op(OperatorKind::Uppercase, 1);
1176 assert_eq!(
1177 step(state, Key::Char('w')),
1178 Outcome::Commit(EngineCmd::ApplyOpMotion {
1179 op: OperatorKind::Uppercase,
1180 motion_key: 'w',
1181 total_count: 1,
1182 })
1183 );
1184 }
1185
1186 #[test]
1187 fn op_reflow_then_ap_commits_text_obj() {
1188 let state = after_op(OperatorKind::Reflow, 1);
1191 let Outcome::Wait(obj_state) = step(state, Key::Char('a')) else {
1192 panic!("expected Wait(OpTextObj)");
1193 };
1194 assert_eq!(
1195 obj_state,
1196 PendingState::OpTextObj {
1197 op: OperatorKind::Reflow,
1198 total_count: 1,
1199 inner: false,
1200 }
1201 );
1202 assert_eq!(
1204 step(obj_state, Key::Char('p')),
1205 Outcome::Commit(EngineCmd::ApplyOpTextObj {
1206 op: OperatorKind::Reflow,
1207 ch: 'p',
1208 inner: false,
1209 total_count: 1,
1210 })
1211 );
1212 }
1213
1214 #[test]
1215 fn op_yank_motion() {
1216 let state = after_op(OperatorKind::Yank, 1);
1217 assert_eq!(
1218 step(state, Key::Char('$')),
1219 Outcome::Commit(EngineCmd::ApplyOpMotion {
1220 op: OperatorKind::Yank,
1221 motion_key: '$',
1222 total_count: 1,
1223 })
1224 );
1225 }
1226
1227 #[test]
1228 fn op_change_motion() {
1229 let state = after_op(OperatorKind::Change, 1);
1230 assert_eq!(
1231 step(state, Key::Char('w')),
1232 Outcome::Commit(EngineCmd::ApplyOpMotion {
1233 op: OperatorKind::Change,
1234 motion_key: 'w',
1235 total_count: 1,
1236 })
1237 );
1238 }
1239
1240 #[test]
1241 fn op_indent_motion() {
1242 let state = after_op(OperatorKind::Indent, 1);
1243 assert_eq!(
1244 step(state, Key::Char('j')),
1245 Outcome::Commit(EngineCmd::ApplyOpMotion {
1246 op: OperatorKind::Indent,
1247 motion_key: 'j',
1248 total_count: 1,
1249 })
1250 );
1251 }
1252
1253 #[test]
1254 fn op_outdent_motion() {
1255 let state = after_op(OperatorKind::Outdent, 1);
1256 assert_eq!(
1257 step(state, Key::Char('k')),
1258 Outcome::Commit(EngineCmd::ApplyOpMotion {
1259 op: OperatorKind::Outdent,
1260 motion_key: 'k',
1261 total_count: 1,
1262 })
1263 );
1264 }
1265
1266 fn op_g(op: OperatorKind, total_count: usize) -> PendingState {
1269 PendingState::OpG { op, total_count }
1270 }
1271
1272 #[test]
1273 fn op_d_then_g_then_g_commits_apply_op_g_for_gg() {
1274 let state = op_g(OperatorKind::Delete, 1);
1276 assert_eq!(
1277 step(state, Key::Char('g')),
1278 Outcome::Commit(EngineCmd::ApplyOpG {
1279 op: OperatorKind::Delete,
1280 ch: 'g',
1281 total_count: 1,
1282 })
1283 );
1284 }
1285
1286 #[test]
1287 fn op_d_then_g_then_e_commits_for_ge() {
1288 let state = op_g(OperatorKind::Delete, 1);
1290 assert_eq!(
1291 step(state, Key::Char('e')),
1292 Outcome::Commit(EngineCmd::ApplyOpG {
1293 op: OperatorKind::Delete,
1294 ch: 'e',
1295 total_count: 1,
1296 })
1297 );
1298 }
1299
1300 #[test]
1301 fn op_d_then_g_then_j_commits_for_gj() {
1302 let state = op_g(OperatorKind::Delete, 1);
1304 assert_eq!(
1305 step(state, Key::Char('j')),
1306 Outcome::Commit(EngineCmd::ApplyOpG {
1307 op: OperatorKind::Delete,
1308 ch: 'j',
1309 total_count: 1,
1310 })
1311 );
1312 }
1313
1314 #[test]
1315 fn op_2d_3g_g_total_count_6() {
1316 let state = PendingState::AfterOp {
1319 op: OperatorKind::Delete,
1320 count1: 2,
1321 inner_count: 3,
1322 };
1323 let Outcome::Wait(op_g_state) = step(state, Key::Char('g')) else {
1324 panic!("expected Wait(OpG)");
1325 };
1326 assert_eq!(
1327 op_g_state,
1328 PendingState::OpG {
1329 op: OperatorKind::Delete,
1330 total_count: 6,
1331 }
1332 );
1333 assert_eq!(
1334 step(op_g_state, Key::Char('g')),
1335 Outcome::Commit(EngineCmd::ApplyOpG {
1336 op: OperatorKind::Delete,
1337 ch: 'g',
1338 total_count: 6,
1339 })
1340 );
1341 }
1342
1343 #[test]
1344 fn op_d_g_then_esc_cancels() {
1345 let state = op_g(OperatorKind::Delete, 1);
1347 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1348 }
1349
1350 #[test]
1351 fn op_d_g_then_enter_cancels() {
1352 let state = op_g(OperatorKind::Delete, 1);
1354 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1355 }
1356
1357 #[test]
1358 fn op_c_then_g_then_g_commits_change_op_g() {
1359 let state = op_g(OperatorKind::Change, 1);
1361 assert_eq!(
1362 step(state, Key::Char('g')),
1363 Outcome::Commit(EngineCmd::ApplyOpG {
1364 op: OperatorKind::Change,
1365 ch: 'g',
1366 total_count: 1,
1367 })
1368 );
1369 }
1370
1371 #[test]
1374 fn select_register_a_commits() {
1375 let state = PendingState::SelectRegister;
1377 assert_eq!(
1378 step(state, Key::Char('a')),
1379 Outcome::Commit(EngineCmd::SetPendingRegister { reg: 'a' })
1380 );
1381 }
1382
1383 #[test]
1384 fn select_register_plus_commits() {
1385 let state = PendingState::SelectRegister;
1387 assert_eq!(
1388 step(state, Key::Char('+')),
1389 Outcome::Commit(EngineCmd::SetPendingRegister { reg: '+' })
1390 );
1391 }
1392
1393 #[test]
1394 fn select_register_underscore_commits() {
1395 let state = PendingState::SelectRegister;
1397 assert_eq!(
1398 step(state, Key::Char('_')),
1399 Outcome::Commit(EngineCmd::SetPendingRegister { reg: '_' })
1400 );
1401 }
1402
1403 #[test]
1404 fn select_register_esc_cancels() {
1405 let state = PendingState::SelectRegister;
1406 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1407 }
1408
1409 #[test]
1410 fn select_register_enter_cancels() {
1411 let state = PendingState::SelectRegister;
1413 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1414 }
1415
1416 #[test]
1419 fn set_mark_a_commits() {
1420 let state = PendingState::SetMark;
1422 assert_eq!(
1423 step(state, Key::Char('a')),
1424 Outcome::Commit(EngineCmd::SetMark { ch: 'a' })
1425 );
1426 }
1427
1428 #[test]
1429 fn set_mark_esc_cancels() {
1430 let state = PendingState::SetMark;
1431 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1432 }
1433
1434 #[test]
1435 fn set_mark_enter_cancels() {
1436 let state = PendingState::SetMark;
1438 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1439 }
1440
1441 #[test]
1444 fn goto_mark_line_a_commits() {
1445 let state = PendingState::GotoMarkLine;
1447 assert_eq!(
1448 step(state, Key::Char('a')),
1449 Outcome::Commit(EngineCmd::GotoMarkLine { ch: 'a' })
1450 );
1451 }
1452
1453 #[test]
1454 fn goto_mark_line_esc_cancels() {
1455 let state = PendingState::GotoMarkLine;
1456 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1457 }
1458
1459 #[test]
1460 fn goto_mark_line_enter_cancels() {
1461 let state = PendingState::GotoMarkLine;
1463 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1464 }
1465
1466 #[test]
1469 fn goto_mark_char_a_commits() {
1470 let state = PendingState::GotoMarkChar;
1472 assert_eq!(
1473 step(state, Key::Char('a')),
1474 Outcome::Commit(EngineCmd::GotoMarkChar { ch: 'a' })
1475 );
1476 }
1477
1478 #[test]
1479 fn goto_mark_char_esc_cancels() {
1480 let state = PendingState::GotoMarkChar;
1481 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1482 }
1483
1484 #[test]
1485 fn goto_mark_char_enter_cancels() {
1486 let state = PendingState::GotoMarkChar;
1488 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1489 }
1490
1491 #[test]
1494 fn record_macro_target_a_commits_start_record() {
1495 let state = PendingState::RecordMacroTarget;
1497 assert_eq!(
1498 step(state, Key::Char('a')),
1499 Outcome::Commit(EngineCmd::StartMacroRecord { reg: 'a' })
1500 );
1501 }
1502
1503 #[test]
1504 fn record_macro_target_capital_a_commits_start_record() {
1505 let state = PendingState::RecordMacroTarget;
1507 assert_eq!(
1508 step(state, Key::Char('A')),
1509 Outcome::Commit(EngineCmd::StartMacroRecord { reg: 'A' })
1510 );
1511 }
1512
1513 #[test]
1514 fn record_macro_target_esc_cancels() {
1515 let state = PendingState::RecordMacroTarget;
1516 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1517 }
1518
1519 #[test]
1520 fn record_macro_target_enter_cancels() {
1521 let state = PendingState::RecordMacroTarget;
1523 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1524 }
1525
1526 #[test]
1527 fn record_macro_target_non_alnum_cancels() {
1528 let state = PendingState::RecordMacroTarget;
1530 assert_eq!(step(state, Key::Char('!')), Outcome::Cancel);
1531 }
1532
1533 #[test]
1536 fn play_macro_target_a_commits_play() {
1537 let state = PendingState::PlayMacroTarget { count: 1 };
1539 assert_eq!(
1540 step(state, Key::Char('a')),
1541 Outcome::Commit(EngineCmd::PlayMacro { reg: 'a', count: 1 })
1542 );
1543 }
1544
1545 #[test]
1546 fn play_macro_target_at_sign_commits_play_with_at() {
1547 let state = PendingState::PlayMacroTarget { count: 1 };
1549 assert_eq!(
1550 step(state, Key::Char('@')),
1551 Outcome::Commit(EngineCmd::PlayMacro { reg: '@', count: 1 })
1552 );
1553 }
1554
1555 #[test]
1556 fn play_macro_target_with_count_3_preserves_count() {
1557 let state = PendingState::PlayMacroTarget { count: 3 };
1559 assert_eq!(
1560 step(state, Key::Char('a')),
1561 Outcome::Commit(EngineCmd::PlayMacro { reg: 'a', count: 3 })
1562 );
1563 }
1564
1565 #[test]
1566 fn play_macro_target_esc_cancels() {
1567 let state = PendingState::PlayMacroTarget { count: 1 };
1568 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1569 }
1570
1571 #[test]
1572 fn play_macro_target_enter_cancels() {
1573 let state = PendingState::PlayMacroTarget { count: 1 };
1575 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1576 }
1577
1578 #[test]
1579 fn play_macro_target_non_alnum_cancels() {
1580 let state = PendingState::PlayMacroTarget { count: 1 };
1582 assert_eq!(step(state, Key::Char('!')), Outcome::Cancel);
1583 }
1584
1585 #[test]
1586 fn play_macro_target_colon_commits_play_macro() {
1587 let state = PendingState::PlayMacroTarget { count: 1 };
1590 assert_eq!(
1591 step(state, Key::Char(':')),
1592 Outcome::Commit(EngineCmd::PlayMacro { reg: ':', count: 1 })
1593 );
1594 }
1595
1596 #[test]
1597 fn play_macro_target_colon_with_count_3_commits() {
1598 let state = PendingState::PlayMacroTarget { count: 3 };
1600 assert_eq!(
1601 step(state, Key::Char(':')),
1602 Outcome::Commit(EngineCmd::PlayMacro { reg: ':', count: 3 })
1603 );
1604 }
1605}