1use crate::VimMode;
75use crate::input::{Input, Key};
76
77use crate::buf_helpers::{
78 buf_cursor_pos, buf_line, buf_line_bytes, buf_line_chars, buf_lines_to_vec, buf_row_count,
79 buf_set_cursor_pos, buf_set_cursor_rc,
80};
81use crate::editor::Editor;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum Mode {
87 #[default]
88 Normal,
89 Insert,
90 Visual,
91 VisualLine,
92 VisualBlock,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Default)]
100pub enum Pending {
101 #[default]
102 None,
103 Op { op: Operator, count1: usize },
106 OpTextObj {
108 op: Operator,
109 count1: usize,
110 inner: bool,
111 },
112 OpG { op: Operator, count1: usize },
114 G,
116 Find { forward: bool, till: bool },
118 OpFind {
120 op: Operator,
121 count1: usize,
122 forward: bool,
123 till: bool,
124 },
125 Replace,
127 VisualTextObj { inner: bool },
130 Z,
132 SetMark,
134 GotoMarkLine,
137 GotoMarkChar,
140 SelectRegister,
143 RecordMacroTarget,
147 PlayMacroTarget { count: usize },
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum Operator {
157 Delete,
158 Change,
159 Yank,
160 Uppercase,
163 Lowercase,
165 ToggleCase,
169 Indent,
174 Outdent,
177 Fold,
181 Reflow,
186 AutoIndent,
190}
191
192#[derive(Debug, Clone, PartialEq, Eq)]
193pub enum Motion {
194 Left,
195 Right,
196 Up,
197 Down,
198 WordFwd,
199 BigWordFwd,
200 WordBack,
201 BigWordBack,
202 WordEnd,
203 BigWordEnd,
204 WordEndBack,
206 BigWordEndBack,
208 LineStart,
209 FirstNonBlank,
210 LineEnd,
211 FileTop,
212 FileBottom,
213 Find {
214 ch: char,
215 forward: bool,
216 till: bool,
217 },
218 FindRepeat {
219 reverse: bool,
220 },
221 MatchBracket,
222 WordAtCursor {
223 forward: bool,
224 whole_word: bool,
227 },
228 SearchNext {
230 reverse: bool,
231 },
232 ViewportTop,
234 ViewportMiddle,
236 ViewportBottom,
238 LastNonBlank,
240 LineMiddle,
243 ParagraphPrev,
245 ParagraphNext,
247 SentencePrev,
249 SentenceNext,
251 ScreenDown,
254 ScreenUp,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum TextObject {
260 Word {
261 big: bool,
262 },
263 Quote(char),
264 Bracket(char),
265 Paragraph,
266 XmlTag,
270 Sentence,
275}
276
277#[derive(Debug, Clone, Copy, PartialEq, Eq)]
279pub enum RangeKind {
280 Exclusive,
282 Inclusive,
284 Linewise,
286}
287
288#[derive(Debug, Clone)]
292pub enum LastChange {
293 OpMotion {
295 op: Operator,
296 motion: Motion,
297 count: usize,
298 inserted: Option<String>,
299 },
300 OpTextObj {
302 op: Operator,
303 obj: TextObject,
304 inner: bool,
305 inserted: Option<String>,
306 },
307 LineOp {
309 op: Operator,
310 count: usize,
311 inserted: Option<String>,
312 },
313 CharDel { forward: bool, count: usize },
315 ReplaceChar { ch: char, count: usize },
317 ToggleCase { count: usize },
319 JoinLine { count: usize },
321 Paste { before: bool, count: usize },
323 DeleteToEol { inserted: Option<String> },
325 OpenLine { above: bool, inserted: String },
327 InsertAt {
329 entry: InsertEntry,
330 inserted: String,
331 count: usize,
332 },
333}
334
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub enum InsertEntry {
337 I,
338 A,
339 ShiftI,
340 ShiftA,
341}
342
343#[derive(Default)]
346pub struct VimState {
347 pub mode: Mode,
352 pub pending: Pending,
354 pub count: usize,
357 pub last_find: Option<(char, bool, bool)>,
359 pub last_change: Option<LastChange>,
361 pub insert_session: Option<InsertSession>,
363 pub visual_anchor: (usize, usize),
367 pub visual_line_anchor: usize,
369 pub block_anchor: (usize, usize),
372 pub block_vcol: usize,
378 pub yank_linewise: bool,
380 pub pending_register: Option<char>,
383 pub recording_macro: Option<char>,
387 pub recording_keys: Vec<crate::input::Input>,
392 pub replaying_macro: bool,
395 pub last_macro: Option<char>,
397 pub last_edit_pos: Option<(usize, usize)>,
400 pub last_insert_pos: Option<(usize, usize)>,
404 pub change_list: Vec<(usize, usize)>,
408 pub change_list_cursor: Option<usize>,
411 pub last_visual: Option<LastVisual>,
414 pub viewport_pinned: bool,
418 pub replaying: bool,
420 pub one_shot_normal: bool,
423 pub search_prompt: Option<SearchPrompt>,
425 pub last_search: Option<String>,
429 pub last_search_forward: bool,
433 pub jump_back: Vec<(usize, usize)>,
438 pub jump_fwd: Vec<(usize, usize)>,
441 pub insert_pending_register: bool,
445 pub change_mark_start: Option<(usize, usize)>,
451 pub search_history: Vec<String>,
455 pub search_history_cursor: Option<usize>,
460 pub last_input_at: Option<std::time::Instant>,
469 pub last_input_host_at: Option<core::time::Duration>,
473 pub(crate) current_mode: crate::VimMode,
479}
480
481pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
482pub(crate) const CHANGE_LIST_MAX: usize = 100;
483
484#[derive(Debug, Clone)]
487pub struct SearchPrompt {
488 pub text: String,
489 pub cursor: usize,
490 pub forward: bool,
491}
492
493#[derive(Debug, Clone)]
494pub struct InsertSession {
495 pub count: usize,
496 pub row_min: usize,
498 pub row_max: usize,
499 pub before_lines: Vec<String>,
503 pub reason: InsertReason,
504}
505
506#[derive(Debug, Clone)]
507pub enum InsertReason {
508 Enter(InsertEntry),
510 Open { above: bool },
512 AfterChange,
515 DeleteToEol,
517 ReplayOnly,
520 BlockEdge { top: usize, bot: usize, col: usize },
524 BlockChange { top: usize, bot: usize, col: usize },
529 Replace,
533}
534
535#[derive(Debug, Clone, Copy)]
545pub struct LastVisual {
546 pub mode: Mode,
547 pub anchor: (usize, usize),
548 pub cursor: (usize, usize),
549 pub block_vcol: usize,
550}
551
552impl VimState {
553 pub fn public_mode(&self) -> VimMode {
554 match self.mode {
555 Mode::Normal => VimMode::Normal,
556 Mode::Insert => VimMode::Insert,
557 Mode::Visual => VimMode::Visual,
558 Mode::VisualLine => VimMode::VisualLine,
559 Mode::VisualBlock => VimMode::VisualBlock,
560 }
561 }
562
563 pub fn force_normal(&mut self) {
564 self.mode = Mode::Normal;
565 self.pending = Pending::None;
566 self.count = 0;
567 self.insert_session = None;
568 self.current_mode = crate::VimMode::Normal;
570 }
571
572 pub(crate) fn clear_pending_prefix(&mut self) {
582 self.pending = Pending::None;
583 self.count = 0;
584 self.pending_register = None;
585 self.insert_pending_register = false;
586 }
587
588 pub(crate) fn widen_insert_row(&mut self, row: usize) {
593 if let Some(ref mut session) = self.insert_session {
594 session.row_min = session.row_min.min(row);
595 session.row_max = session.row_max.max(row);
596 }
597 }
598
599 pub fn is_visual(&self) -> bool {
600 matches!(
601 self.mode,
602 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
603 )
604 }
605
606 pub fn is_visual_char(&self) -> bool {
607 self.mode == Mode::Visual
608 }
609
610 pub(crate) fn pending_count_val(&self) -> Option<u32> {
613 if self.count == 0 {
614 None
615 } else {
616 Some(self.count as u32)
617 }
618 }
619
620 pub(crate) fn is_chord_pending(&self) -> bool {
623 !matches!(self.pending, Pending::None)
624 }
625
626 pub(crate) fn pending_op_char(&self) -> Option<char> {
630 let op = match &self.pending {
631 Pending::Op { op, .. }
632 | Pending::OpTextObj { op, .. }
633 | Pending::OpG { op, .. }
634 | Pending::OpFind { op, .. } => Some(*op),
635 _ => None,
636 };
637 op.map(|o| match o {
638 Operator::Delete => 'd',
639 Operator::Change => 'c',
640 Operator::Yank => 'y',
641 Operator::Uppercase => 'U',
642 Operator::Lowercase => 'u',
643 Operator::ToggleCase => '~',
644 Operator::Indent => '>',
645 Operator::Outdent => '<',
646 Operator::Fold => 'z',
647 Operator::Reflow => 'q',
648 Operator::AutoIndent => '=',
649 })
650 }
651}
652
653pub(crate) fn enter_search<H: crate::types::Host>(
659 ed: &mut Editor<hjkl_buffer::Buffer, H>,
660 forward: bool,
661) {
662 ed.vim.search_prompt = Some(SearchPrompt {
663 text: String::new(),
664 cursor: 0,
665 forward,
666 });
667 ed.vim.search_history_cursor = None;
668 ed.set_search_pattern(None);
672}
673
674fn walk_change_list<H: crate::types::Host>(
678 ed: &mut Editor<hjkl_buffer::Buffer, H>,
679 dir: isize,
680 count: usize,
681) {
682 if ed.vim.change_list.is_empty() {
683 return;
684 }
685 let len = ed.vim.change_list.len();
686 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
687 (None, -1) => len as isize - 1,
688 (None, 1) => return, (Some(i), -1) => i as isize - 1,
690 (Some(i), 1) => i as isize + 1,
691 _ => return,
692 };
693 for _ in 1..count {
694 let next = idx + dir;
695 if next < 0 || next >= len as isize {
696 break;
697 }
698 idx = next;
699 }
700 if idx < 0 || idx >= len as isize {
701 return;
702 }
703 let idx = idx as usize;
704 ed.vim.change_list_cursor = Some(idx);
705 let (row, col) = ed.vim.change_list[idx];
706 ed.jump_cursor(row, col);
707}
708
709fn insert_register_text<H: crate::types::Host>(
714 ed: &mut Editor<hjkl_buffer::Buffer, H>,
715 selector: char,
716) {
717 use hjkl_buffer::Edit;
718 let text = match ed.registers().read(selector) {
719 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
720 _ => return,
721 };
722 ed.sync_buffer_content_from_textarea();
723 let cursor = buf_cursor_pos(&ed.buffer);
724 ed.mutate_edit(Edit::InsertStr {
725 at: cursor,
726 text: text.clone(),
727 });
728 let mut row = cursor.row;
731 let mut col = cursor.col;
732 for ch in text.chars() {
733 if ch == '\n' {
734 row += 1;
735 col = 0;
736 } else {
737 col += 1;
738 }
739 }
740 buf_set_cursor_rc(&mut ed.buffer, row, col);
741 ed.push_buffer_cursor_to_textarea();
742 ed.mark_content_dirty();
743 if let Some(ref mut session) = ed.vim.insert_session {
744 session.row_min = session.row_min.min(row);
745 session.row_max = session.row_max.max(row);
746 }
747}
748
749pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
768 if !settings.autoindent {
769 return String::new();
770 }
771 let base: String = prev_line
773 .chars()
774 .take_while(|c| *c == ' ' || *c == '\t')
775 .collect();
776
777 if settings.smartindent {
778 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
782 if matches!(last_non_ws, Some('{' | '(' | '[')) {
783 let unit = if settings.expandtab {
784 if settings.softtabstop > 0 {
785 " ".repeat(settings.softtabstop)
786 } else {
787 " ".repeat(settings.shiftwidth)
788 }
789 } else {
790 "\t".to_string()
791 };
792 return format!("{base}{unit}");
793 }
794 }
795
796 base
797}
798
799fn try_dedent_close_bracket<H: crate::types::Host>(
809 ed: &mut Editor<hjkl_buffer::Buffer, H>,
810 cursor: hjkl_buffer::Position,
811 ch: char,
812) -> bool {
813 use hjkl_buffer::{Edit, MotionKind, Position};
814
815 if !ed.settings.smartindent {
816 return false;
817 }
818 if !matches!(ch, '}' | ')' | ']') {
819 return false;
820 }
821
822 let line = match buf_line(&ed.buffer, cursor.row) {
823 Some(l) => l.to_string(),
824 None => return false,
825 };
826
827 let before: String = line.chars().take(cursor.col).collect();
829 if !before.chars().all(|c| c == ' ' || c == '\t') {
830 return false;
831 }
832 if before.is_empty() {
833 return false;
835 }
836
837 let unit_len: usize = if ed.settings.expandtab {
839 if ed.settings.softtabstop > 0 {
840 ed.settings.softtabstop
841 } else {
842 ed.settings.shiftwidth
843 }
844 } else {
845 1
847 };
848
849 let strip_len = if ed.settings.expandtab {
851 let spaces = before.chars().filter(|c| *c == ' ').count();
853 if spaces < unit_len {
854 return false;
855 }
856 unit_len
857 } else {
858 if !before.starts_with('\t') {
860 return false;
861 }
862 1
863 };
864
865 ed.mutate_edit(Edit::DeleteRange {
867 start: Position::new(cursor.row, 0),
868 end: Position::new(cursor.row, strip_len),
869 kind: MotionKind::Char,
870 });
871 let new_col = cursor.col.saturating_sub(strip_len);
876 ed.mutate_edit(Edit::InsertChar {
877 at: Position::new(cursor.row, new_col),
878 ch,
879 });
880 true
881}
882
883fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
884 let Some(session) = ed.vim.insert_session.take() else {
885 return;
886 };
887 let lines = buf_lines_to_vec(&ed.buffer);
888 let after_end = session.row_max.min(lines.len().saturating_sub(1));
892 let before_end = session
893 .row_max
894 .min(session.before_lines.len().saturating_sub(1));
895 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
896 session.before_lines[session.row_min..=before_end].join("\n")
897 } else {
898 String::new()
899 };
900 let after = if after_end >= session.row_min && session.row_min < lines.len() {
901 lines[session.row_min..=after_end].join("\n")
902 } else {
903 String::new()
904 };
905 let inserted = extract_inserted(&before, &after);
906 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
907 use hjkl_buffer::{Edit, Position};
908 for _ in 0..session.count - 1 {
909 let (row, col) = ed.cursor();
910 ed.mutate_edit(Edit::InsertStr {
911 at: Position::new(row, col),
912 text: inserted.clone(),
913 });
914 }
915 }
916 fn replicate_block_text<H: crate::types::Host>(
920 ed: &mut Editor<hjkl_buffer::Buffer, H>,
921 inserted: &str,
922 top: usize,
923 bot: usize,
924 col: usize,
925 ) {
926 use hjkl_buffer::{Edit, Position};
927 for r in (top + 1)..=bot {
928 let line_len = buf_line_chars(&ed.buffer, r);
929 if col > line_len {
930 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
931 ed.mutate_edit(Edit::InsertStr {
932 at: Position::new(r, line_len),
933 text: pad,
934 });
935 }
936 ed.mutate_edit(Edit::InsertStr {
937 at: Position::new(r, col),
938 text: inserted.to_string(),
939 });
940 }
941 }
942
943 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
944 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
947 replicate_block_text(ed, &inserted, top, bot, col);
948 buf_set_cursor_rc(&mut ed.buffer, top, col);
949 ed.push_buffer_cursor_to_textarea();
950 }
951 return;
952 }
953 if let InsertReason::BlockChange { top, bot, col } = session.reason {
954 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
958 replicate_block_text(ed, &inserted, top, bot, col);
959 let ins_chars = inserted.chars().count();
960 let line_len = buf_line_chars(&ed.buffer, top);
961 let target_col = (col + ins_chars).min(line_len);
962 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
963 ed.push_buffer_cursor_to_textarea();
964 }
965 return;
966 }
967 if ed.vim.replaying {
968 return;
969 }
970 match session.reason {
971 InsertReason::Enter(entry) => {
972 ed.vim.last_change = Some(LastChange::InsertAt {
973 entry,
974 inserted,
975 count: session.count,
976 });
977 }
978 InsertReason::Open { above } => {
979 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
980 }
981 InsertReason::AfterChange => {
982 if let Some(
983 LastChange::OpMotion { inserted: ins, .. }
984 | LastChange::OpTextObj { inserted: ins, .. }
985 | LastChange::LineOp { inserted: ins, .. },
986 ) = ed.vim.last_change.as_mut()
987 {
988 *ins = Some(inserted);
989 }
990 if let Some(start) = ed.vim.change_mark_start.take() {
996 let end = ed.cursor();
997 ed.set_mark('[', start);
998 ed.set_mark(']', end);
999 }
1000 }
1001 InsertReason::DeleteToEol => {
1002 ed.vim.last_change = Some(LastChange::DeleteToEol {
1003 inserted: Some(inserted),
1004 });
1005 }
1006 InsertReason::ReplayOnly => {}
1007 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1008 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1009 InsertReason::Replace => {
1010 ed.vim.last_change = Some(LastChange::DeleteToEol {
1015 inserted: Some(inserted),
1016 });
1017 }
1018 }
1019}
1020
1021pub(crate) fn begin_insert<H: crate::types::Host>(
1022 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1023 count: usize,
1024 reason: InsertReason,
1025) {
1026 let record = !matches!(reason, InsertReason::ReplayOnly);
1027 if record {
1028 ed.push_undo();
1029 }
1030 let reason = if ed.vim.replaying {
1031 InsertReason::ReplayOnly
1032 } else {
1033 reason
1034 };
1035 let (row, _) = ed.cursor();
1036 ed.vim.insert_session = Some(InsertSession {
1037 count,
1038 row_min: row,
1039 row_max: row,
1040 before_lines: buf_lines_to_vec(&ed.buffer),
1041 reason,
1042 });
1043 ed.vim.mode = Mode::Insert;
1044 ed.vim.current_mode = crate::VimMode::Insert;
1046}
1047
1048pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1063 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1064) {
1065 if !ed.settings.undo_break_on_motion {
1066 return;
1067 }
1068 if ed.vim.replaying {
1069 return;
1070 }
1071 if ed.vim.insert_session.is_none() {
1072 return;
1073 }
1074 ed.push_undo();
1075 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1076 let mut lines: Vec<String> = Vec::with_capacity(n);
1077 for r in 0..n {
1078 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1079 }
1080 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1081 if let Some(ref mut session) = ed.vim.insert_session {
1082 session.before_lines = lines;
1083 session.row_min = row;
1084 session.row_max = row;
1085 }
1086}
1087
1088pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1109 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1110 ch: char,
1111) -> bool {
1112 use hjkl_buffer::{Edit, MotionKind, Position};
1113 ed.sync_buffer_content_from_textarea();
1114 let cursor = buf_cursor_pos(&ed.buffer);
1115 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1116 let in_replace = matches!(
1117 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1118 Some(InsertReason::Replace)
1119 );
1120 if in_replace && cursor.col < line_chars {
1121 ed.mutate_edit(Edit::DeleteRange {
1122 start: cursor,
1123 end: Position::new(cursor.row, cursor.col + 1),
1124 kind: MotionKind::Char,
1125 });
1126 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1127 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1128 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1129 }
1130 ed.push_buffer_cursor_to_textarea();
1131 true
1132}
1133
1134pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1137 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1138) -> bool {
1139 use hjkl_buffer::Edit;
1140 ed.sync_buffer_content_from_textarea();
1141 let cursor = buf_cursor_pos(&ed.buffer);
1142 let prev_line = buf_line(&ed.buffer, cursor.row)
1143 .unwrap_or_default()
1144 .to_string();
1145 let indent = compute_enter_indent(&ed.settings, &prev_line);
1146 let text = format!("\n{indent}");
1147 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1148 ed.push_buffer_cursor_to_textarea();
1149 true
1150}
1151
1152pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1155 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1156) -> bool {
1157 use hjkl_buffer::Edit;
1158 ed.sync_buffer_content_from_textarea();
1159 let cursor = buf_cursor_pos(&ed.buffer);
1160 if ed.settings.expandtab {
1161 let sts = ed.settings.softtabstop;
1162 let n = if sts > 0 {
1163 sts - (cursor.col % sts)
1164 } else {
1165 ed.settings.tabstop.max(1)
1166 };
1167 ed.mutate_edit(Edit::InsertStr {
1168 at: cursor,
1169 text: " ".repeat(n),
1170 });
1171 } else {
1172 ed.mutate_edit(Edit::InsertChar {
1173 at: cursor,
1174 ch: '\t',
1175 });
1176 }
1177 ed.push_buffer_cursor_to_textarea();
1178 true
1179}
1180
1181pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1187 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1188) -> bool {
1189 use hjkl_buffer::{Edit, MotionKind, Position};
1190 ed.sync_buffer_content_from_textarea();
1191 let cursor = buf_cursor_pos(&ed.buffer);
1192 let sts = ed.settings.softtabstop;
1193 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1194 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1195 let chars: Vec<char> = line.chars().collect();
1196 let run_start = cursor.col - sts;
1197 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1198 ed.mutate_edit(Edit::DeleteRange {
1199 start: Position::new(cursor.row, run_start),
1200 end: cursor,
1201 kind: MotionKind::Char,
1202 });
1203 ed.push_buffer_cursor_to_textarea();
1204 return true;
1205 }
1206 }
1207 let result = if cursor.col > 0 {
1208 ed.mutate_edit(Edit::DeleteRange {
1209 start: Position::new(cursor.row, cursor.col - 1),
1210 end: cursor,
1211 kind: MotionKind::Char,
1212 });
1213 true
1214 } else if cursor.row > 0 {
1215 let prev_row = cursor.row - 1;
1216 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1217 ed.mutate_edit(Edit::JoinLines {
1218 row: prev_row,
1219 count: 1,
1220 with_space: false,
1221 });
1222 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1223 true
1224 } else {
1225 false
1226 };
1227 ed.push_buffer_cursor_to_textarea();
1228 result
1229}
1230
1231pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
1234 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1235) -> bool {
1236 use hjkl_buffer::{Edit, MotionKind, Position};
1237 ed.sync_buffer_content_from_textarea();
1238 let cursor = buf_cursor_pos(&ed.buffer);
1239 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1240 let result = if cursor.col < line_chars {
1241 ed.mutate_edit(Edit::DeleteRange {
1242 start: cursor,
1243 end: Position::new(cursor.row, cursor.col + 1),
1244 kind: MotionKind::Char,
1245 });
1246 buf_set_cursor_pos(&mut ed.buffer, cursor);
1247 true
1248 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1249 ed.mutate_edit(Edit::JoinLines {
1250 row: cursor.row,
1251 count: 1,
1252 with_space: false,
1253 });
1254 buf_set_cursor_pos(&mut ed.buffer, cursor);
1255 true
1256 } else {
1257 false
1258 };
1259 ed.push_buffer_cursor_to_textarea();
1260 result
1261}
1262
1263#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1265pub enum InsertDir {
1266 Left,
1267 Right,
1268 Up,
1269 Down,
1270}
1271
1272pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
1275 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1276 dir: InsertDir,
1277) -> bool {
1278 ed.sync_buffer_content_from_textarea();
1279 match dir {
1280 InsertDir::Left => {
1281 crate::motions::move_left(&mut ed.buffer, 1);
1282 }
1283 InsertDir::Right => {
1284 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1285 }
1286 InsertDir::Up => {
1287 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1288 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1289 }
1290 InsertDir::Down => {
1291 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1292 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1293 }
1294 }
1295 break_undo_group_in_insert(ed);
1296 ed.push_buffer_cursor_to_textarea();
1297 false
1298}
1299
1300pub(crate) fn insert_home_bridge<H: crate::types::Host>(
1303 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1304) -> bool {
1305 ed.sync_buffer_content_from_textarea();
1306 crate::motions::move_line_start(&mut ed.buffer);
1307 break_undo_group_in_insert(ed);
1308 ed.push_buffer_cursor_to_textarea();
1309 false
1310}
1311
1312pub(crate) fn insert_end_bridge<H: crate::types::Host>(
1315 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1316) -> bool {
1317 ed.sync_buffer_content_from_textarea();
1318 crate::motions::move_line_end(&mut ed.buffer);
1319 break_undo_group_in_insert(ed);
1320 ed.push_buffer_cursor_to_textarea();
1321 false
1322}
1323
1324pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
1327 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1328 viewport_h: u16,
1329) -> bool {
1330 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1331 scroll_cursor_rows(ed, -rows);
1332 false
1333}
1334
1335pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
1338 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1339 viewport_h: u16,
1340) -> bool {
1341 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1342 scroll_cursor_rows(ed, rows);
1343 false
1344}
1345
1346pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
1350 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1351) -> bool {
1352 use hjkl_buffer::{Edit, MotionKind};
1353 ed.sync_buffer_content_from_textarea();
1354 let cursor = buf_cursor_pos(&ed.buffer);
1355 if cursor.row == 0 && cursor.col == 0 {
1356 return true;
1357 }
1358 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1359 let word_start = buf_cursor_pos(&ed.buffer);
1360 if word_start == cursor {
1361 return true;
1362 }
1363 buf_set_cursor_pos(&mut ed.buffer, cursor);
1364 ed.mutate_edit(Edit::DeleteRange {
1365 start: word_start,
1366 end: cursor,
1367 kind: MotionKind::Char,
1368 });
1369 ed.push_buffer_cursor_to_textarea();
1370 true
1371}
1372
1373pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
1376 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1377) -> bool {
1378 use hjkl_buffer::{Edit, MotionKind, Position};
1379 ed.sync_buffer_content_from_textarea();
1380 let cursor = buf_cursor_pos(&ed.buffer);
1381 if cursor.col > 0 {
1382 ed.mutate_edit(Edit::DeleteRange {
1383 start: Position::new(cursor.row, 0),
1384 end: cursor,
1385 kind: MotionKind::Char,
1386 });
1387 ed.push_buffer_cursor_to_textarea();
1388 }
1389 true
1390}
1391
1392pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
1396 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1397) -> bool {
1398 use hjkl_buffer::{Edit, MotionKind, Position};
1399 ed.sync_buffer_content_from_textarea();
1400 let cursor = buf_cursor_pos(&ed.buffer);
1401 if cursor.col > 0 {
1402 ed.mutate_edit(Edit::DeleteRange {
1403 start: Position::new(cursor.row, cursor.col - 1),
1404 end: cursor,
1405 kind: MotionKind::Char,
1406 });
1407 } else if cursor.row > 0 {
1408 let prev_row = cursor.row - 1;
1409 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1410 ed.mutate_edit(Edit::JoinLines {
1411 row: prev_row,
1412 count: 1,
1413 with_space: false,
1414 });
1415 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1416 }
1417 ed.push_buffer_cursor_to_textarea();
1418 true
1419}
1420
1421pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
1424 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1425) -> bool {
1426 let (row, col) = ed.cursor();
1427 let sw = ed.settings().shiftwidth;
1428 indent_rows(ed, row, row, 1);
1429 ed.jump_cursor(row, col + sw);
1430 true
1431}
1432
1433pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
1436 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1437) -> bool {
1438 let (row, col) = ed.cursor();
1439 let before_len = buf_line_bytes(&ed.buffer, row);
1440 outdent_rows(ed, row, row, 1);
1441 let after_len = buf_line_bytes(&ed.buffer, row);
1442 let stripped = before_len.saturating_sub(after_len);
1443 let new_col = col.saturating_sub(stripped);
1444 ed.jump_cursor(row, new_col);
1445 true
1446}
1447
1448pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
1452 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1453) -> bool {
1454 ed.vim.one_shot_normal = true;
1455 ed.vim.mode = Mode::Normal;
1456 ed.vim.current_mode = crate::VimMode::Normal;
1458 false
1459}
1460
1461pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
1465 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1466) -> bool {
1467 ed.vim.insert_pending_register = true;
1468 false
1469}
1470
1471pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
1475 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1476 reg: char,
1477) -> bool {
1478 insert_register_text(ed, reg);
1479 true
1482}
1483
1484pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
1489 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1490) -> bool {
1491 finish_insert_session(ed);
1492 ed.vim.mode = Mode::Normal;
1493 ed.vim.current_mode = crate::VimMode::Normal;
1495 let col = ed.cursor().1;
1496 ed.vim.last_insert_pos = Some(ed.cursor());
1497 if col > 0 {
1498 crate::motions::move_left(&mut ed.buffer, 1);
1499 ed.push_buffer_cursor_to_textarea();
1500 }
1501 ed.sticky_col = Some(ed.cursor().1);
1502 true
1503}
1504
1505#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1510pub enum ScrollDir {
1511 Down,
1513 Up,
1515}
1516
1517pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
1522 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1523 count: usize,
1524) {
1525 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
1526}
1527
1528pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
1530 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1531 count: usize,
1532) {
1533 move_first_non_whitespace(ed);
1534 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
1535}
1536
1537pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
1539 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1540 count: usize,
1541) {
1542 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1543 ed.push_buffer_cursor_to_textarea();
1544 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
1545}
1546
1547pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
1549 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1550 count: usize,
1551) {
1552 crate::motions::move_line_end(&mut ed.buffer);
1553 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1554 ed.push_buffer_cursor_to_textarea();
1555 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
1556}
1557
1558pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
1560 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1561 count: usize,
1562) {
1563 use hjkl_buffer::{Edit, Position};
1564 ed.push_undo();
1565 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
1566 ed.sync_buffer_content_from_textarea();
1567 let row = buf_cursor_pos(&ed.buffer).row;
1568 let line_chars = buf_line_chars(&ed.buffer, row);
1569 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
1570 let indent = compute_enter_indent(&ed.settings, prev_line);
1571 ed.mutate_edit(Edit::InsertStr {
1572 at: Position::new(row, line_chars),
1573 text: format!("\n{indent}"),
1574 });
1575 ed.push_buffer_cursor_to_textarea();
1576}
1577
1578pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
1580 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1581 count: usize,
1582) {
1583 use hjkl_buffer::{Edit, Position};
1584 ed.push_undo();
1585 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
1586 ed.sync_buffer_content_from_textarea();
1587 let row = buf_cursor_pos(&ed.buffer).row;
1588 let indent = if row > 0 {
1589 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
1590 compute_enter_indent(&ed.settings, above)
1591 } else {
1592 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
1593 cur.chars()
1594 .take_while(|c| *c == ' ' || *c == '\t')
1595 .collect::<String>()
1596 };
1597 ed.mutate_edit(Edit::InsertStr {
1598 at: Position::new(row, 0),
1599 text: format!("{indent}\n"),
1600 });
1601 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1602 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1603 let new_row = buf_cursor_pos(&ed.buffer).row;
1604 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
1605 ed.push_buffer_cursor_to_textarea();
1606}
1607
1608pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
1610 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1611 count: usize,
1612) {
1613 begin_insert(ed, count.max(1), InsertReason::Replace);
1614}
1615
1616pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
1621 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1622 count: usize,
1623) {
1624 do_char_delete(ed, true, count.max(1));
1625 if !ed.vim.replaying {
1626 ed.vim.last_change = Some(LastChange::CharDel {
1627 forward: true,
1628 count: count.max(1),
1629 });
1630 }
1631}
1632
1633pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
1636 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1637 count: usize,
1638) {
1639 do_char_delete(ed, false, count.max(1));
1640 if !ed.vim.replaying {
1641 ed.vim.last_change = Some(LastChange::CharDel {
1642 forward: false,
1643 count: count.max(1),
1644 });
1645 }
1646}
1647
1648pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
1651 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1652 count: usize,
1653) {
1654 use hjkl_buffer::{Edit, MotionKind, Position};
1655 ed.push_undo();
1656 ed.sync_buffer_content_from_textarea();
1657 for _ in 0..count.max(1) {
1658 let cursor = buf_cursor_pos(&ed.buffer);
1659 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1660 if cursor.col >= line_chars {
1661 break;
1662 }
1663 ed.mutate_edit(Edit::DeleteRange {
1664 start: cursor,
1665 end: Position::new(cursor.row, cursor.col + 1),
1666 kind: MotionKind::Char,
1667 });
1668 }
1669 ed.push_buffer_cursor_to_textarea();
1670 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
1671 if !ed.vim.replaying {
1672 ed.vim.last_change = Some(LastChange::OpMotion {
1673 op: Operator::Change,
1674 motion: Motion::Right,
1675 count: count.max(1),
1676 inserted: None,
1677 });
1678 }
1679}
1680
1681pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
1684 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1685 count: usize,
1686) {
1687 execute_line_op(ed, Operator::Change, count.max(1));
1688 if !ed.vim.replaying {
1689 ed.vim.last_change = Some(LastChange::LineOp {
1690 op: Operator::Change,
1691 count: count.max(1),
1692 inserted: None,
1693 });
1694 }
1695}
1696
1697pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1700 ed.push_undo();
1701 delete_to_eol(ed);
1702 crate::motions::move_left(&mut ed.buffer, 1);
1703 ed.push_buffer_cursor_to_textarea();
1704 if !ed.vim.replaying {
1705 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
1706 }
1707}
1708
1709pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1712 ed.push_undo();
1713 delete_to_eol(ed);
1714 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
1715}
1716
1717pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
1719 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1720 count: usize,
1721) {
1722 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
1723}
1724
1725pub(crate) fn join_line_bridge<H: crate::types::Host>(
1728 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1729 count: usize,
1730) {
1731 for _ in 0..count.max(1) {
1732 ed.push_undo();
1733 join_line(ed);
1734 }
1735 if !ed.vim.replaying {
1736 ed.vim.last_change = Some(LastChange::JoinLine {
1737 count: count.max(1),
1738 });
1739 }
1740}
1741
1742pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
1745 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1746 count: usize,
1747) {
1748 for _ in 0..count.max(1) {
1749 ed.push_undo();
1750 toggle_case_at_cursor(ed);
1751 }
1752 if !ed.vim.replaying {
1753 ed.vim.last_change = Some(LastChange::ToggleCase {
1754 count: count.max(1),
1755 });
1756 }
1757}
1758
1759pub(crate) fn paste_after_bridge<H: crate::types::Host>(
1763 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1764 count: usize,
1765) {
1766 do_paste(ed, false, count.max(1));
1767 if !ed.vim.replaying {
1768 ed.vim.last_change = Some(LastChange::Paste {
1769 before: false,
1770 count: count.max(1),
1771 });
1772 }
1773}
1774
1775pub(crate) fn paste_before_bridge<H: crate::types::Host>(
1779 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1780 count: usize,
1781) {
1782 do_paste(ed, true, count.max(1));
1783 if !ed.vim.replaying {
1784 ed.vim.last_change = Some(LastChange::Paste {
1785 before: true,
1786 count: count.max(1),
1787 });
1788 }
1789}
1790
1791pub(crate) fn jump_back_bridge<H: crate::types::Host>(
1796 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1797 count: usize,
1798) {
1799 for _ in 0..count.max(1) {
1800 jump_back(ed);
1801 }
1802}
1803
1804pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
1807 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1808 count: usize,
1809) {
1810 for _ in 0..count.max(1) {
1811 jump_forward(ed);
1812 }
1813}
1814
1815pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
1820 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1821 dir: ScrollDir,
1822 count: usize,
1823) {
1824 let rows = viewport_full_rows(ed, count) as isize;
1825 match dir {
1826 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1827 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1828 }
1829}
1830
1831pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
1834 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1835 dir: ScrollDir,
1836 count: usize,
1837) {
1838 let rows = viewport_half_rows(ed, count) as isize;
1839 match dir {
1840 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1841 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1842 }
1843}
1844
1845pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
1849 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1850 dir: ScrollDir,
1851 count: usize,
1852) {
1853 let n = count.max(1);
1854 let total = buf_row_count(&ed.buffer);
1855 let last = total.saturating_sub(1);
1856 let h = ed.viewport_height_value() as usize;
1857 let vp = ed.host().viewport();
1858 let cur_top = vp.top_row;
1859 let new_top = match dir {
1860 ScrollDir::Down => (cur_top + n).min(last),
1861 ScrollDir::Up => cur_top.saturating_sub(n),
1862 };
1863 ed.set_viewport_top(new_top);
1864 let (row, col) = ed.cursor();
1866 let bot = (new_top + h).saturating_sub(1).min(last);
1867 let clamped = row.max(new_top).min(bot);
1868 if clamped != row {
1869 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
1870 ed.push_buffer_cursor_to_textarea();
1871 }
1872}
1873
1874pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
1879 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1880 forward: bool,
1881 count: usize,
1882) {
1883 if let Some(pattern) = ed.vim.last_search.clone() {
1884 ed.push_search_pattern(&pattern);
1885 }
1886 if ed.search_state().pattern.is_none() {
1887 return;
1888 }
1889 let go_forward = ed.vim.last_search_forward == forward;
1890 for _ in 0..count.max(1) {
1891 if go_forward {
1892 ed.search_advance_forward(true);
1893 } else {
1894 ed.search_advance_backward(true);
1895 }
1896 }
1897 ed.push_buffer_cursor_to_textarea();
1898}
1899
1900pub(crate) fn word_search_bridge<H: crate::types::Host>(
1904 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1905 forward: bool,
1906 whole_word: bool,
1907 count: usize,
1908) {
1909 word_at_cursor_search(ed, forward, whole_word, count.max(1));
1910}
1911
1912#[allow(dead_code)]
1917#[inline]
1918pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1919 do_undo(ed);
1920}
1921
1922#[inline]
1937pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
1938 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1939 mode: Mode,
1940) {
1941 ed.vim.mode = mode;
1942 ed.vim.current_mode = ed.vim.public_mode();
1943}
1944
1945pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
1948 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1949) {
1950 let cur = ed.cursor();
1951 ed.vim.visual_anchor = cur;
1952 set_vim_mode_bridge(ed, Mode::Visual);
1953}
1954
1955pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
1958 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1959) {
1960 let (row, _) = ed.cursor();
1961 ed.vim.visual_line_anchor = row;
1962 set_vim_mode_bridge(ed, Mode::VisualLine);
1963}
1964
1965pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
1969 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1970) {
1971 let cur = ed.cursor();
1972 ed.vim.block_anchor = cur;
1973 ed.vim.block_vcol = cur.1;
1974 set_vim_mode_bridge(ed, Mode::VisualBlock);
1975}
1976
1977pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
1982 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1983) {
1984 let snap: Option<LastVisual> = match ed.vim.mode {
1986 Mode::Visual => Some(LastVisual {
1987 mode: Mode::Visual,
1988 anchor: ed.vim.visual_anchor,
1989 cursor: ed.cursor(),
1990 block_vcol: 0,
1991 }),
1992 Mode::VisualLine => Some(LastVisual {
1993 mode: Mode::VisualLine,
1994 anchor: (ed.vim.visual_line_anchor, 0),
1995 cursor: ed.cursor(),
1996 block_vcol: 0,
1997 }),
1998 Mode::VisualBlock => Some(LastVisual {
1999 mode: Mode::VisualBlock,
2000 anchor: ed.vim.block_anchor,
2001 cursor: ed.cursor(),
2002 block_vcol: ed.vim.block_vcol,
2003 }),
2004 _ => None,
2005 };
2006 ed.vim.pending = Pending::None;
2008 ed.vim.count = 0;
2009 ed.vim.insert_session = None;
2010 set_vim_mode_bridge(ed, Mode::Normal);
2011 if let Some(snap) = snap {
2015 let (lo, hi) = match snap.mode {
2016 Mode::Visual => {
2017 if snap.anchor <= snap.cursor {
2018 (snap.anchor, snap.cursor)
2019 } else {
2020 (snap.cursor, snap.anchor)
2021 }
2022 }
2023 Mode::VisualLine => {
2024 let r_lo = snap.anchor.0.min(snap.cursor.0);
2025 let r_hi = snap.anchor.0.max(snap.cursor.0);
2026 let last_col = ed
2027 .buffer()
2028 .lines()
2029 .get(r_hi)
2030 .map(|l| l.chars().count().saturating_sub(1))
2031 .unwrap_or(0);
2032 ((r_lo, 0), (r_hi, last_col))
2033 }
2034 Mode::VisualBlock => {
2035 let (r1, c1) = snap.anchor;
2036 let (r2, c2) = snap.cursor;
2037 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2038 }
2039 _ => {
2040 if snap.anchor <= snap.cursor {
2041 (snap.anchor, snap.cursor)
2042 } else {
2043 (snap.cursor, snap.anchor)
2044 }
2045 }
2046 };
2047 ed.set_mark('<', lo);
2048 ed.set_mark('>', hi);
2049 ed.vim.last_visual = Some(snap);
2050 }
2051}
2052
2053pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2059 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2060) {
2061 match ed.vim.mode {
2062 Mode::Visual => {
2063 let cur = ed.cursor();
2064 let anchor = ed.vim.visual_anchor;
2065 ed.vim.visual_anchor = cur;
2066 ed.jump_cursor(anchor.0, anchor.1);
2067 }
2068 Mode::VisualLine => {
2069 let cur_row = ed.cursor().0;
2070 let anchor_row = ed.vim.visual_line_anchor;
2071 ed.vim.visual_line_anchor = cur_row;
2072 ed.jump_cursor(anchor_row, 0);
2073 }
2074 Mode::VisualBlock => {
2075 let cur = ed.cursor();
2076 let anchor = ed.vim.block_anchor;
2077 ed.vim.block_anchor = cur;
2078 ed.vim.block_vcol = anchor.1;
2079 ed.jump_cursor(anchor.0, anchor.1);
2080 }
2081 _ => {}
2082 }
2083}
2084
2085pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
2089 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2090) {
2091 if let Some(snap) = ed.vim.last_visual {
2092 match snap.mode {
2093 Mode::Visual => {
2094 ed.vim.visual_anchor = snap.anchor;
2095 set_vim_mode_bridge(ed, Mode::Visual);
2096 }
2097 Mode::VisualLine => {
2098 ed.vim.visual_line_anchor = snap.anchor.0;
2099 set_vim_mode_bridge(ed, Mode::VisualLine);
2100 }
2101 Mode::VisualBlock => {
2102 ed.vim.block_anchor = snap.anchor;
2103 ed.vim.block_vcol = snap.block_vcol;
2104 set_vim_mode_bridge(ed, Mode::VisualBlock);
2105 }
2106 _ => {}
2107 }
2108 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2109 }
2110}
2111
2112pub(crate) fn set_mode_bridge<H: crate::types::Host>(
2118 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2119 mode: crate::VimMode,
2120) {
2121 let internal = match mode {
2122 crate::VimMode::Normal => Mode::Normal,
2123 crate::VimMode::Insert => Mode::Insert,
2124 crate::VimMode::Visual => Mode::Visual,
2125 crate::VimMode::VisualLine => Mode::VisualLine,
2126 crate::VimMode::VisualBlock => Mode::VisualBlock,
2127 };
2128 ed.vim.mode = internal;
2129 ed.vim.current_mode = mode;
2130}
2131
2132pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2149 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2150 ch: char,
2151) {
2152 if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
2153 let pos = ed.cursor();
2158 ed.set_mark(ch, pos);
2159 }
2160 }
2162
2163pub(crate) fn goto_mark<H: crate::types::Host>(
2172 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2173 ch: char,
2174 linewise: bool,
2175) {
2176 let target = match ch {
2177 'a'..='z' | 'A'..='Z' => ed.mark(ch),
2178 '\'' | '`' => ed.vim.jump_back.last().copied(),
2179 '.' => ed.vim.last_edit_pos,
2180 '[' | ']' | '<' | '>' => ed.mark(ch),
2181 _ => None,
2182 };
2183 let Some((row, col)) = target else {
2184 return;
2185 };
2186 let pre = ed.cursor();
2187 let (r, c_clamped) = clamp_pos(ed, (row, col));
2188 if linewise {
2189 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2190 ed.push_buffer_cursor_to_textarea();
2191 move_first_non_whitespace(ed);
2192 } else {
2193 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2194 ed.push_buffer_cursor_to_textarea();
2195 }
2196 if ed.cursor() != pre {
2197 ed.push_jump(pre);
2198 }
2199 ed.sticky_col = Some(ed.cursor().1);
2200}
2201
2202pub fn op_is_change(op: Operator) -> bool {
2206 matches!(op, Operator::Delete | Operator::Change)
2207}
2208
2209pub(crate) const JUMPLIST_MAX: usize = 100;
2213
2214fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2217 let Some(target) = ed.vim.jump_back.pop() else {
2218 return;
2219 };
2220 let cur = ed.cursor();
2221 ed.vim.jump_fwd.push(cur);
2222 let (r, c) = clamp_pos(ed, target);
2223 ed.jump_cursor(r, c);
2224 ed.sticky_col = Some(c);
2225}
2226
2227fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2230 let Some(target) = ed.vim.jump_fwd.pop() else {
2231 return;
2232 };
2233 let cur = ed.cursor();
2234 ed.vim.jump_back.push(cur);
2235 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2236 ed.vim.jump_back.remove(0);
2237 }
2238 let (r, c) = clamp_pos(ed, target);
2239 ed.jump_cursor(r, c);
2240 ed.sticky_col = Some(c);
2241}
2242
2243fn clamp_pos<H: crate::types::Host>(
2246 ed: &Editor<hjkl_buffer::Buffer, H>,
2247 pos: (usize, usize),
2248) -> (usize, usize) {
2249 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2250 let r = pos.0.min(last_row);
2251 let line_len = buf_line_chars(&ed.buffer, r);
2252 let c = pos.1.min(line_len.saturating_sub(1));
2253 (r, c)
2254}
2255
2256fn is_big_jump(motion: &Motion) -> bool {
2258 matches!(
2259 motion,
2260 Motion::FileTop
2261 | Motion::FileBottom
2262 | Motion::MatchBracket
2263 | Motion::WordAtCursor { .. }
2264 | Motion::SearchNext { .. }
2265 | Motion::ViewportTop
2266 | Motion::ViewportMiddle
2267 | Motion::ViewportBottom
2268 )
2269}
2270
2271fn viewport_half_rows<H: crate::types::Host>(
2276 ed: &Editor<hjkl_buffer::Buffer, H>,
2277 count: usize,
2278) -> usize {
2279 let h = ed.viewport_height_value() as usize;
2280 (h / 2).max(1).saturating_mul(count.max(1))
2281}
2282
2283fn viewport_full_rows<H: crate::types::Host>(
2286 ed: &Editor<hjkl_buffer::Buffer, H>,
2287 count: usize,
2288) -> usize {
2289 let h = ed.viewport_height_value() as usize;
2290 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2291}
2292
2293fn scroll_cursor_rows<H: crate::types::Host>(
2298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2299 delta: isize,
2300) {
2301 if delta == 0 {
2302 return;
2303 }
2304 ed.sync_buffer_content_from_textarea();
2305 let (row, _) = ed.cursor();
2306 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2307 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2308 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2309 crate::motions::move_first_non_blank(&mut ed.buffer);
2310 ed.push_buffer_cursor_to_textarea();
2311 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2312}
2313
2314pub fn parse_motion(input: &Input) -> Option<Motion> {
2320 if input.ctrl {
2321 return None;
2322 }
2323 match input.key {
2324 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2325 Key::Char('l') | Key::Right => Some(Motion::Right),
2326 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2327 Key::Char('k') | Key::Up => Some(Motion::Up),
2328 Key::Char('w') => Some(Motion::WordFwd),
2329 Key::Char('W') => Some(Motion::BigWordFwd),
2330 Key::Char('b') => Some(Motion::WordBack),
2331 Key::Char('B') => Some(Motion::BigWordBack),
2332 Key::Char('e') => Some(Motion::WordEnd),
2333 Key::Char('E') => Some(Motion::BigWordEnd),
2334 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2335 Key::Char('^') => Some(Motion::FirstNonBlank),
2336 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2337 Key::Char('G') => Some(Motion::FileBottom),
2338 Key::Char('%') => Some(Motion::MatchBracket),
2339 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2340 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2341 Key::Char('*') => Some(Motion::WordAtCursor {
2342 forward: true,
2343 whole_word: true,
2344 }),
2345 Key::Char('#') => Some(Motion::WordAtCursor {
2346 forward: false,
2347 whole_word: true,
2348 }),
2349 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2350 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2351 Key::Char('H') => Some(Motion::ViewportTop),
2352 Key::Char('M') => Some(Motion::ViewportMiddle),
2353 Key::Char('L') => Some(Motion::ViewportBottom),
2354 Key::Char('{') => Some(Motion::ParagraphPrev),
2355 Key::Char('}') => Some(Motion::ParagraphNext),
2356 Key::Char('(') => Some(Motion::SentencePrev),
2357 Key::Char(')') => Some(Motion::SentenceNext),
2358 _ => None,
2359 }
2360}
2361
2362pub(crate) fn execute_motion<H: crate::types::Host>(
2365 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2366 motion: Motion,
2367 count: usize,
2368) {
2369 let count = count.max(1);
2370 let motion = match motion {
2372 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2373 Some((ch, forward, till)) => Motion::Find {
2374 ch,
2375 forward: if reverse { !forward } else { forward },
2376 till,
2377 },
2378 None => return,
2379 },
2380 other => other,
2381 };
2382 let pre_pos = ed.cursor();
2383 let pre_col = pre_pos.1;
2384 apply_motion_cursor(ed, &motion, count);
2385 let post_pos = ed.cursor();
2386 if is_big_jump(&motion) && pre_pos != post_pos {
2387 ed.push_jump(pre_pos);
2388 }
2389 apply_sticky_col(ed, &motion, pre_col);
2390 ed.sync_buffer_from_textarea();
2395}
2396
2397fn execute_motion_with_block_vcol<H: crate::types::Host>(
2408 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2409 motion: Motion,
2410 count: usize,
2411) {
2412 let motion_copy = motion.clone();
2413 execute_motion(ed, motion, count);
2414 if ed.vim.mode == Mode::VisualBlock {
2415 update_block_vcol(ed, &motion_copy);
2416 }
2417}
2418
2419pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2447 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2448 kind: crate::MotionKind,
2449 count: usize,
2450) {
2451 let count = count.max(1);
2452 match kind {
2453 crate::MotionKind::CharLeft => {
2454 execute_motion_with_block_vcol(ed, Motion::Left, count);
2455 }
2456 crate::MotionKind::CharRight => {
2457 execute_motion_with_block_vcol(ed, Motion::Right, count);
2458 }
2459 crate::MotionKind::LineDown => {
2460 execute_motion_with_block_vcol(ed, Motion::Down, count);
2461 }
2462 crate::MotionKind::LineUp => {
2463 execute_motion_with_block_vcol(ed, Motion::Up, count);
2464 }
2465 crate::MotionKind::FirstNonBlankDown => {
2466 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2471 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2472 crate::motions::move_first_non_blank(&mut ed.buffer);
2473 ed.push_buffer_cursor_to_textarea();
2474 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2475 ed.sync_buffer_from_textarea();
2476 }
2477 crate::MotionKind::FirstNonBlankUp => {
2478 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2481 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2482 crate::motions::move_first_non_blank(&mut ed.buffer);
2483 ed.push_buffer_cursor_to_textarea();
2484 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2485 ed.sync_buffer_from_textarea();
2486 }
2487 crate::MotionKind::WordForward => {
2488 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
2489 }
2490 crate::MotionKind::BigWordForward => {
2491 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
2492 }
2493 crate::MotionKind::WordBackward => {
2494 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
2495 }
2496 crate::MotionKind::BigWordBackward => {
2497 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
2498 }
2499 crate::MotionKind::WordEnd => {
2500 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
2501 }
2502 crate::MotionKind::BigWordEnd => {
2503 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
2504 }
2505 crate::MotionKind::LineStart => {
2506 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
2509 }
2510 crate::MotionKind::FirstNonBlank => {
2511 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
2514 }
2515 crate::MotionKind::GotoLine => {
2516 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
2525 }
2526 crate::MotionKind::LineEnd => {
2527 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
2531 }
2532 crate::MotionKind::FindRepeat => {
2533 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
2537 }
2538 crate::MotionKind::FindRepeatReverse => {
2539 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
2543 }
2544 crate::MotionKind::BracketMatch => {
2545 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
2550 }
2551 crate::MotionKind::ViewportTop => {
2552 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
2555 }
2556 crate::MotionKind::ViewportMiddle => {
2557 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
2560 }
2561 crate::MotionKind::ViewportBottom => {
2562 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
2565 }
2566 crate::MotionKind::HalfPageDown => {
2567 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
2571 }
2572 crate::MotionKind::HalfPageUp => {
2573 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
2576 }
2577 crate::MotionKind::FullPageDown => {
2578 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
2581 }
2582 crate::MotionKind::FullPageUp => {
2583 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
2586 }
2587 }
2588}
2589
2590fn apply_sticky_col<H: crate::types::Host>(
2595 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2596 motion: &Motion,
2597 pre_col: usize,
2598) {
2599 if is_vertical_motion(motion) {
2600 let want = ed.sticky_col.unwrap_or(pre_col);
2601 ed.sticky_col = Some(want);
2604 let (row, _) = ed.cursor();
2605 let line_len = buf_line_chars(&ed.buffer, row);
2606 let max_col = line_len.saturating_sub(1);
2610 let target = want.min(max_col);
2611 ed.jump_cursor(row, target);
2612 } else {
2613 ed.sticky_col = Some(ed.cursor().1);
2616 }
2617}
2618
2619fn is_vertical_motion(motion: &Motion) -> bool {
2620 matches!(
2624 motion,
2625 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2626 )
2627}
2628
2629fn apply_motion_cursor<H: crate::types::Host>(
2630 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2631 motion: &Motion,
2632 count: usize,
2633) {
2634 apply_motion_cursor_ctx(ed, motion, count, false)
2635}
2636
2637pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
2638 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2639 motion: &Motion,
2640 count: usize,
2641 as_operator: bool,
2642) {
2643 match motion {
2644 Motion::Left => {
2645 crate::motions::move_left(&mut ed.buffer, count);
2647 ed.push_buffer_cursor_to_textarea();
2648 }
2649 Motion::Right => {
2650 if as_operator {
2654 crate::motions::move_right_to_end(&mut ed.buffer, count);
2655 } else {
2656 crate::motions::move_right_in_line(&mut ed.buffer, count);
2657 }
2658 ed.push_buffer_cursor_to_textarea();
2659 }
2660 Motion::Up => {
2661 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2665 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2666 ed.push_buffer_cursor_to_textarea();
2667 }
2668 Motion::Down => {
2669 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2670 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2671 ed.push_buffer_cursor_to_textarea();
2672 }
2673 Motion::ScreenUp => {
2674 let v = *ed.host.viewport();
2675 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2676 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2677 ed.push_buffer_cursor_to_textarea();
2678 }
2679 Motion::ScreenDown => {
2680 let v = *ed.host.viewport();
2681 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2682 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2683 ed.push_buffer_cursor_to_textarea();
2684 }
2685 Motion::WordFwd => {
2686 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2687 ed.push_buffer_cursor_to_textarea();
2688 }
2689 Motion::WordBack => {
2690 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2691 ed.push_buffer_cursor_to_textarea();
2692 }
2693 Motion::WordEnd => {
2694 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2695 ed.push_buffer_cursor_to_textarea();
2696 }
2697 Motion::BigWordFwd => {
2698 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2699 ed.push_buffer_cursor_to_textarea();
2700 }
2701 Motion::BigWordBack => {
2702 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2703 ed.push_buffer_cursor_to_textarea();
2704 }
2705 Motion::BigWordEnd => {
2706 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2707 ed.push_buffer_cursor_to_textarea();
2708 }
2709 Motion::WordEndBack => {
2710 crate::motions::move_word_end_back(
2711 &mut ed.buffer,
2712 false,
2713 count,
2714 &ed.settings.iskeyword,
2715 );
2716 ed.push_buffer_cursor_to_textarea();
2717 }
2718 Motion::BigWordEndBack => {
2719 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2720 ed.push_buffer_cursor_to_textarea();
2721 }
2722 Motion::LineStart => {
2723 crate::motions::move_line_start(&mut ed.buffer);
2724 ed.push_buffer_cursor_to_textarea();
2725 }
2726 Motion::FirstNonBlank => {
2727 crate::motions::move_first_non_blank(&mut ed.buffer);
2728 ed.push_buffer_cursor_to_textarea();
2729 }
2730 Motion::LineEnd => {
2731 crate::motions::move_line_end(&mut ed.buffer);
2733 ed.push_buffer_cursor_to_textarea();
2734 }
2735 Motion::FileTop => {
2736 if count > 1 {
2739 crate::motions::move_bottom(&mut ed.buffer, count);
2740 } else {
2741 crate::motions::move_top(&mut ed.buffer);
2742 }
2743 ed.push_buffer_cursor_to_textarea();
2744 }
2745 Motion::FileBottom => {
2746 if count > 1 {
2749 crate::motions::move_bottom(&mut ed.buffer, count);
2750 } else {
2751 crate::motions::move_bottom(&mut ed.buffer, 0);
2752 }
2753 ed.push_buffer_cursor_to_textarea();
2754 }
2755 Motion::Find { ch, forward, till } => {
2756 for _ in 0..count {
2757 if !find_char_on_line(ed, *ch, *forward, *till) {
2758 break;
2759 }
2760 }
2761 }
2762 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2764 let _ = matching_bracket(ed);
2765 }
2766 Motion::WordAtCursor {
2767 forward,
2768 whole_word,
2769 } => {
2770 word_at_cursor_search(ed, *forward, *whole_word, count);
2771 }
2772 Motion::SearchNext { reverse } => {
2773 if let Some(pattern) = ed.vim.last_search.clone() {
2777 ed.push_search_pattern(&pattern);
2778 }
2779 if ed.search_state().pattern.is_none() {
2780 return;
2781 }
2782 let forward = ed.vim.last_search_forward != *reverse;
2786 for _ in 0..count.max(1) {
2787 if forward {
2788 ed.search_advance_forward(true);
2789 } else {
2790 ed.search_advance_backward(true);
2791 }
2792 }
2793 ed.push_buffer_cursor_to_textarea();
2794 }
2795 Motion::ViewportTop => {
2796 let v = *ed.host().viewport();
2797 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2798 ed.push_buffer_cursor_to_textarea();
2799 }
2800 Motion::ViewportMiddle => {
2801 let v = *ed.host().viewport();
2802 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2803 ed.push_buffer_cursor_to_textarea();
2804 }
2805 Motion::ViewportBottom => {
2806 let v = *ed.host().viewport();
2807 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2808 ed.push_buffer_cursor_to_textarea();
2809 }
2810 Motion::LastNonBlank => {
2811 crate::motions::move_last_non_blank(&mut ed.buffer);
2812 ed.push_buffer_cursor_to_textarea();
2813 }
2814 Motion::LineMiddle => {
2815 let row = ed.cursor().0;
2816 let line_chars = buf_line_chars(&ed.buffer, row);
2817 let target = line_chars / 2;
2820 ed.jump_cursor(row, target);
2821 }
2822 Motion::ParagraphPrev => {
2823 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2824 ed.push_buffer_cursor_to_textarea();
2825 }
2826 Motion::ParagraphNext => {
2827 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2828 ed.push_buffer_cursor_to_textarea();
2829 }
2830 Motion::SentencePrev => {
2831 for _ in 0..count.max(1) {
2832 if let Some((row, col)) = sentence_boundary(ed, false) {
2833 ed.jump_cursor(row, col);
2834 }
2835 }
2836 }
2837 Motion::SentenceNext => {
2838 for _ in 0..count.max(1) {
2839 if let Some((row, col)) = sentence_boundary(ed, true) {
2840 ed.jump_cursor(row, col);
2841 }
2842 }
2843 }
2844 }
2845}
2846
2847fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2848 ed.sync_buffer_content_from_textarea();
2854 crate::motions::move_first_non_blank(&mut ed.buffer);
2855 ed.push_buffer_cursor_to_textarea();
2856}
2857
2858fn find_char_on_line<H: crate::types::Host>(
2859 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2860 ch: char,
2861 forward: bool,
2862 till: bool,
2863) -> bool {
2864 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2865 if moved {
2866 ed.push_buffer_cursor_to_textarea();
2867 }
2868 moved
2869}
2870
2871fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2872 let moved = crate::motions::match_bracket(&mut ed.buffer);
2873 if moved {
2874 ed.push_buffer_cursor_to_textarea();
2875 }
2876 moved
2877}
2878
2879fn word_at_cursor_search<H: crate::types::Host>(
2880 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2881 forward: bool,
2882 whole_word: bool,
2883 count: usize,
2884) {
2885 let (row, col) = ed.cursor();
2886 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2887 let chars: Vec<char> = line.chars().collect();
2888 if chars.is_empty() {
2889 return;
2890 }
2891 let spec = ed.settings().iskeyword.clone();
2893 let is_word = |c: char| is_keyword_char(c, &spec);
2894 let mut start = col.min(chars.len().saturating_sub(1));
2895 while start > 0 && is_word(chars[start - 1]) {
2896 start -= 1;
2897 }
2898 let mut end = start;
2899 while end < chars.len() && is_word(chars[end]) {
2900 end += 1;
2901 }
2902 if end <= start {
2903 return;
2904 }
2905 let word: String = chars[start..end].iter().collect();
2906 let escaped = regex_escape(&word);
2907 let pattern = if whole_word {
2908 format!(r"\b{escaped}\b")
2909 } else {
2910 escaped
2911 };
2912 ed.push_search_pattern(&pattern);
2913 if ed.search_state().pattern.is_none() {
2914 return;
2915 }
2916 ed.vim.last_search = Some(pattern);
2918 ed.vim.last_search_forward = forward;
2919 for _ in 0..count.max(1) {
2920 if forward {
2921 ed.search_advance_forward(true);
2922 } else {
2923 ed.search_advance_backward(true);
2924 }
2925 }
2926 ed.push_buffer_cursor_to_textarea();
2927}
2928
2929fn regex_escape(s: &str) -> String {
2930 let mut out = String::with_capacity(s.len());
2931 for c in s.chars() {
2932 if matches!(
2933 c,
2934 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2935 ) {
2936 out.push('\\');
2937 }
2938 out.push(c);
2939 }
2940 out
2941}
2942
2943pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2957 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2958 op: Operator,
2959 motion_key: char,
2960 total_count: usize,
2961) {
2962 let input = Input {
2963 key: Key::Char(motion_key),
2964 ctrl: false,
2965 alt: false,
2966 shift: false,
2967 };
2968 let Some(motion) = parse_motion(&input) else {
2969 return;
2970 };
2971 let motion = match motion {
2972 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2973 Some((ch, forward, till)) => Motion::Find {
2974 ch,
2975 forward: if reverse { !forward } else { forward },
2976 till,
2977 },
2978 None => return,
2979 },
2980 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2982 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2983 m => m,
2984 };
2985 apply_op_with_motion(ed, op, &motion, total_count);
2986 if let Motion::Find { ch, forward, till } = &motion {
2987 ed.vim.last_find = Some((*ch, *forward, *till));
2988 }
2989 if !ed.vim.replaying && op_is_change(op) {
2990 ed.vim.last_change = Some(LastChange::OpMotion {
2991 op,
2992 motion,
2993 count: total_count,
2994 inserted: None,
2995 });
2996 }
2997}
2998
2999pub(crate) fn apply_op_double<H: crate::types::Host>(
3002 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3003 op: Operator,
3004 total_count: usize,
3005) {
3006 execute_line_op(ed, op, total_count);
3007 if !ed.vim.replaying {
3008 ed.vim.last_change = Some(LastChange::LineOp {
3009 op,
3010 count: total_count,
3011 inserted: None,
3012 });
3013 }
3014}
3015
3016pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3026 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3027 op: Operator,
3028 ch: char,
3029 total_count: usize,
3030) {
3031 if matches!(
3034 op,
3035 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3036 ) {
3037 let op_char = match op {
3038 Operator::Uppercase => 'U',
3039 Operator::Lowercase => 'u',
3040 Operator::ToggleCase => '~',
3041 _ => unreachable!(),
3042 };
3043 if ch == op_char {
3044 execute_line_op(ed, op, total_count);
3045 if !ed.vim.replaying {
3046 ed.vim.last_change = Some(LastChange::LineOp {
3047 op,
3048 count: total_count,
3049 inserted: None,
3050 });
3051 }
3052 return;
3053 }
3054 }
3055 let motion = match ch {
3056 'g' => Motion::FileTop,
3057 'e' => Motion::WordEndBack,
3058 'E' => Motion::BigWordEndBack,
3059 'j' => Motion::ScreenDown,
3060 'k' => Motion::ScreenUp,
3061 _ => return, };
3063 apply_op_with_motion(ed, op, &motion, total_count);
3064 if !ed.vim.replaying && op_is_change(op) {
3065 ed.vim.last_change = Some(LastChange::OpMotion {
3066 op,
3067 motion,
3068 count: total_count,
3069 inserted: None,
3070 });
3071 }
3072}
3073
3074pub(crate) fn apply_after_g<H: crate::types::Host>(
3079 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3080 ch: char,
3081 count: usize,
3082) {
3083 match ch {
3084 'g' => {
3085 let pre = ed.cursor();
3087 if count > 1 {
3088 ed.jump_cursor(count - 1, 0);
3089 } else {
3090 ed.jump_cursor(0, 0);
3091 }
3092 move_first_non_whitespace(ed);
3093 if ed.cursor() != pre {
3094 ed.push_jump(pre);
3095 }
3096 }
3097 'e' => execute_motion(ed, Motion::WordEndBack, count),
3098 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3099 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3101 'M' => execute_motion(ed, Motion::LineMiddle, count),
3103 'v' => ed.reenter_last_visual(),
3106 'j' => execute_motion(ed, Motion::ScreenDown, count),
3110 'k' => execute_motion(ed, Motion::ScreenUp, count),
3111 'U' => {
3115 ed.vim.pending = Pending::Op {
3116 op: Operator::Uppercase,
3117 count1: count,
3118 };
3119 }
3120 'u' => {
3121 ed.vim.pending = Pending::Op {
3122 op: Operator::Lowercase,
3123 count1: count,
3124 };
3125 }
3126 '~' => {
3127 ed.vim.pending = Pending::Op {
3128 op: Operator::ToggleCase,
3129 count1: count,
3130 };
3131 }
3132 'q' => {
3133 ed.vim.pending = Pending::Op {
3136 op: Operator::Reflow,
3137 count1: count,
3138 };
3139 }
3140 'J' => {
3141 for _ in 0..count.max(1) {
3143 ed.push_undo();
3144 join_line_raw(ed);
3145 }
3146 if !ed.vim.replaying {
3147 ed.vim.last_change = Some(LastChange::JoinLine {
3148 count: count.max(1),
3149 });
3150 }
3151 }
3152 'd' => {
3153 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3158 }
3159 'i' => {
3164 if let Some((row, col)) = ed.vim.last_insert_pos {
3165 ed.jump_cursor(row, col);
3166 }
3167 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3168 }
3169 ';' => walk_change_list(ed, -1, count.max(1)),
3172 ',' => walk_change_list(ed, 1, count.max(1)),
3173 '*' => execute_motion(
3177 ed,
3178 Motion::WordAtCursor {
3179 forward: true,
3180 whole_word: false,
3181 },
3182 count,
3183 ),
3184 '#' => execute_motion(
3185 ed,
3186 Motion::WordAtCursor {
3187 forward: false,
3188 whole_word: false,
3189 },
3190 count,
3191 ),
3192 _ => {}
3193 }
3194}
3195
3196pub(crate) fn apply_after_z<H: crate::types::Host>(
3201 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3202 ch: char,
3203 count: usize,
3204) {
3205 use crate::editor::CursorScrollTarget;
3206 let row = ed.cursor().0;
3207 match ch {
3208 'z' => {
3209 ed.scroll_cursor_to(CursorScrollTarget::Center);
3210 ed.vim.viewport_pinned = true;
3211 }
3212 't' => {
3213 ed.scroll_cursor_to(CursorScrollTarget::Top);
3214 ed.vim.viewport_pinned = true;
3215 }
3216 'b' => {
3217 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3218 ed.vim.viewport_pinned = true;
3219 }
3220 'o' => {
3225 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3226 }
3227 'c' => {
3228 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3229 }
3230 'a' => {
3231 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3232 }
3233 'R' => {
3234 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3235 }
3236 'M' => {
3237 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3238 }
3239 'E' => {
3240 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3241 }
3242 'd' => {
3243 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3244 }
3245 'f' => {
3246 if matches!(
3247 ed.vim.mode,
3248 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3249 ) {
3250 let anchor_row = match ed.vim.mode {
3253 Mode::VisualLine => ed.vim.visual_line_anchor,
3254 Mode::VisualBlock => ed.vim.block_anchor.0,
3255 _ => ed.vim.visual_anchor.0,
3256 };
3257 let cur = ed.cursor().0;
3258 let top = anchor_row.min(cur);
3259 let bot = anchor_row.max(cur);
3260 ed.apply_fold_op(crate::types::FoldOp::Add {
3261 start_row: top,
3262 end_row: bot,
3263 closed: true,
3264 });
3265 ed.vim.mode = Mode::Normal;
3266 } else {
3267 ed.vim.pending = Pending::Op {
3272 op: Operator::Fold,
3273 count1: count,
3274 };
3275 }
3276 }
3277 _ => {}
3278 }
3279}
3280
3281pub(crate) fn apply_find_char<H: crate::types::Host>(
3287 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3288 ch: char,
3289 forward: bool,
3290 till: bool,
3291 count: usize,
3292) {
3293 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3294 ed.vim.last_find = Some((ch, forward, till));
3295}
3296
3297pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3303 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3304 op: Operator,
3305 ch: char,
3306 forward: bool,
3307 till: bool,
3308 total_count: usize,
3309) {
3310 let motion = Motion::Find { ch, forward, till };
3311 apply_op_with_motion(ed, op, &motion, total_count);
3312 ed.vim.last_find = Some((ch, forward, till));
3313 if !ed.vim.replaying && op_is_change(op) {
3314 ed.vim.last_change = Some(LastChange::OpMotion {
3315 op,
3316 motion,
3317 count: total_count,
3318 inserted: None,
3319 });
3320 }
3321}
3322
3323pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3332 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3333 op: Operator,
3334 ch: char,
3335 inner: bool,
3336 _total_count: usize,
3337) -> bool {
3338 let obj = match ch {
3341 'w' => TextObject::Word { big: false },
3342 'W' => TextObject::Word { big: true },
3343 '"' | '\'' | '`' => TextObject::Quote(ch),
3344 '(' | ')' | 'b' => TextObject::Bracket('('),
3345 '[' | ']' => TextObject::Bracket('['),
3346 '{' | '}' | 'B' => TextObject::Bracket('{'),
3347 '<' | '>' => TextObject::Bracket('<'),
3348 'p' => TextObject::Paragraph,
3349 't' => TextObject::XmlTag,
3350 's' => TextObject::Sentence,
3351 _ => return false,
3352 };
3353 apply_op_with_text_object(ed, op, obj, inner);
3354 if !ed.vim.replaying && op_is_change(op) {
3355 ed.vim.last_change = Some(LastChange::OpTextObj {
3356 op,
3357 obj,
3358 inner,
3359 inserted: None,
3360 });
3361 }
3362 true
3363}
3364
3365pub(crate) fn retreat_one<H: crate::types::Host>(
3367 ed: &Editor<hjkl_buffer::Buffer, H>,
3368 pos: (usize, usize),
3369) -> (usize, usize) {
3370 let (r, c) = pos;
3371 if c > 0 {
3372 (r, c - 1)
3373 } else if r > 0 {
3374 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3375 (r - 1, prev_len)
3376 } else {
3377 (0, 0)
3378 }
3379}
3380
3381fn begin_insert_noundo<H: crate::types::Host>(
3383 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3384 count: usize,
3385 reason: InsertReason,
3386) {
3387 let reason = if ed.vim.replaying {
3388 InsertReason::ReplayOnly
3389 } else {
3390 reason
3391 };
3392 let (row, _) = ed.cursor();
3393 ed.vim.insert_session = Some(InsertSession {
3394 count,
3395 row_min: row,
3396 row_max: row,
3397 before_lines: buf_lines_to_vec(&ed.buffer),
3398 reason,
3399 });
3400 ed.vim.mode = Mode::Insert;
3401 ed.vim.current_mode = crate::VimMode::Insert;
3403}
3404
3405pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
3408 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3409 op: Operator,
3410 motion: &Motion,
3411 count: usize,
3412) {
3413 let start = ed.cursor();
3414 apply_motion_cursor_ctx(ed, motion, count, true);
3419 let end = ed.cursor();
3420 let kind = motion_kind(motion);
3421 ed.jump_cursor(start.0, start.1);
3423 run_operator_over_range(ed, op, start, end, kind);
3424}
3425
3426fn apply_op_with_text_object<H: crate::types::Host>(
3427 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3428 op: Operator,
3429 obj: TextObject,
3430 inner: bool,
3431) {
3432 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3433 return;
3434 };
3435 ed.jump_cursor(start.0, start.1);
3436 run_operator_over_range(ed, op, start, end, kind);
3437}
3438
3439fn motion_kind(motion: &Motion) -> RangeKind {
3440 match motion {
3441 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
3442 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
3443 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3444 RangeKind::Linewise
3445 }
3446 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3447 RangeKind::Inclusive
3448 }
3449 Motion::Find { .. } => RangeKind::Inclusive,
3450 Motion::MatchBracket => RangeKind::Inclusive,
3451 Motion::LineEnd => RangeKind::Inclusive,
3453 _ => RangeKind::Exclusive,
3454 }
3455}
3456
3457fn run_operator_over_range<H: crate::types::Host>(
3458 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3459 op: Operator,
3460 start: (usize, usize),
3461 end: (usize, usize),
3462 kind: RangeKind,
3463) {
3464 let (top, bot) = order(start, end);
3465 if top == bot && !matches!(kind, RangeKind::Linewise) {
3469 return;
3470 }
3471
3472 match op {
3473 Operator::Yank => {
3474 let text = read_vim_range(ed, top, bot, kind);
3475 if !text.is_empty() {
3476 ed.record_yank_to_host(text.clone());
3477 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
3478 }
3479 let rbr = match kind {
3483 RangeKind::Linewise => {
3484 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3485 (bot.0, last_col)
3486 }
3487 RangeKind::Inclusive => (bot.0, bot.1),
3488 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3489 };
3490 ed.set_mark('[', top);
3491 ed.set_mark(']', rbr);
3492 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3493 ed.push_buffer_cursor_to_textarea();
3494 }
3495 Operator::Delete => {
3496 ed.push_undo();
3497 cut_vim_range(ed, top, bot, kind);
3498 if !matches!(kind, RangeKind::Linewise) {
3503 clamp_cursor_to_normal_mode(ed);
3504 }
3505 ed.vim.mode = Mode::Normal;
3506 let pos = ed.cursor();
3510 ed.set_mark('[', pos);
3511 ed.set_mark(']', pos);
3512 }
3513 Operator::Change => {
3514 ed.vim.change_mark_start = Some(top);
3519 ed.push_undo();
3520 cut_vim_range(ed, top, bot, kind);
3521 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3522 }
3523 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3524 apply_case_op_to_selection(ed, op, top, bot, kind);
3525 }
3526 Operator::Indent | Operator::Outdent => {
3527 ed.push_undo();
3530 if op == Operator::Indent {
3531 indent_rows(ed, top.0, bot.0, 1);
3532 } else {
3533 outdent_rows(ed, top.0, bot.0, 1);
3534 }
3535 ed.vim.mode = Mode::Normal;
3536 }
3537 Operator::Fold => {
3538 if bot.0 >= top.0 {
3542 ed.apply_fold_op(crate::types::FoldOp::Add {
3543 start_row: top.0,
3544 end_row: bot.0,
3545 closed: true,
3546 });
3547 }
3548 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3549 ed.push_buffer_cursor_to_textarea();
3550 ed.vim.mode = Mode::Normal;
3551 }
3552 Operator::Reflow => {
3553 ed.push_undo();
3554 reflow_rows(ed, top.0, bot.0);
3555 ed.vim.mode = Mode::Normal;
3556 }
3557 Operator::AutoIndent => {
3558 ed.push_undo();
3560 auto_indent_rows(ed, top.0, bot.0);
3561 ed.vim.mode = Mode::Normal;
3562 }
3563 }
3564}
3565
3566pub(crate) fn delete_range_bridge<H: crate::types::Host>(
3583 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3584 start: (usize, usize),
3585 end: (usize, usize),
3586 kind: RangeKind,
3587 register: char,
3588) {
3589 ed.vim.pending_register = Some(register);
3590 run_operator_over_range(ed, Operator::Delete, start, end, kind);
3591}
3592
3593pub(crate) fn yank_range_bridge<H: crate::types::Host>(
3596 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3597 start: (usize, usize),
3598 end: (usize, usize),
3599 kind: RangeKind,
3600 register: char,
3601) {
3602 ed.vim.pending_register = Some(register);
3603 run_operator_over_range(ed, Operator::Yank, start, end, kind);
3604}
3605
3606pub(crate) fn change_range_bridge<H: crate::types::Host>(
3611 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3612 start: (usize, usize),
3613 end: (usize, usize),
3614 kind: RangeKind,
3615 register: char,
3616) {
3617 ed.vim.pending_register = Some(register);
3618 run_operator_over_range(ed, Operator::Change, start, end, kind);
3619}
3620
3621pub(crate) fn indent_range_bridge<H: crate::types::Host>(
3626 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3627 start: (usize, usize),
3628 end: (usize, usize),
3629 count: i32,
3630 shiftwidth: u32,
3631) {
3632 if count == 0 {
3633 return;
3634 }
3635 let (top_row, bot_row) = if start.0 <= end.0 {
3636 (start.0, end.0)
3637 } else {
3638 (end.0, start.0)
3639 };
3640 let original_sw = ed.settings().shiftwidth;
3642 if shiftwidth > 0 {
3643 ed.settings_mut().shiftwidth = shiftwidth as usize;
3644 }
3645 ed.push_undo();
3646 let abs_count = count.unsigned_abs() as usize;
3647 if count > 0 {
3648 indent_rows(ed, top_row, bot_row, abs_count);
3649 } else {
3650 outdent_rows(ed, top_row, bot_row, abs_count);
3651 }
3652 if shiftwidth > 0 {
3653 ed.settings_mut().shiftwidth = original_sw;
3654 }
3655 ed.vim.mode = Mode::Normal;
3656}
3657
3658pub(crate) fn case_range_bridge<H: crate::types::Host>(
3662 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3663 start: (usize, usize),
3664 end: (usize, usize),
3665 kind: RangeKind,
3666 op: Operator,
3667) {
3668 match op {
3669 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
3670 _ => return,
3671 }
3672 let (top, bot) = order(start, end);
3673 apply_case_op_to_selection(ed, op, top, bot, kind);
3674}
3675
3676pub(crate) fn delete_block_bridge<H: crate::types::Host>(
3697 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3698 top_row: usize,
3699 bot_row: usize,
3700 left_col: usize,
3701 right_col: usize,
3702 register: char,
3703) {
3704 ed.vim.pending_register = Some(register);
3705 let saved_anchor = ed.vim.block_anchor;
3706 let saved_vcol = ed.vim.block_vcol;
3707 ed.vim.block_anchor = (top_row, left_col);
3708 ed.vim.block_vcol = right_col;
3709 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3711 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3713 apply_block_operator(ed, Operator::Delete);
3714 ed.vim.block_anchor = saved_anchor;
3718 ed.vim.block_vcol = saved_vcol;
3719}
3720
3721pub(crate) fn yank_block_bridge<H: crate::types::Host>(
3723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3724 top_row: usize,
3725 bot_row: usize,
3726 left_col: usize,
3727 right_col: usize,
3728 register: char,
3729) {
3730 ed.vim.pending_register = Some(register);
3731 let saved_anchor = ed.vim.block_anchor;
3732 let saved_vcol = ed.vim.block_vcol;
3733 ed.vim.block_anchor = (top_row, left_col);
3734 ed.vim.block_vcol = right_col;
3735 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3736 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3737 apply_block_operator(ed, Operator::Yank);
3738 ed.vim.block_anchor = saved_anchor;
3739 ed.vim.block_vcol = saved_vcol;
3740}
3741
3742pub(crate) fn change_block_bridge<H: crate::types::Host>(
3745 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3746 top_row: usize,
3747 bot_row: usize,
3748 left_col: usize,
3749 right_col: usize,
3750 register: char,
3751) {
3752 ed.vim.pending_register = Some(register);
3753 let saved_anchor = ed.vim.block_anchor;
3754 let saved_vcol = ed.vim.block_vcol;
3755 ed.vim.block_anchor = (top_row, left_col);
3756 ed.vim.block_vcol = right_col;
3757 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3758 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3759 apply_block_operator(ed, Operator::Change);
3760 ed.vim.block_anchor = saved_anchor;
3761 ed.vim.block_vcol = saved_vcol;
3762}
3763
3764pub(crate) fn indent_block_bridge<H: crate::types::Host>(
3768 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3769 top_row: usize,
3770 bot_row: usize,
3771 count: i32,
3772) {
3773 if count == 0 {
3774 return;
3775 }
3776 ed.push_undo();
3777 let abs = count.unsigned_abs() as usize;
3778 if count > 0 {
3779 indent_rows(ed, top_row, bot_row, abs);
3780 } else {
3781 outdent_rows(ed, top_row, bot_row, abs);
3782 }
3783 ed.vim.mode = Mode::Normal;
3784}
3785
3786pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
3790 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3791 start: (usize, usize),
3792 end: (usize, usize),
3793) {
3794 let (top_row, bot_row) = if start.0 <= end.0 {
3795 (start.0, end.0)
3796 } else {
3797 (end.0, start.0)
3798 };
3799 ed.push_undo();
3800 auto_indent_rows(ed, top_row, bot_row);
3801 ed.vim.mode = Mode::Normal;
3802}
3803
3804pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
3815 ed: &Editor<hjkl_buffer::Buffer, H>,
3816) -> Option<((usize, usize), (usize, usize))> {
3817 word_text_object(ed, true, false)
3818}
3819
3820pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
3823 ed: &Editor<hjkl_buffer::Buffer, H>,
3824) -> Option<((usize, usize), (usize, usize))> {
3825 word_text_object(ed, false, false)
3826}
3827
3828pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
3831 ed: &Editor<hjkl_buffer::Buffer, H>,
3832) -> Option<((usize, usize), (usize, usize))> {
3833 word_text_object(ed, true, true)
3834}
3835
3836pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
3839 ed: &Editor<hjkl_buffer::Buffer, H>,
3840) -> Option<((usize, usize), (usize, usize))> {
3841 word_text_object(ed, false, true)
3842}
3843
3844pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
3860 ed: &Editor<hjkl_buffer::Buffer, H>,
3861 quote: char,
3862) -> Option<((usize, usize), (usize, usize))> {
3863 quote_text_object(ed, quote, true)
3864}
3865
3866pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
3869 ed: &Editor<hjkl_buffer::Buffer, H>,
3870 quote: char,
3871) -> Option<((usize, usize), (usize, usize))> {
3872 quote_text_object(ed, quote, false)
3873}
3874
3875pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
3883 ed: &Editor<hjkl_buffer::Buffer, H>,
3884 open: char,
3885) -> Option<((usize, usize), (usize, usize))> {
3886 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
3887}
3888
3889pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
3893 ed: &Editor<hjkl_buffer::Buffer, H>,
3894 open: char,
3895) -> Option<((usize, usize), (usize, usize))> {
3896 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
3897}
3898
3899pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
3904 ed: &Editor<hjkl_buffer::Buffer, H>,
3905) -> Option<((usize, usize), (usize, usize))> {
3906 sentence_text_object(ed, true)
3907}
3908
3909pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
3912 ed: &Editor<hjkl_buffer::Buffer, H>,
3913) -> Option<((usize, usize), (usize, usize))> {
3914 sentence_text_object(ed, false)
3915}
3916
3917pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
3922 ed: &Editor<hjkl_buffer::Buffer, H>,
3923) -> Option<((usize, usize), (usize, usize))> {
3924 paragraph_text_object(ed, true)
3925}
3926
3927pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
3930 ed: &Editor<hjkl_buffer::Buffer, H>,
3931) -> Option<((usize, usize), (usize, usize))> {
3932 paragraph_text_object(ed, false)
3933}
3934
3935pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
3941 ed: &Editor<hjkl_buffer::Buffer, H>,
3942) -> Option<((usize, usize), (usize, usize))> {
3943 tag_text_object(ed, true)
3944}
3945
3946pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
3949 ed: &Editor<hjkl_buffer::Buffer, H>,
3950) -> Option<((usize, usize), (usize, usize))> {
3951 tag_text_object(ed, false)
3952}
3953
3954fn reflow_rows<H: crate::types::Host>(
3959 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3960 top: usize,
3961 bot: usize,
3962) {
3963 let width = ed.settings().textwidth.max(1);
3964 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3965 let bot = bot.min(lines.len().saturating_sub(1));
3966 if top > bot {
3967 return;
3968 }
3969 let original = lines[top..=bot].to_vec();
3970 let mut wrapped: Vec<String> = Vec::new();
3971 let mut paragraph: Vec<String> = Vec::new();
3972 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3973 if para.is_empty() {
3974 return;
3975 }
3976 let words = para.join(" ");
3977 let mut current = String::new();
3978 for word in words.split_whitespace() {
3979 let extra = if current.is_empty() {
3980 word.chars().count()
3981 } else {
3982 current.chars().count() + 1 + word.chars().count()
3983 };
3984 if extra > width && !current.is_empty() {
3985 out.push(std::mem::take(&mut current));
3986 current.push_str(word);
3987 } else if current.is_empty() {
3988 current.push_str(word);
3989 } else {
3990 current.push(' ');
3991 current.push_str(word);
3992 }
3993 }
3994 if !current.is_empty() {
3995 out.push(current);
3996 }
3997 para.clear();
3998 };
3999 for line in &original {
4000 if line.trim().is_empty() {
4001 flush(&mut paragraph, &mut wrapped, width);
4002 wrapped.push(String::new());
4003 } else {
4004 paragraph.push(line.clone());
4005 }
4006 }
4007 flush(&mut paragraph, &mut wrapped, width);
4008
4009 let after: Vec<String> = lines.split_off(bot + 1);
4011 lines.truncate(top);
4012 lines.extend(wrapped);
4013 lines.extend(after);
4014 ed.restore(lines, (top, 0));
4015 ed.mark_content_dirty();
4016}
4017
4018fn apply_case_op_to_selection<H: crate::types::Host>(
4024 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4025 op: Operator,
4026 top: (usize, usize),
4027 bot: (usize, usize),
4028 kind: RangeKind,
4029) {
4030 use hjkl_buffer::Edit;
4031 ed.push_undo();
4032 let saved_yank = ed.yank().to_string();
4033 let saved_yank_linewise = ed.vim.yank_linewise;
4034 let selection = cut_vim_range(ed, top, bot, kind);
4035 let transformed = match op {
4036 Operator::Uppercase => selection.to_uppercase(),
4037 Operator::Lowercase => selection.to_lowercase(),
4038 Operator::ToggleCase => toggle_case_str(&selection),
4039 _ => unreachable!(),
4040 };
4041 if !transformed.is_empty() {
4042 let cursor = buf_cursor_pos(&ed.buffer);
4043 ed.mutate_edit(Edit::InsertStr {
4044 at: cursor,
4045 text: transformed,
4046 });
4047 }
4048 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4049 ed.push_buffer_cursor_to_textarea();
4050 ed.set_yank(saved_yank);
4051 ed.vim.yank_linewise = saved_yank_linewise;
4052 ed.vim.mode = Mode::Normal;
4053}
4054
4055fn indent_rows<H: crate::types::Host>(
4060 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4061 top: usize,
4062 bot: usize,
4063 count: usize,
4064) {
4065 ed.sync_buffer_content_from_textarea();
4066 let width = ed.settings().shiftwidth * count.max(1);
4067 let pad: String = " ".repeat(width);
4068 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4069 let bot = bot.min(lines.len().saturating_sub(1));
4070 for line in lines.iter_mut().take(bot + 1).skip(top) {
4071 if !line.is_empty() {
4072 line.insert_str(0, &pad);
4073 }
4074 }
4075 ed.restore(lines, (top, 0));
4078 move_first_non_whitespace(ed);
4079}
4080
4081fn outdent_rows<H: crate::types::Host>(
4085 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4086 top: usize,
4087 bot: usize,
4088 count: usize,
4089) {
4090 ed.sync_buffer_content_from_textarea();
4091 let width = ed.settings().shiftwidth * count.max(1);
4092 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4093 let bot = bot.min(lines.len().saturating_sub(1));
4094 for line in lines.iter_mut().take(bot + 1).skip(top) {
4095 let strip: usize = line
4096 .chars()
4097 .take(width)
4098 .take_while(|c| *c == ' ' || *c == '\t')
4099 .count();
4100 if strip > 0 {
4101 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4102 line.drain(..byte_len);
4103 }
4104 }
4105 ed.restore(lines, (top, 0));
4106 move_first_non_whitespace(ed);
4107}
4108
4109fn bracket_net(line: &str) -> i32 {
4136 let mut net: i32 = 0;
4137 let mut chars = line.chars().peekable();
4138 while let Some(ch) = chars.next() {
4139 match ch {
4140 '/' if chars.peek() == Some(&'/') => return net,
4142 '"' => {
4143 while let Some(c) = chars.next() {
4145 match c {
4146 '\\' => {
4147 chars.next();
4148 } '"' => break,
4150 _ => {}
4151 }
4152 }
4153 }
4154 '\'' => {
4155 let saved: Vec<char> = chars.clone().take(5).collect();
4164 let close_idx = if saved.first() == Some(&'\\') {
4165 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
4166 } else {
4167 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
4168 };
4169 if let Some(idx) = close_idx {
4170 for _ in 0..=idx {
4171 chars.next();
4172 }
4173 }
4174 }
4176 '{' | '(' | '[' => net += 1,
4177 '}' | ')' | ']' => net -= 1,
4178 _ => {}
4179 }
4180 }
4181 net
4182}
4183
4184fn auto_indent_rows<H: crate::types::Host>(
4206 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4207 top: usize,
4208 bot: usize,
4209) {
4210 ed.sync_buffer_content_from_textarea();
4211 let shiftwidth = ed.settings().shiftwidth;
4212 let expandtab = ed.settings().expandtab;
4213 let indent_unit: String = if expandtab {
4214 " ".repeat(shiftwidth)
4215 } else {
4216 "\t".to_string()
4217 };
4218
4219 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4220 let bot = bot.min(lines.len().saturating_sub(1));
4221
4222 let mut depth: i32 = 0;
4225 for line in lines.iter().take(top) {
4226 depth += bracket_net(line);
4227 if depth < 0 {
4228 depth = 0;
4229 }
4230 }
4231
4232 for line in lines.iter_mut().take(bot + 1).skip(top) {
4233 let trimmed_owned = line.trim_start().to_owned();
4234 if trimmed_owned.is_empty() {
4236 *line = String::new();
4237 continue;
4239 }
4240
4241 let starts_with_close = trimmed_owned
4243 .chars()
4244 .next()
4245 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
4246 let starts_with_dot = trimmed_owned.starts_with('.')
4256 && !trimmed_owned.starts_with("..")
4257 && !trimmed_owned.starts_with(".;");
4258 let effective_depth = if starts_with_close {
4259 depth.saturating_sub(1)
4260 } else if starts_with_dot {
4261 depth.saturating_add(1)
4262 } else {
4263 depth
4264 } as usize;
4265
4266 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
4268
4269 depth += bracket_net(&trimmed_owned);
4271 if depth < 0 {
4272 depth = 0;
4273 }
4274
4275 *line = new_line;
4276 }
4277
4278 ed.restore(lines, (top, 0));
4280 move_first_non_whitespace(ed);
4281 ed.last_indent_range = Some((top, bot));
4283}
4284
4285fn toggle_case_str(s: &str) -> String {
4286 s.chars()
4287 .map(|c| {
4288 if c.is_lowercase() {
4289 c.to_uppercase().next().unwrap_or(c)
4290 } else if c.is_uppercase() {
4291 c.to_lowercase().next().unwrap_or(c)
4292 } else {
4293 c
4294 }
4295 })
4296 .collect()
4297}
4298
4299fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4300 if a <= b { (a, b) } else { (b, a) }
4301}
4302
4303fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4308 let (row, col) = ed.cursor();
4309 let line_chars = buf_line_chars(&ed.buffer, row);
4310 let max_col = line_chars.saturating_sub(1);
4311 if col > max_col {
4312 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4313 ed.push_buffer_cursor_to_textarea();
4314 }
4315}
4316
4317fn execute_line_op<H: crate::types::Host>(
4320 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4321 op: Operator,
4322 count: usize,
4323) {
4324 let (row, col) = ed.cursor();
4325 let total = buf_row_count(&ed.buffer);
4326 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4327
4328 match op {
4329 Operator::Yank => {
4330 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4332 if !text.is_empty() {
4333 ed.record_yank_to_host(text.clone());
4334 ed.record_yank(text, true);
4335 }
4336 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4339 ed.set_mark('[', (row, 0));
4340 ed.set_mark(']', (end_row, last_col));
4341 buf_set_cursor_rc(&mut ed.buffer, row, col);
4342 ed.push_buffer_cursor_to_textarea();
4343 ed.vim.mode = Mode::Normal;
4344 }
4345 Operator::Delete => {
4346 ed.push_undo();
4347 let deleted_through_last = end_row + 1 >= total;
4348 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4349 let total_after = buf_row_count(&ed.buffer);
4353 let raw_target = if deleted_through_last {
4354 row.saturating_sub(1).min(total_after.saturating_sub(1))
4355 } else {
4356 row.min(total_after.saturating_sub(1))
4357 };
4358 let target_row = if raw_target > 0
4364 && raw_target + 1 == total_after
4365 && buf_line(&ed.buffer, raw_target)
4366 .map(str::is_empty)
4367 .unwrap_or(false)
4368 {
4369 raw_target - 1
4370 } else {
4371 raw_target
4372 };
4373 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4374 ed.push_buffer_cursor_to_textarea();
4375 move_first_non_whitespace(ed);
4376 ed.sticky_col = Some(ed.cursor().1);
4377 ed.vim.mode = Mode::Normal;
4378 let pos = ed.cursor();
4381 ed.set_mark('[', pos);
4382 ed.set_mark(']', pos);
4383 }
4384 Operator::Change => {
4385 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4389 ed.vim.change_mark_start = Some((row, 0));
4391 ed.push_undo();
4392 ed.sync_buffer_content_from_textarea();
4393 let payload = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4395 if end_row > row {
4396 ed.mutate_edit(Edit::DeleteRange {
4397 start: Position::new(row + 1, 0),
4398 end: Position::new(end_row, 0),
4399 kind: BufKind::Line,
4400 });
4401 }
4402 let line_chars = buf_line_chars(&ed.buffer, row);
4403 if line_chars > 0 {
4404 ed.mutate_edit(Edit::DeleteRange {
4405 start: Position::new(row, 0),
4406 end: Position::new(row, line_chars),
4407 kind: BufKind::Char,
4408 });
4409 }
4410 if !payload.is_empty() {
4411 ed.record_yank_to_host(payload.clone());
4412 ed.record_delete(payload, true);
4413 }
4414 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4415 ed.push_buffer_cursor_to_textarea();
4416 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4417 }
4418 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4419 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
4423 move_first_non_whitespace(ed);
4426 }
4427 Operator::Indent | Operator::Outdent => {
4428 ed.push_undo();
4430 if op == Operator::Indent {
4431 indent_rows(ed, row, end_row, 1);
4432 } else {
4433 outdent_rows(ed, row, end_row, 1);
4434 }
4435 ed.sticky_col = Some(ed.cursor().1);
4436 ed.vim.mode = Mode::Normal;
4437 }
4438 Operator::Fold => unreachable!("Fold has no line-op double"),
4440 Operator::Reflow => {
4441 ed.push_undo();
4443 reflow_rows(ed, row, end_row);
4444 move_first_non_whitespace(ed);
4445 ed.sticky_col = Some(ed.cursor().1);
4446 ed.vim.mode = Mode::Normal;
4447 }
4448 Operator::AutoIndent => {
4449 ed.push_undo();
4451 auto_indent_rows(ed, row, end_row);
4452 ed.sticky_col = Some(ed.cursor().1);
4453 ed.vim.mode = Mode::Normal;
4454 }
4455 }
4456}
4457
4458pub(crate) fn apply_visual_operator<H: crate::types::Host>(
4461 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4462 op: Operator,
4463) {
4464 match ed.vim.mode {
4465 Mode::VisualLine => {
4466 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4467 let top = cursor_row.min(ed.vim.visual_line_anchor);
4468 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4469 ed.vim.yank_linewise = true;
4470 match op {
4471 Operator::Yank => {
4472 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4473 if !text.is_empty() {
4474 ed.record_yank_to_host(text.clone());
4475 ed.record_yank(text, true);
4476 }
4477 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4478 ed.push_buffer_cursor_to_textarea();
4479 ed.vim.mode = Mode::Normal;
4480 }
4481 Operator::Delete => {
4482 ed.push_undo();
4483 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4484 ed.vim.mode = Mode::Normal;
4485 }
4486 Operator::Change => {
4487 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4490 ed.push_undo();
4491 ed.sync_buffer_content_from_textarea();
4492 let payload = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4493 if bot > top {
4494 ed.mutate_edit(Edit::DeleteRange {
4495 start: Position::new(top + 1, 0),
4496 end: Position::new(bot, 0),
4497 kind: BufKind::Line,
4498 });
4499 }
4500 let line_chars = buf_line_chars(&ed.buffer, top);
4501 if line_chars > 0 {
4502 ed.mutate_edit(Edit::DeleteRange {
4503 start: Position::new(top, 0),
4504 end: Position::new(top, line_chars),
4505 kind: BufKind::Char,
4506 });
4507 }
4508 if !payload.is_empty() {
4509 ed.record_yank_to_host(payload.clone());
4510 ed.record_delete(payload, true);
4511 }
4512 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4513 ed.push_buffer_cursor_to_textarea();
4514 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4515 }
4516 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4517 let bot = buf_cursor_pos(&ed.buffer)
4518 .row
4519 .max(ed.vim.visual_line_anchor);
4520 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
4521 move_first_non_whitespace(ed);
4522 }
4523 Operator::Indent | Operator::Outdent => {
4524 ed.push_undo();
4525 let (cursor_row, _) = ed.cursor();
4526 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4527 if op == Operator::Indent {
4528 indent_rows(ed, top, bot, 1);
4529 } else {
4530 outdent_rows(ed, top, bot, 1);
4531 }
4532 ed.vim.mode = Mode::Normal;
4533 }
4534 Operator::Reflow => {
4535 ed.push_undo();
4536 let (cursor_row, _) = ed.cursor();
4537 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4538 reflow_rows(ed, top, bot);
4539 ed.vim.mode = Mode::Normal;
4540 }
4541 Operator::AutoIndent => {
4542 ed.push_undo();
4543 let (cursor_row, _) = ed.cursor();
4544 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4545 auto_indent_rows(ed, top, bot);
4546 ed.vim.mode = Mode::Normal;
4547 }
4548 Operator::Fold => unreachable!("Visual zf takes its own path"),
4551 }
4552 }
4553 Mode::Visual => {
4554 ed.vim.yank_linewise = false;
4555 let anchor = ed.vim.visual_anchor;
4556 let cursor = ed.cursor();
4557 let (top, bot) = order(anchor, cursor);
4558 match op {
4559 Operator::Yank => {
4560 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
4561 if !text.is_empty() {
4562 ed.record_yank_to_host(text.clone());
4563 ed.record_yank(text, false);
4564 }
4565 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4566 ed.push_buffer_cursor_to_textarea();
4567 ed.vim.mode = Mode::Normal;
4568 }
4569 Operator::Delete => {
4570 ed.push_undo();
4571 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4572 ed.vim.mode = Mode::Normal;
4573 }
4574 Operator::Change => {
4575 ed.push_undo();
4576 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4577 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4578 }
4579 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4580 let anchor = ed.vim.visual_anchor;
4582 let cursor = ed.cursor();
4583 let (top, bot) = order(anchor, cursor);
4584 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
4585 }
4586 Operator::Indent | Operator::Outdent => {
4587 ed.push_undo();
4588 let anchor = ed.vim.visual_anchor;
4589 let cursor = ed.cursor();
4590 let (top, bot) = order(anchor, cursor);
4591 if op == Operator::Indent {
4592 indent_rows(ed, top.0, bot.0, 1);
4593 } else {
4594 outdent_rows(ed, top.0, bot.0, 1);
4595 }
4596 ed.vim.mode = Mode::Normal;
4597 }
4598 Operator::Reflow => {
4599 ed.push_undo();
4600 let anchor = ed.vim.visual_anchor;
4601 let cursor = ed.cursor();
4602 let (top, bot) = order(anchor, cursor);
4603 reflow_rows(ed, top.0, bot.0);
4604 ed.vim.mode = Mode::Normal;
4605 }
4606 Operator::AutoIndent => {
4607 ed.push_undo();
4608 let anchor = ed.vim.visual_anchor;
4609 let cursor = ed.cursor();
4610 let (top, bot) = order(anchor, cursor);
4611 auto_indent_rows(ed, top.0, bot.0);
4612 ed.vim.mode = Mode::Normal;
4613 }
4614 Operator::Fold => unreachable!("Visual zf takes its own path"),
4615 }
4616 }
4617 Mode::VisualBlock => apply_block_operator(ed, op),
4618 _ => {}
4619 }
4620}
4621
4622fn block_bounds<H: crate::types::Host>(
4627 ed: &Editor<hjkl_buffer::Buffer, H>,
4628) -> (usize, usize, usize, usize) {
4629 let (ar, ac) = ed.vim.block_anchor;
4630 let (cr, _) = ed.cursor();
4631 let cc = ed.vim.block_vcol;
4632 let top = ar.min(cr);
4633 let bot = ar.max(cr);
4634 let left = ac.min(cc);
4635 let right = ac.max(cc);
4636 (top, bot, left, right)
4637}
4638
4639pub(crate) fn update_block_vcol<H: crate::types::Host>(
4644 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4645 motion: &Motion,
4646) {
4647 match motion {
4648 Motion::Left
4649 | Motion::Right
4650 | Motion::WordFwd
4651 | Motion::BigWordFwd
4652 | Motion::WordBack
4653 | Motion::BigWordBack
4654 | Motion::WordEnd
4655 | Motion::BigWordEnd
4656 | Motion::WordEndBack
4657 | Motion::BigWordEndBack
4658 | Motion::LineStart
4659 | Motion::FirstNonBlank
4660 | Motion::LineEnd
4661 | Motion::Find { .. }
4662 | Motion::FindRepeat { .. }
4663 | Motion::MatchBracket => {
4664 ed.vim.block_vcol = ed.cursor().1;
4665 }
4666 _ => {}
4668 }
4669}
4670
4671fn apply_block_operator<H: crate::types::Host>(
4676 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4677 op: Operator,
4678) {
4679 let (top, bot, left, right) = block_bounds(ed);
4680 let yank = block_yank(ed, top, bot, left, right);
4682
4683 match op {
4684 Operator::Yank => {
4685 if !yank.is_empty() {
4686 ed.record_yank_to_host(yank.clone());
4687 ed.record_yank(yank, false);
4688 }
4689 ed.vim.mode = Mode::Normal;
4690 ed.jump_cursor(top, left);
4691 }
4692 Operator::Delete => {
4693 ed.push_undo();
4694 delete_block_contents(ed, top, bot, left, right);
4695 if !yank.is_empty() {
4696 ed.record_yank_to_host(yank.clone());
4697 ed.record_delete(yank, false);
4698 }
4699 ed.vim.mode = Mode::Normal;
4700 ed.jump_cursor(top, left);
4701 }
4702 Operator::Change => {
4703 ed.push_undo();
4704 delete_block_contents(ed, top, bot, left, right);
4705 if !yank.is_empty() {
4706 ed.record_yank_to_host(yank.clone());
4707 ed.record_delete(yank, false);
4708 }
4709 ed.jump_cursor(top, left);
4710 begin_insert_noundo(
4711 ed,
4712 1,
4713 InsertReason::BlockChange {
4714 top,
4715 bot,
4716 col: left,
4717 },
4718 );
4719 }
4720 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4721 ed.push_undo();
4722 transform_block_case(ed, op, top, bot, left, right);
4723 ed.vim.mode = Mode::Normal;
4724 ed.jump_cursor(top, left);
4725 }
4726 Operator::Indent | Operator::Outdent => {
4727 ed.push_undo();
4731 if op == Operator::Indent {
4732 indent_rows(ed, top, bot, 1);
4733 } else {
4734 outdent_rows(ed, top, bot, 1);
4735 }
4736 ed.vim.mode = Mode::Normal;
4737 }
4738 Operator::Fold => unreachable!("Visual zf takes its own path"),
4739 Operator::Reflow => {
4740 ed.push_undo();
4744 reflow_rows(ed, top, bot);
4745 ed.vim.mode = Mode::Normal;
4746 }
4747 Operator::AutoIndent => {
4748 ed.push_undo();
4751 auto_indent_rows(ed, top, bot);
4752 ed.vim.mode = Mode::Normal;
4753 }
4754 }
4755}
4756
4757fn transform_block_case<H: crate::types::Host>(
4761 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4762 op: Operator,
4763 top: usize,
4764 bot: usize,
4765 left: usize,
4766 right: usize,
4767) {
4768 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4769 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4770 let chars: Vec<char> = lines[r].chars().collect();
4771 if left >= chars.len() {
4772 continue;
4773 }
4774 let end = (right + 1).min(chars.len());
4775 let head: String = chars[..left].iter().collect();
4776 let mid: String = chars[left..end].iter().collect();
4777 let tail: String = chars[end..].iter().collect();
4778 let transformed = match op {
4779 Operator::Uppercase => mid.to_uppercase(),
4780 Operator::Lowercase => mid.to_lowercase(),
4781 Operator::ToggleCase => toggle_case_str(&mid),
4782 _ => mid,
4783 };
4784 lines[r] = format!("{head}{transformed}{tail}");
4785 }
4786 let saved_yank = ed.yank().to_string();
4787 let saved_linewise = ed.vim.yank_linewise;
4788 ed.restore(lines, (top, left));
4789 ed.set_yank(saved_yank);
4790 ed.vim.yank_linewise = saved_linewise;
4791}
4792
4793fn block_yank<H: crate::types::Host>(
4794 ed: &Editor<hjkl_buffer::Buffer, H>,
4795 top: usize,
4796 bot: usize,
4797 left: usize,
4798 right: usize,
4799) -> String {
4800 let lines = buf_lines_to_vec(&ed.buffer);
4801 let mut rows: Vec<String> = Vec::new();
4802 for r in top..=bot {
4803 let line = match lines.get(r) {
4804 Some(l) => l,
4805 None => break,
4806 };
4807 let chars: Vec<char> = line.chars().collect();
4808 let end = (right + 1).min(chars.len());
4809 if left >= chars.len() {
4810 rows.push(String::new());
4811 } else {
4812 rows.push(chars[left..end].iter().collect());
4813 }
4814 }
4815 rows.join("\n")
4816}
4817
4818fn delete_block_contents<H: crate::types::Host>(
4819 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4820 top: usize,
4821 bot: usize,
4822 left: usize,
4823 right: usize,
4824) {
4825 use hjkl_buffer::{Edit, MotionKind, Position};
4826 ed.sync_buffer_content_from_textarea();
4827 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4828 if last_row < top {
4829 return;
4830 }
4831 ed.mutate_edit(Edit::DeleteRange {
4832 start: Position::new(top, left),
4833 end: Position::new(last_row, right),
4834 kind: MotionKind::Block,
4835 });
4836 ed.push_buffer_cursor_to_textarea();
4837}
4838
4839pub(crate) fn block_replace<H: crate::types::Host>(
4841 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4842 ch: char,
4843) {
4844 let (top, bot, left, right) = block_bounds(ed);
4845 ed.push_undo();
4846 ed.sync_buffer_content_from_textarea();
4847 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4848 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4849 let chars: Vec<char> = lines[r].chars().collect();
4850 if left >= chars.len() {
4851 continue;
4852 }
4853 let end = (right + 1).min(chars.len());
4854 let before: String = chars[..left].iter().collect();
4855 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4856 let after: String = chars[end..].iter().collect();
4857 lines[r] = format!("{before}{middle}{after}");
4858 }
4859 reset_textarea_lines(ed, lines);
4860 ed.vim.mode = Mode::Normal;
4861 ed.jump_cursor(top, left);
4862}
4863
4864fn reset_textarea_lines<H: crate::types::Host>(
4868 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4869 lines: Vec<String>,
4870) {
4871 let cursor = ed.cursor();
4872 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4873 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4874 ed.mark_content_dirty();
4875}
4876
4877type Pos = (usize, usize);
4883
4884pub(crate) fn text_object_range<H: crate::types::Host>(
4888 ed: &Editor<hjkl_buffer::Buffer, H>,
4889 obj: TextObject,
4890 inner: bool,
4891) -> Option<(Pos, Pos, RangeKind)> {
4892 match obj {
4893 TextObject::Word { big } => {
4894 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
4895 }
4896 TextObject::Quote(q) => {
4897 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4898 }
4899 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4900 TextObject::Paragraph => {
4901 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
4902 }
4903 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
4904 TextObject::Sentence => {
4905 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4906 }
4907 }
4908}
4909
4910fn sentence_boundary<H: crate::types::Host>(
4914 ed: &Editor<hjkl_buffer::Buffer, H>,
4915 forward: bool,
4916) -> Option<(usize, usize)> {
4917 let lines = buf_lines_to_vec(&ed.buffer);
4918 if lines.is_empty() {
4919 return None;
4920 }
4921 let pos_to_idx = |pos: (usize, usize)| -> usize {
4922 let mut idx = 0;
4923 for line in lines.iter().take(pos.0) {
4924 idx += line.chars().count() + 1;
4925 }
4926 idx + pos.1
4927 };
4928 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4929 for (r, line) in lines.iter().enumerate() {
4930 let len = line.chars().count();
4931 if idx <= len {
4932 return (r, idx);
4933 }
4934 idx -= len + 1;
4935 }
4936 let last = lines.len().saturating_sub(1);
4937 (last, lines[last].chars().count())
4938 };
4939 let mut chars: Vec<char> = Vec::new();
4940 for (r, line) in lines.iter().enumerate() {
4941 chars.extend(line.chars());
4942 if r + 1 < lines.len() {
4943 chars.push('\n');
4944 }
4945 }
4946 if chars.is_empty() {
4947 return None;
4948 }
4949 let total = chars.len();
4950 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4951 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4952
4953 if forward {
4954 let mut i = cursor_idx + 1;
4957 while i < total {
4958 if is_terminator(chars[i]) {
4959 while i + 1 < total && is_terminator(chars[i + 1]) {
4960 i += 1;
4961 }
4962 if i + 1 >= total {
4963 return None;
4964 }
4965 if chars[i + 1].is_whitespace() {
4966 let mut j = i + 1;
4967 while j < total && chars[j].is_whitespace() {
4968 j += 1;
4969 }
4970 if j >= total {
4971 return None;
4972 }
4973 return Some(idx_to_pos(j));
4974 }
4975 }
4976 i += 1;
4977 }
4978 None
4979 } else {
4980 let find_start = |from: usize| -> Option<usize> {
4984 let mut start = from;
4985 while start > 0 {
4986 let prev = chars[start - 1];
4987 if prev.is_whitespace() {
4988 let mut k = start - 1;
4989 while k > 0 && chars[k - 1].is_whitespace() {
4990 k -= 1;
4991 }
4992 if k > 0 && is_terminator(chars[k - 1]) {
4993 break;
4994 }
4995 }
4996 start -= 1;
4997 }
4998 while start < total && chars[start].is_whitespace() {
4999 start += 1;
5000 }
5001 (start < total).then_some(start)
5002 };
5003 let current_start = find_start(cursor_idx)?;
5004 if current_start < cursor_idx {
5005 return Some(idx_to_pos(current_start));
5006 }
5007 let mut k = current_start;
5010 while k > 0 && chars[k - 1].is_whitespace() {
5011 k -= 1;
5012 }
5013 if k == 0 {
5014 return None;
5015 }
5016 let prev_start = find_start(k - 1)?;
5017 Some(idx_to_pos(prev_start))
5018 }
5019}
5020
5021fn sentence_text_object<H: crate::types::Host>(
5027 ed: &Editor<hjkl_buffer::Buffer, H>,
5028 inner: bool,
5029) -> Option<((usize, usize), (usize, usize))> {
5030 let lines = buf_lines_to_vec(&ed.buffer);
5031 if lines.is_empty() {
5032 return None;
5033 }
5034 let pos_to_idx = |pos: (usize, usize)| -> usize {
5037 let mut idx = 0;
5038 for line in lines.iter().take(pos.0) {
5039 idx += line.chars().count() + 1;
5040 }
5041 idx + pos.1
5042 };
5043 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5044 for (r, line) in lines.iter().enumerate() {
5045 let len = line.chars().count();
5046 if idx <= len {
5047 return (r, idx);
5048 }
5049 idx -= len + 1;
5050 }
5051 let last = lines.len().saturating_sub(1);
5052 (last, lines[last].chars().count())
5053 };
5054 let mut chars: Vec<char> = Vec::new();
5055 for (r, line) in lines.iter().enumerate() {
5056 chars.extend(line.chars());
5057 if r + 1 < lines.len() {
5058 chars.push('\n');
5059 }
5060 }
5061 if chars.is_empty() {
5062 return None;
5063 }
5064
5065 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
5066 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5067
5068 let mut start = cursor_idx;
5072 while start > 0 {
5073 let prev = chars[start - 1];
5074 if prev.is_whitespace() {
5075 let mut k = start - 1;
5079 while k > 0 && chars[k - 1].is_whitespace() {
5080 k -= 1;
5081 }
5082 if k > 0 && is_terminator(chars[k - 1]) {
5083 break;
5084 }
5085 }
5086 start -= 1;
5087 }
5088 while start < chars.len() && chars[start].is_whitespace() {
5091 start += 1;
5092 }
5093 if start >= chars.len() {
5094 return None;
5095 }
5096
5097 let mut end = start;
5100 while end < chars.len() {
5101 if is_terminator(chars[end]) {
5102 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5104 end += 1;
5105 }
5106 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5109 break;
5110 }
5111 }
5112 end += 1;
5113 }
5114 let end_idx = (end + 1).min(chars.len());
5116
5117 let final_end = if inner {
5118 end_idx
5119 } else {
5120 let mut e = end_idx;
5124 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5125 e += 1;
5126 }
5127 e
5128 };
5129
5130 Some((idx_to_pos(start), idx_to_pos(final_end)))
5131}
5132
5133fn tag_text_object<H: crate::types::Host>(
5137 ed: &Editor<hjkl_buffer::Buffer, H>,
5138 inner: bool,
5139) -> Option<((usize, usize), (usize, usize))> {
5140 let lines = buf_lines_to_vec(&ed.buffer);
5141 if lines.is_empty() {
5142 return None;
5143 }
5144 let pos_to_idx = |pos: (usize, usize)| -> usize {
5148 let mut idx = 0;
5149 for line in lines.iter().take(pos.0) {
5150 idx += line.chars().count() + 1;
5151 }
5152 idx + pos.1
5153 };
5154 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5155 for (r, line) in lines.iter().enumerate() {
5156 let len = line.chars().count();
5157 if idx <= len {
5158 return (r, idx);
5159 }
5160 idx -= len + 1;
5161 }
5162 let last = lines.len().saturating_sub(1);
5163 (last, lines[last].chars().count())
5164 };
5165 let mut chars: Vec<char> = Vec::new();
5166 for (r, line) in lines.iter().enumerate() {
5167 chars.extend(line.chars());
5168 if r + 1 < lines.len() {
5169 chars.push('\n');
5170 }
5171 }
5172 let cursor_idx = pos_to_idx(ed.cursor());
5173
5174 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5182 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5183 let mut i = 0;
5184 while i < chars.len() {
5185 if chars[i] != '<' {
5186 i += 1;
5187 continue;
5188 }
5189 let mut j = i + 1;
5190 while j < chars.len() && chars[j] != '>' {
5191 j += 1;
5192 }
5193 if j >= chars.len() {
5194 break;
5195 }
5196 let inside: String = chars[i + 1..j].iter().collect();
5197 let close_end = j + 1;
5198 let trimmed = inside.trim();
5199 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5200 i = close_end;
5201 continue;
5202 }
5203 if let Some(rest) = trimmed.strip_prefix('/') {
5204 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5205 if !name.is_empty()
5206 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5207 {
5208 let (open_start, content_start, _) = stack[stack_idx].clone();
5209 stack.truncate(stack_idx);
5210 let content_end = i;
5211 let candidate = (open_start, content_start, content_end, close_end);
5212 if cursor_idx >= content_start && cursor_idx <= content_end {
5213 innermost = match innermost {
5214 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5215 Some(candidate)
5216 }
5217 None => Some(candidate),
5218 existing => existing,
5219 };
5220 } else if open_start >= cursor_idx && next_after.is_none() {
5221 next_after = Some(candidate);
5222 }
5223 }
5224 } else if !trimmed.ends_with('/') {
5225 let name: String = trimmed
5226 .split(|c: char| c.is_whitespace() || c == '/')
5227 .next()
5228 .unwrap_or("")
5229 .to_string();
5230 if !name.is_empty() {
5231 stack.push((i, close_end, name));
5232 }
5233 }
5234 i = close_end;
5235 }
5236
5237 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5238 if inner {
5239 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5240 } else {
5241 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5242 }
5243}
5244
5245fn is_wordchar(c: char) -> bool {
5246 c.is_alphanumeric() || c == '_'
5247}
5248
5249pub(crate) use hjkl_buffer::is_keyword_char;
5253
5254fn word_text_object<H: crate::types::Host>(
5255 ed: &Editor<hjkl_buffer::Buffer, H>,
5256 inner: bool,
5257 big: bool,
5258) -> Option<((usize, usize), (usize, usize))> {
5259 let (row, col) = ed.cursor();
5260 let line = buf_line(&ed.buffer, row)?;
5261 let chars: Vec<char> = line.chars().collect();
5262 if chars.is_empty() {
5263 return None;
5264 }
5265 let at = col.min(chars.len().saturating_sub(1));
5266 let classify = |c: char| -> u8 {
5267 if c.is_whitespace() {
5268 0
5269 } else if big || is_wordchar(c) {
5270 1
5271 } else {
5272 2
5273 }
5274 };
5275 let cls = classify(chars[at]);
5276 let mut start = at;
5277 while start > 0 && classify(chars[start - 1]) == cls {
5278 start -= 1;
5279 }
5280 let mut end = at;
5281 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5282 end += 1;
5283 }
5284 let char_byte = |i: usize| {
5286 if i >= chars.len() {
5287 line.len()
5288 } else {
5289 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5290 }
5291 };
5292 let mut start_col = char_byte(start);
5293 let mut end_col = char_byte(end + 1);
5295 if !inner {
5296 let mut t = end + 1;
5298 let mut included_trailing = false;
5299 while t < chars.len() && chars[t].is_whitespace() {
5300 included_trailing = true;
5301 t += 1;
5302 }
5303 if included_trailing {
5304 end_col = char_byte(t);
5305 } else {
5306 let mut s = start;
5307 while s > 0 && chars[s - 1].is_whitespace() {
5308 s -= 1;
5309 }
5310 start_col = char_byte(s);
5311 }
5312 }
5313 Some(((row, start_col), (row, end_col)))
5314}
5315
5316fn quote_text_object<H: crate::types::Host>(
5317 ed: &Editor<hjkl_buffer::Buffer, H>,
5318 q: char,
5319 inner: bool,
5320) -> Option<((usize, usize), (usize, usize))> {
5321 let (row, col) = ed.cursor();
5322 let line = buf_line(&ed.buffer, row)?;
5323 let bytes = line.as_bytes();
5324 let q_byte = q as u8;
5325 let mut positions: Vec<usize> = Vec::new();
5327 for (i, &b) in bytes.iter().enumerate() {
5328 if b == q_byte {
5329 positions.push(i);
5330 }
5331 }
5332 if positions.len() < 2 {
5333 return None;
5334 }
5335 let mut open_idx: Option<usize> = None;
5336 let mut close_idx: Option<usize> = None;
5337 for pair in positions.chunks(2) {
5338 if pair.len() < 2 {
5339 break;
5340 }
5341 if col >= pair[0] && col <= pair[1] {
5342 open_idx = Some(pair[0]);
5343 close_idx = Some(pair[1]);
5344 break;
5345 }
5346 if col < pair[0] {
5347 open_idx = Some(pair[0]);
5348 close_idx = Some(pair[1]);
5349 break;
5350 }
5351 }
5352 let open = open_idx?;
5353 let close = close_idx?;
5354 if inner {
5356 if close <= open + 1 {
5357 return None;
5358 }
5359 Some(((row, open + 1), (row, close)))
5360 } else {
5361 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5368 let mut end = after_close;
5370 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5371 end += 1;
5372 }
5373 Some(((row, open), (row, end)))
5374 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5375 let mut start = open;
5377 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5378 start -= 1;
5379 }
5380 Some(((row, start), (row, close + 1)))
5381 } else {
5382 Some(((row, open), (row, close + 1)))
5383 }
5384 }
5385}
5386
5387fn bracket_text_object<H: crate::types::Host>(
5388 ed: &Editor<hjkl_buffer::Buffer, H>,
5389 open: char,
5390 inner: bool,
5391) -> Option<(Pos, Pos, RangeKind)> {
5392 let close = match open {
5393 '(' => ')',
5394 '[' => ']',
5395 '{' => '}',
5396 '<' => '>',
5397 _ => return None,
5398 };
5399 let (row, col) = ed.cursor();
5400 let lines = buf_lines_to_vec(&ed.buffer);
5401 let lines = lines.as_slice();
5402 let open_pos = find_open_bracket(lines, row, col, open, close)
5407 .or_else(|| find_next_open(lines, row, col, open))?;
5408 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5409 if inner {
5411 if close_pos.0 > open_pos.0 + 1 {
5417 let inner_row_start = open_pos.0 + 1;
5419 let inner_row_end = close_pos.0 - 1;
5420 let end_col = lines
5421 .get(inner_row_end)
5422 .map(|l| l.chars().count())
5423 .unwrap_or(0);
5424 return Some((
5425 (inner_row_start, 0),
5426 (inner_row_end, end_col),
5427 RangeKind::Linewise,
5428 ));
5429 }
5430 let inner_start = advance_pos(lines, open_pos);
5431 if inner_start.0 > close_pos.0
5432 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5433 {
5434 return None;
5435 }
5436 Some((inner_start, close_pos, RangeKind::Exclusive))
5437 } else {
5438 Some((
5439 open_pos,
5440 advance_pos(lines, close_pos),
5441 RangeKind::Exclusive,
5442 ))
5443 }
5444}
5445
5446fn find_open_bracket(
5447 lines: &[String],
5448 row: usize,
5449 col: usize,
5450 open: char,
5451 close: char,
5452) -> Option<(usize, usize)> {
5453 let mut depth: i32 = 0;
5454 let mut r = row;
5455 let mut c = col as isize;
5456 loop {
5457 let cur = &lines[r];
5458 let chars: Vec<char> = cur.chars().collect();
5459 if (c as usize) >= chars.len() {
5463 c = chars.len() as isize - 1;
5464 }
5465 while c >= 0 {
5466 let ch = chars[c as usize];
5467 if ch == close {
5468 depth += 1;
5469 } else if ch == open {
5470 if depth == 0 {
5471 return Some((r, c as usize));
5472 }
5473 depth -= 1;
5474 }
5475 c -= 1;
5476 }
5477 if r == 0 {
5478 return None;
5479 }
5480 r -= 1;
5481 c = lines[r].chars().count() as isize - 1;
5482 }
5483}
5484
5485fn find_close_bracket(
5486 lines: &[String],
5487 row: usize,
5488 start_col: usize,
5489 open: char,
5490 close: char,
5491) -> Option<(usize, usize)> {
5492 let mut depth: i32 = 0;
5493 let mut r = row;
5494 let mut c = start_col;
5495 loop {
5496 let cur = &lines[r];
5497 let chars: Vec<char> = cur.chars().collect();
5498 while c < chars.len() {
5499 let ch = chars[c];
5500 if ch == open {
5501 depth += 1;
5502 } else if ch == close {
5503 if depth == 0 {
5504 return Some((r, c));
5505 }
5506 depth -= 1;
5507 }
5508 c += 1;
5509 }
5510 if r + 1 >= lines.len() {
5511 return None;
5512 }
5513 r += 1;
5514 c = 0;
5515 }
5516}
5517
5518fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5522 let mut r = row;
5523 let mut c = col;
5524 while r < lines.len() {
5525 let chars: Vec<char> = lines[r].chars().collect();
5526 while c < chars.len() {
5527 if chars[c] == open {
5528 return Some((r, c));
5529 }
5530 c += 1;
5531 }
5532 r += 1;
5533 c = 0;
5534 }
5535 None
5536}
5537
5538fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5539 let (r, c) = pos;
5540 let line_len = lines[r].chars().count();
5541 if c < line_len {
5542 (r, c + 1)
5543 } else if r + 1 < lines.len() {
5544 (r + 1, 0)
5545 } else {
5546 pos
5547 }
5548}
5549
5550fn paragraph_text_object<H: crate::types::Host>(
5551 ed: &Editor<hjkl_buffer::Buffer, H>,
5552 inner: bool,
5553) -> Option<((usize, usize), (usize, usize))> {
5554 let (row, _) = ed.cursor();
5555 let lines = buf_lines_to_vec(&ed.buffer);
5556 if lines.is_empty() {
5557 return None;
5558 }
5559 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5561 if is_blank(row) {
5562 return None;
5563 }
5564 let mut top = row;
5565 while top > 0 && !is_blank(top - 1) {
5566 top -= 1;
5567 }
5568 let mut bot = row;
5569 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5570 bot += 1;
5571 }
5572 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5574 bot += 1;
5575 }
5576 let end_col = lines[bot].chars().count();
5577 Some(((top, 0), (bot, end_col)))
5578}
5579
5580fn read_vim_range<H: crate::types::Host>(
5586 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5587 start: (usize, usize),
5588 end: (usize, usize),
5589 kind: RangeKind,
5590) -> String {
5591 let (top, bot) = order(start, end);
5592 ed.sync_buffer_content_from_textarea();
5593 let lines = buf_lines_to_vec(&ed.buffer);
5594 match kind {
5595 RangeKind::Linewise => {
5596 let lo = top.0;
5597 let hi = bot.0.min(lines.len().saturating_sub(1));
5598 let mut text = lines[lo..=hi].join("\n");
5599 text.push('\n');
5600 text
5601 }
5602 RangeKind::Inclusive | RangeKind::Exclusive => {
5603 let inclusive = matches!(kind, RangeKind::Inclusive);
5604 let mut out = String::new();
5606 for row in top.0..=bot.0 {
5607 let line = lines.get(row).map(String::as_str).unwrap_or("");
5608 let lo = if row == top.0 { top.1 } else { 0 };
5609 let hi_unclamped = if row == bot.0 {
5610 if inclusive { bot.1 + 1 } else { bot.1 }
5611 } else {
5612 line.chars().count() + 1
5613 };
5614 let row_chars: Vec<char> = line.chars().collect();
5615 let hi = hi_unclamped.min(row_chars.len());
5616 if lo < hi {
5617 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5618 }
5619 if row < bot.0 {
5620 out.push('\n');
5621 }
5622 }
5623 out
5624 }
5625 }
5626}
5627
5628fn cut_vim_range<H: crate::types::Host>(
5637 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5638 start: (usize, usize),
5639 end: (usize, usize),
5640 kind: RangeKind,
5641) -> String {
5642 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5643 let (top, bot) = order(start, end);
5644 ed.sync_buffer_content_from_textarea();
5645 let (buf_start, buf_end, buf_kind) = match kind {
5646 RangeKind::Linewise => (
5647 Position::new(top.0, 0),
5648 Position::new(bot.0, 0),
5649 BufKind::Line,
5650 ),
5651 RangeKind::Inclusive => {
5652 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5653 let next = if bot.1 < line_chars {
5657 Position::new(bot.0, bot.1 + 1)
5658 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5659 Position::new(bot.0 + 1, 0)
5660 } else {
5661 Position::new(bot.0, line_chars)
5662 };
5663 (Position::new(top.0, top.1), next, BufKind::Char)
5664 }
5665 RangeKind::Exclusive => (
5666 Position::new(top.0, top.1),
5667 Position::new(bot.0, bot.1),
5668 BufKind::Char,
5669 ),
5670 };
5671 let inverse = ed.mutate_edit(Edit::DeleteRange {
5672 start: buf_start,
5673 end: buf_end,
5674 kind: buf_kind,
5675 });
5676 let text = match inverse {
5677 Edit::InsertStr { text, .. } => text,
5678 _ => String::new(),
5679 };
5680 if !text.is_empty() {
5681 ed.record_yank_to_host(text.clone());
5682 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
5683 }
5684 ed.push_buffer_cursor_to_textarea();
5685 text
5686}
5687
5688fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5694 use hjkl_buffer::{Edit, MotionKind, Position};
5695 ed.sync_buffer_content_from_textarea();
5696 let cursor = buf_cursor_pos(&ed.buffer);
5697 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5698 if cursor.col >= line_chars {
5699 return;
5700 }
5701 let inverse = ed.mutate_edit(Edit::DeleteRange {
5702 start: cursor,
5703 end: Position::new(cursor.row, line_chars),
5704 kind: MotionKind::Char,
5705 });
5706 if let Edit::InsertStr { text, .. } = inverse
5707 && !text.is_empty()
5708 {
5709 ed.record_yank_to_host(text.clone());
5710 ed.vim.yank_linewise = false;
5711 ed.set_yank(text);
5712 }
5713 buf_set_cursor_pos(&mut ed.buffer, cursor);
5714 ed.push_buffer_cursor_to_textarea();
5715}
5716
5717fn do_char_delete<H: crate::types::Host>(
5718 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5719 forward: bool,
5720 count: usize,
5721) {
5722 use hjkl_buffer::{Edit, MotionKind, Position};
5723 ed.push_undo();
5724 ed.sync_buffer_content_from_textarea();
5725 let mut deleted = String::new();
5728 for _ in 0..count {
5729 let cursor = buf_cursor_pos(&ed.buffer);
5730 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5731 if forward {
5732 if cursor.col >= line_chars {
5735 continue;
5736 }
5737 let inverse = ed.mutate_edit(Edit::DeleteRange {
5738 start: cursor,
5739 end: Position::new(cursor.row, cursor.col + 1),
5740 kind: MotionKind::Char,
5741 });
5742 if let Edit::InsertStr { text, .. } = inverse {
5743 deleted.push_str(&text);
5744 }
5745 } else {
5746 if cursor.col == 0 {
5748 continue;
5749 }
5750 let inverse = ed.mutate_edit(Edit::DeleteRange {
5751 start: Position::new(cursor.row, cursor.col - 1),
5752 end: cursor,
5753 kind: MotionKind::Char,
5754 });
5755 if let Edit::InsertStr { text, .. } = inverse {
5756 deleted = text + &deleted;
5759 }
5760 }
5761 }
5762 if !deleted.is_empty() {
5763 ed.record_yank_to_host(deleted.clone());
5764 ed.record_delete(deleted, false);
5765 }
5766 ed.push_buffer_cursor_to_textarea();
5767}
5768
5769pub(crate) fn adjust_number<H: crate::types::Host>(
5773 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5774 delta: i64,
5775) -> bool {
5776 use hjkl_buffer::{Edit, MotionKind, Position};
5777 ed.sync_buffer_content_from_textarea();
5778 let cursor = buf_cursor_pos(&ed.buffer);
5779 let row = cursor.row;
5780 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5781 Some(l) => l.chars().collect(),
5782 None => return false,
5783 };
5784 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5785 return false;
5786 };
5787 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5788 digit_start - 1
5789 } else {
5790 digit_start
5791 };
5792 let mut span_end = digit_start;
5793 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5794 span_end += 1;
5795 }
5796 let s: String = chars[span_start..span_end].iter().collect();
5797 let Ok(n) = s.parse::<i64>() else {
5798 return false;
5799 };
5800 let new_s = n.saturating_add(delta).to_string();
5801
5802 ed.push_undo();
5803 let span_start_pos = Position::new(row, span_start);
5804 let span_end_pos = Position::new(row, span_end);
5805 ed.mutate_edit(Edit::DeleteRange {
5806 start: span_start_pos,
5807 end: span_end_pos,
5808 kind: MotionKind::Char,
5809 });
5810 ed.mutate_edit(Edit::InsertStr {
5811 at: span_start_pos,
5812 text: new_s.clone(),
5813 });
5814 let new_len = new_s.chars().count();
5815 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5816 ed.push_buffer_cursor_to_textarea();
5817 true
5818}
5819
5820pub(crate) fn replace_char<H: crate::types::Host>(
5821 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5822 ch: char,
5823 count: usize,
5824) {
5825 use hjkl_buffer::{Edit, MotionKind, Position};
5826 ed.push_undo();
5827 ed.sync_buffer_content_from_textarea();
5828 for _ in 0..count {
5829 let cursor = buf_cursor_pos(&ed.buffer);
5830 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5831 if cursor.col >= line_chars {
5832 break;
5833 }
5834 ed.mutate_edit(Edit::DeleteRange {
5835 start: cursor,
5836 end: Position::new(cursor.row, cursor.col + 1),
5837 kind: MotionKind::Char,
5838 });
5839 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5840 }
5841 crate::motions::move_left(&mut ed.buffer, 1);
5843 ed.push_buffer_cursor_to_textarea();
5844}
5845
5846fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5847 use hjkl_buffer::{Edit, MotionKind, Position};
5848 ed.sync_buffer_content_from_textarea();
5849 let cursor = buf_cursor_pos(&ed.buffer);
5850 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5851 return;
5852 };
5853 let toggled = if c.is_uppercase() {
5854 c.to_lowercase().next().unwrap_or(c)
5855 } else {
5856 c.to_uppercase().next().unwrap_or(c)
5857 };
5858 ed.mutate_edit(Edit::DeleteRange {
5859 start: cursor,
5860 end: Position::new(cursor.row, cursor.col + 1),
5861 kind: MotionKind::Char,
5862 });
5863 ed.mutate_edit(Edit::InsertChar {
5864 at: cursor,
5865 ch: toggled,
5866 });
5867}
5868
5869fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5870 use hjkl_buffer::{Edit, Position};
5871 ed.sync_buffer_content_from_textarea();
5872 let row = buf_cursor_pos(&ed.buffer).row;
5873 if row + 1 >= buf_row_count(&ed.buffer) {
5874 return;
5875 }
5876 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5877 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5878 let next_trimmed = next_raw.trim_start();
5879 let cur_chars = cur_line.chars().count();
5880 let next_chars = next_raw.chars().count();
5881 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5884 " "
5885 } else {
5886 ""
5887 };
5888 let joined = format!("{cur_line}{separator}{next_trimmed}");
5889 ed.mutate_edit(Edit::Replace {
5890 start: Position::new(row, 0),
5891 end: Position::new(row + 1, next_chars),
5892 with: joined,
5893 });
5894 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5898 ed.push_buffer_cursor_to_textarea();
5899}
5900
5901fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5904 use hjkl_buffer::Edit;
5905 ed.sync_buffer_content_from_textarea();
5906 let row = buf_cursor_pos(&ed.buffer).row;
5907 if row + 1 >= buf_row_count(&ed.buffer) {
5908 return;
5909 }
5910 let join_col = buf_line_chars(&ed.buffer, row);
5911 ed.mutate_edit(Edit::JoinLines {
5912 row,
5913 count: 1,
5914 with_space: false,
5915 });
5916 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5918 ed.push_buffer_cursor_to_textarea();
5919}
5920
5921fn do_paste<H: crate::types::Host>(
5922 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5923 before: bool,
5924 count: usize,
5925) {
5926 use hjkl_buffer::{Edit, Position};
5927 ed.push_undo();
5928 let selector = ed.vim.pending_register.take();
5933 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5934 Some(slot) => (slot.text.clone(), slot.linewise),
5935 None => {
5941 let s = &ed.registers().unnamed;
5942 (s.text.clone(), s.linewise)
5943 }
5944 };
5945 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5949 for _ in 0..count {
5950 ed.sync_buffer_content_from_textarea();
5951 let yank = yank.clone();
5952 if yank.is_empty() {
5953 continue;
5954 }
5955 if linewise {
5956 let text = yank.trim_matches('\n').to_string();
5960 let row = buf_cursor_pos(&ed.buffer).row;
5961 let target_row = if before {
5962 ed.mutate_edit(Edit::InsertStr {
5963 at: Position::new(row, 0),
5964 text: format!("{text}\n"),
5965 });
5966 row
5967 } else {
5968 let line_chars = buf_line_chars(&ed.buffer, row);
5969 ed.mutate_edit(Edit::InsertStr {
5970 at: Position::new(row, line_chars),
5971 text: format!("\n{text}"),
5972 });
5973 row + 1
5974 };
5975 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5976 crate::motions::move_first_non_blank(&mut ed.buffer);
5977 ed.push_buffer_cursor_to_textarea();
5978 let payload_lines = text.lines().count().max(1);
5980 let bot_row = target_row + payload_lines - 1;
5981 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5982 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5983 } else {
5984 let cursor = buf_cursor_pos(&ed.buffer);
5988 let at = if before {
5989 cursor
5990 } else {
5991 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5992 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5993 };
5994 ed.mutate_edit(Edit::InsertStr {
5995 at,
5996 text: yank.clone(),
5997 });
5998 crate::motions::move_left(&mut ed.buffer, 1);
6001 ed.push_buffer_cursor_to_textarea();
6002 let lo = (at.row, at.col);
6004 let hi = ed.cursor();
6005 paste_mark = Some((lo, hi));
6006 }
6007 }
6008 if let Some((lo, hi)) = paste_mark {
6009 ed.set_mark('[', lo);
6010 ed.set_mark(']', hi);
6011 }
6012 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
6014}
6015
6016pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6017 if let Some((lines, cursor)) = ed.undo_stack.pop() {
6018 let current = ed.snapshot();
6019 ed.redo_stack.push(current);
6020 ed.restore(lines, cursor);
6021 }
6022 ed.vim.mode = Mode::Normal;
6023 clamp_cursor_to_normal_mode(ed);
6027}
6028
6029pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6030 if let Some((lines, cursor)) = ed.redo_stack.pop() {
6031 let current = ed.snapshot();
6032 ed.undo_stack.push(current);
6033 ed.cap_undo();
6034 ed.restore(lines, cursor);
6035 }
6036 ed.vim.mode = Mode::Normal;
6037}
6038
6039fn replay_insert_and_finish<H: crate::types::Host>(
6046 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6047 text: &str,
6048) {
6049 use hjkl_buffer::{Edit, Position};
6050 let cursor = ed.cursor();
6051 ed.mutate_edit(Edit::InsertStr {
6052 at: Position::new(cursor.0, cursor.1),
6053 text: text.to_string(),
6054 });
6055 if ed.vim.insert_session.take().is_some() {
6056 if ed.cursor().1 > 0 {
6057 crate::motions::move_left(&mut ed.buffer, 1);
6058 ed.push_buffer_cursor_to_textarea();
6059 }
6060 ed.vim.mode = Mode::Normal;
6061 }
6062}
6063
6064pub(crate) fn replay_last_change<H: crate::types::Host>(
6065 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6066 outer_count: usize,
6067) {
6068 let Some(change) = ed.vim.last_change.clone() else {
6069 return;
6070 };
6071 ed.vim.replaying = true;
6072 let scale = if outer_count > 0 { outer_count } else { 1 };
6073 match change {
6074 LastChange::OpMotion {
6075 op,
6076 motion,
6077 count,
6078 inserted,
6079 } => {
6080 let total = count.max(1) * scale;
6081 apply_op_with_motion(ed, op, &motion, total);
6082 if let Some(text) = inserted {
6083 replay_insert_and_finish(ed, &text);
6084 }
6085 }
6086 LastChange::OpTextObj {
6087 op,
6088 obj,
6089 inner,
6090 inserted,
6091 } => {
6092 apply_op_with_text_object(ed, op, obj, inner);
6093 if let Some(text) = inserted {
6094 replay_insert_and_finish(ed, &text);
6095 }
6096 }
6097 LastChange::LineOp {
6098 op,
6099 count,
6100 inserted,
6101 } => {
6102 let total = count.max(1) * scale;
6103 execute_line_op(ed, op, total);
6104 if let Some(text) = inserted {
6105 replay_insert_and_finish(ed, &text);
6106 }
6107 }
6108 LastChange::CharDel { forward, count } => {
6109 do_char_delete(ed, forward, count * scale);
6110 }
6111 LastChange::ReplaceChar { ch, count } => {
6112 replace_char(ed, ch, count * scale);
6113 }
6114 LastChange::ToggleCase { count } => {
6115 for _ in 0..count * scale {
6116 ed.push_undo();
6117 toggle_case_at_cursor(ed);
6118 }
6119 }
6120 LastChange::JoinLine { count } => {
6121 for _ in 0..count * scale {
6122 ed.push_undo();
6123 join_line(ed);
6124 }
6125 }
6126 LastChange::Paste { before, count } => {
6127 do_paste(ed, before, count * scale);
6128 }
6129 LastChange::DeleteToEol { inserted } => {
6130 use hjkl_buffer::{Edit, Position};
6131 ed.push_undo();
6132 delete_to_eol(ed);
6133 if let Some(text) = inserted {
6134 let cursor = ed.cursor();
6135 ed.mutate_edit(Edit::InsertStr {
6136 at: Position::new(cursor.0, cursor.1),
6137 text,
6138 });
6139 }
6140 }
6141 LastChange::OpenLine { above, inserted } => {
6142 use hjkl_buffer::{Edit, Position};
6143 ed.push_undo();
6144 ed.sync_buffer_content_from_textarea();
6145 let row = buf_cursor_pos(&ed.buffer).row;
6146 if above {
6147 ed.mutate_edit(Edit::InsertStr {
6148 at: Position::new(row, 0),
6149 text: "\n".to_string(),
6150 });
6151 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6152 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6153 } else {
6154 let line_chars = buf_line_chars(&ed.buffer, row);
6155 ed.mutate_edit(Edit::InsertStr {
6156 at: Position::new(row, line_chars),
6157 text: "\n".to_string(),
6158 });
6159 }
6160 ed.push_buffer_cursor_to_textarea();
6161 let cursor = ed.cursor();
6162 ed.mutate_edit(Edit::InsertStr {
6163 at: Position::new(cursor.0, cursor.1),
6164 text: inserted,
6165 });
6166 }
6167 LastChange::InsertAt {
6168 entry,
6169 inserted,
6170 count,
6171 } => {
6172 use hjkl_buffer::{Edit, Position};
6173 ed.push_undo();
6174 match entry {
6175 InsertEntry::I => {}
6176 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6177 InsertEntry::A => {
6178 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6179 ed.push_buffer_cursor_to_textarea();
6180 }
6181 InsertEntry::ShiftA => {
6182 crate::motions::move_line_end(&mut ed.buffer);
6183 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6184 ed.push_buffer_cursor_to_textarea();
6185 }
6186 }
6187 for _ in 0..count.max(1) {
6188 let cursor = ed.cursor();
6189 ed.mutate_edit(Edit::InsertStr {
6190 at: Position::new(cursor.0, cursor.1),
6191 text: inserted.clone(),
6192 });
6193 }
6194 }
6195 }
6196 ed.vim.replaying = false;
6197}
6198
6199fn extract_inserted(before: &str, after: &str) -> String {
6202 let before_chars: Vec<char> = before.chars().collect();
6203 let after_chars: Vec<char> = after.chars().collect();
6204 if after_chars.len() <= before_chars.len() {
6205 return String::new();
6206 }
6207 let prefix = before_chars
6208 .iter()
6209 .zip(after_chars.iter())
6210 .take_while(|(a, b)| a == b)
6211 .count();
6212 let max_suffix = before_chars.len() - prefix;
6213 let suffix = before_chars
6214 .iter()
6215 .rev()
6216 .zip(after_chars.iter().rev())
6217 .take(max_suffix)
6218 .take_while(|(a, b)| a == b)
6219 .count();
6220 after_chars[prefix..after_chars.len() - suffix]
6221 .iter()
6222 .collect()
6223}
6224
6225