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