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(crate) fn pending_count_val(&self) -> Option<u32> {
609 if self.count == 0 {
610 None
611 } else {
612 Some(self.count as u32)
613 }
614 }
615
616 pub(crate) fn is_chord_pending(&self) -> bool {
619 !matches!(self.pending, Pending::None)
620 }
621
622 pub(crate) fn pending_op_char(&self) -> Option<char> {
626 let op = match &self.pending {
627 Pending::Op { op, .. }
628 | Pending::OpTextObj { op, .. }
629 | Pending::OpG { op, .. }
630 | Pending::OpFind { op, .. } => Some(*op),
631 _ => None,
632 };
633 op.map(|o| match o {
634 Operator::Delete => 'd',
635 Operator::Change => 'c',
636 Operator::Yank => 'y',
637 Operator::Uppercase => 'U',
638 Operator::Lowercase => 'u',
639 Operator::ToggleCase => '~',
640 Operator::Indent => '>',
641 Operator::Outdent => '<',
642 Operator::Fold => 'z',
643 Operator::Reflow => 'q',
644 })
645 }
646}
647
648pub(crate) fn enter_search<H: crate::types::Host>(
654 ed: &mut Editor<hjkl_buffer::Buffer, H>,
655 forward: bool,
656) {
657 ed.vim.search_prompt = Some(SearchPrompt {
658 text: String::new(),
659 cursor: 0,
660 forward,
661 });
662 ed.vim.search_history_cursor = None;
663 ed.set_search_pattern(None);
667}
668
669fn walk_change_list<H: crate::types::Host>(
673 ed: &mut Editor<hjkl_buffer::Buffer, H>,
674 dir: isize,
675 count: usize,
676) {
677 if ed.vim.change_list.is_empty() {
678 return;
679 }
680 let len = ed.vim.change_list.len();
681 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
682 (None, -1) => len as isize - 1,
683 (None, 1) => return, (Some(i), -1) => i as isize - 1,
685 (Some(i), 1) => i as isize + 1,
686 _ => return,
687 };
688 for _ in 1..count {
689 let next = idx + dir;
690 if next < 0 || next >= len as isize {
691 break;
692 }
693 idx = next;
694 }
695 if idx < 0 || idx >= len as isize {
696 return;
697 }
698 let idx = idx as usize;
699 ed.vim.change_list_cursor = Some(idx);
700 let (row, col) = ed.vim.change_list[idx];
701 ed.jump_cursor(row, col);
702}
703
704fn insert_register_text<H: crate::types::Host>(
709 ed: &mut Editor<hjkl_buffer::Buffer, H>,
710 selector: char,
711) {
712 use hjkl_buffer::Edit;
713 let text = match ed.registers().read(selector) {
714 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
715 _ => return,
716 };
717 ed.sync_buffer_content_from_textarea();
718 let cursor = buf_cursor_pos(&ed.buffer);
719 ed.mutate_edit(Edit::InsertStr {
720 at: cursor,
721 text: text.clone(),
722 });
723 let mut row = cursor.row;
726 let mut col = cursor.col;
727 for ch in text.chars() {
728 if ch == '\n' {
729 row += 1;
730 col = 0;
731 } else {
732 col += 1;
733 }
734 }
735 buf_set_cursor_rc(&mut ed.buffer, row, col);
736 ed.push_buffer_cursor_to_textarea();
737 ed.mark_content_dirty();
738 if let Some(ref mut session) = ed.vim.insert_session {
739 session.row_min = session.row_min.min(row);
740 session.row_max = session.row_max.max(row);
741 }
742}
743
744pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
763 if !settings.autoindent {
764 return String::new();
765 }
766 let base: String = prev_line
768 .chars()
769 .take_while(|c| *c == ' ' || *c == '\t')
770 .collect();
771
772 if settings.smartindent {
773 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
777 if matches!(last_non_ws, Some('{' | '(' | '[')) {
778 let unit = if settings.expandtab {
779 if settings.softtabstop > 0 {
780 " ".repeat(settings.softtabstop)
781 } else {
782 " ".repeat(settings.shiftwidth)
783 }
784 } else {
785 "\t".to_string()
786 };
787 return format!("{base}{unit}");
788 }
789 }
790
791 base
792}
793
794fn try_dedent_close_bracket<H: crate::types::Host>(
804 ed: &mut Editor<hjkl_buffer::Buffer, H>,
805 cursor: hjkl_buffer::Position,
806 ch: char,
807) -> bool {
808 use hjkl_buffer::{Edit, MotionKind, Position};
809
810 if !ed.settings.smartindent {
811 return false;
812 }
813 if !matches!(ch, '}' | ')' | ']') {
814 return false;
815 }
816
817 let line = match buf_line(&ed.buffer, cursor.row) {
818 Some(l) => l.to_string(),
819 None => return false,
820 };
821
822 let before: String = line.chars().take(cursor.col).collect();
824 if !before.chars().all(|c| c == ' ' || c == '\t') {
825 return false;
826 }
827 if before.is_empty() {
828 return false;
830 }
831
832 let unit_len: usize = if ed.settings.expandtab {
834 if ed.settings.softtabstop > 0 {
835 ed.settings.softtabstop
836 } else {
837 ed.settings.shiftwidth
838 }
839 } else {
840 1
842 };
843
844 let strip_len = if ed.settings.expandtab {
846 let spaces = before.chars().filter(|c| *c == ' ').count();
848 if spaces < unit_len {
849 return false;
850 }
851 unit_len
852 } else {
853 if !before.starts_with('\t') {
855 return false;
856 }
857 1
858 };
859
860 ed.mutate_edit(Edit::DeleteRange {
862 start: Position::new(cursor.row, 0),
863 end: Position::new(cursor.row, strip_len),
864 kind: MotionKind::Char,
865 });
866 let new_col = cursor.col.saturating_sub(strip_len);
871 ed.mutate_edit(Edit::InsertChar {
872 at: Position::new(cursor.row, new_col),
873 ch,
874 });
875 true
876}
877
878fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
879 let Some(session) = ed.vim.insert_session.take() else {
880 return;
881 };
882 let lines = buf_lines_to_vec(&ed.buffer);
883 let after_end = session.row_max.min(lines.len().saturating_sub(1));
887 let before_end = session
888 .row_max
889 .min(session.before_lines.len().saturating_sub(1));
890 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
891 session.before_lines[session.row_min..=before_end].join("\n")
892 } else {
893 String::new()
894 };
895 let after = if after_end >= session.row_min && session.row_min < lines.len() {
896 lines[session.row_min..=after_end].join("\n")
897 } else {
898 String::new()
899 };
900 let inserted = extract_inserted(&before, &after);
901 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
902 use hjkl_buffer::{Edit, Position};
903 for _ in 0..session.count - 1 {
904 let (row, col) = ed.cursor();
905 ed.mutate_edit(Edit::InsertStr {
906 at: Position::new(row, col),
907 text: inserted.clone(),
908 });
909 }
910 }
911 fn replicate_block_text<H: crate::types::Host>(
915 ed: &mut Editor<hjkl_buffer::Buffer, H>,
916 inserted: &str,
917 top: usize,
918 bot: usize,
919 col: usize,
920 ) {
921 use hjkl_buffer::{Edit, Position};
922 for r in (top + 1)..=bot {
923 let line_len = buf_line_chars(&ed.buffer, r);
924 if col > line_len {
925 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
926 ed.mutate_edit(Edit::InsertStr {
927 at: Position::new(r, line_len),
928 text: pad,
929 });
930 }
931 ed.mutate_edit(Edit::InsertStr {
932 at: Position::new(r, col),
933 text: inserted.to_string(),
934 });
935 }
936 }
937
938 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
939 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
942 replicate_block_text(ed, &inserted, top, bot, col);
943 buf_set_cursor_rc(&mut ed.buffer, top, col);
944 ed.push_buffer_cursor_to_textarea();
945 }
946 return;
947 }
948 if let InsertReason::BlockChange { top, bot, col } = session.reason {
949 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
953 replicate_block_text(ed, &inserted, top, bot, col);
954 let ins_chars = inserted.chars().count();
955 let line_len = buf_line_chars(&ed.buffer, top);
956 let target_col = (col + ins_chars).min(line_len);
957 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
958 ed.push_buffer_cursor_to_textarea();
959 }
960 return;
961 }
962 if ed.vim.replaying {
963 return;
964 }
965 match session.reason {
966 InsertReason::Enter(entry) => {
967 ed.vim.last_change = Some(LastChange::InsertAt {
968 entry,
969 inserted,
970 count: session.count,
971 });
972 }
973 InsertReason::Open { above } => {
974 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
975 }
976 InsertReason::AfterChange => {
977 if let Some(
978 LastChange::OpMotion { inserted: ins, .. }
979 | LastChange::OpTextObj { inserted: ins, .. }
980 | LastChange::LineOp { inserted: ins, .. },
981 ) = ed.vim.last_change.as_mut()
982 {
983 *ins = Some(inserted);
984 }
985 if let Some(start) = ed.vim.change_mark_start.take() {
991 let end = ed.cursor();
992 ed.set_mark('[', start);
993 ed.set_mark(']', end);
994 }
995 }
996 InsertReason::DeleteToEol => {
997 ed.vim.last_change = Some(LastChange::DeleteToEol {
998 inserted: Some(inserted),
999 });
1000 }
1001 InsertReason::ReplayOnly => {}
1002 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1003 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1004 InsertReason::Replace => {
1005 ed.vim.last_change = Some(LastChange::DeleteToEol {
1010 inserted: Some(inserted),
1011 });
1012 }
1013 }
1014}
1015
1016pub(crate) fn begin_insert<H: crate::types::Host>(
1017 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1018 count: usize,
1019 reason: InsertReason,
1020) {
1021 let record = !matches!(reason, InsertReason::ReplayOnly);
1022 if record {
1023 ed.push_undo();
1024 }
1025 let reason = if ed.vim.replaying {
1026 InsertReason::ReplayOnly
1027 } else {
1028 reason
1029 };
1030 let (row, _) = ed.cursor();
1031 ed.vim.insert_session = Some(InsertSession {
1032 count,
1033 row_min: row,
1034 row_max: row,
1035 before_lines: buf_lines_to_vec(&ed.buffer),
1036 reason,
1037 });
1038 ed.vim.mode = Mode::Insert;
1039 ed.vim.current_mode = crate::VimMode::Insert;
1041}
1042
1043pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1058 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1059) {
1060 if !ed.settings.undo_break_on_motion {
1061 return;
1062 }
1063 if ed.vim.replaying {
1064 return;
1065 }
1066 if ed.vim.insert_session.is_none() {
1067 return;
1068 }
1069 ed.push_undo();
1070 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1071 let mut lines: Vec<String> = Vec::with_capacity(n);
1072 for r in 0..n {
1073 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1074 }
1075 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1076 if let Some(ref mut session) = ed.vim.insert_session {
1077 session.before_lines = lines;
1078 session.row_min = row;
1079 session.row_max = row;
1080 }
1081}
1082
1083pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1104 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1105 ch: char,
1106) -> bool {
1107 use hjkl_buffer::{Edit, MotionKind, Position};
1108 ed.sync_buffer_content_from_textarea();
1109 let cursor = buf_cursor_pos(&ed.buffer);
1110 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1111 let in_replace = matches!(
1112 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1113 Some(InsertReason::Replace)
1114 );
1115 if in_replace && cursor.col < line_chars {
1116 ed.mutate_edit(Edit::DeleteRange {
1117 start: cursor,
1118 end: Position::new(cursor.row, cursor.col + 1),
1119 kind: MotionKind::Char,
1120 });
1121 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1122 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1123 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1124 }
1125 ed.push_buffer_cursor_to_textarea();
1126 true
1127}
1128
1129pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1132 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1133) -> bool {
1134 use hjkl_buffer::Edit;
1135 ed.sync_buffer_content_from_textarea();
1136 let cursor = buf_cursor_pos(&ed.buffer);
1137 let prev_line = buf_line(&ed.buffer, cursor.row)
1138 .unwrap_or_default()
1139 .to_string();
1140 let indent = compute_enter_indent(&ed.settings, &prev_line);
1141 let text = format!("\n{indent}");
1142 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1143 ed.push_buffer_cursor_to_textarea();
1144 true
1145}
1146
1147pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1150 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1151) -> bool {
1152 use hjkl_buffer::Edit;
1153 ed.sync_buffer_content_from_textarea();
1154 let cursor = buf_cursor_pos(&ed.buffer);
1155 if ed.settings.expandtab {
1156 let sts = ed.settings.softtabstop;
1157 let n = if sts > 0 {
1158 sts - (cursor.col % sts)
1159 } else {
1160 ed.settings.tabstop.max(1)
1161 };
1162 ed.mutate_edit(Edit::InsertStr {
1163 at: cursor,
1164 text: " ".repeat(n),
1165 });
1166 } else {
1167 ed.mutate_edit(Edit::InsertChar {
1168 at: cursor,
1169 ch: '\t',
1170 });
1171 }
1172 ed.push_buffer_cursor_to_textarea();
1173 true
1174}
1175
1176pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1182 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1183) -> bool {
1184 use hjkl_buffer::{Edit, MotionKind, Position};
1185 ed.sync_buffer_content_from_textarea();
1186 let cursor = buf_cursor_pos(&ed.buffer);
1187 let sts = ed.settings.softtabstop;
1188 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1189 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1190 let chars: Vec<char> = line.chars().collect();
1191 let run_start = cursor.col - sts;
1192 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1193 ed.mutate_edit(Edit::DeleteRange {
1194 start: Position::new(cursor.row, run_start),
1195 end: cursor,
1196 kind: MotionKind::Char,
1197 });
1198 ed.push_buffer_cursor_to_textarea();
1199 return true;
1200 }
1201 }
1202 let result = if cursor.col > 0 {
1203 ed.mutate_edit(Edit::DeleteRange {
1204 start: Position::new(cursor.row, cursor.col - 1),
1205 end: cursor,
1206 kind: MotionKind::Char,
1207 });
1208 true
1209 } else if cursor.row > 0 {
1210 let prev_row = cursor.row - 1;
1211 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1212 ed.mutate_edit(Edit::JoinLines {
1213 row: prev_row,
1214 count: 1,
1215 with_space: false,
1216 });
1217 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1218 true
1219 } else {
1220 false
1221 };
1222 ed.push_buffer_cursor_to_textarea();
1223 result
1224}
1225
1226pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
1229 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1230) -> bool {
1231 use hjkl_buffer::{Edit, MotionKind, Position};
1232 ed.sync_buffer_content_from_textarea();
1233 let cursor = buf_cursor_pos(&ed.buffer);
1234 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1235 let result = if cursor.col < line_chars {
1236 ed.mutate_edit(Edit::DeleteRange {
1237 start: cursor,
1238 end: Position::new(cursor.row, cursor.col + 1),
1239 kind: MotionKind::Char,
1240 });
1241 buf_set_cursor_pos(&mut ed.buffer, cursor);
1242 true
1243 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1244 ed.mutate_edit(Edit::JoinLines {
1245 row: cursor.row,
1246 count: 1,
1247 with_space: false,
1248 });
1249 buf_set_cursor_pos(&mut ed.buffer, cursor);
1250 true
1251 } else {
1252 false
1253 };
1254 ed.push_buffer_cursor_to_textarea();
1255 result
1256}
1257
1258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1260pub enum InsertDir {
1261 Left,
1262 Right,
1263 Up,
1264 Down,
1265}
1266
1267pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
1270 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1271 dir: InsertDir,
1272) -> bool {
1273 ed.sync_buffer_content_from_textarea();
1274 match dir {
1275 InsertDir::Left => {
1276 crate::motions::move_left(&mut ed.buffer, 1);
1277 }
1278 InsertDir::Right => {
1279 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1280 }
1281 InsertDir::Up => {
1282 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1283 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1284 }
1285 InsertDir::Down => {
1286 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1287 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1288 }
1289 }
1290 break_undo_group_in_insert(ed);
1291 ed.push_buffer_cursor_to_textarea();
1292 false
1293}
1294
1295pub(crate) fn insert_home_bridge<H: crate::types::Host>(
1298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1299) -> bool {
1300 ed.sync_buffer_content_from_textarea();
1301 crate::motions::move_line_start(&mut ed.buffer);
1302 break_undo_group_in_insert(ed);
1303 ed.push_buffer_cursor_to_textarea();
1304 false
1305}
1306
1307pub(crate) fn insert_end_bridge<H: crate::types::Host>(
1310 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1311) -> bool {
1312 ed.sync_buffer_content_from_textarea();
1313 crate::motions::move_line_end(&mut ed.buffer);
1314 break_undo_group_in_insert(ed);
1315 ed.push_buffer_cursor_to_textarea();
1316 false
1317}
1318
1319pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
1322 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1323 viewport_h: u16,
1324) -> bool {
1325 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1326 scroll_cursor_rows(ed, -rows);
1327 false
1328}
1329
1330pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
1333 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1334 viewport_h: u16,
1335) -> bool {
1336 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1337 scroll_cursor_rows(ed, rows);
1338 false
1339}
1340
1341pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
1345 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1346) -> bool {
1347 use hjkl_buffer::{Edit, MotionKind};
1348 ed.sync_buffer_content_from_textarea();
1349 let cursor = buf_cursor_pos(&ed.buffer);
1350 if cursor.row == 0 && cursor.col == 0 {
1351 return true;
1352 }
1353 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1354 let word_start = buf_cursor_pos(&ed.buffer);
1355 if word_start == cursor {
1356 return true;
1357 }
1358 buf_set_cursor_pos(&mut ed.buffer, cursor);
1359 ed.mutate_edit(Edit::DeleteRange {
1360 start: word_start,
1361 end: cursor,
1362 kind: MotionKind::Char,
1363 });
1364 ed.push_buffer_cursor_to_textarea();
1365 true
1366}
1367
1368pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
1371 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1372) -> bool {
1373 use hjkl_buffer::{Edit, MotionKind, Position};
1374 ed.sync_buffer_content_from_textarea();
1375 let cursor = buf_cursor_pos(&ed.buffer);
1376 if cursor.col > 0 {
1377 ed.mutate_edit(Edit::DeleteRange {
1378 start: Position::new(cursor.row, 0),
1379 end: cursor,
1380 kind: MotionKind::Char,
1381 });
1382 ed.push_buffer_cursor_to_textarea();
1383 }
1384 true
1385}
1386
1387pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
1391 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1392) -> bool {
1393 use hjkl_buffer::{Edit, MotionKind, Position};
1394 ed.sync_buffer_content_from_textarea();
1395 let cursor = buf_cursor_pos(&ed.buffer);
1396 if cursor.col > 0 {
1397 ed.mutate_edit(Edit::DeleteRange {
1398 start: Position::new(cursor.row, cursor.col - 1),
1399 end: cursor,
1400 kind: MotionKind::Char,
1401 });
1402 } else if cursor.row > 0 {
1403 let prev_row = cursor.row - 1;
1404 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1405 ed.mutate_edit(Edit::JoinLines {
1406 row: prev_row,
1407 count: 1,
1408 with_space: false,
1409 });
1410 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1411 }
1412 ed.push_buffer_cursor_to_textarea();
1413 true
1414}
1415
1416pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
1419 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1420) -> bool {
1421 let (row, col) = ed.cursor();
1422 let sw = ed.settings().shiftwidth;
1423 indent_rows(ed, row, row, 1);
1424 ed.jump_cursor(row, col + sw);
1425 true
1426}
1427
1428pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
1431 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1432) -> bool {
1433 let (row, col) = ed.cursor();
1434 let before_len = buf_line_bytes(&ed.buffer, row);
1435 outdent_rows(ed, row, row, 1);
1436 let after_len = buf_line_bytes(&ed.buffer, row);
1437 let stripped = before_len.saturating_sub(after_len);
1438 let new_col = col.saturating_sub(stripped);
1439 ed.jump_cursor(row, new_col);
1440 true
1441}
1442
1443pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
1447 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1448) -> bool {
1449 ed.vim.one_shot_normal = true;
1450 ed.vim.mode = Mode::Normal;
1451 ed.vim.current_mode = crate::VimMode::Normal;
1453 false
1454}
1455
1456pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
1460 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1461) -> bool {
1462 ed.vim.insert_pending_register = true;
1463 false
1464}
1465
1466pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
1470 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1471 reg: char,
1472) -> bool {
1473 insert_register_text(ed, reg);
1474 true
1477}
1478
1479pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
1484 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1485) -> bool {
1486 finish_insert_session(ed);
1487 ed.vim.mode = Mode::Normal;
1488 ed.vim.current_mode = crate::VimMode::Normal;
1490 let col = ed.cursor().1;
1491 ed.vim.last_insert_pos = Some(ed.cursor());
1492 if col > 0 {
1493 crate::motions::move_left(&mut ed.buffer, 1);
1494 ed.push_buffer_cursor_to_textarea();
1495 }
1496 ed.sticky_col = Some(ed.cursor().1);
1497 true
1498}
1499
1500#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1505pub enum ScrollDir {
1506 Down,
1508 Up,
1510}
1511
1512pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
1517 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1518 count: usize,
1519) {
1520 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
1521}
1522
1523pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
1525 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1526 count: usize,
1527) {
1528 move_first_non_whitespace(ed);
1529 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
1530}
1531
1532pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
1534 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1535 count: usize,
1536) {
1537 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1538 ed.push_buffer_cursor_to_textarea();
1539 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
1540}
1541
1542pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
1544 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1545 count: usize,
1546) {
1547 crate::motions::move_line_end(&mut ed.buffer);
1548 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1549 ed.push_buffer_cursor_to_textarea();
1550 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
1551}
1552
1553pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
1555 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1556 count: usize,
1557) {
1558 use hjkl_buffer::{Edit, Position};
1559 ed.push_undo();
1560 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
1561 ed.sync_buffer_content_from_textarea();
1562 let row = buf_cursor_pos(&ed.buffer).row;
1563 let line_chars = buf_line_chars(&ed.buffer, row);
1564 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
1565 let indent = compute_enter_indent(&ed.settings, prev_line);
1566 ed.mutate_edit(Edit::InsertStr {
1567 at: Position::new(row, line_chars),
1568 text: format!("\n{indent}"),
1569 });
1570 ed.push_buffer_cursor_to_textarea();
1571}
1572
1573pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
1575 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1576 count: usize,
1577) {
1578 use hjkl_buffer::{Edit, Position};
1579 ed.push_undo();
1580 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
1581 ed.sync_buffer_content_from_textarea();
1582 let row = buf_cursor_pos(&ed.buffer).row;
1583 let indent = if row > 0 {
1584 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
1585 compute_enter_indent(&ed.settings, above)
1586 } else {
1587 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
1588 cur.chars()
1589 .take_while(|c| *c == ' ' || *c == '\t')
1590 .collect::<String>()
1591 };
1592 ed.mutate_edit(Edit::InsertStr {
1593 at: Position::new(row, 0),
1594 text: format!("{indent}\n"),
1595 });
1596 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1597 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1598 let new_row = buf_cursor_pos(&ed.buffer).row;
1599 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
1600 ed.push_buffer_cursor_to_textarea();
1601}
1602
1603pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
1605 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1606 count: usize,
1607) {
1608 begin_insert(ed, count.max(1), InsertReason::Replace);
1609}
1610
1611pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
1616 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1617 count: usize,
1618) {
1619 do_char_delete(ed, true, count.max(1));
1620 if !ed.vim.replaying {
1621 ed.vim.last_change = Some(LastChange::CharDel {
1622 forward: true,
1623 count: count.max(1),
1624 });
1625 }
1626}
1627
1628pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
1631 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1632 count: usize,
1633) {
1634 do_char_delete(ed, false, count.max(1));
1635 if !ed.vim.replaying {
1636 ed.vim.last_change = Some(LastChange::CharDel {
1637 forward: false,
1638 count: count.max(1),
1639 });
1640 }
1641}
1642
1643pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
1646 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1647 count: usize,
1648) {
1649 use hjkl_buffer::{Edit, MotionKind, Position};
1650 ed.push_undo();
1651 ed.sync_buffer_content_from_textarea();
1652 for _ in 0..count.max(1) {
1653 let cursor = buf_cursor_pos(&ed.buffer);
1654 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1655 if cursor.col >= line_chars {
1656 break;
1657 }
1658 ed.mutate_edit(Edit::DeleteRange {
1659 start: cursor,
1660 end: Position::new(cursor.row, cursor.col + 1),
1661 kind: MotionKind::Char,
1662 });
1663 }
1664 ed.push_buffer_cursor_to_textarea();
1665 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
1666 if !ed.vim.replaying {
1667 ed.vim.last_change = Some(LastChange::OpMotion {
1668 op: Operator::Change,
1669 motion: Motion::Right,
1670 count: count.max(1),
1671 inserted: None,
1672 });
1673 }
1674}
1675
1676pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
1679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1680 count: usize,
1681) {
1682 execute_line_op(ed, Operator::Change, count.max(1));
1683 if !ed.vim.replaying {
1684 ed.vim.last_change = Some(LastChange::LineOp {
1685 op: Operator::Change,
1686 count: count.max(1),
1687 inserted: None,
1688 });
1689 }
1690}
1691
1692pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1695 ed.push_undo();
1696 delete_to_eol(ed);
1697 crate::motions::move_left(&mut ed.buffer, 1);
1698 ed.push_buffer_cursor_to_textarea();
1699 if !ed.vim.replaying {
1700 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
1701 }
1702}
1703
1704pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1707 ed.push_undo();
1708 delete_to_eol(ed);
1709 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
1710}
1711
1712pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
1714 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1715 count: usize,
1716) {
1717 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
1718}
1719
1720pub(crate) fn join_line_bridge<H: crate::types::Host>(
1723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1724 count: usize,
1725) {
1726 for _ in 0..count.max(1) {
1727 ed.push_undo();
1728 join_line(ed);
1729 }
1730 if !ed.vim.replaying {
1731 ed.vim.last_change = Some(LastChange::JoinLine {
1732 count: count.max(1),
1733 });
1734 }
1735}
1736
1737pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
1740 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1741 count: usize,
1742) {
1743 for _ in 0..count.max(1) {
1744 ed.push_undo();
1745 toggle_case_at_cursor(ed);
1746 }
1747 if !ed.vim.replaying {
1748 ed.vim.last_change = Some(LastChange::ToggleCase {
1749 count: count.max(1),
1750 });
1751 }
1752}
1753
1754pub(crate) fn paste_after_bridge<H: crate::types::Host>(
1758 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1759 count: usize,
1760) {
1761 do_paste(ed, false, count.max(1));
1762 if !ed.vim.replaying {
1763 ed.vim.last_change = Some(LastChange::Paste {
1764 before: false,
1765 count: count.max(1),
1766 });
1767 }
1768}
1769
1770pub(crate) fn paste_before_bridge<H: crate::types::Host>(
1774 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1775 count: usize,
1776) {
1777 do_paste(ed, true, count.max(1));
1778 if !ed.vim.replaying {
1779 ed.vim.last_change = Some(LastChange::Paste {
1780 before: true,
1781 count: count.max(1),
1782 });
1783 }
1784}
1785
1786pub(crate) fn jump_back_bridge<H: crate::types::Host>(
1791 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1792 count: usize,
1793) {
1794 for _ in 0..count.max(1) {
1795 jump_back(ed);
1796 }
1797}
1798
1799pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
1802 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1803 count: usize,
1804) {
1805 for _ in 0..count.max(1) {
1806 jump_forward(ed);
1807 }
1808}
1809
1810pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
1815 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1816 dir: ScrollDir,
1817 count: usize,
1818) {
1819 let rows = viewport_full_rows(ed, count) as isize;
1820 match dir {
1821 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1822 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1823 }
1824}
1825
1826pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
1829 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1830 dir: ScrollDir,
1831 count: usize,
1832) {
1833 let rows = viewport_half_rows(ed, count) as isize;
1834 match dir {
1835 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1836 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1837 }
1838}
1839
1840pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
1844 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1845 dir: ScrollDir,
1846 count: usize,
1847) {
1848 let n = count.max(1);
1849 let total = buf_row_count(&ed.buffer);
1850 let last = total.saturating_sub(1);
1851 let h = ed.viewport_height_value() as usize;
1852 let vp = ed.host().viewport();
1853 let cur_top = vp.top_row;
1854 let new_top = match dir {
1855 ScrollDir::Down => (cur_top + n).min(last),
1856 ScrollDir::Up => cur_top.saturating_sub(n),
1857 };
1858 ed.set_viewport_top(new_top);
1859 let (row, col) = ed.cursor();
1861 let bot = (new_top + h).saturating_sub(1).min(last);
1862 let clamped = row.max(new_top).min(bot);
1863 if clamped != row {
1864 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
1865 ed.push_buffer_cursor_to_textarea();
1866 }
1867}
1868
1869pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
1874 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1875 forward: bool,
1876 count: usize,
1877) {
1878 if let Some(pattern) = ed.vim.last_search.clone() {
1879 ed.push_search_pattern(&pattern);
1880 }
1881 if ed.search_state().pattern.is_none() {
1882 return;
1883 }
1884 let go_forward = ed.vim.last_search_forward == forward;
1885 for _ in 0..count.max(1) {
1886 if go_forward {
1887 ed.search_advance_forward(true);
1888 } else {
1889 ed.search_advance_backward(true);
1890 }
1891 }
1892 ed.push_buffer_cursor_to_textarea();
1893}
1894
1895pub(crate) fn word_search_bridge<H: crate::types::Host>(
1899 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1900 forward: bool,
1901 whole_word: bool,
1902 count: usize,
1903) {
1904 word_at_cursor_search(ed, forward, whole_word, count.max(1));
1905}
1906
1907#[allow(dead_code)]
1912#[inline]
1913pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1914 do_undo(ed);
1915}
1916
1917#[inline]
1932pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
1933 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1934 mode: Mode,
1935) {
1936 ed.vim.mode = mode;
1937 ed.vim.current_mode = ed.vim.public_mode();
1938}
1939
1940pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
1943 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1944) {
1945 let cur = ed.cursor();
1946 ed.vim.visual_anchor = cur;
1947 set_vim_mode_bridge(ed, Mode::Visual);
1948}
1949
1950pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
1953 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1954) {
1955 let (row, _) = ed.cursor();
1956 ed.vim.visual_line_anchor = row;
1957 set_vim_mode_bridge(ed, Mode::VisualLine);
1958}
1959
1960pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
1964 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1965) {
1966 let cur = ed.cursor();
1967 ed.vim.block_anchor = cur;
1968 ed.vim.block_vcol = cur.1;
1969 set_vim_mode_bridge(ed, Mode::VisualBlock);
1970}
1971
1972pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
1977 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1978) {
1979 let snap: Option<LastVisual> = match ed.vim.mode {
1981 Mode::Visual => Some(LastVisual {
1982 mode: Mode::Visual,
1983 anchor: ed.vim.visual_anchor,
1984 cursor: ed.cursor(),
1985 block_vcol: 0,
1986 }),
1987 Mode::VisualLine => Some(LastVisual {
1988 mode: Mode::VisualLine,
1989 anchor: (ed.vim.visual_line_anchor, 0),
1990 cursor: ed.cursor(),
1991 block_vcol: 0,
1992 }),
1993 Mode::VisualBlock => Some(LastVisual {
1994 mode: Mode::VisualBlock,
1995 anchor: ed.vim.block_anchor,
1996 cursor: ed.cursor(),
1997 block_vcol: ed.vim.block_vcol,
1998 }),
1999 _ => None,
2000 };
2001 ed.vim.pending = Pending::None;
2003 ed.vim.count = 0;
2004 ed.vim.insert_session = None;
2005 set_vim_mode_bridge(ed, Mode::Normal);
2006 if let Some(snap) = snap {
2010 let (lo, hi) = match snap.mode {
2011 Mode::Visual => {
2012 if snap.anchor <= snap.cursor {
2013 (snap.anchor, snap.cursor)
2014 } else {
2015 (snap.cursor, snap.anchor)
2016 }
2017 }
2018 Mode::VisualLine => {
2019 let r_lo = snap.anchor.0.min(snap.cursor.0);
2020 let r_hi = snap.anchor.0.max(snap.cursor.0);
2021 let last_col = ed
2022 .buffer()
2023 .lines()
2024 .get(r_hi)
2025 .map(|l| l.chars().count().saturating_sub(1))
2026 .unwrap_or(0);
2027 ((r_lo, 0), (r_hi, last_col))
2028 }
2029 Mode::VisualBlock => {
2030 let (r1, c1) = snap.anchor;
2031 let (r2, c2) = snap.cursor;
2032 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2033 }
2034 _ => {
2035 if snap.anchor <= snap.cursor {
2036 (snap.anchor, snap.cursor)
2037 } else {
2038 (snap.cursor, snap.anchor)
2039 }
2040 }
2041 };
2042 ed.set_mark('<', lo);
2043 ed.set_mark('>', hi);
2044 ed.vim.last_visual = Some(snap);
2045 }
2046}
2047
2048pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2054 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2055) {
2056 match ed.vim.mode {
2057 Mode::Visual => {
2058 let cur = ed.cursor();
2059 let anchor = ed.vim.visual_anchor;
2060 ed.vim.visual_anchor = cur;
2061 ed.jump_cursor(anchor.0, anchor.1);
2062 }
2063 Mode::VisualLine => {
2064 let cur_row = ed.cursor().0;
2065 let anchor_row = ed.vim.visual_line_anchor;
2066 ed.vim.visual_line_anchor = cur_row;
2067 ed.jump_cursor(anchor_row, 0);
2068 }
2069 Mode::VisualBlock => {
2070 let cur = ed.cursor();
2071 let anchor = ed.vim.block_anchor;
2072 ed.vim.block_anchor = cur;
2073 ed.vim.block_vcol = anchor.1;
2074 ed.jump_cursor(anchor.0, anchor.1);
2075 }
2076 _ => {}
2077 }
2078}
2079
2080pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
2084 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2085) {
2086 if let Some(snap) = ed.vim.last_visual {
2087 match snap.mode {
2088 Mode::Visual => {
2089 ed.vim.visual_anchor = snap.anchor;
2090 set_vim_mode_bridge(ed, Mode::Visual);
2091 }
2092 Mode::VisualLine => {
2093 ed.vim.visual_line_anchor = snap.anchor.0;
2094 set_vim_mode_bridge(ed, Mode::VisualLine);
2095 }
2096 Mode::VisualBlock => {
2097 ed.vim.block_anchor = snap.anchor;
2098 ed.vim.block_vcol = snap.block_vcol;
2099 set_vim_mode_bridge(ed, Mode::VisualBlock);
2100 }
2101 _ => {}
2102 }
2103 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2104 }
2105}
2106
2107pub(crate) fn set_mode_bridge<H: crate::types::Host>(
2113 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2114 mode: crate::VimMode,
2115) {
2116 let internal = match mode {
2117 crate::VimMode::Normal => Mode::Normal,
2118 crate::VimMode::Insert => Mode::Insert,
2119 crate::VimMode::Visual => Mode::Visual,
2120 crate::VimMode::VisualLine => Mode::VisualLine,
2121 crate::VimMode::VisualBlock => Mode::VisualBlock,
2122 };
2123 ed.vim.mode = internal;
2124 ed.vim.current_mode = mode;
2125}
2126
2127pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2144 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2145 ch: char,
2146) {
2147 if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
2148 let pos = ed.cursor();
2153 ed.set_mark(ch, pos);
2154 }
2155 }
2157
2158pub(crate) fn goto_mark<H: crate::types::Host>(
2167 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2168 ch: char,
2169 linewise: bool,
2170) {
2171 let target = match ch {
2172 'a'..='z' | 'A'..='Z' => ed.mark(ch),
2173 '\'' | '`' => ed.vim.jump_back.last().copied(),
2174 '.' => ed.vim.last_edit_pos,
2175 '[' | ']' | '<' | '>' => ed.mark(ch),
2176 _ => None,
2177 };
2178 let Some((row, col)) = target else {
2179 return;
2180 };
2181 let pre = ed.cursor();
2182 let (r, c_clamped) = clamp_pos(ed, (row, col));
2183 if linewise {
2184 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2185 ed.push_buffer_cursor_to_textarea();
2186 move_first_non_whitespace(ed);
2187 } else {
2188 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2189 ed.push_buffer_cursor_to_textarea();
2190 }
2191 if ed.cursor() != pre {
2192 ed.push_jump(pre);
2193 }
2194 ed.sticky_col = Some(ed.cursor().1);
2195}
2196
2197pub fn op_is_change(op: Operator) -> bool {
2201 matches!(op, Operator::Delete | Operator::Change)
2202}
2203
2204pub(crate) const JUMPLIST_MAX: usize = 100;
2208
2209fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2212 let Some(target) = ed.vim.jump_back.pop() else {
2213 return;
2214 };
2215 let cur = ed.cursor();
2216 ed.vim.jump_fwd.push(cur);
2217 let (r, c) = clamp_pos(ed, target);
2218 ed.jump_cursor(r, c);
2219 ed.sticky_col = Some(c);
2220}
2221
2222fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2225 let Some(target) = ed.vim.jump_fwd.pop() else {
2226 return;
2227 };
2228 let cur = ed.cursor();
2229 ed.vim.jump_back.push(cur);
2230 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2231 ed.vim.jump_back.remove(0);
2232 }
2233 let (r, c) = clamp_pos(ed, target);
2234 ed.jump_cursor(r, c);
2235 ed.sticky_col = Some(c);
2236}
2237
2238fn clamp_pos<H: crate::types::Host>(
2241 ed: &Editor<hjkl_buffer::Buffer, H>,
2242 pos: (usize, usize),
2243) -> (usize, usize) {
2244 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2245 let r = pos.0.min(last_row);
2246 let line_len = buf_line_chars(&ed.buffer, r);
2247 let c = pos.1.min(line_len.saturating_sub(1));
2248 (r, c)
2249}
2250
2251fn is_big_jump(motion: &Motion) -> bool {
2253 matches!(
2254 motion,
2255 Motion::FileTop
2256 | Motion::FileBottom
2257 | Motion::MatchBracket
2258 | Motion::WordAtCursor { .. }
2259 | Motion::SearchNext { .. }
2260 | Motion::ViewportTop
2261 | Motion::ViewportMiddle
2262 | Motion::ViewportBottom
2263 )
2264}
2265
2266fn viewport_half_rows<H: crate::types::Host>(
2271 ed: &Editor<hjkl_buffer::Buffer, H>,
2272 count: usize,
2273) -> usize {
2274 let h = ed.viewport_height_value() as usize;
2275 (h / 2).max(1).saturating_mul(count.max(1))
2276}
2277
2278fn viewport_full_rows<H: crate::types::Host>(
2281 ed: &Editor<hjkl_buffer::Buffer, H>,
2282 count: usize,
2283) -> usize {
2284 let h = ed.viewport_height_value() as usize;
2285 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2286}
2287
2288fn scroll_cursor_rows<H: crate::types::Host>(
2293 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2294 delta: isize,
2295) {
2296 if delta == 0 {
2297 return;
2298 }
2299 ed.sync_buffer_content_from_textarea();
2300 let (row, _) = ed.cursor();
2301 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2302 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2303 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2304 crate::motions::move_first_non_blank(&mut ed.buffer);
2305 ed.push_buffer_cursor_to_textarea();
2306 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2307}
2308
2309pub fn parse_motion(input: &Input) -> Option<Motion> {
2315 if input.ctrl {
2316 return None;
2317 }
2318 match input.key {
2319 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2320 Key::Char('l') | Key::Right => Some(Motion::Right),
2321 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2322 Key::Char('k') | Key::Up => Some(Motion::Up),
2323 Key::Char('w') => Some(Motion::WordFwd),
2324 Key::Char('W') => Some(Motion::BigWordFwd),
2325 Key::Char('b') => Some(Motion::WordBack),
2326 Key::Char('B') => Some(Motion::BigWordBack),
2327 Key::Char('e') => Some(Motion::WordEnd),
2328 Key::Char('E') => Some(Motion::BigWordEnd),
2329 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2330 Key::Char('^') => Some(Motion::FirstNonBlank),
2331 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2332 Key::Char('G') => Some(Motion::FileBottom),
2333 Key::Char('%') => Some(Motion::MatchBracket),
2334 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2335 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2336 Key::Char('*') => Some(Motion::WordAtCursor {
2337 forward: true,
2338 whole_word: true,
2339 }),
2340 Key::Char('#') => Some(Motion::WordAtCursor {
2341 forward: false,
2342 whole_word: true,
2343 }),
2344 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2345 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2346 Key::Char('H') => Some(Motion::ViewportTop),
2347 Key::Char('M') => Some(Motion::ViewportMiddle),
2348 Key::Char('L') => Some(Motion::ViewportBottom),
2349 Key::Char('{') => Some(Motion::ParagraphPrev),
2350 Key::Char('}') => Some(Motion::ParagraphNext),
2351 Key::Char('(') => Some(Motion::SentencePrev),
2352 Key::Char(')') => Some(Motion::SentenceNext),
2353 _ => None,
2354 }
2355}
2356
2357pub(crate) fn execute_motion<H: crate::types::Host>(
2360 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2361 motion: Motion,
2362 count: usize,
2363) {
2364 let count = count.max(1);
2365 let motion = match motion {
2367 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2368 Some((ch, forward, till)) => Motion::Find {
2369 ch,
2370 forward: if reverse { !forward } else { forward },
2371 till,
2372 },
2373 None => return,
2374 },
2375 other => other,
2376 };
2377 let pre_pos = ed.cursor();
2378 let pre_col = pre_pos.1;
2379 apply_motion_cursor(ed, &motion, count);
2380 let post_pos = ed.cursor();
2381 if is_big_jump(&motion) && pre_pos != post_pos {
2382 ed.push_jump(pre_pos);
2383 }
2384 apply_sticky_col(ed, &motion, pre_col);
2385 ed.sync_buffer_from_textarea();
2390}
2391
2392fn execute_motion_with_block_vcol<H: crate::types::Host>(
2403 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2404 motion: Motion,
2405 count: usize,
2406) {
2407 let motion_copy = motion.clone();
2408 execute_motion(ed, motion, count);
2409 if ed.vim.mode == Mode::VisualBlock {
2410 update_block_vcol(ed, &motion_copy);
2411 }
2412}
2413
2414pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2442 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2443 kind: crate::MotionKind,
2444 count: usize,
2445) {
2446 let count = count.max(1);
2447 match kind {
2448 crate::MotionKind::CharLeft => {
2449 execute_motion_with_block_vcol(ed, Motion::Left, count);
2450 }
2451 crate::MotionKind::CharRight => {
2452 execute_motion_with_block_vcol(ed, Motion::Right, count);
2453 }
2454 crate::MotionKind::LineDown => {
2455 execute_motion_with_block_vcol(ed, Motion::Down, count);
2456 }
2457 crate::MotionKind::LineUp => {
2458 execute_motion_with_block_vcol(ed, Motion::Up, count);
2459 }
2460 crate::MotionKind::FirstNonBlankDown => {
2461 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2466 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2467 crate::motions::move_first_non_blank(&mut ed.buffer);
2468 ed.push_buffer_cursor_to_textarea();
2469 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2470 ed.sync_buffer_from_textarea();
2471 }
2472 crate::MotionKind::FirstNonBlankUp => {
2473 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2476 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2477 crate::motions::move_first_non_blank(&mut ed.buffer);
2478 ed.push_buffer_cursor_to_textarea();
2479 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2480 ed.sync_buffer_from_textarea();
2481 }
2482 crate::MotionKind::WordForward => {
2483 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
2484 }
2485 crate::MotionKind::BigWordForward => {
2486 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
2487 }
2488 crate::MotionKind::WordBackward => {
2489 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
2490 }
2491 crate::MotionKind::BigWordBackward => {
2492 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
2493 }
2494 crate::MotionKind::WordEnd => {
2495 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
2496 }
2497 crate::MotionKind::BigWordEnd => {
2498 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
2499 }
2500 crate::MotionKind::LineStart => {
2501 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
2504 }
2505 crate::MotionKind::FirstNonBlank => {
2506 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
2509 }
2510 crate::MotionKind::GotoLine => {
2511 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
2520 }
2521 crate::MotionKind::LineEnd => {
2522 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
2526 }
2527 crate::MotionKind::FindRepeat => {
2528 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
2532 }
2533 crate::MotionKind::FindRepeatReverse => {
2534 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
2538 }
2539 crate::MotionKind::BracketMatch => {
2540 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
2545 }
2546 crate::MotionKind::ViewportTop => {
2547 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
2550 }
2551 crate::MotionKind::ViewportMiddle => {
2552 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
2555 }
2556 crate::MotionKind::ViewportBottom => {
2557 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
2560 }
2561 crate::MotionKind::HalfPageDown => {
2562 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
2566 }
2567 crate::MotionKind::HalfPageUp => {
2568 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
2571 }
2572 crate::MotionKind::FullPageDown => {
2573 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
2576 }
2577 crate::MotionKind::FullPageUp => {
2578 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
2581 }
2582 }
2583}
2584
2585fn apply_sticky_col<H: crate::types::Host>(
2590 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2591 motion: &Motion,
2592 pre_col: usize,
2593) {
2594 if is_vertical_motion(motion) {
2595 let want = ed.sticky_col.unwrap_or(pre_col);
2596 ed.sticky_col = Some(want);
2599 let (row, _) = ed.cursor();
2600 let line_len = buf_line_chars(&ed.buffer, row);
2601 let max_col = line_len.saturating_sub(1);
2605 let target = want.min(max_col);
2606 ed.jump_cursor(row, target);
2607 } else {
2608 ed.sticky_col = Some(ed.cursor().1);
2611 }
2612}
2613
2614fn is_vertical_motion(motion: &Motion) -> bool {
2615 matches!(
2619 motion,
2620 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2621 )
2622}
2623
2624fn apply_motion_cursor<H: crate::types::Host>(
2625 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2626 motion: &Motion,
2627 count: usize,
2628) {
2629 apply_motion_cursor_ctx(ed, motion, count, false)
2630}
2631
2632fn apply_motion_cursor_ctx<H: crate::types::Host>(
2633 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2634 motion: &Motion,
2635 count: usize,
2636 as_operator: bool,
2637) {
2638 match motion {
2639 Motion::Left => {
2640 crate::motions::move_left(&mut ed.buffer, count);
2642 ed.push_buffer_cursor_to_textarea();
2643 }
2644 Motion::Right => {
2645 if as_operator {
2649 crate::motions::move_right_to_end(&mut ed.buffer, count);
2650 } else {
2651 crate::motions::move_right_in_line(&mut ed.buffer, count);
2652 }
2653 ed.push_buffer_cursor_to_textarea();
2654 }
2655 Motion::Up => {
2656 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2660 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2661 ed.push_buffer_cursor_to_textarea();
2662 }
2663 Motion::Down => {
2664 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2665 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2666 ed.push_buffer_cursor_to_textarea();
2667 }
2668 Motion::ScreenUp => {
2669 let v = *ed.host.viewport();
2670 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2671 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2672 ed.push_buffer_cursor_to_textarea();
2673 }
2674 Motion::ScreenDown => {
2675 let v = *ed.host.viewport();
2676 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2677 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2678 ed.push_buffer_cursor_to_textarea();
2679 }
2680 Motion::WordFwd => {
2681 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2682 ed.push_buffer_cursor_to_textarea();
2683 }
2684 Motion::WordBack => {
2685 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2686 ed.push_buffer_cursor_to_textarea();
2687 }
2688 Motion::WordEnd => {
2689 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2690 ed.push_buffer_cursor_to_textarea();
2691 }
2692 Motion::BigWordFwd => {
2693 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2694 ed.push_buffer_cursor_to_textarea();
2695 }
2696 Motion::BigWordBack => {
2697 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2698 ed.push_buffer_cursor_to_textarea();
2699 }
2700 Motion::BigWordEnd => {
2701 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2702 ed.push_buffer_cursor_to_textarea();
2703 }
2704 Motion::WordEndBack => {
2705 crate::motions::move_word_end_back(
2706 &mut ed.buffer,
2707 false,
2708 count,
2709 &ed.settings.iskeyword,
2710 );
2711 ed.push_buffer_cursor_to_textarea();
2712 }
2713 Motion::BigWordEndBack => {
2714 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2715 ed.push_buffer_cursor_to_textarea();
2716 }
2717 Motion::LineStart => {
2718 crate::motions::move_line_start(&mut ed.buffer);
2719 ed.push_buffer_cursor_to_textarea();
2720 }
2721 Motion::FirstNonBlank => {
2722 crate::motions::move_first_non_blank(&mut ed.buffer);
2723 ed.push_buffer_cursor_to_textarea();
2724 }
2725 Motion::LineEnd => {
2726 crate::motions::move_line_end(&mut ed.buffer);
2728 ed.push_buffer_cursor_to_textarea();
2729 }
2730 Motion::FileTop => {
2731 if count > 1 {
2734 crate::motions::move_bottom(&mut ed.buffer, count);
2735 } else {
2736 crate::motions::move_top(&mut ed.buffer);
2737 }
2738 ed.push_buffer_cursor_to_textarea();
2739 }
2740 Motion::FileBottom => {
2741 if count > 1 {
2744 crate::motions::move_bottom(&mut ed.buffer, count);
2745 } else {
2746 crate::motions::move_bottom(&mut ed.buffer, 0);
2747 }
2748 ed.push_buffer_cursor_to_textarea();
2749 }
2750 Motion::Find { ch, forward, till } => {
2751 for _ in 0..count {
2752 if !find_char_on_line(ed, *ch, *forward, *till) {
2753 break;
2754 }
2755 }
2756 }
2757 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2759 let _ = matching_bracket(ed);
2760 }
2761 Motion::WordAtCursor {
2762 forward,
2763 whole_word,
2764 } => {
2765 word_at_cursor_search(ed, *forward, *whole_word, count);
2766 }
2767 Motion::SearchNext { reverse } => {
2768 if let Some(pattern) = ed.vim.last_search.clone() {
2772 ed.push_search_pattern(&pattern);
2773 }
2774 if ed.search_state().pattern.is_none() {
2775 return;
2776 }
2777 let forward = ed.vim.last_search_forward != *reverse;
2781 for _ in 0..count.max(1) {
2782 if forward {
2783 ed.search_advance_forward(true);
2784 } else {
2785 ed.search_advance_backward(true);
2786 }
2787 }
2788 ed.push_buffer_cursor_to_textarea();
2789 }
2790 Motion::ViewportTop => {
2791 let v = *ed.host().viewport();
2792 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2793 ed.push_buffer_cursor_to_textarea();
2794 }
2795 Motion::ViewportMiddle => {
2796 let v = *ed.host().viewport();
2797 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2798 ed.push_buffer_cursor_to_textarea();
2799 }
2800 Motion::ViewportBottom => {
2801 let v = *ed.host().viewport();
2802 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2803 ed.push_buffer_cursor_to_textarea();
2804 }
2805 Motion::LastNonBlank => {
2806 crate::motions::move_last_non_blank(&mut ed.buffer);
2807 ed.push_buffer_cursor_to_textarea();
2808 }
2809 Motion::LineMiddle => {
2810 let row = ed.cursor().0;
2811 let line_chars = buf_line_chars(&ed.buffer, row);
2812 let target = line_chars / 2;
2815 ed.jump_cursor(row, target);
2816 }
2817 Motion::ParagraphPrev => {
2818 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2819 ed.push_buffer_cursor_to_textarea();
2820 }
2821 Motion::ParagraphNext => {
2822 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2823 ed.push_buffer_cursor_to_textarea();
2824 }
2825 Motion::SentencePrev => {
2826 for _ in 0..count.max(1) {
2827 if let Some((row, col)) = sentence_boundary(ed, false) {
2828 ed.jump_cursor(row, col);
2829 }
2830 }
2831 }
2832 Motion::SentenceNext => {
2833 for _ in 0..count.max(1) {
2834 if let Some((row, col)) = sentence_boundary(ed, true) {
2835 ed.jump_cursor(row, col);
2836 }
2837 }
2838 }
2839 }
2840}
2841
2842fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2843 ed.sync_buffer_content_from_textarea();
2849 crate::motions::move_first_non_blank(&mut ed.buffer);
2850 ed.push_buffer_cursor_to_textarea();
2851}
2852
2853fn find_char_on_line<H: crate::types::Host>(
2854 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2855 ch: char,
2856 forward: bool,
2857 till: bool,
2858) -> bool {
2859 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2860 if moved {
2861 ed.push_buffer_cursor_to_textarea();
2862 }
2863 moved
2864}
2865
2866fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2867 let moved = crate::motions::match_bracket(&mut ed.buffer);
2868 if moved {
2869 ed.push_buffer_cursor_to_textarea();
2870 }
2871 moved
2872}
2873
2874fn word_at_cursor_search<H: crate::types::Host>(
2875 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2876 forward: bool,
2877 whole_word: bool,
2878 count: usize,
2879) {
2880 let (row, col) = ed.cursor();
2881 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2882 let chars: Vec<char> = line.chars().collect();
2883 if chars.is_empty() {
2884 return;
2885 }
2886 let spec = ed.settings().iskeyword.clone();
2888 let is_word = |c: char| is_keyword_char(c, &spec);
2889 let mut start = col.min(chars.len().saturating_sub(1));
2890 while start > 0 && is_word(chars[start - 1]) {
2891 start -= 1;
2892 }
2893 let mut end = start;
2894 while end < chars.len() && is_word(chars[end]) {
2895 end += 1;
2896 }
2897 if end <= start {
2898 return;
2899 }
2900 let word: String = chars[start..end].iter().collect();
2901 let escaped = regex_escape(&word);
2902 let pattern = if whole_word {
2903 format!(r"\b{escaped}\b")
2904 } else {
2905 escaped
2906 };
2907 ed.push_search_pattern(&pattern);
2908 if ed.search_state().pattern.is_none() {
2909 return;
2910 }
2911 ed.vim.last_search = Some(pattern);
2913 ed.vim.last_search_forward = forward;
2914 for _ in 0..count.max(1) {
2915 if forward {
2916 ed.search_advance_forward(true);
2917 } else {
2918 ed.search_advance_backward(true);
2919 }
2920 }
2921 ed.push_buffer_cursor_to_textarea();
2922}
2923
2924fn regex_escape(s: &str) -> String {
2925 let mut out = String::with_capacity(s.len());
2926 for c in s.chars() {
2927 if matches!(
2928 c,
2929 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2930 ) {
2931 out.push('\\');
2932 }
2933 out.push(c);
2934 }
2935 out
2936}
2937
2938pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2952 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2953 op: Operator,
2954 motion_key: char,
2955 total_count: usize,
2956) {
2957 let input = Input {
2958 key: Key::Char(motion_key),
2959 ctrl: false,
2960 alt: false,
2961 shift: false,
2962 };
2963 let Some(motion) = parse_motion(&input) else {
2964 return;
2965 };
2966 let motion = match motion {
2967 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2968 Some((ch, forward, till)) => Motion::Find {
2969 ch,
2970 forward: if reverse { !forward } else { forward },
2971 till,
2972 },
2973 None => return,
2974 },
2975 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2977 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2978 m => m,
2979 };
2980 apply_op_with_motion(ed, op, &motion, total_count);
2981 if let Motion::Find { ch, forward, till } = &motion {
2982 ed.vim.last_find = Some((*ch, *forward, *till));
2983 }
2984 if !ed.vim.replaying && op_is_change(op) {
2985 ed.vim.last_change = Some(LastChange::OpMotion {
2986 op,
2987 motion,
2988 count: total_count,
2989 inserted: None,
2990 });
2991 }
2992}
2993
2994pub(crate) fn apply_op_double<H: crate::types::Host>(
2997 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2998 op: Operator,
2999 total_count: usize,
3000) {
3001 execute_line_op(ed, op, total_count);
3002 if !ed.vim.replaying {
3003 ed.vim.last_change = Some(LastChange::LineOp {
3004 op,
3005 count: total_count,
3006 inserted: None,
3007 });
3008 }
3009}
3010
3011pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3021 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3022 op: Operator,
3023 ch: char,
3024 total_count: usize,
3025) {
3026 if matches!(
3029 op,
3030 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3031 ) {
3032 let op_char = match op {
3033 Operator::Uppercase => 'U',
3034 Operator::Lowercase => 'u',
3035 Operator::ToggleCase => '~',
3036 _ => unreachable!(),
3037 };
3038 if ch == op_char {
3039 execute_line_op(ed, op, total_count);
3040 if !ed.vim.replaying {
3041 ed.vim.last_change = Some(LastChange::LineOp {
3042 op,
3043 count: total_count,
3044 inserted: None,
3045 });
3046 }
3047 return;
3048 }
3049 }
3050 let motion = match ch {
3051 'g' => Motion::FileTop,
3052 'e' => Motion::WordEndBack,
3053 'E' => Motion::BigWordEndBack,
3054 'j' => Motion::ScreenDown,
3055 'k' => Motion::ScreenUp,
3056 _ => return, };
3058 apply_op_with_motion(ed, op, &motion, total_count);
3059 if !ed.vim.replaying && op_is_change(op) {
3060 ed.vim.last_change = Some(LastChange::OpMotion {
3061 op,
3062 motion,
3063 count: total_count,
3064 inserted: None,
3065 });
3066 }
3067}
3068
3069pub(crate) fn apply_after_g<H: crate::types::Host>(
3074 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3075 ch: char,
3076 count: usize,
3077) {
3078 match ch {
3079 'g' => {
3080 let pre = ed.cursor();
3082 if count > 1 {
3083 ed.jump_cursor(count - 1, 0);
3084 } else {
3085 ed.jump_cursor(0, 0);
3086 }
3087 move_first_non_whitespace(ed);
3088 if ed.cursor() != pre {
3089 ed.push_jump(pre);
3090 }
3091 }
3092 'e' => execute_motion(ed, Motion::WordEndBack, count),
3093 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3094 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3096 'M' => execute_motion(ed, Motion::LineMiddle, count),
3098 'v' => ed.reenter_last_visual(),
3101 'j' => execute_motion(ed, Motion::ScreenDown, count),
3105 'k' => execute_motion(ed, Motion::ScreenUp, count),
3106 'U' => {
3110 ed.vim.pending = Pending::Op {
3111 op: Operator::Uppercase,
3112 count1: count,
3113 };
3114 }
3115 'u' => {
3116 ed.vim.pending = Pending::Op {
3117 op: Operator::Lowercase,
3118 count1: count,
3119 };
3120 }
3121 '~' => {
3122 ed.vim.pending = Pending::Op {
3123 op: Operator::ToggleCase,
3124 count1: count,
3125 };
3126 }
3127 'q' => {
3128 ed.vim.pending = Pending::Op {
3131 op: Operator::Reflow,
3132 count1: count,
3133 };
3134 }
3135 'J' => {
3136 for _ in 0..count.max(1) {
3138 ed.push_undo();
3139 join_line_raw(ed);
3140 }
3141 if !ed.vim.replaying {
3142 ed.vim.last_change = Some(LastChange::JoinLine {
3143 count: count.max(1),
3144 });
3145 }
3146 }
3147 'd' => {
3148 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3153 }
3154 'i' => {
3159 if let Some((row, col)) = ed.vim.last_insert_pos {
3160 ed.jump_cursor(row, col);
3161 }
3162 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3163 }
3164 ';' => walk_change_list(ed, -1, count.max(1)),
3167 ',' => walk_change_list(ed, 1, count.max(1)),
3168 '*' => execute_motion(
3172 ed,
3173 Motion::WordAtCursor {
3174 forward: true,
3175 whole_word: false,
3176 },
3177 count,
3178 ),
3179 '#' => execute_motion(
3180 ed,
3181 Motion::WordAtCursor {
3182 forward: false,
3183 whole_word: false,
3184 },
3185 count,
3186 ),
3187 _ => {}
3188 }
3189}
3190
3191pub(crate) fn apply_after_z<H: crate::types::Host>(
3196 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3197 ch: char,
3198 count: usize,
3199) {
3200 use crate::editor::CursorScrollTarget;
3201 let row = ed.cursor().0;
3202 match ch {
3203 'z' => {
3204 ed.scroll_cursor_to(CursorScrollTarget::Center);
3205 ed.vim.viewport_pinned = true;
3206 }
3207 't' => {
3208 ed.scroll_cursor_to(CursorScrollTarget::Top);
3209 ed.vim.viewport_pinned = true;
3210 }
3211 'b' => {
3212 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3213 ed.vim.viewport_pinned = true;
3214 }
3215 'o' => {
3220 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3221 }
3222 'c' => {
3223 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3224 }
3225 'a' => {
3226 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3227 }
3228 'R' => {
3229 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3230 }
3231 'M' => {
3232 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3233 }
3234 'E' => {
3235 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3236 }
3237 'd' => {
3238 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3239 }
3240 'f' => {
3241 if matches!(
3242 ed.vim.mode,
3243 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3244 ) {
3245 let anchor_row = match ed.vim.mode {
3248 Mode::VisualLine => ed.vim.visual_line_anchor,
3249 Mode::VisualBlock => ed.vim.block_anchor.0,
3250 _ => ed.vim.visual_anchor.0,
3251 };
3252 let cur = ed.cursor().0;
3253 let top = anchor_row.min(cur);
3254 let bot = anchor_row.max(cur);
3255 ed.apply_fold_op(crate::types::FoldOp::Add {
3256 start_row: top,
3257 end_row: bot,
3258 closed: true,
3259 });
3260 ed.vim.mode = Mode::Normal;
3261 } else {
3262 ed.vim.pending = Pending::Op {
3267 op: Operator::Fold,
3268 count1: count,
3269 };
3270 }
3271 }
3272 _ => {}
3273 }
3274}
3275
3276pub(crate) fn apply_find_char<H: crate::types::Host>(
3282 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3283 ch: char,
3284 forward: bool,
3285 till: bool,
3286 count: usize,
3287) {
3288 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3289 ed.vim.last_find = Some((ch, forward, till));
3290}
3291
3292pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3299 op: Operator,
3300 ch: char,
3301 forward: bool,
3302 till: bool,
3303 total_count: usize,
3304) {
3305 let motion = Motion::Find { ch, forward, till };
3306 apply_op_with_motion(ed, op, &motion, total_count);
3307 ed.vim.last_find = Some((ch, forward, till));
3308 if !ed.vim.replaying && op_is_change(op) {
3309 ed.vim.last_change = Some(LastChange::OpMotion {
3310 op,
3311 motion,
3312 count: total_count,
3313 inserted: None,
3314 });
3315 }
3316}
3317
3318pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3327 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3328 op: Operator,
3329 ch: char,
3330 inner: bool,
3331 _total_count: usize,
3332) -> bool {
3333 let obj = match ch {
3336 'w' => TextObject::Word { big: false },
3337 'W' => TextObject::Word { big: true },
3338 '"' | '\'' | '`' => TextObject::Quote(ch),
3339 '(' | ')' | 'b' => TextObject::Bracket('('),
3340 '[' | ']' => TextObject::Bracket('['),
3341 '{' | '}' | 'B' => TextObject::Bracket('{'),
3342 '<' | '>' => TextObject::Bracket('<'),
3343 'p' => TextObject::Paragraph,
3344 't' => TextObject::XmlTag,
3345 's' => TextObject::Sentence,
3346 _ => return false,
3347 };
3348 apply_op_with_text_object(ed, op, obj, inner);
3349 if !ed.vim.replaying && op_is_change(op) {
3350 ed.vim.last_change = Some(LastChange::OpTextObj {
3351 op,
3352 obj,
3353 inner,
3354 inserted: None,
3355 });
3356 }
3357 true
3358}
3359
3360pub(crate) fn retreat_one<H: crate::types::Host>(
3362 ed: &Editor<hjkl_buffer::Buffer, H>,
3363 pos: (usize, usize),
3364) -> (usize, usize) {
3365 let (r, c) = pos;
3366 if c > 0 {
3367 (r, c - 1)
3368 } else if r > 0 {
3369 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3370 (r - 1, prev_len)
3371 } else {
3372 (0, 0)
3373 }
3374}
3375
3376fn begin_insert_noundo<H: crate::types::Host>(
3378 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3379 count: usize,
3380 reason: InsertReason,
3381) {
3382 let reason = if ed.vim.replaying {
3383 InsertReason::ReplayOnly
3384 } else {
3385 reason
3386 };
3387 let (row, _) = ed.cursor();
3388 ed.vim.insert_session = Some(InsertSession {
3389 count,
3390 row_min: row,
3391 row_max: row,
3392 before_lines: buf_lines_to_vec(&ed.buffer),
3393 reason,
3394 });
3395 ed.vim.mode = Mode::Insert;
3396 ed.vim.current_mode = crate::VimMode::Insert;
3398}
3399
3400pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
3403 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3404 op: Operator,
3405 motion: &Motion,
3406 count: usize,
3407) {
3408 let start = ed.cursor();
3409 apply_motion_cursor_ctx(ed, motion, count, true);
3414 let end = ed.cursor();
3415 let kind = motion_kind(motion);
3416 ed.jump_cursor(start.0, start.1);
3418 run_operator_over_range(ed, op, start, end, kind);
3419}
3420
3421fn apply_op_with_text_object<H: crate::types::Host>(
3422 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3423 op: Operator,
3424 obj: TextObject,
3425 inner: bool,
3426) {
3427 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3428 return;
3429 };
3430 ed.jump_cursor(start.0, start.1);
3431 run_operator_over_range(ed, op, start, end, kind);
3432}
3433
3434fn motion_kind(motion: &Motion) -> RangeKind {
3435 match motion {
3436 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
3437 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
3438 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3439 RangeKind::Linewise
3440 }
3441 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3442 RangeKind::Inclusive
3443 }
3444 Motion::Find { .. } => RangeKind::Inclusive,
3445 Motion::MatchBracket => RangeKind::Inclusive,
3446 Motion::LineEnd => RangeKind::Inclusive,
3448 _ => RangeKind::Exclusive,
3449 }
3450}
3451
3452fn run_operator_over_range<H: crate::types::Host>(
3453 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3454 op: Operator,
3455 start: (usize, usize),
3456 end: (usize, usize),
3457 kind: RangeKind,
3458) {
3459 let (top, bot) = order(start, end);
3460 if top == bot && !matches!(kind, RangeKind::Linewise) {
3464 return;
3465 }
3466
3467 match op {
3468 Operator::Yank => {
3469 let text = read_vim_range(ed, top, bot, kind);
3470 if !text.is_empty() {
3471 ed.record_yank_to_host(text.clone());
3472 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
3473 }
3474 let rbr = match kind {
3478 RangeKind::Linewise => {
3479 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3480 (bot.0, last_col)
3481 }
3482 RangeKind::Inclusive => (bot.0, bot.1),
3483 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3484 };
3485 ed.set_mark('[', top);
3486 ed.set_mark(']', rbr);
3487 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3488 ed.push_buffer_cursor_to_textarea();
3489 }
3490 Operator::Delete => {
3491 ed.push_undo();
3492 cut_vim_range(ed, top, bot, kind);
3493 if !matches!(kind, RangeKind::Linewise) {
3498 clamp_cursor_to_normal_mode(ed);
3499 }
3500 ed.vim.mode = Mode::Normal;
3501 let pos = ed.cursor();
3505 ed.set_mark('[', pos);
3506 ed.set_mark(']', pos);
3507 }
3508 Operator::Change => {
3509 ed.vim.change_mark_start = Some(top);
3514 ed.push_undo();
3515 cut_vim_range(ed, top, bot, kind);
3516 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3517 }
3518 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3519 apply_case_op_to_selection(ed, op, top, bot, kind);
3520 }
3521 Operator::Indent | Operator::Outdent => {
3522 ed.push_undo();
3525 if op == Operator::Indent {
3526 indent_rows(ed, top.0, bot.0, 1);
3527 } else {
3528 outdent_rows(ed, top.0, bot.0, 1);
3529 }
3530 ed.vim.mode = Mode::Normal;
3531 }
3532 Operator::Fold => {
3533 if bot.0 >= top.0 {
3537 ed.apply_fold_op(crate::types::FoldOp::Add {
3538 start_row: top.0,
3539 end_row: bot.0,
3540 closed: true,
3541 });
3542 }
3543 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3544 ed.push_buffer_cursor_to_textarea();
3545 ed.vim.mode = Mode::Normal;
3546 }
3547 Operator::Reflow => {
3548 ed.push_undo();
3549 reflow_rows(ed, top.0, bot.0);
3550 ed.vim.mode = Mode::Normal;
3551 }
3552 }
3553}
3554
3555pub(crate) fn delete_range_bridge<H: crate::types::Host>(
3572 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3573 start: (usize, usize),
3574 end: (usize, usize),
3575 kind: RangeKind,
3576 register: char,
3577) {
3578 ed.vim.pending_register = Some(register);
3579 run_operator_over_range(ed, Operator::Delete, start, end, kind);
3580}
3581
3582pub(crate) fn yank_range_bridge<H: crate::types::Host>(
3585 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3586 start: (usize, usize),
3587 end: (usize, usize),
3588 kind: RangeKind,
3589 register: char,
3590) {
3591 ed.vim.pending_register = Some(register);
3592 run_operator_over_range(ed, Operator::Yank, start, end, kind);
3593}
3594
3595pub(crate) fn change_range_bridge<H: crate::types::Host>(
3600 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3601 start: (usize, usize),
3602 end: (usize, usize),
3603 kind: RangeKind,
3604 register: char,
3605) {
3606 ed.vim.pending_register = Some(register);
3607 run_operator_over_range(ed, Operator::Change, start, end, kind);
3608}
3609
3610pub(crate) fn indent_range_bridge<H: crate::types::Host>(
3615 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3616 start: (usize, usize),
3617 end: (usize, usize),
3618 count: i32,
3619 shiftwidth: u32,
3620) {
3621 if count == 0 {
3622 return;
3623 }
3624 let (top_row, bot_row) = if start.0 <= end.0 {
3625 (start.0, end.0)
3626 } else {
3627 (end.0, start.0)
3628 };
3629 let original_sw = ed.settings().shiftwidth;
3631 if shiftwidth > 0 {
3632 ed.settings_mut().shiftwidth = shiftwidth as usize;
3633 }
3634 ed.push_undo();
3635 let abs_count = count.unsigned_abs() as usize;
3636 if count > 0 {
3637 indent_rows(ed, top_row, bot_row, abs_count);
3638 } else {
3639 outdent_rows(ed, top_row, bot_row, abs_count);
3640 }
3641 if shiftwidth > 0 {
3642 ed.settings_mut().shiftwidth = original_sw;
3643 }
3644 ed.vim.mode = Mode::Normal;
3645}
3646
3647pub(crate) fn case_range_bridge<H: crate::types::Host>(
3651 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3652 start: (usize, usize),
3653 end: (usize, usize),
3654 kind: RangeKind,
3655 op: Operator,
3656) {
3657 match op {
3658 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
3659 _ => return,
3660 }
3661 let (top, bot) = order(start, end);
3662 apply_case_op_to_selection(ed, op, top, bot, kind);
3663}
3664
3665pub(crate) fn delete_block_bridge<H: crate::types::Host>(
3686 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3687 top_row: usize,
3688 bot_row: usize,
3689 left_col: usize,
3690 right_col: usize,
3691 register: char,
3692) {
3693 ed.vim.pending_register = Some(register);
3694 let saved_anchor = ed.vim.block_anchor;
3695 let saved_vcol = ed.vim.block_vcol;
3696 ed.vim.block_anchor = (top_row, left_col);
3697 ed.vim.block_vcol = right_col;
3698 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3700 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3702 apply_block_operator(ed, Operator::Delete);
3703 ed.vim.block_anchor = saved_anchor;
3707 ed.vim.block_vcol = saved_vcol;
3708}
3709
3710pub(crate) fn yank_block_bridge<H: crate::types::Host>(
3712 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3713 top_row: usize,
3714 bot_row: usize,
3715 left_col: usize,
3716 right_col: usize,
3717 register: char,
3718) {
3719 ed.vim.pending_register = Some(register);
3720 let saved_anchor = ed.vim.block_anchor;
3721 let saved_vcol = ed.vim.block_vcol;
3722 ed.vim.block_anchor = (top_row, left_col);
3723 ed.vim.block_vcol = right_col;
3724 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3725 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3726 apply_block_operator(ed, Operator::Yank);
3727 ed.vim.block_anchor = saved_anchor;
3728 ed.vim.block_vcol = saved_vcol;
3729}
3730
3731pub(crate) fn change_block_bridge<H: crate::types::Host>(
3734 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3735 top_row: usize,
3736 bot_row: usize,
3737 left_col: usize,
3738 right_col: usize,
3739 register: char,
3740) {
3741 ed.vim.pending_register = Some(register);
3742 let saved_anchor = ed.vim.block_anchor;
3743 let saved_vcol = ed.vim.block_vcol;
3744 ed.vim.block_anchor = (top_row, left_col);
3745 ed.vim.block_vcol = right_col;
3746 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3747 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3748 apply_block_operator(ed, Operator::Change);
3749 ed.vim.block_anchor = saved_anchor;
3750 ed.vim.block_vcol = saved_vcol;
3751}
3752
3753pub(crate) fn indent_block_bridge<H: crate::types::Host>(
3757 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3758 top_row: usize,
3759 bot_row: usize,
3760 count: i32,
3761) {
3762 if count == 0 {
3763 return;
3764 }
3765 ed.push_undo();
3766 let abs = count.unsigned_abs() as usize;
3767 if count > 0 {
3768 indent_rows(ed, top_row, bot_row, abs);
3769 } else {
3770 outdent_rows(ed, top_row, bot_row, abs);
3771 }
3772 ed.vim.mode = Mode::Normal;
3773}
3774
3775pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
3786 ed: &Editor<hjkl_buffer::Buffer, H>,
3787) -> Option<((usize, usize), (usize, usize))> {
3788 word_text_object(ed, true, false)
3789}
3790
3791pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
3794 ed: &Editor<hjkl_buffer::Buffer, H>,
3795) -> Option<((usize, usize), (usize, usize))> {
3796 word_text_object(ed, false, false)
3797}
3798
3799pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
3802 ed: &Editor<hjkl_buffer::Buffer, H>,
3803) -> Option<((usize, usize), (usize, usize))> {
3804 word_text_object(ed, true, true)
3805}
3806
3807pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
3810 ed: &Editor<hjkl_buffer::Buffer, H>,
3811) -> Option<((usize, usize), (usize, usize))> {
3812 word_text_object(ed, false, true)
3813}
3814
3815pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
3831 ed: &Editor<hjkl_buffer::Buffer, H>,
3832 quote: char,
3833) -> Option<((usize, usize), (usize, usize))> {
3834 quote_text_object(ed, quote, true)
3835}
3836
3837pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
3840 ed: &Editor<hjkl_buffer::Buffer, H>,
3841 quote: char,
3842) -> Option<((usize, usize), (usize, usize))> {
3843 quote_text_object(ed, quote, false)
3844}
3845
3846pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
3854 ed: &Editor<hjkl_buffer::Buffer, H>,
3855 open: char,
3856) -> Option<((usize, usize), (usize, usize))> {
3857 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
3858}
3859
3860pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
3864 ed: &Editor<hjkl_buffer::Buffer, H>,
3865 open: char,
3866) -> Option<((usize, usize), (usize, usize))> {
3867 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
3868}
3869
3870pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
3875 ed: &Editor<hjkl_buffer::Buffer, H>,
3876) -> Option<((usize, usize), (usize, usize))> {
3877 sentence_text_object(ed, true)
3878}
3879
3880pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
3883 ed: &Editor<hjkl_buffer::Buffer, H>,
3884) -> Option<((usize, usize), (usize, usize))> {
3885 sentence_text_object(ed, false)
3886}
3887
3888pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
3893 ed: &Editor<hjkl_buffer::Buffer, H>,
3894) -> Option<((usize, usize), (usize, usize))> {
3895 paragraph_text_object(ed, true)
3896}
3897
3898pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
3901 ed: &Editor<hjkl_buffer::Buffer, H>,
3902) -> Option<((usize, usize), (usize, usize))> {
3903 paragraph_text_object(ed, false)
3904}
3905
3906pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
3912 ed: &Editor<hjkl_buffer::Buffer, H>,
3913) -> Option<((usize, usize), (usize, usize))> {
3914 tag_text_object(ed, true)
3915}
3916
3917pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
3920 ed: &Editor<hjkl_buffer::Buffer, H>,
3921) -> Option<((usize, usize), (usize, usize))> {
3922 tag_text_object(ed, false)
3923}
3924
3925fn reflow_rows<H: crate::types::Host>(
3930 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3931 top: usize,
3932 bot: usize,
3933) {
3934 let width = ed.settings().textwidth.max(1);
3935 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3936 let bot = bot.min(lines.len().saturating_sub(1));
3937 if top > bot {
3938 return;
3939 }
3940 let original = lines[top..=bot].to_vec();
3941 let mut wrapped: Vec<String> = Vec::new();
3942 let mut paragraph: Vec<String> = Vec::new();
3943 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3944 if para.is_empty() {
3945 return;
3946 }
3947 let words = para.join(" ");
3948 let mut current = String::new();
3949 for word in words.split_whitespace() {
3950 let extra = if current.is_empty() {
3951 word.chars().count()
3952 } else {
3953 current.chars().count() + 1 + word.chars().count()
3954 };
3955 if extra > width && !current.is_empty() {
3956 out.push(std::mem::take(&mut current));
3957 current.push_str(word);
3958 } else if current.is_empty() {
3959 current.push_str(word);
3960 } else {
3961 current.push(' ');
3962 current.push_str(word);
3963 }
3964 }
3965 if !current.is_empty() {
3966 out.push(current);
3967 }
3968 para.clear();
3969 };
3970 for line in &original {
3971 if line.trim().is_empty() {
3972 flush(&mut paragraph, &mut wrapped, width);
3973 wrapped.push(String::new());
3974 } else {
3975 paragraph.push(line.clone());
3976 }
3977 }
3978 flush(&mut paragraph, &mut wrapped, width);
3979
3980 let after: Vec<String> = lines.split_off(bot + 1);
3982 lines.truncate(top);
3983 lines.extend(wrapped);
3984 lines.extend(after);
3985 ed.restore(lines, (top, 0));
3986 ed.mark_content_dirty();
3987}
3988
3989fn apply_case_op_to_selection<H: crate::types::Host>(
3995 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3996 op: Operator,
3997 top: (usize, usize),
3998 bot: (usize, usize),
3999 kind: RangeKind,
4000) {
4001 use hjkl_buffer::Edit;
4002 ed.push_undo();
4003 let saved_yank = ed.yank().to_string();
4004 let saved_yank_linewise = ed.vim.yank_linewise;
4005 let selection = cut_vim_range(ed, top, bot, kind);
4006 let transformed = match op {
4007 Operator::Uppercase => selection.to_uppercase(),
4008 Operator::Lowercase => selection.to_lowercase(),
4009 Operator::ToggleCase => toggle_case_str(&selection),
4010 _ => unreachable!(),
4011 };
4012 if !transformed.is_empty() {
4013 let cursor = buf_cursor_pos(&ed.buffer);
4014 ed.mutate_edit(Edit::InsertStr {
4015 at: cursor,
4016 text: transformed,
4017 });
4018 }
4019 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4020 ed.push_buffer_cursor_to_textarea();
4021 ed.set_yank(saved_yank);
4022 ed.vim.yank_linewise = saved_yank_linewise;
4023 ed.vim.mode = Mode::Normal;
4024}
4025
4026fn indent_rows<H: crate::types::Host>(
4031 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4032 top: usize,
4033 bot: usize,
4034 count: usize,
4035) {
4036 ed.sync_buffer_content_from_textarea();
4037 let width = ed.settings().shiftwidth * count.max(1);
4038 let pad: String = " ".repeat(width);
4039 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4040 let bot = bot.min(lines.len().saturating_sub(1));
4041 for line in lines.iter_mut().take(bot + 1).skip(top) {
4042 if !line.is_empty() {
4043 line.insert_str(0, &pad);
4044 }
4045 }
4046 ed.restore(lines, (top, 0));
4049 move_first_non_whitespace(ed);
4050}
4051
4052fn outdent_rows<H: crate::types::Host>(
4056 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4057 top: usize,
4058 bot: usize,
4059 count: usize,
4060) {
4061 ed.sync_buffer_content_from_textarea();
4062 let width = ed.settings().shiftwidth * count.max(1);
4063 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4064 let bot = bot.min(lines.len().saturating_sub(1));
4065 for line in lines.iter_mut().take(bot + 1).skip(top) {
4066 let strip: usize = line
4067 .chars()
4068 .take(width)
4069 .take_while(|c| *c == ' ' || *c == '\t')
4070 .count();
4071 if strip > 0 {
4072 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4073 line.drain(..byte_len);
4074 }
4075 }
4076 ed.restore(lines, (top, 0));
4077 move_first_non_whitespace(ed);
4078}
4079
4080fn toggle_case_str(s: &str) -> String {
4081 s.chars()
4082 .map(|c| {
4083 if c.is_lowercase() {
4084 c.to_uppercase().next().unwrap_or(c)
4085 } else if c.is_uppercase() {
4086 c.to_lowercase().next().unwrap_or(c)
4087 } else {
4088 c
4089 }
4090 })
4091 .collect()
4092}
4093
4094fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4095 if a <= b { (a, b) } else { (b, a) }
4096}
4097
4098fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4103 let (row, col) = ed.cursor();
4104 let line_chars = buf_line_chars(&ed.buffer, row);
4105 let max_col = line_chars.saturating_sub(1);
4106 if col > max_col {
4107 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4108 ed.push_buffer_cursor_to_textarea();
4109 }
4110}
4111
4112fn execute_line_op<H: crate::types::Host>(
4115 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4116 op: Operator,
4117 count: usize,
4118) {
4119 let (row, col) = ed.cursor();
4120 let total = buf_row_count(&ed.buffer);
4121 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4122
4123 match op {
4124 Operator::Yank => {
4125 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4127 if !text.is_empty() {
4128 ed.record_yank_to_host(text.clone());
4129 ed.record_yank(text, true);
4130 }
4131 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4134 ed.set_mark('[', (row, 0));
4135 ed.set_mark(']', (end_row, last_col));
4136 buf_set_cursor_rc(&mut ed.buffer, row, col);
4137 ed.push_buffer_cursor_to_textarea();
4138 ed.vim.mode = Mode::Normal;
4139 }
4140 Operator::Delete => {
4141 ed.push_undo();
4142 let deleted_through_last = end_row + 1 >= total;
4143 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4144 let total_after = buf_row_count(&ed.buffer);
4148 let raw_target = if deleted_through_last {
4149 row.saturating_sub(1).min(total_after.saturating_sub(1))
4150 } else {
4151 row.min(total_after.saturating_sub(1))
4152 };
4153 let target_row = if raw_target > 0
4159 && raw_target + 1 == total_after
4160 && buf_line(&ed.buffer, raw_target)
4161 .map(str::is_empty)
4162 .unwrap_or(false)
4163 {
4164 raw_target - 1
4165 } else {
4166 raw_target
4167 };
4168 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4169 ed.push_buffer_cursor_to_textarea();
4170 move_first_non_whitespace(ed);
4171 ed.sticky_col = Some(ed.cursor().1);
4172 ed.vim.mode = Mode::Normal;
4173 let pos = ed.cursor();
4176 ed.set_mark('[', pos);
4177 ed.set_mark(']', pos);
4178 }
4179 Operator::Change => {
4180 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4184 ed.vim.change_mark_start = Some((row, 0));
4186 ed.push_undo();
4187 ed.sync_buffer_content_from_textarea();
4188 let payload = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4190 if end_row > row {
4191 ed.mutate_edit(Edit::DeleteRange {
4192 start: Position::new(row + 1, 0),
4193 end: Position::new(end_row, 0),
4194 kind: BufKind::Line,
4195 });
4196 }
4197 let line_chars = buf_line_chars(&ed.buffer, row);
4198 if line_chars > 0 {
4199 ed.mutate_edit(Edit::DeleteRange {
4200 start: Position::new(row, 0),
4201 end: Position::new(row, line_chars),
4202 kind: BufKind::Char,
4203 });
4204 }
4205 if !payload.is_empty() {
4206 ed.record_yank_to_host(payload.clone());
4207 ed.record_delete(payload, true);
4208 }
4209 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4210 ed.push_buffer_cursor_to_textarea();
4211 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4212 }
4213 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4214 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
4218 move_first_non_whitespace(ed);
4221 }
4222 Operator::Indent | Operator::Outdent => {
4223 ed.push_undo();
4225 if op == Operator::Indent {
4226 indent_rows(ed, row, end_row, 1);
4227 } else {
4228 outdent_rows(ed, row, end_row, 1);
4229 }
4230 ed.sticky_col = Some(ed.cursor().1);
4231 ed.vim.mode = Mode::Normal;
4232 }
4233 Operator::Fold => unreachable!("Fold has no line-op double"),
4235 Operator::Reflow => {
4236 ed.push_undo();
4238 reflow_rows(ed, row, end_row);
4239 move_first_non_whitespace(ed);
4240 ed.sticky_col = Some(ed.cursor().1);
4241 ed.vim.mode = Mode::Normal;
4242 }
4243 }
4244}
4245
4246pub(crate) fn apply_visual_operator<H: crate::types::Host>(
4249 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4250 op: Operator,
4251) {
4252 match ed.vim.mode {
4253 Mode::VisualLine => {
4254 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4255 let top = cursor_row.min(ed.vim.visual_line_anchor);
4256 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4257 ed.vim.yank_linewise = true;
4258 match op {
4259 Operator::Yank => {
4260 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4261 if !text.is_empty() {
4262 ed.record_yank_to_host(text.clone());
4263 ed.record_yank(text, true);
4264 }
4265 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4266 ed.push_buffer_cursor_to_textarea();
4267 ed.vim.mode = Mode::Normal;
4268 }
4269 Operator::Delete => {
4270 ed.push_undo();
4271 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4272 ed.vim.mode = Mode::Normal;
4273 }
4274 Operator::Change => {
4275 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4278 ed.push_undo();
4279 ed.sync_buffer_content_from_textarea();
4280 let payload = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4281 if bot > top {
4282 ed.mutate_edit(Edit::DeleteRange {
4283 start: Position::new(top + 1, 0),
4284 end: Position::new(bot, 0),
4285 kind: BufKind::Line,
4286 });
4287 }
4288 let line_chars = buf_line_chars(&ed.buffer, top);
4289 if line_chars > 0 {
4290 ed.mutate_edit(Edit::DeleteRange {
4291 start: Position::new(top, 0),
4292 end: Position::new(top, line_chars),
4293 kind: BufKind::Char,
4294 });
4295 }
4296 if !payload.is_empty() {
4297 ed.record_yank_to_host(payload.clone());
4298 ed.record_delete(payload, true);
4299 }
4300 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4301 ed.push_buffer_cursor_to_textarea();
4302 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4303 }
4304 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4305 let bot = buf_cursor_pos(&ed.buffer)
4306 .row
4307 .max(ed.vim.visual_line_anchor);
4308 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
4309 move_first_non_whitespace(ed);
4310 }
4311 Operator::Indent | Operator::Outdent => {
4312 ed.push_undo();
4313 let (cursor_row, _) = ed.cursor();
4314 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4315 if op == Operator::Indent {
4316 indent_rows(ed, top, bot, 1);
4317 } else {
4318 outdent_rows(ed, top, bot, 1);
4319 }
4320 ed.vim.mode = Mode::Normal;
4321 }
4322 Operator::Reflow => {
4323 ed.push_undo();
4324 let (cursor_row, _) = ed.cursor();
4325 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4326 reflow_rows(ed, top, bot);
4327 ed.vim.mode = Mode::Normal;
4328 }
4329 Operator::Fold => unreachable!("Visual zf takes its own path"),
4332 }
4333 }
4334 Mode::Visual => {
4335 ed.vim.yank_linewise = false;
4336 let anchor = ed.vim.visual_anchor;
4337 let cursor = ed.cursor();
4338 let (top, bot) = order(anchor, cursor);
4339 match op {
4340 Operator::Yank => {
4341 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
4342 if !text.is_empty() {
4343 ed.record_yank_to_host(text.clone());
4344 ed.record_yank(text, false);
4345 }
4346 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4347 ed.push_buffer_cursor_to_textarea();
4348 ed.vim.mode = Mode::Normal;
4349 }
4350 Operator::Delete => {
4351 ed.push_undo();
4352 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4353 ed.vim.mode = Mode::Normal;
4354 }
4355 Operator::Change => {
4356 ed.push_undo();
4357 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4358 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4359 }
4360 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4361 let anchor = ed.vim.visual_anchor;
4363 let cursor = ed.cursor();
4364 let (top, bot) = order(anchor, cursor);
4365 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
4366 }
4367 Operator::Indent | Operator::Outdent => {
4368 ed.push_undo();
4369 let anchor = ed.vim.visual_anchor;
4370 let cursor = ed.cursor();
4371 let (top, bot) = order(anchor, cursor);
4372 if op == Operator::Indent {
4373 indent_rows(ed, top.0, bot.0, 1);
4374 } else {
4375 outdent_rows(ed, top.0, bot.0, 1);
4376 }
4377 ed.vim.mode = Mode::Normal;
4378 }
4379 Operator::Reflow => {
4380 ed.push_undo();
4381 let anchor = ed.vim.visual_anchor;
4382 let cursor = ed.cursor();
4383 let (top, bot) = order(anchor, cursor);
4384 reflow_rows(ed, top.0, bot.0);
4385 ed.vim.mode = Mode::Normal;
4386 }
4387 Operator::Fold => unreachable!("Visual zf takes its own path"),
4388 }
4389 }
4390 Mode::VisualBlock => apply_block_operator(ed, op),
4391 _ => {}
4392 }
4393}
4394
4395fn block_bounds<H: crate::types::Host>(
4400 ed: &Editor<hjkl_buffer::Buffer, H>,
4401) -> (usize, usize, usize, usize) {
4402 let (ar, ac) = ed.vim.block_anchor;
4403 let (cr, _) = ed.cursor();
4404 let cc = ed.vim.block_vcol;
4405 let top = ar.min(cr);
4406 let bot = ar.max(cr);
4407 let left = ac.min(cc);
4408 let right = ac.max(cc);
4409 (top, bot, left, right)
4410}
4411
4412pub(crate) fn update_block_vcol<H: crate::types::Host>(
4417 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4418 motion: &Motion,
4419) {
4420 match motion {
4421 Motion::Left
4422 | Motion::Right
4423 | Motion::WordFwd
4424 | Motion::BigWordFwd
4425 | Motion::WordBack
4426 | Motion::BigWordBack
4427 | Motion::WordEnd
4428 | Motion::BigWordEnd
4429 | Motion::WordEndBack
4430 | Motion::BigWordEndBack
4431 | Motion::LineStart
4432 | Motion::FirstNonBlank
4433 | Motion::LineEnd
4434 | Motion::Find { .. }
4435 | Motion::FindRepeat { .. }
4436 | Motion::MatchBracket => {
4437 ed.vim.block_vcol = ed.cursor().1;
4438 }
4439 _ => {}
4441 }
4442}
4443
4444fn apply_block_operator<H: crate::types::Host>(
4449 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4450 op: Operator,
4451) {
4452 let (top, bot, left, right) = block_bounds(ed);
4453 let yank = block_yank(ed, top, bot, left, right);
4455
4456 match op {
4457 Operator::Yank => {
4458 if !yank.is_empty() {
4459 ed.record_yank_to_host(yank.clone());
4460 ed.record_yank(yank, false);
4461 }
4462 ed.vim.mode = Mode::Normal;
4463 ed.jump_cursor(top, left);
4464 }
4465 Operator::Delete => {
4466 ed.push_undo();
4467 delete_block_contents(ed, top, bot, left, right);
4468 if !yank.is_empty() {
4469 ed.record_yank_to_host(yank.clone());
4470 ed.record_delete(yank, false);
4471 }
4472 ed.vim.mode = Mode::Normal;
4473 ed.jump_cursor(top, left);
4474 }
4475 Operator::Change => {
4476 ed.push_undo();
4477 delete_block_contents(ed, top, bot, left, right);
4478 if !yank.is_empty() {
4479 ed.record_yank_to_host(yank.clone());
4480 ed.record_delete(yank, false);
4481 }
4482 ed.jump_cursor(top, left);
4483 begin_insert_noundo(
4484 ed,
4485 1,
4486 InsertReason::BlockChange {
4487 top,
4488 bot,
4489 col: left,
4490 },
4491 );
4492 }
4493 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4494 ed.push_undo();
4495 transform_block_case(ed, op, top, bot, left, right);
4496 ed.vim.mode = Mode::Normal;
4497 ed.jump_cursor(top, left);
4498 }
4499 Operator::Indent | Operator::Outdent => {
4500 ed.push_undo();
4504 if op == Operator::Indent {
4505 indent_rows(ed, top, bot, 1);
4506 } else {
4507 outdent_rows(ed, top, bot, 1);
4508 }
4509 ed.vim.mode = Mode::Normal;
4510 }
4511 Operator::Fold => unreachable!("Visual zf takes its own path"),
4512 Operator::Reflow => {
4513 ed.push_undo();
4517 reflow_rows(ed, top, bot);
4518 ed.vim.mode = Mode::Normal;
4519 }
4520 }
4521}
4522
4523fn transform_block_case<H: crate::types::Host>(
4527 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4528 op: Operator,
4529 top: usize,
4530 bot: usize,
4531 left: usize,
4532 right: usize,
4533) {
4534 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4535 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4536 let chars: Vec<char> = lines[r].chars().collect();
4537 if left >= chars.len() {
4538 continue;
4539 }
4540 let end = (right + 1).min(chars.len());
4541 let head: String = chars[..left].iter().collect();
4542 let mid: String = chars[left..end].iter().collect();
4543 let tail: String = chars[end..].iter().collect();
4544 let transformed = match op {
4545 Operator::Uppercase => mid.to_uppercase(),
4546 Operator::Lowercase => mid.to_lowercase(),
4547 Operator::ToggleCase => toggle_case_str(&mid),
4548 _ => mid,
4549 };
4550 lines[r] = format!("{head}{transformed}{tail}");
4551 }
4552 let saved_yank = ed.yank().to_string();
4553 let saved_linewise = ed.vim.yank_linewise;
4554 ed.restore(lines, (top, left));
4555 ed.set_yank(saved_yank);
4556 ed.vim.yank_linewise = saved_linewise;
4557}
4558
4559fn block_yank<H: crate::types::Host>(
4560 ed: &Editor<hjkl_buffer::Buffer, H>,
4561 top: usize,
4562 bot: usize,
4563 left: usize,
4564 right: usize,
4565) -> String {
4566 let lines = buf_lines_to_vec(&ed.buffer);
4567 let mut rows: Vec<String> = Vec::new();
4568 for r in top..=bot {
4569 let line = match lines.get(r) {
4570 Some(l) => l,
4571 None => break,
4572 };
4573 let chars: Vec<char> = line.chars().collect();
4574 let end = (right + 1).min(chars.len());
4575 if left >= chars.len() {
4576 rows.push(String::new());
4577 } else {
4578 rows.push(chars[left..end].iter().collect());
4579 }
4580 }
4581 rows.join("\n")
4582}
4583
4584fn delete_block_contents<H: crate::types::Host>(
4585 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4586 top: usize,
4587 bot: usize,
4588 left: usize,
4589 right: usize,
4590) {
4591 use hjkl_buffer::{Edit, MotionKind, Position};
4592 ed.sync_buffer_content_from_textarea();
4593 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4594 if last_row < top {
4595 return;
4596 }
4597 ed.mutate_edit(Edit::DeleteRange {
4598 start: Position::new(top, left),
4599 end: Position::new(last_row, right),
4600 kind: MotionKind::Block,
4601 });
4602 ed.push_buffer_cursor_to_textarea();
4603}
4604
4605pub(crate) fn block_replace<H: crate::types::Host>(
4607 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4608 ch: char,
4609) {
4610 let (top, bot, left, right) = block_bounds(ed);
4611 ed.push_undo();
4612 ed.sync_buffer_content_from_textarea();
4613 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4614 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4615 let chars: Vec<char> = lines[r].chars().collect();
4616 if left >= chars.len() {
4617 continue;
4618 }
4619 let end = (right + 1).min(chars.len());
4620 let before: String = chars[..left].iter().collect();
4621 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4622 let after: String = chars[end..].iter().collect();
4623 lines[r] = format!("{before}{middle}{after}");
4624 }
4625 reset_textarea_lines(ed, lines);
4626 ed.vim.mode = Mode::Normal;
4627 ed.jump_cursor(top, left);
4628}
4629
4630fn reset_textarea_lines<H: crate::types::Host>(
4634 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4635 lines: Vec<String>,
4636) {
4637 let cursor = ed.cursor();
4638 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4639 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4640 ed.mark_content_dirty();
4641}
4642
4643type Pos = (usize, usize);
4649
4650pub(crate) fn text_object_range<H: crate::types::Host>(
4654 ed: &Editor<hjkl_buffer::Buffer, H>,
4655 obj: TextObject,
4656 inner: bool,
4657) -> Option<(Pos, Pos, RangeKind)> {
4658 match obj {
4659 TextObject::Word { big } => {
4660 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
4661 }
4662 TextObject::Quote(q) => {
4663 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4664 }
4665 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4666 TextObject::Paragraph => {
4667 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
4668 }
4669 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
4670 TextObject::Sentence => {
4671 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4672 }
4673 }
4674}
4675
4676fn sentence_boundary<H: crate::types::Host>(
4680 ed: &Editor<hjkl_buffer::Buffer, H>,
4681 forward: bool,
4682) -> Option<(usize, usize)> {
4683 let lines = buf_lines_to_vec(&ed.buffer);
4684 if lines.is_empty() {
4685 return None;
4686 }
4687 let pos_to_idx = |pos: (usize, usize)| -> usize {
4688 let mut idx = 0;
4689 for line in lines.iter().take(pos.0) {
4690 idx += line.chars().count() + 1;
4691 }
4692 idx + pos.1
4693 };
4694 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4695 for (r, line) in lines.iter().enumerate() {
4696 let len = line.chars().count();
4697 if idx <= len {
4698 return (r, idx);
4699 }
4700 idx -= len + 1;
4701 }
4702 let last = lines.len().saturating_sub(1);
4703 (last, lines[last].chars().count())
4704 };
4705 let mut chars: Vec<char> = Vec::new();
4706 for (r, line) in lines.iter().enumerate() {
4707 chars.extend(line.chars());
4708 if r + 1 < lines.len() {
4709 chars.push('\n');
4710 }
4711 }
4712 if chars.is_empty() {
4713 return None;
4714 }
4715 let total = chars.len();
4716 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4717 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4718
4719 if forward {
4720 let mut i = cursor_idx + 1;
4723 while i < total {
4724 if is_terminator(chars[i]) {
4725 while i + 1 < total && is_terminator(chars[i + 1]) {
4726 i += 1;
4727 }
4728 if i + 1 >= total {
4729 return None;
4730 }
4731 if chars[i + 1].is_whitespace() {
4732 let mut j = i + 1;
4733 while j < total && chars[j].is_whitespace() {
4734 j += 1;
4735 }
4736 if j >= total {
4737 return None;
4738 }
4739 return Some(idx_to_pos(j));
4740 }
4741 }
4742 i += 1;
4743 }
4744 None
4745 } else {
4746 let find_start = |from: usize| -> Option<usize> {
4750 let mut start = from;
4751 while start > 0 {
4752 let prev = chars[start - 1];
4753 if prev.is_whitespace() {
4754 let mut k = start - 1;
4755 while k > 0 && chars[k - 1].is_whitespace() {
4756 k -= 1;
4757 }
4758 if k > 0 && is_terminator(chars[k - 1]) {
4759 break;
4760 }
4761 }
4762 start -= 1;
4763 }
4764 while start < total && chars[start].is_whitespace() {
4765 start += 1;
4766 }
4767 (start < total).then_some(start)
4768 };
4769 let current_start = find_start(cursor_idx)?;
4770 if current_start < cursor_idx {
4771 return Some(idx_to_pos(current_start));
4772 }
4773 let mut k = current_start;
4776 while k > 0 && chars[k - 1].is_whitespace() {
4777 k -= 1;
4778 }
4779 if k == 0 {
4780 return None;
4781 }
4782 let prev_start = find_start(k - 1)?;
4783 Some(idx_to_pos(prev_start))
4784 }
4785}
4786
4787fn sentence_text_object<H: crate::types::Host>(
4793 ed: &Editor<hjkl_buffer::Buffer, H>,
4794 inner: bool,
4795) -> Option<((usize, usize), (usize, usize))> {
4796 let lines = buf_lines_to_vec(&ed.buffer);
4797 if lines.is_empty() {
4798 return None;
4799 }
4800 let pos_to_idx = |pos: (usize, usize)| -> usize {
4803 let mut idx = 0;
4804 for line in lines.iter().take(pos.0) {
4805 idx += line.chars().count() + 1;
4806 }
4807 idx + pos.1
4808 };
4809 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4810 for (r, line) in lines.iter().enumerate() {
4811 let len = line.chars().count();
4812 if idx <= len {
4813 return (r, idx);
4814 }
4815 idx -= len + 1;
4816 }
4817 let last = lines.len().saturating_sub(1);
4818 (last, lines[last].chars().count())
4819 };
4820 let mut chars: Vec<char> = Vec::new();
4821 for (r, line) in lines.iter().enumerate() {
4822 chars.extend(line.chars());
4823 if r + 1 < lines.len() {
4824 chars.push('\n');
4825 }
4826 }
4827 if chars.is_empty() {
4828 return None;
4829 }
4830
4831 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4832 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4833
4834 let mut start = cursor_idx;
4838 while start > 0 {
4839 let prev = chars[start - 1];
4840 if prev.is_whitespace() {
4841 let mut k = start - 1;
4845 while k > 0 && chars[k - 1].is_whitespace() {
4846 k -= 1;
4847 }
4848 if k > 0 && is_terminator(chars[k - 1]) {
4849 break;
4850 }
4851 }
4852 start -= 1;
4853 }
4854 while start < chars.len() && chars[start].is_whitespace() {
4857 start += 1;
4858 }
4859 if start >= chars.len() {
4860 return None;
4861 }
4862
4863 let mut end = start;
4866 while end < chars.len() {
4867 if is_terminator(chars[end]) {
4868 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4870 end += 1;
4871 }
4872 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4875 break;
4876 }
4877 }
4878 end += 1;
4879 }
4880 let end_idx = (end + 1).min(chars.len());
4882
4883 let final_end = if inner {
4884 end_idx
4885 } else {
4886 let mut e = end_idx;
4890 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4891 e += 1;
4892 }
4893 e
4894 };
4895
4896 Some((idx_to_pos(start), idx_to_pos(final_end)))
4897}
4898
4899fn tag_text_object<H: crate::types::Host>(
4903 ed: &Editor<hjkl_buffer::Buffer, H>,
4904 inner: bool,
4905) -> Option<((usize, usize), (usize, usize))> {
4906 let lines = buf_lines_to_vec(&ed.buffer);
4907 if lines.is_empty() {
4908 return None;
4909 }
4910 let pos_to_idx = |pos: (usize, usize)| -> usize {
4914 let mut idx = 0;
4915 for line in lines.iter().take(pos.0) {
4916 idx += line.chars().count() + 1;
4917 }
4918 idx + pos.1
4919 };
4920 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4921 for (r, line) in lines.iter().enumerate() {
4922 let len = line.chars().count();
4923 if idx <= len {
4924 return (r, idx);
4925 }
4926 idx -= len + 1;
4927 }
4928 let last = lines.len().saturating_sub(1);
4929 (last, lines[last].chars().count())
4930 };
4931 let mut chars: Vec<char> = Vec::new();
4932 for (r, line) in lines.iter().enumerate() {
4933 chars.extend(line.chars());
4934 if r + 1 < lines.len() {
4935 chars.push('\n');
4936 }
4937 }
4938 let cursor_idx = pos_to_idx(ed.cursor());
4939
4940 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4948 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4949 let mut i = 0;
4950 while i < chars.len() {
4951 if chars[i] != '<' {
4952 i += 1;
4953 continue;
4954 }
4955 let mut j = i + 1;
4956 while j < chars.len() && chars[j] != '>' {
4957 j += 1;
4958 }
4959 if j >= chars.len() {
4960 break;
4961 }
4962 let inside: String = chars[i + 1..j].iter().collect();
4963 let close_end = j + 1;
4964 let trimmed = inside.trim();
4965 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4966 i = close_end;
4967 continue;
4968 }
4969 if let Some(rest) = trimmed.strip_prefix('/') {
4970 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4971 if !name.is_empty()
4972 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4973 {
4974 let (open_start, content_start, _) = stack[stack_idx].clone();
4975 stack.truncate(stack_idx);
4976 let content_end = i;
4977 let candidate = (open_start, content_start, content_end, close_end);
4978 if cursor_idx >= content_start && cursor_idx <= content_end {
4979 innermost = match innermost {
4980 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4981 Some(candidate)
4982 }
4983 None => Some(candidate),
4984 existing => existing,
4985 };
4986 } else if open_start >= cursor_idx && next_after.is_none() {
4987 next_after = Some(candidate);
4988 }
4989 }
4990 } else if !trimmed.ends_with('/') {
4991 let name: String = trimmed
4992 .split(|c: char| c.is_whitespace() || c == '/')
4993 .next()
4994 .unwrap_or("")
4995 .to_string();
4996 if !name.is_empty() {
4997 stack.push((i, close_end, name));
4998 }
4999 }
5000 i = close_end;
5001 }
5002
5003 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5004 if inner {
5005 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5006 } else {
5007 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5008 }
5009}
5010
5011fn is_wordchar(c: char) -> bool {
5012 c.is_alphanumeric() || c == '_'
5013}
5014
5015pub(crate) use hjkl_buffer::is_keyword_char;
5019
5020fn word_text_object<H: crate::types::Host>(
5021 ed: &Editor<hjkl_buffer::Buffer, H>,
5022 inner: bool,
5023 big: bool,
5024) -> Option<((usize, usize), (usize, usize))> {
5025 let (row, col) = ed.cursor();
5026 let line = buf_line(&ed.buffer, row)?;
5027 let chars: Vec<char> = line.chars().collect();
5028 if chars.is_empty() {
5029 return None;
5030 }
5031 let at = col.min(chars.len().saturating_sub(1));
5032 let classify = |c: char| -> u8 {
5033 if c.is_whitespace() {
5034 0
5035 } else if big || is_wordchar(c) {
5036 1
5037 } else {
5038 2
5039 }
5040 };
5041 let cls = classify(chars[at]);
5042 let mut start = at;
5043 while start > 0 && classify(chars[start - 1]) == cls {
5044 start -= 1;
5045 }
5046 let mut end = at;
5047 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5048 end += 1;
5049 }
5050 let char_byte = |i: usize| {
5052 if i >= chars.len() {
5053 line.len()
5054 } else {
5055 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5056 }
5057 };
5058 let mut start_col = char_byte(start);
5059 let mut end_col = char_byte(end + 1);
5061 if !inner {
5062 let mut t = end + 1;
5064 let mut included_trailing = false;
5065 while t < chars.len() && chars[t].is_whitespace() {
5066 included_trailing = true;
5067 t += 1;
5068 }
5069 if included_trailing {
5070 end_col = char_byte(t);
5071 } else {
5072 let mut s = start;
5073 while s > 0 && chars[s - 1].is_whitespace() {
5074 s -= 1;
5075 }
5076 start_col = char_byte(s);
5077 }
5078 }
5079 Some(((row, start_col), (row, end_col)))
5080}
5081
5082fn quote_text_object<H: crate::types::Host>(
5083 ed: &Editor<hjkl_buffer::Buffer, H>,
5084 q: char,
5085 inner: bool,
5086) -> Option<((usize, usize), (usize, usize))> {
5087 let (row, col) = ed.cursor();
5088 let line = buf_line(&ed.buffer, row)?;
5089 let bytes = line.as_bytes();
5090 let q_byte = q as u8;
5091 let mut positions: Vec<usize> = Vec::new();
5093 for (i, &b) in bytes.iter().enumerate() {
5094 if b == q_byte {
5095 positions.push(i);
5096 }
5097 }
5098 if positions.len() < 2 {
5099 return None;
5100 }
5101 let mut open_idx: Option<usize> = None;
5102 let mut close_idx: Option<usize> = None;
5103 for pair in positions.chunks(2) {
5104 if pair.len() < 2 {
5105 break;
5106 }
5107 if col >= pair[0] && col <= pair[1] {
5108 open_idx = Some(pair[0]);
5109 close_idx = Some(pair[1]);
5110 break;
5111 }
5112 if col < pair[0] {
5113 open_idx = Some(pair[0]);
5114 close_idx = Some(pair[1]);
5115 break;
5116 }
5117 }
5118 let open = open_idx?;
5119 let close = close_idx?;
5120 if inner {
5122 if close <= open + 1 {
5123 return None;
5124 }
5125 Some(((row, open + 1), (row, close)))
5126 } else {
5127 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5134 let mut end = after_close;
5136 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5137 end += 1;
5138 }
5139 Some(((row, open), (row, end)))
5140 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5141 let mut start = open;
5143 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5144 start -= 1;
5145 }
5146 Some(((row, start), (row, close + 1)))
5147 } else {
5148 Some(((row, open), (row, close + 1)))
5149 }
5150 }
5151}
5152
5153fn bracket_text_object<H: crate::types::Host>(
5154 ed: &Editor<hjkl_buffer::Buffer, H>,
5155 open: char,
5156 inner: bool,
5157) -> Option<(Pos, Pos, RangeKind)> {
5158 let close = match open {
5159 '(' => ')',
5160 '[' => ']',
5161 '{' => '}',
5162 '<' => '>',
5163 _ => return None,
5164 };
5165 let (row, col) = ed.cursor();
5166 let lines = buf_lines_to_vec(&ed.buffer);
5167 let lines = lines.as_slice();
5168 let open_pos = find_open_bracket(lines, row, col, open, close)
5173 .or_else(|| find_next_open(lines, row, col, open))?;
5174 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5175 if inner {
5177 if close_pos.0 > open_pos.0 + 1 {
5183 let inner_row_start = open_pos.0 + 1;
5185 let inner_row_end = close_pos.0 - 1;
5186 let end_col = lines
5187 .get(inner_row_end)
5188 .map(|l| l.chars().count())
5189 .unwrap_or(0);
5190 return Some((
5191 (inner_row_start, 0),
5192 (inner_row_end, end_col),
5193 RangeKind::Linewise,
5194 ));
5195 }
5196 let inner_start = advance_pos(lines, open_pos);
5197 if inner_start.0 > close_pos.0
5198 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5199 {
5200 return None;
5201 }
5202 Some((inner_start, close_pos, RangeKind::Exclusive))
5203 } else {
5204 Some((
5205 open_pos,
5206 advance_pos(lines, close_pos),
5207 RangeKind::Exclusive,
5208 ))
5209 }
5210}
5211
5212fn find_open_bracket(
5213 lines: &[String],
5214 row: usize,
5215 col: usize,
5216 open: char,
5217 close: char,
5218) -> Option<(usize, usize)> {
5219 let mut depth: i32 = 0;
5220 let mut r = row;
5221 let mut c = col as isize;
5222 loop {
5223 let cur = &lines[r];
5224 let chars: Vec<char> = cur.chars().collect();
5225 if (c as usize) >= chars.len() {
5229 c = chars.len() as isize - 1;
5230 }
5231 while c >= 0 {
5232 let ch = chars[c as usize];
5233 if ch == close {
5234 depth += 1;
5235 } else if ch == open {
5236 if depth == 0 {
5237 return Some((r, c as usize));
5238 }
5239 depth -= 1;
5240 }
5241 c -= 1;
5242 }
5243 if r == 0 {
5244 return None;
5245 }
5246 r -= 1;
5247 c = lines[r].chars().count() as isize - 1;
5248 }
5249}
5250
5251fn find_close_bracket(
5252 lines: &[String],
5253 row: usize,
5254 start_col: usize,
5255 open: char,
5256 close: char,
5257) -> Option<(usize, usize)> {
5258 let mut depth: i32 = 0;
5259 let mut r = row;
5260 let mut c = start_col;
5261 loop {
5262 let cur = &lines[r];
5263 let chars: Vec<char> = cur.chars().collect();
5264 while c < chars.len() {
5265 let ch = chars[c];
5266 if ch == open {
5267 depth += 1;
5268 } else if ch == close {
5269 if depth == 0 {
5270 return Some((r, c));
5271 }
5272 depth -= 1;
5273 }
5274 c += 1;
5275 }
5276 if r + 1 >= lines.len() {
5277 return None;
5278 }
5279 r += 1;
5280 c = 0;
5281 }
5282}
5283
5284fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5288 let mut r = row;
5289 let mut c = col;
5290 while r < lines.len() {
5291 let chars: Vec<char> = lines[r].chars().collect();
5292 while c < chars.len() {
5293 if chars[c] == open {
5294 return Some((r, c));
5295 }
5296 c += 1;
5297 }
5298 r += 1;
5299 c = 0;
5300 }
5301 None
5302}
5303
5304fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5305 let (r, c) = pos;
5306 let line_len = lines[r].chars().count();
5307 if c < line_len {
5308 (r, c + 1)
5309 } else if r + 1 < lines.len() {
5310 (r + 1, 0)
5311 } else {
5312 pos
5313 }
5314}
5315
5316fn paragraph_text_object<H: crate::types::Host>(
5317 ed: &Editor<hjkl_buffer::Buffer, H>,
5318 inner: bool,
5319) -> Option<((usize, usize), (usize, usize))> {
5320 let (row, _) = ed.cursor();
5321 let lines = buf_lines_to_vec(&ed.buffer);
5322 if lines.is_empty() {
5323 return None;
5324 }
5325 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5327 if is_blank(row) {
5328 return None;
5329 }
5330 let mut top = row;
5331 while top > 0 && !is_blank(top - 1) {
5332 top -= 1;
5333 }
5334 let mut bot = row;
5335 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5336 bot += 1;
5337 }
5338 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5340 bot += 1;
5341 }
5342 let end_col = lines[bot].chars().count();
5343 Some(((top, 0), (bot, end_col)))
5344}
5345
5346fn read_vim_range<H: crate::types::Host>(
5352 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5353 start: (usize, usize),
5354 end: (usize, usize),
5355 kind: RangeKind,
5356) -> String {
5357 let (top, bot) = order(start, end);
5358 ed.sync_buffer_content_from_textarea();
5359 let lines = buf_lines_to_vec(&ed.buffer);
5360 match kind {
5361 RangeKind::Linewise => {
5362 let lo = top.0;
5363 let hi = bot.0.min(lines.len().saturating_sub(1));
5364 let mut text = lines[lo..=hi].join("\n");
5365 text.push('\n');
5366 text
5367 }
5368 RangeKind::Inclusive | RangeKind::Exclusive => {
5369 let inclusive = matches!(kind, RangeKind::Inclusive);
5370 let mut out = String::new();
5372 for row in top.0..=bot.0 {
5373 let line = lines.get(row).map(String::as_str).unwrap_or("");
5374 let lo = if row == top.0 { top.1 } else { 0 };
5375 let hi_unclamped = if row == bot.0 {
5376 if inclusive { bot.1 + 1 } else { bot.1 }
5377 } else {
5378 line.chars().count() + 1
5379 };
5380 let row_chars: Vec<char> = line.chars().collect();
5381 let hi = hi_unclamped.min(row_chars.len());
5382 if lo < hi {
5383 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5384 }
5385 if row < bot.0 {
5386 out.push('\n');
5387 }
5388 }
5389 out
5390 }
5391 }
5392}
5393
5394fn cut_vim_range<H: crate::types::Host>(
5403 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5404 start: (usize, usize),
5405 end: (usize, usize),
5406 kind: RangeKind,
5407) -> String {
5408 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5409 let (top, bot) = order(start, end);
5410 ed.sync_buffer_content_from_textarea();
5411 let (buf_start, buf_end, buf_kind) = match kind {
5412 RangeKind::Linewise => (
5413 Position::new(top.0, 0),
5414 Position::new(bot.0, 0),
5415 BufKind::Line,
5416 ),
5417 RangeKind::Inclusive => {
5418 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5419 let next = if bot.1 < line_chars {
5423 Position::new(bot.0, bot.1 + 1)
5424 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5425 Position::new(bot.0 + 1, 0)
5426 } else {
5427 Position::new(bot.0, line_chars)
5428 };
5429 (Position::new(top.0, top.1), next, BufKind::Char)
5430 }
5431 RangeKind::Exclusive => (
5432 Position::new(top.0, top.1),
5433 Position::new(bot.0, bot.1),
5434 BufKind::Char,
5435 ),
5436 };
5437 let inverse = ed.mutate_edit(Edit::DeleteRange {
5438 start: buf_start,
5439 end: buf_end,
5440 kind: buf_kind,
5441 });
5442 let text = match inverse {
5443 Edit::InsertStr { text, .. } => text,
5444 _ => String::new(),
5445 };
5446 if !text.is_empty() {
5447 ed.record_yank_to_host(text.clone());
5448 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
5449 }
5450 ed.push_buffer_cursor_to_textarea();
5451 text
5452}
5453
5454fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5460 use hjkl_buffer::{Edit, MotionKind, Position};
5461 ed.sync_buffer_content_from_textarea();
5462 let cursor = buf_cursor_pos(&ed.buffer);
5463 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5464 if cursor.col >= line_chars {
5465 return;
5466 }
5467 let inverse = ed.mutate_edit(Edit::DeleteRange {
5468 start: cursor,
5469 end: Position::new(cursor.row, line_chars),
5470 kind: MotionKind::Char,
5471 });
5472 if let Edit::InsertStr { text, .. } = inverse
5473 && !text.is_empty()
5474 {
5475 ed.record_yank_to_host(text.clone());
5476 ed.vim.yank_linewise = false;
5477 ed.set_yank(text);
5478 }
5479 buf_set_cursor_pos(&mut ed.buffer, cursor);
5480 ed.push_buffer_cursor_to_textarea();
5481}
5482
5483fn do_char_delete<H: crate::types::Host>(
5484 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5485 forward: bool,
5486 count: usize,
5487) {
5488 use hjkl_buffer::{Edit, MotionKind, Position};
5489 ed.push_undo();
5490 ed.sync_buffer_content_from_textarea();
5491 let mut deleted = String::new();
5494 for _ in 0..count {
5495 let cursor = buf_cursor_pos(&ed.buffer);
5496 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5497 if forward {
5498 if cursor.col >= line_chars {
5501 continue;
5502 }
5503 let inverse = ed.mutate_edit(Edit::DeleteRange {
5504 start: cursor,
5505 end: Position::new(cursor.row, cursor.col + 1),
5506 kind: MotionKind::Char,
5507 });
5508 if let Edit::InsertStr { text, .. } = inverse {
5509 deleted.push_str(&text);
5510 }
5511 } else {
5512 if cursor.col == 0 {
5514 continue;
5515 }
5516 let inverse = ed.mutate_edit(Edit::DeleteRange {
5517 start: Position::new(cursor.row, cursor.col - 1),
5518 end: cursor,
5519 kind: MotionKind::Char,
5520 });
5521 if let Edit::InsertStr { text, .. } = inverse {
5522 deleted = text + &deleted;
5525 }
5526 }
5527 }
5528 if !deleted.is_empty() {
5529 ed.record_yank_to_host(deleted.clone());
5530 ed.record_delete(deleted, false);
5531 }
5532 ed.push_buffer_cursor_to_textarea();
5533}
5534
5535pub(crate) fn adjust_number<H: crate::types::Host>(
5539 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5540 delta: i64,
5541) -> bool {
5542 use hjkl_buffer::{Edit, MotionKind, Position};
5543 ed.sync_buffer_content_from_textarea();
5544 let cursor = buf_cursor_pos(&ed.buffer);
5545 let row = cursor.row;
5546 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5547 Some(l) => l.chars().collect(),
5548 None => return false,
5549 };
5550 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5551 return false;
5552 };
5553 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5554 digit_start - 1
5555 } else {
5556 digit_start
5557 };
5558 let mut span_end = digit_start;
5559 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5560 span_end += 1;
5561 }
5562 let s: String = chars[span_start..span_end].iter().collect();
5563 let Ok(n) = s.parse::<i64>() else {
5564 return false;
5565 };
5566 let new_s = n.saturating_add(delta).to_string();
5567
5568 ed.push_undo();
5569 let span_start_pos = Position::new(row, span_start);
5570 let span_end_pos = Position::new(row, span_end);
5571 ed.mutate_edit(Edit::DeleteRange {
5572 start: span_start_pos,
5573 end: span_end_pos,
5574 kind: MotionKind::Char,
5575 });
5576 ed.mutate_edit(Edit::InsertStr {
5577 at: span_start_pos,
5578 text: new_s.clone(),
5579 });
5580 let new_len = new_s.chars().count();
5581 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5582 ed.push_buffer_cursor_to_textarea();
5583 true
5584}
5585
5586pub(crate) fn replace_char<H: crate::types::Host>(
5587 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5588 ch: char,
5589 count: usize,
5590) {
5591 use hjkl_buffer::{Edit, MotionKind, Position};
5592 ed.push_undo();
5593 ed.sync_buffer_content_from_textarea();
5594 for _ in 0..count {
5595 let cursor = buf_cursor_pos(&ed.buffer);
5596 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5597 if cursor.col >= line_chars {
5598 break;
5599 }
5600 ed.mutate_edit(Edit::DeleteRange {
5601 start: cursor,
5602 end: Position::new(cursor.row, cursor.col + 1),
5603 kind: MotionKind::Char,
5604 });
5605 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5606 }
5607 crate::motions::move_left(&mut ed.buffer, 1);
5609 ed.push_buffer_cursor_to_textarea();
5610}
5611
5612fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5613 use hjkl_buffer::{Edit, MotionKind, Position};
5614 ed.sync_buffer_content_from_textarea();
5615 let cursor = buf_cursor_pos(&ed.buffer);
5616 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5617 return;
5618 };
5619 let toggled = if c.is_uppercase() {
5620 c.to_lowercase().next().unwrap_or(c)
5621 } else {
5622 c.to_uppercase().next().unwrap_or(c)
5623 };
5624 ed.mutate_edit(Edit::DeleteRange {
5625 start: cursor,
5626 end: Position::new(cursor.row, cursor.col + 1),
5627 kind: MotionKind::Char,
5628 });
5629 ed.mutate_edit(Edit::InsertChar {
5630 at: cursor,
5631 ch: toggled,
5632 });
5633}
5634
5635fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5636 use hjkl_buffer::{Edit, Position};
5637 ed.sync_buffer_content_from_textarea();
5638 let row = buf_cursor_pos(&ed.buffer).row;
5639 if row + 1 >= buf_row_count(&ed.buffer) {
5640 return;
5641 }
5642 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5643 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5644 let next_trimmed = next_raw.trim_start();
5645 let cur_chars = cur_line.chars().count();
5646 let next_chars = next_raw.chars().count();
5647 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5650 " "
5651 } else {
5652 ""
5653 };
5654 let joined = format!("{cur_line}{separator}{next_trimmed}");
5655 ed.mutate_edit(Edit::Replace {
5656 start: Position::new(row, 0),
5657 end: Position::new(row + 1, next_chars),
5658 with: joined,
5659 });
5660 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5664 ed.push_buffer_cursor_to_textarea();
5665}
5666
5667fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5670 use hjkl_buffer::Edit;
5671 ed.sync_buffer_content_from_textarea();
5672 let row = buf_cursor_pos(&ed.buffer).row;
5673 if row + 1 >= buf_row_count(&ed.buffer) {
5674 return;
5675 }
5676 let join_col = buf_line_chars(&ed.buffer, row);
5677 ed.mutate_edit(Edit::JoinLines {
5678 row,
5679 count: 1,
5680 with_space: false,
5681 });
5682 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5684 ed.push_buffer_cursor_to_textarea();
5685}
5686
5687fn do_paste<H: crate::types::Host>(
5688 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5689 before: bool,
5690 count: usize,
5691) {
5692 use hjkl_buffer::{Edit, Position};
5693 ed.push_undo();
5694 let selector = ed.vim.pending_register.take();
5699 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5700 Some(slot) => (slot.text.clone(), slot.linewise),
5701 None => {
5707 let s = &ed.registers().unnamed;
5708 (s.text.clone(), s.linewise)
5709 }
5710 };
5711 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5715 for _ in 0..count {
5716 ed.sync_buffer_content_from_textarea();
5717 let yank = yank.clone();
5718 if yank.is_empty() {
5719 continue;
5720 }
5721 if linewise {
5722 let text = yank.trim_matches('\n').to_string();
5726 let row = buf_cursor_pos(&ed.buffer).row;
5727 let target_row = if before {
5728 ed.mutate_edit(Edit::InsertStr {
5729 at: Position::new(row, 0),
5730 text: format!("{text}\n"),
5731 });
5732 row
5733 } else {
5734 let line_chars = buf_line_chars(&ed.buffer, row);
5735 ed.mutate_edit(Edit::InsertStr {
5736 at: Position::new(row, line_chars),
5737 text: format!("\n{text}"),
5738 });
5739 row + 1
5740 };
5741 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5742 crate::motions::move_first_non_blank(&mut ed.buffer);
5743 ed.push_buffer_cursor_to_textarea();
5744 let payload_lines = text.lines().count().max(1);
5746 let bot_row = target_row + payload_lines - 1;
5747 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5748 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5749 } else {
5750 let cursor = buf_cursor_pos(&ed.buffer);
5754 let at = if before {
5755 cursor
5756 } else {
5757 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5758 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5759 };
5760 ed.mutate_edit(Edit::InsertStr {
5761 at,
5762 text: yank.clone(),
5763 });
5764 crate::motions::move_left(&mut ed.buffer, 1);
5767 ed.push_buffer_cursor_to_textarea();
5768 let lo = (at.row, at.col);
5770 let hi = ed.cursor();
5771 paste_mark = Some((lo, hi));
5772 }
5773 }
5774 if let Some((lo, hi)) = paste_mark {
5775 ed.set_mark('[', lo);
5776 ed.set_mark(']', hi);
5777 }
5778 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5780}
5781
5782pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5783 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5784 let current = ed.snapshot();
5785 ed.redo_stack.push(current);
5786 ed.restore(lines, cursor);
5787 }
5788 ed.vim.mode = Mode::Normal;
5789 clamp_cursor_to_normal_mode(ed);
5793}
5794
5795pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5796 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5797 let current = ed.snapshot();
5798 ed.undo_stack.push(current);
5799 ed.cap_undo();
5800 ed.restore(lines, cursor);
5801 }
5802 ed.vim.mode = Mode::Normal;
5803}
5804
5805fn replay_insert_and_finish<H: crate::types::Host>(
5812 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5813 text: &str,
5814) {
5815 use hjkl_buffer::{Edit, Position};
5816 let cursor = ed.cursor();
5817 ed.mutate_edit(Edit::InsertStr {
5818 at: Position::new(cursor.0, cursor.1),
5819 text: text.to_string(),
5820 });
5821 if ed.vim.insert_session.take().is_some() {
5822 if ed.cursor().1 > 0 {
5823 crate::motions::move_left(&mut ed.buffer, 1);
5824 ed.push_buffer_cursor_to_textarea();
5825 }
5826 ed.vim.mode = Mode::Normal;
5827 }
5828}
5829
5830pub(crate) fn replay_last_change<H: crate::types::Host>(
5831 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5832 outer_count: usize,
5833) {
5834 let Some(change) = ed.vim.last_change.clone() else {
5835 return;
5836 };
5837 ed.vim.replaying = true;
5838 let scale = if outer_count > 0 { outer_count } else { 1 };
5839 match change {
5840 LastChange::OpMotion {
5841 op,
5842 motion,
5843 count,
5844 inserted,
5845 } => {
5846 let total = count.max(1) * scale;
5847 apply_op_with_motion(ed, op, &motion, total);
5848 if let Some(text) = inserted {
5849 replay_insert_and_finish(ed, &text);
5850 }
5851 }
5852 LastChange::OpTextObj {
5853 op,
5854 obj,
5855 inner,
5856 inserted,
5857 } => {
5858 apply_op_with_text_object(ed, op, obj, inner);
5859 if let Some(text) = inserted {
5860 replay_insert_and_finish(ed, &text);
5861 }
5862 }
5863 LastChange::LineOp {
5864 op,
5865 count,
5866 inserted,
5867 } => {
5868 let total = count.max(1) * scale;
5869 execute_line_op(ed, op, total);
5870 if let Some(text) = inserted {
5871 replay_insert_and_finish(ed, &text);
5872 }
5873 }
5874 LastChange::CharDel { forward, count } => {
5875 do_char_delete(ed, forward, count * scale);
5876 }
5877 LastChange::ReplaceChar { ch, count } => {
5878 replace_char(ed, ch, count * scale);
5879 }
5880 LastChange::ToggleCase { count } => {
5881 for _ in 0..count * scale {
5882 ed.push_undo();
5883 toggle_case_at_cursor(ed);
5884 }
5885 }
5886 LastChange::JoinLine { count } => {
5887 for _ in 0..count * scale {
5888 ed.push_undo();
5889 join_line(ed);
5890 }
5891 }
5892 LastChange::Paste { before, count } => {
5893 do_paste(ed, before, count * scale);
5894 }
5895 LastChange::DeleteToEol { inserted } => {
5896 use hjkl_buffer::{Edit, Position};
5897 ed.push_undo();
5898 delete_to_eol(ed);
5899 if let Some(text) = inserted {
5900 let cursor = ed.cursor();
5901 ed.mutate_edit(Edit::InsertStr {
5902 at: Position::new(cursor.0, cursor.1),
5903 text,
5904 });
5905 }
5906 }
5907 LastChange::OpenLine { above, inserted } => {
5908 use hjkl_buffer::{Edit, Position};
5909 ed.push_undo();
5910 ed.sync_buffer_content_from_textarea();
5911 let row = buf_cursor_pos(&ed.buffer).row;
5912 if above {
5913 ed.mutate_edit(Edit::InsertStr {
5914 at: Position::new(row, 0),
5915 text: "\n".to_string(),
5916 });
5917 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5918 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5919 } else {
5920 let line_chars = buf_line_chars(&ed.buffer, row);
5921 ed.mutate_edit(Edit::InsertStr {
5922 at: Position::new(row, line_chars),
5923 text: "\n".to_string(),
5924 });
5925 }
5926 ed.push_buffer_cursor_to_textarea();
5927 let cursor = ed.cursor();
5928 ed.mutate_edit(Edit::InsertStr {
5929 at: Position::new(cursor.0, cursor.1),
5930 text: inserted,
5931 });
5932 }
5933 LastChange::InsertAt {
5934 entry,
5935 inserted,
5936 count,
5937 } => {
5938 use hjkl_buffer::{Edit, Position};
5939 ed.push_undo();
5940 match entry {
5941 InsertEntry::I => {}
5942 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5943 InsertEntry::A => {
5944 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5945 ed.push_buffer_cursor_to_textarea();
5946 }
5947 InsertEntry::ShiftA => {
5948 crate::motions::move_line_end(&mut ed.buffer);
5949 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5950 ed.push_buffer_cursor_to_textarea();
5951 }
5952 }
5953 for _ in 0..count.max(1) {
5954 let cursor = ed.cursor();
5955 ed.mutate_edit(Edit::InsertStr {
5956 at: Position::new(cursor.0, cursor.1),
5957 text: inserted.clone(),
5958 });
5959 }
5960 }
5961 }
5962 ed.vim.replaying = false;
5963}
5964
5965fn extract_inserted(before: &str, after: &str) -> String {
5968 let before_chars: Vec<char> = before.chars().collect();
5969 let after_chars: Vec<char> = after.chars().collect();
5970 if after_chars.len() <= before_chars.len() {
5971 return String::new();
5972 }
5973 let prefix = before_chars
5974 .iter()
5975 .zip(after_chars.iter())
5976 .take_while(|(a, b)| a == b)
5977 .count();
5978 let max_suffix = before_chars.len() - prefix;
5979 let suffix = before_chars
5980 .iter()
5981 .rev()
5982 .zip(after_chars.iter().rev())
5983 .take(max_suffix)
5984 .take_while(|(a, b)| a == b)
5985 .count();
5986 after_chars[prefix..after_chars.len() - suffix]
5987 .iter()
5988 .collect()
5989}
5990
5991