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