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 }
65
66#[derive(Debug, Clone, PartialEq, Eq)]
68pub enum Outcome {
69 Wait(PendingState),
71 Commit(crate::cmd::EngineCmd),
73 Cancel,
75 Forward,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
83pub enum Key {
84 Char(char),
85 Esc,
86 Enter,
87 Backspace,
88 Tab,
89 }
91
92pub fn step(state: PendingState, key: Key) -> Outcome {
93 match state {
94 PendingState::Replace { count } => match key {
95 Key::Esc => Outcome::Cancel,
96 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::ReplaceChar { ch, count }),
97 Key::Enter => Outcome::Commit(crate::cmd::EngineCmd::ReplaceChar { ch: '\n', count }),
98 _ => Outcome::Cancel,
99 },
100 PendingState::Find {
101 count,
102 forward,
103 till,
104 } => match key {
105 Key::Esc => Outcome::Cancel,
106 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::FindChar {
107 ch,
108 forward,
109 till,
110 count,
111 }),
112 _ => Outcome::Cancel,
114 },
115 PendingState::AfterG { count } => match key {
116 Key::Esc => Outcome::Cancel,
117 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::AfterGChord { ch, count }),
118 _ => Outcome::Cancel,
120 },
121 PendingState::AfterZ { count } => match key {
122 Key::Esc => Outcome::Cancel,
123 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::AfterZChord { ch, count }),
124 _ => Outcome::Cancel,
126 },
127 PendingState::AfterOp {
128 op,
129 count1,
130 inner_count,
131 } => match key {
132 Key::Esc => Outcome::Cancel,
133 Key::Char(d @ '0'..='9') => {
134 if d == '0' && inner_count == 0 {
136 let total = count1.max(1);
138 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpMotion {
139 op,
140 motion_key: '0',
141 total_count: total,
142 })
143 } else {
144 let new_inner = inner_count
145 .saturating_mul(10)
146 .saturating_add(d as usize - '0' as usize);
147 Outcome::Wait(PendingState::AfterOp {
148 op,
149 count1,
150 inner_count: new_inner,
151 })
152 }
153 }
154 Key::Char(ch) => {
155 let total = count1.max(1) * inner_count.max(1);
156 if ch == op.double_char() {
158 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpDouble {
159 op,
160 total_count: total,
161 })
162 } else if ch == 'i' {
164 Outcome::Commit(crate::cmd::EngineCmd::EnterOpTextObj {
165 op,
166 count1,
167 inner: true,
168 })
169 } else if ch == 'a' {
170 Outcome::Commit(crate::cmd::EngineCmd::EnterOpTextObj {
171 op,
172 count1,
173 inner: false,
174 })
175 } else if ch == 'g' {
177 Outcome::Commit(crate::cmd::EngineCmd::EnterOpG { op, count1 })
178 } else if ch == 'f' {
182 Outcome::Wait(PendingState::OpFind {
183 op,
184 total_count: count1.max(1) * inner_count.max(1),
185 forward: true,
186 till: false,
187 })
188 } else if ch == 'F' {
189 Outcome::Wait(PendingState::OpFind {
190 op,
191 total_count: count1.max(1) * inner_count.max(1),
192 forward: false,
193 till: false,
194 })
195 } else if ch == 't' {
196 Outcome::Wait(PendingState::OpFind {
197 op,
198 total_count: count1.max(1) * inner_count.max(1),
199 forward: true,
200 till: true,
201 })
202 } else if ch == 'T' {
203 Outcome::Wait(PendingState::OpFind {
204 op,
205 total_count: count1.max(1) * inner_count.max(1),
206 forward: false,
207 till: true,
208 })
209 } else {
210 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpMotion {
213 op,
214 motion_key: ch,
215 total_count: total,
216 })
217 }
218 }
219 _ => Outcome::Cancel,
221 },
222 PendingState::OpFind {
223 op,
224 total_count,
225 forward,
226 till,
227 } => match key {
228 Key::Esc => Outcome::Cancel,
229 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::ApplyOpFind {
230 op,
231 ch,
232 forward,
233 till,
234 total_count,
235 }),
236 _ => Outcome::Cancel,
238 },
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::cmd::EngineCmd;
246 use crate::operator::OperatorKind;
247
248 #[test]
251 fn after_g_gg_commits() {
252 let state = PendingState::AfterG { count: 1 };
253 assert_eq!(
254 step(state, Key::Char('g')),
255 Outcome::Commit(EngineCmd::AfterGChord { ch: 'g', count: 1 })
256 );
257 }
258
259 #[test]
260 fn after_g_gv_commits() {
261 let state = PendingState::AfterG { count: 1 };
262 assert_eq!(
263 step(state, Key::Char('v')),
264 Outcome::Commit(EngineCmd::AfterGChord { ch: 'v', count: 1 })
265 );
266 }
267
268 #[test]
269 fn after_g_gu_operator_commits() {
270 let state = PendingState::AfterG { count: 1 };
272 assert_eq!(
273 step(state, Key::Char('U')),
274 Outcome::Commit(EngineCmd::AfterGChord { ch: 'U', count: 1 })
275 );
276 }
277
278 #[test]
279 fn after_g_gi_commits() {
280 let state = PendingState::AfterG { count: 1 };
281 assert_eq!(
282 step(state, Key::Char('i')),
283 Outcome::Commit(EngineCmd::AfterGChord { ch: 'i', count: 1 })
284 );
285 }
286
287 #[test]
288 fn after_g_esc_cancels() {
289 let state = PendingState::AfterG { count: 1 };
290 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
291 }
292
293 #[test]
294 fn after_g_count_carry_through() {
295 let state = PendingState::AfterG { count: 5 };
297 assert_eq!(
298 step(state, Key::Char('g')),
299 Outcome::Commit(EngineCmd::AfterGChord { ch: 'g', count: 5 })
300 );
301 }
302
303 #[test]
304 fn after_g_non_char_cancels() {
305 let state = PendingState::AfterG { count: 1 };
307 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
308 }
309
310 #[test]
313 fn after_z_zz_commits() {
314 let state = PendingState::AfterZ { count: 1 };
315 assert_eq!(
316 step(state, Key::Char('z')),
317 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 1 })
318 );
319 }
320
321 #[test]
322 fn after_z_zf_commits() {
323 let state = PendingState::AfterZ { count: 1 };
324 assert_eq!(
325 step(state, Key::Char('f')),
326 Outcome::Commit(EngineCmd::AfterZChord { ch: 'f', count: 1 })
327 );
328 }
329
330 #[test]
331 fn after_z_esc_cancels() {
332 let state = PendingState::AfterZ { count: 1 };
333 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
334 }
335
336 #[test]
337 fn after_z_count_carry_through() {
338 let state = PendingState::AfterZ { count: 3 };
340 assert_eq!(
341 step(state, Key::Char('z')),
342 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 3 })
343 );
344 }
345
346 #[test]
347 fn after_z_non_char_cancels() {
348 let state = PendingState::AfterZ { count: 1 };
350 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
351 }
352
353 fn after_op(op: OperatorKind, count1: usize) -> PendingState {
356 PendingState::AfterOp {
357 op,
358 count1,
359 inner_count: 0,
360 }
361 }
362
363 #[test]
364 fn op_d_then_w_commits_motion() {
365 let state = after_op(OperatorKind::Delete, 1);
366 assert_eq!(
367 step(state, Key::Char('w')),
368 Outcome::Commit(EngineCmd::ApplyOpMotion {
369 op: OperatorKind::Delete,
370 motion_key: 'w',
371 total_count: 1,
372 })
373 );
374 }
375
376 #[test]
377 fn op_d_then_d_commits_double() {
378 let state = after_op(OperatorKind::Delete, 1);
379 assert_eq!(
380 step(state, Key::Char('d')),
381 Outcome::Commit(EngineCmd::ApplyOpDouble {
382 op: OperatorKind::Delete,
383 total_count: 1,
384 })
385 );
386 }
387
388 #[test]
389 fn op_d_inner_count_d3w_commits_motion_with_count_3() {
390 let state = after_op(OperatorKind::Delete, 1);
392 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
394 panic!("expected Wait");
395 };
396 assert_eq!(
397 state2,
398 PendingState::AfterOp {
399 op: OperatorKind::Delete,
400 count1: 1,
401 inner_count: 3
402 }
403 );
404 assert_eq!(
406 step(state2, Key::Char('w')),
407 Outcome::Commit(EngineCmd::ApplyOpMotion {
408 op: OperatorKind::Delete,
409 motion_key: 'w',
410 total_count: 3,
411 })
412 );
413 }
414
415 #[test]
416 fn op_2d_d_commits_double_with_count_2() {
417 let state = after_op(OperatorKind::Delete, 2);
419 assert_eq!(
420 step(state, Key::Char('d')),
421 Outcome::Commit(EngineCmd::ApplyOpDouble {
422 op: OperatorKind::Delete,
423 total_count: 2,
424 })
425 );
426 }
427
428 #[test]
429 fn op_2d_3w_commits_motion_with_total_6() {
430 let state = after_op(OperatorKind::Delete, 2);
432 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
433 panic!("expected Wait");
434 };
435 assert_eq!(
436 step(state2, Key::Char('w')),
437 Outcome::Commit(EngineCmd::ApplyOpMotion {
438 op: OperatorKind::Delete,
439 motion_key: 'w',
440 total_count: 6,
441 })
442 );
443 }
444
445 #[test]
446 fn op_d_then_i_emits_enter_op_text_obj_inner_true() {
447 let state = after_op(OperatorKind::Delete, 1);
448 assert_eq!(
449 step(state, Key::Char('i')),
450 Outcome::Commit(EngineCmd::EnterOpTextObj {
451 op: OperatorKind::Delete,
452 count1: 1,
453 inner: true,
454 })
455 );
456 }
457
458 #[test]
459 fn op_d_then_a_emits_enter_op_text_obj_inner_false() {
460 let state = after_op(OperatorKind::Delete, 1);
461 assert_eq!(
462 step(state, Key::Char('a')),
463 Outcome::Commit(EngineCmd::EnterOpTextObj {
464 op: OperatorKind::Delete,
465 count1: 1,
466 inner: false,
467 })
468 );
469 }
470
471 #[test]
472 fn op_d_then_g_emits_enter_op_g() {
473 let state = after_op(OperatorKind::Delete, 1);
474 assert_eq!(
475 step(state, Key::Char('g')),
476 Outcome::Commit(EngineCmd::EnterOpG {
477 op: OperatorKind::Delete,
478 count1: 1,
479 })
480 );
481 }
482
483 #[test]
484 fn op_d_then_f_transitions_to_op_find_forward_not_till() {
485 let state = after_op(OperatorKind::Delete, 1);
487 assert_eq!(
488 step(state, Key::Char('f')),
489 Outcome::Wait(PendingState::OpFind {
490 op: OperatorKind::Delete,
491 total_count: 1,
492 forward: true,
493 till: false,
494 })
495 );
496 }
497
498 #[test]
499 fn op_d_then_cap_f_transitions_to_op_find_backward_not_till() {
500 let state = after_op(OperatorKind::Delete, 1);
502 assert_eq!(
503 step(state, Key::Char('F')),
504 Outcome::Wait(PendingState::OpFind {
505 op: OperatorKind::Delete,
506 total_count: 1,
507 forward: false,
508 till: false,
509 })
510 );
511 }
512
513 #[test]
514 fn op_d_then_t_transitions_to_op_find_forward_till() {
515 let state = after_op(OperatorKind::Delete, 1);
517 assert_eq!(
518 step(state, Key::Char('t')),
519 Outcome::Wait(PendingState::OpFind {
520 op: OperatorKind::Delete,
521 total_count: 1,
522 forward: true,
523 till: true,
524 })
525 );
526 }
527
528 #[test]
529 fn op_d_then_cap_t_transitions_to_op_find_backward_till() {
530 let state = after_op(OperatorKind::Delete, 1);
532 assert_eq!(
533 step(state, Key::Char('T')),
534 Outcome::Wait(PendingState::OpFind {
535 op: OperatorKind::Delete,
536 total_count: 1,
537 forward: false,
538 till: true,
539 })
540 );
541 }
542
543 fn op_find(op: OperatorKind, total_count: usize, forward: bool, till: bool) -> PendingState {
546 PendingState::OpFind {
547 op,
548 total_count,
549 forward,
550 till,
551 }
552 }
553
554 #[test]
555 fn op_d_then_f_then_x_commits_apply_op_find() {
556 let state = op_find(OperatorKind::Delete, 1, true, false);
558 assert_eq!(
559 step(state, Key::Char('x')),
560 Outcome::Commit(EngineCmd::ApplyOpFind {
561 op: OperatorKind::Delete,
562 ch: 'x',
563 forward: true,
564 till: false,
565 total_count: 1,
566 })
567 );
568 }
569
570 #[test]
571 fn op_d_then_cap_f_then_x_commits_apply_op_find_backward() {
572 let state = op_find(OperatorKind::Delete, 1, false, false);
574 assert_eq!(
575 step(state, Key::Char('x')),
576 Outcome::Commit(EngineCmd::ApplyOpFind {
577 op: OperatorKind::Delete,
578 ch: 'x',
579 forward: false,
580 till: false,
581 total_count: 1,
582 })
583 );
584 }
585
586 #[test]
587 fn op_d_then_t_then_x_commits_apply_op_find_till() {
588 let state = op_find(OperatorKind::Delete, 1, true, true);
590 assert_eq!(
591 step(state, Key::Char('x')),
592 Outcome::Commit(EngineCmd::ApplyOpFind {
593 op: OperatorKind::Delete,
594 ch: 'x',
595 forward: true,
596 till: true,
597 total_count: 1,
598 })
599 );
600 }
601
602 #[test]
603 fn op_d_then_cap_t_then_x_commits_apply_op_find_backward_till() {
604 let state = op_find(OperatorKind::Delete, 1, false, true);
606 assert_eq!(
607 step(state, Key::Char('x')),
608 Outcome::Commit(EngineCmd::ApplyOpFind {
609 op: OperatorKind::Delete,
610 ch: 'x',
611 forward: false,
612 till: true,
613 total_count: 1,
614 })
615 );
616 }
617
618 #[test]
619 fn op_2d_3f_x_commits_total_count_6() {
620 let state = PendingState::AfterOp {
623 op: OperatorKind::Delete,
624 count1: 2,
625 inner_count: 3,
626 };
627 let Outcome::Wait(op_find_state) = step(state, Key::Char('f')) else {
628 panic!("expected Wait(OpFind)");
629 };
630 assert_eq!(
631 op_find_state,
632 PendingState::OpFind {
633 op: OperatorKind::Delete,
634 total_count: 6,
635 forward: true,
636 till: false,
637 }
638 );
639 assert_eq!(
640 step(op_find_state, Key::Char('x')),
641 Outcome::Commit(EngineCmd::ApplyOpFind {
642 op: OperatorKind::Delete,
643 ch: 'x',
644 forward: true,
645 till: false,
646 total_count: 6,
647 })
648 );
649 }
650
651 #[test]
652 fn op_d_f_then_esc_cancels() {
653 let state = op_find(OperatorKind::Delete, 1, true, false);
655 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
656 }
657
658 #[test]
659 fn op_d_f_then_enter_cancels() {
660 let state = op_find(OperatorKind::Delete, 1, true, false);
662 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
663 }
664
665 #[test]
666 fn op_d_then_esc_cancels() {
667 let state = after_op(OperatorKind::Delete, 1);
668 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
669 }
670
671 #[test]
672 fn op_d_non_char_cancels() {
673 let state = after_op(OperatorKind::Delete, 1);
674 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
675 }
676
677 #[test]
678 fn op_d_bare_zero_is_line_start_motion() {
679 let state = after_op(OperatorKind::Delete, 1);
681 assert_eq!(
682 step(state, Key::Char('0')),
683 Outcome::Commit(EngineCmd::ApplyOpMotion {
684 op: OperatorKind::Delete,
685 motion_key: '0',
686 total_count: 1,
687 })
688 );
689 }
690
691 #[test]
692 fn op_d_zero_accumulates_when_inner_count_nonzero() {
693 let state = after_op(OperatorKind::Delete, 1);
695 let Outcome::Wait(s2) = step(state, Key::Char('1')) else {
696 panic!("expected Wait");
697 };
698 let Outcome::Wait(s3) = step(s2, Key::Char('0')) else {
699 panic!("expected Wait");
700 };
701 assert_eq!(
702 s3,
703 PendingState::AfterOp {
704 op: OperatorKind::Delete,
705 count1: 1,
706 inner_count: 10,
707 }
708 );
709 assert_eq!(
710 step(s3, Key::Char('w')),
711 Outcome::Commit(EngineCmd::ApplyOpMotion {
712 op: OperatorKind::Delete,
713 motion_key: 'w',
714 total_count: 10,
715 })
716 );
717 }
718
719 #[test]
722 fn op_yank_doubled() {
723 let state = after_op(OperatorKind::Yank, 1);
724 assert_eq!(
725 step(state, Key::Char('y')),
726 Outcome::Commit(EngineCmd::ApplyOpDouble {
727 op: OperatorKind::Yank,
728 total_count: 1,
729 })
730 );
731 }
732
733 #[test]
734 fn op_change_doubled() {
735 let state = after_op(OperatorKind::Change, 1);
736 assert_eq!(
737 step(state, Key::Char('c')),
738 Outcome::Commit(EngineCmd::ApplyOpDouble {
739 op: OperatorKind::Change,
740 total_count: 1,
741 })
742 );
743 }
744
745 #[test]
746 fn op_indent_doubled() {
747 let state = after_op(OperatorKind::Indent, 1);
748 assert_eq!(
749 step(state, Key::Char('>')),
750 Outcome::Commit(EngineCmd::ApplyOpDouble {
751 op: OperatorKind::Indent,
752 total_count: 1,
753 })
754 );
755 }
756
757 #[test]
758 fn op_outdent_doubled() {
759 let state = after_op(OperatorKind::Outdent, 1);
760 assert_eq!(
761 step(state, Key::Char('<')),
762 Outcome::Commit(EngineCmd::ApplyOpDouble {
763 op: OperatorKind::Outdent,
764 total_count: 1,
765 })
766 );
767 }
768
769 #[test]
770 fn op_yank_motion() {
771 let state = after_op(OperatorKind::Yank, 1);
772 assert_eq!(
773 step(state, Key::Char('$')),
774 Outcome::Commit(EngineCmd::ApplyOpMotion {
775 op: OperatorKind::Yank,
776 motion_key: '$',
777 total_count: 1,
778 })
779 );
780 }
781
782 #[test]
783 fn op_change_motion() {
784 let state = after_op(OperatorKind::Change, 1);
785 assert_eq!(
786 step(state, Key::Char('w')),
787 Outcome::Commit(EngineCmd::ApplyOpMotion {
788 op: OperatorKind::Change,
789 motion_key: 'w',
790 total_count: 1,
791 })
792 );
793 }
794
795 #[test]
796 fn op_indent_motion() {
797 let state = after_op(OperatorKind::Indent, 1);
798 assert_eq!(
799 step(state, Key::Char('j')),
800 Outcome::Commit(EngineCmd::ApplyOpMotion {
801 op: OperatorKind::Indent,
802 motion_key: 'j',
803 total_count: 1,
804 })
805 );
806 }
807
808 #[test]
809 fn op_outdent_motion() {
810 let state = after_op(OperatorKind::Outdent, 1);
811 assert_eq!(
812 step(state, Key::Char('k')),
813 Outcome::Commit(EngineCmd::ApplyOpMotion {
814 op: OperatorKind::Outdent,
815 motion_key: 'k',
816 total_count: 1,
817 })
818 );
819 }
820}