1pub use hjkl_vim_types::{
75 Abbrev, AbbrevKind, AbbrevTrigger, CHANGE_LIST_MAX, InsertDir, InsertEntry, InsertReason,
76 InsertSession, JUMPLIST_MAX, LastChange, LastHorizontalMotion, LastVisual, Mode, Motion,
77 Operator, Pending, RangeKind, SEARCH_HISTORY_MAX, ScrollDir, SearchPrompt, TextObject,
78};
79
80use crate::VimMode;
81use crate::input::{Input, Key};
82
83use crate::buf_helpers::{
84 buf_cursor_pos, buf_line, buf_line_bytes, buf_line_chars, buf_row_count, buf_set_cursor_pos,
85 buf_set_cursor_rc,
86};
87use crate::editor::Editor;
88
89pub const MAX_COUNT: usize = 999_999_999;
98
99pub(crate) fn rot13_str(s: &str) -> String {
101 s.chars()
102 .map(|c| match c {
103 'a'..='z' => (((c as u8 - b'a' + 13) % 26) + b'a') as char,
104 'A'..='Z' => (((c as u8 - b'A' + 13) % 26) + b'A') as char,
105 _ => c,
106 })
107 .collect()
108}
109
110#[derive(Default)]
115pub struct VimState {
116 pub mode: Mode,
121 pub pending: Pending,
123 pub count: usize,
126 pub last_find: Option<(char, bool, bool)>,
128 pub find_repeat_skip: bool,
132 pub last_change: Option<LastChange>,
134 pub insert_session: Option<InsertSession>,
136 pub visual_anchor: (usize, usize),
140 pub visual_line_anchor: usize,
142 pub block_anchor: (usize, usize),
145 pub block_vcol: usize,
151 pub yank_linewise: bool,
153 pub pending_register: Option<char>,
156 pub recording_macro: Option<char>,
160 pub recording_keys: Vec<crate::input::Input>,
165 pub replaying_macro: bool,
168 pub last_macro: Option<char>,
170 pub last_insert_pos: Option<(usize, usize)>,
174 pub last_visual: Option<LastVisual>,
177 pub viewport_pinned: bool,
181 pub scroll_anim_hint: bool,
184 pub replaying: bool,
186 pub one_shot_normal: bool,
189 pub search_prompt: Option<SearchPrompt>,
191 pub last_search: Option<String>,
195 pub last_search_forward: bool,
199 pub last_insert_text: Option<String>,
202 pub jump_back: Vec<(usize, usize)>,
207 pub jump_fwd: Vec<(usize, usize)>,
210 pub insert_pending_register: bool,
214 pub change_mark_start: Option<(usize, usize)>,
220 pub search_history: Vec<String>,
224 pub search_history_cursor: Option<usize>,
229 pub last_input_at: Option<std::time::Instant>,
238 pub last_input_host_at: Option<core::time::Duration>,
242 pub current_mode: crate::VimMode,
248 pub last_substitute: Option<crate::substitute::SubstituteCmd>,
250 pub pending_closes: Vec<(usize, usize, char)>,
258 pub last_sneak: Option<((char, char), bool)>,
261 pub last_horizontal_motion: LastHorizontalMotion,
264 pub abbrevs: Vec<Abbrev>,
267}
268
269impl VimState {
270 pub fn public_mode(&self) -> VimMode {
271 match self.mode {
272 Mode::Normal => VimMode::Normal,
273 Mode::Insert => VimMode::Insert,
274 Mode::Visual => VimMode::Visual,
275 Mode::VisualLine => VimMode::VisualLine,
276 Mode::VisualBlock => VimMode::VisualBlock,
277 }
278 }
279
280 pub fn force_normal(&mut self) {
281 self.mode = Mode::Normal;
282 self.pending = Pending::None;
283 self.count = 0;
284 self.insert_session = None;
285 self.current_mode = crate::VimMode::Normal;
287 }
288
289 pub fn clear_pending_prefix(&mut self) {
299 self.pending = Pending::None;
300 self.count = 0;
301 self.pending_register = None;
302 self.insert_pending_register = false;
303 }
304
305 pub(crate) fn widen_insert_row(&mut self, row: usize) {
310 if let Some(ref mut session) = self.insert_session {
311 session.row_min = session.row_min.min(row);
312 session.row_max = session.row_max.max(row);
313 }
314 }
315
316 pub fn is_visual(&self) -> bool {
317 matches!(
318 self.mode,
319 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
320 )
321 }
322
323 pub fn is_visual_char(&self) -> bool {
324 self.mode == Mode::Visual
325 }
326
327 pub(crate) fn pending_count_val(&self) -> Option<u32> {
330 if self.count == 0 {
331 None
332 } else {
333 Some(self.count as u32)
334 }
335 }
336
337 pub(crate) fn is_chord_pending(&self) -> bool {
340 !matches!(self.pending, Pending::None)
341 }
342
343 pub(crate) fn pending_op_char(&self) -> Option<char> {
347 let op = match &self.pending {
348 Pending::Op { op, .. }
349 | Pending::OpTextObj { op, .. }
350 | Pending::OpG { op, .. }
351 | Pending::OpFind { op, .. }
352 | Pending::OpSquareBracketOpen { op, .. }
353 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
354 _ => None,
355 };
356 op.map(|o| match o {
357 Operator::Delete => 'd',
358 Operator::Change => 'c',
359 Operator::Yank => 'y',
360 Operator::Uppercase => 'U',
361 Operator::Lowercase => 'u',
362 Operator::ToggleCase => '~',
363 Operator::Indent => '>',
364 Operator::Outdent => '<',
365 Operator::Fold => 'z',
366 Operator::Reflow => 'q',
367 Operator::ReflowKeepCursor => 'w',
368 Operator::AutoIndent => '=',
369 Operator::Filter => '!',
370 Operator::Comment => 'c',
372 Operator::Rot13 => '?',
374 })
375 }
376}
377
378pub(crate) fn enter_search<H: crate::types::Host>(
384 ed: &mut Editor<hjkl_buffer::Buffer, H>,
385 forward: bool,
386) {
387 ed.vim.search_prompt = Some(SearchPrompt {
388 text: String::new(),
389 cursor: 0,
390 forward,
391 operator: None,
392 });
393 ed.vim.search_history_cursor = None;
394 ed.set_search_pattern(None);
398}
399
400pub(crate) fn enter_search_op<H: crate::types::Host>(
404 ed: &mut Editor<hjkl_buffer::Buffer, H>,
405 forward: bool,
406 op: Operator,
407 count: usize,
408) {
409 let origin = ed.cursor();
410 ed.vim.search_prompt = Some(SearchPrompt {
411 text: String::new(),
412 cursor: 0,
413 forward,
414 operator: Some((op, count.max(1), origin)),
415 });
416 ed.vim.search_history_cursor = None;
417 ed.set_search_pattern(None);
418}
419
420pub(crate) fn apply_op_search_range<H: crate::types::Host>(
424 ed: &mut Editor<hjkl_buffer::Buffer, H>,
425 op: Operator,
426 origin: (usize, usize),
427) {
428 let target = ed.cursor();
429 run_operator_over_range(ed, op, origin, target, RangeKind::Exclusive);
430}
431
432fn walk_change_list<H: crate::types::Host>(
436 ed: &mut Editor<hjkl_buffer::Buffer, H>,
437 dir: isize,
438 count: usize,
439) {
440 if ed.change_list.is_empty() {
441 return;
442 }
443 let len = ed.change_list.len();
444 let mut idx: isize = match (ed.change_list_cursor, dir) {
445 (None, -1) => len as isize - 1,
446 (None, 1) => return, (Some(i), -1) => i as isize - 1,
448 (Some(i), 1) => i as isize + 1,
449 _ => return,
450 };
451 for _ in 1..count {
452 let next = idx + dir;
453 if next < 0 || next >= len as isize {
454 break;
455 }
456 idx = next;
457 }
458 if idx < 0 || idx >= len as isize {
459 return;
460 }
461 let idx = idx as usize;
462 ed.change_list_cursor = Some(idx);
463 let (row, col) = ed.change_list[idx];
464 ed.jump_cursor(row, col);
465}
466
467fn insert_register_text<H: crate::types::Host>(
472 ed: &mut Editor<hjkl_buffer::Buffer, H>,
473 selector: char,
474) {
475 use hjkl_buffer::Edit;
476 let text = match selector {
479 '/' => match &ed.vim.last_search {
480 Some(s) if !s.is_empty() => s.clone(),
481 _ => return,
482 },
483 '.' => match &ed.vim.last_insert_text {
484 Some(s) if !s.is_empty() => s.clone(),
485 _ => return,
486 },
487 _ => match ed.registers().read(selector) {
488 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
489 _ => return,
490 },
491 };
492 ed.sync_buffer_content_from_textarea();
493 let cursor = buf_cursor_pos(&ed.buffer);
494 ed.mutate_edit(Edit::InsertStr {
495 at: cursor,
496 text: text.clone(),
497 });
498 let mut row = cursor.row;
501 let mut col = cursor.col;
502 for ch in text.chars() {
503 if ch == '\n' {
504 row += 1;
505 col = 0;
506 } else {
507 col += 1;
508 }
509 }
510 buf_set_cursor_rc(&mut ed.buffer, row, col);
511 ed.push_buffer_cursor_to_textarea();
512 ed.mark_content_dirty();
513 if let Some(ref mut session) = ed.vim.insert_session {
514 session.row_min = session.row_min.min(row);
515 session.row_max = session.row_max.max(row);
516 }
517}
518
519pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
538 if !settings.autoindent {
539 return String::new();
540 }
541 let base: String = prev_line
543 .chars()
544 .take_while(|c| *c == ' ' || *c == '\t')
545 .collect();
546
547 if settings.smartindent {
548 let unit = if settings.expandtab {
549 if settings.softtabstop > 0 {
550 " ".repeat(settings.softtabstop)
551 } else {
552 " ".repeat(settings.shiftwidth)
553 }
554 } else {
555 "\t".to_string()
556 };
557
558 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
560 if matches!(last_non_ws, Some('{' | '(' | '[')) {
561 return format!("{base}{unit}");
562 }
563
564 if is_html_filetype(&settings.filetype) {
569 let trimmed_end_len = prev_line
570 .trim_end_matches(|c: char| c.is_whitespace())
571 .len();
572 let trimmed = &prev_line[..trimmed_end_len];
573 if let Some(stripped) = trimmed.strip_suffix('>')
574 && scan_tag_opener(trimmed, stripped.len()).is_some()
575 {
576 return format!("{base}{unit}");
577 }
578 }
579 }
580
581 base
582}
583
584fn comment_prefixes_for_lang(lang: &str) -> &'static [&'static str] {
590 match lang {
591 "rust" => &["/// ", "//! ", "// "],
592 "c" | "cpp" => &["// "],
593 "python" | "sh" | "bash" | "zsh" | "fish" | "toml" | "yaml" => &["# "],
594 "lua" => &["-- "],
595 "sql" => &["-- "],
596 "vim" | "viml" => &["\" "],
597 _ => &[],
598 }
599}
600
601pub(crate) fn detect_comment_on_line(lang: &str, line: &str) -> Option<(String, &'static str)> {
607 let indent_end = line
608 .char_indices()
609 .find(|(_, c)| *c != ' ' && *c != '\t')
610 .map(|(i, _)| i)
611 .unwrap_or(line.len());
612 let indent = line[..indent_end].to_string();
613 let rest = &line[indent_end..];
614 for &prefix in comment_prefixes_for_lang(lang) {
615 if rest.starts_with(prefix) {
616 return Some((indent, prefix));
617 }
618 let bare = prefix.trim_end_matches(' ');
621 if rest == bare || rest.starts_with(&format!("{bare} ")) {
622 return Some((indent, prefix));
623 }
624 }
625 None
626}
627
628pub(crate) fn continue_comment(
635 buffer: &hjkl_buffer::Buffer,
636 settings: &crate::editor::Settings,
637 row: usize,
638) -> Option<String> {
639 if settings.filetype.is_empty() {
640 return None;
641 }
642 let line = crate::buf_helpers::buf_line(buffer, row)?;
643 let (indent, prefix) = detect_comment_on_line(&settings.filetype, &line)?;
644 Some(format!("{indent}{prefix}"))
645}
646
647fn try_dedent_close_bracket<H: crate::types::Host>(
657 ed: &mut Editor<hjkl_buffer::Buffer, H>,
658 cursor: hjkl_buffer::Position,
659 ch: char,
660) -> bool {
661 use hjkl_buffer::{Edit, MotionKind, Position};
662
663 if !ed.settings.smartindent {
664 return false;
665 }
666 if !matches!(ch, '}' | ')' | ']') {
667 return false;
668 }
669
670 let line = match buf_line(&ed.buffer, cursor.row) {
671 Some(l) => l.to_string(),
672 None => return false,
673 };
674
675 let before: String = line.chars().take(cursor.col).collect();
677 if !before.chars().all(|c| c == ' ' || c == '\t') {
678 return false;
679 }
680 if before.is_empty() {
681 return false;
683 }
684
685 let unit_len: usize = if ed.settings.expandtab {
687 if ed.settings.softtabstop > 0 {
688 ed.settings.softtabstop
689 } else {
690 ed.settings.shiftwidth
691 }
692 } else {
693 1
695 };
696
697 let strip_len = if ed.settings.expandtab {
699 let spaces = before.chars().filter(|c| *c == ' ').count();
701 if spaces < unit_len {
702 return false;
703 }
704 unit_len
705 } else {
706 if !before.starts_with('\t') {
708 return false;
709 }
710 1
711 };
712
713 ed.mutate_edit(Edit::DeleteRange {
715 start: Position::new(cursor.row, 0),
716 end: Position::new(cursor.row, strip_len),
717 kind: MotionKind::Char,
718 });
719 let new_col = cursor.col.saturating_sub(strip_len);
724 ed.mutate_edit(Edit::InsertChar {
725 at: Position::new(cursor.row, new_col),
726 ch,
727 });
728 true
729}
730
731fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
732 let Some(session) = ed.vim.insert_session.take() else {
733 return;
734 };
735 let after_rope = crate::types::Query::rope(&ed.buffer);
736 let before_n = session.before_rope.len_lines();
740 let after_n = after_rope.len_lines();
741 let after_end = session.row_max.min(after_n.saturating_sub(1));
742 let before_end = session.row_max.min(before_n.saturating_sub(1));
743 let before = if before_end >= session.row_min && session.row_min < before_n {
744 rope_row_range_str(&session.before_rope, session.row_min, before_end)
745 } else {
746 String::new()
747 };
748 let after = if after_end >= session.row_min && session.row_min < after_n {
749 rope_row_range_str(&after_rope, session.row_min, after_end)
750 } else {
751 String::new()
752 };
753 let inserted = if matches!(session.reason, InsertReason::Replace) {
757 changed_run(&before, &after)
758 } else {
759 extract_inserted(&before, &after)
760 };
761 if !ed.vim.replaying && !inserted.is_empty() {
763 ed.vim.last_insert_text = Some(inserted.clone());
764 }
765 let open_line = matches!(session.reason, InsertReason::Open { .. });
766 if session.count > 1 && !ed.vim.replaying {
767 use hjkl_buffer::{Edit, Position};
768 if open_line {
769 let (start_row, _) = ed.cursor();
774 let typed = buf_line(&ed.buffer, start_row).unwrap_or_default();
775 for at_row in start_row..start_row + (session.count - 1) {
776 let end = buf_line_chars(&ed.buffer, at_row);
777 ed.mutate_edit(Edit::InsertStr {
778 at: Position::new(at_row, end),
779 text: format!("\n{typed}"),
780 });
781 }
782 } else if !inserted.is_empty() {
783 for _ in 0..session.count - 1 {
785 let (row, col) = ed.cursor();
786 ed.mutate_edit(Edit::InsertStr {
787 at: Position::new(row, col),
788 text: inserted.clone(),
789 });
790 }
791 }
792 }
793 fn replicate_block_text<H: crate::types::Host>(
797 ed: &mut Editor<hjkl_buffer::Buffer, H>,
798 inserted: &str,
799 top: usize,
800 bot: usize,
801 col: usize,
802 ) {
803 use hjkl_buffer::{Edit, Position};
804 for r in (top + 1)..=bot {
805 let line_len = buf_line_chars(&ed.buffer, r);
806 if col > line_len {
807 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
808 ed.mutate_edit(Edit::InsertStr {
809 at: Position::new(r, line_len),
810 text: pad,
811 });
812 }
813 ed.mutate_edit(Edit::InsertStr {
814 at: Position::new(r, col),
815 text: inserted.to_string(),
816 });
817 }
818 }
819
820 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
821 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
824 replicate_block_text(ed, &inserted, top, bot, col);
825 buf_set_cursor_rc(&mut ed.buffer, top, col);
826 ed.push_buffer_cursor_to_textarea();
827 }
828 return;
829 }
830 if let InsertReason::BlockChange { top, bot, col } = session.reason {
831 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
835 replicate_block_text(ed, &inserted, top, bot, col);
836 let ins_chars = inserted.chars().count();
837 let line_len = buf_line_chars(&ed.buffer, top);
838 let target_col = (col + ins_chars).min(line_len);
839 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
840 ed.push_buffer_cursor_to_textarea();
841 }
842 return;
843 }
844 if ed.vim.replaying {
845 return;
846 }
847 match session.reason {
848 InsertReason::Enter(entry) => {
849 ed.vim.last_change = Some(LastChange::InsertAt {
850 entry,
851 inserted,
852 count: session.count,
853 });
854 }
855 InsertReason::Open { above } => {
856 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
857 }
858 InsertReason::AfterChange => {
859 if let Some(
860 LastChange::OpMotion { inserted: ins, .. }
861 | LastChange::OpTextObj { inserted: ins, .. }
862 | LastChange::LineOp { inserted: ins, .. }
863 | LastChange::GnOp { inserted: ins, .. },
864 ) = ed.vim.last_change.as_mut()
865 {
866 *ins = Some(inserted);
867 }
868 if let Some(start) = ed.vim.change_mark_start.take() {
874 let end = ed.cursor();
875 ed.set_mark('[', start);
876 ed.set_mark(']', end);
877 }
878 }
879 InsertReason::DeleteToEol => {
880 ed.vim.last_change = Some(LastChange::DeleteToEol {
881 inserted: Some(inserted),
882 });
883 }
884 InsertReason::ReplayOnly => {}
885 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
886 InsertReason::BlockChange { .. } => unreachable!("handled above"),
887 InsertReason::Replace => {
888 ed.vim.last_change = Some(LastChange::ReplaceMode { text: inserted });
891 }
892 }
893}
894
895pub(crate) fn begin_insert<H: crate::types::Host>(
896 ed: &mut Editor<hjkl_buffer::Buffer, H>,
897 count: usize,
898 reason: InsertReason,
899) {
900 if !ed.settings.modifiable {
902 return;
903 }
904 if ed.view == crate::ViewMode::Blame {
906 ed.view = crate::ViewMode::Normal;
907 return;
908 }
909 let record = !matches!(reason, InsertReason::ReplayOnly);
910 if record {
911 ed.push_undo();
912 }
913 let reason = if ed.vim.replaying {
914 InsertReason::ReplayOnly
915 } else {
916 reason
917 };
918 let (row, col) = ed.cursor();
919 ed.vim.insert_session = Some(InsertSession {
920 count,
921 row_min: row,
922 row_max: row,
923 before_rope: crate::types::Query::rope(&ed.buffer),
924 reason,
925 start_row: row,
926 start_col: col,
927 });
928 ed.vim.mode = Mode::Insert;
929 ed.vim.current_mode = crate::VimMode::Insert;
931 drop_blame_if_left_normal(ed);
932}
933
934pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
949 ed: &mut Editor<hjkl_buffer::Buffer, H>,
950) {
951 if !ed.settings.undo_break_on_motion {
952 return;
953 }
954 if ed.vim.replaying {
955 return;
956 }
957 if ed.vim.insert_session.is_none() {
958 return;
959 }
960 ed.push_undo();
961 let before_rope = crate::types::Query::rope(&ed.buffer);
962 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
963 if let Some(ref mut session) = ed.vim.insert_session {
964 session.before_rope = before_rope;
965 session.row_min = row;
966 session.row_max = row;
967 }
968}
969
970pub(crate) fn maybe_word_undo_break<H: crate::types::Host>(
994 ed: &mut Editor<hjkl_buffer::Buffer, H>,
995 next: char,
996) {
997 use crate::buf_helpers::{buf_cursor_pos, buf_line};
998 use crate::editor::UndoGranularity;
999
1000 if ed.settings.undo_granularity != UndoGranularity::Word {
1002 return;
1003 }
1004 if ed.vim.replaying {
1006 return;
1007 }
1008 let session = match ed.vim.insert_session.as_ref() {
1009 Some(s) => s,
1010 None => return,
1011 };
1012
1013 let cursor = buf_cursor_pos(&ed.buffer);
1014
1015 let is_first_pos = cursor.row == session.start_row && cursor.col == session.start_col;
1017 if is_first_pos {
1018 return;
1019 }
1020
1021 let should_break = if next == '\n' {
1023 true
1024 } else if next.is_whitespace() {
1025 false
1027 } else {
1028 let prev_char = buf_line(&ed.buffer, cursor.row)
1031 .as_deref()
1032 .and_then(|line| line.chars().nth(cursor.col.wrapping_sub(1)));
1033 match prev_char {
1034 Some(p) if p.is_whitespace() => true,
1036 None if cursor.col == 0 => false, _ => false,
1042 }
1043 };
1044
1045 if should_break {
1046 ed.push_undo();
1049 let before_rope = crate::types::Query::rope(&ed.buffer);
1050 let row = cursor.row;
1051 if let Some(ref mut session) = ed.vim.insert_session {
1052 session.before_rope = before_rope;
1053 session.row_min = row;
1054 session.row_max = row;
1055 }
1056 }
1057}
1058
1059fn autopair_close_for(
1091 ch: char,
1092 filetype: &str,
1093 prev_char: Option<char>,
1094 prev2_char: Option<char>,
1095) -> Option<char> {
1096 let is_triple_quote_third =
1102 matches!(ch, '"' | '`' | '\'') && prev_char == Some(ch) && prev2_char == Some(ch);
1103
1104 match ch {
1105 '(' => Some(')'),
1106 '[' => Some(']'),
1107 '{' => Some('}'),
1108 '"' => {
1109 if is_triple_quote_third {
1110 None
1111 } else {
1112 Some('"')
1113 }
1114 }
1115 '`' => {
1116 if is_triple_quote_third {
1117 None
1118 } else {
1119 Some('`')
1120 }
1121 }
1122 '<' => {
1123 if is_html_filetype(filetype) {
1124 Some('>')
1125 } else {
1126 None
1127 }
1128 }
1129 '\'' => {
1130 if is_triple_quote_third {
1131 return None;
1132 }
1133 if prev_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
1136 None
1137 } else {
1138 Some('\'')
1139 }
1140 }
1141 _ => None,
1142 }
1143}
1144
1145fn detect_code_fence_opener(line: &str, cursor_col: usize) -> Option<String> {
1160 if cursor_col != line.chars().count() {
1161 return None;
1162 }
1163 let trimmed = line.trim_start();
1164 let backtick_run = trimmed.chars().take_while(|c| *c == '`').count();
1165 if backtick_run < 3 {
1166 return None;
1167 }
1168 let rest = &trimmed[backtick_run..];
1169 if rest.is_empty() {
1170 return None;
1171 }
1172 let all_lang_chars = rest
1173 .chars()
1174 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-');
1175 if !all_lang_chars {
1176 return None;
1177 }
1178 Some("`".repeat(backtick_run))
1179}
1180
1181fn is_html_filetype(ft: &str) -> bool {
1183 matches!(
1184 ft,
1185 "html" | "xml" | "svg" | "jsx" | "tsx" | "vue" | "svelte"
1186 )
1187}
1188
1189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1205enum TagKind {
1206 Open,
1207 Close,
1208}
1209
1210#[derive(Debug, Clone, PartialEq, Eq)]
1212struct TagSpan {
1213 kind: TagKind,
1214 name: String,
1215 row: usize,
1217 name_start_col: usize,
1219 name_end_col: usize,
1220}
1221
1222fn detect_tag_at_cursor(line: &str, row: usize, col: usize) -> Option<TagSpan> {
1226 let chars: Vec<char> = line.chars().collect();
1227 let mut lt = None;
1229 let mut i = col.min(chars.len());
1230 while i > 0 {
1231 i -= 1;
1232 let c = chars[i];
1233 if c == '<' {
1234 lt = Some(i);
1235 break;
1236 }
1237 if c == '>' {
1239 return None;
1240 }
1241 }
1242 let lt = lt?;
1243 let (kind, name_start) = if chars.get(lt + 1) == Some(&'/') {
1245 (TagKind::Close, lt + 2)
1246 } else {
1247 (TagKind::Open, lt + 1)
1248 };
1249 let first = chars.get(name_start)?;
1251 if !first.is_ascii_alphabetic() {
1252 return None;
1253 }
1254 let mut name_end = name_start;
1256 while name_end < chars.len()
1257 && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-')
1258 {
1259 name_end += 1;
1260 }
1261 if col < name_start || col > name_end {
1265 return None;
1266 }
1267 let name: String = chars[name_start..name_end].iter().collect();
1268 Some(TagSpan {
1269 kind,
1270 name,
1271 row,
1272 name_start_col: name_start,
1273 name_end_col: name_end,
1274 })
1275}
1276
1277fn find_matching_tag(buffer: &hjkl_buffer::Buffer, anchor: &TagSpan) -> Option<TagSpan> {
1291 let row_count = buffer.row_count();
1292 let scan_forward = anchor.kind == TagKind::Open;
1293 let row_iter: Box<dyn Iterator<Item = usize>> = if scan_forward {
1294 Box::new(anchor.row..row_count)
1295 } else {
1296 Box::new((0..=anchor.row).rev())
1297 };
1298 let push_kind = if scan_forward {
1299 TagKind::Open
1300 } else {
1301 TagKind::Close
1302 };
1303 let mut depth: usize = 1;
1304
1305 for r in row_iter {
1306 let line = buf_line(buffer, r)?;
1307 let chars: Vec<char> = line.chars().collect();
1308 let tags = scan_line_tags(&chars, r);
1309 let tags_iter: Box<dyn Iterator<Item = TagSpan>> = if scan_forward {
1310 Box::new(tags.into_iter())
1311 } else {
1312 Box::new(tags.into_iter().rev())
1313 };
1314 for tag in tags_iter {
1315 if r == anchor.row
1317 && tag.name_start_col == anchor.name_start_col
1318 && tag.kind == anchor.kind
1319 {
1320 continue;
1321 }
1322 if r == anchor.row {
1326 if scan_forward && tag.name_start_col < anchor.name_start_col {
1327 continue;
1328 }
1329 if !scan_forward && tag.name_start_col > anchor.name_start_col {
1330 continue;
1331 }
1332 }
1333 if tag.kind == push_kind {
1334 depth += 1;
1335 } else {
1336 depth -= 1;
1337 if depth == 0 {
1338 return Some(tag);
1339 }
1340 }
1341 }
1342 }
1343 None
1344}
1345
1346fn scan_line_tags(chars: &[char], row: usize) -> Vec<TagSpan> {
1350 let mut out = Vec::new();
1351 let n = chars.len();
1352 let mut i = 0;
1353 while i < n {
1354 if chars[i] != '<' {
1355 i += 1;
1356 continue;
1357 }
1358 if chars[i..].starts_with(&['<', '!', '-', '-']) {
1360 let mut j = i + 4;
1361 while j + 2 < n && !(chars[j] == '-' && chars[j + 1] == '-' && chars[j + 2] == '>') {
1362 j += 1;
1363 }
1364 i = (j + 3).min(n);
1365 continue;
1366 }
1367 let (kind, name_start) = if chars.get(i + 1) == Some(&'/') {
1368 (TagKind::Close, i + 2)
1369 } else {
1370 (TagKind::Open, i + 1)
1371 };
1372 if chars
1374 .get(name_start)
1375 .is_none_or(|c| !c.is_ascii_alphabetic())
1376 {
1377 i += 1;
1378 continue;
1379 }
1380 let mut name_end = name_start;
1381 while name_end < n && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-') {
1382 name_end += 1;
1383 }
1384 let mut k = name_end;
1386 let mut self_closing = false;
1387 while k < n {
1388 if chars[k] == '>' {
1389 if k > name_end && chars[k - 1] == '/' {
1390 self_closing = true;
1391 }
1392 break;
1393 }
1394 k += 1;
1395 }
1396 if k >= n {
1397 break;
1399 }
1400 let name: String = chars[name_start..name_end].iter().collect();
1401 if !(self_closing || kind == TagKind::Open && is_void_element(&name)) {
1403 out.push(TagSpan {
1404 kind,
1405 name,
1406 row,
1407 name_start_col: name_start,
1408 name_end_col: name_end,
1409 });
1410 }
1411 i = k + 1;
1412 }
1413 out
1414}
1415
1416pub(crate) fn sync_paired_tag_on_exit<H: crate::types::Host>(
1421 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1422) {
1423 if !is_html_filetype(&ed.settings.filetype) {
1424 return;
1425 }
1426 let (row, col) = ed.cursor();
1427 let line = match buf_line(&ed.buffer, row) {
1428 Some(l) => l,
1429 None => return,
1430 };
1431 let anchor = match detect_tag_at_cursor(&line, row, col) {
1432 Some(t) => t,
1433 None => return,
1434 };
1435 let partner = match find_matching_tag(&ed.buffer, &anchor) {
1436 Some(t) => t,
1437 None => return,
1438 };
1439 if partner.name == anchor.name {
1440 return;
1441 }
1442 use hjkl_buffer::{Edit, MotionKind, Position};
1444 let start = Position::new(partner.row, partner.name_start_col);
1445 let end = Position::new(partner.row, partner.name_end_col);
1446 ed.mutate_edit(Edit::DeleteRange {
1447 start,
1448 end,
1449 kind: MotionKind::Char,
1450 });
1451 ed.mutate_edit(Edit::InsertStr {
1452 at: start,
1453 text: anchor.name.clone(),
1454 });
1455 buf_set_cursor_rc(&mut ed.buffer, row, col);
1458 ed.push_buffer_cursor_to_textarea();
1459}
1460
1461pub fn matching_tag_pair(
1467 buffer: &hjkl_buffer::Buffer,
1468 row: usize,
1469 col: usize,
1470) -> Option<[(usize, usize, usize); 2]> {
1471 let line = buf_line(buffer, row)?;
1472 let anchor = detect_tag_at_cursor(&line, row, col)?;
1473 let partner = find_matching_tag(buffer, &anchor)?;
1474 Some([
1475 (anchor.row, anchor.name_start_col, anchor.name_end_col),
1476 (partner.row, partner.name_start_col, partner.name_end_col),
1477 ])
1478}
1479
1480fn is_void_element(tag: &str) -> bool {
1482 matches!(
1483 tag.to_ascii_lowercase().as_str(),
1484 "area"
1485 | "base"
1486 | "br"
1487 | "col"
1488 | "embed"
1489 | "hr"
1490 | "img"
1491 | "input"
1492 | "link"
1493 | "meta"
1494 | "param"
1495 | "source"
1496 | "track"
1497 | "wbr"
1498 )
1499}
1500
1501fn scan_tag_opener(line: &str, col: usize) -> Option<String> {
1511 let before = if col > 0 { &line[..col] } else { return None };
1514
1515 let lt_pos = before.rfind('<')?;
1517 let inner = &before[lt_pos + 1..]; if inner.starts_with('!') {
1521 return None;
1522 }
1523 if inner.trim_end().ends_with('/') {
1525 return None;
1526 }
1527
1528 let tag: String = inner
1530 .chars()
1531 .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1532 .collect();
1533 if tag.is_empty() {
1534 return None;
1535 }
1536 if !tag
1538 .chars()
1539 .next()
1540 .map(|c| c.is_ascii_alphabetic())
1541 .unwrap_or(false)
1542 {
1543 return None;
1544 }
1545 if is_void_element(&tag) {
1546 return None;
1547 }
1548 Some(tag)
1549}
1550
1551pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1556 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1557 ch: char,
1558) -> bool {
1559 use hjkl_buffer::{Edit, MotionKind, Position};
1560 ed.sync_buffer_content_from_textarea();
1561 let in_replace = matches!(
1562 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1563 Some(InsertReason::Replace)
1564 );
1565
1566 if !in_replace && !ed.vim.abbrevs.is_empty() {
1573 let iskeyword = ed.settings.iskeyword.clone();
1574 if !is_keyword_char(ch, &iskeyword) {
1575 check_and_apply_abbrev(ed, AbbrevTrigger::NonKeyword(ch));
1577 }
1579 }
1580 maybe_word_undo_break(ed, ch);
1585
1586 let cursor = buf_cursor_pos(&ed.buffer);
1588 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1589
1590 if !in_replace
1599 && !ed.vim.pending_closes.is_empty()
1600 && let Some(&(pr, _pc, pch)) = ed.vim.pending_closes.last()
1601 && ch == pch
1602 && cursor.row == pr
1603 {
1604 let char_at_cursor =
1605 buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col));
1606 if char_at_cursor == Some(ch) {
1607 ed.vim.pending_closes.pop();
1608 let filetype = ed.settings.filetype.clone();
1610 let autoclose_tag = ed.settings.autoclose_tag;
1611 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1612 let new_col = cursor.col + 1;
1614 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1615 if let Some(line) = buf_line(&ed.buffer, cursor.row) {
1619 let char_col = new_col.saturating_sub(1);
1620 let byte_col = line
1621 .char_indices()
1622 .nth(char_col)
1623 .map(|(b, _)| b)
1624 .unwrap_or(line.len());
1625 if let Some(tag) = scan_tag_opener(&line, byte_col) {
1626 let close_tag = format!("</{tag}>");
1627 let insert_pos = Position::new(cursor.row, new_col);
1628 ed.mutate_edit(Edit::InsertStr {
1629 at: insert_pos,
1630 text: close_tag,
1631 });
1632 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1634 }
1635 }
1636 } else {
1637 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
1638 }
1639 ed.push_buffer_cursor_to_textarea();
1640 return true;
1641 }
1642 }
1643
1644 if in_replace && cursor.col < line_chars {
1645 ed.vim.pending_closes.clear();
1647 ed.mutate_edit(Edit::DeleteRange {
1648 start: cursor,
1649 end: Position::new(cursor.row, cursor.col + 1),
1650 kind: MotionKind::Char,
1651 });
1652 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1653 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1654 let autopair = ed.settings.autopair;
1656 let filetype = ed.settings.filetype.clone();
1657 let autoclose_tag = ed.settings.autoclose_tag;
1658
1659 let (prev_char, prev2_char) = {
1660 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1661 let chars: Vec<char> = line.chars().collect();
1662 let p1 = if cursor.col > 0 {
1663 chars.get(cursor.col - 1).copied()
1664 } else {
1665 None
1666 };
1667 let p2 = if cursor.col > 1 {
1668 chars.get(cursor.col - 2).copied()
1669 } else {
1670 None
1671 };
1672 (p1, p2)
1673 };
1674
1675 if autopair {
1676 if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
1677 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1679 let after = Position::new(cursor.row, cursor.col + 1);
1682 ed.mutate_edit(Edit::InsertChar {
1683 at: after,
1684 ch: close,
1685 });
1686 let between_col = cursor.col + 1;
1689 buf_set_cursor_rc(&mut ed.buffer, cursor.row, between_col);
1690 ed.vim.pending_closes.push((cursor.row, between_col, close));
1695 ed.push_buffer_cursor_to_textarea();
1696 return true;
1697 }
1698
1699 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1703 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1704 let new_col = cursor.col + 1;
1705 if let Some(line) = buf_line(&ed.buffer, cursor.row) {
1710 let char_col = new_col.saturating_sub(1);
1711 let byte_col = line
1712 .char_indices()
1713 .nth(char_col)
1714 .map(|(b, _)| b)
1715 .unwrap_or(line.len());
1716 if let Some(tag) = scan_tag_opener(&line, byte_col) {
1717 let close_tag = format!("</{tag}>");
1718 let insert_pos = Position::new(cursor.row, new_col);
1719 ed.mutate_edit(Edit::InsertStr {
1720 at: insert_pos,
1721 text: close_tag,
1722 });
1723 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1725 }
1726 }
1727 ed.push_buffer_cursor_to_textarea();
1728 return true;
1729 }
1730 }
1731
1732 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1737 }
1738 ed.push_buffer_cursor_to_textarea();
1739 true
1740}
1741
1742pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1748 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1749) -> bool {
1750 use hjkl_buffer::Edit;
1751 ed.sync_buffer_content_from_textarea();
1752
1753 if !ed.vim.abbrevs.is_empty() {
1757 check_and_apply_abbrev(ed, AbbrevTrigger::Cr);
1758 }
1759
1760 maybe_word_undo_break(ed, '\n');
1764
1765 let cursor = buf_cursor_pos(&ed.buffer);
1766 let prev_line = buf_line(&ed.buffer, cursor.row)
1767 .unwrap_or_default()
1768 .to_string();
1769
1770 if ed.settings.autopair && !ed.vim.pending_closes.is_empty() {
1774 let prev_char = if cursor.col > 0 {
1777 prev_line.chars().nth(cursor.col - 1)
1778 } else {
1779 None
1780 };
1781 let next_char = prev_line.chars().nth(cursor.col);
1782 let is_open_pair = matches!(
1783 (prev_char, next_char),
1784 (Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
1785 );
1786 if is_open_pair {
1787 ed.vim.pending_closes.clear();
1790 let base_indent: String = prev_line
1792 .chars()
1793 .take_while(|c| *c == ' ' || *c == '\t')
1794 .collect();
1795 let inner_indent = if ed.settings.expandtab {
1796 let unit = if ed.settings.softtabstop > 0 {
1797 ed.settings.softtabstop
1798 } else {
1799 ed.settings.shiftwidth
1800 };
1801 format!("{base_indent}{}", " ".repeat(unit))
1802 } else {
1803 format!("{base_indent}\t")
1804 };
1805 let text = format!("\n{inner_indent}\n{base_indent}");
1808 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1809 let new_row = cursor.row + 1;
1811 let new_col = inner_indent.len();
1812 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1813 ed.push_buffer_cursor_to_textarea();
1814 return true;
1815 }
1816 }
1817
1818 if ed.settings.autopair
1827 && let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
1828 {
1829 ed.vim.pending_closes.clear();
1830 let base_indent: String = prev_line
1831 .chars()
1832 .take_while(|c| *c == ' ' || *c == '\t')
1833 .collect();
1834 let text = format!("\n{base_indent}\n{base_indent}{fence}");
1835 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1836 let new_row = cursor.row + 1;
1837 let new_col = base_indent.chars().count();
1838 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1839 ed.push_buffer_cursor_to_textarea();
1840 return true;
1841 }
1842
1843 let comment_cont = if ed.settings.formatoptions.contains('r') {
1845 continue_comment(&ed.buffer, &ed.settings, cursor.row)
1846 } else {
1847 None
1848 };
1849
1850 ed.vim.pending_closes.clear();
1852
1853 let text = if let Some(cont) = comment_cont {
1854 format!("\n{cont}")
1857 } else {
1858 let indent = compute_enter_indent(&ed.settings, &prev_line);
1859 format!("\n{indent}")
1860 };
1861 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1862 ed.push_buffer_cursor_to_textarea();
1863 true
1864}
1865
1866pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1869 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1870) -> bool {
1871 use hjkl_buffer::Edit;
1872 ed.sync_buffer_content_from_textarea();
1873 let cursor = buf_cursor_pos(&ed.buffer);
1874 if ed.settings.expandtab {
1875 let sts = ed.settings.softtabstop;
1876 let n = if sts > 0 {
1877 sts - (cursor.col % sts)
1878 } else {
1879 ed.settings.tabstop.max(1)
1880 };
1881 ed.mutate_edit(Edit::InsertStr {
1882 at: cursor,
1883 text: " ".repeat(n),
1884 });
1885 } else {
1886 ed.mutate_edit(Edit::InsertChar {
1887 at: cursor,
1888 ch: '\t',
1889 });
1890 }
1891 ed.push_buffer_cursor_to_textarea();
1892 true
1893}
1894
1895pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1906 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1907) -> bool {
1908 use hjkl_buffer::{Edit, MotionKind, Position};
1909 ed.sync_buffer_content_from_textarea();
1910 let cursor = buf_cursor_pos(&ed.buffer);
1911
1912 if cursor.col > 0 {
1915 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1916 if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings.filetype, &line) {
1917 let full_prefix = format!("{indent}{prefix}");
1918 let line_trimmed = line.trim_end_matches(' ');
1921 let prefix_trimmed = full_prefix.trim_end_matches(' ');
1922 if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
1923 ed.mutate_edit(Edit::DeleteRange {
1925 start: Position::new(cursor.row, 0),
1926 end: cursor,
1927 kind: MotionKind::Char,
1928 });
1929 ed.push_buffer_cursor_to_textarea();
1930 return true;
1931 }
1932 }
1933 }
1934
1935 let sts = ed.settings.softtabstop;
1936 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1937 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1938 let chars: Vec<char> = line.chars().collect();
1939 let run_start = cursor.col - sts;
1940 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1941 ed.mutate_edit(Edit::DeleteRange {
1942 start: Position::new(cursor.row, run_start),
1943 end: cursor,
1944 kind: MotionKind::Char,
1945 });
1946 ed.push_buffer_cursor_to_textarea();
1947 return true;
1948 }
1949 }
1950 let result = if cursor.col > 0 {
1951 ed.mutate_edit(Edit::DeleteRange {
1952 start: Position::new(cursor.row, cursor.col - 1),
1953 end: cursor,
1954 kind: MotionKind::Char,
1955 });
1956 true
1957 } else if cursor.row > 0 {
1958 let prev_row = cursor.row - 1;
1959 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1960 ed.mutate_edit(Edit::JoinLines {
1961 row: prev_row,
1962 count: 1,
1963 with_space: false,
1964 });
1965 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1966 true
1967 } else {
1968 false
1969 };
1970 ed.push_buffer_cursor_to_textarea();
1971 result
1972}
1973
1974pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
1977 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1978) -> bool {
1979 use hjkl_buffer::{Edit, MotionKind, Position};
1980 ed.sync_buffer_content_from_textarea();
1981 let cursor = buf_cursor_pos(&ed.buffer);
1982 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1983 let result = if cursor.col < line_chars {
1984 ed.mutate_edit(Edit::DeleteRange {
1985 start: cursor,
1986 end: Position::new(cursor.row, cursor.col + 1),
1987 kind: MotionKind::Char,
1988 });
1989 buf_set_cursor_pos(&mut ed.buffer, cursor);
1990 true
1991 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1992 ed.mutate_edit(Edit::JoinLines {
1993 row: cursor.row,
1994 count: 1,
1995 with_space: false,
1996 });
1997 buf_set_cursor_pos(&mut ed.buffer, cursor);
1998 true
1999 } else {
2000 false
2001 };
2002 ed.push_buffer_cursor_to_textarea();
2003 result
2004}
2005
2006pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
2010 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2011 dir: InsertDir,
2012) -> bool {
2013 ed.sync_buffer_content_from_textarea();
2014 ed.vim.pending_closes.clear();
2015 match dir {
2016 InsertDir::Left => {
2017 crate::motions::move_left(&mut ed.buffer, 1);
2018 }
2019 InsertDir::Right => {
2020 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2021 }
2022 InsertDir::Up => {
2023 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2024 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2025 }
2026 InsertDir::Down => {
2027 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2028 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2029 }
2030 }
2031 break_undo_group_in_insert(ed);
2032 ed.push_buffer_cursor_to_textarea();
2033 false
2034}
2035
2036pub(crate) fn insert_home_bridge<H: crate::types::Host>(
2039 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2040) -> bool {
2041 ed.sync_buffer_content_from_textarea();
2042 ed.vim.pending_closes.clear();
2043 crate::motions::move_line_start(&mut ed.buffer);
2044 break_undo_group_in_insert(ed);
2045 ed.push_buffer_cursor_to_textarea();
2046 false
2047}
2048
2049pub(crate) fn insert_end_bridge<H: crate::types::Host>(
2052 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2053) -> bool {
2054 ed.sync_buffer_content_from_textarea();
2055 ed.vim.pending_closes.clear();
2056 crate::motions::move_line_end(&mut ed.buffer);
2057 break_undo_group_in_insert(ed);
2058 ed.push_buffer_cursor_to_textarea();
2059 false
2060}
2061
2062pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
2065 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2066 viewport_h: u16,
2067) -> bool {
2068 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2069 scroll_cursor_rows(ed, -rows);
2070 false
2071}
2072
2073pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
2076 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2077 viewport_h: u16,
2078) -> bool {
2079 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2080 scroll_cursor_rows(ed, rows);
2081 false
2082}
2083
2084pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
2088 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2089) -> bool {
2090 use hjkl_buffer::{Edit, MotionKind};
2091 ed.sync_buffer_content_from_textarea();
2092 let cursor = buf_cursor_pos(&ed.buffer);
2093 if cursor.row == 0 && cursor.col == 0 {
2094 return true;
2095 }
2096 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
2097 let word_start = buf_cursor_pos(&ed.buffer);
2098 if word_start == cursor {
2099 return true;
2100 }
2101 buf_set_cursor_pos(&mut ed.buffer, cursor);
2102 ed.mutate_edit(Edit::DeleteRange {
2103 start: word_start,
2104 end: cursor,
2105 kind: MotionKind::Char,
2106 });
2107 ed.push_buffer_cursor_to_textarea();
2108 true
2109}
2110
2111pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
2114 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2115) -> bool {
2116 use hjkl_buffer::{Edit, MotionKind, Position};
2117 ed.sync_buffer_content_from_textarea();
2118 let cursor = buf_cursor_pos(&ed.buffer);
2119 if cursor.col > 0 {
2120 ed.mutate_edit(Edit::DeleteRange {
2121 start: Position::new(cursor.row, 0),
2122 end: cursor,
2123 kind: MotionKind::Char,
2124 });
2125 ed.push_buffer_cursor_to_textarea();
2126 }
2127 true
2128}
2129
2130pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
2134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2135) -> bool {
2136 use hjkl_buffer::{Edit, MotionKind, Position};
2137 ed.sync_buffer_content_from_textarea();
2138 let cursor = buf_cursor_pos(&ed.buffer);
2139 if cursor.col > 0 {
2140 ed.mutate_edit(Edit::DeleteRange {
2141 start: Position::new(cursor.row, cursor.col - 1),
2142 end: cursor,
2143 kind: MotionKind::Char,
2144 });
2145 } else if cursor.row > 0 {
2146 let prev_row = cursor.row - 1;
2147 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2148 ed.mutate_edit(Edit::JoinLines {
2149 row: prev_row,
2150 count: 1,
2151 with_space: false,
2152 });
2153 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2154 }
2155 ed.push_buffer_cursor_to_textarea();
2156 true
2157}
2158
2159pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
2162 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2163) -> bool {
2164 let (row, col) = ed.cursor();
2165 let sw = ed.settings().shiftwidth;
2166 indent_rows(ed, row, row, 1);
2167 ed.jump_cursor(row, col + sw);
2168 true
2169}
2170
2171pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
2174 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2175) -> bool {
2176 let (row, col) = ed.cursor();
2177 let before_len = buf_line_bytes(&ed.buffer, row);
2178 outdent_rows(ed, row, row, 1);
2179 let after_len = buf_line_bytes(&ed.buffer, row);
2180 let stripped = before_len.saturating_sub(after_len);
2181 let new_col = col.saturating_sub(stripped);
2182 ed.jump_cursor(row, new_col);
2183 true
2184}
2185
2186pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
2190 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2191) -> bool {
2192 ed.vim.one_shot_normal = true;
2193 ed.vim.mode = Mode::Normal;
2194 ed.vim.current_mode = crate::VimMode::Normal;
2196 false
2197}
2198
2199pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
2203 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2204) -> bool {
2205 ed.vim.insert_pending_register = true;
2206 false
2207}
2208
2209pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
2213 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2214 reg: char,
2215) -> bool {
2216 insert_register_text(ed, reg);
2217 true
2220}
2221
2222pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
2228 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2229) -> bool {
2230 ed.vim.pending_closes.clear();
2231
2232 if !ed.vim.abbrevs.is_empty() {
2235 check_and_apply_abbrev(ed, AbbrevTrigger::Esc);
2236 }
2237
2238 finish_insert_session(ed);
2239 sync_paired_tag_on_exit(ed);
2243 ed.vim.mode = Mode::Normal;
2244 ed.vim.current_mode = crate::VimMode::Normal;
2246 let col = ed.cursor().1;
2247 ed.vim.last_insert_pos = Some(ed.cursor());
2248 if col > 0 {
2249 crate::motions::move_left(&mut ed.buffer, 1);
2250 ed.push_buffer_cursor_to_textarea();
2251 }
2252 ed.sticky_col = Some(ed.cursor().1);
2253 true
2254}
2255
2256pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
2263 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2264 count: usize,
2265) {
2266 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2267}
2268
2269pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
2271 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2272 count: usize,
2273) {
2274 move_first_non_whitespace(ed);
2275 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2276}
2277
2278pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
2280 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2281 count: usize,
2282) {
2283 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2284 ed.push_buffer_cursor_to_textarea();
2285 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2286}
2287
2288pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
2290 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2291 count: usize,
2292) {
2293 crate::motions::move_line_end(&mut ed.buffer);
2294 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2295 ed.push_buffer_cursor_to_textarea();
2296 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2297}
2298
2299pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
2303 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2304 count: usize,
2305) {
2306 use hjkl_buffer::{Edit, Position};
2307 ed.push_undo();
2308 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2309 ed.sync_buffer_content_from_textarea();
2310 let row = buf_cursor_pos(&ed.buffer).row;
2311 let line_chars = buf_line_chars(&ed.buffer, row);
2312 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
2313
2314 let comment_cont = if ed.settings.formatoptions.contains('o') {
2316 continue_comment(&ed.buffer, &ed.settings, row)
2317 } else {
2318 None
2319 };
2320
2321 let suffix = if let Some(cont) = comment_cont {
2322 format!("\n{cont}")
2323 } else {
2324 let indent = compute_enter_indent(&ed.settings, &prev_line);
2325 format!("\n{indent}")
2326 };
2327 ed.mutate_edit(Edit::InsertStr {
2328 at: Position::new(row, line_chars),
2329 text: suffix,
2330 });
2331 ed.push_buffer_cursor_to_textarea();
2332}
2333
2334pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
2338 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2339 count: usize,
2340) {
2341 use hjkl_buffer::{Edit, Position};
2342 ed.push_undo();
2343 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2344 ed.sync_buffer_content_from_textarea();
2345 let row = buf_cursor_pos(&ed.buffer).row;
2346
2347 let comment_cont = if ed.settings.formatoptions.contains('o') {
2349 continue_comment(&ed.buffer, &ed.settings, row)
2350 } else {
2351 None
2352 };
2353
2354 let (insert_text, new_line_content) = if let Some(cont) = comment_cont {
2357 let content = cont.clone();
2358 (format!("{cont}\n"), content)
2359 } else {
2360 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
2366 let indent = compute_enter_indent(&ed.settings, &cur);
2367 let content = indent.clone();
2368 (format!("{indent}\n"), content)
2369 };
2370 ed.mutate_edit(Edit::InsertStr {
2371 at: Position::new(row, 0),
2372 text: insert_text,
2373 });
2374 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2375 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2376 let new_row = buf_cursor_pos(&ed.buffer).row;
2377 buf_set_cursor_rc(&mut ed.buffer, new_row, new_line_content.chars().count());
2378 ed.push_buffer_cursor_to_textarea();
2379}
2380
2381pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
2383 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2384 count: usize,
2385) {
2386 begin_insert(ed, count.max(1), InsertReason::Replace);
2388}
2389
2390pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
2395 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2396 count: usize,
2397) {
2398 do_char_delete(ed, true, count.max(1));
2399 if !ed.vim.replaying {
2400 ed.vim.last_change = Some(LastChange::CharDel {
2401 forward: true,
2402 count: count.max(1),
2403 });
2404 }
2405}
2406
2407pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
2410 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2411 count: usize,
2412) {
2413 do_char_delete(ed, false, count.max(1));
2414 if !ed.vim.replaying {
2415 ed.vim.last_change = Some(LastChange::CharDel {
2416 forward: false,
2417 count: count.max(1),
2418 });
2419 }
2420}
2421
2422pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
2425 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2426 count: usize,
2427) {
2428 use hjkl_buffer::{Edit, MotionKind, Position};
2429 ed.push_undo();
2430 ed.sync_buffer_content_from_textarea();
2431 for _ in 0..count.max(1) {
2432 let cursor = buf_cursor_pos(&ed.buffer);
2433 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2434 if cursor.col >= line_chars {
2435 break;
2436 }
2437 ed.mutate_edit(Edit::DeleteRange {
2438 start: cursor,
2439 end: Position::new(cursor.row, cursor.col + 1),
2440 kind: MotionKind::Char,
2441 });
2442 }
2443 ed.push_buffer_cursor_to_textarea();
2444 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
2445 if !ed.vim.replaying {
2446 ed.vim.last_change = Some(LastChange::OpMotion {
2447 op: Operator::Change,
2448 motion: Motion::Right,
2449 count: count.max(1),
2450 inserted: None,
2451 });
2452 }
2453}
2454
2455pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
2458 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2459 count: usize,
2460) {
2461 execute_line_op(ed, Operator::Change, count.max(1));
2462 if !ed.vim.replaying {
2463 ed.vim.last_change = Some(LastChange::LineOp {
2464 op: Operator::Change,
2465 count: count.max(1),
2466 inserted: None,
2467 });
2468 }
2469}
2470
2471pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2474 ed.push_undo();
2475 delete_to_eol(ed);
2476 crate::motions::move_left(&mut ed.buffer, 1);
2477 ed.push_buffer_cursor_to_textarea();
2478 if !ed.vim.replaying {
2479 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2480 }
2481}
2482
2483pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2486 ed.push_undo();
2487 delete_to_eol(ed);
2488 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2489}
2490
2491pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
2493 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2494 count: usize,
2495) {
2496 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2497}
2498
2499pub(crate) fn join_line_bridge<H: crate::types::Host>(
2502 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2503 count: usize,
2504) {
2505 let joins = count.max(2) - 1;
2508 for _ in 0..joins {
2509 ed.push_undo();
2510 if !join_line(ed) {
2511 break;
2512 }
2513 }
2514 if !ed.vim.replaying {
2515 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
2516 }
2517}
2518
2519pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
2522 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2523 count: usize,
2524) {
2525 for _ in 0..count.max(1) {
2526 ed.push_undo();
2527 if !toggle_case_at_cursor(ed) {
2528 break;
2529 }
2530 }
2531 if !ed.vim.replaying {
2532 ed.vim.last_change = Some(LastChange::ToggleCase {
2533 count: count.max(1),
2534 });
2535 }
2536}
2537
2538pub(crate) fn paste_after_bridge<H: crate::types::Host>(
2542 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2543 count: usize,
2544) {
2545 paste_bridge(ed, false, count, false, false);
2546}
2547
2548pub(crate) fn paste_before_bridge<H: crate::types::Host>(
2552 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2553 count: usize,
2554) {
2555 paste_bridge(ed, true, count, false, false);
2556}
2557
2558pub(crate) fn paste_bridge<H: crate::types::Host>(
2561 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2562 before: bool,
2563 count: usize,
2564 cursor_after: bool,
2565 reindent: bool,
2566) {
2567 do_paste(ed, before, count.max(1), cursor_after, reindent);
2568 if !ed.vim.replaying {
2569 ed.vim.last_change = Some(LastChange::Paste {
2570 before,
2571 count: count.max(1),
2572 cursor_after,
2573 reindent,
2574 });
2575 }
2576}
2577
2578pub(crate) fn jump_back_bridge<H: crate::types::Host>(
2583 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2584 count: usize,
2585) {
2586 for _ in 0..count.max(1) {
2587 if !jump_back(ed) {
2588 break;
2589 }
2590 }
2591}
2592
2593pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
2596 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2597 count: usize,
2598) {
2599 for _ in 0..count.max(1) {
2600 if !jump_forward(ed) {
2601 break;
2602 }
2603 }
2604}
2605
2606pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
2611 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2612 dir: ScrollDir,
2613 count: usize,
2614) {
2615 ed.vim.scroll_anim_hint = true;
2616 let rows = viewport_full_rows(ed, count) as isize;
2617 match dir {
2618 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2619 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2620 }
2621}
2622
2623pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
2626 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2627 dir: ScrollDir,
2628 count: usize,
2629) {
2630 ed.vim.scroll_anim_hint = true;
2631 let rows = viewport_half_rows(ed, count) as isize;
2632 match dir {
2633 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2634 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2635 }
2636}
2637
2638pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
2642 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2643 dir: ScrollDir,
2644 count: usize,
2645) {
2646 let n = count.max(1);
2647 let total = buf_row_count(&ed.buffer);
2648 let last = total.saturating_sub(1);
2649 let h = ed.viewport_height_value() as usize;
2650 let vp = ed.host().viewport();
2651 let cur_top = vp.top_row;
2652 let new_top = match dir {
2653 ScrollDir::Down => (cur_top + n).min(last),
2654 ScrollDir::Up => cur_top.saturating_sub(n),
2655 };
2656 ed.set_viewport_top(new_top);
2657 let (row, col) = ed.cursor();
2659 let bot = (new_top + h).saturating_sub(1).min(last);
2660 let clamped = row.max(new_top).min(bot);
2661 if clamped != row {
2662 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
2663 ed.push_buffer_cursor_to_textarea();
2664 }
2665}
2666
2667pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
2672 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2673 forward: bool,
2674 count: usize,
2675) {
2676 if let Some(pattern) = ed.vim.last_search.clone() {
2677 ed.push_search_pattern(&pattern);
2678 }
2679 if ed.search_state().pattern.is_none() {
2680 return;
2681 }
2682 let go_forward = ed.vim.last_search_forward == forward;
2683 for _ in 0..count.max(1) {
2684 if go_forward {
2685 ed.search_advance_forward(true);
2686 } else {
2687 ed.search_advance_backward(true);
2688 }
2689 }
2690 ed.push_buffer_cursor_to_textarea();
2691}
2692
2693pub(crate) fn word_search_bridge<H: crate::types::Host>(
2697 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2698 forward: bool,
2699 whole_word: bool,
2700 count: usize,
2701) {
2702 word_at_cursor_search(ed, forward, whole_word, count.max(1));
2703}
2704
2705#[allow(dead_code)]
2710#[inline]
2711pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2712 do_undo(ed);
2713}
2714
2715#[inline]
2732pub fn drop_blame_if_left_normal<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2733 if ed.vim.current_mode != crate::VimMode::Normal {
2734 ed.view = crate::ViewMode::Normal;
2735 }
2736}
2737
2738#[inline]
2742pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
2743 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2744 mode: Mode,
2745) {
2746 ed.vim.mode = mode;
2747 ed.vim.current_mode = ed.vim.public_mode();
2748 drop_blame_if_left_normal(ed);
2749}
2750
2751pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
2754 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2755) {
2756 let cur = ed.cursor();
2757 ed.vim.visual_anchor = cur;
2758 set_vim_mode_bridge(ed, Mode::Visual);
2759}
2760
2761pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
2764 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2765) {
2766 let (row, _) = ed.cursor();
2767 ed.vim.visual_line_anchor = row;
2768 set_vim_mode_bridge(ed, Mode::VisualLine);
2769}
2770
2771pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
2775 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2776) {
2777 let cur = ed.cursor();
2778 ed.vim.block_anchor = cur;
2779 ed.vim.block_vcol = cur.1;
2780 set_vim_mode_bridge(ed, Mode::VisualBlock);
2781}
2782
2783pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
2788 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2789) {
2790 let snap: Option<LastVisual> = match ed.vim.mode {
2792 Mode::Visual => Some(LastVisual {
2793 mode: Mode::Visual,
2794 anchor: ed.vim.visual_anchor,
2795 cursor: ed.cursor(),
2796 block_vcol: 0,
2797 }),
2798 Mode::VisualLine => Some(LastVisual {
2799 mode: Mode::VisualLine,
2800 anchor: (ed.vim.visual_line_anchor, 0),
2801 cursor: ed.cursor(),
2802 block_vcol: 0,
2803 }),
2804 Mode::VisualBlock => Some(LastVisual {
2805 mode: Mode::VisualBlock,
2806 anchor: ed.vim.block_anchor,
2807 cursor: ed.cursor(),
2808 block_vcol: ed.vim.block_vcol,
2809 }),
2810 _ => None,
2811 };
2812 ed.vim.pending = Pending::None;
2814 ed.vim.count = 0;
2815 ed.vim.insert_session = None;
2816 set_vim_mode_bridge(ed, Mode::Normal);
2817 if let Some(snap) = snap {
2821 let (lo, hi) = match snap.mode {
2822 Mode::Visual => {
2823 if snap.anchor <= snap.cursor {
2824 (snap.anchor, snap.cursor)
2825 } else {
2826 (snap.cursor, snap.anchor)
2827 }
2828 }
2829 Mode::VisualLine => {
2830 let r_lo = snap.anchor.0.min(snap.cursor.0);
2831 let r_hi = snap.anchor.0.max(snap.cursor.0);
2832 let vl_rope = ed.buffer().rope();
2833 let r_hi_clamped = r_hi.min(vl_rope.len_lines().saturating_sub(1));
2834 let last_col = hjkl_buffer::rope_line_str(&vl_rope, r_hi_clamped)
2835 .chars()
2836 .count()
2837 .saturating_sub(1);
2838 ((r_lo, 0), (r_hi, last_col))
2839 }
2840 Mode::VisualBlock => {
2841 let (r1, c1) = snap.anchor;
2842 let (r2, c2) = snap.cursor;
2843 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2844 }
2845 _ => {
2846 if snap.anchor <= snap.cursor {
2847 (snap.anchor, snap.cursor)
2848 } else {
2849 (snap.cursor, snap.anchor)
2850 }
2851 }
2852 };
2853 ed.set_mark('<', lo);
2854 ed.set_mark('>', hi);
2855 ed.vim.last_visual = Some(snap);
2856 }
2857}
2858
2859pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2865 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2866) {
2867 match ed.vim.mode {
2868 Mode::Visual => {
2869 let cur = ed.cursor();
2870 let anchor = ed.vim.visual_anchor;
2871 ed.vim.visual_anchor = cur;
2872 ed.jump_cursor(anchor.0, anchor.1);
2873 }
2874 Mode::VisualLine => {
2875 let cur_row = ed.cursor().0;
2876 let anchor_row = ed.vim.visual_line_anchor;
2877 ed.vim.visual_line_anchor = cur_row;
2878 ed.jump_cursor(anchor_row, 0);
2879 }
2880 Mode::VisualBlock => {
2881 let cur = ed.cursor();
2882 let anchor = ed.vim.block_anchor;
2883 ed.vim.block_anchor = cur;
2884 ed.vim.block_vcol = anchor.1;
2885 ed.jump_cursor(anchor.0, anchor.1);
2886 }
2887 _ => {}
2888 }
2889}
2890
2891pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
2895 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2896) {
2897 if let Some(snap) = ed.vim.last_visual {
2898 match snap.mode {
2899 Mode::Visual => {
2900 ed.vim.visual_anchor = snap.anchor;
2901 set_vim_mode_bridge(ed, Mode::Visual);
2902 }
2903 Mode::VisualLine => {
2904 ed.vim.visual_line_anchor = snap.anchor.0;
2905 set_vim_mode_bridge(ed, Mode::VisualLine);
2906 }
2907 Mode::VisualBlock => {
2908 ed.vim.block_anchor = snap.anchor;
2909 ed.vim.block_vcol = snap.block_vcol;
2910 set_vim_mode_bridge(ed, Mode::VisualBlock);
2911 }
2912 _ => {}
2913 }
2914 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2915 }
2916}
2917
2918pub(crate) fn set_mode_bridge<H: crate::types::Host>(
2924 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2925 mode: crate::VimMode,
2926) {
2927 let internal = match mode {
2928 crate::VimMode::Normal => Mode::Normal,
2929 crate::VimMode::Insert => Mode::Insert,
2930 crate::VimMode::Visual => Mode::Visual,
2931 crate::VimMode::VisualLine => Mode::VisualLine,
2932 crate::VimMode::VisualBlock => Mode::VisualBlock,
2933 };
2934 ed.vim.mode = internal;
2935 ed.vim.current_mode = mode;
2936 drop_blame_if_left_normal(ed);
2937}
2938
2939pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2956 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2957 ch: char,
2958) {
2959 if ch.is_ascii_lowercase() {
2960 let pos = ed.cursor();
2961 ed.set_mark(ch, pos);
2962 } else if ch.is_ascii_uppercase() {
2963 let pos = ed.cursor();
2964 let bid = ed.current_buffer_id();
2965 ed.set_global_mark(ch, bid, pos);
2966 tracing::debug!(
2967 mark = ch as u32,
2968 buffer_id = bid,
2969 row = pos.0,
2970 col = pos.1,
2971 "global mark set"
2972 );
2973 }
2974 }
2976
2977pub(crate) fn goto_mark<H: crate::types::Host>(
2986 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2987 ch: char,
2988 linewise: bool,
2989) {
2990 let target = match ch {
2991 'a'..='z' => ed.mark(ch),
2992 '\'' | '`' => ed.vim.jump_back.last().copied(),
2993 '.' => ed.last_edit_pos,
2994 '[' | ']' | '<' | '>' => ed.mark(ch),
2995 _ => None,
2996 };
2997 let Some((row, col)) = target else {
2998 return;
2999 };
3000 let pre = ed.cursor();
3001 let (r, c_clamped) = clamp_pos(ed, (row, col));
3002 if linewise {
3003 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3004 ed.push_buffer_cursor_to_textarea();
3005 move_first_non_whitespace(ed);
3006 } else {
3007 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3008 ed.push_buffer_cursor_to_textarea();
3009 }
3010 if ed.cursor() != pre {
3011 ed.push_jump(pre);
3012 }
3013 ed.sticky_col = Some(ed.cursor().1);
3014}
3015
3016pub(crate) fn try_goto_mark<H: crate::types::Host>(
3025 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3026 ch: char,
3027 linewise: bool,
3028) -> crate::editor::MarkJump {
3029 use crate::editor::MarkJump;
3030 match ch {
3031 'A'..='Z' => {
3032 let Some((bid, row, col)) = ed.global_mark(ch) else {
3033 return MarkJump::Unset;
3034 };
3035 if bid != ed.current_buffer_id() {
3036 tracing::debug!(
3037 mark = ch as u32,
3038 buffer_id = bid,
3039 row,
3040 col,
3041 "global mark cross-buffer jump"
3042 );
3043 return MarkJump::CrossBuffer {
3044 buffer_id: bid,
3045 row,
3046 col,
3047 };
3048 }
3049 let pre = ed.cursor();
3051 let (r, c_clamped) = clamp_pos(ed, (row, col));
3052 if linewise {
3053 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3054 ed.push_buffer_cursor_to_textarea();
3055 move_first_non_whitespace(ed);
3056 } else {
3057 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3058 ed.push_buffer_cursor_to_textarea();
3059 }
3060 if ed.cursor() != pre {
3061 ed.push_jump(pre);
3062 }
3063 ed.sticky_col = Some(ed.cursor().1);
3064 MarkJump::SameBuffer
3065 }
3066 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
3067 goto_mark(ed, ch, linewise);
3068 MarkJump::SameBuffer
3069 }
3070 _ => MarkJump::Unset,
3071 }
3072}
3073
3074pub fn op_is_change(op: Operator) -> bool {
3078 matches!(op, Operator::Delete | Operator::Change)
3079}
3080
3081fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3087 let Some(target) = ed.vim.jump_back.pop() else {
3088 return false;
3089 };
3090 let cur = ed.cursor();
3091 ed.vim.jump_fwd.push(cur);
3092 let (r, c) = clamp_pos(ed, target);
3093 ed.jump_cursor(r, c);
3094 ed.sticky_col = Some(c);
3095 true
3096}
3097
3098fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3102 let Some(target) = ed.vim.jump_fwd.pop() else {
3103 return false;
3104 };
3105 let cur = ed.cursor();
3106 ed.vim.jump_back.push(cur);
3107 if ed.vim.jump_back.len() > JUMPLIST_MAX {
3108 ed.vim.jump_back.remove(0);
3109 }
3110 let (r, c) = clamp_pos(ed, target);
3111 ed.jump_cursor(r, c);
3112 ed.sticky_col = Some(c);
3113 true
3114}
3115
3116fn clamp_pos<H: crate::types::Host>(
3119 ed: &Editor<hjkl_buffer::Buffer, H>,
3120 pos: (usize, usize),
3121) -> (usize, usize) {
3122 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3123 let r = pos.0.min(last_row);
3124 let line_len = buf_line_chars(&ed.buffer, r);
3125 let c = pos.1.min(line_len.saturating_sub(1));
3126 (r, c)
3127}
3128
3129fn is_big_jump(motion: &Motion) -> bool {
3131 matches!(
3132 motion,
3133 Motion::FileTop
3134 | Motion::FileBottom
3135 | Motion::MatchBracket
3136 | Motion::WordAtCursor { .. }
3137 | Motion::SearchNext { .. }
3138 | Motion::ViewportTop
3139 | Motion::ViewportMiddle
3140 | Motion::ViewportBottom
3141 )
3142}
3143
3144fn viewport_half_rows<H: crate::types::Host>(
3149 ed: &Editor<hjkl_buffer::Buffer, H>,
3150 count: usize,
3151) -> usize {
3152 let h = ed.viewport_height_value() as usize;
3153 (h / 2).max(1).saturating_mul(count.max(1))
3154}
3155
3156fn viewport_full_rows<H: crate::types::Host>(
3159 ed: &Editor<hjkl_buffer::Buffer, H>,
3160 count: usize,
3161) -> usize {
3162 let h = ed.viewport_height_value() as usize;
3163 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
3164}
3165
3166fn scroll_cursor_rows<H: crate::types::Host>(
3171 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3172 delta: isize,
3173) {
3174 if delta == 0 {
3175 return;
3176 }
3177 ed.sync_buffer_content_from_textarea();
3178 let (row, _) = ed.cursor();
3179 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3180 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
3181 buf_set_cursor_rc(&mut ed.buffer, target, 0);
3182 crate::motions::move_first_non_blank(&mut ed.buffer);
3183 ed.push_buffer_cursor_to_textarea();
3184 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3185}
3186
3187pub fn parse_motion(input: &Input) -> Option<Motion> {
3193 if input.ctrl {
3194 if input.key == Key::Char('h') {
3198 return Some(Motion::BackspaceBack);
3199 }
3200 return None;
3201 }
3202 match input.key {
3203 Key::Char('h') | Key::Left => Some(Motion::Left),
3204 Key::Char('l') | Key::Right => Some(Motion::Right),
3205 Key::Char(' ') => Some(Motion::SpaceFwd),
3209 Key::Backspace => Some(Motion::BackspaceBack),
3210 Key::Char('j') | Key::Down => Some(Motion::Down),
3211 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
3213 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
3215 Key::Char('_') => Some(Motion::FirstNonBlankLine),
3217 Key::Char('k') | Key::Up => Some(Motion::Up),
3218 Key::Char('w') => Some(Motion::WordFwd),
3219 Key::Char('W') => Some(Motion::BigWordFwd),
3220 Key::Char('b') => Some(Motion::WordBack),
3221 Key::Char('B') => Some(Motion::BigWordBack),
3222 Key::Char('e') => Some(Motion::WordEnd),
3223 Key::Char('E') => Some(Motion::BigWordEnd),
3224 Key::Char('0') | Key::Home => Some(Motion::LineStart),
3225 Key::Char('^') => Some(Motion::FirstNonBlank),
3226 Key::Char('$') | Key::End => Some(Motion::LineEnd),
3227 Key::Char('G') => Some(Motion::FileBottom),
3228 Key::Char('%') => Some(Motion::MatchBracket),
3229 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
3230 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
3231 Key::Char('*') => Some(Motion::WordAtCursor {
3232 forward: true,
3233 whole_word: true,
3234 }),
3235 Key::Char('#') => Some(Motion::WordAtCursor {
3236 forward: false,
3237 whole_word: true,
3238 }),
3239 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
3240 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
3241 Key::Char('H') => Some(Motion::ViewportTop),
3242 Key::Char('M') => Some(Motion::ViewportMiddle),
3243 Key::Char('L') => Some(Motion::ViewportBottom),
3244 Key::Char('{') => Some(Motion::ParagraphPrev),
3245 Key::Char('}') => Some(Motion::ParagraphNext),
3246 Key::Char('(') => Some(Motion::SentencePrev),
3247 Key::Char(')') => Some(Motion::SentenceNext),
3248 Key::Char('|') => Some(Motion::GotoColumn),
3249 _ => None,
3250 }
3251}
3252
3253pub(crate) fn execute_motion<H: crate::types::Host>(
3256 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3257 motion: Motion,
3258 count: usize,
3259) {
3260 let count = count.clamp(1, MAX_COUNT);
3261 if let Motion::FindRepeat { reverse } = motion
3264 && ed.vim.last_horizontal_motion == LastHorizontalMotion::Sneak
3265 {
3266 if let Some(((c1, c2), fwd)) = ed.vim.last_sneak {
3267 let effective_fwd = if reverse { !fwd } else { fwd };
3268 apply_sneak(ed, c1, c2, effective_fwd, count);
3269 }
3270 return;
3271 }
3272 let motion = match motion {
3276 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3277 Some((ch, forward, till)) => {
3278 ed.vim.find_repeat_skip = true;
3279 Motion::Find {
3280 ch,
3281 forward: if reverse { !forward } else { forward },
3282 till,
3283 }
3284 }
3285 None => return,
3286 },
3287 other => other,
3288 };
3289 let pre_pos = ed.cursor();
3290 let pre_col = pre_pos.1;
3291 apply_motion_cursor(ed, &motion, count);
3292 let post_pos = ed.cursor();
3293 if is_big_jump(&motion) && pre_pos != post_pos {
3294 ed.push_jump(pre_pos);
3295 }
3296 apply_sticky_col(ed, &motion, pre_col);
3297 ed.sync_buffer_from_textarea();
3302}
3303
3304fn execute_motion_with_block_vcol<H: crate::types::Host>(
3315 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3316 motion: Motion,
3317 count: usize,
3318) {
3319 let motion_copy = motion.clone();
3320 execute_motion(ed, motion, count);
3321 if ed.vim.mode == Mode::VisualBlock {
3322 update_block_vcol(ed, &motion_copy);
3323 }
3324}
3325
3326pub(crate) fn apply_motion_kind<H: crate::types::Host>(
3354 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3355 kind: crate::MotionKind,
3356 count: usize,
3357) {
3358 let count = count.max(1);
3359 match kind {
3360 crate::MotionKind::CharLeft => {
3361 execute_motion_with_block_vcol(ed, Motion::Left, count);
3362 }
3363 crate::MotionKind::CharRight => {
3364 execute_motion_with_block_vcol(ed, Motion::Right, count);
3365 }
3366 crate::MotionKind::LineDown => {
3367 execute_motion_with_block_vcol(ed, Motion::Down, count);
3368 }
3369 crate::MotionKind::LineUp => {
3370 execute_motion_with_block_vcol(ed, Motion::Up, count);
3371 }
3372 crate::MotionKind::FirstNonBlankDown => {
3373 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3378 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3379 crate::motions::move_first_non_blank(&mut ed.buffer);
3380 ed.push_buffer_cursor_to_textarea();
3381 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3382 ed.sync_buffer_from_textarea();
3383 }
3384 crate::MotionKind::FirstNonBlankUp => {
3385 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3388 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3389 crate::motions::move_first_non_blank(&mut ed.buffer);
3390 ed.push_buffer_cursor_to_textarea();
3391 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3392 ed.sync_buffer_from_textarea();
3393 }
3394 crate::MotionKind::WordForward => {
3395 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
3396 }
3397 crate::MotionKind::BigWordForward => {
3398 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
3399 }
3400 crate::MotionKind::WordBackward => {
3401 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
3402 }
3403 crate::MotionKind::BigWordBackward => {
3404 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
3405 }
3406 crate::MotionKind::WordEnd => {
3407 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
3408 }
3409 crate::MotionKind::BigWordEnd => {
3410 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
3411 }
3412 crate::MotionKind::LineStart => {
3413 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
3416 }
3417 crate::MotionKind::FirstNonBlank => {
3418 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
3421 }
3422 crate::MotionKind::GotoLine => {
3423 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
3432 }
3433 crate::MotionKind::LineEnd => {
3434 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
3438 }
3439 crate::MotionKind::FindRepeat => {
3440 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
3444 }
3445 crate::MotionKind::FindRepeatReverse => {
3446 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
3450 }
3451 crate::MotionKind::BracketMatch => {
3452 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
3457 }
3458 crate::MotionKind::ViewportTop => {
3459 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
3462 }
3463 crate::MotionKind::ViewportMiddle => {
3464 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
3467 }
3468 crate::MotionKind::ViewportBottom => {
3469 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
3472 }
3473 crate::MotionKind::HalfPageDown => {
3474 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
3478 }
3479 crate::MotionKind::HalfPageUp => {
3480 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
3483 }
3484 crate::MotionKind::FullPageDown => {
3485 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
3488 }
3489 crate::MotionKind::FullPageUp => {
3490 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
3493 }
3494 crate::MotionKind::FirstNonBlankLine => {
3495 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
3496 }
3497 crate::MotionKind::SectionBackward => {
3498 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
3499 }
3500 crate::MotionKind::SectionForward => {
3501 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
3502 }
3503 crate::MotionKind::SectionEndBackward => {
3504 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
3505 }
3506 crate::MotionKind::SectionEndForward => {
3507 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
3508 }
3509 }
3510}
3511
3512fn apply_sticky_col<H: crate::types::Host>(
3517 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3518 motion: &Motion,
3519 pre_col: usize,
3520) {
3521 if is_vertical_motion(motion) {
3522 let want = ed.sticky_col.unwrap_or(pre_col);
3523 ed.sticky_col = Some(want);
3526 let (row, _) = ed.cursor();
3527 let line_len = buf_line_chars(&ed.buffer, row);
3528 let max_col = line_len.saturating_sub(1);
3532 let target = want.min(max_col);
3533 buf_set_cursor_rc(&mut ed.buffer, row, target);
3537 } else {
3538 ed.sticky_col = Some(ed.cursor().1);
3541 }
3542}
3543
3544fn is_vertical_motion(motion: &Motion) -> bool {
3545 matches!(
3549 motion,
3550 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
3551 )
3552}
3553
3554fn apply_motion_cursor<H: crate::types::Host>(
3555 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3556 motion: &Motion,
3557 count: usize,
3558) {
3559 apply_motion_cursor_ctx(ed, motion, count, false)
3560}
3561
3562pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
3563 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3564 motion: &Motion,
3565 count: usize,
3566 as_operator: bool,
3567) {
3568 let count = count
3577 .min(MAX_COUNT)
3578 .min(ed.buffer.rope().len_chars().saturating_add(1));
3579 match motion {
3580 Motion::Left => {
3581 crate::motions::move_left(&mut ed.buffer, count);
3583 ed.push_buffer_cursor_to_textarea();
3584 }
3585 Motion::Right => {
3586 if as_operator {
3590 crate::motions::move_right_to_end(&mut ed.buffer, count);
3591 } else {
3592 crate::motions::move_right_in_line(&mut ed.buffer, count);
3593 }
3594 ed.push_buffer_cursor_to_textarea();
3595 }
3596 Motion::SpaceFwd => {
3597 if as_operator {
3600 crate::motions::move_right_to_end(&mut ed.buffer, count);
3601 } else {
3602 crate::motions::move_space_fwd(&mut ed.buffer, count);
3603 }
3604 ed.push_buffer_cursor_to_textarea();
3605 }
3606 Motion::BackspaceBack => {
3607 if as_operator {
3610 crate::motions::move_left(&mut ed.buffer, count);
3611 } else {
3612 crate::motions::move_backspace_back(&mut ed.buffer, count);
3613 }
3614 ed.push_buffer_cursor_to_textarea();
3615 }
3616 Motion::Up => {
3617 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3621 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3622 ed.push_buffer_cursor_to_textarea();
3623 }
3624 Motion::Down => {
3625 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3626 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3627 ed.push_buffer_cursor_to_textarea();
3628 }
3629 Motion::ScreenUp => {
3630 let v = *ed.host.viewport();
3631 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3632 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3633 ed.push_buffer_cursor_to_textarea();
3634 }
3635 Motion::ScreenDown => {
3636 let v = *ed.host.viewport();
3637 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3638 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3639 ed.push_buffer_cursor_to_textarea();
3640 }
3641 Motion::WordFwd => {
3642 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3643 ed.push_buffer_cursor_to_textarea();
3644 }
3645 Motion::WordBack => {
3646 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3647 ed.push_buffer_cursor_to_textarea();
3648 }
3649 Motion::WordEnd => {
3650 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3651 ed.push_buffer_cursor_to_textarea();
3652 }
3653 Motion::BigWordFwd => {
3654 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3655 ed.push_buffer_cursor_to_textarea();
3656 }
3657 Motion::BigWordBack => {
3658 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3659 ed.push_buffer_cursor_to_textarea();
3660 }
3661 Motion::BigWordEnd => {
3662 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3663 ed.push_buffer_cursor_to_textarea();
3664 }
3665 Motion::WordEndBack => {
3666 crate::motions::move_word_end_back(
3667 &mut ed.buffer,
3668 false,
3669 count,
3670 &ed.settings.iskeyword,
3671 );
3672 ed.push_buffer_cursor_to_textarea();
3673 }
3674 Motion::BigWordEndBack => {
3675 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3676 ed.push_buffer_cursor_to_textarea();
3677 }
3678 Motion::LineStart => {
3679 crate::motions::move_line_start(&mut ed.buffer);
3680 ed.push_buffer_cursor_to_textarea();
3681 }
3682 Motion::FirstNonBlank => {
3683 crate::motions::move_first_non_blank(&mut ed.buffer);
3684 ed.push_buffer_cursor_to_textarea();
3685 }
3686 Motion::LineEnd => {
3687 crate::motions::move_line_end(&mut ed.buffer);
3689 ed.push_buffer_cursor_to_textarea();
3690 }
3691 Motion::FileTop => {
3692 if count > 1 {
3695 crate::motions::move_bottom(&mut ed.buffer, count);
3696 } else {
3697 crate::motions::move_top(&mut ed.buffer);
3698 }
3699 ed.push_buffer_cursor_to_textarea();
3700 }
3701 Motion::FileBottom => {
3702 if count > 1 {
3705 crate::motions::move_bottom(&mut ed.buffer, count);
3706 } else {
3707 crate::motions::move_bottom(&mut ed.buffer, 0);
3708 }
3709 ed.push_buffer_cursor_to_textarea();
3710 }
3711 Motion::Find { ch, forward, till } => {
3712 let repeat = std::mem::take(&mut ed.vim.find_repeat_skip);
3716 for i in 0..count {
3717 let skip_adjacent = repeat || i > 0;
3718 if !find_char_on_line(ed, *ch, *forward, *till, skip_adjacent) {
3719 break;
3720 }
3721 }
3722 }
3723 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
3725 let _ = matching_bracket(ed);
3726 }
3727 Motion::UnmatchedBracket { forward, open } => {
3728 goto_unmatched_bracket(ed, *forward, *open, count);
3729 }
3730 Motion::WordAtCursor {
3731 forward,
3732 whole_word,
3733 } => {
3734 word_at_cursor_search(ed, *forward, *whole_word, count);
3735 }
3736 Motion::SearchNext { reverse } => {
3737 if let Some(pattern) = ed.vim.last_search.clone() {
3741 ed.push_search_pattern(&pattern);
3742 }
3743 if ed.search_state().pattern.is_none() {
3744 return;
3745 }
3746 let forward = ed.vim.last_search_forward != *reverse;
3750 for _ in 0..count.max(1) {
3751 if forward {
3752 ed.search_advance_forward(true);
3753 } else {
3754 ed.search_advance_backward(true);
3755 }
3756 }
3757 ed.push_buffer_cursor_to_textarea();
3758 }
3759 Motion::ViewportTop => {
3760 let v = *ed.host().viewport();
3761 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
3762 ed.push_buffer_cursor_to_textarea();
3763 }
3764 Motion::ViewportMiddle => {
3765 let v = *ed.host().viewport();
3766 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
3767 ed.push_buffer_cursor_to_textarea();
3768 }
3769 Motion::ViewportBottom => {
3770 let v = *ed.host().viewport();
3771 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
3772 ed.push_buffer_cursor_to_textarea();
3773 }
3774 Motion::LastNonBlank => {
3775 crate::motions::move_last_non_blank(&mut ed.buffer);
3776 ed.push_buffer_cursor_to_textarea();
3777 }
3778 Motion::LineMiddle => {
3779 let row = ed.cursor().0;
3780 let line_chars = buf_line_chars(&ed.buffer, row);
3781 let target = line_chars / 2;
3784 ed.jump_cursor(row, target);
3785 }
3786 Motion::ScreenLineMiddle => {
3787 let row = ed.cursor().0;
3790 let width = ed.host().viewport().width as usize;
3791 let last = buf_line_chars(&ed.buffer, row).saturating_sub(1);
3792 let target = (width / 2).min(last);
3793 ed.jump_cursor(row, target);
3794 }
3795 Motion::ParagraphPrev => {
3796 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
3797 ed.push_buffer_cursor_to_textarea();
3798 }
3799 Motion::ParagraphNext => {
3800 crate::motions::move_paragraph_next(&mut ed.buffer, count);
3801 ed.push_buffer_cursor_to_textarea();
3802 }
3803 Motion::SentencePrev => {
3804 for _ in 0..count.max(1) {
3805 if let Some((row, col)) = sentence_boundary(ed, false) {
3806 ed.jump_cursor(row, col);
3807 }
3808 }
3809 }
3810 Motion::SentenceNext => {
3811 for _ in 0..count.max(1) {
3812 if let Some((row, col)) = sentence_boundary(ed, true) {
3813 ed.jump_cursor(row, col);
3814 }
3815 }
3816 }
3817 Motion::SectionBackward => {
3818 crate::motions::move_section_backward(&mut ed.buffer, count);
3819 ed.push_buffer_cursor_to_textarea();
3820 }
3821 Motion::SectionForward => {
3822 crate::motions::move_section_forward(&mut ed.buffer, count);
3823 ed.push_buffer_cursor_to_textarea();
3824 }
3825 Motion::SectionEndBackward => {
3826 crate::motions::move_section_end_backward(&mut ed.buffer, count);
3827 ed.push_buffer_cursor_to_textarea();
3828 }
3829 Motion::SectionEndForward => {
3830 crate::motions::move_section_end_forward(&mut ed.buffer, count);
3831 ed.push_buffer_cursor_to_textarea();
3832 }
3833 Motion::FirstNonBlankNextLine => {
3834 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
3835 ed.push_buffer_cursor_to_textarea();
3836 }
3837 Motion::FirstNonBlankPrevLine => {
3838 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
3839 ed.push_buffer_cursor_to_textarea();
3840 }
3841 Motion::FirstNonBlankLine => {
3842 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
3843 ed.push_buffer_cursor_to_textarea();
3844 }
3845 Motion::GotoColumn => {
3846 crate::motions::move_goto_column(&mut ed.buffer, count);
3847 ed.push_buffer_cursor_to_textarea();
3848 }
3849 }
3850}
3851
3852fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3853 ed.sync_buffer_content_from_textarea();
3859 crate::motions::move_first_non_blank(&mut ed.buffer);
3860 ed.push_buffer_cursor_to_textarea();
3861}
3862
3863fn find_char_on_line<H: crate::types::Host>(
3864 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3865 ch: char,
3866 forward: bool,
3867 till: bool,
3868 skip_adjacent: bool,
3869) -> bool {
3870 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till, skip_adjacent);
3871 if moved {
3872 ed.push_buffer_cursor_to_textarea();
3873 }
3874 moved
3875}
3876
3877fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3878 let moved = crate::motions::match_bracket(&mut ed.buffer);
3879 if moved {
3880 ed.push_buffer_cursor_to_textarea();
3881 }
3882 moved
3883}
3884
3885fn goto_unmatched_bracket<H: crate::types::Host>(
3889 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3890 forward: bool,
3891 open: char,
3892 count: usize,
3893) {
3894 let close = match open {
3895 '(' => ')',
3896 '{' => '}',
3897 _ => return,
3898 };
3899 let cursor = buf_cursor_pos(&ed.buffer);
3900 let rows = buf_row_count(&ed.buffer);
3901 let target = count.max(1);
3902 let mut found = 0usize;
3903 let mut depth = 0i32;
3904
3905 if forward {
3906 let mut r = cursor.row;
3907 let mut from_col = cursor.col + 1;
3908 while r < rows {
3909 let line: Vec<char> = buf_line(&ed.buffer, r)
3910 .unwrap_or_default()
3911 .chars()
3912 .collect();
3913 let mut ci = from_col;
3914 while ci < line.len() {
3915 let ch = line[ci];
3916 if ch == open {
3917 depth += 1;
3918 } else if ch == close {
3919 if depth == 0 {
3920 found += 1;
3921 if found == target {
3922 buf_set_cursor_rc(&mut ed.buffer, r, ci);
3923 ed.push_buffer_cursor_to_textarea();
3924 return;
3925 }
3926 } else {
3927 depth -= 1;
3928 }
3929 }
3930 ci += 1;
3931 }
3932 r += 1;
3933 from_col = 0;
3934 }
3935 } else {
3936 let mut r = cursor.row as isize;
3937 let mut from_col = cursor.col as isize - 1;
3940 while r >= 0 {
3941 let line: Vec<char> = buf_line(&ed.buffer, r as usize)
3942 .unwrap_or_default()
3943 .chars()
3944 .collect();
3945 let mut ci = from_col.min(line.len() as isize - 1);
3946 while ci >= 0 {
3947 let ch = line[ci as usize];
3948 if ch == close {
3949 depth += 1;
3950 } else if ch == open {
3951 if depth == 0 {
3952 found += 1;
3953 if found == target {
3954 buf_set_cursor_rc(&mut ed.buffer, r as usize, ci as usize);
3955 ed.push_buffer_cursor_to_textarea();
3956 return;
3957 }
3958 } else {
3959 depth -= 1;
3960 }
3961 }
3962 ci -= 1;
3963 }
3964 r -= 1;
3965 from_col = isize::MAX;
3966 }
3967 }
3968}
3969
3970fn word_at_cursor_search<H: crate::types::Host>(
3971 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3972 forward: bool,
3973 whole_word: bool,
3974 count: usize,
3975) {
3976 let (row, col) = ed.cursor();
3977 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
3978 let chars: Vec<char> = line.chars().collect();
3979 if chars.is_empty() {
3980 return;
3981 }
3982 let spec = ed.settings().iskeyword.clone();
3984 let is_word = |c: char| is_keyword_char(c, &spec);
3985 let mut start = col.min(chars.len().saturating_sub(1));
3986 while start > 0 && is_word(chars[start - 1]) {
3987 start -= 1;
3988 }
3989 let mut end = start;
3990 while end < chars.len() && is_word(chars[end]) {
3991 end += 1;
3992 }
3993 if end <= start {
3994 return;
3995 }
3996 let word: String = chars[start..end].iter().collect();
3997 let escaped = regex_escape(&word);
3998 let pattern = if whole_word {
3999 format!(r"\b{escaped}\b")
4000 } else {
4001 escaped
4002 };
4003 ed.push_search_pattern(&pattern);
4004 if ed.search_state().pattern.is_none() {
4005 return;
4006 }
4007 ed.vim.last_search = Some(pattern);
4009 ed.vim.last_search_forward = forward;
4010 for _ in 0..count.max(1) {
4011 if forward {
4012 ed.search_advance_forward(true);
4013 } else {
4014 ed.search_advance_backward(true);
4015 }
4016 }
4017 ed.push_buffer_cursor_to_textarea();
4018}
4019
4020fn regex_escape(s: &str) -> String {
4021 let mut out = String::with_capacity(s.len());
4022 for c in s.chars() {
4023 if matches!(
4024 c,
4025 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
4026 ) {
4027 out.push('\\');
4028 }
4029 out.push(c);
4030 }
4031 out
4032}
4033
4034pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
4048 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4049 op: Operator,
4050 motion_key: char,
4051 total_count: usize,
4052) {
4053 let input = Input {
4054 key: Key::Char(motion_key),
4055 ctrl: false,
4056 alt: false,
4057 shift: false,
4058 };
4059 let Some(motion) = parse_motion(&input) else {
4060 return;
4061 };
4062 let cursor_on_nonblank = {
4066 let (r, c) = ed.cursor();
4067 buf_line(&ed.buffer, r)
4068 .and_then(|l| l.chars().nth(c))
4069 .map(|ch| !ch.is_whitespace())
4070 .unwrap_or(false)
4071 };
4072 let motion = match motion {
4073 Motion::FindRepeat { reverse } => match ed.vim.last_find {
4074 Some((ch, forward, till)) => Motion::Find {
4075 ch,
4076 forward: if reverse { !forward } else { forward },
4077 till,
4078 },
4079 None => return,
4080 },
4081 Motion::WordFwd if op == Operator::Change && cursor_on_nonblank => Motion::WordEnd,
4082 Motion::BigWordFwd if op == Operator::Change && cursor_on_nonblank => Motion::BigWordEnd,
4083 m => m,
4084 };
4085 apply_op_with_motion(ed, op, &motion, total_count);
4086 if let Motion::Find { ch, forward, till } = &motion {
4087 ed.vim.last_find = Some((*ch, *forward, *till));
4088 }
4089 if !ed.vim.replaying && op_is_change(op) {
4090 ed.vim.last_change = Some(LastChange::OpMotion {
4091 op,
4092 motion,
4093 count: total_count,
4094 inserted: None,
4095 });
4096 }
4097}
4098
4099pub(crate) fn apply_op_double<H: crate::types::Host>(
4102 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4103 op: Operator,
4104 total_count: usize,
4105) {
4106 if op == Operator::Comment {
4107 let row = buf_cursor_pos(&ed.buffer).row;
4109 let end_row = (row + total_count.max(1) - 1).min(ed.buffer.row_count().saturating_sub(1));
4110 ed.toggle_comment_range(row, end_row);
4111 ed.vim.mode = Mode::Normal;
4112 if !ed.vim.replaying {
4113 ed.vim.last_change = Some(LastChange::LineOp {
4114 op,
4115 count: total_count,
4116 inserted: None,
4117 });
4118 }
4119 return;
4120 }
4121 execute_line_op(ed, op, total_count);
4122 if !ed.vim.replaying {
4123 ed.vim.last_change = Some(LastChange::LineOp {
4124 op,
4125 count: total_count,
4126 inserted: None,
4127 });
4128 }
4129}
4130
4131fn gn_find_range<H: crate::types::Host>(
4136 ed: &Editor<hjkl_buffer::Buffer, H>,
4137 re: ®ex::Regex,
4138 forward: bool,
4139) -> Option<(crate::types::Pos, crate::types::Pos)> {
4140 use crate::types::{Cursor, Pos, Search};
4141 let cursor = Cursor::cursor(&ed.buffer);
4142 let contains =
4143 Search::find_prev(&ed.buffer, cursor, re).filter(|m| m.start <= cursor && cursor < m.end);
4144 let range = if let Some(m) = contains {
4145 m
4146 } else if forward {
4147 Search::find_next(&ed.buffer, cursor, re)?
4148 } else {
4149 Search::find_prev(&ed.buffer, cursor, re)?
4150 };
4151 let end_incl = if range.end.col > 0 {
4152 Pos::new(range.end.line, range.end.col - 1)
4153 } else {
4154 range.end
4155 };
4156 Some((range.start, end_incl))
4157}
4158
4159pub(crate) fn gn_operate<H: crate::types::Host>(
4164 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4165 op: Option<Operator>,
4166 forward: bool,
4167 count: usize,
4168) {
4169 use crate::types::{Cursor, Pos};
4170 if let Some(p) = ed.vim.last_search.clone() {
4172 ed.push_search_pattern(&p);
4173 }
4174 let Some(re) = ed.search_state().pattern.clone() else {
4175 return;
4176 };
4177 ed.sync_buffer_content_from_textarea();
4178
4179 let Some(mut range) = gn_find_range(ed, &re, forward) else {
4180 return;
4181 };
4182 for _ in 1..count.max(1) {
4184 let past = Pos::new(range.1.line, range.1.col + 1);
4185 Cursor::set_cursor(&mut ed.buffer, past);
4186 match gn_find_range(ed, &re, forward) {
4187 Some(r) => range = r,
4188 None => break,
4189 }
4190 }
4191 let start_t = (range.0.line as usize, range.0.col as usize);
4192 let end_t = (range.1.line as usize, range.1.col as usize);
4193
4194 match op {
4195 None => {
4196 ed.vim.visual_anchor = start_t;
4198 buf_set_cursor_rc(&mut ed.buffer, end_t.0, end_t.1);
4199 ed.vim.mode = Mode::Visual;
4200 ed.vim.current_mode = crate::VimMode::Visual;
4201 ed.push_buffer_cursor_to_textarea();
4202 }
4203 Some(Operator::Delete) => {
4204 ed.push_undo();
4205 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4206 clamp_cursor_to_normal_mode(ed);
4209 ed.push_buffer_cursor_to_textarea();
4210 if !ed.vim.replaying {
4211 ed.vim.last_change = Some(LastChange::GnOp {
4212 op: Operator::Delete,
4213 forward,
4214 inserted: None,
4215 });
4216 }
4217 }
4218 Some(Operator::Change) => {
4219 ed.push_undo();
4220 ed.vim.change_mark_start = Some(start_t);
4221 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4222 if !ed.vim.replaying {
4223 ed.vim.last_change = Some(LastChange::GnOp {
4224 op: Operator::Change,
4225 forward,
4226 inserted: None,
4227 });
4228 }
4229 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4230 }
4231 Some(Operator::Yank) => {
4232 let text = read_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4233 if !text.is_empty() {
4234 ed.record_yank_to_host(text.clone());
4235 ed.record_yank(text, false);
4236 }
4237 buf_set_cursor_rc(&mut ed.buffer, start_t.0, start_t.1);
4238 ed.push_buffer_cursor_to_textarea();
4239 }
4240 Some(other @ (Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase)) => {
4241 ed.push_undo();
4244 apply_case_op_to_selection(ed, other, start_t, end_t, RangeKind::Inclusive);
4245 }
4246 Some(_) => {}
4247 }
4248}
4249
4250pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
4261 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4262 op: Operator,
4263 ch: char,
4264 total_count: usize,
4265) {
4266 if matches!(
4269 op,
4270 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13
4271 ) {
4272 let op_char = match op {
4273 Operator::Uppercase => 'U',
4274 Operator::Lowercase => 'u',
4275 Operator::ToggleCase => '~',
4276 Operator::Rot13 => '?',
4277 _ => unreachable!(),
4278 };
4279 if ch == op_char {
4280 execute_line_op(ed, op, total_count);
4281 if !ed.vim.replaying {
4282 ed.vim.last_change = Some(LastChange::LineOp {
4283 op,
4284 count: total_count,
4285 inserted: None,
4286 });
4287 }
4288 return;
4289 }
4290 }
4291 if ch == 'n' || ch == 'N' {
4293 gn_operate(ed, Some(op), ch == 'n', total_count);
4294 return;
4295 }
4296 let motion = match ch {
4297 'g' => Motion::FileTop,
4298 'e' => Motion::WordEndBack,
4299 'E' => Motion::BigWordEndBack,
4300 'j' => Motion::ScreenDown,
4301 'k' => Motion::ScreenUp,
4302 _ => return, };
4304 apply_op_with_motion(ed, op, &motion, total_count);
4305 if !ed.vim.replaying && op_is_change(op) {
4306 ed.vim.last_change = Some(LastChange::OpMotion {
4307 op,
4308 motion,
4309 count: total_count,
4310 inserted: None,
4311 });
4312 }
4313}
4314
4315pub(crate) fn apply_after_g<H: crate::types::Host>(
4320 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4321 ch: char,
4322 count: usize,
4323) {
4324 match ch {
4325 'g' => {
4326 let pre = ed.cursor();
4328 if count > 1 {
4329 ed.jump_cursor(count - 1, 0);
4330 } else {
4331 ed.jump_cursor(0, 0);
4332 }
4333 move_first_non_whitespace(ed);
4334 ed.sticky_col = Some(ed.cursor().1);
4337 if ed.cursor() != pre {
4338 ed.push_jump(pre);
4339 }
4340 }
4341 'e' => execute_motion(ed, Motion::WordEndBack, count),
4342 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
4343 '_' => execute_motion(ed, Motion::LastNonBlank, count),
4345 'M' => execute_motion(ed, Motion::LineMiddle, count),
4347 'm' => execute_motion(ed, Motion::ScreenLineMiddle, count),
4349 'v' => ed.reenter_last_visual(),
4352 'j' => execute_motion(ed, Motion::ScreenDown, count),
4356 'k' => execute_motion(ed, Motion::ScreenUp, count),
4357 'U' => {
4361 ed.vim.pending = Pending::Op {
4362 op: Operator::Uppercase,
4363 count1: count,
4364 };
4365 }
4366 'u' => {
4367 ed.vim.pending = Pending::Op {
4368 op: Operator::Lowercase,
4369 count1: count,
4370 };
4371 }
4372 '~' => {
4373 ed.vim.pending = Pending::Op {
4374 op: Operator::ToggleCase,
4375 count1: count,
4376 };
4377 }
4378 '?' => {
4379 ed.vim.pending = Pending::Op {
4381 op: Operator::Rot13,
4382 count1: count,
4383 };
4384 }
4385 'q' => {
4386 ed.vim.pending = Pending::Op {
4389 op: Operator::Reflow,
4390 count1: count,
4391 };
4392 }
4393 'w' => {
4394 ed.vim.pending = Pending::Op {
4397 op: Operator::ReflowKeepCursor,
4398 count1: count,
4399 };
4400 }
4401 'J' => {
4402 let joins = count.max(2) - 1;
4405 for _ in 0..joins {
4406 ed.push_undo();
4407 if !join_line_raw(ed) {
4408 break;
4409 }
4410 }
4411 if !ed.vim.replaying {
4412 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
4413 }
4414 }
4415 'd' => {
4416 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
4421 }
4422 'i' => {
4427 if let Some((row, col)) = ed.vim.last_insert_pos {
4428 ed.jump_cursor(row, col);
4429 }
4430 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4431 }
4432 'c' => {
4437 ed.vim.pending = Pending::Op {
4438 op: Operator::Comment,
4439 count1: count,
4440 };
4441 }
4442 'p' => paste_bridge(ed, false, count.max(1), true, false),
4445 'P' => paste_bridge(ed, true, count.max(1), true, false),
4446 'n' => gn_operate(ed, None, true, count.max(1)),
4448 'N' => gn_operate(ed, None, false, count.max(1)),
4449 ';' => walk_change_list(ed, -1, count.max(1)),
4452 ',' => walk_change_list(ed, 1, count.max(1)),
4453 '*' => execute_motion(
4457 ed,
4458 Motion::WordAtCursor {
4459 forward: true,
4460 whole_word: false,
4461 },
4462 count,
4463 ),
4464 '#' => execute_motion(
4465 ed,
4466 Motion::WordAtCursor {
4467 forward: false,
4468 whole_word: false,
4469 },
4470 count,
4471 ),
4472 '&' => {
4475 let cmd = match ed.vim.last_substitute.clone() {
4476 Some(c) => c,
4477 None => {
4478 return;
4483 }
4484 };
4485 let last_row = buf_row_count(&ed.buffer).saturating_sub(1) as u32;
4486 let r = 0u32..=last_row;
4487 let _ = crate::substitute::apply_substitute(ed, &cmd, r);
4490 ed.vim.last_substitute = Some(cmd);
4493 }
4494 _ => {}
4495 }
4496}
4497
4498pub(crate) fn ampersand_repeat<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4502 let Some(mut cmd) = ed.vim.last_substitute.clone() else {
4503 return;
4504 };
4505 cmd.flags = crate::substitute::SubstFlags::default();
4506 let row = buf_cursor_pos(&ed.buffer).row as u32;
4507 let _ = crate::substitute::apply_substitute(ed, &cmd, row..=row);
4508}
4509
4510pub(crate) fn apply_after_z<H: crate::types::Host>(
4515 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4516 ch: char,
4517 count: usize,
4518) {
4519 use crate::editor::CursorScrollTarget;
4520 let row = ed.cursor().0;
4521 match ch {
4522 'z' => {
4523 ed.scroll_cursor_to(CursorScrollTarget::Center);
4524 ed.vim.viewport_pinned = true;
4525 ed.vim.scroll_anim_hint = true;
4526 }
4527 't' => {
4528 ed.scroll_cursor_to(CursorScrollTarget::Top);
4529 ed.vim.viewport_pinned = true;
4530 ed.vim.scroll_anim_hint = true;
4531 }
4532 'b' => {
4533 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
4534 ed.vim.viewport_pinned = true;
4535 ed.vim.scroll_anim_hint = true;
4536 }
4537 'o' => {
4542 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
4543 }
4544 'c' => {
4545 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
4546 }
4547 'a' => {
4548 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
4549 }
4550 'R' => {
4551 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
4552 }
4553 'M' => {
4554 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
4555 }
4556 'E' => {
4557 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
4558 }
4559 'd' => {
4560 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
4561 }
4562 'f' => {
4563 if matches!(
4564 ed.vim.mode,
4565 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
4566 ) {
4567 let anchor_row = match ed.vim.mode {
4570 Mode::VisualLine => ed.vim.visual_line_anchor,
4571 Mode::VisualBlock => ed.vim.block_anchor.0,
4572 _ => ed.vim.visual_anchor.0,
4573 };
4574 let cur = ed.cursor().0;
4575 let top = anchor_row.min(cur);
4576 let bot = anchor_row.max(cur);
4577 ed.apply_fold_op(crate::types::FoldOp::Add {
4578 start_row: top,
4579 end_row: bot,
4580 closed: true,
4581 });
4582 ed.vim.mode = Mode::Normal;
4583 } else {
4584 ed.vim.pending = Pending::Op {
4589 op: Operator::Fold,
4590 count1: count,
4591 };
4592 }
4593 }
4594 _ => {}
4595 }
4596}
4597
4598pub(crate) fn apply_find_char<H: crate::types::Host>(
4604 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4605 ch: char,
4606 forward: bool,
4607 till: bool,
4608 count: usize,
4609) {
4610 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
4611 ed.vim.last_find = Some((ch, forward, till));
4612 ed.vim.last_horizontal_motion = LastHorizontalMotion::FindChar;
4613}
4614
4615pub(crate) fn apply_sneak<H: crate::types::Host>(
4627 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4628 c1: char,
4629 c2: char,
4630 forward: bool,
4631 count: usize,
4632) {
4633 let count = count.max(1);
4634 let (start_row, start_col) = ed.cursor();
4635 let row_count = buf_row_count(&ed.buffer);
4636
4637 let result = if forward {
4638 sneak_scan_forward(ed, start_row, start_col, c1, c2, count)
4639 } else {
4640 sneak_scan_backward(ed, start_row, start_col, c1, c2, count)
4641 };
4642
4643 if let Some((row, col)) = result {
4644 buf_set_cursor_rc(&mut ed.buffer, row, col);
4645 ed.push_buffer_cursor_to_textarea();
4646 let _ = row_count; }
4648
4649 ed.vim.last_sneak = Some(((c1, c2), forward));
4650 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4651}
4652
4653fn sneak_scan_forward<H: crate::types::Host>(
4656 ed: &Editor<hjkl_buffer::Buffer, H>,
4657 start_row: usize,
4658 start_col: usize,
4659 c1: char,
4660 c2: char,
4661 count: usize,
4662) -> Option<(usize, usize)> {
4663 let row_count = buf_row_count(&ed.buffer);
4664 let mut hits = 0usize;
4665 for row in start_row..row_count {
4666 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4667 let chars: Vec<char> = line.chars().collect();
4668 let col_start = if row == start_row { start_col + 1 } else { 0 };
4670 if col_start + 1 > chars.len() {
4671 continue;
4672 }
4673 for col in col_start..chars.len().saturating_sub(1) {
4674 if chars[col] == c1 && chars[col + 1] == c2 {
4675 hits += 1;
4676 if hits == count {
4677 return Some((row, col));
4678 }
4679 }
4680 }
4681 }
4682 None
4683}
4684
4685fn sneak_scan_backward<H: crate::types::Host>(
4688 ed: &Editor<hjkl_buffer::Buffer, H>,
4689 start_row: usize,
4690 start_col: usize,
4691 c1: char,
4692 c2: char,
4693 count: usize,
4694) -> Option<(usize, usize)> {
4695 let row_count = buf_row_count(&ed.buffer);
4696 let mut hits = 0usize;
4697 let rows_to_scan = (0..row_count).rev().skip(row_count - start_row - 1);
4699 for row in rows_to_scan {
4700 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4701 let chars: Vec<char> = line.chars().collect();
4702 let col_end = if row == start_row {
4704 start_col.saturating_sub(1)
4705 } else if chars.is_empty() {
4706 continue;
4707 } else {
4708 chars.len().saturating_sub(1)
4709 };
4710 if col_end == 0 {
4711 continue;
4712 }
4713 for col in (0..col_end).rev() {
4715 if col + 1 < chars.len() && chars[col] == c1 && chars[col + 1] == c2 {
4716 hits += 1;
4717 if hits == count {
4718 return Some((row, col));
4719 }
4720 }
4721 }
4722 }
4723 None
4724}
4725
4726pub(crate) fn apply_op_sneak<H: crate::types::Host>(
4733 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4734 op: Operator,
4735 c1: char,
4736 c2: char,
4737 forward: bool,
4738 total_count: usize,
4739) {
4740 let start = ed.cursor();
4741 let result = if forward {
4742 sneak_scan_forward(ed, start.0, start.1, c1, c2, total_count)
4743 } else {
4744 sneak_scan_backward(ed, start.0, start.1, c1, c2, total_count)
4745 };
4746 let Some(end) = result else {
4747 return;
4748 };
4749 ed.jump_cursor(end.0, end.1);
4752 let end_cur = ed.cursor();
4753 ed.jump_cursor(start.0, start.1);
4754 run_operator_over_range(ed, op, start, end_cur, RangeKind::Exclusive);
4755 ed.vim.last_sneak = Some(((c1, c2), forward));
4756 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4757 if !ed.vim.replaying && op_is_change(op) {
4758 }
4762}
4763
4764pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
4770 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4771 op: Operator,
4772 ch: char,
4773 forward: bool,
4774 till: bool,
4775 total_count: usize,
4776) {
4777 let motion = Motion::Find { ch, forward, till };
4778 apply_op_with_motion(ed, op, &motion, total_count);
4779 ed.vim.last_find = Some((ch, forward, till));
4780 if !ed.vim.replaying && op_is_change(op) {
4781 ed.vim.last_change = Some(LastChange::OpMotion {
4782 op,
4783 motion,
4784 count: total_count,
4785 inserted: None,
4786 });
4787 }
4788}
4789
4790pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
4799 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4800 op: Operator,
4801 ch: char,
4802 inner: bool,
4803 total_count: usize,
4804) -> bool {
4805 let obj = match ch {
4808 'w' => TextObject::Word { big: false },
4809 'W' => TextObject::Word { big: true },
4810 '"' | '\'' | '`' => TextObject::Quote(ch),
4811 '(' | ')' | 'b' => TextObject::Bracket('('),
4812 '[' | ']' => TextObject::Bracket('['),
4813 '{' | '}' | 'B' => TextObject::Bracket('{'),
4814 '<' | '>' => TextObject::Bracket('<'),
4815 'p' => TextObject::Paragraph,
4816 't' => TextObject::XmlTag,
4817 's' => TextObject::Sentence,
4818 _ => return false,
4819 };
4820 apply_op_with_text_object(ed, op, obj, inner, total_count.max(1));
4821 if !ed.vim.replaying && op_is_change(op) {
4822 ed.vim.last_change = Some(LastChange::OpTextObj {
4823 op,
4824 obj,
4825 inner,
4826 inserted: None,
4827 });
4828 }
4829 true
4830}
4831
4832pub(crate) fn retreat_one<H: crate::types::Host>(
4834 ed: &Editor<hjkl_buffer::Buffer, H>,
4835 pos: (usize, usize),
4836) -> (usize, usize) {
4837 let (r, c) = pos;
4838 if c > 0 {
4839 (r, c - 1)
4840 } else if r > 0 {
4841 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
4842 (r - 1, prev_len)
4843 } else {
4844 (0, 0)
4845 }
4846}
4847
4848fn begin_insert_noundo<H: crate::types::Host>(
4850 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4851 count: usize,
4852 reason: InsertReason,
4853) {
4854 let reason = if ed.vim.replaying {
4855 InsertReason::ReplayOnly
4856 } else {
4857 reason
4858 };
4859 let (row, col) = ed.cursor();
4860 ed.vim.insert_session = Some(InsertSession {
4861 count,
4862 row_min: row,
4863 row_max: row,
4864 before_rope: crate::types::Query::rope(&ed.buffer),
4865 reason,
4866 start_row: row,
4867 start_col: col,
4868 });
4869 ed.vim.mode = Mode::Insert;
4870 ed.vim.current_mode = crate::VimMode::Insert;
4872 drop_blame_if_left_normal(ed);
4873}
4874
4875pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
4878 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4879 op: Operator,
4880 motion: &Motion,
4881 count: usize,
4882) {
4883 let start = ed.cursor();
4884 apply_motion_cursor_ctx(ed, motion, count, true);
4889 let mut end = ed.cursor();
4890 let mut kind = motion_kind(motion);
4891 if matches!(motion, Motion::WordFwd | Motion::BigWordFwd)
4899 && kind == RangeKind::Exclusive
4900 && end.0 > start.0
4901 && let Some(word_end) = last_word_end_before(ed, start, end)
4902 && word_end.0 < end.0
4903 {
4904 end = word_end;
4905 kind = RangeKind::Inclusive;
4906 }
4907 ed.jump_cursor(start.0, start.1);
4909
4910 if op == Operator::Comment {
4912 let top = start.0.min(end.0);
4913 let bot = start.0.max(end.0);
4914 ed.toggle_comment_range(top, bot);
4915 ed.vim.mode = Mode::Normal;
4916 return;
4917 }
4918
4919 run_operator_over_range(ed, op, start, end, kind);
4920}
4921
4922fn last_word_end_before<H: crate::types::Host>(
4930 ed: &Editor<hjkl_buffer::Buffer, H>,
4931 start: (usize, usize),
4932 end: (usize, usize),
4933) -> Option<(usize, usize)> {
4934 for r in (start.0..=end.0).rev() {
4935 let line = buf_line(&ed.buffer, r).unwrap_or_default();
4936 let lo = if r == start.0 { start.1 } else { 0 };
4937 let hi = if r == end.0 {
4938 end.1
4939 } else {
4940 line.chars().count()
4941 };
4942 let last = line
4943 .chars()
4944 .enumerate()
4945 .filter(|(i, ch)| *i >= lo && *i < hi && *ch != ' ' && *ch != '\t')
4946 .map(|(i, _)| i)
4947 .last();
4948 if let Some(col) = last {
4949 return Some((r, col));
4950 }
4951 }
4952 None
4953}
4954
4955fn apply_op_with_text_object<H: crate::types::Host>(
4956 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4957 op: Operator,
4958 obj: TextObject,
4959 inner: bool,
4960 count: usize,
4961) {
4962 let count = count.min(MAX_COUNT);
4965 let Some((mut start, mut end, mut kind)) = text_object_range(ed, obj, inner, count) else {
4966 return;
4967 };
4968 if inner
4977 && matches!(obj, TextObject::Bracket(_))
4978 && kind == RangeKind::Exclusive
4979 && end.0 > start.0
4980 && end.1 == 0
4981 {
4982 let prev = end.0 - 1;
4983 let prev_len = buf_line_chars(&ed.buffer, prev);
4984 let fnb = buf_line(&ed.buffer, start.0)
4985 .unwrap_or_default()
4986 .chars()
4987 .take_while(|c| *c == ' ' || *c == '\t')
4988 .count();
4989 if start.1 <= fnb {
4990 start = (start.0, 0);
4991 end = (prev, prev_len);
4992 kind = RangeKind::Linewise;
4993 } else {
4994 end = (prev, prev_len.saturating_sub(1));
4995 kind = RangeKind::Inclusive;
4996 }
4997 }
4998 ed.jump_cursor(start.0, start.1);
4999 run_operator_over_range(ed, op, start, end, kind);
5000}
5001
5002fn motion_kind(motion: &Motion) -> RangeKind {
5003 match motion {
5004 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
5005 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
5006 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
5007 RangeKind::Linewise
5008 }
5009 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
5010 RangeKind::Inclusive
5011 }
5012 Motion::Find { .. } => RangeKind::Inclusive,
5013 Motion::MatchBracket => RangeKind::Inclusive,
5014 Motion::UnmatchedBracket { .. } => RangeKind::Exclusive,
5017 Motion::LineEnd => RangeKind::Inclusive,
5019 Motion::FirstNonBlankNextLine
5021 | Motion::FirstNonBlankPrevLine
5022 | Motion::FirstNonBlankLine => RangeKind::Linewise,
5023 Motion::SectionBackward
5025 | Motion::SectionForward
5026 | Motion::SectionEndBackward
5027 | Motion::SectionEndForward => RangeKind::Exclusive,
5028 _ => RangeKind::Exclusive,
5029 }
5030}
5031
5032fn change_linewise_rows<H: crate::types::Host>(
5041 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5042 top_row: usize,
5043 end_row: usize,
5044) {
5045 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5046 ed.vim.change_mark_start = Some((top_row, 0));
5048 ed.push_undo();
5049 ed.sync_buffer_content_from_textarea();
5050 let payload = read_vim_range(ed, (top_row, 0), (end_row, 0), RangeKind::Linewise);
5052 if end_row > top_row {
5054 ed.mutate_edit(Edit::DeleteRange {
5055 start: Position::new(top_row + 1, 0),
5056 end: Position::new(end_row, 0),
5057 kind: BufKind::Line,
5058 });
5059 }
5060 let indent_chars = if ed.settings.autoindent {
5063 let line = hjkl_buffer::rope_line_str(&crate::types::Query::rope(&ed.buffer), top_row);
5064 line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
5065 } else {
5066 0
5067 };
5068 let line_chars = buf_line_chars(&ed.buffer, top_row);
5069 if line_chars > indent_chars {
5070 ed.mutate_edit(Edit::DeleteRange {
5071 start: Position::new(top_row, indent_chars),
5072 end: Position::new(top_row, line_chars),
5073 kind: BufKind::Char,
5074 });
5075 }
5076 if !payload.is_empty() {
5077 ed.record_yank_to_host(payload.clone());
5078 ed.record_delete(payload, true);
5079 }
5080 buf_set_cursor_rc(&mut ed.buffer, top_row, indent_chars);
5081 ed.push_buffer_cursor_to_textarea();
5082 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5083}
5084
5085fn run_operator_over_range<H: crate::types::Host>(
5086 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5087 op: Operator,
5088 start: (usize, usize),
5089 end: (usize, usize),
5090 kind: RangeKind,
5091) {
5092 let (top, bot) = order(start, end);
5093 if top == bot && !matches!(kind, RangeKind::Linewise) {
5098 if op == Operator::Change {
5099 ed.vim.change_mark_start = Some(top);
5100 ed.push_undo();
5101 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5102 }
5103 return;
5104 }
5105
5106 match op {
5107 Operator::Yank => {
5108 let text = read_vim_range(ed, top, bot, kind);
5109 if !text.is_empty() {
5110 ed.record_yank_to_host(text.clone());
5111 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
5112 }
5113 let rbr = match kind {
5117 RangeKind::Linewise => {
5118 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
5119 (bot.0, last_col)
5120 }
5121 RangeKind::Inclusive => (bot.0, bot.1),
5122 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
5123 };
5124 ed.set_mark('[', top);
5125 ed.set_mark(']', rbr);
5126 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5127 ed.push_buffer_cursor_to_textarea();
5128 }
5129 Operator::Delete => {
5130 ed.push_undo();
5131 cut_vim_range(ed, top, bot, kind);
5132 if !matches!(kind, RangeKind::Linewise) {
5137 clamp_cursor_to_normal_mode(ed);
5138 }
5139 ed.vim.mode = Mode::Normal;
5140 let pos = ed.cursor();
5144 ed.set_mark('[', pos);
5145 ed.set_mark(']', pos);
5146 }
5147 Operator::Change => {
5148 if matches!(kind, RangeKind::Linewise) {
5153 change_linewise_rows(ed, top.0, bot.0);
5157 } else {
5158 ed.vim.change_mark_start = Some(top);
5160 ed.push_undo();
5161 cut_vim_range(ed, top, bot, kind);
5162 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5163 }
5164 }
5165 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
5166 apply_case_op_to_selection(ed, op, top, bot, kind);
5167 }
5168 Operator::Indent | Operator::Outdent => {
5169 ed.push_undo();
5172 if op == Operator::Indent {
5173 indent_rows(ed, top.0, bot.0, 1);
5174 } else {
5175 outdent_rows(ed, top.0, bot.0, 1);
5176 }
5177 ed.vim.mode = Mode::Normal;
5178 }
5179 Operator::Fold => {
5180 if bot.0 >= top.0 {
5184 ed.apply_fold_op(crate::types::FoldOp::Add {
5185 start_row: top.0,
5186 end_row: bot.0,
5187 closed: true,
5188 });
5189 }
5190 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5191 ed.push_buffer_cursor_to_textarea();
5192 ed.vim.mode = Mode::Normal;
5193 }
5194 Operator::Reflow => {
5195 ed.push_undo();
5196 reflow_rows(ed, top.0, bot.0);
5197 ed.vim.mode = Mode::Normal;
5198 }
5199 Operator::ReflowKeepCursor => {
5200 let saved = ed.cursor();
5203 ed.push_undo();
5204 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
5205 let (new_row, new_col) = reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
5206 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
5207 ed.push_buffer_cursor_to_textarea();
5208 ed.sticky_col = Some(new_col);
5209 ed.vim.mode = Mode::Normal;
5210 }
5211 Operator::AutoIndent => {
5212 ed.push_undo();
5214 auto_indent_rows(ed, top.0, bot.0);
5215 ed.vim.mode = Mode::Normal;
5216 }
5217 Operator::Filter => {
5218 }
5223 Operator::Comment => {
5224 }
5227 }
5228}
5229
5230pub(crate) fn delete_range_bridge<H: crate::types::Host>(
5247 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5248 start: (usize, usize),
5249 end: (usize, usize),
5250 kind: RangeKind,
5251 register: char,
5252) {
5253 ed.vim.pending_register = Some(register);
5254 run_operator_over_range(ed, Operator::Delete, start, end, kind);
5255}
5256
5257pub(crate) fn yank_range_bridge<H: crate::types::Host>(
5260 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5261 start: (usize, usize),
5262 end: (usize, usize),
5263 kind: RangeKind,
5264 register: char,
5265) {
5266 ed.vim.pending_register = Some(register);
5267 run_operator_over_range(ed, Operator::Yank, start, end, kind);
5268}
5269
5270pub(crate) fn change_range_bridge<H: crate::types::Host>(
5275 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5276 start: (usize, usize),
5277 end: (usize, usize),
5278 kind: RangeKind,
5279 register: char,
5280) {
5281 ed.vim.pending_register = Some(register);
5282 run_operator_over_range(ed, Operator::Change, start, end, kind);
5283}
5284
5285pub(crate) fn indent_range_bridge<H: crate::types::Host>(
5290 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5291 start: (usize, usize),
5292 end: (usize, usize),
5293 count: i32,
5294 shiftwidth: u32,
5295) {
5296 if count == 0 {
5297 return;
5298 }
5299 let (top_row, bot_row) = if start.0 <= end.0 {
5300 (start.0, end.0)
5301 } else {
5302 (end.0, start.0)
5303 };
5304 let original_sw = ed.settings().shiftwidth;
5306 if shiftwidth > 0 {
5307 ed.settings_mut().shiftwidth = shiftwidth as usize;
5308 }
5309 ed.push_undo();
5310 let abs_count = count.unsigned_abs() as usize;
5311 if count > 0 {
5312 indent_rows(ed, top_row, bot_row, abs_count);
5313 } else {
5314 outdent_rows(ed, top_row, bot_row, abs_count);
5315 }
5316 if shiftwidth > 0 {
5317 ed.settings_mut().shiftwidth = original_sw;
5318 }
5319 ed.vim.mode = Mode::Normal;
5320}
5321
5322pub(crate) fn case_range_bridge<H: crate::types::Host>(
5326 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5327 start: (usize, usize),
5328 end: (usize, usize),
5329 kind: RangeKind,
5330 op: Operator,
5331) {
5332 match op {
5333 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {}
5334 _ => return,
5335 }
5336 let (top, bot) = order(start, end);
5337 apply_case_op_to_selection(ed, op, top, bot, kind);
5338}
5339
5340pub(crate) fn delete_block_bridge<H: crate::types::Host>(
5361 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5362 top_row: usize,
5363 bot_row: usize,
5364 left_col: usize,
5365 right_col: usize,
5366 register: char,
5367) {
5368 ed.vim.pending_register = Some(register);
5369 let saved_anchor = ed.vim.block_anchor;
5370 let saved_vcol = ed.vim.block_vcol;
5371 ed.vim.block_anchor = (top_row, left_col);
5372 ed.vim.block_vcol = right_col;
5373 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5375 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5377 apply_block_operator(ed, Operator::Delete, 1);
5378 ed.vim.block_anchor = saved_anchor;
5382 ed.vim.block_vcol = saved_vcol;
5383}
5384
5385pub(crate) fn yank_block_bridge<H: crate::types::Host>(
5387 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5388 top_row: usize,
5389 bot_row: usize,
5390 left_col: usize,
5391 right_col: usize,
5392 register: char,
5393) {
5394 ed.vim.pending_register = Some(register);
5395 let saved_anchor = ed.vim.block_anchor;
5396 let saved_vcol = ed.vim.block_vcol;
5397 ed.vim.block_anchor = (top_row, left_col);
5398 ed.vim.block_vcol = right_col;
5399 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5400 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5401 apply_block_operator(ed, Operator::Yank, 1);
5402 ed.vim.block_anchor = saved_anchor;
5403 ed.vim.block_vcol = saved_vcol;
5404}
5405
5406pub(crate) fn change_block_bridge<H: crate::types::Host>(
5409 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5410 top_row: usize,
5411 bot_row: usize,
5412 left_col: usize,
5413 right_col: usize,
5414 register: char,
5415) {
5416 ed.vim.pending_register = Some(register);
5417 let saved_anchor = ed.vim.block_anchor;
5418 let saved_vcol = ed.vim.block_vcol;
5419 ed.vim.block_anchor = (top_row, left_col);
5420 ed.vim.block_vcol = right_col;
5421 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5422 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5423 apply_block_operator(ed, Operator::Change, 1);
5424 ed.vim.block_anchor = saved_anchor;
5425 ed.vim.block_vcol = saved_vcol;
5426}
5427
5428pub(crate) fn indent_block_bridge<H: crate::types::Host>(
5432 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5433 top_row: usize,
5434 bot_row: usize,
5435 count: i32,
5436) {
5437 if count == 0 {
5438 return;
5439 }
5440 ed.push_undo();
5441 let abs = count.unsigned_abs() as usize;
5442 if count > 0 {
5443 indent_rows(ed, top_row, bot_row, abs);
5444 } else {
5445 outdent_rows(ed, top_row, bot_row, abs);
5446 }
5447 ed.vim.mode = Mode::Normal;
5448}
5449
5450pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
5454 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5455 start: (usize, usize),
5456 end: (usize, usize),
5457) {
5458 let (top_row, bot_row) = if start.0 <= end.0 {
5459 (start.0, end.0)
5460 } else {
5461 (end.0, start.0)
5462 };
5463 ed.push_undo();
5464 auto_indent_rows(ed, top_row, bot_row);
5465 ed.vim.mode = Mode::Normal;
5466}
5467
5468pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
5479 ed: &Editor<hjkl_buffer::Buffer, H>,
5480) -> Option<((usize, usize), (usize, usize))> {
5481 word_text_object(ed, true, false, 1)
5482}
5483
5484pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
5487 ed: &Editor<hjkl_buffer::Buffer, H>,
5488) -> Option<((usize, usize), (usize, usize))> {
5489 word_text_object(ed, false, false, 1)
5490}
5491
5492pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
5495 ed: &Editor<hjkl_buffer::Buffer, H>,
5496) -> Option<((usize, usize), (usize, usize))> {
5497 word_text_object(ed, true, true, 1)
5498}
5499
5500pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
5503 ed: &Editor<hjkl_buffer::Buffer, H>,
5504) -> Option<((usize, usize), (usize, usize))> {
5505 word_text_object(ed, false, true, 1)
5506}
5507
5508pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
5524 ed: &Editor<hjkl_buffer::Buffer, H>,
5525 quote: char,
5526) -> Option<((usize, usize), (usize, usize))> {
5527 quote_text_object(ed, quote, true)
5528}
5529
5530pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
5533 ed: &Editor<hjkl_buffer::Buffer, H>,
5534 quote: char,
5535) -> Option<((usize, usize), (usize, usize))> {
5536 quote_text_object(ed, quote, false)
5537}
5538
5539pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
5547 ed: &Editor<hjkl_buffer::Buffer, H>,
5548 open: char,
5549) -> Option<((usize, usize), (usize, usize))> {
5550 bracket_text_object(ed, open, true, 1).map(|(s, e, _kind)| (s, e))
5551}
5552
5553pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
5557 ed: &Editor<hjkl_buffer::Buffer, H>,
5558 open: char,
5559) -> Option<((usize, usize), (usize, usize))> {
5560 bracket_text_object(ed, open, false, 1).map(|(s, e, _kind)| (s, e))
5561}
5562
5563pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
5568 ed: &Editor<hjkl_buffer::Buffer, H>,
5569) -> Option<((usize, usize), (usize, usize))> {
5570 sentence_text_object(ed, true, 1)
5571}
5572
5573pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
5576 ed: &Editor<hjkl_buffer::Buffer, H>,
5577) -> Option<((usize, usize), (usize, usize))> {
5578 sentence_text_object(ed, false, 1)
5579}
5580
5581pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
5586 ed: &Editor<hjkl_buffer::Buffer, H>,
5587) -> Option<((usize, usize), (usize, usize))> {
5588 paragraph_text_object(ed, true, 1)
5589}
5590
5591pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
5594 ed: &Editor<hjkl_buffer::Buffer, H>,
5595) -> Option<((usize, usize), (usize, usize))> {
5596 paragraph_text_object(ed, false, 1)
5597}
5598
5599pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
5605 ed: &Editor<hjkl_buffer::Buffer, H>,
5606) -> Option<((usize, usize), (usize, usize))> {
5607 tag_text_object(ed, true)
5608}
5609
5610pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
5613 ed: &Editor<hjkl_buffer::Buffer, H>,
5614) -> Option<((usize, usize), (usize, usize))> {
5615 tag_text_object(ed, false)
5616}
5617
5618pub(crate) fn rope_line_to_str(rope: &ropey::Rope, r: usize) -> String {
5623 let s = rope.line(r).to_string();
5624 if s.ends_with('\n') {
5626 s[..s.len() - 1].to_string()
5627 } else {
5628 s
5629 }
5630}
5631
5632pub(crate) fn rope_row_range_str(rope: &ropey::Rope, lo: usize, hi: usize) -> String {
5635 let n = rope.len_lines();
5636 let lo = lo.min(n.saturating_sub(1));
5637 let hi = hi.min(n.saturating_sub(1));
5638 if lo > hi {
5639 return String::new();
5640 }
5641 let start_byte = rope.line_to_byte(lo);
5643 let end_byte = if hi + 1 < n {
5646 rope.line_to_byte(hi + 1).saturating_sub(1)
5649 } else {
5650 rope.len_bytes()
5651 };
5652 rope.byte_slice(start_byte..end_byte).to_string()
5653}
5654
5655pub(crate) fn rope_to_lines_vec(rope: &ropey::Rope) -> Vec<String> {
5659 let n = rope.len_lines();
5660 (0..n).map(|r| rope_line_to_str(rope, r)).collect()
5661}
5662
5663fn greedy_wrap(original: &[String], width: usize) -> Vec<String> {
5667 let mut wrapped: Vec<String> = Vec::new();
5668 let mut paragraph: Vec<String> = Vec::new();
5669 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
5670 if para.is_empty() {
5671 return;
5672 }
5673 let words = para.join(" ");
5674 let mut current = String::new();
5675 for word in words.split_whitespace() {
5676 let extra = if current.is_empty() {
5677 word.chars().count()
5678 } else {
5679 current.chars().count() + 1 + word.chars().count()
5680 };
5681 if extra > width && !current.is_empty() {
5682 out.push(std::mem::take(&mut current));
5683 current.push_str(word);
5684 } else if current.is_empty() {
5685 current.push_str(word);
5686 } else {
5687 current.push(' ');
5688 current.push_str(word);
5689 }
5690 }
5691 if !current.is_empty() {
5692 out.push(current);
5693 }
5694 para.clear();
5695 };
5696 for line in original {
5697 if line.trim().is_empty() {
5698 flush(&mut paragraph, &mut wrapped, width);
5699 wrapped.push(String::new());
5700 } else {
5701 paragraph.push(line.clone());
5702 }
5703 }
5704 flush(&mut paragraph, &mut wrapped, width);
5705 wrapped
5706}
5707
5708fn reflow_rows<H: crate::types::Host>(
5714 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5715 top: usize,
5716 bot: usize,
5717) {
5718 let width = ed.settings().textwidth.max(1);
5719 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5720 let bot = bot.min(lines.len().saturating_sub(1));
5721 if top > bot {
5722 return;
5723 }
5724 let original = lines[top..=bot].to_vec();
5725 let wrapped = greedy_wrap(&original, width);
5726
5727 let last_offset = wrapped
5730 .iter()
5731 .rposition(|l| !l.trim().is_empty())
5732 .unwrap_or(0);
5733 let last_row = top + last_offset;
5734
5735 let after: Vec<String> = lines.split_off(bot + 1);
5737 lines.truncate(top);
5738 lines.extend(wrapped);
5739 lines.extend(after);
5740 ed.restore(lines, (last_row, 0));
5741 move_first_non_whitespace(ed);
5742 ed.mark_content_dirty();
5743}
5744
5745fn reflow_rows_keep_cursor<H: crate::types::Host>(
5749 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5750 top: usize,
5751 bot: usize,
5752) -> (Vec<String>, Vec<String>) {
5753 let width = ed.settings().textwidth.max(1);
5754 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5755 let bot = bot.min(lines.len().saturating_sub(1));
5756 if top > bot {
5757 return (Vec::new(), Vec::new());
5758 }
5759 let original = lines[top..=bot].to_vec();
5760 let wrapped = greedy_wrap(&original, width);
5761
5762 let after: Vec<String> = lines.split_off(bot + 1);
5763 lines.truncate(top);
5764 lines.extend(wrapped.clone());
5765 lines.extend(after);
5766 ed.restore(lines, (top, 0));
5767 ed.mark_content_dirty();
5768 (original, wrapped)
5769}
5770
5771fn reflow_keep_cursor(
5783 top: usize,
5784 cursor_row: usize,
5785 cursor_col: usize,
5786 before_lines: &[String],
5787 after_lines: &[String],
5788) -> (usize, usize) {
5789 let relative_row = cursor_row.saturating_sub(top);
5809 let mut char_offset: usize = 0;
5810 for (i, line) in before_lines.iter().enumerate() {
5811 if i == relative_row {
5812 let line_len = line.chars().count();
5814 char_offset += cursor_col.min(line_len);
5815 break;
5816 }
5817 char_offset += line.chars().count() + 1;
5819 }
5820
5821 let mut remaining = char_offset;
5823 for (i, line) in after_lines.iter().enumerate() {
5824 let len = line.chars().count();
5825 if remaining <= len {
5826 let col = remaining.min(if len == 0 { 0 } else { len.saturating_sub(1) });
5828 return (top + i, col);
5829 }
5830 remaining = remaining.saturating_sub(len + 1);
5832 }
5833
5834 let last = after_lines.len().saturating_sub(1);
5836 let last_len = after_lines
5837 .get(last)
5838 .map(|l| l.chars().count())
5839 .unwrap_or(0);
5840 let col = if last_len == 0 { 0 } else { last_len - 1 };
5841 (top + last, col)
5842}
5843
5844fn apply_case_op_to_selection<H: crate::types::Host>(
5850 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5851 op: Operator,
5852 top: (usize, usize),
5853 bot: (usize, usize),
5854 kind: RangeKind,
5855) {
5856 use hjkl_buffer::Edit;
5857 ed.push_undo();
5858 let saved_yank = ed.yank().to_string();
5859 let saved_yank_linewise = ed.vim.yank_linewise;
5860 let selection = cut_vim_range(ed, top, bot, kind);
5861 let transformed = match op {
5862 Operator::Uppercase => selection.to_uppercase(),
5863 Operator::Lowercase => selection.to_lowercase(),
5864 Operator::ToggleCase => toggle_case_str(&selection),
5865 Operator::Rot13 => rot13_str(&selection),
5866 _ => unreachable!(),
5867 };
5868 if !transformed.is_empty() {
5869 let cursor = buf_cursor_pos(&ed.buffer);
5870 ed.mutate_edit(Edit::InsertStr {
5871 at: cursor,
5872 text: transformed,
5873 });
5874 }
5875 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5876 ed.push_buffer_cursor_to_textarea();
5877 ed.set_yank(saved_yank);
5878 ed.vim.yank_linewise = saved_yank_linewise;
5879 ed.vim.mode = Mode::Normal;
5880}
5881
5882fn indent_rows<H: crate::types::Host>(
5887 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5888 top: usize,
5889 bot: usize,
5890 count: usize,
5891) {
5892 ed.sync_buffer_content_from_textarea();
5893 let width = ed.settings().shiftwidth.saturating_mul(count.max(1));
5894 let pad: String = if ed.settings().expandtab {
5898 " ".repeat(width)
5899 } else {
5900 let tabstop = ed.settings().tabstop.max(1);
5901 let tabs = width / tabstop;
5902 let spaces = width % tabstop;
5903 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
5904 };
5905 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5906 let bot = bot.min(lines.len().saturating_sub(1));
5907 for line in lines.iter_mut().take(bot + 1).skip(top) {
5908 if !line.is_empty() {
5909 line.insert_str(0, &pad);
5910 }
5911 }
5912 ed.restore(lines, (top, 0));
5915 move_first_non_whitespace(ed);
5916}
5917
5918fn outdent_rows<H: crate::types::Host>(
5922 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5923 top: usize,
5924 bot: usize,
5925 count: usize,
5926) {
5927 ed.sync_buffer_content_from_textarea();
5928 let width = ed.settings().shiftwidth.saturating_mul(count.max(1));
5929 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5930 let bot = bot.min(lines.len().saturating_sub(1));
5931 for line in lines.iter_mut().take(bot + 1).skip(top) {
5932 let strip: usize = line
5933 .chars()
5934 .take(width)
5935 .take_while(|c| *c == ' ' || *c == '\t')
5936 .count();
5937 if strip > 0 {
5938 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
5939 line.drain(..byte_len);
5940 }
5941 }
5942 ed.restore(lines, (top, 0));
5943 move_first_non_whitespace(ed);
5944}
5945
5946fn bracket_net(line: &str) -> i32 {
5973 let mut net: i32 = 0;
5974 let mut chars = line.chars().peekable();
5975 while let Some(ch) = chars.next() {
5976 match ch {
5977 '/' if chars.peek() == Some(&'/') => return net,
5979 '"' => {
5980 while let Some(c) = chars.next() {
5982 match c {
5983 '\\' => {
5984 chars.next();
5985 } '"' => break,
5987 _ => {}
5988 }
5989 }
5990 }
5991 '\'' => {
5992 let saved: Vec<char> = chars.clone().take(5).collect();
6001 let close_idx = if saved.first() == Some(&'\\') {
6002 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
6003 } else {
6004 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
6005 };
6006 if let Some(idx) = close_idx {
6007 for _ in 0..=idx {
6008 chars.next();
6009 }
6010 }
6011 }
6013 '{' | '(' | '[' => net += 1,
6014 '}' | ')' | ']' => net -= 1,
6015 _ => {}
6016 }
6017 }
6018 net
6019}
6020
6021fn auto_indent_rows<H: crate::types::Host>(
6043 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6044 top: usize,
6045 bot: usize,
6046) {
6047 ed.sync_buffer_content_from_textarea();
6048 let shiftwidth = ed.settings().shiftwidth;
6049 let expandtab = ed.settings().expandtab;
6050 let indent_unit: String = if expandtab {
6051 " ".repeat(shiftwidth)
6052 } else {
6053 "\t".to_string()
6054 };
6055
6056 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6057 let bot = bot.min(lines.len().saturating_sub(1));
6058
6059 let mut depth: i32 = 0;
6062 for line in lines.iter().take(top) {
6063 depth += bracket_net(line);
6064 if depth < 0 {
6065 depth = 0;
6066 }
6067 }
6068
6069 for line in lines.iter_mut().take(bot + 1).skip(top) {
6070 let trimmed_owned = line.trim_start().to_owned();
6071 if trimmed_owned.is_empty() {
6073 *line = String::new();
6074 continue;
6076 }
6077
6078 let starts_with_close = trimmed_owned
6080 .chars()
6081 .next()
6082 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
6083 let starts_with_dot = trimmed_owned.starts_with('.')
6093 && !trimmed_owned.starts_with("..")
6094 && !trimmed_owned.starts_with(".;");
6095 let effective_depth = if starts_with_close {
6096 depth.saturating_sub(1)
6097 } else if starts_with_dot {
6098 depth.saturating_add(1)
6099 } else {
6100 depth
6101 } as usize;
6102
6103 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
6105
6106 depth += bracket_net(&trimmed_owned);
6108 if depth < 0 {
6109 depth = 0;
6110 }
6111
6112 *line = new_line;
6113 }
6114
6115 ed.restore(lines, (top, 0));
6117 move_first_non_whitespace(ed);
6118 ed.last_indent_range = Some((top, bot));
6120}
6121
6122fn toggle_case_str(s: &str) -> String {
6123 s.chars()
6124 .map(|c| {
6125 if c.is_lowercase() {
6126 c.to_uppercase().next().unwrap_or(c)
6127 } else if c.is_uppercase() {
6128 c.to_lowercase().next().unwrap_or(c)
6129 } else {
6130 c
6131 }
6132 })
6133 .collect()
6134}
6135
6136fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
6137 if a <= b { (a, b) } else { (b, a) }
6138}
6139
6140fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6145 let (row, col) = ed.cursor();
6146 let line_chars = buf_line_chars(&ed.buffer, row);
6147 let max_col = line_chars.saturating_sub(1);
6148 if col > max_col {
6149 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
6150 ed.push_buffer_cursor_to_textarea();
6151 }
6152}
6153
6154fn expand_linewise_over_closed_folds(
6160 buf: &hjkl_buffer::Buffer,
6161 mut start: usize,
6162 mut end: usize,
6163) -> (usize, usize) {
6164 let folds = buf.folds();
6165 if folds.is_empty() {
6166 return (start, end);
6167 }
6168 loop {
6169 let mut changed = false;
6170 for f in &folds {
6171 if !f.closed {
6172 continue;
6173 }
6174 if f.start_row <= end && f.end_row >= start {
6176 if f.start_row < start {
6177 start = f.start_row;
6178 changed = true;
6179 }
6180 if f.end_row > end {
6181 end = f.end_row;
6182 changed = true;
6183 }
6184 }
6185 }
6186 if !changed {
6187 break;
6188 }
6189 }
6190 (start, end)
6191}
6192
6193fn execute_line_op<H: crate::types::Host>(
6194 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6195 op: Operator,
6196 count: usize,
6197) {
6198 let count = count.min(MAX_COUNT);
6201 let (row, col) = ed.cursor();
6202 let total = buf_row_count(&ed.buffer);
6203 let last_content_row = if total >= 2
6212 && buf_line(&ed.buffer, total - 1)
6213 .map(|s| s.is_empty())
6214 .unwrap_or(false)
6215 {
6216 total - 2
6217 } else {
6218 total.saturating_sub(1)
6219 };
6220 if count >= 2 && row >= last_content_row {
6221 return;
6222 }
6223 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
6224
6225 let (row, end_row) = expand_linewise_over_closed_folds(&ed.buffer, row, end_row);
6230
6231 match op {
6232 Operator::Yank => {
6233 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6235 if !text.is_empty() {
6236 ed.record_yank_to_host(text.clone());
6237 ed.record_yank(text, true);
6238 }
6239 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
6242 ed.set_mark('[', (row, 0));
6243 ed.set_mark(']', (end_row, last_col));
6244 buf_set_cursor_rc(&mut ed.buffer, row, col);
6245 ed.push_buffer_cursor_to_textarea();
6246 ed.vim.mode = Mode::Normal;
6247 }
6248 Operator::Delete => {
6249 ed.push_undo();
6250 let deleted_through_last = end_row + 1 >= total;
6251 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6252 let total_after = buf_row_count(&ed.buffer);
6256 let raw_target = if deleted_through_last {
6257 row.saturating_sub(1).min(total_after.saturating_sub(1))
6258 } else {
6259 row.min(total_after.saturating_sub(1))
6260 };
6261 let target_row = if raw_target > 0
6267 && raw_target + 1 == total_after
6268 && buf_line(&ed.buffer, raw_target)
6269 .map(|s| s.is_empty())
6270 .unwrap_or(false)
6271 {
6272 raw_target - 1
6273 } else {
6274 raw_target
6275 };
6276 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6277 ed.push_buffer_cursor_to_textarea();
6278 move_first_non_whitespace(ed);
6279 ed.sticky_col = Some(ed.cursor().1);
6280 ed.vim.mode = Mode::Normal;
6281 let pos = ed.cursor();
6284 ed.set_mark('[', pos);
6285 ed.set_mark(']', pos);
6286 }
6287 Operator::Change => {
6288 change_linewise_rows(ed, row, end_row);
6292 }
6293 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6294 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
6298 move_first_non_whitespace(ed);
6301 }
6302 Operator::Indent | Operator::Outdent => {
6303 ed.push_undo();
6305 if op == Operator::Indent {
6306 indent_rows(ed, row, end_row, 1);
6307 } else {
6308 outdent_rows(ed, row, end_row, 1);
6309 }
6310 ed.sticky_col = Some(ed.cursor().1);
6311 ed.vim.mode = Mode::Normal;
6312 }
6313 Operator::Fold => unreachable!("Fold has no line-op double"),
6315 Operator::Reflow => {
6316 ed.push_undo();
6318 reflow_rows(ed, row, end_row);
6319 move_first_non_whitespace(ed);
6320 ed.sticky_col = Some(ed.cursor().1);
6321 ed.vim.mode = Mode::Normal;
6322 }
6323 Operator::ReflowKeepCursor => {
6324 let saved = ed.cursor();
6327 ed.push_undo();
6328 let (before, after) = reflow_rows_keep_cursor(ed, row, end_row);
6329 let (new_row, new_col) = reflow_keep_cursor(row, saved.0, saved.1, &before, &after);
6330 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6331 ed.push_buffer_cursor_to_textarea();
6332 ed.sticky_col = Some(new_col);
6333 ed.vim.mode = Mode::Normal;
6334 }
6335 Operator::AutoIndent => {
6336 ed.push_undo();
6338 auto_indent_rows(ed, row, end_row);
6339 ed.sticky_col = Some(ed.cursor().1);
6340 ed.vim.mode = Mode::Normal;
6341 }
6342 Operator::Filter => {
6343 }
6345 Operator::Comment => {
6346 }
6351 }
6352}
6353
6354pub(crate) fn apply_visual_operator<H: crate::types::Host>(
6357 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6358 op: Operator,
6359 count: usize,
6360) {
6361 let levels = count.max(1);
6364 match ed.vim.mode {
6365 Mode::VisualLine => {
6366 let cursor_row = buf_cursor_pos(&ed.buffer).row;
6367 let top = cursor_row.min(ed.vim.visual_line_anchor);
6368 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6369 ed.vim.yank_linewise = true;
6370 match op {
6371 Operator::Yank => {
6372 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6373 if !text.is_empty() {
6374 ed.record_yank_to_host(text.clone());
6375 ed.record_yank(text, true);
6376 }
6377 buf_set_cursor_rc(&mut ed.buffer, top, 0);
6378 ed.push_buffer_cursor_to_textarea();
6379 ed.vim.mode = Mode::Normal;
6380 }
6381 Operator::Delete => {
6382 ed.push_undo();
6383 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6384 ed.vim.mode = Mode::Normal;
6385 }
6386 Operator::Change => {
6387 change_linewise_rows(ed, top, bot);
6390 }
6391 Operator::Uppercase
6392 | Operator::Lowercase
6393 | Operator::ToggleCase
6394 | Operator::Rot13 => {
6395 let bot = buf_cursor_pos(&ed.buffer)
6396 .row
6397 .max(ed.vim.visual_line_anchor);
6398 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
6399 move_first_non_whitespace(ed);
6400 }
6401 Operator::Indent | Operator::Outdent => {
6402 ed.push_undo();
6403 let (cursor_row, _) = ed.cursor();
6404 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6405 if op == Operator::Indent {
6406 indent_rows(ed, top, bot, levels);
6407 } else {
6408 outdent_rows(ed, top, bot, levels);
6409 }
6410 ed.vim.mode = Mode::Normal;
6411 }
6412 Operator::Reflow => {
6413 ed.push_undo();
6414 let (cursor_row, _) = ed.cursor();
6415 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6416 reflow_rows(ed, top, bot);
6417 ed.vim.mode = Mode::Normal;
6418 }
6419 Operator::ReflowKeepCursor => {
6420 let saved = ed.cursor();
6421 ed.push_undo();
6422 let (cursor_row, _) = ed.cursor();
6423 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6424 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6425 let (new_row, new_col) =
6426 reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6427 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6428 ed.push_buffer_cursor_to_textarea();
6429 ed.vim.mode = Mode::Normal;
6430 }
6431 Operator::AutoIndent => {
6432 ed.push_undo();
6433 let (cursor_row, _) = ed.cursor();
6434 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6435 auto_indent_rows(ed, top, bot);
6436 ed.vim.mode = Mode::Normal;
6437 }
6438 Operator::Filter => {}
6440 Operator::Comment => {}
6442 Operator::Fold => unreachable!("Visual zf takes its own path"),
6445 }
6446 }
6447 Mode::Visual => {
6448 ed.vim.yank_linewise = false;
6449 let anchor = ed.vim.visual_anchor;
6450 let cursor = ed.cursor();
6451 let (top, bot) = order(anchor, cursor);
6452 match op {
6453 Operator::Yank => {
6454 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
6455 if !text.is_empty() {
6456 ed.record_yank_to_host(text.clone());
6457 ed.record_yank(text, false);
6458 }
6459 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6460 ed.push_buffer_cursor_to_textarea();
6461 ed.vim.mode = Mode::Normal;
6462 }
6463 Operator::Delete => {
6464 ed.push_undo();
6465 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6466 ed.vim.mode = Mode::Normal;
6467 }
6468 Operator::Change => {
6469 ed.push_undo();
6470 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6471 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
6472 }
6473 Operator::Uppercase
6474 | Operator::Lowercase
6475 | Operator::ToggleCase
6476 | Operator::Rot13 => {
6477 let anchor = ed.vim.visual_anchor;
6479 let cursor = ed.cursor();
6480 let (top, bot) = order(anchor, cursor);
6481 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
6482 }
6483 Operator::Indent | Operator::Outdent => {
6484 ed.push_undo();
6485 let anchor = ed.vim.visual_anchor;
6486 let cursor = ed.cursor();
6487 let (top, bot) = order(anchor, cursor);
6488 if op == Operator::Indent {
6489 indent_rows(ed, top.0, bot.0, levels);
6490 } else {
6491 outdent_rows(ed, top.0, bot.0, levels);
6492 }
6493 ed.vim.mode = Mode::Normal;
6494 }
6495 Operator::Reflow => {
6496 ed.push_undo();
6497 let anchor = ed.vim.visual_anchor;
6498 let cursor = ed.cursor();
6499 let (top, bot) = order(anchor, cursor);
6500 reflow_rows(ed, top.0, bot.0);
6501 ed.vim.mode = Mode::Normal;
6502 }
6503 Operator::ReflowKeepCursor => {
6504 let saved = ed.cursor();
6505 ed.push_undo();
6506 let anchor = ed.vim.visual_anchor;
6507 let cursor = ed.cursor();
6508 let (top, bot) = order(anchor, cursor);
6509 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
6510 let (new_row, new_col) =
6511 reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
6512 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6513 ed.push_buffer_cursor_to_textarea();
6514 ed.vim.mode = Mode::Normal;
6515 }
6516 Operator::AutoIndent => {
6517 ed.push_undo();
6518 let anchor = ed.vim.visual_anchor;
6519 let cursor = ed.cursor();
6520 let (top, bot) = order(anchor, cursor);
6521 auto_indent_rows(ed, top.0, bot.0);
6522 ed.vim.mode = Mode::Normal;
6523 }
6524 Operator::Filter => {}
6526 Operator::Comment => {}
6528 Operator::Fold => unreachable!("Visual zf takes its own path"),
6529 }
6530 }
6531 Mode::VisualBlock => apply_block_operator(ed, op, levels),
6532 _ => {}
6533 }
6534}
6535
6536fn block_bounds<H: crate::types::Host>(
6541 ed: &Editor<hjkl_buffer::Buffer, H>,
6542) -> (usize, usize, usize, usize) {
6543 let (ar, ac) = ed.vim.block_anchor;
6544 let (cr, _) = ed.cursor();
6545 let cc = ed.vim.block_vcol;
6546 let top = ar.min(cr);
6547 let bot = ar.max(cr);
6548 let left = ac.min(cc);
6549 let right = ac.max(cc);
6550 (top, bot, left, right)
6551}
6552
6553pub(crate) fn update_block_vcol<H: crate::types::Host>(
6558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6559 motion: &Motion,
6560) {
6561 match motion {
6562 Motion::Left
6563 | Motion::Right
6564 | Motion::SpaceFwd
6565 | Motion::BackspaceBack
6566 | Motion::WordFwd
6567 | Motion::BigWordFwd
6568 | Motion::WordBack
6569 | Motion::BigWordBack
6570 | Motion::WordEnd
6571 | Motion::BigWordEnd
6572 | Motion::WordEndBack
6573 | Motion::BigWordEndBack
6574 | Motion::LineStart
6575 | Motion::FirstNonBlank
6576 | Motion::LineEnd
6577 | Motion::Find { .. }
6578 | Motion::FindRepeat { .. }
6579 | Motion::MatchBracket => {
6580 ed.vim.block_vcol = ed.cursor().1;
6581 }
6582 _ => {}
6584 }
6585}
6586
6587fn apply_block_operator<H: crate::types::Host>(
6592 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6593 op: Operator,
6594 count: usize,
6595) {
6596 let (top, bot, left, right) = block_bounds(ed);
6597 let yank = block_yank(ed, top, bot, left, right);
6599
6600 match op {
6601 Operator::Yank => {
6602 if !yank.is_empty() {
6603 ed.record_yank_to_host(yank.clone());
6604 ed.record_yank(yank, false);
6605 }
6606 ed.vim.mode = Mode::Normal;
6607 ed.jump_cursor(top, left);
6608 }
6609 Operator::Delete => {
6610 ed.push_undo();
6611 delete_block_contents(ed, top, bot, left, right);
6612 if !yank.is_empty() {
6613 ed.record_yank_to_host(yank.clone());
6614 ed.record_delete(yank, false);
6615 }
6616 ed.vim.mode = Mode::Normal;
6617 ed.jump_cursor(top, left);
6618 }
6619 Operator::Change => {
6620 ed.push_undo();
6621 delete_block_contents(ed, top, bot, left, right);
6622 if !yank.is_empty() {
6623 ed.record_yank_to_host(yank.clone());
6624 ed.record_delete(yank, false);
6625 }
6626 ed.jump_cursor(top, left);
6627 begin_insert_noundo(
6628 ed,
6629 1,
6630 InsertReason::BlockChange {
6631 top,
6632 bot,
6633 col: left,
6634 },
6635 );
6636 }
6637 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6638 ed.push_undo();
6639 transform_block_case(ed, op, top, bot, left, right);
6640 ed.vim.mode = Mode::Normal;
6641 ed.jump_cursor(top, left);
6642 }
6643 Operator::Indent | Operator::Outdent => {
6644 ed.push_undo();
6648 if op == Operator::Indent {
6649 indent_rows(ed, top, bot, count.max(1));
6650 } else {
6651 outdent_rows(ed, top, bot, count.max(1));
6652 }
6653 ed.vim.mode = Mode::Normal;
6654 }
6655 Operator::Fold => unreachable!("Visual zf takes its own path"),
6656 Operator::Reflow => {
6657 ed.push_undo();
6661 reflow_rows(ed, top, bot);
6662 ed.vim.mode = Mode::Normal;
6663 }
6664 Operator::ReflowKeepCursor => {
6665 let saved = ed.cursor();
6667 ed.push_undo();
6668 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6669 let (new_row, new_col) = reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6670 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6671 ed.push_buffer_cursor_to_textarea();
6672 ed.vim.mode = Mode::Normal;
6673 }
6674 Operator::AutoIndent => {
6675 ed.push_undo();
6678 auto_indent_rows(ed, top, bot);
6679 ed.vim.mode = Mode::Normal;
6680 }
6681 Operator::Filter => {}
6683 Operator::Comment => {}
6685 }
6686}
6687
6688fn transform_block_case<H: crate::types::Host>(
6692 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6693 op: Operator,
6694 top: usize,
6695 bot: usize,
6696 left: usize,
6697 right: usize,
6698) {
6699 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6700 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6701 let chars: Vec<char> = lines[r].chars().collect();
6702 if left >= chars.len() {
6703 continue;
6704 }
6705 let end = (right + 1).min(chars.len());
6706 let head: String = chars[..left].iter().collect();
6707 let mid: String = chars[left..end].iter().collect();
6708 let tail: String = chars[end..].iter().collect();
6709 let transformed = match op {
6710 Operator::Uppercase => mid.to_uppercase(),
6711 Operator::Lowercase => mid.to_lowercase(),
6712 Operator::ToggleCase => toggle_case_str(&mid),
6713 Operator::Rot13 => rot13_str(&mid),
6714 _ => mid,
6715 };
6716 lines[r] = format!("{head}{transformed}{tail}");
6717 }
6718 let saved_yank = ed.yank().to_string();
6719 let saved_linewise = ed.vim.yank_linewise;
6720 ed.restore(lines, (top, left));
6721 ed.set_yank(saved_yank);
6722 ed.vim.yank_linewise = saved_linewise;
6723}
6724
6725fn block_yank<H: crate::types::Host>(
6726 ed: &Editor<hjkl_buffer::Buffer, H>,
6727 top: usize,
6728 bot: usize,
6729 left: usize,
6730 right: usize,
6731) -> String {
6732 let rope = crate::types::Query::rope(&ed.buffer);
6733 let n = rope.len_lines();
6734 let mut rows: Vec<String> = Vec::new();
6735 for r in top..=bot {
6736 if r >= n {
6737 break;
6738 }
6739 let line = rope_line_to_str(&rope, r);
6740 let chars: Vec<char> = line.chars().collect();
6741 let end = (right + 1).min(chars.len());
6742 if left >= chars.len() {
6743 rows.push(String::new());
6744 } else {
6745 rows.push(chars[left..end].iter().collect());
6746 }
6747 }
6748 rows.join("\n")
6749}
6750
6751fn delete_block_contents<H: crate::types::Host>(
6752 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6753 top: usize,
6754 bot: usize,
6755 left: usize,
6756 right: usize,
6757) {
6758 use hjkl_buffer::{Edit, MotionKind, Position};
6759 ed.sync_buffer_content_from_textarea();
6760 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
6761 if last_row < top {
6762 return;
6763 }
6764 ed.mutate_edit(Edit::DeleteRange {
6765 start: Position::new(top, left),
6766 end: Position::new(last_row, right),
6767 kind: MotionKind::Block,
6768 });
6769 ed.push_buffer_cursor_to_textarea();
6770}
6771
6772pub(crate) fn block_replace<H: crate::types::Host>(
6774 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6775 ch: char,
6776) {
6777 let (top, bot, left, right) = block_bounds(ed);
6778 ed.push_undo();
6779 ed.sync_buffer_content_from_textarea();
6780 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6781 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6782 let chars: Vec<char> = lines[r].chars().collect();
6783 if left >= chars.len() {
6784 continue;
6785 }
6786 let end = (right + 1).min(chars.len());
6787 let before: String = chars[..left].iter().collect();
6788 let middle: String = std::iter::repeat_n(ch, end - left).collect();
6789 let after: String = chars[end..].iter().collect();
6790 lines[r] = format!("{before}{middle}{after}");
6791 }
6792 reset_textarea_lines(ed, lines);
6793 ed.vim.mode = Mode::Normal;
6794 ed.jump_cursor(top, left);
6795}
6796
6797fn reset_textarea_lines<H: crate::types::Host>(
6801 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6802 lines: Vec<String>,
6803) {
6804 let cursor = ed.cursor();
6805 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
6806 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
6807 ed.mark_content_dirty();
6808}
6809
6810type Pos = (usize, usize);
6816
6817pub(crate) fn text_object_range<H: crate::types::Host>(
6821 ed: &Editor<hjkl_buffer::Buffer, H>,
6822 obj: TextObject,
6823 inner: bool,
6824 count: usize,
6825) -> Option<(Pos, Pos, RangeKind)> {
6826 match obj {
6827 TextObject::Word { big } => {
6828 word_text_object(ed, inner, big, count).map(|(s, e)| (s, e, RangeKind::Exclusive))
6829 }
6830 TextObject::Quote(q) => {
6831 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6832 }
6833 TextObject::Bracket(open) => bracket_text_object(ed, open, inner, count),
6834 TextObject::Paragraph => {
6835 paragraph_text_object(ed, inner, count).map(|(s, e)| (s, e, RangeKind::Linewise))
6836 }
6837 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
6838 TextObject::Sentence => {
6839 sentence_text_object(ed, inner, count).map(|(s, e)| (s, e, RangeKind::Exclusive))
6840 }
6841 }
6842}
6843
6844fn sentence_boundary<H: crate::types::Host>(
6848 ed: &Editor<hjkl_buffer::Buffer, H>,
6849 forward: bool,
6850) -> Option<(usize, usize)> {
6851 let rope = crate::types::Query::rope(&ed.buffer);
6852 let n_lines = rope.len_lines();
6853 if n_lines == 0 {
6854 return None;
6855 }
6856 let line_lens: Vec<usize> = (0..n_lines)
6858 .map(|r| rope_line_to_str(&rope, r).chars().count())
6859 .collect();
6860 let pos_to_idx = |pos: (usize, usize)| -> usize {
6861 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6862 idx + pos.1
6863 };
6864 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6865 for (r, &len) in line_lens.iter().enumerate() {
6866 if idx <= len {
6867 return (r, idx);
6868 }
6869 idx -= len + 1;
6870 }
6871 let last = n_lines.saturating_sub(1);
6872 (last, line_lens[last])
6873 };
6874 let mut chars: Vec<char> = rope.chars().collect();
6877 if chars.last() == Some(&'\n') {
6879 chars.pop();
6880 }
6881 if chars.is_empty() {
6882 return None;
6883 }
6884 let total = chars.len();
6885 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
6886 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6887
6888 if forward {
6889 let mut i = cursor_idx + 1;
6892 while i < total {
6893 if is_terminator(chars[i]) {
6894 while i + 1 < total && is_terminator(chars[i + 1]) {
6895 i += 1;
6896 }
6897 if i + 1 >= total {
6898 return None;
6899 }
6900 if chars[i + 1].is_whitespace() {
6901 let mut j = i + 1;
6902 while j < total && chars[j].is_whitespace() {
6903 j += 1;
6904 }
6905 if j >= total {
6906 return None;
6907 }
6908 return Some(idx_to_pos(j));
6909 }
6910 }
6911 i += 1;
6912 }
6913 None
6914 } else {
6915 let find_start = |from: usize| -> Option<usize> {
6919 let mut start = from;
6920 while start > 0 {
6921 let prev = chars[start - 1];
6922 if prev.is_whitespace() {
6923 let mut k = start - 1;
6924 while k > 0 && chars[k - 1].is_whitespace() {
6925 k -= 1;
6926 }
6927 if k > 0 && is_terminator(chars[k - 1]) {
6928 break;
6929 }
6930 }
6931 start -= 1;
6932 }
6933 while start < total && chars[start].is_whitespace() {
6934 start += 1;
6935 }
6936 (start < total).then_some(start)
6937 };
6938 let current_start = find_start(cursor_idx)?;
6939 if current_start < cursor_idx {
6940 return Some(idx_to_pos(current_start));
6941 }
6942 let mut k = current_start;
6945 while k > 0 && chars[k - 1].is_whitespace() {
6946 k -= 1;
6947 }
6948 if k == 0 {
6949 return None;
6950 }
6951 let prev_start = find_start(k - 1)?;
6952 Some(idx_to_pos(prev_start))
6953 }
6954}
6955
6956fn sentence_text_object<H: crate::types::Host>(
6962 ed: &Editor<hjkl_buffer::Buffer, H>,
6963 inner: bool,
6964 count: usize,
6965) -> Option<((usize, usize), (usize, usize))> {
6966 let count = count.max(1);
6967 let rope = crate::types::Query::rope(&ed.buffer);
6968 let n_lines = rope.len_lines();
6969 if n_lines == 0 {
6970 return None;
6971 }
6972 let line_lens: Vec<usize> = (0..n_lines)
6975 .map(|r| rope_line_to_str(&rope, r).chars().count())
6976 .collect();
6977 let pos_to_idx = |pos: (usize, usize)| -> usize {
6978 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6979 idx + pos.1
6980 };
6981 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6982 for (r, &len) in line_lens.iter().enumerate() {
6983 if idx <= len {
6984 return (r, idx);
6985 }
6986 idx -= len + 1;
6987 }
6988 let last = n_lines.saturating_sub(1);
6989 (last, line_lens[last])
6990 };
6991 let mut chars: Vec<char> = rope.chars().collect();
6992 if chars.last() == Some(&'\n') {
6993 chars.pop();
6994 }
6995 if chars.is_empty() {
6996 return None;
6997 }
6998
6999 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
7000 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
7001
7002 let mut start = cursor_idx;
7006 while start > 0 {
7007 let prev = chars[start - 1];
7008 if prev.is_whitespace() {
7009 let mut k = start - 1;
7013 while k > 0 && chars[k - 1].is_whitespace() {
7014 k -= 1;
7015 }
7016 if k > 0 && is_terminator(chars[k - 1]) {
7017 break;
7018 }
7019 }
7020 start -= 1;
7021 }
7022 while start < chars.len() && chars[start].is_whitespace() {
7025 start += 1;
7026 }
7027 if start >= chars.len() {
7028 return None;
7029 }
7030
7031 let mut end = start;
7034 while end < chars.len() {
7035 if is_terminator(chars[end]) {
7036 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
7038 end += 1;
7039 }
7040 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
7043 break;
7044 }
7045 }
7046 end += 1;
7047 }
7048 let mut rem = count - 1;
7051 while rem > 0 {
7052 let mut s = end + 1;
7053 while s < chars.len() && chars[s].is_whitespace() {
7054 s += 1;
7055 }
7056 if s >= chars.len() {
7057 break;
7058 }
7059 let mut e = s;
7060 while e < chars.len() {
7061 if is_terminator(chars[e]) {
7062 while e + 1 < chars.len() && is_terminator(chars[e + 1]) {
7063 e += 1;
7064 }
7065 if e + 1 >= chars.len() || chars[e + 1].is_whitespace() {
7066 break;
7067 }
7068 }
7069 e += 1;
7070 }
7071 end = e;
7072 rem -= 1;
7073 }
7074 let end_idx = (end + 1).min(chars.len());
7076
7077 let final_end = if inner {
7078 end_idx
7079 } else {
7080 let mut e = end_idx;
7084 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
7085 e += 1;
7086 }
7087 e
7088 };
7089
7090 Some((idx_to_pos(start), idx_to_pos(final_end)))
7091}
7092
7093fn tag_text_object<H: crate::types::Host>(
7097 ed: &Editor<hjkl_buffer::Buffer, H>,
7098 inner: bool,
7099) -> Option<((usize, usize), (usize, usize))> {
7100 let rope = crate::types::Query::rope(&ed.buffer);
7101 let n_lines = rope.len_lines();
7102 if n_lines == 0 {
7103 return None;
7104 }
7105 let line_lens: Vec<usize> = (0..n_lines)
7109 .map(|r| rope_line_to_str(&rope, r).chars().count())
7110 .collect();
7111 let pos_to_idx = |pos: (usize, usize)| -> usize {
7112 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
7113 idx + pos.1
7114 };
7115 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
7116 for (r, &len) in line_lens.iter().enumerate() {
7117 if idx <= len {
7118 return (r, idx);
7119 }
7120 idx -= len + 1;
7121 }
7122 let last = n_lines.saturating_sub(1);
7123 (last, line_lens[last])
7124 };
7125 let mut chars: Vec<char> = rope.chars().collect();
7126 if chars.last() == Some(&'\n') {
7127 chars.pop();
7128 }
7129 let cursor_idx = pos_to_idx(ed.cursor());
7130
7131 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
7139 let mut next_after: Option<(usize, usize, usize, usize)> = None;
7140 let mut i = 0;
7141 while i < chars.len() {
7142 if chars[i] != '<' {
7143 i += 1;
7144 continue;
7145 }
7146 let mut j = i + 1;
7147 while j < chars.len() && chars[j] != '>' {
7148 j += 1;
7149 }
7150 if j >= chars.len() {
7151 break;
7152 }
7153 let inside: String = chars[i + 1..j].iter().collect();
7154 let close_end = j + 1;
7155 let trimmed = inside.trim();
7156 if trimmed.starts_with('!') || trimmed.starts_with('?') {
7157 i = close_end;
7158 continue;
7159 }
7160 if let Some(rest) = trimmed.strip_prefix('/') {
7161 let name = rest.split_whitespace().next().unwrap_or("").to_string();
7162 if !name.is_empty()
7163 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
7164 {
7165 let (open_start, content_start, _) = stack[stack_idx].clone();
7166 stack.truncate(stack_idx);
7167 let content_end = i;
7168 let candidate = (open_start, content_start, content_end, close_end);
7169 if cursor_idx >= open_start && cursor_idx < close_end {
7176 innermost = match innermost {
7177 Some((os, _, _, ce)) if os <= open_start && close_end <= ce => {
7178 Some(candidate)
7179 }
7180 None => Some(candidate),
7181 existing => existing,
7182 };
7183 } else if open_start >= cursor_idx && next_after.is_none() {
7184 next_after = Some(candidate);
7185 }
7186 }
7187 } else if !trimmed.ends_with('/') {
7188 let name: String = trimmed
7189 .split(|c: char| c.is_whitespace() || c == '/')
7190 .next()
7191 .unwrap_or("")
7192 .to_string();
7193 if !name.is_empty() {
7194 stack.push((i, close_end, name));
7195 }
7196 }
7197 i = close_end;
7198 }
7199
7200 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
7201 if inner {
7202 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
7203 } else {
7204 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
7205 }
7206}
7207
7208fn is_wordchar(c: char) -> bool {
7209 c.is_alphanumeric() || c == '_'
7210}
7211
7212pub(crate) use hjkl_buffer::is_keyword_char;
7216
7217pub(crate) fn abbrev_kind(lhs: &str, iskeyword: &str) -> AbbrevKind {
7218 let chars: Vec<char> = lhs.chars().collect();
7219 if chars.is_empty() {
7220 return AbbrevKind::NonKw;
7221 }
7222 let last = *chars.last().unwrap();
7223 let last_is_kw = is_keyword_char(last, iskeyword);
7224 if !last_is_kw {
7225 return AbbrevKind::NonKw;
7226 }
7227 let all_kw = chars.iter().all(|&c| is_keyword_char(c, iskeyword));
7229 if all_kw {
7230 AbbrevKind::Full
7231 } else {
7232 AbbrevKind::End
7233 }
7234}
7235
7236pub(crate) fn try_abbrev_expand(
7254 abbrevs: &[Abbrev],
7255 line_before: &str,
7256 mincol: usize,
7257 trigger: AbbrevTrigger,
7258 iskeyword: &str,
7259) -> Option<(usize, String)> {
7260 let chars: Vec<char> = line_before.chars().collect();
7261 let cursor_col = chars.len(); for abbrev in abbrevs {
7264 if !abbrev.insert {
7265 continue;
7266 }
7267 let lhs_chars: Vec<char> = abbrev.lhs.chars().collect();
7268 if lhs_chars.is_empty() {
7269 continue;
7270 }
7271 let lhs_len = lhs_chars.len();
7272
7273 let kind = abbrev_kind(&abbrev.lhs, iskeyword);
7275
7276 match kind {
7278 AbbrevKind::Full | AbbrevKind::End => {
7279 let trigger_char_is_kw = match trigger {
7282 AbbrevTrigger::NonKeyword(c) => is_keyword_char(c, iskeyword),
7283 AbbrevTrigger::CtrlBracket | AbbrevTrigger::Cr | AbbrevTrigger::Esc => false,
7284 };
7285 if trigger_char_is_kw {
7286 continue;
7288 }
7289 }
7290 AbbrevKind::NonKw => {
7291 match trigger {
7293 AbbrevTrigger::Cr | AbbrevTrigger::Esc | AbbrevTrigger::CtrlBracket => {}
7294 AbbrevTrigger::NonKeyword(_) => continue,
7295 }
7296 }
7297 }
7298
7299 if cursor_col < lhs_len {
7301 continue;
7302 }
7303 let lhs_start_col = cursor_col - lhs_len;
7304
7305 if lhs_start_col < mincol {
7307 continue;
7308 }
7309
7310 let text_slice: &[char] = &chars[lhs_start_col..cursor_col];
7312 if text_slice != lhs_chars.as_slice() {
7313 continue;
7314 }
7315
7316 if lhs_start_col > 0 {
7318 let ch_before = chars[lhs_start_col - 1];
7319 match kind {
7320 AbbrevKind::Full => {
7321 if is_keyword_char(ch_before, iskeyword) {
7332 continue; }
7334 if lhs_len == 1 && ch_before != ' ' && ch_before != '\t' {
7335 continue;
7337 }
7338 }
7339 AbbrevKind::End => {
7340 }
7344 AbbrevKind::NonKw => {
7345 if ch_before != ' ' && ch_before != '\t' {
7348 continue;
7349 }
7350 }
7351 }
7352 }
7353 return Some((lhs_len, abbrev.rhs.clone()));
7357 }
7358
7359 None
7360}
7361
7362pub(crate) fn check_and_apply_abbrev<H: crate::types::Host>(
7371 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7372 trigger: AbbrevTrigger,
7373) -> bool {
7374 use hjkl_buffer::{Edit, Position};
7375
7376 let cursor = buf_cursor_pos(&ed.buffer);
7378 let row = cursor.row;
7379 let col = cursor.col;
7380 let line_before: String = {
7381 let line = buf_line(&ed.buffer, row).unwrap_or_default();
7382 line.chars().take(col).collect()
7383 };
7384 let (mincol, on_start_row) = if let Some(ref s) = ed.vim.insert_session {
7385 if row == s.start_row {
7386 (s.start_col, true)
7387 } else {
7388 (0, false)
7389 }
7390 } else {
7391 (0, false)
7392 };
7393 if on_start_row && col <= mincol {
7395 return false;
7396 }
7397
7398 let iskeyword = ed.settings.iskeyword.clone();
7399 let abbrevs = ed.vim.abbrevs.clone();
7400
7401 let Some((lhs_len, rhs)) =
7402 try_abbrev_expand(&abbrevs, &line_before, mincol, trigger, &iskeyword)
7403 else {
7404 return false;
7405 };
7406
7407 let lhs_start = col.saturating_sub(lhs_len);
7409 if lhs_len > 0 {
7410 ed.mutate_edit(Edit::DeleteRange {
7411 start: Position::new(row, lhs_start),
7412 end: Position::new(row, col),
7413 kind: hjkl_buffer::MotionKind::Char,
7414 });
7415 }
7416
7417 let insert_pos = Position::new(row, lhs_start);
7419 if !rhs.is_empty() {
7420 ed.mutate_edit(Edit::InsertStr {
7421 at: insert_pos,
7422 text: rhs.clone(),
7423 });
7424 }
7425
7426 let new_col = lhs_start + rhs.chars().count();
7428 buf_set_cursor_rc(&mut ed.buffer, row, new_col);
7429 ed.push_buffer_cursor_to_textarea();
7430
7431 true
7432}
7433
7434fn word_text_object<H: crate::types::Host>(
7435 ed: &Editor<hjkl_buffer::Buffer, H>,
7436 inner: bool,
7437 big: bool,
7438 count: usize,
7439) -> Option<((usize, usize), (usize, usize))> {
7440 let count = count.max(1);
7441 let (row, col) = ed.cursor();
7442 let line = buf_line(&ed.buffer, row)?;
7443 let chars: Vec<char> = line.chars().collect();
7444 if chars.is_empty() {
7445 return None;
7446 }
7447 let len = chars.len();
7448 let at = col.min(len.saturating_sub(1));
7449 let classify = |c: char| -> u8 {
7450 if c.is_whitespace() {
7451 0
7452 } else if big || is_wordchar(c) {
7453 1
7454 } else {
7455 2
7456 }
7457 };
7458 let cls = classify(chars[at]);
7459 let mut start = at;
7460 while start > 0 && classify(chars[start - 1]) == cls {
7461 start -= 1;
7462 }
7463 let mut end = at;
7464 while end + 1 < len && classify(chars[end + 1]) == cls {
7465 end += 1;
7466 }
7467 let mut start_col = start;
7473 let end_col;
7476 if inner {
7477 let mut rem = count - 1;
7480 while rem > 0 && end + 1 < len {
7481 let next_kind = classify(chars[end + 1]);
7482 end += 1;
7483 while end + 1 < len && classify(chars[end + 1]) == next_kind {
7484 end += 1;
7485 }
7486 rem -= 1;
7487 }
7488 end_col = end + 1;
7489 } else if cls == 0 {
7490 let mut e = end;
7495 let mut rem = count;
7496 while rem > 0 && e + 1 < len {
7497 while e + 1 < len && chars[e + 1].is_whitespace() {
7500 e += 1;
7501 }
7502 if e + 1 >= len {
7503 break;
7504 }
7505 e += 1;
7507 let k = classify(chars[e]);
7508 while e + 1 < len && classify(chars[e + 1]) == k {
7509 e += 1;
7510 }
7511 rem -= 1;
7512 }
7513 end_col = e + 1;
7514 } else {
7515 let mut e = end;
7520 let mut words_done = 1;
7521 let mut included_trailing = false;
7522 loop {
7523 let mut t = e + 1;
7524 let mut got_ws = false;
7525 while t < len && chars[t].is_whitespace() {
7526 got_ws = true;
7527 t += 1;
7528 }
7529 if words_done == count {
7530 if got_ws {
7531 e = t - 1;
7532 included_trailing = true;
7533 }
7534 break;
7535 }
7536 if t >= len {
7537 break; }
7539 e = t;
7541 let k = classify(chars[e]);
7542 while e + 1 < len && classify(chars[e + 1]) == k {
7543 e += 1;
7544 }
7545 words_done += 1;
7546 }
7547 end_col = e + 1;
7548 if !included_trailing {
7549 let mut s = start;
7550 while s > 0 && chars[s - 1].is_whitespace() {
7551 s -= 1;
7552 }
7553 start_col = s;
7554 }
7555 }
7556 Some(((row, start_col), (row, end_col)))
7557}
7558
7559fn quote_text_object<H: crate::types::Host>(
7560 ed: &Editor<hjkl_buffer::Buffer, H>,
7561 q: char,
7562 inner: bool,
7563) -> Option<((usize, usize), (usize, usize))> {
7564 let (row, col) = ed.cursor();
7565 let line = buf_line(&ed.buffer, row)?;
7566 let chars: Vec<char> = line.chars().collect();
7571 let mut positions: Vec<usize> = Vec::new();
7573 for (i, &c) in chars.iter().enumerate() {
7574 if c == q {
7575 positions.push(i);
7576 }
7577 }
7578 if positions.len() < 2 {
7579 return None;
7580 }
7581 let mut open_idx: Option<usize> = None;
7582 let mut close_idx: Option<usize> = None;
7583 for pair in positions.chunks(2) {
7584 if pair.len() < 2 {
7585 break;
7586 }
7587 if col >= pair[0] && col <= pair[1] {
7588 open_idx = Some(pair[0]);
7589 close_idx = Some(pair[1]);
7590 break;
7591 }
7592 if col < pair[0] {
7593 open_idx = Some(pair[0]);
7594 close_idx = Some(pair[1]);
7595 break;
7596 }
7597 }
7598 let open = open_idx?;
7599 let close = close_idx?;
7600 if inner {
7602 if close <= open + 1 {
7603 return None;
7604 }
7605 Some(((row, open + 1), (row, close)))
7606 } else {
7607 let after_close = close + 1; if after_close < chars.len() && chars[after_close].is_ascii_whitespace() {
7614 let mut end = after_close;
7616 while end < chars.len() && chars[end].is_ascii_whitespace() {
7617 end += 1;
7618 }
7619 Some(((row, open), (row, end)))
7620 } else if open > 0 && chars[open - 1].is_ascii_whitespace() {
7621 let mut start = open;
7623 while start > 0 && chars[start - 1].is_ascii_whitespace() {
7624 start -= 1;
7625 }
7626 Some(((row, start), (row, close + 1)))
7627 } else {
7628 Some(((row, open), (row, close + 1)))
7629 }
7630 }
7631}
7632
7633fn bracket_text_object<H: crate::types::Host>(
7634 ed: &Editor<hjkl_buffer::Buffer, H>,
7635 open: char,
7636 inner: bool,
7637 count: usize,
7638) -> Option<(Pos, Pos, RangeKind)> {
7639 let close = match open {
7640 '(' => ')',
7641 '[' => ']',
7642 '{' => '}',
7643 '<' => '>',
7644 _ => return None,
7645 };
7646 let (row, col) = ed.cursor();
7647 let lines = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
7648 let lines = lines.as_slice();
7649 let cursor_char = lines.get(row).and_then(|l| l.chars().nth(col));
7655 let (open_pos, close_pos) = if cursor_char == Some(close) {
7656 let open_pos = if col > 0 {
7657 find_open_bracket(lines, row, col - 1, open, close)
7658 } else if row > 0 {
7659 let pr = row - 1;
7660 let pc = lines[pr].chars().count().saturating_sub(1);
7661 find_open_bracket(lines, pr, pc, open, close)
7662 } else {
7663 None
7664 }?;
7665 (open_pos, (row, col))
7666 } else {
7667 let open_pos = find_open_bracket(lines, row, col, open, close)
7672 .or_else(|| find_next_open(lines, row, col, open))?;
7673 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
7674 (open_pos, close_pos)
7675 };
7676 let (open_pos, close_pos) = {
7680 let (mut op, mut cp) = (open_pos, close_pos);
7681 for _ in 1..count.max(1) {
7682 let outer = if op.1 > 0 {
7683 find_open_bracket(lines, op.0, op.1 - 1, open, close)
7684 } else if op.0 > 0 {
7685 let pr = op.0 - 1;
7686 let pc = lines[pr].chars().count().saturating_sub(1);
7687 find_open_bracket(lines, pr, pc, open, close)
7688 } else {
7689 None
7690 };
7691 let Some(oo) = outer else { break };
7692 let Some(oc) = find_close_bracket(lines, oo.0, oo.1 + 1, open, close) else {
7693 break;
7694 };
7695 op = oo;
7696 cp = oc;
7697 }
7698 (op, cp)
7699 };
7700 if inner {
7702 let open_line_len = lines[open_pos.0].chars().count();
7713 let inner_start = if open_pos.1 + 1 >= open_line_len && open_pos.0 + 1 < lines.len() {
7714 (open_pos.0 + 1, 0)
7715 } else {
7716 advance_pos(lines, open_pos)
7717 };
7718 if inner_start.0 > close_pos.0
7721 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
7722 {
7723 return Some((inner_start, inner_start, RangeKind::Exclusive));
7724 }
7725 if close_pos.0 > open_pos.0 {
7732 let mut saw_ws = false;
7733 let mut saw_other = false;
7734 for r in inner_start.0..=close_pos.0 {
7735 let line: Vec<char> = lines
7736 .get(r)
7737 .map(|l| l.chars().collect())
7738 .unwrap_or_default();
7739 let from = if r == inner_start.0 { inner_start.1 } else { 0 };
7740 let to = if r == close_pos.0 {
7741 close_pos.1
7742 } else {
7743 line.len()
7744 };
7745 for &c in line
7746 .iter()
7747 .take(to.min(line.len()))
7748 .skip(from.min(line.len()))
7749 {
7750 if c == ' ' || c == '\t' {
7751 saw_ws = true;
7752 } else {
7753 saw_other = true;
7754 }
7755 }
7756 }
7757 if saw_ws && !saw_other {
7758 return Some((inner_start, inner_start, RangeKind::Exclusive));
7759 }
7760 }
7761 Some((inner_start, close_pos, RangeKind::Exclusive))
7762 } else {
7763 Some((
7764 open_pos,
7765 advance_pos(lines, close_pos),
7766 RangeKind::Exclusive,
7767 ))
7768 }
7769}
7770
7771fn find_open_bracket(
7772 lines: &[String],
7773 row: usize,
7774 col: usize,
7775 open: char,
7776 close: char,
7777) -> Option<(usize, usize)> {
7778 let mut depth: i32 = 0;
7779 let mut r = row;
7780 let mut c = col as isize;
7781 loop {
7782 let cur = &lines[r];
7783 let chars: Vec<char> = cur.chars().collect();
7784 if (c as usize) >= chars.len() {
7788 c = chars.len() as isize - 1;
7789 }
7790 while c >= 0 {
7791 let ch = chars[c as usize];
7792 if ch == close {
7793 depth += 1;
7794 } else if ch == open {
7795 if depth == 0 {
7796 return Some((r, c as usize));
7797 }
7798 depth -= 1;
7799 }
7800 c -= 1;
7801 }
7802 if r == 0 {
7803 return None;
7804 }
7805 r -= 1;
7806 c = lines[r].chars().count() as isize - 1;
7807 }
7808}
7809
7810fn find_close_bracket(
7811 lines: &[String],
7812 row: usize,
7813 start_col: usize,
7814 open: char,
7815 close: char,
7816) -> Option<(usize, usize)> {
7817 let mut depth: i32 = 0;
7818 let mut r = row;
7819 let mut c = start_col;
7820 loop {
7821 let cur = &lines[r];
7822 let chars: Vec<char> = cur.chars().collect();
7823 while c < chars.len() {
7824 let ch = chars[c];
7825 if ch == open {
7826 depth += 1;
7827 } else if ch == close {
7828 if depth == 0 {
7829 return Some((r, c));
7830 }
7831 depth -= 1;
7832 }
7833 c += 1;
7834 }
7835 if r + 1 >= lines.len() {
7836 return None;
7837 }
7838 r += 1;
7839 c = 0;
7840 }
7841}
7842
7843fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
7847 let mut r = row;
7848 let mut c = col;
7849 while r < lines.len() {
7850 let chars: Vec<char> = lines[r].chars().collect();
7851 while c < chars.len() {
7852 if chars[c] == open {
7853 return Some((r, c));
7854 }
7855 c += 1;
7856 }
7857 r += 1;
7858 c = 0;
7859 }
7860 None
7861}
7862
7863fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
7864 let (r, c) = pos;
7865 let line_len = lines[r].chars().count();
7866 if c < line_len {
7867 (r, c + 1)
7868 } else if r + 1 < lines.len() {
7869 (r + 1, 0)
7870 } else {
7871 pos
7872 }
7873}
7874
7875fn paragraph_text_object<H: crate::types::Host>(
7876 ed: &Editor<hjkl_buffer::Buffer, H>,
7877 inner: bool,
7878 count: usize,
7879) -> Option<((usize, usize), (usize, usize))> {
7880 let count = count.max(1);
7881 let (row, _) = ed.cursor();
7882 let rope = crate::types::Query::rope(&ed.buffer);
7883 let n_lines = rope.len_lines();
7884 if n_lines == 0 {
7885 return None;
7886 }
7887 let is_blank = |r: usize| -> bool {
7889 if r >= n_lines {
7890 return true;
7891 }
7892 rope_line_to_str(&rope, r).trim().is_empty()
7893 };
7894 if is_blank(row) {
7895 return None;
7896 }
7897 let mut top = row;
7898 while top > 0 && !is_blank(top - 1) {
7899 top -= 1;
7900 }
7901 let mut bot = row;
7902 while bot + 1 < n_lines && !is_blank(bot + 1) {
7903 bot += 1;
7904 }
7905 if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
7907 bot += 1;
7908 }
7909 let mut rem = count - 1;
7915 while rem > 0 && bot + 1 < n_lines {
7916 if inner {
7917 let blank_next = is_blank(bot + 1);
7918 bot += 1;
7919 while bot + 1 < n_lines && is_blank(bot + 1) == blank_next {
7920 bot += 1;
7921 }
7922 } else {
7923 while bot + 1 < n_lines && !is_blank(bot + 1) {
7924 bot += 1;
7925 }
7926 while bot + 1 < n_lines && is_blank(bot + 1) {
7927 bot += 1;
7928 }
7929 }
7930 rem -= 1;
7931 }
7932 let end_col = rope_line_to_str(&rope, bot).chars().count();
7933 Some(((top, 0), (bot, end_col)))
7934}
7935
7936fn read_vim_range<H: crate::types::Host>(
7942 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7943 start: (usize, usize),
7944 end: (usize, usize),
7945 kind: RangeKind,
7946) -> String {
7947 let (top, bot) = order(start, end);
7948 ed.sync_buffer_content_from_textarea();
7949 let rope = crate::types::Query::rope(&ed.buffer);
7950 let n_lines = rope.len_lines();
7951 match kind {
7952 RangeKind::Linewise => {
7953 let lo = top.0;
7954 let hi = bot.0.min(n_lines.saturating_sub(1));
7955 let mut text = rope_row_range_str(&rope, lo, hi);
7956 text.push('\n');
7957 text
7958 }
7959 RangeKind::Inclusive | RangeKind::Exclusive => {
7960 let inclusive = matches!(kind, RangeKind::Inclusive);
7961 let mut out = String::new();
7963 for row in top.0..=bot.0 {
7964 if row >= n_lines {
7965 break;
7966 }
7967 let line = rope_line_to_str(&rope, row);
7968 let lo = if row == top.0 { top.1 } else { 0 };
7969 let hi_unclamped = if row == bot.0 {
7970 if inclusive { bot.1 + 1 } else { bot.1 }
7971 } else {
7972 line.chars().count() + 1
7973 };
7974 let row_chars: Vec<char> = line.chars().collect();
7975 let hi = hi_unclamped.min(row_chars.len());
7976 if lo < hi {
7977 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
7978 }
7979 if row < bot.0 {
7980 out.push('\n');
7981 }
7982 }
7983 out
7984 }
7985 }
7986}
7987
7988fn cut_vim_range<H: crate::types::Host>(
7997 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7998 start: (usize, usize),
7999 end: (usize, usize),
8000 kind: RangeKind,
8001) -> String {
8002 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
8003 let (top, bot) = order(start, end);
8004 ed.sync_buffer_content_from_textarea();
8005 let (buf_start, buf_end, buf_kind) = match kind {
8006 RangeKind::Linewise => (
8007 Position::new(top.0, 0),
8008 Position::new(bot.0, 0),
8009 BufKind::Line,
8010 ),
8011 RangeKind::Inclusive => {
8012 let line_chars = buf_line_chars(&ed.buffer, bot.0);
8013 let next = if bot.1 < line_chars {
8017 Position::new(bot.0, bot.1 + 1)
8018 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
8019 Position::new(bot.0 + 1, 0)
8020 } else {
8021 Position::new(bot.0, line_chars)
8022 };
8023 (Position::new(top.0, top.1), next, BufKind::Char)
8024 }
8025 RangeKind::Exclusive => (
8026 Position::new(top.0, top.1),
8027 Position::new(bot.0, bot.1),
8028 BufKind::Char,
8029 ),
8030 };
8031 let inverse = ed.mutate_edit(Edit::DeleteRange {
8032 start: buf_start,
8033 end: buf_end,
8034 kind: buf_kind,
8035 });
8036 let text = match inverse {
8037 Edit::InsertStr { text, .. } => text,
8038 _ => String::new(),
8039 };
8040 if !text.is_empty() {
8041 ed.record_yank_to_host(text.clone());
8042 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
8043 }
8044 ed.push_buffer_cursor_to_textarea();
8045 text
8046}
8047
8048fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8054 use hjkl_buffer::{Edit, MotionKind, Position};
8055 ed.sync_buffer_content_from_textarea();
8056 let cursor = buf_cursor_pos(&ed.buffer);
8057 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8058 if cursor.col >= line_chars {
8059 return;
8060 }
8061 let inverse = ed.mutate_edit(Edit::DeleteRange {
8062 start: cursor,
8063 end: Position::new(cursor.row, line_chars),
8064 kind: MotionKind::Char,
8065 });
8066 if let Edit::InsertStr { text, .. } = inverse
8067 && !text.is_empty()
8068 {
8069 ed.record_yank_to_host(text.clone());
8070 ed.vim.yank_linewise = false;
8071 ed.set_yank(text);
8072 }
8073 buf_set_cursor_pos(&mut ed.buffer, cursor);
8074 ed.push_buffer_cursor_to_textarea();
8075}
8076
8077fn do_char_delete<H: crate::types::Host>(
8078 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8079 forward: bool,
8080 count: usize,
8081) {
8082 use hjkl_buffer::{Edit, MotionKind, Position};
8083 ed.push_undo();
8084 ed.sync_buffer_content_from_textarea();
8085 let mut deleted = String::new();
8088 for _ in 0..count {
8089 let cursor = buf_cursor_pos(&ed.buffer);
8090 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8091 if forward {
8092 if cursor.col >= line_chars {
8098 break;
8099 }
8100 let inverse = ed.mutate_edit(Edit::DeleteRange {
8101 start: cursor,
8102 end: Position::new(cursor.row, cursor.col + 1),
8103 kind: MotionKind::Char,
8104 });
8105 if let Edit::InsertStr { text, .. } = inverse {
8106 deleted.push_str(&text);
8107 }
8108 } else {
8109 if cursor.col == 0 {
8112 break;
8113 }
8114 let inverse = ed.mutate_edit(Edit::DeleteRange {
8115 start: Position::new(cursor.row, cursor.col - 1),
8116 end: cursor,
8117 kind: MotionKind::Char,
8118 });
8119 if let Edit::InsertStr { text, .. } = inverse {
8120 deleted = text + &deleted;
8123 }
8124 }
8125 }
8126 if !deleted.is_empty() {
8127 ed.record_yank_to_host(deleted.clone());
8128 ed.record_delete(deleted, false);
8129 }
8130 ed.push_buffer_cursor_to_textarea();
8131}
8132
8133pub(crate) fn adjust_number<H: crate::types::Host>(
8138 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8139 delta: i64,
8140) -> bool {
8141 use hjkl_buffer::{Edit, MotionKind, Position};
8142 ed.sync_buffer_content_from_textarea();
8143 let cursor = buf_cursor_pos(&ed.buffer);
8144 let row = cursor.row;
8145 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
8146 Some(l) => l.chars().collect(),
8147 None => return false,
8148 };
8149 let len = chars.len();
8150
8151 let is_hex_prefix = |i: usize| {
8154 chars[i] == '0'
8155 && i + 1 < len
8156 && matches!(chars[i + 1], 'x' | 'X')
8157 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
8158 };
8159 let mut i = cursor.col;
8160 let mut hex = false;
8161 loop {
8162 if i >= len {
8163 return false;
8164 }
8165 if is_hex_prefix(i) {
8166 hex = true;
8167 break;
8168 }
8169 if chars[i].is_ascii_digit() {
8170 break;
8171 }
8172 i += 1;
8173 }
8174
8175 let (span_start, span_end, new_s) = if hex {
8176 let digits_start = i + 2;
8178 let mut digits_end = digits_start;
8179 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
8180 digits_end += 1;
8181 }
8182 let hexs: String = chars[digits_start..digits_end].iter().collect();
8183 let Ok(n) = u64::from_str_radix(&hexs, 16) else {
8184 return false;
8185 };
8186 let new_val = (n as i128 + delta as i128).max(0) as u64;
8187 let width = digits_end - digits_start;
8188 let prefix: String = chars[i..digits_start].iter().collect();
8189 (i, digits_end, format!("{prefix}{new_val:0width$x}"))
8190 } else {
8191 let digit_start = i;
8193 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
8194 digit_start - 1
8195 } else {
8196 digit_start
8197 };
8198 let mut span_end = digit_start;
8199 while span_end < len && chars[span_end].is_ascii_digit() {
8200 span_end += 1;
8201 }
8202 let s: String = chars[span_start..span_end].iter().collect();
8203 let Ok(n) = s.parse::<i64>() else {
8204 return false;
8205 };
8206 (span_start, span_end, n.saturating_add(delta).to_string())
8207 };
8208
8209 ed.push_undo();
8210 let span_start_pos = Position::new(row, span_start);
8211 let span_end_pos = Position::new(row, span_end);
8212 ed.mutate_edit(Edit::DeleteRange {
8213 start: span_start_pos,
8214 end: span_end_pos,
8215 kind: MotionKind::Char,
8216 });
8217 ed.mutate_edit(Edit::InsertStr {
8218 at: span_start_pos,
8219 text: new_s.clone(),
8220 });
8221 let new_len = new_s.chars().count();
8222 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
8223 ed.push_buffer_cursor_to_textarea();
8224 true
8225}
8226
8227pub(crate) fn replace_char<H: crate::types::Host>(
8228 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8229 ch: char,
8230 count: usize,
8231) {
8232 use hjkl_buffer::{Edit, MotionKind, Position};
8233 ed.sync_buffer_content_from_textarea();
8234 let start = buf_cursor_pos(&ed.buffer);
8238 let start_line_chars = buf_line_chars(&ed.buffer, start.row);
8239 if count == 0 || start.col + count > start_line_chars {
8240 return;
8241 }
8242 ed.push_undo();
8243 for _ in 0..count {
8244 let cursor = buf_cursor_pos(&ed.buffer);
8245 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8246 if cursor.col >= line_chars {
8247 break;
8248 }
8249 ed.mutate_edit(Edit::DeleteRange {
8250 start: cursor,
8251 end: Position::new(cursor.row, cursor.col + 1),
8252 kind: MotionKind::Char,
8253 });
8254 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
8255 }
8256 crate::motions::move_left(&mut ed.buffer, 1);
8258 ed.push_buffer_cursor_to_textarea();
8259}
8260
8261fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
8265 use hjkl_buffer::{Edit, MotionKind, Position};
8266 ed.sync_buffer_content_from_textarea();
8267 let cursor = buf_cursor_pos(&ed.buffer);
8268 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
8269 return false;
8270 };
8271 let toggled = if c.is_uppercase() {
8272 c.to_lowercase().next().unwrap_or(c)
8273 } else {
8274 c.to_uppercase().next().unwrap_or(c)
8275 };
8276 ed.mutate_edit(Edit::DeleteRange {
8277 start: cursor,
8278 end: Position::new(cursor.row, cursor.col + 1),
8279 kind: MotionKind::Char,
8280 });
8281 ed.mutate_edit(Edit::InsertChar {
8282 at: cursor,
8283 ch: toggled,
8284 });
8285 true
8286}
8287
8288fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
8291 use hjkl_buffer::{Edit, Position};
8292 ed.sync_buffer_content_from_textarea();
8293 let row = buf_cursor_pos(&ed.buffer).row;
8294 if row + 1 >= buf_row_count(&ed.buffer) {
8295 return false;
8296 }
8297 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8298 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
8299 let next_trimmed = next_raw.trim_start();
8300 let cur_chars = cur_line.chars().count();
8301 let next_chars = next_raw.chars().count();
8302 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
8305 " "
8306 } else {
8307 ""
8308 };
8309 let joined = format!("{cur_line}{separator}{next_trimmed}");
8310 ed.mutate_edit(Edit::Replace {
8311 start: Position::new(row, 0),
8312 end: Position::new(row + 1, next_chars),
8313 with: joined,
8314 });
8315 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
8319 ed.push_buffer_cursor_to_textarea();
8320 true
8321}
8322
8323fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
8327 use hjkl_buffer::Edit;
8328 ed.sync_buffer_content_from_textarea();
8329 let row = buf_cursor_pos(&ed.buffer).row;
8330 if row + 1 >= buf_row_count(&ed.buffer) {
8331 return false;
8332 }
8333 let join_col = buf_line_chars(&ed.buffer, row);
8334 ed.mutate_edit(Edit::JoinLines {
8335 row,
8336 count: 1,
8337 with_space: false,
8338 });
8339 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
8341 ed.push_buffer_cursor_to_textarea();
8342 true
8343}
8344
8345pub(crate) fn visual_join<H: crate::types::Host>(
8349 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8350 with_space: bool,
8351) {
8352 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8353 let (top, bot) = match ed.vim.mode {
8354 Mode::VisualLine => (
8355 cursor_row.min(ed.vim.visual_line_anchor),
8356 cursor_row.max(ed.vim.visual_line_anchor),
8357 ),
8358 Mode::VisualBlock => {
8359 let a = ed.vim.block_anchor.0;
8360 (a.min(cursor_row), a.max(cursor_row))
8361 }
8362 Mode::Visual => {
8363 let a = ed.vim.visual_anchor.0;
8364 (a.min(cursor_row), a.max(cursor_row))
8365 }
8366 _ => return,
8367 };
8368 let joins = (bot - top).max(1);
8371 ed.push_undo();
8372 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8373 ed.push_buffer_cursor_to_textarea();
8374 for _ in 0..joins {
8375 let joined = if with_space {
8376 join_line(ed)
8377 } else {
8378 join_line_raw(ed)
8379 };
8380 if !joined {
8381 break;
8382 }
8383 }
8384 ed.vim.mode = Mode::Normal;
8385 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8386}
8387
8388pub(crate) fn goto_percent<H: crate::types::Host>(
8391 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8392 count: usize,
8393) {
8394 let rows = buf_row_count(&ed.buffer);
8395 if rows == 0 {
8396 return;
8397 }
8398 let total = if rows >= 2
8401 && buf_line(&ed.buffer, rows - 1)
8402 .map(|s| s.is_empty())
8403 .unwrap_or(false)
8404 {
8405 rows - 1
8406 } else {
8407 rows
8408 };
8409 let line = count.saturating_mul(total).div_ceil(100).clamp(1, total);
8413 let pre = ed.cursor();
8414 ed.jump_cursor(line - 1, 0);
8415 move_first_non_whitespace(ed);
8416 ed.sticky_col = Some(ed.cursor().1);
8417 if ed.cursor() != pre {
8418 ed.push_jump(pre);
8419 }
8420}
8421
8422fn indent_width(s: &str, tabstop: usize) -> usize {
8425 let ts = tabstop.max(1);
8426 let mut w = 0usize;
8427 for c in s.chars() {
8428 match c {
8429 ' ' => w += 1,
8430 '\t' => w += ts - (w % ts),
8431 _ => break,
8432 }
8433 }
8434 w
8435}
8436
8437fn build_indent(width: usize, settings: &crate::editor::Settings) -> String {
8440 if settings.expandtab {
8441 return " ".repeat(width);
8442 }
8443 let ts = settings.tabstop.max(1);
8444 let tabs = width / ts;
8445 let spaces = width % ts;
8446 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
8447}
8448
8449fn reindent_block(text: &str, target_width: usize, settings: &crate::editor::Settings) -> String {
8452 let ts = settings.tabstop.max(1);
8453 let lines: Vec<&str> = text.split('\n').collect();
8454 let first_width = lines.first().map(|l| indent_width(l, ts)).unwrap_or(0);
8455 let delta = target_width as isize - first_width as isize;
8456 lines
8457 .iter()
8458 .map(|line| {
8459 let trimmed = line.trim_start_matches([' ', '\t']);
8460 if trimmed.is_empty() {
8461 return String::new();
8463 }
8464 let old_w = indent_width(line, ts) as isize;
8465 let new_w = (old_w + delta).max(0) as usize;
8466 format!("{}{}", build_indent(new_w, settings), trimmed)
8467 })
8468 .collect::<Vec<_>>()
8469 .join("\n")
8470}
8471
8472fn do_paste<H: crate::types::Host>(
8473 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8474 before: bool,
8475 count: usize,
8476 cursor_after: bool,
8477 reindent: bool,
8478) {
8479 use hjkl_buffer::{Edit, Position};
8480 ed.push_undo();
8481 let selector = ed.vim.pending_register.take();
8486 let (yank, linewise) = {
8487 let regs = ed.registers();
8488 match selector.and_then(|c| regs.read(c)) {
8489 Some(slot) => (slot.text.clone(), slot.linewise),
8490 None => (regs.unnamed.text.clone(), regs.unnamed.linewise),
8496 }
8497 };
8498 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
8502 let original_row_for_linewise_after = if linewise && !before {
8508 let r = buf_cursor_pos(&ed.buffer).row;
8511 let (_, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, r, r);
8512 Some(fold_end)
8513 } else {
8514 None
8515 };
8516 if yank.is_empty() {
8519 return;
8520 }
8521 for _ in 0..count {
8522 ed.sync_buffer_content_from_textarea();
8523 let yank = yank.clone();
8524 if linewise {
8525 let mut text = yank.trim_matches('\n').to_string();
8529 let row = buf_cursor_pos(&ed.buffer).row;
8530 if reindent {
8532 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8533 let target_w = indent_width(&cur_line, ed.settings.tabstop.max(1));
8534 text = reindent_block(&text, target_w, &ed.settings);
8535 }
8536 let (fold_start, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, row, row);
8540 let target_row = if before {
8541 ed.mutate_edit(Edit::InsertStr {
8542 at: Position::new(fold_start, 0),
8543 text: format!("{text}\n"),
8544 });
8545 fold_start
8546 } else {
8547 let line_chars = buf_line_chars(&ed.buffer, fold_end);
8548 ed.mutate_edit(Edit::InsertStr {
8549 at: Position::new(fold_end, line_chars),
8550 text: format!("\n{text}"),
8551 });
8552 fold_end + 1
8553 };
8554 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
8555 crate::motions::move_first_non_blank(&mut ed.buffer);
8556 ed.push_buffer_cursor_to_textarea();
8557 let payload_lines = text.lines().count().max(1);
8559 let bot_row = target_row + payload_lines - 1;
8560 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
8561 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
8562 } else {
8563 let cursor = buf_cursor_pos(&ed.buffer);
8567 let at = if before {
8568 cursor
8569 } else {
8570 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8571 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
8572 };
8573 ed.mutate_edit(Edit::InsertStr {
8574 at,
8575 text: yank.clone(),
8576 });
8577 if !cursor_after && ed.cursor().1 > 0 {
8582 crate::motions::move_left(&mut ed.buffer, 1);
8583 ed.push_buffer_cursor_to_textarea();
8584 }
8585 let lo = (at.row, at.col);
8587 let hi = if cursor_after {
8588 let c = ed.cursor();
8589 (c.0, c.1.saturating_sub(1))
8590 } else {
8591 ed.cursor()
8592 };
8593 paste_mark = Some((lo, hi));
8594 }
8595 }
8596 if let Some((lo, hi)) = paste_mark {
8597 ed.set_mark('[', lo);
8598 ed.set_mark(']', hi);
8599 }
8600 if cursor_after && linewise {
8603 if let Some((_, (bot_row, _))) = paste_mark {
8604 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
8605 let target = (bot_row + 1).min(last_row);
8606 buf_set_cursor_rc(&mut ed.buffer, target, 0);
8607 ed.push_buffer_cursor_to_textarea();
8608 }
8609 } else if let Some(orig_row) = original_row_for_linewise_after {
8610 let first_target = orig_row.saturating_add(1);
8615 buf_set_cursor_rc(&mut ed.buffer, first_target, 0);
8616 crate::motions::move_first_non_blank(&mut ed.buffer);
8617 ed.push_buffer_cursor_to_textarea();
8618 }
8619 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8621}
8622
8623pub(crate) fn visual_paste<H: crate::types::Host>(
8628 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8629 before: bool,
8630) {
8631 use hjkl_buffer::{Edit, Position};
8632 ed.sync_buffer_content_from_textarea();
8633
8634 let selector = ed.vim.pending_register.take();
8637 let (reg_text, reg_linewise) = {
8638 let regs = ed.registers();
8639 match selector.and_then(|c| regs.read(c)) {
8640 Some(slot) => (slot.text.clone(), slot.linewise),
8641 None => (regs.unnamed.text.clone(), regs.unnamed.linewise),
8642 }
8643 };
8644 let saved_unnamed = before.then(|| ed.registers().unnamed.clone());
8646
8647 let mode = ed.vim.mode;
8648 ed.push_undo();
8649
8650 match mode {
8651 Mode::VisualLine => {
8652 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8653 let top = cursor_row.min(ed.vim.visual_line_anchor);
8654 let bot = cursor_row.max(ed.vim.visual_line_anchor);
8655 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
8657 let text = reg_text.trim_matches('\n').to_string();
8659 let line_count = buf_row_count(&ed.buffer);
8660 if top >= line_count {
8661 let last = line_count.saturating_sub(1);
8664 let lc = buf_line_chars(&ed.buffer, last);
8665 ed.mutate_edit(Edit::InsertStr {
8666 at: Position::new(last, lc),
8667 text: format!("\n{text}"),
8668 });
8669 buf_set_cursor_rc(&mut ed.buffer, last + 1, 0);
8670 } else {
8671 ed.mutate_edit(Edit::InsertStr {
8672 at: Position::new(top, 0),
8673 text: format!("{text}\n"),
8674 });
8675 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8676 }
8677 crate::motions::move_first_non_blank(&mut ed.buffer);
8678 ed.push_buffer_cursor_to_textarea();
8679 }
8680 Mode::Visual | Mode::VisualBlock => {
8681 let anchor = if mode == Mode::VisualBlock {
8682 ed.vim.block_anchor
8683 } else {
8684 ed.vim.visual_anchor
8685 };
8686 let cursor = ed.cursor();
8687 let (top, bot) = order(anchor, cursor);
8688 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
8690 if reg_linewise {
8692 let text = reg_text.trim_matches('\n').to_string();
8694 let lc = buf_line_chars(&ed.buffer, top.0);
8695 ed.mutate_edit(Edit::InsertStr {
8696 at: Position::new(top.0, lc),
8697 text: format!("\n{text}"),
8698 });
8699 buf_set_cursor_rc(&mut ed.buffer, top.0 + 1, 0);
8700 crate::motions::move_first_non_blank(&mut ed.buffer);
8701 } else {
8702 ed.mutate_edit(Edit::InsertStr {
8703 at: Position::new(top.0, top.1),
8704 text: reg_text.clone(),
8705 });
8706 let inserted_len = reg_text.chars().count();
8708 let last_col = top.1 + inserted_len.saturating_sub(1);
8709 buf_set_cursor_rc(&mut ed.buffer, top.0, last_col);
8710 }
8711 ed.push_buffer_cursor_to_textarea();
8712 }
8713 _ => {}
8714 }
8715
8716 if let Some(slot) = saved_unnamed {
8718 ed.registers_mut().unnamed = slot;
8719 }
8720 ed.vim.mode = Mode::Normal;
8721 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8722}
8723
8724pub(crate) fn adjust_number_visual<H: crate::types::Host>(
8729 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8730 delta: i64,
8731 sequential: bool,
8732) {
8733 use hjkl_buffer::{Edit, MotionKind, Position};
8734 ed.sync_buffer_content_from_textarea();
8735 let mode = ed.vim.mode;
8736 let cursor = buf_cursor_pos(&ed.buffer);
8737
8738 let (top, bot, mut scan_col_first, block_left) = match mode {
8740 Mode::VisualLine => {
8741 let t = cursor.row.min(ed.vim.visual_line_anchor);
8742 let b = cursor.row.max(ed.vim.visual_line_anchor);
8743 (t, b, 0usize, None)
8744 }
8745 Mode::Visual => {
8746 let (a, c) = order(ed.vim.visual_anchor, (cursor.row, cursor.col));
8747 (a.0, c.0, a.1, None)
8748 }
8749 Mode::VisualBlock => {
8750 let (a, c) = order(ed.vim.block_anchor, (cursor.row, cursor.col));
8751 let left = a.1.min(c.1);
8752 (a.0, c.0, left, Some(left))
8753 }
8754 _ => return,
8755 };
8756
8757 ed.push_undo();
8758 let mut found_count: i64 = 0;
8759 for row in top..=bot {
8760 let start_col = match block_left {
8761 Some(left) => left,
8762 None => {
8763 let c = if row == top { scan_col_first } else { 0 };
8766 scan_col_first = 0;
8767 c
8768 }
8769 };
8770 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
8771 Some(l) => l.chars().collect(),
8772 None => continue,
8773 };
8774 let Some(digit_start) =
8775 (start_col.min(chars.len())..chars.len()).find(|&i| chars[i].is_ascii_digit())
8776 else {
8777 continue;
8778 };
8779 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
8780 digit_start - 1
8781 } else {
8782 digit_start
8783 };
8784 let mut span_end = digit_start;
8785 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
8786 span_end += 1;
8787 }
8788 let s: String = chars[span_start..span_end].iter().collect();
8789 let Ok(n) = s.parse::<i64>() else {
8790 continue;
8791 };
8792 found_count += 1;
8793 let this_delta = if sequential {
8794 delta.saturating_mul(found_count)
8795 } else {
8796 delta
8797 };
8798 let new_s = n.saturating_add(this_delta).to_string();
8799 let span_start_pos = Position::new(row, span_start);
8800 let span_end_pos = Position::new(row, span_end);
8801 ed.mutate_edit(Edit::DeleteRange {
8802 start: span_start_pos,
8803 end: span_end_pos,
8804 kind: MotionKind::Char,
8805 });
8806 ed.mutate_edit(Edit::InsertStr {
8807 at: span_start_pos,
8808 text: new_s,
8809 });
8810 }
8811 buf_set_cursor_rc(&mut ed.buffer, top, block_left.unwrap_or(0));
8813 ed.push_buffer_cursor_to_textarea();
8814 ed.vim.mode = Mode::Normal;
8815 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8816}
8817
8818pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8819 if let Some(entry) = ed.buffer.pop_undo_entry() {
8820 let (cur_rope, cur_cursor) = ed.snapshot();
8821 ed.buffer.push_redo_entry(hjkl_buffer::UndoEntry {
8822 rope: cur_rope,
8823 cursor: cur_cursor,
8824 timestamp: entry.timestamp,
8825 });
8826 ed.restore_rope(entry.rope, entry.cursor);
8827 }
8828 ed.vim.mode = Mode::Normal;
8829 clamp_cursor_to_normal_mode(ed);
8833}
8834
8835pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8836 if let Some(entry) = ed.buffer.pop_redo_entry() {
8837 let (cur_rope, cur_cursor) = ed.snapshot();
8838 let before = cur_rope.clone();
8839 ed.buffer.push_undo_entry(hjkl_buffer::UndoEntry {
8840 rope: cur_rope,
8841 cursor: cur_cursor,
8842 timestamp: entry.timestamp,
8843 });
8844 ed.cap_undo();
8845 ed.restore_rope(entry.rope, entry.cursor);
8846 let after = crate::types::Query::rope(&ed.buffer);
8850 if let Some((row, col)) = first_diff_pos(&before, &after) {
8851 buf_set_cursor_rc(&mut ed.buffer, row, col);
8852 ed.push_buffer_cursor_to_textarea();
8853 }
8854 }
8855 ed.vim.mode = Mode::Normal;
8856 clamp_cursor_to_normal_mode(ed);
8857}
8858
8859fn first_diff_pos(a: &ropey::Rope, b: &ropey::Rope) -> Option<(usize, usize)> {
8862 let rows = a.len_lines().max(b.len_lines());
8863 for r in 0..rows {
8864 let la = if r < a.len_lines() {
8865 hjkl_buffer::rope_line_str(a, r)
8866 } else {
8867 String::new()
8868 };
8869 let lb = if r < b.len_lines() {
8870 hjkl_buffer::rope_line_str(b, r)
8871 } else {
8872 String::new()
8873 };
8874 if la != lb {
8875 let col = la
8876 .chars()
8877 .zip(lb.chars())
8878 .take_while(|(x, y)| x == y)
8879 .count();
8880 return Some((r, col));
8881 }
8882 }
8883 None
8884}
8885
8886fn replay_insert_and_finish<H: crate::types::Host>(
8893 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8894 text: &str,
8895) {
8896 use hjkl_buffer::{Edit, Position};
8897 let cursor = ed.cursor();
8898 ed.mutate_edit(Edit::InsertStr {
8899 at: Position::new(cursor.0, cursor.1),
8900 text: text.to_string(),
8901 });
8902 if ed.vim.insert_session.take().is_some() {
8903 if ed.cursor().1 > 0 {
8904 crate::motions::move_left(&mut ed.buffer, 1);
8905 ed.push_buffer_cursor_to_textarea();
8906 }
8907 ed.vim.mode = Mode::Normal;
8908 }
8909}
8910
8911pub(crate) fn replay_last_change<H: crate::types::Host>(
8912 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8913 outer_count: usize,
8914) {
8915 let Some(change) = ed.vim.last_change.clone() else {
8916 return;
8917 };
8918 ed.vim.replaying = true;
8919 let explicit = if outer_count > 0 {
8925 Some(outer_count.min(MAX_COUNT))
8926 } else {
8927 None
8928 };
8929 let scaled = |c: usize| explicit.unwrap_or(c).min(MAX_COUNT);
8930 match change {
8931 LastChange::OpMotion {
8932 op,
8933 motion,
8934 count,
8935 inserted,
8936 } => {
8937 let total = scaled(count.max(1));
8938 apply_op_with_motion(ed, op, &motion, total);
8939 if let Some(text) = inserted {
8940 replay_insert_and_finish(ed, &text);
8941 }
8942 }
8943 LastChange::OpTextObj {
8944 op,
8945 obj,
8946 inner,
8947 inserted,
8948 } => {
8949 apply_op_with_text_object(ed, op, obj, inner, 1);
8952 if let Some(text) = inserted {
8953 replay_insert_and_finish(ed, &text);
8954 }
8955 }
8956 LastChange::LineOp {
8957 op,
8958 count,
8959 inserted,
8960 } => {
8961 let total = scaled(count.max(1));
8962 execute_line_op(ed, op, total);
8963 if let Some(text) = inserted {
8964 replay_insert_and_finish(ed, &text);
8965 }
8966 }
8967 LastChange::CharDel { forward, count } => {
8968 do_char_delete(ed, forward, scaled(count));
8969 }
8970 LastChange::ReplaceChar { ch, count } => {
8971 replace_char(ed, ch, scaled(count));
8972 }
8973 LastChange::ToggleCase { count } => {
8974 for _ in 0..scaled(count) {
8975 ed.push_undo();
8976 if !toggle_case_at_cursor(ed) {
8977 break;
8978 }
8979 }
8980 }
8981 LastChange::JoinLine { count } => {
8982 for _ in 0..scaled(count) {
8983 ed.push_undo();
8984 if !join_line(ed) {
8985 break;
8986 }
8987 }
8988 }
8989 LastChange::Paste {
8990 before,
8991 count,
8992 cursor_after,
8993 reindent,
8994 } => {
8995 do_paste(ed, before, scaled(count), cursor_after, reindent);
8996 }
8997 LastChange::GnOp {
8998 op,
8999 forward,
9000 inserted,
9001 } => {
9002 gn_operate(ed, Some(op), forward, 1);
9003 if let Some(text) = inserted {
9004 replay_insert_and_finish(ed, &text);
9005 }
9006 }
9007 LastChange::ReplaceMode { text } => {
9008 use hjkl_buffer::{Edit, MotionKind, Position};
9009 ed.push_undo();
9010 for ch in text.chars() {
9011 let cursor = buf_cursor_pos(&ed.buffer);
9012 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
9013 if cursor.col < line_chars {
9014 ed.mutate_edit(Edit::DeleteRange {
9016 start: cursor,
9017 end: Position::new(cursor.row, cursor.col + 1),
9018 kind: MotionKind::Char,
9019 });
9020 }
9021 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
9022 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
9023 }
9024 if ed.cursor().1 > 0 {
9026 crate::motions::move_left(&mut ed.buffer, 1);
9027 }
9028 ed.push_buffer_cursor_to_textarea();
9029 }
9030 LastChange::DeleteToEol { inserted } => {
9031 use hjkl_buffer::{Edit, Position};
9032 ed.push_undo();
9033 delete_to_eol(ed);
9034 if let Some(text) = inserted {
9035 let cursor = ed.cursor();
9036 ed.mutate_edit(Edit::InsertStr {
9037 at: Position::new(cursor.0, cursor.1),
9038 text,
9039 });
9040 }
9041 }
9042 LastChange::OpenLine { above, inserted } => {
9043 use hjkl_buffer::{Edit, Position};
9044 ed.push_undo();
9045 ed.sync_buffer_content_from_textarea();
9046 let row = buf_cursor_pos(&ed.buffer).row;
9047 if above {
9048 ed.mutate_edit(Edit::InsertStr {
9049 at: Position::new(row, 0),
9050 text: "\n".to_string(),
9051 });
9052 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
9053 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
9054 } else {
9055 let line_chars = buf_line_chars(&ed.buffer, row);
9056 ed.mutate_edit(Edit::InsertStr {
9057 at: Position::new(row, line_chars),
9058 text: "\n".to_string(),
9059 });
9060 }
9061 ed.push_buffer_cursor_to_textarea();
9062 let cursor = ed.cursor();
9063 ed.mutate_edit(Edit::InsertStr {
9064 at: Position::new(cursor.0, cursor.1),
9065 text: inserted,
9066 });
9067 }
9068 LastChange::InsertAt {
9069 entry,
9070 inserted,
9071 count,
9072 } => {
9073 use hjkl_buffer::{Edit, Position};
9074 ed.push_undo();
9075 match entry {
9076 InsertEntry::I => {}
9077 InsertEntry::ShiftI => move_first_non_whitespace(ed),
9078 InsertEntry::A => {
9079 crate::motions::move_right_to_end(&mut ed.buffer, 1);
9080 ed.push_buffer_cursor_to_textarea();
9081 }
9082 InsertEntry::ShiftA => {
9083 crate::motions::move_line_end(&mut ed.buffer);
9084 crate::motions::move_right_to_end(&mut ed.buffer, 1);
9085 ed.push_buffer_cursor_to_textarea();
9086 }
9087 }
9088 for _ in 0..count.max(1) {
9089 let cursor = ed.cursor();
9090 ed.mutate_edit(Edit::InsertStr {
9091 at: Position::new(cursor.0, cursor.1),
9092 text: inserted.clone(),
9093 });
9094 }
9095 }
9096 }
9097 ed.vim.replaying = false;
9098}
9099
9100fn changed_run(before: &str, after: &str) -> String {
9106 let a: Vec<char> = before.chars().collect();
9107 let b: Vec<char> = after.chars().collect();
9108 let prefix = a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count();
9109 let max_suffix = a.len().min(b.len()) - prefix;
9110 let suffix = a
9111 .iter()
9112 .rev()
9113 .zip(b.iter().rev())
9114 .take(max_suffix)
9115 .take_while(|(x, y)| x == y)
9116 .count();
9117 b[prefix..b.len() - suffix].iter().collect()
9118}
9119
9120fn extract_inserted(before: &str, after: &str) -> String {
9121 let before_chars: Vec<char> = before.chars().collect();
9122 let after_chars: Vec<char> = after.chars().collect();
9123 if after_chars.len() <= before_chars.len() {
9124 return String::new();
9125 }
9126 let prefix = before_chars
9127 .iter()
9128 .zip(after_chars.iter())
9129 .take_while(|(a, b)| a == b)
9130 .count();
9131 let max_suffix = before_chars.len() - prefix;
9132 let suffix = before_chars
9133 .iter()
9134 .rev()
9135 .zip(after_chars.iter().rev())
9136 .take(max_suffix)
9137 .take_while(|(a, b)| a == b)
9138 .count();
9139 after_chars[prefix..after_chars.len() - suffix]
9140 .iter()
9141 .collect()
9142}
9143
9144#[cfg(test)]
9147mod replace_char_tests {
9148 use crate::{DefaultHost, Editor, Options};
9149 use hjkl_buffer::{Buffer, rope_line_str};
9150
9151 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9152 rope_line_str(&ed.buffer().rope(), row)
9153 }
9154
9155 #[test]
9156 fn replace_char_count_exceeding_line_replaces_nothing() {
9157 let buf = Buffer::from_str("ab\ncd");
9158 let mut ed = Editor::new(buf, DefaultHost::new(), Options::default());
9159 ed.replace_char_at('x', 3);
9162 assert_eq!(line(&ed, 0), "ab", "partial replace must not happen");
9163 assert_eq!(line(&ed, 1), "cd", "must not spill onto the next line");
9164 }
9165
9166 #[test]
9167 fn replace_char_count_fitting_replaces_run() {
9168 let buf = Buffer::from_str("abc");
9169 let mut ed = Editor::new(buf, DefaultHost::new(), Options::default());
9170 ed.replace_char_at('x', 2);
9171 assert_eq!(line(&ed, 0), "xxc");
9172 }
9173}
9174
9175#[cfg(test)]
9176mod comment_continuation_tests {
9177 use super::*;
9178 use crate::{DefaultHost, Editor, Options};
9179 use hjkl_buffer::Buffer;
9180
9181 fn make_editor_with_lang(lang: &str, content: &str) -> Editor<Buffer, DefaultHost> {
9182 let buf = Buffer::from_str(content);
9183 let host = DefaultHost::new();
9184 let opts = Options {
9185 filetype: lang.to_string(),
9186 formatoptions: "ro".to_string(),
9187 ..Options::default()
9188 };
9189 Editor::new(buf, host, opts)
9190 }
9191
9192 #[test]
9193 fn detect_rust_doc_comment() {
9194 let result = detect_comment_on_line("rust", "/// foo bar");
9195 assert!(result.is_some());
9196 let (indent, prefix) = result.unwrap();
9197 assert_eq!(indent, "");
9198 assert_eq!(prefix, "/// ");
9199 }
9200
9201 #[test]
9202 fn detect_rust_inner_doc_comment() {
9203 let result = detect_comment_on_line("rust", "//! crate docs");
9204 assert!(result.is_some());
9205 let (_, prefix) = result.unwrap();
9206 assert_eq!(prefix, "//! ");
9207 }
9208
9209 #[test]
9210 fn detect_rust_plain_comment() {
9211 let result = detect_comment_on_line("rust", "// normal comment");
9212 assert!(result.is_some());
9213 let (_, prefix) = result.unwrap();
9214 assert_eq!(prefix, "// ");
9215 }
9216
9217 #[test]
9218 fn detect_indented_comment() {
9219 let result = detect_comment_on_line("rust", " // indented");
9220 assert!(result.is_some());
9221 let (indent, prefix) = result.unwrap();
9222 assert_eq!(indent, " ");
9223 assert_eq!(prefix, "// ");
9224 }
9225
9226 #[test]
9227 fn detect_python_hash() {
9228 let result = detect_comment_on_line("python", "# comment");
9229 assert!(result.is_some());
9230 let (_, prefix) = result.unwrap();
9231 assert_eq!(prefix, "# ");
9232 }
9233
9234 #[test]
9235 fn detect_lua_double_dash() {
9236 let result = detect_comment_on_line("lua", "-- a lua comment");
9237 assert!(result.is_some());
9238 let (_, prefix) = result.unwrap();
9239 assert_eq!(prefix, "-- ");
9240 }
9241
9242 #[test]
9243 fn detect_non_comment_is_none() {
9244 assert!(detect_comment_on_line("rust", "let x = 1;").is_none());
9245 assert!(detect_comment_on_line("python", "x = 1").is_none());
9246 }
9247
9248 #[test]
9249 fn detect_bare_double_slash_still_matches() {
9250 assert!(detect_comment_on_line("rust", "//").is_some());
9252 }
9253
9254 #[test]
9255 fn rust_doc_before_plain() {
9256 let result = detect_comment_on_line("rust", "/// outer doc");
9258 let (_, prefix) = result.unwrap();
9259 assert_eq!(prefix, "/// ", "/// must match before //");
9260 }
9261
9262 #[test]
9263 fn continue_comment_returns_prefix_for_comment_row() {
9264 let ed = make_editor_with_lang("rust", "/// hello\n");
9265 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9266 assert_eq!(cont, Some("/// ".to_string()));
9267 }
9268
9269 #[test]
9270 fn continue_comment_returns_none_for_non_comment() {
9271 let ed = make_editor_with_lang("rust", "let x = 1;\n");
9272 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9273 assert!(cont.is_none());
9274 }
9275
9276 #[test]
9277 fn continue_comment_returns_none_when_filetype_empty() {
9278 let buf = Buffer::from_str("// hello\n");
9279 let host = DefaultHost::new();
9280 let ed = Editor::new(buf, host, Options::default());
9282 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9283 assert!(cont.is_none());
9284 }
9285}
9286
9287#[cfg(test)]
9288mod comment_toggle_tests {
9289 use super::*;
9290 use crate::{DefaultHost, Editor, Options};
9291 use hjkl_buffer::Buffer;
9292
9293 fn make_rust_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9294 let buf = Buffer::from_str(content);
9295 let host = DefaultHost::new();
9296 let opts = Options {
9297 filetype: "rust".to_string(),
9298 ..Options::default()
9299 };
9300 Editor::new(buf, host, opts)
9301 }
9302
9303 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9304 buf_line(&ed.buffer, row).unwrap_or_default()
9305 }
9306
9307 #[test]
9310 fn gcc_comments_rust_line() {
9311 let mut ed = make_rust_editor("let x = 1;");
9312 ed.toggle_comment_range(0, 0);
9313 assert_eq!(line(&ed, 0), "// let x = 1;");
9314 }
9315
9316 #[test]
9317 fn gcc_uncomments_rust_line() {
9318 let mut ed = make_rust_editor("// let x = 1;");
9319 ed.toggle_comment_range(0, 0);
9320 assert_eq!(line(&ed, 0), "let x = 1;");
9321 }
9322
9323 #[test]
9324 fn gcc_indent_preserving() {
9325 let mut ed = make_rust_editor(" let x = 1;");
9327 ed.toggle_comment_range(0, 0);
9328 assert_eq!(line(&ed, 0), " // let x = 1;");
9329 }
9330
9331 #[test]
9332 fn gcc_indent_preserving_uncomment() {
9333 let mut ed = make_rust_editor(" // let x = 1;");
9334 ed.toggle_comment_range(0, 0);
9335 assert_eq!(line(&ed, 0), " let x = 1;");
9336 }
9337
9338 #[test]
9341 fn toggle_multi_line_all_uncommented() {
9342 let content = "let a = 1;\nlet b = 2;\nlet c = 3;";
9343 let mut ed = make_rust_editor(content);
9344 ed.toggle_comment_range(0, 2);
9345 assert_eq!(line(&ed, 0), "// let a = 1;");
9346 assert_eq!(line(&ed, 1), "// let b = 2;");
9347 assert_eq!(line(&ed, 2), "// let c = 3;");
9348 }
9349
9350 #[test]
9351 fn toggle_multi_line_all_commented() {
9352 let content = "// let a = 1;\n// let b = 2;\n// let c = 3;";
9353 let mut ed = make_rust_editor(content);
9354 ed.toggle_comment_range(0, 2);
9355 assert_eq!(line(&ed, 0), "let a = 1;");
9356 assert_eq!(line(&ed, 1), "let b = 2;");
9357 assert_eq!(line(&ed, 2), "let c = 3;");
9358 }
9359
9360 #[test]
9363 fn toggle_mixed_state_comments_all() {
9364 let content = "let a = 1;\n// let b = 2;\nlet c = 3;\n// let d = 4;\nlet e = 5;";
9366 let mut ed = make_rust_editor(content);
9367 ed.toggle_comment_range(0, 4);
9368 for r in 0..5 {
9369 assert!(
9370 line(&ed, r).trim_start().starts_with("//"),
9371 "row {r} not commented: {:?}",
9372 line(&ed, r)
9373 );
9374 }
9375 }
9376
9377 #[test]
9380 fn blank_lines_not_commented() {
9381 let content = "let a = 1;\n\nlet b = 2;";
9382 let mut ed = make_rust_editor(content);
9383 ed.toggle_comment_range(0, 2);
9384 assert_eq!(line(&ed, 0), "// let a = 1;");
9385 assert_eq!(line(&ed, 1), ""); assert_eq!(line(&ed, 2), "// let b = 2;");
9387 }
9388
9389 #[test]
9392 fn python_comment_toggle() {
9393 let buf = Buffer::from_str("x = 1\ny = 2");
9394 let host = DefaultHost::new();
9395 let opts = Options {
9396 filetype: "python".to_string(),
9397 ..Options::default()
9398 };
9399 let mut ed = Editor::new(buf, host, opts);
9400 ed.toggle_comment_range(0, 1);
9401 assert_eq!(line(&ed, 0), "# x = 1");
9402 assert_eq!(line(&ed, 1), "# y = 2");
9403 ed.toggle_comment_range(0, 1);
9405 assert_eq!(line(&ed, 0), "x = 1");
9406 assert_eq!(line(&ed, 1), "y = 2");
9407 }
9408
9409 #[test]
9412 fn commentstring_override_via_setting() {
9413 let buf = Buffer::from_str("hello world");
9414 let host = DefaultHost::new();
9415 let opts = Options {
9416 filetype: "rust".to_string(),
9417 ..Options::default()
9418 };
9419 let mut ed = Editor::new(buf, host, opts);
9420 ed.settings_mut().commentstring = "# %s".to_string();
9422 ed.toggle_comment_range(0, 0);
9423 assert_eq!(line(&ed, 0), "# hello world");
9424 }
9425
9426 #[test]
9429 fn unknown_lang_no_op() {
9430 let buf = Buffer::from_str("hello");
9431 let host = DefaultHost::new();
9432 let opts = Options::default(); let mut ed = Editor::new(buf, host, opts);
9434 ed.toggle_comment_range(0, 0);
9435 assert_eq!(line(&ed, 0), "hello");
9437 }
9438}
9439
9440#[cfg(test)]
9443mod g_ampersand_tests {
9444 use super::*;
9445 use crate::{DefaultHost, Editor, Options};
9446 use hjkl_buffer::{Buffer, rope_line_str};
9447
9448 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9449 let buf = Buffer::from_str(content);
9450 let host = DefaultHost::new();
9451 Editor::new(buf, host, Options::default())
9452 }
9453
9454 fn buf_line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9455 let rope = ed.buffer().rope();
9456 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
9457 }
9458
9459 #[test]
9462 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
9463 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
9464 let cmd = crate::substitute::parse_substitute("/foo/bar/").unwrap();
9466 ed.set_last_substitute(cmd);
9467 apply_after_g(&mut ed, '&', 1);
9469 assert_eq!(buf_line(&ed, 0), "bar");
9470 assert_eq!(buf_line(&ed, 1), "bar bar foo");
9472 assert_eq!(buf_line(&ed, 2), "baz");
9473 }
9474
9475 #[test]
9477 fn g_ampersand_with_g_flag_replaces_all_per_line() {
9478 let mut ed = make_editor("foo foo\nfoo");
9479 let cmd = crate::substitute::parse_substitute("/foo/bar/g").unwrap();
9480 ed.set_last_substitute(cmd);
9481 apply_after_g(&mut ed, '&', 1);
9482 assert_eq!(buf_line(&ed, 0), "bar bar");
9483 assert_eq!(buf_line(&ed, 1), "bar");
9484 }
9485
9486 #[test]
9488 fn g_ampersand_noop_when_no_prior_substitute() {
9489 let mut ed = make_editor("foo\nbar");
9490 apply_after_g(&mut ed, '&', 1);
9492 assert_eq!(buf_line(&ed, 0), "foo");
9493 assert_eq!(buf_line(&ed, 1), "bar");
9494 }
9495}
9496
9497#[cfg(test)]
9500mod sneak_tests {
9501 use super::*;
9502 use crate::{DefaultHost, Editor, Options};
9503 use hjkl_buffer::Buffer;
9504
9505 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9506 let buf = Buffer::from_str(content);
9507 let host = DefaultHost::new();
9508 Editor::new(buf, host, Options::default())
9509 }
9510
9511 #[test]
9513 fn sneak_forward_jumps_to_two_char_digraph() {
9514 let mut ed = make_editor("foo bar baz qux\n");
9515 ed.jump_cursor(0, 0);
9516 ed.sneak('b', 'a', true, 1);
9517 assert_eq!(ed.cursor(), (0, 4), "cursor should land on 'ba' in 'bar'");
9518 }
9519
9520 #[test]
9522 fn sneak_backward_jumps_to_prior_match() {
9523 let mut ed = make_editor("foo bar baz qux\n");
9524 ed.jump_cursor(0, 12);
9525 ed.sneak('b', 'a', false, 1);
9526 assert_eq!(
9527 ed.cursor(),
9528 (0, 8),
9529 "backward sneak should find 'ba' in 'baz'"
9530 );
9531 }
9532
9533 #[test]
9535 fn sneak_repeat_semicolon_next_match() {
9536 let mut ed = make_editor("foo bar baz qux\n");
9537 ed.jump_cursor(0, 0);
9538 ed.sneak('b', 'a', true, 1);
9540 assert_eq!(ed.cursor(), (0, 4));
9541 execute_motion(&mut ed, Motion::FindRepeat { reverse: false }, 1);
9543 assert_eq!(ed.cursor(), (0, 8), "semicolon should jump to next 'ba'");
9544 }
9545
9546 #[test]
9548 fn sneak_repeat_comma_prev_match() {
9549 let mut ed = make_editor("foo bar baz qux\n");
9550 ed.jump_cursor(0, 0);
9551 ed.sneak('b', 'a', true, 1);
9552 assert_eq!(ed.cursor(), (0, 4));
9553 let pre = ed.cursor();
9555 execute_motion(&mut ed, Motion::FindRepeat { reverse: true }, 1);
9556 assert_eq!(
9557 ed.cursor(),
9558 pre,
9559 "comma with no prior match should leave cursor unchanged"
9560 );
9561 }
9562
9563 #[test]
9565 fn sneak_s_searches_backward() {
9566 let mut ed = make_editor("foo bar baz qux\n");
9567 ed.jump_cursor(0, 12);
9568 ed.sneak('b', 'a', false, 1);
9569 assert_eq!(ed.cursor(), (0, 8));
9570 }
9571
9572 #[test]
9574 fn sneak_with_count_jumps_to_nth() {
9575 let mut ed = make_editor("foo bar baz qux\n");
9576 ed.jump_cursor(0, 0);
9577 ed.sneak('b', 'a', true, 2);
9578 assert_eq!(ed.cursor(), (0, 8), "count=2 should jump to 2nd 'ba'");
9579 }
9580
9581 #[test]
9583 fn sneak_no_match_cursor_stays() {
9584 let mut ed = make_editor("foo bar baz qux\n");
9585 ed.jump_cursor(0, 0);
9586 let pre = ed.cursor();
9587 ed.sneak('x', 'x', true, 1);
9588 assert_eq!(ed.cursor(), pre, "no match should leave cursor unchanged");
9589 }
9590
9591 #[test]
9593 fn operator_pending_dsab_deletes_to_digraph() {
9594 let mut ed = make_editor("hello ab world\n");
9595 ed.jump_cursor(0, 0);
9596 ed.apply_op_sneak(Operator::Delete, 'a', 'b', true, 1);
9597 let content = ed.content();
9599 assert!(
9600 content.starts_with("ab world"),
9601 "dsab should delete 'hello ' leaving 'ab world'; got: {content:?}"
9602 );
9603 }
9604
9605 #[test]
9607 fn sneak_cross_line_match() {
9608 let mut ed = make_editor("foo\nbar baz\n");
9609 ed.jump_cursor(0, 0);
9610 ed.sneak('b', 'a', true, 1);
9611 assert_eq!(ed.cursor(), (1, 0), "sneak should cross line boundary");
9612 }
9613
9614 #[test]
9616 fn sneak_updates_last_sneak_state() {
9617 let mut ed = make_editor("foo bar baz\n");
9618 ed.jump_cursor(0, 0);
9619 ed.sneak('b', 'a', true, 1);
9620 let ls = ed.last_sneak();
9621 assert_eq!(
9622 ls,
9623 Some((('b', 'a'), true)),
9624 "last_sneak should record the digraph and direction"
9625 );
9626 }
9627}
9628
9629#[cfg(test)]
9638mod indent_count_tests {
9639 use super::*;
9640 use crate::{DefaultHost, Editor, Options};
9641 use hjkl_buffer::Buffer;
9642
9643 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9644 let buf = Buffer::from_str(content);
9645 let mut ed = Editor::new(buf, DefaultHost::new(), Options::default());
9646 ed.settings_mut().expandtab = true;
9647 ed.settings_mut().shiftwidth = 4;
9648 ed
9649 }
9650
9651 fn content(ed: &Editor<Buffer, DefaultHost>) -> String {
9652 (*ed.buffer().content_joined()).clone()
9653 }
9654
9655 #[test]
9656 fn count_indent_operates_on_n_lines() {
9657 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9658 ed.jump_cursor(0, 0);
9659 execute_line_op(&mut ed, Operator::Indent, 3);
9660 assert_eq!(content(&ed), " a\n b\n c\nd\ne\nf\n");
9661 }
9662
9663 #[test]
9665 fn indent_noexpandtab_inserts_tab() {
9666 let mut ed = make_editor("hello\n");
9667 ed.settings_mut().expandtab = false;
9668 ed.settings_mut().shiftwidth = 4;
9669 ed.settings_mut().tabstop = 4;
9670 ed.jump_cursor(0, 0);
9671 execute_line_op(&mut ed, Operator::Indent, 1);
9672 assert_eq!(content(&ed), "\thello\n");
9673 }
9674
9675 #[test]
9678 fn indent_noexpandtab_subtab_remainder_is_spaces() {
9679 let mut ed = make_editor("hello\n");
9680 ed.settings_mut().expandtab = false;
9681 ed.settings_mut().shiftwidth = 2;
9682 ed.settings_mut().tabstop = 4;
9683 ed.jump_cursor(0, 0);
9684 execute_line_op(&mut ed, Operator::Indent, 1);
9685 assert_eq!(content(&ed), " hello\n");
9686 }
9687
9688 #[test]
9689 fn count_indent_clamps_to_buffer_end() {
9690 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9691 ed.jump_cursor(0, 0);
9692 execute_line_op(&mut ed, Operator::Indent, 10);
9693 assert_eq!(content(&ed), " a\n b\n c\n d\n e\n f\n");
9694 }
9695
9696 #[test]
9697 fn count_outdent_clamps_to_buffer_end() {
9698 let mut ed = make_editor(" a\n b\n c\n");
9699 ed.jump_cursor(0, 0);
9700 execute_line_op(&mut ed, Operator::Outdent, 10);
9701 assert_eq!(content(&ed), "a\nb\nc\n");
9702 }
9703
9704 #[test]
9705 fn count_indent_on_last_line_is_noop() {
9706 let mut ed = make_editor("a\nb\nc\n");
9707 ed.jump_cursor(2, 0); execute_line_op(&mut ed, Operator::Indent, 5);
9709 assert_eq!(
9710 content(&ed),
9711 "a\nb\nc\n",
9712 "5>> on last line must abort (E16)"
9713 );
9714 }
9715
9716 #[test]
9717 fn count_indent_on_single_line_is_noop() {
9718 let mut ed = make_editor("x\n");
9719 ed.jump_cursor(0, 0);
9720 execute_line_op(&mut ed, Operator::Indent, 5);
9721 assert_eq!(content(&ed), "x\n", "5>> on the only line must abort (E16)");
9722 }
9723
9724 #[test]
9725 fn count_outdent_on_last_line_is_noop() {
9726 let mut ed = make_editor(" a\n b\n c\n");
9727 ed.jump_cursor(2, 0);
9728 execute_line_op(&mut ed, Operator::Outdent, 5);
9729 assert_eq!(content(&ed), " a\n b\n c\n");
9730 }
9731
9732 #[test]
9733 fn single_indent_on_last_line_still_works() {
9734 let mut ed = make_editor("a\nb\nc\n");
9736 ed.jump_cursor(2, 0);
9737 execute_line_op(&mut ed, Operator::Indent, 1);
9738 assert_eq!(content(&ed), "a\nb\n c\n");
9739 }
9740}
9741
9742#[cfg(test)]
9745mod abbrev_tests {
9746 use super::{Abbrev, AbbrevKind, AbbrevTrigger, abbrev_kind, try_abbrev_expand};
9747 use AbbrevKind::{End, Full, NonKw};
9748
9749 const ISK: &str = "@,48-57,_,192-255"; fn make_abbrev(lhs: &str, rhs: &str) -> Abbrev {
9752 Abbrev {
9753 lhs: lhs.to_string(),
9754 rhs: rhs.to_string(),
9755 insert: true,
9756 cmdline: false,
9757 noremap: false,
9758 }
9759 }
9760
9761 fn expand(
9762 abbrevs: &[Abbrev],
9763 before: &str,
9764 mincol: usize,
9765 trig: AbbrevTrigger,
9766 ) -> Option<(usize, String)> {
9767 try_abbrev_expand(abbrevs, before, mincol, trig, ISK)
9768 }
9769
9770 #[test]
9773 fn fullid_all_keyword_chars() {
9774 assert_eq!(abbrev_kind("teh", ISK), Full);
9775 assert_eq!(abbrev_kind("abc123", ISK), Full);
9776 assert_eq!(abbrev_kind("_foo", ISK), Full);
9777 }
9778
9779 #[test]
9780 fn endid_ends_with_kw_has_nonkw() {
9781 assert_eq!(abbrev_kind("#i", ISK), End);
9782 assert_eq!(abbrev_kind("#include", ISK), End);
9783 }
9784
9785 #[test]
9786 fn nonid_ends_with_nonkw() {
9787 assert_eq!(abbrev_kind(";;", ISK), NonKw);
9788 assert_eq!(abbrev_kind("->", ISK), NonKw);
9789 }
9790
9791 #[test]
9794 fn fullid_expands_on_space_trigger() {
9795 let abbrevs = [make_abbrev("teh", "the")];
9796 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword(' '));
9797 assert_eq!(r, Some((3, "the".to_string())));
9798 }
9799
9800 #[test]
9801 fn fullid_expands_on_esc_trigger() {
9802 let abbrevs = [make_abbrev("teh", "the")];
9803 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Esc);
9804 assert_eq!(r, Some((3, "the".to_string())));
9805 }
9806
9807 #[test]
9808 fn fullid_expands_on_cr_trigger() {
9809 let abbrevs = [make_abbrev("teh", "the")];
9810 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Cr);
9811 assert_eq!(r, Some((3, "the".to_string())));
9812 }
9813
9814 #[test]
9815 fn fullid_expands_on_ctrl_bracket() {
9816 let abbrevs = [make_abbrev("teh", "the")];
9817 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::CtrlBracket);
9818 assert_eq!(r, Some((3, "the".to_string())));
9819 }
9820
9821 #[test]
9822 fn fullid_does_not_expand_on_keyword_trigger() {
9823 let abbrevs = [make_abbrev("teh", "the")];
9825 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword('a'));
9826 assert_eq!(r, None);
9828 }
9829
9830 #[test]
9831 fn fullid_no_expand_when_lhs_not_at_end() {
9832 let abbrevs = [make_abbrev("teh", "the")];
9833 let r = expand(&abbrevs, "ateh", 0, AbbrevTrigger::NonKeyword(' '));
9835 assert_eq!(r, None);
9836 }
9837
9838 #[test]
9839 fn fullid_expands_after_nonkw_prefix() {
9840 let abbrevs = [make_abbrev("teh", "the")];
9841 let r = expand(&abbrevs, "!teh", 0, AbbrevTrigger::NonKeyword(' '));
9843 assert_eq!(r, Some((3, "the".to_string())));
9844 }
9845
9846 #[test]
9847 fn fullid_single_char_no_expand_after_nonblank_nonkw() {
9848 let abbrevs = [make_abbrev("a", "b")];
9849 let r = expand(&abbrevs, "!a", 0, AbbrevTrigger::NonKeyword(' '));
9851 assert_eq!(r, None);
9852 }
9853
9854 #[test]
9855 fn fullid_single_char_expands_after_space() {
9856 let abbrevs = [make_abbrev("a", "b")];
9857 let r = expand(&abbrevs, " a", 0, AbbrevTrigger::NonKeyword(' '));
9859 assert_eq!(r, Some((1, "b".to_string())));
9860 }
9861
9862 #[test]
9865 fn mincol_blocks_consuming_preexisting_text() {
9866 let abbrevs = [make_abbrev("teh", "the")];
9867 let r = expand(&abbrevs, "teh", 3, AbbrevTrigger::NonKeyword(' '));
9869 assert_eq!(r, None);
9870 }
9871
9872 #[test]
9873 fn mincol_allows_match_starting_at_mincol() {
9874 let abbrevs = [make_abbrev("teh", "the")];
9875 let r = expand(&abbrevs, "!! teh", 3, AbbrevTrigger::NonKeyword(' '));
9878 assert_eq!(r, Some((3, "the".to_string())));
9879 }
9880
9881 #[test]
9884 fn endid_expands_on_space_trigger() {
9885 let abbrevs = [make_abbrev("#i", "#include")];
9886 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::NonKeyword(' '));
9887 assert_eq!(r, Some((2, "#include".to_string())));
9888 }
9889
9890 #[test]
9891 fn endid_expands_on_esc_trigger() {
9892 let abbrevs = [make_abbrev("#i", "#include")];
9893 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::Esc);
9894 assert_eq!(r, Some((2, "#include".to_string())));
9895 }
9896
9897 #[test]
9900 fn nonid_expands_on_esc_trigger() {
9901 let abbrevs = [make_abbrev(";;", "std::endl;")];
9902 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Esc);
9903 assert_eq!(r, Some((2, "std::endl;".to_string())));
9904 }
9905
9906 #[test]
9907 fn nonid_expands_on_cr_trigger() {
9908 let abbrevs = [make_abbrev(";;", "std::endl;")];
9909 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Cr);
9910 assert_eq!(r, Some((2, "std::endl;".to_string())));
9911 }
9912
9913 #[test]
9914 fn nonid_does_not_expand_on_nonkw_trigger() {
9915 let abbrevs = [make_abbrev(";;", "std::endl;")];
9917 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::NonKeyword(' '));
9918 assert_eq!(r, None);
9919 }
9920
9921 #[test]
9922 fn nonid_expands_on_ctrl_bracket() {
9923 let abbrevs = [make_abbrev(";;", "std::endl;")];
9924 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::CtrlBracket);
9925 assert_eq!(r, Some((2, "std::endl;".to_string())));
9926 }
9927
9928 #[test]
9931 fn multiword_rhs_expansion() {
9932 let abbrevs = [make_abbrev("hw", "hello world")];
9933 let r = expand(&abbrevs, "hw", 0, AbbrevTrigger::NonKeyword(' '));
9934 assert_eq!(r, Some((2, "hello world".to_string())));
9935 }
9936
9937 #[test]
9940 fn no_match_returns_none() {
9941 let abbrevs = [make_abbrev("teh", "the")];
9942 let r = expand(&abbrevs, "xyz", 0, AbbrevTrigger::NonKeyword(' '));
9943 assert_eq!(r, None);
9944 }
9945
9946 #[test]
9947 fn empty_abbrevs_returns_none() {
9948 let r = expand(&[], "teh", 0, AbbrevTrigger::NonKeyword(' '));
9949 assert_eq!(r, None);
9950 }
9951
9952 #[test]
9953 fn empty_before_text_returns_none() {
9954 let abbrevs = [make_abbrev("teh", "the")];
9955 let r = expand(&abbrevs, "", 0, AbbrevTrigger::NonKeyword(' '));
9956 assert_eq!(r, None);
9957 }
9958}
9959
9960#[cfg(test)]
9963mod scan_tag_opener_multibyte_tests {
9964 use crate::types::Options;
9965 use crate::{DefaultHost, Editor};
9966 use hjkl_buffer::Buffer;
9967
9968 fn html_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9969 let buf = Buffer::from_str(content);
9970 let host = DefaultHost::new();
9971 let mut ed = Editor::new(buf, host, Options::default());
9972 ed.settings.filetype = "html".to_string();
9973 ed.settings.autoclose_tag = true;
9974 ed.settings.autopair = true;
9975 ed
9976 }
9977
9978 #[test]
9985 fn autoclose_gt_after_multibyte_no_panic() {
9986 let mut ed = html_editor("ñ");
9987 ed.enter_insert_i(1);
9989 ed.jump_cursor(0, 1);
9991 ed.insert_char('>');
9993 let rope = ed.buffer().rope();
9995 let line = hjkl_buffer::rope_line_str(&rope, 0);
9996 assert!(line.contains('>'), "inserted > must appear in buffer");
9997 }
9998
9999 #[test]
10016 fn autoclose_gt_direct_after_multibyte_no_panic() {
10017 let mut ed = html_editor("ñ");
10020 ed.enter_insert_i(1);
10021 ed.jump_cursor(0, 1); ed.insert_char('>');
10024 let rope = ed.buffer().rope();
10025 let line = hjkl_buffer::rope_line_str(&rope, 0);
10026 assert!(
10027 line.contains('>'),
10028 "inserted > must appear in buffer, got: {line:?}"
10029 );
10030 }
10031}