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) * 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: count1.max(1) * inner_count.max(1),
215 inner: true,
216 })
217 } else if ch == 'a' {
218 Outcome::Wait(PendingState::OpTextObj {
219 op,
220 total_count: count1.max(1) * inner_count.max(1),
221 inner: false,
222 })
223 } else if ch == 'g' {
228 Outcome::Wait(PendingState::OpG {
229 op,
230 total_count: count1.max(1) * inner_count.max(1),
231 })
232 } else if ch == 'f' {
236 Outcome::Wait(PendingState::OpFind {
237 op,
238 total_count: count1.max(1) * inner_count.max(1),
239 forward: true,
240 till: false,
241 })
242 } else if ch == 'F' {
243 Outcome::Wait(PendingState::OpFind {
244 op,
245 total_count: count1.max(1) * inner_count.max(1),
246 forward: false,
247 till: false,
248 })
249 } else if ch == 't' {
250 Outcome::Wait(PendingState::OpFind {
251 op,
252 total_count: count1.max(1) * inner_count.max(1),
253 forward: true,
254 till: true,
255 })
256 } else if ch == 'T' {
257 Outcome::Wait(PendingState::OpFind {
258 op,
259 total_count: count1.max(1) * inner_count.max(1),
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]
437 fn after_z_zz_commits() {
438 let state = PendingState::AfterZ { count: 1 };
439 assert_eq!(
440 step(state, Key::Char('z')),
441 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 1 })
442 );
443 }
444
445 #[test]
446 fn after_z_zf_commits() {
447 let state = PendingState::AfterZ { count: 1 };
448 assert_eq!(
449 step(state, Key::Char('f')),
450 Outcome::Commit(EngineCmd::AfterZChord { ch: 'f', count: 1 })
451 );
452 }
453
454 #[test]
455 fn after_z_esc_cancels() {
456 let state = PendingState::AfterZ { count: 1 };
457 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
458 }
459
460 #[test]
461 fn after_z_count_carry_through() {
462 let state = PendingState::AfterZ { count: 3 };
464 assert_eq!(
465 step(state, Key::Char('z')),
466 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 3 })
467 );
468 }
469
470 #[test]
471 fn after_z_non_char_cancels() {
472 let state = PendingState::AfterZ { count: 1 };
474 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
475 }
476
477 fn after_op(op: OperatorKind, count1: usize) -> PendingState {
480 PendingState::AfterOp {
481 op,
482 count1,
483 inner_count: 0,
484 }
485 }
486
487 #[test]
488 fn op_d_then_w_commits_motion() {
489 let state = after_op(OperatorKind::Delete, 1);
490 assert_eq!(
491 step(state, Key::Char('w')),
492 Outcome::Commit(EngineCmd::ApplyOpMotion {
493 op: OperatorKind::Delete,
494 motion_key: 'w',
495 total_count: 1,
496 })
497 );
498 }
499
500 #[test]
501 fn op_d_then_d_commits_double() {
502 let state = after_op(OperatorKind::Delete, 1);
503 assert_eq!(
504 step(state, Key::Char('d')),
505 Outcome::Commit(EngineCmd::ApplyOpDouble {
506 op: OperatorKind::Delete,
507 total_count: 1,
508 })
509 );
510 }
511
512 #[test]
513 fn op_d_inner_count_d3w_commits_motion_with_count_3() {
514 let state = after_op(OperatorKind::Delete, 1);
516 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
518 panic!("expected Wait");
519 };
520 assert_eq!(
521 state2,
522 PendingState::AfterOp {
523 op: OperatorKind::Delete,
524 count1: 1,
525 inner_count: 3
526 }
527 );
528 assert_eq!(
530 step(state2, Key::Char('w')),
531 Outcome::Commit(EngineCmd::ApplyOpMotion {
532 op: OperatorKind::Delete,
533 motion_key: 'w',
534 total_count: 3,
535 })
536 );
537 }
538
539 #[test]
540 fn op_2d_d_commits_double_with_count_2() {
541 let state = after_op(OperatorKind::Delete, 2);
543 assert_eq!(
544 step(state, Key::Char('d')),
545 Outcome::Commit(EngineCmd::ApplyOpDouble {
546 op: OperatorKind::Delete,
547 total_count: 2,
548 })
549 );
550 }
551
552 #[test]
553 fn op_2d_3w_commits_motion_with_total_6() {
554 let state = after_op(OperatorKind::Delete, 2);
556 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
557 panic!("expected Wait");
558 };
559 assert_eq!(
560 step(state2, Key::Char('w')),
561 Outcome::Commit(EngineCmd::ApplyOpMotion {
562 op: OperatorKind::Delete,
563 motion_key: 'w',
564 total_count: 6,
565 })
566 );
567 }
568
569 #[test]
570 fn op_d_then_i_transitions_to_op_text_obj_inner() {
571 let state = after_op(OperatorKind::Delete, 1);
573 assert_eq!(
574 step(state, Key::Char('i')),
575 Outcome::Wait(PendingState::OpTextObj {
576 op: OperatorKind::Delete,
577 total_count: 1,
578 inner: true,
579 })
580 );
581 }
582
583 #[test]
584 fn op_d_then_a_transitions_to_op_text_obj_around() {
585 let state = after_op(OperatorKind::Delete, 1);
587 assert_eq!(
588 step(state, Key::Char('a')),
589 Outcome::Wait(PendingState::OpTextObj {
590 op: OperatorKind::Delete,
591 total_count: 1,
592 inner: false,
593 })
594 );
595 }
596
597 #[test]
598 fn op_d_then_g_transitions_to_op_g() {
599 let state = after_op(OperatorKind::Delete, 1);
600 assert_eq!(
601 step(state, Key::Char('g')),
602 Outcome::Wait(PendingState::OpG {
603 op: OperatorKind::Delete,
604 total_count: 1,
605 })
606 );
607 }
608
609 #[test]
610 fn op_d_then_f_transitions_to_op_find_forward_not_till() {
611 let state = after_op(OperatorKind::Delete, 1);
613 assert_eq!(
614 step(state, Key::Char('f')),
615 Outcome::Wait(PendingState::OpFind {
616 op: OperatorKind::Delete,
617 total_count: 1,
618 forward: true,
619 till: false,
620 })
621 );
622 }
623
624 #[test]
625 fn op_d_then_cap_f_transitions_to_op_find_backward_not_till() {
626 let state = after_op(OperatorKind::Delete, 1);
628 assert_eq!(
629 step(state, Key::Char('F')),
630 Outcome::Wait(PendingState::OpFind {
631 op: OperatorKind::Delete,
632 total_count: 1,
633 forward: false,
634 till: false,
635 })
636 );
637 }
638
639 #[test]
640 fn op_d_then_t_transitions_to_op_find_forward_till() {
641 let state = after_op(OperatorKind::Delete, 1);
643 assert_eq!(
644 step(state, Key::Char('t')),
645 Outcome::Wait(PendingState::OpFind {
646 op: OperatorKind::Delete,
647 total_count: 1,
648 forward: true,
649 till: true,
650 })
651 );
652 }
653
654 #[test]
655 fn op_d_then_cap_t_transitions_to_op_find_backward_till() {
656 let state = after_op(OperatorKind::Delete, 1);
658 assert_eq!(
659 step(state, Key::Char('T')),
660 Outcome::Wait(PendingState::OpFind {
661 op: OperatorKind::Delete,
662 total_count: 1,
663 forward: false,
664 till: true,
665 })
666 );
667 }
668
669 fn op_find(op: OperatorKind, total_count: usize, forward: bool, till: bool) -> PendingState {
672 PendingState::OpFind {
673 op,
674 total_count,
675 forward,
676 till,
677 }
678 }
679
680 #[test]
681 fn op_d_then_f_then_x_commits_apply_op_find() {
682 let state = op_find(OperatorKind::Delete, 1, true, false);
684 assert_eq!(
685 step(state, Key::Char('x')),
686 Outcome::Commit(EngineCmd::ApplyOpFind {
687 op: OperatorKind::Delete,
688 ch: 'x',
689 forward: true,
690 till: false,
691 total_count: 1,
692 })
693 );
694 }
695
696 #[test]
697 fn op_d_then_cap_f_then_x_commits_apply_op_find_backward() {
698 let state = op_find(OperatorKind::Delete, 1, false, false);
700 assert_eq!(
701 step(state, Key::Char('x')),
702 Outcome::Commit(EngineCmd::ApplyOpFind {
703 op: OperatorKind::Delete,
704 ch: 'x',
705 forward: false,
706 till: false,
707 total_count: 1,
708 })
709 );
710 }
711
712 #[test]
713 fn op_d_then_t_then_x_commits_apply_op_find_till() {
714 let state = op_find(OperatorKind::Delete, 1, true, true);
716 assert_eq!(
717 step(state, Key::Char('x')),
718 Outcome::Commit(EngineCmd::ApplyOpFind {
719 op: OperatorKind::Delete,
720 ch: 'x',
721 forward: true,
722 till: true,
723 total_count: 1,
724 })
725 );
726 }
727
728 #[test]
729 fn op_d_then_cap_t_then_x_commits_apply_op_find_backward_till() {
730 let state = op_find(OperatorKind::Delete, 1, false, true);
732 assert_eq!(
733 step(state, Key::Char('x')),
734 Outcome::Commit(EngineCmd::ApplyOpFind {
735 op: OperatorKind::Delete,
736 ch: 'x',
737 forward: false,
738 till: true,
739 total_count: 1,
740 })
741 );
742 }
743
744 #[test]
745 fn op_2d_3f_x_commits_total_count_6() {
746 let state = PendingState::AfterOp {
749 op: OperatorKind::Delete,
750 count1: 2,
751 inner_count: 3,
752 };
753 let Outcome::Wait(op_find_state) = step(state, Key::Char('f')) else {
754 panic!("expected Wait(OpFind)");
755 };
756 assert_eq!(
757 op_find_state,
758 PendingState::OpFind {
759 op: OperatorKind::Delete,
760 total_count: 6,
761 forward: true,
762 till: false,
763 }
764 );
765 assert_eq!(
766 step(op_find_state, Key::Char('x')),
767 Outcome::Commit(EngineCmd::ApplyOpFind {
768 op: OperatorKind::Delete,
769 ch: 'x',
770 forward: true,
771 till: false,
772 total_count: 6,
773 })
774 );
775 }
776
777 #[test]
778 fn op_d_f_then_esc_cancels() {
779 let state = op_find(OperatorKind::Delete, 1, true, false);
781 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
782 }
783
784 #[test]
785 fn op_d_f_then_enter_cancels() {
786 let state = op_find(OperatorKind::Delete, 1, true, false);
788 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
789 }
790
791 #[test]
792 fn op_d_then_esc_cancels() {
793 let state = after_op(OperatorKind::Delete, 1);
794 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
795 }
796
797 #[test]
798 fn op_d_non_char_cancels() {
799 let state = after_op(OperatorKind::Delete, 1);
800 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
801 }
802
803 fn op_text_obj(op: OperatorKind, total_count: usize, inner: bool) -> PendingState {
806 PendingState::OpTextObj {
807 op,
808 total_count,
809 inner,
810 }
811 }
812
813 #[test]
814 fn op_d_then_i_then_w_commits_apply_op_text_obj_inner() {
815 let state = op_text_obj(OperatorKind::Delete, 1, true);
817 assert_eq!(
818 step(state, Key::Char('w')),
819 Outcome::Commit(EngineCmd::ApplyOpTextObj {
820 op: OperatorKind::Delete,
821 ch: 'w',
822 inner: true,
823 total_count: 1,
824 })
825 );
826 }
827
828 #[test]
829 fn op_d_then_a_then_w_commits_apply_op_text_obj_around() {
830 let state = op_text_obj(OperatorKind::Delete, 1, false);
832 assert_eq!(
833 step(state, Key::Char('w')),
834 Outcome::Commit(EngineCmd::ApplyOpTextObj {
835 op: OperatorKind::Delete,
836 ch: 'w',
837 inner: false,
838 total_count: 1,
839 })
840 );
841 }
842
843 #[test]
844 fn op_d_then_i_then_quote_commits_with_quote_char() {
845 let state = op_text_obj(OperatorKind::Delete, 1, true);
847 assert_eq!(
848 step(state, Key::Char('"')),
849 Outcome::Commit(EngineCmd::ApplyOpTextObj {
850 op: OperatorKind::Delete,
851 ch: '"',
852 inner: true,
853 total_count: 1,
854 })
855 );
856 }
857
858 #[test]
859 fn op_d_then_i_then_paren_commits_with_paren() {
860 let state = op_text_obj(OperatorKind::Delete, 1, true);
862 assert_eq!(
863 step(state, Key::Char('(')),
864 Outcome::Commit(EngineCmd::ApplyOpTextObj {
865 op: OperatorKind::Delete,
866 ch: '(',
867 inner: true,
868 total_count: 1,
869 })
870 );
871 }
872
873 #[test]
874 fn op_c_then_i_then_p_commits_change_paragraph_inner() {
875 let state = op_text_obj(OperatorKind::Change, 1, true);
877 assert_eq!(
878 step(state, Key::Char('p')),
879 Outcome::Commit(EngineCmd::ApplyOpTextObj {
880 op: OperatorKind::Change,
881 ch: 'p',
882 inner: true,
883 total_count: 1,
884 })
885 );
886 }
887
888 #[test]
889 fn op_d_i_then_esc_cancels() {
890 let state = op_text_obj(OperatorKind::Delete, 1, true);
892 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
893 }
894
895 #[test]
896 fn op_d_i_then_enter_cancels() {
897 let state = op_text_obj(OperatorKind::Delete, 1, true);
899 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
900 }
901
902 #[test]
903 fn op_2d_i_w_total_count_2_preserved() {
904 let state = PendingState::AfterOp {
907 op: OperatorKind::Delete,
908 count1: 2,
909 inner_count: 0,
910 };
911 let Outcome::Wait(obj_state) = step(state, Key::Char('i')) else {
912 panic!("expected Wait(OpTextObj)");
913 };
914 assert_eq!(
915 obj_state,
916 PendingState::OpTextObj {
917 op: OperatorKind::Delete,
918 total_count: 2,
919 inner: true,
920 }
921 );
922 assert_eq!(
923 step(obj_state, Key::Char('w')),
924 Outcome::Commit(EngineCmd::ApplyOpTextObj {
925 op: OperatorKind::Delete,
926 ch: 'w',
927 inner: true,
928 total_count: 2,
929 })
930 );
931 }
932
933 #[test]
934 fn op_d_bare_zero_is_line_start_motion() {
935 let state = after_op(OperatorKind::Delete, 1);
937 assert_eq!(
938 step(state, Key::Char('0')),
939 Outcome::Commit(EngineCmd::ApplyOpMotion {
940 op: OperatorKind::Delete,
941 motion_key: '0',
942 total_count: 1,
943 })
944 );
945 }
946
947 #[test]
948 fn op_d_zero_accumulates_when_inner_count_nonzero() {
949 let state = after_op(OperatorKind::Delete, 1);
951 let Outcome::Wait(s2) = step(state, Key::Char('1')) else {
952 panic!("expected Wait");
953 };
954 let Outcome::Wait(s3) = step(s2, Key::Char('0')) else {
955 panic!("expected Wait");
956 };
957 assert_eq!(
958 s3,
959 PendingState::AfterOp {
960 op: OperatorKind::Delete,
961 count1: 1,
962 inner_count: 10,
963 }
964 );
965 assert_eq!(
966 step(s3, Key::Char('w')),
967 Outcome::Commit(EngineCmd::ApplyOpMotion {
968 op: OperatorKind::Delete,
969 motion_key: 'w',
970 total_count: 10,
971 })
972 );
973 }
974
975 #[test]
978 fn op_yank_doubled() {
979 let state = after_op(OperatorKind::Yank, 1);
980 assert_eq!(
981 step(state, Key::Char('y')),
982 Outcome::Commit(EngineCmd::ApplyOpDouble {
983 op: OperatorKind::Yank,
984 total_count: 1,
985 })
986 );
987 }
988
989 #[test]
990 fn op_change_doubled() {
991 let state = after_op(OperatorKind::Change, 1);
992 assert_eq!(
993 step(state, Key::Char('c')),
994 Outcome::Commit(EngineCmd::ApplyOpDouble {
995 op: OperatorKind::Change,
996 total_count: 1,
997 })
998 );
999 }
1000
1001 #[test]
1002 fn op_indent_doubled() {
1003 let state = after_op(OperatorKind::Indent, 1);
1004 assert_eq!(
1005 step(state, Key::Char('>')),
1006 Outcome::Commit(EngineCmd::ApplyOpDouble {
1007 op: OperatorKind::Indent,
1008 total_count: 1,
1009 })
1010 );
1011 }
1012
1013 #[test]
1014 fn op_outdent_doubled() {
1015 let state = after_op(OperatorKind::Outdent, 1);
1016 assert_eq!(
1017 step(state, Key::Char('<')),
1018 Outcome::Commit(EngineCmd::ApplyOpDouble {
1019 op: OperatorKind::Outdent,
1020 total_count: 1,
1021 })
1022 );
1023 }
1024
1025 #[test]
1028 fn op_uppercase_then_cap_u_commits_double() {
1029 let state = after_op(OperatorKind::Uppercase, 1);
1031 assert_eq!(
1032 step(state, Key::Char('U')),
1033 Outcome::Commit(EngineCmd::ApplyOpDouble {
1034 op: OperatorKind::Uppercase,
1035 total_count: 1,
1036 })
1037 );
1038 }
1039
1040 #[test]
1041 fn op_lowercase_then_u_commits_double() {
1042 let state = after_op(OperatorKind::Lowercase, 1);
1044 assert_eq!(
1045 step(state, Key::Char('u')),
1046 Outcome::Commit(EngineCmd::ApplyOpDouble {
1047 op: OperatorKind::Lowercase,
1048 total_count: 1,
1049 })
1050 );
1051 }
1052
1053 #[test]
1054 fn op_togglecase_then_tilde_commits_double() {
1055 let state = after_op(OperatorKind::ToggleCase, 1);
1057 assert_eq!(
1058 step(state, Key::Char('~')),
1059 Outcome::Commit(EngineCmd::ApplyOpDouble {
1060 op: OperatorKind::ToggleCase,
1061 total_count: 1,
1062 })
1063 );
1064 }
1065
1066 #[test]
1067 fn op_reflow_then_q_commits_double() {
1068 let state = after_op(OperatorKind::Reflow, 1);
1070 assert_eq!(
1071 step(state, Key::Char('q')),
1072 Outcome::Commit(EngineCmd::ApplyOpDouble {
1073 op: OperatorKind::Reflow,
1074 total_count: 1,
1075 })
1076 );
1077 }
1078
1079 #[test]
1080 fn op_uppercase_then_w_commits_motion() {
1081 let state = after_op(OperatorKind::Uppercase, 1);
1083 assert_eq!(
1084 step(state, Key::Char('w')),
1085 Outcome::Commit(EngineCmd::ApplyOpMotion {
1086 op: OperatorKind::Uppercase,
1087 motion_key: 'w',
1088 total_count: 1,
1089 })
1090 );
1091 }
1092
1093 #[test]
1094 fn op_reflow_then_ap_commits_text_obj() {
1095 let state = after_op(OperatorKind::Reflow, 1);
1098 let Outcome::Wait(obj_state) = step(state, Key::Char('a')) else {
1099 panic!("expected Wait(OpTextObj)");
1100 };
1101 assert_eq!(
1102 obj_state,
1103 PendingState::OpTextObj {
1104 op: OperatorKind::Reflow,
1105 total_count: 1,
1106 inner: false,
1107 }
1108 );
1109 assert_eq!(
1111 step(obj_state, Key::Char('p')),
1112 Outcome::Commit(EngineCmd::ApplyOpTextObj {
1113 op: OperatorKind::Reflow,
1114 ch: 'p',
1115 inner: false,
1116 total_count: 1,
1117 })
1118 );
1119 }
1120
1121 #[test]
1122 fn op_yank_motion() {
1123 let state = after_op(OperatorKind::Yank, 1);
1124 assert_eq!(
1125 step(state, Key::Char('$')),
1126 Outcome::Commit(EngineCmd::ApplyOpMotion {
1127 op: OperatorKind::Yank,
1128 motion_key: '$',
1129 total_count: 1,
1130 })
1131 );
1132 }
1133
1134 #[test]
1135 fn op_change_motion() {
1136 let state = after_op(OperatorKind::Change, 1);
1137 assert_eq!(
1138 step(state, Key::Char('w')),
1139 Outcome::Commit(EngineCmd::ApplyOpMotion {
1140 op: OperatorKind::Change,
1141 motion_key: 'w',
1142 total_count: 1,
1143 })
1144 );
1145 }
1146
1147 #[test]
1148 fn op_indent_motion() {
1149 let state = after_op(OperatorKind::Indent, 1);
1150 assert_eq!(
1151 step(state, Key::Char('j')),
1152 Outcome::Commit(EngineCmd::ApplyOpMotion {
1153 op: OperatorKind::Indent,
1154 motion_key: 'j',
1155 total_count: 1,
1156 })
1157 );
1158 }
1159
1160 #[test]
1161 fn op_outdent_motion() {
1162 let state = after_op(OperatorKind::Outdent, 1);
1163 assert_eq!(
1164 step(state, Key::Char('k')),
1165 Outcome::Commit(EngineCmd::ApplyOpMotion {
1166 op: OperatorKind::Outdent,
1167 motion_key: 'k',
1168 total_count: 1,
1169 })
1170 );
1171 }
1172
1173 fn op_g(op: OperatorKind, total_count: usize) -> PendingState {
1176 PendingState::OpG { op, total_count }
1177 }
1178
1179 #[test]
1180 fn op_d_then_g_then_g_commits_apply_op_g_for_gg() {
1181 let state = op_g(OperatorKind::Delete, 1);
1183 assert_eq!(
1184 step(state, Key::Char('g')),
1185 Outcome::Commit(EngineCmd::ApplyOpG {
1186 op: OperatorKind::Delete,
1187 ch: 'g',
1188 total_count: 1,
1189 })
1190 );
1191 }
1192
1193 #[test]
1194 fn op_d_then_g_then_e_commits_for_ge() {
1195 let state = op_g(OperatorKind::Delete, 1);
1197 assert_eq!(
1198 step(state, Key::Char('e')),
1199 Outcome::Commit(EngineCmd::ApplyOpG {
1200 op: OperatorKind::Delete,
1201 ch: 'e',
1202 total_count: 1,
1203 })
1204 );
1205 }
1206
1207 #[test]
1208 fn op_d_then_g_then_j_commits_for_gj() {
1209 let state = op_g(OperatorKind::Delete, 1);
1211 assert_eq!(
1212 step(state, Key::Char('j')),
1213 Outcome::Commit(EngineCmd::ApplyOpG {
1214 op: OperatorKind::Delete,
1215 ch: 'j',
1216 total_count: 1,
1217 })
1218 );
1219 }
1220
1221 #[test]
1222 fn op_2d_3g_g_total_count_6() {
1223 let state = PendingState::AfterOp {
1226 op: OperatorKind::Delete,
1227 count1: 2,
1228 inner_count: 3,
1229 };
1230 let Outcome::Wait(op_g_state) = step(state, Key::Char('g')) else {
1231 panic!("expected Wait(OpG)");
1232 };
1233 assert_eq!(
1234 op_g_state,
1235 PendingState::OpG {
1236 op: OperatorKind::Delete,
1237 total_count: 6,
1238 }
1239 );
1240 assert_eq!(
1241 step(op_g_state, Key::Char('g')),
1242 Outcome::Commit(EngineCmd::ApplyOpG {
1243 op: OperatorKind::Delete,
1244 ch: 'g',
1245 total_count: 6,
1246 })
1247 );
1248 }
1249
1250 #[test]
1251 fn op_d_g_then_esc_cancels() {
1252 let state = op_g(OperatorKind::Delete, 1);
1254 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1255 }
1256
1257 #[test]
1258 fn op_d_g_then_enter_cancels() {
1259 let state = op_g(OperatorKind::Delete, 1);
1261 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1262 }
1263
1264 #[test]
1265 fn op_c_then_g_then_g_commits_change_op_g() {
1266 let state = op_g(OperatorKind::Change, 1);
1268 assert_eq!(
1269 step(state, Key::Char('g')),
1270 Outcome::Commit(EngineCmd::ApplyOpG {
1271 op: OperatorKind::Change,
1272 ch: 'g',
1273 total_count: 1,
1274 })
1275 );
1276 }
1277
1278 #[test]
1281 fn select_register_a_commits() {
1282 let state = PendingState::SelectRegister;
1284 assert_eq!(
1285 step(state, Key::Char('a')),
1286 Outcome::Commit(EngineCmd::SetPendingRegister { reg: 'a' })
1287 );
1288 }
1289
1290 #[test]
1291 fn select_register_plus_commits() {
1292 let state = PendingState::SelectRegister;
1294 assert_eq!(
1295 step(state, Key::Char('+')),
1296 Outcome::Commit(EngineCmd::SetPendingRegister { reg: '+' })
1297 );
1298 }
1299
1300 #[test]
1301 fn select_register_underscore_commits() {
1302 let state = PendingState::SelectRegister;
1304 assert_eq!(
1305 step(state, Key::Char('_')),
1306 Outcome::Commit(EngineCmd::SetPendingRegister { reg: '_' })
1307 );
1308 }
1309
1310 #[test]
1311 fn select_register_esc_cancels() {
1312 let state = PendingState::SelectRegister;
1313 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1314 }
1315
1316 #[test]
1317 fn select_register_enter_cancels() {
1318 let state = PendingState::SelectRegister;
1320 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1321 }
1322
1323 #[test]
1326 fn set_mark_a_commits() {
1327 let state = PendingState::SetMark;
1329 assert_eq!(
1330 step(state, Key::Char('a')),
1331 Outcome::Commit(EngineCmd::SetMark { ch: 'a' })
1332 );
1333 }
1334
1335 #[test]
1336 fn set_mark_esc_cancels() {
1337 let state = PendingState::SetMark;
1338 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1339 }
1340
1341 #[test]
1342 fn set_mark_enter_cancels() {
1343 let state = PendingState::SetMark;
1345 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1346 }
1347
1348 #[test]
1351 fn goto_mark_line_a_commits() {
1352 let state = PendingState::GotoMarkLine;
1354 assert_eq!(
1355 step(state, Key::Char('a')),
1356 Outcome::Commit(EngineCmd::GotoMarkLine { ch: 'a' })
1357 );
1358 }
1359
1360 #[test]
1361 fn goto_mark_line_esc_cancels() {
1362 let state = PendingState::GotoMarkLine;
1363 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1364 }
1365
1366 #[test]
1367 fn goto_mark_line_enter_cancels() {
1368 let state = PendingState::GotoMarkLine;
1370 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1371 }
1372
1373 #[test]
1376 fn goto_mark_char_a_commits() {
1377 let state = PendingState::GotoMarkChar;
1379 assert_eq!(
1380 step(state, Key::Char('a')),
1381 Outcome::Commit(EngineCmd::GotoMarkChar { ch: 'a' })
1382 );
1383 }
1384
1385 #[test]
1386 fn goto_mark_char_esc_cancels() {
1387 let state = PendingState::GotoMarkChar;
1388 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1389 }
1390
1391 #[test]
1392 fn goto_mark_char_enter_cancels() {
1393 let state = PendingState::GotoMarkChar;
1395 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1396 }
1397
1398 #[test]
1401 fn record_macro_target_a_commits_start_record() {
1402 let state = PendingState::RecordMacroTarget;
1404 assert_eq!(
1405 step(state, Key::Char('a')),
1406 Outcome::Commit(EngineCmd::StartMacroRecord { reg: 'a' })
1407 );
1408 }
1409
1410 #[test]
1411 fn record_macro_target_capital_a_commits_start_record() {
1412 let state = PendingState::RecordMacroTarget;
1414 assert_eq!(
1415 step(state, Key::Char('A')),
1416 Outcome::Commit(EngineCmd::StartMacroRecord { reg: 'A' })
1417 );
1418 }
1419
1420 #[test]
1421 fn record_macro_target_esc_cancels() {
1422 let state = PendingState::RecordMacroTarget;
1423 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1424 }
1425
1426 #[test]
1427 fn record_macro_target_enter_cancels() {
1428 let state = PendingState::RecordMacroTarget;
1430 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1431 }
1432
1433 #[test]
1434 fn record_macro_target_non_alnum_cancels() {
1435 let state = PendingState::RecordMacroTarget;
1437 assert_eq!(step(state, Key::Char('!')), Outcome::Cancel);
1438 }
1439
1440 #[test]
1443 fn play_macro_target_a_commits_play() {
1444 let state = PendingState::PlayMacroTarget { count: 1 };
1446 assert_eq!(
1447 step(state, Key::Char('a')),
1448 Outcome::Commit(EngineCmd::PlayMacro { reg: 'a', count: 1 })
1449 );
1450 }
1451
1452 #[test]
1453 fn play_macro_target_at_sign_commits_play_with_at() {
1454 let state = PendingState::PlayMacroTarget { count: 1 };
1456 assert_eq!(
1457 step(state, Key::Char('@')),
1458 Outcome::Commit(EngineCmd::PlayMacro { reg: '@', count: 1 })
1459 );
1460 }
1461
1462 #[test]
1463 fn play_macro_target_with_count_3_preserves_count() {
1464 let state = PendingState::PlayMacroTarget { count: 3 };
1466 assert_eq!(
1467 step(state, Key::Char('a')),
1468 Outcome::Commit(EngineCmd::PlayMacro { reg: 'a', count: 3 })
1469 );
1470 }
1471
1472 #[test]
1473 fn play_macro_target_esc_cancels() {
1474 let state = PendingState::PlayMacroTarget { count: 1 };
1475 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1476 }
1477
1478 #[test]
1479 fn play_macro_target_enter_cancels() {
1480 let state = PendingState::PlayMacroTarget { count: 1 };
1482 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1483 }
1484
1485 #[test]
1486 fn play_macro_target_non_alnum_cancels() {
1487 let state = PendingState::PlayMacroTarget { count: 1 };
1489 assert_eq!(step(state, Key::Char('!')), Outcome::Cancel);
1490 }
1491
1492 #[test]
1493 fn play_macro_target_colon_commits_play_macro() {
1494 let state = PendingState::PlayMacroTarget { count: 1 };
1497 assert_eq!(
1498 step(state, Key::Char(':')),
1499 Outcome::Commit(EngineCmd::PlayMacro { reg: ':', count: 1 })
1500 );
1501 }
1502
1503 #[test]
1504 fn play_macro_target_colon_with_count_3_commits() {
1505 let state = PendingState::PlayMacroTarget { count: 3 };
1507 assert_eq!(
1508 step(state, Key::Char(':')),
1509 Outcome::Commit(EngineCmd::PlayMacro { reg: ':', count: 3 })
1510 );
1511 }
1512}