1use hjkl_vim_types::{LastHorizontalMotion, Mode, Motion};
6
7use hjkl_engine::input::{Input, Key};
8
9use super::*;
10use crate::vim_state::{vim, vim_mut};
11use hjkl_engine::buf_helpers::{
12 buf_cursor_pos, buf_line, buf_line_chars, buf_row_count, buf_set_cursor_rc,
13};
14use hjkl_engine::{Editor, Move};
15
16pub fn parse_motion(input: &Input) -> Option<Motion> {
20 if input.ctrl {
21 if input.key == Key::Char('h') {
25 return Some(Motion::BackspaceBack);
26 }
27 return None;
28 }
29 match input.key {
30 Key::Char('h') | Key::Left => Some(Motion::Left),
31 Key::Char('l') | Key::Right => Some(Motion::Right),
32 Key::Char(' ') => Some(Motion::SpaceFwd),
36 Key::Backspace => Some(Motion::BackspaceBack),
37 Key::Char('j') | Key::Down => Some(Motion::Down),
38 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
40 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
42 Key::Char('_') => Some(Motion::FirstNonBlankLine),
44 Key::Char('k') | Key::Up => Some(Motion::Up),
45 Key::Char('w') => Some(Motion::WordFwd),
46 Key::Char('W') => Some(Motion::BigWordFwd),
47 Key::Char('b') => Some(Motion::WordBack),
48 Key::Char('B') => Some(Motion::BigWordBack),
49 Key::Char('e') => Some(Motion::WordEnd),
50 Key::Char('E') => Some(Motion::BigWordEnd),
51 Key::Char('0') | Key::Home => Some(Motion::LineStart),
52 Key::Char('^') => Some(Motion::FirstNonBlank),
53 Key::Char('$') | Key::End => Some(Motion::LineEnd),
54 Key::Char('G') => Some(Motion::FileBottom),
55 Key::Char('%') => Some(Motion::MatchBracket),
56 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
57 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
58 Key::Char('*') => Some(Motion::WordAtCursor {
59 forward: true,
60 whole_word: true,
61 }),
62 Key::Char('#') => Some(Motion::WordAtCursor {
63 forward: false,
64 whole_word: true,
65 }),
66 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
67 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
68 Key::Char('H') => Some(Motion::ViewportTop),
69 Key::Char('M') => Some(Motion::ViewportMiddle),
70 Key::Char('L') => Some(Motion::ViewportBottom),
71 Key::Char('{') => Some(Motion::ParagraphPrev),
72 Key::Char('}') => Some(Motion::ParagraphNext),
73 Key::Char('(') => Some(Motion::SentencePrev),
74 Key::Char(')') => Some(Motion::SentenceNext),
75 Key::Char('|') => Some(Motion::GotoColumn),
76 _ => None,
77 }
78}
79pub fn execute_motion<H: hjkl_engine::types::Host>(
80 ed: &mut Editor<hjkl_buffer::View, H>,
81 motion: Motion,
82 count: usize,
83) {
84 let count = count.clamp(1, MAX_COUNT);
85 if let Motion::FindRepeat { reverse } = motion
88 && vim(ed).last_horizontal_motion == LastHorizontalMotion::Sneak
89 {
90 if let Some(((c1, c2), fwd)) = vim(ed).last_sneak {
91 let effective_fwd = if reverse { !fwd } else { fwd };
92 apply_sneak(ed, c1, c2, effective_fwd, count);
93 }
94 return;
95 }
96 let motion = match motion {
100 Motion::FindRepeat { reverse } => match vim(ed).last_find {
101 Some((ch, forward, till)) => {
102 vim_mut(ed).find_repeat_skip = true;
103 Motion::Find {
104 ch,
105 forward: if reverse { !forward } else { forward },
106 till,
107 }
108 }
109 None => return,
110 },
111 other => other,
112 };
113 let pre_pos = ed.cursor();
114 apply_motion_cursor(ed, &motion, count);
115 let post_pos = ed.cursor();
116 if is_big_jump(&motion) && pre_pos != post_pos {
117 ed.push_jump(pre_pos);
118 }
119 match motion_class(&motion) {
125 MotionClass::Vertical => {
126 ed.move_cursor(Move::Vertical { row: ed.cursor().0 });
130 }
131 MotionClass::Jump => {
132 let pos = ed.cursor();
135 ed.move_cursor(Move::Jump {
136 row: pos.0,
137 col: pos.1,
138 });
139 }
140 MotionClass::Horizontal
144 if pre_pos == post_pos
145 && matches!(
146 motion,
147 Motion::Left | Motion::Right | Motion::SpaceFwd | Motion::BackspaceBack
148 ) => {}
149 MotionClass::Horizontal => {
150 ed.move_cursor(Move::Horizontal { col: ed.cursor().1 });
152 if matches!(motion, Motion::LineEnd) {
156 ed.set_sticky_col(Some(usize::MAX));
157 }
158 }
159 }
160 if motion_opens_fold(&motion, count, pre_pos != post_pos) {
161 foldopen_cursor(ed);
162 }
163 ed.sync_buffer_from_textarea();
168}
169pub fn execute_motion_with_block_vcol<H: hjkl_engine::types::Host>(
178 ed: &mut Editor<hjkl_buffer::View, H>,
179 motion: Motion,
180 count: usize,
181) {
182 let motion_copy = motion.clone();
183 execute_motion(ed, motion, count);
184 if vim(ed).mode == Mode::VisualBlock {
185 update_block_vcol(ed, &motion_copy);
186 }
187}
188pub fn apply_motion_kind<H: hjkl_engine::types::Host>(
216 ed: &mut Editor<hjkl_buffer::View, H>,
217 kind: hjkl_engine::MotionKind,
218 count: usize,
219) {
220 let count = count.max(1);
221 match kind {
222 hjkl_engine::MotionKind::CharLeft => {
223 execute_motion_with_block_vcol(ed, Motion::Left, count);
224 }
225 hjkl_engine::MotionKind::CharRight => {
226 execute_motion_with_block_vcol(ed, Motion::Right, count);
227 }
228 hjkl_engine::MotionKind::LineDown => {
229 execute_motion_with_block_vcol(ed, Motion::Down, count);
230 }
231 hjkl_engine::MotionKind::LineUp => {
232 execute_motion_with_block_vcol(ed, Motion::Up, count);
233 }
234 hjkl_engine::MotionKind::FirstNonBlankDown => {
235 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
242 let mut sticky = ed.sticky_col();
243 let tabstop = ed.settings().tabstop;
244 hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
245 ed.set_sticky_col(sticky);
246 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
247 let pos = ed.cursor();
248 ed.move_cursor(Move::Jump {
249 row: pos.0,
250 col: pos.1,
251 });
252 ed.sync_buffer_from_textarea();
253 }
254 hjkl_engine::MotionKind::FirstNonBlankUp => {
255 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
258 let mut sticky = ed.sticky_col();
259 let tabstop = ed.settings().tabstop;
260 hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
261 ed.set_sticky_col(sticky);
262 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
263 let pos = ed.cursor();
264 ed.move_cursor(Move::Jump {
265 row: pos.0,
266 col: pos.1,
267 });
268 ed.sync_buffer_from_textarea();
269 }
270 hjkl_engine::MotionKind::WordForward => {
271 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
272 }
273 hjkl_engine::MotionKind::BigWordForward => {
274 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
275 }
276 hjkl_engine::MotionKind::WordBackward => {
277 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
278 }
279 hjkl_engine::MotionKind::BigWordBackward => {
280 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
281 }
282 hjkl_engine::MotionKind::WordEnd => {
283 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
284 }
285 hjkl_engine::MotionKind::BigWordEnd => {
286 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
287 }
288 hjkl_engine::MotionKind::LineStart => {
289 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
292 }
293 hjkl_engine::MotionKind::FirstNonBlank => {
294 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
297 }
298 hjkl_engine::MotionKind::GotoLine => {
299 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
308 }
309 hjkl_engine::MotionKind::LineEnd => {
310 execute_motion_with_block_vcol(ed, Motion::LineEnd, count);
313 }
314 hjkl_engine::MotionKind::FindRepeat => {
315 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
319 }
320 hjkl_engine::MotionKind::FindRepeatReverse => {
321 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
325 }
326 hjkl_engine::MotionKind::BracketMatch => {
327 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
332 }
333 hjkl_engine::MotionKind::ViewportTop => {
334 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
337 }
338 hjkl_engine::MotionKind::ViewportMiddle => {
339 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
342 }
343 hjkl_engine::MotionKind::ViewportBottom => {
344 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
347 }
348 hjkl_engine::MotionKind::HalfPageDown => {
349 {
353 let d = ed.viewport_half_rows(count) as isize;
354 ed.scroll_cursor_rows(d);
355 }
356 }
357 hjkl_engine::MotionKind::HalfPageUp => {
358 {
361 let d = -(ed.viewport_half_rows(count) as isize);
362 ed.scroll_cursor_rows(d);
363 }
364 }
365 hjkl_engine::MotionKind::FullPageDown => {
366 {
369 let d = ed.viewport_full_rows(count) as isize;
370 ed.scroll_cursor_rows(d);
371 }
372 }
373 hjkl_engine::MotionKind::FullPageUp => {
374 {
377 let d = -(ed.viewport_full_rows(count) as isize);
378 ed.scroll_cursor_rows(d);
379 }
380 }
381 hjkl_engine::MotionKind::FirstNonBlankLine => {
382 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
383 }
384 hjkl_engine::MotionKind::SectionBackward => {
385 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
386 }
387 hjkl_engine::MotionKind::SectionForward => {
388 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
389 }
390 hjkl_engine::MotionKind::SectionEndBackward => {
391 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
392 }
393 hjkl_engine::MotionKind::SectionEndForward => {
394 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
395 }
396 _ => {}
401 }
402}
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
418enum MotionClass {
419 Vertical,
422 Jump,
425 Horizontal,
428}
429
430fn motion_class(motion: &Motion) -> MotionClass {
431 use MotionClass::*;
432 match motion {
433 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => Vertical,
437 Motion::FileTop
442 | Motion::FileBottom
443 | Motion::MatchBracket
444 | Motion::UnmatchedBracket { .. }
445 | Motion::WordAtCursor { .. }
446 | Motion::SearchNext { .. }
447 | Motion::ViewportTop
448 | Motion::ViewportMiddle
449 | Motion::ViewportBottom
450 | Motion::ParagraphPrev
451 | Motion::ParagraphNext
452 | Motion::SentencePrev
453 | Motion::SentenceNext
454 | Motion::SectionBackward
455 | Motion::SectionForward
456 | Motion::SectionEndBackward
457 | Motion::SectionEndForward
458 | Motion::FirstNonBlankNextLine
459 | Motion::FirstNonBlankPrevLine
460 | Motion::FirstNonBlankLine => Jump,
461 Motion::Left
465 | Motion::Right
466 | Motion::SpaceFwd
467 | Motion::BackspaceBack
468 | Motion::WordFwd
469 | Motion::BigWordFwd
470 | Motion::WordBack
471 | Motion::BigWordBack
472 | Motion::WordEnd
473 | Motion::BigWordEnd
474 | Motion::ChangeWordEnd { .. }
475 | Motion::WordEndBack
476 | Motion::BigWordEndBack
477 | Motion::LineStart
478 | Motion::FirstNonBlank
479 | Motion::LineEnd
480 | Motion::Find { .. }
481 | Motion::FindRepeat { .. }
482 | Motion::LastNonBlank
483 | Motion::LineMiddle
484 | Motion::ScreenLineMiddle
485 | Motion::GotoColumn => Horizontal,
486 }
488}
489pub fn foldopen_cursor<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
495 let st = vim(ed);
496 if st.mode != Mode::Normal || st.replaying_macro {
497 return;
498 }
499 let row = ed.cursor().0;
500 ed.buffer_mut().open_folds_containing(row);
501}
502fn motion_opens_fold(motion: &Motion, count: usize, moved: bool) -> bool {
510 match motion {
511 Motion::LineStart | Motion::FirstNonBlank => true,
514 Motion::LineEnd => moved || count == 1,
515 Motion::Left
517 | Motion::Right
518 | Motion::SpaceFwd
519 | Motion::BackspaceBack
520 | Motion::WordFwd
521 | Motion::BigWordFwd
522 | Motion::WordBack
523 | Motion::BigWordBack
524 | Motion::WordEnd
525 | Motion::BigWordEnd
526 | Motion::Find { .. }
527 | Motion::FindRepeat { .. }
528 | Motion::MatchBracket
530 | Motion::UnmatchedBracket { .. }
532 | Motion::ParagraphPrev
533 | Motion::ParagraphNext
534 | Motion::SentencePrev
535 | Motion::SentenceNext
536 | Motion::SectionBackward
537 | Motion::SectionForward
538 | Motion::SectionEndBackward
539 | Motion::SectionEndForward
540 | Motion::WordAtCursor { .. }
542 | Motion::SearchNext { .. } => moved,
543 Motion::Up
544 | Motion::Down
545 | Motion::ScreenUp
546 | Motion::ScreenDown
547 | Motion::ChangeWordEnd { .. }
548 | Motion::WordEndBack
549 | Motion::BigWordEndBack
550 | Motion::FileTop
551 | Motion::FileBottom
552 | Motion::ViewportTop
553 | Motion::ViewportMiddle
554 | Motion::ViewportBottom
555 | Motion::LastNonBlank
556 | Motion::LineMiddle
557 | Motion::ScreenLineMiddle
558 | Motion::FirstNonBlankNextLine
559 | Motion::FirstNonBlankPrevLine
560 | Motion::FirstNonBlankLine
561 | Motion::GotoColumn => false,
562 }
563}
564fn count_lines_down<H: hjkl_engine::types::Host>(
571 ed: &mut Editor<hjkl_buffer::View, H>,
572 count: usize,
573) -> bool {
574 if count <= 1 {
575 return true;
576 }
577 if ed.cursor().0 >= last_content_row(ed) {
578 return false;
579 }
580 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
581 let mut sticky = ed.sticky_col();
582 let tabstop = ed.settings().tabstop;
583 hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count - 1, &mut sticky, tabstop);
584 ed.set_sticky_col(sticky);
585 true
586}
587pub fn apply_motion_cursor<H: hjkl_engine::types::Host>(
588 ed: &mut Editor<hjkl_buffer::View, H>,
589 motion: &Motion,
590 count: usize,
591) {
592 apply_motion_cursor_ctx(ed, motion, count, false)
593}
594pub fn apply_motion_cursor_ctx<H: hjkl_engine::types::Host>(
595 ed: &mut Editor<hjkl_buffer::View, H>,
596 motion: &Motion,
597 count: usize,
598 as_operator: bool,
599) {
600 let count = count
609 .min(MAX_COUNT)
610 .min(ed.buffer().rope().len_chars().saturating_add(1));
611 match motion {
612 Motion::Left => {
613 hjkl_engine::motions::move_left(ed.buffer_mut(), count);
615 }
616 Motion::Right => {
617 if as_operator || vim(ed).is_visual() {
622 hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
623 } else {
624 hjkl_engine::motions::move_right_in_line(ed.buffer_mut(), count);
625 }
626 }
627 Motion::SpaceFwd => {
628 if as_operator {
631 hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
632 } else {
633 hjkl_engine::motions::move_space_fwd(ed.buffer_mut(), count);
634 }
635 }
636 Motion::BackspaceBack => {
637 if as_operator {
640 hjkl_engine::motions::move_left(ed.buffer_mut(), count);
641 } else {
642 hjkl_engine::motions::move_backspace_back(ed.buffer_mut(), count);
643 }
644 }
645 Motion::Up => {
646 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
650 let mut sticky = ed.sticky_col();
651 let tabstop = ed.settings().tabstop;
652 hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
653 ed.set_sticky_col(sticky);
654 }
655 Motion::Down => {
656 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
657 let mut sticky = ed.sticky_col();
658 let tabstop = ed.settings().tabstop;
659 hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
660 ed.set_sticky_col(sticky);
661 }
662 Motion::ScreenUp => {
663 let v = *ed.host().viewport();
664 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
665 let mut sticky = ed.sticky_col();
666 let tabstop = ed.settings().tabstop;
667 hjkl_engine::motions::move_screen_up(
668 ed.buffer_mut(),
669 &folds,
670 &v,
671 count,
672 &mut sticky,
673 tabstop,
674 );
675 ed.set_sticky_col(sticky);
676 }
677 Motion::ScreenDown => {
678 let v = *ed.host().viewport();
679 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
680 let mut sticky = ed.sticky_col();
681 let tabstop = ed.settings().tabstop;
682 hjkl_engine::motions::move_screen_down(
683 ed.buffer_mut(),
684 &folds,
685 &v,
686 count,
687 &mut sticky,
688 tabstop,
689 );
690 ed.set_sticky_col(sticky);
691 }
692 Motion::WordFwd | Motion::BigWordFwd => {
693 let big = matches!(motion, Motion::BigWordFwd);
694 let iskeyword = ed.settings().iskeyword.clone();
695 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
696 hjkl_engine::motions::move_word_fwd(ed.buffer_mut(), &folds, big, count, &iskeyword);
697 if !as_operator {
700 let (row, col) = ed.cursor();
701 let line_len = buf_line_chars(ed.buffer(), row);
702 if col > line_len.saturating_sub(1) {
703 ed.jump_cursor(row, line_len.saturating_sub(1));
704 }
705 }
706 }
707 Motion::WordBack | Motion::BigWordBack => {
708 let big = matches!(motion, Motion::BigWordBack);
709 let iskeyword = ed.settings().iskeyword.clone();
710 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
711 hjkl_engine::motions::move_word_back(ed.buffer_mut(), &folds, big, count, &iskeyword);
712 }
713 Motion::WordEnd | Motion::BigWordEnd | Motion::ChangeWordEnd { .. } => {
714 let (big, stop) = match motion {
715 Motion::BigWordEnd => (true, false),
716 Motion::ChangeWordEnd { big } => (*big, true),
717 _ => (false, false),
718 };
719 let iskeyword = ed.settings().iskeyword.clone();
720 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
721 hjkl_engine::motions::move_word_end(
722 ed.buffer_mut(),
723 &folds,
724 big,
725 count,
726 stop,
727 &iskeyword,
728 );
729 }
730 Motion::WordEndBack => {
731 let iskeyword = ed.settings().iskeyword.clone();
732 hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), false, count, &iskeyword);
733 }
734 Motion::BigWordEndBack => {
735 let iskeyword = ed.settings().iskeyword.clone();
736 hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), true, count, &iskeyword);
737 }
738 Motion::LineStart => {
739 hjkl_engine::motions::move_line_start(ed.buffer_mut());
740 }
741 Motion::FirstNonBlank => {
742 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
743 }
744 Motion::LineEnd => {
745 if count_lines_down(ed, count) {
749 if vim(ed).is_visual() && !as_operator {
750 let row = ed.cursor().0;
751 let len = buf_line_chars(ed.buffer(), row);
752 buf_set_cursor_rc(ed.buffer_mut(), row, len);
753 } else {
754 hjkl_engine::motions::move_line_end(ed.buffer_mut());
755 }
756 }
757 }
758 Motion::FileTop => {
759 if count > 1 {
762 hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
763 } else {
764 hjkl_engine::motions::move_top(ed.buffer_mut());
765 }
766 }
767 Motion::FileBottom => {
768 if count > 1 {
771 hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
772 } else {
773 hjkl_engine::motions::move_bottom(ed.buffer_mut(), 0);
774 }
775 }
776 Motion::Find { ch, forward, till } => {
777 let repeat = std::mem::take(&mut vim_mut(ed).find_repeat_skip);
781 for i in 0..count {
782 let skip_adjacent = repeat || i > 0;
783 if !find_char_on_line(ed, *ch, *forward, *till, skip_adjacent) {
784 break;
785 }
786 }
787 }
788 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
790 let _ = matching_bracket(ed);
791 }
792 Motion::UnmatchedBracket { forward, open } => {
793 goto_unmatched_bracket(ed, *forward, *open, count);
794 }
795 Motion::WordAtCursor {
796 forward,
797 whole_word,
798 } => {
799 word_at_cursor_search(ed, *forward, *whole_word, count);
800 }
801 Motion::SearchNext { reverse } => {
802 if let Some(pattern) = ed.last_search_pattern() {
806 ed.push_search_pattern(&pattern);
807 }
808 if ed.search_state().pattern.is_none() {
809 return;
810 }
811 let forward = ed.last_search_forward() != *reverse;
815 for _ in 0..count.max(1) {
816 if forward {
817 ed.search_advance_forward(true);
818 } else {
819 ed.search_advance_backward(true);
820 }
821 }
822 }
823 Motion::ViewportTop => {
824 let v = *ed.host().viewport();
825 hjkl_engine::motions::move_viewport_top(ed.buffer_mut(), &v, count.saturating_sub(1));
826 }
827 Motion::ViewportMiddle => {
828 let v = *ed.host().viewport();
829 hjkl_engine::motions::move_viewport_middle(ed.buffer_mut(), &v);
830 }
831 Motion::ViewportBottom => {
832 let v = *ed.host().viewport();
833 hjkl_engine::motions::move_viewport_bottom(
834 ed.buffer_mut(),
835 &v,
836 count.saturating_sub(1),
837 );
838 }
839 Motion::LastNonBlank => {
840 if count_lines_down(ed, count) {
842 hjkl_engine::motions::move_last_non_blank(ed.buffer_mut());
843 }
844 }
845 Motion::LineMiddle => {
846 let row = ed.cursor().0;
847 let line_chars = buf_line_chars(ed.buffer(), row);
848 let target = line_chars / 2;
851 ed.jump_cursor(row, target);
852 }
853 Motion::ScreenLineMiddle => {
854 let row = ed.cursor().0;
857 let width = ed.host().viewport().width as usize;
858 let last = buf_line_chars(ed.buffer(), row).saturating_sub(1);
859 let target = (width / 2).min(last);
860 ed.jump_cursor(row, target);
861 }
862 Motion::ParagraphPrev => {
863 hjkl_engine::motions::move_paragraph_prev(ed.buffer_mut(), count);
864 }
865 Motion::ParagraphNext => {
866 hjkl_engine::motions::move_paragraph_next(ed.buffer_mut(), count);
867 }
868 Motion::SentencePrev => {
874 let start = ed.cursor();
875 for _ in 0..count.max(1) {
876 match sentence_boundary(ed, false) {
877 Some((row, col)) => ed.jump_cursor(row, col),
878 None => {
879 ed.jump_cursor(start.0, start.1);
880 break;
881 }
882 }
883 }
884 }
885 Motion::SentenceNext => {
886 use crate::vim::text_object::SentenceStep;
887 let start = ed.cursor();
888 let total = count.max(1);
889 for i in 0..total {
890 match crate::vim::text_object::sentence_step_forward(ed) {
891 SentenceStep::Boundary((row, col)) => ed.jump_cursor(row, col),
892 SentenceStep::AtEnd => {}
895 SentenceStep::EndOfBuffer((row, col)) => {
896 if i + 1 == total {
897 ed.jump_cursor(row, col);
898 } else {
899 ed.jump_cursor(start.0, start.1);
900 break;
901 }
902 }
903 }
904 }
905 }
906 Motion::SectionBackward => {
907 hjkl_engine::motions::move_section_backward(ed.buffer_mut(), count);
908 }
909 Motion::SectionForward => {
910 hjkl_engine::motions::move_section_forward(ed.buffer_mut(), count);
911 }
912 Motion::SectionEndBackward => {
913 hjkl_engine::motions::move_section_end_backward(ed.buffer_mut(), count);
914 }
915 Motion::SectionEndForward => {
916 hjkl_engine::motions::move_section_end_forward(ed.buffer_mut(), count);
917 }
918 Motion::FirstNonBlankNextLine => {
919 hjkl_engine::motions::move_first_non_blank_next_line(ed.buffer_mut(), count);
920 }
921 Motion::FirstNonBlankPrevLine => {
922 hjkl_engine::motions::move_first_non_blank_prev_line(ed.buffer_mut(), count);
923 }
924 Motion::FirstNonBlankLine => {
925 hjkl_engine::motions::move_first_non_blank_line(ed.buffer_mut(), count);
926 }
927 Motion::GotoColumn => {
928 hjkl_engine::motions::move_goto_column(ed.buffer_mut(), count);
929 }
930 }
931}
932pub fn move_first_non_whitespace<H: hjkl_engine::types::Host>(
933 ed: &mut Editor<hjkl_buffer::View, H>,
934) {
935 ed.sync_buffer_content_from_textarea();
941 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
942}
943pub fn move_to_insert_start<H: hjkl_engine::types::Host>(ed: &mut Editor<hjkl_buffer::View, H>) {
947 move_first_non_whitespace(ed);
948 let row = ed.cursor().0;
949 let line = buf_line(ed.buffer(), row).unwrap_or_default();
950 if !line.is_empty() && line.chars().all(char::is_whitespace) {
951 buf_set_cursor_rc(ed.buffer_mut(), row, line.chars().count());
952 }
953}
954pub fn find_char_on_line<H: hjkl_engine::types::Host>(
955 ed: &mut Editor<hjkl_buffer::View, H>,
956 ch: char,
957 forward: bool,
958 till: bool,
959 skip_adjacent: bool,
960) -> bool {
961 hjkl_engine::motions::find_char_on_line(ed.buffer_mut(), ch, forward, till, skip_adjacent)
962}
963pub fn matching_bracket<H: hjkl_engine::types::Host>(
964 ed: &mut Editor<hjkl_buffer::View, H>,
965) -> bool {
966 hjkl_engine::motions::match_bracket(ed.buffer_mut())
967}
968pub fn goto_unmatched_bracket<H: hjkl_engine::types::Host>(
972 ed: &mut Editor<hjkl_buffer::View, H>,
973 forward: bool,
974 open: char,
975 count: usize,
976) {
977 let close = match open {
978 '(' => ')',
979 '{' => '}',
980 _ => return,
981 };
982 let cursor = buf_cursor_pos(ed.buffer());
983 let rows = buf_row_count(ed.buffer());
984 let target = count.max(1);
985 let mut found = 0usize;
986 let mut depth = 0i32;
987
988 if forward {
989 let mut r = cursor.row;
990 let mut from_col = cursor.col + 1;
991 while r < rows {
992 let line: Vec<char> = buf_line(ed.buffer(), r)
993 .unwrap_or_default()
994 .chars()
995 .collect();
996 let mut ci = from_col;
997 while ci < line.len() {
998 let ch = line[ci];
999 if ch == open {
1000 depth += 1;
1001 } else if ch == close {
1002 if depth == 0 {
1003 found += 1;
1004 if found == target {
1005 buf_set_cursor_rc(ed.buffer_mut(), r, ci);
1006 return;
1007 }
1008 } else {
1009 depth -= 1;
1010 }
1011 }
1012 ci += 1;
1013 }
1014 r += 1;
1015 from_col = 0;
1016 }
1017 } else {
1018 let mut r = cursor.row as isize;
1019 let mut from_col = cursor.col as isize - 1;
1022 while r >= 0 {
1023 let line: Vec<char> = buf_line(ed.buffer(), r as usize)
1024 .unwrap_or_default()
1025 .chars()
1026 .collect();
1027 let mut ci = from_col.min(line.len() as isize - 1);
1028 while ci >= 0 {
1029 let ch = line[ci as usize];
1030 if ch == close {
1031 depth += 1;
1032 } else if ch == open {
1033 if depth == 0 {
1034 found += 1;
1035 if found == target {
1036 buf_set_cursor_rc(ed.buffer_mut(), r as usize, ci as usize);
1037 return;
1038 }
1039 } else {
1040 depth -= 1;
1041 }
1042 }
1043 ci -= 1;
1044 }
1045 r -= 1;
1046 from_col = isize::MAX;
1047 }
1048 }
1049}
1050pub fn word_at_cursor_search<H: hjkl_engine::types::Host>(
1051 ed: &mut Editor<hjkl_buffer::View, H>,
1052 forward: bool,
1053 whole_word: bool,
1054 count: usize,
1055) {
1056 let (row, col) = ed.cursor();
1057 let line: String = buf_line(ed.buffer(), row).unwrap_or_default();
1058 let chars: Vec<char> = line.chars().collect();
1059 if chars.is_empty() {
1060 return;
1061 }
1062 let spec = ed.settings().iskeyword.clone();
1064 let is_word = |c: char| is_keyword_char(c, &spec);
1065 let mut start = col.min(chars.len().saturating_sub(1));
1066 if is_word(chars[start]) {
1067 while start > 0 && is_word(chars[start - 1]) {
1068 start -= 1;
1069 }
1070 } else {
1071 while start < chars.len() && !is_word(chars[start]) {
1080 start += 1;
1081 }
1082 if start >= chars.len() {
1083 return;
1084 }
1085 buf_set_cursor_rc(ed.buffer_mut(), row, start);
1086 }
1087 let mut end = start;
1088 while end < chars.len() && is_word(chars[end]) {
1089 end += 1;
1090 }
1091 if end <= start {
1092 return;
1093 }
1094 let word: String = chars[start..end].iter().collect();
1095 let escaped = regex_escape(&word);
1096 let pattern = if whole_word {
1097 format!(r"\b{escaped}\b")
1098 } else {
1099 escaped
1100 };
1101 search_pattern_and_advance(ed, pattern, forward, count);
1102}
1103
1104pub fn search_selected_text<H: hjkl_engine::types::Host>(
1105 ed: &mut Editor<hjkl_buffer::View, H>,
1106 selected: &str,
1107 forward: bool,
1108 count: usize,
1109) -> bool {
1110 if selected.is_empty() {
1111 return false;
1112 }
1113 let selected = selected.replace('\\', r"\\").replace('\n', r"\n");
1114 let pattern = format!(r"\V{selected}");
1115 let (installed, found) = search_pattern_and_advance(ed, pattern.clone(), forward, count);
1116 if installed {
1117 ed.record_search_history(&pattern);
1118 }
1119 found
1120}
1121
1122fn search_pattern_and_advance<H: hjkl_engine::types::Host>(
1123 ed: &mut Editor<hjkl_buffer::View, H>,
1124 pattern: String,
1125 forward: bool,
1126 count: usize,
1127) -> (bool, bool) {
1128 ed.push_search_pattern(&pattern);
1129 if ed.search_state().pattern.is_none() {
1130 return (false, false);
1131 }
1132 ed.set_last_search_pattern_only(Some(pattern));
1134 ed.set_last_search_forward_only(forward);
1135 let mut found = false;
1136 for _ in 0..count.max(1) {
1137 found |= if forward {
1138 ed.search_advance_forward(true)
1139 } else {
1140 ed.search_advance_backward(true)
1141 };
1142 }
1143 (true, found)
1144}
1145pub fn regex_escape(s: &str) -> String {
1146 let mut out = String::with_capacity(s.len());
1147 for c in s.chars() {
1148 if matches!(
1149 c,
1150 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
1151 ) {
1152 out.push('\\');
1153 }
1154 out.push(c);
1155 }
1156 out
1157}