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 SquareBracketOpen,
154 SquareBracketClose,
157 OpSquareBracketOpen { op: Operator, count1: usize },
159 OpSquareBracketClose { op: Operator, count1: usize },
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum Operator {
167 Delete,
168 Change,
169 Yank,
170 Uppercase,
173 Lowercase,
175 ToggleCase,
179 Indent,
184 Outdent,
187 Fold,
191 Reflow,
196 AutoIndent,
200 Filter,
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub enum Motion {
209 Left,
210 Right,
211 Up,
212 Down,
213 WordFwd,
214 BigWordFwd,
215 WordBack,
216 BigWordBack,
217 WordEnd,
218 BigWordEnd,
219 WordEndBack,
221 BigWordEndBack,
223 LineStart,
224 FirstNonBlank,
225 LineEnd,
226 FileTop,
227 FileBottom,
228 Find {
229 ch: char,
230 forward: bool,
231 till: bool,
232 },
233 FindRepeat {
234 reverse: bool,
235 },
236 MatchBracket,
237 WordAtCursor {
238 forward: bool,
239 whole_word: bool,
242 },
243 SearchNext {
245 reverse: bool,
246 },
247 ViewportTop,
249 ViewportMiddle,
251 ViewportBottom,
253 LastNonBlank,
255 LineMiddle,
258 ParagraphPrev,
260 ParagraphNext,
262 SentencePrev,
264 SentenceNext,
266 ScreenDown,
269 ScreenUp,
271 SectionBackward,
274 SectionForward,
276 SectionEndBackward,
279 SectionEndForward,
281 FirstNonBlankNextLine,
283 FirstNonBlankPrevLine,
285 FirstNonBlankLine,
287 GotoColumn,
290}
291
292#[derive(Debug, Clone, Copy, PartialEq, Eq)]
293pub enum TextObject {
294 Word {
295 big: bool,
296 },
297 Quote(char),
298 Bracket(char),
299 Paragraph,
300 XmlTag,
304 Sentence,
309}
310
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
313pub enum RangeKind {
314 Exclusive,
316 Inclusive,
318 Linewise,
320}
321
322#[derive(Debug, Clone)]
326pub enum LastChange {
327 OpMotion {
329 op: Operator,
330 motion: Motion,
331 count: usize,
332 inserted: Option<String>,
333 },
334 OpTextObj {
336 op: Operator,
337 obj: TextObject,
338 inner: bool,
339 inserted: Option<String>,
340 },
341 LineOp {
343 op: Operator,
344 count: usize,
345 inserted: Option<String>,
346 },
347 CharDel { forward: bool, count: usize },
349 ReplaceChar { ch: char, count: usize },
351 ToggleCase { count: usize },
353 JoinLine { count: usize },
355 Paste { before: bool, count: usize },
357 DeleteToEol { inserted: Option<String> },
359 OpenLine { above: bool, inserted: String },
361 InsertAt {
363 entry: InsertEntry,
364 inserted: String,
365 count: usize,
366 },
367}
368
369#[derive(Debug, Clone, Copy, PartialEq, Eq)]
370pub enum InsertEntry {
371 I,
372 A,
373 ShiftI,
374 ShiftA,
375}
376
377#[derive(Default)]
380pub struct VimState {
381 pub mode: Mode,
386 pub pending: Pending,
388 pub count: usize,
391 pub last_find: Option<(char, bool, bool)>,
393 pub last_change: Option<LastChange>,
395 pub insert_session: Option<InsertSession>,
397 pub visual_anchor: (usize, usize),
401 pub visual_line_anchor: usize,
403 pub block_anchor: (usize, usize),
406 pub block_vcol: usize,
412 pub yank_linewise: bool,
414 pub pending_register: Option<char>,
417 pub recording_macro: Option<char>,
421 pub recording_keys: Vec<crate::input::Input>,
426 pub replaying_macro: bool,
429 pub last_macro: Option<char>,
431 pub last_edit_pos: Option<(usize, usize)>,
434 pub last_insert_pos: Option<(usize, usize)>,
438 pub change_list: Vec<(usize, usize)>,
442 pub change_list_cursor: Option<usize>,
445 pub last_visual: Option<LastVisual>,
448 pub viewport_pinned: bool,
452 pub replaying: bool,
454 pub one_shot_normal: bool,
457 pub search_prompt: Option<SearchPrompt>,
459 pub last_search: Option<String>,
463 pub last_search_forward: bool,
467 pub jump_back: Vec<(usize, usize)>,
472 pub jump_fwd: Vec<(usize, usize)>,
475 pub insert_pending_register: bool,
479 pub change_mark_start: Option<(usize, usize)>,
485 pub search_history: Vec<String>,
489 pub search_history_cursor: Option<usize>,
494 pub last_input_at: Option<std::time::Instant>,
503 pub last_input_host_at: Option<core::time::Duration>,
507 pub(crate) current_mode: crate::VimMode,
513 pub last_substitute: Option<crate::substitute::SubstituteCmd>,
515}
516
517pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
518pub(crate) const CHANGE_LIST_MAX: usize = 100;
519
520#[derive(Debug, Clone)]
523pub struct SearchPrompt {
524 pub text: String,
525 pub cursor: usize,
526 pub forward: bool,
527}
528
529#[derive(Debug, Clone)]
530pub struct InsertSession {
531 pub count: usize,
532 pub row_min: usize,
534 pub row_max: usize,
535 pub before_lines: Vec<String>,
539 pub reason: InsertReason,
540}
541
542#[derive(Debug, Clone)]
543pub enum InsertReason {
544 Enter(InsertEntry),
546 Open { above: bool },
548 AfterChange,
551 DeleteToEol,
553 ReplayOnly,
556 BlockEdge { top: usize, bot: usize, col: usize },
560 BlockChange { top: usize, bot: usize, col: usize },
565 Replace,
569}
570
571#[derive(Debug, Clone, Copy)]
581pub struct LastVisual {
582 pub mode: Mode,
583 pub anchor: (usize, usize),
584 pub cursor: (usize, usize),
585 pub block_vcol: usize,
586}
587
588impl VimState {
589 pub fn public_mode(&self) -> VimMode {
590 match self.mode {
591 Mode::Normal => VimMode::Normal,
592 Mode::Insert => VimMode::Insert,
593 Mode::Visual => VimMode::Visual,
594 Mode::VisualLine => VimMode::VisualLine,
595 Mode::VisualBlock => VimMode::VisualBlock,
596 }
597 }
598
599 pub fn force_normal(&mut self) {
600 self.mode = Mode::Normal;
601 self.pending = Pending::None;
602 self.count = 0;
603 self.insert_session = None;
604 self.current_mode = crate::VimMode::Normal;
606 }
607
608 pub(crate) fn clear_pending_prefix(&mut self) {
618 self.pending = Pending::None;
619 self.count = 0;
620 self.pending_register = None;
621 self.insert_pending_register = false;
622 }
623
624 pub(crate) fn widen_insert_row(&mut self, row: usize) {
629 if let Some(ref mut session) = self.insert_session {
630 session.row_min = session.row_min.min(row);
631 session.row_max = session.row_max.max(row);
632 }
633 }
634
635 pub fn is_visual(&self) -> bool {
636 matches!(
637 self.mode,
638 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
639 )
640 }
641
642 pub fn is_visual_char(&self) -> bool {
643 self.mode == Mode::Visual
644 }
645
646 pub(crate) fn pending_count_val(&self) -> Option<u32> {
649 if self.count == 0 {
650 None
651 } else {
652 Some(self.count as u32)
653 }
654 }
655
656 pub(crate) fn is_chord_pending(&self) -> bool {
659 !matches!(self.pending, Pending::None)
660 }
661
662 pub(crate) fn pending_op_char(&self) -> Option<char> {
666 let op = match &self.pending {
667 Pending::Op { op, .. }
668 | Pending::OpTextObj { op, .. }
669 | Pending::OpG { op, .. }
670 | Pending::OpFind { op, .. }
671 | Pending::OpSquareBracketOpen { op, .. }
672 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
673 _ => None,
674 };
675 op.map(|o| match o {
676 Operator::Delete => 'd',
677 Operator::Change => 'c',
678 Operator::Yank => 'y',
679 Operator::Uppercase => 'U',
680 Operator::Lowercase => 'u',
681 Operator::ToggleCase => '~',
682 Operator::Indent => '>',
683 Operator::Outdent => '<',
684 Operator::Fold => 'z',
685 Operator::Reflow => 'q',
686 Operator::AutoIndent => '=',
687 Operator::Filter => '!',
688 })
689 }
690}
691
692pub(crate) fn enter_search<H: crate::types::Host>(
698 ed: &mut Editor<hjkl_buffer::Buffer, H>,
699 forward: bool,
700) {
701 ed.vim.search_prompt = Some(SearchPrompt {
702 text: String::new(),
703 cursor: 0,
704 forward,
705 });
706 ed.vim.search_history_cursor = None;
707 ed.set_search_pattern(None);
711}
712
713fn walk_change_list<H: crate::types::Host>(
717 ed: &mut Editor<hjkl_buffer::Buffer, H>,
718 dir: isize,
719 count: usize,
720) {
721 if ed.vim.change_list.is_empty() {
722 return;
723 }
724 let len = ed.vim.change_list.len();
725 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
726 (None, -1) => len as isize - 1,
727 (None, 1) => return, (Some(i), -1) => i as isize - 1,
729 (Some(i), 1) => i as isize + 1,
730 _ => return,
731 };
732 for _ in 1..count {
733 let next = idx + dir;
734 if next < 0 || next >= len as isize {
735 break;
736 }
737 idx = next;
738 }
739 if idx < 0 || idx >= len as isize {
740 return;
741 }
742 let idx = idx as usize;
743 ed.vim.change_list_cursor = Some(idx);
744 let (row, col) = ed.vim.change_list[idx];
745 ed.jump_cursor(row, col);
746}
747
748fn insert_register_text<H: crate::types::Host>(
753 ed: &mut Editor<hjkl_buffer::Buffer, H>,
754 selector: char,
755) {
756 use hjkl_buffer::Edit;
757 let text = match ed.registers().read(selector) {
758 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
759 _ => return,
760 };
761 ed.sync_buffer_content_from_textarea();
762 let cursor = buf_cursor_pos(&ed.buffer);
763 ed.mutate_edit(Edit::InsertStr {
764 at: cursor,
765 text: text.clone(),
766 });
767 let mut row = cursor.row;
770 let mut col = cursor.col;
771 for ch in text.chars() {
772 if ch == '\n' {
773 row += 1;
774 col = 0;
775 } else {
776 col += 1;
777 }
778 }
779 buf_set_cursor_rc(&mut ed.buffer, row, col);
780 ed.push_buffer_cursor_to_textarea();
781 ed.mark_content_dirty();
782 if let Some(ref mut session) = ed.vim.insert_session {
783 session.row_min = session.row_min.min(row);
784 session.row_max = session.row_max.max(row);
785 }
786}
787
788pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
807 if !settings.autoindent {
808 return String::new();
809 }
810 let base: String = prev_line
812 .chars()
813 .take_while(|c| *c == ' ' || *c == '\t')
814 .collect();
815
816 if settings.smartindent {
817 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
821 if matches!(last_non_ws, Some('{' | '(' | '[')) {
822 let unit = if settings.expandtab {
823 if settings.softtabstop > 0 {
824 " ".repeat(settings.softtabstop)
825 } else {
826 " ".repeat(settings.shiftwidth)
827 }
828 } else {
829 "\t".to_string()
830 };
831 return format!("{base}{unit}");
832 }
833 }
834
835 base
836}
837
838fn try_dedent_close_bracket<H: crate::types::Host>(
848 ed: &mut Editor<hjkl_buffer::Buffer, H>,
849 cursor: hjkl_buffer::Position,
850 ch: char,
851) -> bool {
852 use hjkl_buffer::{Edit, MotionKind, Position};
853
854 if !ed.settings.smartindent {
855 return false;
856 }
857 if !matches!(ch, '}' | ')' | ']') {
858 return false;
859 }
860
861 let line = match buf_line(&ed.buffer, cursor.row) {
862 Some(l) => l.to_string(),
863 None => return false,
864 };
865
866 let before: String = line.chars().take(cursor.col).collect();
868 if !before.chars().all(|c| c == ' ' || c == '\t') {
869 return false;
870 }
871 if before.is_empty() {
872 return false;
874 }
875
876 let unit_len: usize = if ed.settings.expandtab {
878 if ed.settings.softtabstop > 0 {
879 ed.settings.softtabstop
880 } else {
881 ed.settings.shiftwidth
882 }
883 } else {
884 1
886 };
887
888 let strip_len = if ed.settings.expandtab {
890 let spaces = before.chars().filter(|c| *c == ' ').count();
892 if spaces < unit_len {
893 return false;
894 }
895 unit_len
896 } else {
897 if !before.starts_with('\t') {
899 return false;
900 }
901 1
902 };
903
904 ed.mutate_edit(Edit::DeleteRange {
906 start: Position::new(cursor.row, 0),
907 end: Position::new(cursor.row, strip_len),
908 kind: MotionKind::Char,
909 });
910 let new_col = cursor.col.saturating_sub(strip_len);
915 ed.mutate_edit(Edit::InsertChar {
916 at: Position::new(cursor.row, new_col),
917 ch,
918 });
919 true
920}
921
922fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
923 let Some(session) = ed.vim.insert_session.take() else {
924 return;
925 };
926 let lines = buf_lines_to_vec(&ed.buffer);
927 let after_end = session.row_max.min(lines.len().saturating_sub(1));
931 let before_end = session
932 .row_max
933 .min(session.before_lines.len().saturating_sub(1));
934 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
935 session.before_lines[session.row_min..=before_end].join("\n")
936 } else {
937 String::new()
938 };
939 let after = if after_end >= session.row_min && session.row_min < lines.len() {
940 lines[session.row_min..=after_end].join("\n")
941 } else {
942 String::new()
943 };
944 let inserted = extract_inserted(&before, &after);
945 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
946 use hjkl_buffer::{Edit, Position};
947 for _ in 0..session.count - 1 {
948 let (row, col) = ed.cursor();
949 ed.mutate_edit(Edit::InsertStr {
950 at: Position::new(row, col),
951 text: inserted.clone(),
952 });
953 }
954 }
955 fn replicate_block_text<H: crate::types::Host>(
959 ed: &mut Editor<hjkl_buffer::Buffer, H>,
960 inserted: &str,
961 top: usize,
962 bot: usize,
963 col: usize,
964 ) {
965 use hjkl_buffer::{Edit, Position};
966 for r in (top + 1)..=bot {
967 let line_len = buf_line_chars(&ed.buffer, r);
968 if col > line_len {
969 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
970 ed.mutate_edit(Edit::InsertStr {
971 at: Position::new(r, line_len),
972 text: pad,
973 });
974 }
975 ed.mutate_edit(Edit::InsertStr {
976 at: Position::new(r, col),
977 text: inserted.to_string(),
978 });
979 }
980 }
981
982 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
983 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
986 replicate_block_text(ed, &inserted, top, bot, col);
987 buf_set_cursor_rc(&mut ed.buffer, top, col);
988 ed.push_buffer_cursor_to_textarea();
989 }
990 return;
991 }
992 if let InsertReason::BlockChange { top, bot, col } = session.reason {
993 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
997 replicate_block_text(ed, &inserted, top, bot, col);
998 let ins_chars = inserted.chars().count();
999 let line_len = buf_line_chars(&ed.buffer, top);
1000 let target_col = (col + ins_chars).min(line_len);
1001 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1002 ed.push_buffer_cursor_to_textarea();
1003 }
1004 return;
1005 }
1006 if ed.vim.replaying {
1007 return;
1008 }
1009 match session.reason {
1010 InsertReason::Enter(entry) => {
1011 ed.vim.last_change = Some(LastChange::InsertAt {
1012 entry,
1013 inserted,
1014 count: session.count,
1015 });
1016 }
1017 InsertReason::Open { above } => {
1018 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1019 }
1020 InsertReason::AfterChange => {
1021 if let Some(
1022 LastChange::OpMotion { inserted: ins, .. }
1023 | LastChange::OpTextObj { inserted: ins, .. }
1024 | LastChange::LineOp { inserted: ins, .. },
1025 ) = ed.vim.last_change.as_mut()
1026 {
1027 *ins = Some(inserted);
1028 }
1029 if let Some(start) = ed.vim.change_mark_start.take() {
1035 let end = ed.cursor();
1036 ed.set_mark('[', start);
1037 ed.set_mark(']', end);
1038 }
1039 }
1040 InsertReason::DeleteToEol => {
1041 ed.vim.last_change = Some(LastChange::DeleteToEol {
1042 inserted: Some(inserted),
1043 });
1044 }
1045 InsertReason::ReplayOnly => {}
1046 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1047 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1048 InsertReason::Replace => {
1049 ed.vim.last_change = Some(LastChange::DeleteToEol {
1054 inserted: Some(inserted),
1055 });
1056 }
1057 }
1058}
1059
1060pub(crate) fn begin_insert<H: crate::types::Host>(
1061 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1062 count: usize,
1063 reason: InsertReason,
1064) {
1065 let record = !matches!(reason, InsertReason::ReplayOnly);
1066 if record {
1067 ed.push_undo();
1068 }
1069 let reason = if ed.vim.replaying {
1070 InsertReason::ReplayOnly
1071 } else {
1072 reason
1073 };
1074 let (row, _) = ed.cursor();
1075 ed.vim.insert_session = Some(InsertSession {
1076 count,
1077 row_min: row,
1078 row_max: row,
1079 before_lines: buf_lines_to_vec(&ed.buffer),
1080 reason,
1081 });
1082 ed.vim.mode = Mode::Insert;
1083 ed.vim.current_mode = crate::VimMode::Insert;
1085}
1086
1087pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1102 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1103) {
1104 if !ed.settings.undo_break_on_motion {
1105 return;
1106 }
1107 if ed.vim.replaying {
1108 return;
1109 }
1110 if ed.vim.insert_session.is_none() {
1111 return;
1112 }
1113 ed.push_undo();
1114 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1115 let mut lines: Vec<String> = Vec::with_capacity(n);
1116 for r in 0..n {
1117 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1118 }
1119 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1120 if let Some(ref mut session) = ed.vim.insert_session {
1121 session.before_lines = lines;
1122 session.row_min = row;
1123 session.row_max = row;
1124 }
1125}
1126
1127pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1148 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1149 ch: char,
1150) -> bool {
1151 use hjkl_buffer::{Edit, MotionKind, Position};
1152 ed.sync_buffer_content_from_textarea();
1153 let cursor = buf_cursor_pos(&ed.buffer);
1154 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1155 let in_replace = matches!(
1156 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1157 Some(InsertReason::Replace)
1158 );
1159 if in_replace && cursor.col < line_chars {
1160 ed.mutate_edit(Edit::DeleteRange {
1161 start: cursor,
1162 end: Position::new(cursor.row, cursor.col + 1),
1163 kind: MotionKind::Char,
1164 });
1165 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1166 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1167 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1168 }
1169 ed.push_buffer_cursor_to_textarea();
1170 true
1171}
1172
1173pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1176 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1177) -> bool {
1178 use hjkl_buffer::Edit;
1179 ed.sync_buffer_content_from_textarea();
1180 let cursor = buf_cursor_pos(&ed.buffer);
1181 let prev_line = buf_line(&ed.buffer, cursor.row)
1182 .unwrap_or_default()
1183 .to_string();
1184 let indent = compute_enter_indent(&ed.settings, &prev_line);
1185 let text = format!("\n{indent}");
1186 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1187 ed.push_buffer_cursor_to_textarea();
1188 true
1189}
1190
1191pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1194 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1195) -> bool {
1196 use hjkl_buffer::Edit;
1197 ed.sync_buffer_content_from_textarea();
1198 let cursor = buf_cursor_pos(&ed.buffer);
1199 if ed.settings.expandtab {
1200 let sts = ed.settings.softtabstop;
1201 let n = if sts > 0 {
1202 sts - (cursor.col % sts)
1203 } else {
1204 ed.settings.tabstop.max(1)
1205 };
1206 ed.mutate_edit(Edit::InsertStr {
1207 at: cursor,
1208 text: " ".repeat(n),
1209 });
1210 } else {
1211 ed.mutate_edit(Edit::InsertChar {
1212 at: cursor,
1213 ch: '\t',
1214 });
1215 }
1216 ed.push_buffer_cursor_to_textarea();
1217 true
1218}
1219
1220pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1226 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1227) -> bool {
1228 use hjkl_buffer::{Edit, MotionKind, Position};
1229 ed.sync_buffer_content_from_textarea();
1230 let cursor = buf_cursor_pos(&ed.buffer);
1231 let sts = ed.settings.softtabstop;
1232 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1233 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1234 let chars: Vec<char> = line.chars().collect();
1235 let run_start = cursor.col - sts;
1236 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1237 ed.mutate_edit(Edit::DeleteRange {
1238 start: Position::new(cursor.row, run_start),
1239 end: cursor,
1240 kind: MotionKind::Char,
1241 });
1242 ed.push_buffer_cursor_to_textarea();
1243 return true;
1244 }
1245 }
1246 let result = if cursor.col > 0 {
1247 ed.mutate_edit(Edit::DeleteRange {
1248 start: Position::new(cursor.row, cursor.col - 1),
1249 end: cursor,
1250 kind: MotionKind::Char,
1251 });
1252 true
1253 } else if cursor.row > 0 {
1254 let prev_row = cursor.row - 1;
1255 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1256 ed.mutate_edit(Edit::JoinLines {
1257 row: prev_row,
1258 count: 1,
1259 with_space: false,
1260 });
1261 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1262 true
1263 } else {
1264 false
1265 };
1266 ed.push_buffer_cursor_to_textarea();
1267 result
1268}
1269
1270pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
1273 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1274) -> bool {
1275 use hjkl_buffer::{Edit, MotionKind, Position};
1276 ed.sync_buffer_content_from_textarea();
1277 let cursor = buf_cursor_pos(&ed.buffer);
1278 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1279 let result = if cursor.col < line_chars {
1280 ed.mutate_edit(Edit::DeleteRange {
1281 start: cursor,
1282 end: Position::new(cursor.row, cursor.col + 1),
1283 kind: MotionKind::Char,
1284 });
1285 buf_set_cursor_pos(&mut ed.buffer, cursor);
1286 true
1287 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1288 ed.mutate_edit(Edit::JoinLines {
1289 row: cursor.row,
1290 count: 1,
1291 with_space: false,
1292 });
1293 buf_set_cursor_pos(&mut ed.buffer, cursor);
1294 true
1295 } else {
1296 false
1297 };
1298 ed.push_buffer_cursor_to_textarea();
1299 result
1300}
1301
1302#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1304pub enum InsertDir {
1305 Left,
1306 Right,
1307 Up,
1308 Down,
1309}
1310
1311pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
1314 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1315 dir: InsertDir,
1316) -> bool {
1317 ed.sync_buffer_content_from_textarea();
1318 match dir {
1319 InsertDir::Left => {
1320 crate::motions::move_left(&mut ed.buffer, 1);
1321 }
1322 InsertDir::Right => {
1323 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1324 }
1325 InsertDir::Up => {
1326 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1327 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1328 }
1329 InsertDir::Down => {
1330 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1331 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1332 }
1333 }
1334 break_undo_group_in_insert(ed);
1335 ed.push_buffer_cursor_to_textarea();
1336 false
1337}
1338
1339pub(crate) fn insert_home_bridge<H: crate::types::Host>(
1342 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1343) -> bool {
1344 ed.sync_buffer_content_from_textarea();
1345 crate::motions::move_line_start(&mut ed.buffer);
1346 break_undo_group_in_insert(ed);
1347 ed.push_buffer_cursor_to_textarea();
1348 false
1349}
1350
1351pub(crate) fn insert_end_bridge<H: crate::types::Host>(
1354 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1355) -> bool {
1356 ed.sync_buffer_content_from_textarea();
1357 crate::motions::move_line_end(&mut ed.buffer);
1358 break_undo_group_in_insert(ed);
1359 ed.push_buffer_cursor_to_textarea();
1360 false
1361}
1362
1363pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
1366 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1367 viewport_h: u16,
1368) -> bool {
1369 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1370 scroll_cursor_rows(ed, -rows);
1371 false
1372}
1373
1374pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
1377 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1378 viewport_h: u16,
1379) -> bool {
1380 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1381 scroll_cursor_rows(ed, rows);
1382 false
1383}
1384
1385pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
1389 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1390) -> bool {
1391 use hjkl_buffer::{Edit, MotionKind};
1392 ed.sync_buffer_content_from_textarea();
1393 let cursor = buf_cursor_pos(&ed.buffer);
1394 if cursor.row == 0 && cursor.col == 0 {
1395 return true;
1396 }
1397 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1398 let word_start = buf_cursor_pos(&ed.buffer);
1399 if word_start == cursor {
1400 return true;
1401 }
1402 buf_set_cursor_pos(&mut ed.buffer, cursor);
1403 ed.mutate_edit(Edit::DeleteRange {
1404 start: word_start,
1405 end: cursor,
1406 kind: MotionKind::Char,
1407 });
1408 ed.push_buffer_cursor_to_textarea();
1409 true
1410}
1411
1412pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
1415 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1416) -> bool {
1417 use hjkl_buffer::{Edit, MotionKind, Position};
1418 ed.sync_buffer_content_from_textarea();
1419 let cursor = buf_cursor_pos(&ed.buffer);
1420 if cursor.col > 0 {
1421 ed.mutate_edit(Edit::DeleteRange {
1422 start: Position::new(cursor.row, 0),
1423 end: cursor,
1424 kind: MotionKind::Char,
1425 });
1426 ed.push_buffer_cursor_to_textarea();
1427 }
1428 true
1429}
1430
1431pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
1435 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1436) -> bool {
1437 use hjkl_buffer::{Edit, MotionKind, Position};
1438 ed.sync_buffer_content_from_textarea();
1439 let cursor = buf_cursor_pos(&ed.buffer);
1440 if cursor.col > 0 {
1441 ed.mutate_edit(Edit::DeleteRange {
1442 start: Position::new(cursor.row, cursor.col - 1),
1443 end: cursor,
1444 kind: MotionKind::Char,
1445 });
1446 } else if cursor.row > 0 {
1447 let prev_row = cursor.row - 1;
1448 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1449 ed.mutate_edit(Edit::JoinLines {
1450 row: prev_row,
1451 count: 1,
1452 with_space: false,
1453 });
1454 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1455 }
1456 ed.push_buffer_cursor_to_textarea();
1457 true
1458}
1459
1460pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
1463 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1464) -> bool {
1465 let (row, col) = ed.cursor();
1466 let sw = ed.settings().shiftwidth;
1467 indent_rows(ed, row, row, 1);
1468 ed.jump_cursor(row, col + sw);
1469 true
1470}
1471
1472pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
1475 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1476) -> bool {
1477 let (row, col) = ed.cursor();
1478 let before_len = buf_line_bytes(&ed.buffer, row);
1479 outdent_rows(ed, row, row, 1);
1480 let after_len = buf_line_bytes(&ed.buffer, row);
1481 let stripped = before_len.saturating_sub(after_len);
1482 let new_col = col.saturating_sub(stripped);
1483 ed.jump_cursor(row, new_col);
1484 true
1485}
1486
1487pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
1491 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1492) -> bool {
1493 ed.vim.one_shot_normal = true;
1494 ed.vim.mode = Mode::Normal;
1495 ed.vim.current_mode = crate::VimMode::Normal;
1497 false
1498}
1499
1500pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
1504 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1505) -> bool {
1506 ed.vim.insert_pending_register = true;
1507 false
1508}
1509
1510pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
1514 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1515 reg: char,
1516) -> bool {
1517 insert_register_text(ed, reg);
1518 true
1521}
1522
1523pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
1528 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1529) -> bool {
1530 finish_insert_session(ed);
1531 ed.vim.mode = Mode::Normal;
1532 ed.vim.current_mode = crate::VimMode::Normal;
1534 let col = ed.cursor().1;
1535 ed.vim.last_insert_pos = Some(ed.cursor());
1536 if col > 0 {
1537 crate::motions::move_left(&mut ed.buffer, 1);
1538 ed.push_buffer_cursor_to_textarea();
1539 }
1540 ed.sticky_col = Some(ed.cursor().1);
1541 true
1542}
1543
1544#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1549pub enum ScrollDir {
1550 Down,
1552 Up,
1554}
1555
1556pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
1561 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1562 count: usize,
1563) {
1564 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
1565}
1566
1567pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
1569 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1570 count: usize,
1571) {
1572 move_first_non_whitespace(ed);
1573 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
1574}
1575
1576pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
1578 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1579 count: usize,
1580) {
1581 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1582 ed.push_buffer_cursor_to_textarea();
1583 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
1584}
1585
1586pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
1588 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1589 count: usize,
1590) {
1591 crate::motions::move_line_end(&mut ed.buffer);
1592 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1593 ed.push_buffer_cursor_to_textarea();
1594 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
1595}
1596
1597pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
1599 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1600 count: usize,
1601) {
1602 use hjkl_buffer::{Edit, Position};
1603 ed.push_undo();
1604 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
1605 ed.sync_buffer_content_from_textarea();
1606 let row = buf_cursor_pos(&ed.buffer).row;
1607 let line_chars = buf_line_chars(&ed.buffer, row);
1608 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
1609 let indent = compute_enter_indent(&ed.settings, &prev_line);
1610 ed.mutate_edit(Edit::InsertStr {
1611 at: Position::new(row, line_chars),
1612 text: format!("\n{indent}"),
1613 });
1614 ed.push_buffer_cursor_to_textarea();
1615}
1616
1617pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
1619 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1620 count: usize,
1621) {
1622 use hjkl_buffer::{Edit, Position};
1623 ed.push_undo();
1624 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
1625 ed.sync_buffer_content_from_textarea();
1626 let row = buf_cursor_pos(&ed.buffer).row;
1627 let indent = if row > 0 {
1628 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
1629 compute_enter_indent(&ed.settings, &above)
1630 } else {
1631 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
1632 cur.chars()
1633 .take_while(|c| *c == ' ' || *c == '\t')
1634 .collect::<String>()
1635 };
1636 ed.mutate_edit(Edit::InsertStr {
1637 at: Position::new(row, 0),
1638 text: format!("{indent}\n"),
1639 });
1640 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1641 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1642 let new_row = buf_cursor_pos(&ed.buffer).row;
1643 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
1644 ed.push_buffer_cursor_to_textarea();
1645}
1646
1647pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
1649 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1650 count: usize,
1651) {
1652 begin_insert(ed, count.max(1), InsertReason::Replace);
1653}
1654
1655pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
1660 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1661 count: usize,
1662) {
1663 do_char_delete(ed, true, count.max(1));
1664 if !ed.vim.replaying {
1665 ed.vim.last_change = Some(LastChange::CharDel {
1666 forward: true,
1667 count: count.max(1),
1668 });
1669 }
1670}
1671
1672pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
1675 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1676 count: usize,
1677) {
1678 do_char_delete(ed, false, count.max(1));
1679 if !ed.vim.replaying {
1680 ed.vim.last_change = Some(LastChange::CharDel {
1681 forward: false,
1682 count: count.max(1),
1683 });
1684 }
1685}
1686
1687pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
1690 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1691 count: usize,
1692) {
1693 use hjkl_buffer::{Edit, MotionKind, Position};
1694 ed.push_undo();
1695 ed.sync_buffer_content_from_textarea();
1696 for _ in 0..count.max(1) {
1697 let cursor = buf_cursor_pos(&ed.buffer);
1698 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1699 if cursor.col >= line_chars {
1700 break;
1701 }
1702 ed.mutate_edit(Edit::DeleteRange {
1703 start: cursor,
1704 end: Position::new(cursor.row, cursor.col + 1),
1705 kind: MotionKind::Char,
1706 });
1707 }
1708 ed.push_buffer_cursor_to_textarea();
1709 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
1710 if !ed.vim.replaying {
1711 ed.vim.last_change = Some(LastChange::OpMotion {
1712 op: Operator::Change,
1713 motion: Motion::Right,
1714 count: count.max(1),
1715 inserted: None,
1716 });
1717 }
1718}
1719
1720pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
1723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1724 count: usize,
1725) {
1726 execute_line_op(ed, Operator::Change, count.max(1));
1727 if !ed.vim.replaying {
1728 ed.vim.last_change = Some(LastChange::LineOp {
1729 op: Operator::Change,
1730 count: count.max(1),
1731 inserted: None,
1732 });
1733 }
1734}
1735
1736pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1739 ed.push_undo();
1740 delete_to_eol(ed);
1741 crate::motions::move_left(&mut ed.buffer, 1);
1742 ed.push_buffer_cursor_to_textarea();
1743 if !ed.vim.replaying {
1744 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
1745 }
1746}
1747
1748pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1751 ed.push_undo();
1752 delete_to_eol(ed);
1753 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
1754}
1755
1756pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
1758 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1759 count: usize,
1760) {
1761 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
1762}
1763
1764pub(crate) fn join_line_bridge<H: crate::types::Host>(
1767 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1768 count: usize,
1769) {
1770 for _ in 0..count.max(1) {
1771 ed.push_undo();
1772 join_line(ed);
1773 }
1774 if !ed.vim.replaying {
1775 ed.vim.last_change = Some(LastChange::JoinLine {
1776 count: count.max(1),
1777 });
1778 }
1779}
1780
1781pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
1784 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1785 count: usize,
1786) {
1787 for _ in 0..count.max(1) {
1788 ed.push_undo();
1789 toggle_case_at_cursor(ed);
1790 }
1791 if !ed.vim.replaying {
1792 ed.vim.last_change = Some(LastChange::ToggleCase {
1793 count: count.max(1),
1794 });
1795 }
1796}
1797
1798pub(crate) fn paste_after_bridge<H: crate::types::Host>(
1802 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1803 count: usize,
1804) {
1805 do_paste(ed, false, count.max(1));
1806 if !ed.vim.replaying {
1807 ed.vim.last_change = Some(LastChange::Paste {
1808 before: false,
1809 count: count.max(1),
1810 });
1811 }
1812}
1813
1814pub(crate) fn paste_before_bridge<H: crate::types::Host>(
1818 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1819 count: usize,
1820) {
1821 do_paste(ed, true, count.max(1));
1822 if !ed.vim.replaying {
1823 ed.vim.last_change = Some(LastChange::Paste {
1824 before: true,
1825 count: count.max(1),
1826 });
1827 }
1828}
1829
1830pub(crate) fn jump_back_bridge<H: crate::types::Host>(
1835 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1836 count: usize,
1837) {
1838 for _ in 0..count.max(1) {
1839 jump_back(ed);
1840 }
1841}
1842
1843pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
1846 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1847 count: usize,
1848) {
1849 for _ in 0..count.max(1) {
1850 jump_forward(ed);
1851 }
1852}
1853
1854pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
1859 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1860 dir: ScrollDir,
1861 count: usize,
1862) {
1863 let rows = viewport_full_rows(ed, count) as isize;
1864 match dir {
1865 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1866 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1867 }
1868}
1869
1870pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
1873 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1874 dir: ScrollDir,
1875 count: usize,
1876) {
1877 let rows = viewport_half_rows(ed, count) as isize;
1878 match dir {
1879 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1880 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1881 }
1882}
1883
1884pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
1888 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1889 dir: ScrollDir,
1890 count: usize,
1891) {
1892 let n = count.max(1);
1893 let total = buf_row_count(&ed.buffer);
1894 let last = total.saturating_sub(1);
1895 let h = ed.viewport_height_value() as usize;
1896 let vp = ed.host().viewport();
1897 let cur_top = vp.top_row;
1898 let new_top = match dir {
1899 ScrollDir::Down => (cur_top + n).min(last),
1900 ScrollDir::Up => cur_top.saturating_sub(n),
1901 };
1902 ed.set_viewport_top(new_top);
1903 let (row, col) = ed.cursor();
1905 let bot = (new_top + h).saturating_sub(1).min(last);
1906 let clamped = row.max(new_top).min(bot);
1907 if clamped != row {
1908 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
1909 ed.push_buffer_cursor_to_textarea();
1910 }
1911}
1912
1913pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
1918 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1919 forward: bool,
1920 count: usize,
1921) {
1922 if let Some(pattern) = ed.vim.last_search.clone() {
1923 ed.push_search_pattern(&pattern);
1924 }
1925 if ed.search_state().pattern.is_none() {
1926 return;
1927 }
1928 let go_forward = ed.vim.last_search_forward == forward;
1929 for _ in 0..count.max(1) {
1930 if go_forward {
1931 ed.search_advance_forward(true);
1932 } else {
1933 ed.search_advance_backward(true);
1934 }
1935 }
1936 ed.push_buffer_cursor_to_textarea();
1937}
1938
1939pub(crate) fn word_search_bridge<H: crate::types::Host>(
1943 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1944 forward: bool,
1945 whole_word: bool,
1946 count: usize,
1947) {
1948 word_at_cursor_search(ed, forward, whole_word, count.max(1));
1949}
1950
1951#[allow(dead_code)]
1956#[inline]
1957pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1958 do_undo(ed);
1959}
1960
1961#[inline]
1976pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
1977 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1978 mode: Mode,
1979) {
1980 ed.vim.mode = mode;
1981 ed.vim.current_mode = ed.vim.public_mode();
1982}
1983
1984pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
1987 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1988) {
1989 let cur = ed.cursor();
1990 ed.vim.visual_anchor = cur;
1991 set_vim_mode_bridge(ed, Mode::Visual);
1992}
1993
1994pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
1997 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1998) {
1999 let (row, _) = ed.cursor();
2000 ed.vim.visual_line_anchor = row;
2001 set_vim_mode_bridge(ed, Mode::VisualLine);
2002}
2003
2004pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
2008 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2009) {
2010 let cur = ed.cursor();
2011 ed.vim.block_anchor = cur;
2012 ed.vim.block_vcol = cur.1;
2013 set_vim_mode_bridge(ed, Mode::VisualBlock);
2014}
2015
2016pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
2021 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2022) {
2023 let snap: Option<LastVisual> = match ed.vim.mode {
2025 Mode::Visual => Some(LastVisual {
2026 mode: Mode::Visual,
2027 anchor: ed.vim.visual_anchor,
2028 cursor: ed.cursor(),
2029 block_vcol: 0,
2030 }),
2031 Mode::VisualLine => Some(LastVisual {
2032 mode: Mode::VisualLine,
2033 anchor: (ed.vim.visual_line_anchor, 0),
2034 cursor: ed.cursor(),
2035 block_vcol: 0,
2036 }),
2037 Mode::VisualBlock => Some(LastVisual {
2038 mode: Mode::VisualBlock,
2039 anchor: ed.vim.block_anchor,
2040 cursor: ed.cursor(),
2041 block_vcol: ed.vim.block_vcol,
2042 }),
2043 _ => None,
2044 };
2045 ed.vim.pending = Pending::None;
2047 ed.vim.count = 0;
2048 ed.vim.insert_session = None;
2049 set_vim_mode_bridge(ed, Mode::Normal);
2050 if let Some(snap) = snap {
2054 let (lo, hi) = match snap.mode {
2055 Mode::Visual => {
2056 if snap.anchor <= snap.cursor {
2057 (snap.anchor, snap.cursor)
2058 } else {
2059 (snap.cursor, snap.anchor)
2060 }
2061 }
2062 Mode::VisualLine => {
2063 let r_lo = snap.anchor.0.min(snap.cursor.0);
2064 let r_hi = snap.anchor.0.max(snap.cursor.0);
2065 let last_col = ed
2066 .buffer()
2067 .lines()
2068 .get(r_hi)
2069 .map(|l| l.chars().count().saturating_sub(1))
2070 .unwrap_or(0);
2071 ((r_lo, 0), (r_hi, last_col))
2072 }
2073 Mode::VisualBlock => {
2074 let (r1, c1) = snap.anchor;
2075 let (r2, c2) = snap.cursor;
2076 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2077 }
2078 _ => {
2079 if snap.anchor <= snap.cursor {
2080 (snap.anchor, snap.cursor)
2081 } else {
2082 (snap.cursor, snap.anchor)
2083 }
2084 }
2085 };
2086 ed.set_mark('<', lo);
2087 ed.set_mark('>', hi);
2088 ed.vim.last_visual = Some(snap);
2089 }
2090}
2091
2092pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2098 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2099) {
2100 match ed.vim.mode {
2101 Mode::Visual => {
2102 let cur = ed.cursor();
2103 let anchor = ed.vim.visual_anchor;
2104 ed.vim.visual_anchor = cur;
2105 ed.jump_cursor(anchor.0, anchor.1);
2106 }
2107 Mode::VisualLine => {
2108 let cur_row = ed.cursor().0;
2109 let anchor_row = ed.vim.visual_line_anchor;
2110 ed.vim.visual_line_anchor = cur_row;
2111 ed.jump_cursor(anchor_row, 0);
2112 }
2113 Mode::VisualBlock => {
2114 let cur = ed.cursor();
2115 let anchor = ed.vim.block_anchor;
2116 ed.vim.block_anchor = cur;
2117 ed.vim.block_vcol = anchor.1;
2118 ed.jump_cursor(anchor.0, anchor.1);
2119 }
2120 _ => {}
2121 }
2122}
2123
2124pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
2128 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2129) {
2130 if let Some(snap) = ed.vim.last_visual {
2131 match snap.mode {
2132 Mode::Visual => {
2133 ed.vim.visual_anchor = snap.anchor;
2134 set_vim_mode_bridge(ed, Mode::Visual);
2135 }
2136 Mode::VisualLine => {
2137 ed.vim.visual_line_anchor = snap.anchor.0;
2138 set_vim_mode_bridge(ed, Mode::VisualLine);
2139 }
2140 Mode::VisualBlock => {
2141 ed.vim.block_anchor = snap.anchor;
2142 ed.vim.block_vcol = snap.block_vcol;
2143 set_vim_mode_bridge(ed, Mode::VisualBlock);
2144 }
2145 _ => {}
2146 }
2147 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2148 }
2149}
2150
2151pub(crate) fn set_mode_bridge<H: crate::types::Host>(
2157 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2158 mode: crate::VimMode,
2159) {
2160 let internal = match mode {
2161 crate::VimMode::Normal => Mode::Normal,
2162 crate::VimMode::Insert => Mode::Insert,
2163 crate::VimMode::Visual => Mode::Visual,
2164 crate::VimMode::VisualLine => Mode::VisualLine,
2165 crate::VimMode::VisualBlock => Mode::VisualBlock,
2166 };
2167 ed.vim.mode = internal;
2168 ed.vim.current_mode = mode;
2169}
2170
2171pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2188 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2189 ch: char,
2190) {
2191 if ch.is_ascii_lowercase() {
2192 let pos = ed.cursor();
2193 ed.set_mark(ch, pos);
2194 } else if ch.is_ascii_uppercase() {
2195 let pos = ed.cursor();
2196 let bid = ed.current_buffer_id();
2197 ed.set_global_mark(ch, bid, pos);
2198 tracing::debug!(
2199 mark = ch as u32,
2200 buffer_id = bid,
2201 row = pos.0,
2202 col = pos.1,
2203 "global mark set"
2204 );
2205 }
2206 }
2208
2209pub(crate) fn goto_mark<H: crate::types::Host>(
2218 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2219 ch: char,
2220 linewise: bool,
2221) {
2222 let target = match ch {
2223 'a'..='z' => ed.mark(ch),
2224 '\'' | '`' => ed.vim.jump_back.last().copied(),
2225 '.' => ed.vim.last_edit_pos,
2226 '[' | ']' | '<' | '>' => ed.mark(ch),
2227 _ => None,
2228 };
2229 let Some((row, col)) = target else {
2230 return;
2231 };
2232 let pre = ed.cursor();
2233 let (r, c_clamped) = clamp_pos(ed, (row, col));
2234 if linewise {
2235 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2236 ed.push_buffer_cursor_to_textarea();
2237 move_first_non_whitespace(ed);
2238 } else {
2239 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2240 ed.push_buffer_cursor_to_textarea();
2241 }
2242 if ed.cursor() != pre {
2243 ed.push_jump(pre);
2244 }
2245 ed.sticky_col = Some(ed.cursor().1);
2246}
2247
2248pub(crate) fn try_goto_mark<H: crate::types::Host>(
2257 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2258 ch: char,
2259 linewise: bool,
2260) -> crate::editor::MarkJump {
2261 use crate::editor::MarkJump;
2262 match ch {
2263 'A'..='Z' => {
2264 let Some((bid, row, col)) = ed.global_mark(ch) else {
2265 return MarkJump::Unset;
2266 };
2267 if bid != ed.current_buffer_id() {
2268 tracing::debug!(
2269 mark = ch as u32,
2270 buffer_id = bid,
2271 row,
2272 col,
2273 "global mark cross-buffer jump"
2274 );
2275 return MarkJump::CrossBuffer {
2276 buffer_id: bid,
2277 row,
2278 col,
2279 };
2280 }
2281 let pre = ed.cursor();
2283 let (r, c_clamped) = clamp_pos(ed, (row, col));
2284 if linewise {
2285 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2286 ed.push_buffer_cursor_to_textarea();
2287 move_first_non_whitespace(ed);
2288 } else {
2289 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2290 ed.push_buffer_cursor_to_textarea();
2291 }
2292 if ed.cursor() != pre {
2293 ed.push_jump(pre);
2294 }
2295 ed.sticky_col = Some(ed.cursor().1);
2296 MarkJump::SameBuffer
2297 }
2298 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
2299 goto_mark(ed, ch, linewise);
2300 MarkJump::SameBuffer
2301 }
2302 _ => MarkJump::Unset,
2303 }
2304}
2305
2306pub fn op_is_change(op: Operator) -> bool {
2310 matches!(op, Operator::Delete | Operator::Change)
2311}
2312
2313pub(crate) const JUMPLIST_MAX: usize = 100;
2317
2318fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2321 let Some(target) = ed.vim.jump_back.pop() else {
2322 return;
2323 };
2324 let cur = ed.cursor();
2325 ed.vim.jump_fwd.push(cur);
2326 let (r, c) = clamp_pos(ed, target);
2327 ed.jump_cursor(r, c);
2328 ed.sticky_col = Some(c);
2329}
2330
2331fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2334 let Some(target) = ed.vim.jump_fwd.pop() else {
2335 return;
2336 };
2337 let cur = ed.cursor();
2338 ed.vim.jump_back.push(cur);
2339 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2340 ed.vim.jump_back.remove(0);
2341 }
2342 let (r, c) = clamp_pos(ed, target);
2343 ed.jump_cursor(r, c);
2344 ed.sticky_col = Some(c);
2345}
2346
2347fn clamp_pos<H: crate::types::Host>(
2350 ed: &Editor<hjkl_buffer::Buffer, H>,
2351 pos: (usize, usize),
2352) -> (usize, usize) {
2353 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2354 let r = pos.0.min(last_row);
2355 let line_len = buf_line_chars(&ed.buffer, r);
2356 let c = pos.1.min(line_len.saturating_sub(1));
2357 (r, c)
2358}
2359
2360fn is_big_jump(motion: &Motion) -> bool {
2362 matches!(
2363 motion,
2364 Motion::FileTop
2365 | Motion::FileBottom
2366 | Motion::MatchBracket
2367 | Motion::WordAtCursor { .. }
2368 | Motion::SearchNext { .. }
2369 | Motion::ViewportTop
2370 | Motion::ViewportMiddle
2371 | Motion::ViewportBottom
2372 )
2373}
2374
2375fn viewport_half_rows<H: crate::types::Host>(
2380 ed: &Editor<hjkl_buffer::Buffer, H>,
2381 count: usize,
2382) -> usize {
2383 let h = ed.viewport_height_value() as usize;
2384 (h / 2).max(1).saturating_mul(count.max(1))
2385}
2386
2387fn viewport_full_rows<H: crate::types::Host>(
2390 ed: &Editor<hjkl_buffer::Buffer, H>,
2391 count: usize,
2392) -> usize {
2393 let h = ed.viewport_height_value() as usize;
2394 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2395}
2396
2397fn scroll_cursor_rows<H: crate::types::Host>(
2402 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2403 delta: isize,
2404) {
2405 if delta == 0 {
2406 return;
2407 }
2408 ed.sync_buffer_content_from_textarea();
2409 let (row, _) = ed.cursor();
2410 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2411 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2412 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2413 crate::motions::move_first_non_blank(&mut ed.buffer);
2414 ed.push_buffer_cursor_to_textarea();
2415 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2416}
2417
2418pub fn parse_motion(input: &Input) -> Option<Motion> {
2424 if input.ctrl {
2425 return None;
2426 }
2427 match input.key {
2428 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2429 Key::Char('l') | Key::Right => Some(Motion::Right),
2430 Key::Char('j') | Key::Down => Some(Motion::Down),
2431 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
2433 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
2435 Key::Char('_') => Some(Motion::FirstNonBlankLine),
2437 Key::Char('k') | Key::Up => Some(Motion::Up),
2438 Key::Char('w') => Some(Motion::WordFwd),
2439 Key::Char('W') => Some(Motion::BigWordFwd),
2440 Key::Char('b') => Some(Motion::WordBack),
2441 Key::Char('B') => Some(Motion::BigWordBack),
2442 Key::Char('e') => Some(Motion::WordEnd),
2443 Key::Char('E') => Some(Motion::BigWordEnd),
2444 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2445 Key::Char('^') => Some(Motion::FirstNonBlank),
2446 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2447 Key::Char('G') => Some(Motion::FileBottom),
2448 Key::Char('%') => Some(Motion::MatchBracket),
2449 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2450 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2451 Key::Char('*') => Some(Motion::WordAtCursor {
2452 forward: true,
2453 whole_word: true,
2454 }),
2455 Key::Char('#') => Some(Motion::WordAtCursor {
2456 forward: false,
2457 whole_word: true,
2458 }),
2459 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2460 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2461 Key::Char('H') => Some(Motion::ViewportTop),
2462 Key::Char('M') => Some(Motion::ViewportMiddle),
2463 Key::Char('L') => Some(Motion::ViewportBottom),
2464 Key::Char('{') => Some(Motion::ParagraphPrev),
2465 Key::Char('}') => Some(Motion::ParagraphNext),
2466 Key::Char('(') => Some(Motion::SentencePrev),
2467 Key::Char(')') => Some(Motion::SentenceNext),
2468 Key::Char('|') => Some(Motion::GotoColumn),
2469 _ => None,
2470 }
2471}
2472
2473pub(crate) fn execute_motion<H: crate::types::Host>(
2476 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2477 motion: Motion,
2478 count: usize,
2479) {
2480 let count = count.max(1);
2481 let motion = match motion {
2483 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2484 Some((ch, forward, till)) => Motion::Find {
2485 ch,
2486 forward: if reverse { !forward } else { forward },
2487 till,
2488 },
2489 None => return,
2490 },
2491 other => other,
2492 };
2493 let pre_pos = ed.cursor();
2494 let pre_col = pre_pos.1;
2495 apply_motion_cursor(ed, &motion, count);
2496 let post_pos = ed.cursor();
2497 if is_big_jump(&motion) && pre_pos != post_pos {
2498 ed.push_jump(pre_pos);
2499 }
2500 apply_sticky_col(ed, &motion, pre_col);
2501 ed.sync_buffer_from_textarea();
2506}
2507
2508fn execute_motion_with_block_vcol<H: crate::types::Host>(
2519 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2520 motion: Motion,
2521 count: usize,
2522) {
2523 let motion_copy = motion.clone();
2524 execute_motion(ed, motion, count);
2525 if ed.vim.mode == Mode::VisualBlock {
2526 update_block_vcol(ed, &motion_copy);
2527 }
2528}
2529
2530pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2559 kind: crate::MotionKind,
2560 count: usize,
2561) {
2562 let count = count.max(1);
2563 match kind {
2564 crate::MotionKind::CharLeft => {
2565 execute_motion_with_block_vcol(ed, Motion::Left, count);
2566 }
2567 crate::MotionKind::CharRight => {
2568 execute_motion_with_block_vcol(ed, Motion::Right, count);
2569 }
2570 crate::MotionKind::LineDown => {
2571 execute_motion_with_block_vcol(ed, Motion::Down, count);
2572 }
2573 crate::MotionKind::LineUp => {
2574 execute_motion_with_block_vcol(ed, Motion::Up, count);
2575 }
2576 crate::MotionKind::FirstNonBlankDown => {
2577 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2582 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2583 crate::motions::move_first_non_blank(&mut ed.buffer);
2584 ed.push_buffer_cursor_to_textarea();
2585 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2586 ed.sync_buffer_from_textarea();
2587 }
2588 crate::MotionKind::FirstNonBlankUp => {
2589 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2592 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2593 crate::motions::move_first_non_blank(&mut ed.buffer);
2594 ed.push_buffer_cursor_to_textarea();
2595 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2596 ed.sync_buffer_from_textarea();
2597 }
2598 crate::MotionKind::WordForward => {
2599 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
2600 }
2601 crate::MotionKind::BigWordForward => {
2602 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
2603 }
2604 crate::MotionKind::WordBackward => {
2605 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
2606 }
2607 crate::MotionKind::BigWordBackward => {
2608 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
2609 }
2610 crate::MotionKind::WordEnd => {
2611 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
2612 }
2613 crate::MotionKind::BigWordEnd => {
2614 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
2615 }
2616 crate::MotionKind::LineStart => {
2617 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
2620 }
2621 crate::MotionKind::FirstNonBlank => {
2622 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
2625 }
2626 crate::MotionKind::GotoLine => {
2627 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
2636 }
2637 crate::MotionKind::LineEnd => {
2638 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
2642 }
2643 crate::MotionKind::FindRepeat => {
2644 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
2648 }
2649 crate::MotionKind::FindRepeatReverse => {
2650 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
2654 }
2655 crate::MotionKind::BracketMatch => {
2656 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
2661 }
2662 crate::MotionKind::ViewportTop => {
2663 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
2666 }
2667 crate::MotionKind::ViewportMiddle => {
2668 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
2671 }
2672 crate::MotionKind::ViewportBottom => {
2673 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
2676 }
2677 crate::MotionKind::HalfPageDown => {
2678 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
2682 }
2683 crate::MotionKind::HalfPageUp => {
2684 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
2687 }
2688 crate::MotionKind::FullPageDown => {
2689 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
2692 }
2693 crate::MotionKind::FullPageUp => {
2694 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
2697 }
2698 crate::MotionKind::FirstNonBlankLine => {
2699 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
2700 }
2701 crate::MotionKind::SectionBackward => {
2702 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
2703 }
2704 crate::MotionKind::SectionForward => {
2705 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
2706 }
2707 crate::MotionKind::SectionEndBackward => {
2708 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
2709 }
2710 crate::MotionKind::SectionEndForward => {
2711 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
2712 }
2713 }
2714}
2715
2716fn apply_sticky_col<H: crate::types::Host>(
2721 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2722 motion: &Motion,
2723 pre_col: usize,
2724) {
2725 if is_vertical_motion(motion) {
2726 let want = ed.sticky_col.unwrap_or(pre_col);
2727 ed.sticky_col = Some(want);
2730 let (row, _) = ed.cursor();
2731 let line_len = buf_line_chars(&ed.buffer, row);
2732 let max_col = line_len.saturating_sub(1);
2736 let target = want.min(max_col);
2737 buf_set_cursor_rc(&mut ed.buffer, row, target);
2741 } else {
2742 ed.sticky_col = Some(ed.cursor().1);
2745 }
2746}
2747
2748fn is_vertical_motion(motion: &Motion) -> bool {
2749 matches!(
2753 motion,
2754 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2755 )
2756}
2757
2758fn apply_motion_cursor<H: crate::types::Host>(
2759 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2760 motion: &Motion,
2761 count: usize,
2762) {
2763 apply_motion_cursor_ctx(ed, motion, count, false)
2764}
2765
2766pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
2767 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2768 motion: &Motion,
2769 count: usize,
2770 as_operator: bool,
2771) {
2772 match motion {
2773 Motion::Left => {
2774 crate::motions::move_left(&mut ed.buffer, count);
2776 ed.push_buffer_cursor_to_textarea();
2777 }
2778 Motion::Right => {
2779 if as_operator {
2783 crate::motions::move_right_to_end(&mut ed.buffer, count);
2784 } else {
2785 crate::motions::move_right_in_line(&mut ed.buffer, count);
2786 }
2787 ed.push_buffer_cursor_to_textarea();
2788 }
2789 Motion::Up => {
2790 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2794 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2795 ed.push_buffer_cursor_to_textarea();
2796 }
2797 Motion::Down => {
2798 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2799 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2800 ed.push_buffer_cursor_to_textarea();
2801 }
2802 Motion::ScreenUp => {
2803 let v = *ed.host.viewport();
2804 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2805 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2806 ed.push_buffer_cursor_to_textarea();
2807 }
2808 Motion::ScreenDown => {
2809 let v = *ed.host.viewport();
2810 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2811 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2812 ed.push_buffer_cursor_to_textarea();
2813 }
2814 Motion::WordFwd => {
2815 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2816 ed.push_buffer_cursor_to_textarea();
2817 }
2818 Motion::WordBack => {
2819 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2820 ed.push_buffer_cursor_to_textarea();
2821 }
2822 Motion::WordEnd => {
2823 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2824 ed.push_buffer_cursor_to_textarea();
2825 }
2826 Motion::BigWordFwd => {
2827 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2828 ed.push_buffer_cursor_to_textarea();
2829 }
2830 Motion::BigWordBack => {
2831 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2832 ed.push_buffer_cursor_to_textarea();
2833 }
2834 Motion::BigWordEnd => {
2835 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2836 ed.push_buffer_cursor_to_textarea();
2837 }
2838 Motion::WordEndBack => {
2839 crate::motions::move_word_end_back(
2840 &mut ed.buffer,
2841 false,
2842 count,
2843 &ed.settings.iskeyword,
2844 );
2845 ed.push_buffer_cursor_to_textarea();
2846 }
2847 Motion::BigWordEndBack => {
2848 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2849 ed.push_buffer_cursor_to_textarea();
2850 }
2851 Motion::LineStart => {
2852 crate::motions::move_line_start(&mut ed.buffer);
2853 ed.push_buffer_cursor_to_textarea();
2854 }
2855 Motion::FirstNonBlank => {
2856 crate::motions::move_first_non_blank(&mut ed.buffer);
2857 ed.push_buffer_cursor_to_textarea();
2858 }
2859 Motion::LineEnd => {
2860 crate::motions::move_line_end(&mut ed.buffer);
2862 ed.push_buffer_cursor_to_textarea();
2863 }
2864 Motion::FileTop => {
2865 if count > 1 {
2868 crate::motions::move_bottom(&mut ed.buffer, count);
2869 } else {
2870 crate::motions::move_top(&mut ed.buffer);
2871 }
2872 ed.push_buffer_cursor_to_textarea();
2873 }
2874 Motion::FileBottom => {
2875 if count > 1 {
2878 crate::motions::move_bottom(&mut ed.buffer, count);
2879 } else {
2880 crate::motions::move_bottom(&mut ed.buffer, 0);
2881 }
2882 ed.push_buffer_cursor_to_textarea();
2883 }
2884 Motion::Find { ch, forward, till } => {
2885 for _ in 0..count {
2886 if !find_char_on_line(ed, *ch, *forward, *till) {
2887 break;
2888 }
2889 }
2890 }
2891 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2893 let _ = matching_bracket(ed);
2894 }
2895 Motion::WordAtCursor {
2896 forward,
2897 whole_word,
2898 } => {
2899 word_at_cursor_search(ed, *forward, *whole_word, count);
2900 }
2901 Motion::SearchNext { reverse } => {
2902 if let Some(pattern) = ed.vim.last_search.clone() {
2906 ed.push_search_pattern(&pattern);
2907 }
2908 if ed.search_state().pattern.is_none() {
2909 return;
2910 }
2911 let forward = ed.vim.last_search_forward != *reverse;
2915 for _ in 0..count.max(1) {
2916 if forward {
2917 ed.search_advance_forward(true);
2918 } else {
2919 ed.search_advance_backward(true);
2920 }
2921 }
2922 ed.push_buffer_cursor_to_textarea();
2923 }
2924 Motion::ViewportTop => {
2925 let v = *ed.host().viewport();
2926 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2927 ed.push_buffer_cursor_to_textarea();
2928 }
2929 Motion::ViewportMiddle => {
2930 let v = *ed.host().viewport();
2931 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2932 ed.push_buffer_cursor_to_textarea();
2933 }
2934 Motion::ViewportBottom => {
2935 let v = *ed.host().viewport();
2936 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2937 ed.push_buffer_cursor_to_textarea();
2938 }
2939 Motion::LastNonBlank => {
2940 crate::motions::move_last_non_blank(&mut ed.buffer);
2941 ed.push_buffer_cursor_to_textarea();
2942 }
2943 Motion::LineMiddle => {
2944 let row = ed.cursor().0;
2945 let line_chars = buf_line_chars(&ed.buffer, row);
2946 let target = line_chars / 2;
2949 ed.jump_cursor(row, target);
2950 }
2951 Motion::ParagraphPrev => {
2952 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2953 ed.push_buffer_cursor_to_textarea();
2954 }
2955 Motion::ParagraphNext => {
2956 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2957 ed.push_buffer_cursor_to_textarea();
2958 }
2959 Motion::SentencePrev => {
2960 for _ in 0..count.max(1) {
2961 if let Some((row, col)) = sentence_boundary(ed, false) {
2962 ed.jump_cursor(row, col);
2963 }
2964 }
2965 }
2966 Motion::SentenceNext => {
2967 for _ in 0..count.max(1) {
2968 if let Some((row, col)) = sentence_boundary(ed, true) {
2969 ed.jump_cursor(row, col);
2970 }
2971 }
2972 }
2973 Motion::SectionBackward => {
2974 crate::motions::move_section_backward(&mut ed.buffer, count);
2975 ed.push_buffer_cursor_to_textarea();
2976 }
2977 Motion::SectionForward => {
2978 crate::motions::move_section_forward(&mut ed.buffer, count);
2979 ed.push_buffer_cursor_to_textarea();
2980 }
2981 Motion::SectionEndBackward => {
2982 crate::motions::move_section_end_backward(&mut ed.buffer, count);
2983 ed.push_buffer_cursor_to_textarea();
2984 }
2985 Motion::SectionEndForward => {
2986 crate::motions::move_section_end_forward(&mut ed.buffer, count);
2987 ed.push_buffer_cursor_to_textarea();
2988 }
2989 Motion::FirstNonBlankNextLine => {
2990 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
2991 ed.push_buffer_cursor_to_textarea();
2992 }
2993 Motion::FirstNonBlankPrevLine => {
2994 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
2995 ed.push_buffer_cursor_to_textarea();
2996 }
2997 Motion::FirstNonBlankLine => {
2998 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
2999 ed.push_buffer_cursor_to_textarea();
3000 }
3001 Motion::GotoColumn => {
3002 crate::motions::move_goto_column(&mut ed.buffer, count);
3003 ed.push_buffer_cursor_to_textarea();
3004 }
3005 }
3006}
3007
3008fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3009 ed.sync_buffer_content_from_textarea();
3015 crate::motions::move_first_non_blank(&mut ed.buffer);
3016 ed.push_buffer_cursor_to_textarea();
3017}
3018
3019fn find_char_on_line<H: crate::types::Host>(
3020 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3021 ch: char,
3022 forward: bool,
3023 till: bool,
3024) -> bool {
3025 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
3026 if moved {
3027 ed.push_buffer_cursor_to_textarea();
3028 }
3029 moved
3030}
3031
3032fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3033 let moved = crate::motions::match_bracket(&mut ed.buffer);
3034 if moved {
3035 ed.push_buffer_cursor_to_textarea();
3036 }
3037 moved
3038}
3039
3040fn word_at_cursor_search<H: crate::types::Host>(
3041 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3042 forward: bool,
3043 whole_word: bool,
3044 count: usize,
3045) {
3046 let (row, col) = ed.cursor();
3047 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
3048 let chars: Vec<char> = line.chars().collect();
3049 if chars.is_empty() {
3050 return;
3051 }
3052 let spec = ed.settings().iskeyword.clone();
3054 let is_word = |c: char| is_keyword_char(c, &spec);
3055 let mut start = col.min(chars.len().saturating_sub(1));
3056 while start > 0 && is_word(chars[start - 1]) {
3057 start -= 1;
3058 }
3059 let mut end = start;
3060 while end < chars.len() && is_word(chars[end]) {
3061 end += 1;
3062 }
3063 if end <= start {
3064 return;
3065 }
3066 let word: String = chars[start..end].iter().collect();
3067 let escaped = regex_escape(&word);
3068 let pattern = if whole_word {
3069 format!(r"\b{escaped}\b")
3070 } else {
3071 escaped
3072 };
3073 ed.push_search_pattern(&pattern);
3074 if ed.search_state().pattern.is_none() {
3075 return;
3076 }
3077 ed.vim.last_search = Some(pattern);
3079 ed.vim.last_search_forward = forward;
3080 for _ in 0..count.max(1) {
3081 if forward {
3082 ed.search_advance_forward(true);
3083 } else {
3084 ed.search_advance_backward(true);
3085 }
3086 }
3087 ed.push_buffer_cursor_to_textarea();
3088}
3089
3090fn regex_escape(s: &str) -> String {
3091 let mut out = String::with_capacity(s.len());
3092 for c in s.chars() {
3093 if matches!(
3094 c,
3095 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
3096 ) {
3097 out.push('\\');
3098 }
3099 out.push(c);
3100 }
3101 out
3102}
3103
3104pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
3118 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3119 op: Operator,
3120 motion_key: char,
3121 total_count: usize,
3122) {
3123 let input = Input {
3124 key: Key::Char(motion_key),
3125 ctrl: false,
3126 alt: false,
3127 shift: false,
3128 };
3129 let Some(motion) = parse_motion(&input) else {
3130 return;
3131 };
3132 let motion = match motion {
3133 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3134 Some((ch, forward, till)) => Motion::Find {
3135 ch,
3136 forward: if reverse { !forward } else { forward },
3137 till,
3138 },
3139 None => return,
3140 },
3141 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3143 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3144 m => m,
3145 };
3146 apply_op_with_motion(ed, op, &motion, total_count);
3147 if let Motion::Find { ch, forward, till } = &motion {
3148 ed.vim.last_find = Some((*ch, *forward, *till));
3149 }
3150 if !ed.vim.replaying && op_is_change(op) {
3151 ed.vim.last_change = Some(LastChange::OpMotion {
3152 op,
3153 motion,
3154 count: total_count,
3155 inserted: None,
3156 });
3157 }
3158}
3159
3160pub(crate) fn apply_op_double<H: crate::types::Host>(
3163 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3164 op: Operator,
3165 total_count: usize,
3166) {
3167 execute_line_op(ed, op, total_count);
3168 if !ed.vim.replaying {
3169 ed.vim.last_change = Some(LastChange::LineOp {
3170 op,
3171 count: total_count,
3172 inserted: None,
3173 });
3174 }
3175}
3176
3177pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3187 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3188 op: Operator,
3189 ch: char,
3190 total_count: usize,
3191) {
3192 if matches!(
3195 op,
3196 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3197 ) {
3198 let op_char = match op {
3199 Operator::Uppercase => 'U',
3200 Operator::Lowercase => 'u',
3201 Operator::ToggleCase => '~',
3202 _ => unreachable!(),
3203 };
3204 if ch == op_char {
3205 execute_line_op(ed, op, total_count);
3206 if !ed.vim.replaying {
3207 ed.vim.last_change = Some(LastChange::LineOp {
3208 op,
3209 count: total_count,
3210 inserted: None,
3211 });
3212 }
3213 return;
3214 }
3215 }
3216 let motion = match ch {
3217 'g' => Motion::FileTop,
3218 'e' => Motion::WordEndBack,
3219 'E' => Motion::BigWordEndBack,
3220 'j' => Motion::ScreenDown,
3221 'k' => Motion::ScreenUp,
3222 _ => return, };
3224 apply_op_with_motion(ed, op, &motion, total_count);
3225 if !ed.vim.replaying && op_is_change(op) {
3226 ed.vim.last_change = Some(LastChange::OpMotion {
3227 op,
3228 motion,
3229 count: total_count,
3230 inserted: None,
3231 });
3232 }
3233}
3234
3235pub(crate) fn apply_after_g<H: crate::types::Host>(
3240 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3241 ch: char,
3242 count: usize,
3243) {
3244 match ch {
3245 'g' => {
3246 let pre = ed.cursor();
3248 if count > 1 {
3249 ed.jump_cursor(count - 1, 0);
3250 } else {
3251 ed.jump_cursor(0, 0);
3252 }
3253 move_first_non_whitespace(ed);
3254 ed.sticky_col = Some(ed.cursor().1);
3257 if ed.cursor() != pre {
3258 ed.push_jump(pre);
3259 }
3260 }
3261 'e' => execute_motion(ed, Motion::WordEndBack, count),
3262 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3263 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3265 'M' => execute_motion(ed, Motion::LineMiddle, count),
3267 'v' => ed.reenter_last_visual(),
3270 'j' => execute_motion(ed, Motion::ScreenDown, count),
3274 'k' => execute_motion(ed, Motion::ScreenUp, count),
3275 'U' => {
3279 ed.vim.pending = Pending::Op {
3280 op: Operator::Uppercase,
3281 count1: count,
3282 };
3283 }
3284 'u' => {
3285 ed.vim.pending = Pending::Op {
3286 op: Operator::Lowercase,
3287 count1: count,
3288 };
3289 }
3290 '~' => {
3291 ed.vim.pending = Pending::Op {
3292 op: Operator::ToggleCase,
3293 count1: count,
3294 };
3295 }
3296 'q' => {
3297 ed.vim.pending = Pending::Op {
3300 op: Operator::Reflow,
3301 count1: count,
3302 };
3303 }
3304 'J' => {
3305 for _ in 0..count.max(1) {
3307 ed.push_undo();
3308 join_line_raw(ed);
3309 }
3310 if !ed.vim.replaying {
3311 ed.vim.last_change = Some(LastChange::JoinLine {
3312 count: count.max(1),
3313 });
3314 }
3315 }
3316 'd' => {
3317 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3322 }
3323 'i' => {
3328 if let Some((row, col)) = ed.vim.last_insert_pos {
3329 ed.jump_cursor(row, col);
3330 }
3331 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3332 }
3333 ';' => walk_change_list(ed, -1, count.max(1)),
3336 ',' => walk_change_list(ed, 1, count.max(1)),
3337 '*' => execute_motion(
3341 ed,
3342 Motion::WordAtCursor {
3343 forward: true,
3344 whole_word: false,
3345 },
3346 count,
3347 ),
3348 '#' => execute_motion(
3349 ed,
3350 Motion::WordAtCursor {
3351 forward: false,
3352 whole_word: false,
3353 },
3354 count,
3355 ),
3356 _ => {}
3357 }
3358}
3359
3360pub(crate) fn apply_after_z<H: crate::types::Host>(
3365 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3366 ch: char,
3367 count: usize,
3368) {
3369 use crate::editor::CursorScrollTarget;
3370 let row = ed.cursor().0;
3371 match ch {
3372 'z' => {
3373 ed.scroll_cursor_to(CursorScrollTarget::Center);
3374 ed.vim.viewport_pinned = true;
3375 }
3376 't' => {
3377 ed.scroll_cursor_to(CursorScrollTarget::Top);
3378 ed.vim.viewport_pinned = true;
3379 }
3380 'b' => {
3381 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3382 ed.vim.viewport_pinned = true;
3383 }
3384 'o' => {
3389 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3390 }
3391 'c' => {
3392 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3393 }
3394 'a' => {
3395 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3396 }
3397 'R' => {
3398 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3399 }
3400 'M' => {
3401 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3402 }
3403 'E' => {
3404 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3405 }
3406 'd' => {
3407 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3408 }
3409 'f' => {
3410 if matches!(
3411 ed.vim.mode,
3412 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3413 ) {
3414 let anchor_row = match ed.vim.mode {
3417 Mode::VisualLine => ed.vim.visual_line_anchor,
3418 Mode::VisualBlock => ed.vim.block_anchor.0,
3419 _ => ed.vim.visual_anchor.0,
3420 };
3421 let cur = ed.cursor().0;
3422 let top = anchor_row.min(cur);
3423 let bot = anchor_row.max(cur);
3424 ed.apply_fold_op(crate::types::FoldOp::Add {
3425 start_row: top,
3426 end_row: bot,
3427 closed: true,
3428 });
3429 ed.vim.mode = Mode::Normal;
3430 } else {
3431 ed.vim.pending = Pending::Op {
3436 op: Operator::Fold,
3437 count1: count,
3438 };
3439 }
3440 }
3441 _ => {}
3442 }
3443}
3444
3445pub(crate) fn apply_find_char<H: crate::types::Host>(
3451 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3452 ch: char,
3453 forward: bool,
3454 till: bool,
3455 count: usize,
3456) {
3457 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3458 ed.vim.last_find = Some((ch, forward, till));
3459}
3460
3461pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3467 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3468 op: Operator,
3469 ch: char,
3470 forward: bool,
3471 till: bool,
3472 total_count: usize,
3473) {
3474 let motion = Motion::Find { ch, forward, till };
3475 apply_op_with_motion(ed, op, &motion, total_count);
3476 ed.vim.last_find = Some((ch, forward, till));
3477 if !ed.vim.replaying && op_is_change(op) {
3478 ed.vim.last_change = Some(LastChange::OpMotion {
3479 op,
3480 motion,
3481 count: total_count,
3482 inserted: None,
3483 });
3484 }
3485}
3486
3487pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3496 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3497 op: Operator,
3498 ch: char,
3499 inner: bool,
3500 _total_count: usize,
3501) -> bool {
3502 let obj = match ch {
3505 'w' => TextObject::Word { big: false },
3506 'W' => TextObject::Word { big: true },
3507 '"' | '\'' | '`' => TextObject::Quote(ch),
3508 '(' | ')' | 'b' => TextObject::Bracket('('),
3509 '[' | ']' => TextObject::Bracket('['),
3510 '{' | '}' | 'B' => TextObject::Bracket('{'),
3511 '<' | '>' => TextObject::Bracket('<'),
3512 'p' => TextObject::Paragraph,
3513 't' => TextObject::XmlTag,
3514 's' => TextObject::Sentence,
3515 _ => return false,
3516 };
3517 apply_op_with_text_object(ed, op, obj, inner);
3518 if !ed.vim.replaying && op_is_change(op) {
3519 ed.vim.last_change = Some(LastChange::OpTextObj {
3520 op,
3521 obj,
3522 inner,
3523 inserted: None,
3524 });
3525 }
3526 true
3527}
3528
3529pub(crate) fn retreat_one<H: crate::types::Host>(
3531 ed: &Editor<hjkl_buffer::Buffer, H>,
3532 pos: (usize, usize),
3533) -> (usize, usize) {
3534 let (r, c) = pos;
3535 if c > 0 {
3536 (r, c - 1)
3537 } else if r > 0 {
3538 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3539 (r - 1, prev_len)
3540 } else {
3541 (0, 0)
3542 }
3543}
3544
3545fn begin_insert_noundo<H: crate::types::Host>(
3547 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3548 count: usize,
3549 reason: InsertReason,
3550) {
3551 let reason = if ed.vim.replaying {
3552 InsertReason::ReplayOnly
3553 } else {
3554 reason
3555 };
3556 let (row, _) = ed.cursor();
3557 ed.vim.insert_session = Some(InsertSession {
3558 count,
3559 row_min: row,
3560 row_max: row,
3561 before_lines: buf_lines_to_vec(&ed.buffer),
3562 reason,
3563 });
3564 ed.vim.mode = Mode::Insert;
3565 ed.vim.current_mode = crate::VimMode::Insert;
3567}
3568
3569pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
3572 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3573 op: Operator,
3574 motion: &Motion,
3575 count: usize,
3576) {
3577 let start = ed.cursor();
3578 apply_motion_cursor_ctx(ed, motion, count, true);
3583 let end = ed.cursor();
3584 let kind = motion_kind(motion);
3585 ed.jump_cursor(start.0, start.1);
3587 run_operator_over_range(ed, op, start, end, kind);
3588}
3589
3590fn apply_op_with_text_object<H: crate::types::Host>(
3591 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3592 op: Operator,
3593 obj: TextObject,
3594 inner: bool,
3595) {
3596 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3597 return;
3598 };
3599 ed.jump_cursor(start.0, start.1);
3600 run_operator_over_range(ed, op, start, end, kind);
3601}
3602
3603fn motion_kind(motion: &Motion) -> RangeKind {
3604 match motion {
3605 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
3606 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
3607 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3608 RangeKind::Linewise
3609 }
3610 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3611 RangeKind::Inclusive
3612 }
3613 Motion::Find { .. } => RangeKind::Inclusive,
3614 Motion::MatchBracket => RangeKind::Inclusive,
3615 Motion::LineEnd => RangeKind::Inclusive,
3617 Motion::FirstNonBlankNextLine
3619 | Motion::FirstNonBlankPrevLine
3620 | Motion::FirstNonBlankLine => RangeKind::Linewise,
3621 Motion::SectionBackward
3623 | Motion::SectionForward
3624 | Motion::SectionEndBackward
3625 | Motion::SectionEndForward => RangeKind::Exclusive,
3626 _ => RangeKind::Exclusive,
3627 }
3628}
3629
3630fn run_operator_over_range<H: crate::types::Host>(
3631 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3632 op: Operator,
3633 start: (usize, usize),
3634 end: (usize, usize),
3635 kind: RangeKind,
3636) {
3637 let (top, bot) = order(start, end);
3638 if top == bot && !matches!(kind, RangeKind::Linewise) {
3642 return;
3643 }
3644
3645 match op {
3646 Operator::Yank => {
3647 let text = read_vim_range(ed, top, bot, kind);
3648 if !text.is_empty() {
3649 ed.record_yank_to_host(text.clone());
3650 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
3651 }
3652 let rbr = match kind {
3656 RangeKind::Linewise => {
3657 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3658 (bot.0, last_col)
3659 }
3660 RangeKind::Inclusive => (bot.0, bot.1),
3661 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3662 };
3663 ed.set_mark('[', top);
3664 ed.set_mark(']', rbr);
3665 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3666 ed.push_buffer_cursor_to_textarea();
3667 }
3668 Operator::Delete => {
3669 ed.push_undo();
3670 cut_vim_range(ed, top, bot, kind);
3671 if !matches!(kind, RangeKind::Linewise) {
3676 clamp_cursor_to_normal_mode(ed);
3677 }
3678 ed.vim.mode = Mode::Normal;
3679 let pos = ed.cursor();
3683 ed.set_mark('[', pos);
3684 ed.set_mark(']', pos);
3685 }
3686 Operator::Change => {
3687 ed.vim.change_mark_start = Some(top);
3692 ed.push_undo();
3693 cut_vim_range(ed, top, bot, kind);
3694 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3695 }
3696 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3697 apply_case_op_to_selection(ed, op, top, bot, kind);
3698 }
3699 Operator::Indent | Operator::Outdent => {
3700 ed.push_undo();
3703 if op == Operator::Indent {
3704 indent_rows(ed, top.0, bot.0, 1);
3705 } else {
3706 outdent_rows(ed, top.0, bot.0, 1);
3707 }
3708 ed.vim.mode = Mode::Normal;
3709 }
3710 Operator::Fold => {
3711 if bot.0 >= top.0 {
3715 ed.apply_fold_op(crate::types::FoldOp::Add {
3716 start_row: top.0,
3717 end_row: bot.0,
3718 closed: true,
3719 });
3720 }
3721 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3722 ed.push_buffer_cursor_to_textarea();
3723 ed.vim.mode = Mode::Normal;
3724 }
3725 Operator::Reflow => {
3726 ed.push_undo();
3727 reflow_rows(ed, top.0, bot.0);
3728 ed.vim.mode = Mode::Normal;
3729 }
3730 Operator::AutoIndent => {
3731 ed.push_undo();
3733 auto_indent_rows(ed, top.0, bot.0);
3734 ed.vim.mode = Mode::Normal;
3735 }
3736 Operator::Filter => {
3737 }
3742 }
3743}
3744
3745pub(crate) fn delete_range_bridge<H: crate::types::Host>(
3762 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3763 start: (usize, usize),
3764 end: (usize, usize),
3765 kind: RangeKind,
3766 register: char,
3767) {
3768 ed.vim.pending_register = Some(register);
3769 run_operator_over_range(ed, Operator::Delete, start, end, kind);
3770}
3771
3772pub(crate) fn yank_range_bridge<H: crate::types::Host>(
3775 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3776 start: (usize, usize),
3777 end: (usize, usize),
3778 kind: RangeKind,
3779 register: char,
3780) {
3781 ed.vim.pending_register = Some(register);
3782 run_operator_over_range(ed, Operator::Yank, start, end, kind);
3783}
3784
3785pub(crate) fn change_range_bridge<H: crate::types::Host>(
3790 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3791 start: (usize, usize),
3792 end: (usize, usize),
3793 kind: RangeKind,
3794 register: char,
3795) {
3796 ed.vim.pending_register = Some(register);
3797 run_operator_over_range(ed, Operator::Change, start, end, kind);
3798}
3799
3800pub(crate) fn indent_range_bridge<H: crate::types::Host>(
3805 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3806 start: (usize, usize),
3807 end: (usize, usize),
3808 count: i32,
3809 shiftwidth: u32,
3810) {
3811 if count == 0 {
3812 return;
3813 }
3814 let (top_row, bot_row) = if start.0 <= end.0 {
3815 (start.0, end.0)
3816 } else {
3817 (end.0, start.0)
3818 };
3819 let original_sw = ed.settings().shiftwidth;
3821 if shiftwidth > 0 {
3822 ed.settings_mut().shiftwidth = shiftwidth as usize;
3823 }
3824 ed.push_undo();
3825 let abs_count = count.unsigned_abs() as usize;
3826 if count > 0 {
3827 indent_rows(ed, top_row, bot_row, abs_count);
3828 } else {
3829 outdent_rows(ed, top_row, bot_row, abs_count);
3830 }
3831 if shiftwidth > 0 {
3832 ed.settings_mut().shiftwidth = original_sw;
3833 }
3834 ed.vim.mode = Mode::Normal;
3835}
3836
3837pub(crate) fn case_range_bridge<H: crate::types::Host>(
3841 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3842 start: (usize, usize),
3843 end: (usize, usize),
3844 kind: RangeKind,
3845 op: Operator,
3846) {
3847 match op {
3848 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
3849 _ => return,
3850 }
3851 let (top, bot) = order(start, end);
3852 apply_case_op_to_selection(ed, op, top, bot, kind);
3853}
3854
3855pub(crate) fn delete_block_bridge<H: crate::types::Host>(
3876 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3877 top_row: usize,
3878 bot_row: usize,
3879 left_col: usize,
3880 right_col: usize,
3881 register: char,
3882) {
3883 ed.vim.pending_register = Some(register);
3884 let saved_anchor = ed.vim.block_anchor;
3885 let saved_vcol = ed.vim.block_vcol;
3886 ed.vim.block_anchor = (top_row, left_col);
3887 ed.vim.block_vcol = right_col;
3888 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3890 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3892 apply_block_operator(ed, Operator::Delete);
3893 ed.vim.block_anchor = saved_anchor;
3897 ed.vim.block_vcol = saved_vcol;
3898}
3899
3900pub(crate) fn yank_block_bridge<H: crate::types::Host>(
3902 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3903 top_row: usize,
3904 bot_row: usize,
3905 left_col: usize,
3906 right_col: usize,
3907 register: char,
3908) {
3909 ed.vim.pending_register = Some(register);
3910 let saved_anchor = ed.vim.block_anchor;
3911 let saved_vcol = ed.vim.block_vcol;
3912 ed.vim.block_anchor = (top_row, left_col);
3913 ed.vim.block_vcol = right_col;
3914 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3915 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3916 apply_block_operator(ed, Operator::Yank);
3917 ed.vim.block_anchor = saved_anchor;
3918 ed.vim.block_vcol = saved_vcol;
3919}
3920
3921pub(crate) fn change_block_bridge<H: crate::types::Host>(
3924 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3925 top_row: usize,
3926 bot_row: usize,
3927 left_col: usize,
3928 right_col: usize,
3929 register: char,
3930) {
3931 ed.vim.pending_register = Some(register);
3932 let saved_anchor = ed.vim.block_anchor;
3933 let saved_vcol = ed.vim.block_vcol;
3934 ed.vim.block_anchor = (top_row, left_col);
3935 ed.vim.block_vcol = right_col;
3936 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3937 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3938 apply_block_operator(ed, Operator::Change);
3939 ed.vim.block_anchor = saved_anchor;
3940 ed.vim.block_vcol = saved_vcol;
3941}
3942
3943pub(crate) fn indent_block_bridge<H: crate::types::Host>(
3947 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3948 top_row: usize,
3949 bot_row: usize,
3950 count: i32,
3951) {
3952 if count == 0 {
3953 return;
3954 }
3955 ed.push_undo();
3956 let abs = count.unsigned_abs() as usize;
3957 if count > 0 {
3958 indent_rows(ed, top_row, bot_row, abs);
3959 } else {
3960 outdent_rows(ed, top_row, bot_row, abs);
3961 }
3962 ed.vim.mode = Mode::Normal;
3963}
3964
3965pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
3969 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3970 start: (usize, usize),
3971 end: (usize, usize),
3972) {
3973 let (top_row, bot_row) = if start.0 <= end.0 {
3974 (start.0, end.0)
3975 } else {
3976 (end.0, start.0)
3977 };
3978 ed.push_undo();
3979 auto_indent_rows(ed, top_row, bot_row);
3980 ed.vim.mode = Mode::Normal;
3981}
3982
3983pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
3994 ed: &Editor<hjkl_buffer::Buffer, H>,
3995) -> Option<((usize, usize), (usize, usize))> {
3996 word_text_object(ed, true, false)
3997}
3998
3999pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
4002 ed: &Editor<hjkl_buffer::Buffer, H>,
4003) -> Option<((usize, usize), (usize, usize))> {
4004 word_text_object(ed, false, false)
4005}
4006
4007pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
4010 ed: &Editor<hjkl_buffer::Buffer, H>,
4011) -> Option<((usize, usize), (usize, usize))> {
4012 word_text_object(ed, true, true)
4013}
4014
4015pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
4018 ed: &Editor<hjkl_buffer::Buffer, H>,
4019) -> Option<((usize, usize), (usize, usize))> {
4020 word_text_object(ed, false, true)
4021}
4022
4023pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
4039 ed: &Editor<hjkl_buffer::Buffer, H>,
4040 quote: char,
4041) -> Option<((usize, usize), (usize, usize))> {
4042 quote_text_object(ed, quote, true)
4043}
4044
4045pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
4048 ed: &Editor<hjkl_buffer::Buffer, H>,
4049 quote: char,
4050) -> Option<((usize, usize), (usize, usize))> {
4051 quote_text_object(ed, quote, false)
4052}
4053
4054pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
4062 ed: &Editor<hjkl_buffer::Buffer, H>,
4063 open: char,
4064) -> Option<((usize, usize), (usize, usize))> {
4065 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
4066}
4067
4068pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
4072 ed: &Editor<hjkl_buffer::Buffer, H>,
4073 open: char,
4074) -> Option<((usize, usize), (usize, usize))> {
4075 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
4076}
4077
4078pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
4083 ed: &Editor<hjkl_buffer::Buffer, H>,
4084) -> Option<((usize, usize), (usize, usize))> {
4085 sentence_text_object(ed, true)
4086}
4087
4088pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
4091 ed: &Editor<hjkl_buffer::Buffer, H>,
4092) -> Option<((usize, usize), (usize, usize))> {
4093 sentence_text_object(ed, false)
4094}
4095
4096pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
4101 ed: &Editor<hjkl_buffer::Buffer, H>,
4102) -> Option<((usize, usize), (usize, usize))> {
4103 paragraph_text_object(ed, true)
4104}
4105
4106pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
4109 ed: &Editor<hjkl_buffer::Buffer, H>,
4110) -> Option<((usize, usize), (usize, usize))> {
4111 paragraph_text_object(ed, false)
4112}
4113
4114pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
4120 ed: &Editor<hjkl_buffer::Buffer, H>,
4121) -> Option<((usize, usize), (usize, usize))> {
4122 tag_text_object(ed, true)
4123}
4124
4125pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
4128 ed: &Editor<hjkl_buffer::Buffer, H>,
4129) -> Option<((usize, usize), (usize, usize))> {
4130 tag_text_object(ed, false)
4131}
4132
4133fn reflow_rows<H: crate::types::Host>(
4138 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4139 top: usize,
4140 bot: usize,
4141) {
4142 let width = ed.settings().textwidth.max(1);
4143 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4144 let bot = bot.min(lines.len().saturating_sub(1));
4145 if top > bot {
4146 return;
4147 }
4148 let original = lines[top..=bot].to_vec();
4149 let mut wrapped: Vec<String> = Vec::new();
4150 let mut paragraph: Vec<String> = Vec::new();
4151 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4152 if para.is_empty() {
4153 return;
4154 }
4155 let words = para.join(" ");
4156 let mut current = String::new();
4157 for word in words.split_whitespace() {
4158 let extra = if current.is_empty() {
4159 word.chars().count()
4160 } else {
4161 current.chars().count() + 1 + word.chars().count()
4162 };
4163 if extra > width && !current.is_empty() {
4164 out.push(std::mem::take(&mut current));
4165 current.push_str(word);
4166 } else if current.is_empty() {
4167 current.push_str(word);
4168 } else {
4169 current.push(' ');
4170 current.push_str(word);
4171 }
4172 }
4173 if !current.is_empty() {
4174 out.push(current);
4175 }
4176 para.clear();
4177 };
4178 for line in &original {
4179 if line.trim().is_empty() {
4180 flush(&mut paragraph, &mut wrapped, width);
4181 wrapped.push(String::new());
4182 } else {
4183 paragraph.push(line.clone());
4184 }
4185 }
4186 flush(&mut paragraph, &mut wrapped, width);
4187
4188 let after: Vec<String> = lines.split_off(bot + 1);
4190 lines.truncate(top);
4191 lines.extend(wrapped);
4192 lines.extend(after);
4193 ed.restore(lines, (top, 0));
4194 ed.mark_content_dirty();
4195}
4196
4197fn apply_case_op_to_selection<H: crate::types::Host>(
4203 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4204 op: Operator,
4205 top: (usize, usize),
4206 bot: (usize, usize),
4207 kind: RangeKind,
4208) {
4209 use hjkl_buffer::Edit;
4210 ed.push_undo();
4211 let saved_yank = ed.yank().to_string();
4212 let saved_yank_linewise = ed.vim.yank_linewise;
4213 let selection = cut_vim_range(ed, top, bot, kind);
4214 let transformed = match op {
4215 Operator::Uppercase => selection.to_uppercase(),
4216 Operator::Lowercase => selection.to_lowercase(),
4217 Operator::ToggleCase => toggle_case_str(&selection),
4218 _ => unreachable!(),
4219 };
4220 if !transformed.is_empty() {
4221 let cursor = buf_cursor_pos(&ed.buffer);
4222 ed.mutate_edit(Edit::InsertStr {
4223 at: cursor,
4224 text: transformed,
4225 });
4226 }
4227 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4228 ed.push_buffer_cursor_to_textarea();
4229 ed.set_yank(saved_yank);
4230 ed.vim.yank_linewise = saved_yank_linewise;
4231 ed.vim.mode = Mode::Normal;
4232}
4233
4234fn indent_rows<H: crate::types::Host>(
4239 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4240 top: usize,
4241 bot: usize,
4242 count: usize,
4243) {
4244 ed.sync_buffer_content_from_textarea();
4245 let width = ed.settings().shiftwidth * count.max(1);
4246 let pad: String = " ".repeat(width);
4247 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4248 let bot = bot.min(lines.len().saturating_sub(1));
4249 for line in lines.iter_mut().take(bot + 1).skip(top) {
4250 if !line.is_empty() {
4251 line.insert_str(0, &pad);
4252 }
4253 }
4254 ed.restore(lines, (top, 0));
4257 move_first_non_whitespace(ed);
4258}
4259
4260fn outdent_rows<H: crate::types::Host>(
4264 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4265 top: usize,
4266 bot: usize,
4267 count: usize,
4268) {
4269 ed.sync_buffer_content_from_textarea();
4270 let width = ed.settings().shiftwidth * count.max(1);
4271 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4272 let bot = bot.min(lines.len().saturating_sub(1));
4273 for line in lines.iter_mut().take(bot + 1).skip(top) {
4274 let strip: usize = line
4275 .chars()
4276 .take(width)
4277 .take_while(|c| *c == ' ' || *c == '\t')
4278 .count();
4279 if strip > 0 {
4280 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4281 line.drain(..byte_len);
4282 }
4283 }
4284 ed.restore(lines, (top, 0));
4285 move_first_non_whitespace(ed);
4286}
4287
4288fn bracket_net(line: &str) -> i32 {
4315 let mut net: i32 = 0;
4316 let mut chars = line.chars().peekable();
4317 while let Some(ch) = chars.next() {
4318 match ch {
4319 '/' if chars.peek() == Some(&'/') => return net,
4321 '"' => {
4322 while let Some(c) = chars.next() {
4324 match c {
4325 '\\' => {
4326 chars.next();
4327 } '"' => break,
4329 _ => {}
4330 }
4331 }
4332 }
4333 '\'' => {
4334 let saved: Vec<char> = chars.clone().take(5).collect();
4343 let close_idx = if saved.first() == Some(&'\\') {
4344 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
4345 } else {
4346 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
4347 };
4348 if let Some(idx) = close_idx {
4349 for _ in 0..=idx {
4350 chars.next();
4351 }
4352 }
4353 }
4355 '{' | '(' | '[' => net += 1,
4356 '}' | ')' | ']' => net -= 1,
4357 _ => {}
4358 }
4359 }
4360 net
4361}
4362
4363fn auto_indent_rows<H: crate::types::Host>(
4385 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4386 top: usize,
4387 bot: usize,
4388) {
4389 ed.sync_buffer_content_from_textarea();
4390 let shiftwidth = ed.settings().shiftwidth;
4391 let expandtab = ed.settings().expandtab;
4392 let indent_unit: String = if expandtab {
4393 " ".repeat(shiftwidth)
4394 } else {
4395 "\t".to_string()
4396 };
4397
4398 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4399 let bot = bot.min(lines.len().saturating_sub(1));
4400
4401 let mut depth: i32 = 0;
4404 for line in lines.iter().take(top) {
4405 depth += bracket_net(line);
4406 if depth < 0 {
4407 depth = 0;
4408 }
4409 }
4410
4411 for line in lines.iter_mut().take(bot + 1).skip(top) {
4412 let trimmed_owned = line.trim_start().to_owned();
4413 if trimmed_owned.is_empty() {
4415 *line = String::new();
4416 continue;
4418 }
4419
4420 let starts_with_close = trimmed_owned
4422 .chars()
4423 .next()
4424 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
4425 let starts_with_dot = trimmed_owned.starts_with('.')
4435 && !trimmed_owned.starts_with("..")
4436 && !trimmed_owned.starts_with(".;");
4437 let effective_depth = if starts_with_close {
4438 depth.saturating_sub(1)
4439 } else if starts_with_dot {
4440 depth.saturating_add(1)
4441 } else {
4442 depth
4443 } as usize;
4444
4445 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
4447
4448 depth += bracket_net(&trimmed_owned);
4450 if depth < 0 {
4451 depth = 0;
4452 }
4453
4454 *line = new_line;
4455 }
4456
4457 ed.restore(lines, (top, 0));
4459 move_first_non_whitespace(ed);
4460 ed.last_indent_range = Some((top, bot));
4462}
4463
4464fn toggle_case_str(s: &str) -> String {
4465 s.chars()
4466 .map(|c| {
4467 if c.is_lowercase() {
4468 c.to_uppercase().next().unwrap_or(c)
4469 } else if c.is_uppercase() {
4470 c.to_lowercase().next().unwrap_or(c)
4471 } else {
4472 c
4473 }
4474 })
4475 .collect()
4476}
4477
4478fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4479 if a <= b { (a, b) } else { (b, a) }
4480}
4481
4482fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4487 let (row, col) = ed.cursor();
4488 let line_chars = buf_line_chars(&ed.buffer, row);
4489 let max_col = line_chars.saturating_sub(1);
4490 if col > max_col {
4491 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4492 ed.push_buffer_cursor_to_textarea();
4493 }
4494}
4495
4496fn execute_line_op<H: crate::types::Host>(
4499 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4500 op: Operator,
4501 count: usize,
4502) {
4503 let (row, col) = ed.cursor();
4504 let total = buf_row_count(&ed.buffer);
4505 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4506
4507 match op {
4508 Operator::Yank => {
4509 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4511 if !text.is_empty() {
4512 ed.record_yank_to_host(text.clone());
4513 ed.record_yank(text, true);
4514 }
4515 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4518 ed.set_mark('[', (row, 0));
4519 ed.set_mark(']', (end_row, last_col));
4520 buf_set_cursor_rc(&mut ed.buffer, row, col);
4521 ed.push_buffer_cursor_to_textarea();
4522 ed.vim.mode = Mode::Normal;
4523 }
4524 Operator::Delete => {
4525 ed.push_undo();
4526 let deleted_through_last = end_row + 1 >= total;
4527 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4528 let total_after = buf_row_count(&ed.buffer);
4532 let raw_target = if deleted_through_last {
4533 row.saturating_sub(1).min(total_after.saturating_sub(1))
4534 } else {
4535 row.min(total_after.saturating_sub(1))
4536 };
4537 let target_row = if raw_target > 0
4543 && raw_target + 1 == total_after
4544 && buf_line(&ed.buffer, raw_target)
4545 .map(|s| s.is_empty())
4546 .unwrap_or(false)
4547 {
4548 raw_target - 1
4549 } else {
4550 raw_target
4551 };
4552 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4553 ed.push_buffer_cursor_to_textarea();
4554 move_first_non_whitespace(ed);
4555 ed.sticky_col = Some(ed.cursor().1);
4556 ed.vim.mode = Mode::Normal;
4557 let pos = ed.cursor();
4560 ed.set_mark('[', pos);
4561 ed.set_mark(']', pos);
4562 }
4563 Operator::Change => {
4564 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4568 ed.vim.change_mark_start = Some((row, 0));
4570 ed.push_undo();
4571 ed.sync_buffer_content_from_textarea();
4572 let payload = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4574 if end_row > row {
4575 ed.mutate_edit(Edit::DeleteRange {
4576 start: Position::new(row + 1, 0),
4577 end: Position::new(end_row, 0),
4578 kind: BufKind::Line,
4579 });
4580 }
4581 let line_chars = buf_line_chars(&ed.buffer, row);
4582 if line_chars > 0 {
4583 ed.mutate_edit(Edit::DeleteRange {
4584 start: Position::new(row, 0),
4585 end: Position::new(row, line_chars),
4586 kind: BufKind::Char,
4587 });
4588 }
4589 if !payload.is_empty() {
4590 ed.record_yank_to_host(payload.clone());
4591 ed.record_delete(payload, true);
4592 }
4593 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4594 ed.push_buffer_cursor_to_textarea();
4595 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4596 }
4597 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4598 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
4602 move_first_non_whitespace(ed);
4605 }
4606 Operator::Indent | Operator::Outdent => {
4607 ed.push_undo();
4609 if op == Operator::Indent {
4610 indent_rows(ed, row, end_row, 1);
4611 } else {
4612 outdent_rows(ed, row, end_row, 1);
4613 }
4614 ed.sticky_col = Some(ed.cursor().1);
4615 ed.vim.mode = Mode::Normal;
4616 }
4617 Operator::Fold => unreachable!("Fold has no line-op double"),
4619 Operator::Reflow => {
4620 ed.push_undo();
4622 reflow_rows(ed, row, end_row);
4623 move_first_non_whitespace(ed);
4624 ed.sticky_col = Some(ed.cursor().1);
4625 ed.vim.mode = Mode::Normal;
4626 }
4627 Operator::AutoIndent => {
4628 ed.push_undo();
4630 auto_indent_rows(ed, row, end_row);
4631 ed.sticky_col = Some(ed.cursor().1);
4632 ed.vim.mode = Mode::Normal;
4633 }
4634 Operator::Filter => {
4635 }
4637 }
4638}
4639
4640pub(crate) fn apply_visual_operator<H: crate::types::Host>(
4643 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4644 op: Operator,
4645) {
4646 match ed.vim.mode {
4647 Mode::VisualLine => {
4648 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4649 let top = cursor_row.min(ed.vim.visual_line_anchor);
4650 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4651 ed.vim.yank_linewise = true;
4652 match op {
4653 Operator::Yank => {
4654 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4655 if !text.is_empty() {
4656 ed.record_yank_to_host(text.clone());
4657 ed.record_yank(text, true);
4658 }
4659 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4660 ed.push_buffer_cursor_to_textarea();
4661 ed.vim.mode = Mode::Normal;
4662 }
4663 Operator::Delete => {
4664 ed.push_undo();
4665 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4666 ed.vim.mode = Mode::Normal;
4667 }
4668 Operator::Change => {
4669 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4672 ed.push_undo();
4673 ed.sync_buffer_content_from_textarea();
4674 let payload = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4675 if bot > top {
4676 ed.mutate_edit(Edit::DeleteRange {
4677 start: Position::new(top + 1, 0),
4678 end: Position::new(bot, 0),
4679 kind: BufKind::Line,
4680 });
4681 }
4682 let line_chars = buf_line_chars(&ed.buffer, top);
4683 if line_chars > 0 {
4684 ed.mutate_edit(Edit::DeleteRange {
4685 start: Position::new(top, 0),
4686 end: Position::new(top, line_chars),
4687 kind: BufKind::Char,
4688 });
4689 }
4690 if !payload.is_empty() {
4691 ed.record_yank_to_host(payload.clone());
4692 ed.record_delete(payload, true);
4693 }
4694 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4695 ed.push_buffer_cursor_to_textarea();
4696 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4697 }
4698 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4699 let bot = buf_cursor_pos(&ed.buffer)
4700 .row
4701 .max(ed.vim.visual_line_anchor);
4702 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
4703 move_first_non_whitespace(ed);
4704 }
4705 Operator::Indent | Operator::Outdent => {
4706 ed.push_undo();
4707 let (cursor_row, _) = ed.cursor();
4708 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4709 if op == Operator::Indent {
4710 indent_rows(ed, top, bot, 1);
4711 } else {
4712 outdent_rows(ed, top, bot, 1);
4713 }
4714 ed.vim.mode = Mode::Normal;
4715 }
4716 Operator::Reflow => {
4717 ed.push_undo();
4718 let (cursor_row, _) = ed.cursor();
4719 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4720 reflow_rows(ed, top, bot);
4721 ed.vim.mode = Mode::Normal;
4722 }
4723 Operator::AutoIndent => {
4724 ed.push_undo();
4725 let (cursor_row, _) = ed.cursor();
4726 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4727 auto_indent_rows(ed, top, bot);
4728 ed.vim.mode = Mode::Normal;
4729 }
4730 Operator::Filter => {}
4732 Operator::Fold => unreachable!("Visual zf takes its own path"),
4735 }
4736 }
4737 Mode::Visual => {
4738 ed.vim.yank_linewise = false;
4739 let anchor = ed.vim.visual_anchor;
4740 let cursor = ed.cursor();
4741 let (top, bot) = order(anchor, cursor);
4742 match op {
4743 Operator::Yank => {
4744 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
4745 if !text.is_empty() {
4746 ed.record_yank_to_host(text.clone());
4747 ed.record_yank(text, false);
4748 }
4749 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4750 ed.push_buffer_cursor_to_textarea();
4751 ed.vim.mode = Mode::Normal;
4752 }
4753 Operator::Delete => {
4754 ed.push_undo();
4755 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4756 ed.vim.mode = Mode::Normal;
4757 }
4758 Operator::Change => {
4759 ed.push_undo();
4760 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4761 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4762 }
4763 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4764 let anchor = ed.vim.visual_anchor;
4766 let cursor = ed.cursor();
4767 let (top, bot) = order(anchor, cursor);
4768 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
4769 }
4770 Operator::Indent | Operator::Outdent => {
4771 ed.push_undo();
4772 let anchor = ed.vim.visual_anchor;
4773 let cursor = ed.cursor();
4774 let (top, bot) = order(anchor, cursor);
4775 if op == Operator::Indent {
4776 indent_rows(ed, top.0, bot.0, 1);
4777 } else {
4778 outdent_rows(ed, top.0, bot.0, 1);
4779 }
4780 ed.vim.mode = Mode::Normal;
4781 }
4782 Operator::Reflow => {
4783 ed.push_undo();
4784 let anchor = ed.vim.visual_anchor;
4785 let cursor = ed.cursor();
4786 let (top, bot) = order(anchor, cursor);
4787 reflow_rows(ed, top.0, bot.0);
4788 ed.vim.mode = Mode::Normal;
4789 }
4790 Operator::AutoIndent => {
4791 ed.push_undo();
4792 let anchor = ed.vim.visual_anchor;
4793 let cursor = ed.cursor();
4794 let (top, bot) = order(anchor, cursor);
4795 auto_indent_rows(ed, top.0, bot.0);
4796 ed.vim.mode = Mode::Normal;
4797 }
4798 Operator::Filter => {}
4800 Operator::Fold => unreachable!("Visual zf takes its own path"),
4801 }
4802 }
4803 Mode::VisualBlock => apply_block_operator(ed, op),
4804 _ => {}
4805 }
4806}
4807
4808fn block_bounds<H: crate::types::Host>(
4813 ed: &Editor<hjkl_buffer::Buffer, H>,
4814) -> (usize, usize, usize, usize) {
4815 let (ar, ac) = ed.vim.block_anchor;
4816 let (cr, _) = ed.cursor();
4817 let cc = ed.vim.block_vcol;
4818 let top = ar.min(cr);
4819 let bot = ar.max(cr);
4820 let left = ac.min(cc);
4821 let right = ac.max(cc);
4822 (top, bot, left, right)
4823}
4824
4825pub(crate) fn update_block_vcol<H: crate::types::Host>(
4830 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4831 motion: &Motion,
4832) {
4833 match motion {
4834 Motion::Left
4835 | Motion::Right
4836 | Motion::WordFwd
4837 | Motion::BigWordFwd
4838 | Motion::WordBack
4839 | Motion::BigWordBack
4840 | Motion::WordEnd
4841 | Motion::BigWordEnd
4842 | Motion::WordEndBack
4843 | Motion::BigWordEndBack
4844 | Motion::LineStart
4845 | Motion::FirstNonBlank
4846 | Motion::LineEnd
4847 | Motion::Find { .. }
4848 | Motion::FindRepeat { .. }
4849 | Motion::MatchBracket => {
4850 ed.vim.block_vcol = ed.cursor().1;
4851 }
4852 _ => {}
4854 }
4855}
4856
4857fn apply_block_operator<H: crate::types::Host>(
4862 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4863 op: Operator,
4864) {
4865 let (top, bot, left, right) = block_bounds(ed);
4866 let yank = block_yank(ed, top, bot, left, right);
4868
4869 match op {
4870 Operator::Yank => {
4871 if !yank.is_empty() {
4872 ed.record_yank_to_host(yank.clone());
4873 ed.record_yank(yank, false);
4874 }
4875 ed.vim.mode = Mode::Normal;
4876 ed.jump_cursor(top, left);
4877 }
4878 Operator::Delete => {
4879 ed.push_undo();
4880 delete_block_contents(ed, top, bot, left, right);
4881 if !yank.is_empty() {
4882 ed.record_yank_to_host(yank.clone());
4883 ed.record_delete(yank, false);
4884 }
4885 ed.vim.mode = Mode::Normal;
4886 ed.jump_cursor(top, left);
4887 }
4888 Operator::Change => {
4889 ed.push_undo();
4890 delete_block_contents(ed, top, bot, left, right);
4891 if !yank.is_empty() {
4892 ed.record_yank_to_host(yank.clone());
4893 ed.record_delete(yank, false);
4894 }
4895 ed.jump_cursor(top, left);
4896 begin_insert_noundo(
4897 ed,
4898 1,
4899 InsertReason::BlockChange {
4900 top,
4901 bot,
4902 col: left,
4903 },
4904 );
4905 }
4906 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4907 ed.push_undo();
4908 transform_block_case(ed, op, top, bot, left, right);
4909 ed.vim.mode = Mode::Normal;
4910 ed.jump_cursor(top, left);
4911 }
4912 Operator::Indent | Operator::Outdent => {
4913 ed.push_undo();
4917 if op == Operator::Indent {
4918 indent_rows(ed, top, bot, 1);
4919 } else {
4920 outdent_rows(ed, top, bot, 1);
4921 }
4922 ed.vim.mode = Mode::Normal;
4923 }
4924 Operator::Fold => unreachable!("Visual zf takes its own path"),
4925 Operator::Reflow => {
4926 ed.push_undo();
4930 reflow_rows(ed, top, bot);
4931 ed.vim.mode = Mode::Normal;
4932 }
4933 Operator::AutoIndent => {
4934 ed.push_undo();
4937 auto_indent_rows(ed, top, bot);
4938 ed.vim.mode = Mode::Normal;
4939 }
4940 Operator::Filter => {}
4942 }
4943}
4944
4945fn transform_block_case<H: crate::types::Host>(
4949 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4950 op: Operator,
4951 top: usize,
4952 bot: usize,
4953 left: usize,
4954 right: usize,
4955) {
4956 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4957 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4958 let chars: Vec<char> = lines[r].chars().collect();
4959 if left >= chars.len() {
4960 continue;
4961 }
4962 let end = (right + 1).min(chars.len());
4963 let head: String = chars[..left].iter().collect();
4964 let mid: String = chars[left..end].iter().collect();
4965 let tail: String = chars[end..].iter().collect();
4966 let transformed = match op {
4967 Operator::Uppercase => mid.to_uppercase(),
4968 Operator::Lowercase => mid.to_lowercase(),
4969 Operator::ToggleCase => toggle_case_str(&mid),
4970 _ => mid,
4971 };
4972 lines[r] = format!("{head}{transformed}{tail}");
4973 }
4974 let saved_yank = ed.yank().to_string();
4975 let saved_linewise = ed.vim.yank_linewise;
4976 ed.restore(lines, (top, left));
4977 ed.set_yank(saved_yank);
4978 ed.vim.yank_linewise = saved_linewise;
4979}
4980
4981fn block_yank<H: crate::types::Host>(
4982 ed: &Editor<hjkl_buffer::Buffer, H>,
4983 top: usize,
4984 bot: usize,
4985 left: usize,
4986 right: usize,
4987) -> String {
4988 let lines = buf_lines_to_vec(&ed.buffer);
4989 let mut rows: Vec<String> = Vec::new();
4990 for r in top..=bot {
4991 let line = match lines.get(r) {
4992 Some(l) => l,
4993 None => break,
4994 };
4995 let chars: Vec<char> = line.chars().collect();
4996 let end = (right + 1).min(chars.len());
4997 if left >= chars.len() {
4998 rows.push(String::new());
4999 } else {
5000 rows.push(chars[left..end].iter().collect());
5001 }
5002 }
5003 rows.join("\n")
5004}
5005
5006fn delete_block_contents<H: crate::types::Host>(
5007 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5008 top: usize,
5009 bot: usize,
5010 left: usize,
5011 right: usize,
5012) {
5013 use hjkl_buffer::{Edit, MotionKind, Position};
5014 ed.sync_buffer_content_from_textarea();
5015 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
5016 if last_row < top {
5017 return;
5018 }
5019 ed.mutate_edit(Edit::DeleteRange {
5020 start: Position::new(top, left),
5021 end: Position::new(last_row, right),
5022 kind: MotionKind::Block,
5023 });
5024 ed.push_buffer_cursor_to_textarea();
5025}
5026
5027pub(crate) fn block_replace<H: crate::types::Host>(
5029 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5030 ch: char,
5031) {
5032 let (top, bot, left, right) = block_bounds(ed);
5033 ed.push_undo();
5034 ed.sync_buffer_content_from_textarea();
5035 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
5036 for r in top..=bot.min(lines.len().saturating_sub(1)) {
5037 let chars: Vec<char> = lines[r].chars().collect();
5038 if left >= chars.len() {
5039 continue;
5040 }
5041 let end = (right + 1).min(chars.len());
5042 let before: String = chars[..left].iter().collect();
5043 let middle: String = std::iter::repeat_n(ch, end - left).collect();
5044 let after: String = chars[end..].iter().collect();
5045 lines[r] = format!("{before}{middle}{after}");
5046 }
5047 reset_textarea_lines(ed, lines);
5048 ed.vim.mode = Mode::Normal;
5049 ed.jump_cursor(top, left);
5050}
5051
5052fn reset_textarea_lines<H: crate::types::Host>(
5056 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5057 lines: Vec<String>,
5058) {
5059 let cursor = ed.cursor();
5060 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
5061 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
5062 ed.mark_content_dirty();
5063}
5064
5065type Pos = (usize, usize);
5071
5072pub(crate) fn text_object_range<H: crate::types::Host>(
5076 ed: &Editor<hjkl_buffer::Buffer, H>,
5077 obj: TextObject,
5078 inner: bool,
5079) -> Option<(Pos, Pos, RangeKind)> {
5080 match obj {
5081 TextObject::Word { big } => {
5082 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
5083 }
5084 TextObject::Quote(q) => {
5085 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
5086 }
5087 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
5088 TextObject::Paragraph => {
5089 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
5090 }
5091 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
5092 TextObject::Sentence => {
5093 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
5094 }
5095 }
5096}
5097
5098fn sentence_boundary<H: crate::types::Host>(
5102 ed: &Editor<hjkl_buffer::Buffer, H>,
5103 forward: bool,
5104) -> Option<(usize, usize)> {
5105 let lines = buf_lines_to_vec(&ed.buffer);
5106 if lines.is_empty() {
5107 return None;
5108 }
5109 let pos_to_idx = |pos: (usize, usize)| -> usize {
5110 let mut idx = 0;
5111 for line in lines.iter().take(pos.0) {
5112 idx += line.chars().count() + 1;
5113 }
5114 idx + pos.1
5115 };
5116 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5117 for (r, line) in lines.iter().enumerate() {
5118 let len = line.chars().count();
5119 if idx <= len {
5120 return (r, idx);
5121 }
5122 idx -= len + 1;
5123 }
5124 let last = lines.len().saturating_sub(1);
5125 (last, lines[last].chars().count())
5126 };
5127 let mut chars: Vec<char> = Vec::new();
5128 for (r, line) in lines.iter().enumerate() {
5129 chars.extend(line.chars());
5130 if r + 1 < lines.len() {
5131 chars.push('\n');
5132 }
5133 }
5134 if chars.is_empty() {
5135 return None;
5136 }
5137 let total = chars.len();
5138 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
5139 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5140
5141 if forward {
5142 let mut i = cursor_idx + 1;
5145 while i < total {
5146 if is_terminator(chars[i]) {
5147 while i + 1 < total && is_terminator(chars[i + 1]) {
5148 i += 1;
5149 }
5150 if i + 1 >= total {
5151 return None;
5152 }
5153 if chars[i + 1].is_whitespace() {
5154 let mut j = i + 1;
5155 while j < total && chars[j].is_whitespace() {
5156 j += 1;
5157 }
5158 if j >= total {
5159 return None;
5160 }
5161 return Some(idx_to_pos(j));
5162 }
5163 }
5164 i += 1;
5165 }
5166 None
5167 } else {
5168 let find_start = |from: usize| -> Option<usize> {
5172 let mut start = from;
5173 while start > 0 {
5174 let prev = chars[start - 1];
5175 if prev.is_whitespace() {
5176 let mut k = start - 1;
5177 while k > 0 && chars[k - 1].is_whitespace() {
5178 k -= 1;
5179 }
5180 if k > 0 && is_terminator(chars[k - 1]) {
5181 break;
5182 }
5183 }
5184 start -= 1;
5185 }
5186 while start < total && chars[start].is_whitespace() {
5187 start += 1;
5188 }
5189 (start < total).then_some(start)
5190 };
5191 let current_start = find_start(cursor_idx)?;
5192 if current_start < cursor_idx {
5193 return Some(idx_to_pos(current_start));
5194 }
5195 let mut k = current_start;
5198 while k > 0 && chars[k - 1].is_whitespace() {
5199 k -= 1;
5200 }
5201 if k == 0 {
5202 return None;
5203 }
5204 let prev_start = find_start(k - 1)?;
5205 Some(idx_to_pos(prev_start))
5206 }
5207}
5208
5209fn sentence_text_object<H: crate::types::Host>(
5215 ed: &Editor<hjkl_buffer::Buffer, H>,
5216 inner: bool,
5217) -> Option<((usize, usize), (usize, usize))> {
5218 let lines = buf_lines_to_vec(&ed.buffer);
5219 if lines.is_empty() {
5220 return None;
5221 }
5222 let pos_to_idx = |pos: (usize, usize)| -> usize {
5225 let mut idx = 0;
5226 for line in lines.iter().take(pos.0) {
5227 idx += line.chars().count() + 1;
5228 }
5229 idx + pos.1
5230 };
5231 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5232 for (r, line) in lines.iter().enumerate() {
5233 let len = line.chars().count();
5234 if idx <= len {
5235 return (r, idx);
5236 }
5237 idx -= len + 1;
5238 }
5239 let last = lines.len().saturating_sub(1);
5240 (last, lines[last].chars().count())
5241 };
5242 let mut chars: Vec<char> = Vec::new();
5243 for (r, line) in lines.iter().enumerate() {
5244 chars.extend(line.chars());
5245 if r + 1 < lines.len() {
5246 chars.push('\n');
5247 }
5248 }
5249 if chars.is_empty() {
5250 return None;
5251 }
5252
5253 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
5254 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5255
5256 let mut start = cursor_idx;
5260 while start > 0 {
5261 let prev = chars[start - 1];
5262 if prev.is_whitespace() {
5263 let mut k = start - 1;
5267 while k > 0 && chars[k - 1].is_whitespace() {
5268 k -= 1;
5269 }
5270 if k > 0 && is_terminator(chars[k - 1]) {
5271 break;
5272 }
5273 }
5274 start -= 1;
5275 }
5276 while start < chars.len() && chars[start].is_whitespace() {
5279 start += 1;
5280 }
5281 if start >= chars.len() {
5282 return None;
5283 }
5284
5285 let mut end = start;
5288 while end < chars.len() {
5289 if is_terminator(chars[end]) {
5290 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5292 end += 1;
5293 }
5294 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5297 break;
5298 }
5299 }
5300 end += 1;
5301 }
5302 let end_idx = (end + 1).min(chars.len());
5304
5305 let final_end = if inner {
5306 end_idx
5307 } else {
5308 let mut e = end_idx;
5312 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5313 e += 1;
5314 }
5315 e
5316 };
5317
5318 Some((idx_to_pos(start), idx_to_pos(final_end)))
5319}
5320
5321fn tag_text_object<H: crate::types::Host>(
5325 ed: &Editor<hjkl_buffer::Buffer, H>,
5326 inner: bool,
5327) -> Option<((usize, usize), (usize, usize))> {
5328 let lines = buf_lines_to_vec(&ed.buffer);
5329 if lines.is_empty() {
5330 return None;
5331 }
5332 let pos_to_idx = |pos: (usize, usize)| -> usize {
5336 let mut idx = 0;
5337 for line in lines.iter().take(pos.0) {
5338 idx += line.chars().count() + 1;
5339 }
5340 idx + pos.1
5341 };
5342 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5343 for (r, line) in lines.iter().enumerate() {
5344 let len = line.chars().count();
5345 if idx <= len {
5346 return (r, idx);
5347 }
5348 idx -= len + 1;
5349 }
5350 let last = lines.len().saturating_sub(1);
5351 (last, lines[last].chars().count())
5352 };
5353 let mut chars: Vec<char> = Vec::new();
5354 for (r, line) in lines.iter().enumerate() {
5355 chars.extend(line.chars());
5356 if r + 1 < lines.len() {
5357 chars.push('\n');
5358 }
5359 }
5360 let cursor_idx = pos_to_idx(ed.cursor());
5361
5362 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5370 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5371 let mut i = 0;
5372 while i < chars.len() {
5373 if chars[i] != '<' {
5374 i += 1;
5375 continue;
5376 }
5377 let mut j = i + 1;
5378 while j < chars.len() && chars[j] != '>' {
5379 j += 1;
5380 }
5381 if j >= chars.len() {
5382 break;
5383 }
5384 let inside: String = chars[i + 1..j].iter().collect();
5385 let close_end = j + 1;
5386 let trimmed = inside.trim();
5387 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5388 i = close_end;
5389 continue;
5390 }
5391 if let Some(rest) = trimmed.strip_prefix('/') {
5392 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5393 if !name.is_empty()
5394 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5395 {
5396 let (open_start, content_start, _) = stack[stack_idx].clone();
5397 stack.truncate(stack_idx);
5398 let content_end = i;
5399 let candidate = (open_start, content_start, content_end, close_end);
5400 if cursor_idx >= content_start && cursor_idx <= content_end {
5401 innermost = match innermost {
5402 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5403 Some(candidate)
5404 }
5405 None => Some(candidate),
5406 existing => existing,
5407 };
5408 } else if open_start >= cursor_idx && next_after.is_none() {
5409 next_after = Some(candidate);
5410 }
5411 }
5412 } else if !trimmed.ends_with('/') {
5413 let name: String = trimmed
5414 .split(|c: char| c.is_whitespace() || c == '/')
5415 .next()
5416 .unwrap_or("")
5417 .to_string();
5418 if !name.is_empty() {
5419 stack.push((i, close_end, name));
5420 }
5421 }
5422 i = close_end;
5423 }
5424
5425 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5426 if inner {
5427 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5428 } else {
5429 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5430 }
5431}
5432
5433fn is_wordchar(c: char) -> bool {
5434 c.is_alphanumeric() || c == '_'
5435}
5436
5437pub(crate) use hjkl_buffer::is_keyword_char;
5441
5442fn word_text_object<H: crate::types::Host>(
5443 ed: &Editor<hjkl_buffer::Buffer, H>,
5444 inner: bool,
5445 big: bool,
5446) -> Option<((usize, usize), (usize, usize))> {
5447 let (row, col) = ed.cursor();
5448 let line = buf_line(&ed.buffer, row)?;
5449 let chars: Vec<char> = line.chars().collect();
5450 if chars.is_empty() {
5451 return None;
5452 }
5453 let at = col.min(chars.len().saturating_sub(1));
5454 let classify = |c: char| -> u8 {
5455 if c.is_whitespace() {
5456 0
5457 } else if big || is_wordchar(c) {
5458 1
5459 } else {
5460 2
5461 }
5462 };
5463 let cls = classify(chars[at]);
5464 let mut start = at;
5465 while start > 0 && classify(chars[start - 1]) == cls {
5466 start -= 1;
5467 }
5468 let mut end = at;
5469 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5470 end += 1;
5471 }
5472 let char_byte = |i: usize| {
5474 if i >= chars.len() {
5475 line.len()
5476 } else {
5477 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5478 }
5479 };
5480 let mut start_col = char_byte(start);
5481 let mut end_col = char_byte(end + 1);
5483 if !inner {
5484 let mut t = end + 1;
5486 let mut included_trailing = false;
5487 while t < chars.len() && chars[t].is_whitespace() {
5488 included_trailing = true;
5489 t += 1;
5490 }
5491 if included_trailing {
5492 end_col = char_byte(t);
5493 } else {
5494 let mut s = start;
5495 while s > 0 && chars[s - 1].is_whitespace() {
5496 s -= 1;
5497 }
5498 start_col = char_byte(s);
5499 }
5500 }
5501 Some(((row, start_col), (row, end_col)))
5502}
5503
5504fn quote_text_object<H: crate::types::Host>(
5505 ed: &Editor<hjkl_buffer::Buffer, H>,
5506 q: char,
5507 inner: bool,
5508) -> Option<((usize, usize), (usize, usize))> {
5509 let (row, col) = ed.cursor();
5510 let line = buf_line(&ed.buffer, row)?;
5511 let bytes = line.as_bytes();
5512 let q_byte = q as u8;
5513 let mut positions: Vec<usize> = Vec::new();
5515 for (i, &b) in bytes.iter().enumerate() {
5516 if b == q_byte {
5517 positions.push(i);
5518 }
5519 }
5520 if positions.len() < 2 {
5521 return None;
5522 }
5523 let mut open_idx: Option<usize> = None;
5524 let mut close_idx: Option<usize> = None;
5525 for pair in positions.chunks(2) {
5526 if pair.len() < 2 {
5527 break;
5528 }
5529 if col >= pair[0] && col <= pair[1] {
5530 open_idx = Some(pair[0]);
5531 close_idx = Some(pair[1]);
5532 break;
5533 }
5534 if col < pair[0] {
5535 open_idx = Some(pair[0]);
5536 close_idx = Some(pair[1]);
5537 break;
5538 }
5539 }
5540 let open = open_idx?;
5541 let close = close_idx?;
5542 if inner {
5544 if close <= open + 1 {
5545 return None;
5546 }
5547 Some(((row, open + 1), (row, close)))
5548 } else {
5549 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5556 let mut end = after_close;
5558 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5559 end += 1;
5560 }
5561 Some(((row, open), (row, end)))
5562 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5563 let mut start = open;
5565 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5566 start -= 1;
5567 }
5568 Some(((row, start), (row, close + 1)))
5569 } else {
5570 Some(((row, open), (row, close + 1)))
5571 }
5572 }
5573}
5574
5575fn bracket_text_object<H: crate::types::Host>(
5576 ed: &Editor<hjkl_buffer::Buffer, H>,
5577 open: char,
5578 inner: bool,
5579) -> Option<(Pos, Pos, RangeKind)> {
5580 let close = match open {
5581 '(' => ')',
5582 '[' => ']',
5583 '{' => '}',
5584 '<' => '>',
5585 _ => return None,
5586 };
5587 let (row, col) = ed.cursor();
5588 let lines = buf_lines_to_vec(&ed.buffer);
5589 let lines = lines.as_slice();
5590 let open_pos = find_open_bracket(lines, row, col, open, close)
5595 .or_else(|| find_next_open(lines, row, col, open))?;
5596 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5597 if inner {
5599 if close_pos.0 > open_pos.0 + 1 {
5605 let inner_row_start = open_pos.0 + 1;
5607 let inner_row_end = close_pos.0 - 1;
5608 let end_col = lines
5609 .get(inner_row_end)
5610 .map(|l| l.chars().count())
5611 .unwrap_or(0);
5612 return Some((
5613 (inner_row_start, 0),
5614 (inner_row_end, end_col),
5615 RangeKind::Linewise,
5616 ));
5617 }
5618 let inner_start = advance_pos(lines, open_pos);
5619 if inner_start.0 > close_pos.0
5620 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5621 {
5622 return None;
5623 }
5624 Some((inner_start, close_pos, RangeKind::Exclusive))
5625 } else {
5626 Some((
5627 open_pos,
5628 advance_pos(lines, close_pos),
5629 RangeKind::Exclusive,
5630 ))
5631 }
5632}
5633
5634fn find_open_bracket(
5635 lines: &[String],
5636 row: usize,
5637 col: usize,
5638 open: char,
5639 close: char,
5640) -> Option<(usize, usize)> {
5641 let mut depth: i32 = 0;
5642 let mut r = row;
5643 let mut c = col as isize;
5644 loop {
5645 let cur = &lines[r];
5646 let chars: Vec<char> = cur.chars().collect();
5647 if (c as usize) >= chars.len() {
5651 c = chars.len() as isize - 1;
5652 }
5653 while c >= 0 {
5654 let ch = chars[c as usize];
5655 if ch == close {
5656 depth += 1;
5657 } else if ch == open {
5658 if depth == 0 {
5659 return Some((r, c as usize));
5660 }
5661 depth -= 1;
5662 }
5663 c -= 1;
5664 }
5665 if r == 0 {
5666 return None;
5667 }
5668 r -= 1;
5669 c = lines[r].chars().count() as isize - 1;
5670 }
5671}
5672
5673fn find_close_bracket(
5674 lines: &[String],
5675 row: usize,
5676 start_col: usize,
5677 open: char,
5678 close: char,
5679) -> Option<(usize, usize)> {
5680 let mut depth: i32 = 0;
5681 let mut r = row;
5682 let mut c = start_col;
5683 loop {
5684 let cur = &lines[r];
5685 let chars: Vec<char> = cur.chars().collect();
5686 while c < chars.len() {
5687 let ch = chars[c];
5688 if ch == open {
5689 depth += 1;
5690 } else if ch == close {
5691 if depth == 0 {
5692 return Some((r, c));
5693 }
5694 depth -= 1;
5695 }
5696 c += 1;
5697 }
5698 if r + 1 >= lines.len() {
5699 return None;
5700 }
5701 r += 1;
5702 c = 0;
5703 }
5704}
5705
5706fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5710 let mut r = row;
5711 let mut c = col;
5712 while r < lines.len() {
5713 let chars: Vec<char> = lines[r].chars().collect();
5714 while c < chars.len() {
5715 if chars[c] == open {
5716 return Some((r, c));
5717 }
5718 c += 1;
5719 }
5720 r += 1;
5721 c = 0;
5722 }
5723 None
5724}
5725
5726fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5727 let (r, c) = pos;
5728 let line_len = lines[r].chars().count();
5729 if c < line_len {
5730 (r, c + 1)
5731 } else if r + 1 < lines.len() {
5732 (r + 1, 0)
5733 } else {
5734 pos
5735 }
5736}
5737
5738fn paragraph_text_object<H: crate::types::Host>(
5739 ed: &Editor<hjkl_buffer::Buffer, H>,
5740 inner: bool,
5741) -> Option<((usize, usize), (usize, usize))> {
5742 let (row, _) = ed.cursor();
5743 let lines = buf_lines_to_vec(&ed.buffer);
5744 if lines.is_empty() {
5745 return None;
5746 }
5747 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5749 if is_blank(row) {
5750 return None;
5751 }
5752 let mut top = row;
5753 while top > 0 && !is_blank(top - 1) {
5754 top -= 1;
5755 }
5756 let mut bot = row;
5757 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5758 bot += 1;
5759 }
5760 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5762 bot += 1;
5763 }
5764 let end_col = lines[bot].chars().count();
5765 Some(((top, 0), (bot, end_col)))
5766}
5767
5768fn read_vim_range<H: crate::types::Host>(
5774 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5775 start: (usize, usize),
5776 end: (usize, usize),
5777 kind: RangeKind,
5778) -> String {
5779 let (top, bot) = order(start, end);
5780 ed.sync_buffer_content_from_textarea();
5781 let lines = buf_lines_to_vec(&ed.buffer);
5782 match kind {
5783 RangeKind::Linewise => {
5784 let lo = top.0;
5785 let hi = bot.0.min(lines.len().saturating_sub(1));
5786 let mut text = lines[lo..=hi].join("\n");
5787 text.push('\n');
5788 text
5789 }
5790 RangeKind::Inclusive | RangeKind::Exclusive => {
5791 let inclusive = matches!(kind, RangeKind::Inclusive);
5792 let mut out = String::new();
5794 for row in top.0..=bot.0 {
5795 let line = lines.get(row).map(String::as_str).unwrap_or("");
5796 let lo = if row == top.0 { top.1 } else { 0 };
5797 let hi_unclamped = if row == bot.0 {
5798 if inclusive { bot.1 + 1 } else { bot.1 }
5799 } else {
5800 line.chars().count() + 1
5801 };
5802 let row_chars: Vec<char> = line.chars().collect();
5803 let hi = hi_unclamped.min(row_chars.len());
5804 if lo < hi {
5805 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5806 }
5807 if row < bot.0 {
5808 out.push('\n');
5809 }
5810 }
5811 out
5812 }
5813 }
5814}
5815
5816fn cut_vim_range<H: crate::types::Host>(
5825 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5826 start: (usize, usize),
5827 end: (usize, usize),
5828 kind: RangeKind,
5829) -> String {
5830 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5831 let (top, bot) = order(start, end);
5832 ed.sync_buffer_content_from_textarea();
5833 let (buf_start, buf_end, buf_kind) = match kind {
5834 RangeKind::Linewise => (
5835 Position::new(top.0, 0),
5836 Position::new(bot.0, 0),
5837 BufKind::Line,
5838 ),
5839 RangeKind::Inclusive => {
5840 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5841 let next = if bot.1 < line_chars {
5845 Position::new(bot.0, bot.1 + 1)
5846 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5847 Position::new(bot.0 + 1, 0)
5848 } else {
5849 Position::new(bot.0, line_chars)
5850 };
5851 (Position::new(top.0, top.1), next, BufKind::Char)
5852 }
5853 RangeKind::Exclusive => (
5854 Position::new(top.0, top.1),
5855 Position::new(bot.0, bot.1),
5856 BufKind::Char,
5857 ),
5858 };
5859 let inverse = ed.mutate_edit(Edit::DeleteRange {
5860 start: buf_start,
5861 end: buf_end,
5862 kind: buf_kind,
5863 });
5864 let text = match inverse {
5865 Edit::InsertStr { text, .. } => text,
5866 _ => String::new(),
5867 };
5868 if !text.is_empty() {
5869 ed.record_yank_to_host(text.clone());
5870 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
5871 }
5872 ed.push_buffer_cursor_to_textarea();
5873 text
5874}
5875
5876fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5882 use hjkl_buffer::{Edit, MotionKind, Position};
5883 ed.sync_buffer_content_from_textarea();
5884 let cursor = buf_cursor_pos(&ed.buffer);
5885 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5886 if cursor.col >= line_chars {
5887 return;
5888 }
5889 let inverse = ed.mutate_edit(Edit::DeleteRange {
5890 start: cursor,
5891 end: Position::new(cursor.row, line_chars),
5892 kind: MotionKind::Char,
5893 });
5894 if let Edit::InsertStr { text, .. } = inverse
5895 && !text.is_empty()
5896 {
5897 ed.record_yank_to_host(text.clone());
5898 ed.vim.yank_linewise = false;
5899 ed.set_yank(text);
5900 }
5901 buf_set_cursor_pos(&mut ed.buffer, cursor);
5902 ed.push_buffer_cursor_to_textarea();
5903}
5904
5905fn do_char_delete<H: crate::types::Host>(
5906 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5907 forward: bool,
5908 count: usize,
5909) {
5910 use hjkl_buffer::{Edit, MotionKind, Position};
5911 ed.push_undo();
5912 ed.sync_buffer_content_from_textarea();
5913 let mut deleted = String::new();
5916 for _ in 0..count {
5917 let cursor = buf_cursor_pos(&ed.buffer);
5918 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5919 if forward {
5920 if cursor.col >= line_chars {
5923 continue;
5924 }
5925 let inverse = ed.mutate_edit(Edit::DeleteRange {
5926 start: cursor,
5927 end: Position::new(cursor.row, cursor.col + 1),
5928 kind: MotionKind::Char,
5929 });
5930 if let Edit::InsertStr { text, .. } = inverse {
5931 deleted.push_str(&text);
5932 }
5933 } else {
5934 if cursor.col == 0 {
5936 continue;
5937 }
5938 let inverse = ed.mutate_edit(Edit::DeleteRange {
5939 start: Position::new(cursor.row, cursor.col - 1),
5940 end: cursor,
5941 kind: MotionKind::Char,
5942 });
5943 if let Edit::InsertStr { text, .. } = inverse {
5944 deleted = text + &deleted;
5947 }
5948 }
5949 }
5950 if !deleted.is_empty() {
5951 ed.record_yank_to_host(deleted.clone());
5952 ed.record_delete(deleted, false);
5953 }
5954 ed.push_buffer_cursor_to_textarea();
5955}
5956
5957pub(crate) fn adjust_number<H: crate::types::Host>(
5961 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5962 delta: i64,
5963) -> bool {
5964 use hjkl_buffer::{Edit, MotionKind, Position};
5965 ed.sync_buffer_content_from_textarea();
5966 let cursor = buf_cursor_pos(&ed.buffer);
5967 let row = cursor.row;
5968 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5969 Some(l) => l.chars().collect(),
5970 None => return false,
5971 };
5972 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5973 return false;
5974 };
5975 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5976 digit_start - 1
5977 } else {
5978 digit_start
5979 };
5980 let mut span_end = digit_start;
5981 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5982 span_end += 1;
5983 }
5984 let s: String = chars[span_start..span_end].iter().collect();
5985 let Ok(n) = s.parse::<i64>() else {
5986 return false;
5987 };
5988 let new_s = n.saturating_add(delta).to_string();
5989
5990 ed.push_undo();
5991 let span_start_pos = Position::new(row, span_start);
5992 let span_end_pos = Position::new(row, span_end);
5993 ed.mutate_edit(Edit::DeleteRange {
5994 start: span_start_pos,
5995 end: span_end_pos,
5996 kind: MotionKind::Char,
5997 });
5998 ed.mutate_edit(Edit::InsertStr {
5999 at: span_start_pos,
6000 text: new_s.clone(),
6001 });
6002 let new_len = new_s.chars().count();
6003 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
6004 ed.push_buffer_cursor_to_textarea();
6005 true
6006}
6007
6008pub(crate) fn replace_char<H: crate::types::Host>(
6009 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6010 ch: char,
6011 count: usize,
6012) {
6013 use hjkl_buffer::{Edit, MotionKind, Position};
6014 ed.push_undo();
6015 ed.sync_buffer_content_from_textarea();
6016 for _ in 0..count {
6017 let cursor = buf_cursor_pos(&ed.buffer);
6018 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6019 if cursor.col >= line_chars {
6020 break;
6021 }
6022 ed.mutate_edit(Edit::DeleteRange {
6023 start: cursor,
6024 end: Position::new(cursor.row, cursor.col + 1),
6025 kind: MotionKind::Char,
6026 });
6027 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
6028 }
6029 crate::motions::move_left(&mut ed.buffer, 1);
6031 ed.push_buffer_cursor_to_textarea();
6032}
6033
6034fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6035 use hjkl_buffer::{Edit, MotionKind, Position};
6036 ed.sync_buffer_content_from_textarea();
6037 let cursor = buf_cursor_pos(&ed.buffer);
6038 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
6039 return;
6040 };
6041 let toggled = if c.is_uppercase() {
6042 c.to_lowercase().next().unwrap_or(c)
6043 } else {
6044 c.to_uppercase().next().unwrap_or(c)
6045 };
6046 ed.mutate_edit(Edit::DeleteRange {
6047 start: cursor,
6048 end: Position::new(cursor.row, cursor.col + 1),
6049 kind: MotionKind::Char,
6050 });
6051 ed.mutate_edit(Edit::InsertChar {
6052 at: cursor,
6053 ch: toggled,
6054 });
6055}
6056
6057fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6058 use hjkl_buffer::{Edit, Position};
6059 ed.sync_buffer_content_from_textarea();
6060 let row = buf_cursor_pos(&ed.buffer).row;
6061 if row + 1 >= buf_row_count(&ed.buffer) {
6062 return;
6063 }
6064 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
6065 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
6066 let next_trimmed = next_raw.trim_start();
6067 let cur_chars = cur_line.chars().count();
6068 let next_chars = next_raw.chars().count();
6069 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
6072 " "
6073 } else {
6074 ""
6075 };
6076 let joined = format!("{cur_line}{separator}{next_trimmed}");
6077 ed.mutate_edit(Edit::Replace {
6078 start: Position::new(row, 0),
6079 end: Position::new(row + 1, next_chars),
6080 with: joined,
6081 });
6082 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
6086 ed.push_buffer_cursor_to_textarea();
6087}
6088
6089fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6092 use hjkl_buffer::Edit;
6093 ed.sync_buffer_content_from_textarea();
6094 let row = buf_cursor_pos(&ed.buffer).row;
6095 if row + 1 >= buf_row_count(&ed.buffer) {
6096 return;
6097 }
6098 let join_col = buf_line_chars(&ed.buffer, row);
6099 ed.mutate_edit(Edit::JoinLines {
6100 row,
6101 count: 1,
6102 with_space: false,
6103 });
6104 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
6106 ed.push_buffer_cursor_to_textarea();
6107}
6108
6109fn do_paste<H: crate::types::Host>(
6110 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6111 before: bool,
6112 count: usize,
6113) {
6114 use hjkl_buffer::{Edit, Position};
6115 ed.push_undo();
6116 let selector = ed.vim.pending_register.take();
6121 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
6122 Some(slot) => (slot.text.clone(), slot.linewise),
6123 None => {
6129 let s = &ed.registers().unnamed;
6130 (s.text.clone(), s.linewise)
6131 }
6132 };
6133 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
6137 for _ in 0..count {
6138 ed.sync_buffer_content_from_textarea();
6139 let yank = yank.clone();
6140 if yank.is_empty() {
6141 continue;
6142 }
6143 if linewise {
6144 let text = yank.trim_matches('\n').to_string();
6148 let row = buf_cursor_pos(&ed.buffer).row;
6149 let target_row = if before {
6150 ed.mutate_edit(Edit::InsertStr {
6151 at: Position::new(row, 0),
6152 text: format!("{text}\n"),
6153 });
6154 row
6155 } else {
6156 let line_chars = buf_line_chars(&ed.buffer, row);
6157 ed.mutate_edit(Edit::InsertStr {
6158 at: Position::new(row, line_chars),
6159 text: format!("\n{text}"),
6160 });
6161 row + 1
6162 };
6163 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6164 crate::motions::move_first_non_blank(&mut ed.buffer);
6165 ed.push_buffer_cursor_to_textarea();
6166 let payload_lines = text.lines().count().max(1);
6168 let bot_row = target_row + payload_lines - 1;
6169 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
6170 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
6171 } else {
6172 let cursor = buf_cursor_pos(&ed.buffer);
6176 let at = if before {
6177 cursor
6178 } else {
6179 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6180 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
6181 };
6182 ed.mutate_edit(Edit::InsertStr {
6183 at,
6184 text: yank.clone(),
6185 });
6186 crate::motions::move_left(&mut ed.buffer, 1);
6189 ed.push_buffer_cursor_to_textarea();
6190 let lo = (at.row, at.col);
6192 let hi = ed.cursor();
6193 paste_mark = Some((lo, hi));
6194 }
6195 }
6196 if let Some((lo, hi)) = paste_mark {
6197 ed.set_mark('[', lo);
6198 ed.set_mark(']', hi);
6199 }
6200 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
6202}
6203
6204pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6205 if let Some((lines, cursor)) = ed.undo_stack.pop() {
6206 let current = ed.snapshot();
6207 ed.redo_stack.push(current);
6208 ed.restore(lines, cursor);
6209 }
6210 ed.vim.mode = Mode::Normal;
6211 clamp_cursor_to_normal_mode(ed);
6215}
6216
6217pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6218 if let Some((lines, cursor)) = ed.redo_stack.pop() {
6219 let current = ed.snapshot();
6220 ed.undo_stack.push(current);
6221 ed.cap_undo();
6222 ed.restore(lines, cursor);
6223 }
6224 ed.vim.mode = Mode::Normal;
6225}
6226
6227fn replay_insert_and_finish<H: crate::types::Host>(
6234 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6235 text: &str,
6236) {
6237 use hjkl_buffer::{Edit, Position};
6238 let cursor = ed.cursor();
6239 ed.mutate_edit(Edit::InsertStr {
6240 at: Position::new(cursor.0, cursor.1),
6241 text: text.to_string(),
6242 });
6243 if ed.vim.insert_session.take().is_some() {
6244 if ed.cursor().1 > 0 {
6245 crate::motions::move_left(&mut ed.buffer, 1);
6246 ed.push_buffer_cursor_to_textarea();
6247 }
6248 ed.vim.mode = Mode::Normal;
6249 }
6250}
6251
6252pub(crate) fn replay_last_change<H: crate::types::Host>(
6253 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6254 outer_count: usize,
6255) {
6256 let Some(change) = ed.vim.last_change.clone() else {
6257 return;
6258 };
6259 ed.vim.replaying = true;
6260 let scale = if outer_count > 0 { outer_count } else { 1 };
6261 match change {
6262 LastChange::OpMotion {
6263 op,
6264 motion,
6265 count,
6266 inserted,
6267 } => {
6268 let total = count.max(1) * scale;
6269 apply_op_with_motion(ed, op, &motion, total);
6270 if let Some(text) = inserted {
6271 replay_insert_and_finish(ed, &text);
6272 }
6273 }
6274 LastChange::OpTextObj {
6275 op,
6276 obj,
6277 inner,
6278 inserted,
6279 } => {
6280 apply_op_with_text_object(ed, op, obj, inner);
6281 if let Some(text) = inserted {
6282 replay_insert_and_finish(ed, &text);
6283 }
6284 }
6285 LastChange::LineOp {
6286 op,
6287 count,
6288 inserted,
6289 } => {
6290 let total = count.max(1) * scale;
6291 execute_line_op(ed, op, total);
6292 if let Some(text) = inserted {
6293 replay_insert_and_finish(ed, &text);
6294 }
6295 }
6296 LastChange::CharDel { forward, count } => {
6297 do_char_delete(ed, forward, count * scale);
6298 }
6299 LastChange::ReplaceChar { ch, count } => {
6300 replace_char(ed, ch, count * scale);
6301 }
6302 LastChange::ToggleCase { count } => {
6303 for _ in 0..count * scale {
6304 ed.push_undo();
6305 toggle_case_at_cursor(ed);
6306 }
6307 }
6308 LastChange::JoinLine { count } => {
6309 for _ in 0..count * scale {
6310 ed.push_undo();
6311 join_line(ed);
6312 }
6313 }
6314 LastChange::Paste { before, count } => {
6315 do_paste(ed, before, count * scale);
6316 }
6317 LastChange::DeleteToEol { inserted } => {
6318 use hjkl_buffer::{Edit, Position};
6319 ed.push_undo();
6320 delete_to_eol(ed);
6321 if let Some(text) = inserted {
6322 let cursor = ed.cursor();
6323 ed.mutate_edit(Edit::InsertStr {
6324 at: Position::new(cursor.0, cursor.1),
6325 text,
6326 });
6327 }
6328 }
6329 LastChange::OpenLine { above, inserted } => {
6330 use hjkl_buffer::{Edit, Position};
6331 ed.push_undo();
6332 ed.sync_buffer_content_from_textarea();
6333 let row = buf_cursor_pos(&ed.buffer).row;
6334 if above {
6335 ed.mutate_edit(Edit::InsertStr {
6336 at: Position::new(row, 0),
6337 text: "\n".to_string(),
6338 });
6339 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6340 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6341 } else {
6342 let line_chars = buf_line_chars(&ed.buffer, row);
6343 ed.mutate_edit(Edit::InsertStr {
6344 at: Position::new(row, line_chars),
6345 text: "\n".to_string(),
6346 });
6347 }
6348 ed.push_buffer_cursor_to_textarea();
6349 let cursor = ed.cursor();
6350 ed.mutate_edit(Edit::InsertStr {
6351 at: Position::new(cursor.0, cursor.1),
6352 text: inserted,
6353 });
6354 }
6355 LastChange::InsertAt {
6356 entry,
6357 inserted,
6358 count,
6359 } => {
6360 use hjkl_buffer::{Edit, Position};
6361 ed.push_undo();
6362 match entry {
6363 InsertEntry::I => {}
6364 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6365 InsertEntry::A => {
6366 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6367 ed.push_buffer_cursor_to_textarea();
6368 }
6369 InsertEntry::ShiftA => {
6370 crate::motions::move_line_end(&mut ed.buffer);
6371 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6372 ed.push_buffer_cursor_to_textarea();
6373 }
6374 }
6375 for _ in 0..count.max(1) {
6376 let cursor = ed.cursor();
6377 ed.mutate_edit(Edit::InsertStr {
6378 at: Position::new(cursor.0, cursor.1),
6379 text: inserted.clone(),
6380 });
6381 }
6382 }
6383 }
6384 ed.vim.replaying = false;
6385}
6386
6387fn extract_inserted(before: &str, after: &str) -> String {
6390 let before_chars: Vec<char> = before.chars().collect();
6391 let after_chars: Vec<char> = after.chars().collect();
6392 if after_chars.len() <= before_chars.len() {
6393 return String::new();
6394 }
6395 let prefix = before_chars
6396 .iter()
6397 .zip(after_chars.iter())
6398 .take_while(|(a, b)| a == b)
6399 .count();
6400 let max_suffix = before_chars.len() - prefix;
6401 let suffix = before_chars
6402 .iter()
6403 .rev()
6404 .zip(after_chars.iter().rev())
6405 .take(max_suffix)
6406 .take_while(|(a, b)| a == b)
6407 .count();
6408 after_chars[prefix..after_chars.len() - suffix]
6409 .iter()
6410 .collect()
6411}
6412
6413