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