1use hjkl_engine::{
10 FsmMode, Host, Input, Key, LastChange, Motion, Operator, Pending, ScrollDir, VimMode,
11 op_is_change, parse_motion,
12};
13
14pub fn step_normal<H: Host>(
21 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
22 input: Input,
23) -> bool {
24 if let Key::Char(d @ '0'..='9') = input.key
26 && !input.ctrl
27 && !input.alt
28 && !matches!(
29 ed.pending(),
30 Pending::Replace
31 | Pending::Find { .. }
32 | Pending::OpFind { .. }
33 | Pending::VisualTextObj { .. }
34 )
35 && (d != '0' || ed.count() > 0)
36 {
37 ed.accumulate_count_digit(d as usize - '0' as usize);
38 return true;
39 }
40
41 match ed.take_pending() {
43 Pending::Replace => return handle_replace(ed, input),
44 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
45 Pending::OpFind {
46 op,
47 count1,
48 forward,
49 till,
50 } => return handle_op_find_target(ed, input, op, count1, forward, till),
51 Pending::G => return handle_after_g(ed, input),
52 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
53 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
54 Pending::OpTextObj { op, count1, inner } => {
55 return handle_text_object(ed, input, op, count1, inner);
56 }
57 Pending::VisualTextObj { inner } => {
58 return handle_visual_text_obj(ed, input, inner);
59 }
60 Pending::Z => return handle_after_z(ed, input),
61 Pending::SetMark => return handle_set_mark(ed, input),
62 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
63 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
64 Pending::SelectRegister => return handle_select_register(ed, input),
65 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
66 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
67 Pending::None => {}
68 }
69
70 let count = ed.take_count();
71
72 match input.key {
74 Key::Esc => {
75 ed.force_normal();
76 return true;
77 }
78 Key::Char('v') if !input.ctrl && ed.fsm_mode() == FsmMode::Normal => {
79 ed.set_visual_anchor(ed.cursor());
80 ed.set_mode(VimMode::Visual);
81 return true;
82 }
83 Key::Char('V') if !input.ctrl && ed.fsm_mode() == FsmMode::Normal => {
84 let (row, _) = ed.cursor();
85 ed.set_visual_line_anchor(row);
86 ed.set_mode(VimMode::VisualLine);
87 return true;
88 }
89 Key::Char('v') if !input.ctrl && ed.fsm_mode() == FsmMode::VisualLine => {
90 ed.set_visual_anchor(ed.cursor());
91 ed.set_mode(VimMode::Visual);
92 return true;
93 }
94 Key::Char('V') if !input.ctrl && ed.fsm_mode() == FsmMode::Visual => {
95 let (row, _) = ed.cursor();
96 ed.set_visual_line_anchor(row);
97 ed.set_mode(VimMode::VisualLine);
98 return true;
99 }
100 Key::Char('v') if input.ctrl && ed.fsm_mode() == FsmMode::Normal => {
101 let cur = ed.cursor();
102 ed.set_block_anchor(cur);
103 ed.set_block_vcol(cur.1);
104 ed.set_mode(VimMode::VisualBlock);
105 return true;
106 }
107 Key::Char('v') if input.ctrl && ed.fsm_mode() == FsmMode::VisualBlock => {
108 ed.set_mode(VimMode::Normal);
110 return true;
111 }
112 Key::Char('o') if !input.ctrl => match ed.fsm_mode() {
115 FsmMode::Visual => {
116 let cur = ed.cursor();
117 let anchor = ed.visual_anchor();
118 ed.set_visual_anchor(cur);
119 ed.jump_cursor(anchor.0, anchor.1);
120 return true;
121 }
122 FsmMode::VisualLine => {
123 let cur_row = ed.cursor().0;
124 let anchor_row = ed.visual_line_anchor();
125 ed.set_visual_line_anchor(cur_row);
126 ed.jump_cursor(anchor_row, 0);
127 return true;
128 }
129 FsmMode::VisualBlock => {
130 let cur = ed.cursor();
131 let anchor = ed.block_anchor();
132 ed.set_block_anchor(cur);
133 ed.set_block_vcol(anchor.1);
134 ed.jump_cursor(anchor.0, anchor.1);
135 return true;
136 }
137 _ => {}
138 },
139 _ => {}
140 }
141
142 if ed.is_visual()
144 && let Some(op) = visual_operator(&input)
145 {
146 ed.apply_visual_operator(op);
147 return true;
148 }
149
150 if ed.fsm_mode() == FsmMode::VisualBlock && !input.ctrl {
154 match input.key {
155 Key::Char('r') => {
156 ed.set_pending(Pending::Replace);
157 return true;
158 }
159 Key::Char('I') => {
160 let (top, bot, left, _right) = ed.visual_block_bounds();
161 ed.visual_block_insert_at_left(top, bot, left);
162 return true;
163 }
164 Key::Char('A') => {
165 let (top, bot, _left, right) = ed.visual_block_bounds();
166 let line_len = ed.line_char_count(top);
167 let col = (right + 1).min(line_len);
168 ed.visual_block_append_at_right(top, bot, col);
169 return true;
170 }
171 _ => {}
172 }
173 }
174
175 if matches!(ed.fsm_mode(), FsmMode::Visual | FsmMode::VisualLine)
177 && !input.ctrl
178 && matches!(input.key, Key::Char('i') | Key::Char('a'))
179 {
180 let inner = matches!(input.key, Key::Char('i'));
181 ed.set_pending(Pending::VisualTextObj { inner });
182 return true;
183 }
184
185 if input.ctrl
190 && let Key::Char(c) = input.key
191 {
192 match c {
193 'd' => {
194 ed.scroll_half_page(ScrollDir::Down, count);
195 return true;
196 }
197 'u' => {
198 ed.scroll_half_page(ScrollDir::Up, count);
199 return true;
200 }
201 'f' => {
202 ed.scroll_full_page(ScrollDir::Down, count);
203 return true;
204 }
205 'b' => {
206 ed.scroll_full_page(ScrollDir::Up, count);
207 return true;
208 }
209 'e' if ed.fsm_mode() == FsmMode::Normal => {
210 ed.scroll_line(ScrollDir::Down, count);
211 return true;
212 }
213 'y' if ed.fsm_mode() == FsmMode::Normal => {
214 ed.scroll_line(ScrollDir::Up, count);
215 return true;
216 }
217 'r' => {
218 ed.redo();
219 return true;
220 }
221 'a' if ed.fsm_mode() == FsmMode::Normal => {
222 ed.adjust_number(count.max(1) as i64);
223 return true;
224 }
225 'x' if ed.fsm_mode() == FsmMode::Normal => {
226 ed.adjust_number(-(count.max(1) as i64));
227 return true;
228 }
229 'o' if ed.fsm_mode() == FsmMode::Normal => {
230 ed.jump_back(count);
231 return true;
232 }
233 'i' if ed.fsm_mode() == FsmMode::Normal => {
234 ed.jump_forward(count);
235 return true;
236 }
237 _ => {}
238 }
239 }
240
241 if !input.ctrl && input.key == Key::Tab && ed.fsm_mode() == FsmMode::Normal {
243 ed.jump_forward(count);
244 return true;
245 }
246
247 if let Some(motion) = parse_motion(&input) {
249 ed.execute_motion(motion.clone(), count);
250 if ed.fsm_mode() == FsmMode::VisualBlock {
252 ed.update_block_vcol(&motion);
253 }
254 if let Motion::Find { ch, forward, till } = motion {
255 ed.set_last_find(Some((ch, forward, till)));
256 }
257 return true;
258 }
259
260 if ed.fsm_mode() == FsmMode::Normal && handle_normal_only(ed, &input, count) {
262 return true;
263 }
264
265 if ed.fsm_mode() == FsmMode::Normal
267 && let Key::Char(op_ch) = input.key
268 && !input.ctrl
269 && let Some(op) = char_to_operator(op_ch)
270 {
271 ed.set_pending(Pending::Op { op, count1: count });
272 return true;
273 }
274
275 if ed.fsm_mode() == FsmMode::Normal
277 && let Some((forward, till)) = find_entry(&input)
278 {
279 ed.set_count(count);
280 ed.set_pending(Pending::Find { forward, till });
281 return true;
282 }
283
284 if !input.ctrl && input.key == Key::Char('g') && ed.fsm_mode() == FsmMode::Normal {
286 ed.set_count(count);
287 ed.set_pending(Pending::G);
288 return true;
289 }
290
291 if !input.ctrl
293 && input.key == Key::Char('z')
294 && matches!(
295 ed.fsm_mode(),
296 FsmMode::Normal | FsmMode::Visual | FsmMode::VisualLine | FsmMode::VisualBlock
297 )
298 {
299 ed.set_pending(Pending::Z);
300 return true;
301 }
302
303 if !input.ctrl
309 && matches!(
310 ed.fsm_mode(),
311 FsmMode::Normal | FsmMode::Visual | FsmMode::VisualLine | FsmMode::VisualBlock
312 )
313 && input.key == Key::Char('`')
314 {
315 ed.set_pending(Pending::GotoMarkChar);
316 return true;
317 }
318 if !input.ctrl && ed.fsm_mode() == FsmMode::Normal {
319 match input.key {
320 Key::Char('m') => {
321 ed.set_pending(Pending::SetMark);
322 return true;
323 }
324 Key::Char('\'') => {
325 ed.set_pending(Pending::GotoMarkLine);
326 return true;
327 }
328 Key::Char('`') => {
329 ed.set_pending(Pending::GotoMarkChar);
331 return true;
332 }
333 Key::Char('"') => {
334 ed.set_pending(Pending::SelectRegister);
337 return true;
338 }
339 Key::Char('@') => {
340 ed.set_pending(Pending::PlayMacroTarget { count });
344 return true;
345 }
346 Key::Char('q') if ed.recording_macro().is_none() => {
347 ed.set_pending(Pending::RecordMacroTarget);
352 return true;
353 }
354 _ => {}
355 }
356 }
357
358 true
360}
361
362fn handle_normal_only<H: Host>(
366 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
367 input: &Input,
368 count: usize,
369) -> bool {
370 if input.ctrl {
371 return false;
372 }
373 match input.key {
374 Key::Char('i') => {
375 ed.enter_insert_i(count);
376 true
377 }
378 Key::Char('I') => {
379 ed.enter_insert_shift_i(count);
380 true
381 }
382 Key::Char('a') => {
383 ed.enter_insert_a(count);
384 true
385 }
386 Key::Char('A') => {
387 ed.enter_insert_shift_a(count);
388 true
389 }
390 Key::Char('R') => {
391 ed.enter_replace_mode(count);
392 true
393 }
394 Key::Char('o') => {
395 ed.open_line_below(count);
396 true
397 }
398 Key::Char('O') => {
399 ed.open_line_above(count);
400 true
401 }
402 Key::Char('x') => {
403 ed.delete_char_forward(count);
404 true
405 }
406 Key::Char('X') => {
407 ed.delete_char_backward(count);
408 true
409 }
410 Key::Char('~') => {
411 ed.toggle_case_at_cursor(count);
412 true
413 }
414 Key::Char('J') => {
415 ed.join_line(count);
416 true
417 }
418 Key::Char('D') => {
419 ed.delete_to_eol();
420 true
421 }
422 Key::Char('Y') => {
423 ed.yank_to_eol(count);
424 true
425 }
426 Key::Char('C') => {
427 ed.change_to_eol();
428 true
429 }
430 Key::Char('s') => {
431 ed.substitute_char(count);
432 true
433 }
434 Key::Char('S') => {
435 ed.substitute_line(count);
436 true
437 }
438 Key::Char('p') => {
439 ed.paste_after(count);
440 true
441 }
442 Key::Char('P') => {
443 ed.paste_before(count);
444 true
445 }
446 Key::Char('u') => {
447 ed.undo();
448 true
449 }
450 Key::Char('r') => {
451 ed.set_count(count);
452 ed.set_pending(Pending::Replace);
453 true
454 }
455 Key::Char('/') => {
456 ed.enter_search(true);
457 true
458 }
459 Key::Char('?') => {
460 ed.enter_search(false);
461 true
462 }
463 Key::Char('.') => {
464 ed.replay_last_change(count);
465 true
466 }
467 _ => false,
468 }
469}
470
471fn handle_set_mark<H: Host>(
474 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
475 input: Input,
476) -> bool {
477 if let Key::Char(c) = input.key {
478 ed.set_mark_at_cursor(c);
479 }
480 true
481}
482
483fn handle_select_register<H: Host>(
484 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
485 input: Input,
486) -> bool {
487 if let Key::Char(c) = input.key {
488 ed.set_pending_register(c);
489 }
490 true
491}
492
493fn handle_record_macro_target<H: Host>(
494 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
495 input: Input,
496) -> bool {
497 if let Key::Char(c) = input.key
498 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
499 {
500 ed.set_recording_macro(Some(c));
501 if c.is_ascii_uppercase() {
504 let lower = c.to_ascii_lowercase();
505 let text = ed
509 .registers()
510 .read(lower)
511 .map(|s| s.text.clone())
512 .unwrap_or_default();
513 ed.set_recording_keys(hjkl_engine::decode_macro(&text));
514 } else {
515 ed.set_recording_keys(vec![]);
516 }
517 }
518 true
519}
520
521fn handle_play_macro_target<H: Host>(
522 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
523 input: Input,
524 count: usize,
525) -> bool {
526 let reg = match input.key {
527 Key::Char('@') => ed.last_macro(),
528 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
529 Some(c.to_ascii_lowercase())
530 }
531 _ => None,
532 };
533 let Some(reg) = reg else {
534 return true;
535 };
536 let text = match ed.registers().read(reg) {
539 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
540 _ => return true,
541 };
542 let keys = hjkl_engine::decode_macro(&text);
543 ed.set_last_macro(Some(reg));
544 let times = count.max(1);
545 let was_replaying = ed.is_replaying_macro_raw();
546 ed.set_replaying_macro_raw(true);
547 for _ in 0..times {
548 for k in keys.iter().copied() {
549 crate::dispatch_input(ed, k);
550 }
551 }
552 ed.set_replaying_macro_raw(was_replaying);
553 true
554}
555
556fn handle_goto_mark<H: Host>(
557 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
558 input: Input,
559 linewise: bool,
560) -> bool {
561 let Key::Char(c) = input.key else {
562 return true;
563 };
564 if linewise {
565 ed.goto_mark_line(c);
566 } else {
567 ed.goto_mark_char(c);
568 }
569 true
570}
571
572fn handle_after_op<H: Host>(
573 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
574 input: Input,
575 op: Operator,
576 count1: usize,
577) -> bool {
578 if let Key::Char(d @ '0'..='9') = input.key
580 && !input.ctrl
581 && (d != '0' || ed.count() > 0)
582 {
583 ed.accumulate_count_digit(d as usize - '0' as usize);
584 ed.set_pending(Pending::Op { op, count1 });
585 return true;
586 }
587
588 if input.key == Key::Esc {
590 ed.reset_count();
591 return true;
592 }
593
594 let double_ch = match op {
598 Operator::Delete => Some('d'),
599 Operator::Change => Some('c'),
600 Operator::Yank => Some('y'),
601 Operator::Indent => Some('>'),
602 Operator::Outdent => Some('<'),
603 Operator::Uppercase => Some('U'),
604 Operator::Lowercase => Some('u'),
605 Operator::ToggleCase => Some('~'),
606 Operator::Fold => None,
607 Operator::Reflow => Some('q'),
610 Operator::AutoIndent => Some('='),
612 };
613 if let Key::Char(c) = input.key
614 && !input.ctrl
615 && Some(c) == double_ch
616 {
617 let count2 = ed.take_count();
618 let total = count1.max(1) * count2.max(1);
619 ed.apply_op_double(op, total);
620 return true;
621 }
622
623 if let Key::Char('i') | Key::Char('a') = input.key
625 && !input.ctrl
626 {
627 let inner = matches!(input.key, Key::Char('i'));
628 ed.set_pending(Pending::OpTextObj { op, count1, inner });
629 return true;
630 }
631
632 if input.key == Key::Char('g') && !input.ctrl {
634 ed.set_pending(Pending::OpG { op, count1 });
635 return true;
636 }
637
638 if let Some((forward, till)) = find_entry(&input) {
640 ed.set_pending(Pending::OpFind {
641 op,
642 count1,
643 forward,
644 till,
645 });
646 return true;
647 }
648
649 let count2 = ed.take_count();
651 let total = count1.max(1) * count2.max(1);
652 if let Some(motion) = parse_motion(&input) {
653 let motion = match motion {
654 Motion::FindRepeat { reverse } => match ed.last_find() {
655 Some((ch, forward, till)) => Motion::Find {
656 ch,
657 forward: if reverse { !forward } else { forward },
658 till,
659 },
660 None => return true,
661 },
662 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
666 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
667 m => m,
668 };
669 ed.apply_op_with_motion_direct(op, &motion, total);
670 if let Motion::Find { ch, forward, till } = &motion {
671 ed.set_last_find(Some((*ch, *forward, *till)));
672 }
673 if !ed.is_replaying() && op_is_change(op) {
674 ed.set_last_change(Some(LastChange::OpMotion {
675 op,
676 motion,
677 count: total,
678 inserted: None,
679 }));
680 }
681 return true;
682 }
683
684 true
686}
687
688fn handle_op_after_g<H: Host>(
689 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
690 input: Input,
691 op: Operator,
692 count1: usize,
693) -> bool {
694 if input.ctrl {
695 return true;
696 }
697 let count2 = ed.take_count();
698 let total = count1.max(1) * count2.max(1);
699 if let Key::Char(ch) = input.key {
700 ed.apply_op_g(op, ch, total);
701 }
702 true
703}
704
705fn handle_after_g<H: Host>(
706 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
707 input: Input,
708) -> bool {
709 let count = ed.take_count();
710 if let Key::Char(ch) = input.key {
713 ed.after_g(ch, count);
714 }
715 true
716}
717
718fn handle_after_z<H: Host>(
719 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
720 input: Input,
721) -> bool {
722 let count = ed.take_count();
723 if let Key::Char(ch) = input.key {
726 ed.after_z(ch, count);
727 }
728 true
729}
730
731fn handle_replace<H: Host>(
732 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
733 input: Input,
734) -> bool {
735 if let Key::Char(ch) = input.key {
736 if ed.fsm_mode() == FsmMode::VisualBlock {
737 ed.replace_block_char(ch);
738 return true;
739 }
740 let count = ed.take_count();
741 ed.replace_char_at(ch, count.max(1));
742 if !ed.is_replaying() {
743 ed.set_last_change(Some(LastChange::ReplaceChar {
744 ch,
745 count: count.max(1),
746 }));
747 }
748 }
749 true
750}
751
752fn handle_find_target<H: Host>(
753 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
754 input: Input,
755 forward: bool,
756 till: bool,
757) -> bool {
758 let Key::Char(ch) = input.key else {
759 return true;
760 };
761 let count = ed.take_count();
762 ed.find_char(ch, forward, till, count.max(1));
763 true
764}
765
766fn handle_op_find_target<H: Host>(
767 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
768 input: Input,
769 op: Operator,
770 count1: usize,
771 forward: bool,
772 till: bool,
773) -> bool {
774 let Key::Char(ch) = input.key else {
775 return true;
776 };
777 let count2 = ed.take_count();
778 let total = count1.max(1) * count2.max(1);
779 ed.apply_op_find(op, ch, forward, till, total);
780 true
781}
782
783fn handle_text_object<H: Host>(
784 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
785 input: Input,
786 op: Operator,
787 _count1: usize,
788 inner: bool,
789) -> bool {
790 let Key::Char(ch) = input.key else {
791 return true;
792 };
793 ed.apply_op_text_obj(op, ch, inner, 1);
796 true
797}
798
799fn handle_visual_text_obj<H: Host>(
800 ed: &mut hjkl_engine::Editor<hjkl_buffer::Buffer, H>,
801 input: Input,
802 inner: bool,
803) -> bool {
804 let Key::Char(ch) = input.key else {
805 return true;
806 };
807 ed.visual_text_obj_extend(ch, inner);
808 true
809}
810
811fn char_to_operator(c: char) -> Option<Operator> {
814 match c {
815 'd' => Some(Operator::Delete),
816 'c' => Some(Operator::Change),
817 'y' => Some(Operator::Yank),
818 '>' => Some(Operator::Indent),
819 '<' => Some(Operator::Outdent),
820 '=' => Some(Operator::AutoIndent),
821 _ => None,
822 }
823}
824
825fn visual_operator(input: &Input) -> Option<Operator> {
826 if input.ctrl {
827 return None;
828 }
829 match input.key {
830 Key::Char('y') => Some(Operator::Yank),
831 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
832 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
833 Key::Char('U') => Some(Operator::Uppercase),
835 Key::Char('u') => Some(Operator::Lowercase),
836 Key::Char('~') => Some(Operator::ToggleCase),
837 Key::Char('>') => Some(Operator::Indent),
839 Key::Char('<') => Some(Operator::Outdent),
840 Key::Char('=') => Some(Operator::AutoIndent),
842 _ => None,
843 }
844}
845
846fn find_entry(input: &Input) -> Option<(bool, bool)> {
847 if input.ctrl {
848 return None;
849 }
850 match input.key {
851 Key::Char('f') => Some((true, false)),
852 Key::Char('F') => Some((false, false)),
853 Key::Char('t') => Some((true, true)),
854 Key::Char('T') => Some((false, true)),
855 _ => None,
856 }
857}