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 last_substitute: Option<crate::substitute::SubstituteCmd>,
565 pub pending_closes: Vec<(usize, usize, char)>,
573 pub last_sneak: Option<((char, char), bool)>,
576 pub last_horizontal_motion: LastHorizontalMotion,
579}
580
581pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
582pub(crate) const CHANGE_LIST_MAX: usize = 100;
583
584#[derive(Debug, Clone)]
587pub struct SearchPrompt {
588 pub text: String,
589 pub cursor: usize,
590 pub forward: bool,
591}
592
593#[derive(Debug, Clone)]
594pub struct InsertSession {
595 pub count: usize,
596 pub row_min: usize,
598 pub row_max: usize,
599 pub before_rope: ropey::Rope,
604 pub reason: InsertReason,
605}
606
607#[derive(Debug, Clone)]
608pub enum InsertReason {
609 Enter(InsertEntry),
611 Open { above: bool },
613 AfterChange,
616 DeleteToEol,
618 ReplayOnly,
621 BlockEdge { top: usize, bot: usize, col: usize },
625 BlockChange { top: usize, bot: usize, col: usize },
630 Replace,
634}
635
636#[derive(Debug, Clone, Copy)]
646pub struct LastVisual {
647 pub mode: Mode,
648 pub anchor: (usize, usize),
649 pub cursor: (usize, usize),
650 pub block_vcol: usize,
651}
652
653impl VimState {
654 pub fn public_mode(&self) -> VimMode {
655 match self.mode {
656 Mode::Normal => VimMode::Normal,
657 Mode::Insert => VimMode::Insert,
658 Mode::Visual => VimMode::Visual,
659 Mode::VisualLine => VimMode::VisualLine,
660 Mode::VisualBlock => VimMode::VisualBlock,
661 }
662 }
663
664 pub fn force_normal(&mut self) {
665 self.mode = Mode::Normal;
666 self.pending = Pending::None;
667 self.count = 0;
668 self.insert_session = None;
669 self.current_mode = crate::VimMode::Normal;
671 }
672
673 pub(crate) fn clear_pending_prefix(&mut self) {
683 self.pending = Pending::None;
684 self.count = 0;
685 self.pending_register = None;
686 self.insert_pending_register = false;
687 }
688
689 pub(crate) fn widen_insert_row(&mut self, row: usize) {
694 if let Some(ref mut session) = self.insert_session {
695 session.row_min = session.row_min.min(row);
696 session.row_max = session.row_max.max(row);
697 }
698 }
699
700 pub fn is_visual(&self) -> bool {
701 matches!(
702 self.mode,
703 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
704 )
705 }
706
707 pub fn is_visual_char(&self) -> bool {
708 self.mode == Mode::Visual
709 }
710
711 pub(crate) fn pending_count_val(&self) -> Option<u32> {
714 if self.count == 0 {
715 None
716 } else {
717 Some(self.count as u32)
718 }
719 }
720
721 pub(crate) fn is_chord_pending(&self) -> bool {
724 !matches!(self.pending, Pending::None)
725 }
726
727 pub(crate) fn pending_op_char(&self) -> Option<char> {
731 let op = match &self.pending {
732 Pending::Op { op, .. }
733 | Pending::OpTextObj { op, .. }
734 | Pending::OpG { op, .. }
735 | Pending::OpFind { op, .. }
736 | Pending::OpSquareBracketOpen { op, .. }
737 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
738 _ => None,
739 };
740 op.map(|o| match o {
741 Operator::Delete => 'd',
742 Operator::Change => 'c',
743 Operator::Yank => 'y',
744 Operator::Uppercase => 'U',
745 Operator::Lowercase => 'u',
746 Operator::ToggleCase => '~',
747 Operator::Indent => '>',
748 Operator::Outdent => '<',
749 Operator::Fold => 'z',
750 Operator::Reflow => 'q',
751 Operator::ReflowKeepCursor => 'w',
752 Operator::AutoIndent => '=',
753 Operator::Filter => '!',
754 Operator::Comment => 'c',
756 })
757 }
758}
759
760pub(crate) fn enter_search<H: crate::types::Host>(
766 ed: &mut Editor<hjkl_buffer::Buffer, H>,
767 forward: bool,
768) {
769 ed.vim.search_prompt = Some(SearchPrompt {
770 text: String::new(),
771 cursor: 0,
772 forward,
773 });
774 ed.vim.search_history_cursor = None;
775 ed.set_search_pattern(None);
779}
780
781fn walk_change_list<H: crate::types::Host>(
785 ed: &mut Editor<hjkl_buffer::Buffer, H>,
786 dir: isize,
787 count: usize,
788) {
789 if ed.vim.change_list.is_empty() {
790 return;
791 }
792 let len = ed.vim.change_list.len();
793 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
794 (None, -1) => len as isize - 1,
795 (None, 1) => return, (Some(i), -1) => i as isize - 1,
797 (Some(i), 1) => i as isize + 1,
798 _ => return,
799 };
800 for _ in 1..count {
801 let next = idx + dir;
802 if next < 0 || next >= len as isize {
803 break;
804 }
805 idx = next;
806 }
807 if idx < 0 || idx >= len as isize {
808 return;
809 }
810 let idx = idx as usize;
811 ed.vim.change_list_cursor = Some(idx);
812 let (row, col) = ed.vim.change_list[idx];
813 ed.jump_cursor(row, col);
814}
815
816fn insert_register_text<H: crate::types::Host>(
821 ed: &mut Editor<hjkl_buffer::Buffer, H>,
822 selector: char,
823) {
824 use hjkl_buffer::Edit;
825 let text = match ed.registers().read(selector) {
826 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
827 _ => return,
828 };
829 ed.sync_buffer_content_from_textarea();
830 let cursor = buf_cursor_pos(&ed.buffer);
831 ed.mutate_edit(Edit::InsertStr {
832 at: cursor,
833 text: text.clone(),
834 });
835 let mut row = cursor.row;
838 let mut col = cursor.col;
839 for ch in text.chars() {
840 if ch == '\n' {
841 row += 1;
842 col = 0;
843 } else {
844 col += 1;
845 }
846 }
847 buf_set_cursor_rc(&mut ed.buffer, row, col);
848 ed.push_buffer_cursor_to_textarea();
849 ed.mark_content_dirty();
850 if let Some(ref mut session) = ed.vim.insert_session {
851 session.row_min = session.row_min.min(row);
852 session.row_max = session.row_max.max(row);
853 }
854}
855
856pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
875 if !settings.autoindent {
876 return String::new();
877 }
878 let base: String = prev_line
880 .chars()
881 .take_while(|c| *c == ' ' || *c == '\t')
882 .collect();
883
884 if settings.smartindent {
885 let unit = if settings.expandtab {
886 if settings.softtabstop > 0 {
887 " ".repeat(settings.softtabstop)
888 } else {
889 " ".repeat(settings.shiftwidth)
890 }
891 } else {
892 "\t".to_string()
893 };
894
895 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
897 if matches!(last_non_ws, Some('{' | '(' | '[')) {
898 return format!("{base}{unit}");
899 }
900
901 if is_html_filetype(&settings.filetype) {
906 let trimmed_end_len = prev_line
907 .trim_end_matches(|c: char| c.is_whitespace())
908 .len();
909 let trimmed = &prev_line[..trimmed_end_len];
910 if let Some(stripped) = trimmed.strip_suffix('>')
911 && scan_tag_opener(trimmed, stripped.len()).is_some()
912 {
913 return format!("{base}{unit}");
914 }
915 }
916 }
917
918 base
919}
920
921fn comment_prefixes_for_lang(lang: &str) -> &'static [&'static str] {
927 match lang {
928 "rust" => &["/// ", "//! ", "// "],
929 "c" | "cpp" => &["// "],
930 "python" | "sh" | "bash" | "zsh" | "fish" | "toml" | "yaml" => &["# "],
931 "lua" => &["-- "],
932 "sql" => &["-- "],
933 "vim" | "viml" => &["\" "],
934 _ => &[],
935 }
936}
937
938pub(crate) fn detect_comment_on_line(lang: &str, line: &str) -> Option<(String, &'static str)> {
944 let indent_end = line
945 .char_indices()
946 .find(|(_, c)| *c != ' ' && *c != '\t')
947 .map(|(i, _)| i)
948 .unwrap_or(line.len());
949 let indent = line[..indent_end].to_string();
950 let rest = &line[indent_end..];
951 for &prefix in comment_prefixes_for_lang(lang) {
952 if rest.starts_with(prefix) {
953 return Some((indent, prefix));
954 }
955 let bare = prefix.trim_end_matches(' ');
958 if rest == bare || rest.starts_with(&format!("{bare} ")) {
959 return Some((indent, prefix));
960 }
961 }
962 None
963}
964
965pub(crate) fn continue_comment(
972 buffer: &hjkl_buffer::Buffer,
973 settings: &crate::editor::Settings,
974 row: usize,
975) -> Option<String> {
976 if settings.filetype.is_empty() {
977 return None;
978 }
979 let line = crate::buf_helpers::buf_line(buffer, row)?;
980 let (indent, prefix) = detect_comment_on_line(&settings.filetype, &line)?;
981 Some(format!("{indent}{prefix}"))
982}
983
984fn try_dedent_close_bracket<H: crate::types::Host>(
994 ed: &mut Editor<hjkl_buffer::Buffer, H>,
995 cursor: hjkl_buffer::Position,
996 ch: char,
997) -> bool {
998 use hjkl_buffer::{Edit, MotionKind, Position};
999
1000 if !ed.settings.smartindent {
1001 return false;
1002 }
1003 if !matches!(ch, '}' | ')' | ']') {
1004 return false;
1005 }
1006
1007 let line = match buf_line(&ed.buffer, cursor.row) {
1008 Some(l) => l.to_string(),
1009 None => return false,
1010 };
1011
1012 let before: String = line.chars().take(cursor.col).collect();
1014 if !before.chars().all(|c| c == ' ' || c == '\t') {
1015 return false;
1016 }
1017 if before.is_empty() {
1018 return false;
1020 }
1021
1022 let unit_len: usize = if ed.settings.expandtab {
1024 if ed.settings.softtabstop > 0 {
1025 ed.settings.softtabstop
1026 } else {
1027 ed.settings.shiftwidth
1028 }
1029 } else {
1030 1
1032 };
1033
1034 let strip_len = if ed.settings.expandtab {
1036 let spaces = before.chars().filter(|c| *c == ' ').count();
1038 if spaces < unit_len {
1039 return false;
1040 }
1041 unit_len
1042 } else {
1043 if !before.starts_with('\t') {
1045 return false;
1046 }
1047 1
1048 };
1049
1050 ed.mutate_edit(Edit::DeleteRange {
1052 start: Position::new(cursor.row, 0),
1053 end: Position::new(cursor.row, strip_len),
1054 kind: MotionKind::Char,
1055 });
1056 let new_col = cursor.col.saturating_sub(strip_len);
1061 ed.mutate_edit(Edit::InsertChar {
1062 at: Position::new(cursor.row, new_col),
1063 ch,
1064 });
1065 true
1066}
1067
1068fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1069 let Some(session) = ed.vim.insert_session.take() else {
1070 return;
1071 };
1072 let after_rope = crate::types::Query::rope(&ed.buffer);
1073 let before_n = session.before_rope.len_lines();
1077 let after_n = after_rope.len_lines();
1078 let after_end = session.row_max.min(after_n.saturating_sub(1));
1079 let before_end = session.row_max.min(before_n.saturating_sub(1));
1080 let before = if before_end >= session.row_min && session.row_min < before_n {
1081 rope_row_range_str(&session.before_rope, session.row_min, before_end)
1082 } else {
1083 String::new()
1084 };
1085 let after = if after_end >= session.row_min && session.row_min < after_n {
1086 rope_row_range_str(&after_rope, session.row_min, after_end)
1087 } else {
1088 String::new()
1089 };
1090 let inserted = extract_inserted(&before, &after);
1091 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1092 use hjkl_buffer::{Edit, Position};
1093 for _ in 0..session.count - 1 {
1094 let (row, col) = ed.cursor();
1095 ed.mutate_edit(Edit::InsertStr {
1096 at: Position::new(row, col),
1097 text: inserted.clone(),
1098 });
1099 }
1100 }
1101 fn replicate_block_text<H: crate::types::Host>(
1105 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1106 inserted: &str,
1107 top: usize,
1108 bot: usize,
1109 col: usize,
1110 ) {
1111 use hjkl_buffer::{Edit, Position};
1112 for r in (top + 1)..=bot {
1113 let line_len = buf_line_chars(&ed.buffer, r);
1114 if col > line_len {
1115 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1116 ed.mutate_edit(Edit::InsertStr {
1117 at: Position::new(r, line_len),
1118 text: pad,
1119 });
1120 }
1121 ed.mutate_edit(Edit::InsertStr {
1122 at: Position::new(r, col),
1123 text: inserted.to_string(),
1124 });
1125 }
1126 }
1127
1128 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1129 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1132 replicate_block_text(ed, &inserted, top, bot, col);
1133 buf_set_cursor_rc(&mut ed.buffer, top, col);
1134 ed.push_buffer_cursor_to_textarea();
1135 }
1136 return;
1137 }
1138 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1139 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1143 replicate_block_text(ed, &inserted, top, bot, col);
1144 let ins_chars = inserted.chars().count();
1145 let line_len = buf_line_chars(&ed.buffer, top);
1146 let target_col = (col + ins_chars).min(line_len);
1147 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1148 ed.push_buffer_cursor_to_textarea();
1149 }
1150 return;
1151 }
1152 if ed.vim.replaying {
1153 return;
1154 }
1155 match session.reason {
1156 InsertReason::Enter(entry) => {
1157 ed.vim.last_change = Some(LastChange::InsertAt {
1158 entry,
1159 inserted,
1160 count: session.count,
1161 });
1162 }
1163 InsertReason::Open { above } => {
1164 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1165 }
1166 InsertReason::AfterChange => {
1167 if let Some(
1168 LastChange::OpMotion { inserted: ins, .. }
1169 | LastChange::OpTextObj { inserted: ins, .. }
1170 | LastChange::LineOp { inserted: ins, .. },
1171 ) = ed.vim.last_change.as_mut()
1172 {
1173 *ins = Some(inserted);
1174 }
1175 if let Some(start) = ed.vim.change_mark_start.take() {
1181 let end = ed.cursor();
1182 ed.set_mark('[', start);
1183 ed.set_mark(']', end);
1184 }
1185 }
1186 InsertReason::DeleteToEol => {
1187 ed.vim.last_change = Some(LastChange::DeleteToEol {
1188 inserted: Some(inserted),
1189 });
1190 }
1191 InsertReason::ReplayOnly => {}
1192 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1193 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1194 InsertReason::Replace => {
1195 ed.vim.last_change = Some(LastChange::DeleteToEol {
1200 inserted: Some(inserted),
1201 });
1202 }
1203 }
1204}
1205
1206pub(crate) fn begin_insert<H: crate::types::Host>(
1207 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1208 count: usize,
1209 reason: InsertReason,
1210) {
1211 let record = !matches!(reason, InsertReason::ReplayOnly);
1212 if record {
1213 ed.push_undo();
1214 }
1215 let reason = if ed.vim.replaying {
1216 InsertReason::ReplayOnly
1217 } else {
1218 reason
1219 };
1220 let (row, _) = ed.cursor();
1221 ed.vim.insert_session = Some(InsertSession {
1222 count,
1223 row_min: row,
1224 row_max: row,
1225 before_rope: crate::types::Query::rope(&ed.buffer),
1226 reason,
1227 });
1228 ed.vim.mode = Mode::Insert;
1229 ed.vim.current_mode = crate::VimMode::Insert;
1231}
1232
1233pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1248 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1249) {
1250 if !ed.settings.undo_break_on_motion {
1251 return;
1252 }
1253 if ed.vim.replaying {
1254 return;
1255 }
1256 if ed.vim.insert_session.is_none() {
1257 return;
1258 }
1259 ed.push_undo();
1260 let before_rope = crate::types::Query::rope(&ed.buffer);
1261 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1262 if let Some(ref mut session) = ed.vim.insert_session {
1263 session.before_rope = before_rope;
1264 session.row_min = row;
1265 session.row_max = row;
1266 }
1267}
1268
1269fn autopair_close_for(
1301 ch: char,
1302 filetype: &str,
1303 prev_char: Option<char>,
1304 prev2_char: Option<char>,
1305) -> Option<char> {
1306 let is_triple_quote_third =
1312 matches!(ch, '"' | '`' | '\'') && prev_char == Some(ch) && prev2_char == Some(ch);
1313
1314 match ch {
1315 '(' => Some(')'),
1316 '[' => Some(']'),
1317 '{' => Some('}'),
1318 '"' => {
1319 if is_triple_quote_third {
1320 None
1321 } else {
1322 Some('"')
1323 }
1324 }
1325 '`' => {
1326 if is_triple_quote_third {
1327 None
1328 } else {
1329 Some('`')
1330 }
1331 }
1332 '<' => {
1333 if is_html_filetype(filetype) {
1334 Some('>')
1335 } else {
1336 None
1337 }
1338 }
1339 '\'' => {
1340 if is_triple_quote_third {
1341 return None;
1342 }
1343 if prev_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
1346 None
1347 } else {
1348 Some('\'')
1349 }
1350 }
1351 _ => None,
1352 }
1353}
1354
1355fn detect_code_fence_opener(line: &str, cursor_col: usize) -> Option<String> {
1370 if cursor_col != line.chars().count() {
1371 return None;
1372 }
1373 let trimmed = line.trim_start();
1374 let backtick_run = trimmed.chars().take_while(|c| *c == '`').count();
1375 if backtick_run < 3 {
1376 return None;
1377 }
1378 let rest = &trimmed[backtick_run..];
1379 if rest.is_empty() {
1380 return None;
1381 }
1382 let all_lang_chars = rest
1383 .chars()
1384 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-');
1385 if !all_lang_chars {
1386 return None;
1387 }
1388 Some("`".repeat(backtick_run))
1389}
1390
1391fn is_html_filetype(ft: &str) -> bool {
1393 matches!(
1394 ft,
1395 "html" | "xml" | "svg" | "jsx" | "tsx" | "vue" | "svelte"
1396 )
1397}
1398
1399#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1415enum TagKind {
1416 Open,
1417 Close,
1418}
1419
1420#[derive(Debug, Clone, PartialEq, Eq)]
1422struct TagSpan {
1423 kind: TagKind,
1424 name: String,
1425 row: usize,
1427 name_start_col: usize,
1429 name_end_col: usize,
1430}
1431
1432fn detect_tag_at_cursor(line: &str, row: usize, col: usize) -> Option<TagSpan> {
1436 let chars: Vec<char> = line.chars().collect();
1437 let mut lt = None;
1439 let mut i = col.min(chars.len());
1440 while i > 0 {
1441 i -= 1;
1442 let c = chars[i];
1443 if c == '<' {
1444 lt = Some(i);
1445 break;
1446 }
1447 if c == '>' {
1449 return None;
1450 }
1451 }
1452 let lt = lt?;
1453 let (kind, name_start) = if chars.get(lt + 1) == Some(&'/') {
1455 (TagKind::Close, lt + 2)
1456 } else {
1457 (TagKind::Open, lt + 1)
1458 };
1459 let first = chars.get(name_start)?;
1461 if !first.is_ascii_alphabetic() {
1462 return None;
1463 }
1464 let mut name_end = name_start;
1466 while name_end < chars.len()
1467 && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-')
1468 {
1469 name_end += 1;
1470 }
1471 if col < name_start || col > name_end {
1475 return None;
1476 }
1477 let name: String = chars[name_start..name_end].iter().collect();
1478 Some(TagSpan {
1479 kind,
1480 name,
1481 row,
1482 name_start_col: name_start,
1483 name_end_col: name_end,
1484 })
1485}
1486
1487fn find_matching_tag(buffer: &hjkl_buffer::Buffer, anchor: &TagSpan) -> Option<TagSpan> {
1501 let row_count = buffer.row_count();
1502 let scan_forward = anchor.kind == TagKind::Open;
1503 let row_iter: Box<dyn Iterator<Item = usize>> = if scan_forward {
1504 Box::new(anchor.row..row_count)
1505 } else {
1506 Box::new((0..=anchor.row).rev())
1507 };
1508 let push_kind = if scan_forward {
1509 TagKind::Open
1510 } else {
1511 TagKind::Close
1512 };
1513 let mut depth: usize = 1;
1514
1515 for r in row_iter {
1516 let line = buf_line(buffer, r)?;
1517 let chars: Vec<char> = line.chars().collect();
1518 let tags = scan_line_tags(&chars, r);
1519 let tags_iter: Box<dyn Iterator<Item = TagSpan>> = if scan_forward {
1520 Box::new(tags.into_iter())
1521 } else {
1522 Box::new(tags.into_iter().rev())
1523 };
1524 for tag in tags_iter {
1525 if r == anchor.row
1527 && tag.name_start_col == anchor.name_start_col
1528 && tag.kind == anchor.kind
1529 {
1530 continue;
1531 }
1532 if r == anchor.row {
1536 if scan_forward && tag.name_start_col < anchor.name_start_col {
1537 continue;
1538 }
1539 if !scan_forward && tag.name_start_col > anchor.name_start_col {
1540 continue;
1541 }
1542 }
1543 if tag.kind == push_kind {
1544 depth += 1;
1545 } else {
1546 depth -= 1;
1547 if depth == 0 {
1548 return Some(tag);
1549 }
1550 }
1551 }
1552 }
1553 None
1554}
1555
1556fn scan_line_tags(chars: &[char], row: usize) -> Vec<TagSpan> {
1560 let mut out = Vec::new();
1561 let n = chars.len();
1562 let mut i = 0;
1563 while i < n {
1564 if chars[i] != '<' {
1565 i += 1;
1566 continue;
1567 }
1568 if chars[i..].starts_with(&['<', '!', '-', '-']) {
1570 let mut j = i + 4;
1571 while j + 2 < n && !(chars[j] == '-' && chars[j + 1] == '-' && chars[j + 2] == '>') {
1572 j += 1;
1573 }
1574 i = (j + 3).min(n);
1575 continue;
1576 }
1577 let (kind, name_start) = if chars.get(i + 1) == Some(&'/') {
1578 (TagKind::Close, i + 2)
1579 } else {
1580 (TagKind::Open, i + 1)
1581 };
1582 if chars
1584 .get(name_start)
1585 .is_none_or(|c| !c.is_ascii_alphabetic())
1586 {
1587 i += 1;
1588 continue;
1589 }
1590 let mut name_end = name_start;
1591 while name_end < n && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-') {
1592 name_end += 1;
1593 }
1594 let mut k = name_end;
1596 let mut self_closing = false;
1597 while k < n {
1598 if chars[k] == '>' {
1599 if k > name_end && chars[k - 1] == '/' {
1600 self_closing = true;
1601 }
1602 break;
1603 }
1604 k += 1;
1605 }
1606 if k >= n {
1607 break;
1609 }
1610 let name: String = chars[name_start..name_end].iter().collect();
1611 if !(self_closing || kind == TagKind::Open && is_void_element(&name)) {
1613 out.push(TagSpan {
1614 kind,
1615 name,
1616 row,
1617 name_start_col: name_start,
1618 name_end_col: name_end,
1619 });
1620 }
1621 i = k + 1;
1622 }
1623 out
1624}
1625
1626pub(crate) fn sync_paired_tag_on_exit<H: crate::types::Host>(
1631 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1632) {
1633 if !is_html_filetype(&ed.settings.filetype) {
1634 return;
1635 }
1636 let (row, col) = ed.cursor();
1637 let line = match buf_line(&ed.buffer, row) {
1638 Some(l) => l,
1639 None => return,
1640 };
1641 let anchor = match detect_tag_at_cursor(&line, row, col) {
1642 Some(t) => t,
1643 None => return,
1644 };
1645 let partner = match find_matching_tag(&ed.buffer, &anchor) {
1646 Some(t) => t,
1647 None => return,
1648 };
1649 if partner.name == anchor.name {
1650 return;
1651 }
1652 use hjkl_buffer::{Edit, MotionKind, Position};
1654 let start = Position::new(partner.row, partner.name_start_col);
1655 let end = Position::new(partner.row, partner.name_end_col);
1656 ed.mutate_edit(Edit::DeleteRange {
1657 start,
1658 end,
1659 kind: MotionKind::Char,
1660 });
1661 ed.mutate_edit(Edit::InsertStr {
1662 at: start,
1663 text: anchor.name.clone(),
1664 });
1665 buf_set_cursor_rc(&mut ed.buffer, row, col);
1668 ed.push_buffer_cursor_to_textarea();
1669}
1670
1671fn is_void_element(tag: &str) -> bool {
1673 matches!(
1674 tag.to_ascii_lowercase().as_str(),
1675 "area"
1676 | "base"
1677 | "br"
1678 | "col"
1679 | "embed"
1680 | "hr"
1681 | "img"
1682 | "input"
1683 | "link"
1684 | "meta"
1685 | "param"
1686 | "source"
1687 | "track"
1688 | "wbr"
1689 )
1690}
1691
1692fn scan_tag_opener(line: &str, col: usize) -> Option<String> {
1702 let before = if col > 0 { &line[..col] } else { return None };
1705
1706 let lt_pos = before.rfind('<')?;
1708 let inner = &before[lt_pos + 1..]; if inner.starts_with('!') {
1712 return None;
1713 }
1714 if inner.trim_end().ends_with('/') {
1716 return None;
1717 }
1718
1719 let tag: String = inner
1721 .chars()
1722 .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1723 .collect();
1724 if tag.is_empty() {
1725 return None;
1726 }
1727 if !tag
1729 .chars()
1730 .next()
1731 .map(|c| c.is_ascii_alphabetic())
1732 .unwrap_or(false)
1733 {
1734 return None;
1735 }
1736 if is_void_element(&tag) {
1737 return None;
1738 }
1739 Some(tag)
1740}
1741
1742pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1747 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1748 ch: char,
1749) -> bool {
1750 use hjkl_buffer::{Edit, MotionKind, Position};
1751 ed.sync_buffer_content_from_textarea();
1752 let cursor = buf_cursor_pos(&ed.buffer);
1753 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1754 let in_replace = matches!(
1755 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1756 Some(InsertReason::Replace)
1757 );
1758
1759 if !in_replace
1768 && !ed.vim.pending_closes.is_empty()
1769 && let Some(&(pr, _pc, pch)) = ed.vim.pending_closes.last()
1770 && ch == pch
1771 && cursor.row == pr
1772 {
1773 let char_at_cursor =
1774 buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col));
1775 if char_at_cursor == Some(ch) {
1776 ed.vim.pending_closes.pop();
1777 let filetype = ed.settings.filetype.clone();
1779 let autoclose_tag = ed.settings.autoclose_tag;
1780 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1781 let new_col = cursor.col + 1;
1783 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1784 if let Some(line) = buf_line(&ed.buffer, cursor.row)
1786 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
1787 {
1788 let close_tag = format!("</{tag}>");
1789 let insert_pos = Position::new(cursor.row, new_col);
1790 ed.mutate_edit(Edit::InsertStr {
1791 at: insert_pos,
1792 text: close_tag,
1793 });
1794 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1796 }
1797 } else {
1798 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
1799 }
1800 ed.push_buffer_cursor_to_textarea();
1801 return true;
1802 }
1803 }
1804
1805 if in_replace && cursor.col < line_chars {
1806 ed.vim.pending_closes.clear();
1808 ed.mutate_edit(Edit::DeleteRange {
1809 start: cursor,
1810 end: Position::new(cursor.row, cursor.col + 1),
1811 kind: MotionKind::Char,
1812 });
1813 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1814 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1815 let autopair = ed.settings.autopair;
1817 let filetype = ed.settings.filetype.clone();
1818 let autoclose_tag = ed.settings.autoclose_tag;
1819
1820 let (prev_char, prev2_char) = {
1821 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1822 let chars: Vec<char> = line.chars().collect();
1823 let p1 = if cursor.col > 0 {
1824 chars.get(cursor.col - 1).copied()
1825 } else {
1826 None
1827 };
1828 let p2 = if cursor.col > 1 {
1829 chars.get(cursor.col - 2).copied()
1830 } else {
1831 None
1832 };
1833 (p1, p2)
1834 };
1835
1836 if autopair {
1837 if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
1838 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1840 let after = Position::new(cursor.row, cursor.col + 1);
1843 ed.mutate_edit(Edit::InsertChar {
1844 at: after,
1845 ch: close,
1846 });
1847 let between_col = cursor.col + 1;
1850 buf_set_cursor_rc(&mut ed.buffer, cursor.row, between_col);
1851 ed.vim.pending_closes.push((cursor.row, between_col, close));
1856 ed.push_buffer_cursor_to_textarea();
1857 return true;
1858 }
1859
1860 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1864 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1865 let new_col = cursor.col + 1;
1866 if let Some(line) = buf_line(&ed.buffer, cursor.row)
1869 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
1870 {
1871 let close_tag = format!("</{tag}>");
1872 let insert_pos = Position::new(cursor.row, new_col);
1873 ed.mutate_edit(Edit::InsertStr {
1874 at: insert_pos,
1875 text: close_tag,
1876 });
1877 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1879 }
1880 ed.push_buffer_cursor_to_textarea();
1881 return true;
1882 }
1883 }
1884
1885 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1890 }
1891 ed.push_buffer_cursor_to_textarea();
1892 true
1893}
1894
1895pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1901 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1902) -> bool {
1903 use hjkl_buffer::Edit;
1904 ed.sync_buffer_content_from_textarea();
1905 let cursor = buf_cursor_pos(&ed.buffer);
1906 let prev_line = buf_line(&ed.buffer, cursor.row)
1907 .unwrap_or_default()
1908 .to_string();
1909
1910 if ed.settings.autopair && !ed.vim.pending_closes.is_empty() {
1914 let prev_char = if cursor.col > 0 {
1917 prev_line.chars().nth(cursor.col - 1)
1918 } else {
1919 None
1920 };
1921 let next_char = prev_line.chars().nth(cursor.col);
1922 let is_open_pair = matches!(
1923 (prev_char, next_char),
1924 (Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
1925 );
1926 if is_open_pair {
1927 ed.vim.pending_closes.clear();
1930 let base_indent: String = prev_line
1932 .chars()
1933 .take_while(|c| *c == ' ' || *c == '\t')
1934 .collect();
1935 let inner_indent = if ed.settings.expandtab {
1936 let unit = if ed.settings.softtabstop > 0 {
1937 ed.settings.softtabstop
1938 } else {
1939 ed.settings.shiftwidth
1940 };
1941 format!("{base_indent}{}", " ".repeat(unit))
1942 } else {
1943 format!("{base_indent}\t")
1944 };
1945 let text = format!("\n{inner_indent}\n{base_indent}");
1948 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1949 let new_row = cursor.row + 1;
1951 let new_col = inner_indent.len();
1952 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1953 ed.push_buffer_cursor_to_textarea();
1954 return true;
1955 }
1956 }
1957
1958 if ed.settings.autopair
1967 && let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
1968 {
1969 ed.vim.pending_closes.clear();
1970 let base_indent: String = prev_line
1971 .chars()
1972 .take_while(|c| *c == ' ' || *c == '\t')
1973 .collect();
1974 let text = format!("\n{base_indent}\n{base_indent}{fence}");
1975 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1976 let new_row = cursor.row + 1;
1977 let new_col = base_indent.chars().count();
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 let comment_cont = if ed.settings.formatoptions.contains('r') {
1985 continue_comment(&ed.buffer, &ed.settings, cursor.row)
1986 } else {
1987 None
1988 };
1989
1990 ed.vim.pending_closes.clear();
1992
1993 let text = if let Some(cont) = comment_cont {
1994 format!("\n{cont}")
1997 } else {
1998 let indent = compute_enter_indent(&ed.settings, &prev_line);
1999 format!("\n{indent}")
2000 };
2001 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2002 ed.push_buffer_cursor_to_textarea();
2003 true
2004}
2005
2006pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
2009 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2010) -> bool {
2011 use hjkl_buffer::Edit;
2012 ed.sync_buffer_content_from_textarea();
2013 let cursor = buf_cursor_pos(&ed.buffer);
2014 if ed.settings.expandtab {
2015 let sts = ed.settings.softtabstop;
2016 let n = if sts > 0 {
2017 sts - (cursor.col % sts)
2018 } else {
2019 ed.settings.tabstop.max(1)
2020 };
2021 ed.mutate_edit(Edit::InsertStr {
2022 at: cursor,
2023 text: " ".repeat(n),
2024 });
2025 } else {
2026 ed.mutate_edit(Edit::InsertChar {
2027 at: cursor,
2028 ch: '\t',
2029 });
2030 }
2031 ed.push_buffer_cursor_to_textarea();
2032 true
2033}
2034
2035pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
2046 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2047) -> bool {
2048 use hjkl_buffer::{Edit, MotionKind, Position};
2049 ed.sync_buffer_content_from_textarea();
2050 let cursor = buf_cursor_pos(&ed.buffer);
2051
2052 if cursor.col > 0 {
2055 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2056 if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings.filetype, &line) {
2057 let full_prefix = format!("{indent}{prefix}");
2058 let line_trimmed = line.trim_end_matches(' ');
2061 let prefix_trimmed = full_prefix.trim_end_matches(' ');
2062 if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
2063 ed.mutate_edit(Edit::DeleteRange {
2065 start: Position::new(cursor.row, 0),
2066 end: cursor,
2067 kind: MotionKind::Char,
2068 });
2069 ed.push_buffer_cursor_to_textarea();
2070 return true;
2071 }
2072 }
2073 }
2074
2075 let sts = ed.settings.softtabstop;
2076 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
2077 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2078 let chars: Vec<char> = line.chars().collect();
2079 let run_start = cursor.col - sts;
2080 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
2081 ed.mutate_edit(Edit::DeleteRange {
2082 start: Position::new(cursor.row, run_start),
2083 end: cursor,
2084 kind: MotionKind::Char,
2085 });
2086 ed.push_buffer_cursor_to_textarea();
2087 return true;
2088 }
2089 }
2090 let result = if cursor.col > 0 {
2091 ed.mutate_edit(Edit::DeleteRange {
2092 start: Position::new(cursor.row, cursor.col - 1),
2093 end: cursor,
2094 kind: MotionKind::Char,
2095 });
2096 true
2097 } else if cursor.row > 0 {
2098 let prev_row = cursor.row - 1;
2099 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2100 ed.mutate_edit(Edit::JoinLines {
2101 row: prev_row,
2102 count: 1,
2103 with_space: false,
2104 });
2105 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2106 true
2107 } else {
2108 false
2109 };
2110 ed.push_buffer_cursor_to_textarea();
2111 result
2112}
2113
2114pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
2117 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2118) -> bool {
2119 use hjkl_buffer::{Edit, MotionKind, Position};
2120 ed.sync_buffer_content_from_textarea();
2121 let cursor = buf_cursor_pos(&ed.buffer);
2122 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2123 let result = if cursor.col < line_chars {
2124 ed.mutate_edit(Edit::DeleteRange {
2125 start: cursor,
2126 end: Position::new(cursor.row, cursor.col + 1),
2127 kind: MotionKind::Char,
2128 });
2129 buf_set_cursor_pos(&mut ed.buffer, cursor);
2130 true
2131 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
2132 ed.mutate_edit(Edit::JoinLines {
2133 row: cursor.row,
2134 count: 1,
2135 with_space: false,
2136 });
2137 buf_set_cursor_pos(&mut ed.buffer, cursor);
2138 true
2139 } else {
2140 false
2141 };
2142 ed.push_buffer_cursor_to_textarea();
2143 result
2144}
2145
2146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2148pub enum InsertDir {
2149 Left,
2150 Right,
2151 Up,
2152 Down,
2153}
2154
2155pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
2159 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2160 dir: InsertDir,
2161) -> bool {
2162 ed.sync_buffer_content_from_textarea();
2163 ed.vim.pending_closes.clear();
2164 match dir {
2165 InsertDir::Left => {
2166 crate::motions::move_left(&mut ed.buffer, 1);
2167 }
2168 InsertDir::Right => {
2169 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2170 }
2171 InsertDir::Up => {
2172 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2173 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2174 }
2175 InsertDir::Down => {
2176 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2177 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2178 }
2179 }
2180 break_undo_group_in_insert(ed);
2181 ed.push_buffer_cursor_to_textarea();
2182 false
2183}
2184
2185pub(crate) fn insert_home_bridge<H: crate::types::Host>(
2188 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2189) -> bool {
2190 ed.sync_buffer_content_from_textarea();
2191 ed.vim.pending_closes.clear();
2192 crate::motions::move_line_start(&mut ed.buffer);
2193 break_undo_group_in_insert(ed);
2194 ed.push_buffer_cursor_to_textarea();
2195 false
2196}
2197
2198pub(crate) fn insert_end_bridge<H: crate::types::Host>(
2201 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2202) -> bool {
2203 ed.sync_buffer_content_from_textarea();
2204 ed.vim.pending_closes.clear();
2205 crate::motions::move_line_end(&mut ed.buffer);
2206 break_undo_group_in_insert(ed);
2207 ed.push_buffer_cursor_to_textarea();
2208 false
2209}
2210
2211pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
2214 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2215 viewport_h: u16,
2216) -> bool {
2217 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2218 scroll_cursor_rows(ed, -rows);
2219 false
2220}
2221
2222pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
2225 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2226 viewport_h: u16,
2227) -> bool {
2228 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2229 scroll_cursor_rows(ed, rows);
2230 false
2231}
2232
2233pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
2237 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2238) -> bool {
2239 use hjkl_buffer::{Edit, MotionKind};
2240 ed.sync_buffer_content_from_textarea();
2241 let cursor = buf_cursor_pos(&ed.buffer);
2242 if cursor.row == 0 && cursor.col == 0 {
2243 return true;
2244 }
2245 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
2246 let word_start = buf_cursor_pos(&ed.buffer);
2247 if word_start == cursor {
2248 return true;
2249 }
2250 buf_set_cursor_pos(&mut ed.buffer, cursor);
2251 ed.mutate_edit(Edit::DeleteRange {
2252 start: word_start,
2253 end: cursor,
2254 kind: MotionKind::Char,
2255 });
2256 ed.push_buffer_cursor_to_textarea();
2257 true
2258}
2259
2260pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
2263 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2264) -> bool {
2265 use hjkl_buffer::{Edit, MotionKind, Position};
2266 ed.sync_buffer_content_from_textarea();
2267 let cursor = buf_cursor_pos(&ed.buffer);
2268 if cursor.col > 0 {
2269 ed.mutate_edit(Edit::DeleteRange {
2270 start: Position::new(cursor.row, 0),
2271 end: cursor,
2272 kind: MotionKind::Char,
2273 });
2274 ed.push_buffer_cursor_to_textarea();
2275 }
2276 true
2277}
2278
2279pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
2283 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2284) -> bool {
2285 use hjkl_buffer::{Edit, MotionKind, Position};
2286 ed.sync_buffer_content_from_textarea();
2287 let cursor = buf_cursor_pos(&ed.buffer);
2288 if cursor.col > 0 {
2289 ed.mutate_edit(Edit::DeleteRange {
2290 start: Position::new(cursor.row, cursor.col - 1),
2291 end: cursor,
2292 kind: MotionKind::Char,
2293 });
2294 } else if cursor.row > 0 {
2295 let prev_row = cursor.row - 1;
2296 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2297 ed.mutate_edit(Edit::JoinLines {
2298 row: prev_row,
2299 count: 1,
2300 with_space: false,
2301 });
2302 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2303 }
2304 ed.push_buffer_cursor_to_textarea();
2305 true
2306}
2307
2308pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
2311 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2312) -> bool {
2313 let (row, col) = ed.cursor();
2314 let sw = ed.settings().shiftwidth;
2315 indent_rows(ed, row, row, 1);
2316 ed.jump_cursor(row, col + sw);
2317 true
2318}
2319
2320pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
2323 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2324) -> bool {
2325 let (row, col) = ed.cursor();
2326 let before_len = buf_line_bytes(&ed.buffer, row);
2327 outdent_rows(ed, row, row, 1);
2328 let after_len = buf_line_bytes(&ed.buffer, row);
2329 let stripped = before_len.saturating_sub(after_len);
2330 let new_col = col.saturating_sub(stripped);
2331 ed.jump_cursor(row, new_col);
2332 true
2333}
2334
2335pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
2339 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2340) -> bool {
2341 ed.vim.one_shot_normal = true;
2342 ed.vim.mode = Mode::Normal;
2343 ed.vim.current_mode = crate::VimMode::Normal;
2345 false
2346}
2347
2348pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
2352 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2353) -> bool {
2354 ed.vim.insert_pending_register = true;
2355 false
2356}
2357
2358pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
2362 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2363 reg: char,
2364) -> bool {
2365 insert_register_text(ed, reg);
2366 true
2369}
2370
2371pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
2377 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2378) -> bool {
2379 ed.vim.pending_closes.clear();
2380 finish_insert_session(ed);
2381 sync_paired_tag_on_exit(ed);
2385 ed.vim.mode = Mode::Normal;
2386 ed.vim.current_mode = crate::VimMode::Normal;
2388 let col = ed.cursor().1;
2389 ed.vim.last_insert_pos = Some(ed.cursor());
2390 if col > 0 {
2391 crate::motions::move_left(&mut ed.buffer, 1);
2392 ed.push_buffer_cursor_to_textarea();
2393 }
2394 ed.sticky_col = Some(ed.cursor().1);
2395 true
2396}
2397
2398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2403pub enum ScrollDir {
2404 Down,
2406 Up,
2408}
2409
2410pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
2415 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2416 count: usize,
2417) {
2418 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2419}
2420
2421pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
2423 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2424 count: usize,
2425) {
2426 move_first_non_whitespace(ed);
2427 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2428}
2429
2430pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
2432 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2433 count: usize,
2434) {
2435 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2436 ed.push_buffer_cursor_to_textarea();
2437 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2438}
2439
2440pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
2442 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2443 count: usize,
2444) {
2445 crate::motions::move_line_end(&mut ed.buffer);
2446 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2447 ed.push_buffer_cursor_to_textarea();
2448 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2449}
2450
2451pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
2455 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2456 count: usize,
2457) {
2458 use hjkl_buffer::{Edit, Position};
2459 ed.push_undo();
2460 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2461 ed.sync_buffer_content_from_textarea();
2462 let row = buf_cursor_pos(&ed.buffer).row;
2463 let line_chars = buf_line_chars(&ed.buffer, row);
2464 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
2465
2466 let comment_cont = if ed.settings.formatoptions.contains('o') {
2468 continue_comment(&ed.buffer, &ed.settings, row)
2469 } else {
2470 None
2471 };
2472
2473 let suffix = if let Some(cont) = comment_cont {
2474 format!("\n{cont}")
2475 } else {
2476 let indent = compute_enter_indent(&ed.settings, &prev_line);
2477 format!("\n{indent}")
2478 };
2479 ed.mutate_edit(Edit::InsertStr {
2480 at: Position::new(row, line_chars),
2481 text: suffix,
2482 });
2483 ed.push_buffer_cursor_to_textarea();
2484}
2485
2486pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
2490 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2491 count: usize,
2492) {
2493 use hjkl_buffer::{Edit, Position};
2494 ed.push_undo();
2495 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2496 ed.sync_buffer_content_from_textarea();
2497 let row = buf_cursor_pos(&ed.buffer).row;
2498
2499 let comment_cont = if ed.settings.formatoptions.contains('o') {
2501 continue_comment(&ed.buffer, &ed.settings, row)
2502 } else {
2503 None
2504 };
2505
2506 let (insert_text, new_line_content) = if let Some(cont) = comment_cont {
2509 let content = cont.clone();
2510 (format!("{cont}\n"), content)
2511 } else {
2512 let indent = if row > 0 {
2513 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
2514 compute_enter_indent(&ed.settings, &above)
2515 } else {
2516 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
2517 cur.chars()
2518 .take_while(|c| *c == ' ' || *c == '\t')
2519 .collect::<String>()
2520 };
2521 let content = indent.clone();
2522 (format!("{indent}\n"), content)
2523 };
2524 ed.mutate_edit(Edit::InsertStr {
2525 at: Position::new(row, 0),
2526 text: insert_text,
2527 });
2528 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2529 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2530 let new_row = buf_cursor_pos(&ed.buffer).row;
2531 buf_set_cursor_rc(&mut ed.buffer, new_row, new_line_content.chars().count());
2532 ed.push_buffer_cursor_to_textarea();
2533}
2534
2535pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
2537 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2538 count: usize,
2539) {
2540 begin_insert(ed, count.max(1), InsertReason::Replace);
2541}
2542
2543pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
2548 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2549 count: usize,
2550) {
2551 do_char_delete(ed, true, count.max(1));
2552 if !ed.vim.replaying {
2553 ed.vim.last_change = Some(LastChange::CharDel {
2554 forward: true,
2555 count: count.max(1),
2556 });
2557 }
2558}
2559
2560pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
2563 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2564 count: usize,
2565) {
2566 do_char_delete(ed, false, count.max(1));
2567 if !ed.vim.replaying {
2568 ed.vim.last_change = Some(LastChange::CharDel {
2569 forward: false,
2570 count: count.max(1),
2571 });
2572 }
2573}
2574
2575pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
2578 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2579 count: usize,
2580) {
2581 use hjkl_buffer::{Edit, MotionKind, Position};
2582 ed.push_undo();
2583 ed.sync_buffer_content_from_textarea();
2584 for _ in 0..count.max(1) {
2585 let cursor = buf_cursor_pos(&ed.buffer);
2586 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2587 if cursor.col >= line_chars {
2588 break;
2589 }
2590 ed.mutate_edit(Edit::DeleteRange {
2591 start: cursor,
2592 end: Position::new(cursor.row, cursor.col + 1),
2593 kind: MotionKind::Char,
2594 });
2595 }
2596 ed.push_buffer_cursor_to_textarea();
2597 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
2598 if !ed.vim.replaying {
2599 ed.vim.last_change = Some(LastChange::OpMotion {
2600 op: Operator::Change,
2601 motion: Motion::Right,
2602 count: count.max(1),
2603 inserted: None,
2604 });
2605 }
2606}
2607
2608pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
2611 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2612 count: usize,
2613) {
2614 execute_line_op(ed, Operator::Change, count.max(1));
2615 if !ed.vim.replaying {
2616 ed.vim.last_change = Some(LastChange::LineOp {
2617 op: Operator::Change,
2618 count: count.max(1),
2619 inserted: None,
2620 });
2621 }
2622}
2623
2624pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2627 ed.push_undo();
2628 delete_to_eol(ed);
2629 crate::motions::move_left(&mut ed.buffer, 1);
2630 ed.push_buffer_cursor_to_textarea();
2631 if !ed.vim.replaying {
2632 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2633 }
2634}
2635
2636pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2639 ed.push_undo();
2640 delete_to_eol(ed);
2641 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2642}
2643
2644pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
2646 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2647 count: usize,
2648) {
2649 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2650}
2651
2652pub(crate) fn join_line_bridge<H: crate::types::Host>(
2655 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2656 count: usize,
2657) {
2658 for _ in 0..count.max(1) {
2659 ed.push_undo();
2660 join_line(ed);
2661 }
2662 if !ed.vim.replaying {
2663 ed.vim.last_change = Some(LastChange::JoinLine {
2664 count: count.max(1),
2665 });
2666 }
2667}
2668
2669pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
2672 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2673 count: usize,
2674) {
2675 for _ in 0..count.max(1) {
2676 ed.push_undo();
2677 toggle_case_at_cursor(ed);
2678 }
2679 if !ed.vim.replaying {
2680 ed.vim.last_change = Some(LastChange::ToggleCase {
2681 count: count.max(1),
2682 });
2683 }
2684}
2685
2686pub(crate) fn paste_after_bridge<H: crate::types::Host>(
2690 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2691 count: usize,
2692) {
2693 do_paste(ed, false, count.max(1));
2694 if !ed.vim.replaying {
2695 ed.vim.last_change = Some(LastChange::Paste {
2696 before: false,
2697 count: count.max(1),
2698 });
2699 }
2700}
2701
2702pub(crate) fn paste_before_bridge<H: crate::types::Host>(
2706 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2707 count: usize,
2708) {
2709 do_paste(ed, true, count.max(1));
2710 if !ed.vim.replaying {
2711 ed.vim.last_change = Some(LastChange::Paste {
2712 before: true,
2713 count: count.max(1),
2714 });
2715 }
2716}
2717
2718pub(crate) fn jump_back_bridge<H: crate::types::Host>(
2723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2724 count: usize,
2725) {
2726 for _ in 0..count.max(1) {
2727 jump_back(ed);
2728 }
2729}
2730
2731pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
2734 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2735 count: usize,
2736) {
2737 for _ in 0..count.max(1) {
2738 jump_forward(ed);
2739 }
2740}
2741
2742pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
2747 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2748 dir: ScrollDir,
2749 count: usize,
2750) {
2751 let rows = viewport_full_rows(ed, count) as isize;
2752 match dir {
2753 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2754 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2755 }
2756}
2757
2758pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
2761 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2762 dir: ScrollDir,
2763 count: usize,
2764) {
2765 let rows = viewport_half_rows(ed, count) as isize;
2766 match dir {
2767 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2768 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2769 }
2770}
2771
2772pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
2776 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2777 dir: ScrollDir,
2778 count: usize,
2779) {
2780 let n = count.max(1);
2781 let total = buf_row_count(&ed.buffer);
2782 let last = total.saturating_sub(1);
2783 let h = ed.viewport_height_value() as usize;
2784 let vp = ed.host().viewport();
2785 let cur_top = vp.top_row;
2786 let new_top = match dir {
2787 ScrollDir::Down => (cur_top + n).min(last),
2788 ScrollDir::Up => cur_top.saturating_sub(n),
2789 };
2790 ed.set_viewport_top(new_top);
2791 let (row, col) = ed.cursor();
2793 let bot = (new_top + h).saturating_sub(1).min(last);
2794 let clamped = row.max(new_top).min(bot);
2795 if clamped != row {
2796 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
2797 ed.push_buffer_cursor_to_textarea();
2798 }
2799}
2800
2801pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
2806 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2807 forward: bool,
2808 count: usize,
2809) {
2810 if let Some(pattern) = ed.vim.last_search.clone() {
2811 ed.push_search_pattern(&pattern);
2812 }
2813 if ed.search_state().pattern.is_none() {
2814 return;
2815 }
2816 let go_forward = ed.vim.last_search_forward == forward;
2817 for _ in 0..count.max(1) {
2818 if go_forward {
2819 ed.search_advance_forward(true);
2820 } else {
2821 ed.search_advance_backward(true);
2822 }
2823 }
2824 ed.push_buffer_cursor_to_textarea();
2825}
2826
2827pub(crate) fn word_search_bridge<H: crate::types::Host>(
2831 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2832 forward: bool,
2833 whole_word: bool,
2834 count: usize,
2835) {
2836 word_at_cursor_search(ed, forward, whole_word, count.max(1));
2837}
2838
2839#[allow(dead_code)]
2844#[inline]
2845pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2846 do_undo(ed);
2847}
2848
2849#[inline]
2864pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
2865 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2866 mode: Mode,
2867) {
2868 ed.vim.mode = mode;
2869 ed.vim.current_mode = ed.vim.public_mode();
2870}
2871
2872pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
2875 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2876) {
2877 let cur = ed.cursor();
2878 ed.vim.visual_anchor = cur;
2879 set_vim_mode_bridge(ed, Mode::Visual);
2880}
2881
2882pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
2885 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2886) {
2887 let (row, _) = ed.cursor();
2888 ed.vim.visual_line_anchor = row;
2889 set_vim_mode_bridge(ed, Mode::VisualLine);
2890}
2891
2892pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
2896 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2897) {
2898 let cur = ed.cursor();
2899 ed.vim.block_anchor = cur;
2900 ed.vim.block_vcol = cur.1;
2901 set_vim_mode_bridge(ed, Mode::VisualBlock);
2902}
2903
2904pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
2909 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2910) {
2911 let snap: Option<LastVisual> = match ed.vim.mode {
2913 Mode::Visual => Some(LastVisual {
2914 mode: Mode::Visual,
2915 anchor: ed.vim.visual_anchor,
2916 cursor: ed.cursor(),
2917 block_vcol: 0,
2918 }),
2919 Mode::VisualLine => Some(LastVisual {
2920 mode: Mode::VisualLine,
2921 anchor: (ed.vim.visual_line_anchor, 0),
2922 cursor: ed.cursor(),
2923 block_vcol: 0,
2924 }),
2925 Mode::VisualBlock => Some(LastVisual {
2926 mode: Mode::VisualBlock,
2927 anchor: ed.vim.block_anchor,
2928 cursor: ed.cursor(),
2929 block_vcol: ed.vim.block_vcol,
2930 }),
2931 _ => None,
2932 };
2933 ed.vim.pending = Pending::None;
2935 ed.vim.count = 0;
2936 ed.vim.insert_session = None;
2937 set_vim_mode_bridge(ed, Mode::Normal);
2938 if let Some(snap) = snap {
2942 let (lo, hi) = match snap.mode {
2943 Mode::Visual => {
2944 if snap.anchor <= snap.cursor {
2945 (snap.anchor, snap.cursor)
2946 } else {
2947 (snap.cursor, snap.anchor)
2948 }
2949 }
2950 Mode::VisualLine => {
2951 let r_lo = snap.anchor.0.min(snap.cursor.0);
2952 let r_hi = snap.anchor.0.max(snap.cursor.0);
2953 let vl_rope = ed.buffer().rope();
2954 let r_hi_clamped = r_hi.min(vl_rope.len_lines().saturating_sub(1));
2955 let last_col = hjkl_buffer::rope_line_str(&vl_rope, r_hi_clamped)
2956 .chars()
2957 .count()
2958 .saturating_sub(1);
2959 ((r_lo, 0), (r_hi, last_col))
2960 }
2961 Mode::VisualBlock => {
2962 let (r1, c1) = snap.anchor;
2963 let (r2, c2) = snap.cursor;
2964 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2965 }
2966 _ => {
2967 if snap.anchor <= snap.cursor {
2968 (snap.anchor, snap.cursor)
2969 } else {
2970 (snap.cursor, snap.anchor)
2971 }
2972 }
2973 };
2974 ed.set_mark('<', lo);
2975 ed.set_mark('>', hi);
2976 ed.vim.last_visual = Some(snap);
2977 }
2978}
2979
2980pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2986 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2987) {
2988 match ed.vim.mode {
2989 Mode::Visual => {
2990 let cur = ed.cursor();
2991 let anchor = ed.vim.visual_anchor;
2992 ed.vim.visual_anchor = cur;
2993 ed.jump_cursor(anchor.0, anchor.1);
2994 }
2995 Mode::VisualLine => {
2996 let cur_row = ed.cursor().0;
2997 let anchor_row = ed.vim.visual_line_anchor;
2998 ed.vim.visual_line_anchor = cur_row;
2999 ed.jump_cursor(anchor_row, 0);
3000 }
3001 Mode::VisualBlock => {
3002 let cur = ed.cursor();
3003 let anchor = ed.vim.block_anchor;
3004 ed.vim.block_anchor = cur;
3005 ed.vim.block_vcol = anchor.1;
3006 ed.jump_cursor(anchor.0, anchor.1);
3007 }
3008 _ => {}
3009 }
3010}
3011
3012pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
3016 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3017) {
3018 if let Some(snap) = ed.vim.last_visual {
3019 match snap.mode {
3020 Mode::Visual => {
3021 ed.vim.visual_anchor = snap.anchor;
3022 set_vim_mode_bridge(ed, Mode::Visual);
3023 }
3024 Mode::VisualLine => {
3025 ed.vim.visual_line_anchor = snap.anchor.0;
3026 set_vim_mode_bridge(ed, Mode::VisualLine);
3027 }
3028 Mode::VisualBlock => {
3029 ed.vim.block_anchor = snap.anchor;
3030 ed.vim.block_vcol = snap.block_vcol;
3031 set_vim_mode_bridge(ed, Mode::VisualBlock);
3032 }
3033 _ => {}
3034 }
3035 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3036 }
3037}
3038
3039pub(crate) fn set_mode_bridge<H: crate::types::Host>(
3045 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3046 mode: crate::VimMode,
3047) {
3048 let internal = match mode {
3049 crate::VimMode::Normal => Mode::Normal,
3050 crate::VimMode::Insert => Mode::Insert,
3051 crate::VimMode::Visual => Mode::Visual,
3052 crate::VimMode::VisualLine => Mode::VisualLine,
3053 crate::VimMode::VisualBlock => Mode::VisualBlock,
3054 };
3055 ed.vim.mode = internal;
3056 ed.vim.current_mode = mode;
3057}
3058
3059pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
3076 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3077 ch: char,
3078) {
3079 if ch.is_ascii_lowercase() {
3080 let pos = ed.cursor();
3081 ed.set_mark(ch, pos);
3082 } else if ch.is_ascii_uppercase() {
3083 let pos = ed.cursor();
3084 let bid = ed.current_buffer_id();
3085 ed.set_global_mark(ch, bid, pos);
3086 tracing::debug!(
3087 mark = ch as u32,
3088 buffer_id = bid,
3089 row = pos.0,
3090 col = pos.1,
3091 "global mark set"
3092 );
3093 }
3094 }
3096
3097pub(crate) fn goto_mark<H: crate::types::Host>(
3106 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3107 ch: char,
3108 linewise: bool,
3109) {
3110 let target = match ch {
3111 'a'..='z' => ed.mark(ch),
3112 '\'' | '`' => ed.vim.jump_back.last().copied(),
3113 '.' => ed.vim.last_edit_pos,
3114 '[' | ']' | '<' | '>' => ed.mark(ch),
3115 _ => None,
3116 };
3117 let Some((row, col)) = target else {
3118 return;
3119 };
3120 let pre = ed.cursor();
3121 let (r, c_clamped) = clamp_pos(ed, (row, col));
3122 if linewise {
3123 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3124 ed.push_buffer_cursor_to_textarea();
3125 move_first_non_whitespace(ed);
3126 } else {
3127 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3128 ed.push_buffer_cursor_to_textarea();
3129 }
3130 if ed.cursor() != pre {
3131 ed.push_jump(pre);
3132 }
3133 ed.sticky_col = Some(ed.cursor().1);
3134}
3135
3136pub(crate) fn try_goto_mark<H: crate::types::Host>(
3145 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3146 ch: char,
3147 linewise: bool,
3148) -> crate::editor::MarkJump {
3149 use crate::editor::MarkJump;
3150 match ch {
3151 'A'..='Z' => {
3152 let Some((bid, row, col)) = ed.global_mark(ch) else {
3153 return MarkJump::Unset;
3154 };
3155 if bid != ed.current_buffer_id() {
3156 tracing::debug!(
3157 mark = ch as u32,
3158 buffer_id = bid,
3159 row,
3160 col,
3161 "global mark cross-buffer jump"
3162 );
3163 return MarkJump::CrossBuffer {
3164 buffer_id: bid,
3165 row,
3166 col,
3167 };
3168 }
3169 let pre = ed.cursor();
3171 let (r, c_clamped) = clamp_pos(ed, (row, col));
3172 if linewise {
3173 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3174 ed.push_buffer_cursor_to_textarea();
3175 move_first_non_whitespace(ed);
3176 } else {
3177 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3178 ed.push_buffer_cursor_to_textarea();
3179 }
3180 if ed.cursor() != pre {
3181 ed.push_jump(pre);
3182 }
3183 ed.sticky_col = Some(ed.cursor().1);
3184 MarkJump::SameBuffer
3185 }
3186 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
3187 goto_mark(ed, ch, linewise);
3188 MarkJump::SameBuffer
3189 }
3190 _ => MarkJump::Unset,
3191 }
3192}
3193
3194pub fn op_is_change(op: Operator) -> bool {
3198 matches!(op, Operator::Delete | Operator::Change)
3199}
3200
3201pub(crate) const JUMPLIST_MAX: usize = 100;
3205
3206fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3209 let Some(target) = ed.vim.jump_back.pop() else {
3210 return;
3211 };
3212 let cur = ed.cursor();
3213 ed.vim.jump_fwd.push(cur);
3214 let (r, c) = clamp_pos(ed, target);
3215 ed.jump_cursor(r, c);
3216 ed.sticky_col = Some(c);
3217}
3218
3219fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3222 let Some(target) = ed.vim.jump_fwd.pop() else {
3223 return;
3224 };
3225 let cur = ed.cursor();
3226 ed.vim.jump_back.push(cur);
3227 if ed.vim.jump_back.len() > JUMPLIST_MAX {
3228 ed.vim.jump_back.remove(0);
3229 }
3230 let (r, c) = clamp_pos(ed, target);
3231 ed.jump_cursor(r, c);
3232 ed.sticky_col = Some(c);
3233}
3234
3235fn clamp_pos<H: crate::types::Host>(
3238 ed: &Editor<hjkl_buffer::Buffer, H>,
3239 pos: (usize, usize),
3240) -> (usize, usize) {
3241 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3242 let r = pos.0.min(last_row);
3243 let line_len = buf_line_chars(&ed.buffer, r);
3244 let c = pos.1.min(line_len.saturating_sub(1));
3245 (r, c)
3246}
3247
3248fn is_big_jump(motion: &Motion) -> bool {
3250 matches!(
3251 motion,
3252 Motion::FileTop
3253 | Motion::FileBottom
3254 | Motion::MatchBracket
3255 | Motion::WordAtCursor { .. }
3256 | Motion::SearchNext { .. }
3257 | Motion::ViewportTop
3258 | Motion::ViewportMiddle
3259 | Motion::ViewportBottom
3260 )
3261}
3262
3263fn viewport_half_rows<H: crate::types::Host>(
3268 ed: &Editor<hjkl_buffer::Buffer, H>,
3269 count: usize,
3270) -> usize {
3271 let h = ed.viewport_height_value() as usize;
3272 (h / 2).max(1).saturating_mul(count.max(1))
3273}
3274
3275fn viewport_full_rows<H: crate::types::Host>(
3278 ed: &Editor<hjkl_buffer::Buffer, H>,
3279 count: usize,
3280) -> usize {
3281 let h = ed.viewport_height_value() as usize;
3282 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
3283}
3284
3285fn scroll_cursor_rows<H: crate::types::Host>(
3290 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3291 delta: isize,
3292) {
3293 if delta == 0 {
3294 return;
3295 }
3296 ed.sync_buffer_content_from_textarea();
3297 let (row, _) = ed.cursor();
3298 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3299 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
3300 buf_set_cursor_rc(&mut ed.buffer, target, 0);
3301 crate::motions::move_first_non_blank(&mut ed.buffer);
3302 ed.push_buffer_cursor_to_textarea();
3303 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3304}
3305
3306pub fn parse_motion(input: &Input) -> Option<Motion> {
3312 if input.ctrl {
3313 return None;
3314 }
3315 match input.key {
3316 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
3317 Key::Char('l') | Key::Right => Some(Motion::Right),
3318 Key::Char('j') | Key::Down => Some(Motion::Down),
3319 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
3321 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
3323 Key::Char('_') => Some(Motion::FirstNonBlankLine),
3325 Key::Char('k') | Key::Up => Some(Motion::Up),
3326 Key::Char('w') => Some(Motion::WordFwd),
3327 Key::Char('W') => Some(Motion::BigWordFwd),
3328 Key::Char('b') => Some(Motion::WordBack),
3329 Key::Char('B') => Some(Motion::BigWordBack),
3330 Key::Char('e') => Some(Motion::WordEnd),
3331 Key::Char('E') => Some(Motion::BigWordEnd),
3332 Key::Char('0') | Key::Home => Some(Motion::LineStart),
3333 Key::Char('^') => Some(Motion::FirstNonBlank),
3334 Key::Char('$') | Key::End => Some(Motion::LineEnd),
3335 Key::Char('G') => Some(Motion::FileBottom),
3336 Key::Char('%') => Some(Motion::MatchBracket),
3337 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
3338 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
3339 Key::Char('*') => Some(Motion::WordAtCursor {
3340 forward: true,
3341 whole_word: true,
3342 }),
3343 Key::Char('#') => Some(Motion::WordAtCursor {
3344 forward: false,
3345 whole_word: true,
3346 }),
3347 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
3348 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
3349 Key::Char('H') => Some(Motion::ViewportTop),
3350 Key::Char('M') => Some(Motion::ViewportMiddle),
3351 Key::Char('L') => Some(Motion::ViewportBottom),
3352 Key::Char('{') => Some(Motion::ParagraphPrev),
3353 Key::Char('}') => Some(Motion::ParagraphNext),
3354 Key::Char('(') => Some(Motion::SentencePrev),
3355 Key::Char(')') => Some(Motion::SentenceNext),
3356 Key::Char('|') => Some(Motion::GotoColumn),
3357 _ => None,
3358 }
3359}
3360
3361pub(crate) fn execute_motion<H: crate::types::Host>(
3364 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3365 motion: Motion,
3366 count: usize,
3367) {
3368 let count = count.max(1);
3369 if let Motion::FindRepeat { reverse } = motion
3372 && ed.vim.last_horizontal_motion == LastHorizontalMotion::Sneak
3373 {
3374 if let Some(((c1, c2), fwd)) = ed.vim.last_sneak {
3375 let effective_fwd = if reverse { !fwd } else { fwd };
3376 apply_sneak(ed, c1, c2, effective_fwd, count);
3377 }
3378 return;
3379 }
3380 let motion = match motion {
3382 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3383 Some((ch, forward, till)) => Motion::Find {
3384 ch,
3385 forward: if reverse { !forward } else { forward },
3386 till,
3387 },
3388 None => return,
3389 },
3390 other => other,
3391 };
3392 let pre_pos = ed.cursor();
3393 let pre_col = pre_pos.1;
3394 apply_motion_cursor(ed, &motion, count);
3395 let post_pos = ed.cursor();
3396 if is_big_jump(&motion) && pre_pos != post_pos {
3397 ed.push_jump(pre_pos);
3398 }
3399 apply_sticky_col(ed, &motion, pre_col);
3400 ed.sync_buffer_from_textarea();
3405}
3406
3407fn execute_motion_with_block_vcol<H: crate::types::Host>(
3418 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3419 motion: Motion,
3420 count: usize,
3421) {
3422 let motion_copy = motion.clone();
3423 execute_motion(ed, motion, count);
3424 if ed.vim.mode == Mode::VisualBlock {
3425 update_block_vcol(ed, &motion_copy);
3426 }
3427}
3428
3429pub(crate) fn apply_motion_kind<H: crate::types::Host>(
3457 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3458 kind: crate::MotionKind,
3459 count: usize,
3460) {
3461 let count = count.max(1);
3462 match kind {
3463 crate::MotionKind::CharLeft => {
3464 execute_motion_with_block_vcol(ed, Motion::Left, count);
3465 }
3466 crate::MotionKind::CharRight => {
3467 execute_motion_with_block_vcol(ed, Motion::Right, count);
3468 }
3469 crate::MotionKind::LineDown => {
3470 execute_motion_with_block_vcol(ed, Motion::Down, count);
3471 }
3472 crate::MotionKind::LineUp => {
3473 execute_motion_with_block_vcol(ed, Motion::Up, count);
3474 }
3475 crate::MotionKind::FirstNonBlankDown => {
3476 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3481 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3482 crate::motions::move_first_non_blank(&mut ed.buffer);
3483 ed.push_buffer_cursor_to_textarea();
3484 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3485 ed.sync_buffer_from_textarea();
3486 }
3487 crate::MotionKind::FirstNonBlankUp => {
3488 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3491 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3492 crate::motions::move_first_non_blank(&mut ed.buffer);
3493 ed.push_buffer_cursor_to_textarea();
3494 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3495 ed.sync_buffer_from_textarea();
3496 }
3497 crate::MotionKind::WordForward => {
3498 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
3499 }
3500 crate::MotionKind::BigWordForward => {
3501 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
3502 }
3503 crate::MotionKind::WordBackward => {
3504 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
3505 }
3506 crate::MotionKind::BigWordBackward => {
3507 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
3508 }
3509 crate::MotionKind::WordEnd => {
3510 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
3511 }
3512 crate::MotionKind::BigWordEnd => {
3513 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
3514 }
3515 crate::MotionKind::LineStart => {
3516 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
3519 }
3520 crate::MotionKind::FirstNonBlank => {
3521 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
3524 }
3525 crate::MotionKind::GotoLine => {
3526 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
3535 }
3536 crate::MotionKind::LineEnd => {
3537 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
3541 }
3542 crate::MotionKind::FindRepeat => {
3543 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
3547 }
3548 crate::MotionKind::FindRepeatReverse => {
3549 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
3553 }
3554 crate::MotionKind::BracketMatch => {
3555 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
3560 }
3561 crate::MotionKind::ViewportTop => {
3562 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
3565 }
3566 crate::MotionKind::ViewportMiddle => {
3567 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
3570 }
3571 crate::MotionKind::ViewportBottom => {
3572 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
3575 }
3576 crate::MotionKind::HalfPageDown => {
3577 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
3581 }
3582 crate::MotionKind::HalfPageUp => {
3583 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
3586 }
3587 crate::MotionKind::FullPageDown => {
3588 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
3591 }
3592 crate::MotionKind::FullPageUp => {
3593 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
3596 }
3597 crate::MotionKind::FirstNonBlankLine => {
3598 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
3599 }
3600 crate::MotionKind::SectionBackward => {
3601 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
3602 }
3603 crate::MotionKind::SectionForward => {
3604 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
3605 }
3606 crate::MotionKind::SectionEndBackward => {
3607 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
3608 }
3609 crate::MotionKind::SectionEndForward => {
3610 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
3611 }
3612 }
3613}
3614
3615fn apply_sticky_col<H: crate::types::Host>(
3620 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3621 motion: &Motion,
3622 pre_col: usize,
3623) {
3624 if is_vertical_motion(motion) {
3625 let want = ed.sticky_col.unwrap_or(pre_col);
3626 ed.sticky_col = Some(want);
3629 let (row, _) = ed.cursor();
3630 let line_len = buf_line_chars(&ed.buffer, row);
3631 let max_col = line_len.saturating_sub(1);
3635 let target = want.min(max_col);
3636 buf_set_cursor_rc(&mut ed.buffer, row, target);
3640 } else {
3641 ed.sticky_col = Some(ed.cursor().1);
3644 }
3645}
3646
3647fn is_vertical_motion(motion: &Motion) -> bool {
3648 matches!(
3652 motion,
3653 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
3654 )
3655}
3656
3657fn apply_motion_cursor<H: crate::types::Host>(
3658 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3659 motion: &Motion,
3660 count: usize,
3661) {
3662 apply_motion_cursor_ctx(ed, motion, count, false)
3663}
3664
3665pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
3666 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3667 motion: &Motion,
3668 count: usize,
3669 as_operator: bool,
3670) {
3671 match motion {
3672 Motion::Left => {
3673 crate::motions::move_left(&mut ed.buffer, count);
3675 ed.push_buffer_cursor_to_textarea();
3676 }
3677 Motion::Right => {
3678 if as_operator {
3682 crate::motions::move_right_to_end(&mut ed.buffer, count);
3683 } else {
3684 crate::motions::move_right_in_line(&mut ed.buffer, count);
3685 }
3686 ed.push_buffer_cursor_to_textarea();
3687 }
3688 Motion::Up => {
3689 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3693 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3694 ed.push_buffer_cursor_to_textarea();
3695 }
3696 Motion::Down => {
3697 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3698 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3699 ed.push_buffer_cursor_to_textarea();
3700 }
3701 Motion::ScreenUp => {
3702 let v = *ed.host.viewport();
3703 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3704 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3705 ed.push_buffer_cursor_to_textarea();
3706 }
3707 Motion::ScreenDown => {
3708 let v = *ed.host.viewport();
3709 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3710 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3711 ed.push_buffer_cursor_to_textarea();
3712 }
3713 Motion::WordFwd => {
3714 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3715 ed.push_buffer_cursor_to_textarea();
3716 }
3717 Motion::WordBack => {
3718 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3719 ed.push_buffer_cursor_to_textarea();
3720 }
3721 Motion::WordEnd => {
3722 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3723 ed.push_buffer_cursor_to_textarea();
3724 }
3725 Motion::BigWordFwd => {
3726 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3727 ed.push_buffer_cursor_to_textarea();
3728 }
3729 Motion::BigWordBack => {
3730 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3731 ed.push_buffer_cursor_to_textarea();
3732 }
3733 Motion::BigWordEnd => {
3734 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3735 ed.push_buffer_cursor_to_textarea();
3736 }
3737 Motion::WordEndBack => {
3738 crate::motions::move_word_end_back(
3739 &mut ed.buffer,
3740 false,
3741 count,
3742 &ed.settings.iskeyword,
3743 );
3744 ed.push_buffer_cursor_to_textarea();
3745 }
3746 Motion::BigWordEndBack => {
3747 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3748 ed.push_buffer_cursor_to_textarea();
3749 }
3750 Motion::LineStart => {
3751 crate::motions::move_line_start(&mut ed.buffer);
3752 ed.push_buffer_cursor_to_textarea();
3753 }
3754 Motion::FirstNonBlank => {
3755 crate::motions::move_first_non_blank(&mut ed.buffer);
3756 ed.push_buffer_cursor_to_textarea();
3757 }
3758 Motion::LineEnd => {
3759 crate::motions::move_line_end(&mut ed.buffer);
3761 ed.push_buffer_cursor_to_textarea();
3762 }
3763 Motion::FileTop => {
3764 if count > 1 {
3767 crate::motions::move_bottom(&mut ed.buffer, count);
3768 } else {
3769 crate::motions::move_top(&mut ed.buffer);
3770 }
3771 ed.push_buffer_cursor_to_textarea();
3772 }
3773 Motion::FileBottom => {
3774 if count > 1 {
3777 crate::motions::move_bottom(&mut ed.buffer, count);
3778 } else {
3779 crate::motions::move_bottom(&mut ed.buffer, 0);
3780 }
3781 ed.push_buffer_cursor_to_textarea();
3782 }
3783 Motion::Find { ch, forward, till } => {
3784 for _ in 0..count {
3785 if !find_char_on_line(ed, *ch, *forward, *till) {
3786 break;
3787 }
3788 }
3789 }
3790 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
3792 let _ = matching_bracket(ed);
3793 }
3794 Motion::WordAtCursor {
3795 forward,
3796 whole_word,
3797 } => {
3798 word_at_cursor_search(ed, *forward, *whole_word, count);
3799 }
3800 Motion::SearchNext { reverse } => {
3801 if let Some(pattern) = ed.vim.last_search.clone() {
3805 ed.push_search_pattern(&pattern);
3806 }
3807 if ed.search_state().pattern.is_none() {
3808 return;
3809 }
3810 let forward = ed.vim.last_search_forward != *reverse;
3814 for _ in 0..count.max(1) {
3815 if forward {
3816 ed.search_advance_forward(true);
3817 } else {
3818 ed.search_advance_backward(true);
3819 }
3820 }
3821 ed.push_buffer_cursor_to_textarea();
3822 }
3823 Motion::ViewportTop => {
3824 let v = *ed.host().viewport();
3825 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
3826 ed.push_buffer_cursor_to_textarea();
3827 }
3828 Motion::ViewportMiddle => {
3829 let v = *ed.host().viewport();
3830 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
3831 ed.push_buffer_cursor_to_textarea();
3832 }
3833 Motion::ViewportBottom => {
3834 let v = *ed.host().viewport();
3835 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
3836 ed.push_buffer_cursor_to_textarea();
3837 }
3838 Motion::LastNonBlank => {
3839 crate::motions::move_last_non_blank(&mut ed.buffer);
3840 ed.push_buffer_cursor_to_textarea();
3841 }
3842 Motion::LineMiddle => {
3843 let row = ed.cursor().0;
3844 let line_chars = buf_line_chars(&ed.buffer, row);
3845 let target = line_chars / 2;
3848 ed.jump_cursor(row, target);
3849 }
3850 Motion::ParagraphPrev => {
3851 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
3852 ed.push_buffer_cursor_to_textarea();
3853 }
3854 Motion::ParagraphNext => {
3855 crate::motions::move_paragraph_next(&mut ed.buffer, count);
3856 ed.push_buffer_cursor_to_textarea();
3857 }
3858 Motion::SentencePrev => {
3859 for _ in 0..count.max(1) {
3860 if let Some((row, col)) = sentence_boundary(ed, false) {
3861 ed.jump_cursor(row, col);
3862 }
3863 }
3864 }
3865 Motion::SentenceNext => {
3866 for _ in 0..count.max(1) {
3867 if let Some((row, col)) = sentence_boundary(ed, true) {
3868 ed.jump_cursor(row, col);
3869 }
3870 }
3871 }
3872 Motion::SectionBackward => {
3873 crate::motions::move_section_backward(&mut ed.buffer, count);
3874 ed.push_buffer_cursor_to_textarea();
3875 }
3876 Motion::SectionForward => {
3877 crate::motions::move_section_forward(&mut ed.buffer, count);
3878 ed.push_buffer_cursor_to_textarea();
3879 }
3880 Motion::SectionEndBackward => {
3881 crate::motions::move_section_end_backward(&mut ed.buffer, count);
3882 ed.push_buffer_cursor_to_textarea();
3883 }
3884 Motion::SectionEndForward => {
3885 crate::motions::move_section_end_forward(&mut ed.buffer, count);
3886 ed.push_buffer_cursor_to_textarea();
3887 }
3888 Motion::FirstNonBlankNextLine => {
3889 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
3890 ed.push_buffer_cursor_to_textarea();
3891 }
3892 Motion::FirstNonBlankPrevLine => {
3893 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
3894 ed.push_buffer_cursor_to_textarea();
3895 }
3896 Motion::FirstNonBlankLine => {
3897 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
3898 ed.push_buffer_cursor_to_textarea();
3899 }
3900 Motion::GotoColumn => {
3901 crate::motions::move_goto_column(&mut ed.buffer, count);
3902 ed.push_buffer_cursor_to_textarea();
3903 }
3904 }
3905}
3906
3907fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3908 ed.sync_buffer_content_from_textarea();
3914 crate::motions::move_first_non_blank(&mut ed.buffer);
3915 ed.push_buffer_cursor_to_textarea();
3916}
3917
3918fn find_char_on_line<H: crate::types::Host>(
3919 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3920 ch: char,
3921 forward: bool,
3922 till: bool,
3923) -> bool {
3924 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
3925 if moved {
3926 ed.push_buffer_cursor_to_textarea();
3927 }
3928 moved
3929}
3930
3931fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3932 let moved = crate::motions::match_bracket(&mut ed.buffer);
3933 if moved {
3934 ed.push_buffer_cursor_to_textarea();
3935 }
3936 moved
3937}
3938
3939fn word_at_cursor_search<H: crate::types::Host>(
3940 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3941 forward: bool,
3942 whole_word: bool,
3943 count: usize,
3944) {
3945 let (row, col) = ed.cursor();
3946 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
3947 let chars: Vec<char> = line.chars().collect();
3948 if chars.is_empty() {
3949 return;
3950 }
3951 let spec = ed.settings().iskeyword.clone();
3953 let is_word = |c: char| is_keyword_char(c, &spec);
3954 let mut start = col.min(chars.len().saturating_sub(1));
3955 while start > 0 && is_word(chars[start - 1]) {
3956 start -= 1;
3957 }
3958 let mut end = start;
3959 while end < chars.len() && is_word(chars[end]) {
3960 end += 1;
3961 }
3962 if end <= start {
3963 return;
3964 }
3965 let word: String = chars[start..end].iter().collect();
3966 let escaped = regex_escape(&word);
3967 let pattern = if whole_word {
3968 format!(r"\b{escaped}\b")
3969 } else {
3970 escaped
3971 };
3972 ed.push_search_pattern(&pattern);
3973 if ed.search_state().pattern.is_none() {
3974 return;
3975 }
3976 ed.vim.last_search = Some(pattern);
3978 ed.vim.last_search_forward = forward;
3979 for _ in 0..count.max(1) {
3980 if forward {
3981 ed.search_advance_forward(true);
3982 } else {
3983 ed.search_advance_backward(true);
3984 }
3985 }
3986 ed.push_buffer_cursor_to_textarea();
3987}
3988
3989fn regex_escape(s: &str) -> String {
3990 let mut out = String::with_capacity(s.len());
3991 for c in s.chars() {
3992 if matches!(
3993 c,
3994 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
3995 ) {
3996 out.push('\\');
3997 }
3998 out.push(c);
3999 }
4000 out
4001}
4002
4003pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
4017 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4018 op: Operator,
4019 motion_key: char,
4020 total_count: usize,
4021) {
4022 let input = Input {
4023 key: Key::Char(motion_key),
4024 ctrl: false,
4025 alt: false,
4026 shift: false,
4027 };
4028 let Some(motion) = parse_motion(&input) else {
4029 return;
4030 };
4031 let motion = match motion {
4032 Motion::FindRepeat { reverse } => match ed.vim.last_find {
4033 Some((ch, forward, till)) => Motion::Find {
4034 ch,
4035 forward: if reverse { !forward } else { forward },
4036 till,
4037 },
4038 None => return,
4039 },
4040 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
4042 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
4043 m => m,
4044 };
4045 apply_op_with_motion(ed, op, &motion, total_count);
4046 if let Motion::Find { ch, forward, till } = &motion {
4047 ed.vim.last_find = Some((*ch, *forward, *till));
4048 }
4049 if !ed.vim.replaying && op_is_change(op) {
4050 ed.vim.last_change = Some(LastChange::OpMotion {
4051 op,
4052 motion,
4053 count: total_count,
4054 inserted: None,
4055 });
4056 }
4057}
4058
4059pub(crate) fn apply_op_double<H: crate::types::Host>(
4062 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4063 op: Operator,
4064 total_count: usize,
4065) {
4066 if op == Operator::Comment {
4067 let row = buf_cursor_pos(&ed.buffer).row;
4069 let end_row = (row + total_count.max(1) - 1).min(ed.buffer.row_count().saturating_sub(1));
4070 ed.toggle_comment_range(row, end_row);
4071 ed.vim.mode = Mode::Normal;
4072 if !ed.vim.replaying {
4073 ed.vim.last_change = Some(LastChange::LineOp {
4074 op,
4075 count: total_count,
4076 inserted: None,
4077 });
4078 }
4079 return;
4080 }
4081 execute_line_op(ed, op, total_count);
4082 if !ed.vim.replaying {
4083 ed.vim.last_change = Some(LastChange::LineOp {
4084 op,
4085 count: total_count,
4086 inserted: None,
4087 });
4088 }
4089}
4090
4091pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
4101 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4102 op: Operator,
4103 ch: char,
4104 total_count: usize,
4105) {
4106 if matches!(
4109 op,
4110 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
4111 ) {
4112 let op_char = match op {
4113 Operator::Uppercase => 'U',
4114 Operator::Lowercase => 'u',
4115 Operator::ToggleCase => '~',
4116 _ => unreachable!(),
4117 };
4118 if ch == op_char {
4119 execute_line_op(ed, op, total_count);
4120 if !ed.vim.replaying {
4121 ed.vim.last_change = Some(LastChange::LineOp {
4122 op,
4123 count: total_count,
4124 inserted: None,
4125 });
4126 }
4127 return;
4128 }
4129 }
4130 let motion = match ch {
4131 'g' => Motion::FileTop,
4132 'e' => Motion::WordEndBack,
4133 'E' => Motion::BigWordEndBack,
4134 'j' => Motion::ScreenDown,
4135 'k' => Motion::ScreenUp,
4136 _ => return, };
4138 apply_op_with_motion(ed, op, &motion, total_count);
4139 if !ed.vim.replaying && op_is_change(op) {
4140 ed.vim.last_change = Some(LastChange::OpMotion {
4141 op,
4142 motion,
4143 count: total_count,
4144 inserted: None,
4145 });
4146 }
4147}
4148
4149pub(crate) fn apply_after_g<H: crate::types::Host>(
4154 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4155 ch: char,
4156 count: usize,
4157) {
4158 match ch {
4159 'g' => {
4160 let pre = ed.cursor();
4162 if count > 1 {
4163 ed.jump_cursor(count - 1, 0);
4164 } else {
4165 ed.jump_cursor(0, 0);
4166 }
4167 move_first_non_whitespace(ed);
4168 ed.sticky_col = Some(ed.cursor().1);
4171 if ed.cursor() != pre {
4172 ed.push_jump(pre);
4173 }
4174 }
4175 'e' => execute_motion(ed, Motion::WordEndBack, count),
4176 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
4177 '_' => execute_motion(ed, Motion::LastNonBlank, count),
4179 'M' => execute_motion(ed, Motion::LineMiddle, count),
4181 'v' => ed.reenter_last_visual(),
4184 'j' => execute_motion(ed, Motion::ScreenDown, count),
4188 'k' => execute_motion(ed, Motion::ScreenUp, count),
4189 'U' => {
4193 ed.vim.pending = Pending::Op {
4194 op: Operator::Uppercase,
4195 count1: count,
4196 };
4197 }
4198 'u' => {
4199 ed.vim.pending = Pending::Op {
4200 op: Operator::Lowercase,
4201 count1: count,
4202 };
4203 }
4204 '~' => {
4205 ed.vim.pending = Pending::Op {
4206 op: Operator::ToggleCase,
4207 count1: count,
4208 };
4209 }
4210 'q' => {
4211 ed.vim.pending = Pending::Op {
4214 op: Operator::Reflow,
4215 count1: count,
4216 };
4217 }
4218 'w' => {
4219 ed.vim.pending = Pending::Op {
4222 op: Operator::ReflowKeepCursor,
4223 count1: count,
4224 };
4225 }
4226 'J' => {
4227 for _ in 0..count.max(1) {
4229 ed.push_undo();
4230 join_line_raw(ed);
4231 }
4232 if !ed.vim.replaying {
4233 ed.vim.last_change = Some(LastChange::JoinLine {
4234 count: count.max(1),
4235 });
4236 }
4237 }
4238 'd' => {
4239 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
4244 }
4245 'i' => {
4250 if let Some((row, col)) = ed.vim.last_insert_pos {
4251 ed.jump_cursor(row, col);
4252 }
4253 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4254 }
4255 'c' => {
4260 ed.vim.pending = Pending::Op {
4261 op: Operator::Comment,
4262 count1: count,
4263 };
4264 }
4265 ';' => walk_change_list(ed, -1, count.max(1)),
4268 ',' => walk_change_list(ed, 1, count.max(1)),
4269 '*' => execute_motion(
4273 ed,
4274 Motion::WordAtCursor {
4275 forward: true,
4276 whole_word: false,
4277 },
4278 count,
4279 ),
4280 '#' => execute_motion(
4281 ed,
4282 Motion::WordAtCursor {
4283 forward: false,
4284 whole_word: false,
4285 },
4286 count,
4287 ),
4288 '&' => {
4291 let cmd = match ed.vim.last_substitute.clone() {
4292 Some(c) => c,
4293 None => {
4294 return;
4299 }
4300 };
4301 let last_row = buf_row_count(&ed.buffer).saturating_sub(1) as u32;
4302 let r = 0u32..=last_row;
4303 let _ = crate::substitute::apply_substitute(ed, &cmd, r);
4306 ed.vim.last_substitute = Some(cmd);
4309 }
4310 _ => {}
4311 }
4312}
4313
4314pub(crate) fn apply_after_z<H: crate::types::Host>(
4319 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4320 ch: char,
4321 count: usize,
4322) {
4323 use crate::editor::CursorScrollTarget;
4324 let row = ed.cursor().0;
4325 match ch {
4326 'z' => {
4327 ed.scroll_cursor_to(CursorScrollTarget::Center);
4328 ed.vim.viewport_pinned = true;
4329 }
4330 't' => {
4331 ed.scroll_cursor_to(CursorScrollTarget::Top);
4332 ed.vim.viewport_pinned = true;
4333 }
4334 'b' => {
4335 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
4336 ed.vim.viewport_pinned = true;
4337 }
4338 'o' => {
4343 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
4344 }
4345 'c' => {
4346 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
4347 }
4348 'a' => {
4349 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
4350 }
4351 'R' => {
4352 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
4353 }
4354 'M' => {
4355 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
4356 }
4357 'E' => {
4358 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
4359 }
4360 'd' => {
4361 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
4362 }
4363 'f' => {
4364 if matches!(
4365 ed.vim.mode,
4366 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
4367 ) {
4368 let anchor_row = match ed.vim.mode {
4371 Mode::VisualLine => ed.vim.visual_line_anchor,
4372 Mode::VisualBlock => ed.vim.block_anchor.0,
4373 _ => ed.vim.visual_anchor.0,
4374 };
4375 let cur = ed.cursor().0;
4376 let top = anchor_row.min(cur);
4377 let bot = anchor_row.max(cur);
4378 ed.apply_fold_op(crate::types::FoldOp::Add {
4379 start_row: top,
4380 end_row: bot,
4381 closed: true,
4382 });
4383 ed.vim.mode = Mode::Normal;
4384 } else {
4385 ed.vim.pending = Pending::Op {
4390 op: Operator::Fold,
4391 count1: count,
4392 };
4393 }
4394 }
4395 _ => {}
4396 }
4397}
4398
4399pub(crate) fn apply_find_char<H: crate::types::Host>(
4405 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4406 ch: char,
4407 forward: bool,
4408 till: bool,
4409 count: usize,
4410) {
4411 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
4412 ed.vim.last_find = Some((ch, forward, till));
4413 ed.vim.last_horizontal_motion = LastHorizontalMotion::FindChar;
4414}
4415
4416pub(crate) fn apply_sneak<H: crate::types::Host>(
4428 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4429 c1: char,
4430 c2: char,
4431 forward: bool,
4432 count: usize,
4433) {
4434 let count = count.max(1);
4435 let (start_row, start_col) = ed.cursor();
4436 let row_count = buf_row_count(&ed.buffer);
4437
4438 let result = if forward {
4439 sneak_scan_forward(ed, start_row, start_col, c1, c2, count)
4440 } else {
4441 sneak_scan_backward(ed, start_row, start_col, c1, c2, count)
4442 };
4443
4444 if let Some((row, col)) = result {
4445 buf_set_cursor_rc(&mut ed.buffer, row, col);
4446 ed.push_buffer_cursor_to_textarea();
4447 let _ = row_count; }
4449
4450 ed.vim.last_sneak = Some(((c1, c2), forward));
4451 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4452}
4453
4454fn sneak_scan_forward<H: crate::types::Host>(
4457 ed: &Editor<hjkl_buffer::Buffer, H>,
4458 start_row: usize,
4459 start_col: usize,
4460 c1: char,
4461 c2: char,
4462 count: usize,
4463) -> Option<(usize, usize)> {
4464 let row_count = buf_row_count(&ed.buffer);
4465 let mut hits = 0usize;
4466 for row in start_row..row_count {
4467 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4468 let chars: Vec<char> = line.chars().collect();
4469 let col_start = if row == start_row { start_col + 1 } else { 0 };
4471 if col_start + 1 > chars.len() {
4472 continue;
4473 }
4474 for col in col_start..chars.len().saturating_sub(1) {
4475 if chars[col] == c1 && chars[col + 1] == c2 {
4476 hits += 1;
4477 if hits == count {
4478 return Some((row, col));
4479 }
4480 }
4481 }
4482 }
4483 None
4484}
4485
4486fn sneak_scan_backward<H: crate::types::Host>(
4489 ed: &Editor<hjkl_buffer::Buffer, H>,
4490 start_row: usize,
4491 start_col: usize,
4492 c1: char,
4493 c2: char,
4494 count: usize,
4495) -> Option<(usize, usize)> {
4496 let row_count = buf_row_count(&ed.buffer);
4497 let mut hits = 0usize;
4498 let rows_to_scan = (0..row_count).rev().skip(row_count - start_row - 1);
4500 for row in rows_to_scan {
4501 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4502 let chars: Vec<char> = line.chars().collect();
4503 let col_end = if row == start_row {
4505 start_col.saturating_sub(1)
4506 } else if chars.is_empty() {
4507 continue;
4508 } else {
4509 chars.len().saturating_sub(1)
4510 };
4511 if col_end == 0 {
4512 continue;
4513 }
4514 for col in (0..col_end).rev() {
4516 if col + 1 < chars.len() && chars[col] == c1 && chars[col + 1] == c2 {
4517 hits += 1;
4518 if hits == count {
4519 return Some((row, col));
4520 }
4521 }
4522 }
4523 }
4524 None
4525}
4526
4527pub(crate) fn apply_op_sneak<H: crate::types::Host>(
4534 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4535 op: Operator,
4536 c1: char,
4537 c2: char,
4538 forward: bool,
4539 total_count: usize,
4540) {
4541 let start = ed.cursor();
4542 let result = if forward {
4543 sneak_scan_forward(ed, start.0, start.1, c1, c2, total_count)
4544 } else {
4545 sneak_scan_backward(ed, start.0, start.1, c1, c2, total_count)
4546 };
4547 let Some(end) = result else {
4548 return;
4549 };
4550 ed.jump_cursor(end.0, end.1);
4553 let end_cur = ed.cursor();
4554 ed.jump_cursor(start.0, start.1);
4555 run_operator_over_range(ed, op, start, end_cur, RangeKind::Exclusive);
4556 ed.vim.last_sneak = Some(((c1, c2), forward));
4557 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4558 if !ed.vim.replaying && op_is_change(op) {
4559 }
4563}
4564
4565pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
4571 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4572 op: Operator,
4573 ch: char,
4574 forward: bool,
4575 till: bool,
4576 total_count: usize,
4577) {
4578 let motion = Motion::Find { ch, forward, till };
4579 apply_op_with_motion(ed, op, &motion, total_count);
4580 ed.vim.last_find = Some((ch, forward, till));
4581 if !ed.vim.replaying && op_is_change(op) {
4582 ed.vim.last_change = Some(LastChange::OpMotion {
4583 op,
4584 motion,
4585 count: total_count,
4586 inserted: None,
4587 });
4588 }
4589}
4590
4591pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
4600 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4601 op: Operator,
4602 ch: char,
4603 inner: bool,
4604 total_count: usize,
4605) -> bool {
4606 let obj = match ch {
4609 'w' => TextObject::Word { big: false },
4610 'W' => TextObject::Word { big: true },
4611 '"' | '\'' | '`' => TextObject::Quote(ch),
4612 '(' | ')' | 'b' => TextObject::Bracket('('),
4613 '[' | ']' => TextObject::Bracket('['),
4614 '{' | '}' | 'B' => TextObject::Bracket('{'),
4615 '<' | '>' => TextObject::Bracket('<'),
4616 'p' => TextObject::Paragraph,
4617 't' => TextObject::XmlTag,
4618 's' => TextObject::Sentence,
4619 _ => return false,
4620 };
4621 apply_op_with_text_object(ed, op, obj, inner, total_count.max(1));
4622 if !ed.vim.replaying && op_is_change(op) {
4623 ed.vim.last_change = Some(LastChange::OpTextObj {
4624 op,
4625 obj,
4626 inner,
4627 inserted: None,
4628 });
4629 }
4630 true
4631}
4632
4633pub(crate) fn retreat_one<H: crate::types::Host>(
4635 ed: &Editor<hjkl_buffer::Buffer, H>,
4636 pos: (usize, usize),
4637) -> (usize, usize) {
4638 let (r, c) = pos;
4639 if c > 0 {
4640 (r, c - 1)
4641 } else if r > 0 {
4642 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
4643 (r - 1, prev_len)
4644 } else {
4645 (0, 0)
4646 }
4647}
4648
4649fn begin_insert_noundo<H: crate::types::Host>(
4651 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4652 count: usize,
4653 reason: InsertReason,
4654) {
4655 let reason = if ed.vim.replaying {
4656 InsertReason::ReplayOnly
4657 } else {
4658 reason
4659 };
4660 let (row, _) = ed.cursor();
4661 ed.vim.insert_session = Some(InsertSession {
4662 count,
4663 row_min: row,
4664 row_max: row,
4665 before_rope: crate::types::Query::rope(&ed.buffer),
4666 reason,
4667 });
4668 ed.vim.mode = Mode::Insert;
4669 ed.vim.current_mode = crate::VimMode::Insert;
4671}
4672
4673pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
4676 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4677 op: Operator,
4678 motion: &Motion,
4679 count: usize,
4680) {
4681 let start = ed.cursor();
4682 apply_motion_cursor_ctx(ed, motion, count, true);
4687 let end = ed.cursor();
4688 let kind = motion_kind(motion);
4689 ed.jump_cursor(start.0, start.1);
4691
4692 if op == Operator::Comment {
4694 let top = start.0.min(end.0);
4695 let bot = start.0.max(end.0);
4696 ed.toggle_comment_range(top, bot);
4697 ed.vim.mode = Mode::Normal;
4698 return;
4699 }
4700
4701 run_operator_over_range(ed, op, start, end, kind);
4702}
4703
4704fn apply_op_with_text_object<H: crate::types::Host>(
4705 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4706 op: Operator,
4707 obj: TextObject,
4708 inner: bool,
4709 count: usize,
4710) {
4711 let Some((start, end, kind)) = text_object_range(ed, obj, inner, count) else {
4712 return;
4713 };
4714 ed.jump_cursor(start.0, start.1);
4715 run_operator_over_range(ed, op, start, end, kind);
4716}
4717
4718fn motion_kind(motion: &Motion) -> RangeKind {
4719 match motion {
4720 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
4721 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
4722 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
4723 RangeKind::Linewise
4724 }
4725 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
4726 RangeKind::Inclusive
4727 }
4728 Motion::Find { .. } => RangeKind::Inclusive,
4729 Motion::MatchBracket => RangeKind::Inclusive,
4730 Motion::LineEnd => RangeKind::Inclusive,
4732 Motion::FirstNonBlankNextLine
4734 | Motion::FirstNonBlankPrevLine
4735 | Motion::FirstNonBlankLine => RangeKind::Linewise,
4736 Motion::SectionBackward
4738 | Motion::SectionForward
4739 | Motion::SectionEndBackward
4740 | Motion::SectionEndForward => RangeKind::Exclusive,
4741 _ => RangeKind::Exclusive,
4742 }
4743}
4744
4745fn change_linewise_rows<H: crate::types::Host>(
4754 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4755 top_row: usize,
4756 end_row: usize,
4757) {
4758 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4759 ed.vim.change_mark_start = Some((top_row, 0));
4761 ed.push_undo();
4762 ed.sync_buffer_content_from_textarea();
4763 let payload = read_vim_range(ed, (top_row, 0), (end_row, 0), RangeKind::Linewise);
4765 if end_row > top_row {
4767 ed.mutate_edit(Edit::DeleteRange {
4768 start: Position::new(top_row + 1, 0),
4769 end: Position::new(end_row, 0),
4770 kind: BufKind::Line,
4771 });
4772 }
4773 let indent_chars = if ed.settings.autoindent {
4776 let line = hjkl_buffer::rope_line_str(&crate::types::Query::rope(&ed.buffer), top_row);
4777 line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
4778 } else {
4779 0
4780 };
4781 let line_chars = buf_line_chars(&ed.buffer, top_row);
4782 if line_chars > indent_chars {
4783 ed.mutate_edit(Edit::DeleteRange {
4784 start: Position::new(top_row, indent_chars),
4785 end: Position::new(top_row, line_chars),
4786 kind: BufKind::Char,
4787 });
4788 }
4789 if !payload.is_empty() {
4790 ed.record_yank_to_host(payload.clone());
4791 ed.record_delete(payload, true);
4792 }
4793 buf_set_cursor_rc(&mut ed.buffer, top_row, indent_chars);
4794 ed.push_buffer_cursor_to_textarea();
4795 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4796}
4797
4798fn run_operator_over_range<H: crate::types::Host>(
4799 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4800 op: Operator,
4801 start: (usize, usize),
4802 end: (usize, usize),
4803 kind: RangeKind,
4804) {
4805 let (top, bot) = order(start, end);
4806 if top == bot && !matches!(kind, RangeKind::Linewise) {
4810 return;
4811 }
4812
4813 match op {
4814 Operator::Yank => {
4815 let text = read_vim_range(ed, top, bot, kind);
4816 if !text.is_empty() {
4817 ed.record_yank_to_host(text.clone());
4818 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
4819 }
4820 let rbr = match kind {
4824 RangeKind::Linewise => {
4825 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
4826 (bot.0, last_col)
4827 }
4828 RangeKind::Inclusive => (bot.0, bot.1),
4829 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
4830 };
4831 ed.set_mark('[', top);
4832 ed.set_mark(']', rbr);
4833 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4834 ed.push_buffer_cursor_to_textarea();
4835 }
4836 Operator::Delete => {
4837 ed.push_undo();
4838 cut_vim_range(ed, top, bot, kind);
4839 if !matches!(kind, RangeKind::Linewise) {
4844 clamp_cursor_to_normal_mode(ed);
4845 }
4846 ed.vim.mode = Mode::Normal;
4847 let pos = ed.cursor();
4851 ed.set_mark('[', pos);
4852 ed.set_mark(']', pos);
4853 }
4854 Operator::Change => {
4855 if matches!(kind, RangeKind::Linewise) {
4860 change_linewise_rows(ed, top.0, bot.0);
4864 } else {
4865 ed.vim.change_mark_start = Some(top);
4867 ed.push_undo();
4868 cut_vim_range(ed, top, bot, kind);
4869 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4870 }
4871 }
4872 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4873 apply_case_op_to_selection(ed, op, top, bot, kind);
4874 }
4875 Operator::Indent | Operator::Outdent => {
4876 ed.push_undo();
4879 if op == Operator::Indent {
4880 indent_rows(ed, top.0, bot.0, 1);
4881 } else {
4882 outdent_rows(ed, top.0, bot.0, 1);
4883 }
4884 ed.vim.mode = Mode::Normal;
4885 }
4886 Operator::Fold => {
4887 if bot.0 >= top.0 {
4891 ed.apply_fold_op(crate::types::FoldOp::Add {
4892 start_row: top.0,
4893 end_row: bot.0,
4894 closed: true,
4895 });
4896 }
4897 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4898 ed.push_buffer_cursor_to_textarea();
4899 ed.vim.mode = Mode::Normal;
4900 }
4901 Operator::Reflow => {
4902 ed.push_undo();
4903 reflow_rows(ed, top.0, bot.0);
4904 ed.vim.mode = Mode::Normal;
4905 }
4906 Operator::ReflowKeepCursor => {
4907 let saved = ed.cursor();
4910 ed.push_undo();
4911 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
4912 let (new_row, new_col) = reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
4913 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
4914 ed.push_buffer_cursor_to_textarea();
4915 ed.sticky_col = Some(new_col);
4916 ed.vim.mode = Mode::Normal;
4917 }
4918 Operator::AutoIndent => {
4919 ed.push_undo();
4921 auto_indent_rows(ed, top.0, bot.0);
4922 ed.vim.mode = Mode::Normal;
4923 }
4924 Operator::Filter => {
4925 }
4930 Operator::Comment => {
4931 }
4934 }
4935}
4936
4937pub(crate) fn delete_range_bridge<H: crate::types::Host>(
4954 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4955 start: (usize, usize),
4956 end: (usize, usize),
4957 kind: RangeKind,
4958 register: char,
4959) {
4960 ed.vim.pending_register = Some(register);
4961 run_operator_over_range(ed, Operator::Delete, start, end, kind);
4962}
4963
4964pub(crate) fn yank_range_bridge<H: crate::types::Host>(
4967 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4968 start: (usize, usize),
4969 end: (usize, usize),
4970 kind: RangeKind,
4971 register: char,
4972) {
4973 ed.vim.pending_register = Some(register);
4974 run_operator_over_range(ed, Operator::Yank, start, end, kind);
4975}
4976
4977pub(crate) fn change_range_bridge<H: crate::types::Host>(
4982 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4983 start: (usize, usize),
4984 end: (usize, usize),
4985 kind: RangeKind,
4986 register: char,
4987) {
4988 ed.vim.pending_register = Some(register);
4989 run_operator_over_range(ed, Operator::Change, start, end, kind);
4990}
4991
4992pub(crate) fn indent_range_bridge<H: crate::types::Host>(
4997 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4998 start: (usize, usize),
4999 end: (usize, usize),
5000 count: i32,
5001 shiftwidth: u32,
5002) {
5003 if count == 0 {
5004 return;
5005 }
5006 let (top_row, bot_row) = if start.0 <= end.0 {
5007 (start.0, end.0)
5008 } else {
5009 (end.0, start.0)
5010 };
5011 let original_sw = ed.settings().shiftwidth;
5013 if shiftwidth > 0 {
5014 ed.settings_mut().shiftwidth = shiftwidth as usize;
5015 }
5016 ed.push_undo();
5017 let abs_count = count.unsigned_abs() as usize;
5018 if count > 0 {
5019 indent_rows(ed, top_row, bot_row, abs_count);
5020 } else {
5021 outdent_rows(ed, top_row, bot_row, abs_count);
5022 }
5023 if shiftwidth > 0 {
5024 ed.settings_mut().shiftwidth = original_sw;
5025 }
5026 ed.vim.mode = Mode::Normal;
5027}
5028
5029pub(crate) fn case_range_bridge<H: crate::types::Host>(
5033 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5034 start: (usize, usize),
5035 end: (usize, usize),
5036 kind: RangeKind,
5037 op: Operator,
5038) {
5039 match op {
5040 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
5041 _ => return,
5042 }
5043 let (top, bot) = order(start, end);
5044 apply_case_op_to_selection(ed, op, top, bot, kind);
5045}
5046
5047pub(crate) fn delete_block_bridge<H: crate::types::Host>(
5068 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5069 top_row: usize,
5070 bot_row: usize,
5071 left_col: usize,
5072 right_col: usize,
5073 register: char,
5074) {
5075 ed.vim.pending_register = Some(register);
5076 let saved_anchor = ed.vim.block_anchor;
5077 let saved_vcol = ed.vim.block_vcol;
5078 ed.vim.block_anchor = (top_row, left_col);
5079 ed.vim.block_vcol = right_col;
5080 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5082 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5084 apply_block_operator(ed, Operator::Delete);
5085 ed.vim.block_anchor = saved_anchor;
5089 ed.vim.block_vcol = saved_vcol;
5090}
5091
5092pub(crate) fn yank_block_bridge<H: crate::types::Host>(
5094 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5095 top_row: usize,
5096 bot_row: usize,
5097 left_col: usize,
5098 right_col: usize,
5099 register: char,
5100) {
5101 ed.vim.pending_register = Some(register);
5102 let saved_anchor = ed.vim.block_anchor;
5103 let saved_vcol = ed.vim.block_vcol;
5104 ed.vim.block_anchor = (top_row, left_col);
5105 ed.vim.block_vcol = right_col;
5106 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5107 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5108 apply_block_operator(ed, Operator::Yank);
5109 ed.vim.block_anchor = saved_anchor;
5110 ed.vim.block_vcol = saved_vcol;
5111}
5112
5113pub(crate) fn change_block_bridge<H: crate::types::Host>(
5116 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5117 top_row: usize,
5118 bot_row: usize,
5119 left_col: usize,
5120 right_col: usize,
5121 register: char,
5122) {
5123 ed.vim.pending_register = Some(register);
5124 let saved_anchor = ed.vim.block_anchor;
5125 let saved_vcol = ed.vim.block_vcol;
5126 ed.vim.block_anchor = (top_row, left_col);
5127 ed.vim.block_vcol = right_col;
5128 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5129 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5130 apply_block_operator(ed, Operator::Change);
5131 ed.vim.block_anchor = saved_anchor;
5132 ed.vim.block_vcol = saved_vcol;
5133}
5134
5135pub(crate) fn indent_block_bridge<H: crate::types::Host>(
5139 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5140 top_row: usize,
5141 bot_row: usize,
5142 count: i32,
5143) {
5144 if count == 0 {
5145 return;
5146 }
5147 ed.push_undo();
5148 let abs = count.unsigned_abs() as usize;
5149 if count > 0 {
5150 indent_rows(ed, top_row, bot_row, abs);
5151 } else {
5152 outdent_rows(ed, top_row, bot_row, abs);
5153 }
5154 ed.vim.mode = Mode::Normal;
5155}
5156
5157pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
5161 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5162 start: (usize, usize),
5163 end: (usize, usize),
5164) {
5165 let (top_row, bot_row) = if start.0 <= end.0 {
5166 (start.0, end.0)
5167 } else {
5168 (end.0, start.0)
5169 };
5170 ed.push_undo();
5171 auto_indent_rows(ed, top_row, bot_row);
5172 ed.vim.mode = Mode::Normal;
5173}
5174
5175pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
5186 ed: &Editor<hjkl_buffer::Buffer, H>,
5187) -> Option<((usize, usize), (usize, usize))> {
5188 word_text_object(ed, true, false)
5189}
5190
5191pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
5194 ed: &Editor<hjkl_buffer::Buffer, H>,
5195) -> Option<((usize, usize), (usize, usize))> {
5196 word_text_object(ed, false, false)
5197}
5198
5199pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
5202 ed: &Editor<hjkl_buffer::Buffer, H>,
5203) -> Option<((usize, usize), (usize, usize))> {
5204 word_text_object(ed, true, true)
5205}
5206
5207pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
5210 ed: &Editor<hjkl_buffer::Buffer, H>,
5211) -> Option<((usize, usize), (usize, usize))> {
5212 word_text_object(ed, false, true)
5213}
5214
5215pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
5231 ed: &Editor<hjkl_buffer::Buffer, H>,
5232 quote: char,
5233) -> Option<((usize, usize), (usize, usize))> {
5234 quote_text_object(ed, quote, true)
5235}
5236
5237pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
5240 ed: &Editor<hjkl_buffer::Buffer, H>,
5241 quote: char,
5242) -> Option<((usize, usize), (usize, usize))> {
5243 quote_text_object(ed, quote, false)
5244}
5245
5246pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
5254 ed: &Editor<hjkl_buffer::Buffer, H>,
5255 open: char,
5256) -> Option<((usize, usize), (usize, usize))> {
5257 bracket_text_object(ed, open, true, 1).map(|(s, e, _kind)| (s, e))
5258}
5259
5260pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
5264 ed: &Editor<hjkl_buffer::Buffer, H>,
5265 open: char,
5266) -> Option<((usize, usize), (usize, usize))> {
5267 bracket_text_object(ed, open, false, 1).map(|(s, e, _kind)| (s, e))
5268}
5269
5270pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
5275 ed: &Editor<hjkl_buffer::Buffer, H>,
5276) -> Option<((usize, usize), (usize, usize))> {
5277 sentence_text_object(ed, true)
5278}
5279
5280pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
5283 ed: &Editor<hjkl_buffer::Buffer, H>,
5284) -> Option<((usize, usize), (usize, usize))> {
5285 sentence_text_object(ed, false)
5286}
5287
5288pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
5293 ed: &Editor<hjkl_buffer::Buffer, H>,
5294) -> Option<((usize, usize), (usize, usize))> {
5295 paragraph_text_object(ed, true)
5296}
5297
5298pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
5301 ed: &Editor<hjkl_buffer::Buffer, H>,
5302) -> Option<((usize, usize), (usize, usize))> {
5303 paragraph_text_object(ed, false)
5304}
5305
5306pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
5312 ed: &Editor<hjkl_buffer::Buffer, H>,
5313) -> Option<((usize, usize), (usize, usize))> {
5314 tag_text_object(ed, true)
5315}
5316
5317pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
5320 ed: &Editor<hjkl_buffer::Buffer, H>,
5321) -> Option<((usize, usize), (usize, usize))> {
5322 tag_text_object(ed, false)
5323}
5324
5325pub(crate) fn rope_line_to_str(rope: &ropey::Rope, r: usize) -> String {
5330 let s = rope.line(r).to_string();
5331 if s.ends_with('\n') {
5333 s[..s.len() - 1].to_string()
5334 } else {
5335 s
5336 }
5337}
5338
5339pub(crate) fn rope_row_range_str(rope: &ropey::Rope, lo: usize, hi: usize) -> String {
5342 let n = rope.len_lines();
5343 let lo = lo.min(n.saturating_sub(1));
5344 let hi = hi.min(n.saturating_sub(1));
5345 if lo > hi {
5346 return String::new();
5347 }
5348 let start_byte = rope.line_to_byte(lo);
5350 let end_byte = if hi + 1 < n {
5353 rope.line_to_byte(hi + 1).saturating_sub(1)
5356 } else {
5357 rope.len_bytes()
5358 };
5359 rope.byte_slice(start_byte..end_byte).to_string()
5360}
5361
5362pub(crate) fn rope_to_lines_vec(rope: &ropey::Rope) -> Vec<String> {
5366 let n = rope.len_lines();
5367 (0..n).map(|r| rope_line_to_str(rope, r)).collect()
5368}
5369
5370fn greedy_wrap(original: &[String], width: usize) -> Vec<String> {
5374 let mut wrapped: Vec<String> = Vec::new();
5375 let mut paragraph: Vec<String> = Vec::new();
5376 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
5377 if para.is_empty() {
5378 return;
5379 }
5380 let words = para.join(" ");
5381 let mut current = String::new();
5382 for word in words.split_whitespace() {
5383 let extra = if current.is_empty() {
5384 word.chars().count()
5385 } else {
5386 current.chars().count() + 1 + word.chars().count()
5387 };
5388 if extra > width && !current.is_empty() {
5389 out.push(std::mem::take(&mut current));
5390 current.push_str(word);
5391 } else if current.is_empty() {
5392 current.push_str(word);
5393 } else {
5394 current.push(' ');
5395 current.push_str(word);
5396 }
5397 }
5398 if !current.is_empty() {
5399 out.push(current);
5400 }
5401 para.clear();
5402 };
5403 for line in original {
5404 if line.trim().is_empty() {
5405 flush(&mut paragraph, &mut wrapped, width);
5406 wrapped.push(String::new());
5407 } else {
5408 paragraph.push(line.clone());
5409 }
5410 }
5411 flush(&mut paragraph, &mut wrapped, width);
5412 wrapped
5413}
5414
5415fn reflow_rows<H: crate::types::Host>(
5421 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5422 top: usize,
5423 bot: usize,
5424) {
5425 let width = ed.settings().textwidth.max(1);
5426 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5427 let bot = bot.min(lines.len().saturating_sub(1));
5428 if top > bot {
5429 return;
5430 }
5431 let original = lines[top..=bot].to_vec();
5432 let wrapped = greedy_wrap(&original, width);
5433
5434 let after: Vec<String> = lines.split_off(bot + 1);
5436 lines.truncate(top);
5437 lines.extend(wrapped);
5438 lines.extend(after);
5439 ed.restore(lines, (top, 0));
5440 ed.mark_content_dirty();
5441}
5442
5443fn reflow_rows_keep_cursor<H: crate::types::Host>(
5447 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5448 top: usize,
5449 bot: usize,
5450) -> (Vec<String>, Vec<String>) {
5451 let width = ed.settings().textwidth.max(1);
5452 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5453 let bot = bot.min(lines.len().saturating_sub(1));
5454 if top > bot {
5455 return (Vec::new(), Vec::new());
5456 }
5457 let original = lines[top..=bot].to_vec();
5458 let wrapped = greedy_wrap(&original, width);
5459
5460 let after: Vec<String> = lines.split_off(bot + 1);
5461 lines.truncate(top);
5462 lines.extend(wrapped.clone());
5463 lines.extend(after);
5464 ed.restore(lines, (top, 0));
5465 ed.mark_content_dirty();
5466 (original, wrapped)
5467}
5468
5469fn reflow_keep_cursor(
5481 top: usize,
5482 cursor_row: usize,
5483 cursor_col: usize,
5484 before_lines: &[String],
5485 after_lines: &[String],
5486) -> (usize, usize) {
5487 let relative_row = cursor_row.saturating_sub(top);
5507 let mut char_offset: usize = 0;
5508 for (i, line) in before_lines.iter().enumerate() {
5509 if i == relative_row {
5510 let line_len = line.chars().count();
5512 char_offset += cursor_col.min(line_len);
5513 break;
5514 }
5515 char_offset += line.chars().count() + 1;
5517 }
5518
5519 let mut remaining = char_offset;
5521 for (i, line) in after_lines.iter().enumerate() {
5522 let len = line.chars().count();
5523 if remaining <= len {
5524 let col = remaining.min(if len == 0 { 0 } else { len.saturating_sub(1) });
5526 return (top + i, col);
5527 }
5528 remaining = remaining.saturating_sub(len + 1);
5530 }
5531
5532 let last = after_lines.len().saturating_sub(1);
5534 let last_len = after_lines
5535 .get(last)
5536 .map(|l| l.chars().count())
5537 .unwrap_or(0);
5538 let col = if last_len == 0 { 0 } else { last_len - 1 };
5539 (top + last, col)
5540}
5541
5542fn apply_case_op_to_selection<H: crate::types::Host>(
5548 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5549 op: Operator,
5550 top: (usize, usize),
5551 bot: (usize, usize),
5552 kind: RangeKind,
5553) {
5554 use hjkl_buffer::Edit;
5555 ed.push_undo();
5556 let saved_yank = ed.yank().to_string();
5557 let saved_yank_linewise = ed.vim.yank_linewise;
5558 let selection = cut_vim_range(ed, top, bot, kind);
5559 let transformed = match op {
5560 Operator::Uppercase => selection.to_uppercase(),
5561 Operator::Lowercase => selection.to_lowercase(),
5562 Operator::ToggleCase => toggle_case_str(&selection),
5563 _ => unreachable!(),
5564 };
5565 if !transformed.is_empty() {
5566 let cursor = buf_cursor_pos(&ed.buffer);
5567 ed.mutate_edit(Edit::InsertStr {
5568 at: cursor,
5569 text: transformed,
5570 });
5571 }
5572 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5573 ed.push_buffer_cursor_to_textarea();
5574 ed.set_yank(saved_yank);
5575 ed.vim.yank_linewise = saved_yank_linewise;
5576 ed.vim.mode = Mode::Normal;
5577}
5578
5579fn indent_rows<H: crate::types::Host>(
5584 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5585 top: usize,
5586 bot: usize,
5587 count: usize,
5588) {
5589 ed.sync_buffer_content_from_textarea();
5590 let width = ed.settings().shiftwidth * count.max(1);
5591 let pad: String = " ".repeat(width);
5592 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5593 let bot = bot.min(lines.len().saturating_sub(1));
5594 for line in lines.iter_mut().take(bot + 1).skip(top) {
5595 if !line.is_empty() {
5596 line.insert_str(0, &pad);
5597 }
5598 }
5599 ed.restore(lines, (top, 0));
5602 move_first_non_whitespace(ed);
5603}
5604
5605fn outdent_rows<H: crate::types::Host>(
5609 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5610 top: usize,
5611 bot: usize,
5612 count: usize,
5613) {
5614 ed.sync_buffer_content_from_textarea();
5615 let width = ed.settings().shiftwidth * count.max(1);
5616 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5617 let bot = bot.min(lines.len().saturating_sub(1));
5618 for line in lines.iter_mut().take(bot + 1).skip(top) {
5619 let strip: usize = line
5620 .chars()
5621 .take(width)
5622 .take_while(|c| *c == ' ' || *c == '\t')
5623 .count();
5624 if strip > 0 {
5625 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
5626 line.drain(..byte_len);
5627 }
5628 }
5629 ed.restore(lines, (top, 0));
5630 move_first_non_whitespace(ed);
5631}
5632
5633fn bracket_net(line: &str) -> i32 {
5660 let mut net: i32 = 0;
5661 let mut chars = line.chars().peekable();
5662 while let Some(ch) = chars.next() {
5663 match ch {
5664 '/' if chars.peek() == Some(&'/') => return net,
5666 '"' => {
5667 while let Some(c) = chars.next() {
5669 match c {
5670 '\\' => {
5671 chars.next();
5672 } '"' => break,
5674 _ => {}
5675 }
5676 }
5677 }
5678 '\'' => {
5679 let saved: Vec<char> = chars.clone().take(5).collect();
5688 let close_idx = if saved.first() == Some(&'\\') {
5689 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
5690 } else {
5691 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
5692 };
5693 if let Some(idx) = close_idx {
5694 for _ in 0..=idx {
5695 chars.next();
5696 }
5697 }
5698 }
5700 '{' | '(' | '[' => net += 1,
5701 '}' | ')' | ']' => net -= 1,
5702 _ => {}
5703 }
5704 }
5705 net
5706}
5707
5708fn auto_indent_rows<H: crate::types::Host>(
5730 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5731 top: usize,
5732 bot: usize,
5733) {
5734 ed.sync_buffer_content_from_textarea();
5735 let shiftwidth = ed.settings().shiftwidth;
5736 let expandtab = ed.settings().expandtab;
5737 let indent_unit: String = if expandtab {
5738 " ".repeat(shiftwidth)
5739 } else {
5740 "\t".to_string()
5741 };
5742
5743 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5744 let bot = bot.min(lines.len().saturating_sub(1));
5745
5746 let mut depth: i32 = 0;
5749 for line in lines.iter().take(top) {
5750 depth += bracket_net(line);
5751 if depth < 0 {
5752 depth = 0;
5753 }
5754 }
5755
5756 for line in lines.iter_mut().take(bot + 1).skip(top) {
5757 let trimmed_owned = line.trim_start().to_owned();
5758 if trimmed_owned.is_empty() {
5760 *line = String::new();
5761 continue;
5763 }
5764
5765 let starts_with_close = trimmed_owned
5767 .chars()
5768 .next()
5769 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
5770 let starts_with_dot = trimmed_owned.starts_with('.')
5780 && !trimmed_owned.starts_with("..")
5781 && !trimmed_owned.starts_with(".;");
5782 let effective_depth = if starts_with_close {
5783 depth.saturating_sub(1)
5784 } else if starts_with_dot {
5785 depth.saturating_add(1)
5786 } else {
5787 depth
5788 } as usize;
5789
5790 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
5792
5793 depth += bracket_net(&trimmed_owned);
5795 if depth < 0 {
5796 depth = 0;
5797 }
5798
5799 *line = new_line;
5800 }
5801
5802 ed.restore(lines, (top, 0));
5804 move_first_non_whitespace(ed);
5805 ed.last_indent_range = Some((top, bot));
5807}
5808
5809fn toggle_case_str(s: &str) -> String {
5810 s.chars()
5811 .map(|c| {
5812 if c.is_lowercase() {
5813 c.to_uppercase().next().unwrap_or(c)
5814 } else if c.is_uppercase() {
5815 c.to_lowercase().next().unwrap_or(c)
5816 } else {
5817 c
5818 }
5819 })
5820 .collect()
5821}
5822
5823fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
5824 if a <= b { (a, b) } else { (b, a) }
5825}
5826
5827fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5832 let (row, col) = ed.cursor();
5833 let line_chars = buf_line_chars(&ed.buffer, row);
5834 let max_col = line_chars.saturating_sub(1);
5835 if col > max_col {
5836 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
5837 ed.push_buffer_cursor_to_textarea();
5838 }
5839}
5840
5841fn execute_line_op<H: crate::types::Host>(
5844 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5845 op: Operator,
5846 count: usize,
5847) {
5848 let (row, col) = ed.cursor();
5849 let total = buf_row_count(&ed.buffer);
5850 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
5851
5852 match op {
5853 Operator::Yank => {
5854 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
5856 if !text.is_empty() {
5857 ed.record_yank_to_host(text.clone());
5858 ed.record_yank(text, true);
5859 }
5860 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
5863 ed.set_mark('[', (row, 0));
5864 ed.set_mark(']', (end_row, last_col));
5865 buf_set_cursor_rc(&mut ed.buffer, row, col);
5866 ed.push_buffer_cursor_to_textarea();
5867 ed.vim.mode = Mode::Normal;
5868 }
5869 Operator::Delete => {
5870 ed.push_undo();
5871 let deleted_through_last = end_row + 1 >= total;
5872 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
5873 let total_after = buf_row_count(&ed.buffer);
5877 let raw_target = if deleted_through_last {
5878 row.saturating_sub(1).min(total_after.saturating_sub(1))
5879 } else {
5880 row.min(total_after.saturating_sub(1))
5881 };
5882 let target_row = if raw_target > 0
5888 && raw_target + 1 == total_after
5889 && buf_line(&ed.buffer, raw_target)
5890 .map(|s| s.is_empty())
5891 .unwrap_or(false)
5892 {
5893 raw_target - 1
5894 } else {
5895 raw_target
5896 };
5897 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5898 ed.push_buffer_cursor_to_textarea();
5899 move_first_non_whitespace(ed);
5900 ed.sticky_col = Some(ed.cursor().1);
5901 ed.vim.mode = Mode::Normal;
5902 let pos = ed.cursor();
5905 ed.set_mark('[', pos);
5906 ed.set_mark(']', pos);
5907 }
5908 Operator::Change => {
5909 change_linewise_rows(ed, row, end_row);
5913 }
5914 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5915 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
5919 move_first_non_whitespace(ed);
5922 }
5923 Operator::Indent | Operator::Outdent => {
5924 ed.push_undo();
5926 if op == Operator::Indent {
5927 indent_rows(ed, row, end_row, 1);
5928 } else {
5929 outdent_rows(ed, row, end_row, 1);
5930 }
5931 ed.sticky_col = Some(ed.cursor().1);
5932 ed.vim.mode = Mode::Normal;
5933 }
5934 Operator::Fold => unreachable!("Fold has no line-op double"),
5936 Operator::Reflow => {
5937 ed.push_undo();
5939 reflow_rows(ed, row, end_row);
5940 move_first_non_whitespace(ed);
5941 ed.sticky_col = Some(ed.cursor().1);
5942 ed.vim.mode = Mode::Normal;
5943 }
5944 Operator::ReflowKeepCursor => {
5945 let saved = ed.cursor();
5948 ed.push_undo();
5949 let (before, after) = reflow_rows_keep_cursor(ed, row, end_row);
5950 let (new_row, new_col) = reflow_keep_cursor(row, saved.0, saved.1, &before, &after);
5951 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
5952 ed.push_buffer_cursor_to_textarea();
5953 ed.sticky_col = Some(new_col);
5954 ed.vim.mode = Mode::Normal;
5955 }
5956 Operator::AutoIndent => {
5957 ed.push_undo();
5959 auto_indent_rows(ed, row, end_row);
5960 ed.sticky_col = Some(ed.cursor().1);
5961 ed.vim.mode = Mode::Normal;
5962 }
5963 Operator::Filter => {
5964 }
5966 Operator::Comment => {
5967 }
5972 }
5973}
5974
5975pub(crate) fn apply_visual_operator<H: crate::types::Host>(
5978 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5979 op: Operator,
5980) {
5981 match ed.vim.mode {
5982 Mode::VisualLine => {
5983 let cursor_row = buf_cursor_pos(&ed.buffer).row;
5984 let top = cursor_row.min(ed.vim.visual_line_anchor);
5985 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5986 ed.vim.yank_linewise = true;
5987 match op {
5988 Operator::Yank => {
5989 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
5990 if !text.is_empty() {
5991 ed.record_yank_to_host(text.clone());
5992 ed.record_yank(text, true);
5993 }
5994 buf_set_cursor_rc(&mut ed.buffer, top, 0);
5995 ed.push_buffer_cursor_to_textarea();
5996 ed.vim.mode = Mode::Normal;
5997 }
5998 Operator::Delete => {
5999 ed.push_undo();
6000 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6001 ed.vim.mode = Mode::Normal;
6002 }
6003 Operator::Change => {
6004 change_linewise_rows(ed, top, bot);
6007 }
6008 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
6009 let bot = buf_cursor_pos(&ed.buffer)
6010 .row
6011 .max(ed.vim.visual_line_anchor);
6012 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
6013 move_first_non_whitespace(ed);
6014 }
6015 Operator::Indent | Operator::Outdent => {
6016 ed.push_undo();
6017 let (cursor_row, _) = ed.cursor();
6018 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6019 if op == Operator::Indent {
6020 indent_rows(ed, top, bot, 1);
6021 } else {
6022 outdent_rows(ed, top, bot, 1);
6023 }
6024 ed.vim.mode = Mode::Normal;
6025 }
6026 Operator::Reflow => {
6027 ed.push_undo();
6028 let (cursor_row, _) = ed.cursor();
6029 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6030 reflow_rows(ed, top, bot);
6031 ed.vim.mode = Mode::Normal;
6032 }
6033 Operator::ReflowKeepCursor => {
6034 let saved = ed.cursor();
6035 ed.push_undo();
6036 let (cursor_row, _) = ed.cursor();
6037 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6038 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6039 let (new_row, new_col) =
6040 reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6041 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6042 ed.push_buffer_cursor_to_textarea();
6043 ed.vim.mode = Mode::Normal;
6044 }
6045 Operator::AutoIndent => {
6046 ed.push_undo();
6047 let (cursor_row, _) = ed.cursor();
6048 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6049 auto_indent_rows(ed, top, bot);
6050 ed.vim.mode = Mode::Normal;
6051 }
6052 Operator::Filter => {}
6054 Operator::Comment => {}
6056 Operator::Fold => unreachable!("Visual zf takes its own path"),
6059 }
6060 }
6061 Mode::Visual => {
6062 ed.vim.yank_linewise = false;
6063 let anchor = ed.vim.visual_anchor;
6064 let cursor = ed.cursor();
6065 let (top, bot) = order(anchor, cursor);
6066 match op {
6067 Operator::Yank => {
6068 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
6069 if !text.is_empty() {
6070 ed.record_yank_to_host(text.clone());
6071 ed.record_yank(text, false);
6072 }
6073 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6074 ed.push_buffer_cursor_to_textarea();
6075 ed.vim.mode = Mode::Normal;
6076 }
6077 Operator::Delete => {
6078 ed.push_undo();
6079 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6080 ed.vim.mode = Mode::Normal;
6081 }
6082 Operator::Change => {
6083 ed.push_undo();
6084 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6085 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
6086 }
6087 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
6088 let anchor = ed.vim.visual_anchor;
6090 let cursor = ed.cursor();
6091 let (top, bot) = order(anchor, cursor);
6092 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
6093 }
6094 Operator::Indent | Operator::Outdent => {
6095 ed.push_undo();
6096 let anchor = ed.vim.visual_anchor;
6097 let cursor = ed.cursor();
6098 let (top, bot) = order(anchor, cursor);
6099 if op == Operator::Indent {
6100 indent_rows(ed, top.0, bot.0, 1);
6101 } else {
6102 outdent_rows(ed, top.0, bot.0, 1);
6103 }
6104 ed.vim.mode = Mode::Normal;
6105 }
6106 Operator::Reflow => {
6107 ed.push_undo();
6108 let anchor = ed.vim.visual_anchor;
6109 let cursor = ed.cursor();
6110 let (top, bot) = order(anchor, cursor);
6111 reflow_rows(ed, top.0, bot.0);
6112 ed.vim.mode = Mode::Normal;
6113 }
6114 Operator::ReflowKeepCursor => {
6115 let saved = ed.cursor();
6116 ed.push_undo();
6117 let anchor = ed.vim.visual_anchor;
6118 let cursor = ed.cursor();
6119 let (top, bot) = order(anchor, cursor);
6120 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
6121 let (new_row, new_col) =
6122 reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
6123 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6124 ed.push_buffer_cursor_to_textarea();
6125 ed.vim.mode = Mode::Normal;
6126 }
6127 Operator::AutoIndent => {
6128 ed.push_undo();
6129 let anchor = ed.vim.visual_anchor;
6130 let cursor = ed.cursor();
6131 let (top, bot) = order(anchor, cursor);
6132 auto_indent_rows(ed, top.0, bot.0);
6133 ed.vim.mode = Mode::Normal;
6134 }
6135 Operator::Filter => {}
6137 Operator::Comment => {}
6139 Operator::Fold => unreachable!("Visual zf takes its own path"),
6140 }
6141 }
6142 Mode::VisualBlock => apply_block_operator(ed, op),
6143 _ => {}
6144 }
6145}
6146
6147fn block_bounds<H: crate::types::Host>(
6152 ed: &Editor<hjkl_buffer::Buffer, H>,
6153) -> (usize, usize, usize, usize) {
6154 let (ar, ac) = ed.vim.block_anchor;
6155 let (cr, _) = ed.cursor();
6156 let cc = ed.vim.block_vcol;
6157 let top = ar.min(cr);
6158 let bot = ar.max(cr);
6159 let left = ac.min(cc);
6160 let right = ac.max(cc);
6161 (top, bot, left, right)
6162}
6163
6164pub(crate) fn update_block_vcol<H: crate::types::Host>(
6169 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6170 motion: &Motion,
6171) {
6172 match motion {
6173 Motion::Left
6174 | Motion::Right
6175 | Motion::WordFwd
6176 | Motion::BigWordFwd
6177 | Motion::WordBack
6178 | Motion::BigWordBack
6179 | Motion::WordEnd
6180 | Motion::BigWordEnd
6181 | Motion::WordEndBack
6182 | Motion::BigWordEndBack
6183 | Motion::LineStart
6184 | Motion::FirstNonBlank
6185 | Motion::LineEnd
6186 | Motion::Find { .. }
6187 | Motion::FindRepeat { .. }
6188 | Motion::MatchBracket => {
6189 ed.vim.block_vcol = ed.cursor().1;
6190 }
6191 _ => {}
6193 }
6194}
6195
6196fn apply_block_operator<H: crate::types::Host>(
6201 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6202 op: Operator,
6203) {
6204 let (top, bot, left, right) = block_bounds(ed);
6205 let yank = block_yank(ed, top, bot, left, right);
6207
6208 match op {
6209 Operator::Yank => {
6210 if !yank.is_empty() {
6211 ed.record_yank_to_host(yank.clone());
6212 ed.record_yank(yank, false);
6213 }
6214 ed.vim.mode = Mode::Normal;
6215 ed.jump_cursor(top, left);
6216 }
6217 Operator::Delete => {
6218 ed.push_undo();
6219 delete_block_contents(ed, top, bot, left, right);
6220 if !yank.is_empty() {
6221 ed.record_yank_to_host(yank.clone());
6222 ed.record_delete(yank, false);
6223 }
6224 ed.vim.mode = Mode::Normal;
6225 ed.jump_cursor(top, left);
6226 }
6227 Operator::Change => {
6228 ed.push_undo();
6229 delete_block_contents(ed, top, bot, left, right);
6230 if !yank.is_empty() {
6231 ed.record_yank_to_host(yank.clone());
6232 ed.record_delete(yank, false);
6233 }
6234 ed.jump_cursor(top, left);
6235 begin_insert_noundo(
6236 ed,
6237 1,
6238 InsertReason::BlockChange {
6239 top,
6240 bot,
6241 col: left,
6242 },
6243 );
6244 }
6245 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
6246 ed.push_undo();
6247 transform_block_case(ed, op, top, bot, left, right);
6248 ed.vim.mode = Mode::Normal;
6249 ed.jump_cursor(top, left);
6250 }
6251 Operator::Indent | Operator::Outdent => {
6252 ed.push_undo();
6256 if op == Operator::Indent {
6257 indent_rows(ed, top, bot, 1);
6258 } else {
6259 outdent_rows(ed, top, bot, 1);
6260 }
6261 ed.vim.mode = Mode::Normal;
6262 }
6263 Operator::Fold => unreachable!("Visual zf takes its own path"),
6264 Operator::Reflow => {
6265 ed.push_undo();
6269 reflow_rows(ed, top, bot);
6270 ed.vim.mode = Mode::Normal;
6271 }
6272 Operator::ReflowKeepCursor => {
6273 let saved = ed.cursor();
6275 ed.push_undo();
6276 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6277 let (new_row, new_col) = reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6278 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6279 ed.push_buffer_cursor_to_textarea();
6280 ed.vim.mode = Mode::Normal;
6281 }
6282 Operator::AutoIndent => {
6283 ed.push_undo();
6286 auto_indent_rows(ed, top, bot);
6287 ed.vim.mode = Mode::Normal;
6288 }
6289 Operator::Filter => {}
6291 Operator::Comment => {}
6293 }
6294}
6295
6296fn transform_block_case<H: crate::types::Host>(
6300 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6301 op: Operator,
6302 top: usize,
6303 bot: usize,
6304 left: usize,
6305 right: usize,
6306) {
6307 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6308 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6309 let chars: Vec<char> = lines[r].chars().collect();
6310 if left >= chars.len() {
6311 continue;
6312 }
6313 let end = (right + 1).min(chars.len());
6314 let head: String = chars[..left].iter().collect();
6315 let mid: String = chars[left..end].iter().collect();
6316 let tail: String = chars[end..].iter().collect();
6317 let transformed = match op {
6318 Operator::Uppercase => mid.to_uppercase(),
6319 Operator::Lowercase => mid.to_lowercase(),
6320 Operator::ToggleCase => toggle_case_str(&mid),
6321 _ => mid,
6322 };
6323 lines[r] = format!("{head}{transformed}{tail}");
6324 }
6325 let saved_yank = ed.yank().to_string();
6326 let saved_linewise = ed.vim.yank_linewise;
6327 ed.restore(lines, (top, left));
6328 ed.set_yank(saved_yank);
6329 ed.vim.yank_linewise = saved_linewise;
6330}
6331
6332fn block_yank<H: crate::types::Host>(
6333 ed: &Editor<hjkl_buffer::Buffer, H>,
6334 top: usize,
6335 bot: usize,
6336 left: usize,
6337 right: usize,
6338) -> String {
6339 let rope = crate::types::Query::rope(&ed.buffer);
6340 let n = rope.len_lines();
6341 let mut rows: Vec<String> = Vec::new();
6342 for r in top..=bot {
6343 if r >= n {
6344 break;
6345 }
6346 let line = rope_line_to_str(&rope, r);
6347 let chars: Vec<char> = line.chars().collect();
6348 let end = (right + 1).min(chars.len());
6349 if left >= chars.len() {
6350 rows.push(String::new());
6351 } else {
6352 rows.push(chars[left..end].iter().collect());
6353 }
6354 }
6355 rows.join("\n")
6356}
6357
6358fn delete_block_contents<H: crate::types::Host>(
6359 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6360 top: usize,
6361 bot: usize,
6362 left: usize,
6363 right: usize,
6364) {
6365 use hjkl_buffer::{Edit, MotionKind, Position};
6366 ed.sync_buffer_content_from_textarea();
6367 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
6368 if last_row < top {
6369 return;
6370 }
6371 ed.mutate_edit(Edit::DeleteRange {
6372 start: Position::new(top, left),
6373 end: Position::new(last_row, right),
6374 kind: MotionKind::Block,
6375 });
6376 ed.push_buffer_cursor_to_textarea();
6377}
6378
6379pub(crate) fn block_replace<H: crate::types::Host>(
6381 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6382 ch: char,
6383) {
6384 let (top, bot, left, right) = block_bounds(ed);
6385 ed.push_undo();
6386 ed.sync_buffer_content_from_textarea();
6387 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6388 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6389 let chars: Vec<char> = lines[r].chars().collect();
6390 if left >= chars.len() {
6391 continue;
6392 }
6393 let end = (right + 1).min(chars.len());
6394 let before: String = chars[..left].iter().collect();
6395 let middle: String = std::iter::repeat_n(ch, end - left).collect();
6396 let after: String = chars[end..].iter().collect();
6397 lines[r] = format!("{before}{middle}{after}");
6398 }
6399 reset_textarea_lines(ed, lines);
6400 ed.vim.mode = Mode::Normal;
6401 ed.jump_cursor(top, left);
6402}
6403
6404fn reset_textarea_lines<H: crate::types::Host>(
6408 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6409 lines: Vec<String>,
6410) {
6411 let cursor = ed.cursor();
6412 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
6413 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
6414 ed.mark_content_dirty();
6415}
6416
6417type Pos = (usize, usize);
6423
6424pub(crate) fn text_object_range<H: crate::types::Host>(
6428 ed: &Editor<hjkl_buffer::Buffer, H>,
6429 obj: TextObject,
6430 inner: bool,
6431 count: usize,
6432) -> Option<(Pos, Pos, RangeKind)> {
6433 match obj {
6434 TextObject::Word { big } => {
6435 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
6436 }
6437 TextObject::Quote(q) => {
6438 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6439 }
6440 TextObject::Bracket(open) => bracket_text_object(ed, open, inner, count),
6441 TextObject::Paragraph => {
6442 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
6443 }
6444 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
6445 TextObject::Sentence => {
6446 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6447 }
6448 }
6449}
6450
6451fn sentence_boundary<H: crate::types::Host>(
6455 ed: &Editor<hjkl_buffer::Buffer, H>,
6456 forward: bool,
6457) -> Option<(usize, usize)> {
6458 let rope = crate::types::Query::rope(&ed.buffer);
6459 let n_lines = rope.len_lines();
6460 if n_lines == 0 {
6461 return None;
6462 }
6463 let line_lens: Vec<usize> = (0..n_lines)
6465 .map(|r| rope_line_to_str(&rope, r).chars().count())
6466 .collect();
6467 let pos_to_idx = |pos: (usize, usize)| -> usize {
6468 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6469 idx + pos.1
6470 };
6471 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6472 for (r, &len) in line_lens.iter().enumerate() {
6473 if idx <= len {
6474 return (r, idx);
6475 }
6476 idx -= len + 1;
6477 }
6478 let last = n_lines.saturating_sub(1);
6479 (last, line_lens[last])
6480 };
6481 let mut chars: Vec<char> = rope.chars().collect();
6484 if chars.last() == Some(&'\n') {
6486 chars.pop();
6487 }
6488 if chars.is_empty() {
6489 return None;
6490 }
6491 let total = chars.len();
6492 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
6493 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6494
6495 if forward {
6496 let mut i = cursor_idx + 1;
6499 while i < total {
6500 if is_terminator(chars[i]) {
6501 while i + 1 < total && is_terminator(chars[i + 1]) {
6502 i += 1;
6503 }
6504 if i + 1 >= total {
6505 return None;
6506 }
6507 if chars[i + 1].is_whitespace() {
6508 let mut j = i + 1;
6509 while j < total && chars[j].is_whitespace() {
6510 j += 1;
6511 }
6512 if j >= total {
6513 return None;
6514 }
6515 return Some(idx_to_pos(j));
6516 }
6517 }
6518 i += 1;
6519 }
6520 None
6521 } else {
6522 let find_start = |from: usize| -> Option<usize> {
6526 let mut start = from;
6527 while start > 0 {
6528 let prev = chars[start - 1];
6529 if prev.is_whitespace() {
6530 let mut k = start - 1;
6531 while k > 0 && chars[k - 1].is_whitespace() {
6532 k -= 1;
6533 }
6534 if k > 0 && is_terminator(chars[k - 1]) {
6535 break;
6536 }
6537 }
6538 start -= 1;
6539 }
6540 while start < total && chars[start].is_whitespace() {
6541 start += 1;
6542 }
6543 (start < total).then_some(start)
6544 };
6545 let current_start = find_start(cursor_idx)?;
6546 if current_start < cursor_idx {
6547 return Some(idx_to_pos(current_start));
6548 }
6549 let mut k = current_start;
6552 while k > 0 && chars[k - 1].is_whitespace() {
6553 k -= 1;
6554 }
6555 if k == 0 {
6556 return None;
6557 }
6558 let prev_start = find_start(k - 1)?;
6559 Some(idx_to_pos(prev_start))
6560 }
6561}
6562
6563fn sentence_text_object<H: crate::types::Host>(
6569 ed: &Editor<hjkl_buffer::Buffer, H>,
6570 inner: bool,
6571) -> Option<((usize, usize), (usize, usize))> {
6572 let rope = crate::types::Query::rope(&ed.buffer);
6573 let n_lines = rope.len_lines();
6574 if n_lines == 0 {
6575 return None;
6576 }
6577 let line_lens: Vec<usize> = (0..n_lines)
6580 .map(|r| rope_line_to_str(&rope, r).chars().count())
6581 .collect();
6582 let pos_to_idx = |pos: (usize, usize)| -> usize {
6583 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6584 idx + pos.1
6585 };
6586 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6587 for (r, &len) in line_lens.iter().enumerate() {
6588 if idx <= len {
6589 return (r, idx);
6590 }
6591 idx -= len + 1;
6592 }
6593 let last = n_lines.saturating_sub(1);
6594 (last, line_lens[last])
6595 };
6596 let mut chars: Vec<char> = rope.chars().collect();
6597 if chars.last() == Some(&'\n') {
6598 chars.pop();
6599 }
6600 if chars.is_empty() {
6601 return None;
6602 }
6603
6604 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
6605 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6606
6607 let mut start = cursor_idx;
6611 while start > 0 {
6612 let prev = chars[start - 1];
6613 if prev.is_whitespace() {
6614 let mut k = start - 1;
6618 while k > 0 && chars[k - 1].is_whitespace() {
6619 k -= 1;
6620 }
6621 if k > 0 && is_terminator(chars[k - 1]) {
6622 break;
6623 }
6624 }
6625 start -= 1;
6626 }
6627 while start < chars.len() && chars[start].is_whitespace() {
6630 start += 1;
6631 }
6632 if start >= chars.len() {
6633 return None;
6634 }
6635
6636 let mut end = start;
6639 while end < chars.len() {
6640 if is_terminator(chars[end]) {
6641 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
6643 end += 1;
6644 }
6645 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
6648 break;
6649 }
6650 }
6651 end += 1;
6652 }
6653 let end_idx = (end + 1).min(chars.len());
6655
6656 let final_end = if inner {
6657 end_idx
6658 } else {
6659 let mut e = end_idx;
6663 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
6664 e += 1;
6665 }
6666 e
6667 };
6668
6669 Some((idx_to_pos(start), idx_to_pos(final_end)))
6670}
6671
6672fn tag_text_object<H: crate::types::Host>(
6676 ed: &Editor<hjkl_buffer::Buffer, H>,
6677 inner: bool,
6678) -> Option<((usize, usize), (usize, usize))> {
6679 let rope = crate::types::Query::rope(&ed.buffer);
6680 let n_lines = rope.len_lines();
6681 if n_lines == 0 {
6682 return None;
6683 }
6684 let line_lens: Vec<usize> = (0..n_lines)
6688 .map(|r| rope_line_to_str(&rope, r).chars().count())
6689 .collect();
6690 let pos_to_idx = |pos: (usize, usize)| -> usize {
6691 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6692 idx + pos.1
6693 };
6694 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6695 for (r, &len) in line_lens.iter().enumerate() {
6696 if idx <= len {
6697 return (r, idx);
6698 }
6699 idx -= len + 1;
6700 }
6701 let last = n_lines.saturating_sub(1);
6702 (last, line_lens[last])
6703 };
6704 let mut chars: Vec<char> = rope.chars().collect();
6705 if chars.last() == Some(&'\n') {
6706 chars.pop();
6707 }
6708 let cursor_idx = pos_to_idx(ed.cursor());
6709
6710 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
6718 let mut next_after: Option<(usize, usize, usize, usize)> = None;
6719 let mut i = 0;
6720 while i < chars.len() {
6721 if chars[i] != '<' {
6722 i += 1;
6723 continue;
6724 }
6725 let mut j = i + 1;
6726 while j < chars.len() && chars[j] != '>' {
6727 j += 1;
6728 }
6729 if j >= chars.len() {
6730 break;
6731 }
6732 let inside: String = chars[i + 1..j].iter().collect();
6733 let close_end = j + 1;
6734 let trimmed = inside.trim();
6735 if trimmed.starts_with('!') || trimmed.starts_with('?') {
6736 i = close_end;
6737 continue;
6738 }
6739 if let Some(rest) = trimmed.strip_prefix('/') {
6740 let name = rest.split_whitespace().next().unwrap_or("").to_string();
6741 if !name.is_empty()
6742 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
6743 {
6744 let (open_start, content_start, _) = stack[stack_idx].clone();
6745 stack.truncate(stack_idx);
6746 let content_end = i;
6747 let candidate = (open_start, content_start, content_end, close_end);
6748 if cursor_idx >= content_start && cursor_idx <= content_end {
6749 innermost = match innermost {
6750 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
6751 Some(candidate)
6752 }
6753 None => Some(candidate),
6754 existing => existing,
6755 };
6756 } else if open_start >= cursor_idx && next_after.is_none() {
6757 next_after = Some(candidate);
6758 }
6759 }
6760 } else if !trimmed.ends_with('/') {
6761 let name: String = trimmed
6762 .split(|c: char| c.is_whitespace() || c == '/')
6763 .next()
6764 .unwrap_or("")
6765 .to_string();
6766 if !name.is_empty() {
6767 stack.push((i, close_end, name));
6768 }
6769 }
6770 i = close_end;
6771 }
6772
6773 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
6774 if inner {
6775 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
6776 } else {
6777 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
6778 }
6779}
6780
6781fn is_wordchar(c: char) -> bool {
6782 c.is_alphanumeric() || c == '_'
6783}
6784
6785pub(crate) use hjkl_buffer::is_keyword_char;
6789
6790fn word_text_object<H: crate::types::Host>(
6791 ed: &Editor<hjkl_buffer::Buffer, H>,
6792 inner: bool,
6793 big: bool,
6794) -> Option<((usize, usize), (usize, usize))> {
6795 let (row, col) = ed.cursor();
6796 let line = buf_line(&ed.buffer, row)?;
6797 let chars: Vec<char> = line.chars().collect();
6798 if chars.is_empty() {
6799 return None;
6800 }
6801 let at = col.min(chars.len().saturating_sub(1));
6802 let classify = |c: char| -> u8 {
6803 if c.is_whitespace() {
6804 0
6805 } else if big || is_wordchar(c) {
6806 1
6807 } else {
6808 2
6809 }
6810 };
6811 let cls = classify(chars[at]);
6812 let mut start = at;
6813 while start > 0 && classify(chars[start - 1]) == cls {
6814 start -= 1;
6815 }
6816 let mut end = at;
6817 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
6818 end += 1;
6819 }
6820 let char_byte = |i: usize| {
6822 if i >= chars.len() {
6823 line.len()
6824 } else {
6825 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
6826 }
6827 };
6828 let mut start_col = char_byte(start);
6829 let mut end_col = char_byte(end + 1);
6831 if !inner {
6832 let mut t = end + 1;
6834 let mut included_trailing = false;
6835 while t < chars.len() && chars[t].is_whitespace() {
6836 included_trailing = true;
6837 t += 1;
6838 }
6839 if included_trailing {
6840 end_col = char_byte(t);
6841 } else {
6842 let mut s = start;
6843 while s > 0 && chars[s - 1].is_whitespace() {
6844 s -= 1;
6845 }
6846 start_col = char_byte(s);
6847 }
6848 }
6849 Some(((row, start_col), (row, end_col)))
6850}
6851
6852fn quote_text_object<H: crate::types::Host>(
6853 ed: &Editor<hjkl_buffer::Buffer, H>,
6854 q: char,
6855 inner: bool,
6856) -> Option<((usize, usize), (usize, usize))> {
6857 let (row, col) = ed.cursor();
6858 let line = buf_line(&ed.buffer, row)?;
6859 let bytes = line.as_bytes();
6860 let q_byte = q as u8;
6861 let mut positions: Vec<usize> = Vec::new();
6863 for (i, &b) in bytes.iter().enumerate() {
6864 if b == q_byte {
6865 positions.push(i);
6866 }
6867 }
6868 if positions.len() < 2 {
6869 return None;
6870 }
6871 let mut open_idx: Option<usize> = None;
6872 let mut close_idx: Option<usize> = None;
6873 for pair in positions.chunks(2) {
6874 if pair.len() < 2 {
6875 break;
6876 }
6877 if col >= pair[0] && col <= pair[1] {
6878 open_idx = Some(pair[0]);
6879 close_idx = Some(pair[1]);
6880 break;
6881 }
6882 if col < pair[0] {
6883 open_idx = Some(pair[0]);
6884 close_idx = Some(pair[1]);
6885 break;
6886 }
6887 }
6888 let open = open_idx?;
6889 let close = close_idx?;
6890 if inner {
6892 if close <= open + 1 {
6893 return None;
6894 }
6895 Some(((row, open + 1), (row, close)))
6896 } else {
6897 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
6904 let mut end = after_close;
6906 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
6907 end += 1;
6908 }
6909 Some(((row, open), (row, end)))
6910 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
6911 let mut start = open;
6913 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
6914 start -= 1;
6915 }
6916 Some(((row, start), (row, close + 1)))
6917 } else {
6918 Some(((row, open), (row, close + 1)))
6919 }
6920 }
6921}
6922
6923fn bracket_text_object<H: crate::types::Host>(
6924 ed: &Editor<hjkl_buffer::Buffer, H>,
6925 open: char,
6926 inner: bool,
6927 count: usize,
6928) -> Option<(Pos, Pos, RangeKind)> {
6929 let close = match open {
6930 '(' => ')',
6931 '[' => ']',
6932 '{' => '}',
6933 '<' => '>',
6934 _ => return None,
6935 };
6936 let (row, col) = ed.cursor();
6937 let lines = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6938 let lines = lines.as_slice();
6939 let cursor_char = lines.get(row).and_then(|l| l.chars().nth(col));
6945 let (open_pos, close_pos) = if cursor_char == Some(close) {
6946 let open_pos = if col > 0 {
6947 find_open_bracket(lines, row, col - 1, open, close)
6948 } else if row > 0 {
6949 let pr = row - 1;
6950 let pc = lines[pr].chars().count().saturating_sub(1);
6951 find_open_bracket(lines, pr, pc, open, close)
6952 } else {
6953 None
6954 }?;
6955 (open_pos, (row, col))
6956 } else {
6957 let open_pos = find_open_bracket(lines, row, col, open, close)
6962 .or_else(|| find_next_open(lines, row, col, open))?;
6963 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
6964 (open_pos, close_pos)
6965 };
6966 let (open_pos, close_pos) = {
6970 let (mut op, mut cp) = (open_pos, close_pos);
6971 for _ in 1..count.max(1) {
6972 let outer = if op.1 > 0 {
6973 find_open_bracket(lines, op.0, op.1 - 1, open, close)
6974 } else if op.0 > 0 {
6975 let pr = op.0 - 1;
6976 let pc = lines[pr].chars().count().saturating_sub(1);
6977 find_open_bracket(lines, pr, pc, open, close)
6978 } else {
6979 None
6980 };
6981 let Some(oo) = outer else { break };
6982 let Some(oc) = find_close_bracket(lines, oo.0, oo.1 + 1, open, close) else {
6983 break;
6984 };
6985 op = oo;
6986 cp = oc;
6987 }
6988 (op, cp)
6989 };
6990 if inner {
6992 if close_pos.0 > open_pos.0 + 1 {
6998 let inner_row_start = open_pos.0 + 1;
7000 let inner_row_end = close_pos.0 - 1;
7001 let end_col = lines
7002 .get(inner_row_end)
7003 .map(|l| l.chars().count())
7004 .unwrap_or(0);
7005 return Some((
7006 (inner_row_start, 0),
7007 (inner_row_end, end_col),
7008 RangeKind::Linewise,
7009 ));
7010 }
7011 let inner_start = advance_pos(lines, open_pos);
7012 if inner_start.0 > close_pos.0
7013 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
7014 {
7015 return None;
7016 }
7017 Some((inner_start, close_pos, RangeKind::Exclusive))
7018 } else {
7019 Some((
7020 open_pos,
7021 advance_pos(lines, close_pos),
7022 RangeKind::Exclusive,
7023 ))
7024 }
7025}
7026
7027fn find_open_bracket(
7028 lines: &[String],
7029 row: usize,
7030 col: usize,
7031 open: char,
7032 close: char,
7033) -> Option<(usize, usize)> {
7034 let mut depth: i32 = 0;
7035 let mut r = row;
7036 let mut c = col as isize;
7037 loop {
7038 let cur = &lines[r];
7039 let chars: Vec<char> = cur.chars().collect();
7040 if (c as usize) >= chars.len() {
7044 c = chars.len() as isize - 1;
7045 }
7046 while c >= 0 {
7047 let ch = chars[c as usize];
7048 if ch == close {
7049 depth += 1;
7050 } else if ch == open {
7051 if depth == 0 {
7052 return Some((r, c as usize));
7053 }
7054 depth -= 1;
7055 }
7056 c -= 1;
7057 }
7058 if r == 0 {
7059 return None;
7060 }
7061 r -= 1;
7062 c = lines[r].chars().count() as isize - 1;
7063 }
7064}
7065
7066fn find_close_bracket(
7067 lines: &[String],
7068 row: usize,
7069 start_col: usize,
7070 open: char,
7071 close: char,
7072) -> Option<(usize, usize)> {
7073 let mut depth: i32 = 0;
7074 let mut r = row;
7075 let mut c = start_col;
7076 loop {
7077 let cur = &lines[r];
7078 let chars: Vec<char> = cur.chars().collect();
7079 while c < chars.len() {
7080 let ch = chars[c];
7081 if ch == open {
7082 depth += 1;
7083 } else if ch == close {
7084 if depth == 0 {
7085 return Some((r, c));
7086 }
7087 depth -= 1;
7088 }
7089 c += 1;
7090 }
7091 if r + 1 >= lines.len() {
7092 return None;
7093 }
7094 r += 1;
7095 c = 0;
7096 }
7097}
7098
7099fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
7103 let mut r = row;
7104 let mut c = col;
7105 while r < lines.len() {
7106 let chars: Vec<char> = lines[r].chars().collect();
7107 while c < chars.len() {
7108 if chars[c] == open {
7109 return Some((r, c));
7110 }
7111 c += 1;
7112 }
7113 r += 1;
7114 c = 0;
7115 }
7116 None
7117}
7118
7119fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
7120 let (r, c) = pos;
7121 let line_len = lines[r].chars().count();
7122 if c < line_len {
7123 (r, c + 1)
7124 } else if r + 1 < lines.len() {
7125 (r + 1, 0)
7126 } else {
7127 pos
7128 }
7129}
7130
7131fn paragraph_text_object<H: crate::types::Host>(
7132 ed: &Editor<hjkl_buffer::Buffer, H>,
7133 inner: bool,
7134) -> Option<((usize, usize), (usize, usize))> {
7135 let (row, _) = ed.cursor();
7136 let rope = crate::types::Query::rope(&ed.buffer);
7137 let n_lines = rope.len_lines();
7138 if n_lines == 0 {
7139 return None;
7140 }
7141 let is_blank = |r: usize| -> bool {
7143 if r >= n_lines {
7144 return true;
7145 }
7146 rope_line_to_str(&rope, r).trim().is_empty()
7147 };
7148 if is_blank(row) {
7149 return None;
7150 }
7151 let mut top = row;
7152 while top > 0 && !is_blank(top - 1) {
7153 top -= 1;
7154 }
7155 let mut bot = row;
7156 while bot + 1 < n_lines && !is_blank(bot + 1) {
7157 bot += 1;
7158 }
7159 if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
7161 bot += 1;
7162 }
7163 let end_col = rope_line_to_str(&rope, bot).chars().count();
7164 Some(((top, 0), (bot, end_col)))
7165}
7166
7167fn read_vim_range<H: crate::types::Host>(
7173 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7174 start: (usize, usize),
7175 end: (usize, usize),
7176 kind: RangeKind,
7177) -> String {
7178 let (top, bot) = order(start, end);
7179 ed.sync_buffer_content_from_textarea();
7180 let rope = crate::types::Query::rope(&ed.buffer);
7181 let n_lines = rope.len_lines();
7182 match kind {
7183 RangeKind::Linewise => {
7184 let lo = top.0;
7185 let hi = bot.0.min(n_lines.saturating_sub(1));
7186 let mut text = rope_row_range_str(&rope, lo, hi);
7187 text.push('\n');
7188 text
7189 }
7190 RangeKind::Inclusive | RangeKind::Exclusive => {
7191 let inclusive = matches!(kind, RangeKind::Inclusive);
7192 let mut out = String::new();
7194 for row in top.0..=bot.0 {
7195 if row >= n_lines {
7196 break;
7197 }
7198 let line = rope_line_to_str(&rope, row);
7199 let lo = if row == top.0 { top.1 } else { 0 };
7200 let hi_unclamped = if row == bot.0 {
7201 if inclusive { bot.1 + 1 } else { bot.1 }
7202 } else {
7203 line.chars().count() + 1
7204 };
7205 let row_chars: Vec<char> = line.chars().collect();
7206 let hi = hi_unclamped.min(row_chars.len());
7207 if lo < hi {
7208 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
7209 }
7210 if row < bot.0 {
7211 out.push('\n');
7212 }
7213 }
7214 out
7215 }
7216 }
7217}
7218
7219fn cut_vim_range<H: crate::types::Host>(
7228 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7229 start: (usize, usize),
7230 end: (usize, usize),
7231 kind: RangeKind,
7232) -> String {
7233 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
7234 let (top, bot) = order(start, end);
7235 ed.sync_buffer_content_from_textarea();
7236 let (buf_start, buf_end, buf_kind) = match kind {
7237 RangeKind::Linewise => (
7238 Position::new(top.0, 0),
7239 Position::new(bot.0, 0),
7240 BufKind::Line,
7241 ),
7242 RangeKind::Inclusive => {
7243 let line_chars = buf_line_chars(&ed.buffer, bot.0);
7244 let next = if bot.1 < line_chars {
7248 Position::new(bot.0, bot.1 + 1)
7249 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
7250 Position::new(bot.0 + 1, 0)
7251 } else {
7252 Position::new(bot.0, line_chars)
7253 };
7254 (Position::new(top.0, top.1), next, BufKind::Char)
7255 }
7256 RangeKind::Exclusive => (
7257 Position::new(top.0, top.1),
7258 Position::new(bot.0, bot.1),
7259 BufKind::Char,
7260 ),
7261 };
7262 let inverse = ed.mutate_edit(Edit::DeleteRange {
7263 start: buf_start,
7264 end: buf_end,
7265 kind: buf_kind,
7266 });
7267 let text = match inverse {
7268 Edit::InsertStr { text, .. } => text,
7269 _ => String::new(),
7270 };
7271 if !text.is_empty() {
7272 ed.record_yank_to_host(text.clone());
7273 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
7274 }
7275 ed.push_buffer_cursor_to_textarea();
7276 text
7277}
7278
7279fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7285 use hjkl_buffer::{Edit, MotionKind, Position};
7286 ed.sync_buffer_content_from_textarea();
7287 let cursor = buf_cursor_pos(&ed.buffer);
7288 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7289 if cursor.col >= line_chars {
7290 return;
7291 }
7292 let inverse = ed.mutate_edit(Edit::DeleteRange {
7293 start: cursor,
7294 end: Position::new(cursor.row, line_chars),
7295 kind: MotionKind::Char,
7296 });
7297 if let Edit::InsertStr { text, .. } = inverse
7298 && !text.is_empty()
7299 {
7300 ed.record_yank_to_host(text.clone());
7301 ed.vim.yank_linewise = false;
7302 ed.set_yank(text);
7303 }
7304 buf_set_cursor_pos(&mut ed.buffer, cursor);
7305 ed.push_buffer_cursor_to_textarea();
7306}
7307
7308fn do_char_delete<H: crate::types::Host>(
7309 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7310 forward: bool,
7311 count: usize,
7312) {
7313 use hjkl_buffer::{Edit, MotionKind, Position};
7314 ed.push_undo();
7315 ed.sync_buffer_content_from_textarea();
7316 let mut deleted = String::new();
7319 for _ in 0..count {
7320 let cursor = buf_cursor_pos(&ed.buffer);
7321 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7322 if forward {
7323 if cursor.col >= line_chars {
7326 continue;
7327 }
7328 let inverse = ed.mutate_edit(Edit::DeleteRange {
7329 start: cursor,
7330 end: Position::new(cursor.row, cursor.col + 1),
7331 kind: MotionKind::Char,
7332 });
7333 if let Edit::InsertStr { text, .. } = inverse {
7334 deleted.push_str(&text);
7335 }
7336 } else {
7337 if cursor.col == 0 {
7339 continue;
7340 }
7341 let inverse = ed.mutate_edit(Edit::DeleteRange {
7342 start: Position::new(cursor.row, cursor.col - 1),
7343 end: cursor,
7344 kind: MotionKind::Char,
7345 });
7346 if let Edit::InsertStr { text, .. } = inverse {
7347 deleted = text + &deleted;
7350 }
7351 }
7352 }
7353 if !deleted.is_empty() {
7354 ed.record_yank_to_host(deleted.clone());
7355 ed.record_delete(deleted, false);
7356 }
7357 ed.push_buffer_cursor_to_textarea();
7358}
7359
7360pub(crate) fn adjust_number<H: crate::types::Host>(
7364 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7365 delta: i64,
7366) -> bool {
7367 use hjkl_buffer::{Edit, MotionKind, Position};
7368 ed.sync_buffer_content_from_textarea();
7369 let cursor = buf_cursor_pos(&ed.buffer);
7370 let row = cursor.row;
7371 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
7372 Some(l) => l.chars().collect(),
7373 None => return false,
7374 };
7375 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
7376 return false;
7377 };
7378 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
7379 digit_start - 1
7380 } else {
7381 digit_start
7382 };
7383 let mut span_end = digit_start;
7384 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
7385 span_end += 1;
7386 }
7387 let s: String = chars[span_start..span_end].iter().collect();
7388 let Ok(n) = s.parse::<i64>() else {
7389 return false;
7390 };
7391 let new_s = n.saturating_add(delta).to_string();
7392
7393 ed.push_undo();
7394 let span_start_pos = Position::new(row, span_start);
7395 let span_end_pos = Position::new(row, span_end);
7396 ed.mutate_edit(Edit::DeleteRange {
7397 start: span_start_pos,
7398 end: span_end_pos,
7399 kind: MotionKind::Char,
7400 });
7401 ed.mutate_edit(Edit::InsertStr {
7402 at: span_start_pos,
7403 text: new_s.clone(),
7404 });
7405 let new_len = new_s.chars().count();
7406 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
7407 ed.push_buffer_cursor_to_textarea();
7408 true
7409}
7410
7411pub(crate) fn replace_char<H: crate::types::Host>(
7412 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7413 ch: char,
7414 count: usize,
7415) {
7416 use hjkl_buffer::{Edit, MotionKind, Position};
7417 ed.push_undo();
7418 ed.sync_buffer_content_from_textarea();
7419 for _ in 0..count {
7420 let cursor = buf_cursor_pos(&ed.buffer);
7421 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7422 if cursor.col >= line_chars {
7423 break;
7424 }
7425 ed.mutate_edit(Edit::DeleteRange {
7426 start: cursor,
7427 end: Position::new(cursor.row, cursor.col + 1),
7428 kind: MotionKind::Char,
7429 });
7430 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
7431 }
7432 crate::motions::move_left(&mut ed.buffer, 1);
7434 ed.push_buffer_cursor_to_textarea();
7435}
7436
7437fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7438 use hjkl_buffer::{Edit, MotionKind, Position};
7439 ed.sync_buffer_content_from_textarea();
7440 let cursor = buf_cursor_pos(&ed.buffer);
7441 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
7442 return;
7443 };
7444 let toggled = if c.is_uppercase() {
7445 c.to_lowercase().next().unwrap_or(c)
7446 } else {
7447 c.to_uppercase().next().unwrap_or(c)
7448 };
7449 ed.mutate_edit(Edit::DeleteRange {
7450 start: cursor,
7451 end: Position::new(cursor.row, cursor.col + 1),
7452 kind: MotionKind::Char,
7453 });
7454 ed.mutate_edit(Edit::InsertChar {
7455 at: cursor,
7456 ch: toggled,
7457 });
7458}
7459
7460fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7461 use hjkl_buffer::{Edit, Position};
7462 ed.sync_buffer_content_from_textarea();
7463 let row = buf_cursor_pos(&ed.buffer).row;
7464 if row + 1 >= buf_row_count(&ed.buffer) {
7465 return;
7466 }
7467 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
7468 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
7469 let next_trimmed = next_raw.trim_start();
7470 let cur_chars = cur_line.chars().count();
7471 let next_chars = next_raw.chars().count();
7472 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
7475 " "
7476 } else {
7477 ""
7478 };
7479 let joined = format!("{cur_line}{separator}{next_trimmed}");
7480 ed.mutate_edit(Edit::Replace {
7481 start: Position::new(row, 0),
7482 end: Position::new(row + 1, next_chars),
7483 with: joined,
7484 });
7485 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
7489 ed.push_buffer_cursor_to_textarea();
7490}
7491
7492fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7495 use hjkl_buffer::Edit;
7496 ed.sync_buffer_content_from_textarea();
7497 let row = buf_cursor_pos(&ed.buffer).row;
7498 if row + 1 >= buf_row_count(&ed.buffer) {
7499 return;
7500 }
7501 let join_col = buf_line_chars(&ed.buffer, row);
7502 ed.mutate_edit(Edit::JoinLines {
7503 row,
7504 count: 1,
7505 with_space: false,
7506 });
7507 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
7509 ed.push_buffer_cursor_to_textarea();
7510}
7511
7512fn do_paste<H: crate::types::Host>(
7513 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7514 before: bool,
7515 count: usize,
7516) {
7517 use hjkl_buffer::{Edit, Position};
7518 ed.push_undo();
7519 let selector = ed.vim.pending_register.take();
7524 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
7525 Some(slot) => (slot.text.clone(), slot.linewise),
7526 None => {
7532 let s = &ed.registers().unnamed;
7533 (s.text.clone(), s.linewise)
7534 }
7535 };
7536 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
7540 let original_row_for_linewise_after = if linewise && !before {
7546 Some(buf_cursor_pos(&ed.buffer).row)
7547 } else {
7548 None
7549 };
7550 for _ in 0..count {
7551 ed.sync_buffer_content_from_textarea();
7552 let yank = yank.clone();
7553 if yank.is_empty() {
7554 continue;
7555 }
7556 if linewise {
7557 let text = yank.trim_matches('\n').to_string();
7561 let row = buf_cursor_pos(&ed.buffer).row;
7562 let target_row = if before {
7563 ed.mutate_edit(Edit::InsertStr {
7564 at: Position::new(row, 0),
7565 text: format!("{text}\n"),
7566 });
7567 row
7568 } else {
7569 let line_chars = buf_line_chars(&ed.buffer, row);
7570 ed.mutate_edit(Edit::InsertStr {
7571 at: Position::new(row, line_chars),
7572 text: format!("\n{text}"),
7573 });
7574 row + 1
7575 };
7576 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
7577 crate::motions::move_first_non_blank(&mut ed.buffer);
7578 ed.push_buffer_cursor_to_textarea();
7579 let payload_lines = text.lines().count().max(1);
7581 let bot_row = target_row + payload_lines - 1;
7582 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
7583 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
7584 } else {
7585 let cursor = buf_cursor_pos(&ed.buffer);
7589 let at = if before {
7590 cursor
7591 } else {
7592 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7593 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
7594 };
7595 ed.mutate_edit(Edit::InsertStr {
7596 at,
7597 text: yank.clone(),
7598 });
7599 crate::motions::move_left(&mut ed.buffer, 1);
7602 ed.push_buffer_cursor_to_textarea();
7603 let lo = (at.row, at.col);
7605 let hi = ed.cursor();
7606 paste_mark = Some((lo, hi));
7607 }
7608 }
7609 if let Some((lo, hi)) = paste_mark {
7610 ed.set_mark('[', lo);
7611 ed.set_mark(']', hi);
7612 }
7613 if let Some(orig_row) = original_row_for_linewise_after {
7618 let first_target = orig_row.saturating_add(1);
7619 buf_set_cursor_rc(&mut ed.buffer, first_target, 0);
7620 crate::motions::move_first_non_blank(&mut ed.buffer);
7621 ed.push_buffer_cursor_to_textarea();
7622 }
7623 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
7625}
7626
7627pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7628 if let Some(entry) = ed.undo_stack.pop() {
7629 let (cur_rope, cur_cursor) = ed.snapshot();
7630 ed.redo_stack.push(crate::editor::UndoEntry {
7631 rope: cur_rope,
7632 cursor: cur_cursor,
7633 timestamp: entry.timestamp,
7634 });
7635 ed.restore_rope(entry.rope, entry.cursor);
7636 }
7637 ed.vim.mode = Mode::Normal;
7638 clamp_cursor_to_normal_mode(ed);
7642}
7643
7644pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7645 if let Some(entry) = ed.redo_stack.pop() {
7646 let (cur_rope, cur_cursor) = ed.snapshot();
7647 ed.undo_stack.push(crate::editor::UndoEntry {
7648 rope: cur_rope,
7649 cursor: cur_cursor,
7650 timestamp: entry.timestamp,
7651 });
7652 ed.cap_undo();
7653 ed.restore_rope(entry.rope, entry.cursor);
7654 }
7655 ed.vim.mode = Mode::Normal;
7656}
7657
7658fn replay_insert_and_finish<H: crate::types::Host>(
7665 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7666 text: &str,
7667) {
7668 use hjkl_buffer::{Edit, Position};
7669 let cursor = ed.cursor();
7670 ed.mutate_edit(Edit::InsertStr {
7671 at: Position::new(cursor.0, cursor.1),
7672 text: text.to_string(),
7673 });
7674 if ed.vim.insert_session.take().is_some() {
7675 if ed.cursor().1 > 0 {
7676 crate::motions::move_left(&mut ed.buffer, 1);
7677 ed.push_buffer_cursor_to_textarea();
7678 }
7679 ed.vim.mode = Mode::Normal;
7680 }
7681}
7682
7683pub(crate) fn replay_last_change<H: crate::types::Host>(
7684 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7685 outer_count: usize,
7686) {
7687 let Some(change) = ed.vim.last_change.clone() else {
7688 return;
7689 };
7690 ed.vim.replaying = true;
7691 let scale = if outer_count > 0 { outer_count } else { 1 };
7692 match change {
7693 LastChange::OpMotion {
7694 op,
7695 motion,
7696 count,
7697 inserted,
7698 } => {
7699 let total = count.max(1) * scale;
7700 apply_op_with_motion(ed, op, &motion, total);
7701 if let Some(text) = inserted {
7702 replay_insert_and_finish(ed, &text);
7703 }
7704 }
7705 LastChange::OpTextObj {
7706 op,
7707 obj,
7708 inner,
7709 inserted,
7710 } => {
7711 apply_op_with_text_object(ed, op, obj, inner, 1);
7714 if let Some(text) = inserted {
7715 replay_insert_and_finish(ed, &text);
7716 }
7717 }
7718 LastChange::LineOp {
7719 op,
7720 count,
7721 inserted,
7722 } => {
7723 let total = count.max(1) * scale;
7724 execute_line_op(ed, op, total);
7725 if let Some(text) = inserted {
7726 replay_insert_and_finish(ed, &text);
7727 }
7728 }
7729 LastChange::CharDel { forward, count } => {
7730 do_char_delete(ed, forward, count * scale);
7731 }
7732 LastChange::ReplaceChar { ch, count } => {
7733 replace_char(ed, ch, count * scale);
7734 }
7735 LastChange::ToggleCase { count } => {
7736 for _ in 0..count * scale {
7737 ed.push_undo();
7738 toggle_case_at_cursor(ed);
7739 }
7740 }
7741 LastChange::JoinLine { count } => {
7742 for _ in 0..count * scale {
7743 ed.push_undo();
7744 join_line(ed);
7745 }
7746 }
7747 LastChange::Paste { before, count } => {
7748 do_paste(ed, before, count * scale);
7749 }
7750 LastChange::DeleteToEol { inserted } => {
7751 use hjkl_buffer::{Edit, Position};
7752 ed.push_undo();
7753 delete_to_eol(ed);
7754 if let Some(text) = inserted {
7755 let cursor = ed.cursor();
7756 ed.mutate_edit(Edit::InsertStr {
7757 at: Position::new(cursor.0, cursor.1),
7758 text,
7759 });
7760 }
7761 }
7762 LastChange::OpenLine { above, inserted } => {
7763 use hjkl_buffer::{Edit, Position};
7764 ed.push_undo();
7765 ed.sync_buffer_content_from_textarea();
7766 let row = buf_cursor_pos(&ed.buffer).row;
7767 if above {
7768 ed.mutate_edit(Edit::InsertStr {
7769 at: Position::new(row, 0),
7770 text: "\n".to_string(),
7771 });
7772 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
7773 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
7774 } else {
7775 let line_chars = buf_line_chars(&ed.buffer, row);
7776 ed.mutate_edit(Edit::InsertStr {
7777 at: Position::new(row, line_chars),
7778 text: "\n".to_string(),
7779 });
7780 }
7781 ed.push_buffer_cursor_to_textarea();
7782 let cursor = ed.cursor();
7783 ed.mutate_edit(Edit::InsertStr {
7784 at: Position::new(cursor.0, cursor.1),
7785 text: inserted,
7786 });
7787 }
7788 LastChange::InsertAt {
7789 entry,
7790 inserted,
7791 count,
7792 } => {
7793 use hjkl_buffer::{Edit, Position};
7794 ed.push_undo();
7795 match entry {
7796 InsertEntry::I => {}
7797 InsertEntry::ShiftI => move_first_non_whitespace(ed),
7798 InsertEntry::A => {
7799 crate::motions::move_right_to_end(&mut ed.buffer, 1);
7800 ed.push_buffer_cursor_to_textarea();
7801 }
7802 InsertEntry::ShiftA => {
7803 crate::motions::move_line_end(&mut ed.buffer);
7804 crate::motions::move_right_to_end(&mut ed.buffer, 1);
7805 ed.push_buffer_cursor_to_textarea();
7806 }
7807 }
7808 for _ in 0..count.max(1) {
7809 let cursor = ed.cursor();
7810 ed.mutate_edit(Edit::InsertStr {
7811 at: Position::new(cursor.0, cursor.1),
7812 text: inserted.clone(),
7813 });
7814 }
7815 }
7816 }
7817 ed.vim.replaying = false;
7818}
7819
7820fn extract_inserted(before: &str, after: &str) -> String {
7823 let before_chars: Vec<char> = before.chars().collect();
7824 let after_chars: Vec<char> = after.chars().collect();
7825 if after_chars.len() <= before_chars.len() {
7826 return String::new();
7827 }
7828 let prefix = before_chars
7829 .iter()
7830 .zip(after_chars.iter())
7831 .take_while(|(a, b)| a == b)
7832 .count();
7833 let max_suffix = before_chars.len() - prefix;
7834 let suffix = before_chars
7835 .iter()
7836 .rev()
7837 .zip(after_chars.iter().rev())
7838 .take(max_suffix)
7839 .take_while(|(a, b)| a == b)
7840 .count();
7841 after_chars[prefix..after_chars.len() - suffix]
7842 .iter()
7843 .collect()
7844}
7845
7846#[cfg(test)]
7849mod comment_continuation_tests {
7850 use super::*;
7851 use crate::{DefaultHost, Editor, Options};
7852 use hjkl_buffer::Buffer;
7853
7854 fn make_editor_with_lang(lang: &str, content: &str) -> Editor<Buffer, DefaultHost> {
7855 let buf = Buffer::from_str(content);
7856 let host = DefaultHost::new();
7857 let opts = Options {
7858 filetype: lang.to_string(),
7859 formatoptions: "ro".to_string(),
7860 ..Options::default()
7861 };
7862 Editor::new(buf, host, opts)
7863 }
7864
7865 #[test]
7866 fn detect_rust_doc_comment() {
7867 let result = detect_comment_on_line("rust", "/// foo bar");
7868 assert!(result.is_some());
7869 let (indent, prefix) = result.unwrap();
7870 assert_eq!(indent, "");
7871 assert_eq!(prefix, "/// ");
7872 }
7873
7874 #[test]
7875 fn detect_rust_inner_doc_comment() {
7876 let result = detect_comment_on_line("rust", "//! crate docs");
7877 assert!(result.is_some());
7878 let (_, prefix) = result.unwrap();
7879 assert_eq!(prefix, "//! ");
7880 }
7881
7882 #[test]
7883 fn detect_rust_plain_comment() {
7884 let result = detect_comment_on_line("rust", "// normal comment");
7885 assert!(result.is_some());
7886 let (_, prefix) = result.unwrap();
7887 assert_eq!(prefix, "// ");
7888 }
7889
7890 #[test]
7891 fn detect_indented_comment() {
7892 let result = detect_comment_on_line("rust", " // indented");
7893 assert!(result.is_some());
7894 let (indent, prefix) = result.unwrap();
7895 assert_eq!(indent, " ");
7896 assert_eq!(prefix, "// ");
7897 }
7898
7899 #[test]
7900 fn detect_python_hash() {
7901 let result = detect_comment_on_line("python", "# comment");
7902 assert!(result.is_some());
7903 let (_, prefix) = result.unwrap();
7904 assert_eq!(prefix, "# ");
7905 }
7906
7907 #[test]
7908 fn detect_lua_double_dash() {
7909 let result = detect_comment_on_line("lua", "-- a lua comment");
7910 assert!(result.is_some());
7911 let (_, prefix) = result.unwrap();
7912 assert_eq!(prefix, "-- ");
7913 }
7914
7915 #[test]
7916 fn detect_non_comment_is_none() {
7917 assert!(detect_comment_on_line("rust", "let x = 1;").is_none());
7918 assert!(detect_comment_on_line("python", "x = 1").is_none());
7919 }
7920
7921 #[test]
7922 fn detect_bare_double_slash_still_matches() {
7923 assert!(detect_comment_on_line("rust", "//").is_some());
7925 }
7926
7927 #[test]
7928 fn rust_doc_before_plain() {
7929 let result = detect_comment_on_line("rust", "/// outer doc");
7931 let (_, prefix) = result.unwrap();
7932 assert_eq!(prefix, "/// ", "/// must match before //");
7933 }
7934
7935 #[test]
7936 fn continue_comment_returns_prefix_for_comment_row() {
7937 let ed = make_editor_with_lang("rust", "/// hello\n");
7938 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7939 assert_eq!(cont, Some("/// ".to_string()));
7940 }
7941
7942 #[test]
7943 fn continue_comment_returns_none_for_non_comment() {
7944 let ed = make_editor_with_lang("rust", "let x = 1;\n");
7945 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7946 assert!(cont.is_none());
7947 }
7948
7949 #[test]
7950 fn continue_comment_returns_none_when_filetype_empty() {
7951 let buf = Buffer::from_str("// hello\n");
7952 let host = DefaultHost::new();
7953 let ed = Editor::new(buf, host, Options::default());
7955 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7956 assert!(cont.is_none());
7957 }
7958}
7959
7960#[cfg(test)]
7961mod comment_toggle_tests {
7962 use super::*;
7963 use crate::{DefaultHost, Editor, Options};
7964 use hjkl_buffer::Buffer;
7965
7966 fn make_rust_editor(content: &str) -> Editor<Buffer, DefaultHost> {
7967 let buf = Buffer::from_str(content);
7968 let host = DefaultHost::new();
7969 let opts = Options {
7970 filetype: "rust".to_string(),
7971 ..Options::default()
7972 };
7973 Editor::new(buf, host, opts)
7974 }
7975
7976 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
7977 buf_line(&ed.buffer, row).unwrap_or_default()
7978 }
7979
7980 #[test]
7983 fn gcc_comments_rust_line() {
7984 let mut ed = make_rust_editor("let x = 1;");
7985 ed.toggle_comment_range(0, 0);
7986 assert_eq!(line(&ed, 0), "// let x = 1;");
7987 }
7988
7989 #[test]
7990 fn gcc_uncomments_rust_line() {
7991 let mut ed = make_rust_editor("// let x = 1;");
7992 ed.toggle_comment_range(0, 0);
7993 assert_eq!(line(&ed, 0), "let x = 1;");
7994 }
7995
7996 #[test]
7997 fn gcc_indent_preserving() {
7998 let mut ed = make_rust_editor(" let x = 1;");
8000 ed.toggle_comment_range(0, 0);
8001 assert_eq!(line(&ed, 0), " // let x = 1;");
8002 }
8003
8004 #[test]
8005 fn gcc_indent_preserving_uncomment() {
8006 let mut ed = make_rust_editor(" // let x = 1;");
8007 ed.toggle_comment_range(0, 0);
8008 assert_eq!(line(&ed, 0), " let x = 1;");
8009 }
8010
8011 #[test]
8014 fn toggle_multi_line_all_uncommented() {
8015 let content = "let a = 1;\nlet b = 2;\nlet c = 3;";
8016 let mut ed = make_rust_editor(content);
8017 ed.toggle_comment_range(0, 2);
8018 assert_eq!(line(&ed, 0), "// let a = 1;");
8019 assert_eq!(line(&ed, 1), "// let b = 2;");
8020 assert_eq!(line(&ed, 2), "// let c = 3;");
8021 }
8022
8023 #[test]
8024 fn toggle_multi_line_all_commented() {
8025 let content = "// let a = 1;\n// let b = 2;\n// let c = 3;";
8026 let mut ed = make_rust_editor(content);
8027 ed.toggle_comment_range(0, 2);
8028 assert_eq!(line(&ed, 0), "let a = 1;");
8029 assert_eq!(line(&ed, 1), "let b = 2;");
8030 assert_eq!(line(&ed, 2), "let c = 3;");
8031 }
8032
8033 #[test]
8036 fn toggle_mixed_state_comments_all() {
8037 let content = "let a = 1;\n// let b = 2;\nlet c = 3;\n// let d = 4;\nlet e = 5;";
8039 let mut ed = make_rust_editor(content);
8040 ed.toggle_comment_range(0, 4);
8041 for r in 0..5 {
8042 assert!(
8043 line(&ed, r).trim_start().starts_with("//"),
8044 "row {r} not commented: {:?}",
8045 line(&ed, r)
8046 );
8047 }
8048 }
8049
8050 #[test]
8053 fn blank_lines_not_commented() {
8054 let content = "let a = 1;\n\nlet b = 2;";
8055 let mut ed = make_rust_editor(content);
8056 ed.toggle_comment_range(0, 2);
8057 assert_eq!(line(&ed, 0), "// let a = 1;");
8058 assert_eq!(line(&ed, 1), ""); assert_eq!(line(&ed, 2), "// let b = 2;");
8060 }
8061
8062 #[test]
8065 fn python_comment_toggle() {
8066 let buf = Buffer::from_str("x = 1\ny = 2");
8067 let host = DefaultHost::new();
8068 let opts = Options {
8069 filetype: "python".to_string(),
8070 ..Options::default()
8071 };
8072 let mut ed = Editor::new(buf, host, opts);
8073 ed.toggle_comment_range(0, 1);
8074 assert_eq!(line(&ed, 0), "# x = 1");
8075 assert_eq!(line(&ed, 1), "# y = 2");
8076 ed.toggle_comment_range(0, 1);
8078 assert_eq!(line(&ed, 0), "x = 1");
8079 assert_eq!(line(&ed, 1), "y = 2");
8080 }
8081
8082 #[test]
8085 fn commentstring_override_via_setting() {
8086 let buf = Buffer::from_str("hello world");
8087 let host = DefaultHost::new();
8088 let opts = Options {
8089 filetype: "rust".to_string(),
8090 ..Options::default()
8091 };
8092 let mut ed = Editor::new(buf, host, opts);
8093 ed.settings_mut().commentstring = "# %s".to_string();
8095 ed.toggle_comment_range(0, 0);
8096 assert_eq!(line(&ed, 0), "# hello world");
8097 }
8098
8099 #[test]
8102 fn unknown_lang_no_op() {
8103 let buf = Buffer::from_str("hello");
8104 let host = DefaultHost::new();
8105 let opts = Options::default(); let mut ed = Editor::new(buf, host, opts);
8107 ed.toggle_comment_range(0, 0);
8108 assert_eq!(line(&ed, 0), "hello");
8110 }
8111}
8112
8113#[cfg(test)]
8116mod g_ampersand_tests {
8117 use super::*;
8118 use crate::{DefaultHost, Editor, Options};
8119 use hjkl_buffer::{Buffer, rope_line_str};
8120
8121 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
8122 let buf = Buffer::from_str(content);
8123 let host = DefaultHost::new();
8124 Editor::new(buf, host, Options::default())
8125 }
8126
8127 fn buf_line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
8128 let rope = ed.buffer().rope();
8129 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
8130 }
8131
8132 #[test]
8135 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
8136 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
8137 let cmd = crate::substitute::parse_substitute("/foo/bar/").unwrap();
8139 ed.set_last_substitute(cmd);
8140 apply_after_g(&mut ed, '&', 1);
8142 assert_eq!(buf_line(&ed, 0), "bar");
8143 assert_eq!(buf_line(&ed, 1), "bar bar foo");
8145 assert_eq!(buf_line(&ed, 2), "baz");
8146 }
8147
8148 #[test]
8150 fn g_ampersand_with_g_flag_replaces_all_per_line() {
8151 let mut ed = make_editor("foo foo\nfoo");
8152 let cmd = crate::substitute::parse_substitute("/foo/bar/g").unwrap();
8153 ed.set_last_substitute(cmd);
8154 apply_after_g(&mut ed, '&', 1);
8155 assert_eq!(buf_line(&ed, 0), "bar bar");
8156 assert_eq!(buf_line(&ed, 1), "bar");
8157 }
8158
8159 #[test]
8161 fn g_ampersand_noop_when_no_prior_substitute() {
8162 let mut ed = make_editor("foo\nbar");
8163 apply_after_g(&mut ed, '&', 1);
8165 assert_eq!(buf_line(&ed, 0), "foo");
8166 assert_eq!(buf_line(&ed, 1), "bar");
8167 }
8168}
8169
8170#[cfg(test)]
8173mod sneak_tests {
8174 use super::*;
8175 use crate::{DefaultHost, Editor, Options};
8176 use hjkl_buffer::Buffer;
8177
8178 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
8179 let buf = Buffer::from_str(content);
8180 let host = DefaultHost::new();
8181 Editor::new(buf, host, Options::default())
8182 }
8183
8184 #[test]
8186 fn sneak_forward_jumps_to_two_char_digraph() {
8187 let mut ed = make_editor("foo bar baz qux\n");
8188 ed.jump_cursor(0, 0);
8189 ed.sneak('b', 'a', true, 1);
8190 assert_eq!(ed.cursor(), (0, 4), "cursor should land on 'ba' in 'bar'");
8191 }
8192
8193 #[test]
8195 fn sneak_backward_jumps_to_prior_match() {
8196 let mut ed = make_editor("foo bar baz qux\n");
8197 ed.jump_cursor(0, 12);
8198 ed.sneak('b', 'a', false, 1);
8199 assert_eq!(
8200 ed.cursor(),
8201 (0, 8),
8202 "backward sneak should find 'ba' in 'baz'"
8203 );
8204 }
8205
8206 #[test]
8208 fn sneak_repeat_semicolon_next_match() {
8209 let mut ed = make_editor("foo bar baz qux\n");
8210 ed.jump_cursor(0, 0);
8211 ed.sneak('b', 'a', true, 1);
8213 assert_eq!(ed.cursor(), (0, 4));
8214 execute_motion(&mut ed, Motion::FindRepeat { reverse: false }, 1);
8216 assert_eq!(ed.cursor(), (0, 8), "semicolon should jump to next 'ba'");
8217 }
8218
8219 #[test]
8221 fn sneak_repeat_comma_prev_match() {
8222 let mut ed = make_editor("foo bar baz qux\n");
8223 ed.jump_cursor(0, 0);
8224 ed.sneak('b', 'a', true, 1);
8225 assert_eq!(ed.cursor(), (0, 4));
8226 let pre = ed.cursor();
8228 execute_motion(&mut ed, Motion::FindRepeat { reverse: true }, 1);
8229 assert_eq!(
8230 ed.cursor(),
8231 pre,
8232 "comma with no prior match should leave cursor unchanged"
8233 );
8234 }
8235
8236 #[test]
8238 fn sneak_s_searches_backward() {
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!(ed.cursor(), (0, 8));
8243 }
8244
8245 #[test]
8247 fn sneak_with_count_jumps_to_nth() {
8248 let mut ed = make_editor("foo bar baz qux\n");
8249 ed.jump_cursor(0, 0);
8250 ed.sneak('b', 'a', true, 2);
8251 assert_eq!(ed.cursor(), (0, 8), "count=2 should jump to 2nd 'ba'");
8252 }
8253
8254 #[test]
8256 fn sneak_no_match_cursor_stays() {
8257 let mut ed = make_editor("foo bar baz qux\n");
8258 ed.jump_cursor(0, 0);
8259 let pre = ed.cursor();
8260 ed.sneak('x', 'x', true, 1);
8261 assert_eq!(ed.cursor(), pre, "no match should leave cursor unchanged");
8262 }
8263
8264 #[test]
8266 fn operator_pending_dsab_deletes_to_digraph() {
8267 let mut ed = make_editor("hello ab world\n");
8268 ed.jump_cursor(0, 0);
8269 ed.apply_op_sneak(Operator::Delete, 'a', 'b', true, 1);
8270 let content = ed.content();
8272 assert!(
8273 content.starts_with("ab world"),
8274 "dsab should delete 'hello ' leaving 'ab world'; got: {content:?}"
8275 );
8276 }
8277
8278 #[test]
8280 fn sneak_cross_line_match() {
8281 let mut ed = make_editor("foo\nbar baz\n");
8282 ed.jump_cursor(0, 0);
8283 ed.sneak('b', 'a', true, 1);
8284 assert_eq!(ed.cursor(), (1, 0), "sneak should cross line boundary");
8285 }
8286
8287 #[test]
8289 fn sneak_updates_last_sneak_state() {
8290 let mut ed = make_editor("foo bar baz\n");
8291 ed.jump_cursor(0, 0);
8292 ed.sneak('b', 'a', true, 1);
8293 let ls = ed.last_sneak();
8294 assert_eq!(
8295 ls,
8296 Some((('b', 'a'), true)),
8297 "last_sneak should record the digraph and direction"
8298 );
8299 }
8300}