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_row_count, buf_set_cursor_pos,
79 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 SneakFirst { forward: bool, count: usize },
165 SneakSecond {
168 c1: char,
169 forward: bool,
170 count: usize,
171 },
172 OpSneakFirst {
175 op: Operator,
176 count1: usize,
177 forward: bool,
178 },
179 OpSneakSecond {
181 op: Operator,
182 count1: usize,
183 c1: char,
184 forward: bool,
185 },
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub enum Operator {
192 Delete,
193 Change,
194 Yank,
195 Uppercase,
198 Lowercase,
200 ToggleCase,
204 Indent,
209 Outdent,
212 Fold,
216 Reflow,
221 ReflowKeepCursor,
226 AutoIndent,
230 Filter,
235 Comment,
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub enum Motion {
243 Left,
244 Right,
245 Up,
246 Down,
247 WordFwd,
248 BigWordFwd,
249 WordBack,
250 BigWordBack,
251 WordEnd,
252 BigWordEnd,
253 WordEndBack,
255 BigWordEndBack,
257 LineStart,
258 FirstNonBlank,
259 LineEnd,
260 FileTop,
261 FileBottom,
262 Find {
263 ch: char,
264 forward: bool,
265 till: bool,
266 },
267 FindRepeat {
268 reverse: bool,
269 },
270 MatchBracket,
271 WordAtCursor {
272 forward: bool,
273 whole_word: bool,
276 },
277 SearchNext {
279 reverse: bool,
280 },
281 ViewportTop,
283 ViewportMiddle,
285 ViewportBottom,
287 LastNonBlank,
289 LineMiddle,
292 ParagraphPrev,
294 ParagraphNext,
296 SentencePrev,
298 SentenceNext,
300 ScreenDown,
303 ScreenUp,
305 SectionBackward,
308 SectionForward,
310 SectionEndBackward,
313 SectionEndForward,
315 FirstNonBlankNextLine,
317 FirstNonBlankPrevLine,
319 FirstNonBlankLine,
321 GotoColumn,
324}
325
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum TextObject {
328 Word {
329 big: bool,
330 },
331 Quote(char),
332 Bracket(char),
333 Paragraph,
334 XmlTag,
338 Sentence,
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
347pub enum RangeKind {
348 Exclusive,
350 Inclusive,
352 Linewise,
354}
355
356#[derive(Debug, Clone)]
360pub enum LastChange {
361 OpMotion {
363 op: Operator,
364 motion: Motion,
365 count: usize,
366 inserted: Option<String>,
367 },
368 OpTextObj {
370 op: Operator,
371 obj: TextObject,
372 inner: bool,
373 inserted: Option<String>,
374 },
375 LineOp {
377 op: Operator,
378 count: usize,
379 inserted: Option<String>,
380 },
381 CharDel { forward: bool, count: usize },
383 ReplaceChar { ch: char, count: usize },
385 ToggleCase { count: usize },
387 JoinLine { count: usize },
389 Paste { before: bool, count: usize },
391 DeleteToEol { inserted: Option<String> },
393 OpenLine { above: bool, inserted: String },
395 InsertAt {
397 entry: InsertEntry,
398 inserted: String,
399 count: usize,
400 },
401}
402
403#[derive(Debug, Clone, Copy, PartialEq, Eq)]
404pub enum InsertEntry {
405 I,
406 A,
407 ShiftI,
408 ShiftA,
409}
410
411#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
422pub enum LastHorizontalMotion {
423 #[default]
424 None,
425 FindChar,
426 Sneak,
427}
428
429#[derive(Default)]
430pub struct VimState {
431 pub mode: Mode,
436 pub pending: Pending,
438 pub count: usize,
441 pub last_find: Option<(char, bool, bool)>,
443 pub last_change: Option<LastChange>,
445 pub insert_session: Option<InsertSession>,
447 pub visual_anchor: (usize, usize),
451 pub visual_line_anchor: usize,
453 pub block_anchor: (usize, usize),
456 pub block_vcol: usize,
462 pub yank_linewise: bool,
464 pub pending_register: Option<char>,
467 pub recording_macro: Option<char>,
471 pub recording_keys: Vec<crate::input::Input>,
476 pub replaying_macro: bool,
479 pub last_macro: Option<char>,
481 pub last_edit_pos: Option<(usize, usize)>,
484 pub last_insert_pos: Option<(usize, usize)>,
488 pub change_list: Vec<(usize, usize)>,
492 pub change_list_cursor: Option<usize>,
495 pub last_visual: Option<LastVisual>,
498 pub viewport_pinned: bool,
502 pub replaying: bool,
504 pub one_shot_normal: bool,
507 pub search_prompt: Option<SearchPrompt>,
509 pub last_search: Option<String>,
513 pub last_search_forward: bool,
517 pub jump_back: Vec<(usize, usize)>,
522 pub jump_fwd: Vec<(usize, usize)>,
525 pub insert_pending_register: bool,
529 pub change_mark_start: Option<(usize, usize)>,
535 pub search_history: Vec<String>,
539 pub search_history_cursor: Option<usize>,
544 pub last_input_at: Option<std::time::Instant>,
553 pub last_input_host_at: Option<core::time::Duration>,
557 pub(crate) current_mode: crate::VimMode,
563 pub(crate) view: crate::ViewMode,
569 pub last_substitute: Option<crate::substitute::SubstituteCmd>,
571 pub pending_closes: Vec<(usize, usize, char)>,
579 pub last_sneak: Option<((char, char), bool)>,
582 pub last_horizontal_motion: LastHorizontalMotion,
585}
586
587pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
588pub(crate) const CHANGE_LIST_MAX: usize = 100;
589
590#[derive(Debug, Clone)]
593pub struct SearchPrompt {
594 pub text: String,
595 pub cursor: usize,
596 pub forward: bool,
597}
598
599#[derive(Debug, Clone)]
600pub struct InsertSession {
601 pub count: usize,
602 pub row_min: usize,
604 pub row_max: usize,
605 pub before_rope: ropey::Rope,
610 pub reason: InsertReason,
611}
612
613#[derive(Debug, Clone)]
614pub enum InsertReason {
615 Enter(InsertEntry),
617 Open { above: bool },
619 AfterChange,
622 DeleteToEol,
624 ReplayOnly,
627 BlockEdge { top: usize, bot: usize, col: usize },
631 BlockChange { top: usize, bot: usize, col: usize },
636 Replace,
640}
641
642#[derive(Debug, Clone, Copy)]
652pub struct LastVisual {
653 pub mode: Mode,
654 pub anchor: (usize, usize),
655 pub cursor: (usize, usize),
656 pub block_vcol: usize,
657}
658
659impl VimState {
660 pub fn public_mode(&self) -> VimMode {
661 match self.mode {
662 Mode::Normal => VimMode::Normal,
663 Mode::Insert => VimMode::Insert,
664 Mode::Visual => VimMode::Visual,
665 Mode::VisualLine => VimMode::VisualLine,
666 Mode::VisualBlock => VimMode::VisualBlock,
667 }
668 }
669
670 pub fn force_normal(&mut self) {
671 self.mode = Mode::Normal;
672 self.pending = Pending::None;
673 self.count = 0;
674 self.insert_session = None;
675 self.current_mode = crate::VimMode::Normal;
677 }
678
679 pub(crate) fn clear_pending_prefix(&mut self) {
689 self.pending = Pending::None;
690 self.count = 0;
691 self.pending_register = None;
692 self.insert_pending_register = false;
693 }
694
695 pub(crate) fn widen_insert_row(&mut self, row: usize) {
700 if let Some(ref mut session) = self.insert_session {
701 session.row_min = session.row_min.min(row);
702 session.row_max = session.row_max.max(row);
703 }
704 }
705
706 pub fn is_visual(&self) -> bool {
707 matches!(
708 self.mode,
709 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
710 )
711 }
712
713 pub fn is_visual_char(&self) -> bool {
714 self.mode == Mode::Visual
715 }
716
717 pub(crate) fn pending_count_val(&self) -> Option<u32> {
720 if self.count == 0 {
721 None
722 } else {
723 Some(self.count as u32)
724 }
725 }
726
727 pub(crate) fn is_chord_pending(&self) -> bool {
730 !matches!(self.pending, Pending::None)
731 }
732
733 pub(crate) fn pending_op_char(&self) -> Option<char> {
737 let op = match &self.pending {
738 Pending::Op { op, .. }
739 | Pending::OpTextObj { op, .. }
740 | Pending::OpG { op, .. }
741 | Pending::OpFind { op, .. }
742 | Pending::OpSquareBracketOpen { op, .. }
743 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
744 _ => None,
745 };
746 op.map(|o| match o {
747 Operator::Delete => 'd',
748 Operator::Change => 'c',
749 Operator::Yank => 'y',
750 Operator::Uppercase => 'U',
751 Operator::Lowercase => 'u',
752 Operator::ToggleCase => '~',
753 Operator::Indent => '>',
754 Operator::Outdent => '<',
755 Operator::Fold => 'z',
756 Operator::Reflow => 'q',
757 Operator::ReflowKeepCursor => 'w',
758 Operator::AutoIndent => '=',
759 Operator::Filter => '!',
760 Operator::Comment => 'c',
762 })
763 }
764}
765
766pub(crate) fn enter_search<H: crate::types::Host>(
772 ed: &mut Editor<hjkl_buffer::Buffer, H>,
773 forward: bool,
774) {
775 ed.vim.search_prompt = Some(SearchPrompt {
776 text: String::new(),
777 cursor: 0,
778 forward,
779 });
780 ed.vim.search_history_cursor = None;
781 ed.set_search_pattern(None);
785}
786
787fn walk_change_list<H: crate::types::Host>(
791 ed: &mut Editor<hjkl_buffer::Buffer, H>,
792 dir: isize,
793 count: usize,
794) {
795 if ed.vim.change_list.is_empty() {
796 return;
797 }
798 let len = ed.vim.change_list.len();
799 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
800 (None, -1) => len as isize - 1,
801 (None, 1) => return, (Some(i), -1) => i as isize - 1,
803 (Some(i), 1) => i as isize + 1,
804 _ => return,
805 };
806 for _ in 1..count {
807 let next = idx + dir;
808 if next < 0 || next >= len as isize {
809 break;
810 }
811 idx = next;
812 }
813 if idx < 0 || idx >= len as isize {
814 return;
815 }
816 let idx = idx as usize;
817 ed.vim.change_list_cursor = Some(idx);
818 let (row, col) = ed.vim.change_list[idx];
819 ed.jump_cursor(row, col);
820}
821
822fn insert_register_text<H: crate::types::Host>(
827 ed: &mut Editor<hjkl_buffer::Buffer, H>,
828 selector: char,
829) {
830 use hjkl_buffer::Edit;
831 let text = match ed.registers().read(selector) {
832 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
833 _ => return,
834 };
835 ed.sync_buffer_content_from_textarea();
836 let cursor = buf_cursor_pos(&ed.buffer);
837 ed.mutate_edit(Edit::InsertStr {
838 at: cursor,
839 text: text.clone(),
840 });
841 let mut row = cursor.row;
844 let mut col = cursor.col;
845 for ch in text.chars() {
846 if ch == '\n' {
847 row += 1;
848 col = 0;
849 } else {
850 col += 1;
851 }
852 }
853 buf_set_cursor_rc(&mut ed.buffer, row, col);
854 ed.push_buffer_cursor_to_textarea();
855 ed.mark_content_dirty();
856 if let Some(ref mut session) = ed.vim.insert_session {
857 session.row_min = session.row_min.min(row);
858 session.row_max = session.row_max.max(row);
859 }
860}
861
862pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
881 if !settings.autoindent {
882 return String::new();
883 }
884 let base: String = prev_line
886 .chars()
887 .take_while(|c| *c == ' ' || *c == '\t')
888 .collect();
889
890 if settings.smartindent {
891 let unit = if settings.expandtab {
892 if settings.softtabstop > 0 {
893 " ".repeat(settings.softtabstop)
894 } else {
895 " ".repeat(settings.shiftwidth)
896 }
897 } else {
898 "\t".to_string()
899 };
900
901 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
903 if matches!(last_non_ws, Some('{' | '(' | '[')) {
904 return format!("{base}{unit}");
905 }
906
907 if is_html_filetype(&settings.filetype) {
912 let trimmed_end_len = prev_line
913 .trim_end_matches(|c: char| c.is_whitespace())
914 .len();
915 let trimmed = &prev_line[..trimmed_end_len];
916 if let Some(stripped) = trimmed.strip_suffix('>')
917 && scan_tag_opener(trimmed, stripped.len()).is_some()
918 {
919 return format!("{base}{unit}");
920 }
921 }
922 }
923
924 base
925}
926
927fn comment_prefixes_for_lang(lang: &str) -> &'static [&'static str] {
933 match lang {
934 "rust" => &["/// ", "//! ", "// "],
935 "c" | "cpp" => &["// "],
936 "python" | "sh" | "bash" | "zsh" | "fish" | "toml" | "yaml" => &["# "],
937 "lua" => &["-- "],
938 "sql" => &["-- "],
939 "vim" | "viml" => &["\" "],
940 _ => &[],
941 }
942}
943
944pub(crate) fn detect_comment_on_line(lang: &str, line: &str) -> Option<(String, &'static str)> {
950 let indent_end = line
951 .char_indices()
952 .find(|(_, c)| *c != ' ' && *c != '\t')
953 .map(|(i, _)| i)
954 .unwrap_or(line.len());
955 let indent = line[..indent_end].to_string();
956 let rest = &line[indent_end..];
957 for &prefix in comment_prefixes_for_lang(lang) {
958 if rest.starts_with(prefix) {
959 return Some((indent, prefix));
960 }
961 let bare = prefix.trim_end_matches(' ');
964 if rest == bare || rest.starts_with(&format!("{bare} ")) {
965 return Some((indent, prefix));
966 }
967 }
968 None
969}
970
971pub(crate) fn continue_comment(
978 buffer: &hjkl_buffer::Buffer,
979 settings: &crate::editor::Settings,
980 row: usize,
981) -> Option<String> {
982 if settings.filetype.is_empty() {
983 return None;
984 }
985 let line = crate::buf_helpers::buf_line(buffer, row)?;
986 let (indent, prefix) = detect_comment_on_line(&settings.filetype, &line)?;
987 Some(format!("{indent}{prefix}"))
988}
989
990fn try_dedent_close_bracket<H: crate::types::Host>(
1000 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1001 cursor: hjkl_buffer::Position,
1002 ch: char,
1003) -> bool {
1004 use hjkl_buffer::{Edit, MotionKind, Position};
1005
1006 if !ed.settings.smartindent {
1007 return false;
1008 }
1009 if !matches!(ch, '}' | ')' | ']') {
1010 return false;
1011 }
1012
1013 let line = match buf_line(&ed.buffer, cursor.row) {
1014 Some(l) => l.to_string(),
1015 None => return false,
1016 };
1017
1018 let before: String = line.chars().take(cursor.col).collect();
1020 if !before.chars().all(|c| c == ' ' || c == '\t') {
1021 return false;
1022 }
1023 if before.is_empty() {
1024 return false;
1026 }
1027
1028 let unit_len: usize = if ed.settings.expandtab {
1030 if ed.settings.softtabstop > 0 {
1031 ed.settings.softtabstop
1032 } else {
1033 ed.settings.shiftwidth
1034 }
1035 } else {
1036 1
1038 };
1039
1040 let strip_len = if ed.settings.expandtab {
1042 let spaces = before.chars().filter(|c| *c == ' ').count();
1044 if spaces < unit_len {
1045 return false;
1046 }
1047 unit_len
1048 } else {
1049 if !before.starts_with('\t') {
1051 return false;
1052 }
1053 1
1054 };
1055
1056 ed.mutate_edit(Edit::DeleteRange {
1058 start: Position::new(cursor.row, 0),
1059 end: Position::new(cursor.row, strip_len),
1060 kind: MotionKind::Char,
1061 });
1062 let new_col = cursor.col.saturating_sub(strip_len);
1067 ed.mutate_edit(Edit::InsertChar {
1068 at: Position::new(cursor.row, new_col),
1069 ch,
1070 });
1071 true
1072}
1073
1074fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1075 let Some(session) = ed.vim.insert_session.take() else {
1076 return;
1077 };
1078 let after_rope = crate::types::Query::rope(&ed.buffer);
1079 let before_n = session.before_rope.len_lines();
1083 let after_n = after_rope.len_lines();
1084 let after_end = session.row_max.min(after_n.saturating_sub(1));
1085 let before_end = session.row_max.min(before_n.saturating_sub(1));
1086 let before = if before_end >= session.row_min && session.row_min < before_n {
1087 rope_row_range_str(&session.before_rope, session.row_min, before_end)
1088 } else {
1089 String::new()
1090 };
1091 let after = if after_end >= session.row_min && session.row_min < after_n {
1092 rope_row_range_str(&after_rope, session.row_min, after_end)
1093 } else {
1094 String::new()
1095 };
1096 let inserted = extract_inserted(&before, &after);
1097 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1098 use hjkl_buffer::{Edit, Position};
1099 for _ in 0..session.count - 1 {
1100 let (row, col) = ed.cursor();
1101 ed.mutate_edit(Edit::InsertStr {
1102 at: Position::new(row, col),
1103 text: inserted.clone(),
1104 });
1105 }
1106 }
1107 fn replicate_block_text<H: crate::types::Host>(
1111 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1112 inserted: &str,
1113 top: usize,
1114 bot: usize,
1115 col: usize,
1116 ) {
1117 use hjkl_buffer::{Edit, Position};
1118 for r in (top + 1)..=bot {
1119 let line_len = buf_line_chars(&ed.buffer, r);
1120 if col > line_len {
1121 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1122 ed.mutate_edit(Edit::InsertStr {
1123 at: Position::new(r, line_len),
1124 text: pad,
1125 });
1126 }
1127 ed.mutate_edit(Edit::InsertStr {
1128 at: Position::new(r, col),
1129 text: inserted.to_string(),
1130 });
1131 }
1132 }
1133
1134 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1135 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1138 replicate_block_text(ed, &inserted, top, bot, col);
1139 buf_set_cursor_rc(&mut ed.buffer, top, col);
1140 ed.push_buffer_cursor_to_textarea();
1141 }
1142 return;
1143 }
1144 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1145 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1149 replicate_block_text(ed, &inserted, top, bot, col);
1150 let ins_chars = inserted.chars().count();
1151 let line_len = buf_line_chars(&ed.buffer, top);
1152 let target_col = (col + ins_chars).min(line_len);
1153 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1154 ed.push_buffer_cursor_to_textarea();
1155 }
1156 return;
1157 }
1158 if ed.vim.replaying {
1159 return;
1160 }
1161 match session.reason {
1162 InsertReason::Enter(entry) => {
1163 ed.vim.last_change = Some(LastChange::InsertAt {
1164 entry,
1165 inserted,
1166 count: session.count,
1167 });
1168 }
1169 InsertReason::Open { above } => {
1170 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1171 }
1172 InsertReason::AfterChange => {
1173 if let Some(
1174 LastChange::OpMotion { inserted: ins, .. }
1175 | LastChange::OpTextObj { inserted: ins, .. }
1176 | LastChange::LineOp { inserted: ins, .. },
1177 ) = ed.vim.last_change.as_mut()
1178 {
1179 *ins = Some(inserted);
1180 }
1181 if let Some(start) = ed.vim.change_mark_start.take() {
1187 let end = ed.cursor();
1188 ed.set_mark('[', start);
1189 ed.set_mark(']', end);
1190 }
1191 }
1192 InsertReason::DeleteToEol => {
1193 ed.vim.last_change = Some(LastChange::DeleteToEol {
1194 inserted: Some(inserted),
1195 });
1196 }
1197 InsertReason::ReplayOnly => {}
1198 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1199 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1200 InsertReason::Replace => {
1201 ed.vim.last_change = Some(LastChange::DeleteToEol {
1206 inserted: Some(inserted),
1207 });
1208 }
1209 }
1210}
1211
1212pub(crate) fn begin_insert<H: crate::types::Host>(
1213 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1214 count: usize,
1215 reason: InsertReason,
1216) {
1217 let record = !matches!(reason, InsertReason::ReplayOnly);
1218 if record {
1219 ed.push_undo();
1220 }
1221 let reason = if ed.vim.replaying {
1222 InsertReason::ReplayOnly
1223 } else {
1224 reason
1225 };
1226 let (row, _) = ed.cursor();
1227 ed.vim.insert_session = Some(InsertSession {
1228 count,
1229 row_min: row,
1230 row_max: row,
1231 before_rope: crate::types::Query::rope(&ed.buffer),
1232 reason,
1233 });
1234 ed.vim.mode = Mode::Insert;
1235 ed.vim.current_mode = crate::VimMode::Insert;
1237 drop_blame_if_left_normal(ed);
1238}
1239
1240pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1255 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1256) {
1257 if !ed.settings.undo_break_on_motion {
1258 return;
1259 }
1260 if ed.vim.replaying {
1261 return;
1262 }
1263 if ed.vim.insert_session.is_none() {
1264 return;
1265 }
1266 ed.push_undo();
1267 let before_rope = crate::types::Query::rope(&ed.buffer);
1268 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1269 if let Some(ref mut session) = ed.vim.insert_session {
1270 session.before_rope = before_rope;
1271 session.row_min = row;
1272 session.row_max = row;
1273 }
1274}
1275
1276fn autopair_close_for(
1308 ch: char,
1309 filetype: &str,
1310 prev_char: Option<char>,
1311 prev2_char: Option<char>,
1312) -> Option<char> {
1313 let is_triple_quote_third =
1319 matches!(ch, '"' | '`' | '\'') && prev_char == Some(ch) && prev2_char == Some(ch);
1320
1321 match ch {
1322 '(' => Some(')'),
1323 '[' => Some(']'),
1324 '{' => Some('}'),
1325 '"' => {
1326 if is_triple_quote_third {
1327 None
1328 } else {
1329 Some('"')
1330 }
1331 }
1332 '`' => {
1333 if is_triple_quote_third {
1334 None
1335 } else {
1336 Some('`')
1337 }
1338 }
1339 '<' => {
1340 if is_html_filetype(filetype) {
1341 Some('>')
1342 } else {
1343 None
1344 }
1345 }
1346 '\'' => {
1347 if is_triple_quote_third {
1348 return None;
1349 }
1350 if prev_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
1353 None
1354 } else {
1355 Some('\'')
1356 }
1357 }
1358 _ => None,
1359 }
1360}
1361
1362fn detect_code_fence_opener(line: &str, cursor_col: usize) -> Option<String> {
1377 if cursor_col != line.chars().count() {
1378 return None;
1379 }
1380 let trimmed = line.trim_start();
1381 let backtick_run = trimmed.chars().take_while(|c| *c == '`').count();
1382 if backtick_run < 3 {
1383 return None;
1384 }
1385 let rest = &trimmed[backtick_run..];
1386 if rest.is_empty() {
1387 return None;
1388 }
1389 let all_lang_chars = rest
1390 .chars()
1391 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-');
1392 if !all_lang_chars {
1393 return None;
1394 }
1395 Some("`".repeat(backtick_run))
1396}
1397
1398fn is_html_filetype(ft: &str) -> bool {
1400 matches!(
1401 ft,
1402 "html" | "xml" | "svg" | "jsx" | "tsx" | "vue" | "svelte"
1403 )
1404}
1405
1406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1422enum TagKind {
1423 Open,
1424 Close,
1425}
1426
1427#[derive(Debug, Clone, PartialEq, Eq)]
1429struct TagSpan {
1430 kind: TagKind,
1431 name: String,
1432 row: usize,
1434 name_start_col: usize,
1436 name_end_col: usize,
1437}
1438
1439fn detect_tag_at_cursor(line: &str, row: usize, col: usize) -> Option<TagSpan> {
1443 let chars: Vec<char> = line.chars().collect();
1444 let mut lt = None;
1446 let mut i = col.min(chars.len());
1447 while i > 0 {
1448 i -= 1;
1449 let c = chars[i];
1450 if c == '<' {
1451 lt = Some(i);
1452 break;
1453 }
1454 if c == '>' {
1456 return None;
1457 }
1458 }
1459 let lt = lt?;
1460 let (kind, name_start) = if chars.get(lt + 1) == Some(&'/') {
1462 (TagKind::Close, lt + 2)
1463 } else {
1464 (TagKind::Open, lt + 1)
1465 };
1466 let first = chars.get(name_start)?;
1468 if !first.is_ascii_alphabetic() {
1469 return None;
1470 }
1471 let mut name_end = name_start;
1473 while name_end < chars.len()
1474 && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-')
1475 {
1476 name_end += 1;
1477 }
1478 if col < name_start || col > name_end {
1482 return None;
1483 }
1484 let name: String = chars[name_start..name_end].iter().collect();
1485 Some(TagSpan {
1486 kind,
1487 name,
1488 row,
1489 name_start_col: name_start,
1490 name_end_col: name_end,
1491 })
1492}
1493
1494fn find_matching_tag(buffer: &hjkl_buffer::Buffer, anchor: &TagSpan) -> Option<TagSpan> {
1508 let row_count = buffer.row_count();
1509 let scan_forward = anchor.kind == TagKind::Open;
1510 let row_iter: Box<dyn Iterator<Item = usize>> = if scan_forward {
1511 Box::new(anchor.row..row_count)
1512 } else {
1513 Box::new((0..=anchor.row).rev())
1514 };
1515 let push_kind = if scan_forward {
1516 TagKind::Open
1517 } else {
1518 TagKind::Close
1519 };
1520 let mut depth: usize = 1;
1521
1522 for r in row_iter {
1523 let line = buf_line(buffer, r)?;
1524 let chars: Vec<char> = line.chars().collect();
1525 let tags = scan_line_tags(&chars, r);
1526 let tags_iter: Box<dyn Iterator<Item = TagSpan>> = if scan_forward {
1527 Box::new(tags.into_iter())
1528 } else {
1529 Box::new(tags.into_iter().rev())
1530 };
1531 for tag in tags_iter {
1532 if r == anchor.row
1534 && tag.name_start_col == anchor.name_start_col
1535 && tag.kind == anchor.kind
1536 {
1537 continue;
1538 }
1539 if r == anchor.row {
1543 if scan_forward && tag.name_start_col < anchor.name_start_col {
1544 continue;
1545 }
1546 if !scan_forward && tag.name_start_col > anchor.name_start_col {
1547 continue;
1548 }
1549 }
1550 if tag.kind == push_kind {
1551 depth += 1;
1552 } else {
1553 depth -= 1;
1554 if depth == 0 {
1555 return Some(tag);
1556 }
1557 }
1558 }
1559 }
1560 None
1561}
1562
1563fn scan_line_tags(chars: &[char], row: usize) -> Vec<TagSpan> {
1567 let mut out = Vec::new();
1568 let n = chars.len();
1569 let mut i = 0;
1570 while i < n {
1571 if chars[i] != '<' {
1572 i += 1;
1573 continue;
1574 }
1575 if chars[i..].starts_with(&['<', '!', '-', '-']) {
1577 let mut j = i + 4;
1578 while j + 2 < n && !(chars[j] == '-' && chars[j + 1] == '-' && chars[j + 2] == '>') {
1579 j += 1;
1580 }
1581 i = (j + 3).min(n);
1582 continue;
1583 }
1584 let (kind, name_start) = if chars.get(i + 1) == Some(&'/') {
1585 (TagKind::Close, i + 2)
1586 } else {
1587 (TagKind::Open, i + 1)
1588 };
1589 if chars
1591 .get(name_start)
1592 .is_none_or(|c| !c.is_ascii_alphabetic())
1593 {
1594 i += 1;
1595 continue;
1596 }
1597 let mut name_end = name_start;
1598 while name_end < n && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-') {
1599 name_end += 1;
1600 }
1601 let mut k = name_end;
1603 let mut self_closing = false;
1604 while k < n {
1605 if chars[k] == '>' {
1606 if k > name_end && chars[k - 1] == '/' {
1607 self_closing = true;
1608 }
1609 break;
1610 }
1611 k += 1;
1612 }
1613 if k >= n {
1614 break;
1616 }
1617 let name: String = chars[name_start..name_end].iter().collect();
1618 if !(self_closing || kind == TagKind::Open && is_void_element(&name)) {
1620 out.push(TagSpan {
1621 kind,
1622 name,
1623 row,
1624 name_start_col: name_start,
1625 name_end_col: name_end,
1626 });
1627 }
1628 i = k + 1;
1629 }
1630 out
1631}
1632
1633pub(crate) fn sync_paired_tag_on_exit<H: crate::types::Host>(
1638 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1639) {
1640 if !is_html_filetype(&ed.settings.filetype) {
1641 return;
1642 }
1643 let (row, col) = ed.cursor();
1644 let line = match buf_line(&ed.buffer, row) {
1645 Some(l) => l,
1646 None => return,
1647 };
1648 let anchor = match detect_tag_at_cursor(&line, row, col) {
1649 Some(t) => t,
1650 None => return,
1651 };
1652 let partner = match find_matching_tag(&ed.buffer, &anchor) {
1653 Some(t) => t,
1654 None => return,
1655 };
1656 if partner.name == anchor.name {
1657 return;
1658 }
1659 use hjkl_buffer::{Edit, MotionKind, Position};
1661 let start = Position::new(partner.row, partner.name_start_col);
1662 let end = Position::new(partner.row, partner.name_end_col);
1663 ed.mutate_edit(Edit::DeleteRange {
1664 start,
1665 end,
1666 kind: MotionKind::Char,
1667 });
1668 ed.mutate_edit(Edit::InsertStr {
1669 at: start,
1670 text: anchor.name.clone(),
1671 });
1672 buf_set_cursor_rc(&mut ed.buffer, row, col);
1675 ed.push_buffer_cursor_to_textarea();
1676}
1677
1678pub fn matching_tag_pair(
1684 buffer: &hjkl_buffer::Buffer,
1685 row: usize,
1686 col: usize,
1687) -> Option<[(usize, usize, usize); 2]> {
1688 let line = buf_line(buffer, row)?;
1689 let anchor = detect_tag_at_cursor(&line, row, col)?;
1690 let partner = find_matching_tag(buffer, &anchor)?;
1691 Some([
1692 (anchor.row, anchor.name_start_col, anchor.name_end_col),
1693 (partner.row, partner.name_start_col, partner.name_end_col),
1694 ])
1695}
1696
1697fn is_void_element(tag: &str) -> bool {
1699 matches!(
1700 tag.to_ascii_lowercase().as_str(),
1701 "area"
1702 | "base"
1703 | "br"
1704 | "col"
1705 | "embed"
1706 | "hr"
1707 | "img"
1708 | "input"
1709 | "link"
1710 | "meta"
1711 | "param"
1712 | "source"
1713 | "track"
1714 | "wbr"
1715 )
1716}
1717
1718fn scan_tag_opener(line: &str, col: usize) -> Option<String> {
1728 let before = if col > 0 { &line[..col] } else { return None };
1731
1732 let lt_pos = before.rfind('<')?;
1734 let inner = &before[lt_pos + 1..]; if inner.starts_with('!') {
1738 return None;
1739 }
1740 if inner.trim_end().ends_with('/') {
1742 return None;
1743 }
1744
1745 let tag: String = inner
1747 .chars()
1748 .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1749 .collect();
1750 if tag.is_empty() {
1751 return None;
1752 }
1753 if !tag
1755 .chars()
1756 .next()
1757 .map(|c| c.is_ascii_alphabetic())
1758 .unwrap_or(false)
1759 {
1760 return None;
1761 }
1762 if is_void_element(&tag) {
1763 return None;
1764 }
1765 Some(tag)
1766}
1767
1768pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1773 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1774 ch: char,
1775) -> bool {
1776 use hjkl_buffer::{Edit, MotionKind, Position};
1777 ed.sync_buffer_content_from_textarea();
1778 let cursor = buf_cursor_pos(&ed.buffer);
1779 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1780 let in_replace = matches!(
1781 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1782 Some(InsertReason::Replace)
1783 );
1784
1785 if !in_replace
1794 && !ed.vim.pending_closes.is_empty()
1795 && let Some(&(pr, _pc, pch)) = ed.vim.pending_closes.last()
1796 && ch == pch
1797 && cursor.row == pr
1798 {
1799 let char_at_cursor =
1800 buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col));
1801 if char_at_cursor == Some(ch) {
1802 ed.vim.pending_closes.pop();
1803 let filetype = ed.settings.filetype.clone();
1805 let autoclose_tag = ed.settings.autoclose_tag;
1806 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1807 let new_col = cursor.col + 1;
1809 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1810 if let Some(line) = buf_line(&ed.buffer, cursor.row)
1812 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
1813 {
1814 let close_tag = format!("</{tag}>");
1815 let insert_pos = Position::new(cursor.row, new_col);
1816 ed.mutate_edit(Edit::InsertStr {
1817 at: insert_pos,
1818 text: close_tag,
1819 });
1820 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1822 }
1823 } else {
1824 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
1825 }
1826 ed.push_buffer_cursor_to_textarea();
1827 return true;
1828 }
1829 }
1830
1831 if in_replace && cursor.col < line_chars {
1832 ed.vim.pending_closes.clear();
1834 ed.mutate_edit(Edit::DeleteRange {
1835 start: cursor,
1836 end: Position::new(cursor.row, cursor.col + 1),
1837 kind: MotionKind::Char,
1838 });
1839 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1840 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1841 let autopair = ed.settings.autopair;
1843 let filetype = ed.settings.filetype.clone();
1844 let autoclose_tag = ed.settings.autoclose_tag;
1845
1846 let (prev_char, prev2_char) = {
1847 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1848 let chars: Vec<char> = line.chars().collect();
1849 let p1 = if cursor.col > 0 {
1850 chars.get(cursor.col - 1).copied()
1851 } else {
1852 None
1853 };
1854 let p2 = if cursor.col > 1 {
1855 chars.get(cursor.col - 2).copied()
1856 } else {
1857 None
1858 };
1859 (p1, p2)
1860 };
1861
1862 if autopair {
1863 if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
1864 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1866 let after = Position::new(cursor.row, cursor.col + 1);
1869 ed.mutate_edit(Edit::InsertChar {
1870 at: after,
1871 ch: close,
1872 });
1873 let between_col = cursor.col + 1;
1876 buf_set_cursor_rc(&mut ed.buffer, cursor.row, between_col);
1877 ed.vim.pending_closes.push((cursor.row, between_col, close));
1882 ed.push_buffer_cursor_to_textarea();
1883 return true;
1884 }
1885
1886 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1890 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1891 let new_col = cursor.col + 1;
1892 if let Some(line) = buf_line(&ed.buffer, cursor.row)
1895 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
1896 {
1897 let close_tag = format!("</{tag}>");
1898 let insert_pos = Position::new(cursor.row, new_col);
1899 ed.mutate_edit(Edit::InsertStr {
1900 at: insert_pos,
1901 text: close_tag,
1902 });
1903 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1905 }
1906 ed.push_buffer_cursor_to_textarea();
1907 return true;
1908 }
1909 }
1910
1911 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1916 }
1917 ed.push_buffer_cursor_to_textarea();
1918 true
1919}
1920
1921pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1927 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1928) -> bool {
1929 use hjkl_buffer::Edit;
1930 ed.sync_buffer_content_from_textarea();
1931 let cursor = buf_cursor_pos(&ed.buffer);
1932 let prev_line = buf_line(&ed.buffer, cursor.row)
1933 .unwrap_or_default()
1934 .to_string();
1935
1936 if ed.settings.autopair && !ed.vim.pending_closes.is_empty() {
1940 let prev_char = if cursor.col > 0 {
1943 prev_line.chars().nth(cursor.col - 1)
1944 } else {
1945 None
1946 };
1947 let next_char = prev_line.chars().nth(cursor.col);
1948 let is_open_pair = matches!(
1949 (prev_char, next_char),
1950 (Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
1951 );
1952 if is_open_pair {
1953 ed.vim.pending_closes.clear();
1956 let base_indent: String = prev_line
1958 .chars()
1959 .take_while(|c| *c == ' ' || *c == '\t')
1960 .collect();
1961 let inner_indent = if ed.settings.expandtab {
1962 let unit = if ed.settings.softtabstop > 0 {
1963 ed.settings.softtabstop
1964 } else {
1965 ed.settings.shiftwidth
1966 };
1967 format!("{base_indent}{}", " ".repeat(unit))
1968 } else {
1969 format!("{base_indent}\t")
1970 };
1971 let text = format!("\n{inner_indent}\n{base_indent}");
1974 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1975 let new_row = cursor.row + 1;
1977 let new_col = inner_indent.len();
1978 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1979 ed.push_buffer_cursor_to_textarea();
1980 return true;
1981 }
1982 }
1983
1984 if ed.settings.autopair
1993 && let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
1994 {
1995 ed.vim.pending_closes.clear();
1996 let base_indent: String = prev_line
1997 .chars()
1998 .take_while(|c| *c == ' ' || *c == '\t')
1999 .collect();
2000 let text = format!("\n{base_indent}\n{base_indent}{fence}");
2001 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2002 let new_row = cursor.row + 1;
2003 let new_col = base_indent.chars().count();
2004 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
2005 ed.push_buffer_cursor_to_textarea();
2006 return true;
2007 }
2008
2009 let comment_cont = if ed.settings.formatoptions.contains('r') {
2011 continue_comment(&ed.buffer, &ed.settings, cursor.row)
2012 } else {
2013 None
2014 };
2015
2016 ed.vim.pending_closes.clear();
2018
2019 let text = if let Some(cont) = comment_cont {
2020 format!("\n{cont}")
2023 } else {
2024 let indent = compute_enter_indent(&ed.settings, &prev_line);
2025 format!("\n{indent}")
2026 };
2027 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2028 ed.push_buffer_cursor_to_textarea();
2029 true
2030}
2031
2032pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
2035 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2036) -> bool {
2037 use hjkl_buffer::Edit;
2038 ed.sync_buffer_content_from_textarea();
2039 let cursor = buf_cursor_pos(&ed.buffer);
2040 if ed.settings.expandtab {
2041 let sts = ed.settings.softtabstop;
2042 let n = if sts > 0 {
2043 sts - (cursor.col % sts)
2044 } else {
2045 ed.settings.tabstop.max(1)
2046 };
2047 ed.mutate_edit(Edit::InsertStr {
2048 at: cursor,
2049 text: " ".repeat(n),
2050 });
2051 } else {
2052 ed.mutate_edit(Edit::InsertChar {
2053 at: cursor,
2054 ch: '\t',
2055 });
2056 }
2057 ed.push_buffer_cursor_to_textarea();
2058 true
2059}
2060
2061pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
2072 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2073) -> bool {
2074 use hjkl_buffer::{Edit, MotionKind, Position};
2075 ed.sync_buffer_content_from_textarea();
2076 let cursor = buf_cursor_pos(&ed.buffer);
2077
2078 if cursor.col > 0 {
2081 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2082 if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings.filetype, &line) {
2083 let full_prefix = format!("{indent}{prefix}");
2084 let line_trimmed = line.trim_end_matches(' ');
2087 let prefix_trimmed = full_prefix.trim_end_matches(' ');
2088 if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
2089 ed.mutate_edit(Edit::DeleteRange {
2091 start: Position::new(cursor.row, 0),
2092 end: cursor,
2093 kind: MotionKind::Char,
2094 });
2095 ed.push_buffer_cursor_to_textarea();
2096 return true;
2097 }
2098 }
2099 }
2100
2101 let sts = ed.settings.softtabstop;
2102 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
2103 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2104 let chars: Vec<char> = line.chars().collect();
2105 let run_start = cursor.col - sts;
2106 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
2107 ed.mutate_edit(Edit::DeleteRange {
2108 start: Position::new(cursor.row, run_start),
2109 end: cursor,
2110 kind: MotionKind::Char,
2111 });
2112 ed.push_buffer_cursor_to_textarea();
2113 return true;
2114 }
2115 }
2116 let result = if cursor.col > 0 {
2117 ed.mutate_edit(Edit::DeleteRange {
2118 start: Position::new(cursor.row, cursor.col - 1),
2119 end: cursor,
2120 kind: MotionKind::Char,
2121 });
2122 true
2123 } else if cursor.row > 0 {
2124 let prev_row = cursor.row - 1;
2125 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2126 ed.mutate_edit(Edit::JoinLines {
2127 row: prev_row,
2128 count: 1,
2129 with_space: false,
2130 });
2131 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2132 true
2133 } else {
2134 false
2135 };
2136 ed.push_buffer_cursor_to_textarea();
2137 result
2138}
2139
2140pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
2143 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2144) -> bool {
2145 use hjkl_buffer::{Edit, MotionKind, Position};
2146 ed.sync_buffer_content_from_textarea();
2147 let cursor = buf_cursor_pos(&ed.buffer);
2148 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2149 let result = if cursor.col < line_chars {
2150 ed.mutate_edit(Edit::DeleteRange {
2151 start: cursor,
2152 end: Position::new(cursor.row, cursor.col + 1),
2153 kind: MotionKind::Char,
2154 });
2155 buf_set_cursor_pos(&mut ed.buffer, cursor);
2156 true
2157 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
2158 ed.mutate_edit(Edit::JoinLines {
2159 row: cursor.row,
2160 count: 1,
2161 with_space: false,
2162 });
2163 buf_set_cursor_pos(&mut ed.buffer, cursor);
2164 true
2165 } else {
2166 false
2167 };
2168 ed.push_buffer_cursor_to_textarea();
2169 result
2170}
2171
2172#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2174pub enum InsertDir {
2175 Left,
2176 Right,
2177 Up,
2178 Down,
2179}
2180
2181pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
2185 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2186 dir: InsertDir,
2187) -> bool {
2188 ed.sync_buffer_content_from_textarea();
2189 ed.vim.pending_closes.clear();
2190 match dir {
2191 InsertDir::Left => {
2192 crate::motions::move_left(&mut ed.buffer, 1);
2193 }
2194 InsertDir::Right => {
2195 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2196 }
2197 InsertDir::Up => {
2198 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2199 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2200 }
2201 InsertDir::Down => {
2202 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2203 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2204 }
2205 }
2206 break_undo_group_in_insert(ed);
2207 ed.push_buffer_cursor_to_textarea();
2208 false
2209}
2210
2211pub(crate) fn insert_home_bridge<H: crate::types::Host>(
2214 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2215) -> bool {
2216 ed.sync_buffer_content_from_textarea();
2217 ed.vim.pending_closes.clear();
2218 crate::motions::move_line_start(&mut ed.buffer);
2219 break_undo_group_in_insert(ed);
2220 ed.push_buffer_cursor_to_textarea();
2221 false
2222}
2223
2224pub(crate) fn insert_end_bridge<H: crate::types::Host>(
2227 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2228) -> bool {
2229 ed.sync_buffer_content_from_textarea();
2230 ed.vim.pending_closes.clear();
2231 crate::motions::move_line_end(&mut ed.buffer);
2232 break_undo_group_in_insert(ed);
2233 ed.push_buffer_cursor_to_textarea();
2234 false
2235}
2236
2237pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
2240 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2241 viewport_h: u16,
2242) -> bool {
2243 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2244 scroll_cursor_rows(ed, -rows);
2245 false
2246}
2247
2248pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
2251 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2252 viewport_h: u16,
2253) -> bool {
2254 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2255 scroll_cursor_rows(ed, rows);
2256 false
2257}
2258
2259pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
2263 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2264) -> bool {
2265 use hjkl_buffer::{Edit, MotionKind};
2266 ed.sync_buffer_content_from_textarea();
2267 let cursor = buf_cursor_pos(&ed.buffer);
2268 if cursor.row == 0 && cursor.col == 0 {
2269 return true;
2270 }
2271 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
2272 let word_start = buf_cursor_pos(&ed.buffer);
2273 if word_start == cursor {
2274 return true;
2275 }
2276 buf_set_cursor_pos(&mut ed.buffer, cursor);
2277 ed.mutate_edit(Edit::DeleteRange {
2278 start: word_start,
2279 end: cursor,
2280 kind: MotionKind::Char,
2281 });
2282 ed.push_buffer_cursor_to_textarea();
2283 true
2284}
2285
2286pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
2289 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2290) -> bool {
2291 use hjkl_buffer::{Edit, MotionKind, Position};
2292 ed.sync_buffer_content_from_textarea();
2293 let cursor = buf_cursor_pos(&ed.buffer);
2294 if cursor.col > 0 {
2295 ed.mutate_edit(Edit::DeleteRange {
2296 start: Position::new(cursor.row, 0),
2297 end: cursor,
2298 kind: MotionKind::Char,
2299 });
2300 ed.push_buffer_cursor_to_textarea();
2301 }
2302 true
2303}
2304
2305pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
2309 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2310) -> bool {
2311 use hjkl_buffer::{Edit, MotionKind, Position};
2312 ed.sync_buffer_content_from_textarea();
2313 let cursor = buf_cursor_pos(&ed.buffer);
2314 if cursor.col > 0 {
2315 ed.mutate_edit(Edit::DeleteRange {
2316 start: Position::new(cursor.row, cursor.col - 1),
2317 end: cursor,
2318 kind: MotionKind::Char,
2319 });
2320 } else if cursor.row > 0 {
2321 let prev_row = cursor.row - 1;
2322 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2323 ed.mutate_edit(Edit::JoinLines {
2324 row: prev_row,
2325 count: 1,
2326 with_space: false,
2327 });
2328 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2329 }
2330 ed.push_buffer_cursor_to_textarea();
2331 true
2332}
2333
2334pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
2337 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2338) -> bool {
2339 let (row, col) = ed.cursor();
2340 let sw = ed.settings().shiftwidth;
2341 indent_rows(ed, row, row, 1);
2342 ed.jump_cursor(row, col + sw);
2343 true
2344}
2345
2346pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
2349 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2350) -> bool {
2351 let (row, col) = ed.cursor();
2352 let before_len = buf_line_bytes(&ed.buffer, row);
2353 outdent_rows(ed, row, row, 1);
2354 let after_len = buf_line_bytes(&ed.buffer, row);
2355 let stripped = before_len.saturating_sub(after_len);
2356 let new_col = col.saturating_sub(stripped);
2357 ed.jump_cursor(row, new_col);
2358 true
2359}
2360
2361pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
2365 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2366) -> bool {
2367 ed.vim.one_shot_normal = true;
2368 ed.vim.mode = Mode::Normal;
2369 ed.vim.current_mode = crate::VimMode::Normal;
2371 false
2372}
2373
2374pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
2378 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2379) -> bool {
2380 ed.vim.insert_pending_register = true;
2381 false
2382}
2383
2384pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
2388 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2389 reg: char,
2390) -> bool {
2391 insert_register_text(ed, reg);
2392 true
2395}
2396
2397pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
2403 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2404) -> bool {
2405 ed.vim.pending_closes.clear();
2406 finish_insert_session(ed);
2407 sync_paired_tag_on_exit(ed);
2411 ed.vim.mode = Mode::Normal;
2412 ed.vim.current_mode = crate::VimMode::Normal;
2414 let col = ed.cursor().1;
2415 ed.vim.last_insert_pos = Some(ed.cursor());
2416 if col > 0 {
2417 crate::motions::move_left(&mut ed.buffer, 1);
2418 ed.push_buffer_cursor_to_textarea();
2419 }
2420 ed.sticky_col = Some(ed.cursor().1);
2421 true
2422}
2423
2424#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2429pub enum ScrollDir {
2430 Down,
2432 Up,
2434}
2435
2436pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
2441 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2442 count: usize,
2443) {
2444 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2445}
2446
2447pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
2449 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2450 count: usize,
2451) {
2452 move_first_non_whitespace(ed);
2453 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2454}
2455
2456pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
2458 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2459 count: usize,
2460) {
2461 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2462 ed.push_buffer_cursor_to_textarea();
2463 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2464}
2465
2466pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
2468 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2469 count: usize,
2470) {
2471 crate::motions::move_line_end(&mut ed.buffer);
2472 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2473 ed.push_buffer_cursor_to_textarea();
2474 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2475}
2476
2477pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
2481 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2482 count: usize,
2483) {
2484 use hjkl_buffer::{Edit, Position};
2485 ed.push_undo();
2486 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2487 ed.sync_buffer_content_from_textarea();
2488 let row = buf_cursor_pos(&ed.buffer).row;
2489 let line_chars = buf_line_chars(&ed.buffer, row);
2490 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
2491
2492 let comment_cont = if ed.settings.formatoptions.contains('o') {
2494 continue_comment(&ed.buffer, &ed.settings, row)
2495 } else {
2496 None
2497 };
2498
2499 let suffix = if let Some(cont) = comment_cont {
2500 format!("\n{cont}")
2501 } else {
2502 let indent = compute_enter_indent(&ed.settings, &prev_line);
2503 format!("\n{indent}")
2504 };
2505 ed.mutate_edit(Edit::InsertStr {
2506 at: Position::new(row, line_chars),
2507 text: suffix,
2508 });
2509 ed.push_buffer_cursor_to_textarea();
2510}
2511
2512pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
2516 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2517 count: usize,
2518) {
2519 use hjkl_buffer::{Edit, Position};
2520 ed.push_undo();
2521 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2522 ed.sync_buffer_content_from_textarea();
2523 let row = buf_cursor_pos(&ed.buffer).row;
2524
2525 let comment_cont = if ed.settings.formatoptions.contains('o') {
2527 continue_comment(&ed.buffer, &ed.settings, row)
2528 } else {
2529 None
2530 };
2531
2532 let (insert_text, new_line_content) = if let Some(cont) = comment_cont {
2535 let content = cont.clone();
2536 (format!("{cont}\n"), content)
2537 } else {
2538 let indent = if row > 0 {
2539 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
2540 compute_enter_indent(&ed.settings, &above)
2541 } else {
2542 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
2543 cur.chars()
2544 .take_while(|c| *c == ' ' || *c == '\t')
2545 .collect::<String>()
2546 };
2547 let content = indent.clone();
2548 (format!("{indent}\n"), content)
2549 };
2550 ed.mutate_edit(Edit::InsertStr {
2551 at: Position::new(row, 0),
2552 text: insert_text,
2553 });
2554 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2555 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2556 let new_row = buf_cursor_pos(&ed.buffer).row;
2557 buf_set_cursor_rc(&mut ed.buffer, new_row, new_line_content.chars().count());
2558 ed.push_buffer_cursor_to_textarea();
2559}
2560
2561pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
2563 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2564 count: usize,
2565) {
2566 begin_insert(ed, count.max(1), InsertReason::Replace);
2567}
2568
2569pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
2574 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2575 count: usize,
2576) {
2577 do_char_delete(ed, true, count.max(1));
2578 if !ed.vim.replaying {
2579 ed.vim.last_change = Some(LastChange::CharDel {
2580 forward: true,
2581 count: count.max(1),
2582 });
2583 }
2584}
2585
2586pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
2589 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2590 count: usize,
2591) {
2592 do_char_delete(ed, false, count.max(1));
2593 if !ed.vim.replaying {
2594 ed.vim.last_change = Some(LastChange::CharDel {
2595 forward: false,
2596 count: count.max(1),
2597 });
2598 }
2599}
2600
2601pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
2604 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2605 count: usize,
2606) {
2607 use hjkl_buffer::{Edit, MotionKind, Position};
2608 ed.push_undo();
2609 ed.sync_buffer_content_from_textarea();
2610 for _ in 0..count.max(1) {
2611 let cursor = buf_cursor_pos(&ed.buffer);
2612 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2613 if cursor.col >= line_chars {
2614 break;
2615 }
2616 ed.mutate_edit(Edit::DeleteRange {
2617 start: cursor,
2618 end: Position::new(cursor.row, cursor.col + 1),
2619 kind: MotionKind::Char,
2620 });
2621 }
2622 ed.push_buffer_cursor_to_textarea();
2623 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
2624 if !ed.vim.replaying {
2625 ed.vim.last_change = Some(LastChange::OpMotion {
2626 op: Operator::Change,
2627 motion: Motion::Right,
2628 count: count.max(1),
2629 inserted: None,
2630 });
2631 }
2632}
2633
2634pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
2637 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2638 count: usize,
2639) {
2640 execute_line_op(ed, Operator::Change, count.max(1));
2641 if !ed.vim.replaying {
2642 ed.vim.last_change = Some(LastChange::LineOp {
2643 op: Operator::Change,
2644 count: count.max(1),
2645 inserted: None,
2646 });
2647 }
2648}
2649
2650pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2653 ed.push_undo();
2654 delete_to_eol(ed);
2655 crate::motions::move_left(&mut ed.buffer, 1);
2656 ed.push_buffer_cursor_to_textarea();
2657 if !ed.vim.replaying {
2658 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2659 }
2660}
2661
2662pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2665 ed.push_undo();
2666 delete_to_eol(ed);
2667 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2668}
2669
2670pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
2672 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2673 count: usize,
2674) {
2675 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2676}
2677
2678pub(crate) fn join_line_bridge<H: crate::types::Host>(
2681 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2682 count: usize,
2683) {
2684 for _ in 0..count.max(1) {
2685 ed.push_undo();
2686 join_line(ed);
2687 }
2688 if !ed.vim.replaying {
2689 ed.vim.last_change = Some(LastChange::JoinLine {
2690 count: count.max(1),
2691 });
2692 }
2693}
2694
2695pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
2698 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2699 count: usize,
2700) {
2701 for _ in 0..count.max(1) {
2702 ed.push_undo();
2703 toggle_case_at_cursor(ed);
2704 }
2705 if !ed.vim.replaying {
2706 ed.vim.last_change = Some(LastChange::ToggleCase {
2707 count: count.max(1),
2708 });
2709 }
2710}
2711
2712pub(crate) fn paste_after_bridge<H: crate::types::Host>(
2716 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2717 count: usize,
2718) {
2719 do_paste(ed, false, count.max(1));
2720 if !ed.vim.replaying {
2721 ed.vim.last_change = Some(LastChange::Paste {
2722 before: false,
2723 count: count.max(1),
2724 });
2725 }
2726}
2727
2728pub(crate) fn paste_before_bridge<H: crate::types::Host>(
2732 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2733 count: usize,
2734) {
2735 do_paste(ed, true, count.max(1));
2736 if !ed.vim.replaying {
2737 ed.vim.last_change = Some(LastChange::Paste {
2738 before: true,
2739 count: count.max(1),
2740 });
2741 }
2742}
2743
2744pub(crate) fn jump_back_bridge<H: crate::types::Host>(
2749 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2750 count: usize,
2751) {
2752 for _ in 0..count.max(1) {
2753 jump_back(ed);
2754 }
2755}
2756
2757pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
2760 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2761 count: usize,
2762) {
2763 for _ in 0..count.max(1) {
2764 jump_forward(ed);
2765 }
2766}
2767
2768pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
2773 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2774 dir: ScrollDir,
2775 count: usize,
2776) {
2777 let rows = viewport_full_rows(ed, count) as isize;
2778 match dir {
2779 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2780 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2781 }
2782}
2783
2784pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
2787 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2788 dir: ScrollDir,
2789 count: usize,
2790) {
2791 let rows = viewport_half_rows(ed, count) as isize;
2792 match dir {
2793 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2794 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2795 }
2796}
2797
2798pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
2802 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2803 dir: ScrollDir,
2804 count: usize,
2805) {
2806 let n = count.max(1);
2807 let total = buf_row_count(&ed.buffer);
2808 let last = total.saturating_sub(1);
2809 let h = ed.viewport_height_value() as usize;
2810 let vp = ed.host().viewport();
2811 let cur_top = vp.top_row;
2812 let new_top = match dir {
2813 ScrollDir::Down => (cur_top + n).min(last),
2814 ScrollDir::Up => cur_top.saturating_sub(n),
2815 };
2816 ed.set_viewport_top(new_top);
2817 let (row, col) = ed.cursor();
2819 let bot = (new_top + h).saturating_sub(1).min(last);
2820 let clamped = row.max(new_top).min(bot);
2821 if clamped != row {
2822 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
2823 ed.push_buffer_cursor_to_textarea();
2824 }
2825}
2826
2827pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
2832 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2833 forward: bool,
2834 count: usize,
2835) {
2836 if let Some(pattern) = ed.vim.last_search.clone() {
2837 ed.push_search_pattern(&pattern);
2838 }
2839 if ed.search_state().pattern.is_none() {
2840 return;
2841 }
2842 let go_forward = ed.vim.last_search_forward == forward;
2843 for _ in 0..count.max(1) {
2844 if go_forward {
2845 ed.search_advance_forward(true);
2846 } else {
2847 ed.search_advance_backward(true);
2848 }
2849 }
2850 ed.push_buffer_cursor_to_textarea();
2851}
2852
2853pub(crate) fn word_search_bridge<H: crate::types::Host>(
2857 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2858 forward: bool,
2859 whole_word: bool,
2860 count: usize,
2861) {
2862 word_at_cursor_search(ed, forward, whole_word, count.max(1));
2863}
2864
2865#[allow(dead_code)]
2870#[inline]
2871pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2872 do_undo(ed);
2873}
2874
2875#[inline]
2892pub(crate) fn drop_blame_if_left_normal<H: crate::types::Host>(
2893 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2894) {
2895 if ed.vim.current_mode != crate::VimMode::Normal {
2896 ed.vim.view = crate::ViewMode::Normal;
2897 }
2898}
2899
2900#[inline]
2904pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
2905 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2906 mode: Mode,
2907) {
2908 ed.vim.mode = mode;
2909 ed.vim.current_mode = ed.vim.public_mode();
2910 drop_blame_if_left_normal(ed);
2911}
2912
2913pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
2916 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2917) {
2918 let cur = ed.cursor();
2919 ed.vim.visual_anchor = cur;
2920 set_vim_mode_bridge(ed, Mode::Visual);
2921}
2922
2923pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
2926 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2927) {
2928 let (row, _) = ed.cursor();
2929 ed.vim.visual_line_anchor = row;
2930 set_vim_mode_bridge(ed, Mode::VisualLine);
2931}
2932
2933pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
2937 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2938) {
2939 let cur = ed.cursor();
2940 ed.vim.block_anchor = cur;
2941 ed.vim.block_vcol = cur.1;
2942 set_vim_mode_bridge(ed, Mode::VisualBlock);
2943}
2944
2945pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
2950 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2951) {
2952 let snap: Option<LastVisual> = match ed.vim.mode {
2954 Mode::Visual => Some(LastVisual {
2955 mode: Mode::Visual,
2956 anchor: ed.vim.visual_anchor,
2957 cursor: ed.cursor(),
2958 block_vcol: 0,
2959 }),
2960 Mode::VisualLine => Some(LastVisual {
2961 mode: Mode::VisualLine,
2962 anchor: (ed.vim.visual_line_anchor, 0),
2963 cursor: ed.cursor(),
2964 block_vcol: 0,
2965 }),
2966 Mode::VisualBlock => Some(LastVisual {
2967 mode: Mode::VisualBlock,
2968 anchor: ed.vim.block_anchor,
2969 cursor: ed.cursor(),
2970 block_vcol: ed.vim.block_vcol,
2971 }),
2972 _ => None,
2973 };
2974 ed.vim.pending = Pending::None;
2976 ed.vim.count = 0;
2977 ed.vim.insert_session = None;
2978 set_vim_mode_bridge(ed, Mode::Normal);
2979 if let Some(snap) = snap {
2983 let (lo, hi) = match snap.mode {
2984 Mode::Visual => {
2985 if snap.anchor <= snap.cursor {
2986 (snap.anchor, snap.cursor)
2987 } else {
2988 (snap.cursor, snap.anchor)
2989 }
2990 }
2991 Mode::VisualLine => {
2992 let r_lo = snap.anchor.0.min(snap.cursor.0);
2993 let r_hi = snap.anchor.0.max(snap.cursor.0);
2994 let vl_rope = ed.buffer().rope();
2995 let r_hi_clamped = r_hi.min(vl_rope.len_lines().saturating_sub(1));
2996 let last_col = hjkl_buffer::rope_line_str(&vl_rope, r_hi_clamped)
2997 .chars()
2998 .count()
2999 .saturating_sub(1);
3000 ((r_lo, 0), (r_hi, last_col))
3001 }
3002 Mode::VisualBlock => {
3003 let (r1, c1) = snap.anchor;
3004 let (r2, c2) = snap.cursor;
3005 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
3006 }
3007 _ => {
3008 if snap.anchor <= snap.cursor {
3009 (snap.anchor, snap.cursor)
3010 } else {
3011 (snap.cursor, snap.anchor)
3012 }
3013 }
3014 };
3015 ed.set_mark('<', lo);
3016 ed.set_mark('>', hi);
3017 ed.vim.last_visual = Some(snap);
3018 }
3019}
3020
3021pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
3027 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3028) {
3029 match ed.vim.mode {
3030 Mode::Visual => {
3031 let cur = ed.cursor();
3032 let anchor = ed.vim.visual_anchor;
3033 ed.vim.visual_anchor = cur;
3034 ed.jump_cursor(anchor.0, anchor.1);
3035 }
3036 Mode::VisualLine => {
3037 let cur_row = ed.cursor().0;
3038 let anchor_row = ed.vim.visual_line_anchor;
3039 ed.vim.visual_line_anchor = cur_row;
3040 ed.jump_cursor(anchor_row, 0);
3041 }
3042 Mode::VisualBlock => {
3043 let cur = ed.cursor();
3044 let anchor = ed.vim.block_anchor;
3045 ed.vim.block_anchor = cur;
3046 ed.vim.block_vcol = anchor.1;
3047 ed.jump_cursor(anchor.0, anchor.1);
3048 }
3049 _ => {}
3050 }
3051}
3052
3053pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
3057 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3058) {
3059 if let Some(snap) = ed.vim.last_visual {
3060 match snap.mode {
3061 Mode::Visual => {
3062 ed.vim.visual_anchor = snap.anchor;
3063 set_vim_mode_bridge(ed, Mode::Visual);
3064 }
3065 Mode::VisualLine => {
3066 ed.vim.visual_line_anchor = snap.anchor.0;
3067 set_vim_mode_bridge(ed, Mode::VisualLine);
3068 }
3069 Mode::VisualBlock => {
3070 ed.vim.block_anchor = snap.anchor;
3071 ed.vim.block_vcol = snap.block_vcol;
3072 set_vim_mode_bridge(ed, Mode::VisualBlock);
3073 }
3074 _ => {}
3075 }
3076 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3077 }
3078}
3079
3080pub(crate) fn set_mode_bridge<H: crate::types::Host>(
3086 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3087 mode: crate::VimMode,
3088) {
3089 let internal = match mode {
3090 crate::VimMode::Normal => Mode::Normal,
3091 crate::VimMode::Insert => Mode::Insert,
3092 crate::VimMode::Visual => Mode::Visual,
3093 crate::VimMode::VisualLine => Mode::VisualLine,
3094 crate::VimMode::VisualBlock => Mode::VisualBlock,
3095 };
3096 ed.vim.mode = internal;
3097 ed.vim.current_mode = mode;
3098 drop_blame_if_left_normal(ed);
3099}
3100
3101pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
3118 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3119 ch: char,
3120) {
3121 if ch.is_ascii_lowercase() {
3122 let pos = ed.cursor();
3123 ed.set_mark(ch, pos);
3124 } else if ch.is_ascii_uppercase() {
3125 let pos = ed.cursor();
3126 let bid = ed.current_buffer_id();
3127 ed.set_global_mark(ch, bid, pos);
3128 tracing::debug!(
3129 mark = ch as u32,
3130 buffer_id = bid,
3131 row = pos.0,
3132 col = pos.1,
3133 "global mark set"
3134 );
3135 }
3136 }
3138
3139pub(crate) fn goto_mark<H: crate::types::Host>(
3148 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3149 ch: char,
3150 linewise: bool,
3151) {
3152 let target = match ch {
3153 'a'..='z' => ed.mark(ch),
3154 '\'' | '`' => ed.vim.jump_back.last().copied(),
3155 '.' => ed.vim.last_edit_pos,
3156 '[' | ']' | '<' | '>' => ed.mark(ch),
3157 _ => None,
3158 };
3159 let Some((row, col)) = target else {
3160 return;
3161 };
3162 let pre = ed.cursor();
3163 let (r, c_clamped) = clamp_pos(ed, (row, col));
3164 if linewise {
3165 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3166 ed.push_buffer_cursor_to_textarea();
3167 move_first_non_whitespace(ed);
3168 } else {
3169 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3170 ed.push_buffer_cursor_to_textarea();
3171 }
3172 if ed.cursor() != pre {
3173 ed.push_jump(pre);
3174 }
3175 ed.sticky_col = Some(ed.cursor().1);
3176}
3177
3178pub(crate) fn try_goto_mark<H: crate::types::Host>(
3187 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3188 ch: char,
3189 linewise: bool,
3190) -> crate::editor::MarkJump {
3191 use crate::editor::MarkJump;
3192 match ch {
3193 'A'..='Z' => {
3194 let Some((bid, row, col)) = ed.global_mark(ch) else {
3195 return MarkJump::Unset;
3196 };
3197 if bid != ed.current_buffer_id() {
3198 tracing::debug!(
3199 mark = ch as u32,
3200 buffer_id = bid,
3201 row,
3202 col,
3203 "global mark cross-buffer jump"
3204 );
3205 return MarkJump::CrossBuffer {
3206 buffer_id: bid,
3207 row,
3208 col,
3209 };
3210 }
3211 let pre = ed.cursor();
3213 let (r, c_clamped) = clamp_pos(ed, (row, col));
3214 if linewise {
3215 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3216 ed.push_buffer_cursor_to_textarea();
3217 move_first_non_whitespace(ed);
3218 } else {
3219 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3220 ed.push_buffer_cursor_to_textarea();
3221 }
3222 if ed.cursor() != pre {
3223 ed.push_jump(pre);
3224 }
3225 ed.sticky_col = Some(ed.cursor().1);
3226 MarkJump::SameBuffer
3227 }
3228 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
3229 goto_mark(ed, ch, linewise);
3230 MarkJump::SameBuffer
3231 }
3232 _ => MarkJump::Unset,
3233 }
3234}
3235
3236pub fn op_is_change(op: Operator) -> bool {
3240 matches!(op, Operator::Delete | Operator::Change)
3241}
3242
3243pub(crate) const JUMPLIST_MAX: usize = 100;
3247
3248fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3251 let Some(target) = ed.vim.jump_back.pop() else {
3252 return;
3253 };
3254 let cur = ed.cursor();
3255 ed.vim.jump_fwd.push(cur);
3256 let (r, c) = clamp_pos(ed, target);
3257 ed.jump_cursor(r, c);
3258 ed.sticky_col = Some(c);
3259}
3260
3261fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3264 let Some(target) = ed.vim.jump_fwd.pop() else {
3265 return;
3266 };
3267 let cur = ed.cursor();
3268 ed.vim.jump_back.push(cur);
3269 if ed.vim.jump_back.len() > JUMPLIST_MAX {
3270 ed.vim.jump_back.remove(0);
3271 }
3272 let (r, c) = clamp_pos(ed, target);
3273 ed.jump_cursor(r, c);
3274 ed.sticky_col = Some(c);
3275}
3276
3277fn clamp_pos<H: crate::types::Host>(
3280 ed: &Editor<hjkl_buffer::Buffer, H>,
3281 pos: (usize, usize),
3282) -> (usize, usize) {
3283 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3284 let r = pos.0.min(last_row);
3285 let line_len = buf_line_chars(&ed.buffer, r);
3286 let c = pos.1.min(line_len.saturating_sub(1));
3287 (r, c)
3288}
3289
3290fn is_big_jump(motion: &Motion) -> bool {
3292 matches!(
3293 motion,
3294 Motion::FileTop
3295 | Motion::FileBottom
3296 | Motion::MatchBracket
3297 | Motion::WordAtCursor { .. }
3298 | Motion::SearchNext { .. }
3299 | Motion::ViewportTop
3300 | Motion::ViewportMiddle
3301 | Motion::ViewportBottom
3302 )
3303}
3304
3305fn viewport_half_rows<H: crate::types::Host>(
3310 ed: &Editor<hjkl_buffer::Buffer, H>,
3311 count: usize,
3312) -> usize {
3313 let h = ed.viewport_height_value() as usize;
3314 (h / 2).max(1).saturating_mul(count.max(1))
3315}
3316
3317fn viewport_full_rows<H: crate::types::Host>(
3320 ed: &Editor<hjkl_buffer::Buffer, H>,
3321 count: usize,
3322) -> usize {
3323 let h = ed.viewport_height_value() as usize;
3324 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
3325}
3326
3327fn scroll_cursor_rows<H: crate::types::Host>(
3332 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3333 delta: isize,
3334) {
3335 if delta == 0 {
3336 return;
3337 }
3338 ed.sync_buffer_content_from_textarea();
3339 let (row, _) = ed.cursor();
3340 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3341 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
3342 buf_set_cursor_rc(&mut ed.buffer, target, 0);
3343 crate::motions::move_first_non_blank(&mut ed.buffer);
3344 ed.push_buffer_cursor_to_textarea();
3345 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3346}
3347
3348pub fn parse_motion(input: &Input) -> Option<Motion> {
3354 if input.ctrl {
3355 return None;
3356 }
3357 match input.key {
3358 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
3359 Key::Char('l') | Key::Right => Some(Motion::Right),
3360 Key::Char('j') | Key::Down => Some(Motion::Down),
3361 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
3363 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
3365 Key::Char('_') => Some(Motion::FirstNonBlankLine),
3367 Key::Char('k') | Key::Up => Some(Motion::Up),
3368 Key::Char('w') => Some(Motion::WordFwd),
3369 Key::Char('W') => Some(Motion::BigWordFwd),
3370 Key::Char('b') => Some(Motion::WordBack),
3371 Key::Char('B') => Some(Motion::BigWordBack),
3372 Key::Char('e') => Some(Motion::WordEnd),
3373 Key::Char('E') => Some(Motion::BigWordEnd),
3374 Key::Char('0') | Key::Home => Some(Motion::LineStart),
3375 Key::Char('^') => Some(Motion::FirstNonBlank),
3376 Key::Char('$') | Key::End => Some(Motion::LineEnd),
3377 Key::Char('G') => Some(Motion::FileBottom),
3378 Key::Char('%') => Some(Motion::MatchBracket),
3379 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
3380 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
3381 Key::Char('*') => Some(Motion::WordAtCursor {
3382 forward: true,
3383 whole_word: true,
3384 }),
3385 Key::Char('#') => Some(Motion::WordAtCursor {
3386 forward: false,
3387 whole_word: true,
3388 }),
3389 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
3390 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
3391 Key::Char('H') => Some(Motion::ViewportTop),
3392 Key::Char('M') => Some(Motion::ViewportMiddle),
3393 Key::Char('L') => Some(Motion::ViewportBottom),
3394 Key::Char('{') => Some(Motion::ParagraphPrev),
3395 Key::Char('}') => Some(Motion::ParagraphNext),
3396 Key::Char('(') => Some(Motion::SentencePrev),
3397 Key::Char(')') => Some(Motion::SentenceNext),
3398 Key::Char('|') => Some(Motion::GotoColumn),
3399 _ => None,
3400 }
3401}
3402
3403pub(crate) fn execute_motion<H: crate::types::Host>(
3406 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3407 motion: Motion,
3408 count: usize,
3409) {
3410 let count = count.max(1);
3411 if let Motion::FindRepeat { reverse } = motion
3414 && ed.vim.last_horizontal_motion == LastHorizontalMotion::Sneak
3415 {
3416 if let Some(((c1, c2), fwd)) = ed.vim.last_sneak {
3417 let effective_fwd = if reverse { !fwd } else { fwd };
3418 apply_sneak(ed, c1, c2, effective_fwd, count);
3419 }
3420 return;
3421 }
3422 let motion = match motion {
3424 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3425 Some((ch, forward, till)) => Motion::Find {
3426 ch,
3427 forward: if reverse { !forward } else { forward },
3428 till,
3429 },
3430 None => return,
3431 },
3432 other => other,
3433 };
3434 let pre_pos = ed.cursor();
3435 let pre_col = pre_pos.1;
3436 apply_motion_cursor(ed, &motion, count);
3437 let post_pos = ed.cursor();
3438 if is_big_jump(&motion) && pre_pos != post_pos {
3439 ed.push_jump(pre_pos);
3440 }
3441 apply_sticky_col(ed, &motion, pre_col);
3442 ed.sync_buffer_from_textarea();
3447}
3448
3449fn execute_motion_with_block_vcol<H: crate::types::Host>(
3460 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3461 motion: Motion,
3462 count: usize,
3463) {
3464 let motion_copy = motion.clone();
3465 execute_motion(ed, motion, count);
3466 if ed.vim.mode == Mode::VisualBlock {
3467 update_block_vcol(ed, &motion_copy);
3468 }
3469}
3470
3471pub(crate) fn apply_motion_kind<H: crate::types::Host>(
3499 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3500 kind: crate::MotionKind,
3501 count: usize,
3502) {
3503 let count = count.max(1);
3504 match kind {
3505 crate::MotionKind::CharLeft => {
3506 execute_motion_with_block_vcol(ed, Motion::Left, count);
3507 }
3508 crate::MotionKind::CharRight => {
3509 execute_motion_with_block_vcol(ed, Motion::Right, count);
3510 }
3511 crate::MotionKind::LineDown => {
3512 execute_motion_with_block_vcol(ed, Motion::Down, count);
3513 }
3514 crate::MotionKind::LineUp => {
3515 execute_motion_with_block_vcol(ed, Motion::Up, count);
3516 }
3517 crate::MotionKind::FirstNonBlankDown => {
3518 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3523 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3524 crate::motions::move_first_non_blank(&mut ed.buffer);
3525 ed.push_buffer_cursor_to_textarea();
3526 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3527 ed.sync_buffer_from_textarea();
3528 }
3529 crate::MotionKind::FirstNonBlankUp => {
3530 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3533 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3534 crate::motions::move_first_non_blank(&mut ed.buffer);
3535 ed.push_buffer_cursor_to_textarea();
3536 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3537 ed.sync_buffer_from_textarea();
3538 }
3539 crate::MotionKind::WordForward => {
3540 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
3541 }
3542 crate::MotionKind::BigWordForward => {
3543 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
3544 }
3545 crate::MotionKind::WordBackward => {
3546 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
3547 }
3548 crate::MotionKind::BigWordBackward => {
3549 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
3550 }
3551 crate::MotionKind::WordEnd => {
3552 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
3553 }
3554 crate::MotionKind::BigWordEnd => {
3555 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
3556 }
3557 crate::MotionKind::LineStart => {
3558 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
3561 }
3562 crate::MotionKind::FirstNonBlank => {
3563 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
3566 }
3567 crate::MotionKind::GotoLine => {
3568 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
3577 }
3578 crate::MotionKind::LineEnd => {
3579 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
3583 }
3584 crate::MotionKind::FindRepeat => {
3585 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
3589 }
3590 crate::MotionKind::FindRepeatReverse => {
3591 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
3595 }
3596 crate::MotionKind::BracketMatch => {
3597 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
3602 }
3603 crate::MotionKind::ViewportTop => {
3604 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
3607 }
3608 crate::MotionKind::ViewportMiddle => {
3609 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
3612 }
3613 crate::MotionKind::ViewportBottom => {
3614 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
3617 }
3618 crate::MotionKind::HalfPageDown => {
3619 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
3623 }
3624 crate::MotionKind::HalfPageUp => {
3625 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
3628 }
3629 crate::MotionKind::FullPageDown => {
3630 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
3633 }
3634 crate::MotionKind::FullPageUp => {
3635 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
3638 }
3639 crate::MotionKind::FirstNonBlankLine => {
3640 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
3641 }
3642 crate::MotionKind::SectionBackward => {
3643 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
3644 }
3645 crate::MotionKind::SectionForward => {
3646 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
3647 }
3648 crate::MotionKind::SectionEndBackward => {
3649 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
3650 }
3651 crate::MotionKind::SectionEndForward => {
3652 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
3653 }
3654 }
3655}
3656
3657fn apply_sticky_col<H: crate::types::Host>(
3662 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3663 motion: &Motion,
3664 pre_col: usize,
3665) {
3666 if is_vertical_motion(motion) {
3667 let want = ed.sticky_col.unwrap_or(pre_col);
3668 ed.sticky_col = Some(want);
3671 let (row, _) = ed.cursor();
3672 let line_len = buf_line_chars(&ed.buffer, row);
3673 let max_col = line_len.saturating_sub(1);
3677 let target = want.min(max_col);
3678 buf_set_cursor_rc(&mut ed.buffer, row, target);
3682 } else {
3683 ed.sticky_col = Some(ed.cursor().1);
3686 }
3687}
3688
3689fn is_vertical_motion(motion: &Motion) -> bool {
3690 matches!(
3694 motion,
3695 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
3696 )
3697}
3698
3699fn apply_motion_cursor<H: crate::types::Host>(
3700 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3701 motion: &Motion,
3702 count: usize,
3703) {
3704 apply_motion_cursor_ctx(ed, motion, count, false)
3705}
3706
3707pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
3708 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3709 motion: &Motion,
3710 count: usize,
3711 as_operator: bool,
3712) {
3713 match motion {
3714 Motion::Left => {
3715 crate::motions::move_left(&mut ed.buffer, count);
3717 ed.push_buffer_cursor_to_textarea();
3718 }
3719 Motion::Right => {
3720 if as_operator {
3724 crate::motions::move_right_to_end(&mut ed.buffer, count);
3725 } else {
3726 crate::motions::move_right_in_line(&mut ed.buffer, count);
3727 }
3728 ed.push_buffer_cursor_to_textarea();
3729 }
3730 Motion::Up => {
3731 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3735 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3736 ed.push_buffer_cursor_to_textarea();
3737 }
3738 Motion::Down => {
3739 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3740 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3741 ed.push_buffer_cursor_to_textarea();
3742 }
3743 Motion::ScreenUp => {
3744 let v = *ed.host.viewport();
3745 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3746 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3747 ed.push_buffer_cursor_to_textarea();
3748 }
3749 Motion::ScreenDown => {
3750 let v = *ed.host.viewport();
3751 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3752 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3753 ed.push_buffer_cursor_to_textarea();
3754 }
3755 Motion::WordFwd => {
3756 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3757 ed.push_buffer_cursor_to_textarea();
3758 }
3759 Motion::WordBack => {
3760 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3761 ed.push_buffer_cursor_to_textarea();
3762 }
3763 Motion::WordEnd => {
3764 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3765 ed.push_buffer_cursor_to_textarea();
3766 }
3767 Motion::BigWordFwd => {
3768 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3769 ed.push_buffer_cursor_to_textarea();
3770 }
3771 Motion::BigWordBack => {
3772 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3773 ed.push_buffer_cursor_to_textarea();
3774 }
3775 Motion::BigWordEnd => {
3776 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3777 ed.push_buffer_cursor_to_textarea();
3778 }
3779 Motion::WordEndBack => {
3780 crate::motions::move_word_end_back(
3781 &mut ed.buffer,
3782 false,
3783 count,
3784 &ed.settings.iskeyword,
3785 );
3786 ed.push_buffer_cursor_to_textarea();
3787 }
3788 Motion::BigWordEndBack => {
3789 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3790 ed.push_buffer_cursor_to_textarea();
3791 }
3792 Motion::LineStart => {
3793 crate::motions::move_line_start(&mut ed.buffer);
3794 ed.push_buffer_cursor_to_textarea();
3795 }
3796 Motion::FirstNonBlank => {
3797 crate::motions::move_first_non_blank(&mut ed.buffer);
3798 ed.push_buffer_cursor_to_textarea();
3799 }
3800 Motion::LineEnd => {
3801 crate::motions::move_line_end(&mut ed.buffer);
3803 ed.push_buffer_cursor_to_textarea();
3804 }
3805 Motion::FileTop => {
3806 if count > 1 {
3809 crate::motions::move_bottom(&mut ed.buffer, count);
3810 } else {
3811 crate::motions::move_top(&mut ed.buffer);
3812 }
3813 ed.push_buffer_cursor_to_textarea();
3814 }
3815 Motion::FileBottom => {
3816 if count > 1 {
3819 crate::motions::move_bottom(&mut ed.buffer, count);
3820 } else {
3821 crate::motions::move_bottom(&mut ed.buffer, 0);
3822 }
3823 ed.push_buffer_cursor_to_textarea();
3824 }
3825 Motion::Find { ch, forward, till } => {
3826 for _ in 0..count {
3827 if !find_char_on_line(ed, *ch, *forward, *till) {
3828 break;
3829 }
3830 }
3831 }
3832 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
3834 let _ = matching_bracket(ed);
3835 }
3836 Motion::WordAtCursor {
3837 forward,
3838 whole_word,
3839 } => {
3840 word_at_cursor_search(ed, *forward, *whole_word, count);
3841 }
3842 Motion::SearchNext { reverse } => {
3843 if let Some(pattern) = ed.vim.last_search.clone() {
3847 ed.push_search_pattern(&pattern);
3848 }
3849 if ed.search_state().pattern.is_none() {
3850 return;
3851 }
3852 let forward = ed.vim.last_search_forward != *reverse;
3856 for _ in 0..count.max(1) {
3857 if forward {
3858 ed.search_advance_forward(true);
3859 } else {
3860 ed.search_advance_backward(true);
3861 }
3862 }
3863 ed.push_buffer_cursor_to_textarea();
3864 }
3865 Motion::ViewportTop => {
3866 let v = *ed.host().viewport();
3867 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
3868 ed.push_buffer_cursor_to_textarea();
3869 }
3870 Motion::ViewportMiddle => {
3871 let v = *ed.host().viewport();
3872 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
3873 ed.push_buffer_cursor_to_textarea();
3874 }
3875 Motion::ViewportBottom => {
3876 let v = *ed.host().viewport();
3877 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
3878 ed.push_buffer_cursor_to_textarea();
3879 }
3880 Motion::LastNonBlank => {
3881 crate::motions::move_last_non_blank(&mut ed.buffer);
3882 ed.push_buffer_cursor_to_textarea();
3883 }
3884 Motion::LineMiddle => {
3885 let row = ed.cursor().0;
3886 let line_chars = buf_line_chars(&ed.buffer, row);
3887 let target = line_chars / 2;
3890 ed.jump_cursor(row, target);
3891 }
3892 Motion::ParagraphPrev => {
3893 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
3894 ed.push_buffer_cursor_to_textarea();
3895 }
3896 Motion::ParagraphNext => {
3897 crate::motions::move_paragraph_next(&mut ed.buffer, count);
3898 ed.push_buffer_cursor_to_textarea();
3899 }
3900 Motion::SentencePrev => {
3901 for _ in 0..count.max(1) {
3902 if let Some((row, col)) = sentence_boundary(ed, false) {
3903 ed.jump_cursor(row, col);
3904 }
3905 }
3906 }
3907 Motion::SentenceNext => {
3908 for _ in 0..count.max(1) {
3909 if let Some((row, col)) = sentence_boundary(ed, true) {
3910 ed.jump_cursor(row, col);
3911 }
3912 }
3913 }
3914 Motion::SectionBackward => {
3915 crate::motions::move_section_backward(&mut ed.buffer, count);
3916 ed.push_buffer_cursor_to_textarea();
3917 }
3918 Motion::SectionForward => {
3919 crate::motions::move_section_forward(&mut ed.buffer, count);
3920 ed.push_buffer_cursor_to_textarea();
3921 }
3922 Motion::SectionEndBackward => {
3923 crate::motions::move_section_end_backward(&mut ed.buffer, count);
3924 ed.push_buffer_cursor_to_textarea();
3925 }
3926 Motion::SectionEndForward => {
3927 crate::motions::move_section_end_forward(&mut ed.buffer, count);
3928 ed.push_buffer_cursor_to_textarea();
3929 }
3930 Motion::FirstNonBlankNextLine => {
3931 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
3932 ed.push_buffer_cursor_to_textarea();
3933 }
3934 Motion::FirstNonBlankPrevLine => {
3935 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
3936 ed.push_buffer_cursor_to_textarea();
3937 }
3938 Motion::FirstNonBlankLine => {
3939 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
3940 ed.push_buffer_cursor_to_textarea();
3941 }
3942 Motion::GotoColumn => {
3943 crate::motions::move_goto_column(&mut ed.buffer, count);
3944 ed.push_buffer_cursor_to_textarea();
3945 }
3946 }
3947}
3948
3949fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3950 ed.sync_buffer_content_from_textarea();
3956 crate::motions::move_first_non_blank(&mut ed.buffer);
3957 ed.push_buffer_cursor_to_textarea();
3958}
3959
3960fn find_char_on_line<H: crate::types::Host>(
3961 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3962 ch: char,
3963 forward: bool,
3964 till: bool,
3965) -> bool {
3966 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
3967 if moved {
3968 ed.push_buffer_cursor_to_textarea();
3969 }
3970 moved
3971}
3972
3973fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3974 let moved = crate::motions::match_bracket(&mut ed.buffer);
3975 if moved {
3976 ed.push_buffer_cursor_to_textarea();
3977 }
3978 moved
3979}
3980
3981fn word_at_cursor_search<H: crate::types::Host>(
3982 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3983 forward: bool,
3984 whole_word: bool,
3985 count: usize,
3986) {
3987 let (row, col) = ed.cursor();
3988 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
3989 let chars: Vec<char> = line.chars().collect();
3990 if chars.is_empty() {
3991 return;
3992 }
3993 let spec = ed.settings().iskeyword.clone();
3995 let is_word = |c: char| is_keyword_char(c, &spec);
3996 let mut start = col.min(chars.len().saturating_sub(1));
3997 while start > 0 && is_word(chars[start - 1]) {
3998 start -= 1;
3999 }
4000 let mut end = start;
4001 while end < chars.len() && is_word(chars[end]) {
4002 end += 1;
4003 }
4004 if end <= start {
4005 return;
4006 }
4007 let word: String = chars[start..end].iter().collect();
4008 let escaped = regex_escape(&word);
4009 let pattern = if whole_word {
4010 format!(r"\b{escaped}\b")
4011 } else {
4012 escaped
4013 };
4014 ed.push_search_pattern(&pattern);
4015 if ed.search_state().pattern.is_none() {
4016 return;
4017 }
4018 ed.vim.last_search = Some(pattern);
4020 ed.vim.last_search_forward = forward;
4021 for _ in 0..count.max(1) {
4022 if forward {
4023 ed.search_advance_forward(true);
4024 } else {
4025 ed.search_advance_backward(true);
4026 }
4027 }
4028 ed.push_buffer_cursor_to_textarea();
4029}
4030
4031fn regex_escape(s: &str) -> String {
4032 let mut out = String::with_capacity(s.len());
4033 for c in s.chars() {
4034 if matches!(
4035 c,
4036 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
4037 ) {
4038 out.push('\\');
4039 }
4040 out.push(c);
4041 }
4042 out
4043}
4044
4045pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
4059 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4060 op: Operator,
4061 motion_key: char,
4062 total_count: usize,
4063) {
4064 let input = Input {
4065 key: Key::Char(motion_key),
4066 ctrl: false,
4067 alt: false,
4068 shift: false,
4069 };
4070 let Some(motion) = parse_motion(&input) else {
4071 return;
4072 };
4073 let motion = match motion {
4074 Motion::FindRepeat { reverse } => match ed.vim.last_find {
4075 Some((ch, forward, till)) => Motion::Find {
4076 ch,
4077 forward: if reverse { !forward } else { forward },
4078 till,
4079 },
4080 None => return,
4081 },
4082 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
4084 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
4085 m => m,
4086 };
4087 apply_op_with_motion(ed, op, &motion, total_count);
4088 if let Motion::Find { ch, forward, till } = &motion {
4089 ed.vim.last_find = Some((*ch, *forward, *till));
4090 }
4091 if !ed.vim.replaying && op_is_change(op) {
4092 ed.vim.last_change = Some(LastChange::OpMotion {
4093 op,
4094 motion,
4095 count: total_count,
4096 inserted: None,
4097 });
4098 }
4099}
4100
4101pub(crate) fn apply_op_double<H: crate::types::Host>(
4104 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4105 op: Operator,
4106 total_count: usize,
4107) {
4108 if op == Operator::Comment {
4109 let row = buf_cursor_pos(&ed.buffer).row;
4111 let end_row = (row + total_count.max(1) - 1).min(ed.buffer.row_count().saturating_sub(1));
4112 ed.toggle_comment_range(row, end_row);
4113 ed.vim.mode = Mode::Normal;
4114 if !ed.vim.replaying {
4115 ed.vim.last_change = Some(LastChange::LineOp {
4116 op,
4117 count: total_count,
4118 inserted: None,
4119 });
4120 }
4121 return;
4122 }
4123 execute_line_op(ed, op, total_count);
4124 if !ed.vim.replaying {
4125 ed.vim.last_change = Some(LastChange::LineOp {
4126 op,
4127 count: total_count,
4128 inserted: None,
4129 });
4130 }
4131}
4132
4133pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
4143 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4144 op: Operator,
4145 ch: char,
4146 total_count: usize,
4147) {
4148 if matches!(
4151 op,
4152 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
4153 ) {
4154 let op_char = match op {
4155 Operator::Uppercase => 'U',
4156 Operator::Lowercase => 'u',
4157 Operator::ToggleCase => '~',
4158 _ => unreachable!(),
4159 };
4160 if ch == op_char {
4161 execute_line_op(ed, op, total_count);
4162 if !ed.vim.replaying {
4163 ed.vim.last_change = Some(LastChange::LineOp {
4164 op,
4165 count: total_count,
4166 inserted: None,
4167 });
4168 }
4169 return;
4170 }
4171 }
4172 let motion = match ch {
4173 'g' => Motion::FileTop,
4174 'e' => Motion::WordEndBack,
4175 'E' => Motion::BigWordEndBack,
4176 'j' => Motion::ScreenDown,
4177 'k' => Motion::ScreenUp,
4178 _ => return, };
4180 apply_op_with_motion(ed, op, &motion, total_count);
4181 if !ed.vim.replaying && op_is_change(op) {
4182 ed.vim.last_change = Some(LastChange::OpMotion {
4183 op,
4184 motion,
4185 count: total_count,
4186 inserted: None,
4187 });
4188 }
4189}
4190
4191pub(crate) fn apply_after_g<H: crate::types::Host>(
4196 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4197 ch: char,
4198 count: usize,
4199) {
4200 match ch {
4201 'g' => {
4202 let pre = ed.cursor();
4204 if count > 1 {
4205 ed.jump_cursor(count - 1, 0);
4206 } else {
4207 ed.jump_cursor(0, 0);
4208 }
4209 move_first_non_whitespace(ed);
4210 ed.sticky_col = Some(ed.cursor().1);
4213 if ed.cursor() != pre {
4214 ed.push_jump(pre);
4215 }
4216 }
4217 'e' => execute_motion(ed, Motion::WordEndBack, count),
4218 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
4219 '_' => execute_motion(ed, Motion::LastNonBlank, count),
4221 'M' => execute_motion(ed, Motion::LineMiddle, count),
4223 'v' => ed.reenter_last_visual(),
4226 'j' => execute_motion(ed, Motion::ScreenDown, count),
4230 'k' => execute_motion(ed, Motion::ScreenUp, count),
4231 'U' => {
4235 ed.vim.pending = Pending::Op {
4236 op: Operator::Uppercase,
4237 count1: count,
4238 };
4239 }
4240 'u' => {
4241 ed.vim.pending = Pending::Op {
4242 op: Operator::Lowercase,
4243 count1: count,
4244 };
4245 }
4246 '~' => {
4247 ed.vim.pending = Pending::Op {
4248 op: Operator::ToggleCase,
4249 count1: count,
4250 };
4251 }
4252 'q' => {
4253 ed.vim.pending = Pending::Op {
4256 op: Operator::Reflow,
4257 count1: count,
4258 };
4259 }
4260 'w' => {
4261 ed.vim.pending = Pending::Op {
4264 op: Operator::ReflowKeepCursor,
4265 count1: count,
4266 };
4267 }
4268 'J' => {
4269 for _ in 0..count.max(1) {
4271 ed.push_undo();
4272 join_line_raw(ed);
4273 }
4274 if !ed.vim.replaying {
4275 ed.vim.last_change = Some(LastChange::JoinLine {
4276 count: count.max(1),
4277 });
4278 }
4279 }
4280 'd' => {
4281 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
4286 }
4287 'i' => {
4292 if let Some((row, col)) = ed.vim.last_insert_pos {
4293 ed.jump_cursor(row, col);
4294 }
4295 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4296 }
4297 'c' => {
4302 ed.vim.pending = Pending::Op {
4303 op: Operator::Comment,
4304 count1: count,
4305 };
4306 }
4307 ';' => walk_change_list(ed, -1, count.max(1)),
4310 ',' => walk_change_list(ed, 1, count.max(1)),
4311 '*' => execute_motion(
4315 ed,
4316 Motion::WordAtCursor {
4317 forward: true,
4318 whole_word: false,
4319 },
4320 count,
4321 ),
4322 '#' => execute_motion(
4323 ed,
4324 Motion::WordAtCursor {
4325 forward: false,
4326 whole_word: false,
4327 },
4328 count,
4329 ),
4330 '&' => {
4333 let cmd = match ed.vim.last_substitute.clone() {
4334 Some(c) => c,
4335 None => {
4336 return;
4341 }
4342 };
4343 let last_row = buf_row_count(&ed.buffer).saturating_sub(1) as u32;
4344 let r = 0u32..=last_row;
4345 let _ = crate::substitute::apply_substitute(ed, &cmd, r);
4348 ed.vim.last_substitute = Some(cmd);
4351 }
4352 _ => {}
4353 }
4354}
4355
4356pub(crate) fn apply_after_z<H: crate::types::Host>(
4361 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4362 ch: char,
4363 count: usize,
4364) {
4365 use crate::editor::CursorScrollTarget;
4366 let row = ed.cursor().0;
4367 match ch {
4368 'z' => {
4369 ed.scroll_cursor_to(CursorScrollTarget::Center);
4370 ed.vim.viewport_pinned = true;
4371 }
4372 't' => {
4373 ed.scroll_cursor_to(CursorScrollTarget::Top);
4374 ed.vim.viewport_pinned = true;
4375 }
4376 'b' => {
4377 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
4378 ed.vim.viewport_pinned = true;
4379 }
4380 'o' => {
4385 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
4386 }
4387 'c' => {
4388 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
4389 }
4390 'a' => {
4391 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
4392 }
4393 'R' => {
4394 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
4395 }
4396 'M' => {
4397 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
4398 }
4399 'E' => {
4400 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
4401 }
4402 'd' => {
4403 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
4404 }
4405 'f' => {
4406 if matches!(
4407 ed.vim.mode,
4408 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
4409 ) {
4410 let anchor_row = match ed.vim.mode {
4413 Mode::VisualLine => ed.vim.visual_line_anchor,
4414 Mode::VisualBlock => ed.vim.block_anchor.0,
4415 _ => ed.vim.visual_anchor.0,
4416 };
4417 let cur = ed.cursor().0;
4418 let top = anchor_row.min(cur);
4419 let bot = anchor_row.max(cur);
4420 ed.apply_fold_op(crate::types::FoldOp::Add {
4421 start_row: top,
4422 end_row: bot,
4423 closed: true,
4424 });
4425 ed.vim.mode = Mode::Normal;
4426 } else {
4427 ed.vim.pending = Pending::Op {
4432 op: Operator::Fold,
4433 count1: count,
4434 };
4435 }
4436 }
4437 _ => {}
4438 }
4439}
4440
4441pub(crate) fn apply_find_char<H: crate::types::Host>(
4447 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4448 ch: char,
4449 forward: bool,
4450 till: bool,
4451 count: usize,
4452) {
4453 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
4454 ed.vim.last_find = Some((ch, forward, till));
4455 ed.vim.last_horizontal_motion = LastHorizontalMotion::FindChar;
4456}
4457
4458pub(crate) fn apply_sneak<H: crate::types::Host>(
4470 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4471 c1: char,
4472 c2: char,
4473 forward: bool,
4474 count: usize,
4475) {
4476 let count = count.max(1);
4477 let (start_row, start_col) = ed.cursor();
4478 let row_count = buf_row_count(&ed.buffer);
4479
4480 let result = if forward {
4481 sneak_scan_forward(ed, start_row, start_col, c1, c2, count)
4482 } else {
4483 sneak_scan_backward(ed, start_row, start_col, c1, c2, count)
4484 };
4485
4486 if let Some((row, col)) = result {
4487 buf_set_cursor_rc(&mut ed.buffer, row, col);
4488 ed.push_buffer_cursor_to_textarea();
4489 let _ = row_count; }
4491
4492 ed.vim.last_sneak = Some(((c1, c2), forward));
4493 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4494}
4495
4496fn sneak_scan_forward<H: crate::types::Host>(
4499 ed: &Editor<hjkl_buffer::Buffer, H>,
4500 start_row: usize,
4501 start_col: usize,
4502 c1: char,
4503 c2: char,
4504 count: usize,
4505) -> Option<(usize, usize)> {
4506 let row_count = buf_row_count(&ed.buffer);
4507 let mut hits = 0usize;
4508 for row in start_row..row_count {
4509 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4510 let chars: Vec<char> = line.chars().collect();
4511 let col_start = if row == start_row { start_col + 1 } else { 0 };
4513 if col_start + 1 > chars.len() {
4514 continue;
4515 }
4516 for col in col_start..chars.len().saturating_sub(1) {
4517 if chars[col] == c1 && chars[col + 1] == c2 {
4518 hits += 1;
4519 if hits == count {
4520 return Some((row, col));
4521 }
4522 }
4523 }
4524 }
4525 None
4526}
4527
4528fn sneak_scan_backward<H: crate::types::Host>(
4531 ed: &Editor<hjkl_buffer::Buffer, H>,
4532 start_row: usize,
4533 start_col: usize,
4534 c1: char,
4535 c2: char,
4536 count: usize,
4537) -> Option<(usize, usize)> {
4538 let row_count = buf_row_count(&ed.buffer);
4539 let mut hits = 0usize;
4540 let rows_to_scan = (0..row_count).rev().skip(row_count - start_row - 1);
4542 for row in rows_to_scan {
4543 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4544 let chars: Vec<char> = line.chars().collect();
4545 let col_end = if row == start_row {
4547 start_col.saturating_sub(1)
4548 } else if chars.is_empty() {
4549 continue;
4550 } else {
4551 chars.len().saturating_sub(1)
4552 };
4553 if col_end == 0 {
4554 continue;
4555 }
4556 for col in (0..col_end).rev() {
4558 if col + 1 < chars.len() && chars[col] == c1 && chars[col + 1] == c2 {
4559 hits += 1;
4560 if hits == count {
4561 return Some((row, col));
4562 }
4563 }
4564 }
4565 }
4566 None
4567}
4568
4569pub(crate) fn apply_op_sneak<H: crate::types::Host>(
4576 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4577 op: Operator,
4578 c1: char,
4579 c2: char,
4580 forward: bool,
4581 total_count: usize,
4582) {
4583 let start = ed.cursor();
4584 let result = if forward {
4585 sneak_scan_forward(ed, start.0, start.1, c1, c2, total_count)
4586 } else {
4587 sneak_scan_backward(ed, start.0, start.1, c1, c2, total_count)
4588 };
4589 let Some(end) = result else {
4590 return;
4591 };
4592 ed.jump_cursor(end.0, end.1);
4595 let end_cur = ed.cursor();
4596 ed.jump_cursor(start.0, start.1);
4597 run_operator_over_range(ed, op, start, end_cur, RangeKind::Exclusive);
4598 ed.vim.last_sneak = Some(((c1, c2), forward));
4599 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4600 if !ed.vim.replaying && op_is_change(op) {
4601 }
4605}
4606
4607pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
4613 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4614 op: Operator,
4615 ch: char,
4616 forward: bool,
4617 till: bool,
4618 total_count: usize,
4619) {
4620 let motion = Motion::Find { ch, forward, till };
4621 apply_op_with_motion(ed, op, &motion, total_count);
4622 ed.vim.last_find = Some((ch, forward, till));
4623 if !ed.vim.replaying && op_is_change(op) {
4624 ed.vim.last_change = Some(LastChange::OpMotion {
4625 op,
4626 motion,
4627 count: total_count,
4628 inserted: None,
4629 });
4630 }
4631}
4632
4633pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
4642 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4643 op: Operator,
4644 ch: char,
4645 inner: bool,
4646 total_count: usize,
4647) -> bool {
4648 let obj = match ch {
4651 'w' => TextObject::Word { big: false },
4652 'W' => TextObject::Word { big: true },
4653 '"' | '\'' | '`' => TextObject::Quote(ch),
4654 '(' | ')' | 'b' => TextObject::Bracket('('),
4655 '[' | ']' => TextObject::Bracket('['),
4656 '{' | '}' | 'B' => TextObject::Bracket('{'),
4657 '<' | '>' => TextObject::Bracket('<'),
4658 'p' => TextObject::Paragraph,
4659 't' => TextObject::XmlTag,
4660 's' => TextObject::Sentence,
4661 _ => return false,
4662 };
4663 apply_op_with_text_object(ed, op, obj, inner, total_count.max(1));
4664 if !ed.vim.replaying && op_is_change(op) {
4665 ed.vim.last_change = Some(LastChange::OpTextObj {
4666 op,
4667 obj,
4668 inner,
4669 inserted: None,
4670 });
4671 }
4672 true
4673}
4674
4675pub(crate) fn retreat_one<H: crate::types::Host>(
4677 ed: &Editor<hjkl_buffer::Buffer, H>,
4678 pos: (usize, usize),
4679) -> (usize, usize) {
4680 let (r, c) = pos;
4681 if c > 0 {
4682 (r, c - 1)
4683 } else if r > 0 {
4684 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
4685 (r - 1, prev_len)
4686 } else {
4687 (0, 0)
4688 }
4689}
4690
4691fn begin_insert_noundo<H: crate::types::Host>(
4693 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4694 count: usize,
4695 reason: InsertReason,
4696) {
4697 let reason = if ed.vim.replaying {
4698 InsertReason::ReplayOnly
4699 } else {
4700 reason
4701 };
4702 let (row, _) = ed.cursor();
4703 ed.vim.insert_session = Some(InsertSession {
4704 count,
4705 row_min: row,
4706 row_max: row,
4707 before_rope: crate::types::Query::rope(&ed.buffer),
4708 reason,
4709 });
4710 ed.vim.mode = Mode::Insert;
4711 ed.vim.current_mode = crate::VimMode::Insert;
4713 drop_blame_if_left_normal(ed);
4714}
4715
4716pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
4719 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4720 op: Operator,
4721 motion: &Motion,
4722 count: usize,
4723) {
4724 let start = ed.cursor();
4725 apply_motion_cursor_ctx(ed, motion, count, true);
4730 let end = ed.cursor();
4731 let kind = motion_kind(motion);
4732 ed.jump_cursor(start.0, start.1);
4734
4735 if op == Operator::Comment {
4737 let top = start.0.min(end.0);
4738 let bot = start.0.max(end.0);
4739 ed.toggle_comment_range(top, bot);
4740 ed.vim.mode = Mode::Normal;
4741 return;
4742 }
4743
4744 run_operator_over_range(ed, op, start, end, kind);
4745}
4746
4747fn apply_op_with_text_object<H: crate::types::Host>(
4748 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4749 op: Operator,
4750 obj: TextObject,
4751 inner: bool,
4752 count: usize,
4753) {
4754 let Some((start, end, kind)) = text_object_range(ed, obj, inner, count) else {
4755 return;
4756 };
4757 ed.jump_cursor(start.0, start.1);
4758 run_operator_over_range(ed, op, start, end, kind);
4759}
4760
4761fn motion_kind(motion: &Motion) -> RangeKind {
4762 match motion {
4763 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
4764 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
4765 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
4766 RangeKind::Linewise
4767 }
4768 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
4769 RangeKind::Inclusive
4770 }
4771 Motion::Find { .. } => RangeKind::Inclusive,
4772 Motion::MatchBracket => RangeKind::Inclusive,
4773 Motion::LineEnd => RangeKind::Inclusive,
4775 Motion::FirstNonBlankNextLine
4777 | Motion::FirstNonBlankPrevLine
4778 | Motion::FirstNonBlankLine => RangeKind::Linewise,
4779 Motion::SectionBackward
4781 | Motion::SectionForward
4782 | Motion::SectionEndBackward
4783 | Motion::SectionEndForward => RangeKind::Exclusive,
4784 _ => RangeKind::Exclusive,
4785 }
4786}
4787
4788fn change_linewise_rows<H: crate::types::Host>(
4797 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4798 top_row: usize,
4799 end_row: usize,
4800) {
4801 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4802 ed.vim.change_mark_start = Some((top_row, 0));
4804 ed.push_undo();
4805 ed.sync_buffer_content_from_textarea();
4806 let payload = read_vim_range(ed, (top_row, 0), (end_row, 0), RangeKind::Linewise);
4808 if end_row > top_row {
4810 ed.mutate_edit(Edit::DeleteRange {
4811 start: Position::new(top_row + 1, 0),
4812 end: Position::new(end_row, 0),
4813 kind: BufKind::Line,
4814 });
4815 }
4816 let indent_chars = if ed.settings.autoindent {
4819 let line = hjkl_buffer::rope_line_str(&crate::types::Query::rope(&ed.buffer), top_row);
4820 line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
4821 } else {
4822 0
4823 };
4824 let line_chars = buf_line_chars(&ed.buffer, top_row);
4825 if line_chars > indent_chars {
4826 ed.mutate_edit(Edit::DeleteRange {
4827 start: Position::new(top_row, indent_chars),
4828 end: Position::new(top_row, line_chars),
4829 kind: BufKind::Char,
4830 });
4831 }
4832 if !payload.is_empty() {
4833 ed.record_yank_to_host(payload.clone());
4834 ed.record_delete(payload, true);
4835 }
4836 buf_set_cursor_rc(&mut ed.buffer, top_row, indent_chars);
4837 ed.push_buffer_cursor_to_textarea();
4838 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4839}
4840
4841fn run_operator_over_range<H: crate::types::Host>(
4842 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4843 op: Operator,
4844 start: (usize, usize),
4845 end: (usize, usize),
4846 kind: RangeKind,
4847) {
4848 let (top, bot) = order(start, end);
4849 if top == bot && !matches!(kind, RangeKind::Linewise) {
4853 return;
4854 }
4855
4856 match op {
4857 Operator::Yank => {
4858 let text = read_vim_range(ed, top, bot, kind);
4859 if !text.is_empty() {
4860 ed.record_yank_to_host(text.clone());
4861 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
4862 }
4863 let rbr = match kind {
4867 RangeKind::Linewise => {
4868 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
4869 (bot.0, last_col)
4870 }
4871 RangeKind::Inclusive => (bot.0, bot.1),
4872 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
4873 };
4874 ed.set_mark('[', top);
4875 ed.set_mark(']', rbr);
4876 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4877 ed.push_buffer_cursor_to_textarea();
4878 }
4879 Operator::Delete => {
4880 ed.push_undo();
4881 cut_vim_range(ed, top, bot, kind);
4882 if !matches!(kind, RangeKind::Linewise) {
4887 clamp_cursor_to_normal_mode(ed);
4888 }
4889 ed.vim.mode = Mode::Normal;
4890 let pos = ed.cursor();
4894 ed.set_mark('[', pos);
4895 ed.set_mark(']', pos);
4896 }
4897 Operator::Change => {
4898 if matches!(kind, RangeKind::Linewise) {
4903 change_linewise_rows(ed, top.0, bot.0);
4907 } else {
4908 ed.vim.change_mark_start = Some(top);
4910 ed.push_undo();
4911 cut_vim_range(ed, top, bot, kind);
4912 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4913 }
4914 }
4915 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4916 apply_case_op_to_selection(ed, op, top, bot, kind);
4917 }
4918 Operator::Indent | Operator::Outdent => {
4919 ed.push_undo();
4922 if op == Operator::Indent {
4923 indent_rows(ed, top.0, bot.0, 1);
4924 } else {
4925 outdent_rows(ed, top.0, bot.0, 1);
4926 }
4927 ed.vim.mode = Mode::Normal;
4928 }
4929 Operator::Fold => {
4930 if bot.0 >= top.0 {
4934 ed.apply_fold_op(crate::types::FoldOp::Add {
4935 start_row: top.0,
4936 end_row: bot.0,
4937 closed: true,
4938 });
4939 }
4940 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4941 ed.push_buffer_cursor_to_textarea();
4942 ed.vim.mode = Mode::Normal;
4943 }
4944 Operator::Reflow => {
4945 ed.push_undo();
4946 reflow_rows(ed, top.0, bot.0);
4947 ed.vim.mode = Mode::Normal;
4948 }
4949 Operator::ReflowKeepCursor => {
4950 let saved = ed.cursor();
4953 ed.push_undo();
4954 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
4955 let (new_row, new_col) = reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
4956 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
4957 ed.push_buffer_cursor_to_textarea();
4958 ed.sticky_col = Some(new_col);
4959 ed.vim.mode = Mode::Normal;
4960 }
4961 Operator::AutoIndent => {
4962 ed.push_undo();
4964 auto_indent_rows(ed, top.0, bot.0);
4965 ed.vim.mode = Mode::Normal;
4966 }
4967 Operator::Filter => {
4968 }
4973 Operator::Comment => {
4974 }
4977 }
4978}
4979
4980pub(crate) fn delete_range_bridge<H: crate::types::Host>(
4997 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4998 start: (usize, usize),
4999 end: (usize, usize),
5000 kind: RangeKind,
5001 register: char,
5002) {
5003 ed.vim.pending_register = Some(register);
5004 run_operator_over_range(ed, Operator::Delete, start, end, kind);
5005}
5006
5007pub(crate) fn yank_range_bridge<H: crate::types::Host>(
5010 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5011 start: (usize, usize),
5012 end: (usize, usize),
5013 kind: RangeKind,
5014 register: char,
5015) {
5016 ed.vim.pending_register = Some(register);
5017 run_operator_over_range(ed, Operator::Yank, start, end, kind);
5018}
5019
5020pub(crate) fn change_range_bridge<H: crate::types::Host>(
5025 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5026 start: (usize, usize),
5027 end: (usize, usize),
5028 kind: RangeKind,
5029 register: char,
5030) {
5031 ed.vim.pending_register = Some(register);
5032 run_operator_over_range(ed, Operator::Change, start, end, kind);
5033}
5034
5035pub(crate) fn indent_range_bridge<H: crate::types::Host>(
5040 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5041 start: (usize, usize),
5042 end: (usize, usize),
5043 count: i32,
5044 shiftwidth: u32,
5045) {
5046 if count == 0 {
5047 return;
5048 }
5049 let (top_row, bot_row) = if start.0 <= end.0 {
5050 (start.0, end.0)
5051 } else {
5052 (end.0, start.0)
5053 };
5054 let original_sw = ed.settings().shiftwidth;
5056 if shiftwidth > 0 {
5057 ed.settings_mut().shiftwidth = shiftwidth as usize;
5058 }
5059 ed.push_undo();
5060 let abs_count = count.unsigned_abs() as usize;
5061 if count > 0 {
5062 indent_rows(ed, top_row, bot_row, abs_count);
5063 } else {
5064 outdent_rows(ed, top_row, bot_row, abs_count);
5065 }
5066 if shiftwidth > 0 {
5067 ed.settings_mut().shiftwidth = original_sw;
5068 }
5069 ed.vim.mode = Mode::Normal;
5070}
5071
5072pub(crate) fn case_range_bridge<H: crate::types::Host>(
5076 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5077 start: (usize, usize),
5078 end: (usize, usize),
5079 kind: RangeKind,
5080 op: Operator,
5081) {
5082 match op {
5083 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
5084 _ => return,
5085 }
5086 let (top, bot) = order(start, end);
5087 apply_case_op_to_selection(ed, op, top, bot, kind);
5088}
5089
5090pub(crate) fn delete_block_bridge<H: crate::types::Host>(
5111 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5112 top_row: usize,
5113 bot_row: usize,
5114 left_col: usize,
5115 right_col: usize,
5116 register: char,
5117) {
5118 ed.vim.pending_register = Some(register);
5119 let saved_anchor = ed.vim.block_anchor;
5120 let saved_vcol = ed.vim.block_vcol;
5121 ed.vim.block_anchor = (top_row, left_col);
5122 ed.vim.block_vcol = right_col;
5123 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5125 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5127 apply_block_operator(ed, Operator::Delete);
5128 ed.vim.block_anchor = saved_anchor;
5132 ed.vim.block_vcol = saved_vcol;
5133}
5134
5135pub(crate) fn yank_block_bridge<H: crate::types::Host>(
5137 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5138 top_row: usize,
5139 bot_row: usize,
5140 left_col: usize,
5141 right_col: usize,
5142 register: char,
5143) {
5144 ed.vim.pending_register = Some(register);
5145 let saved_anchor = ed.vim.block_anchor;
5146 let saved_vcol = ed.vim.block_vcol;
5147 ed.vim.block_anchor = (top_row, left_col);
5148 ed.vim.block_vcol = right_col;
5149 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5150 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5151 apply_block_operator(ed, Operator::Yank);
5152 ed.vim.block_anchor = saved_anchor;
5153 ed.vim.block_vcol = saved_vcol;
5154}
5155
5156pub(crate) fn change_block_bridge<H: crate::types::Host>(
5159 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5160 top_row: usize,
5161 bot_row: usize,
5162 left_col: usize,
5163 right_col: usize,
5164 register: char,
5165) {
5166 ed.vim.pending_register = Some(register);
5167 let saved_anchor = ed.vim.block_anchor;
5168 let saved_vcol = ed.vim.block_vcol;
5169 ed.vim.block_anchor = (top_row, left_col);
5170 ed.vim.block_vcol = right_col;
5171 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5172 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5173 apply_block_operator(ed, Operator::Change);
5174 ed.vim.block_anchor = saved_anchor;
5175 ed.vim.block_vcol = saved_vcol;
5176}
5177
5178pub(crate) fn indent_block_bridge<H: crate::types::Host>(
5182 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5183 top_row: usize,
5184 bot_row: usize,
5185 count: i32,
5186) {
5187 if count == 0 {
5188 return;
5189 }
5190 ed.push_undo();
5191 let abs = count.unsigned_abs() as usize;
5192 if count > 0 {
5193 indent_rows(ed, top_row, bot_row, abs);
5194 } else {
5195 outdent_rows(ed, top_row, bot_row, abs);
5196 }
5197 ed.vim.mode = Mode::Normal;
5198}
5199
5200pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
5204 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5205 start: (usize, usize),
5206 end: (usize, usize),
5207) {
5208 let (top_row, bot_row) = if start.0 <= end.0 {
5209 (start.0, end.0)
5210 } else {
5211 (end.0, start.0)
5212 };
5213 ed.push_undo();
5214 auto_indent_rows(ed, top_row, bot_row);
5215 ed.vim.mode = Mode::Normal;
5216}
5217
5218pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
5229 ed: &Editor<hjkl_buffer::Buffer, H>,
5230) -> Option<((usize, usize), (usize, usize))> {
5231 word_text_object(ed, true, false)
5232}
5233
5234pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
5237 ed: &Editor<hjkl_buffer::Buffer, H>,
5238) -> Option<((usize, usize), (usize, usize))> {
5239 word_text_object(ed, false, false)
5240}
5241
5242pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
5245 ed: &Editor<hjkl_buffer::Buffer, H>,
5246) -> Option<((usize, usize), (usize, usize))> {
5247 word_text_object(ed, true, true)
5248}
5249
5250pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
5253 ed: &Editor<hjkl_buffer::Buffer, H>,
5254) -> Option<((usize, usize), (usize, usize))> {
5255 word_text_object(ed, false, true)
5256}
5257
5258pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
5274 ed: &Editor<hjkl_buffer::Buffer, H>,
5275 quote: char,
5276) -> Option<((usize, usize), (usize, usize))> {
5277 quote_text_object(ed, quote, true)
5278}
5279
5280pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
5283 ed: &Editor<hjkl_buffer::Buffer, H>,
5284 quote: char,
5285) -> Option<((usize, usize), (usize, usize))> {
5286 quote_text_object(ed, quote, false)
5287}
5288
5289pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
5297 ed: &Editor<hjkl_buffer::Buffer, H>,
5298 open: char,
5299) -> Option<((usize, usize), (usize, usize))> {
5300 bracket_text_object(ed, open, true, 1).map(|(s, e, _kind)| (s, e))
5301}
5302
5303pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
5307 ed: &Editor<hjkl_buffer::Buffer, H>,
5308 open: char,
5309) -> Option<((usize, usize), (usize, usize))> {
5310 bracket_text_object(ed, open, false, 1).map(|(s, e, _kind)| (s, e))
5311}
5312
5313pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
5318 ed: &Editor<hjkl_buffer::Buffer, H>,
5319) -> Option<((usize, usize), (usize, usize))> {
5320 sentence_text_object(ed, true)
5321}
5322
5323pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
5326 ed: &Editor<hjkl_buffer::Buffer, H>,
5327) -> Option<((usize, usize), (usize, usize))> {
5328 sentence_text_object(ed, false)
5329}
5330
5331pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
5336 ed: &Editor<hjkl_buffer::Buffer, H>,
5337) -> Option<((usize, usize), (usize, usize))> {
5338 paragraph_text_object(ed, true)
5339}
5340
5341pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
5344 ed: &Editor<hjkl_buffer::Buffer, H>,
5345) -> Option<((usize, usize), (usize, usize))> {
5346 paragraph_text_object(ed, false)
5347}
5348
5349pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
5355 ed: &Editor<hjkl_buffer::Buffer, H>,
5356) -> Option<((usize, usize), (usize, usize))> {
5357 tag_text_object(ed, true)
5358}
5359
5360pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
5363 ed: &Editor<hjkl_buffer::Buffer, H>,
5364) -> Option<((usize, usize), (usize, usize))> {
5365 tag_text_object(ed, false)
5366}
5367
5368pub(crate) fn rope_line_to_str(rope: &ropey::Rope, r: usize) -> String {
5373 let s = rope.line(r).to_string();
5374 if s.ends_with('\n') {
5376 s[..s.len() - 1].to_string()
5377 } else {
5378 s
5379 }
5380}
5381
5382pub(crate) fn rope_row_range_str(rope: &ropey::Rope, lo: usize, hi: usize) -> String {
5385 let n = rope.len_lines();
5386 let lo = lo.min(n.saturating_sub(1));
5387 let hi = hi.min(n.saturating_sub(1));
5388 if lo > hi {
5389 return String::new();
5390 }
5391 let start_byte = rope.line_to_byte(lo);
5393 let end_byte = if hi + 1 < n {
5396 rope.line_to_byte(hi + 1).saturating_sub(1)
5399 } else {
5400 rope.len_bytes()
5401 };
5402 rope.byte_slice(start_byte..end_byte).to_string()
5403}
5404
5405pub(crate) fn rope_to_lines_vec(rope: &ropey::Rope) -> Vec<String> {
5409 let n = rope.len_lines();
5410 (0..n).map(|r| rope_line_to_str(rope, r)).collect()
5411}
5412
5413fn greedy_wrap(original: &[String], width: usize) -> Vec<String> {
5417 let mut wrapped: Vec<String> = Vec::new();
5418 let mut paragraph: Vec<String> = Vec::new();
5419 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
5420 if para.is_empty() {
5421 return;
5422 }
5423 let words = para.join(" ");
5424 let mut current = String::new();
5425 for word in words.split_whitespace() {
5426 let extra = if current.is_empty() {
5427 word.chars().count()
5428 } else {
5429 current.chars().count() + 1 + word.chars().count()
5430 };
5431 if extra > width && !current.is_empty() {
5432 out.push(std::mem::take(&mut current));
5433 current.push_str(word);
5434 } else if current.is_empty() {
5435 current.push_str(word);
5436 } else {
5437 current.push(' ');
5438 current.push_str(word);
5439 }
5440 }
5441 if !current.is_empty() {
5442 out.push(current);
5443 }
5444 para.clear();
5445 };
5446 for line in original {
5447 if line.trim().is_empty() {
5448 flush(&mut paragraph, &mut wrapped, width);
5449 wrapped.push(String::new());
5450 } else {
5451 paragraph.push(line.clone());
5452 }
5453 }
5454 flush(&mut paragraph, &mut wrapped, width);
5455 wrapped
5456}
5457
5458fn reflow_rows<H: crate::types::Host>(
5464 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5465 top: usize,
5466 bot: usize,
5467) {
5468 let width = ed.settings().textwidth.max(1);
5469 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5470 let bot = bot.min(lines.len().saturating_sub(1));
5471 if top > bot {
5472 return;
5473 }
5474 let original = lines[top..=bot].to_vec();
5475 let wrapped = greedy_wrap(&original, width);
5476
5477 let after: Vec<String> = lines.split_off(bot + 1);
5479 lines.truncate(top);
5480 lines.extend(wrapped);
5481 lines.extend(after);
5482 ed.restore(lines, (top, 0));
5483 ed.mark_content_dirty();
5484}
5485
5486fn reflow_rows_keep_cursor<H: crate::types::Host>(
5490 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5491 top: usize,
5492 bot: usize,
5493) -> (Vec<String>, Vec<String>) {
5494 let width = ed.settings().textwidth.max(1);
5495 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5496 let bot = bot.min(lines.len().saturating_sub(1));
5497 if top > bot {
5498 return (Vec::new(), Vec::new());
5499 }
5500 let original = lines[top..=bot].to_vec();
5501 let wrapped = greedy_wrap(&original, width);
5502
5503 let after: Vec<String> = lines.split_off(bot + 1);
5504 lines.truncate(top);
5505 lines.extend(wrapped.clone());
5506 lines.extend(after);
5507 ed.restore(lines, (top, 0));
5508 ed.mark_content_dirty();
5509 (original, wrapped)
5510}
5511
5512fn reflow_keep_cursor(
5524 top: usize,
5525 cursor_row: usize,
5526 cursor_col: usize,
5527 before_lines: &[String],
5528 after_lines: &[String],
5529) -> (usize, usize) {
5530 let relative_row = cursor_row.saturating_sub(top);
5550 let mut char_offset: usize = 0;
5551 for (i, line) in before_lines.iter().enumerate() {
5552 if i == relative_row {
5553 let line_len = line.chars().count();
5555 char_offset += cursor_col.min(line_len);
5556 break;
5557 }
5558 char_offset += line.chars().count() + 1;
5560 }
5561
5562 let mut remaining = char_offset;
5564 for (i, line) in after_lines.iter().enumerate() {
5565 let len = line.chars().count();
5566 if remaining <= len {
5567 let col = remaining.min(if len == 0 { 0 } else { len.saturating_sub(1) });
5569 return (top + i, col);
5570 }
5571 remaining = remaining.saturating_sub(len + 1);
5573 }
5574
5575 let last = after_lines.len().saturating_sub(1);
5577 let last_len = after_lines
5578 .get(last)
5579 .map(|l| l.chars().count())
5580 .unwrap_or(0);
5581 let col = if last_len == 0 { 0 } else { last_len - 1 };
5582 (top + last, col)
5583}
5584
5585fn apply_case_op_to_selection<H: crate::types::Host>(
5591 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5592 op: Operator,
5593 top: (usize, usize),
5594 bot: (usize, usize),
5595 kind: RangeKind,
5596) {
5597 use hjkl_buffer::Edit;
5598 ed.push_undo();
5599 let saved_yank = ed.yank().to_string();
5600 let saved_yank_linewise = ed.vim.yank_linewise;
5601 let selection = cut_vim_range(ed, top, bot, kind);
5602 let transformed = match op {
5603 Operator::Uppercase => selection.to_uppercase(),
5604 Operator::Lowercase => selection.to_lowercase(),
5605 Operator::ToggleCase => toggle_case_str(&selection),
5606 _ => unreachable!(),
5607 };
5608 if !transformed.is_empty() {
5609 let cursor = buf_cursor_pos(&ed.buffer);
5610 ed.mutate_edit(Edit::InsertStr {
5611 at: cursor,
5612 text: transformed,
5613 });
5614 }
5615 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5616 ed.push_buffer_cursor_to_textarea();
5617 ed.set_yank(saved_yank);
5618 ed.vim.yank_linewise = saved_yank_linewise;
5619 ed.vim.mode = Mode::Normal;
5620}
5621
5622fn indent_rows<H: crate::types::Host>(
5627 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5628 top: usize,
5629 bot: usize,
5630 count: usize,
5631) {
5632 ed.sync_buffer_content_from_textarea();
5633 let width = ed.settings().shiftwidth * count.max(1);
5634 let pad: String = " ".repeat(width);
5635 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5636 let bot = bot.min(lines.len().saturating_sub(1));
5637 for line in lines.iter_mut().take(bot + 1).skip(top) {
5638 if !line.is_empty() {
5639 line.insert_str(0, &pad);
5640 }
5641 }
5642 ed.restore(lines, (top, 0));
5645 move_first_non_whitespace(ed);
5646}
5647
5648fn outdent_rows<H: crate::types::Host>(
5652 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5653 top: usize,
5654 bot: usize,
5655 count: usize,
5656) {
5657 ed.sync_buffer_content_from_textarea();
5658 let width = ed.settings().shiftwidth * count.max(1);
5659 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5660 let bot = bot.min(lines.len().saturating_sub(1));
5661 for line in lines.iter_mut().take(bot + 1).skip(top) {
5662 let strip: usize = line
5663 .chars()
5664 .take(width)
5665 .take_while(|c| *c == ' ' || *c == '\t')
5666 .count();
5667 if strip > 0 {
5668 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
5669 line.drain(..byte_len);
5670 }
5671 }
5672 ed.restore(lines, (top, 0));
5673 move_first_non_whitespace(ed);
5674}
5675
5676fn bracket_net(line: &str) -> i32 {
5703 let mut net: i32 = 0;
5704 let mut chars = line.chars().peekable();
5705 while let Some(ch) = chars.next() {
5706 match ch {
5707 '/' if chars.peek() == Some(&'/') => return net,
5709 '"' => {
5710 while let Some(c) = chars.next() {
5712 match c {
5713 '\\' => {
5714 chars.next();
5715 } '"' => break,
5717 _ => {}
5718 }
5719 }
5720 }
5721 '\'' => {
5722 let saved: Vec<char> = chars.clone().take(5).collect();
5731 let close_idx = if saved.first() == Some(&'\\') {
5732 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
5733 } else {
5734 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
5735 };
5736 if let Some(idx) = close_idx {
5737 for _ in 0..=idx {
5738 chars.next();
5739 }
5740 }
5741 }
5743 '{' | '(' | '[' => net += 1,
5744 '}' | ')' | ']' => net -= 1,
5745 _ => {}
5746 }
5747 }
5748 net
5749}
5750
5751fn auto_indent_rows<H: crate::types::Host>(
5773 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5774 top: usize,
5775 bot: usize,
5776) {
5777 ed.sync_buffer_content_from_textarea();
5778 let shiftwidth = ed.settings().shiftwidth;
5779 let expandtab = ed.settings().expandtab;
5780 let indent_unit: String = if expandtab {
5781 " ".repeat(shiftwidth)
5782 } else {
5783 "\t".to_string()
5784 };
5785
5786 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5787 let bot = bot.min(lines.len().saturating_sub(1));
5788
5789 let mut depth: i32 = 0;
5792 for line in lines.iter().take(top) {
5793 depth += bracket_net(line);
5794 if depth < 0 {
5795 depth = 0;
5796 }
5797 }
5798
5799 for line in lines.iter_mut().take(bot + 1).skip(top) {
5800 let trimmed_owned = line.trim_start().to_owned();
5801 if trimmed_owned.is_empty() {
5803 *line = String::new();
5804 continue;
5806 }
5807
5808 let starts_with_close = trimmed_owned
5810 .chars()
5811 .next()
5812 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
5813 let starts_with_dot = trimmed_owned.starts_with('.')
5823 && !trimmed_owned.starts_with("..")
5824 && !trimmed_owned.starts_with(".;");
5825 let effective_depth = if starts_with_close {
5826 depth.saturating_sub(1)
5827 } else if starts_with_dot {
5828 depth.saturating_add(1)
5829 } else {
5830 depth
5831 } as usize;
5832
5833 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
5835
5836 depth += bracket_net(&trimmed_owned);
5838 if depth < 0 {
5839 depth = 0;
5840 }
5841
5842 *line = new_line;
5843 }
5844
5845 ed.restore(lines, (top, 0));
5847 move_first_non_whitespace(ed);
5848 ed.last_indent_range = Some((top, bot));
5850}
5851
5852fn toggle_case_str(s: &str) -> String {
5853 s.chars()
5854 .map(|c| {
5855 if c.is_lowercase() {
5856 c.to_uppercase().next().unwrap_or(c)
5857 } else if c.is_uppercase() {
5858 c.to_lowercase().next().unwrap_or(c)
5859 } else {
5860 c
5861 }
5862 })
5863 .collect()
5864}
5865
5866fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
5867 if a <= b { (a, b) } else { (b, a) }
5868}
5869
5870fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5875 let (row, col) = ed.cursor();
5876 let line_chars = buf_line_chars(&ed.buffer, row);
5877 let max_col = line_chars.saturating_sub(1);
5878 if col > max_col {
5879 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
5880 ed.push_buffer_cursor_to_textarea();
5881 }
5882}
5883
5884fn execute_line_op<H: crate::types::Host>(
5887 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5888 op: Operator,
5889 count: usize,
5890) {
5891 let (row, col) = ed.cursor();
5892 let total = buf_row_count(&ed.buffer);
5893 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
5894
5895 match op {
5896 Operator::Yank => {
5897 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
5899 if !text.is_empty() {
5900 ed.record_yank_to_host(text.clone());
5901 ed.record_yank(text, true);
5902 }
5903 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
5906 ed.set_mark('[', (row, 0));
5907 ed.set_mark(']', (end_row, last_col));
5908 buf_set_cursor_rc(&mut ed.buffer, row, col);
5909 ed.push_buffer_cursor_to_textarea();
5910 ed.vim.mode = Mode::Normal;
5911 }
5912 Operator::Delete => {
5913 ed.push_undo();
5914 let deleted_through_last = end_row + 1 >= total;
5915 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
5916 let total_after = buf_row_count(&ed.buffer);
5920 let raw_target = if deleted_through_last {
5921 row.saturating_sub(1).min(total_after.saturating_sub(1))
5922 } else {
5923 row.min(total_after.saturating_sub(1))
5924 };
5925 let target_row = if raw_target > 0
5931 && raw_target + 1 == total_after
5932 && buf_line(&ed.buffer, raw_target)
5933 .map(|s| s.is_empty())
5934 .unwrap_or(false)
5935 {
5936 raw_target - 1
5937 } else {
5938 raw_target
5939 };
5940 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5941 ed.push_buffer_cursor_to_textarea();
5942 move_first_non_whitespace(ed);
5943 ed.sticky_col = Some(ed.cursor().1);
5944 ed.vim.mode = Mode::Normal;
5945 let pos = ed.cursor();
5948 ed.set_mark('[', pos);
5949 ed.set_mark(']', pos);
5950 }
5951 Operator::Change => {
5952 change_linewise_rows(ed, row, end_row);
5956 }
5957 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5958 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
5962 move_first_non_whitespace(ed);
5965 }
5966 Operator::Indent | Operator::Outdent => {
5967 ed.push_undo();
5969 if op == Operator::Indent {
5970 indent_rows(ed, row, end_row, 1);
5971 } else {
5972 outdent_rows(ed, row, end_row, 1);
5973 }
5974 ed.sticky_col = Some(ed.cursor().1);
5975 ed.vim.mode = Mode::Normal;
5976 }
5977 Operator::Fold => unreachable!("Fold has no line-op double"),
5979 Operator::Reflow => {
5980 ed.push_undo();
5982 reflow_rows(ed, row, end_row);
5983 move_first_non_whitespace(ed);
5984 ed.sticky_col = Some(ed.cursor().1);
5985 ed.vim.mode = Mode::Normal;
5986 }
5987 Operator::ReflowKeepCursor => {
5988 let saved = ed.cursor();
5991 ed.push_undo();
5992 let (before, after) = reflow_rows_keep_cursor(ed, row, end_row);
5993 let (new_row, new_col) = reflow_keep_cursor(row, saved.0, saved.1, &before, &after);
5994 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
5995 ed.push_buffer_cursor_to_textarea();
5996 ed.sticky_col = Some(new_col);
5997 ed.vim.mode = Mode::Normal;
5998 }
5999 Operator::AutoIndent => {
6000 ed.push_undo();
6002 auto_indent_rows(ed, row, end_row);
6003 ed.sticky_col = Some(ed.cursor().1);
6004 ed.vim.mode = Mode::Normal;
6005 }
6006 Operator::Filter => {
6007 }
6009 Operator::Comment => {
6010 }
6015 }
6016}
6017
6018pub(crate) fn apply_visual_operator<H: crate::types::Host>(
6021 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6022 op: Operator,
6023) {
6024 match ed.vim.mode {
6025 Mode::VisualLine => {
6026 let cursor_row = buf_cursor_pos(&ed.buffer).row;
6027 let top = cursor_row.min(ed.vim.visual_line_anchor);
6028 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6029 ed.vim.yank_linewise = true;
6030 match op {
6031 Operator::Yank => {
6032 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6033 if !text.is_empty() {
6034 ed.record_yank_to_host(text.clone());
6035 ed.record_yank(text, true);
6036 }
6037 buf_set_cursor_rc(&mut ed.buffer, top, 0);
6038 ed.push_buffer_cursor_to_textarea();
6039 ed.vim.mode = Mode::Normal;
6040 }
6041 Operator::Delete => {
6042 ed.push_undo();
6043 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6044 ed.vim.mode = Mode::Normal;
6045 }
6046 Operator::Change => {
6047 change_linewise_rows(ed, top, bot);
6050 }
6051 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
6052 let bot = buf_cursor_pos(&ed.buffer)
6053 .row
6054 .max(ed.vim.visual_line_anchor);
6055 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
6056 move_first_non_whitespace(ed);
6057 }
6058 Operator::Indent | Operator::Outdent => {
6059 ed.push_undo();
6060 let (cursor_row, _) = ed.cursor();
6061 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6062 if op == Operator::Indent {
6063 indent_rows(ed, top, bot, 1);
6064 } else {
6065 outdent_rows(ed, top, bot, 1);
6066 }
6067 ed.vim.mode = Mode::Normal;
6068 }
6069 Operator::Reflow => {
6070 ed.push_undo();
6071 let (cursor_row, _) = ed.cursor();
6072 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6073 reflow_rows(ed, top, bot);
6074 ed.vim.mode = Mode::Normal;
6075 }
6076 Operator::ReflowKeepCursor => {
6077 let saved = ed.cursor();
6078 ed.push_undo();
6079 let (cursor_row, _) = ed.cursor();
6080 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6081 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6082 let (new_row, new_col) =
6083 reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6084 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6085 ed.push_buffer_cursor_to_textarea();
6086 ed.vim.mode = Mode::Normal;
6087 }
6088 Operator::AutoIndent => {
6089 ed.push_undo();
6090 let (cursor_row, _) = ed.cursor();
6091 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6092 auto_indent_rows(ed, top, bot);
6093 ed.vim.mode = Mode::Normal;
6094 }
6095 Operator::Filter => {}
6097 Operator::Comment => {}
6099 Operator::Fold => unreachable!("Visual zf takes its own path"),
6102 }
6103 }
6104 Mode::Visual => {
6105 ed.vim.yank_linewise = false;
6106 let anchor = ed.vim.visual_anchor;
6107 let cursor = ed.cursor();
6108 let (top, bot) = order(anchor, cursor);
6109 match op {
6110 Operator::Yank => {
6111 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
6112 if !text.is_empty() {
6113 ed.record_yank_to_host(text.clone());
6114 ed.record_yank(text, false);
6115 }
6116 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6117 ed.push_buffer_cursor_to_textarea();
6118 ed.vim.mode = Mode::Normal;
6119 }
6120 Operator::Delete => {
6121 ed.push_undo();
6122 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6123 ed.vim.mode = Mode::Normal;
6124 }
6125 Operator::Change => {
6126 ed.push_undo();
6127 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6128 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
6129 }
6130 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
6131 let anchor = ed.vim.visual_anchor;
6133 let cursor = ed.cursor();
6134 let (top, bot) = order(anchor, cursor);
6135 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
6136 }
6137 Operator::Indent | Operator::Outdent => {
6138 ed.push_undo();
6139 let anchor = ed.vim.visual_anchor;
6140 let cursor = ed.cursor();
6141 let (top, bot) = order(anchor, cursor);
6142 if op == Operator::Indent {
6143 indent_rows(ed, top.0, bot.0, 1);
6144 } else {
6145 outdent_rows(ed, top.0, bot.0, 1);
6146 }
6147 ed.vim.mode = Mode::Normal;
6148 }
6149 Operator::Reflow => {
6150 ed.push_undo();
6151 let anchor = ed.vim.visual_anchor;
6152 let cursor = ed.cursor();
6153 let (top, bot) = order(anchor, cursor);
6154 reflow_rows(ed, top.0, bot.0);
6155 ed.vim.mode = Mode::Normal;
6156 }
6157 Operator::ReflowKeepCursor => {
6158 let saved = ed.cursor();
6159 ed.push_undo();
6160 let anchor = ed.vim.visual_anchor;
6161 let cursor = ed.cursor();
6162 let (top, bot) = order(anchor, cursor);
6163 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
6164 let (new_row, new_col) =
6165 reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
6166 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6167 ed.push_buffer_cursor_to_textarea();
6168 ed.vim.mode = Mode::Normal;
6169 }
6170 Operator::AutoIndent => {
6171 ed.push_undo();
6172 let anchor = ed.vim.visual_anchor;
6173 let cursor = ed.cursor();
6174 let (top, bot) = order(anchor, cursor);
6175 auto_indent_rows(ed, top.0, bot.0);
6176 ed.vim.mode = Mode::Normal;
6177 }
6178 Operator::Filter => {}
6180 Operator::Comment => {}
6182 Operator::Fold => unreachable!("Visual zf takes its own path"),
6183 }
6184 }
6185 Mode::VisualBlock => apply_block_operator(ed, op),
6186 _ => {}
6187 }
6188}
6189
6190fn block_bounds<H: crate::types::Host>(
6195 ed: &Editor<hjkl_buffer::Buffer, H>,
6196) -> (usize, usize, usize, usize) {
6197 let (ar, ac) = ed.vim.block_anchor;
6198 let (cr, _) = ed.cursor();
6199 let cc = ed.vim.block_vcol;
6200 let top = ar.min(cr);
6201 let bot = ar.max(cr);
6202 let left = ac.min(cc);
6203 let right = ac.max(cc);
6204 (top, bot, left, right)
6205}
6206
6207pub(crate) fn update_block_vcol<H: crate::types::Host>(
6212 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6213 motion: &Motion,
6214) {
6215 match motion {
6216 Motion::Left
6217 | Motion::Right
6218 | Motion::WordFwd
6219 | Motion::BigWordFwd
6220 | Motion::WordBack
6221 | Motion::BigWordBack
6222 | Motion::WordEnd
6223 | Motion::BigWordEnd
6224 | Motion::WordEndBack
6225 | Motion::BigWordEndBack
6226 | Motion::LineStart
6227 | Motion::FirstNonBlank
6228 | Motion::LineEnd
6229 | Motion::Find { .. }
6230 | Motion::FindRepeat { .. }
6231 | Motion::MatchBracket => {
6232 ed.vim.block_vcol = ed.cursor().1;
6233 }
6234 _ => {}
6236 }
6237}
6238
6239fn apply_block_operator<H: crate::types::Host>(
6244 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6245 op: Operator,
6246) {
6247 let (top, bot, left, right) = block_bounds(ed);
6248 let yank = block_yank(ed, top, bot, left, right);
6250
6251 match op {
6252 Operator::Yank => {
6253 if !yank.is_empty() {
6254 ed.record_yank_to_host(yank.clone());
6255 ed.record_yank(yank, false);
6256 }
6257 ed.vim.mode = Mode::Normal;
6258 ed.jump_cursor(top, left);
6259 }
6260 Operator::Delete => {
6261 ed.push_undo();
6262 delete_block_contents(ed, top, bot, left, right);
6263 if !yank.is_empty() {
6264 ed.record_yank_to_host(yank.clone());
6265 ed.record_delete(yank, false);
6266 }
6267 ed.vim.mode = Mode::Normal;
6268 ed.jump_cursor(top, left);
6269 }
6270 Operator::Change => {
6271 ed.push_undo();
6272 delete_block_contents(ed, top, bot, left, right);
6273 if !yank.is_empty() {
6274 ed.record_yank_to_host(yank.clone());
6275 ed.record_delete(yank, false);
6276 }
6277 ed.jump_cursor(top, left);
6278 begin_insert_noundo(
6279 ed,
6280 1,
6281 InsertReason::BlockChange {
6282 top,
6283 bot,
6284 col: left,
6285 },
6286 );
6287 }
6288 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
6289 ed.push_undo();
6290 transform_block_case(ed, op, top, bot, left, right);
6291 ed.vim.mode = Mode::Normal;
6292 ed.jump_cursor(top, left);
6293 }
6294 Operator::Indent | Operator::Outdent => {
6295 ed.push_undo();
6299 if op == Operator::Indent {
6300 indent_rows(ed, top, bot, 1);
6301 } else {
6302 outdent_rows(ed, top, bot, 1);
6303 }
6304 ed.vim.mode = Mode::Normal;
6305 }
6306 Operator::Fold => unreachable!("Visual zf takes its own path"),
6307 Operator::Reflow => {
6308 ed.push_undo();
6312 reflow_rows(ed, top, bot);
6313 ed.vim.mode = Mode::Normal;
6314 }
6315 Operator::ReflowKeepCursor => {
6316 let saved = ed.cursor();
6318 ed.push_undo();
6319 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6320 let (new_row, new_col) = reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6321 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6322 ed.push_buffer_cursor_to_textarea();
6323 ed.vim.mode = Mode::Normal;
6324 }
6325 Operator::AutoIndent => {
6326 ed.push_undo();
6329 auto_indent_rows(ed, top, bot);
6330 ed.vim.mode = Mode::Normal;
6331 }
6332 Operator::Filter => {}
6334 Operator::Comment => {}
6336 }
6337}
6338
6339fn transform_block_case<H: crate::types::Host>(
6343 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6344 op: Operator,
6345 top: usize,
6346 bot: usize,
6347 left: usize,
6348 right: usize,
6349) {
6350 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6351 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6352 let chars: Vec<char> = lines[r].chars().collect();
6353 if left >= chars.len() {
6354 continue;
6355 }
6356 let end = (right + 1).min(chars.len());
6357 let head: String = chars[..left].iter().collect();
6358 let mid: String = chars[left..end].iter().collect();
6359 let tail: String = chars[end..].iter().collect();
6360 let transformed = match op {
6361 Operator::Uppercase => mid.to_uppercase(),
6362 Operator::Lowercase => mid.to_lowercase(),
6363 Operator::ToggleCase => toggle_case_str(&mid),
6364 _ => mid,
6365 };
6366 lines[r] = format!("{head}{transformed}{tail}");
6367 }
6368 let saved_yank = ed.yank().to_string();
6369 let saved_linewise = ed.vim.yank_linewise;
6370 ed.restore(lines, (top, left));
6371 ed.set_yank(saved_yank);
6372 ed.vim.yank_linewise = saved_linewise;
6373}
6374
6375fn block_yank<H: crate::types::Host>(
6376 ed: &Editor<hjkl_buffer::Buffer, H>,
6377 top: usize,
6378 bot: usize,
6379 left: usize,
6380 right: usize,
6381) -> String {
6382 let rope = crate::types::Query::rope(&ed.buffer);
6383 let n = rope.len_lines();
6384 let mut rows: Vec<String> = Vec::new();
6385 for r in top..=bot {
6386 if r >= n {
6387 break;
6388 }
6389 let line = rope_line_to_str(&rope, r);
6390 let chars: Vec<char> = line.chars().collect();
6391 let end = (right + 1).min(chars.len());
6392 if left >= chars.len() {
6393 rows.push(String::new());
6394 } else {
6395 rows.push(chars[left..end].iter().collect());
6396 }
6397 }
6398 rows.join("\n")
6399}
6400
6401fn delete_block_contents<H: crate::types::Host>(
6402 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6403 top: usize,
6404 bot: usize,
6405 left: usize,
6406 right: usize,
6407) {
6408 use hjkl_buffer::{Edit, MotionKind, Position};
6409 ed.sync_buffer_content_from_textarea();
6410 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
6411 if last_row < top {
6412 return;
6413 }
6414 ed.mutate_edit(Edit::DeleteRange {
6415 start: Position::new(top, left),
6416 end: Position::new(last_row, right),
6417 kind: MotionKind::Block,
6418 });
6419 ed.push_buffer_cursor_to_textarea();
6420}
6421
6422pub(crate) fn block_replace<H: crate::types::Host>(
6424 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6425 ch: char,
6426) {
6427 let (top, bot, left, right) = block_bounds(ed);
6428 ed.push_undo();
6429 ed.sync_buffer_content_from_textarea();
6430 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6431 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6432 let chars: Vec<char> = lines[r].chars().collect();
6433 if left >= chars.len() {
6434 continue;
6435 }
6436 let end = (right + 1).min(chars.len());
6437 let before: String = chars[..left].iter().collect();
6438 let middle: String = std::iter::repeat_n(ch, end - left).collect();
6439 let after: String = chars[end..].iter().collect();
6440 lines[r] = format!("{before}{middle}{after}");
6441 }
6442 reset_textarea_lines(ed, lines);
6443 ed.vim.mode = Mode::Normal;
6444 ed.jump_cursor(top, left);
6445}
6446
6447fn reset_textarea_lines<H: crate::types::Host>(
6451 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6452 lines: Vec<String>,
6453) {
6454 let cursor = ed.cursor();
6455 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
6456 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
6457 ed.mark_content_dirty();
6458}
6459
6460type Pos = (usize, usize);
6466
6467pub(crate) fn text_object_range<H: crate::types::Host>(
6471 ed: &Editor<hjkl_buffer::Buffer, H>,
6472 obj: TextObject,
6473 inner: bool,
6474 count: usize,
6475) -> Option<(Pos, Pos, RangeKind)> {
6476 match obj {
6477 TextObject::Word { big } => {
6478 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
6479 }
6480 TextObject::Quote(q) => {
6481 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6482 }
6483 TextObject::Bracket(open) => bracket_text_object(ed, open, inner, count),
6484 TextObject::Paragraph => {
6485 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
6486 }
6487 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
6488 TextObject::Sentence => {
6489 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6490 }
6491 }
6492}
6493
6494fn sentence_boundary<H: crate::types::Host>(
6498 ed: &Editor<hjkl_buffer::Buffer, H>,
6499 forward: bool,
6500) -> Option<(usize, usize)> {
6501 let rope = crate::types::Query::rope(&ed.buffer);
6502 let n_lines = rope.len_lines();
6503 if n_lines == 0 {
6504 return None;
6505 }
6506 let line_lens: Vec<usize> = (0..n_lines)
6508 .map(|r| rope_line_to_str(&rope, r).chars().count())
6509 .collect();
6510 let pos_to_idx = |pos: (usize, usize)| -> usize {
6511 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6512 idx + pos.1
6513 };
6514 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6515 for (r, &len) in line_lens.iter().enumerate() {
6516 if idx <= len {
6517 return (r, idx);
6518 }
6519 idx -= len + 1;
6520 }
6521 let last = n_lines.saturating_sub(1);
6522 (last, line_lens[last])
6523 };
6524 let mut chars: Vec<char> = rope.chars().collect();
6527 if chars.last() == Some(&'\n') {
6529 chars.pop();
6530 }
6531 if chars.is_empty() {
6532 return None;
6533 }
6534 let total = chars.len();
6535 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
6536 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6537
6538 if forward {
6539 let mut i = cursor_idx + 1;
6542 while i < total {
6543 if is_terminator(chars[i]) {
6544 while i + 1 < total && is_terminator(chars[i + 1]) {
6545 i += 1;
6546 }
6547 if i + 1 >= total {
6548 return None;
6549 }
6550 if chars[i + 1].is_whitespace() {
6551 let mut j = i + 1;
6552 while j < total && chars[j].is_whitespace() {
6553 j += 1;
6554 }
6555 if j >= total {
6556 return None;
6557 }
6558 return Some(idx_to_pos(j));
6559 }
6560 }
6561 i += 1;
6562 }
6563 None
6564 } else {
6565 let find_start = |from: usize| -> Option<usize> {
6569 let mut start = from;
6570 while start > 0 {
6571 let prev = chars[start - 1];
6572 if prev.is_whitespace() {
6573 let mut k = start - 1;
6574 while k > 0 && chars[k - 1].is_whitespace() {
6575 k -= 1;
6576 }
6577 if k > 0 && is_terminator(chars[k - 1]) {
6578 break;
6579 }
6580 }
6581 start -= 1;
6582 }
6583 while start < total && chars[start].is_whitespace() {
6584 start += 1;
6585 }
6586 (start < total).then_some(start)
6587 };
6588 let current_start = find_start(cursor_idx)?;
6589 if current_start < cursor_idx {
6590 return Some(idx_to_pos(current_start));
6591 }
6592 let mut k = current_start;
6595 while k > 0 && chars[k - 1].is_whitespace() {
6596 k -= 1;
6597 }
6598 if k == 0 {
6599 return None;
6600 }
6601 let prev_start = find_start(k - 1)?;
6602 Some(idx_to_pos(prev_start))
6603 }
6604}
6605
6606fn sentence_text_object<H: crate::types::Host>(
6612 ed: &Editor<hjkl_buffer::Buffer, H>,
6613 inner: bool,
6614) -> Option<((usize, usize), (usize, usize))> {
6615 let rope = crate::types::Query::rope(&ed.buffer);
6616 let n_lines = rope.len_lines();
6617 if n_lines == 0 {
6618 return None;
6619 }
6620 let line_lens: Vec<usize> = (0..n_lines)
6623 .map(|r| rope_line_to_str(&rope, r).chars().count())
6624 .collect();
6625 let pos_to_idx = |pos: (usize, usize)| -> usize {
6626 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6627 idx + pos.1
6628 };
6629 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6630 for (r, &len) in line_lens.iter().enumerate() {
6631 if idx <= len {
6632 return (r, idx);
6633 }
6634 idx -= len + 1;
6635 }
6636 let last = n_lines.saturating_sub(1);
6637 (last, line_lens[last])
6638 };
6639 let mut chars: Vec<char> = rope.chars().collect();
6640 if chars.last() == Some(&'\n') {
6641 chars.pop();
6642 }
6643 if chars.is_empty() {
6644 return None;
6645 }
6646
6647 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
6648 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6649
6650 let mut start = cursor_idx;
6654 while start > 0 {
6655 let prev = chars[start - 1];
6656 if prev.is_whitespace() {
6657 let mut k = start - 1;
6661 while k > 0 && chars[k - 1].is_whitespace() {
6662 k -= 1;
6663 }
6664 if k > 0 && is_terminator(chars[k - 1]) {
6665 break;
6666 }
6667 }
6668 start -= 1;
6669 }
6670 while start < chars.len() && chars[start].is_whitespace() {
6673 start += 1;
6674 }
6675 if start >= chars.len() {
6676 return None;
6677 }
6678
6679 let mut end = start;
6682 while end < chars.len() {
6683 if is_terminator(chars[end]) {
6684 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
6686 end += 1;
6687 }
6688 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
6691 break;
6692 }
6693 }
6694 end += 1;
6695 }
6696 let end_idx = (end + 1).min(chars.len());
6698
6699 let final_end = if inner {
6700 end_idx
6701 } else {
6702 let mut e = end_idx;
6706 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
6707 e += 1;
6708 }
6709 e
6710 };
6711
6712 Some((idx_to_pos(start), idx_to_pos(final_end)))
6713}
6714
6715fn tag_text_object<H: crate::types::Host>(
6719 ed: &Editor<hjkl_buffer::Buffer, H>,
6720 inner: bool,
6721) -> Option<((usize, usize), (usize, usize))> {
6722 let rope = crate::types::Query::rope(&ed.buffer);
6723 let n_lines = rope.len_lines();
6724 if n_lines == 0 {
6725 return None;
6726 }
6727 let line_lens: Vec<usize> = (0..n_lines)
6731 .map(|r| rope_line_to_str(&rope, r).chars().count())
6732 .collect();
6733 let pos_to_idx = |pos: (usize, usize)| -> usize {
6734 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6735 idx + pos.1
6736 };
6737 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6738 for (r, &len) in line_lens.iter().enumerate() {
6739 if idx <= len {
6740 return (r, idx);
6741 }
6742 idx -= len + 1;
6743 }
6744 let last = n_lines.saturating_sub(1);
6745 (last, line_lens[last])
6746 };
6747 let mut chars: Vec<char> = rope.chars().collect();
6748 if chars.last() == Some(&'\n') {
6749 chars.pop();
6750 }
6751 let cursor_idx = pos_to_idx(ed.cursor());
6752
6753 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
6761 let mut next_after: Option<(usize, usize, usize, usize)> = None;
6762 let mut i = 0;
6763 while i < chars.len() {
6764 if chars[i] != '<' {
6765 i += 1;
6766 continue;
6767 }
6768 let mut j = i + 1;
6769 while j < chars.len() && chars[j] != '>' {
6770 j += 1;
6771 }
6772 if j >= chars.len() {
6773 break;
6774 }
6775 let inside: String = chars[i + 1..j].iter().collect();
6776 let close_end = j + 1;
6777 let trimmed = inside.trim();
6778 if trimmed.starts_with('!') || trimmed.starts_with('?') {
6779 i = close_end;
6780 continue;
6781 }
6782 if let Some(rest) = trimmed.strip_prefix('/') {
6783 let name = rest.split_whitespace().next().unwrap_or("").to_string();
6784 if !name.is_empty()
6785 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
6786 {
6787 let (open_start, content_start, _) = stack[stack_idx].clone();
6788 stack.truncate(stack_idx);
6789 let content_end = i;
6790 let candidate = (open_start, content_start, content_end, close_end);
6791 if cursor_idx >= content_start && cursor_idx <= content_end {
6792 innermost = match innermost {
6793 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
6794 Some(candidate)
6795 }
6796 None => Some(candidate),
6797 existing => existing,
6798 };
6799 } else if open_start >= cursor_idx && next_after.is_none() {
6800 next_after = Some(candidate);
6801 }
6802 }
6803 } else if !trimmed.ends_with('/') {
6804 let name: String = trimmed
6805 .split(|c: char| c.is_whitespace() || c == '/')
6806 .next()
6807 .unwrap_or("")
6808 .to_string();
6809 if !name.is_empty() {
6810 stack.push((i, close_end, name));
6811 }
6812 }
6813 i = close_end;
6814 }
6815
6816 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
6817 if inner {
6818 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
6819 } else {
6820 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
6821 }
6822}
6823
6824fn is_wordchar(c: char) -> bool {
6825 c.is_alphanumeric() || c == '_'
6826}
6827
6828pub(crate) use hjkl_buffer::is_keyword_char;
6832
6833fn word_text_object<H: crate::types::Host>(
6834 ed: &Editor<hjkl_buffer::Buffer, H>,
6835 inner: bool,
6836 big: bool,
6837) -> Option<((usize, usize), (usize, usize))> {
6838 let (row, col) = ed.cursor();
6839 let line = buf_line(&ed.buffer, row)?;
6840 let chars: Vec<char> = line.chars().collect();
6841 if chars.is_empty() {
6842 return None;
6843 }
6844 let at = col.min(chars.len().saturating_sub(1));
6845 let classify = |c: char| -> u8 {
6846 if c.is_whitespace() {
6847 0
6848 } else if big || is_wordchar(c) {
6849 1
6850 } else {
6851 2
6852 }
6853 };
6854 let cls = classify(chars[at]);
6855 let mut start = at;
6856 while start > 0 && classify(chars[start - 1]) == cls {
6857 start -= 1;
6858 }
6859 let mut end = at;
6860 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
6861 end += 1;
6862 }
6863 let char_byte = |i: usize| {
6865 if i >= chars.len() {
6866 line.len()
6867 } else {
6868 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
6869 }
6870 };
6871 let mut start_col = char_byte(start);
6872 let mut end_col = char_byte(end + 1);
6874 if !inner {
6875 let mut t = end + 1;
6877 let mut included_trailing = false;
6878 while t < chars.len() && chars[t].is_whitespace() {
6879 included_trailing = true;
6880 t += 1;
6881 }
6882 if included_trailing {
6883 end_col = char_byte(t);
6884 } else {
6885 let mut s = start;
6886 while s > 0 && chars[s - 1].is_whitespace() {
6887 s -= 1;
6888 }
6889 start_col = char_byte(s);
6890 }
6891 }
6892 Some(((row, start_col), (row, end_col)))
6893}
6894
6895fn quote_text_object<H: crate::types::Host>(
6896 ed: &Editor<hjkl_buffer::Buffer, H>,
6897 q: char,
6898 inner: bool,
6899) -> Option<((usize, usize), (usize, usize))> {
6900 let (row, col) = ed.cursor();
6901 let line = buf_line(&ed.buffer, row)?;
6902 let bytes = line.as_bytes();
6903 let q_byte = q as u8;
6904 let mut positions: Vec<usize> = Vec::new();
6906 for (i, &b) in bytes.iter().enumerate() {
6907 if b == q_byte {
6908 positions.push(i);
6909 }
6910 }
6911 if positions.len() < 2 {
6912 return None;
6913 }
6914 let mut open_idx: Option<usize> = None;
6915 let mut close_idx: Option<usize> = None;
6916 for pair in positions.chunks(2) {
6917 if pair.len() < 2 {
6918 break;
6919 }
6920 if col >= pair[0] && col <= pair[1] {
6921 open_idx = Some(pair[0]);
6922 close_idx = Some(pair[1]);
6923 break;
6924 }
6925 if col < pair[0] {
6926 open_idx = Some(pair[0]);
6927 close_idx = Some(pair[1]);
6928 break;
6929 }
6930 }
6931 let open = open_idx?;
6932 let close = close_idx?;
6933 if inner {
6935 if close <= open + 1 {
6936 return None;
6937 }
6938 Some(((row, open + 1), (row, close)))
6939 } else {
6940 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
6947 let mut end = after_close;
6949 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
6950 end += 1;
6951 }
6952 Some(((row, open), (row, end)))
6953 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
6954 let mut start = open;
6956 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
6957 start -= 1;
6958 }
6959 Some(((row, start), (row, close + 1)))
6960 } else {
6961 Some(((row, open), (row, close + 1)))
6962 }
6963 }
6964}
6965
6966fn bracket_text_object<H: crate::types::Host>(
6967 ed: &Editor<hjkl_buffer::Buffer, H>,
6968 open: char,
6969 inner: bool,
6970 count: usize,
6971) -> Option<(Pos, Pos, RangeKind)> {
6972 let close = match open {
6973 '(' => ')',
6974 '[' => ']',
6975 '{' => '}',
6976 '<' => '>',
6977 _ => return None,
6978 };
6979 let (row, col) = ed.cursor();
6980 let lines = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6981 let lines = lines.as_slice();
6982 let cursor_char = lines.get(row).and_then(|l| l.chars().nth(col));
6988 let (open_pos, close_pos) = if cursor_char == Some(close) {
6989 let open_pos = if col > 0 {
6990 find_open_bracket(lines, row, col - 1, open, close)
6991 } else if row > 0 {
6992 let pr = row - 1;
6993 let pc = lines[pr].chars().count().saturating_sub(1);
6994 find_open_bracket(lines, pr, pc, open, close)
6995 } else {
6996 None
6997 }?;
6998 (open_pos, (row, col))
6999 } else {
7000 let open_pos = find_open_bracket(lines, row, col, open, close)
7005 .or_else(|| find_next_open(lines, row, col, open))?;
7006 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
7007 (open_pos, close_pos)
7008 };
7009 let (open_pos, close_pos) = {
7013 let (mut op, mut cp) = (open_pos, close_pos);
7014 for _ in 1..count.max(1) {
7015 let outer = if op.1 > 0 {
7016 find_open_bracket(lines, op.0, op.1 - 1, open, close)
7017 } else if op.0 > 0 {
7018 let pr = op.0 - 1;
7019 let pc = lines[pr].chars().count().saturating_sub(1);
7020 find_open_bracket(lines, pr, pc, open, close)
7021 } else {
7022 None
7023 };
7024 let Some(oo) = outer else { break };
7025 let Some(oc) = find_close_bracket(lines, oo.0, oo.1 + 1, open, close) else {
7026 break;
7027 };
7028 op = oo;
7029 cp = oc;
7030 }
7031 (op, cp)
7032 };
7033 if inner {
7035 if close_pos.0 > open_pos.0 + 1 {
7041 let inner_row_start = open_pos.0 + 1;
7043 let inner_row_end = close_pos.0 - 1;
7044 let end_col = lines
7045 .get(inner_row_end)
7046 .map(|l| l.chars().count())
7047 .unwrap_or(0);
7048 return Some((
7049 (inner_row_start, 0),
7050 (inner_row_end, end_col),
7051 RangeKind::Linewise,
7052 ));
7053 }
7054 let inner_start = advance_pos(lines, open_pos);
7055 if inner_start.0 > close_pos.0
7056 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
7057 {
7058 return None;
7059 }
7060 Some((inner_start, close_pos, RangeKind::Exclusive))
7061 } else {
7062 Some((
7063 open_pos,
7064 advance_pos(lines, close_pos),
7065 RangeKind::Exclusive,
7066 ))
7067 }
7068}
7069
7070fn find_open_bracket(
7071 lines: &[String],
7072 row: usize,
7073 col: usize,
7074 open: char,
7075 close: char,
7076) -> Option<(usize, usize)> {
7077 let mut depth: i32 = 0;
7078 let mut r = row;
7079 let mut c = col as isize;
7080 loop {
7081 let cur = &lines[r];
7082 let chars: Vec<char> = cur.chars().collect();
7083 if (c as usize) >= chars.len() {
7087 c = chars.len() as isize - 1;
7088 }
7089 while c >= 0 {
7090 let ch = chars[c as usize];
7091 if ch == close {
7092 depth += 1;
7093 } else if ch == open {
7094 if depth == 0 {
7095 return Some((r, c as usize));
7096 }
7097 depth -= 1;
7098 }
7099 c -= 1;
7100 }
7101 if r == 0 {
7102 return None;
7103 }
7104 r -= 1;
7105 c = lines[r].chars().count() as isize - 1;
7106 }
7107}
7108
7109fn find_close_bracket(
7110 lines: &[String],
7111 row: usize,
7112 start_col: usize,
7113 open: char,
7114 close: char,
7115) -> Option<(usize, usize)> {
7116 let mut depth: i32 = 0;
7117 let mut r = row;
7118 let mut c = start_col;
7119 loop {
7120 let cur = &lines[r];
7121 let chars: Vec<char> = cur.chars().collect();
7122 while c < chars.len() {
7123 let ch = chars[c];
7124 if ch == open {
7125 depth += 1;
7126 } else if ch == close {
7127 if depth == 0 {
7128 return Some((r, c));
7129 }
7130 depth -= 1;
7131 }
7132 c += 1;
7133 }
7134 if r + 1 >= lines.len() {
7135 return None;
7136 }
7137 r += 1;
7138 c = 0;
7139 }
7140}
7141
7142fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
7146 let mut r = row;
7147 let mut c = col;
7148 while r < lines.len() {
7149 let chars: Vec<char> = lines[r].chars().collect();
7150 while c < chars.len() {
7151 if chars[c] == open {
7152 return Some((r, c));
7153 }
7154 c += 1;
7155 }
7156 r += 1;
7157 c = 0;
7158 }
7159 None
7160}
7161
7162fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
7163 let (r, c) = pos;
7164 let line_len = lines[r].chars().count();
7165 if c < line_len {
7166 (r, c + 1)
7167 } else if r + 1 < lines.len() {
7168 (r + 1, 0)
7169 } else {
7170 pos
7171 }
7172}
7173
7174fn paragraph_text_object<H: crate::types::Host>(
7175 ed: &Editor<hjkl_buffer::Buffer, H>,
7176 inner: bool,
7177) -> Option<((usize, usize), (usize, usize))> {
7178 let (row, _) = ed.cursor();
7179 let rope = crate::types::Query::rope(&ed.buffer);
7180 let n_lines = rope.len_lines();
7181 if n_lines == 0 {
7182 return None;
7183 }
7184 let is_blank = |r: usize| -> bool {
7186 if r >= n_lines {
7187 return true;
7188 }
7189 rope_line_to_str(&rope, r).trim().is_empty()
7190 };
7191 if is_blank(row) {
7192 return None;
7193 }
7194 let mut top = row;
7195 while top > 0 && !is_blank(top - 1) {
7196 top -= 1;
7197 }
7198 let mut bot = row;
7199 while bot + 1 < n_lines && !is_blank(bot + 1) {
7200 bot += 1;
7201 }
7202 if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
7204 bot += 1;
7205 }
7206 let end_col = rope_line_to_str(&rope, bot).chars().count();
7207 Some(((top, 0), (bot, end_col)))
7208}
7209
7210fn read_vim_range<H: crate::types::Host>(
7216 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7217 start: (usize, usize),
7218 end: (usize, usize),
7219 kind: RangeKind,
7220) -> String {
7221 let (top, bot) = order(start, end);
7222 ed.sync_buffer_content_from_textarea();
7223 let rope = crate::types::Query::rope(&ed.buffer);
7224 let n_lines = rope.len_lines();
7225 match kind {
7226 RangeKind::Linewise => {
7227 let lo = top.0;
7228 let hi = bot.0.min(n_lines.saturating_sub(1));
7229 let mut text = rope_row_range_str(&rope, lo, hi);
7230 text.push('\n');
7231 text
7232 }
7233 RangeKind::Inclusive | RangeKind::Exclusive => {
7234 let inclusive = matches!(kind, RangeKind::Inclusive);
7235 let mut out = String::new();
7237 for row in top.0..=bot.0 {
7238 if row >= n_lines {
7239 break;
7240 }
7241 let line = rope_line_to_str(&rope, row);
7242 let lo = if row == top.0 { top.1 } else { 0 };
7243 let hi_unclamped = if row == bot.0 {
7244 if inclusive { bot.1 + 1 } else { bot.1 }
7245 } else {
7246 line.chars().count() + 1
7247 };
7248 let row_chars: Vec<char> = line.chars().collect();
7249 let hi = hi_unclamped.min(row_chars.len());
7250 if lo < hi {
7251 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
7252 }
7253 if row < bot.0 {
7254 out.push('\n');
7255 }
7256 }
7257 out
7258 }
7259 }
7260}
7261
7262fn cut_vim_range<H: crate::types::Host>(
7271 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7272 start: (usize, usize),
7273 end: (usize, usize),
7274 kind: RangeKind,
7275) -> String {
7276 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
7277 let (top, bot) = order(start, end);
7278 ed.sync_buffer_content_from_textarea();
7279 let (buf_start, buf_end, buf_kind) = match kind {
7280 RangeKind::Linewise => (
7281 Position::new(top.0, 0),
7282 Position::new(bot.0, 0),
7283 BufKind::Line,
7284 ),
7285 RangeKind::Inclusive => {
7286 let line_chars = buf_line_chars(&ed.buffer, bot.0);
7287 let next = if bot.1 < line_chars {
7291 Position::new(bot.0, bot.1 + 1)
7292 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
7293 Position::new(bot.0 + 1, 0)
7294 } else {
7295 Position::new(bot.0, line_chars)
7296 };
7297 (Position::new(top.0, top.1), next, BufKind::Char)
7298 }
7299 RangeKind::Exclusive => (
7300 Position::new(top.0, top.1),
7301 Position::new(bot.0, bot.1),
7302 BufKind::Char,
7303 ),
7304 };
7305 let inverse = ed.mutate_edit(Edit::DeleteRange {
7306 start: buf_start,
7307 end: buf_end,
7308 kind: buf_kind,
7309 });
7310 let text = match inverse {
7311 Edit::InsertStr { text, .. } => text,
7312 _ => String::new(),
7313 };
7314 if !text.is_empty() {
7315 ed.record_yank_to_host(text.clone());
7316 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
7317 }
7318 ed.push_buffer_cursor_to_textarea();
7319 text
7320}
7321
7322fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7328 use hjkl_buffer::{Edit, MotionKind, Position};
7329 ed.sync_buffer_content_from_textarea();
7330 let cursor = buf_cursor_pos(&ed.buffer);
7331 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7332 if cursor.col >= line_chars {
7333 return;
7334 }
7335 let inverse = ed.mutate_edit(Edit::DeleteRange {
7336 start: cursor,
7337 end: Position::new(cursor.row, line_chars),
7338 kind: MotionKind::Char,
7339 });
7340 if let Edit::InsertStr { text, .. } = inverse
7341 && !text.is_empty()
7342 {
7343 ed.record_yank_to_host(text.clone());
7344 ed.vim.yank_linewise = false;
7345 ed.set_yank(text);
7346 }
7347 buf_set_cursor_pos(&mut ed.buffer, cursor);
7348 ed.push_buffer_cursor_to_textarea();
7349}
7350
7351fn do_char_delete<H: crate::types::Host>(
7352 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7353 forward: bool,
7354 count: usize,
7355) {
7356 use hjkl_buffer::{Edit, MotionKind, Position};
7357 ed.push_undo();
7358 ed.sync_buffer_content_from_textarea();
7359 let mut deleted = String::new();
7362 for _ in 0..count {
7363 let cursor = buf_cursor_pos(&ed.buffer);
7364 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7365 if forward {
7366 if cursor.col >= line_chars {
7369 continue;
7370 }
7371 let inverse = ed.mutate_edit(Edit::DeleteRange {
7372 start: cursor,
7373 end: Position::new(cursor.row, cursor.col + 1),
7374 kind: MotionKind::Char,
7375 });
7376 if let Edit::InsertStr { text, .. } = inverse {
7377 deleted.push_str(&text);
7378 }
7379 } else {
7380 if cursor.col == 0 {
7382 continue;
7383 }
7384 let inverse = ed.mutate_edit(Edit::DeleteRange {
7385 start: Position::new(cursor.row, cursor.col - 1),
7386 end: cursor,
7387 kind: MotionKind::Char,
7388 });
7389 if let Edit::InsertStr { text, .. } = inverse {
7390 deleted = text + &deleted;
7393 }
7394 }
7395 }
7396 if !deleted.is_empty() {
7397 ed.record_yank_to_host(deleted.clone());
7398 ed.record_delete(deleted, false);
7399 }
7400 ed.push_buffer_cursor_to_textarea();
7401}
7402
7403pub(crate) fn adjust_number<H: crate::types::Host>(
7407 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7408 delta: i64,
7409) -> bool {
7410 use hjkl_buffer::{Edit, MotionKind, Position};
7411 ed.sync_buffer_content_from_textarea();
7412 let cursor = buf_cursor_pos(&ed.buffer);
7413 let row = cursor.row;
7414 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
7415 Some(l) => l.chars().collect(),
7416 None => return false,
7417 };
7418 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
7419 return false;
7420 };
7421 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
7422 digit_start - 1
7423 } else {
7424 digit_start
7425 };
7426 let mut span_end = digit_start;
7427 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
7428 span_end += 1;
7429 }
7430 let s: String = chars[span_start..span_end].iter().collect();
7431 let Ok(n) = s.parse::<i64>() else {
7432 return false;
7433 };
7434 let new_s = n.saturating_add(delta).to_string();
7435
7436 ed.push_undo();
7437 let span_start_pos = Position::new(row, span_start);
7438 let span_end_pos = Position::new(row, span_end);
7439 ed.mutate_edit(Edit::DeleteRange {
7440 start: span_start_pos,
7441 end: span_end_pos,
7442 kind: MotionKind::Char,
7443 });
7444 ed.mutate_edit(Edit::InsertStr {
7445 at: span_start_pos,
7446 text: new_s.clone(),
7447 });
7448 let new_len = new_s.chars().count();
7449 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
7450 ed.push_buffer_cursor_to_textarea();
7451 true
7452}
7453
7454pub(crate) fn replace_char<H: crate::types::Host>(
7455 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7456 ch: char,
7457 count: usize,
7458) {
7459 use hjkl_buffer::{Edit, MotionKind, Position};
7460 ed.push_undo();
7461 ed.sync_buffer_content_from_textarea();
7462 for _ in 0..count {
7463 let cursor = buf_cursor_pos(&ed.buffer);
7464 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7465 if cursor.col >= line_chars {
7466 break;
7467 }
7468 ed.mutate_edit(Edit::DeleteRange {
7469 start: cursor,
7470 end: Position::new(cursor.row, cursor.col + 1),
7471 kind: MotionKind::Char,
7472 });
7473 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
7474 }
7475 crate::motions::move_left(&mut ed.buffer, 1);
7477 ed.push_buffer_cursor_to_textarea();
7478}
7479
7480fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7481 use hjkl_buffer::{Edit, MotionKind, Position};
7482 ed.sync_buffer_content_from_textarea();
7483 let cursor = buf_cursor_pos(&ed.buffer);
7484 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
7485 return;
7486 };
7487 let toggled = if c.is_uppercase() {
7488 c.to_lowercase().next().unwrap_or(c)
7489 } else {
7490 c.to_uppercase().next().unwrap_or(c)
7491 };
7492 ed.mutate_edit(Edit::DeleteRange {
7493 start: cursor,
7494 end: Position::new(cursor.row, cursor.col + 1),
7495 kind: MotionKind::Char,
7496 });
7497 ed.mutate_edit(Edit::InsertChar {
7498 at: cursor,
7499 ch: toggled,
7500 });
7501}
7502
7503fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7504 use hjkl_buffer::{Edit, Position};
7505 ed.sync_buffer_content_from_textarea();
7506 let row = buf_cursor_pos(&ed.buffer).row;
7507 if row + 1 >= buf_row_count(&ed.buffer) {
7508 return;
7509 }
7510 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
7511 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
7512 let next_trimmed = next_raw.trim_start();
7513 let cur_chars = cur_line.chars().count();
7514 let next_chars = next_raw.chars().count();
7515 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
7518 " "
7519 } else {
7520 ""
7521 };
7522 let joined = format!("{cur_line}{separator}{next_trimmed}");
7523 ed.mutate_edit(Edit::Replace {
7524 start: Position::new(row, 0),
7525 end: Position::new(row + 1, next_chars),
7526 with: joined,
7527 });
7528 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
7532 ed.push_buffer_cursor_to_textarea();
7533}
7534
7535fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7538 use hjkl_buffer::Edit;
7539 ed.sync_buffer_content_from_textarea();
7540 let row = buf_cursor_pos(&ed.buffer).row;
7541 if row + 1 >= buf_row_count(&ed.buffer) {
7542 return;
7543 }
7544 let join_col = buf_line_chars(&ed.buffer, row);
7545 ed.mutate_edit(Edit::JoinLines {
7546 row,
7547 count: 1,
7548 with_space: false,
7549 });
7550 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
7552 ed.push_buffer_cursor_to_textarea();
7553}
7554
7555fn do_paste<H: crate::types::Host>(
7556 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7557 before: bool,
7558 count: usize,
7559) {
7560 use hjkl_buffer::{Edit, Position};
7561 ed.push_undo();
7562 let selector = ed.vim.pending_register.take();
7567 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
7568 Some(slot) => (slot.text.clone(), slot.linewise),
7569 None => {
7575 let s = &ed.registers().unnamed;
7576 (s.text.clone(), s.linewise)
7577 }
7578 };
7579 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
7583 let original_row_for_linewise_after = if linewise && !before {
7589 Some(buf_cursor_pos(&ed.buffer).row)
7590 } else {
7591 None
7592 };
7593 for _ in 0..count {
7594 ed.sync_buffer_content_from_textarea();
7595 let yank = yank.clone();
7596 if yank.is_empty() {
7597 continue;
7598 }
7599 if linewise {
7600 let text = yank.trim_matches('\n').to_string();
7604 let row = buf_cursor_pos(&ed.buffer).row;
7605 let target_row = if before {
7606 ed.mutate_edit(Edit::InsertStr {
7607 at: Position::new(row, 0),
7608 text: format!("{text}\n"),
7609 });
7610 row
7611 } else {
7612 let line_chars = buf_line_chars(&ed.buffer, row);
7613 ed.mutate_edit(Edit::InsertStr {
7614 at: Position::new(row, line_chars),
7615 text: format!("\n{text}"),
7616 });
7617 row + 1
7618 };
7619 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
7620 crate::motions::move_first_non_blank(&mut ed.buffer);
7621 ed.push_buffer_cursor_to_textarea();
7622 let payload_lines = text.lines().count().max(1);
7624 let bot_row = target_row + payload_lines - 1;
7625 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
7626 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
7627 } else {
7628 let cursor = buf_cursor_pos(&ed.buffer);
7632 let at = if before {
7633 cursor
7634 } else {
7635 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7636 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
7637 };
7638 ed.mutate_edit(Edit::InsertStr {
7639 at,
7640 text: yank.clone(),
7641 });
7642 crate::motions::move_left(&mut ed.buffer, 1);
7645 ed.push_buffer_cursor_to_textarea();
7646 let lo = (at.row, at.col);
7648 let hi = ed.cursor();
7649 paste_mark = Some((lo, hi));
7650 }
7651 }
7652 if let Some((lo, hi)) = paste_mark {
7653 ed.set_mark('[', lo);
7654 ed.set_mark(']', hi);
7655 }
7656 if let Some(orig_row) = original_row_for_linewise_after {
7661 let first_target = orig_row.saturating_add(1);
7662 buf_set_cursor_rc(&mut ed.buffer, first_target, 0);
7663 crate::motions::move_first_non_blank(&mut ed.buffer);
7664 ed.push_buffer_cursor_to_textarea();
7665 }
7666 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
7668}
7669
7670pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7671 if let Some(entry) = ed.undo_stack.pop() {
7672 let (cur_rope, cur_cursor) = ed.snapshot();
7673 ed.redo_stack.push(crate::editor::UndoEntry {
7674 rope: cur_rope,
7675 cursor: cur_cursor,
7676 timestamp: entry.timestamp,
7677 });
7678 ed.restore_rope(entry.rope, entry.cursor);
7679 }
7680 ed.vim.mode = Mode::Normal;
7681 clamp_cursor_to_normal_mode(ed);
7685}
7686
7687pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7688 if let Some(entry) = ed.redo_stack.pop() {
7689 let (cur_rope, cur_cursor) = ed.snapshot();
7690 ed.undo_stack.push(crate::editor::UndoEntry {
7691 rope: cur_rope,
7692 cursor: cur_cursor,
7693 timestamp: entry.timestamp,
7694 });
7695 ed.cap_undo();
7696 ed.restore_rope(entry.rope, entry.cursor);
7697 }
7698 ed.vim.mode = Mode::Normal;
7699}
7700
7701fn replay_insert_and_finish<H: crate::types::Host>(
7708 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7709 text: &str,
7710) {
7711 use hjkl_buffer::{Edit, Position};
7712 let cursor = ed.cursor();
7713 ed.mutate_edit(Edit::InsertStr {
7714 at: Position::new(cursor.0, cursor.1),
7715 text: text.to_string(),
7716 });
7717 if ed.vim.insert_session.take().is_some() {
7718 if ed.cursor().1 > 0 {
7719 crate::motions::move_left(&mut ed.buffer, 1);
7720 ed.push_buffer_cursor_to_textarea();
7721 }
7722 ed.vim.mode = Mode::Normal;
7723 }
7724}
7725
7726pub(crate) fn replay_last_change<H: crate::types::Host>(
7727 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7728 outer_count: usize,
7729) {
7730 let Some(change) = ed.vim.last_change.clone() else {
7731 return;
7732 };
7733 ed.vim.replaying = true;
7734 let scale = if outer_count > 0 { outer_count } else { 1 };
7735 match change {
7736 LastChange::OpMotion {
7737 op,
7738 motion,
7739 count,
7740 inserted,
7741 } => {
7742 let total = count.max(1) * scale;
7743 apply_op_with_motion(ed, op, &motion, total);
7744 if let Some(text) = inserted {
7745 replay_insert_and_finish(ed, &text);
7746 }
7747 }
7748 LastChange::OpTextObj {
7749 op,
7750 obj,
7751 inner,
7752 inserted,
7753 } => {
7754 apply_op_with_text_object(ed, op, obj, inner, 1);
7757 if let Some(text) = inserted {
7758 replay_insert_and_finish(ed, &text);
7759 }
7760 }
7761 LastChange::LineOp {
7762 op,
7763 count,
7764 inserted,
7765 } => {
7766 let total = count.max(1) * scale;
7767 execute_line_op(ed, op, total);
7768 if let Some(text) = inserted {
7769 replay_insert_and_finish(ed, &text);
7770 }
7771 }
7772 LastChange::CharDel { forward, count } => {
7773 do_char_delete(ed, forward, count * scale);
7774 }
7775 LastChange::ReplaceChar { ch, count } => {
7776 replace_char(ed, ch, count * scale);
7777 }
7778 LastChange::ToggleCase { count } => {
7779 for _ in 0..count * scale {
7780 ed.push_undo();
7781 toggle_case_at_cursor(ed);
7782 }
7783 }
7784 LastChange::JoinLine { count } => {
7785 for _ in 0..count * scale {
7786 ed.push_undo();
7787 join_line(ed);
7788 }
7789 }
7790 LastChange::Paste { before, count } => {
7791 do_paste(ed, before, count * scale);
7792 }
7793 LastChange::DeleteToEol { inserted } => {
7794 use hjkl_buffer::{Edit, Position};
7795 ed.push_undo();
7796 delete_to_eol(ed);
7797 if let Some(text) = inserted {
7798 let cursor = ed.cursor();
7799 ed.mutate_edit(Edit::InsertStr {
7800 at: Position::new(cursor.0, cursor.1),
7801 text,
7802 });
7803 }
7804 }
7805 LastChange::OpenLine { above, inserted } => {
7806 use hjkl_buffer::{Edit, Position};
7807 ed.push_undo();
7808 ed.sync_buffer_content_from_textarea();
7809 let row = buf_cursor_pos(&ed.buffer).row;
7810 if above {
7811 ed.mutate_edit(Edit::InsertStr {
7812 at: Position::new(row, 0),
7813 text: "\n".to_string(),
7814 });
7815 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
7816 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
7817 } else {
7818 let line_chars = buf_line_chars(&ed.buffer, row);
7819 ed.mutate_edit(Edit::InsertStr {
7820 at: Position::new(row, line_chars),
7821 text: "\n".to_string(),
7822 });
7823 }
7824 ed.push_buffer_cursor_to_textarea();
7825 let cursor = ed.cursor();
7826 ed.mutate_edit(Edit::InsertStr {
7827 at: Position::new(cursor.0, cursor.1),
7828 text: inserted,
7829 });
7830 }
7831 LastChange::InsertAt {
7832 entry,
7833 inserted,
7834 count,
7835 } => {
7836 use hjkl_buffer::{Edit, Position};
7837 ed.push_undo();
7838 match entry {
7839 InsertEntry::I => {}
7840 InsertEntry::ShiftI => move_first_non_whitespace(ed),
7841 InsertEntry::A => {
7842 crate::motions::move_right_to_end(&mut ed.buffer, 1);
7843 ed.push_buffer_cursor_to_textarea();
7844 }
7845 InsertEntry::ShiftA => {
7846 crate::motions::move_line_end(&mut ed.buffer);
7847 crate::motions::move_right_to_end(&mut ed.buffer, 1);
7848 ed.push_buffer_cursor_to_textarea();
7849 }
7850 }
7851 for _ in 0..count.max(1) {
7852 let cursor = ed.cursor();
7853 ed.mutate_edit(Edit::InsertStr {
7854 at: Position::new(cursor.0, cursor.1),
7855 text: inserted.clone(),
7856 });
7857 }
7858 }
7859 }
7860 ed.vim.replaying = false;
7861}
7862
7863fn extract_inserted(before: &str, after: &str) -> String {
7866 let before_chars: Vec<char> = before.chars().collect();
7867 let after_chars: Vec<char> = after.chars().collect();
7868 if after_chars.len() <= before_chars.len() {
7869 return String::new();
7870 }
7871 let prefix = before_chars
7872 .iter()
7873 .zip(after_chars.iter())
7874 .take_while(|(a, b)| a == b)
7875 .count();
7876 let max_suffix = before_chars.len() - prefix;
7877 let suffix = before_chars
7878 .iter()
7879 .rev()
7880 .zip(after_chars.iter().rev())
7881 .take(max_suffix)
7882 .take_while(|(a, b)| a == b)
7883 .count();
7884 after_chars[prefix..after_chars.len() - suffix]
7885 .iter()
7886 .collect()
7887}
7888
7889#[cfg(test)]
7892mod comment_continuation_tests {
7893 use super::*;
7894 use crate::{DefaultHost, Editor, Options};
7895 use hjkl_buffer::Buffer;
7896
7897 fn make_editor_with_lang(lang: &str, content: &str) -> Editor<Buffer, DefaultHost> {
7898 let buf = Buffer::from_str(content);
7899 let host = DefaultHost::new();
7900 let opts = Options {
7901 filetype: lang.to_string(),
7902 formatoptions: "ro".to_string(),
7903 ..Options::default()
7904 };
7905 Editor::new(buf, host, opts)
7906 }
7907
7908 #[test]
7909 fn detect_rust_doc_comment() {
7910 let result = detect_comment_on_line("rust", "/// foo bar");
7911 assert!(result.is_some());
7912 let (indent, prefix) = result.unwrap();
7913 assert_eq!(indent, "");
7914 assert_eq!(prefix, "/// ");
7915 }
7916
7917 #[test]
7918 fn detect_rust_inner_doc_comment() {
7919 let result = detect_comment_on_line("rust", "//! crate docs");
7920 assert!(result.is_some());
7921 let (_, prefix) = result.unwrap();
7922 assert_eq!(prefix, "//! ");
7923 }
7924
7925 #[test]
7926 fn detect_rust_plain_comment() {
7927 let result = detect_comment_on_line("rust", "// normal comment");
7928 assert!(result.is_some());
7929 let (_, prefix) = result.unwrap();
7930 assert_eq!(prefix, "// ");
7931 }
7932
7933 #[test]
7934 fn detect_indented_comment() {
7935 let result = detect_comment_on_line("rust", " // indented");
7936 assert!(result.is_some());
7937 let (indent, prefix) = result.unwrap();
7938 assert_eq!(indent, " ");
7939 assert_eq!(prefix, "// ");
7940 }
7941
7942 #[test]
7943 fn detect_python_hash() {
7944 let result = detect_comment_on_line("python", "# comment");
7945 assert!(result.is_some());
7946 let (_, prefix) = result.unwrap();
7947 assert_eq!(prefix, "# ");
7948 }
7949
7950 #[test]
7951 fn detect_lua_double_dash() {
7952 let result = detect_comment_on_line("lua", "-- a lua comment");
7953 assert!(result.is_some());
7954 let (_, prefix) = result.unwrap();
7955 assert_eq!(prefix, "-- ");
7956 }
7957
7958 #[test]
7959 fn detect_non_comment_is_none() {
7960 assert!(detect_comment_on_line("rust", "let x = 1;").is_none());
7961 assert!(detect_comment_on_line("python", "x = 1").is_none());
7962 }
7963
7964 #[test]
7965 fn detect_bare_double_slash_still_matches() {
7966 assert!(detect_comment_on_line("rust", "//").is_some());
7968 }
7969
7970 #[test]
7971 fn rust_doc_before_plain() {
7972 let result = detect_comment_on_line("rust", "/// outer doc");
7974 let (_, prefix) = result.unwrap();
7975 assert_eq!(prefix, "/// ", "/// must match before //");
7976 }
7977
7978 #[test]
7979 fn continue_comment_returns_prefix_for_comment_row() {
7980 let ed = make_editor_with_lang("rust", "/// hello\n");
7981 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7982 assert_eq!(cont, Some("/// ".to_string()));
7983 }
7984
7985 #[test]
7986 fn continue_comment_returns_none_for_non_comment() {
7987 let ed = make_editor_with_lang("rust", "let x = 1;\n");
7988 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7989 assert!(cont.is_none());
7990 }
7991
7992 #[test]
7993 fn continue_comment_returns_none_when_filetype_empty() {
7994 let buf = Buffer::from_str("// hello\n");
7995 let host = DefaultHost::new();
7996 let ed = Editor::new(buf, host, Options::default());
7998 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7999 assert!(cont.is_none());
8000 }
8001}
8002
8003#[cfg(test)]
8004mod comment_toggle_tests {
8005 use super::*;
8006 use crate::{DefaultHost, Editor, Options};
8007 use hjkl_buffer::Buffer;
8008
8009 fn make_rust_editor(content: &str) -> Editor<Buffer, DefaultHost> {
8010 let buf = Buffer::from_str(content);
8011 let host = DefaultHost::new();
8012 let opts = Options {
8013 filetype: "rust".to_string(),
8014 ..Options::default()
8015 };
8016 Editor::new(buf, host, opts)
8017 }
8018
8019 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
8020 buf_line(&ed.buffer, row).unwrap_or_default()
8021 }
8022
8023 #[test]
8026 fn gcc_comments_rust_line() {
8027 let mut ed = make_rust_editor("let x = 1;");
8028 ed.toggle_comment_range(0, 0);
8029 assert_eq!(line(&ed, 0), "// let x = 1;");
8030 }
8031
8032 #[test]
8033 fn gcc_uncomments_rust_line() {
8034 let mut ed = make_rust_editor("// let x = 1;");
8035 ed.toggle_comment_range(0, 0);
8036 assert_eq!(line(&ed, 0), "let x = 1;");
8037 }
8038
8039 #[test]
8040 fn gcc_indent_preserving() {
8041 let mut ed = make_rust_editor(" let x = 1;");
8043 ed.toggle_comment_range(0, 0);
8044 assert_eq!(line(&ed, 0), " // let x = 1;");
8045 }
8046
8047 #[test]
8048 fn gcc_indent_preserving_uncomment() {
8049 let mut ed = make_rust_editor(" // let x = 1;");
8050 ed.toggle_comment_range(0, 0);
8051 assert_eq!(line(&ed, 0), " let x = 1;");
8052 }
8053
8054 #[test]
8057 fn toggle_multi_line_all_uncommented() {
8058 let content = "let a = 1;\nlet b = 2;\nlet c = 3;";
8059 let mut ed = make_rust_editor(content);
8060 ed.toggle_comment_range(0, 2);
8061 assert_eq!(line(&ed, 0), "// let a = 1;");
8062 assert_eq!(line(&ed, 1), "// let b = 2;");
8063 assert_eq!(line(&ed, 2), "// let c = 3;");
8064 }
8065
8066 #[test]
8067 fn toggle_multi_line_all_commented() {
8068 let content = "// let a = 1;\n// let b = 2;\n// let c = 3;";
8069 let mut ed = make_rust_editor(content);
8070 ed.toggle_comment_range(0, 2);
8071 assert_eq!(line(&ed, 0), "let a = 1;");
8072 assert_eq!(line(&ed, 1), "let b = 2;");
8073 assert_eq!(line(&ed, 2), "let c = 3;");
8074 }
8075
8076 #[test]
8079 fn toggle_mixed_state_comments_all() {
8080 let content = "let a = 1;\n// let b = 2;\nlet c = 3;\n// let d = 4;\nlet e = 5;";
8082 let mut ed = make_rust_editor(content);
8083 ed.toggle_comment_range(0, 4);
8084 for r in 0..5 {
8085 assert!(
8086 line(&ed, r).trim_start().starts_with("//"),
8087 "row {r} not commented: {:?}",
8088 line(&ed, r)
8089 );
8090 }
8091 }
8092
8093 #[test]
8096 fn blank_lines_not_commented() {
8097 let content = "let a = 1;\n\nlet b = 2;";
8098 let mut ed = make_rust_editor(content);
8099 ed.toggle_comment_range(0, 2);
8100 assert_eq!(line(&ed, 0), "// let a = 1;");
8101 assert_eq!(line(&ed, 1), ""); assert_eq!(line(&ed, 2), "// let b = 2;");
8103 }
8104
8105 #[test]
8108 fn python_comment_toggle() {
8109 let buf = Buffer::from_str("x = 1\ny = 2");
8110 let host = DefaultHost::new();
8111 let opts = Options {
8112 filetype: "python".to_string(),
8113 ..Options::default()
8114 };
8115 let mut ed = Editor::new(buf, host, opts);
8116 ed.toggle_comment_range(0, 1);
8117 assert_eq!(line(&ed, 0), "# x = 1");
8118 assert_eq!(line(&ed, 1), "# y = 2");
8119 ed.toggle_comment_range(0, 1);
8121 assert_eq!(line(&ed, 0), "x = 1");
8122 assert_eq!(line(&ed, 1), "y = 2");
8123 }
8124
8125 #[test]
8128 fn commentstring_override_via_setting() {
8129 let buf = Buffer::from_str("hello world");
8130 let host = DefaultHost::new();
8131 let opts = Options {
8132 filetype: "rust".to_string(),
8133 ..Options::default()
8134 };
8135 let mut ed = Editor::new(buf, host, opts);
8136 ed.settings_mut().commentstring = "# %s".to_string();
8138 ed.toggle_comment_range(0, 0);
8139 assert_eq!(line(&ed, 0), "# hello world");
8140 }
8141
8142 #[test]
8145 fn unknown_lang_no_op() {
8146 let buf = Buffer::from_str("hello");
8147 let host = DefaultHost::new();
8148 let opts = Options::default(); let mut ed = Editor::new(buf, host, opts);
8150 ed.toggle_comment_range(0, 0);
8151 assert_eq!(line(&ed, 0), "hello");
8153 }
8154}
8155
8156#[cfg(test)]
8159mod g_ampersand_tests {
8160 use super::*;
8161 use crate::{DefaultHost, Editor, Options};
8162 use hjkl_buffer::{Buffer, rope_line_str};
8163
8164 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
8165 let buf = Buffer::from_str(content);
8166 let host = DefaultHost::new();
8167 Editor::new(buf, host, Options::default())
8168 }
8169
8170 fn buf_line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
8171 let rope = ed.buffer().rope();
8172 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
8173 }
8174
8175 #[test]
8178 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
8179 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
8180 let cmd = crate::substitute::parse_substitute("/foo/bar/").unwrap();
8182 ed.set_last_substitute(cmd);
8183 apply_after_g(&mut ed, '&', 1);
8185 assert_eq!(buf_line(&ed, 0), "bar");
8186 assert_eq!(buf_line(&ed, 1), "bar bar foo");
8188 assert_eq!(buf_line(&ed, 2), "baz");
8189 }
8190
8191 #[test]
8193 fn g_ampersand_with_g_flag_replaces_all_per_line() {
8194 let mut ed = make_editor("foo foo\nfoo");
8195 let cmd = crate::substitute::parse_substitute("/foo/bar/g").unwrap();
8196 ed.set_last_substitute(cmd);
8197 apply_after_g(&mut ed, '&', 1);
8198 assert_eq!(buf_line(&ed, 0), "bar bar");
8199 assert_eq!(buf_line(&ed, 1), "bar");
8200 }
8201
8202 #[test]
8204 fn g_ampersand_noop_when_no_prior_substitute() {
8205 let mut ed = make_editor("foo\nbar");
8206 apply_after_g(&mut ed, '&', 1);
8208 assert_eq!(buf_line(&ed, 0), "foo");
8209 assert_eq!(buf_line(&ed, 1), "bar");
8210 }
8211}
8212
8213#[cfg(test)]
8216mod sneak_tests {
8217 use super::*;
8218 use crate::{DefaultHost, Editor, Options};
8219 use hjkl_buffer::Buffer;
8220
8221 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
8222 let buf = Buffer::from_str(content);
8223 let host = DefaultHost::new();
8224 Editor::new(buf, host, Options::default())
8225 }
8226
8227 #[test]
8229 fn sneak_forward_jumps_to_two_char_digraph() {
8230 let mut ed = make_editor("foo bar baz qux\n");
8231 ed.jump_cursor(0, 0);
8232 ed.sneak('b', 'a', true, 1);
8233 assert_eq!(ed.cursor(), (0, 4), "cursor should land on 'ba' in 'bar'");
8234 }
8235
8236 #[test]
8238 fn sneak_backward_jumps_to_prior_match() {
8239 let mut ed = make_editor("foo bar baz qux\n");
8240 ed.jump_cursor(0, 12);
8241 ed.sneak('b', 'a', false, 1);
8242 assert_eq!(
8243 ed.cursor(),
8244 (0, 8),
8245 "backward sneak should find 'ba' in 'baz'"
8246 );
8247 }
8248
8249 #[test]
8251 fn sneak_repeat_semicolon_next_match() {
8252 let mut ed = make_editor("foo bar baz qux\n");
8253 ed.jump_cursor(0, 0);
8254 ed.sneak('b', 'a', true, 1);
8256 assert_eq!(ed.cursor(), (0, 4));
8257 execute_motion(&mut ed, Motion::FindRepeat { reverse: false }, 1);
8259 assert_eq!(ed.cursor(), (0, 8), "semicolon should jump to next 'ba'");
8260 }
8261
8262 #[test]
8264 fn sneak_repeat_comma_prev_match() {
8265 let mut ed = make_editor("foo bar baz qux\n");
8266 ed.jump_cursor(0, 0);
8267 ed.sneak('b', 'a', true, 1);
8268 assert_eq!(ed.cursor(), (0, 4));
8269 let pre = ed.cursor();
8271 execute_motion(&mut ed, Motion::FindRepeat { reverse: true }, 1);
8272 assert_eq!(
8273 ed.cursor(),
8274 pre,
8275 "comma with no prior match should leave cursor unchanged"
8276 );
8277 }
8278
8279 #[test]
8281 fn sneak_s_searches_backward() {
8282 let mut ed = make_editor("foo bar baz qux\n");
8283 ed.jump_cursor(0, 12);
8284 ed.sneak('b', 'a', false, 1);
8285 assert_eq!(ed.cursor(), (0, 8));
8286 }
8287
8288 #[test]
8290 fn sneak_with_count_jumps_to_nth() {
8291 let mut ed = make_editor("foo bar baz qux\n");
8292 ed.jump_cursor(0, 0);
8293 ed.sneak('b', 'a', true, 2);
8294 assert_eq!(ed.cursor(), (0, 8), "count=2 should jump to 2nd 'ba'");
8295 }
8296
8297 #[test]
8299 fn sneak_no_match_cursor_stays() {
8300 let mut ed = make_editor("foo bar baz qux\n");
8301 ed.jump_cursor(0, 0);
8302 let pre = ed.cursor();
8303 ed.sneak('x', 'x', true, 1);
8304 assert_eq!(ed.cursor(), pre, "no match should leave cursor unchanged");
8305 }
8306
8307 #[test]
8309 fn operator_pending_dsab_deletes_to_digraph() {
8310 let mut ed = make_editor("hello ab world\n");
8311 ed.jump_cursor(0, 0);
8312 ed.apply_op_sneak(Operator::Delete, 'a', 'b', true, 1);
8313 let content = ed.content();
8315 assert!(
8316 content.starts_with("ab world"),
8317 "dsab should delete 'hello ' leaving 'ab world'; got: {content:?}"
8318 );
8319 }
8320
8321 #[test]
8323 fn sneak_cross_line_match() {
8324 let mut ed = make_editor("foo\nbar baz\n");
8325 ed.jump_cursor(0, 0);
8326 ed.sneak('b', 'a', true, 1);
8327 assert_eq!(ed.cursor(), (1, 0), "sneak should cross line boundary");
8328 }
8329
8330 #[test]
8332 fn sneak_updates_last_sneak_state() {
8333 let mut ed = make_editor("foo bar baz\n");
8334 ed.jump_cursor(0, 0);
8335 ed.sneak('b', 'a', true, 1);
8336 let ls = ed.last_sneak();
8337 assert_eq!(
8338 ls,
8339 Some((('b', 'a'), true)),
8340 "last_sneak should record the digraph and direction"
8341 );
8342 }
8343}