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::Editor;
12use hjkl_engine::buf_helpers::{
13 buf_cursor_pos, buf_line, buf_line_chars, buf_row_count, buf_set_cursor_rc,
14};
15
16use hjkl_buffer::{char_col_to_visual_col, visual_col_to_char_col};
17
18pub fn parse_motion(input: &Input) -> Option<Motion> {
22 if input.ctrl {
23 if input.key == Key::Char('h') {
27 return Some(Motion::BackspaceBack);
28 }
29 return None;
30 }
31 match input.key {
32 Key::Char('h') | Key::Left => Some(Motion::Left),
33 Key::Char('l') | Key::Right => Some(Motion::Right),
34 Key::Char(' ') => Some(Motion::SpaceFwd),
38 Key::Backspace => Some(Motion::BackspaceBack),
39 Key::Char('j') | Key::Down => Some(Motion::Down),
40 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
42 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
44 Key::Char('_') => Some(Motion::FirstNonBlankLine),
46 Key::Char('k') | Key::Up => Some(Motion::Up),
47 Key::Char('w') => Some(Motion::WordFwd),
48 Key::Char('W') => Some(Motion::BigWordFwd),
49 Key::Char('b') => Some(Motion::WordBack),
50 Key::Char('B') => Some(Motion::BigWordBack),
51 Key::Char('e') => Some(Motion::WordEnd),
52 Key::Char('E') => Some(Motion::BigWordEnd),
53 Key::Char('0') | Key::Home => Some(Motion::LineStart),
54 Key::Char('^') => Some(Motion::FirstNonBlank),
55 Key::Char('$') | Key::End => Some(Motion::LineEnd),
56 Key::Char('G') => Some(Motion::FileBottom),
57 Key::Char('%') => Some(Motion::MatchBracket),
58 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
59 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
60 Key::Char('*') => Some(Motion::WordAtCursor {
61 forward: true,
62 whole_word: true,
63 }),
64 Key::Char('#') => Some(Motion::WordAtCursor {
65 forward: false,
66 whole_word: true,
67 }),
68 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
69 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
70 Key::Char('H') => Some(Motion::ViewportTop),
71 Key::Char('M') => Some(Motion::ViewportMiddle),
72 Key::Char('L') => Some(Motion::ViewportBottom),
73 Key::Char('{') => Some(Motion::ParagraphPrev),
74 Key::Char('}') => Some(Motion::ParagraphNext),
75 Key::Char('(') => Some(Motion::SentencePrev),
76 Key::Char(')') => Some(Motion::SentenceNext),
77 Key::Char('|') => Some(Motion::GotoColumn),
78 _ => None,
79 }
80}
81pub fn execute_motion<H: hjkl_engine::types::Host>(
82 ed: &mut Editor<hjkl_buffer::View, H>,
83 motion: Motion,
84 count: usize,
85) {
86 let count = count.clamp(1, MAX_COUNT);
87 if let Motion::FindRepeat { reverse } = motion
90 && vim(ed).last_horizontal_motion == LastHorizontalMotion::Sneak
91 {
92 if let Some(((c1, c2), fwd)) = vim(ed).last_sneak {
93 let effective_fwd = if reverse { !fwd } else { fwd };
94 apply_sneak(ed, c1, c2, effective_fwd, count);
95 }
96 return;
97 }
98 let motion = match motion {
102 Motion::FindRepeat { reverse } => match vim(ed).last_find {
103 Some((ch, forward, till)) => {
104 vim_mut(ed).find_repeat_skip = true;
105 Motion::Find {
106 ch,
107 forward: if reverse { !forward } else { forward },
108 till,
109 }
110 }
111 None => return,
112 },
113 other => other,
114 };
115 let pre_pos = ed.cursor();
116 let pre_col = pre_pos.1;
117 apply_motion_cursor(ed, &motion, count);
118 let post_pos = ed.cursor();
119 if is_big_jump(&motion) && pre_pos != post_pos {
120 ed.push_jump(pre_pos);
121 }
122 apply_sticky_col(ed, &motion, pre_col);
123 ed.sync_buffer_from_textarea();
128}
129pub fn execute_motion_with_block_vcol<H: hjkl_engine::types::Host>(
138 ed: &mut Editor<hjkl_buffer::View, H>,
139 motion: Motion,
140 count: usize,
141) {
142 let motion_copy = motion.clone();
143 execute_motion(ed, motion, count);
144 if vim(ed).mode == Mode::VisualBlock {
145 update_block_vcol(ed, &motion_copy);
146 }
147}
148pub fn apply_motion_kind<H: hjkl_engine::types::Host>(
176 ed: &mut Editor<hjkl_buffer::View, H>,
177 kind: hjkl_engine::MotionKind,
178 count: usize,
179) {
180 let count = count.max(1);
181 match kind {
182 hjkl_engine::MotionKind::CharLeft => {
183 execute_motion_with_block_vcol(ed, Motion::Left, count);
184 }
185 hjkl_engine::MotionKind::CharRight => {
186 execute_motion_with_block_vcol(ed, Motion::Right, count);
187 }
188 hjkl_engine::MotionKind::LineDown => {
189 execute_motion_with_block_vcol(ed, Motion::Down, count);
190 }
191 hjkl_engine::MotionKind::LineUp => {
192 execute_motion_with_block_vcol(ed, Motion::Up, count);
193 }
194 hjkl_engine::MotionKind::FirstNonBlankDown => {
195 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
200 let mut sticky = ed.sticky_col();
201 let tabstop = ed.settings().tabstop;
202 hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
203 ed.set_sticky_col(sticky);
204 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
205 let pos = buf_cursor_pos(ed.buffer());
206 let line = buf_line(ed.buffer(), pos.row).unwrap_or_default();
207 ed.set_sticky_col(Some(char_col_to_visual_col(&line, pos.col, tabstop)));
208 ed.sync_buffer_from_textarea();
209 }
210 hjkl_engine::MotionKind::FirstNonBlankUp => {
211 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
214 let mut sticky = ed.sticky_col();
215 let tabstop = ed.settings().tabstop;
216 hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
217 ed.set_sticky_col(sticky);
218 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
219 let pos = buf_cursor_pos(ed.buffer());
220 let line = buf_line(ed.buffer(), pos.row).unwrap_or_default();
221 ed.set_sticky_col(Some(char_col_to_visual_col(&line, pos.col, tabstop)));
222 ed.sync_buffer_from_textarea();
223 }
224 hjkl_engine::MotionKind::WordForward => {
225 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
226 }
227 hjkl_engine::MotionKind::BigWordForward => {
228 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
229 }
230 hjkl_engine::MotionKind::WordBackward => {
231 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
232 }
233 hjkl_engine::MotionKind::BigWordBackward => {
234 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
235 }
236 hjkl_engine::MotionKind::WordEnd => {
237 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
238 }
239 hjkl_engine::MotionKind::BigWordEnd => {
240 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
241 }
242 hjkl_engine::MotionKind::LineStart => {
243 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
246 }
247 hjkl_engine::MotionKind::FirstNonBlank => {
248 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
251 }
252 hjkl_engine::MotionKind::GotoLine => {
253 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
262 }
263 hjkl_engine::MotionKind::LineEnd => {
264 execute_motion_with_block_vcol(ed, Motion::LineEnd, count);
267 }
268 hjkl_engine::MotionKind::FindRepeat => {
269 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
273 }
274 hjkl_engine::MotionKind::FindRepeatReverse => {
275 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
279 }
280 hjkl_engine::MotionKind::BracketMatch => {
281 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
286 }
287 hjkl_engine::MotionKind::ViewportTop => {
288 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
291 }
292 hjkl_engine::MotionKind::ViewportMiddle => {
293 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
296 }
297 hjkl_engine::MotionKind::ViewportBottom => {
298 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
301 }
302 hjkl_engine::MotionKind::HalfPageDown => {
303 {
307 let d = ed.viewport_half_rows(count) as isize;
308 ed.scroll_cursor_rows(d);
309 }
310 }
311 hjkl_engine::MotionKind::HalfPageUp => {
312 {
315 let d = -(ed.viewport_half_rows(count) as isize);
316 ed.scroll_cursor_rows(d);
317 }
318 }
319 hjkl_engine::MotionKind::FullPageDown => {
320 {
323 let d = ed.viewport_full_rows(count) as isize;
324 ed.scroll_cursor_rows(d);
325 }
326 }
327 hjkl_engine::MotionKind::FullPageUp => {
328 {
331 let d = -(ed.viewport_full_rows(count) as isize);
332 ed.scroll_cursor_rows(d);
333 }
334 }
335 hjkl_engine::MotionKind::FirstNonBlankLine => {
336 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
337 }
338 hjkl_engine::MotionKind::SectionBackward => {
339 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
340 }
341 hjkl_engine::MotionKind::SectionForward => {
342 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
343 }
344 hjkl_engine::MotionKind::SectionEndBackward => {
345 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
346 }
347 hjkl_engine::MotionKind::SectionEndForward => {
348 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
349 }
350 _ => {}
355 }
356}
357pub fn apply_sticky_col<H: hjkl_engine::types::Host>(
362 ed: &mut Editor<hjkl_buffer::View, H>,
363 motion: &Motion,
364 pre_col: usize,
365) {
366 if is_vertical_motion(motion) {
367 let want = ed.sticky_col().unwrap_or_else(|| {
371 let (row, _) = ed.cursor();
372 let line = buf_line(ed.buffer(), row).unwrap_or_default();
373 char_col_to_visual_col(&line, pre_col, ed.settings().tabstop)
374 });
375 ed.set_sticky_col(Some(want));
378 let (row, _) = ed.cursor();
379 let line = buf_line(ed.buffer(), row).unwrap_or_default();
380 let char_col = visual_col_to_char_col(&line, want, ed.settings().tabstop);
384 let line_len = buf_line_chars(ed.buffer(), row);
385 let max_col = line_len.saturating_sub(1);
386 let target = char_col.min(max_col);
387 buf_set_cursor_rc(ed.buffer_mut(), row, target);
391 } else {
392 let (row, col) = ed.cursor();
395 let line = buf_line(ed.buffer(), row).unwrap_or_default();
396 ed.set_sticky_col(Some(char_col_to_visual_col(
397 &line,
398 col,
399 ed.settings().tabstop,
400 )));
401 }
402}
403pub fn is_vertical_motion(motion: &Motion) -> bool {
404 matches!(
408 motion,
409 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
410 )
411}
412pub fn apply_motion_cursor<H: hjkl_engine::types::Host>(
413 ed: &mut Editor<hjkl_buffer::View, H>,
414 motion: &Motion,
415 count: usize,
416) {
417 apply_motion_cursor_ctx(ed, motion, count, false)
418}
419pub fn apply_motion_cursor_ctx<H: hjkl_engine::types::Host>(
420 ed: &mut Editor<hjkl_buffer::View, H>,
421 motion: &Motion,
422 count: usize,
423 as_operator: bool,
424) {
425 let count = count
434 .min(MAX_COUNT)
435 .min(ed.buffer().rope().len_chars().saturating_add(1));
436 match motion {
437 Motion::Left => {
438 hjkl_engine::motions::move_left(ed.buffer_mut(), count);
440 }
441 Motion::Right => {
442 if as_operator {
446 hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
447 } else {
448 hjkl_engine::motions::move_right_in_line(ed.buffer_mut(), count);
449 }
450 }
451 Motion::SpaceFwd => {
452 if as_operator {
455 hjkl_engine::motions::move_right_to_end(ed.buffer_mut(), count);
456 } else {
457 hjkl_engine::motions::move_space_fwd(ed.buffer_mut(), count);
458 }
459 }
460 Motion::BackspaceBack => {
461 if as_operator {
464 hjkl_engine::motions::move_left(ed.buffer_mut(), count);
465 } else {
466 hjkl_engine::motions::move_backspace_back(ed.buffer_mut(), count);
467 }
468 }
469 Motion::Up => {
470 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
474 let mut sticky = ed.sticky_col();
475 let tabstop = ed.settings().tabstop;
476 hjkl_engine::motions::move_up(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
477 ed.set_sticky_col(sticky);
478 }
479 Motion::Down => {
480 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
481 let mut sticky = ed.sticky_col();
482 let tabstop = ed.settings().tabstop;
483 hjkl_engine::motions::move_down(ed.buffer_mut(), &folds, count, &mut sticky, tabstop);
484 ed.set_sticky_col(sticky);
485 }
486 Motion::ScreenUp => {
487 let v = *ed.host().viewport();
488 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
489 let mut sticky = ed.sticky_col();
490 let tabstop = ed.settings().tabstop;
491 hjkl_engine::motions::move_screen_up(
492 ed.buffer_mut(),
493 &folds,
494 &v,
495 count,
496 &mut sticky,
497 tabstop,
498 );
499 ed.set_sticky_col(sticky);
500 }
501 Motion::ScreenDown => {
502 let v = *ed.host().viewport();
503 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
504 let mut sticky = ed.sticky_col();
505 let tabstop = ed.settings().tabstop;
506 hjkl_engine::motions::move_screen_down(
507 ed.buffer_mut(),
508 &folds,
509 &v,
510 count,
511 &mut sticky,
512 tabstop,
513 );
514 ed.set_sticky_col(sticky);
515 }
516 Motion::WordFwd => {
517 let iskeyword = ed.settings().iskeyword.clone();
518 hjkl_engine::motions::move_word_fwd(ed.buffer_mut(), false, count, &iskeyword);
519 if !as_operator {
522 let (row, col) = ed.cursor();
523 let line_len = buf_line_chars(ed.buffer(), row);
524 if col > line_len.saturating_sub(1) {
525 ed.jump_cursor(row, line_len.saturating_sub(1));
526 }
527 }
528 }
529 Motion::WordBack => {
530 let iskeyword = ed.settings().iskeyword.clone();
531 hjkl_engine::motions::move_word_back(ed.buffer_mut(), false, count, &iskeyword);
532 }
533 Motion::WordEnd => {
534 let iskeyword = ed.settings().iskeyword.clone();
535 hjkl_engine::motions::move_word_end(ed.buffer_mut(), false, count, &iskeyword);
536 }
537 Motion::BigWordFwd => {
538 let iskeyword = ed.settings().iskeyword.clone();
539 hjkl_engine::motions::move_word_fwd(ed.buffer_mut(), true, count, &iskeyword);
540 if !as_operator {
542 let (row, col) = ed.cursor();
543 let line_len = buf_line_chars(ed.buffer(), row);
544 if col > line_len.saturating_sub(1) {
545 ed.jump_cursor(row, line_len.saturating_sub(1));
546 }
547 }
548 }
549 Motion::BigWordBack => {
550 let iskeyword = ed.settings().iskeyword.clone();
551 hjkl_engine::motions::move_word_back(ed.buffer_mut(), true, count, &iskeyword);
552 }
553 Motion::BigWordEnd => {
554 let iskeyword = ed.settings().iskeyword.clone();
555 hjkl_engine::motions::move_word_end(ed.buffer_mut(), true, count, &iskeyword);
556 }
557 Motion::WordEndBack => {
558 let iskeyword = ed.settings().iskeyword.clone();
559 hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), false, count, &iskeyword);
560 }
561 Motion::BigWordEndBack => {
562 let iskeyword = ed.settings().iskeyword.clone();
563 hjkl_engine::motions::move_word_end_back(ed.buffer_mut(), true, count, &iskeyword);
564 }
565 Motion::LineStart => {
566 hjkl_engine::motions::move_line_start(ed.buffer_mut());
567 }
568 Motion::FirstNonBlank => {
569 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
570 }
571 Motion::LineEnd => {
572 if count > 1 {
575 let folds = hjkl_engine::SnapshotFoldProvider::from_buffer(ed.buffer());
576 let mut sticky = ed.sticky_col();
577 let tabstop = ed.settings().tabstop;
578 hjkl_engine::motions::move_down(
579 ed.buffer_mut(),
580 &folds,
581 count - 1,
582 &mut sticky,
583 tabstop,
584 );
585 ed.set_sticky_col(sticky);
586 }
587 hjkl_engine::motions::move_line_end(ed.buffer_mut());
588 }
589 Motion::FileTop => {
590 if count > 1 {
593 hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
594 } else {
595 hjkl_engine::motions::move_top(ed.buffer_mut());
596 }
597 }
598 Motion::FileBottom => {
599 if count > 1 {
602 hjkl_engine::motions::move_bottom(ed.buffer_mut(), count);
603 } else {
604 hjkl_engine::motions::move_bottom(ed.buffer_mut(), 0);
605 }
606 }
607 Motion::Find { ch, forward, till } => {
608 let repeat = std::mem::take(&mut vim_mut(ed).find_repeat_skip);
612 for i in 0..count {
613 let skip_adjacent = repeat || i > 0;
614 if !find_char_on_line(ed, *ch, *forward, *till, skip_adjacent) {
615 break;
616 }
617 }
618 }
619 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
621 let _ = matching_bracket(ed);
622 }
623 Motion::UnmatchedBracket { forward, open } => {
624 goto_unmatched_bracket(ed, *forward, *open, count);
625 }
626 Motion::WordAtCursor {
627 forward,
628 whole_word,
629 } => {
630 word_at_cursor_search(ed, *forward, *whole_word, count);
631 }
632 Motion::SearchNext { reverse } => {
633 if let Some(pattern) = ed.last_search_pattern() {
637 ed.push_search_pattern(&pattern);
638 }
639 if ed.search_state().pattern.is_none() {
640 return;
641 }
642 let forward = ed.last_search_forward() != *reverse;
646 for _ in 0..count.max(1) {
647 if forward {
648 ed.search_advance_forward(true);
649 } else {
650 ed.search_advance_backward(true);
651 }
652 }
653 }
654 Motion::ViewportTop => {
655 let v = *ed.host().viewport();
656 hjkl_engine::motions::move_viewport_top(ed.buffer_mut(), &v, count.saturating_sub(1));
657 }
658 Motion::ViewportMiddle => {
659 let v = *ed.host().viewport();
660 hjkl_engine::motions::move_viewport_middle(ed.buffer_mut(), &v);
661 }
662 Motion::ViewportBottom => {
663 let v = *ed.host().viewport();
664 hjkl_engine::motions::move_viewport_bottom(
665 ed.buffer_mut(),
666 &v,
667 count.saturating_sub(1),
668 );
669 }
670 Motion::LastNonBlank => {
671 hjkl_engine::motions::move_last_non_blank(ed.buffer_mut());
672 }
673 Motion::LineMiddle => {
674 let row = ed.cursor().0;
675 let line_chars = buf_line_chars(ed.buffer(), row);
676 let target = line_chars / 2;
679 ed.jump_cursor(row, target);
680 }
681 Motion::ScreenLineMiddle => {
682 let row = ed.cursor().0;
685 let width = ed.host().viewport().width as usize;
686 let last = buf_line_chars(ed.buffer(), row).saturating_sub(1);
687 let target = (width / 2).min(last);
688 ed.jump_cursor(row, target);
689 }
690 Motion::ParagraphPrev => {
691 hjkl_engine::motions::move_paragraph_prev(ed.buffer_mut(), count);
692 }
693 Motion::ParagraphNext => {
694 hjkl_engine::motions::move_paragraph_next(ed.buffer_mut(), count);
695 }
696 Motion::SentencePrev => {
697 for _ in 0..count.max(1) {
698 if let Some((row, col)) = sentence_boundary(ed, false) {
699 ed.jump_cursor(row, col);
700 }
701 }
702 }
703 Motion::SentenceNext => {
704 for _ in 0..count.max(1) {
705 if let Some((row, col)) = sentence_boundary(ed, true) {
706 ed.jump_cursor(row, col);
707 }
708 }
709 }
710 Motion::SectionBackward => {
711 hjkl_engine::motions::move_section_backward(ed.buffer_mut(), count);
712 }
713 Motion::SectionForward => {
714 hjkl_engine::motions::move_section_forward(ed.buffer_mut(), count);
715 }
716 Motion::SectionEndBackward => {
717 hjkl_engine::motions::move_section_end_backward(ed.buffer_mut(), count);
718 }
719 Motion::SectionEndForward => {
720 hjkl_engine::motions::move_section_end_forward(ed.buffer_mut(), count);
721 }
722 Motion::FirstNonBlankNextLine => {
723 hjkl_engine::motions::move_first_non_blank_next_line(ed.buffer_mut(), count);
724 }
725 Motion::FirstNonBlankPrevLine => {
726 hjkl_engine::motions::move_first_non_blank_prev_line(ed.buffer_mut(), count);
727 }
728 Motion::FirstNonBlankLine => {
729 hjkl_engine::motions::move_first_non_blank_line(ed.buffer_mut(), count);
730 }
731 Motion::GotoColumn => {
732 hjkl_engine::motions::move_goto_column(ed.buffer_mut(), count);
733 }
734 }
735}
736pub fn move_first_non_whitespace<H: hjkl_engine::types::Host>(
737 ed: &mut Editor<hjkl_buffer::View, H>,
738) {
739 ed.sync_buffer_content_from_textarea();
745 hjkl_engine::motions::move_first_non_blank(ed.buffer_mut());
746}
747pub fn find_char_on_line<H: hjkl_engine::types::Host>(
748 ed: &mut Editor<hjkl_buffer::View, H>,
749 ch: char,
750 forward: bool,
751 till: bool,
752 skip_adjacent: bool,
753) -> bool {
754 hjkl_engine::motions::find_char_on_line(ed.buffer_mut(), ch, forward, till, skip_adjacent)
755}
756pub fn matching_bracket<H: hjkl_engine::types::Host>(
757 ed: &mut Editor<hjkl_buffer::View, H>,
758) -> bool {
759 hjkl_engine::motions::match_bracket(ed.buffer_mut())
760}
761pub fn goto_unmatched_bracket<H: hjkl_engine::types::Host>(
765 ed: &mut Editor<hjkl_buffer::View, H>,
766 forward: bool,
767 open: char,
768 count: usize,
769) {
770 let close = match open {
771 '(' => ')',
772 '{' => '}',
773 _ => return,
774 };
775 let cursor = buf_cursor_pos(ed.buffer());
776 let rows = buf_row_count(ed.buffer());
777 let target = count.max(1);
778 let mut found = 0usize;
779 let mut depth = 0i32;
780
781 if forward {
782 let mut r = cursor.row;
783 let mut from_col = cursor.col + 1;
784 while r < rows {
785 let line: Vec<char> = buf_line(ed.buffer(), r)
786 .unwrap_or_default()
787 .chars()
788 .collect();
789 let mut ci = from_col;
790 while ci < line.len() {
791 let ch = line[ci];
792 if ch == open {
793 depth += 1;
794 } else if ch == close {
795 if depth == 0 {
796 found += 1;
797 if found == target {
798 buf_set_cursor_rc(ed.buffer_mut(), r, ci);
799 return;
800 }
801 } else {
802 depth -= 1;
803 }
804 }
805 ci += 1;
806 }
807 r += 1;
808 from_col = 0;
809 }
810 } else {
811 let mut r = cursor.row as isize;
812 let mut from_col = cursor.col as isize - 1;
815 while r >= 0 {
816 let line: Vec<char> = buf_line(ed.buffer(), r as usize)
817 .unwrap_or_default()
818 .chars()
819 .collect();
820 let mut ci = from_col.min(line.len() as isize - 1);
821 while ci >= 0 {
822 let ch = line[ci as usize];
823 if ch == close {
824 depth += 1;
825 } else if ch == open {
826 if depth == 0 {
827 found += 1;
828 if found == target {
829 buf_set_cursor_rc(ed.buffer_mut(), r as usize, ci as usize);
830 return;
831 }
832 } else {
833 depth -= 1;
834 }
835 }
836 ci -= 1;
837 }
838 r -= 1;
839 from_col = isize::MAX;
840 }
841 }
842}
843pub fn word_at_cursor_search<H: hjkl_engine::types::Host>(
844 ed: &mut Editor<hjkl_buffer::View, H>,
845 forward: bool,
846 whole_word: bool,
847 count: usize,
848) {
849 let (row, col) = ed.cursor();
850 let line: String = buf_line(ed.buffer(), row).unwrap_or_default();
851 let chars: Vec<char> = line.chars().collect();
852 if chars.is_empty() {
853 return;
854 }
855 let spec = ed.settings().iskeyword.clone();
857 let is_word = |c: char| is_keyword_char(c, &spec);
858 let mut start = col.min(chars.len().saturating_sub(1));
859 if is_word(chars[start]) {
860 while start > 0 && is_word(chars[start - 1]) {
861 start -= 1;
862 }
863 } else {
864 while start < chars.len() && !is_word(chars[start]) {
873 start += 1;
874 }
875 if start >= chars.len() {
876 return;
877 }
878 buf_set_cursor_rc(ed.buffer_mut(), row, start);
879 }
880 let mut end = start;
881 while end < chars.len() && is_word(chars[end]) {
882 end += 1;
883 }
884 if end <= start {
885 return;
886 }
887 let word: String = chars[start..end].iter().collect();
888 let escaped = regex_escape(&word);
889 let pattern = if whole_word {
890 format!(r"\b{escaped}\b")
891 } else {
892 escaped
893 };
894 ed.push_search_pattern(&pattern);
895 if ed.search_state().pattern.is_none() {
896 return;
897 }
898 ed.set_last_search_pattern_only(Some(pattern));
900 ed.set_last_search_forward_only(forward);
901 for _ in 0..count.max(1) {
902 if forward {
903 ed.search_advance_forward(true);
904 } else {
905 ed.search_advance_backward(true);
906 }
907 }
908}
909pub fn regex_escape(s: &str) -> String {
910 let mut out = String::with_capacity(s.len());
911 for c in s.chars() {
912 if matches!(
913 c,
914 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
915 ) {
916 out.push('\\');
917 }
918 out.push(c);
919 }
920 out
921}