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 }
48
49#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum Outcome {
52 Wait(PendingState),
54 Commit(crate::cmd::EngineCmd),
56 Cancel,
58 Forward,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub enum Key {
67 Char(char),
68 Esc,
69 Enter,
70 Backspace,
71 Tab,
72 }
74
75pub fn step(state: PendingState, key: Key) -> Outcome {
76 match state {
77 PendingState::Replace { count } => match key {
78 Key::Esc => Outcome::Cancel,
79 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::ReplaceChar { ch, count }),
80 Key::Enter => Outcome::Commit(crate::cmd::EngineCmd::ReplaceChar { ch: '\n', count }),
81 _ => Outcome::Cancel,
82 },
83 PendingState::Find {
84 count,
85 forward,
86 till,
87 } => match key {
88 Key::Esc => Outcome::Cancel,
89 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::FindChar {
90 ch,
91 forward,
92 till,
93 count,
94 }),
95 _ => Outcome::Cancel,
97 },
98 PendingState::AfterG { count } => match key {
99 Key::Esc => Outcome::Cancel,
100 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::AfterGChord { ch, count }),
101 _ => Outcome::Cancel,
103 },
104 PendingState::AfterZ { count } => match key {
105 Key::Esc => Outcome::Cancel,
106 Key::Char(ch) => Outcome::Commit(crate::cmd::EngineCmd::AfterZChord { ch, count }),
107 _ => Outcome::Cancel,
109 },
110 PendingState::AfterOp {
111 op,
112 count1,
113 inner_count,
114 } => match key {
115 Key::Esc => Outcome::Cancel,
116 Key::Char(d @ '0'..='9') => {
117 if d == '0' && inner_count == 0 {
119 let total = count1.max(1);
121 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpMotion {
122 op,
123 motion_key: '0',
124 total_count: total,
125 })
126 } else {
127 let new_inner = inner_count
128 .saturating_mul(10)
129 .saturating_add(d as usize - '0' as usize);
130 Outcome::Wait(PendingState::AfterOp {
131 op,
132 count1,
133 inner_count: new_inner,
134 })
135 }
136 }
137 Key::Char(ch) => {
138 let total = count1.max(1) * inner_count.max(1);
139 if ch == op.double_char() {
141 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpDouble {
142 op,
143 total_count: total,
144 })
145 } else if ch == 'i' {
147 Outcome::Commit(crate::cmd::EngineCmd::EnterOpTextObj {
148 op,
149 count1,
150 inner: true,
151 })
152 } else if ch == 'a' {
153 Outcome::Commit(crate::cmd::EngineCmd::EnterOpTextObj {
154 op,
155 count1,
156 inner: false,
157 })
158 } else if ch == 'g' {
160 Outcome::Commit(crate::cmd::EngineCmd::EnterOpG { op, count1 })
161 } else if ch == 'f' {
163 Outcome::Commit(crate::cmd::EngineCmd::EnterOpFind {
164 op,
165 count1,
166 forward: true,
167 till: false,
168 })
169 } else if ch == 'F' {
170 Outcome::Commit(crate::cmd::EngineCmd::EnterOpFind {
171 op,
172 count1,
173 forward: false,
174 till: false,
175 })
176 } else if ch == 't' {
177 Outcome::Commit(crate::cmd::EngineCmd::EnterOpFind {
178 op,
179 count1,
180 forward: true,
181 till: true,
182 })
183 } else if ch == 'T' {
184 Outcome::Commit(crate::cmd::EngineCmd::EnterOpFind {
185 op,
186 count1,
187 forward: false,
188 till: true,
189 })
190 } else {
191 Outcome::Commit(crate::cmd::EngineCmd::ApplyOpMotion {
194 op,
195 motion_key: ch,
196 total_count: total,
197 })
198 }
199 }
200 _ => Outcome::Cancel,
202 },
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use crate::cmd::EngineCmd;
210 use crate::operator::OperatorKind;
211
212 #[test]
215 fn after_g_gg_commits() {
216 let state = PendingState::AfterG { count: 1 };
217 assert_eq!(
218 step(state, Key::Char('g')),
219 Outcome::Commit(EngineCmd::AfterGChord { ch: 'g', count: 1 })
220 );
221 }
222
223 #[test]
224 fn after_g_gv_commits() {
225 let state = PendingState::AfterG { count: 1 };
226 assert_eq!(
227 step(state, Key::Char('v')),
228 Outcome::Commit(EngineCmd::AfterGChord { ch: 'v', count: 1 })
229 );
230 }
231
232 #[test]
233 fn after_g_gu_operator_commits() {
234 let state = PendingState::AfterG { count: 1 };
236 assert_eq!(
237 step(state, Key::Char('U')),
238 Outcome::Commit(EngineCmd::AfterGChord { ch: 'U', count: 1 })
239 );
240 }
241
242 #[test]
243 fn after_g_gi_commits() {
244 let state = PendingState::AfterG { count: 1 };
245 assert_eq!(
246 step(state, Key::Char('i')),
247 Outcome::Commit(EngineCmd::AfterGChord { ch: 'i', count: 1 })
248 );
249 }
250
251 #[test]
252 fn after_g_esc_cancels() {
253 let state = PendingState::AfterG { count: 1 };
254 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
255 }
256
257 #[test]
258 fn after_g_count_carry_through() {
259 let state = PendingState::AfterG { count: 5 };
261 assert_eq!(
262 step(state, Key::Char('g')),
263 Outcome::Commit(EngineCmd::AfterGChord { ch: 'g', count: 5 })
264 );
265 }
266
267 #[test]
268 fn after_g_non_char_cancels() {
269 let state = PendingState::AfterG { count: 1 };
271 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
272 }
273
274 #[test]
277 fn after_z_zz_commits() {
278 let state = PendingState::AfterZ { count: 1 };
279 assert_eq!(
280 step(state, Key::Char('z')),
281 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 1 })
282 );
283 }
284
285 #[test]
286 fn after_z_zf_commits() {
287 let state = PendingState::AfterZ { count: 1 };
288 assert_eq!(
289 step(state, Key::Char('f')),
290 Outcome::Commit(EngineCmd::AfterZChord { ch: 'f', count: 1 })
291 );
292 }
293
294 #[test]
295 fn after_z_esc_cancels() {
296 let state = PendingState::AfterZ { count: 1 };
297 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
298 }
299
300 #[test]
301 fn after_z_count_carry_through() {
302 let state = PendingState::AfterZ { count: 3 };
304 assert_eq!(
305 step(state, Key::Char('z')),
306 Outcome::Commit(EngineCmd::AfterZChord { ch: 'z', count: 3 })
307 );
308 }
309
310 #[test]
311 fn after_z_non_char_cancels() {
312 let state = PendingState::AfterZ { count: 1 };
314 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
315 }
316
317 fn after_op(op: OperatorKind, count1: usize) -> PendingState {
320 PendingState::AfterOp {
321 op,
322 count1,
323 inner_count: 0,
324 }
325 }
326
327 #[test]
328 fn op_d_then_w_commits_motion() {
329 let state = after_op(OperatorKind::Delete, 1);
330 assert_eq!(
331 step(state, Key::Char('w')),
332 Outcome::Commit(EngineCmd::ApplyOpMotion {
333 op: OperatorKind::Delete,
334 motion_key: 'w',
335 total_count: 1,
336 })
337 );
338 }
339
340 #[test]
341 fn op_d_then_d_commits_double() {
342 let state = after_op(OperatorKind::Delete, 1);
343 assert_eq!(
344 step(state, Key::Char('d')),
345 Outcome::Commit(EngineCmd::ApplyOpDouble {
346 op: OperatorKind::Delete,
347 total_count: 1,
348 })
349 );
350 }
351
352 #[test]
353 fn op_d_inner_count_d3w_commits_motion_with_count_3() {
354 let state = after_op(OperatorKind::Delete, 1);
356 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
358 panic!("expected Wait");
359 };
360 assert_eq!(
361 state2,
362 PendingState::AfterOp {
363 op: OperatorKind::Delete,
364 count1: 1,
365 inner_count: 3
366 }
367 );
368 assert_eq!(
370 step(state2, Key::Char('w')),
371 Outcome::Commit(EngineCmd::ApplyOpMotion {
372 op: OperatorKind::Delete,
373 motion_key: 'w',
374 total_count: 3,
375 })
376 );
377 }
378
379 #[test]
380 fn op_2d_d_commits_double_with_count_2() {
381 let state = after_op(OperatorKind::Delete, 2);
383 assert_eq!(
384 step(state, Key::Char('d')),
385 Outcome::Commit(EngineCmd::ApplyOpDouble {
386 op: OperatorKind::Delete,
387 total_count: 2,
388 })
389 );
390 }
391
392 #[test]
393 fn op_2d_3w_commits_motion_with_total_6() {
394 let state = after_op(OperatorKind::Delete, 2);
396 let Outcome::Wait(state2) = step(state, Key::Char('3')) else {
397 panic!("expected Wait");
398 };
399 assert_eq!(
400 step(state2, Key::Char('w')),
401 Outcome::Commit(EngineCmd::ApplyOpMotion {
402 op: OperatorKind::Delete,
403 motion_key: 'w',
404 total_count: 6,
405 })
406 );
407 }
408
409 #[test]
410 fn op_d_then_i_emits_enter_op_text_obj_inner_true() {
411 let state = after_op(OperatorKind::Delete, 1);
412 assert_eq!(
413 step(state, Key::Char('i')),
414 Outcome::Commit(EngineCmd::EnterOpTextObj {
415 op: OperatorKind::Delete,
416 count1: 1,
417 inner: true,
418 })
419 );
420 }
421
422 #[test]
423 fn op_d_then_a_emits_enter_op_text_obj_inner_false() {
424 let state = after_op(OperatorKind::Delete, 1);
425 assert_eq!(
426 step(state, Key::Char('a')),
427 Outcome::Commit(EngineCmd::EnterOpTextObj {
428 op: OperatorKind::Delete,
429 count1: 1,
430 inner: false,
431 })
432 );
433 }
434
435 #[test]
436 fn op_d_then_g_emits_enter_op_g() {
437 let state = after_op(OperatorKind::Delete, 1);
438 assert_eq!(
439 step(state, Key::Char('g')),
440 Outcome::Commit(EngineCmd::EnterOpG {
441 op: OperatorKind::Delete,
442 count1: 1,
443 })
444 );
445 }
446
447 #[test]
448 fn op_d_then_f_emits_enter_op_find_forward_not_till() {
449 let state = after_op(OperatorKind::Delete, 1);
450 assert_eq!(
451 step(state, Key::Char('f')),
452 Outcome::Commit(EngineCmd::EnterOpFind {
453 op: OperatorKind::Delete,
454 count1: 1,
455 forward: true,
456 till: false,
457 })
458 );
459 }
460
461 #[test]
462 fn op_d_then_cap_f_emits_enter_op_find_backward_not_till() {
463 let state = after_op(OperatorKind::Delete, 1);
464 assert_eq!(
465 step(state, Key::Char('F')),
466 Outcome::Commit(EngineCmd::EnterOpFind {
467 op: OperatorKind::Delete,
468 count1: 1,
469 forward: false,
470 till: false,
471 })
472 );
473 }
474
475 #[test]
476 fn op_d_then_t_emits_enter_op_find_forward_till() {
477 let state = after_op(OperatorKind::Delete, 1);
478 assert_eq!(
479 step(state, Key::Char('t')),
480 Outcome::Commit(EngineCmd::EnterOpFind {
481 op: OperatorKind::Delete,
482 count1: 1,
483 forward: true,
484 till: true,
485 })
486 );
487 }
488
489 #[test]
490 fn op_d_then_cap_t_emits_enter_op_find_backward_till() {
491 let state = after_op(OperatorKind::Delete, 1);
492 assert_eq!(
493 step(state, Key::Char('T')),
494 Outcome::Commit(EngineCmd::EnterOpFind {
495 op: OperatorKind::Delete,
496 count1: 1,
497 forward: false,
498 till: true,
499 })
500 );
501 }
502
503 #[test]
504 fn op_d_then_esc_cancels() {
505 let state = after_op(OperatorKind::Delete, 1);
506 assert_eq!(step(state, Key::Esc), Outcome::Cancel);
507 }
508
509 #[test]
510 fn op_d_non_char_cancels() {
511 let state = after_op(OperatorKind::Delete, 1);
512 assert_eq!(step(state, Key::Enter), Outcome::Cancel);
513 }
514
515 #[test]
516 fn op_d_bare_zero_is_line_start_motion() {
517 let state = after_op(OperatorKind::Delete, 1);
519 assert_eq!(
520 step(state, Key::Char('0')),
521 Outcome::Commit(EngineCmd::ApplyOpMotion {
522 op: OperatorKind::Delete,
523 motion_key: '0',
524 total_count: 1,
525 })
526 );
527 }
528
529 #[test]
530 fn op_d_zero_accumulates_when_inner_count_nonzero() {
531 let state = after_op(OperatorKind::Delete, 1);
533 let Outcome::Wait(s2) = step(state, Key::Char('1')) else {
534 panic!("expected Wait");
535 };
536 let Outcome::Wait(s3) = step(s2, Key::Char('0')) else {
537 panic!("expected Wait");
538 };
539 assert_eq!(
540 s3,
541 PendingState::AfterOp {
542 op: OperatorKind::Delete,
543 count1: 1,
544 inner_count: 10,
545 }
546 );
547 assert_eq!(
548 step(s3, Key::Char('w')),
549 Outcome::Commit(EngineCmd::ApplyOpMotion {
550 op: OperatorKind::Delete,
551 motion_key: 'w',
552 total_count: 10,
553 })
554 );
555 }
556
557 #[test]
560 fn op_yank_doubled() {
561 let state = after_op(OperatorKind::Yank, 1);
562 assert_eq!(
563 step(state, Key::Char('y')),
564 Outcome::Commit(EngineCmd::ApplyOpDouble {
565 op: OperatorKind::Yank,
566 total_count: 1,
567 })
568 );
569 }
570
571 #[test]
572 fn op_change_doubled() {
573 let state = after_op(OperatorKind::Change, 1);
574 assert_eq!(
575 step(state, Key::Char('c')),
576 Outcome::Commit(EngineCmd::ApplyOpDouble {
577 op: OperatorKind::Change,
578 total_count: 1,
579 })
580 );
581 }
582
583 #[test]
584 fn op_indent_doubled() {
585 let state = after_op(OperatorKind::Indent, 1);
586 assert_eq!(
587 step(state, Key::Char('>')),
588 Outcome::Commit(EngineCmd::ApplyOpDouble {
589 op: OperatorKind::Indent,
590 total_count: 1,
591 })
592 );
593 }
594
595 #[test]
596 fn op_outdent_doubled() {
597 let state = after_op(OperatorKind::Outdent, 1);
598 assert_eq!(
599 step(state, Key::Char('<')),
600 Outcome::Commit(EngineCmd::ApplyOpDouble {
601 op: OperatorKind::Outdent,
602 total_count: 1,
603 })
604 );
605 }
606
607 #[test]
608 fn op_yank_motion() {
609 let state = after_op(OperatorKind::Yank, 1);
610 assert_eq!(
611 step(state, Key::Char('$')),
612 Outcome::Commit(EngineCmd::ApplyOpMotion {
613 op: OperatorKind::Yank,
614 motion_key: '$',
615 total_count: 1,
616 })
617 );
618 }
619
620 #[test]
621 fn op_change_motion() {
622 let state = after_op(OperatorKind::Change, 1);
623 assert_eq!(
624 step(state, Key::Char('w')),
625 Outcome::Commit(EngineCmd::ApplyOpMotion {
626 op: OperatorKind::Change,
627 motion_key: 'w',
628 total_count: 1,
629 })
630 );
631 }
632
633 #[test]
634 fn op_indent_motion() {
635 let state = after_op(OperatorKind::Indent, 1);
636 assert_eq!(
637 step(state, Key::Char('j')),
638 Outcome::Commit(EngineCmd::ApplyOpMotion {
639 op: OperatorKind::Indent,
640 motion_key: 'j',
641 total_count: 1,
642 })
643 );
644 }
645
646 #[test]
647 fn op_outdent_motion() {
648 let state = after_op(OperatorKind::Outdent, 1);
649 assert_eq!(
650 step(state, Key::Char('k')),
651 Outcome::Commit(EngineCmd::ApplyOpMotion {
652 op: OperatorKind::Outdent,
653 motion_key: 'k',
654 total_count: 1,
655 })
656 );
657 }
658}