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 enum WordMotionFolds {
22 Aware(hjkl_engine::SnapshotFoldProvider),
23 Blind(hjkl_engine::types::NoopFoldProvider),
24}
25
26impl WordMotionFolds {
27 pub fn as_provider(&self) -> &dyn hjkl_engine::types::FoldProvider {
28 match self {
29 Self::Aware(f) => f,
30 Self::Blind(f) => f,
31 }
32 }
33}
34
35pub fn word_motion_folds<H: hjkl_engine::types::Host>(
41 ed: &Editor<hjkl_buffer::View, H>,
42) -> WordMotionFolds {
43 if cursor_left_fold_open(ed.buffer(), ed.cursor()) {
44 WordMotionFolds::Blind(hjkl_engine::types::NoopFoldProvider)
45 } else {
46 WordMotionFolds::Aware(hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer()))
47 }
48}
49
50pub fn parse_motion(input: &Input) -> Option<Motion> {
54 if input.ctrl {
55 if input.key == Key::Char('h') {
59 return Some(Motion::BackspaceBack);
60 }
61 return None;
62 }
63 match input.key {
64 Key::Char('h') | Key::Left => Some(Motion::Left),
65 Key::Char('l') | Key::Right => Some(Motion::Right),
66 Key::Char(' ') => Some(Motion::SpaceFwd),
70 Key::Backspace => Some(Motion::BackspaceBack),
71 Key::Char('j') | Key::Down => Some(Motion::Down),
72 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
74 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
76 Key::Char('_') => Some(Motion::FirstNonBlankLine),
78 Key::Char('k') | Key::Up => Some(Motion::Up),
79 Key::Char('w') => Some(Motion::WordFwd),
80 Key::Char('W') => Some(Motion::BigWordFwd),
81 Key::Char('b') => Some(Motion::WordBack),
82 Key::Char('B') => Some(Motion::BigWordBack),
83 Key::Char('e') => Some(Motion::WordEnd),
84 Key::Char('E') => Some(Motion::BigWordEnd),
85 Key::Char('0') | Key::Home => Some(Motion::LineStart),
86 Key::Char('^') => Some(Motion::FirstNonBlank),
87 Key::Char('$') | Key::End => Some(Motion::LineEnd),
88 Key::Char('G') => Some(Motion::FileBottom),
89 Key::Char('%') => Some(Motion::MatchBracket),
90 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
91 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
92 Key::Char('*') => Some(Motion::WordAtCursor {
93 forward: true,
94 whole_word: true,
95 }),
96 Key::Char('#') => Some(Motion::WordAtCursor {
97 forward: false,
98 whole_word: true,
99 }),
100 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
101 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
102 Key::Char('H') => Some(Motion::ViewportTop),
103 Key::Char('M') => Some(Motion::ViewportMiddle),
104 Key::Char('L') => Some(Motion::ViewportBottom),
105 Key::Char('{') => Some(Motion::ParagraphPrev),
106 Key::Char('}') => Some(Motion::ParagraphNext),
107 Key::Char('(') => Some(Motion::SentencePrev),
108 Key::Char(')') => Some(Motion::SentenceNext),
109 Key::Char('|') => Some(Motion::GotoColumn),
110 _ => None,
111 }
112}
113pub fn execute_motion<H: hjkl_engine::types::Host>(
114 ed: &mut Editor<hjkl_buffer::View, H>,
115 motion: Motion,
116 count: usize,
117) {
118 let count = count.clamp(1, MAX_COUNT);
119 if let Motion::FindRepeat { reverse } = motion
122 && vim(ed).last_horizontal_motion == LastHorizontalMotion::Sneak
123 {
124 if let Some(((c1, c2), fwd)) = vim(ed).last_sneak {
125 let effective_fwd = if reverse { !fwd } else { fwd };
126 apply_sneak(ed, c1, c2, effective_fwd, count);
127 }
128 return;
129 }
130 let motion = match motion {
134 Motion::FindRepeat { reverse } => match vim(ed).last_find {
135 Some((ch, forward, till)) => {
136 vim_mut(ed).find_repeat_skip = true;
137 Motion::Find {
138 ch,
139 forward: if reverse { !forward } else { forward },
140 till,
141 }
142 }
143 None => return,
144 },
145 other => other,
146 };
147 let pre_pos = ed.cursor();
148 apply_motion_cursor(ed, &motion, count);
149 let post_pos = ed.cursor();
150 if is_big_jump(&motion) && pre_pos != post_pos {
151 ed.push_jump(pre_pos);
152 }
153 match motion_class(&motion) {
159 MotionClass::Vertical => {
160 ed.move_cursor(Move::Vertical { row: ed.cursor().0 });
164 }
165 MotionClass::Jump => {
166 let pos = ed.cursor();
169 ed.move_cursor(Move::Jump {
170 row: pos.0,
171 col: pos.1,
172 });
173 }
174 MotionClass::Horizontal => {
175 ed.move_cursor(Move::Horizontal { col: ed.cursor().1 });
177 }
178 }
179 ed.sync_buffer_from_textarea();
184}
185pub fn execute_motion_with_block_vcol<H: hjkl_engine::types::Host>(
194 ed: &mut Editor<hjkl_buffer::View, H>,
195 motion: Motion,
196 count: usize,
197) {
198 let motion_copy = motion.clone();
199 execute_motion(ed, motion, count);
200 if vim(ed).mode == Mode::VisualBlock {
201 update_block_vcol(ed, &motion_copy);
202 }
203}
204pub fn apply_motion_kind<H: hjkl_engine::types::Host>(
232 ed: &mut Editor<hjkl_buffer::View, H>,
233 kind: hjkl_engine::MotionKind,
234 count: usize,
235) {
236 let count = count.max(1);
237 match kind {
238 hjkl_engine::MotionKind::CharLeft => {
239 execute_motion_with_block_vcol(ed, Motion::Left, count);
240 }
241 hjkl_engine::MotionKind::CharRight => {
242 execute_motion_with_block_vcol(ed, Motion::Right, count);
243 }
244 hjkl_engine::MotionKind::LineDown => {
245 execute_motion_with_block_vcol(ed, Motion::Down, count);
246 }
247 hjkl_engine::MotionKind::LineUp => {
248 execute_motion_with_block_vcol(ed, Motion::Up, count);
249 }
250 hjkl_engine::MotionKind::FirstNonBlankDown => {
251 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_down(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::FirstNonBlankUp => {
271 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
274 let mut sticky = ed.sticky_col();
275 let tabstop = ed.settings().tabstop;
276 hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
277 ed.set_sticky_col(sticky);
278 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
279 let pos = ed.cursor();
280 ed.move_cursor(Move::Jump {
281 row: pos.0,
282 col: pos.1,
283 });
284 ed.sync_buffer_from_textarea();
285 }
286 hjkl_engine::MotionKind::WordForward => {
287 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
288 }
289 hjkl_engine::MotionKind::BigWordForward => {
290 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
291 }
292 hjkl_engine::MotionKind::WordBackward => {
293 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
294 }
295 hjkl_engine::MotionKind::BigWordBackward => {
296 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
297 }
298 hjkl_engine::MotionKind::WordEnd => {
299 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
300 }
301 hjkl_engine::MotionKind::BigWordEnd => {
302 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
303 }
304 hjkl_engine::MotionKind::LineStart => {
305 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
308 }
309 hjkl_engine::MotionKind::FirstNonBlank => {
310 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
313 }
314 hjkl_engine::MotionKind::GotoLine => {
315 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
324 }
325 hjkl_engine::MotionKind::LineEnd => {
326 execute_motion_with_block_vcol(ed, Motion::LineEnd, count);
329 }
330 hjkl_engine::MotionKind::FindRepeat => {
331 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
335 }
336 hjkl_engine::MotionKind::FindRepeatReverse => {
337 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
341 }
342 hjkl_engine::MotionKind::BracketMatch => {
343 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
348 }
349 hjkl_engine::MotionKind::ViewportTop => {
350 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
353 }
354 hjkl_engine::MotionKind::ViewportMiddle => {
355 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
358 }
359 hjkl_engine::MotionKind::ViewportBottom => {
360 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
363 }
364 hjkl_engine::MotionKind::HalfPageDown => {
365 {
369 let d = ed.viewport_half_rows(count) as isize;
370 ed.scroll_cursor_rows(d);
371 }
372 }
373 hjkl_engine::MotionKind::HalfPageUp => {
374 {
377 let d = -(ed.viewport_half_rows(count) as isize);
378 ed.scroll_cursor_rows(d);
379 }
380 }
381 hjkl_engine::MotionKind::FullPageDown => {
382 {
385 let d = ed.viewport_full_rows(count) as isize;
386 ed.scroll_cursor_rows(d);
387 }
388 }
389 hjkl_engine::MotionKind::FullPageUp => {
390 {
393 let d = -(ed.viewport_full_rows(count) as isize);
394 ed.scroll_cursor_rows(d);
395 }
396 }
397 hjkl_engine::MotionKind::FirstNonBlankLine => {
398 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
399 }
400 hjkl_engine::MotionKind::SectionBackward => {
401 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
402 }
403 hjkl_engine::MotionKind::SectionForward => {
404 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
405 }
406 hjkl_engine::MotionKind::SectionEndBackward => {
407 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
408 }
409 hjkl_engine::MotionKind::SectionEndForward => {
410 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
411 }
412 _ => {}
417 }
418}
419#[derive(Debug, Clone, Copy, PartialEq, Eq)]
434enum MotionClass {
435 Vertical,
438 Jump,
441 Horizontal,
444}
445
446fn motion_class(motion: &Motion) -> MotionClass {
447 use MotionClass::*;
448 match motion {
449 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => Vertical,
453 Motion::FileTop
458 | Motion::FileBottom
459 | Motion::MatchBracket
460 | Motion::UnmatchedBracket { .. }
461 | Motion::WordAtCursor { .. }
462 | Motion::SearchNext { .. }
463 | Motion::ViewportTop
464 | Motion::ViewportMiddle
465 | Motion::ViewportBottom
466 | Motion::ParagraphPrev
467 | Motion::ParagraphNext
468 | Motion::SentencePrev
469 | Motion::SentenceNext
470 | Motion::SectionBackward
471 | Motion::SectionForward
472 | Motion::SectionEndBackward
473 | Motion::SectionEndForward
474 | Motion::FirstNonBlankNextLine
475 | Motion::FirstNonBlankPrevLine
476 | Motion::FirstNonBlankLine => Jump,
477 Motion::Left
481 | Motion::Right
482 | Motion::SpaceFwd
483 | Motion::BackspaceBack
484 | Motion::WordFwd
485 | Motion::BigWordFwd
486 | Motion::WordBack
487 | Motion::BigWordBack
488 | Motion::WordEnd
489 | Motion::BigWordEnd
490 | Motion::ChangeWordEnd { .. }
491 | Motion::WordEndBack
492 | Motion::BigWordEndBack
493 | Motion::LineStart
494 | Motion::FirstNonBlank
495 | Motion::LineEnd
496 | Motion::Find { .. }
497 | Motion::FindRepeat { .. }
498 | Motion::LastNonBlank
499 | Motion::LineMiddle
500 | Motion::ScreenLineMiddle
501 | Motion::GotoColumn => Horizontal,
502 }
504}
505pub fn apply_motion_cursor<H: hjkl_engine::types::Host>(
506 ed: &mut Editor<hjkl_buffer::View, H>,
507 motion: &Motion,
508 count: usize,
509) {
510 apply_motion_cursor_ctx(ed, motion, count, false)
511}
512pub fn apply_motion_cursor_ctx<H: hjkl_engine::types::Host>(
513 ed: &mut Editor<hjkl_buffer::View, H>,
514 motion: &Motion,
515 count: usize,
516 as_operator: bool,
517) {
518 let count = count
527 .min(MAX_COUNT)
528 .min(ed.buffer().rope().len_chars().saturating_add(1));
529 match motion {
530 Motion::Left => {
531 hjkl_engine::motions::move_left(ed.buffer_mut(), count);
533 }
534 Motion::Right => {
535 if as_operator {
539 hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
540 } else {
541 hjkl_engine::motions::move_right_in_line(ed.buffer_mut(), count);
542 }
543 }
544 Motion::SpaceFwd => {
545 if as_operator {
548 hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
549 } else {
550 hjkl_engine::motions::move_space_fwd(ed.buffer_mut(), count);
551 }
552 }
553 Motion::BackspaceBack => {
554 if as_operator {
557 hjkl_engine::motions::move_left(ed.buffer_mut(), count);
558 } else {
559 hjkl_engine::motions::move_backspace_back(ed.buffer_mut(), count);
560 }
561 }
562 Motion::Up => {
563 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
567 let mut sticky = ed.sticky_col();
568 let tabstop = ed.settings().tabstop;
569 hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
570 ed.set_sticky_col(sticky);
571 }
572 Motion::Down => {
573 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
574 let mut sticky = ed.sticky_col();
575 let tabstop = ed.settings().tabstop;
576 hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
577 ed.set_sticky_col(sticky);
578 }
579 Motion::ScreenUp => {
580 let v = *ed.host().viewport();
581 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
582 let mut sticky = ed.sticky_col();
583 let tabstop = ed.settings().tabstop;
584 hjkl_engine::motions::move_screen_up(
585 ed.buffer_mut(),
586 &folds,
587 &v,
588 count,
589 &mut sticky,
590 tabstop,
591 );
592 ed.set_sticky_col(sticky);
593 }
594 Motion::ScreenDown => {
595 let v = *ed.host().viewport();
596 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
597 let mut sticky = ed.sticky_col();
598 let tabstop = ed.settings().tabstop;
599 hjkl_engine::motions::move_screen_down(
600 ed.buffer_mut(),
601 &folds,
602 &v,
603 count,
604 &mut sticky,
605 tabstop,
606 );
607 ed.set_sticky_col(sticky);
608 }
609 Motion::WordFwd | Motion::BigWordFwd => {
610 let big = matches!(motion, Motion::BigWordFwd);
611 let iskeyword = ed.settings().iskeyword.clone();
612 let folds = word_motion_folds(ed);
613 hjkl_engine::motions::move_word_fwd(
614 ed.buffer_mut(),
615 folds.as_provider(),
616 big,
617 count,
618 &iskeyword,
619 );
620 if !as_operator {
623 let (row, col) = ed.cursor();
624 let line_len = buf_line_chars(ed.buffer(), row);
625 if col > line_len.saturating_sub(1) {
626 ed.jump_cursor(row, line_len.saturating_sub(1));
627 }
628 }
629 }
630 Motion::WordBack | Motion::BigWordBack => {
631 let big = matches!(motion, Motion::BigWordBack);
632 let iskeyword = ed.settings().iskeyword.clone();
633 let folds = word_motion_folds(ed);
634 hjkl_engine::motions::move_word_back(
635 ed.buffer_mut(),
636 folds.as_provider(),
637 big,
638 count,
639 &iskeyword,
640 );
641 }
642 Motion::WordEnd | Motion::BigWordEnd | Motion::ChangeWordEnd { .. } => {
643 let (big, stop) = match motion {
644 Motion::BigWordEnd => (true, false),
645 Motion::ChangeWordEnd { big } => (*big, true),
646 _ => (false, false),
647 };
648 let iskeyword = ed.settings().iskeyword.clone();
649 let folds = word_motion_folds(ed);
650 hjkl_engine::motions::move_word_end(
651 ed.buffer_mut(),
652 folds.as_provider(),
653 big,
654 count,
655 stop,
656 &iskeyword,
657 );
658 }
659 Motion::WordEndBack => {
660 let iskeyword = ed.settings().iskeyword.clone();
661 hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), false, count, &iskeyword);
662 }
663 Motion::BigWordEndBack => {
664 let iskeyword = ed.settings().iskeyword.clone();
665 hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), true, count, &iskeyword);
666 }
667 Motion::LineStart => {
668 hjkl_engine::motions::move_line_start(ed.buffer_mut());
669 }
670 Motion::FirstNonBlank => {
671 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
672 }
673 Motion::LineEnd => {
674 if count > 1 {
677 if ed.cursor().0 >= last_content_row(ed) {
685 return;
686 }
687 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
688 let mut sticky = ed.sticky_col();
689 let tabstop = ed.settings().tabstop;
690 hjkl_engine::motions::move_down(
691 ed.buffer_mut(),
692 &folds,
693 count - 1,
694 &mut sticky,
695 tabstop,
696 );
697 ed.set_sticky_col(sticky);
698 }
699 hjkl_engine::motions::move_line_end(ed.buffer_mut());
700 }
701 Motion::FileTop => {
702 if count > 1 {
705 hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
706 } else {
707 hjkl_engine::motions::move_top(ed.buffer_mut());
708 }
709 }
710 Motion::FileBottom => {
711 if count > 1 {
714 hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
715 } else {
716 hjkl_engine::motions::move_bottom(ed.buffer_mut(), 0);
717 }
718 }
719 Motion::Find { ch, forward, till } => {
720 let repeat = std::mem::take(&mut vim_mut(ed).find_repeat_skip);
724 for i in 0..count {
725 let skip_adjacent = repeat || i > 0;
726 if !find_char_on_line(ed, *ch, *forward, *till, skip_adjacent) {
727 break;
728 }
729 }
730 }
731 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
733 let _ = matching_bracket(ed);
734 }
735 Motion::UnmatchedBracket { forward, open } => {
736 goto_unmatched_bracket(ed, *forward, *open, count);
737 }
738 Motion::WordAtCursor {
739 forward,
740 whole_word,
741 } => {
742 word_at_cursor_search(ed, *forward, *whole_word, count);
743 }
744 Motion::SearchNext { reverse } => {
745 if let Some(pattern) = ed.last_search_pattern() {
749 ed.push_search_pattern(&pattern);
750 }
751 if ed.search_state().pattern.is_none() {
752 return;
753 }
754 let forward = ed.last_search_forward() != *reverse;
758 for _ in 0..count.max(1) {
759 if forward {
760 ed.search_advance_forward(true);
761 } else {
762 ed.search_advance_backward(true);
763 }
764 }
765 }
766 Motion::ViewportTop => {
767 let v = *ed.host().viewport();
768 hjkl_engine::motions::move_viewport_top(ed.buffer_mut(), &v, count.saturating_sub(1));
769 }
770 Motion::ViewportMiddle => {
771 let v = *ed.host().viewport();
772 hjkl_engine::motions::move_viewport_middle(ed.buffer_mut(), &v);
773 }
774 Motion::ViewportBottom => {
775 let v = *ed.host().viewport();
776 hjkl_engine::motions::move_viewport_bottom(
777 ed.buffer_mut(),
778 &v,
779 count.saturating_sub(1),
780 );
781 }
782 Motion::LastNonBlank => {
783 hjkl_engine::motions::move_last_non_blank(ed.buffer_mut());
784 }
785 Motion::LineMiddle => {
786 let row = ed.cursor().0;
787 let line_chars = buf_line_chars(ed.buffer(), row);
788 let target = line_chars / 2;
791 ed.jump_cursor(row, target);
792 }
793 Motion::ScreenLineMiddle => {
794 let row = ed.cursor().0;
797 let width = ed.host().viewport().width as usize;
798 let last = buf_line_chars(ed.buffer(), row).saturating_sub(1);
799 let target = (width / 2).min(last);
800 ed.jump_cursor(row, target);
801 }
802 Motion::ParagraphPrev => {
803 hjkl_engine::motions::move_paragraph_prev(ed.buffer_mut(), count);
804 }
805 Motion::ParagraphNext => {
806 hjkl_engine::motions::move_paragraph_next(ed.buffer_mut(), count);
807 }
808 Motion::SentencePrev => {
814 let start = ed.cursor();
815 for _ in 0..count.max(1) {
816 match sentence_boundary(ed, false) {
817 Some((row, col)) => ed.jump_cursor(row, col),
818 None => {
819 ed.jump_cursor(start.0, start.1);
820 break;
821 }
822 }
823 }
824 }
825 Motion::SentenceNext => {
826 use crate::vim::text_object::SentenceStep;
827 let start = ed.cursor();
828 let total = count.max(1);
829 for i in 0..total {
830 match crate::vim::text_object::sentence_step_forward(ed) {
831 SentenceStep::Boundary((row, col)) => ed.jump_cursor(row, col),
832 SentenceStep::AtEnd => {}
835 SentenceStep::EndOfBuffer((row, col)) => {
836 if i + 1 == total {
837 ed.jump_cursor(row, col);
838 } else {
839 ed.jump_cursor(start.0, start.1);
840 break;
841 }
842 }
843 }
844 }
845 }
846 Motion::SectionBackward => {
847 hjkl_engine::motions::move_section_backward(ed.buffer_mut(), count);
848 }
849 Motion::SectionForward => {
850 hjkl_engine::motions::move_section_forward(ed.buffer_mut(), count);
851 }
852 Motion::SectionEndBackward => {
853 hjkl_engine::motions::move_section_end_backward(ed.buffer_mut(), count);
854 }
855 Motion::SectionEndForward => {
856 hjkl_engine::motions::move_section_end_forward(ed.buffer_mut(), count);
857 }
858 Motion::FirstNonBlankNextLine => {
859 hjkl_engine::motions::move_first_non_blank_next_line(ed.buffer_mut(), count);
860 }
861 Motion::FirstNonBlankPrevLine => {
862 hjkl_engine::motions::move_first_non_blank_prev_line(ed.buffer_mut(), count);
863 }
864 Motion::FirstNonBlankLine => {
865 hjkl_engine::motions::move_first_non_blank_line(ed.buffer_mut(), count);
866 }
867 Motion::GotoColumn => {
868 hjkl_engine::motions::move_goto_column(ed.buffer_mut(), count);
869 }
870 }
871}
872pub fn move_first_non_whitespace<H: hjkl_engine::types::Host>(
873 ed: &mut Editor<hjkl_buffer::View, H>,
874) {
875 ed.sync_buffer_content_from_textarea();
881 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
882}
883pub fn find_char_on_line<H: hjkl_engine::types::Host>(
884 ed: &mut Editor<hjkl_buffer::View, H>,
885 ch: char,
886 forward: bool,
887 till: bool,
888 skip_adjacent: bool,
889) -> bool {
890 hjkl_engine::motions::find_char_on_line(ed.buffer_mut(), ch, forward, till, skip_adjacent)
891}
892pub fn matching_bracket<H: hjkl_engine::types::Host>(
893 ed: &mut Editor<hjkl_buffer::View, H>,
894) -> bool {
895 hjkl_engine::motions::match_bracket(ed.buffer_mut())
896}
897pub fn goto_unmatched_bracket<H: hjkl_engine::types::Host>(
901 ed: &mut Editor<hjkl_buffer::View, H>,
902 forward: bool,
903 open: char,
904 count: usize,
905) {
906 let close = match open {
907 '(' => ')',
908 '{' => '}',
909 _ => return,
910 };
911 let cursor = buf_cursor_pos(ed.buffer());
912 let rows = buf_row_count(ed.buffer());
913 let target = count.max(1);
914 let mut found = 0usize;
915 let mut depth = 0i32;
916
917 if forward {
918 let mut r = cursor.row;
919 let mut from_col = cursor.col + 1;
920 while r < rows {
921 let line: Vec<char> = buf_line(ed.buffer(), r)
922 .unwrap_or_default()
923 .chars()
924 .collect();
925 let mut ci = from_col;
926 while ci < line.len() {
927 let ch = line[ci];
928 if ch == open {
929 depth += 1;
930 } else if ch == close {
931 if depth == 0 {
932 found += 1;
933 if found == target {
934 buf_set_cursor_rc(ed.buffer_mut(), r, ci);
935 return;
936 }
937 } else {
938 depth -= 1;
939 }
940 }
941 ci += 1;
942 }
943 r += 1;
944 from_col = 0;
945 }
946 } else {
947 let mut r = cursor.row as isize;
948 let mut from_col = cursor.col as isize - 1;
951 while r >= 0 {
952 let line: Vec<char> = buf_line(ed.buffer(), r as usize)
953 .unwrap_or_default()
954 .chars()
955 .collect();
956 let mut ci = from_col.min(line.len() as isize - 1);
957 while ci >= 0 {
958 let ch = line[ci as usize];
959 if ch == close {
960 depth += 1;
961 } else if ch == open {
962 if depth == 0 {
963 found += 1;
964 if found == target {
965 buf_set_cursor_rc(ed.buffer_mut(), r as usize, ci as usize);
966 return;
967 }
968 } else {
969 depth -= 1;
970 }
971 }
972 ci -= 1;
973 }
974 r -= 1;
975 from_col = isize::MAX;
976 }
977 }
978}
979pub fn word_at_cursor_search<H: hjkl_engine::types::Host>(
980 ed: &mut Editor<hjkl_buffer::View, H>,
981 forward: bool,
982 whole_word: bool,
983 count: usize,
984) {
985 let (row, col) = ed.cursor();
986 let line: String = buf_line(ed.buffer(), row).unwrap_or_default();
987 let chars: Vec<char> = line.chars().collect();
988 if chars.is_empty() {
989 return;
990 }
991 let spec = ed.settings().iskeyword.clone();
993 let is_word = |c: char| is_keyword_char(c, &spec);
994 let mut start = col.min(chars.len().saturating_sub(1));
995 if is_word(chars[start]) {
996 while start > 0 && is_word(chars[start - 1]) {
997 start -= 1;
998 }
999 } else {
1000 while start < chars.len() && !is_word(chars[start]) {
1009 start += 1;
1010 }
1011 if start >= chars.len() {
1012 return;
1013 }
1014 buf_set_cursor_rc(ed.buffer_mut(), row, start);
1015 }
1016 let mut end = start;
1017 while end < chars.len() && is_word(chars[end]) {
1018 end += 1;
1019 }
1020 if end <= start {
1021 return;
1022 }
1023 let word: String = chars[start..end].iter().collect();
1024 let escaped = regex_escape(&word);
1025 let pattern = if whole_word {
1026 format!(r"\b{escaped}\b")
1027 } else {
1028 escaped
1029 };
1030 search_pattern_and_advance(ed, pattern, forward, count);
1031}
1032
1033pub fn search_selected_text<H: hjkl_engine::types::Host>(
1034 ed: &mut Editor<hjkl_buffer::View, H>,
1035 selected: &str,
1036 forward: bool,
1037 count: usize,
1038) -> bool {
1039 if selected.is_empty() {
1040 return false;
1041 }
1042 let selected = selected.replace('\\', r"\\").replace('\n', r"\n");
1043 let pattern = format!(r"\V{selected}");
1044 let (installed, found) = search_pattern_and_advance(ed, pattern.clone(), forward, count);
1045 if installed {
1046 ed.record_search_history(&pattern);
1047 }
1048 found
1049}
1050
1051fn search_pattern_and_advance<H: hjkl_engine::types::Host>(
1052 ed: &mut Editor<hjkl_buffer::View, H>,
1053 pattern: String,
1054 forward: bool,
1055 count: usize,
1056) -> (bool, bool) {
1057 ed.push_search_pattern(&pattern);
1058 if ed.search_state().pattern.is_none() {
1059 return (false, false);
1060 }
1061 ed.set_last_search_pattern_only(Some(pattern));
1063 ed.set_last_search_forward_only(forward);
1064 let mut found = false;
1065 for _ in 0..count.max(1) {
1066 found |= if forward {
1067 ed.search_advance_forward(true)
1068 } else {
1069 ed.search_advance_backward(true)
1070 };
1071 }
1072 (true, found)
1073}
1074pub fn regex_escape(s: &str) -> String {
1075 let mut out = String::with_capacity(s.len());
1076 for c in s.chars() {
1077 if matches!(
1078 c,
1079 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
1080 ) {
1081 out.push('\\');
1082 }
1083 out.push(c);
1084 }
1085 out
1086}