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}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
203pub enum Motion {
204 Left,
205 Right,
206 Up,
207 Down,
208 WordFwd,
209 BigWordFwd,
210 WordBack,
211 BigWordBack,
212 WordEnd,
213 BigWordEnd,
214 WordEndBack,
216 BigWordEndBack,
218 LineStart,
219 FirstNonBlank,
220 LineEnd,
221 FileTop,
222 FileBottom,
223 Find {
224 ch: char,
225 forward: bool,
226 till: bool,
227 },
228 FindRepeat {
229 reverse: bool,
230 },
231 MatchBracket,
232 WordAtCursor {
233 forward: bool,
234 whole_word: bool,
237 },
238 SearchNext {
240 reverse: bool,
241 },
242 ViewportTop,
244 ViewportMiddle,
246 ViewportBottom,
248 LastNonBlank,
250 LineMiddle,
253 ParagraphPrev,
255 ParagraphNext,
257 SentencePrev,
259 SentenceNext,
261 ScreenDown,
264 ScreenUp,
266 SectionBackward,
269 SectionForward,
271 SectionEndBackward,
274 SectionEndForward,
276 FirstNonBlankNextLine,
278 FirstNonBlankPrevLine,
280 FirstNonBlankLine,
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub enum TextObject {
286 Word {
287 big: bool,
288 },
289 Quote(char),
290 Bracket(char),
291 Paragraph,
292 XmlTag,
296 Sentence,
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq)]
305pub enum RangeKind {
306 Exclusive,
308 Inclusive,
310 Linewise,
312}
313
314#[derive(Debug, Clone)]
318pub enum LastChange {
319 OpMotion {
321 op: Operator,
322 motion: Motion,
323 count: usize,
324 inserted: Option<String>,
325 },
326 OpTextObj {
328 op: Operator,
329 obj: TextObject,
330 inner: bool,
331 inserted: Option<String>,
332 },
333 LineOp {
335 op: Operator,
336 count: usize,
337 inserted: Option<String>,
338 },
339 CharDel { forward: bool, count: usize },
341 ReplaceChar { ch: char, count: usize },
343 ToggleCase { count: usize },
345 JoinLine { count: usize },
347 Paste { before: bool, count: usize },
349 DeleteToEol { inserted: Option<String> },
351 OpenLine { above: bool, inserted: String },
353 InsertAt {
355 entry: InsertEntry,
356 inserted: String,
357 count: usize,
358 },
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub enum InsertEntry {
363 I,
364 A,
365 ShiftI,
366 ShiftA,
367}
368
369#[derive(Default)]
372pub struct VimState {
373 pub mode: Mode,
378 pub pending: Pending,
380 pub count: usize,
383 pub last_find: Option<(char, bool, bool)>,
385 pub last_change: Option<LastChange>,
387 pub insert_session: Option<InsertSession>,
389 pub visual_anchor: (usize, usize),
393 pub visual_line_anchor: usize,
395 pub block_anchor: (usize, usize),
398 pub block_vcol: usize,
404 pub yank_linewise: bool,
406 pub pending_register: Option<char>,
409 pub recording_macro: Option<char>,
413 pub recording_keys: Vec<crate::input::Input>,
418 pub replaying_macro: bool,
421 pub last_macro: Option<char>,
423 pub last_edit_pos: Option<(usize, usize)>,
426 pub last_insert_pos: Option<(usize, usize)>,
430 pub change_list: Vec<(usize, usize)>,
434 pub change_list_cursor: Option<usize>,
437 pub last_visual: Option<LastVisual>,
440 pub viewport_pinned: bool,
444 pub replaying: bool,
446 pub one_shot_normal: bool,
449 pub search_prompt: Option<SearchPrompt>,
451 pub last_search: Option<String>,
455 pub last_search_forward: bool,
459 pub jump_back: Vec<(usize, usize)>,
464 pub jump_fwd: Vec<(usize, usize)>,
467 pub insert_pending_register: bool,
471 pub change_mark_start: Option<(usize, usize)>,
477 pub search_history: Vec<String>,
481 pub search_history_cursor: Option<usize>,
486 pub last_input_at: Option<std::time::Instant>,
495 pub last_input_host_at: Option<core::time::Duration>,
499 pub(crate) current_mode: crate::VimMode,
505}
506
507pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
508pub(crate) const CHANGE_LIST_MAX: usize = 100;
509
510#[derive(Debug, Clone)]
513pub struct SearchPrompt {
514 pub text: String,
515 pub cursor: usize,
516 pub forward: bool,
517}
518
519#[derive(Debug, Clone)]
520pub struct InsertSession {
521 pub count: usize,
522 pub row_min: usize,
524 pub row_max: usize,
525 pub before_lines: Vec<String>,
529 pub reason: InsertReason,
530}
531
532#[derive(Debug, Clone)]
533pub enum InsertReason {
534 Enter(InsertEntry),
536 Open { above: bool },
538 AfterChange,
541 DeleteToEol,
543 ReplayOnly,
546 BlockEdge { top: usize, bot: usize, col: usize },
550 BlockChange { top: usize, bot: usize, col: usize },
555 Replace,
559}
560
561#[derive(Debug, Clone, Copy)]
571pub struct LastVisual {
572 pub mode: Mode,
573 pub anchor: (usize, usize),
574 pub cursor: (usize, usize),
575 pub block_vcol: usize,
576}
577
578impl VimState {
579 pub fn public_mode(&self) -> VimMode {
580 match self.mode {
581 Mode::Normal => VimMode::Normal,
582 Mode::Insert => VimMode::Insert,
583 Mode::Visual => VimMode::Visual,
584 Mode::VisualLine => VimMode::VisualLine,
585 Mode::VisualBlock => VimMode::VisualBlock,
586 }
587 }
588
589 pub fn force_normal(&mut self) {
590 self.mode = Mode::Normal;
591 self.pending = Pending::None;
592 self.count = 0;
593 self.insert_session = None;
594 self.current_mode = crate::VimMode::Normal;
596 }
597
598 pub(crate) fn clear_pending_prefix(&mut self) {
608 self.pending = Pending::None;
609 self.count = 0;
610 self.pending_register = None;
611 self.insert_pending_register = false;
612 }
613
614 pub(crate) fn widen_insert_row(&mut self, row: usize) {
619 if let Some(ref mut session) = self.insert_session {
620 session.row_min = session.row_min.min(row);
621 session.row_max = session.row_max.max(row);
622 }
623 }
624
625 pub fn is_visual(&self) -> bool {
626 matches!(
627 self.mode,
628 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
629 )
630 }
631
632 pub fn is_visual_char(&self) -> bool {
633 self.mode == Mode::Visual
634 }
635
636 pub(crate) fn pending_count_val(&self) -> Option<u32> {
639 if self.count == 0 {
640 None
641 } else {
642 Some(self.count as u32)
643 }
644 }
645
646 pub(crate) fn is_chord_pending(&self) -> bool {
649 !matches!(self.pending, Pending::None)
650 }
651
652 pub(crate) fn pending_op_char(&self) -> Option<char> {
656 let op = match &self.pending {
657 Pending::Op { op, .. }
658 | Pending::OpTextObj { op, .. }
659 | Pending::OpG { op, .. }
660 | Pending::OpFind { op, .. }
661 | Pending::OpSquareBracketOpen { op, .. }
662 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
663 _ => None,
664 };
665 op.map(|o| match o {
666 Operator::Delete => 'd',
667 Operator::Change => 'c',
668 Operator::Yank => 'y',
669 Operator::Uppercase => 'U',
670 Operator::Lowercase => 'u',
671 Operator::ToggleCase => '~',
672 Operator::Indent => '>',
673 Operator::Outdent => '<',
674 Operator::Fold => 'z',
675 Operator::Reflow => 'q',
676 Operator::AutoIndent => '=',
677 })
678 }
679}
680
681pub(crate) fn enter_search<H: crate::types::Host>(
687 ed: &mut Editor<hjkl_buffer::Buffer, H>,
688 forward: bool,
689) {
690 ed.vim.search_prompt = Some(SearchPrompt {
691 text: String::new(),
692 cursor: 0,
693 forward,
694 });
695 ed.vim.search_history_cursor = None;
696 ed.set_search_pattern(None);
700}
701
702fn walk_change_list<H: crate::types::Host>(
706 ed: &mut Editor<hjkl_buffer::Buffer, H>,
707 dir: isize,
708 count: usize,
709) {
710 if ed.vim.change_list.is_empty() {
711 return;
712 }
713 let len = ed.vim.change_list.len();
714 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
715 (None, -1) => len as isize - 1,
716 (None, 1) => return, (Some(i), -1) => i as isize - 1,
718 (Some(i), 1) => i as isize + 1,
719 _ => return,
720 };
721 for _ in 1..count {
722 let next = idx + dir;
723 if next < 0 || next >= len as isize {
724 break;
725 }
726 idx = next;
727 }
728 if idx < 0 || idx >= len as isize {
729 return;
730 }
731 let idx = idx as usize;
732 ed.vim.change_list_cursor = Some(idx);
733 let (row, col) = ed.vim.change_list[idx];
734 ed.jump_cursor(row, col);
735}
736
737fn insert_register_text<H: crate::types::Host>(
742 ed: &mut Editor<hjkl_buffer::Buffer, H>,
743 selector: char,
744) {
745 use hjkl_buffer::Edit;
746 let text = match ed.registers().read(selector) {
747 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
748 _ => return,
749 };
750 ed.sync_buffer_content_from_textarea();
751 let cursor = buf_cursor_pos(&ed.buffer);
752 ed.mutate_edit(Edit::InsertStr {
753 at: cursor,
754 text: text.clone(),
755 });
756 let mut row = cursor.row;
759 let mut col = cursor.col;
760 for ch in text.chars() {
761 if ch == '\n' {
762 row += 1;
763 col = 0;
764 } else {
765 col += 1;
766 }
767 }
768 buf_set_cursor_rc(&mut ed.buffer, row, col);
769 ed.push_buffer_cursor_to_textarea();
770 ed.mark_content_dirty();
771 if let Some(ref mut session) = ed.vim.insert_session {
772 session.row_min = session.row_min.min(row);
773 session.row_max = session.row_max.max(row);
774 }
775}
776
777pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
796 if !settings.autoindent {
797 return String::new();
798 }
799 let base: String = prev_line
801 .chars()
802 .take_while(|c| *c == ' ' || *c == '\t')
803 .collect();
804
805 if settings.smartindent {
806 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
810 if matches!(last_non_ws, Some('{' | '(' | '[')) {
811 let unit = if settings.expandtab {
812 if settings.softtabstop > 0 {
813 " ".repeat(settings.softtabstop)
814 } else {
815 " ".repeat(settings.shiftwidth)
816 }
817 } else {
818 "\t".to_string()
819 };
820 return format!("{base}{unit}");
821 }
822 }
823
824 base
825}
826
827fn try_dedent_close_bracket<H: crate::types::Host>(
837 ed: &mut Editor<hjkl_buffer::Buffer, H>,
838 cursor: hjkl_buffer::Position,
839 ch: char,
840) -> bool {
841 use hjkl_buffer::{Edit, MotionKind, Position};
842
843 if !ed.settings.smartindent {
844 return false;
845 }
846 if !matches!(ch, '}' | ')' | ']') {
847 return false;
848 }
849
850 let line = match buf_line(&ed.buffer, cursor.row) {
851 Some(l) => l.to_string(),
852 None => return false,
853 };
854
855 let before: String = line.chars().take(cursor.col).collect();
857 if !before.chars().all(|c| c == ' ' || c == '\t') {
858 return false;
859 }
860 if before.is_empty() {
861 return false;
863 }
864
865 let unit_len: usize = if ed.settings.expandtab {
867 if ed.settings.softtabstop > 0 {
868 ed.settings.softtabstop
869 } else {
870 ed.settings.shiftwidth
871 }
872 } else {
873 1
875 };
876
877 let strip_len = if ed.settings.expandtab {
879 let spaces = before.chars().filter(|c| *c == ' ').count();
881 if spaces < unit_len {
882 return false;
883 }
884 unit_len
885 } else {
886 if !before.starts_with('\t') {
888 return false;
889 }
890 1
891 };
892
893 ed.mutate_edit(Edit::DeleteRange {
895 start: Position::new(cursor.row, 0),
896 end: Position::new(cursor.row, strip_len),
897 kind: MotionKind::Char,
898 });
899 let new_col = cursor.col.saturating_sub(strip_len);
904 ed.mutate_edit(Edit::InsertChar {
905 at: Position::new(cursor.row, new_col),
906 ch,
907 });
908 true
909}
910
911fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
912 let Some(session) = ed.vim.insert_session.take() else {
913 return;
914 };
915 let lines = buf_lines_to_vec(&ed.buffer);
916 let after_end = session.row_max.min(lines.len().saturating_sub(1));
920 let before_end = session
921 .row_max
922 .min(session.before_lines.len().saturating_sub(1));
923 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
924 session.before_lines[session.row_min..=before_end].join("\n")
925 } else {
926 String::new()
927 };
928 let after = if after_end >= session.row_min && session.row_min < lines.len() {
929 lines[session.row_min..=after_end].join("\n")
930 } else {
931 String::new()
932 };
933 let inserted = extract_inserted(&before, &after);
934 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
935 use hjkl_buffer::{Edit, Position};
936 for _ in 0..session.count - 1 {
937 let (row, col) = ed.cursor();
938 ed.mutate_edit(Edit::InsertStr {
939 at: Position::new(row, col),
940 text: inserted.clone(),
941 });
942 }
943 }
944 fn replicate_block_text<H: crate::types::Host>(
948 ed: &mut Editor<hjkl_buffer::Buffer, H>,
949 inserted: &str,
950 top: usize,
951 bot: usize,
952 col: usize,
953 ) {
954 use hjkl_buffer::{Edit, Position};
955 for r in (top + 1)..=bot {
956 let line_len = buf_line_chars(&ed.buffer, r);
957 if col > line_len {
958 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
959 ed.mutate_edit(Edit::InsertStr {
960 at: Position::new(r, line_len),
961 text: pad,
962 });
963 }
964 ed.mutate_edit(Edit::InsertStr {
965 at: Position::new(r, col),
966 text: inserted.to_string(),
967 });
968 }
969 }
970
971 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
972 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
975 replicate_block_text(ed, &inserted, top, bot, col);
976 buf_set_cursor_rc(&mut ed.buffer, top, col);
977 ed.push_buffer_cursor_to_textarea();
978 }
979 return;
980 }
981 if let InsertReason::BlockChange { top, bot, col } = session.reason {
982 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
986 replicate_block_text(ed, &inserted, top, bot, col);
987 let ins_chars = inserted.chars().count();
988 let line_len = buf_line_chars(&ed.buffer, top);
989 let target_col = (col + ins_chars).min(line_len);
990 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
991 ed.push_buffer_cursor_to_textarea();
992 }
993 return;
994 }
995 if ed.vim.replaying {
996 return;
997 }
998 match session.reason {
999 InsertReason::Enter(entry) => {
1000 ed.vim.last_change = Some(LastChange::InsertAt {
1001 entry,
1002 inserted,
1003 count: session.count,
1004 });
1005 }
1006 InsertReason::Open { above } => {
1007 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1008 }
1009 InsertReason::AfterChange => {
1010 if let Some(
1011 LastChange::OpMotion { inserted: ins, .. }
1012 | LastChange::OpTextObj { inserted: ins, .. }
1013 | LastChange::LineOp { inserted: ins, .. },
1014 ) = ed.vim.last_change.as_mut()
1015 {
1016 *ins = Some(inserted);
1017 }
1018 if let Some(start) = ed.vim.change_mark_start.take() {
1024 let end = ed.cursor();
1025 ed.set_mark('[', start);
1026 ed.set_mark(']', end);
1027 }
1028 }
1029 InsertReason::DeleteToEol => {
1030 ed.vim.last_change = Some(LastChange::DeleteToEol {
1031 inserted: Some(inserted),
1032 });
1033 }
1034 InsertReason::ReplayOnly => {}
1035 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1036 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1037 InsertReason::Replace => {
1038 ed.vim.last_change = Some(LastChange::DeleteToEol {
1043 inserted: Some(inserted),
1044 });
1045 }
1046 }
1047}
1048
1049pub(crate) fn begin_insert<H: crate::types::Host>(
1050 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1051 count: usize,
1052 reason: InsertReason,
1053) {
1054 let record = !matches!(reason, InsertReason::ReplayOnly);
1055 if record {
1056 ed.push_undo();
1057 }
1058 let reason = if ed.vim.replaying {
1059 InsertReason::ReplayOnly
1060 } else {
1061 reason
1062 };
1063 let (row, _) = ed.cursor();
1064 ed.vim.insert_session = Some(InsertSession {
1065 count,
1066 row_min: row,
1067 row_max: row,
1068 before_lines: buf_lines_to_vec(&ed.buffer),
1069 reason,
1070 });
1071 ed.vim.mode = Mode::Insert;
1072 ed.vim.current_mode = crate::VimMode::Insert;
1074}
1075
1076pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1091 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1092) {
1093 if !ed.settings.undo_break_on_motion {
1094 return;
1095 }
1096 if ed.vim.replaying {
1097 return;
1098 }
1099 if ed.vim.insert_session.is_none() {
1100 return;
1101 }
1102 ed.push_undo();
1103 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1104 let mut lines: Vec<String> = Vec::with_capacity(n);
1105 for r in 0..n {
1106 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1107 }
1108 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1109 if let Some(ref mut session) = ed.vim.insert_session {
1110 session.before_lines = lines;
1111 session.row_min = row;
1112 session.row_max = row;
1113 }
1114}
1115
1116pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1137 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1138 ch: char,
1139) -> bool {
1140 use hjkl_buffer::{Edit, MotionKind, Position};
1141 ed.sync_buffer_content_from_textarea();
1142 let cursor = buf_cursor_pos(&ed.buffer);
1143 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1144 let in_replace = matches!(
1145 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1146 Some(InsertReason::Replace)
1147 );
1148 if in_replace && cursor.col < line_chars {
1149 ed.mutate_edit(Edit::DeleteRange {
1150 start: cursor,
1151 end: Position::new(cursor.row, cursor.col + 1),
1152 kind: MotionKind::Char,
1153 });
1154 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1155 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1156 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1157 }
1158 ed.push_buffer_cursor_to_textarea();
1159 true
1160}
1161
1162pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1165 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1166) -> bool {
1167 use hjkl_buffer::Edit;
1168 ed.sync_buffer_content_from_textarea();
1169 let cursor = buf_cursor_pos(&ed.buffer);
1170 let prev_line = buf_line(&ed.buffer, cursor.row)
1171 .unwrap_or_default()
1172 .to_string();
1173 let indent = compute_enter_indent(&ed.settings, &prev_line);
1174 let text = format!("\n{indent}");
1175 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1176 ed.push_buffer_cursor_to_textarea();
1177 true
1178}
1179
1180pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1183 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1184) -> bool {
1185 use hjkl_buffer::Edit;
1186 ed.sync_buffer_content_from_textarea();
1187 let cursor = buf_cursor_pos(&ed.buffer);
1188 if ed.settings.expandtab {
1189 let sts = ed.settings.softtabstop;
1190 let n = if sts > 0 {
1191 sts - (cursor.col % sts)
1192 } else {
1193 ed.settings.tabstop.max(1)
1194 };
1195 ed.mutate_edit(Edit::InsertStr {
1196 at: cursor,
1197 text: " ".repeat(n),
1198 });
1199 } else {
1200 ed.mutate_edit(Edit::InsertChar {
1201 at: cursor,
1202 ch: '\t',
1203 });
1204 }
1205 ed.push_buffer_cursor_to_textarea();
1206 true
1207}
1208
1209pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1215 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1216) -> bool {
1217 use hjkl_buffer::{Edit, MotionKind, Position};
1218 ed.sync_buffer_content_from_textarea();
1219 let cursor = buf_cursor_pos(&ed.buffer);
1220 let sts = ed.settings.softtabstop;
1221 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1222 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1223 let chars: Vec<char> = line.chars().collect();
1224 let run_start = cursor.col - sts;
1225 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1226 ed.mutate_edit(Edit::DeleteRange {
1227 start: Position::new(cursor.row, run_start),
1228 end: cursor,
1229 kind: MotionKind::Char,
1230 });
1231 ed.push_buffer_cursor_to_textarea();
1232 return true;
1233 }
1234 }
1235 let result = if cursor.col > 0 {
1236 ed.mutate_edit(Edit::DeleteRange {
1237 start: Position::new(cursor.row, cursor.col - 1),
1238 end: cursor,
1239 kind: MotionKind::Char,
1240 });
1241 true
1242 } else if cursor.row > 0 {
1243 let prev_row = cursor.row - 1;
1244 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1245 ed.mutate_edit(Edit::JoinLines {
1246 row: prev_row,
1247 count: 1,
1248 with_space: false,
1249 });
1250 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1251 true
1252 } else {
1253 false
1254 };
1255 ed.push_buffer_cursor_to_textarea();
1256 result
1257}
1258
1259pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
1262 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1263) -> bool {
1264 use hjkl_buffer::{Edit, MotionKind, Position};
1265 ed.sync_buffer_content_from_textarea();
1266 let cursor = buf_cursor_pos(&ed.buffer);
1267 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1268 let result = if cursor.col < line_chars {
1269 ed.mutate_edit(Edit::DeleteRange {
1270 start: cursor,
1271 end: Position::new(cursor.row, cursor.col + 1),
1272 kind: MotionKind::Char,
1273 });
1274 buf_set_cursor_pos(&mut ed.buffer, cursor);
1275 true
1276 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1277 ed.mutate_edit(Edit::JoinLines {
1278 row: cursor.row,
1279 count: 1,
1280 with_space: false,
1281 });
1282 buf_set_cursor_pos(&mut ed.buffer, cursor);
1283 true
1284 } else {
1285 false
1286 };
1287 ed.push_buffer_cursor_to_textarea();
1288 result
1289}
1290
1291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1293pub enum InsertDir {
1294 Left,
1295 Right,
1296 Up,
1297 Down,
1298}
1299
1300pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
1303 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1304 dir: InsertDir,
1305) -> bool {
1306 ed.sync_buffer_content_from_textarea();
1307 match dir {
1308 InsertDir::Left => {
1309 crate::motions::move_left(&mut ed.buffer, 1);
1310 }
1311 InsertDir::Right => {
1312 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1313 }
1314 InsertDir::Up => {
1315 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1316 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1317 }
1318 InsertDir::Down => {
1319 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1320 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1321 }
1322 }
1323 break_undo_group_in_insert(ed);
1324 ed.push_buffer_cursor_to_textarea();
1325 false
1326}
1327
1328pub(crate) fn insert_home_bridge<H: crate::types::Host>(
1331 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1332) -> bool {
1333 ed.sync_buffer_content_from_textarea();
1334 crate::motions::move_line_start(&mut ed.buffer);
1335 break_undo_group_in_insert(ed);
1336 ed.push_buffer_cursor_to_textarea();
1337 false
1338}
1339
1340pub(crate) fn insert_end_bridge<H: crate::types::Host>(
1343 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1344) -> bool {
1345 ed.sync_buffer_content_from_textarea();
1346 crate::motions::move_line_end(&mut ed.buffer);
1347 break_undo_group_in_insert(ed);
1348 ed.push_buffer_cursor_to_textarea();
1349 false
1350}
1351
1352pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
1355 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1356 viewport_h: u16,
1357) -> bool {
1358 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1359 scroll_cursor_rows(ed, -rows);
1360 false
1361}
1362
1363pub(crate) fn insert_pagedown_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_ctrl_w_bridge<H: crate::types::Host>(
1378 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1379) -> bool {
1380 use hjkl_buffer::{Edit, MotionKind};
1381 ed.sync_buffer_content_from_textarea();
1382 let cursor = buf_cursor_pos(&ed.buffer);
1383 if cursor.row == 0 && cursor.col == 0 {
1384 return true;
1385 }
1386 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1387 let word_start = buf_cursor_pos(&ed.buffer);
1388 if word_start == cursor {
1389 return true;
1390 }
1391 buf_set_cursor_pos(&mut ed.buffer, cursor);
1392 ed.mutate_edit(Edit::DeleteRange {
1393 start: word_start,
1394 end: cursor,
1395 kind: MotionKind::Char,
1396 });
1397 ed.push_buffer_cursor_to_textarea();
1398 true
1399}
1400
1401pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
1404 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1405) -> bool {
1406 use hjkl_buffer::{Edit, MotionKind, Position};
1407 ed.sync_buffer_content_from_textarea();
1408 let cursor = buf_cursor_pos(&ed.buffer);
1409 if cursor.col > 0 {
1410 ed.mutate_edit(Edit::DeleteRange {
1411 start: Position::new(cursor.row, 0),
1412 end: cursor,
1413 kind: MotionKind::Char,
1414 });
1415 ed.push_buffer_cursor_to_textarea();
1416 }
1417 true
1418}
1419
1420pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
1424 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1425) -> bool {
1426 use hjkl_buffer::{Edit, MotionKind, Position};
1427 ed.sync_buffer_content_from_textarea();
1428 let cursor = buf_cursor_pos(&ed.buffer);
1429 if cursor.col > 0 {
1430 ed.mutate_edit(Edit::DeleteRange {
1431 start: Position::new(cursor.row, cursor.col - 1),
1432 end: cursor,
1433 kind: MotionKind::Char,
1434 });
1435 } else if cursor.row > 0 {
1436 let prev_row = cursor.row - 1;
1437 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1438 ed.mutate_edit(Edit::JoinLines {
1439 row: prev_row,
1440 count: 1,
1441 with_space: false,
1442 });
1443 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1444 }
1445 ed.push_buffer_cursor_to_textarea();
1446 true
1447}
1448
1449pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
1452 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1453) -> bool {
1454 let (row, col) = ed.cursor();
1455 let sw = ed.settings().shiftwidth;
1456 indent_rows(ed, row, row, 1);
1457 ed.jump_cursor(row, col + sw);
1458 true
1459}
1460
1461pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
1464 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1465) -> bool {
1466 let (row, col) = ed.cursor();
1467 let before_len = buf_line_bytes(&ed.buffer, row);
1468 outdent_rows(ed, row, row, 1);
1469 let after_len = buf_line_bytes(&ed.buffer, row);
1470 let stripped = before_len.saturating_sub(after_len);
1471 let new_col = col.saturating_sub(stripped);
1472 ed.jump_cursor(row, new_col);
1473 true
1474}
1475
1476pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
1480 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1481) -> bool {
1482 ed.vim.one_shot_normal = true;
1483 ed.vim.mode = Mode::Normal;
1484 ed.vim.current_mode = crate::VimMode::Normal;
1486 false
1487}
1488
1489pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
1493 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1494) -> bool {
1495 ed.vim.insert_pending_register = true;
1496 false
1497}
1498
1499pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
1503 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1504 reg: char,
1505) -> bool {
1506 insert_register_text(ed, reg);
1507 true
1510}
1511
1512pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
1517 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1518) -> bool {
1519 finish_insert_session(ed);
1520 ed.vim.mode = Mode::Normal;
1521 ed.vim.current_mode = crate::VimMode::Normal;
1523 let col = ed.cursor().1;
1524 ed.vim.last_insert_pos = Some(ed.cursor());
1525 if col > 0 {
1526 crate::motions::move_left(&mut ed.buffer, 1);
1527 ed.push_buffer_cursor_to_textarea();
1528 }
1529 ed.sticky_col = Some(ed.cursor().1);
1530 true
1531}
1532
1533#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1538pub enum ScrollDir {
1539 Down,
1541 Up,
1543}
1544
1545pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
1550 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1551 count: usize,
1552) {
1553 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
1554}
1555
1556pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
1558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1559 count: usize,
1560) {
1561 move_first_non_whitespace(ed);
1562 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
1563}
1564
1565pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
1567 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1568 count: usize,
1569) {
1570 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1571 ed.push_buffer_cursor_to_textarea();
1572 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
1573}
1574
1575pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
1577 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1578 count: usize,
1579) {
1580 crate::motions::move_line_end(&mut ed.buffer);
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::ShiftA));
1584}
1585
1586pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
1588 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1589 count: usize,
1590) {
1591 use hjkl_buffer::{Edit, Position};
1592 ed.push_undo();
1593 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
1594 ed.sync_buffer_content_from_textarea();
1595 let row = buf_cursor_pos(&ed.buffer).row;
1596 let line_chars = buf_line_chars(&ed.buffer, row);
1597 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
1598 let indent = compute_enter_indent(&ed.settings, &prev_line);
1599 ed.mutate_edit(Edit::InsertStr {
1600 at: Position::new(row, line_chars),
1601 text: format!("\n{indent}"),
1602 });
1603 ed.push_buffer_cursor_to_textarea();
1604}
1605
1606pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
1608 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1609 count: usize,
1610) {
1611 use hjkl_buffer::{Edit, Position};
1612 ed.push_undo();
1613 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
1614 ed.sync_buffer_content_from_textarea();
1615 let row = buf_cursor_pos(&ed.buffer).row;
1616 let indent = if row > 0 {
1617 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
1618 compute_enter_indent(&ed.settings, &above)
1619 } else {
1620 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
1621 cur.chars()
1622 .take_while(|c| *c == ' ' || *c == '\t')
1623 .collect::<String>()
1624 };
1625 ed.mutate_edit(Edit::InsertStr {
1626 at: Position::new(row, 0),
1627 text: format!("{indent}\n"),
1628 });
1629 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1630 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1631 let new_row = buf_cursor_pos(&ed.buffer).row;
1632 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
1633 ed.push_buffer_cursor_to_textarea();
1634}
1635
1636pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
1638 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1639 count: usize,
1640) {
1641 begin_insert(ed, count.max(1), InsertReason::Replace);
1642}
1643
1644pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
1649 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1650 count: usize,
1651) {
1652 do_char_delete(ed, true, count.max(1));
1653 if !ed.vim.replaying {
1654 ed.vim.last_change = Some(LastChange::CharDel {
1655 forward: true,
1656 count: count.max(1),
1657 });
1658 }
1659}
1660
1661pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
1664 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1665 count: usize,
1666) {
1667 do_char_delete(ed, false, count.max(1));
1668 if !ed.vim.replaying {
1669 ed.vim.last_change = Some(LastChange::CharDel {
1670 forward: false,
1671 count: count.max(1),
1672 });
1673 }
1674}
1675
1676pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
1679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1680 count: usize,
1681) {
1682 use hjkl_buffer::{Edit, MotionKind, Position};
1683 ed.push_undo();
1684 ed.sync_buffer_content_from_textarea();
1685 for _ in 0..count.max(1) {
1686 let cursor = buf_cursor_pos(&ed.buffer);
1687 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1688 if cursor.col >= line_chars {
1689 break;
1690 }
1691 ed.mutate_edit(Edit::DeleteRange {
1692 start: cursor,
1693 end: Position::new(cursor.row, cursor.col + 1),
1694 kind: MotionKind::Char,
1695 });
1696 }
1697 ed.push_buffer_cursor_to_textarea();
1698 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
1699 if !ed.vim.replaying {
1700 ed.vim.last_change = Some(LastChange::OpMotion {
1701 op: Operator::Change,
1702 motion: Motion::Right,
1703 count: count.max(1),
1704 inserted: None,
1705 });
1706 }
1707}
1708
1709pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
1712 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1713 count: usize,
1714) {
1715 execute_line_op(ed, Operator::Change, count.max(1));
1716 if !ed.vim.replaying {
1717 ed.vim.last_change = Some(LastChange::LineOp {
1718 op: Operator::Change,
1719 count: count.max(1),
1720 inserted: None,
1721 });
1722 }
1723}
1724
1725pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1728 ed.push_undo();
1729 delete_to_eol(ed);
1730 crate::motions::move_left(&mut ed.buffer, 1);
1731 ed.push_buffer_cursor_to_textarea();
1732 if !ed.vim.replaying {
1733 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
1734 }
1735}
1736
1737pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1740 ed.push_undo();
1741 delete_to_eol(ed);
1742 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
1743}
1744
1745pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
1747 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1748 count: usize,
1749) {
1750 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
1751}
1752
1753pub(crate) fn join_line_bridge<H: crate::types::Host>(
1756 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1757 count: usize,
1758) {
1759 for _ in 0..count.max(1) {
1760 ed.push_undo();
1761 join_line(ed);
1762 }
1763 if !ed.vim.replaying {
1764 ed.vim.last_change = Some(LastChange::JoinLine {
1765 count: count.max(1),
1766 });
1767 }
1768}
1769
1770pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
1773 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1774 count: usize,
1775) {
1776 for _ in 0..count.max(1) {
1777 ed.push_undo();
1778 toggle_case_at_cursor(ed);
1779 }
1780 if !ed.vim.replaying {
1781 ed.vim.last_change = Some(LastChange::ToggleCase {
1782 count: count.max(1),
1783 });
1784 }
1785}
1786
1787pub(crate) fn paste_after_bridge<H: crate::types::Host>(
1791 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1792 count: usize,
1793) {
1794 do_paste(ed, false, count.max(1));
1795 if !ed.vim.replaying {
1796 ed.vim.last_change = Some(LastChange::Paste {
1797 before: false,
1798 count: count.max(1),
1799 });
1800 }
1801}
1802
1803pub(crate) fn paste_before_bridge<H: crate::types::Host>(
1807 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1808 count: usize,
1809) {
1810 do_paste(ed, true, count.max(1));
1811 if !ed.vim.replaying {
1812 ed.vim.last_change = Some(LastChange::Paste {
1813 before: true,
1814 count: count.max(1),
1815 });
1816 }
1817}
1818
1819pub(crate) fn jump_back_bridge<H: crate::types::Host>(
1824 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1825 count: usize,
1826) {
1827 for _ in 0..count.max(1) {
1828 jump_back(ed);
1829 }
1830}
1831
1832pub(crate) fn jump_forward_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_forward(ed);
1840 }
1841}
1842
1843pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
1848 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1849 dir: ScrollDir,
1850 count: usize,
1851) {
1852 let rows = viewport_full_rows(ed, count) as isize;
1853 match dir {
1854 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1855 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1856 }
1857}
1858
1859pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
1862 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1863 dir: ScrollDir,
1864 count: usize,
1865) {
1866 let rows = viewport_half_rows(ed, count) as isize;
1867 match dir {
1868 ScrollDir::Down => scroll_cursor_rows(ed, rows),
1869 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
1870 }
1871}
1872
1873pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
1877 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1878 dir: ScrollDir,
1879 count: usize,
1880) {
1881 let n = count.max(1);
1882 let total = buf_row_count(&ed.buffer);
1883 let last = total.saturating_sub(1);
1884 let h = ed.viewport_height_value() as usize;
1885 let vp = ed.host().viewport();
1886 let cur_top = vp.top_row;
1887 let new_top = match dir {
1888 ScrollDir::Down => (cur_top + n).min(last),
1889 ScrollDir::Up => cur_top.saturating_sub(n),
1890 };
1891 ed.set_viewport_top(new_top);
1892 let (row, col) = ed.cursor();
1894 let bot = (new_top + h).saturating_sub(1).min(last);
1895 let clamped = row.max(new_top).min(bot);
1896 if clamped != row {
1897 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
1898 ed.push_buffer_cursor_to_textarea();
1899 }
1900}
1901
1902pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
1907 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1908 forward: bool,
1909 count: usize,
1910) {
1911 if let Some(pattern) = ed.vim.last_search.clone() {
1912 ed.push_search_pattern(&pattern);
1913 }
1914 if ed.search_state().pattern.is_none() {
1915 return;
1916 }
1917 let go_forward = ed.vim.last_search_forward == forward;
1918 for _ in 0..count.max(1) {
1919 if go_forward {
1920 ed.search_advance_forward(true);
1921 } else {
1922 ed.search_advance_backward(true);
1923 }
1924 }
1925 ed.push_buffer_cursor_to_textarea();
1926}
1927
1928pub(crate) fn word_search_bridge<H: crate::types::Host>(
1932 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1933 forward: bool,
1934 whole_word: bool,
1935 count: usize,
1936) {
1937 word_at_cursor_search(ed, forward, whole_word, count.max(1));
1938}
1939
1940#[allow(dead_code)]
1945#[inline]
1946pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1947 do_undo(ed);
1948}
1949
1950#[inline]
1965pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
1966 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1967 mode: Mode,
1968) {
1969 ed.vim.mode = mode;
1970 ed.vim.current_mode = ed.vim.public_mode();
1971}
1972
1973pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
1976 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1977) {
1978 let cur = ed.cursor();
1979 ed.vim.visual_anchor = cur;
1980 set_vim_mode_bridge(ed, Mode::Visual);
1981}
1982
1983pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
1986 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1987) {
1988 let (row, _) = ed.cursor();
1989 ed.vim.visual_line_anchor = row;
1990 set_vim_mode_bridge(ed, Mode::VisualLine);
1991}
1992
1993pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
1997 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1998) {
1999 let cur = ed.cursor();
2000 ed.vim.block_anchor = cur;
2001 ed.vim.block_vcol = cur.1;
2002 set_vim_mode_bridge(ed, Mode::VisualBlock);
2003}
2004
2005pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
2010 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2011) {
2012 let snap: Option<LastVisual> = match ed.vim.mode {
2014 Mode::Visual => Some(LastVisual {
2015 mode: Mode::Visual,
2016 anchor: ed.vim.visual_anchor,
2017 cursor: ed.cursor(),
2018 block_vcol: 0,
2019 }),
2020 Mode::VisualLine => Some(LastVisual {
2021 mode: Mode::VisualLine,
2022 anchor: (ed.vim.visual_line_anchor, 0),
2023 cursor: ed.cursor(),
2024 block_vcol: 0,
2025 }),
2026 Mode::VisualBlock => Some(LastVisual {
2027 mode: Mode::VisualBlock,
2028 anchor: ed.vim.block_anchor,
2029 cursor: ed.cursor(),
2030 block_vcol: ed.vim.block_vcol,
2031 }),
2032 _ => None,
2033 };
2034 ed.vim.pending = Pending::None;
2036 ed.vim.count = 0;
2037 ed.vim.insert_session = None;
2038 set_vim_mode_bridge(ed, Mode::Normal);
2039 if let Some(snap) = snap {
2043 let (lo, hi) = match snap.mode {
2044 Mode::Visual => {
2045 if snap.anchor <= snap.cursor {
2046 (snap.anchor, snap.cursor)
2047 } else {
2048 (snap.cursor, snap.anchor)
2049 }
2050 }
2051 Mode::VisualLine => {
2052 let r_lo = snap.anchor.0.min(snap.cursor.0);
2053 let r_hi = snap.anchor.0.max(snap.cursor.0);
2054 let last_col = ed
2055 .buffer()
2056 .lines()
2057 .get(r_hi)
2058 .map(|l| l.chars().count().saturating_sub(1))
2059 .unwrap_or(0);
2060 ((r_lo, 0), (r_hi, last_col))
2061 }
2062 Mode::VisualBlock => {
2063 let (r1, c1) = snap.anchor;
2064 let (r2, c2) = snap.cursor;
2065 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2066 }
2067 _ => {
2068 if snap.anchor <= snap.cursor {
2069 (snap.anchor, snap.cursor)
2070 } else {
2071 (snap.cursor, snap.anchor)
2072 }
2073 }
2074 };
2075 ed.set_mark('<', lo);
2076 ed.set_mark('>', hi);
2077 ed.vim.last_visual = Some(snap);
2078 }
2079}
2080
2081pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2087 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2088) {
2089 match ed.vim.mode {
2090 Mode::Visual => {
2091 let cur = ed.cursor();
2092 let anchor = ed.vim.visual_anchor;
2093 ed.vim.visual_anchor = cur;
2094 ed.jump_cursor(anchor.0, anchor.1);
2095 }
2096 Mode::VisualLine => {
2097 let cur_row = ed.cursor().0;
2098 let anchor_row = ed.vim.visual_line_anchor;
2099 ed.vim.visual_line_anchor = cur_row;
2100 ed.jump_cursor(anchor_row, 0);
2101 }
2102 Mode::VisualBlock => {
2103 let cur = ed.cursor();
2104 let anchor = ed.vim.block_anchor;
2105 ed.vim.block_anchor = cur;
2106 ed.vim.block_vcol = anchor.1;
2107 ed.jump_cursor(anchor.0, anchor.1);
2108 }
2109 _ => {}
2110 }
2111}
2112
2113pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
2117 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2118) {
2119 if let Some(snap) = ed.vim.last_visual {
2120 match snap.mode {
2121 Mode::Visual => {
2122 ed.vim.visual_anchor = snap.anchor;
2123 set_vim_mode_bridge(ed, Mode::Visual);
2124 }
2125 Mode::VisualLine => {
2126 ed.vim.visual_line_anchor = snap.anchor.0;
2127 set_vim_mode_bridge(ed, Mode::VisualLine);
2128 }
2129 Mode::VisualBlock => {
2130 ed.vim.block_anchor = snap.anchor;
2131 ed.vim.block_vcol = snap.block_vcol;
2132 set_vim_mode_bridge(ed, Mode::VisualBlock);
2133 }
2134 _ => {}
2135 }
2136 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2137 }
2138}
2139
2140pub(crate) fn set_mode_bridge<H: crate::types::Host>(
2146 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2147 mode: crate::VimMode,
2148) {
2149 let internal = match mode {
2150 crate::VimMode::Normal => Mode::Normal,
2151 crate::VimMode::Insert => Mode::Insert,
2152 crate::VimMode::Visual => Mode::Visual,
2153 crate::VimMode::VisualLine => Mode::VisualLine,
2154 crate::VimMode::VisualBlock => Mode::VisualBlock,
2155 };
2156 ed.vim.mode = internal;
2157 ed.vim.current_mode = mode;
2158}
2159
2160pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2177 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2178 ch: char,
2179) {
2180 if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
2181 let pos = ed.cursor();
2186 ed.set_mark(ch, pos);
2187 }
2188 }
2190
2191pub(crate) fn goto_mark<H: crate::types::Host>(
2200 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2201 ch: char,
2202 linewise: bool,
2203) {
2204 let target = match ch {
2205 'a'..='z' | 'A'..='Z' => ed.mark(ch),
2206 '\'' | '`' => ed.vim.jump_back.last().copied(),
2207 '.' => ed.vim.last_edit_pos,
2208 '[' | ']' | '<' | '>' => ed.mark(ch),
2209 _ => None,
2210 };
2211 let Some((row, col)) = target else {
2212 return;
2213 };
2214 let pre = ed.cursor();
2215 let (r, c_clamped) = clamp_pos(ed, (row, col));
2216 if linewise {
2217 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2218 ed.push_buffer_cursor_to_textarea();
2219 move_first_non_whitespace(ed);
2220 } else {
2221 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2222 ed.push_buffer_cursor_to_textarea();
2223 }
2224 if ed.cursor() != pre {
2225 ed.push_jump(pre);
2226 }
2227 ed.sticky_col = Some(ed.cursor().1);
2228}
2229
2230pub fn op_is_change(op: Operator) -> bool {
2234 matches!(op, Operator::Delete | Operator::Change)
2235}
2236
2237pub(crate) const JUMPLIST_MAX: usize = 100;
2241
2242fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2245 let Some(target) = ed.vim.jump_back.pop() else {
2246 return;
2247 };
2248 let cur = ed.cursor();
2249 ed.vim.jump_fwd.push(cur);
2250 let (r, c) = clamp_pos(ed, target);
2251 ed.jump_cursor(r, c);
2252 ed.sticky_col = Some(c);
2253}
2254
2255fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2258 let Some(target) = ed.vim.jump_fwd.pop() else {
2259 return;
2260 };
2261 let cur = ed.cursor();
2262 ed.vim.jump_back.push(cur);
2263 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2264 ed.vim.jump_back.remove(0);
2265 }
2266 let (r, c) = clamp_pos(ed, target);
2267 ed.jump_cursor(r, c);
2268 ed.sticky_col = Some(c);
2269}
2270
2271fn clamp_pos<H: crate::types::Host>(
2274 ed: &Editor<hjkl_buffer::Buffer, H>,
2275 pos: (usize, usize),
2276) -> (usize, usize) {
2277 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2278 let r = pos.0.min(last_row);
2279 let line_len = buf_line_chars(&ed.buffer, r);
2280 let c = pos.1.min(line_len.saturating_sub(1));
2281 (r, c)
2282}
2283
2284fn is_big_jump(motion: &Motion) -> bool {
2286 matches!(
2287 motion,
2288 Motion::FileTop
2289 | Motion::FileBottom
2290 | Motion::MatchBracket
2291 | Motion::WordAtCursor { .. }
2292 | Motion::SearchNext { .. }
2293 | Motion::ViewportTop
2294 | Motion::ViewportMiddle
2295 | Motion::ViewportBottom
2296 )
2297}
2298
2299fn viewport_half_rows<H: crate::types::Host>(
2304 ed: &Editor<hjkl_buffer::Buffer, H>,
2305 count: usize,
2306) -> usize {
2307 let h = ed.viewport_height_value() as usize;
2308 (h / 2).max(1).saturating_mul(count.max(1))
2309}
2310
2311fn viewport_full_rows<H: crate::types::Host>(
2314 ed: &Editor<hjkl_buffer::Buffer, H>,
2315 count: usize,
2316) -> usize {
2317 let h = ed.viewport_height_value() as usize;
2318 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2319}
2320
2321fn scroll_cursor_rows<H: crate::types::Host>(
2326 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2327 delta: isize,
2328) {
2329 if delta == 0 {
2330 return;
2331 }
2332 ed.sync_buffer_content_from_textarea();
2333 let (row, _) = ed.cursor();
2334 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2335 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2336 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2337 crate::motions::move_first_non_blank(&mut ed.buffer);
2338 ed.push_buffer_cursor_to_textarea();
2339 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2340}
2341
2342pub fn parse_motion(input: &Input) -> Option<Motion> {
2348 if input.ctrl {
2349 return None;
2350 }
2351 match input.key {
2352 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2353 Key::Char('l') | Key::Right => Some(Motion::Right),
2354 Key::Char('j') | Key::Down => Some(Motion::Down),
2355 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
2357 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
2359 Key::Char('_') => Some(Motion::FirstNonBlankLine),
2361 Key::Char('k') | Key::Up => Some(Motion::Up),
2362 Key::Char('w') => Some(Motion::WordFwd),
2363 Key::Char('W') => Some(Motion::BigWordFwd),
2364 Key::Char('b') => Some(Motion::WordBack),
2365 Key::Char('B') => Some(Motion::BigWordBack),
2366 Key::Char('e') => Some(Motion::WordEnd),
2367 Key::Char('E') => Some(Motion::BigWordEnd),
2368 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2369 Key::Char('^') => Some(Motion::FirstNonBlank),
2370 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2371 Key::Char('G') => Some(Motion::FileBottom),
2372 Key::Char('%') => Some(Motion::MatchBracket),
2373 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2374 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2375 Key::Char('*') => Some(Motion::WordAtCursor {
2376 forward: true,
2377 whole_word: true,
2378 }),
2379 Key::Char('#') => Some(Motion::WordAtCursor {
2380 forward: false,
2381 whole_word: true,
2382 }),
2383 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2384 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2385 Key::Char('H') => Some(Motion::ViewportTop),
2386 Key::Char('M') => Some(Motion::ViewportMiddle),
2387 Key::Char('L') => Some(Motion::ViewportBottom),
2388 Key::Char('{') => Some(Motion::ParagraphPrev),
2389 Key::Char('}') => Some(Motion::ParagraphNext),
2390 Key::Char('(') => Some(Motion::SentencePrev),
2391 Key::Char(')') => Some(Motion::SentenceNext),
2392 _ => None,
2393 }
2394}
2395
2396pub(crate) fn execute_motion<H: crate::types::Host>(
2399 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2400 motion: Motion,
2401 count: usize,
2402) {
2403 let count = count.max(1);
2404 let motion = match motion {
2406 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2407 Some((ch, forward, till)) => Motion::Find {
2408 ch,
2409 forward: if reverse { !forward } else { forward },
2410 till,
2411 },
2412 None => return,
2413 },
2414 other => other,
2415 };
2416 let pre_pos = ed.cursor();
2417 let pre_col = pre_pos.1;
2418 apply_motion_cursor(ed, &motion, count);
2419 let post_pos = ed.cursor();
2420 if is_big_jump(&motion) && pre_pos != post_pos {
2421 ed.push_jump(pre_pos);
2422 }
2423 apply_sticky_col(ed, &motion, pre_col);
2424 ed.sync_buffer_from_textarea();
2429}
2430
2431fn execute_motion_with_block_vcol<H: crate::types::Host>(
2442 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2443 motion: Motion,
2444 count: usize,
2445) {
2446 let motion_copy = motion.clone();
2447 execute_motion(ed, motion, count);
2448 if ed.vim.mode == Mode::VisualBlock {
2449 update_block_vcol(ed, &motion_copy);
2450 }
2451}
2452
2453pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2481 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2482 kind: crate::MotionKind,
2483 count: usize,
2484) {
2485 let count = count.max(1);
2486 match kind {
2487 crate::MotionKind::CharLeft => {
2488 execute_motion_with_block_vcol(ed, Motion::Left, count);
2489 }
2490 crate::MotionKind::CharRight => {
2491 execute_motion_with_block_vcol(ed, Motion::Right, count);
2492 }
2493 crate::MotionKind::LineDown => {
2494 execute_motion_with_block_vcol(ed, Motion::Down, count);
2495 }
2496 crate::MotionKind::LineUp => {
2497 execute_motion_with_block_vcol(ed, Motion::Up, count);
2498 }
2499 crate::MotionKind::FirstNonBlankDown => {
2500 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2505 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2506 crate::motions::move_first_non_blank(&mut ed.buffer);
2507 ed.push_buffer_cursor_to_textarea();
2508 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2509 ed.sync_buffer_from_textarea();
2510 }
2511 crate::MotionKind::FirstNonBlankUp => {
2512 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2515 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2516 crate::motions::move_first_non_blank(&mut ed.buffer);
2517 ed.push_buffer_cursor_to_textarea();
2518 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2519 ed.sync_buffer_from_textarea();
2520 }
2521 crate::MotionKind::WordForward => {
2522 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
2523 }
2524 crate::MotionKind::BigWordForward => {
2525 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
2526 }
2527 crate::MotionKind::WordBackward => {
2528 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
2529 }
2530 crate::MotionKind::BigWordBackward => {
2531 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
2532 }
2533 crate::MotionKind::WordEnd => {
2534 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
2535 }
2536 crate::MotionKind::BigWordEnd => {
2537 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
2538 }
2539 crate::MotionKind::LineStart => {
2540 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
2543 }
2544 crate::MotionKind::FirstNonBlank => {
2545 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
2548 }
2549 crate::MotionKind::GotoLine => {
2550 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
2559 }
2560 crate::MotionKind::LineEnd => {
2561 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
2565 }
2566 crate::MotionKind::FindRepeat => {
2567 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
2571 }
2572 crate::MotionKind::FindRepeatReverse => {
2573 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
2577 }
2578 crate::MotionKind::BracketMatch => {
2579 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
2584 }
2585 crate::MotionKind::ViewportTop => {
2586 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
2589 }
2590 crate::MotionKind::ViewportMiddle => {
2591 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
2594 }
2595 crate::MotionKind::ViewportBottom => {
2596 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
2599 }
2600 crate::MotionKind::HalfPageDown => {
2601 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
2605 }
2606 crate::MotionKind::HalfPageUp => {
2607 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
2610 }
2611 crate::MotionKind::FullPageDown => {
2612 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
2615 }
2616 crate::MotionKind::FullPageUp => {
2617 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
2620 }
2621 crate::MotionKind::FirstNonBlankLine => {
2622 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
2623 }
2624 crate::MotionKind::SectionBackward => {
2625 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
2626 }
2627 crate::MotionKind::SectionForward => {
2628 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
2629 }
2630 crate::MotionKind::SectionEndBackward => {
2631 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
2632 }
2633 crate::MotionKind::SectionEndForward => {
2634 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
2635 }
2636 }
2637}
2638
2639fn apply_sticky_col<H: crate::types::Host>(
2644 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2645 motion: &Motion,
2646 pre_col: usize,
2647) {
2648 if is_vertical_motion(motion) {
2649 let want = ed.sticky_col.unwrap_or(pre_col);
2650 ed.sticky_col = Some(want);
2653 let (row, _) = ed.cursor();
2654 let line_len = buf_line_chars(&ed.buffer, row);
2655 let max_col = line_len.saturating_sub(1);
2659 let target = want.min(max_col);
2660 buf_set_cursor_rc(&mut ed.buffer, row, target);
2664 } else {
2665 ed.sticky_col = Some(ed.cursor().1);
2668 }
2669}
2670
2671fn is_vertical_motion(motion: &Motion) -> bool {
2672 matches!(
2676 motion,
2677 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2678 )
2679}
2680
2681fn apply_motion_cursor<H: crate::types::Host>(
2682 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2683 motion: &Motion,
2684 count: usize,
2685) {
2686 apply_motion_cursor_ctx(ed, motion, count, false)
2687}
2688
2689pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
2690 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2691 motion: &Motion,
2692 count: usize,
2693 as_operator: bool,
2694) {
2695 match motion {
2696 Motion::Left => {
2697 crate::motions::move_left(&mut ed.buffer, count);
2699 ed.push_buffer_cursor_to_textarea();
2700 }
2701 Motion::Right => {
2702 if as_operator {
2706 crate::motions::move_right_to_end(&mut ed.buffer, count);
2707 } else {
2708 crate::motions::move_right_in_line(&mut ed.buffer, count);
2709 }
2710 ed.push_buffer_cursor_to_textarea();
2711 }
2712 Motion::Up => {
2713 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2717 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2718 ed.push_buffer_cursor_to_textarea();
2719 }
2720 Motion::Down => {
2721 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2722 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2723 ed.push_buffer_cursor_to_textarea();
2724 }
2725 Motion::ScreenUp => {
2726 let v = *ed.host.viewport();
2727 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2728 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2729 ed.push_buffer_cursor_to_textarea();
2730 }
2731 Motion::ScreenDown => {
2732 let v = *ed.host.viewport();
2733 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2734 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2735 ed.push_buffer_cursor_to_textarea();
2736 }
2737 Motion::WordFwd => {
2738 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2739 ed.push_buffer_cursor_to_textarea();
2740 }
2741 Motion::WordBack => {
2742 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2743 ed.push_buffer_cursor_to_textarea();
2744 }
2745 Motion::WordEnd => {
2746 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2747 ed.push_buffer_cursor_to_textarea();
2748 }
2749 Motion::BigWordFwd => {
2750 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2751 ed.push_buffer_cursor_to_textarea();
2752 }
2753 Motion::BigWordBack => {
2754 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2755 ed.push_buffer_cursor_to_textarea();
2756 }
2757 Motion::BigWordEnd => {
2758 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2759 ed.push_buffer_cursor_to_textarea();
2760 }
2761 Motion::WordEndBack => {
2762 crate::motions::move_word_end_back(
2763 &mut ed.buffer,
2764 false,
2765 count,
2766 &ed.settings.iskeyword,
2767 );
2768 ed.push_buffer_cursor_to_textarea();
2769 }
2770 Motion::BigWordEndBack => {
2771 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2772 ed.push_buffer_cursor_to_textarea();
2773 }
2774 Motion::LineStart => {
2775 crate::motions::move_line_start(&mut ed.buffer);
2776 ed.push_buffer_cursor_to_textarea();
2777 }
2778 Motion::FirstNonBlank => {
2779 crate::motions::move_first_non_blank(&mut ed.buffer);
2780 ed.push_buffer_cursor_to_textarea();
2781 }
2782 Motion::LineEnd => {
2783 crate::motions::move_line_end(&mut ed.buffer);
2785 ed.push_buffer_cursor_to_textarea();
2786 }
2787 Motion::FileTop => {
2788 if count > 1 {
2791 crate::motions::move_bottom(&mut ed.buffer, count);
2792 } else {
2793 crate::motions::move_top(&mut ed.buffer);
2794 }
2795 ed.push_buffer_cursor_to_textarea();
2796 }
2797 Motion::FileBottom => {
2798 if count > 1 {
2801 crate::motions::move_bottom(&mut ed.buffer, count);
2802 } else {
2803 crate::motions::move_bottom(&mut ed.buffer, 0);
2804 }
2805 ed.push_buffer_cursor_to_textarea();
2806 }
2807 Motion::Find { ch, forward, till } => {
2808 for _ in 0..count {
2809 if !find_char_on_line(ed, *ch, *forward, *till) {
2810 break;
2811 }
2812 }
2813 }
2814 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2816 let _ = matching_bracket(ed);
2817 }
2818 Motion::WordAtCursor {
2819 forward,
2820 whole_word,
2821 } => {
2822 word_at_cursor_search(ed, *forward, *whole_word, count);
2823 }
2824 Motion::SearchNext { reverse } => {
2825 if let Some(pattern) = ed.vim.last_search.clone() {
2829 ed.push_search_pattern(&pattern);
2830 }
2831 if ed.search_state().pattern.is_none() {
2832 return;
2833 }
2834 let forward = ed.vim.last_search_forward != *reverse;
2838 for _ in 0..count.max(1) {
2839 if forward {
2840 ed.search_advance_forward(true);
2841 } else {
2842 ed.search_advance_backward(true);
2843 }
2844 }
2845 ed.push_buffer_cursor_to_textarea();
2846 }
2847 Motion::ViewportTop => {
2848 let v = *ed.host().viewport();
2849 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2850 ed.push_buffer_cursor_to_textarea();
2851 }
2852 Motion::ViewportMiddle => {
2853 let v = *ed.host().viewport();
2854 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2855 ed.push_buffer_cursor_to_textarea();
2856 }
2857 Motion::ViewportBottom => {
2858 let v = *ed.host().viewport();
2859 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2860 ed.push_buffer_cursor_to_textarea();
2861 }
2862 Motion::LastNonBlank => {
2863 crate::motions::move_last_non_blank(&mut ed.buffer);
2864 ed.push_buffer_cursor_to_textarea();
2865 }
2866 Motion::LineMiddle => {
2867 let row = ed.cursor().0;
2868 let line_chars = buf_line_chars(&ed.buffer, row);
2869 let target = line_chars / 2;
2872 ed.jump_cursor(row, target);
2873 }
2874 Motion::ParagraphPrev => {
2875 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2876 ed.push_buffer_cursor_to_textarea();
2877 }
2878 Motion::ParagraphNext => {
2879 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2880 ed.push_buffer_cursor_to_textarea();
2881 }
2882 Motion::SentencePrev => {
2883 for _ in 0..count.max(1) {
2884 if let Some((row, col)) = sentence_boundary(ed, false) {
2885 ed.jump_cursor(row, col);
2886 }
2887 }
2888 }
2889 Motion::SentenceNext => {
2890 for _ in 0..count.max(1) {
2891 if let Some((row, col)) = sentence_boundary(ed, true) {
2892 ed.jump_cursor(row, col);
2893 }
2894 }
2895 }
2896 Motion::SectionBackward => {
2897 crate::motions::move_section_backward(&mut ed.buffer, count);
2898 ed.push_buffer_cursor_to_textarea();
2899 }
2900 Motion::SectionForward => {
2901 crate::motions::move_section_forward(&mut ed.buffer, count);
2902 ed.push_buffer_cursor_to_textarea();
2903 }
2904 Motion::SectionEndBackward => {
2905 crate::motions::move_section_end_backward(&mut ed.buffer, count);
2906 ed.push_buffer_cursor_to_textarea();
2907 }
2908 Motion::SectionEndForward => {
2909 crate::motions::move_section_end_forward(&mut ed.buffer, count);
2910 ed.push_buffer_cursor_to_textarea();
2911 }
2912 Motion::FirstNonBlankNextLine => {
2913 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
2914 ed.push_buffer_cursor_to_textarea();
2915 }
2916 Motion::FirstNonBlankPrevLine => {
2917 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
2918 ed.push_buffer_cursor_to_textarea();
2919 }
2920 Motion::FirstNonBlankLine => {
2921 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
2922 ed.push_buffer_cursor_to_textarea();
2923 }
2924 }
2925}
2926
2927fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2928 ed.sync_buffer_content_from_textarea();
2934 crate::motions::move_first_non_blank(&mut ed.buffer);
2935 ed.push_buffer_cursor_to_textarea();
2936}
2937
2938fn find_char_on_line<H: crate::types::Host>(
2939 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2940 ch: char,
2941 forward: bool,
2942 till: bool,
2943) -> bool {
2944 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2945 if moved {
2946 ed.push_buffer_cursor_to_textarea();
2947 }
2948 moved
2949}
2950
2951fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2952 let moved = crate::motions::match_bracket(&mut ed.buffer);
2953 if moved {
2954 ed.push_buffer_cursor_to_textarea();
2955 }
2956 moved
2957}
2958
2959fn word_at_cursor_search<H: crate::types::Host>(
2960 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2961 forward: bool,
2962 whole_word: bool,
2963 count: usize,
2964) {
2965 let (row, col) = ed.cursor();
2966 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
2967 let chars: Vec<char> = line.chars().collect();
2968 if chars.is_empty() {
2969 return;
2970 }
2971 let spec = ed.settings().iskeyword.clone();
2973 let is_word = |c: char| is_keyword_char(c, &spec);
2974 let mut start = col.min(chars.len().saturating_sub(1));
2975 while start > 0 && is_word(chars[start - 1]) {
2976 start -= 1;
2977 }
2978 let mut end = start;
2979 while end < chars.len() && is_word(chars[end]) {
2980 end += 1;
2981 }
2982 if end <= start {
2983 return;
2984 }
2985 let word: String = chars[start..end].iter().collect();
2986 let escaped = regex_escape(&word);
2987 let pattern = if whole_word {
2988 format!(r"\b{escaped}\b")
2989 } else {
2990 escaped
2991 };
2992 ed.push_search_pattern(&pattern);
2993 if ed.search_state().pattern.is_none() {
2994 return;
2995 }
2996 ed.vim.last_search = Some(pattern);
2998 ed.vim.last_search_forward = forward;
2999 for _ in 0..count.max(1) {
3000 if forward {
3001 ed.search_advance_forward(true);
3002 } else {
3003 ed.search_advance_backward(true);
3004 }
3005 }
3006 ed.push_buffer_cursor_to_textarea();
3007}
3008
3009fn regex_escape(s: &str) -> String {
3010 let mut out = String::with_capacity(s.len());
3011 for c in s.chars() {
3012 if matches!(
3013 c,
3014 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
3015 ) {
3016 out.push('\\');
3017 }
3018 out.push(c);
3019 }
3020 out
3021}
3022
3023pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
3037 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3038 op: Operator,
3039 motion_key: char,
3040 total_count: usize,
3041) {
3042 let input = Input {
3043 key: Key::Char(motion_key),
3044 ctrl: false,
3045 alt: false,
3046 shift: false,
3047 };
3048 let Some(motion) = parse_motion(&input) else {
3049 return;
3050 };
3051 let motion = match motion {
3052 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3053 Some((ch, forward, till)) => Motion::Find {
3054 ch,
3055 forward: if reverse { !forward } else { forward },
3056 till,
3057 },
3058 None => return,
3059 },
3060 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3062 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3063 m => m,
3064 };
3065 apply_op_with_motion(ed, op, &motion, total_count);
3066 if let Motion::Find { ch, forward, till } = &motion {
3067 ed.vim.last_find = Some((*ch, *forward, *till));
3068 }
3069 if !ed.vim.replaying && op_is_change(op) {
3070 ed.vim.last_change = Some(LastChange::OpMotion {
3071 op,
3072 motion,
3073 count: total_count,
3074 inserted: None,
3075 });
3076 }
3077}
3078
3079pub(crate) fn apply_op_double<H: crate::types::Host>(
3082 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3083 op: Operator,
3084 total_count: usize,
3085) {
3086 execute_line_op(ed, op, total_count);
3087 if !ed.vim.replaying {
3088 ed.vim.last_change = Some(LastChange::LineOp {
3089 op,
3090 count: total_count,
3091 inserted: None,
3092 });
3093 }
3094}
3095
3096pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3106 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3107 op: Operator,
3108 ch: char,
3109 total_count: usize,
3110) {
3111 if matches!(
3114 op,
3115 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3116 ) {
3117 let op_char = match op {
3118 Operator::Uppercase => 'U',
3119 Operator::Lowercase => 'u',
3120 Operator::ToggleCase => '~',
3121 _ => unreachable!(),
3122 };
3123 if ch == op_char {
3124 execute_line_op(ed, op, total_count);
3125 if !ed.vim.replaying {
3126 ed.vim.last_change = Some(LastChange::LineOp {
3127 op,
3128 count: total_count,
3129 inserted: None,
3130 });
3131 }
3132 return;
3133 }
3134 }
3135 let motion = match ch {
3136 'g' => Motion::FileTop,
3137 'e' => Motion::WordEndBack,
3138 'E' => Motion::BigWordEndBack,
3139 'j' => Motion::ScreenDown,
3140 'k' => Motion::ScreenUp,
3141 _ => return, };
3143 apply_op_with_motion(ed, op, &motion, total_count);
3144 if !ed.vim.replaying && op_is_change(op) {
3145 ed.vim.last_change = Some(LastChange::OpMotion {
3146 op,
3147 motion,
3148 count: total_count,
3149 inserted: None,
3150 });
3151 }
3152}
3153
3154pub(crate) fn apply_after_g<H: crate::types::Host>(
3159 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3160 ch: char,
3161 count: usize,
3162) {
3163 match ch {
3164 'g' => {
3165 let pre = ed.cursor();
3167 if count > 1 {
3168 ed.jump_cursor(count - 1, 0);
3169 } else {
3170 ed.jump_cursor(0, 0);
3171 }
3172 move_first_non_whitespace(ed);
3173 ed.sticky_col = Some(ed.cursor().1);
3176 if ed.cursor() != pre {
3177 ed.push_jump(pre);
3178 }
3179 }
3180 'e' => execute_motion(ed, Motion::WordEndBack, count),
3181 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3182 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3184 'M' => execute_motion(ed, Motion::LineMiddle, count),
3186 'v' => ed.reenter_last_visual(),
3189 'j' => execute_motion(ed, Motion::ScreenDown, count),
3193 'k' => execute_motion(ed, Motion::ScreenUp, count),
3194 'U' => {
3198 ed.vim.pending = Pending::Op {
3199 op: Operator::Uppercase,
3200 count1: count,
3201 };
3202 }
3203 'u' => {
3204 ed.vim.pending = Pending::Op {
3205 op: Operator::Lowercase,
3206 count1: count,
3207 };
3208 }
3209 '~' => {
3210 ed.vim.pending = Pending::Op {
3211 op: Operator::ToggleCase,
3212 count1: count,
3213 };
3214 }
3215 'q' => {
3216 ed.vim.pending = Pending::Op {
3219 op: Operator::Reflow,
3220 count1: count,
3221 };
3222 }
3223 'J' => {
3224 for _ in 0..count.max(1) {
3226 ed.push_undo();
3227 join_line_raw(ed);
3228 }
3229 if !ed.vim.replaying {
3230 ed.vim.last_change = Some(LastChange::JoinLine {
3231 count: count.max(1),
3232 });
3233 }
3234 }
3235 'd' => {
3236 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3241 }
3242 'i' => {
3247 if let Some((row, col)) = ed.vim.last_insert_pos {
3248 ed.jump_cursor(row, col);
3249 }
3250 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3251 }
3252 ';' => walk_change_list(ed, -1, count.max(1)),
3255 ',' => walk_change_list(ed, 1, count.max(1)),
3256 '*' => execute_motion(
3260 ed,
3261 Motion::WordAtCursor {
3262 forward: true,
3263 whole_word: false,
3264 },
3265 count,
3266 ),
3267 '#' => execute_motion(
3268 ed,
3269 Motion::WordAtCursor {
3270 forward: false,
3271 whole_word: false,
3272 },
3273 count,
3274 ),
3275 _ => {}
3276 }
3277}
3278
3279pub(crate) fn apply_after_z<H: crate::types::Host>(
3284 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3285 ch: char,
3286 count: usize,
3287) {
3288 use crate::editor::CursorScrollTarget;
3289 let row = ed.cursor().0;
3290 match ch {
3291 'z' => {
3292 ed.scroll_cursor_to(CursorScrollTarget::Center);
3293 ed.vim.viewport_pinned = true;
3294 }
3295 't' => {
3296 ed.scroll_cursor_to(CursorScrollTarget::Top);
3297 ed.vim.viewport_pinned = true;
3298 }
3299 'b' => {
3300 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3301 ed.vim.viewport_pinned = true;
3302 }
3303 'o' => {
3308 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3309 }
3310 'c' => {
3311 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3312 }
3313 'a' => {
3314 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3315 }
3316 'R' => {
3317 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3318 }
3319 'M' => {
3320 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3321 }
3322 'E' => {
3323 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3324 }
3325 'd' => {
3326 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3327 }
3328 'f' => {
3329 if matches!(
3330 ed.vim.mode,
3331 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3332 ) {
3333 let anchor_row = match ed.vim.mode {
3336 Mode::VisualLine => ed.vim.visual_line_anchor,
3337 Mode::VisualBlock => ed.vim.block_anchor.0,
3338 _ => ed.vim.visual_anchor.0,
3339 };
3340 let cur = ed.cursor().0;
3341 let top = anchor_row.min(cur);
3342 let bot = anchor_row.max(cur);
3343 ed.apply_fold_op(crate::types::FoldOp::Add {
3344 start_row: top,
3345 end_row: bot,
3346 closed: true,
3347 });
3348 ed.vim.mode = Mode::Normal;
3349 } else {
3350 ed.vim.pending = Pending::Op {
3355 op: Operator::Fold,
3356 count1: count,
3357 };
3358 }
3359 }
3360 _ => {}
3361 }
3362}
3363
3364pub(crate) fn apply_find_char<H: crate::types::Host>(
3370 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3371 ch: char,
3372 forward: bool,
3373 till: bool,
3374 count: usize,
3375) {
3376 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3377 ed.vim.last_find = Some((ch, forward, till));
3378}
3379
3380pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3386 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3387 op: Operator,
3388 ch: char,
3389 forward: bool,
3390 till: bool,
3391 total_count: usize,
3392) {
3393 let motion = Motion::Find { ch, forward, till };
3394 apply_op_with_motion(ed, op, &motion, total_count);
3395 ed.vim.last_find = Some((ch, forward, till));
3396 if !ed.vim.replaying && op_is_change(op) {
3397 ed.vim.last_change = Some(LastChange::OpMotion {
3398 op,
3399 motion,
3400 count: total_count,
3401 inserted: None,
3402 });
3403 }
3404}
3405
3406pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3415 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3416 op: Operator,
3417 ch: char,
3418 inner: bool,
3419 _total_count: usize,
3420) -> bool {
3421 let obj = match ch {
3424 'w' => TextObject::Word { big: false },
3425 'W' => TextObject::Word { big: true },
3426 '"' | '\'' | '`' => TextObject::Quote(ch),
3427 '(' | ')' | 'b' => TextObject::Bracket('('),
3428 '[' | ']' => TextObject::Bracket('['),
3429 '{' | '}' | 'B' => TextObject::Bracket('{'),
3430 '<' | '>' => TextObject::Bracket('<'),
3431 'p' => TextObject::Paragraph,
3432 't' => TextObject::XmlTag,
3433 's' => TextObject::Sentence,
3434 _ => return false,
3435 };
3436 apply_op_with_text_object(ed, op, obj, inner);
3437 if !ed.vim.replaying && op_is_change(op) {
3438 ed.vim.last_change = Some(LastChange::OpTextObj {
3439 op,
3440 obj,
3441 inner,
3442 inserted: None,
3443 });
3444 }
3445 true
3446}
3447
3448pub(crate) fn retreat_one<H: crate::types::Host>(
3450 ed: &Editor<hjkl_buffer::Buffer, H>,
3451 pos: (usize, usize),
3452) -> (usize, usize) {
3453 let (r, c) = pos;
3454 if c > 0 {
3455 (r, c - 1)
3456 } else if r > 0 {
3457 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3458 (r - 1, prev_len)
3459 } else {
3460 (0, 0)
3461 }
3462}
3463
3464fn begin_insert_noundo<H: crate::types::Host>(
3466 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3467 count: usize,
3468 reason: InsertReason,
3469) {
3470 let reason = if ed.vim.replaying {
3471 InsertReason::ReplayOnly
3472 } else {
3473 reason
3474 };
3475 let (row, _) = ed.cursor();
3476 ed.vim.insert_session = Some(InsertSession {
3477 count,
3478 row_min: row,
3479 row_max: row,
3480 before_lines: buf_lines_to_vec(&ed.buffer),
3481 reason,
3482 });
3483 ed.vim.mode = Mode::Insert;
3484 ed.vim.current_mode = crate::VimMode::Insert;
3486}
3487
3488pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
3491 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3492 op: Operator,
3493 motion: &Motion,
3494 count: usize,
3495) {
3496 let start = ed.cursor();
3497 apply_motion_cursor_ctx(ed, motion, count, true);
3502 let end = ed.cursor();
3503 let kind = motion_kind(motion);
3504 ed.jump_cursor(start.0, start.1);
3506 run_operator_over_range(ed, op, start, end, kind);
3507}
3508
3509fn apply_op_with_text_object<H: crate::types::Host>(
3510 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3511 op: Operator,
3512 obj: TextObject,
3513 inner: bool,
3514) {
3515 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3516 return;
3517 };
3518 ed.jump_cursor(start.0, start.1);
3519 run_operator_over_range(ed, op, start, end, kind);
3520}
3521
3522fn motion_kind(motion: &Motion) -> RangeKind {
3523 match motion {
3524 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
3525 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
3526 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3527 RangeKind::Linewise
3528 }
3529 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3530 RangeKind::Inclusive
3531 }
3532 Motion::Find { .. } => RangeKind::Inclusive,
3533 Motion::MatchBracket => RangeKind::Inclusive,
3534 Motion::LineEnd => RangeKind::Inclusive,
3536 Motion::FirstNonBlankNextLine
3538 | Motion::FirstNonBlankPrevLine
3539 | Motion::FirstNonBlankLine => RangeKind::Linewise,
3540 Motion::SectionBackward
3542 | Motion::SectionForward
3543 | Motion::SectionEndBackward
3544 | Motion::SectionEndForward => RangeKind::Exclusive,
3545 _ => RangeKind::Exclusive,
3546 }
3547}
3548
3549fn run_operator_over_range<H: crate::types::Host>(
3550 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3551 op: Operator,
3552 start: (usize, usize),
3553 end: (usize, usize),
3554 kind: RangeKind,
3555) {
3556 let (top, bot) = order(start, end);
3557 if top == bot && !matches!(kind, RangeKind::Linewise) {
3561 return;
3562 }
3563
3564 match op {
3565 Operator::Yank => {
3566 let text = read_vim_range(ed, top, bot, kind);
3567 if !text.is_empty() {
3568 ed.record_yank_to_host(text.clone());
3569 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
3570 }
3571 let rbr = match kind {
3575 RangeKind::Linewise => {
3576 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3577 (bot.0, last_col)
3578 }
3579 RangeKind::Inclusive => (bot.0, bot.1),
3580 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3581 };
3582 ed.set_mark('[', top);
3583 ed.set_mark(']', rbr);
3584 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3585 ed.push_buffer_cursor_to_textarea();
3586 }
3587 Operator::Delete => {
3588 ed.push_undo();
3589 cut_vim_range(ed, top, bot, kind);
3590 if !matches!(kind, RangeKind::Linewise) {
3595 clamp_cursor_to_normal_mode(ed);
3596 }
3597 ed.vim.mode = Mode::Normal;
3598 let pos = ed.cursor();
3602 ed.set_mark('[', pos);
3603 ed.set_mark(']', pos);
3604 }
3605 Operator::Change => {
3606 ed.vim.change_mark_start = Some(top);
3611 ed.push_undo();
3612 cut_vim_range(ed, top, bot, kind);
3613 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3614 }
3615 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3616 apply_case_op_to_selection(ed, op, top, bot, kind);
3617 }
3618 Operator::Indent | Operator::Outdent => {
3619 ed.push_undo();
3622 if op == Operator::Indent {
3623 indent_rows(ed, top.0, bot.0, 1);
3624 } else {
3625 outdent_rows(ed, top.0, bot.0, 1);
3626 }
3627 ed.vim.mode = Mode::Normal;
3628 }
3629 Operator::Fold => {
3630 if bot.0 >= top.0 {
3634 ed.apply_fold_op(crate::types::FoldOp::Add {
3635 start_row: top.0,
3636 end_row: bot.0,
3637 closed: true,
3638 });
3639 }
3640 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3641 ed.push_buffer_cursor_to_textarea();
3642 ed.vim.mode = Mode::Normal;
3643 }
3644 Operator::Reflow => {
3645 ed.push_undo();
3646 reflow_rows(ed, top.0, bot.0);
3647 ed.vim.mode = Mode::Normal;
3648 }
3649 Operator::AutoIndent => {
3650 ed.push_undo();
3652 auto_indent_rows(ed, top.0, bot.0);
3653 ed.vim.mode = Mode::Normal;
3654 }
3655 }
3656}
3657
3658pub(crate) fn delete_range_bridge<H: crate::types::Host>(
3675 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3676 start: (usize, usize),
3677 end: (usize, usize),
3678 kind: RangeKind,
3679 register: char,
3680) {
3681 ed.vim.pending_register = Some(register);
3682 run_operator_over_range(ed, Operator::Delete, start, end, kind);
3683}
3684
3685pub(crate) fn yank_range_bridge<H: crate::types::Host>(
3688 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3689 start: (usize, usize),
3690 end: (usize, usize),
3691 kind: RangeKind,
3692 register: char,
3693) {
3694 ed.vim.pending_register = Some(register);
3695 run_operator_over_range(ed, Operator::Yank, start, end, kind);
3696}
3697
3698pub(crate) fn change_range_bridge<H: crate::types::Host>(
3703 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3704 start: (usize, usize),
3705 end: (usize, usize),
3706 kind: RangeKind,
3707 register: char,
3708) {
3709 ed.vim.pending_register = Some(register);
3710 run_operator_over_range(ed, Operator::Change, start, end, kind);
3711}
3712
3713pub(crate) fn indent_range_bridge<H: crate::types::Host>(
3718 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3719 start: (usize, usize),
3720 end: (usize, usize),
3721 count: i32,
3722 shiftwidth: u32,
3723) {
3724 if count == 0 {
3725 return;
3726 }
3727 let (top_row, bot_row) = if start.0 <= end.0 {
3728 (start.0, end.0)
3729 } else {
3730 (end.0, start.0)
3731 };
3732 let original_sw = ed.settings().shiftwidth;
3734 if shiftwidth > 0 {
3735 ed.settings_mut().shiftwidth = shiftwidth as usize;
3736 }
3737 ed.push_undo();
3738 let abs_count = count.unsigned_abs() as usize;
3739 if count > 0 {
3740 indent_rows(ed, top_row, bot_row, abs_count);
3741 } else {
3742 outdent_rows(ed, top_row, bot_row, abs_count);
3743 }
3744 if shiftwidth > 0 {
3745 ed.settings_mut().shiftwidth = original_sw;
3746 }
3747 ed.vim.mode = Mode::Normal;
3748}
3749
3750pub(crate) fn case_range_bridge<H: crate::types::Host>(
3754 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3755 start: (usize, usize),
3756 end: (usize, usize),
3757 kind: RangeKind,
3758 op: Operator,
3759) {
3760 match op {
3761 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
3762 _ => return,
3763 }
3764 let (top, bot) = order(start, end);
3765 apply_case_op_to_selection(ed, op, top, bot, kind);
3766}
3767
3768pub(crate) fn delete_block_bridge<H: crate::types::Host>(
3789 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3790 top_row: usize,
3791 bot_row: usize,
3792 left_col: usize,
3793 right_col: usize,
3794 register: char,
3795) {
3796 ed.vim.pending_register = Some(register);
3797 let saved_anchor = ed.vim.block_anchor;
3798 let saved_vcol = ed.vim.block_vcol;
3799 ed.vim.block_anchor = (top_row, left_col);
3800 ed.vim.block_vcol = right_col;
3801 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3803 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3805 apply_block_operator(ed, Operator::Delete);
3806 ed.vim.block_anchor = saved_anchor;
3810 ed.vim.block_vcol = saved_vcol;
3811}
3812
3813pub(crate) fn yank_block_bridge<H: crate::types::Host>(
3815 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3816 top_row: usize,
3817 bot_row: usize,
3818 left_col: usize,
3819 right_col: usize,
3820 register: char,
3821) {
3822 ed.vim.pending_register = Some(register);
3823 let saved_anchor = ed.vim.block_anchor;
3824 let saved_vcol = ed.vim.block_vcol;
3825 ed.vim.block_anchor = (top_row, left_col);
3826 ed.vim.block_vcol = right_col;
3827 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3828 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3829 apply_block_operator(ed, Operator::Yank);
3830 ed.vim.block_anchor = saved_anchor;
3831 ed.vim.block_vcol = saved_vcol;
3832}
3833
3834pub(crate) fn change_block_bridge<H: crate::types::Host>(
3837 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3838 top_row: usize,
3839 bot_row: usize,
3840 left_col: usize,
3841 right_col: usize,
3842 register: char,
3843) {
3844 ed.vim.pending_register = Some(register);
3845 let saved_anchor = ed.vim.block_anchor;
3846 let saved_vcol = ed.vim.block_vcol;
3847 ed.vim.block_anchor = (top_row, left_col);
3848 ed.vim.block_vcol = right_col;
3849 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
3850 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
3851 apply_block_operator(ed, Operator::Change);
3852 ed.vim.block_anchor = saved_anchor;
3853 ed.vim.block_vcol = saved_vcol;
3854}
3855
3856pub(crate) fn indent_block_bridge<H: crate::types::Host>(
3860 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3861 top_row: usize,
3862 bot_row: usize,
3863 count: i32,
3864) {
3865 if count == 0 {
3866 return;
3867 }
3868 ed.push_undo();
3869 let abs = count.unsigned_abs() as usize;
3870 if count > 0 {
3871 indent_rows(ed, top_row, bot_row, abs);
3872 } else {
3873 outdent_rows(ed, top_row, bot_row, abs);
3874 }
3875 ed.vim.mode = Mode::Normal;
3876}
3877
3878pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
3882 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3883 start: (usize, usize),
3884 end: (usize, usize),
3885) {
3886 let (top_row, bot_row) = if start.0 <= end.0 {
3887 (start.0, end.0)
3888 } else {
3889 (end.0, start.0)
3890 };
3891 ed.push_undo();
3892 auto_indent_rows(ed, top_row, bot_row);
3893 ed.vim.mode = Mode::Normal;
3894}
3895
3896pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
3907 ed: &Editor<hjkl_buffer::Buffer, H>,
3908) -> Option<((usize, usize), (usize, usize))> {
3909 word_text_object(ed, true, false)
3910}
3911
3912pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
3915 ed: &Editor<hjkl_buffer::Buffer, H>,
3916) -> Option<((usize, usize), (usize, usize))> {
3917 word_text_object(ed, false, false)
3918}
3919
3920pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
3923 ed: &Editor<hjkl_buffer::Buffer, H>,
3924) -> Option<((usize, usize), (usize, usize))> {
3925 word_text_object(ed, true, true)
3926}
3927
3928pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
3931 ed: &Editor<hjkl_buffer::Buffer, H>,
3932) -> Option<((usize, usize), (usize, usize))> {
3933 word_text_object(ed, false, true)
3934}
3935
3936pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
3952 ed: &Editor<hjkl_buffer::Buffer, H>,
3953 quote: char,
3954) -> Option<((usize, usize), (usize, usize))> {
3955 quote_text_object(ed, quote, true)
3956}
3957
3958pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
3961 ed: &Editor<hjkl_buffer::Buffer, H>,
3962 quote: char,
3963) -> Option<((usize, usize), (usize, usize))> {
3964 quote_text_object(ed, quote, false)
3965}
3966
3967pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
3975 ed: &Editor<hjkl_buffer::Buffer, H>,
3976 open: char,
3977) -> Option<((usize, usize), (usize, usize))> {
3978 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
3979}
3980
3981pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
3985 ed: &Editor<hjkl_buffer::Buffer, H>,
3986 open: char,
3987) -> Option<((usize, usize), (usize, usize))> {
3988 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
3989}
3990
3991pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
3996 ed: &Editor<hjkl_buffer::Buffer, H>,
3997) -> Option<((usize, usize), (usize, usize))> {
3998 sentence_text_object(ed, true)
3999}
4000
4001pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
4004 ed: &Editor<hjkl_buffer::Buffer, H>,
4005) -> Option<((usize, usize), (usize, usize))> {
4006 sentence_text_object(ed, false)
4007}
4008
4009pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
4014 ed: &Editor<hjkl_buffer::Buffer, H>,
4015) -> Option<((usize, usize), (usize, usize))> {
4016 paragraph_text_object(ed, true)
4017}
4018
4019pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
4022 ed: &Editor<hjkl_buffer::Buffer, H>,
4023) -> Option<((usize, usize), (usize, usize))> {
4024 paragraph_text_object(ed, false)
4025}
4026
4027pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
4033 ed: &Editor<hjkl_buffer::Buffer, H>,
4034) -> Option<((usize, usize), (usize, usize))> {
4035 tag_text_object(ed, true)
4036}
4037
4038pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
4041 ed: &Editor<hjkl_buffer::Buffer, H>,
4042) -> Option<((usize, usize), (usize, usize))> {
4043 tag_text_object(ed, false)
4044}
4045
4046fn reflow_rows<H: crate::types::Host>(
4051 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4052 top: usize,
4053 bot: usize,
4054) {
4055 let width = ed.settings().textwidth.max(1);
4056 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4057 let bot = bot.min(lines.len().saturating_sub(1));
4058 if top > bot {
4059 return;
4060 }
4061 let original = lines[top..=bot].to_vec();
4062 let mut wrapped: Vec<String> = Vec::new();
4063 let mut paragraph: Vec<String> = Vec::new();
4064 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4065 if para.is_empty() {
4066 return;
4067 }
4068 let words = para.join(" ");
4069 let mut current = String::new();
4070 for word in words.split_whitespace() {
4071 let extra = if current.is_empty() {
4072 word.chars().count()
4073 } else {
4074 current.chars().count() + 1 + word.chars().count()
4075 };
4076 if extra > width && !current.is_empty() {
4077 out.push(std::mem::take(&mut current));
4078 current.push_str(word);
4079 } else if current.is_empty() {
4080 current.push_str(word);
4081 } else {
4082 current.push(' ');
4083 current.push_str(word);
4084 }
4085 }
4086 if !current.is_empty() {
4087 out.push(current);
4088 }
4089 para.clear();
4090 };
4091 for line in &original {
4092 if line.trim().is_empty() {
4093 flush(&mut paragraph, &mut wrapped, width);
4094 wrapped.push(String::new());
4095 } else {
4096 paragraph.push(line.clone());
4097 }
4098 }
4099 flush(&mut paragraph, &mut wrapped, width);
4100
4101 let after: Vec<String> = lines.split_off(bot + 1);
4103 lines.truncate(top);
4104 lines.extend(wrapped);
4105 lines.extend(after);
4106 ed.restore(lines, (top, 0));
4107 ed.mark_content_dirty();
4108}
4109
4110fn apply_case_op_to_selection<H: crate::types::Host>(
4116 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4117 op: Operator,
4118 top: (usize, usize),
4119 bot: (usize, usize),
4120 kind: RangeKind,
4121) {
4122 use hjkl_buffer::Edit;
4123 ed.push_undo();
4124 let saved_yank = ed.yank().to_string();
4125 let saved_yank_linewise = ed.vim.yank_linewise;
4126 let selection = cut_vim_range(ed, top, bot, kind);
4127 let transformed = match op {
4128 Operator::Uppercase => selection.to_uppercase(),
4129 Operator::Lowercase => selection.to_lowercase(),
4130 Operator::ToggleCase => toggle_case_str(&selection),
4131 _ => unreachable!(),
4132 };
4133 if !transformed.is_empty() {
4134 let cursor = buf_cursor_pos(&ed.buffer);
4135 ed.mutate_edit(Edit::InsertStr {
4136 at: cursor,
4137 text: transformed,
4138 });
4139 }
4140 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4141 ed.push_buffer_cursor_to_textarea();
4142 ed.set_yank(saved_yank);
4143 ed.vim.yank_linewise = saved_yank_linewise;
4144 ed.vim.mode = Mode::Normal;
4145}
4146
4147fn indent_rows<H: crate::types::Host>(
4152 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4153 top: usize,
4154 bot: usize,
4155 count: usize,
4156) {
4157 ed.sync_buffer_content_from_textarea();
4158 let width = ed.settings().shiftwidth * count.max(1);
4159 let pad: String = " ".repeat(width);
4160 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4161 let bot = bot.min(lines.len().saturating_sub(1));
4162 for line in lines.iter_mut().take(bot + 1).skip(top) {
4163 if !line.is_empty() {
4164 line.insert_str(0, &pad);
4165 }
4166 }
4167 ed.restore(lines, (top, 0));
4170 move_first_non_whitespace(ed);
4171}
4172
4173fn outdent_rows<H: crate::types::Host>(
4177 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4178 top: usize,
4179 bot: usize,
4180 count: usize,
4181) {
4182 ed.sync_buffer_content_from_textarea();
4183 let width = ed.settings().shiftwidth * count.max(1);
4184 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4185 let bot = bot.min(lines.len().saturating_sub(1));
4186 for line in lines.iter_mut().take(bot + 1).skip(top) {
4187 let strip: usize = line
4188 .chars()
4189 .take(width)
4190 .take_while(|c| *c == ' ' || *c == '\t')
4191 .count();
4192 if strip > 0 {
4193 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4194 line.drain(..byte_len);
4195 }
4196 }
4197 ed.restore(lines, (top, 0));
4198 move_first_non_whitespace(ed);
4199}
4200
4201fn bracket_net(line: &str) -> i32 {
4228 let mut net: i32 = 0;
4229 let mut chars = line.chars().peekable();
4230 while let Some(ch) = chars.next() {
4231 match ch {
4232 '/' if chars.peek() == Some(&'/') => return net,
4234 '"' => {
4235 while let Some(c) = chars.next() {
4237 match c {
4238 '\\' => {
4239 chars.next();
4240 } '"' => break,
4242 _ => {}
4243 }
4244 }
4245 }
4246 '\'' => {
4247 let saved: Vec<char> = chars.clone().take(5).collect();
4256 let close_idx = if saved.first() == Some(&'\\') {
4257 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
4258 } else {
4259 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
4260 };
4261 if let Some(idx) = close_idx {
4262 for _ in 0..=idx {
4263 chars.next();
4264 }
4265 }
4266 }
4268 '{' | '(' | '[' => net += 1,
4269 '}' | ')' | ']' => net -= 1,
4270 _ => {}
4271 }
4272 }
4273 net
4274}
4275
4276fn auto_indent_rows<H: crate::types::Host>(
4298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4299 top: usize,
4300 bot: usize,
4301) {
4302 ed.sync_buffer_content_from_textarea();
4303 let shiftwidth = ed.settings().shiftwidth;
4304 let expandtab = ed.settings().expandtab;
4305 let indent_unit: String = if expandtab {
4306 " ".repeat(shiftwidth)
4307 } else {
4308 "\t".to_string()
4309 };
4310
4311 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4312 let bot = bot.min(lines.len().saturating_sub(1));
4313
4314 let mut depth: i32 = 0;
4317 for line in lines.iter().take(top) {
4318 depth += bracket_net(line);
4319 if depth < 0 {
4320 depth = 0;
4321 }
4322 }
4323
4324 for line in lines.iter_mut().take(bot + 1).skip(top) {
4325 let trimmed_owned = line.trim_start().to_owned();
4326 if trimmed_owned.is_empty() {
4328 *line = String::new();
4329 continue;
4331 }
4332
4333 let starts_with_close = trimmed_owned
4335 .chars()
4336 .next()
4337 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
4338 let starts_with_dot = trimmed_owned.starts_with('.')
4348 && !trimmed_owned.starts_with("..")
4349 && !trimmed_owned.starts_with(".;");
4350 let effective_depth = if starts_with_close {
4351 depth.saturating_sub(1)
4352 } else if starts_with_dot {
4353 depth.saturating_add(1)
4354 } else {
4355 depth
4356 } as usize;
4357
4358 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
4360
4361 depth += bracket_net(&trimmed_owned);
4363 if depth < 0 {
4364 depth = 0;
4365 }
4366
4367 *line = new_line;
4368 }
4369
4370 ed.restore(lines, (top, 0));
4372 move_first_non_whitespace(ed);
4373 ed.last_indent_range = Some((top, bot));
4375}
4376
4377fn toggle_case_str(s: &str) -> String {
4378 s.chars()
4379 .map(|c| {
4380 if c.is_lowercase() {
4381 c.to_uppercase().next().unwrap_or(c)
4382 } else if c.is_uppercase() {
4383 c.to_lowercase().next().unwrap_or(c)
4384 } else {
4385 c
4386 }
4387 })
4388 .collect()
4389}
4390
4391fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4392 if a <= b { (a, b) } else { (b, a) }
4393}
4394
4395fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4400 let (row, col) = ed.cursor();
4401 let line_chars = buf_line_chars(&ed.buffer, row);
4402 let max_col = line_chars.saturating_sub(1);
4403 if col > max_col {
4404 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4405 ed.push_buffer_cursor_to_textarea();
4406 }
4407}
4408
4409fn execute_line_op<H: crate::types::Host>(
4412 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4413 op: Operator,
4414 count: usize,
4415) {
4416 let (row, col) = ed.cursor();
4417 let total = buf_row_count(&ed.buffer);
4418 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4419
4420 match op {
4421 Operator::Yank => {
4422 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4424 if !text.is_empty() {
4425 ed.record_yank_to_host(text.clone());
4426 ed.record_yank(text, true);
4427 }
4428 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4431 ed.set_mark('[', (row, 0));
4432 ed.set_mark(']', (end_row, last_col));
4433 buf_set_cursor_rc(&mut ed.buffer, row, col);
4434 ed.push_buffer_cursor_to_textarea();
4435 ed.vim.mode = Mode::Normal;
4436 }
4437 Operator::Delete => {
4438 ed.push_undo();
4439 let deleted_through_last = end_row + 1 >= total;
4440 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4441 let total_after = buf_row_count(&ed.buffer);
4445 let raw_target = if deleted_through_last {
4446 row.saturating_sub(1).min(total_after.saturating_sub(1))
4447 } else {
4448 row.min(total_after.saturating_sub(1))
4449 };
4450 let target_row = if raw_target > 0
4456 && raw_target + 1 == total_after
4457 && buf_line(&ed.buffer, raw_target)
4458 .map(|s| s.is_empty())
4459 .unwrap_or(false)
4460 {
4461 raw_target - 1
4462 } else {
4463 raw_target
4464 };
4465 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4466 ed.push_buffer_cursor_to_textarea();
4467 move_first_non_whitespace(ed);
4468 ed.sticky_col = Some(ed.cursor().1);
4469 ed.vim.mode = Mode::Normal;
4470 let pos = ed.cursor();
4473 ed.set_mark('[', pos);
4474 ed.set_mark(']', pos);
4475 }
4476 Operator::Change => {
4477 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4481 ed.vim.change_mark_start = Some((row, 0));
4483 ed.push_undo();
4484 ed.sync_buffer_content_from_textarea();
4485 let payload = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
4487 if end_row > row {
4488 ed.mutate_edit(Edit::DeleteRange {
4489 start: Position::new(row + 1, 0),
4490 end: Position::new(end_row, 0),
4491 kind: BufKind::Line,
4492 });
4493 }
4494 let line_chars = buf_line_chars(&ed.buffer, row);
4495 if line_chars > 0 {
4496 ed.mutate_edit(Edit::DeleteRange {
4497 start: Position::new(row, 0),
4498 end: Position::new(row, line_chars),
4499 kind: BufKind::Char,
4500 });
4501 }
4502 if !payload.is_empty() {
4503 ed.record_yank_to_host(payload.clone());
4504 ed.record_delete(payload, true);
4505 }
4506 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4507 ed.push_buffer_cursor_to_textarea();
4508 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4509 }
4510 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4511 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
4515 move_first_non_whitespace(ed);
4518 }
4519 Operator::Indent | Operator::Outdent => {
4520 ed.push_undo();
4522 if op == Operator::Indent {
4523 indent_rows(ed, row, end_row, 1);
4524 } else {
4525 outdent_rows(ed, row, end_row, 1);
4526 }
4527 ed.sticky_col = Some(ed.cursor().1);
4528 ed.vim.mode = Mode::Normal;
4529 }
4530 Operator::Fold => unreachable!("Fold has no line-op double"),
4532 Operator::Reflow => {
4533 ed.push_undo();
4535 reflow_rows(ed, row, end_row);
4536 move_first_non_whitespace(ed);
4537 ed.sticky_col = Some(ed.cursor().1);
4538 ed.vim.mode = Mode::Normal;
4539 }
4540 Operator::AutoIndent => {
4541 ed.push_undo();
4543 auto_indent_rows(ed, row, end_row);
4544 ed.sticky_col = Some(ed.cursor().1);
4545 ed.vim.mode = Mode::Normal;
4546 }
4547 }
4548}
4549
4550pub(crate) fn apply_visual_operator<H: crate::types::Host>(
4553 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4554 op: Operator,
4555) {
4556 match ed.vim.mode {
4557 Mode::VisualLine => {
4558 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4559 let top = cursor_row.min(ed.vim.visual_line_anchor);
4560 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4561 ed.vim.yank_linewise = true;
4562 match op {
4563 Operator::Yank => {
4564 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4565 if !text.is_empty() {
4566 ed.record_yank_to_host(text.clone());
4567 ed.record_yank(text, true);
4568 }
4569 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4570 ed.push_buffer_cursor_to_textarea();
4571 ed.vim.mode = Mode::Normal;
4572 }
4573 Operator::Delete => {
4574 ed.push_undo();
4575 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4576 ed.vim.mode = Mode::Normal;
4577 }
4578 Operator::Change => {
4579 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4582 ed.push_undo();
4583 ed.sync_buffer_content_from_textarea();
4584 let payload = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
4585 if bot > top {
4586 ed.mutate_edit(Edit::DeleteRange {
4587 start: Position::new(top + 1, 0),
4588 end: Position::new(bot, 0),
4589 kind: BufKind::Line,
4590 });
4591 }
4592 let line_chars = buf_line_chars(&ed.buffer, top);
4593 if line_chars > 0 {
4594 ed.mutate_edit(Edit::DeleteRange {
4595 start: Position::new(top, 0),
4596 end: Position::new(top, line_chars),
4597 kind: BufKind::Char,
4598 });
4599 }
4600 if !payload.is_empty() {
4601 ed.record_yank_to_host(payload.clone());
4602 ed.record_delete(payload, true);
4603 }
4604 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4605 ed.push_buffer_cursor_to_textarea();
4606 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4607 }
4608 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4609 let bot = buf_cursor_pos(&ed.buffer)
4610 .row
4611 .max(ed.vim.visual_line_anchor);
4612 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
4613 move_first_non_whitespace(ed);
4614 }
4615 Operator::Indent | Operator::Outdent => {
4616 ed.push_undo();
4617 let (cursor_row, _) = ed.cursor();
4618 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4619 if op == Operator::Indent {
4620 indent_rows(ed, top, bot, 1);
4621 } else {
4622 outdent_rows(ed, top, bot, 1);
4623 }
4624 ed.vim.mode = Mode::Normal;
4625 }
4626 Operator::Reflow => {
4627 ed.push_undo();
4628 let (cursor_row, _) = ed.cursor();
4629 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4630 reflow_rows(ed, top, bot);
4631 ed.vim.mode = Mode::Normal;
4632 }
4633 Operator::AutoIndent => {
4634 ed.push_undo();
4635 let (cursor_row, _) = ed.cursor();
4636 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4637 auto_indent_rows(ed, top, bot);
4638 ed.vim.mode = Mode::Normal;
4639 }
4640 Operator::Fold => unreachable!("Visual zf takes its own path"),
4643 }
4644 }
4645 Mode::Visual => {
4646 ed.vim.yank_linewise = false;
4647 let anchor = ed.vim.visual_anchor;
4648 let cursor = ed.cursor();
4649 let (top, bot) = order(anchor, cursor);
4650 match op {
4651 Operator::Yank => {
4652 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
4653 if !text.is_empty() {
4654 ed.record_yank_to_host(text.clone());
4655 ed.record_yank(text, false);
4656 }
4657 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4658 ed.push_buffer_cursor_to_textarea();
4659 ed.vim.mode = Mode::Normal;
4660 }
4661 Operator::Delete => {
4662 ed.push_undo();
4663 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4664 ed.vim.mode = Mode::Normal;
4665 }
4666 Operator::Change => {
4667 ed.push_undo();
4668 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
4669 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4670 }
4671 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4672 let anchor = ed.vim.visual_anchor;
4674 let cursor = ed.cursor();
4675 let (top, bot) = order(anchor, cursor);
4676 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
4677 }
4678 Operator::Indent | Operator::Outdent => {
4679 ed.push_undo();
4680 let anchor = ed.vim.visual_anchor;
4681 let cursor = ed.cursor();
4682 let (top, bot) = order(anchor, cursor);
4683 if op == Operator::Indent {
4684 indent_rows(ed, top.0, bot.0, 1);
4685 } else {
4686 outdent_rows(ed, top.0, bot.0, 1);
4687 }
4688 ed.vim.mode = Mode::Normal;
4689 }
4690 Operator::Reflow => {
4691 ed.push_undo();
4692 let anchor = ed.vim.visual_anchor;
4693 let cursor = ed.cursor();
4694 let (top, bot) = order(anchor, cursor);
4695 reflow_rows(ed, top.0, bot.0);
4696 ed.vim.mode = Mode::Normal;
4697 }
4698 Operator::AutoIndent => {
4699 ed.push_undo();
4700 let anchor = ed.vim.visual_anchor;
4701 let cursor = ed.cursor();
4702 let (top, bot) = order(anchor, cursor);
4703 auto_indent_rows(ed, top.0, bot.0);
4704 ed.vim.mode = Mode::Normal;
4705 }
4706 Operator::Fold => unreachable!("Visual zf takes its own path"),
4707 }
4708 }
4709 Mode::VisualBlock => apply_block_operator(ed, op),
4710 _ => {}
4711 }
4712}
4713
4714fn block_bounds<H: crate::types::Host>(
4719 ed: &Editor<hjkl_buffer::Buffer, H>,
4720) -> (usize, usize, usize, usize) {
4721 let (ar, ac) = ed.vim.block_anchor;
4722 let (cr, _) = ed.cursor();
4723 let cc = ed.vim.block_vcol;
4724 let top = ar.min(cr);
4725 let bot = ar.max(cr);
4726 let left = ac.min(cc);
4727 let right = ac.max(cc);
4728 (top, bot, left, right)
4729}
4730
4731pub(crate) fn update_block_vcol<H: crate::types::Host>(
4736 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4737 motion: &Motion,
4738) {
4739 match motion {
4740 Motion::Left
4741 | Motion::Right
4742 | Motion::WordFwd
4743 | Motion::BigWordFwd
4744 | Motion::WordBack
4745 | Motion::BigWordBack
4746 | Motion::WordEnd
4747 | Motion::BigWordEnd
4748 | Motion::WordEndBack
4749 | Motion::BigWordEndBack
4750 | Motion::LineStart
4751 | Motion::FirstNonBlank
4752 | Motion::LineEnd
4753 | Motion::Find { .. }
4754 | Motion::FindRepeat { .. }
4755 | Motion::MatchBracket => {
4756 ed.vim.block_vcol = ed.cursor().1;
4757 }
4758 _ => {}
4760 }
4761}
4762
4763fn apply_block_operator<H: crate::types::Host>(
4768 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4769 op: Operator,
4770) {
4771 let (top, bot, left, right) = block_bounds(ed);
4772 let yank = block_yank(ed, top, bot, left, right);
4774
4775 match op {
4776 Operator::Yank => {
4777 if !yank.is_empty() {
4778 ed.record_yank_to_host(yank.clone());
4779 ed.record_yank(yank, false);
4780 }
4781 ed.vim.mode = Mode::Normal;
4782 ed.jump_cursor(top, left);
4783 }
4784 Operator::Delete => {
4785 ed.push_undo();
4786 delete_block_contents(ed, top, bot, left, right);
4787 if !yank.is_empty() {
4788 ed.record_yank_to_host(yank.clone());
4789 ed.record_delete(yank, false);
4790 }
4791 ed.vim.mode = Mode::Normal;
4792 ed.jump_cursor(top, left);
4793 }
4794 Operator::Change => {
4795 ed.push_undo();
4796 delete_block_contents(ed, top, bot, left, right);
4797 if !yank.is_empty() {
4798 ed.record_yank_to_host(yank.clone());
4799 ed.record_delete(yank, false);
4800 }
4801 ed.jump_cursor(top, left);
4802 begin_insert_noundo(
4803 ed,
4804 1,
4805 InsertReason::BlockChange {
4806 top,
4807 bot,
4808 col: left,
4809 },
4810 );
4811 }
4812 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4813 ed.push_undo();
4814 transform_block_case(ed, op, top, bot, left, right);
4815 ed.vim.mode = Mode::Normal;
4816 ed.jump_cursor(top, left);
4817 }
4818 Operator::Indent | Operator::Outdent => {
4819 ed.push_undo();
4823 if op == Operator::Indent {
4824 indent_rows(ed, top, bot, 1);
4825 } else {
4826 outdent_rows(ed, top, bot, 1);
4827 }
4828 ed.vim.mode = Mode::Normal;
4829 }
4830 Operator::Fold => unreachable!("Visual zf takes its own path"),
4831 Operator::Reflow => {
4832 ed.push_undo();
4836 reflow_rows(ed, top, bot);
4837 ed.vim.mode = Mode::Normal;
4838 }
4839 Operator::AutoIndent => {
4840 ed.push_undo();
4843 auto_indent_rows(ed, top, bot);
4844 ed.vim.mode = Mode::Normal;
4845 }
4846 }
4847}
4848
4849fn transform_block_case<H: crate::types::Host>(
4853 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4854 op: Operator,
4855 top: usize,
4856 bot: usize,
4857 left: usize,
4858 right: usize,
4859) {
4860 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4861 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4862 let chars: Vec<char> = lines[r].chars().collect();
4863 if left >= chars.len() {
4864 continue;
4865 }
4866 let end = (right + 1).min(chars.len());
4867 let head: String = chars[..left].iter().collect();
4868 let mid: String = chars[left..end].iter().collect();
4869 let tail: String = chars[end..].iter().collect();
4870 let transformed = match op {
4871 Operator::Uppercase => mid.to_uppercase(),
4872 Operator::Lowercase => mid.to_lowercase(),
4873 Operator::ToggleCase => toggle_case_str(&mid),
4874 _ => mid,
4875 };
4876 lines[r] = format!("{head}{transformed}{tail}");
4877 }
4878 let saved_yank = ed.yank().to_string();
4879 let saved_linewise = ed.vim.yank_linewise;
4880 ed.restore(lines, (top, left));
4881 ed.set_yank(saved_yank);
4882 ed.vim.yank_linewise = saved_linewise;
4883}
4884
4885fn block_yank<H: crate::types::Host>(
4886 ed: &Editor<hjkl_buffer::Buffer, H>,
4887 top: usize,
4888 bot: usize,
4889 left: usize,
4890 right: usize,
4891) -> String {
4892 let lines = buf_lines_to_vec(&ed.buffer);
4893 let mut rows: Vec<String> = Vec::new();
4894 for r in top..=bot {
4895 let line = match lines.get(r) {
4896 Some(l) => l,
4897 None => break,
4898 };
4899 let chars: Vec<char> = line.chars().collect();
4900 let end = (right + 1).min(chars.len());
4901 if left >= chars.len() {
4902 rows.push(String::new());
4903 } else {
4904 rows.push(chars[left..end].iter().collect());
4905 }
4906 }
4907 rows.join("\n")
4908}
4909
4910fn delete_block_contents<H: crate::types::Host>(
4911 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4912 top: usize,
4913 bot: usize,
4914 left: usize,
4915 right: usize,
4916) {
4917 use hjkl_buffer::{Edit, MotionKind, Position};
4918 ed.sync_buffer_content_from_textarea();
4919 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4920 if last_row < top {
4921 return;
4922 }
4923 ed.mutate_edit(Edit::DeleteRange {
4924 start: Position::new(top, left),
4925 end: Position::new(last_row, right),
4926 kind: MotionKind::Block,
4927 });
4928 ed.push_buffer_cursor_to_textarea();
4929}
4930
4931pub(crate) fn block_replace<H: crate::types::Host>(
4933 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4934 ch: char,
4935) {
4936 let (top, bot, left, right) = block_bounds(ed);
4937 ed.push_undo();
4938 ed.sync_buffer_content_from_textarea();
4939 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4940 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4941 let chars: Vec<char> = lines[r].chars().collect();
4942 if left >= chars.len() {
4943 continue;
4944 }
4945 let end = (right + 1).min(chars.len());
4946 let before: String = chars[..left].iter().collect();
4947 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4948 let after: String = chars[end..].iter().collect();
4949 lines[r] = format!("{before}{middle}{after}");
4950 }
4951 reset_textarea_lines(ed, lines);
4952 ed.vim.mode = Mode::Normal;
4953 ed.jump_cursor(top, left);
4954}
4955
4956fn reset_textarea_lines<H: crate::types::Host>(
4960 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4961 lines: Vec<String>,
4962) {
4963 let cursor = ed.cursor();
4964 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4965 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4966 ed.mark_content_dirty();
4967}
4968
4969type Pos = (usize, usize);
4975
4976pub(crate) fn text_object_range<H: crate::types::Host>(
4980 ed: &Editor<hjkl_buffer::Buffer, H>,
4981 obj: TextObject,
4982 inner: bool,
4983) -> Option<(Pos, Pos, RangeKind)> {
4984 match obj {
4985 TextObject::Word { big } => {
4986 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
4987 }
4988 TextObject::Quote(q) => {
4989 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4990 }
4991 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4992 TextObject::Paragraph => {
4993 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
4994 }
4995 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
4996 TextObject::Sentence => {
4997 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
4998 }
4999 }
5000}
5001
5002fn sentence_boundary<H: crate::types::Host>(
5006 ed: &Editor<hjkl_buffer::Buffer, H>,
5007 forward: bool,
5008) -> Option<(usize, usize)> {
5009 let lines = buf_lines_to_vec(&ed.buffer);
5010 if lines.is_empty() {
5011 return None;
5012 }
5013 let pos_to_idx = |pos: (usize, usize)| -> usize {
5014 let mut idx = 0;
5015 for line in lines.iter().take(pos.0) {
5016 idx += line.chars().count() + 1;
5017 }
5018 idx + pos.1
5019 };
5020 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5021 for (r, line) in lines.iter().enumerate() {
5022 let len = line.chars().count();
5023 if idx <= len {
5024 return (r, idx);
5025 }
5026 idx -= len + 1;
5027 }
5028 let last = lines.len().saturating_sub(1);
5029 (last, lines[last].chars().count())
5030 };
5031 let mut chars: Vec<char> = Vec::new();
5032 for (r, line) in lines.iter().enumerate() {
5033 chars.extend(line.chars());
5034 if r + 1 < lines.len() {
5035 chars.push('\n');
5036 }
5037 }
5038 if chars.is_empty() {
5039 return None;
5040 }
5041 let total = chars.len();
5042 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
5043 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5044
5045 if forward {
5046 let mut i = cursor_idx + 1;
5049 while i < total {
5050 if is_terminator(chars[i]) {
5051 while i + 1 < total && is_terminator(chars[i + 1]) {
5052 i += 1;
5053 }
5054 if i + 1 >= total {
5055 return None;
5056 }
5057 if chars[i + 1].is_whitespace() {
5058 let mut j = i + 1;
5059 while j < total && chars[j].is_whitespace() {
5060 j += 1;
5061 }
5062 if j >= total {
5063 return None;
5064 }
5065 return Some(idx_to_pos(j));
5066 }
5067 }
5068 i += 1;
5069 }
5070 None
5071 } else {
5072 let find_start = |from: usize| -> Option<usize> {
5076 let mut start = from;
5077 while start > 0 {
5078 let prev = chars[start - 1];
5079 if prev.is_whitespace() {
5080 let mut k = start - 1;
5081 while k > 0 && chars[k - 1].is_whitespace() {
5082 k -= 1;
5083 }
5084 if k > 0 && is_terminator(chars[k - 1]) {
5085 break;
5086 }
5087 }
5088 start -= 1;
5089 }
5090 while start < total && chars[start].is_whitespace() {
5091 start += 1;
5092 }
5093 (start < total).then_some(start)
5094 };
5095 let current_start = find_start(cursor_idx)?;
5096 if current_start < cursor_idx {
5097 return Some(idx_to_pos(current_start));
5098 }
5099 let mut k = current_start;
5102 while k > 0 && chars[k - 1].is_whitespace() {
5103 k -= 1;
5104 }
5105 if k == 0 {
5106 return None;
5107 }
5108 let prev_start = find_start(k - 1)?;
5109 Some(idx_to_pos(prev_start))
5110 }
5111}
5112
5113fn sentence_text_object<H: crate::types::Host>(
5119 ed: &Editor<hjkl_buffer::Buffer, H>,
5120 inner: bool,
5121) -> Option<((usize, usize), (usize, usize))> {
5122 let lines = buf_lines_to_vec(&ed.buffer);
5123 if lines.is_empty() {
5124 return None;
5125 }
5126 let pos_to_idx = |pos: (usize, usize)| -> usize {
5129 let mut idx = 0;
5130 for line in lines.iter().take(pos.0) {
5131 idx += line.chars().count() + 1;
5132 }
5133 idx + pos.1
5134 };
5135 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5136 for (r, line) in lines.iter().enumerate() {
5137 let len = line.chars().count();
5138 if idx <= len {
5139 return (r, idx);
5140 }
5141 idx -= len + 1;
5142 }
5143 let last = lines.len().saturating_sub(1);
5144 (last, lines[last].chars().count())
5145 };
5146 let mut chars: Vec<char> = Vec::new();
5147 for (r, line) in lines.iter().enumerate() {
5148 chars.extend(line.chars());
5149 if r + 1 < lines.len() {
5150 chars.push('\n');
5151 }
5152 }
5153 if chars.is_empty() {
5154 return None;
5155 }
5156
5157 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
5158 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5159
5160 let mut start = cursor_idx;
5164 while start > 0 {
5165 let prev = chars[start - 1];
5166 if prev.is_whitespace() {
5167 let mut k = start - 1;
5171 while k > 0 && chars[k - 1].is_whitespace() {
5172 k -= 1;
5173 }
5174 if k > 0 && is_terminator(chars[k - 1]) {
5175 break;
5176 }
5177 }
5178 start -= 1;
5179 }
5180 while start < chars.len() && chars[start].is_whitespace() {
5183 start += 1;
5184 }
5185 if start >= chars.len() {
5186 return None;
5187 }
5188
5189 let mut end = start;
5192 while end < chars.len() {
5193 if is_terminator(chars[end]) {
5194 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5196 end += 1;
5197 }
5198 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5201 break;
5202 }
5203 }
5204 end += 1;
5205 }
5206 let end_idx = (end + 1).min(chars.len());
5208
5209 let final_end = if inner {
5210 end_idx
5211 } else {
5212 let mut e = end_idx;
5216 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5217 e += 1;
5218 }
5219 e
5220 };
5221
5222 Some((idx_to_pos(start), idx_to_pos(final_end)))
5223}
5224
5225fn tag_text_object<H: crate::types::Host>(
5229 ed: &Editor<hjkl_buffer::Buffer, H>,
5230 inner: bool,
5231) -> Option<((usize, usize), (usize, usize))> {
5232 let lines = buf_lines_to_vec(&ed.buffer);
5233 if lines.is_empty() {
5234 return None;
5235 }
5236 let pos_to_idx = |pos: (usize, usize)| -> usize {
5240 let mut idx = 0;
5241 for line in lines.iter().take(pos.0) {
5242 idx += line.chars().count() + 1;
5243 }
5244 idx + pos.1
5245 };
5246 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5247 for (r, line) in lines.iter().enumerate() {
5248 let len = line.chars().count();
5249 if idx <= len {
5250 return (r, idx);
5251 }
5252 idx -= len + 1;
5253 }
5254 let last = lines.len().saturating_sub(1);
5255 (last, lines[last].chars().count())
5256 };
5257 let mut chars: Vec<char> = Vec::new();
5258 for (r, line) in lines.iter().enumerate() {
5259 chars.extend(line.chars());
5260 if r + 1 < lines.len() {
5261 chars.push('\n');
5262 }
5263 }
5264 let cursor_idx = pos_to_idx(ed.cursor());
5265
5266 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5274 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5275 let mut i = 0;
5276 while i < chars.len() {
5277 if chars[i] != '<' {
5278 i += 1;
5279 continue;
5280 }
5281 let mut j = i + 1;
5282 while j < chars.len() && chars[j] != '>' {
5283 j += 1;
5284 }
5285 if j >= chars.len() {
5286 break;
5287 }
5288 let inside: String = chars[i + 1..j].iter().collect();
5289 let close_end = j + 1;
5290 let trimmed = inside.trim();
5291 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5292 i = close_end;
5293 continue;
5294 }
5295 if let Some(rest) = trimmed.strip_prefix('/') {
5296 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5297 if !name.is_empty()
5298 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5299 {
5300 let (open_start, content_start, _) = stack[stack_idx].clone();
5301 stack.truncate(stack_idx);
5302 let content_end = i;
5303 let candidate = (open_start, content_start, content_end, close_end);
5304 if cursor_idx >= content_start && cursor_idx <= content_end {
5305 innermost = match innermost {
5306 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5307 Some(candidate)
5308 }
5309 None => Some(candidate),
5310 existing => existing,
5311 };
5312 } else if open_start >= cursor_idx && next_after.is_none() {
5313 next_after = Some(candidate);
5314 }
5315 }
5316 } else if !trimmed.ends_with('/') {
5317 let name: String = trimmed
5318 .split(|c: char| c.is_whitespace() || c == '/')
5319 .next()
5320 .unwrap_or("")
5321 .to_string();
5322 if !name.is_empty() {
5323 stack.push((i, close_end, name));
5324 }
5325 }
5326 i = close_end;
5327 }
5328
5329 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5330 if inner {
5331 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5332 } else {
5333 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5334 }
5335}
5336
5337fn is_wordchar(c: char) -> bool {
5338 c.is_alphanumeric() || c == '_'
5339}
5340
5341pub(crate) use hjkl_buffer::is_keyword_char;
5345
5346fn word_text_object<H: crate::types::Host>(
5347 ed: &Editor<hjkl_buffer::Buffer, H>,
5348 inner: bool,
5349 big: bool,
5350) -> Option<((usize, usize), (usize, usize))> {
5351 let (row, col) = ed.cursor();
5352 let line = buf_line(&ed.buffer, row)?;
5353 let chars: Vec<char> = line.chars().collect();
5354 if chars.is_empty() {
5355 return None;
5356 }
5357 let at = col.min(chars.len().saturating_sub(1));
5358 let classify = |c: char| -> u8 {
5359 if c.is_whitespace() {
5360 0
5361 } else if big || is_wordchar(c) {
5362 1
5363 } else {
5364 2
5365 }
5366 };
5367 let cls = classify(chars[at]);
5368 let mut start = at;
5369 while start > 0 && classify(chars[start - 1]) == cls {
5370 start -= 1;
5371 }
5372 let mut end = at;
5373 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5374 end += 1;
5375 }
5376 let char_byte = |i: usize| {
5378 if i >= chars.len() {
5379 line.len()
5380 } else {
5381 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5382 }
5383 };
5384 let mut start_col = char_byte(start);
5385 let mut end_col = char_byte(end + 1);
5387 if !inner {
5388 let mut t = end + 1;
5390 let mut included_trailing = false;
5391 while t < chars.len() && chars[t].is_whitespace() {
5392 included_trailing = true;
5393 t += 1;
5394 }
5395 if included_trailing {
5396 end_col = char_byte(t);
5397 } else {
5398 let mut s = start;
5399 while s > 0 && chars[s - 1].is_whitespace() {
5400 s -= 1;
5401 }
5402 start_col = char_byte(s);
5403 }
5404 }
5405 Some(((row, start_col), (row, end_col)))
5406}
5407
5408fn quote_text_object<H: crate::types::Host>(
5409 ed: &Editor<hjkl_buffer::Buffer, H>,
5410 q: char,
5411 inner: bool,
5412) -> Option<((usize, usize), (usize, usize))> {
5413 let (row, col) = ed.cursor();
5414 let line = buf_line(&ed.buffer, row)?;
5415 let bytes = line.as_bytes();
5416 let q_byte = q as u8;
5417 let mut positions: Vec<usize> = Vec::new();
5419 for (i, &b) in bytes.iter().enumerate() {
5420 if b == q_byte {
5421 positions.push(i);
5422 }
5423 }
5424 if positions.len() < 2 {
5425 return None;
5426 }
5427 let mut open_idx: Option<usize> = None;
5428 let mut close_idx: Option<usize> = None;
5429 for pair in positions.chunks(2) {
5430 if pair.len() < 2 {
5431 break;
5432 }
5433 if col >= pair[0] && col <= pair[1] {
5434 open_idx = Some(pair[0]);
5435 close_idx = Some(pair[1]);
5436 break;
5437 }
5438 if col < pair[0] {
5439 open_idx = Some(pair[0]);
5440 close_idx = Some(pair[1]);
5441 break;
5442 }
5443 }
5444 let open = open_idx?;
5445 let close = close_idx?;
5446 if inner {
5448 if close <= open + 1 {
5449 return None;
5450 }
5451 Some(((row, open + 1), (row, close)))
5452 } else {
5453 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5460 let mut end = after_close;
5462 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5463 end += 1;
5464 }
5465 Some(((row, open), (row, end)))
5466 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5467 let mut start = open;
5469 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5470 start -= 1;
5471 }
5472 Some(((row, start), (row, close + 1)))
5473 } else {
5474 Some(((row, open), (row, close + 1)))
5475 }
5476 }
5477}
5478
5479fn bracket_text_object<H: crate::types::Host>(
5480 ed: &Editor<hjkl_buffer::Buffer, H>,
5481 open: char,
5482 inner: bool,
5483) -> Option<(Pos, Pos, RangeKind)> {
5484 let close = match open {
5485 '(' => ')',
5486 '[' => ']',
5487 '{' => '}',
5488 '<' => '>',
5489 _ => return None,
5490 };
5491 let (row, col) = ed.cursor();
5492 let lines = buf_lines_to_vec(&ed.buffer);
5493 let lines = lines.as_slice();
5494 let open_pos = find_open_bracket(lines, row, col, open, close)
5499 .or_else(|| find_next_open(lines, row, col, open))?;
5500 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5501 if inner {
5503 if close_pos.0 > open_pos.0 + 1 {
5509 let inner_row_start = open_pos.0 + 1;
5511 let inner_row_end = close_pos.0 - 1;
5512 let end_col = lines
5513 .get(inner_row_end)
5514 .map(|l| l.chars().count())
5515 .unwrap_or(0);
5516 return Some((
5517 (inner_row_start, 0),
5518 (inner_row_end, end_col),
5519 RangeKind::Linewise,
5520 ));
5521 }
5522 let inner_start = advance_pos(lines, open_pos);
5523 if inner_start.0 > close_pos.0
5524 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5525 {
5526 return None;
5527 }
5528 Some((inner_start, close_pos, RangeKind::Exclusive))
5529 } else {
5530 Some((
5531 open_pos,
5532 advance_pos(lines, close_pos),
5533 RangeKind::Exclusive,
5534 ))
5535 }
5536}
5537
5538fn find_open_bracket(
5539 lines: &[String],
5540 row: usize,
5541 col: usize,
5542 open: char,
5543 close: char,
5544) -> Option<(usize, usize)> {
5545 let mut depth: i32 = 0;
5546 let mut r = row;
5547 let mut c = col as isize;
5548 loop {
5549 let cur = &lines[r];
5550 let chars: Vec<char> = cur.chars().collect();
5551 if (c as usize) >= chars.len() {
5555 c = chars.len() as isize - 1;
5556 }
5557 while c >= 0 {
5558 let ch = chars[c as usize];
5559 if ch == close {
5560 depth += 1;
5561 } else if ch == open {
5562 if depth == 0 {
5563 return Some((r, c as usize));
5564 }
5565 depth -= 1;
5566 }
5567 c -= 1;
5568 }
5569 if r == 0 {
5570 return None;
5571 }
5572 r -= 1;
5573 c = lines[r].chars().count() as isize - 1;
5574 }
5575}
5576
5577fn find_close_bracket(
5578 lines: &[String],
5579 row: usize,
5580 start_col: usize,
5581 open: char,
5582 close: char,
5583) -> Option<(usize, usize)> {
5584 let mut depth: i32 = 0;
5585 let mut r = row;
5586 let mut c = start_col;
5587 loop {
5588 let cur = &lines[r];
5589 let chars: Vec<char> = cur.chars().collect();
5590 while c < chars.len() {
5591 let ch = chars[c];
5592 if ch == open {
5593 depth += 1;
5594 } else if ch == close {
5595 if depth == 0 {
5596 return Some((r, c));
5597 }
5598 depth -= 1;
5599 }
5600 c += 1;
5601 }
5602 if r + 1 >= lines.len() {
5603 return None;
5604 }
5605 r += 1;
5606 c = 0;
5607 }
5608}
5609
5610fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5614 let mut r = row;
5615 let mut c = col;
5616 while r < lines.len() {
5617 let chars: Vec<char> = lines[r].chars().collect();
5618 while c < chars.len() {
5619 if chars[c] == open {
5620 return Some((r, c));
5621 }
5622 c += 1;
5623 }
5624 r += 1;
5625 c = 0;
5626 }
5627 None
5628}
5629
5630fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5631 let (r, c) = pos;
5632 let line_len = lines[r].chars().count();
5633 if c < line_len {
5634 (r, c + 1)
5635 } else if r + 1 < lines.len() {
5636 (r + 1, 0)
5637 } else {
5638 pos
5639 }
5640}
5641
5642fn paragraph_text_object<H: crate::types::Host>(
5643 ed: &Editor<hjkl_buffer::Buffer, H>,
5644 inner: bool,
5645) -> Option<((usize, usize), (usize, usize))> {
5646 let (row, _) = ed.cursor();
5647 let lines = buf_lines_to_vec(&ed.buffer);
5648 if lines.is_empty() {
5649 return None;
5650 }
5651 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5653 if is_blank(row) {
5654 return None;
5655 }
5656 let mut top = row;
5657 while top > 0 && !is_blank(top - 1) {
5658 top -= 1;
5659 }
5660 let mut bot = row;
5661 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5662 bot += 1;
5663 }
5664 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5666 bot += 1;
5667 }
5668 let end_col = lines[bot].chars().count();
5669 Some(((top, 0), (bot, end_col)))
5670}
5671
5672fn read_vim_range<H: crate::types::Host>(
5678 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5679 start: (usize, usize),
5680 end: (usize, usize),
5681 kind: RangeKind,
5682) -> String {
5683 let (top, bot) = order(start, end);
5684 ed.sync_buffer_content_from_textarea();
5685 let lines = buf_lines_to_vec(&ed.buffer);
5686 match kind {
5687 RangeKind::Linewise => {
5688 let lo = top.0;
5689 let hi = bot.0.min(lines.len().saturating_sub(1));
5690 let mut text = lines[lo..=hi].join("\n");
5691 text.push('\n');
5692 text
5693 }
5694 RangeKind::Inclusive | RangeKind::Exclusive => {
5695 let inclusive = matches!(kind, RangeKind::Inclusive);
5696 let mut out = String::new();
5698 for row in top.0..=bot.0 {
5699 let line = lines.get(row).map(String::as_str).unwrap_or("");
5700 let lo = if row == top.0 { top.1 } else { 0 };
5701 let hi_unclamped = if row == bot.0 {
5702 if inclusive { bot.1 + 1 } else { bot.1 }
5703 } else {
5704 line.chars().count() + 1
5705 };
5706 let row_chars: Vec<char> = line.chars().collect();
5707 let hi = hi_unclamped.min(row_chars.len());
5708 if lo < hi {
5709 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5710 }
5711 if row < bot.0 {
5712 out.push('\n');
5713 }
5714 }
5715 out
5716 }
5717 }
5718}
5719
5720fn cut_vim_range<H: crate::types::Host>(
5729 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5730 start: (usize, usize),
5731 end: (usize, usize),
5732 kind: RangeKind,
5733) -> String {
5734 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5735 let (top, bot) = order(start, end);
5736 ed.sync_buffer_content_from_textarea();
5737 let (buf_start, buf_end, buf_kind) = match kind {
5738 RangeKind::Linewise => (
5739 Position::new(top.0, 0),
5740 Position::new(bot.0, 0),
5741 BufKind::Line,
5742 ),
5743 RangeKind::Inclusive => {
5744 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5745 let next = if bot.1 < line_chars {
5749 Position::new(bot.0, bot.1 + 1)
5750 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5751 Position::new(bot.0 + 1, 0)
5752 } else {
5753 Position::new(bot.0, line_chars)
5754 };
5755 (Position::new(top.0, top.1), next, BufKind::Char)
5756 }
5757 RangeKind::Exclusive => (
5758 Position::new(top.0, top.1),
5759 Position::new(bot.0, bot.1),
5760 BufKind::Char,
5761 ),
5762 };
5763 let inverse = ed.mutate_edit(Edit::DeleteRange {
5764 start: buf_start,
5765 end: buf_end,
5766 kind: buf_kind,
5767 });
5768 let text = match inverse {
5769 Edit::InsertStr { text, .. } => text,
5770 _ => String::new(),
5771 };
5772 if !text.is_empty() {
5773 ed.record_yank_to_host(text.clone());
5774 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
5775 }
5776 ed.push_buffer_cursor_to_textarea();
5777 text
5778}
5779
5780fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5786 use hjkl_buffer::{Edit, MotionKind, Position};
5787 ed.sync_buffer_content_from_textarea();
5788 let cursor = buf_cursor_pos(&ed.buffer);
5789 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5790 if cursor.col >= line_chars {
5791 return;
5792 }
5793 let inverse = ed.mutate_edit(Edit::DeleteRange {
5794 start: cursor,
5795 end: Position::new(cursor.row, line_chars),
5796 kind: MotionKind::Char,
5797 });
5798 if let Edit::InsertStr { text, .. } = inverse
5799 && !text.is_empty()
5800 {
5801 ed.record_yank_to_host(text.clone());
5802 ed.vim.yank_linewise = false;
5803 ed.set_yank(text);
5804 }
5805 buf_set_cursor_pos(&mut ed.buffer, cursor);
5806 ed.push_buffer_cursor_to_textarea();
5807}
5808
5809fn do_char_delete<H: crate::types::Host>(
5810 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5811 forward: bool,
5812 count: usize,
5813) {
5814 use hjkl_buffer::{Edit, MotionKind, Position};
5815 ed.push_undo();
5816 ed.sync_buffer_content_from_textarea();
5817 let mut deleted = String::new();
5820 for _ in 0..count {
5821 let cursor = buf_cursor_pos(&ed.buffer);
5822 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5823 if forward {
5824 if cursor.col >= line_chars {
5827 continue;
5828 }
5829 let inverse = ed.mutate_edit(Edit::DeleteRange {
5830 start: cursor,
5831 end: Position::new(cursor.row, cursor.col + 1),
5832 kind: MotionKind::Char,
5833 });
5834 if let Edit::InsertStr { text, .. } = inverse {
5835 deleted.push_str(&text);
5836 }
5837 } else {
5838 if cursor.col == 0 {
5840 continue;
5841 }
5842 let inverse = ed.mutate_edit(Edit::DeleteRange {
5843 start: Position::new(cursor.row, cursor.col - 1),
5844 end: cursor,
5845 kind: MotionKind::Char,
5846 });
5847 if let Edit::InsertStr { text, .. } = inverse {
5848 deleted = text + &deleted;
5851 }
5852 }
5853 }
5854 if !deleted.is_empty() {
5855 ed.record_yank_to_host(deleted.clone());
5856 ed.record_delete(deleted, false);
5857 }
5858 ed.push_buffer_cursor_to_textarea();
5859}
5860
5861pub(crate) fn adjust_number<H: crate::types::Host>(
5865 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5866 delta: i64,
5867) -> bool {
5868 use hjkl_buffer::{Edit, MotionKind, Position};
5869 ed.sync_buffer_content_from_textarea();
5870 let cursor = buf_cursor_pos(&ed.buffer);
5871 let row = cursor.row;
5872 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5873 Some(l) => l.chars().collect(),
5874 None => return false,
5875 };
5876 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5877 return false;
5878 };
5879 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5880 digit_start - 1
5881 } else {
5882 digit_start
5883 };
5884 let mut span_end = digit_start;
5885 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5886 span_end += 1;
5887 }
5888 let s: String = chars[span_start..span_end].iter().collect();
5889 let Ok(n) = s.parse::<i64>() else {
5890 return false;
5891 };
5892 let new_s = n.saturating_add(delta).to_string();
5893
5894 ed.push_undo();
5895 let span_start_pos = Position::new(row, span_start);
5896 let span_end_pos = Position::new(row, span_end);
5897 ed.mutate_edit(Edit::DeleteRange {
5898 start: span_start_pos,
5899 end: span_end_pos,
5900 kind: MotionKind::Char,
5901 });
5902 ed.mutate_edit(Edit::InsertStr {
5903 at: span_start_pos,
5904 text: new_s.clone(),
5905 });
5906 let new_len = new_s.chars().count();
5907 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5908 ed.push_buffer_cursor_to_textarea();
5909 true
5910}
5911
5912pub(crate) fn replace_char<H: crate::types::Host>(
5913 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5914 ch: char,
5915 count: usize,
5916) {
5917 use hjkl_buffer::{Edit, MotionKind, Position};
5918 ed.push_undo();
5919 ed.sync_buffer_content_from_textarea();
5920 for _ in 0..count {
5921 let cursor = buf_cursor_pos(&ed.buffer);
5922 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5923 if cursor.col >= line_chars {
5924 break;
5925 }
5926 ed.mutate_edit(Edit::DeleteRange {
5927 start: cursor,
5928 end: Position::new(cursor.row, cursor.col + 1),
5929 kind: MotionKind::Char,
5930 });
5931 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5932 }
5933 crate::motions::move_left(&mut ed.buffer, 1);
5935 ed.push_buffer_cursor_to_textarea();
5936}
5937
5938fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5939 use hjkl_buffer::{Edit, MotionKind, Position};
5940 ed.sync_buffer_content_from_textarea();
5941 let cursor = buf_cursor_pos(&ed.buffer);
5942 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5943 return;
5944 };
5945 let toggled = if c.is_uppercase() {
5946 c.to_lowercase().next().unwrap_or(c)
5947 } else {
5948 c.to_uppercase().next().unwrap_or(c)
5949 };
5950 ed.mutate_edit(Edit::DeleteRange {
5951 start: cursor,
5952 end: Position::new(cursor.row, cursor.col + 1),
5953 kind: MotionKind::Char,
5954 });
5955 ed.mutate_edit(Edit::InsertChar {
5956 at: cursor,
5957 ch: toggled,
5958 });
5959}
5960
5961fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5962 use hjkl_buffer::{Edit, Position};
5963 ed.sync_buffer_content_from_textarea();
5964 let row = buf_cursor_pos(&ed.buffer).row;
5965 if row + 1 >= buf_row_count(&ed.buffer) {
5966 return;
5967 }
5968 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
5969 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
5970 let next_trimmed = next_raw.trim_start();
5971 let cur_chars = cur_line.chars().count();
5972 let next_chars = next_raw.chars().count();
5973 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5976 " "
5977 } else {
5978 ""
5979 };
5980 let joined = format!("{cur_line}{separator}{next_trimmed}");
5981 ed.mutate_edit(Edit::Replace {
5982 start: Position::new(row, 0),
5983 end: Position::new(row + 1, next_chars),
5984 with: joined,
5985 });
5986 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5990 ed.push_buffer_cursor_to_textarea();
5991}
5992
5993fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5996 use hjkl_buffer::Edit;
5997 ed.sync_buffer_content_from_textarea();
5998 let row = buf_cursor_pos(&ed.buffer).row;
5999 if row + 1 >= buf_row_count(&ed.buffer) {
6000 return;
6001 }
6002 let join_col = buf_line_chars(&ed.buffer, row);
6003 ed.mutate_edit(Edit::JoinLines {
6004 row,
6005 count: 1,
6006 with_space: false,
6007 });
6008 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
6010 ed.push_buffer_cursor_to_textarea();
6011}
6012
6013fn do_paste<H: crate::types::Host>(
6014 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6015 before: bool,
6016 count: usize,
6017) {
6018 use hjkl_buffer::{Edit, Position};
6019 ed.push_undo();
6020 let selector = ed.vim.pending_register.take();
6025 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
6026 Some(slot) => (slot.text.clone(), slot.linewise),
6027 None => {
6033 let s = &ed.registers().unnamed;
6034 (s.text.clone(), s.linewise)
6035 }
6036 };
6037 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
6041 for _ in 0..count {
6042 ed.sync_buffer_content_from_textarea();
6043 let yank = yank.clone();
6044 if yank.is_empty() {
6045 continue;
6046 }
6047 if linewise {
6048 let text = yank.trim_matches('\n').to_string();
6052 let row = buf_cursor_pos(&ed.buffer).row;
6053 let target_row = if before {
6054 ed.mutate_edit(Edit::InsertStr {
6055 at: Position::new(row, 0),
6056 text: format!("{text}\n"),
6057 });
6058 row
6059 } else {
6060 let line_chars = buf_line_chars(&ed.buffer, row);
6061 ed.mutate_edit(Edit::InsertStr {
6062 at: Position::new(row, line_chars),
6063 text: format!("\n{text}"),
6064 });
6065 row + 1
6066 };
6067 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6068 crate::motions::move_first_non_blank(&mut ed.buffer);
6069 ed.push_buffer_cursor_to_textarea();
6070 let payload_lines = text.lines().count().max(1);
6072 let bot_row = target_row + payload_lines - 1;
6073 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
6074 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
6075 } else {
6076 let cursor = buf_cursor_pos(&ed.buffer);
6080 let at = if before {
6081 cursor
6082 } else {
6083 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6084 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
6085 };
6086 ed.mutate_edit(Edit::InsertStr {
6087 at,
6088 text: yank.clone(),
6089 });
6090 crate::motions::move_left(&mut ed.buffer, 1);
6093 ed.push_buffer_cursor_to_textarea();
6094 let lo = (at.row, at.col);
6096 let hi = ed.cursor();
6097 paste_mark = Some((lo, hi));
6098 }
6099 }
6100 if let Some((lo, hi)) = paste_mark {
6101 ed.set_mark('[', lo);
6102 ed.set_mark(']', hi);
6103 }
6104 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
6106}
6107
6108pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6109 if let Some((lines, cursor)) = ed.undo_stack.pop() {
6110 let current = ed.snapshot();
6111 ed.redo_stack.push(current);
6112 ed.restore(lines, cursor);
6113 }
6114 ed.vim.mode = Mode::Normal;
6115 clamp_cursor_to_normal_mode(ed);
6119}
6120
6121pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6122 if let Some((lines, cursor)) = ed.redo_stack.pop() {
6123 let current = ed.snapshot();
6124 ed.undo_stack.push(current);
6125 ed.cap_undo();
6126 ed.restore(lines, cursor);
6127 }
6128 ed.vim.mode = Mode::Normal;
6129}
6130
6131fn replay_insert_and_finish<H: crate::types::Host>(
6138 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6139 text: &str,
6140) {
6141 use hjkl_buffer::{Edit, Position};
6142 let cursor = ed.cursor();
6143 ed.mutate_edit(Edit::InsertStr {
6144 at: Position::new(cursor.0, cursor.1),
6145 text: text.to_string(),
6146 });
6147 if ed.vim.insert_session.take().is_some() {
6148 if ed.cursor().1 > 0 {
6149 crate::motions::move_left(&mut ed.buffer, 1);
6150 ed.push_buffer_cursor_to_textarea();
6151 }
6152 ed.vim.mode = Mode::Normal;
6153 }
6154}
6155
6156pub(crate) fn replay_last_change<H: crate::types::Host>(
6157 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6158 outer_count: usize,
6159) {
6160 let Some(change) = ed.vim.last_change.clone() else {
6161 return;
6162 };
6163 ed.vim.replaying = true;
6164 let scale = if outer_count > 0 { outer_count } else { 1 };
6165 match change {
6166 LastChange::OpMotion {
6167 op,
6168 motion,
6169 count,
6170 inserted,
6171 } => {
6172 let total = count.max(1) * scale;
6173 apply_op_with_motion(ed, op, &motion, total);
6174 if let Some(text) = inserted {
6175 replay_insert_and_finish(ed, &text);
6176 }
6177 }
6178 LastChange::OpTextObj {
6179 op,
6180 obj,
6181 inner,
6182 inserted,
6183 } => {
6184 apply_op_with_text_object(ed, op, obj, inner);
6185 if let Some(text) = inserted {
6186 replay_insert_and_finish(ed, &text);
6187 }
6188 }
6189 LastChange::LineOp {
6190 op,
6191 count,
6192 inserted,
6193 } => {
6194 let total = count.max(1) * scale;
6195 execute_line_op(ed, op, total);
6196 if let Some(text) = inserted {
6197 replay_insert_and_finish(ed, &text);
6198 }
6199 }
6200 LastChange::CharDel { forward, count } => {
6201 do_char_delete(ed, forward, count * scale);
6202 }
6203 LastChange::ReplaceChar { ch, count } => {
6204 replace_char(ed, ch, count * scale);
6205 }
6206 LastChange::ToggleCase { count } => {
6207 for _ in 0..count * scale {
6208 ed.push_undo();
6209 toggle_case_at_cursor(ed);
6210 }
6211 }
6212 LastChange::JoinLine { count } => {
6213 for _ in 0..count * scale {
6214 ed.push_undo();
6215 join_line(ed);
6216 }
6217 }
6218 LastChange::Paste { before, count } => {
6219 do_paste(ed, before, count * scale);
6220 }
6221 LastChange::DeleteToEol { inserted } => {
6222 use hjkl_buffer::{Edit, Position};
6223 ed.push_undo();
6224 delete_to_eol(ed);
6225 if let Some(text) = inserted {
6226 let cursor = ed.cursor();
6227 ed.mutate_edit(Edit::InsertStr {
6228 at: Position::new(cursor.0, cursor.1),
6229 text,
6230 });
6231 }
6232 }
6233 LastChange::OpenLine { above, inserted } => {
6234 use hjkl_buffer::{Edit, Position};
6235 ed.push_undo();
6236 ed.sync_buffer_content_from_textarea();
6237 let row = buf_cursor_pos(&ed.buffer).row;
6238 if above {
6239 ed.mutate_edit(Edit::InsertStr {
6240 at: Position::new(row, 0),
6241 text: "\n".to_string(),
6242 });
6243 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6244 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6245 } else {
6246 let line_chars = buf_line_chars(&ed.buffer, row);
6247 ed.mutate_edit(Edit::InsertStr {
6248 at: Position::new(row, line_chars),
6249 text: "\n".to_string(),
6250 });
6251 }
6252 ed.push_buffer_cursor_to_textarea();
6253 let cursor = ed.cursor();
6254 ed.mutate_edit(Edit::InsertStr {
6255 at: Position::new(cursor.0, cursor.1),
6256 text: inserted,
6257 });
6258 }
6259 LastChange::InsertAt {
6260 entry,
6261 inserted,
6262 count,
6263 } => {
6264 use hjkl_buffer::{Edit, Position};
6265 ed.push_undo();
6266 match entry {
6267 InsertEntry::I => {}
6268 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6269 InsertEntry::A => {
6270 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6271 ed.push_buffer_cursor_to_textarea();
6272 }
6273 InsertEntry::ShiftA => {
6274 crate::motions::move_line_end(&mut ed.buffer);
6275 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6276 ed.push_buffer_cursor_to_textarea();
6277 }
6278 }
6279 for _ in 0..count.max(1) {
6280 let cursor = ed.cursor();
6281 ed.mutate_edit(Edit::InsertStr {
6282 at: Position::new(cursor.0, cursor.1),
6283 text: inserted.clone(),
6284 });
6285 }
6286 }
6287 }
6288 ed.vim.replaying = false;
6289}
6290
6291fn extract_inserted(before: &str, after: &str) -> String {
6294 let before_chars: Vec<char> = before.chars().collect();
6295 let after_chars: Vec<char> = after.chars().collect();
6296 if after_chars.len() <= before_chars.len() {
6297 return String::new();
6298 }
6299 let prefix = before_chars
6300 .iter()
6301 .zip(after_chars.iter())
6302 .take_while(|(a, b)| a == b)
6303 .count();
6304 let max_suffix = before_chars.len() - prefix;
6305 let suffix = before_chars
6306 .iter()
6307 .rev()
6308 .zip(after_chars.iter().rev())
6309 .take(max_suffix)
6310 .take_while(|(a, b)| a == b)
6311 .count();
6312 after_chars[prefix..after_chars.len() - suffix]
6313 .iter()
6314 .collect()
6315}
6316
6317