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 buf_set_cursor_rc(&mut ed.buffer, row, target);
2615 } else {
2616 ed.sticky_col = Some(ed.cursor().1);
2619 }
2620}
2621
2622fn is_vertical_motion(motion: &Motion) -> bool {
2623 matches!(
2627 motion,
2628 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2629 )
2630}
2631
2632fn apply_motion_cursor<H: crate::types::Host>(
2633 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2634 motion: &Motion,
2635 count: usize,
2636) {
2637 apply_motion_cursor_ctx(ed, motion, count, false)
2638}
2639
2640pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
2641 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2642 motion: &Motion,
2643 count: usize,
2644 as_operator: bool,
2645) {
2646 match motion {
2647 Motion::Left => {
2648 crate::motions::move_left(&mut ed.buffer, count);
2650 ed.push_buffer_cursor_to_textarea();
2651 }
2652 Motion::Right => {
2653 if as_operator {
2657 crate::motions::move_right_to_end(&mut ed.buffer, count);
2658 } else {
2659 crate::motions::move_right_in_line(&mut ed.buffer, count);
2660 }
2661 ed.push_buffer_cursor_to_textarea();
2662 }
2663 Motion::Up => {
2664 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2668 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2669 ed.push_buffer_cursor_to_textarea();
2670 }
2671 Motion::Down => {
2672 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2673 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2674 ed.push_buffer_cursor_to_textarea();
2675 }
2676 Motion::ScreenUp => {
2677 let v = *ed.host.viewport();
2678 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2679 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2680 ed.push_buffer_cursor_to_textarea();
2681 }
2682 Motion::ScreenDown => {
2683 let v = *ed.host.viewport();
2684 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2685 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2686 ed.push_buffer_cursor_to_textarea();
2687 }
2688 Motion::WordFwd => {
2689 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2690 ed.push_buffer_cursor_to_textarea();
2691 }
2692 Motion::WordBack => {
2693 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2694 ed.push_buffer_cursor_to_textarea();
2695 }
2696 Motion::WordEnd => {
2697 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2698 ed.push_buffer_cursor_to_textarea();
2699 }
2700 Motion::BigWordFwd => {
2701 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2702 ed.push_buffer_cursor_to_textarea();
2703 }
2704 Motion::BigWordBack => {
2705 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2706 ed.push_buffer_cursor_to_textarea();
2707 }
2708 Motion::BigWordEnd => {
2709 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2710 ed.push_buffer_cursor_to_textarea();
2711 }
2712 Motion::WordEndBack => {
2713 crate::motions::move_word_end_back(
2714 &mut ed.buffer,
2715 false,
2716 count,
2717 &ed.settings.iskeyword,
2718 );
2719 ed.push_buffer_cursor_to_textarea();
2720 }
2721 Motion::BigWordEndBack => {
2722 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2723 ed.push_buffer_cursor_to_textarea();
2724 }
2725 Motion::LineStart => {
2726 crate::motions::move_line_start(&mut ed.buffer);
2727 ed.push_buffer_cursor_to_textarea();
2728 }
2729 Motion::FirstNonBlank => {
2730 crate::motions::move_first_non_blank(&mut ed.buffer);
2731 ed.push_buffer_cursor_to_textarea();
2732 }
2733 Motion::LineEnd => {
2734 crate::motions::move_line_end(&mut ed.buffer);
2736 ed.push_buffer_cursor_to_textarea();
2737 }
2738 Motion::FileTop => {
2739 if count > 1 {
2742 crate::motions::move_bottom(&mut ed.buffer, count);
2743 } else {
2744 crate::motions::move_top(&mut ed.buffer);
2745 }
2746 ed.push_buffer_cursor_to_textarea();
2747 }
2748 Motion::FileBottom => {
2749 if count > 1 {
2752 crate::motions::move_bottom(&mut ed.buffer, count);
2753 } else {
2754 crate::motions::move_bottom(&mut ed.buffer, 0);
2755 }
2756 ed.push_buffer_cursor_to_textarea();
2757 }
2758 Motion::Find { ch, forward, till } => {
2759 for _ in 0..count {
2760 if !find_char_on_line(ed, *ch, *forward, *till) {
2761 break;
2762 }
2763 }
2764 }
2765 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2767 let _ = matching_bracket(ed);
2768 }
2769 Motion::WordAtCursor {
2770 forward,
2771 whole_word,
2772 } => {
2773 word_at_cursor_search(ed, *forward, *whole_word, count);
2774 }
2775 Motion::SearchNext { reverse } => {
2776 if let Some(pattern) = ed.vim.last_search.clone() {
2780 ed.push_search_pattern(&pattern);
2781 }
2782 if ed.search_state().pattern.is_none() {
2783 return;
2784 }
2785 let forward = ed.vim.last_search_forward != *reverse;
2789 for _ in 0..count.max(1) {
2790 if forward {
2791 ed.search_advance_forward(true);
2792 } else {
2793 ed.search_advance_backward(true);
2794 }
2795 }
2796 ed.push_buffer_cursor_to_textarea();
2797 }
2798 Motion::ViewportTop => {
2799 let v = *ed.host().viewport();
2800 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2801 ed.push_buffer_cursor_to_textarea();
2802 }
2803 Motion::ViewportMiddle => {
2804 let v = *ed.host().viewport();
2805 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2806 ed.push_buffer_cursor_to_textarea();
2807 }
2808 Motion::ViewportBottom => {
2809 let v = *ed.host().viewport();
2810 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2811 ed.push_buffer_cursor_to_textarea();
2812 }
2813 Motion::LastNonBlank => {
2814 crate::motions::move_last_non_blank(&mut ed.buffer);
2815 ed.push_buffer_cursor_to_textarea();
2816 }
2817 Motion::LineMiddle => {
2818 let row = ed.cursor().0;
2819 let line_chars = buf_line_chars(&ed.buffer, row);
2820 let target = line_chars / 2;
2823 ed.jump_cursor(row, target);
2824 }
2825 Motion::ParagraphPrev => {
2826 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2827 ed.push_buffer_cursor_to_textarea();
2828 }
2829 Motion::ParagraphNext => {
2830 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2831 ed.push_buffer_cursor_to_textarea();
2832 }
2833 Motion::SentencePrev => {
2834 for _ in 0..count.max(1) {
2835 if let Some((row, col)) = sentence_boundary(ed, false) {
2836 ed.jump_cursor(row, col);
2837 }
2838 }
2839 }
2840 Motion::SentenceNext => {
2841 for _ in 0..count.max(1) {
2842 if let Some((row, col)) = sentence_boundary(ed, true) {
2843 ed.jump_cursor(row, col);
2844 }
2845 }
2846 }
2847 }
2848}
2849
2850fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2851 ed.sync_buffer_content_from_textarea();
2857 crate::motions::move_first_non_blank(&mut ed.buffer);
2858 ed.push_buffer_cursor_to_textarea();
2859}
2860
2861fn find_char_on_line<H: crate::types::Host>(
2862 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2863 ch: char,
2864 forward: bool,
2865 till: bool,
2866) -> bool {
2867 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2868 if moved {
2869 ed.push_buffer_cursor_to_textarea();
2870 }
2871 moved
2872}
2873
2874fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2875 let moved = crate::motions::match_bracket(&mut ed.buffer);
2876 if moved {
2877 ed.push_buffer_cursor_to_textarea();
2878 }
2879 moved
2880}
2881
2882fn word_at_cursor_search<H: crate::types::Host>(
2883 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2884 forward: bool,
2885 whole_word: bool,
2886 count: usize,
2887) {
2888 let (row, col) = ed.cursor();
2889 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2890 let chars: Vec<char> = line.chars().collect();
2891 if chars.is_empty() {
2892 return;
2893 }
2894 let spec = ed.settings().iskeyword.clone();
2896 let is_word = |c: char| is_keyword_char(c, &spec);
2897 let mut start = col.min(chars.len().saturating_sub(1));
2898 while start > 0 && is_word(chars[start - 1]) {
2899 start -= 1;
2900 }
2901 let mut end = start;
2902 while end < chars.len() && is_word(chars[end]) {
2903 end += 1;
2904 }
2905 if end <= start {
2906 return;
2907 }
2908 let word: String = chars[start..end].iter().collect();
2909 let escaped = regex_escape(&word);
2910 let pattern = if whole_word {
2911 format!(r"\b{escaped}\b")
2912 } else {
2913 escaped
2914 };
2915 ed.push_search_pattern(&pattern);
2916 if ed.search_state().pattern.is_none() {
2917 return;
2918 }
2919 ed.vim.last_search = Some(pattern);
2921 ed.vim.last_search_forward = forward;
2922 for _ in 0..count.max(1) {
2923 if forward {
2924 ed.search_advance_forward(true);
2925 } else {
2926 ed.search_advance_backward(true);
2927 }
2928 }
2929 ed.push_buffer_cursor_to_textarea();
2930}
2931
2932fn regex_escape(s: &str) -> String {
2933 let mut out = String::with_capacity(s.len());
2934 for c in s.chars() {
2935 if matches!(
2936 c,
2937 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2938 ) {
2939 out.push('\\');
2940 }
2941 out.push(c);
2942 }
2943 out
2944}
2945
2946pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2960 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2961 op: Operator,
2962 motion_key: char,
2963 total_count: usize,
2964) {
2965 let input = Input {
2966 key: Key::Char(motion_key),
2967 ctrl: false,
2968 alt: false,
2969 shift: false,
2970 };
2971 let Some(motion) = parse_motion(&input) else {
2972 return;
2973 };
2974 let motion = match motion {
2975 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2976 Some((ch, forward, till)) => Motion::Find {
2977 ch,
2978 forward: if reverse { !forward } else { forward },
2979 till,
2980 },
2981 None => return,
2982 },
2983 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2985 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2986 m => m,
2987 };
2988 apply_op_with_motion(ed, op, &motion, total_count);
2989 if let Motion::Find { ch, forward, till } = &motion {
2990 ed.vim.last_find = Some((*ch, *forward, *till));
2991 }
2992 if !ed.vim.replaying && op_is_change(op) {
2993 ed.vim.last_change = Some(LastChange::OpMotion {
2994 op,
2995 motion,
2996 count: total_count,
2997 inserted: None,
2998 });
2999 }
3000}
3001
3002pub(crate) fn apply_op_double<H: crate::types::Host>(
3005 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3006 op: Operator,
3007 total_count: usize,
3008) {
3009 execute_line_op(ed, op, total_count);
3010 if !ed.vim.replaying {
3011 ed.vim.last_change = Some(LastChange::LineOp {
3012 op,
3013 count: total_count,
3014 inserted: None,
3015 });
3016 }
3017}
3018
3019pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3029 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3030 op: Operator,
3031 ch: char,
3032 total_count: usize,
3033) {
3034 if matches!(
3037 op,
3038 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3039 ) {
3040 let op_char = match op {
3041 Operator::Uppercase => 'U',
3042 Operator::Lowercase => 'u',
3043 Operator::ToggleCase => '~',
3044 _ => unreachable!(),
3045 };
3046 if ch == op_char {
3047 execute_line_op(ed, op, total_count);
3048 if !ed.vim.replaying {
3049 ed.vim.last_change = Some(LastChange::LineOp {
3050 op,
3051 count: total_count,
3052 inserted: None,
3053 });
3054 }
3055 return;
3056 }
3057 }
3058 let motion = match ch {
3059 'g' => Motion::FileTop,
3060 'e' => Motion::WordEndBack,
3061 'E' => Motion::BigWordEndBack,
3062 'j' => Motion::ScreenDown,
3063 'k' => Motion::ScreenUp,
3064 _ => return, };
3066 apply_op_with_motion(ed, op, &motion, total_count);
3067 if !ed.vim.replaying && op_is_change(op) {
3068 ed.vim.last_change = Some(LastChange::OpMotion {
3069 op,
3070 motion,
3071 count: total_count,
3072 inserted: None,
3073 });
3074 }
3075}
3076
3077pub(crate) fn apply_after_g<H: crate::types::Host>(
3082 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3083 ch: char,
3084 count: usize,
3085) {
3086 match ch {
3087 'g' => {
3088 let pre = ed.cursor();
3090 if count > 1 {
3091 ed.jump_cursor(count - 1, 0);
3092 } else {
3093 ed.jump_cursor(0, 0);
3094 }
3095 move_first_non_whitespace(ed);
3096 ed.sticky_col = Some(ed.cursor().1);
3099 if ed.cursor() != pre {
3100 ed.push_jump(pre);
3101 }
3102 }
3103 'e' => execute_motion(ed, Motion::WordEndBack, count),
3104 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3105 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3107 'M' => execute_motion(ed, Motion::LineMiddle, count),
3109 'v' => ed.reenter_last_visual(),
3112 'j' => execute_motion(ed, Motion::ScreenDown, count),
3116 'k' => execute_motion(ed, Motion::ScreenUp, count),
3117 'U' => {
3121 ed.vim.pending = Pending::Op {
3122 op: Operator::Uppercase,
3123 count1: count,
3124 };
3125 }
3126 'u' => {
3127 ed.vim.pending = Pending::Op {
3128 op: Operator::Lowercase,
3129 count1: count,
3130 };
3131 }
3132 '~' => {
3133 ed.vim.pending = Pending::Op {
3134 op: Operator::ToggleCase,
3135 count1: count,
3136 };
3137 }
3138 'q' => {
3139 ed.vim.pending = Pending::Op {
3142 op: Operator::Reflow,
3143 count1: count,
3144 };
3145 }
3146 'J' => {
3147 for _ in 0..count.max(1) {
3149 ed.push_undo();
3150 join_line_raw(ed);
3151 }
3152 if !ed.vim.replaying {
3153 ed.vim.last_change = Some(LastChange::JoinLine {
3154 count: count.max(1),
3155 });
3156 }
3157 }
3158 'd' => {
3159 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3164 }
3165 'i' => {
3170 if let Some((row, col)) = ed.vim.last_insert_pos {
3171 ed.jump_cursor(row, col);
3172 }
3173 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3174 }
3175 ';' => walk_change_list(ed, -1, count.max(1)),
3178 ',' => walk_change_list(ed, 1, count.max(1)),
3179 '*' => execute_motion(
3183 ed,
3184 Motion::WordAtCursor {
3185 forward: true,
3186 whole_word: false,
3187 },
3188 count,
3189 ),
3190 '#' => execute_motion(
3191 ed,
3192 Motion::WordAtCursor {
3193 forward: false,
3194 whole_word: false,
3195 },
3196 count,
3197 ),
3198 _ => {}
3199 }
3200}
3201
3202pub(crate) fn apply_after_z<H: crate::types::Host>(
3207 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3208 ch: char,
3209 count: usize,
3210) {
3211 use crate::editor::CursorScrollTarget;
3212 let row = ed.cursor().0;
3213 match ch {
3214 'z' => {
3215 ed.scroll_cursor_to(CursorScrollTarget::Center);
3216 ed.vim.viewport_pinned = true;
3217 }
3218 't' => {
3219 ed.scroll_cursor_to(CursorScrollTarget::Top);
3220 ed.vim.viewport_pinned = true;
3221 }
3222 'b' => {
3223 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3224 ed.vim.viewport_pinned = true;
3225 }
3226 'o' => {
3231 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3232 }
3233 'c' => {
3234 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3235 }
3236 'a' => {
3237 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3238 }
3239 'R' => {
3240 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3241 }
3242 'M' => {
3243 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3244 }
3245 'E' => {
3246 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3247 }
3248 'd' => {
3249 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3250 }
3251 'f' => {
3252 if matches!(
3253 ed.vim.mode,
3254 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3255 ) {
3256 let anchor_row = match ed.vim.mode {
3259 Mode::VisualLine => ed.vim.visual_line_anchor,
3260 Mode::VisualBlock => ed.vim.block_anchor.0,
3261 _ => ed.vim.visual_anchor.0,
3262 };
3263 let cur = ed.cursor().0;
3264 let top = anchor_row.min(cur);
3265 let bot = anchor_row.max(cur);
3266 ed.apply_fold_op(crate::types::FoldOp::Add {
3267 start_row: top,
3268 end_row: bot,
3269 closed: true,
3270 });
3271 ed.vim.mode = Mode::Normal;
3272 } else {
3273 ed.vim.pending = Pending::Op {
3278 op: Operator::Fold,
3279 count1: count,
3280 };
3281 }
3282 }
3283 _ => {}
3284 }
3285}
3286
3287pub(crate) fn apply_find_char<H: crate::types::Host>(
3293 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3294 ch: char,
3295 forward: bool,
3296 till: bool,
3297 count: usize,
3298) {
3299 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3300 ed.vim.last_find = Some((ch, forward, till));
3301}
3302
3303pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3309 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3310 op: Operator,
3311 ch: char,
3312 forward: bool,
3313 till: bool,
3314 total_count: usize,
3315) {
3316 let motion = Motion::Find { ch, forward, till };
3317 apply_op_with_motion(ed, op, &motion, total_count);
3318 ed.vim.last_find = Some((ch, forward, till));
3319 if !ed.vim.replaying && op_is_change(op) {
3320 ed.vim.last_change = Some(LastChange::OpMotion {
3321 op,
3322 motion,
3323 count: total_count,
3324 inserted: None,
3325 });
3326 }
3327}
3328
3329pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3338 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3339 op: Operator,
3340 ch: char,
3341 inner: bool,
3342 _total_count: usize,
3343) -> bool {
3344 let obj = match ch {
3347 'w' => TextObject::Word { big: false },
3348 'W' => TextObject::Word { big: true },
3349 '"' | '\'' | '`' => TextObject::Quote(ch),
3350 '(' | ')' | 'b' => TextObject::Bracket('('),
3351 '[' | ']' => TextObject::Bracket('['),
3352 '{' | '}' | 'B' => TextObject::Bracket('{'),
3353 '<' | '>' => TextObject::Bracket('<'),
3354 'p' => TextObject::Paragraph,
3355 't' => TextObject::XmlTag,
3356 's' => TextObject::Sentence,
3357 _ => return false,
3358 };
3359 apply_op_with_text_object(ed, op, obj, inner);
3360 if !ed.vim.replaying && op_is_change(op) {
3361 ed.vim.last_change = Some(LastChange::OpTextObj {
3362 op,
3363 obj,
3364 inner,
3365 inserted: None,
3366 });
3367 }
3368 true
3369}
3370
3371pub(crate) fn retreat_one<H: crate::types::Host>(
3373 ed: &Editor<hjkl_buffer::Buffer, H>,
3374 pos: (usize, usize),
3375) -> (usize, usize) {
3376 let (r, c) = pos;
3377 if c > 0 {
3378 (r, c - 1)
3379 } else if r > 0 {
3380 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3381 (r - 1, prev_len)
3382 } else {
3383 (0, 0)
3384 }
3385}
3386
3387fn begin_insert_noundo<H: crate::types::Host>(
3389 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3390 count: usize,
3391 reason: InsertReason,
3392) {
3393 let reason = if ed.vim.replaying {
3394 InsertReason::ReplayOnly
3395 } else {
3396 reason
3397 };
3398 let (row, _) = ed.cursor();
3399 ed.vim.insert_session = Some(InsertSession {
3400 count,
3401 row_min: row,
3402 row_max: row,
3403 before_lines: buf_lines_to_vec(&ed.buffer),
3404 reason,
3405 });
3406 ed.vim.mode = Mode::Insert;
3407 ed.vim.current_mode = crate::VimMode::Insert;
3409}
3410
3411pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
3414 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3415 op: Operator,
3416 motion: &Motion,
3417 count: usize,
3418) {
3419 let start = ed.cursor();
3420 apply_motion_cursor_ctx(ed, motion, count, true);
3425 let end = ed.cursor();
3426 let kind = motion_kind(motion);
3427 ed.jump_cursor(start.0, start.1);
3429 run_operator_over_range(ed, op, start, end, kind);
3430}
3431
3432fn apply_op_with_text_object<H: crate::types::Host>(
3433 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3434 op: Operator,
3435 obj: TextObject,
3436 inner: bool,
3437) {
3438 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3439 return;
3440 };
3441 ed.jump_cursor(start.0, start.1);
3442 run_operator_over_range(ed, op, start, end, kind);
3443}
3444
3445fn motion_kind(motion: &Motion) -> RangeKind {
3446 match motion {
3447 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
3448 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
3449 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3450 RangeKind::Linewise
3451 }
3452 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3453 RangeKind::Inclusive
3454 }
3455 Motion::Find { .. } => RangeKind::Inclusive,
3456 Motion::MatchBracket => RangeKind::Inclusive,
3457 Motion::LineEnd => RangeKind::Inclusive,
3459 _ => RangeKind::Exclusive,
3460 }
3461}
3462
3463fn run_operator_over_range<H: crate::types::Host>(
3464 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3465 op: Operator,
3466 start: (usize, usize),
3467 end: (usize, usize),
3468 kind: RangeKind,
3469) {
3470 let (top, bot) = order(start, end);
3471 if top == bot && !matches!(kind, RangeKind::Linewise) {
3475 return;
3476 }
3477
3478 match op {
3479 Operator::Yank => {
3480 let text = read_vim_range(ed, top, bot, kind);
3481 if !text.is_empty() {
3482 ed.record_yank_to_host(text.clone());
3483 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
3484 }
3485 let rbr = match kind {
3489 RangeKind::Linewise => {
3490 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3491 (bot.0, last_col)
3492 }
3493 RangeKind::Inclusive => (bot.0, bot.1),
3494 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3495 };
3496 ed.set_mark('[', top);
3497 ed.set_mark(']', rbr);
3498 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3499 ed.push_buffer_cursor_to_textarea();
3500 }
3501 Operator::Delete => {
3502 ed.push_undo();
3503 cut_vim_range(ed, top, bot, kind);
3504 if !matches!(kind, RangeKind::Linewise) {
3509 clamp_cursor_to_normal_mode(ed);
3510 }
3511 ed.vim.mode = Mode::Normal;
3512 let pos = ed.cursor();
3516 ed.set_mark('[', pos);
3517 ed.set_mark(']', pos);
3518 }
3519 Operator::Change => {
3520 ed.vim.change_mark_start = Some(top);
3525 ed.push_undo();
3526 cut_vim_range(ed, top, bot, kind);
3527 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3528 }
3529 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3530 apply_case_op_to_selection(ed, op, top, bot, kind);
3531 }
3532 Operator::Indent | Operator::Outdent => {
3533 ed.push_undo();
3536 if op == Operator::Indent {
3537 indent_rows(ed, top.0, bot.0, 1);
3538 } else {
3539 outdent_rows(ed, top.0, bot.0, 1);
3540 }
3541 ed.vim.mode = Mode::Normal;
3542 }
3543 Operator::Fold => {
3544 if bot.0 >= top.0 {
3548 ed.apply_fold_op(crate::types::FoldOp::Add {
3549 start_row: top.0,
3550 end_row: bot.0,
3551 closed: true,
3552 });
3553 }
3554 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3555 ed.push_buffer_cursor_to_textarea();
3556 ed.vim.mode = Mode::Normal;
3557 }
3558 Operator::Reflow => {
3559 ed.push_undo();
3560 reflow_rows(ed, top.0, bot.0);
3561 ed.vim.mode = Mode::Normal;
3562 }
3563 Operator::AutoIndent => {
3564 ed.push_undo();
3566 auto_indent_rows(ed, top.0, bot.0);
3567 ed.vim.mode = Mode::Normal;
3568 }
3569 }
3570}
3571
3572pub(crate) fn delete_range_bridge<H: crate::types::Host>(
3589 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3590 start: (usize, usize),
3591 end: (usize, usize),
3592 kind: RangeKind,
3593 register: char,
3594) {
3595 ed.vim.pending_register = Some(register);
3596 run_operator_over_range(ed, Operator::Delete, start, end, kind);
3597}
3598
3599pub(crate) fn yank_range_bridge<H: crate::types::Host>(
3602 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3603 start: (usize, usize),
3604 end: (usize, usize),
3605 kind: RangeKind,
3606 register: char,
3607) {
3608 ed.vim.pending_register = Some(register);
3609 run_operator_over_range(ed, Operator::Yank, start, end, kind);
3610}
3611
3612pub(crate) fn change_range_bridge<H: crate::types::Host>(
3617 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3618 start: (usize, usize),
3619 end: (usize, usize),
3620 kind: RangeKind,
3621 register: char,
3622) {
3623 ed.vim.pending_register = Some(register);
3624 run_operator_over_range(ed, Operator::Change, start, end, kind);
3625}
3626
3627pub(crate) fn indent_range_bridge<H: crate::types::Host>(
3632 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3633 start: (usize, usize),
3634 end: (usize, usize),
3635 count: i32,
3636 shiftwidth: u32,
3637) {
3638 if count == 0 {
3639 return;
3640 }
3641 let (top_row, bot_row) = if start.0 <= end.0 {
3642 (start.0, end.0)
3643 } else {
3644 (end.0, start.0)
3645 };
3646 let original_sw = ed.settings().shiftwidth;
3648 if shiftwidth > 0 {
3649 ed.settings_mut().shiftwidth = shiftwidth as usize;
3650 }
3651 ed.push_undo();
3652 let abs_count = count.unsigned_abs() as usize;
3653 if count > 0 {
3654 indent_rows(ed, top_row, bot_row, abs_count);
3655 } else {
3656 outdent_rows(ed, top_row, bot_row, abs_count);
3657 }
3658 if shiftwidth > 0 {
3659 ed.settings_mut().shiftwidth = original_sw;
3660 }
3661 ed.vim.mode = Mode::Normal;
3662}
3663
3664pub(crate) fn case_range_bridge<H: crate::types::Host>(
3668 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3669 start: (usize, usize),
3670 end: (usize, usize),
3671 kind: RangeKind,
3672 op: Operator,
3673) {
3674 match op {
3675 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
3676 _ => return,
3677 }
3678 let (top, bot) = order(start, end);
3679 apply_case_op_to_selection(ed, op, top, bot, kind);
3680}
3681
3682pub(crate) fn delete_block_bridge<H: crate::types::Host>(
3703 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3704 top_row: usize,
3705 bot_row: usize,
3706 left_col: usize,
3707 right_col: usize,
3708 register: char,
3709) {
3710 ed.vim.pending_register = Some(register);
3711 let saved_anchor = ed.vim.block_anchor;
3712 let saved_vcol = ed.vim.block_vcol;
3713 ed.vim.block_anchor = (top_row, left_col);
3714 ed.vim.block_vcol = right_col;
3715 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3717 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3719 apply_block_operator(ed, Operator::Delete);
3720 ed.vim.block_anchor = saved_anchor;
3724 ed.vim.block_vcol = saved_vcol;
3725}
3726
3727pub(crate) fn yank_block_bridge<H: crate::types::Host>(
3729 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3730 top_row: usize,
3731 bot_row: usize,
3732 left_col: usize,
3733 right_col: usize,
3734 register: char,
3735) {
3736 ed.vim.pending_register = Some(register);
3737 let saved_anchor = ed.vim.block_anchor;
3738 let saved_vcol = ed.vim.block_vcol;
3739 ed.vim.block_anchor = (top_row, left_col);
3740 ed.vim.block_vcol = right_col;
3741 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3742 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3743 apply_block_operator(ed, Operator::Yank);
3744 ed.vim.block_anchor = saved_anchor;
3745 ed.vim.block_vcol = saved_vcol;
3746}
3747
3748pub(crate) fn change_block_bridge<H: crate::types::Host>(
3751 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3752 top_row: usize,
3753 bot_row: usize,
3754 left_col: usize,
3755 right_col: usize,
3756 register: char,
3757) {
3758 ed.vim.pending_register = Some(register);
3759 let saved_anchor = ed.vim.block_anchor;
3760 let saved_vcol = ed.vim.block_vcol;
3761 ed.vim.block_anchor = (top_row, left_col);
3762 ed.vim.block_vcol = right_col;
3763 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3764 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3765 apply_block_operator(ed, Operator::Change);
3766 ed.vim.block_anchor = saved_anchor;
3767 ed.vim.block_vcol = saved_vcol;
3768}
3769
3770pub(crate) fn indent_block_bridge<H: crate::types::Host>(
3774 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3775 top_row: usize,
3776 bot_row: usize,
3777 count: i32,
3778) {
3779 if count == 0 {
3780 return;
3781 }
3782 ed.push_undo();
3783 let abs = count.unsigned_abs() as usize;
3784 if count > 0 {
3785 indent_rows(ed, top_row, bot_row, abs);
3786 } else {
3787 outdent_rows(ed, top_row, bot_row, abs);
3788 }
3789 ed.vim.mode = Mode::Normal;
3790}
3791
3792pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
3796 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3797 start: (usize, usize),
3798 end: (usize, usize),
3799) {
3800 let (top_row, bot_row) = if start.0 <= end.0 {
3801 (start.0, end.0)
3802 } else {
3803 (end.0, start.0)
3804 };
3805 ed.push_undo();
3806 auto_indent_rows(ed, top_row, bot_row);
3807 ed.vim.mode = Mode::Normal;
3808}
3809
3810pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
3821 ed: &Editor<hjkl_buffer::Buffer, H>,
3822) -> Option<((usize, usize), (usize, usize))> {
3823 word_text_object(ed, true, false)
3824}
3825
3826pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
3829 ed: &Editor<hjkl_buffer::Buffer, H>,
3830) -> Option<((usize, usize), (usize, usize))> {
3831 word_text_object(ed, false, false)
3832}
3833
3834pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
3837 ed: &Editor<hjkl_buffer::Buffer, H>,
3838) -> Option<((usize, usize), (usize, usize))> {
3839 word_text_object(ed, true, true)
3840}
3841
3842pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
3845 ed: &Editor<hjkl_buffer::Buffer, H>,
3846) -> Option<((usize, usize), (usize, usize))> {
3847 word_text_object(ed, false, true)
3848}
3849
3850pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
3866 ed: &Editor<hjkl_buffer::Buffer, H>,
3867 quote: char,
3868) -> Option<((usize, usize), (usize, usize))> {
3869 quote_text_object(ed, quote, true)
3870}
3871
3872pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
3875 ed: &Editor<hjkl_buffer::Buffer, H>,
3876 quote: char,
3877) -> Option<((usize, usize), (usize, usize))> {
3878 quote_text_object(ed, quote, false)
3879}
3880
3881pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
3889 ed: &Editor<hjkl_buffer::Buffer, H>,
3890 open: char,
3891) -> Option<((usize, usize), (usize, usize))> {
3892 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
3893}
3894
3895pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
3899 ed: &Editor<hjkl_buffer::Buffer, H>,
3900 open: char,
3901) -> Option<((usize, usize), (usize, usize))> {
3902 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
3903}
3904
3905pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
3910 ed: &Editor<hjkl_buffer::Buffer, H>,
3911) -> Option<((usize, usize), (usize, usize))> {
3912 sentence_text_object(ed, true)
3913}
3914
3915pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
3918 ed: &Editor<hjkl_buffer::Buffer, H>,
3919) -> Option<((usize, usize), (usize, usize))> {
3920 sentence_text_object(ed, false)
3921}
3922
3923pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
3928 ed: &Editor<hjkl_buffer::Buffer, H>,
3929) -> Option<((usize, usize), (usize, usize))> {
3930 paragraph_text_object(ed, true)
3931}
3932
3933pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
3936 ed: &Editor<hjkl_buffer::Buffer, H>,
3937) -> Option<((usize, usize), (usize, usize))> {
3938 paragraph_text_object(ed, false)
3939}
3940
3941pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
3947 ed: &Editor<hjkl_buffer::Buffer, H>,
3948) -> Option<((usize, usize), (usize, usize))> {
3949 tag_text_object(ed, true)
3950}
3951
3952pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
3955 ed: &Editor<hjkl_buffer::Buffer, H>,
3956) -> Option<((usize, usize), (usize, usize))> {
3957 tag_text_object(ed, false)
3958}
3959
3960fn reflow_rows<H: crate::types::Host>(
3965 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3966 top: usize,
3967 bot: usize,
3968) {
3969 let width = ed.settings().textwidth.max(1);
3970 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3971 let bot = bot.min(lines.len().saturating_sub(1));
3972 if top > bot {
3973 return;
3974 }
3975 let original = lines[top..=bot].to_vec();
3976 let mut wrapped: Vec<String> = Vec::new();
3977 let mut paragraph: Vec<String> = Vec::new();
3978 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3979 if para.is_empty() {
3980 return;
3981 }
3982 let words = para.join(" ");
3983 let mut current = String::new();
3984 for word in words.split_whitespace() {
3985 let extra = if current.is_empty() {
3986 word.chars().count()
3987 } else {
3988 current.chars().count() + 1 + word.chars().count()
3989 };
3990 if extra > width && !current.is_empty() {
3991 out.push(std::mem::take(&mut current));
3992 current.push_str(word);
3993 } else if current.is_empty() {
3994 current.push_str(word);
3995 } else {
3996 current.push(' ');
3997 current.push_str(word);
3998 }
3999 }
4000 if !current.is_empty() {
4001 out.push(current);
4002 }
4003 para.clear();
4004 };
4005 for line in &original {
4006 if line.trim().is_empty() {
4007 flush(&mut paragraph, &mut wrapped, width);
4008 wrapped.push(String::new());
4009 } else {
4010 paragraph.push(line.clone());
4011 }
4012 }
4013 flush(&mut paragraph, &mut wrapped, width);
4014
4015 let after: Vec<String> = lines.split_off(bot + 1);
4017 lines.truncate(top);
4018 lines.extend(wrapped);
4019 lines.extend(after);
4020 ed.restore(lines, (top, 0));
4021 ed.mark_content_dirty();
4022}
4023
4024fn apply_case_op_to_selection<H: crate::types::Host>(
4030 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4031 op: Operator,
4032 top: (usize, usize),
4033 bot: (usize, usize),
4034 kind: RangeKind,
4035) {
4036 use hjkl_buffer::Edit;
4037 ed.push_undo();
4038 let saved_yank = ed.yank().to_string();
4039 let saved_yank_linewise = ed.vim.yank_linewise;
4040 let selection = cut_vim_range(ed, top, bot, kind);
4041 let transformed = match op {
4042 Operator::Uppercase => selection.to_uppercase(),
4043 Operator::Lowercase => selection.to_lowercase(),
4044 Operator::ToggleCase => toggle_case_str(&selection),
4045 _ => unreachable!(),
4046 };
4047 if !transformed.is_empty() {
4048 let cursor = buf_cursor_pos(&ed.buffer);
4049 ed.mutate_edit(Edit::InsertStr {
4050 at: cursor,
4051 text: transformed,
4052 });
4053 }
4054 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4055 ed.push_buffer_cursor_to_textarea();
4056 ed.set_yank(saved_yank);
4057 ed.vim.yank_linewise = saved_yank_linewise;
4058 ed.vim.mode = Mode::Normal;
4059}
4060
4061fn indent_rows<H: crate::types::Host>(
4066 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4067 top: usize,
4068 bot: usize,
4069 count: usize,
4070) {
4071 ed.sync_buffer_content_from_textarea();
4072 let width = ed.settings().shiftwidth * count.max(1);
4073 let pad: String = " ".repeat(width);
4074 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4075 let bot = bot.min(lines.len().saturating_sub(1));
4076 for line in lines.iter_mut().take(bot + 1).skip(top) {
4077 if !line.is_empty() {
4078 line.insert_str(0, &pad);
4079 }
4080 }
4081 ed.restore(lines, (top, 0));
4084 move_first_non_whitespace(ed);
4085}
4086
4087fn outdent_rows<H: crate::types::Host>(
4091 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4092 top: usize,
4093 bot: usize,
4094 count: usize,
4095) {
4096 ed.sync_buffer_content_from_textarea();
4097 let width = ed.settings().shiftwidth * count.max(1);
4098 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4099 let bot = bot.min(lines.len().saturating_sub(1));
4100 for line in lines.iter_mut().take(bot + 1).skip(top) {
4101 let strip: usize = line
4102 .chars()
4103 .take(width)
4104 .take_while(|c| *c == ' ' || *c == '\t')
4105 .count();
4106 if strip > 0 {
4107 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4108 line.drain(..byte_len);
4109 }
4110 }
4111 ed.restore(lines, (top, 0));
4112 move_first_non_whitespace(ed);
4113}
4114
4115fn bracket_net(line: &str) -> i32 {
4142 let mut net: i32 = 0;
4143 let mut chars = line.chars().peekable();
4144 while let Some(ch) = chars.next() {
4145 match ch {
4146 '/' if chars.peek() == Some(&'/') => return net,
4148 '"' => {
4149 while let Some(c) = chars.next() {
4151 match c {
4152 '\\' => {
4153 chars.next();
4154 } '"' => break,
4156 _ => {}
4157 }
4158 }
4159 }
4160 '\'' => {
4161 let saved: Vec<char> = chars.clone().take(5).collect();
4170 let close_idx = if saved.first() == Some(&'\\') {
4171 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
4172 } else {
4173 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
4174 };
4175 if let Some(idx) = close_idx {
4176 for _ in 0..=idx {
4177 chars.next();
4178 }
4179 }
4180 }
4182 '{' | '(' | '[' => net += 1,
4183 '}' | ')' | ']' => net -= 1,
4184 _ => {}
4185 }
4186 }
4187 net
4188}
4189
4190fn auto_indent_rows<H: crate::types::Host>(
4212 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4213 top: usize,
4214 bot: usize,
4215) {
4216 ed.sync_buffer_content_from_textarea();
4217 let shiftwidth = ed.settings().shiftwidth;
4218 let expandtab = ed.settings().expandtab;
4219 let indent_unit: String = if expandtab {
4220 " ".repeat(shiftwidth)
4221 } else {
4222 "\t".to_string()
4223 };
4224
4225 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4226 let bot = bot.min(lines.len().saturating_sub(1));
4227
4228 let mut depth: i32 = 0;
4231 for line in lines.iter().take(top) {
4232 depth += bracket_net(line);
4233 if depth < 0 {
4234 depth = 0;
4235 }
4236 }
4237
4238 for line in lines.iter_mut().take(bot + 1).skip(top) {
4239 let trimmed_owned = line.trim_start().to_owned();
4240 if trimmed_owned.is_empty() {
4242 *line = String::new();
4243 continue;
4245 }
4246
4247 let starts_with_close = trimmed_owned
4249 .chars()
4250 .next()
4251 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
4252 let starts_with_dot = trimmed_owned.starts_with('.')
4262 && !trimmed_owned.starts_with("..")
4263 && !trimmed_owned.starts_with(".;");
4264 let effective_depth = if starts_with_close {
4265 depth.saturating_sub(1)
4266 } else if starts_with_dot {
4267 depth.saturating_add(1)
4268 } else {
4269 depth
4270 } as usize;
4271
4272 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
4274
4275 depth += bracket_net(&trimmed_owned);
4277 if depth < 0 {
4278 depth = 0;
4279 }
4280
4281 *line = new_line;
4282 }
4283
4284 ed.restore(lines, (top, 0));
4286 move_first_non_whitespace(ed);
4287 ed.last_indent_range = Some((top, bot));
4289}
4290
4291fn toggle_case_str(s: &str) -> String {
4292 s.chars()
4293 .map(|c| {
4294 if c.is_lowercase() {
4295 c.to_uppercase().next().unwrap_or(c)
4296 } else if c.is_uppercase() {
4297 c.to_lowercase().next().unwrap_or(c)
4298 } else {
4299 c
4300 }
4301 })
4302 .collect()
4303}
4304
4305fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4306 if a <= b { (a, b) } else { (b, a) }
4307}
4308
4309fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4314 let (row, col) = ed.cursor();
4315 let line_chars = buf_line_chars(&ed.buffer, row);
4316 let max_col = line_chars.saturating_sub(1);
4317 if col > max_col {
4318 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4319 ed.push_buffer_cursor_to_textarea();
4320 }
4321}
4322
4323fn execute_line_op<H: crate::types::Host>(
4326 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4327 op: Operator,
4328 count: usize,
4329) {
4330 let (row, col) = ed.cursor();
4331 let total = buf_row_count(&ed.buffer);
4332 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4333
4334 match op {
4335 Operator::Yank => {
4336 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4338 if !text.is_empty() {
4339 ed.record_yank_to_host(text.clone());
4340 ed.record_yank(text, true);
4341 }
4342 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4345 ed.set_mark('[', (row, 0));
4346 ed.set_mark(']', (end_row, last_col));
4347 buf_set_cursor_rc(&mut ed.buffer, row, col);
4348 ed.push_buffer_cursor_to_textarea();
4349 ed.vim.mode = Mode::Normal;
4350 }
4351 Operator::Delete => {
4352 ed.push_undo();
4353 let deleted_through_last = end_row + 1 >= total;
4354 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4355 let total_after = buf_row_count(&ed.buffer);
4359 let raw_target = if deleted_through_last {
4360 row.saturating_sub(1).min(total_after.saturating_sub(1))
4361 } else {
4362 row.min(total_after.saturating_sub(1))
4363 };
4364 let target_row = if raw_target > 0
4370 && raw_target + 1 == total_after
4371 && buf_line(&ed.buffer, raw_target)
4372 .map(str::is_empty)
4373 .unwrap_or(false)
4374 {
4375 raw_target - 1
4376 } else {
4377 raw_target
4378 };
4379 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4380 ed.push_buffer_cursor_to_textarea();
4381 move_first_non_whitespace(ed);
4382 ed.sticky_col = Some(ed.cursor().1);
4383 ed.vim.mode = Mode::Normal;
4384 let pos = ed.cursor();
4387 ed.set_mark('[', pos);
4388 ed.set_mark(']', pos);
4389 }
4390 Operator::Change => {
4391 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4395 ed.vim.change_mark_start = Some((row, 0));
4397 ed.push_undo();
4398 ed.sync_buffer_content_from_textarea();
4399 let payload = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4401 if end_row > row {
4402 ed.mutate_edit(Edit::DeleteRange {
4403 start: Position::new(row + 1, 0),
4404 end: Position::new(end_row, 0),
4405 kind: BufKind::Line,
4406 });
4407 }
4408 let line_chars = buf_line_chars(&ed.buffer, row);
4409 if line_chars > 0 {
4410 ed.mutate_edit(Edit::DeleteRange {
4411 start: Position::new(row, 0),
4412 end: Position::new(row, line_chars),
4413 kind: BufKind::Char,
4414 });
4415 }
4416 if !payload.is_empty() {
4417 ed.record_yank_to_host(payload.clone());
4418 ed.record_delete(payload, true);
4419 }
4420 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4421 ed.push_buffer_cursor_to_textarea();
4422 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4423 }
4424 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4425 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
4429 move_first_non_whitespace(ed);
4432 }
4433 Operator::Indent | Operator::Outdent => {
4434 ed.push_undo();
4436 if op == Operator::Indent {
4437 indent_rows(ed, row, end_row, 1);
4438 } else {
4439 outdent_rows(ed, row, end_row, 1);
4440 }
4441 ed.sticky_col = Some(ed.cursor().1);
4442 ed.vim.mode = Mode::Normal;
4443 }
4444 Operator::Fold => unreachable!("Fold has no line-op double"),
4446 Operator::Reflow => {
4447 ed.push_undo();
4449 reflow_rows(ed, row, end_row);
4450 move_first_non_whitespace(ed);
4451 ed.sticky_col = Some(ed.cursor().1);
4452 ed.vim.mode = Mode::Normal;
4453 }
4454 Operator::AutoIndent => {
4455 ed.push_undo();
4457 auto_indent_rows(ed, row, end_row);
4458 ed.sticky_col = Some(ed.cursor().1);
4459 ed.vim.mode = Mode::Normal;
4460 }
4461 }
4462}
4463
4464pub(crate) fn apply_visual_operator<H: crate::types::Host>(
4467 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4468 op: Operator,
4469) {
4470 match ed.vim.mode {
4471 Mode::VisualLine => {
4472 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4473 let top = cursor_row.min(ed.vim.visual_line_anchor);
4474 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4475 ed.vim.yank_linewise = true;
4476 match op {
4477 Operator::Yank => {
4478 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4479 if !text.is_empty() {
4480 ed.record_yank_to_host(text.clone());
4481 ed.record_yank(text, true);
4482 }
4483 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4484 ed.push_buffer_cursor_to_textarea();
4485 ed.vim.mode = Mode::Normal;
4486 }
4487 Operator::Delete => {
4488 ed.push_undo();
4489 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4490 ed.vim.mode = Mode::Normal;
4491 }
4492 Operator::Change => {
4493 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4496 ed.push_undo();
4497 ed.sync_buffer_content_from_textarea();
4498 let payload = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4499 if bot > top {
4500 ed.mutate_edit(Edit::DeleteRange {
4501 start: Position::new(top + 1, 0),
4502 end: Position::new(bot, 0),
4503 kind: BufKind::Line,
4504 });
4505 }
4506 let line_chars = buf_line_chars(&ed.buffer, top);
4507 if line_chars > 0 {
4508 ed.mutate_edit(Edit::DeleteRange {
4509 start: Position::new(top, 0),
4510 end: Position::new(top, line_chars),
4511 kind: BufKind::Char,
4512 });
4513 }
4514 if !payload.is_empty() {
4515 ed.record_yank_to_host(payload.clone());
4516 ed.record_delete(payload, true);
4517 }
4518 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4519 ed.push_buffer_cursor_to_textarea();
4520 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4521 }
4522 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4523 let bot = buf_cursor_pos(&ed.buffer)
4524 .row
4525 .max(ed.vim.visual_line_anchor);
4526 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
4527 move_first_non_whitespace(ed);
4528 }
4529 Operator::Indent | Operator::Outdent => {
4530 ed.push_undo();
4531 let (cursor_row, _) = ed.cursor();
4532 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4533 if op == Operator::Indent {
4534 indent_rows(ed, top, bot, 1);
4535 } else {
4536 outdent_rows(ed, top, bot, 1);
4537 }
4538 ed.vim.mode = Mode::Normal;
4539 }
4540 Operator::Reflow => {
4541 ed.push_undo();
4542 let (cursor_row, _) = ed.cursor();
4543 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4544 reflow_rows(ed, top, bot);
4545 ed.vim.mode = Mode::Normal;
4546 }
4547 Operator::AutoIndent => {
4548 ed.push_undo();
4549 let (cursor_row, _) = ed.cursor();
4550 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4551 auto_indent_rows(ed, top, bot);
4552 ed.vim.mode = Mode::Normal;
4553 }
4554 Operator::Fold => unreachable!("Visual zf takes its own path"),
4557 }
4558 }
4559 Mode::Visual => {
4560 ed.vim.yank_linewise = false;
4561 let anchor = ed.vim.visual_anchor;
4562 let cursor = ed.cursor();
4563 let (top, bot) = order(anchor, cursor);
4564 match op {
4565 Operator::Yank => {
4566 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
4567 if !text.is_empty() {
4568 ed.record_yank_to_host(text.clone());
4569 ed.record_yank(text, false);
4570 }
4571 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4572 ed.push_buffer_cursor_to_textarea();
4573 ed.vim.mode = Mode::Normal;
4574 }
4575 Operator::Delete => {
4576 ed.push_undo();
4577 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4578 ed.vim.mode = Mode::Normal;
4579 }
4580 Operator::Change => {
4581 ed.push_undo();
4582 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4583 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4584 }
4585 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4586 let anchor = ed.vim.visual_anchor;
4588 let cursor = ed.cursor();
4589 let (top, bot) = order(anchor, cursor);
4590 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
4591 }
4592 Operator::Indent | Operator::Outdent => {
4593 ed.push_undo();
4594 let anchor = ed.vim.visual_anchor;
4595 let cursor = ed.cursor();
4596 let (top, bot) = order(anchor, cursor);
4597 if op == Operator::Indent {
4598 indent_rows(ed, top.0, bot.0, 1);
4599 } else {
4600 outdent_rows(ed, top.0, bot.0, 1);
4601 }
4602 ed.vim.mode = Mode::Normal;
4603 }
4604 Operator::Reflow => {
4605 ed.push_undo();
4606 let anchor = ed.vim.visual_anchor;
4607 let cursor = ed.cursor();
4608 let (top, bot) = order(anchor, cursor);
4609 reflow_rows(ed, top.0, bot.0);
4610 ed.vim.mode = Mode::Normal;
4611 }
4612 Operator::AutoIndent => {
4613 ed.push_undo();
4614 let anchor = ed.vim.visual_anchor;
4615 let cursor = ed.cursor();
4616 let (top, bot) = order(anchor, cursor);
4617 auto_indent_rows(ed, top.0, bot.0);
4618 ed.vim.mode = Mode::Normal;
4619 }
4620 Operator::Fold => unreachable!("Visual zf takes its own path"),
4621 }
4622 }
4623 Mode::VisualBlock => apply_block_operator(ed, op),
4624 _ => {}
4625 }
4626}
4627
4628fn block_bounds<H: crate::types::Host>(
4633 ed: &Editor<hjkl_buffer::Buffer, H>,
4634) -> (usize, usize, usize, usize) {
4635 let (ar, ac) = ed.vim.block_anchor;
4636 let (cr, _) = ed.cursor();
4637 let cc = ed.vim.block_vcol;
4638 let top = ar.min(cr);
4639 let bot = ar.max(cr);
4640 let left = ac.min(cc);
4641 let right = ac.max(cc);
4642 (top, bot, left, right)
4643}
4644
4645pub(crate) fn update_block_vcol<H: crate::types::Host>(
4650 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4651 motion: &Motion,
4652) {
4653 match motion {
4654 Motion::Left
4655 | Motion::Right
4656 | Motion::WordFwd
4657 | Motion::BigWordFwd
4658 | Motion::WordBack
4659 | Motion::BigWordBack
4660 | Motion::WordEnd
4661 | Motion::BigWordEnd
4662 | Motion::WordEndBack
4663 | Motion::BigWordEndBack
4664 | Motion::LineStart
4665 | Motion::FirstNonBlank
4666 | Motion::LineEnd
4667 | Motion::Find { .. }
4668 | Motion::FindRepeat { .. }
4669 | Motion::MatchBracket => {
4670 ed.vim.block_vcol = ed.cursor().1;
4671 }
4672 _ => {}
4674 }
4675}
4676
4677fn apply_block_operator<H: crate::types::Host>(
4682 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4683 op: Operator,
4684) {
4685 let (top, bot, left, right) = block_bounds(ed);
4686 let yank = block_yank(ed, top, bot, left, right);
4688
4689 match op {
4690 Operator::Yank => {
4691 if !yank.is_empty() {
4692 ed.record_yank_to_host(yank.clone());
4693 ed.record_yank(yank, false);
4694 }
4695 ed.vim.mode = Mode::Normal;
4696 ed.jump_cursor(top, left);
4697 }
4698 Operator::Delete => {
4699 ed.push_undo();
4700 delete_block_contents(ed, top, bot, left, right);
4701 if !yank.is_empty() {
4702 ed.record_yank_to_host(yank.clone());
4703 ed.record_delete(yank, false);
4704 }
4705 ed.vim.mode = Mode::Normal;
4706 ed.jump_cursor(top, left);
4707 }
4708 Operator::Change => {
4709 ed.push_undo();
4710 delete_block_contents(ed, top, bot, left, right);
4711 if !yank.is_empty() {
4712 ed.record_yank_to_host(yank.clone());
4713 ed.record_delete(yank, false);
4714 }
4715 ed.jump_cursor(top, left);
4716 begin_insert_noundo(
4717 ed,
4718 1,
4719 InsertReason::BlockChange {
4720 top,
4721 bot,
4722 col: left,
4723 },
4724 );
4725 }
4726 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4727 ed.push_undo();
4728 transform_block_case(ed, op, top, bot, left, right);
4729 ed.vim.mode = Mode::Normal;
4730 ed.jump_cursor(top, left);
4731 }
4732 Operator::Indent | Operator::Outdent => {
4733 ed.push_undo();
4737 if op == Operator::Indent {
4738 indent_rows(ed, top, bot, 1);
4739 } else {
4740 outdent_rows(ed, top, bot, 1);
4741 }
4742 ed.vim.mode = Mode::Normal;
4743 }
4744 Operator::Fold => unreachable!("Visual zf takes its own path"),
4745 Operator::Reflow => {
4746 ed.push_undo();
4750 reflow_rows(ed, top, bot);
4751 ed.vim.mode = Mode::Normal;
4752 }
4753 Operator::AutoIndent => {
4754 ed.push_undo();
4757 auto_indent_rows(ed, top, bot);
4758 ed.vim.mode = Mode::Normal;
4759 }
4760 }
4761}
4762
4763fn transform_block_case<H: crate::types::Host>(
4767 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4768 op: Operator,
4769 top: usize,
4770 bot: usize,
4771 left: usize,
4772 right: usize,
4773) {
4774 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4775 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4776 let chars: Vec<char> = lines[r].chars().collect();
4777 if left >= chars.len() {
4778 continue;
4779 }
4780 let end = (right + 1).min(chars.len());
4781 let head: String = chars[..left].iter().collect();
4782 let mid: String = chars[left..end].iter().collect();
4783 let tail: String = chars[end..].iter().collect();
4784 let transformed = match op {
4785 Operator::Uppercase => mid.to_uppercase(),
4786 Operator::Lowercase => mid.to_lowercase(),
4787 Operator::ToggleCase => toggle_case_str(&mid),
4788 _ => mid,
4789 };
4790 lines[r] = format!("{head}{transformed}{tail}");
4791 }
4792 let saved_yank = ed.yank().to_string();
4793 let saved_linewise = ed.vim.yank_linewise;
4794 ed.restore(lines, (top, left));
4795 ed.set_yank(saved_yank);
4796 ed.vim.yank_linewise = saved_linewise;
4797}
4798
4799fn block_yank<H: crate::types::Host>(
4800 ed: &Editor<hjkl_buffer::Buffer, H>,
4801 top: usize,
4802 bot: usize,
4803 left: usize,
4804 right: usize,
4805) -> String {
4806 let lines = buf_lines_to_vec(&ed.buffer);
4807 let mut rows: Vec<String> = Vec::new();
4808 for r in top..=bot {
4809 let line = match lines.get(r) {
4810 Some(l) => l,
4811 None => break,
4812 };
4813 let chars: Vec<char> = line.chars().collect();
4814 let end = (right + 1).min(chars.len());
4815 if left >= chars.len() {
4816 rows.push(String::new());
4817 } else {
4818 rows.push(chars[left..end].iter().collect());
4819 }
4820 }
4821 rows.join("\n")
4822}
4823
4824fn delete_block_contents<H: crate::types::Host>(
4825 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4826 top: usize,
4827 bot: usize,
4828 left: usize,
4829 right: usize,
4830) {
4831 use hjkl_buffer::{Edit, MotionKind, Position};
4832 ed.sync_buffer_content_from_textarea();
4833 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4834 if last_row < top {
4835 return;
4836 }
4837 ed.mutate_edit(Edit::DeleteRange {
4838 start: Position::new(top, left),
4839 end: Position::new(last_row, right),
4840 kind: MotionKind::Block,
4841 });
4842 ed.push_buffer_cursor_to_textarea();
4843}
4844
4845pub(crate) fn block_replace<H: crate::types::Host>(
4847 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4848 ch: char,
4849) {
4850 let (top, bot, left, right) = block_bounds(ed);
4851 ed.push_undo();
4852 ed.sync_buffer_content_from_textarea();
4853 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4854 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4855 let chars: Vec<char> = lines[r].chars().collect();
4856 if left >= chars.len() {
4857 continue;
4858 }
4859 let end = (right + 1).min(chars.len());
4860 let before: String = chars[..left].iter().collect();
4861 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4862 let after: String = chars[end..].iter().collect();
4863 lines[r] = format!("{before}{middle}{after}");
4864 }
4865 reset_textarea_lines(ed, lines);
4866 ed.vim.mode = Mode::Normal;
4867 ed.jump_cursor(top, left);
4868}
4869
4870fn reset_textarea_lines<H: crate::types::Host>(
4874 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4875 lines: Vec<String>,
4876) {
4877 let cursor = ed.cursor();
4878 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4879 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4880 ed.mark_content_dirty();
4881}
4882
4883type Pos = (usize, usize);
4889
4890pub(crate) fn text_object_range<H: crate::types::Host>(
4894 ed: &Editor<hjkl_buffer::Buffer, H>,
4895 obj: TextObject,
4896 inner: bool,
4897) -> Option<(Pos, Pos, RangeKind)> {
4898 match obj {
4899 TextObject::Word { big } => {
4900 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
4901 }
4902 TextObject::Quote(q) => {
4903 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4904 }
4905 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4906 TextObject::Paragraph => {
4907 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
4908 }
4909 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
4910 TextObject::Sentence => {
4911 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4912 }
4913 }
4914}
4915
4916fn sentence_boundary<H: crate::types::Host>(
4920 ed: &Editor<hjkl_buffer::Buffer, H>,
4921 forward: bool,
4922) -> Option<(usize, usize)> {
4923 let lines = buf_lines_to_vec(&ed.buffer);
4924 if lines.is_empty() {
4925 return None;
4926 }
4927 let pos_to_idx = |pos: (usize, usize)| -> usize {
4928 let mut idx = 0;
4929 for line in lines.iter().take(pos.0) {
4930 idx += line.chars().count() + 1;
4931 }
4932 idx + pos.1
4933 };
4934 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4935 for (r, line) in lines.iter().enumerate() {
4936 let len = line.chars().count();
4937 if idx <= len {
4938 return (r, idx);
4939 }
4940 idx -= len + 1;
4941 }
4942 let last = lines.len().saturating_sub(1);
4943 (last, lines[last].chars().count())
4944 };
4945 let mut chars: Vec<char> = Vec::new();
4946 for (r, line) in lines.iter().enumerate() {
4947 chars.extend(line.chars());
4948 if r + 1 < lines.len() {
4949 chars.push('\n');
4950 }
4951 }
4952 if chars.is_empty() {
4953 return None;
4954 }
4955 let total = chars.len();
4956 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4957 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4958
4959 if forward {
4960 let mut i = cursor_idx + 1;
4963 while i < total {
4964 if is_terminator(chars[i]) {
4965 while i + 1 < total && is_terminator(chars[i + 1]) {
4966 i += 1;
4967 }
4968 if i + 1 >= total {
4969 return None;
4970 }
4971 if chars[i + 1].is_whitespace() {
4972 let mut j = i + 1;
4973 while j < total && chars[j].is_whitespace() {
4974 j += 1;
4975 }
4976 if j >= total {
4977 return None;
4978 }
4979 return Some(idx_to_pos(j));
4980 }
4981 }
4982 i += 1;
4983 }
4984 None
4985 } else {
4986 let find_start = |from: usize| -> Option<usize> {
4990 let mut start = from;
4991 while start > 0 {
4992 let prev = chars[start - 1];
4993 if prev.is_whitespace() {
4994 let mut k = start - 1;
4995 while k > 0 && chars[k - 1].is_whitespace() {
4996 k -= 1;
4997 }
4998 if k > 0 && is_terminator(chars[k - 1]) {
4999 break;
5000 }
5001 }
5002 start -= 1;
5003 }
5004 while start < total && chars[start].is_whitespace() {
5005 start += 1;
5006 }
5007 (start < total).then_some(start)
5008 };
5009 let current_start = find_start(cursor_idx)?;
5010 if current_start < cursor_idx {
5011 return Some(idx_to_pos(current_start));
5012 }
5013 let mut k = current_start;
5016 while k > 0 && chars[k - 1].is_whitespace() {
5017 k -= 1;
5018 }
5019 if k == 0 {
5020 return None;
5021 }
5022 let prev_start = find_start(k - 1)?;
5023 Some(idx_to_pos(prev_start))
5024 }
5025}
5026
5027fn sentence_text_object<H: crate::types::Host>(
5033 ed: &Editor<hjkl_buffer::Buffer, H>,
5034 inner: bool,
5035) -> Option<((usize, usize), (usize, usize))> {
5036 let lines = buf_lines_to_vec(&ed.buffer);
5037 if lines.is_empty() {
5038 return None;
5039 }
5040 let pos_to_idx = |pos: (usize, usize)| -> usize {
5043 let mut idx = 0;
5044 for line in lines.iter().take(pos.0) {
5045 idx += line.chars().count() + 1;
5046 }
5047 idx + pos.1
5048 };
5049 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5050 for (r, line) in lines.iter().enumerate() {
5051 let len = line.chars().count();
5052 if idx <= len {
5053 return (r, idx);
5054 }
5055 idx -= len + 1;
5056 }
5057 let last = lines.len().saturating_sub(1);
5058 (last, lines[last].chars().count())
5059 };
5060 let mut chars: Vec<char> = Vec::new();
5061 for (r, line) in lines.iter().enumerate() {
5062 chars.extend(line.chars());
5063 if r + 1 < lines.len() {
5064 chars.push('\n');
5065 }
5066 }
5067 if chars.is_empty() {
5068 return None;
5069 }
5070
5071 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
5072 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5073
5074 let mut start = cursor_idx;
5078 while start > 0 {
5079 let prev = chars[start - 1];
5080 if prev.is_whitespace() {
5081 let mut k = start - 1;
5085 while k > 0 && chars[k - 1].is_whitespace() {
5086 k -= 1;
5087 }
5088 if k > 0 && is_terminator(chars[k - 1]) {
5089 break;
5090 }
5091 }
5092 start -= 1;
5093 }
5094 while start < chars.len() && chars[start].is_whitespace() {
5097 start += 1;
5098 }
5099 if start >= chars.len() {
5100 return None;
5101 }
5102
5103 let mut end = start;
5106 while end < chars.len() {
5107 if is_terminator(chars[end]) {
5108 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5110 end += 1;
5111 }
5112 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5115 break;
5116 }
5117 }
5118 end += 1;
5119 }
5120 let end_idx = (end + 1).min(chars.len());
5122
5123 let final_end = if inner {
5124 end_idx
5125 } else {
5126 let mut e = end_idx;
5130 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5131 e += 1;
5132 }
5133 e
5134 };
5135
5136 Some((idx_to_pos(start), idx_to_pos(final_end)))
5137}
5138
5139fn tag_text_object<H: crate::types::Host>(
5143 ed: &Editor<hjkl_buffer::Buffer, H>,
5144 inner: bool,
5145) -> Option<((usize, usize), (usize, usize))> {
5146 let lines = buf_lines_to_vec(&ed.buffer);
5147 if lines.is_empty() {
5148 return None;
5149 }
5150 let pos_to_idx = |pos: (usize, usize)| -> usize {
5154 let mut idx = 0;
5155 for line in lines.iter().take(pos.0) {
5156 idx += line.chars().count() + 1;
5157 }
5158 idx + pos.1
5159 };
5160 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5161 for (r, line) in lines.iter().enumerate() {
5162 let len = line.chars().count();
5163 if idx <= len {
5164 return (r, idx);
5165 }
5166 idx -= len + 1;
5167 }
5168 let last = lines.len().saturating_sub(1);
5169 (last, lines[last].chars().count())
5170 };
5171 let mut chars: Vec<char> = Vec::new();
5172 for (r, line) in lines.iter().enumerate() {
5173 chars.extend(line.chars());
5174 if r + 1 < lines.len() {
5175 chars.push('\n');
5176 }
5177 }
5178 let cursor_idx = pos_to_idx(ed.cursor());
5179
5180 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5188 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5189 let mut i = 0;
5190 while i < chars.len() {
5191 if chars[i] != '<' {
5192 i += 1;
5193 continue;
5194 }
5195 let mut j = i + 1;
5196 while j < chars.len() && chars[j] != '>' {
5197 j += 1;
5198 }
5199 if j >= chars.len() {
5200 break;
5201 }
5202 let inside: String = chars[i + 1..j].iter().collect();
5203 let close_end = j + 1;
5204 let trimmed = inside.trim();
5205 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5206 i = close_end;
5207 continue;
5208 }
5209 if let Some(rest) = trimmed.strip_prefix('/') {
5210 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5211 if !name.is_empty()
5212 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5213 {
5214 let (open_start, content_start, _) = stack[stack_idx].clone();
5215 stack.truncate(stack_idx);
5216 let content_end = i;
5217 let candidate = (open_start, content_start, content_end, close_end);
5218 if cursor_idx >= content_start && cursor_idx <= content_end {
5219 innermost = match innermost {
5220 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5221 Some(candidate)
5222 }
5223 None => Some(candidate),
5224 existing => existing,
5225 };
5226 } else if open_start >= cursor_idx && next_after.is_none() {
5227 next_after = Some(candidate);
5228 }
5229 }
5230 } else if !trimmed.ends_with('/') {
5231 let name: String = trimmed
5232 .split(|c: char| c.is_whitespace() || c == '/')
5233 .next()
5234 .unwrap_or("")
5235 .to_string();
5236 if !name.is_empty() {
5237 stack.push((i, close_end, name));
5238 }
5239 }
5240 i = close_end;
5241 }
5242
5243 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5244 if inner {
5245 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5246 } else {
5247 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5248 }
5249}
5250
5251fn is_wordchar(c: char) -> bool {
5252 c.is_alphanumeric() || c == '_'
5253}
5254
5255pub(crate) use hjkl_buffer::is_keyword_char;
5259
5260fn word_text_object<H: crate::types::Host>(
5261 ed: &Editor<hjkl_buffer::Buffer, H>,
5262 inner: bool,
5263 big: bool,
5264) -> Option<((usize, usize), (usize, usize))> {
5265 let (row, col) = ed.cursor();
5266 let line = buf_line(&ed.buffer, row)?;
5267 let chars: Vec<char> = line.chars().collect();
5268 if chars.is_empty() {
5269 return None;
5270 }
5271 let at = col.min(chars.len().saturating_sub(1));
5272 let classify = |c: char| -> u8 {
5273 if c.is_whitespace() {
5274 0
5275 } else if big || is_wordchar(c) {
5276 1
5277 } else {
5278 2
5279 }
5280 };
5281 let cls = classify(chars[at]);
5282 let mut start = at;
5283 while start > 0 && classify(chars[start - 1]) == cls {
5284 start -= 1;
5285 }
5286 let mut end = at;
5287 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5288 end += 1;
5289 }
5290 let char_byte = |i: usize| {
5292 if i >= chars.len() {
5293 line.len()
5294 } else {
5295 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5296 }
5297 };
5298 let mut start_col = char_byte(start);
5299 let mut end_col = char_byte(end + 1);
5301 if !inner {
5302 let mut t = end + 1;
5304 let mut included_trailing = false;
5305 while t < chars.len() && chars[t].is_whitespace() {
5306 included_trailing = true;
5307 t += 1;
5308 }
5309 if included_trailing {
5310 end_col = char_byte(t);
5311 } else {
5312 let mut s = start;
5313 while s > 0 && chars[s - 1].is_whitespace() {
5314 s -= 1;
5315 }
5316 start_col = char_byte(s);
5317 }
5318 }
5319 Some(((row, start_col), (row, end_col)))
5320}
5321
5322fn quote_text_object<H: crate::types::Host>(
5323 ed: &Editor<hjkl_buffer::Buffer, H>,
5324 q: char,
5325 inner: bool,
5326) -> Option<((usize, usize), (usize, usize))> {
5327 let (row, col) = ed.cursor();
5328 let line = buf_line(&ed.buffer, row)?;
5329 let bytes = line.as_bytes();
5330 let q_byte = q as u8;
5331 let mut positions: Vec<usize> = Vec::new();
5333 for (i, &b) in bytes.iter().enumerate() {
5334 if b == q_byte {
5335 positions.push(i);
5336 }
5337 }
5338 if positions.len() < 2 {
5339 return None;
5340 }
5341 let mut open_idx: Option<usize> = None;
5342 let mut close_idx: Option<usize> = None;
5343 for pair in positions.chunks(2) {
5344 if pair.len() < 2 {
5345 break;
5346 }
5347 if col >= pair[0] && col <= pair[1] {
5348 open_idx = Some(pair[0]);
5349 close_idx = Some(pair[1]);
5350 break;
5351 }
5352 if col < pair[0] {
5353 open_idx = Some(pair[0]);
5354 close_idx = Some(pair[1]);
5355 break;
5356 }
5357 }
5358 let open = open_idx?;
5359 let close = close_idx?;
5360 if inner {
5362 if close <= open + 1 {
5363 return None;
5364 }
5365 Some(((row, open + 1), (row, close)))
5366 } else {
5367 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5374 let mut end = after_close;
5376 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5377 end += 1;
5378 }
5379 Some(((row, open), (row, end)))
5380 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5381 let mut start = open;
5383 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5384 start -= 1;
5385 }
5386 Some(((row, start), (row, close + 1)))
5387 } else {
5388 Some(((row, open), (row, close + 1)))
5389 }
5390 }
5391}
5392
5393fn bracket_text_object<H: crate::types::Host>(
5394 ed: &Editor<hjkl_buffer::Buffer, H>,
5395 open: char,
5396 inner: bool,
5397) -> Option<(Pos, Pos, RangeKind)> {
5398 let close = match open {
5399 '(' => ')',
5400 '[' => ']',
5401 '{' => '}',
5402 '<' => '>',
5403 _ => return None,
5404 };
5405 let (row, col) = ed.cursor();
5406 let lines = buf_lines_to_vec(&ed.buffer);
5407 let lines = lines.as_slice();
5408 let open_pos = find_open_bracket(lines, row, col, open, close)
5413 .or_else(|| find_next_open(lines, row, col, open))?;
5414 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5415 if inner {
5417 if close_pos.0 > open_pos.0 + 1 {
5423 let inner_row_start = open_pos.0 + 1;
5425 let inner_row_end = close_pos.0 - 1;
5426 let end_col = lines
5427 .get(inner_row_end)
5428 .map(|l| l.chars().count())
5429 .unwrap_or(0);
5430 return Some((
5431 (inner_row_start, 0),
5432 (inner_row_end, end_col),
5433 RangeKind::Linewise,
5434 ));
5435 }
5436 let inner_start = advance_pos(lines, open_pos);
5437 if inner_start.0 > close_pos.0
5438 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5439 {
5440 return None;
5441 }
5442 Some((inner_start, close_pos, RangeKind::Exclusive))
5443 } else {
5444 Some((
5445 open_pos,
5446 advance_pos(lines, close_pos),
5447 RangeKind::Exclusive,
5448 ))
5449 }
5450}
5451
5452fn find_open_bracket(
5453 lines: &[String],
5454 row: usize,
5455 col: usize,
5456 open: char,
5457 close: char,
5458) -> Option<(usize, usize)> {
5459 let mut depth: i32 = 0;
5460 let mut r = row;
5461 let mut c = col as isize;
5462 loop {
5463 let cur = &lines[r];
5464 let chars: Vec<char> = cur.chars().collect();
5465 if (c as usize) >= chars.len() {
5469 c = chars.len() as isize - 1;
5470 }
5471 while c >= 0 {
5472 let ch = chars[c as usize];
5473 if ch == close {
5474 depth += 1;
5475 } else if ch == open {
5476 if depth == 0 {
5477 return Some((r, c as usize));
5478 }
5479 depth -= 1;
5480 }
5481 c -= 1;
5482 }
5483 if r == 0 {
5484 return None;
5485 }
5486 r -= 1;
5487 c = lines[r].chars().count() as isize - 1;
5488 }
5489}
5490
5491fn find_close_bracket(
5492 lines: &[String],
5493 row: usize,
5494 start_col: usize,
5495 open: char,
5496 close: char,
5497) -> Option<(usize, usize)> {
5498 let mut depth: i32 = 0;
5499 let mut r = row;
5500 let mut c = start_col;
5501 loop {
5502 let cur = &lines[r];
5503 let chars: Vec<char> = cur.chars().collect();
5504 while c < chars.len() {
5505 let ch = chars[c];
5506 if ch == open {
5507 depth += 1;
5508 } else if ch == close {
5509 if depth == 0 {
5510 return Some((r, c));
5511 }
5512 depth -= 1;
5513 }
5514 c += 1;
5515 }
5516 if r + 1 >= lines.len() {
5517 return None;
5518 }
5519 r += 1;
5520 c = 0;
5521 }
5522}
5523
5524fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5528 let mut r = row;
5529 let mut c = col;
5530 while r < lines.len() {
5531 let chars: Vec<char> = lines[r].chars().collect();
5532 while c < chars.len() {
5533 if chars[c] == open {
5534 return Some((r, c));
5535 }
5536 c += 1;
5537 }
5538 r += 1;
5539 c = 0;
5540 }
5541 None
5542}
5543
5544fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5545 let (r, c) = pos;
5546 let line_len = lines[r].chars().count();
5547 if c < line_len {
5548 (r, c + 1)
5549 } else if r + 1 < lines.len() {
5550 (r + 1, 0)
5551 } else {
5552 pos
5553 }
5554}
5555
5556fn paragraph_text_object<H: crate::types::Host>(
5557 ed: &Editor<hjkl_buffer::Buffer, H>,
5558 inner: bool,
5559) -> Option<((usize, usize), (usize, usize))> {
5560 let (row, _) = ed.cursor();
5561 let lines = buf_lines_to_vec(&ed.buffer);
5562 if lines.is_empty() {
5563 return None;
5564 }
5565 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5567 if is_blank(row) {
5568 return None;
5569 }
5570 let mut top = row;
5571 while top > 0 && !is_blank(top - 1) {
5572 top -= 1;
5573 }
5574 let mut bot = row;
5575 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5576 bot += 1;
5577 }
5578 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5580 bot += 1;
5581 }
5582 let end_col = lines[bot].chars().count();
5583 Some(((top, 0), (bot, end_col)))
5584}
5585
5586fn read_vim_range<H: crate::types::Host>(
5592 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5593 start: (usize, usize),
5594 end: (usize, usize),
5595 kind: RangeKind,
5596) -> String {
5597 let (top, bot) = order(start, end);
5598 ed.sync_buffer_content_from_textarea();
5599 let lines = buf_lines_to_vec(&ed.buffer);
5600 match kind {
5601 RangeKind::Linewise => {
5602 let lo = top.0;
5603 let hi = bot.0.min(lines.len().saturating_sub(1));
5604 let mut text = lines[lo..=hi].join("\n");
5605 text.push('\n');
5606 text
5607 }
5608 RangeKind::Inclusive | RangeKind::Exclusive => {
5609 let inclusive = matches!(kind, RangeKind::Inclusive);
5610 let mut out = String::new();
5612 for row in top.0..=bot.0 {
5613 let line = lines.get(row).map(String::as_str).unwrap_or("");
5614 let lo = if row == top.0 { top.1 } else { 0 };
5615 let hi_unclamped = if row == bot.0 {
5616 if inclusive { bot.1 + 1 } else { bot.1 }
5617 } else {
5618 line.chars().count() + 1
5619 };
5620 let row_chars: Vec<char> = line.chars().collect();
5621 let hi = hi_unclamped.min(row_chars.len());
5622 if lo < hi {
5623 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5624 }
5625 if row < bot.0 {
5626 out.push('\n');
5627 }
5628 }
5629 out
5630 }
5631 }
5632}
5633
5634fn cut_vim_range<H: crate::types::Host>(
5643 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5644 start: (usize, usize),
5645 end: (usize, usize),
5646 kind: RangeKind,
5647) -> String {
5648 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5649 let (top, bot) = order(start, end);
5650 ed.sync_buffer_content_from_textarea();
5651 let (buf_start, buf_end, buf_kind) = match kind {
5652 RangeKind::Linewise => (
5653 Position::new(top.0, 0),
5654 Position::new(bot.0, 0),
5655 BufKind::Line,
5656 ),
5657 RangeKind::Inclusive => {
5658 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5659 let next = if bot.1 < line_chars {
5663 Position::new(bot.0, bot.1 + 1)
5664 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5665 Position::new(bot.0 + 1, 0)
5666 } else {
5667 Position::new(bot.0, line_chars)
5668 };
5669 (Position::new(top.0, top.1), next, BufKind::Char)
5670 }
5671 RangeKind::Exclusive => (
5672 Position::new(top.0, top.1),
5673 Position::new(bot.0, bot.1),
5674 BufKind::Char,
5675 ),
5676 };
5677 let inverse = ed.mutate_edit(Edit::DeleteRange {
5678 start: buf_start,
5679 end: buf_end,
5680 kind: buf_kind,
5681 });
5682 let text = match inverse {
5683 Edit::InsertStr { text, .. } => text,
5684 _ => String::new(),
5685 };
5686 if !text.is_empty() {
5687 ed.record_yank_to_host(text.clone());
5688 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
5689 }
5690 ed.push_buffer_cursor_to_textarea();
5691 text
5692}
5693
5694fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5700 use hjkl_buffer::{Edit, MotionKind, Position};
5701 ed.sync_buffer_content_from_textarea();
5702 let cursor = buf_cursor_pos(&ed.buffer);
5703 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5704 if cursor.col >= line_chars {
5705 return;
5706 }
5707 let inverse = ed.mutate_edit(Edit::DeleteRange {
5708 start: cursor,
5709 end: Position::new(cursor.row, line_chars),
5710 kind: MotionKind::Char,
5711 });
5712 if let Edit::InsertStr { text, .. } = inverse
5713 && !text.is_empty()
5714 {
5715 ed.record_yank_to_host(text.clone());
5716 ed.vim.yank_linewise = false;
5717 ed.set_yank(text);
5718 }
5719 buf_set_cursor_pos(&mut ed.buffer, cursor);
5720 ed.push_buffer_cursor_to_textarea();
5721}
5722
5723fn do_char_delete<H: crate::types::Host>(
5724 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5725 forward: bool,
5726 count: usize,
5727) {
5728 use hjkl_buffer::{Edit, MotionKind, Position};
5729 ed.push_undo();
5730 ed.sync_buffer_content_from_textarea();
5731 let mut deleted = String::new();
5734 for _ in 0..count {
5735 let cursor = buf_cursor_pos(&ed.buffer);
5736 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5737 if forward {
5738 if cursor.col >= line_chars {
5741 continue;
5742 }
5743 let inverse = ed.mutate_edit(Edit::DeleteRange {
5744 start: cursor,
5745 end: Position::new(cursor.row, cursor.col + 1),
5746 kind: MotionKind::Char,
5747 });
5748 if let Edit::InsertStr { text, .. } = inverse {
5749 deleted.push_str(&text);
5750 }
5751 } else {
5752 if cursor.col == 0 {
5754 continue;
5755 }
5756 let inverse = ed.mutate_edit(Edit::DeleteRange {
5757 start: Position::new(cursor.row, cursor.col - 1),
5758 end: cursor,
5759 kind: MotionKind::Char,
5760 });
5761 if let Edit::InsertStr { text, .. } = inverse {
5762 deleted = text + &deleted;
5765 }
5766 }
5767 }
5768 if !deleted.is_empty() {
5769 ed.record_yank_to_host(deleted.clone());
5770 ed.record_delete(deleted, false);
5771 }
5772 ed.push_buffer_cursor_to_textarea();
5773}
5774
5775pub(crate) fn adjust_number<H: crate::types::Host>(
5779 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5780 delta: i64,
5781) -> bool {
5782 use hjkl_buffer::{Edit, MotionKind, Position};
5783 ed.sync_buffer_content_from_textarea();
5784 let cursor = buf_cursor_pos(&ed.buffer);
5785 let row = cursor.row;
5786 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5787 Some(l) => l.chars().collect(),
5788 None => return false,
5789 };
5790 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5791 return false;
5792 };
5793 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5794 digit_start - 1
5795 } else {
5796 digit_start
5797 };
5798 let mut span_end = digit_start;
5799 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5800 span_end += 1;
5801 }
5802 let s: String = chars[span_start..span_end].iter().collect();
5803 let Ok(n) = s.parse::<i64>() else {
5804 return false;
5805 };
5806 let new_s = n.saturating_add(delta).to_string();
5807
5808 ed.push_undo();
5809 let span_start_pos = Position::new(row, span_start);
5810 let span_end_pos = Position::new(row, span_end);
5811 ed.mutate_edit(Edit::DeleteRange {
5812 start: span_start_pos,
5813 end: span_end_pos,
5814 kind: MotionKind::Char,
5815 });
5816 ed.mutate_edit(Edit::InsertStr {
5817 at: span_start_pos,
5818 text: new_s.clone(),
5819 });
5820 let new_len = new_s.chars().count();
5821 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5822 ed.push_buffer_cursor_to_textarea();
5823 true
5824}
5825
5826pub(crate) fn replace_char<H: crate::types::Host>(
5827 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5828 ch: char,
5829 count: usize,
5830) {
5831 use hjkl_buffer::{Edit, MotionKind, Position};
5832 ed.push_undo();
5833 ed.sync_buffer_content_from_textarea();
5834 for _ in 0..count {
5835 let cursor = buf_cursor_pos(&ed.buffer);
5836 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5837 if cursor.col >= line_chars {
5838 break;
5839 }
5840 ed.mutate_edit(Edit::DeleteRange {
5841 start: cursor,
5842 end: Position::new(cursor.row, cursor.col + 1),
5843 kind: MotionKind::Char,
5844 });
5845 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5846 }
5847 crate::motions::move_left(&mut ed.buffer, 1);
5849 ed.push_buffer_cursor_to_textarea();
5850}
5851
5852fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5853 use hjkl_buffer::{Edit, MotionKind, Position};
5854 ed.sync_buffer_content_from_textarea();
5855 let cursor = buf_cursor_pos(&ed.buffer);
5856 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5857 return;
5858 };
5859 let toggled = if c.is_uppercase() {
5860 c.to_lowercase().next().unwrap_or(c)
5861 } else {
5862 c.to_uppercase().next().unwrap_or(c)
5863 };
5864 ed.mutate_edit(Edit::DeleteRange {
5865 start: cursor,
5866 end: Position::new(cursor.row, cursor.col + 1),
5867 kind: MotionKind::Char,
5868 });
5869 ed.mutate_edit(Edit::InsertChar {
5870 at: cursor,
5871 ch: toggled,
5872 });
5873}
5874
5875fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5876 use hjkl_buffer::{Edit, Position};
5877 ed.sync_buffer_content_from_textarea();
5878 let row = buf_cursor_pos(&ed.buffer).row;
5879 if row + 1 >= buf_row_count(&ed.buffer) {
5880 return;
5881 }
5882 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5883 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5884 let next_trimmed = next_raw.trim_start();
5885 let cur_chars = cur_line.chars().count();
5886 let next_chars = next_raw.chars().count();
5887 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5890 " "
5891 } else {
5892 ""
5893 };
5894 let joined = format!("{cur_line}{separator}{next_trimmed}");
5895 ed.mutate_edit(Edit::Replace {
5896 start: Position::new(row, 0),
5897 end: Position::new(row + 1, next_chars),
5898 with: joined,
5899 });
5900 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5904 ed.push_buffer_cursor_to_textarea();
5905}
5906
5907fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5910 use hjkl_buffer::Edit;
5911 ed.sync_buffer_content_from_textarea();
5912 let row = buf_cursor_pos(&ed.buffer).row;
5913 if row + 1 >= buf_row_count(&ed.buffer) {
5914 return;
5915 }
5916 let join_col = buf_line_chars(&ed.buffer, row);
5917 ed.mutate_edit(Edit::JoinLines {
5918 row,
5919 count: 1,
5920 with_space: false,
5921 });
5922 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5924 ed.push_buffer_cursor_to_textarea();
5925}
5926
5927fn do_paste<H: crate::types::Host>(
5928 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5929 before: bool,
5930 count: usize,
5931) {
5932 use hjkl_buffer::{Edit, Position};
5933 ed.push_undo();
5934 let selector = ed.vim.pending_register.take();
5939 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5940 Some(slot) => (slot.text.clone(), slot.linewise),
5941 None => {
5947 let s = &ed.registers().unnamed;
5948 (s.text.clone(), s.linewise)
5949 }
5950 };
5951 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5955 for _ in 0..count {
5956 ed.sync_buffer_content_from_textarea();
5957 let yank = yank.clone();
5958 if yank.is_empty() {
5959 continue;
5960 }
5961 if linewise {
5962 let text = yank.trim_matches('\n').to_string();
5966 let row = buf_cursor_pos(&ed.buffer).row;
5967 let target_row = if before {
5968 ed.mutate_edit(Edit::InsertStr {
5969 at: Position::new(row, 0),
5970 text: format!("{text}\n"),
5971 });
5972 row
5973 } else {
5974 let line_chars = buf_line_chars(&ed.buffer, row);
5975 ed.mutate_edit(Edit::InsertStr {
5976 at: Position::new(row, line_chars),
5977 text: format!("\n{text}"),
5978 });
5979 row + 1
5980 };
5981 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5982 crate::motions::move_first_non_blank(&mut ed.buffer);
5983 ed.push_buffer_cursor_to_textarea();
5984 let payload_lines = text.lines().count().max(1);
5986 let bot_row = target_row + payload_lines - 1;
5987 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5988 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5989 } else {
5990 let cursor = buf_cursor_pos(&ed.buffer);
5994 let at = if before {
5995 cursor
5996 } else {
5997 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5998 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5999 };
6000 ed.mutate_edit(Edit::InsertStr {
6001 at,
6002 text: yank.clone(),
6003 });
6004 crate::motions::move_left(&mut ed.buffer, 1);
6007 ed.push_buffer_cursor_to_textarea();
6008 let lo = (at.row, at.col);
6010 let hi = ed.cursor();
6011 paste_mark = Some((lo, hi));
6012 }
6013 }
6014 if let Some((lo, hi)) = paste_mark {
6015 ed.set_mark('[', lo);
6016 ed.set_mark(']', hi);
6017 }
6018 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
6020}
6021
6022pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6023 if let Some((lines, cursor)) = ed.undo_stack.pop() {
6024 let current = ed.snapshot();
6025 ed.redo_stack.push(current);
6026 ed.restore(lines, cursor);
6027 }
6028 ed.vim.mode = Mode::Normal;
6029 clamp_cursor_to_normal_mode(ed);
6033}
6034
6035pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6036 if let Some((lines, cursor)) = ed.redo_stack.pop() {
6037 let current = ed.snapshot();
6038 ed.undo_stack.push(current);
6039 ed.cap_undo();
6040 ed.restore(lines, cursor);
6041 }
6042 ed.vim.mode = Mode::Normal;
6043}
6044
6045fn replay_insert_and_finish<H: crate::types::Host>(
6052 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6053 text: &str,
6054) {
6055 use hjkl_buffer::{Edit, Position};
6056 let cursor = ed.cursor();
6057 ed.mutate_edit(Edit::InsertStr {
6058 at: Position::new(cursor.0, cursor.1),
6059 text: text.to_string(),
6060 });
6061 if ed.vim.insert_session.take().is_some() {
6062 if ed.cursor().1 > 0 {
6063 crate::motions::move_left(&mut ed.buffer, 1);
6064 ed.push_buffer_cursor_to_textarea();
6065 }
6066 ed.vim.mode = Mode::Normal;
6067 }
6068}
6069
6070pub(crate) fn replay_last_change<H: crate::types::Host>(
6071 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6072 outer_count: usize,
6073) {
6074 let Some(change) = ed.vim.last_change.clone() else {
6075 return;
6076 };
6077 ed.vim.replaying = true;
6078 let scale = if outer_count > 0 { outer_count } else { 1 };
6079 match change {
6080 LastChange::OpMotion {
6081 op,
6082 motion,
6083 count,
6084 inserted,
6085 } => {
6086 let total = count.max(1) * scale;
6087 apply_op_with_motion(ed, op, &motion, total);
6088 if let Some(text) = inserted {
6089 replay_insert_and_finish(ed, &text);
6090 }
6091 }
6092 LastChange::OpTextObj {
6093 op,
6094 obj,
6095 inner,
6096 inserted,
6097 } => {
6098 apply_op_with_text_object(ed, op, obj, inner);
6099 if let Some(text) = inserted {
6100 replay_insert_and_finish(ed, &text);
6101 }
6102 }
6103 LastChange::LineOp {
6104 op,
6105 count,
6106 inserted,
6107 } => {
6108 let total = count.max(1) * scale;
6109 execute_line_op(ed, op, total);
6110 if let Some(text) = inserted {
6111 replay_insert_and_finish(ed, &text);
6112 }
6113 }
6114 LastChange::CharDel { forward, count } => {
6115 do_char_delete(ed, forward, count * scale);
6116 }
6117 LastChange::ReplaceChar { ch, count } => {
6118 replace_char(ed, ch, count * scale);
6119 }
6120 LastChange::ToggleCase { count } => {
6121 for _ in 0..count * scale {
6122 ed.push_undo();
6123 toggle_case_at_cursor(ed);
6124 }
6125 }
6126 LastChange::JoinLine { count } => {
6127 for _ in 0..count * scale {
6128 ed.push_undo();
6129 join_line(ed);
6130 }
6131 }
6132 LastChange::Paste { before, count } => {
6133 do_paste(ed, before, count * scale);
6134 }
6135 LastChange::DeleteToEol { inserted } => {
6136 use hjkl_buffer::{Edit, Position};
6137 ed.push_undo();
6138 delete_to_eol(ed);
6139 if let Some(text) = inserted {
6140 let cursor = ed.cursor();
6141 ed.mutate_edit(Edit::InsertStr {
6142 at: Position::new(cursor.0, cursor.1),
6143 text,
6144 });
6145 }
6146 }
6147 LastChange::OpenLine { above, inserted } => {
6148 use hjkl_buffer::{Edit, Position};
6149 ed.push_undo();
6150 ed.sync_buffer_content_from_textarea();
6151 let row = buf_cursor_pos(&ed.buffer).row;
6152 if above {
6153 ed.mutate_edit(Edit::InsertStr {
6154 at: Position::new(row, 0),
6155 text: "\n".to_string(),
6156 });
6157 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6158 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6159 } else {
6160 let line_chars = buf_line_chars(&ed.buffer, row);
6161 ed.mutate_edit(Edit::InsertStr {
6162 at: Position::new(row, line_chars),
6163 text: "\n".to_string(),
6164 });
6165 }
6166 ed.push_buffer_cursor_to_textarea();
6167 let cursor = ed.cursor();
6168 ed.mutate_edit(Edit::InsertStr {
6169 at: Position::new(cursor.0, cursor.1),
6170 text: inserted,
6171 });
6172 }
6173 LastChange::InsertAt {
6174 entry,
6175 inserted,
6176 count,
6177 } => {
6178 use hjkl_buffer::{Edit, Position};
6179 ed.push_undo();
6180 match entry {
6181 InsertEntry::I => {}
6182 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6183 InsertEntry::A => {
6184 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6185 ed.push_buffer_cursor_to_textarea();
6186 }
6187 InsertEntry::ShiftA => {
6188 crate::motions::move_line_end(&mut ed.buffer);
6189 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6190 ed.push_buffer_cursor_to_textarea();
6191 }
6192 }
6193 for _ in 0..count.max(1) {
6194 let cursor = ed.cursor();
6195 ed.mutate_edit(Edit::InsertStr {
6196 at: Position::new(cursor.0, cursor.1),
6197 text: inserted.clone(),
6198 });
6199 }
6200 }
6201 }
6202 ed.vim.replaying = false;
6203}
6204
6205fn extract_inserted(before: &str, after: &str) -> String {
6208 let before_chars: Vec<char> = before.chars().collect();
6209 let after_chars: Vec<char> = after.chars().collect();
6210 if after_chars.len() <= before_chars.len() {
6211 return String::new();
6212 }
6213 let prefix = before_chars
6214 .iter()
6215 .zip(after_chars.iter())
6216 .take_while(|(a, b)| a == b)
6217 .count();
6218 let max_suffix = before_chars.len() - prefix;
6219 let suffix = before_chars
6220 .iter()
6221 .rev()
6222 .zip(after_chars.iter().rev())
6223 .take(max_suffix)
6224 .take_while(|(a, b)| a == b)
6225 .count();
6226 after_chars[prefix..after_chars.len() - suffix]
6227 .iter()
6228 .collect()
6229}
6230
6231