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]
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]
989 fn op_yank_doubled() {
990 let state = after_op(OperatorKind::Yank, 1);
991 assert_eq!(
992 step(state, Key::Char('y')),
993 Outcome::Commit(EngineCmd::ApplyOpDouble {
994 op: OperatorKind::Yank,
995 total_count: 1,
996 })
997 );
998 }
999
1000 #[test]
1001 fn op_change_doubled() {
1002 let state = after_op(OperatorKind::Change, 1);
1003 assert_eq!(
1004 step(state, Key::Char('c')),
1005 Outcome::Commit(EngineCmd::ApplyOpDouble {
1006 op: OperatorKind::Change,
1007 total_count: 1,
1008 })
1009 );
1010 }
1011
1012 #[test]
1013 fn op_indent_doubled() {
1014 let state = after_op(OperatorKind::Indent, 1);
1015 assert_eq!(
1016 step(state, Key::Char('>')),
1017 Outcome::Commit(EngineCmd::ApplyOpDouble {
1018 op: OperatorKind::Indent,
1019 total_count: 1,
1020 })
1021 );
1022 }
1023
1024 #[test]
1025 fn op_outdent_doubled() {
1026 let state = after_op(OperatorKind::Outdent, 1);
1027 assert_eq!(
1028 step(state, Key::Char('<')),
1029 Outcome::Commit(EngineCmd::ApplyOpDouble {
1030 op: OperatorKind::Outdent,
1031 total_count: 1,
1032 })
1033 );
1034 }
1035
1036 #[test]
1039 fn op_uppercase_then_cap_u_commits_double() {
1040 let state = after_op(OperatorKind::Uppercase, 1);
1042 assert_eq!(
1043 step(state, Key::Char('U')),
1044 Outcome::Commit(EngineCmd::ApplyOpDouble {
1045 op: OperatorKind::Uppercase,
1046 total_count: 1,
1047 })
1048 );
1049 }
1050
1051 #[test]
1052 fn op_lowercase_then_u_commits_double() {
1053 let state = after_op(OperatorKind::Lowercase, 1);
1055 assert_eq!(
1056 step(state, Key::Char('u')),
1057 Outcome::Commit(EngineCmd::ApplyOpDouble {
1058 op: OperatorKind::Lowercase,
1059 total_count: 1,
1060 })
1061 );
1062 }
1063
1064 #[test]
1065 fn op_togglecase_then_tilde_commits_double() {
1066 let state = after_op(OperatorKind::ToggleCase, 1);
1068 assert_eq!(
1069 step(state, Key::Char('~')),
1070 Outcome::Commit(EngineCmd::ApplyOpDouble {
1071 op: OperatorKind::ToggleCase,
1072 total_count: 1,
1073 })
1074 );
1075 }
1076
1077 #[test]
1078 fn op_reflow_then_q_commits_double() {
1079 let state = after_op(OperatorKind::Reflow, 1);
1081 assert_eq!(
1082 step(state, Key::Char('q')),
1083 Outcome::Commit(EngineCmd::ApplyOpDouble {
1084 op: OperatorKind::Reflow,
1085 total_count: 1,
1086 })
1087 );
1088 }
1089
1090 #[test]
1091 fn op_uppercase_then_w_commits_motion() {
1092 let state = after_op(OperatorKind::Uppercase, 1);
1094 assert_eq!(
1095 step(state, Key::Char('w')),
1096 Outcome::Commit(EngineCmd::ApplyOpMotion {
1097 op: OperatorKind::Uppercase,
1098 motion_key: 'w',
1099 total_count: 1,
1100 })
1101 );
1102 }
1103
1104 #[test]
1105 fn op_reflow_then_ap_commits_text_obj() {
1106 let state = after_op(OperatorKind::Reflow, 1);
1109 let Outcome::Wait(obj_state) = step(state, Key::Char('a')) else {
1110 panic!("expected Wait(OpTextObj)");
1111 };
1112 assert_eq!(
1113 obj_state,
1114 PendingState::OpTextObj {
1115 op: OperatorKind::Reflow,
1116 total_count: 1,
1117 inner: false,
1118 }
1119 );
1120 assert_eq!(
1122 step(obj_state, Key::Char('p')),
1123 Outcome::Commit(EngineCmd::ApplyOpTextObj {
1124 op: OperatorKind::Reflow,
1125 ch: 'p',
1126 inner: false,
1127 total_count: 1,
1128 })
1129 );
1130 }
1131
1132 #[test]
1133 fn op_yank_motion() {
1134 let state = after_op(OperatorKind::Yank, 1);
1135 assert_eq!(
1136 step(state, Key::Char('$')),
1137 Outcome::Commit(EngineCmd::ApplyOpMotion {
1138 op: OperatorKind::Yank,
1139 motion_key: '$',
1140 total_count: 1,
1141 })
1142 );
1143 }
1144
1145 #[test]
1146 fn op_change_motion() {
1147 let state = after_op(OperatorKind::Change, 1);
1148 assert_eq!(
1149 step(state, Key::Char('w')),
1150 Outcome::Commit(EngineCmd::ApplyOpMotion {
1151 op: OperatorKind::Change,
1152 motion_key: 'w',
1153 total_count: 1,
1154 })
1155 );
1156 }
1157
1158 #[test]
1159 fn op_indent_motion() {
1160 let state = after_op(OperatorKind::Indent, 1);
1161 assert_eq!(
1162 step(state, Key::Char('j')),
1163 Outcome::Commit(EngineCmd::ApplyOpMotion {
1164 op: OperatorKind::Indent,
1165 motion_key: 'j',
1166 total_count: 1,
1167 })
1168 );
1169 }
1170
1171 #[test]
1172 fn op_outdent_motion() {
1173 let state = after_op(OperatorKind::Outdent, 1);
1174 assert_eq!(
1175 step(state, Key::Char('k')),
1176 Outcome::Commit(EngineCmd::ApplyOpMotion {
1177 op: OperatorKind::Outdent,
1178 motion_key: 'k',
1179 total_count: 1,
1180 })
1181 );
1182 }
1183
1184 fn op_g(op: OperatorKind, total_count: usize) -> PendingState {
1187 PendingState::OpG { op, total_count }
1188 }
1189
1190 #[test]
1191 fn op_d_then_g_then_g_commits_apply_op_g_for_gg() {
1192 let state = op_g(OperatorKind::Delete, 1);
1194 assert_eq!(
1195 step(state, Key::Char('g')),
1196 Outcome::Commit(EngineCmd::ApplyOpG {
1197 op: OperatorKind::Delete,
1198 ch: 'g',
1199 total_count: 1,
1200 })
1201 );
1202 }
1203
1204 #[test]
1205 fn op_d_then_g_then_e_commits_for_ge() {
1206 let state = op_g(OperatorKind::Delete, 1);
1208 assert_eq!(
1209 step(state, Key::Char('e')),
1210 Outcome::Commit(EngineCmd::ApplyOpG {
1211 op: OperatorKind::Delete,
1212 ch: 'e',
1213 total_count: 1,
1214 })
1215 );
1216 }
1217
1218 #[test]
1219 fn op_d_then_g_then_j_commits_for_gj() {
1220 let state = op_g(OperatorKind::Delete, 1);
1222 assert_eq!(
1223 step(state, Key::Char('j')),
1224 Outcome::Commit(EngineCmd::ApplyOpG {
1225 op: OperatorKind::Delete,
1226 ch: 'j',
1227 total_count: 1,
1228 })
1229 );
1230 }
1231
1232 #[test]
1233 fn op_2d_3g_g_total_count_6() {
1234 let state = PendingState::AfterOp {
1237 op: OperatorKind::Delete,
1238 count1: 2,
1239 inner_count: 3,
1240 };
1241 let Outcome::Wait(op_g_state) = step(state, Key::Char('g')) else {
1242 panic!("expected Wait(OpG)");
1243 };
1244 assert_eq!(
1245 op_g_state,
1246 PendingState::OpG {
1247 op: OperatorKind::Delete,
1248 total_count: 6,
1249 }
1250 );
1251 assert_eq!(
1252 step(op_g_state, Key::Char('g')),
1253 Outcome::Commit(EngineCmd::ApplyOpG {
1254 op: OperatorKind::Delete,
1255 ch: 'g',
1256 total_count: 6,
1257 })
1258 );
1259 }
1260
1261 #[test]
1262 fn op_d_g_then_esc_cancels() {
1263 let state = op_g(OperatorKind::Delete, 1);
1265 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1266 }
1267
1268 #[test]
1269 fn op_d_g_then_enter_cancels() {
1270 let state = op_g(OperatorKind::Delete, 1);
1272 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1273 }
1274
1275 #[test]
1276 fn op_c_then_g_then_g_commits_change_op_g() {
1277 let state = op_g(OperatorKind::Change, 1);
1279 assert_eq!(
1280 step(state, Key::Char('g')),
1281 Outcome::Commit(EngineCmd::ApplyOpG {
1282 op: OperatorKind::Change,
1283 ch: 'g',
1284 total_count: 1,
1285 })
1286 );
1287 }
1288
1289 #[test]
1292 fn select_register_a_commits() {
1293 let state = PendingState::SelectRegister;
1295 assert_eq!(
1296 step(state, Key::Char('a')),
1297 Outcome::Commit(EngineCmd::SetPendingRegister { reg: 'a' })
1298 );
1299 }
1300
1301 #[test]
1302 fn select_register_plus_commits() {
1303 let state = PendingState::SelectRegister;
1305 assert_eq!(
1306 step(state, Key::Char('+')),
1307 Outcome::Commit(EngineCmd::SetPendingRegister { reg: '+' })
1308 );
1309 }
1310
1311 #[test]
1312 fn select_register_underscore_commits() {
1313 let state = PendingState::SelectRegister;
1315 assert_eq!(
1316 step(state, Key::Char('_')),
1317 Outcome::Commit(EngineCmd::SetPendingRegister { reg: '_' })
1318 );
1319 }
1320
1321 #[test]
1322 fn select_register_esc_cancels() {
1323 let state = PendingState::SelectRegister;
1324 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1325 }
1326
1327 #[test]
1328 fn select_register_enter_cancels() {
1329 let state = PendingState::SelectRegister;
1331 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1332 }
1333
1334 #[test]
1337 fn set_mark_a_commits() {
1338 let state = PendingState::SetMark;
1340 assert_eq!(
1341 step(state, Key::Char('a')),
1342 Outcome::Commit(EngineCmd::SetMark { ch: 'a' })
1343 );
1344 }
1345
1346 #[test]
1347 fn set_mark_esc_cancels() {
1348 let state = PendingState::SetMark;
1349 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1350 }
1351
1352 #[test]
1353 fn set_mark_enter_cancels() {
1354 let state = PendingState::SetMark;
1356 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1357 }
1358
1359 #[test]
1362 fn goto_mark_line_a_commits() {
1363 let state = PendingState::GotoMarkLine;
1365 assert_eq!(
1366 step(state, Key::Char('a')),
1367 Outcome::Commit(EngineCmd::GotoMarkLine { ch: 'a' })
1368 );
1369 }
1370
1371 #[test]
1372 fn goto_mark_line_esc_cancels() {
1373 let state = PendingState::GotoMarkLine;
1374 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1375 }
1376
1377 #[test]
1378 fn goto_mark_line_enter_cancels() {
1379 let state = PendingState::GotoMarkLine;
1381 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1382 }
1383
1384 #[test]
1387 fn goto_mark_char_a_commits() {
1388 let state = PendingState::GotoMarkChar;
1390 assert_eq!(
1391 step(state, Key::Char('a')),
1392 Outcome::Commit(EngineCmd::GotoMarkChar { ch: 'a' })
1393 );
1394 }
1395
1396 #[test]
1397 fn goto_mark_char_esc_cancels() {
1398 let state = PendingState::GotoMarkChar;
1399 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1400 }
1401
1402 #[test]
1403 fn goto_mark_char_enter_cancels() {
1404 let state = PendingState::GotoMarkChar;
1406 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1407 }
1408
1409 #[test]
1412 fn record_macro_target_a_commits_start_record() {
1413 let state = PendingState::RecordMacroTarget;
1415 assert_eq!(
1416 step(state, Key::Char('a')),
1417 Outcome::Commit(EngineCmd::StartMacroRecord { reg: 'a' })
1418 );
1419 }
1420
1421 #[test]
1422 fn record_macro_target_capital_a_commits_start_record() {
1423 let state = PendingState::RecordMacroTarget;
1425 assert_eq!(
1426 step(state, Key::Char('A')),
1427 Outcome::Commit(EngineCmd::StartMacroRecord { reg: 'A' })
1428 );
1429 }
1430
1431 #[test]
1432 fn record_macro_target_esc_cancels() {
1433 let state = PendingState::RecordMacroTarget;
1434 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1435 }
1436
1437 #[test]
1438 fn record_macro_target_enter_cancels() {
1439 let state = PendingState::RecordMacroTarget;
1441 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1442 }
1443
1444 #[test]
1445 fn record_macro_target_non_alnum_cancels() {
1446 let state = PendingState::RecordMacroTarget;
1448 assert_eq!(step(state, Key::Char('!')), Outcome::Cancel);
1449 }
1450
1451 #[test]
1454 fn play_macro_target_a_commits_play() {
1455 let state = PendingState::PlayMacroTarget { count: 1 };
1457 assert_eq!(
1458 step(state, Key::Char('a')),
1459 Outcome::Commit(EngineCmd::PlayMacro { reg: 'a', count: 1 })
1460 );
1461 }
1462
1463 #[test]
1464 fn play_macro_target_at_sign_commits_play_with_at() {
1465 let state = PendingState::PlayMacroTarget { count: 1 };
1467 assert_eq!(
1468 step(state, Key::Char('@')),
1469 Outcome::Commit(EngineCmd::PlayMacro { reg: '@', count: 1 })
1470 );
1471 }
1472
1473 #[test]
1474 fn play_macro_target_with_count_3_preserves_count() {
1475 let state = PendingState::PlayMacroTarget { count: 3 };
1477 assert_eq!(
1478 step(state, Key::Char('a')),
1479 Outcome::Commit(EngineCmd::PlayMacro { reg: 'a', count: 3 })
1480 );
1481 }
1482
1483 #[test]
1484 fn play_macro_target_esc_cancels() {
1485 let state = PendingState::PlayMacroTarget { count: 1 };
1486 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
1487 }
1488
1489 #[test]
1490 fn play_macro_target_enter_cancels() {
1491 let state = PendingState::PlayMacroTarget { count: 1 };
1493 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
1494 }
1495
1496 #[test]
1497 fn play_macro_target_non_alnum_cancels() {
1498 let state = PendingState::PlayMacroTarget { count: 1 };
1500 assert_eq!(step(state, Key::Char('!')), Outcome::Cancel);
1501 }
1502
1503 #[test]
1504 fn play_macro_target_colon_commits_play_macro() {
1505 let state = PendingState::PlayMacroTarget { count: 1 };
1508 assert_eq!(
1509 step(state, Key::Char(':')),
1510 Outcome::Commit(EngineCmd::PlayMacro { reg: ':', count: 1 })
1511 );
1512 }
1513
1514 #[test]
1515 fn play_macro_target_colon_with_count_3_commits() {
1516 let state = PendingState::PlayMacroTarget { count: 3 };
1518 assert_eq!(
1519 step(state, Key::Char(':')),
1520 Outcome::Commit(EngineCmd::PlayMacro { reg: ':', count: 3 })
1521 );
1522 }
1523}