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 Rot13,
242}
243
244pub(crate) fn rot13_str(s: &str) -> String {
246 s.chars()
247 .map(|c| match c {
248 'a'..='z' => (((c as u8 - b'a' + 13) % 26) + b'a') as char,
249 'A'..='Z' => (((c as u8 - b'A' + 13) % 26) + b'A') as char,
250 _ => c,
251 })
252 .collect()
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
256pub enum Motion {
257 Left,
258 Right,
259 Up,
260 Down,
261 WordFwd,
262 BigWordFwd,
263 WordBack,
264 BigWordBack,
265 WordEnd,
266 BigWordEnd,
267 WordEndBack,
269 BigWordEndBack,
271 LineStart,
272 FirstNonBlank,
273 LineEnd,
274 FileTop,
275 FileBottom,
276 Find {
277 ch: char,
278 forward: bool,
279 till: bool,
280 },
281 FindRepeat {
282 reverse: bool,
283 },
284 MatchBracket,
285 UnmatchedBracket {
289 forward: bool,
290 open: char,
291 },
292 WordAtCursor {
293 forward: bool,
294 whole_word: bool,
297 },
298 SearchNext {
300 reverse: bool,
301 },
302 ViewportTop,
304 ViewportMiddle,
306 ViewportBottom,
308 LastNonBlank,
310 LineMiddle,
313 ParagraphPrev,
315 ParagraphNext,
317 SentencePrev,
319 SentenceNext,
321 ScreenDown,
324 ScreenUp,
326 SectionBackward,
329 SectionForward,
331 SectionEndBackward,
334 SectionEndForward,
336 FirstNonBlankNextLine,
338 FirstNonBlankPrevLine,
340 FirstNonBlankLine,
342 GotoColumn,
345}
346
347#[derive(Debug, Clone, Copy, PartialEq, Eq)]
348pub enum TextObject {
349 Word {
350 big: bool,
351 },
352 Quote(char),
353 Bracket(char),
354 Paragraph,
355 XmlTag,
359 Sentence,
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq)]
368pub enum RangeKind {
369 Exclusive,
371 Inclusive,
373 Linewise,
375}
376
377#[derive(Debug, Clone)]
381pub enum LastChange {
382 OpMotion {
384 op: Operator,
385 motion: Motion,
386 count: usize,
387 inserted: Option<String>,
388 },
389 OpTextObj {
391 op: Operator,
392 obj: TextObject,
393 inner: bool,
394 inserted: Option<String>,
395 },
396 LineOp {
398 op: Operator,
399 count: usize,
400 inserted: Option<String>,
401 },
402 CharDel { forward: bool, count: usize },
404 ReplaceChar { ch: char, count: usize },
406 ToggleCase { count: usize },
408 JoinLine { count: usize },
410 Paste {
412 before: bool,
413 count: usize,
414 cursor_after: bool,
416 reindent: bool,
418 },
419 DeleteToEol { inserted: Option<String> },
421 OpenLine { above: bool, inserted: String },
423 InsertAt {
425 entry: InsertEntry,
426 inserted: String,
427 count: usize,
428 },
429 GnOp {
432 op: Operator,
433 forward: bool,
434 inserted: Option<String>,
435 },
436 ReplaceMode { text: String },
438}
439
440#[derive(Debug, Clone, Copy, PartialEq, Eq)]
441pub enum InsertEntry {
442 I,
443 A,
444 ShiftI,
445 ShiftA,
446}
447
448#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
459pub enum LastHorizontalMotion {
460 #[default]
461 None,
462 FindChar,
463 Sneak,
464}
465
466#[derive(Debug, Clone)]
476pub struct Abbrev {
477 pub lhs: String,
478 pub rhs: String,
479 pub insert: bool,
480 pub cmdline: bool,
481 pub noremap: bool,
482}
483
484#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub enum AbbrevTrigger {
487 NonKeyword(char),
489 CtrlBracket,
491 Cr,
493 Esc,
495}
496
497#[derive(Default)]
498pub struct VimState {
499 pub mode: Mode,
504 pub pending: Pending,
506 pub count: usize,
509 pub last_find: Option<(char, bool, bool)>,
511 pub last_change: Option<LastChange>,
513 pub insert_session: Option<InsertSession>,
515 pub visual_anchor: (usize, usize),
519 pub visual_line_anchor: usize,
521 pub block_anchor: (usize, usize),
524 pub block_vcol: usize,
530 pub yank_linewise: bool,
532 pub pending_register: Option<char>,
535 pub recording_macro: Option<char>,
539 pub recording_keys: Vec<crate::input::Input>,
544 pub replaying_macro: bool,
547 pub last_macro: Option<char>,
549 pub last_edit_pos: Option<(usize, usize)>,
552 pub last_insert_pos: Option<(usize, usize)>,
556 pub change_list: Vec<(usize, usize)>,
560 pub change_list_cursor: Option<usize>,
563 pub last_visual: Option<LastVisual>,
566 pub viewport_pinned: bool,
570 pub replaying: bool,
572 pub one_shot_normal: bool,
575 pub search_prompt: Option<SearchPrompt>,
577 pub last_search: Option<String>,
581 pub last_search_forward: bool,
585 pub last_insert_text: Option<String>,
588 pub jump_back: Vec<(usize, usize)>,
593 pub jump_fwd: Vec<(usize, usize)>,
596 pub insert_pending_register: bool,
600 pub change_mark_start: Option<(usize, usize)>,
606 pub search_history: Vec<String>,
610 pub search_history_cursor: Option<usize>,
615 pub last_input_at: Option<std::time::Instant>,
624 pub last_input_host_at: Option<core::time::Duration>,
628 pub(crate) current_mode: crate::VimMode,
634 pub(crate) view: crate::ViewMode,
640 pub last_substitute: Option<crate::substitute::SubstituteCmd>,
642 pub pending_closes: Vec<(usize, usize, char)>,
650 pub last_sneak: Option<((char, char), bool)>,
653 pub last_horizontal_motion: LastHorizontalMotion,
656 pub abbrevs: Vec<Abbrev>,
659}
660
661pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
662pub(crate) const CHANGE_LIST_MAX: usize = 100;
663
664#[derive(Debug, Clone)]
667pub struct SearchPrompt {
668 pub text: String,
669 pub cursor: usize,
670 pub forward: bool,
671 pub operator: Option<(Operator, usize, (usize, usize))>,
676}
677
678#[derive(Debug, Clone)]
679pub struct InsertSession {
680 pub count: usize,
681 pub row_min: usize,
683 pub row_max: usize,
684 pub before_rope: ropey::Rope,
689 pub reason: InsertReason,
690 pub start_row: usize,
695 pub start_col: usize,
696}
697
698#[derive(Debug, Clone)]
699pub enum InsertReason {
700 Enter(InsertEntry),
702 Open { above: bool },
704 AfterChange,
707 DeleteToEol,
709 ReplayOnly,
712 BlockEdge { top: usize, bot: usize, col: usize },
716 BlockChange { top: usize, bot: usize, col: usize },
721 Replace,
725}
726
727#[derive(Debug, Clone, Copy)]
737pub struct LastVisual {
738 pub mode: Mode,
739 pub anchor: (usize, usize),
740 pub cursor: (usize, usize),
741 pub block_vcol: usize,
742}
743
744impl VimState {
745 pub fn public_mode(&self) -> VimMode {
746 match self.mode {
747 Mode::Normal => VimMode::Normal,
748 Mode::Insert => VimMode::Insert,
749 Mode::Visual => VimMode::Visual,
750 Mode::VisualLine => VimMode::VisualLine,
751 Mode::VisualBlock => VimMode::VisualBlock,
752 }
753 }
754
755 pub fn force_normal(&mut self) {
756 self.mode = Mode::Normal;
757 self.pending = Pending::None;
758 self.count = 0;
759 self.insert_session = None;
760 self.current_mode = crate::VimMode::Normal;
762 }
763
764 pub(crate) fn clear_pending_prefix(&mut self) {
774 self.pending = Pending::None;
775 self.count = 0;
776 self.pending_register = None;
777 self.insert_pending_register = false;
778 }
779
780 pub(crate) fn widen_insert_row(&mut self, row: usize) {
785 if let Some(ref mut session) = self.insert_session {
786 session.row_min = session.row_min.min(row);
787 session.row_max = session.row_max.max(row);
788 }
789 }
790
791 pub fn is_visual(&self) -> bool {
792 matches!(
793 self.mode,
794 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
795 )
796 }
797
798 pub fn is_visual_char(&self) -> bool {
799 self.mode == Mode::Visual
800 }
801
802 pub(crate) fn pending_count_val(&self) -> Option<u32> {
805 if self.count == 0 {
806 None
807 } else {
808 Some(self.count as u32)
809 }
810 }
811
812 pub(crate) fn is_chord_pending(&self) -> bool {
815 !matches!(self.pending, Pending::None)
816 }
817
818 pub(crate) fn pending_op_char(&self) -> Option<char> {
822 let op = match &self.pending {
823 Pending::Op { op, .. }
824 | Pending::OpTextObj { op, .. }
825 | Pending::OpG { op, .. }
826 | Pending::OpFind { op, .. }
827 | Pending::OpSquareBracketOpen { op, .. }
828 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
829 _ => None,
830 };
831 op.map(|o| match o {
832 Operator::Delete => 'd',
833 Operator::Change => 'c',
834 Operator::Yank => 'y',
835 Operator::Uppercase => 'U',
836 Operator::Lowercase => 'u',
837 Operator::ToggleCase => '~',
838 Operator::Indent => '>',
839 Operator::Outdent => '<',
840 Operator::Fold => 'z',
841 Operator::Reflow => 'q',
842 Operator::ReflowKeepCursor => 'w',
843 Operator::AutoIndent => '=',
844 Operator::Filter => '!',
845 Operator::Comment => 'c',
847 Operator::Rot13 => '?',
849 })
850 }
851}
852
853pub(crate) fn enter_search<H: crate::types::Host>(
859 ed: &mut Editor<hjkl_buffer::Buffer, H>,
860 forward: bool,
861) {
862 ed.vim.search_prompt = Some(SearchPrompt {
863 text: String::new(),
864 cursor: 0,
865 forward,
866 operator: None,
867 });
868 ed.vim.search_history_cursor = None;
869 ed.set_search_pattern(None);
873}
874
875pub(crate) fn enter_search_op<H: crate::types::Host>(
879 ed: &mut Editor<hjkl_buffer::Buffer, H>,
880 forward: bool,
881 op: Operator,
882 count: usize,
883) {
884 let origin = ed.cursor();
885 ed.vim.search_prompt = Some(SearchPrompt {
886 text: String::new(),
887 cursor: 0,
888 forward,
889 operator: Some((op, count.max(1), origin)),
890 });
891 ed.vim.search_history_cursor = None;
892 ed.set_search_pattern(None);
893}
894
895pub(crate) fn apply_op_search_range<H: crate::types::Host>(
899 ed: &mut Editor<hjkl_buffer::Buffer, H>,
900 op: Operator,
901 origin: (usize, usize),
902) {
903 let target = ed.cursor();
904 run_operator_over_range(ed, op, origin, target, RangeKind::Exclusive);
905}
906
907fn walk_change_list<H: crate::types::Host>(
911 ed: &mut Editor<hjkl_buffer::Buffer, H>,
912 dir: isize,
913 count: usize,
914) {
915 if ed.vim.change_list.is_empty() {
916 return;
917 }
918 let len = ed.vim.change_list.len();
919 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
920 (None, -1) => len as isize - 1,
921 (None, 1) => return, (Some(i), -1) => i as isize - 1,
923 (Some(i), 1) => i as isize + 1,
924 _ => return,
925 };
926 for _ in 1..count {
927 let next = idx + dir;
928 if next < 0 || next >= len as isize {
929 break;
930 }
931 idx = next;
932 }
933 if idx < 0 || idx >= len as isize {
934 return;
935 }
936 let idx = idx as usize;
937 ed.vim.change_list_cursor = Some(idx);
938 let (row, col) = ed.vim.change_list[idx];
939 ed.jump_cursor(row, col);
940}
941
942fn insert_register_text<H: crate::types::Host>(
947 ed: &mut Editor<hjkl_buffer::Buffer, H>,
948 selector: char,
949) {
950 use hjkl_buffer::Edit;
951 let text = match selector {
954 '/' => match &ed.vim.last_search {
955 Some(s) if !s.is_empty() => s.clone(),
956 _ => return,
957 },
958 '.' => match &ed.vim.last_insert_text {
959 Some(s) if !s.is_empty() => s.clone(),
960 _ => return,
961 },
962 _ => match ed.registers().read(selector) {
963 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
964 _ => return,
965 },
966 };
967 ed.sync_buffer_content_from_textarea();
968 let cursor = buf_cursor_pos(&ed.buffer);
969 ed.mutate_edit(Edit::InsertStr {
970 at: cursor,
971 text: text.clone(),
972 });
973 let mut row = cursor.row;
976 let mut col = cursor.col;
977 for ch in text.chars() {
978 if ch == '\n' {
979 row += 1;
980 col = 0;
981 } else {
982 col += 1;
983 }
984 }
985 buf_set_cursor_rc(&mut ed.buffer, row, col);
986 ed.push_buffer_cursor_to_textarea();
987 ed.mark_content_dirty();
988 if let Some(ref mut session) = ed.vim.insert_session {
989 session.row_min = session.row_min.min(row);
990 session.row_max = session.row_max.max(row);
991 }
992}
993
994pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1013 if !settings.autoindent {
1014 return String::new();
1015 }
1016 let base: String = prev_line
1018 .chars()
1019 .take_while(|c| *c == ' ' || *c == '\t')
1020 .collect();
1021
1022 if settings.smartindent {
1023 let unit = if settings.expandtab {
1024 if settings.softtabstop > 0 {
1025 " ".repeat(settings.softtabstop)
1026 } else {
1027 " ".repeat(settings.shiftwidth)
1028 }
1029 } else {
1030 "\t".to_string()
1031 };
1032
1033 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1035 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1036 return format!("{base}{unit}");
1037 }
1038
1039 if is_html_filetype(&settings.filetype) {
1044 let trimmed_end_len = prev_line
1045 .trim_end_matches(|c: char| c.is_whitespace())
1046 .len();
1047 let trimmed = &prev_line[..trimmed_end_len];
1048 if let Some(stripped) = trimmed.strip_suffix('>')
1049 && scan_tag_opener(trimmed, stripped.len()).is_some()
1050 {
1051 return format!("{base}{unit}");
1052 }
1053 }
1054 }
1055
1056 base
1057}
1058
1059fn comment_prefixes_for_lang(lang: &str) -> &'static [&'static str] {
1065 match lang {
1066 "rust" => &["/// ", "//! ", "// "],
1067 "c" | "cpp" => &["// "],
1068 "python" | "sh" | "bash" | "zsh" | "fish" | "toml" | "yaml" => &["# "],
1069 "lua" => &["-- "],
1070 "sql" => &["-- "],
1071 "vim" | "viml" => &["\" "],
1072 _ => &[],
1073 }
1074}
1075
1076pub(crate) fn detect_comment_on_line(lang: &str, line: &str) -> Option<(String, &'static str)> {
1082 let indent_end = line
1083 .char_indices()
1084 .find(|(_, c)| *c != ' ' && *c != '\t')
1085 .map(|(i, _)| i)
1086 .unwrap_or(line.len());
1087 let indent = line[..indent_end].to_string();
1088 let rest = &line[indent_end..];
1089 for &prefix in comment_prefixes_for_lang(lang) {
1090 if rest.starts_with(prefix) {
1091 return Some((indent, prefix));
1092 }
1093 let bare = prefix.trim_end_matches(' ');
1096 if rest == bare || rest.starts_with(&format!("{bare} ")) {
1097 return Some((indent, prefix));
1098 }
1099 }
1100 None
1101}
1102
1103pub(crate) fn continue_comment(
1110 buffer: &hjkl_buffer::Buffer,
1111 settings: &crate::editor::Settings,
1112 row: usize,
1113) -> Option<String> {
1114 if settings.filetype.is_empty() {
1115 return None;
1116 }
1117 let line = crate::buf_helpers::buf_line(buffer, row)?;
1118 let (indent, prefix) = detect_comment_on_line(&settings.filetype, &line)?;
1119 Some(format!("{indent}{prefix}"))
1120}
1121
1122fn try_dedent_close_bracket<H: crate::types::Host>(
1132 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1133 cursor: hjkl_buffer::Position,
1134 ch: char,
1135) -> bool {
1136 use hjkl_buffer::{Edit, MotionKind, Position};
1137
1138 if !ed.settings.smartindent {
1139 return false;
1140 }
1141 if !matches!(ch, '}' | ')' | ']') {
1142 return false;
1143 }
1144
1145 let line = match buf_line(&ed.buffer, cursor.row) {
1146 Some(l) => l.to_string(),
1147 None => return false,
1148 };
1149
1150 let before: String = line.chars().take(cursor.col).collect();
1152 if !before.chars().all(|c| c == ' ' || c == '\t') {
1153 return false;
1154 }
1155 if before.is_empty() {
1156 return false;
1158 }
1159
1160 let unit_len: usize = if ed.settings.expandtab {
1162 if ed.settings.softtabstop > 0 {
1163 ed.settings.softtabstop
1164 } else {
1165 ed.settings.shiftwidth
1166 }
1167 } else {
1168 1
1170 };
1171
1172 let strip_len = if ed.settings.expandtab {
1174 let spaces = before.chars().filter(|c| *c == ' ').count();
1176 if spaces < unit_len {
1177 return false;
1178 }
1179 unit_len
1180 } else {
1181 if !before.starts_with('\t') {
1183 return false;
1184 }
1185 1
1186 };
1187
1188 ed.mutate_edit(Edit::DeleteRange {
1190 start: Position::new(cursor.row, 0),
1191 end: Position::new(cursor.row, strip_len),
1192 kind: MotionKind::Char,
1193 });
1194 let new_col = cursor.col.saturating_sub(strip_len);
1199 ed.mutate_edit(Edit::InsertChar {
1200 at: Position::new(cursor.row, new_col),
1201 ch,
1202 });
1203 true
1204}
1205
1206fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1207 let Some(session) = ed.vim.insert_session.take() else {
1208 return;
1209 };
1210 let after_rope = crate::types::Query::rope(&ed.buffer);
1211 let before_n = session.before_rope.len_lines();
1215 let after_n = after_rope.len_lines();
1216 let after_end = session.row_max.min(after_n.saturating_sub(1));
1217 let before_end = session.row_max.min(before_n.saturating_sub(1));
1218 let before = if before_end >= session.row_min && session.row_min < before_n {
1219 rope_row_range_str(&session.before_rope, session.row_min, before_end)
1220 } else {
1221 String::new()
1222 };
1223 let after = if after_end >= session.row_min && session.row_min < after_n {
1224 rope_row_range_str(&after_rope, session.row_min, after_end)
1225 } else {
1226 String::new()
1227 };
1228 let inserted = if matches!(session.reason, InsertReason::Replace) {
1232 changed_run(&before, &after)
1233 } else {
1234 extract_inserted(&before, &after)
1235 };
1236 if !ed.vim.replaying && !inserted.is_empty() {
1238 ed.vim.last_insert_text = Some(inserted.clone());
1239 }
1240 let open_line = matches!(session.reason, InsertReason::Open { .. });
1241 if session.count > 1 && !ed.vim.replaying {
1242 use hjkl_buffer::{Edit, Position};
1243 if open_line {
1244 let (start_row, _) = ed.cursor();
1249 let typed = buf_line(&ed.buffer, start_row).unwrap_or_default();
1250 for at_row in start_row..start_row + (session.count - 1) {
1251 let end = buf_line_chars(&ed.buffer, at_row);
1252 ed.mutate_edit(Edit::InsertStr {
1253 at: Position::new(at_row, end),
1254 text: format!("\n{typed}"),
1255 });
1256 }
1257 } else if !inserted.is_empty() {
1258 for _ in 0..session.count - 1 {
1260 let (row, col) = ed.cursor();
1261 ed.mutate_edit(Edit::InsertStr {
1262 at: Position::new(row, col),
1263 text: inserted.clone(),
1264 });
1265 }
1266 }
1267 }
1268 fn replicate_block_text<H: crate::types::Host>(
1272 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1273 inserted: &str,
1274 top: usize,
1275 bot: usize,
1276 col: usize,
1277 ) {
1278 use hjkl_buffer::{Edit, Position};
1279 for r in (top + 1)..=bot {
1280 let line_len = buf_line_chars(&ed.buffer, r);
1281 if col > line_len {
1282 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1283 ed.mutate_edit(Edit::InsertStr {
1284 at: Position::new(r, line_len),
1285 text: pad,
1286 });
1287 }
1288 ed.mutate_edit(Edit::InsertStr {
1289 at: Position::new(r, col),
1290 text: inserted.to_string(),
1291 });
1292 }
1293 }
1294
1295 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1296 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1299 replicate_block_text(ed, &inserted, top, bot, col);
1300 buf_set_cursor_rc(&mut ed.buffer, top, col);
1301 ed.push_buffer_cursor_to_textarea();
1302 }
1303 return;
1304 }
1305 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1306 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1310 replicate_block_text(ed, &inserted, top, bot, col);
1311 let ins_chars = inserted.chars().count();
1312 let line_len = buf_line_chars(&ed.buffer, top);
1313 let target_col = (col + ins_chars).min(line_len);
1314 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1315 ed.push_buffer_cursor_to_textarea();
1316 }
1317 return;
1318 }
1319 if ed.vim.replaying {
1320 return;
1321 }
1322 match session.reason {
1323 InsertReason::Enter(entry) => {
1324 ed.vim.last_change = Some(LastChange::InsertAt {
1325 entry,
1326 inserted,
1327 count: session.count,
1328 });
1329 }
1330 InsertReason::Open { above } => {
1331 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1332 }
1333 InsertReason::AfterChange => {
1334 if let Some(
1335 LastChange::OpMotion { inserted: ins, .. }
1336 | LastChange::OpTextObj { inserted: ins, .. }
1337 | LastChange::LineOp { inserted: ins, .. }
1338 | LastChange::GnOp { inserted: ins, .. },
1339 ) = ed.vim.last_change.as_mut()
1340 {
1341 *ins = Some(inserted);
1342 }
1343 if let Some(start) = ed.vim.change_mark_start.take() {
1349 let end = ed.cursor();
1350 ed.set_mark('[', start);
1351 ed.set_mark(']', end);
1352 }
1353 }
1354 InsertReason::DeleteToEol => {
1355 ed.vim.last_change = Some(LastChange::DeleteToEol {
1356 inserted: Some(inserted),
1357 });
1358 }
1359 InsertReason::ReplayOnly => {}
1360 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1361 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1362 InsertReason::Replace => {
1363 ed.vim.last_change = Some(LastChange::ReplaceMode { text: inserted });
1366 }
1367 }
1368}
1369
1370pub(crate) fn begin_insert<H: crate::types::Host>(
1371 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1372 count: usize,
1373 reason: InsertReason,
1374) {
1375 if !ed.settings.modifiable {
1377 return;
1378 }
1379 if ed.vim.view == crate::ViewMode::Blame {
1381 ed.vim.view = crate::ViewMode::Normal;
1382 return;
1383 }
1384 let record = !matches!(reason, InsertReason::ReplayOnly);
1385 if record {
1386 ed.push_undo();
1387 }
1388 let reason = if ed.vim.replaying {
1389 InsertReason::ReplayOnly
1390 } else {
1391 reason
1392 };
1393 let (row, col) = ed.cursor();
1394 ed.vim.insert_session = Some(InsertSession {
1395 count,
1396 row_min: row,
1397 row_max: row,
1398 before_rope: crate::types::Query::rope(&ed.buffer),
1399 reason,
1400 start_row: row,
1401 start_col: col,
1402 });
1403 ed.vim.mode = Mode::Insert;
1404 ed.vim.current_mode = crate::VimMode::Insert;
1406 drop_blame_if_left_normal(ed);
1407}
1408
1409pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1424 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1425) {
1426 if !ed.settings.undo_break_on_motion {
1427 return;
1428 }
1429 if ed.vim.replaying {
1430 return;
1431 }
1432 if ed.vim.insert_session.is_none() {
1433 return;
1434 }
1435 ed.push_undo();
1436 let before_rope = crate::types::Query::rope(&ed.buffer);
1437 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1438 if let Some(ref mut session) = ed.vim.insert_session {
1439 session.before_rope = before_rope;
1440 session.row_min = row;
1441 session.row_max = row;
1442 }
1443}
1444
1445fn autopair_close_for(
1477 ch: char,
1478 filetype: &str,
1479 prev_char: Option<char>,
1480 prev2_char: Option<char>,
1481) -> Option<char> {
1482 let is_triple_quote_third =
1488 matches!(ch, '"' | '`' | '\'') && prev_char == Some(ch) && prev2_char == Some(ch);
1489
1490 match ch {
1491 '(' => Some(')'),
1492 '[' => Some(']'),
1493 '{' => Some('}'),
1494 '"' => {
1495 if is_triple_quote_third {
1496 None
1497 } else {
1498 Some('"')
1499 }
1500 }
1501 '`' => {
1502 if is_triple_quote_third {
1503 None
1504 } else {
1505 Some('`')
1506 }
1507 }
1508 '<' => {
1509 if is_html_filetype(filetype) {
1510 Some('>')
1511 } else {
1512 None
1513 }
1514 }
1515 '\'' => {
1516 if is_triple_quote_third {
1517 return None;
1518 }
1519 if prev_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
1522 None
1523 } else {
1524 Some('\'')
1525 }
1526 }
1527 _ => None,
1528 }
1529}
1530
1531fn detect_code_fence_opener(line: &str, cursor_col: usize) -> Option<String> {
1546 if cursor_col != line.chars().count() {
1547 return None;
1548 }
1549 let trimmed = line.trim_start();
1550 let backtick_run = trimmed.chars().take_while(|c| *c == '`').count();
1551 if backtick_run < 3 {
1552 return None;
1553 }
1554 let rest = &trimmed[backtick_run..];
1555 if rest.is_empty() {
1556 return None;
1557 }
1558 let all_lang_chars = rest
1559 .chars()
1560 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-');
1561 if !all_lang_chars {
1562 return None;
1563 }
1564 Some("`".repeat(backtick_run))
1565}
1566
1567fn is_html_filetype(ft: &str) -> bool {
1569 matches!(
1570 ft,
1571 "html" | "xml" | "svg" | "jsx" | "tsx" | "vue" | "svelte"
1572 )
1573}
1574
1575#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1591enum TagKind {
1592 Open,
1593 Close,
1594}
1595
1596#[derive(Debug, Clone, PartialEq, Eq)]
1598struct TagSpan {
1599 kind: TagKind,
1600 name: String,
1601 row: usize,
1603 name_start_col: usize,
1605 name_end_col: usize,
1606}
1607
1608fn detect_tag_at_cursor(line: &str, row: usize, col: usize) -> Option<TagSpan> {
1612 let chars: Vec<char> = line.chars().collect();
1613 let mut lt = None;
1615 let mut i = col.min(chars.len());
1616 while i > 0 {
1617 i -= 1;
1618 let c = chars[i];
1619 if c == '<' {
1620 lt = Some(i);
1621 break;
1622 }
1623 if c == '>' {
1625 return None;
1626 }
1627 }
1628 let lt = lt?;
1629 let (kind, name_start) = if chars.get(lt + 1) == Some(&'/') {
1631 (TagKind::Close, lt + 2)
1632 } else {
1633 (TagKind::Open, lt + 1)
1634 };
1635 let first = chars.get(name_start)?;
1637 if !first.is_ascii_alphabetic() {
1638 return None;
1639 }
1640 let mut name_end = name_start;
1642 while name_end < chars.len()
1643 && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-')
1644 {
1645 name_end += 1;
1646 }
1647 if col < name_start || col > name_end {
1651 return None;
1652 }
1653 let name: String = chars[name_start..name_end].iter().collect();
1654 Some(TagSpan {
1655 kind,
1656 name,
1657 row,
1658 name_start_col: name_start,
1659 name_end_col: name_end,
1660 })
1661}
1662
1663fn find_matching_tag(buffer: &hjkl_buffer::Buffer, anchor: &TagSpan) -> Option<TagSpan> {
1677 let row_count = buffer.row_count();
1678 let scan_forward = anchor.kind == TagKind::Open;
1679 let row_iter: Box<dyn Iterator<Item = usize>> = if scan_forward {
1680 Box::new(anchor.row..row_count)
1681 } else {
1682 Box::new((0..=anchor.row).rev())
1683 };
1684 let push_kind = if scan_forward {
1685 TagKind::Open
1686 } else {
1687 TagKind::Close
1688 };
1689 let mut depth: usize = 1;
1690
1691 for r in row_iter {
1692 let line = buf_line(buffer, r)?;
1693 let chars: Vec<char> = line.chars().collect();
1694 let tags = scan_line_tags(&chars, r);
1695 let tags_iter: Box<dyn Iterator<Item = TagSpan>> = if scan_forward {
1696 Box::new(tags.into_iter())
1697 } else {
1698 Box::new(tags.into_iter().rev())
1699 };
1700 for tag in tags_iter {
1701 if r == anchor.row
1703 && tag.name_start_col == anchor.name_start_col
1704 && tag.kind == anchor.kind
1705 {
1706 continue;
1707 }
1708 if r == anchor.row {
1712 if scan_forward && tag.name_start_col < anchor.name_start_col {
1713 continue;
1714 }
1715 if !scan_forward && tag.name_start_col > anchor.name_start_col {
1716 continue;
1717 }
1718 }
1719 if tag.kind == push_kind {
1720 depth += 1;
1721 } else {
1722 depth -= 1;
1723 if depth == 0 {
1724 return Some(tag);
1725 }
1726 }
1727 }
1728 }
1729 None
1730}
1731
1732fn scan_line_tags(chars: &[char], row: usize) -> Vec<TagSpan> {
1736 let mut out = Vec::new();
1737 let n = chars.len();
1738 let mut i = 0;
1739 while i < n {
1740 if chars[i] != '<' {
1741 i += 1;
1742 continue;
1743 }
1744 if chars[i..].starts_with(&['<', '!', '-', '-']) {
1746 let mut j = i + 4;
1747 while j + 2 < n && !(chars[j] == '-' && chars[j + 1] == '-' && chars[j + 2] == '>') {
1748 j += 1;
1749 }
1750 i = (j + 3).min(n);
1751 continue;
1752 }
1753 let (kind, name_start) = if chars.get(i + 1) == Some(&'/') {
1754 (TagKind::Close, i + 2)
1755 } else {
1756 (TagKind::Open, i + 1)
1757 };
1758 if chars
1760 .get(name_start)
1761 .is_none_or(|c| !c.is_ascii_alphabetic())
1762 {
1763 i += 1;
1764 continue;
1765 }
1766 let mut name_end = name_start;
1767 while name_end < n && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-') {
1768 name_end += 1;
1769 }
1770 let mut k = name_end;
1772 let mut self_closing = false;
1773 while k < n {
1774 if chars[k] == '>' {
1775 if k > name_end && chars[k - 1] == '/' {
1776 self_closing = true;
1777 }
1778 break;
1779 }
1780 k += 1;
1781 }
1782 if k >= n {
1783 break;
1785 }
1786 let name: String = chars[name_start..name_end].iter().collect();
1787 if !(self_closing || kind == TagKind::Open && is_void_element(&name)) {
1789 out.push(TagSpan {
1790 kind,
1791 name,
1792 row,
1793 name_start_col: name_start,
1794 name_end_col: name_end,
1795 });
1796 }
1797 i = k + 1;
1798 }
1799 out
1800}
1801
1802pub(crate) fn sync_paired_tag_on_exit<H: crate::types::Host>(
1807 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1808) {
1809 if !is_html_filetype(&ed.settings.filetype) {
1810 return;
1811 }
1812 let (row, col) = ed.cursor();
1813 let line = match buf_line(&ed.buffer, row) {
1814 Some(l) => l,
1815 None => return,
1816 };
1817 let anchor = match detect_tag_at_cursor(&line, row, col) {
1818 Some(t) => t,
1819 None => return,
1820 };
1821 let partner = match find_matching_tag(&ed.buffer, &anchor) {
1822 Some(t) => t,
1823 None => return,
1824 };
1825 if partner.name == anchor.name {
1826 return;
1827 }
1828 use hjkl_buffer::{Edit, MotionKind, Position};
1830 let start = Position::new(partner.row, partner.name_start_col);
1831 let end = Position::new(partner.row, partner.name_end_col);
1832 ed.mutate_edit(Edit::DeleteRange {
1833 start,
1834 end,
1835 kind: MotionKind::Char,
1836 });
1837 ed.mutate_edit(Edit::InsertStr {
1838 at: start,
1839 text: anchor.name.clone(),
1840 });
1841 buf_set_cursor_rc(&mut ed.buffer, row, col);
1844 ed.push_buffer_cursor_to_textarea();
1845}
1846
1847pub fn matching_tag_pair(
1853 buffer: &hjkl_buffer::Buffer,
1854 row: usize,
1855 col: usize,
1856) -> Option<[(usize, usize, usize); 2]> {
1857 let line = buf_line(buffer, row)?;
1858 let anchor = detect_tag_at_cursor(&line, row, col)?;
1859 let partner = find_matching_tag(buffer, &anchor)?;
1860 Some([
1861 (anchor.row, anchor.name_start_col, anchor.name_end_col),
1862 (partner.row, partner.name_start_col, partner.name_end_col),
1863 ])
1864}
1865
1866fn is_void_element(tag: &str) -> bool {
1868 matches!(
1869 tag.to_ascii_lowercase().as_str(),
1870 "area"
1871 | "base"
1872 | "br"
1873 | "col"
1874 | "embed"
1875 | "hr"
1876 | "img"
1877 | "input"
1878 | "link"
1879 | "meta"
1880 | "param"
1881 | "source"
1882 | "track"
1883 | "wbr"
1884 )
1885}
1886
1887fn scan_tag_opener(line: &str, col: usize) -> Option<String> {
1897 let before = if col > 0 { &line[..col] } else { return None };
1900
1901 let lt_pos = before.rfind('<')?;
1903 let inner = &before[lt_pos + 1..]; if inner.starts_with('!') {
1907 return None;
1908 }
1909 if inner.trim_end().ends_with('/') {
1911 return None;
1912 }
1913
1914 let tag: String = inner
1916 .chars()
1917 .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1918 .collect();
1919 if tag.is_empty() {
1920 return None;
1921 }
1922 if !tag
1924 .chars()
1925 .next()
1926 .map(|c| c.is_ascii_alphabetic())
1927 .unwrap_or(false)
1928 {
1929 return None;
1930 }
1931 if is_void_element(&tag) {
1932 return None;
1933 }
1934 Some(tag)
1935}
1936
1937pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1942 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1943 ch: char,
1944) -> bool {
1945 use hjkl_buffer::{Edit, MotionKind, Position};
1946 ed.sync_buffer_content_from_textarea();
1947 let in_replace = matches!(
1948 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1949 Some(InsertReason::Replace)
1950 );
1951
1952 if !in_replace && !ed.vim.abbrevs.is_empty() {
1959 let iskeyword = ed.settings.iskeyword.clone();
1960 if !is_keyword_char(ch, &iskeyword) {
1961 check_and_apply_abbrev(ed, AbbrevTrigger::NonKeyword(ch));
1963 }
1965 }
1966 let cursor = buf_cursor_pos(&ed.buffer);
1968 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1969
1970 if !in_replace
1979 && !ed.vim.pending_closes.is_empty()
1980 && let Some(&(pr, _pc, pch)) = ed.vim.pending_closes.last()
1981 && ch == pch
1982 && cursor.row == pr
1983 {
1984 let char_at_cursor =
1985 buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col));
1986 if char_at_cursor == Some(ch) {
1987 ed.vim.pending_closes.pop();
1988 let filetype = ed.settings.filetype.clone();
1990 let autoclose_tag = ed.settings.autoclose_tag;
1991 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1992 let new_col = cursor.col + 1;
1994 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1995 if let Some(line) = buf_line(&ed.buffer, cursor.row)
1997 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
1998 {
1999 let close_tag = format!("</{tag}>");
2000 let insert_pos = Position::new(cursor.row, new_col);
2001 ed.mutate_edit(Edit::InsertStr {
2002 at: insert_pos,
2003 text: close_tag,
2004 });
2005 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
2007 }
2008 } else {
2009 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
2010 }
2011 ed.push_buffer_cursor_to_textarea();
2012 return true;
2013 }
2014 }
2015
2016 if in_replace && cursor.col < line_chars {
2017 ed.vim.pending_closes.clear();
2019 ed.mutate_edit(Edit::DeleteRange {
2020 start: cursor,
2021 end: Position::new(cursor.row, cursor.col + 1),
2022 kind: MotionKind::Char,
2023 });
2024 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2025 } else if !try_dedent_close_bracket(ed, cursor, ch) {
2026 let autopair = ed.settings.autopair;
2028 let filetype = ed.settings.filetype.clone();
2029 let autoclose_tag = ed.settings.autoclose_tag;
2030
2031 let (prev_char, prev2_char) = {
2032 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2033 let chars: Vec<char> = line.chars().collect();
2034 let p1 = if cursor.col > 0 {
2035 chars.get(cursor.col - 1).copied()
2036 } else {
2037 None
2038 };
2039 let p2 = if cursor.col > 1 {
2040 chars.get(cursor.col - 2).copied()
2041 } else {
2042 None
2043 };
2044 (p1, p2)
2045 };
2046
2047 if autopair {
2048 if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
2049 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2051 let after = Position::new(cursor.row, cursor.col + 1);
2054 ed.mutate_edit(Edit::InsertChar {
2055 at: after,
2056 ch: close,
2057 });
2058 let between_col = cursor.col + 1;
2061 buf_set_cursor_rc(&mut ed.buffer, cursor.row, between_col);
2062 ed.vim.pending_closes.push((cursor.row, between_col, close));
2067 ed.push_buffer_cursor_to_textarea();
2068 return true;
2069 }
2070
2071 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
2075 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2076 let new_col = cursor.col + 1;
2077 if let Some(line) = buf_line(&ed.buffer, cursor.row)
2080 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
2081 {
2082 let close_tag = format!("</{tag}>");
2083 let insert_pos = Position::new(cursor.row, new_col);
2084 ed.mutate_edit(Edit::InsertStr {
2085 at: insert_pos,
2086 text: close_tag,
2087 });
2088 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
2090 }
2091 ed.push_buffer_cursor_to_textarea();
2092 return true;
2093 }
2094 }
2095
2096 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2101 }
2102 ed.push_buffer_cursor_to_textarea();
2103 true
2104}
2105
2106pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
2112 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2113) -> bool {
2114 use hjkl_buffer::Edit;
2115 ed.sync_buffer_content_from_textarea();
2116
2117 if !ed.vim.abbrevs.is_empty() {
2121 check_and_apply_abbrev(ed, AbbrevTrigger::Cr);
2122 }
2123
2124 let cursor = buf_cursor_pos(&ed.buffer);
2125 let prev_line = buf_line(&ed.buffer, cursor.row)
2126 .unwrap_or_default()
2127 .to_string();
2128
2129 if ed.settings.autopair && !ed.vim.pending_closes.is_empty() {
2133 let prev_char = if cursor.col > 0 {
2136 prev_line.chars().nth(cursor.col - 1)
2137 } else {
2138 None
2139 };
2140 let next_char = prev_line.chars().nth(cursor.col);
2141 let is_open_pair = matches!(
2142 (prev_char, next_char),
2143 (Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
2144 );
2145 if is_open_pair {
2146 ed.vim.pending_closes.clear();
2149 let base_indent: String = prev_line
2151 .chars()
2152 .take_while(|c| *c == ' ' || *c == '\t')
2153 .collect();
2154 let inner_indent = if ed.settings.expandtab {
2155 let unit = if ed.settings.softtabstop > 0 {
2156 ed.settings.softtabstop
2157 } else {
2158 ed.settings.shiftwidth
2159 };
2160 format!("{base_indent}{}", " ".repeat(unit))
2161 } else {
2162 format!("{base_indent}\t")
2163 };
2164 let text = format!("\n{inner_indent}\n{base_indent}");
2167 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2168 let new_row = cursor.row + 1;
2170 let new_col = inner_indent.len();
2171 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
2172 ed.push_buffer_cursor_to_textarea();
2173 return true;
2174 }
2175 }
2176
2177 if ed.settings.autopair
2186 && let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
2187 {
2188 ed.vim.pending_closes.clear();
2189 let base_indent: String = prev_line
2190 .chars()
2191 .take_while(|c| *c == ' ' || *c == '\t')
2192 .collect();
2193 let text = format!("\n{base_indent}\n{base_indent}{fence}");
2194 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2195 let new_row = cursor.row + 1;
2196 let new_col = base_indent.chars().count();
2197 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
2198 ed.push_buffer_cursor_to_textarea();
2199 return true;
2200 }
2201
2202 let comment_cont = if ed.settings.formatoptions.contains('r') {
2204 continue_comment(&ed.buffer, &ed.settings, cursor.row)
2205 } else {
2206 None
2207 };
2208
2209 ed.vim.pending_closes.clear();
2211
2212 let text = if let Some(cont) = comment_cont {
2213 format!("\n{cont}")
2216 } else {
2217 let indent = compute_enter_indent(&ed.settings, &prev_line);
2218 format!("\n{indent}")
2219 };
2220 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2221 ed.push_buffer_cursor_to_textarea();
2222 true
2223}
2224
2225pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
2228 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2229) -> bool {
2230 use hjkl_buffer::Edit;
2231 ed.sync_buffer_content_from_textarea();
2232 let cursor = buf_cursor_pos(&ed.buffer);
2233 if ed.settings.expandtab {
2234 let sts = ed.settings.softtabstop;
2235 let n = if sts > 0 {
2236 sts - (cursor.col % sts)
2237 } else {
2238 ed.settings.tabstop.max(1)
2239 };
2240 ed.mutate_edit(Edit::InsertStr {
2241 at: cursor,
2242 text: " ".repeat(n),
2243 });
2244 } else {
2245 ed.mutate_edit(Edit::InsertChar {
2246 at: cursor,
2247 ch: '\t',
2248 });
2249 }
2250 ed.push_buffer_cursor_to_textarea();
2251 true
2252}
2253
2254pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
2265 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2266) -> bool {
2267 use hjkl_buffer::{Edit, MotionKind, Position};
2268 ed.sync_buffer_content_from_textarea();
2269 let cursor = buf_cursor_pos(&ed.buffer);
2270
2271 if cursor.col > 0 {
2274 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2275 if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings.filetype, &line) {
2276 let full_prefix = format!("{indent}{prefix}");
2277 let line_trimmed = line.trim_end_matches(' ');
2280 let prefix_trimmed = full_prefix.trim_end_matches(' ');
2281 if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
2282 ed.mutate_edit(Edit::DeleteRange {
2284 start: Position::new(cursor.row, 0),
2285 end: cursor,
2286 kind: MotionKind::Char,
2287 });
2288 ed.push_buffer_cursor_to_textarea();
2289 return true;
2290 }
2291 }
2292 }
2293
2294 let sts = ed.settings.softtabstop;
2295 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
2296 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2297 let chars: Vec<char> = line.chars().collect();
2298 let run_start = cursor.col - sts;
2299 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
2300 ed.mutate_edit(Edit::DeleteRange {
2301 start: Position::new(cursor.row, run_start),
2302 end: cursor,
2303 kind: MotionKind::Char,
2304 });
2305 ed.push_buffer_cursor_to_textarea();
2306 return true;
2307 }
2308 }
2309 let result = if cursor.col > 0 {
2310 ed.mutate_edit(Edit::DeleteRange {
2311 start: Position::new(cursor.row, cursor.col - 1),
2312 end: cursor,
2313 kind: MotionKind::Char,
2314 });
2315 true
2316 } else if cursor.row > 0 {
2317 let prev_row = cursor.row - 1;
2318 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2319 ed.mutate_edit(Edit::JoinLines {
2320 row: prev_row,
2321 count: 1,
2322 with_space: false,
2323 });
2324 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2325 true
2326 } else {
2327 false
2328 };
2329 ed.push_buffer_cursor_to_textarea();
2330 result
2331}
2332
2333pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
2336 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2337) -> bool {
2338 use hjkl_buffer::{Edit, MotionKind, Position};
2339 ed.sync_buffer_content_from_textarea();
2340 let cursor = buf_cursor_pos(&ed.buffer);
2341 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2342 let result = if cursor.col < line_chars {
2343 ed.mutate_edit(Edit::DeleteRange {
2344 start: cursor,
2345 end: Position::new(cursor.row, cursor.col + 1),
2346 kind: MotionKind::Char,
2347 });
2348 buf_set_cursor_pos(&mut ed.buffer, cursor);
2349 true
2350 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
2351 ed.mutate_edit(Edit::JoinLines {
2352 row: cursor.row,
2353 count: 1,
2354 with_space: false,
2355 });
2356 buf_set_cursor_pos(&mut ed.buffer, cursor);
2357 true
2358 } else {
2359 false
2360 };
2361 ed.push_buffer_cursor_to_textarea();
2362 result
2363}
2364
2365#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2367pub enum InsertDir {
2368 Left,
2369 Right,
2370 Up,
2371 Down,
2372}
2373
2374pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
2378 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2379 dir: InsertDir,
2380) -> bool {
2381 ed.sync_buffer_content_from_textarea();
2382 ed.vim.pending_closes.clear();
2383 match dir {
2384 InsertDir::Left => {
2385 crate::motions::move_left(&mut ed.buffer, 1);
2386 }
2387 InsertDir::Right => {
2388 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2389 }
2390 InsertDir::Up => {
2391 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2392 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2393 }
2394 InsertDir::Down => {
2395 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2396 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2397 }
2398 }
2399 break_undo_group_in_insert(ed);
2400 ed.push_buffer_cursor_to_textarea();
2401 false
2402}
2403
2404pub(crate) fn insert_home_bridge<H: crate::types::Host>(
2407 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2408) -> bool {
2409 ed.sync_buffer_content_from_textarea();
2410 ed.vim.pending_closes.clear();
2411 crate::motions::move_line_start(&mut ed.buffer);
2412 break_undo_group_in_insert(ed);
2413 ed.push_buffer_cursor_to_textarea();
2414 false
2415}
2416
2417pub(crate) fn insert_end_bridge<H: crate::types::Host>(
2420 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2421) -> bool {
2422 ed.sync_buffer_content_from_textarea();
2423 ed.vim.pending_closes.clear();
2424 crate::motions::move_line_end(&mut ed.buffer);
2425 break_undo_group_in_insert(ed);
2426 ed.push_buffer_cursor_to_textarea();
2427 false
2428}
2429
2430pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
2433 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2434 viewport_h: u16,
2435) -> bool {
2436 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2437 scroll_cursor_rows(ed, -rows);
2438 false
2439}
2440
2441pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
2444 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2445 viewport_h: u16,
2446) -> bool {
2447 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2448 scroll_cursor_rows(ed, rows);
2449 false
2450}
2451
2452pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
2456 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2457) -> bool {
2458 use hjkl_buffer::{Edit, MotionKind};
2459 ed.sync_buffer_content_from_textarea();
2460 let cursor = buf_cursor_pos(&ed.buffer);
2461 if cursor.row == 0 && cursor.col == 0 {
2462 return true;
2463 }
2464 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
2465 let word_start = buf_cursor_pos(&ed.buffer);
2466 if word_start == cursor {
2467 return true;
2468 }
2469 buf_set_cursor_pos(&mut ed.buffer, cursor);
2470 ed.mutate_edit(Edit::DeleteRange {
2471 start: word_start,
2472 end: cursor,
2473 kind: MotionKind::Char,
2474 });
2475 ed.push_buffer_cursor_to_textarea();
2476 true
2477}
2478
2479pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
2482 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2483) -> bool {
2484 use hjkl_buffer::{Edit, MotionKind, Position};
2485 ed.sync_buffer_content_from_textarea();
2486 let cursor = buf_cursor_pos(&ed.buffer);
2487 if cursor.col > 0 {
2488 ed.mutate_edit(Edit::DeleteRange {
2489 start: Position::new(cursor.row, 0),
2490 end: cursor,
2491 kind: MotionKind::Char,
2492 });
2493 ed.push_buffer_cursor_to_textarea();
2494 }
2495 true
2496}
2497
2498pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
2502 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2503) -> bool {
2504 use hjkl_buffer::{Edit, MotionKind, Position};
2505 ed.sync_buffer_content_from_textarea();
2506 let cursor = buf_cursor_pos(&ed.buffer);
2507 if cursor.col > 0 {
2508 ed.mutate_edit(Edit::DeleteRange {
2509 start: Position::new(cursor.row, cursor.col - 1),
2510 end: cursor,
2511 kind: MotionKind::Char,
2512 });
2513 } else if cursor.row > 0 {
2514 let prev_row = cursor.row - 1;
2515 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2516 ed.mutate_edit(Edit::JoinLines {
2517 row: prev_row,
2518 count: 1,
2519 with_space: false,
2520 });
2521 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2522 }
2523 ed.push_buffer_cursor_to_textarea();
2524 true
2525}
2526
2527pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
2530 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2531) -> bool {
2532 let (row, col) = ed.cursor();
2533 let sw = ed.settings().shiftwidth;
2534 indent_rows(ed, row, row, 1);
2535 ed.jump_cursor(row, col + sw);
2536 true
2537}
2538
2539pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
2542 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2543) -> bool {
2544 let (row, col) = ed.cursor();
2545 let before_len = buf_line_bytes(&ed.buffer, row);
2546 outdent_rows(ed, row, row, 1);
2547 let after_len = buf_line_bytes(&ed.buffer, row);
2548 let stripped = before_len.saturating_sub(after_len);
2549 let new_col = col.saturating_sub(stripped);
2550 ed.jump_cursor(row, new_col);
2551 true
2552}
2553
2554pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
2558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2559) -> bool {
2560 ed.vim.one_shot_normal = true;
2561 ed.vim.mode = Mode::Normal;
2562 ed.vim.current_mode = crate::VimMode::Normal;
2564 false
2565}
2566
2567pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
2571 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2572) -> bool {
2573 ed.vim.insert_pending_register = true;
2574 false
2575}
2576
2577pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
2581 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2582 reg: char,
2583) -> bool {
2584 insert_register_text(ed, reg);
2585 true
2588}
2589
2590pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
2596 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2597) -> bool {
2598 ed.vim.pending_closes.clear();
2599
2600 if !ed.vim.abbrevs.is_empty() {
2603 check_and_apply_abbrev(ed, AbbrevTrigger::Esc);
2604 }
2605
2606 finish_insert_session(ed);
2607 sync_paired_tag_on_exit(ed);
2611 ed.vim.mode = Mode::Normal;
2612 ed.vim.current_mode = crate::VimMode::Normal;
2614 let col = ed.cursor().1;
2615 ed.vim.last_insert_pos = Some(ed.cursor());
2616 if col > 0 {
2617 crate::motions::move_left(&mut ed.buffer, 1);
2618 ed.push_buffer_cursor_to_textarea();
2619 }
2620 ed.sticky_col = Some(ed.cursor().1);
2621 true
2622}
2623
2624#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2629pub enum ScrollDir {
2630 Down,
2632 Up,
2634}
2635
2636pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
2641 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2642 count: usize,
2643) {
2644 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2645}
2646
2647pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
2649 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2650 count: usize,
2651) {
2652 move_first_non_whitespace(ed);
2653 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2654}
2655
2656pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
2658 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2659 count: usize,
2660) {
2661 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2662 ed.push_buffer_cursor_to_textarea();
2663 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2664}
2665
2666pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
2668 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2669 count: usize,
2670) {
2671 crate::motions::move_line_end(&mut ed.buffer);
2672 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2673 ed.push_buffer_cursor_to_textarea();
2674 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2675}
2676
2677pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
2681 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2682 count: usize,
2683) {
2684 use hjkl_buffer::{Edit, Position};
2685 ed.push_undo();
2686 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2687 ed.sync_buffer_content_from_textarea();
2688 let row = buf_cursor_pos(&ed.buffer).row;
2689 let line_chars = buf_line_chars(&ed.buffer, row);
2690 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
2691
2692 let comment_cont = if ed.settings.formatoptions.contains('o') {
2694 continue_comment(&ed.buffer, &ed.settings, row)
2695 } else {
2696 None
2697 };
2698
2699 let suffix = if let Some(cont) = comment_cont {
2700 format!("\n{cont}")
2701 } else {
2702 let indent = compute_enter_indent(&ed.settings, &prev_line);
2703 format!("\n{indent}")
2704 };
2705 ed.mutate_edit(Edit::InsertStr {
2706 at: Position::new(row, line_chars),
2707 text: suffix,
2708 });
2709 ed.push_buffer_cursor_to_textarea();
2710}
2711
2712pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
2716 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2717 count: usize,
2718) {
2719 use hjkl_buffer::{Edit, Position};
2720 ed.push_undo();
2721 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2722 ed.sync_buffer_content_from_textarea();
2723 let row = buf_cursor_pos(&ed.buffer).row;
2724
2725 let comment_cont = if ed.settings.formatoptions.contains('o') {
2727 continue_comment(&ed.buffer, &ed.settings, row)
2728 } else {
2729 None
2730 };
2731
2732 let (insert_text, new_line_content) = if let Some(cont) = comment_cont {
2735 let content = cont.clone();
2736 (format!("{cont}\n"), content)
2737 } else {
2738 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
2744 let indent = compute_enter_indent(&ed.settings, &cur);
2745 let content = indent.clone();
2746 (format!("{indent}\n"), content)
2747 };
2748 ed.mutate_edit(Edit::InsertStr {
2749 at: Position::new(row, 0),
2750 text: insert_text,
2751 });
2752 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2753 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2754 let new_row = buf_cursor_pos(&ed.buffer).row;
2755 buf_set_cursor_rc(&mut ed.buffer, new_row, new_line_content.chars().count());
2756 ed.push_buffer_cursor_to_textarea();
2757}
2758
2759pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
2761 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2762 count: usize,
2763) {
2764 begin_insert(ed, count.max(1), InsertReason::Replace);
2766}
2767
2768pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
2773 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2774 count: usize,
2775) {
2776 do_char_delete(ed, true, count.max(1));
2777 if !ed.vim.replaying {
2778 ed.vim.last_change = Some(LastChange::CharDel {
2779 forward: true,
2780 count: count.max(1),
2781 });
2782 }
2783}
2784
2785pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
2788 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2789 count: usize,
2790) {
2791 do_char_delete(ed, false, count.max(1));
2792 if !ed.vim.replaying {
2793 ed.vim.last_change = Some(LastChange::CharDel {
2794 forward: false,
2795 count: count.max(1),
2796 });
2797 }
2798}
2799
2800pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
2803 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2804 count: usize,
2805) {
2806 use hjkl_buffer::{Edit, MotionKind, Position};
2807 ed.push_undo();
2808 ed.sync_buffer_content_from_textarea();
2809 for _ in 0..count.max(1) {
2810 let cursor = buf_cursor_pos(&ed.buffer);
2811 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2812 if cursor.col >= line_chars {
2813 break;
2814 }
2815 ed.mutate_edit(Edit::DeleteRange {
2816 start: cursor,
2817 end: Position::new(cursor.row, cursor.col + 1),
2818 kind: MotionKind::Char,
2819 });
2820 }
2821 ed.push_buffer_cursor_to_textarea();
2822 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
2823 if !ed.vim.replaying {
2824 ed.vim.last_change = Some(LastChange::OpMotion {
2825 op: Operator::Change,
2826 motion: Motion::Right,
2827 count: count.max(1),
2828 inserted: None,
2829 });
2830 }
2831}
2832
2833pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
2836 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2837 count: usize,
2838) {
2839 execute_line_op(ed, Operator::Change, count.max(1));
2840 if !ed.vim.replaying {
2841 ed.vim.last_change = Some(LastChange::LineOp {
2842 op: Operator::Change,
2843 count: count.max(1),
2844 inserted: None,
2845 });
2846 }
2847}
2848
2849pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2852 ed.push_undo();
2853 delete_to_eol(ed);
2854 crate::motions::move_left(&mut ed.buffer, 1);
2855 ed.push_buffer_cursor_to_textarea();
2856 if !ed.vim.replaying {
2857 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2858 }
2859}
2860
2861pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2864 ed.push_undo();
2865 delete_to_eol(ed);
2866 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2867}
2868
2869pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
2871 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2872 count: usize,
2873) {
2874 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2875}
2876
2877pub(crate) fn join_line_bridge<H: crate::types::Host>(
2880 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2881 count: usize,
2882) {
2883 let joins = count.max(2) - 1;
2886 for _ in 0..joins {
2887 ed.push_undo();
2888 join_line(ed);
2889 }
2890 if !ed.vim.replaying {
2891 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
2892 }
2893}
2894
2895pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
2898 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2899 count: usize,
2900) {
2901 for _ in 0..count.max(1) {
2902 ed.push_undo();
2903 toggle_case_at_cursor(ed);
2904 }
2905 if !ed.vim.replaying {
2906 ed.vim.last_change = Some(LastChange::ToggleCase {
2907 count: count.max(1),
2908 });
2909 }
2910}
2911
2912pub(crate) fn paste_after_bridge<H: crate::types::Host>(
2916 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2917 count: usize,
2918) {
2919 paste_bridge(ed, false, count, false, false);
2920}
2921
2922pub(crate) fn paste_before_bridge<H: crate::types::Host>(
2926 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2927 count: usize,
2928) {
2929 paste_bridge(ed, true, count, false, false);
2930}
2931
2932pub(crate) fn paste_bridge<H: crate::types::Host>(
2935 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2936 before: bool,
2937 count: usize,
2938 cursor_after: bool,
2939 reindent: bool,
2940) {
2941 do_paste(ed, before, count.max(1), cursor_after, reindent);
2942 if !ed.vim.replaying {
2943 ed.vim.last_change = Some(LastChange::Paste {
2944 before,
2945 count: count.max(1),
2946 cursor_after,
2947 reindent,
2948 });
2949 }
2950}
2951
2952pub(crate) fn jump_back_bridge<H: crate::types::Host>(
2957 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2958 count: usize,
2959) {
2960 for _ in 0..count.max(1) {
2961 jump_back(ed);
2962 }
2963}
2964
2965pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
2968 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2969 count: usize,
2970) {
2971 for _ in 0..count.max(1) {
2972 jump_forward(ed);
2973 }
2974}
2975
2976pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
2981 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2982 dir: ScrollDir,
2983 count: usize,
2984) {
2985 let rows = viewport_full_rows(ed, count) as isize;
2986 match dir {
2987 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2988 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2989 }
2990}
2991
2992pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
2995 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2996 dir: ScrollDir,
2997 count: usize,
2998) {
2999 let rows = viewport_half_rows(ed, count) as isize;
3000 match dir {
3001 ScrollDir::Down => scroll_cursor_rows(ed, rows),
3002 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
3003 }
3004}
3005
3006pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
3010 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3011 dir: ScrollDir,
3012 count: usize,
3013) {
3014 let n = count.max(1);
3015 let total = buf_row_count(&ed.buffer);
3016 let last = total.saturating_sub(1);
3017 let h = ed.viewport_height_value() as usize;
3018 let vp = ed.host().viewport();
3019 let cur_top = vp.top_row;
3020 let new_top = match dir {
3021 ScrollDir::Down => (cur_top + n).min(last),
3022 ScrollDir::Up => cur_top.saturating_sub(n),
3023 };
3024 ed.set_viewport_top(new_top);
3025 let (row, col) = ed.cursor();
3027 let bot = (new_top + h).saturating_sub(1).min(last);
3028 let clamped = row.max(new_top).min(bot);
3029 if clamped != row {
3030 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
3031 ed.push_buffer_cursor_to_textarea();
3032 }
3033}
3034
3035pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
3040 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3041 forward: bool,
3042 count: usize,
3043) {
3044 if let Some(pattern) = ed.vim.last_search.clone() {
3045 ed.push_search_pattern(&pattern);
3046 }
3047 if ed.search_state().pattern.is_none() {
3048 return;
3049 }
3050 let go_forward = ed.vim.last_search_forward == forward;
3051 for _ in 0..count.max(1) {
3052 if go_forward {
3053 ed.search_advance_forward(true);
3054 } else {
3055 ed.search_advance_backward(true);
3056 }
3057 }
3058 ed.push_buffer_cursor_to_textarea();
3059}
3060
3061pub(crate) fn word_search_bridge<H: crate::types::Host>(
3065 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3066 forward: bool,
3067 whole_word: bool,
3068 count: usize,
3069) {
3070 word_at_cursor_search(ed, forward, whole_word, count.max(1));
3071}
3072
3073#[allow(dead_code)]
3078#[inline]
3079pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3080 do_undo(ed);
3081}
3082
3083#[inline]
3100pub(crate) fn drop_blame_if_left_normal<H: crate::types::Host>(
3101 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3102) {
3103 if ed.vim.current_mode != crate::VimMode::Normal {
3104 ed.vim.view = crate::ViewMode::Normal;
3105 }
3106}
3107
3108#[inline]
3112pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
3113 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3114 mode: Mode,
3115) {
3116 ed.vim.mode = mode;
3117 ed.vim.current_mode = ed.vim.public_mode();
3118 drop_blame_if_left_normal(ed);
3119}
3120
3121pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
3124 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3125) {
3126 let cur = ed.cursor();
3127 ed.vim.visual_anchor = cur;
3128 set_vim_mode_bridge(ed, Mode::Visual);
3129}
3130
3131pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
3134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3135) {
3136 let (row, _) = ed.cursor();
3137 ed.vim.visual_line_anchor = row;
3138 set_vim_mode_bridge(ed, Mode::VisualLine);
3139}
3140
3141pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
3145 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3146) {
3147 let cur = ed.cursor();
3148 ed.vim.block_anchor = cur;
3149 ed.vim.block_vcol = cur.1;
3150 set_vim_mode_bridge(ed, Mode::VisualBlock);
3151}
3152
3153pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
3158 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3159) {
3160 let snap: Option<LastVisual> = match ed.vim.mode {
3162 Mode::Visual => Some(LastVisual {
3163 mode: Mode::Visual,
3164 anchor: ed.vim.visual_anchor,
3165 cursor: ed.cursor(),
3166 block_vcol: 0,
3167 }),
3168 Mode::VisualLine => Some(LastVisual {
3169 mode: Mode::VisualLine,
3170 anchor: (ed.vim.visual_line_anchor, 0),
3171 cursor: ed.cursor(),
3172 block_vcol: 0,
3173 }),
3174 Mode::VisualBlock => Some(LastVisual {
3175 mode: Mode::VisualBlock,
3176 anchor: ed.vim.block_anchor,
3177 cursor: ed.cursor(),
3178 block_vcol: ed.vim.block_vcol,
3179 }),
3180 _ => None,
3181 };
3182 ed.vim.pending = Pending::None;
3184 ed.vim.count = 0;
3185 ed.vim.insert_session = None;
3186 set_vim_mode_bridge(ed, Mode::Normal);
3187 if let Some(snap) = snap {
3191 let (lo, hi) = match snap.mode {
3192 Mode::Visual => {
3193 if snap.anchor <= snap.cursor {
3194 (snap.anchor, snap.cursor)
3195 } else {
3196 (snap.cursor, snap.anchor)
3197 }
3198 }
3199 Mode::VisualLine => {
3200 let r_lo = snap.anchor.0.min(snap.cursor.0);
3201 let r_hi = snap.anchor.0.max(snap.cursor.0);
3202 let vl_rope = ed.buffer().rope();
3203 let r_hi_clamped = r_hi.min(vl_rope.len_lines().saturating_sub(1));
3204 let last_col = hjkl_buffer::rope_line_str(&vl_rope, r_hi_clamped)
3205 .chars()
3206 .count()
3207 .saturating_sub(1);
3208 ((r_lo, 0), (r_hi, last_col))
3209 }
3210 Mode::VisualBlock => {
3211 let (r1, c1) = snap.anchor;
3212 let (r2, c2) = snap.cursor;
3213 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
3214 }
3215 _ => {
3216 if snap.anchor <= snap.cursor {
3217 (snap.anchor, snap.cursor)
3218 } else {
3219 (snap.cursor, snap.anchor)
3220 }
3221 }
3222 };
3223 ed.set_mark('<', lo);
3224 ed.set_mark('>', hi);
3225 ed.vim.last_visual = Some(snap);
3226 }
3227}
3228
3229pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
3235 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3236) {
3237 match ed.vim.mode {
3238 Mode::Visual => {
3239 let cur = ed.cursor();
3240 let anchor = ed.vim.visual_anchor;
3241 ed.vim.visual_anchor = cur;
3242 ed.jump_cursor(anchor.0, anchor.1);
3243 }
3244 Mode::VisualLine => {
3245 let cur_row = ed.cursor().0;
3246 let anchor_row = ed.vim.visual_line_anchor;
3247 ed.vim.visual_line_anchor = cur_row;
3248 ed.jump_cursor(anchor_row, 0);
3249 }
3250 Mode::VisualBlock => {
3251 let cur = ed.cursor();
3252 let anchor = ed.vim.block_anchor;
3253 ed.vim.block_anchor = cur;
3254 ed.vim.block_vcol = anchor.1;
3255 ed.jump_cursor(anchor.0, anchor.1);
3256 }
3257 _ => {}
3258 }
3259}
3260
3261pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
3265 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3266) {
3267 if let Some(snap) = ed.vim.last_visual {
3268 match snap.mode {
3269 Mode::Visual => {
3270 ed.vim.visual_anchor = snap.anchor;
3271 set_vim_mode_bridge(ed, Mode::Visual);
3272 }
3273 Mode::VisualLine => {
3274 ed.vim.visual_line_anchor = snap.anchor.0;
3275 set_vim_mode_bridge(ed, Mode::VisualLine);
3276 }
3277 Mode::VisualBlock => {
3278 ed.vim.block_anchor = snap.anchor;
3279 ed.vim.block_vcol = snap.block_vcol;
3280 set_vim_mode_bridge(ed, Mode::VisualBlock);
3281 }
3282 _ => {}
3283 }
3284 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3285 }
3286}
3287
3288pub(crate) fn set_mode_bridge<H: crate::types::Host>(
3294 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3295 mode: crate::VimMode,
3296) {
3297 let internal = match mode {
3298 crate::VimMode::Normal => Mode::Normal,
3299 crate::VimMode::Insert => Mode::Insert,
3300 crate::VimMode::Visual => Mode::Visual,
3301 crate::VimMode::VisualLine => Mode::VisualLine,
3302 crate::VimMode::VisualBlock => Mode::VisualBlock,
3303 };
3304 ed.vim.mode = internal;
3305 ed.vim.current_mode = mode;
3306 drop_blame_if_left_normal(ed);
3307}
3308
3309pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
3326 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3327 ch: char,
3328) {
3329 if ch.is_ascii_lowercase() {
3330 let pos = ed.cursor();
3331 ed.set_mark(ch, pos);
3332 } else if ch.is_ascii_uppercase() {
3333 let pos = ed.cursor();
3334 let bid = ed.current_buffer_id();
3335 ed.set_global_mark(ch, bid, pos);
3336 tracing::debug!(
3337 mark = ch as u32,
3338 buffer_id = bid,
3339 row = pos.0,
3340 col = pos.1,
3341 "global mark set"
3342 );
3343 }
3344 }
3346
3347pub(crate) fn goto_mark<H: crate::types::Host>(
3356 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3357 ch: char,
3358 linewise: bool,
3359) {
3360 let target = match ch {
3361 'a'..='z' => ed.mark(ch),
3362 '\'' | '`' => ed.vim.jump_back.last().copied(),
3363 '.' => ed.vim.last_edit_pos,
3364 '[' | ']' | '<' | '>' => ed.mark(ch),
3365 _ => None,
3366 };
3367 let Some((row, col)) = target else {
3368 return;
3369 };
3370 let pre = ed.cursor();
3371 let (r, c_clamped) = clamp_pos(ed, (row, col));
3372 if linewise {
3373 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3374 ed.push_buffer_cursor_to_textarea();
3375 move_first_non_whitespace(ed);
3376 } else {
3377 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3378 ed.push_buffer_cursor_to_textarea();
3379 }
3380 if ed.cursor() != pre {
3381 ed.push_jump(pre);
3382 }
3383 ed.sticky_col = Some(ed.cursor().1);
3384}
3385
3386pub(crate) fn try_goto_mark<H: crate::types::Host>(
3395 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3396 ch: char,
3397 linewise: bool,
3398) -> crate::editor::MarkJump {
3399 use crate::editor::MarkJump;
3400 match ch {
3401 'A'..='Z' => {
3402 let Some((bid, row, col)) = ed.global_mark(ch) else {
3403 return MarkJump::Unset;
3404 };
3405 if bid != ed.current_buffer_id() {
3406 tracing::debug!(
3407 mark = ch as u32,
3408 buffer_id = bid,
3409 row,
3410 col,
3411 "global mark cross-buffer jump"
3412 );
3413 return MarkJump::CrossBuffer {
3414 buffer_id: bid,
3415 row,
3416 col,
3417 };
3418 }
3419 let pre = ed.cursor();
3421 let (r, c_clamped) = clamp_pos(ed, (row, col));
3422 if linewise {
3423 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3424 ed.push_buffer_cursor_to_textarea();
3425 move_first_non_whitespace(ed);
3426 } else {
3427 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3428 ed.push_buffer_cursor_to_textarea();
3429 }
3430 if ed.cursor() != pre {
3431 ed.push_jump(pre);
3432 }
3433 ed.sticky_col = Some(ed.cursor().1);
3434 MarkJump::SameBuffer
3435 }
3436 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
3437 goto_mark(ed, ch, linewise);
3438 MarkJump::SameBuffer
3439 }
3440 _ => MarkJump::Unset,
3441 }
3442}
3443
3444pub fn op_is_change(op: Operator) -> bool {
3448 matches!(op, Operator::Delete | Operator::Change)
3449}
3450
3451pub(crate) const JUMPLIST_MAX: usize = 100;
3455
3456fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3459 let Some(target) = ed.vim.jump_back.pop() else {
3460 return;
3461 };
3462 let cur = ed.cursor();
3463 ed.vim.jump_fwd.push(cur);
3464 let (r, c) = clamp_pos(ed, target);
3465 ed.jump_cursor(r, c);
3466 ed.sticky_col = Some(c);
3467}
3468
3469fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3472 let Some(target) = ed.vim.jump_fwd.pop() else {
3473 return;
3474 };
3475 let cur = ed.cursor();
3476 ed.vim.jump_back.push(cur);
3477 if ed.vim.jump_back.len() > JUMPLIST_MAX {
3478 ed.vim.jump_back.remove(0);
3479 }
3480 let (r, c) = clamp_pos(ed, target);
3481 ed.jump_cursor(r, c);
3482 ed.sticky_col = Some(c);
3483}
3484
3485fn clamp_pos<H: crate::types::Host>(
3488 ed: &Editor<hjkl_buffer::Buffer, H>,
3489 pos: (usize, usize),
3490) -> (usize, usize) {
3491 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3492 let r = pos.0.min(last_row);
3493 let line_len = buf_line_chars(&ed.buffer, r);
3494 let c = pos.1.min(line_len.saturating_sub(1));
3495 (r, c)
3496}
3497
3498fn is_big_jump(motion: &Motion) -> bool {
3500 matches!(
3501 motion,
3502 Motion::FileTop
3503 | Motion::FileBottom
3504 | Motion::MatchBracket
3505 | Motion::WordAtCursor { .. }
3506 | Motion::SearchNext { .. }
3507 | Motion::ViewportTop
3508 | Motion::ViewportMiddle
3509 | Motion::ViewportBottom
3510 )
3511}
3512
3513fn viewport_half_rows<H: crate::types::Host>(
3518 ed: &Editor<hjkl_buffer::Buffer, H>,
3519 count: usize,
3520) -> usize {
3521 let h = ed.viewport_height_value() as usize;
3522 (h / 2).max(1).saturating_mul(count.max(1))
3523}
3524
3525fn viewport_full_rows<H: crate::types::Host>(
3528 ed: &Editor<hjkl_buffer::Buffer, H>,
3529 count: usize,
3530) -> usize {
3531 let h = ed.viewport_height_value() as usize;
3532 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
3533}
3534
3535fn scroll_cursor_rows<H: crate::types::Host>(
3540 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3541 delta: isize,
3542) {
3543 if delta == 0 {
3544 return;
3545 }
3546 ed.sync_buffer_content_from_textarea();
3547 let (row, _) = ed.cursor();
3548 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3549 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
3550 buf_set_cursor_rc(&mut ed.buffer, target, 0);
3551 crate::motions::move_first_non_blank(&mut ed.buffer);
3552 ed.push_buffer_cursor_to_textarea();
3553 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3554}
3555
3556pub fn parse_motion(input: &Input) -> Option<Motion> {
3562 if input.ctrl {
3563 return None;
3564 }
3565 match input.key {
3566 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
3567 Key::Char('l') | Key::Right => Some(Motion::Right),
3568 Key::Char('j') | Key::Down => Some(Motion::Down),
3569 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
3571 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
3573 Key::Char('_') => Some(Motion::FirstNonBlankLine),
3575 Key::Char('k') | Key::Up => Some(Motion::Up),
3576 Key::Char('w') => Some(Motion::WordFwd),
3577 Key::Char('W') => Some(Motion::BigWordFwd),
3578 Key::Char('b') => Some(Motion::WordBack),
3579 Key::Char('B') => Some(Motion::BigWordBack),
3580 Key::Char('e') => Some(Motion::WordEnd),
3581 Key::Char('E') => Some(Motion::BigWordEnd),
3582 Key::Char('0') | Key::Home => Some(Motion::LineStart),
3583 Key::Char('^') => Some(Motion::FirstNonBlank),
3584 Key::Char('$') | Key::End => Some(Motion::LineEnd),
3585 Key::Char('G') => Some(Motion::FileBottom),
3586 Key::Char('%') => Some(Motion::MatchBracket),
3587 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
3588 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
3589 Key::Char('*') => Some(Motion::WordAtCursor {
3590 forward: true,
3591 whole_word: true,
3592 }),
3593 Key::Char('#') => Some(Motion::WordAtCursor {
3594 forward: false,
3595 whole_word: true,
3596 }),
3597 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
3598 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
3599 Key::Char('H') => Some(Motion::ViewportTop),
3600 Key::Char('M') => Some(Motion::ViewportMiddle),
3601 Key::Char('L') => Some(Motion::ViewportBottom),
3602 Key::Char('{') => Some(Motion::ParagraphPrev),
3603 Key::Char('}') => Some(Motion::ParagraphNext),
3604 Key::Char('(') => Some(Motion::SentencePrev),
3605 Key::Char(')') => Some(Motion::SentenceNext),
3606 Key::Char('|') => Some(Motion::GotoColumn),
3607 _ => None,
3608 }
3609}
3610
3611pub(crate) fn execute_motion<H: crate::types::Host>(
3614 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3615 motion: Motion,
3616 count: usize,
3617) {
3618 let count = count.max(1);
3619 if let Motion::FindRepeat { reverse } = motion
3622 && ed.vim.last_horizontal_motion == LastHorizontalMotion::Sneak
3623 {
3624 if let Some(((c1, c2), fwd)) = ed.vim.last_sneak {
3625 let effective_fwd = if reverse { !fwd } else { fwd };
3626 apply_sneak(ed, c1, c2, effective_fwd, count);
3627 }
3628 return;
3629 }
3630 let motion = match motion {
3632 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3633 Some((ch, forward, till)) => Motion::Find {
3634 ch,
3635 forward: if reverse { !forward } else { forward },
3636 till,
3637 },
3638 None => return,
3639 },
3640 other => other,
3641 };
3642 let pre_pos = ed.cursor();
3643 let pre_col = pre_pos.1;
3644 apply_motion_cursor(ed, &motion, count);
3645 let post_pos = ed.cursor();
3646 if is_big_jump(&motion) && pre_pos != post_pos {
3647 ed.push_jump(pre_pos);
3648 }
3649 apply_sticky_col(ed, &motion, pre_col);
3650 ed.sync_buffer_from_textarea();
3655}
3656
3657fn execute_motion_with_block_vcol<H: crate::types::Host>(
3668 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3669 motion: Motion,
3670 count: usize,
3671) {
3672 let motion_copy = motion.clone();
3673 execute_motion(ed, motion, count);
3674 if ed.vim.mode == Mode::VisualBlock {
3675 update_block_vcol(ed, &motion_copy);
3676 }
3677}
3678
3679pub(crate) fn apply_motion_kind<H: crate::types::Host>(
3707 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3708 kind: crate::MotionKind,
3709 count: usize,
3710) {
3711 let count = count.max(1);
3712 match kind {
3713 crate::MotionKind::CharLeft => {
3714 execute_motion_with_block_vcol(ed, Motion::Left, count);
3715 }
3716 crate::MotionKind::CharRight => {
3717 execute_motion_with_block_vcol(ed, Motion::Right, count);
3718 }
3719 crate::MotionKind::LineDown => {
3720 execute_motion_with_block_vcol(ed, Motion::Down, count);
3721 }
3722 crate::MotionKind::LineUp => {
3723 execute_motion_with_block_vcol(ed, Motion::Up, count);
3724 }
3725 crate::MotionKind::FirstNonBlankDown => {
3726 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3731 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3732 crate::motions::move_first_non_blank(&mut ed.buffer);
3733 ed.push_buffer_cursor_to_textarea();
3734 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3735 ed.sync_buffer_from_textarea();
3736 }
3737 crate::MotionKind::FirstNonBlankUp => {
3738 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3741 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3742 crate::motions::move_first_non_blank(&mut ed.buffer);
3743 ed.push_buffer_cursor_to_textarea();
3744 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3745 ed.sync_buffer_from_textarea();
3746 }
3747 crate::MotionKind::WordForward => {
3748 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
3749 }
3750 crate::MotionKind::BigWordForward => {
3751 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
3752 }
3753 crate::MotionKind::WordBackward => {
3754 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
3755 }
3756 crate::MotionKind::BigWordBackward => {
3757 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
3758 }
3759 crate::MotionKind::WordEnd => {
3760 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
3761 }
3762 crate::MotionKind::BigWordEnd => {
3763 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
3764 }
3765 crate::MotionKind::LineStart => {
3766 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
3769 }
3770 crate::MotionKind::FirstNonBlank => {
3771 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
3774 }
3775 crate::MotionKind::GotoLine => {
3776 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
3785 }
3786 crate::MotionKind::LineEnd => {
3787 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
3791 }
3792 crate::MotionKind::FindRepeat => {
3793 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
3797 }
3798 crate::MotionKind::FindRepeatReverse => {
3799 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
3803 }
3804 crate::MotionKind::BracketMatch => {
3805 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
3810 }
3811 crate::MotionKind::ViewportTop => {
3812 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
3815 }
3816 crate::MotionKind::ViewportMiddle => {
3817 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
3820 }
3821 crate::MotionKind::ViewportBottom => {
3822 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
3825 }
3826 crate::MotionKind::HalfPageDown => {
3827 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
3831 }
3832 crate::MotionKind::HalfPageUp => {
3833 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
3836 }
3837 crate::MotionKind::FullPageDown => {
3838 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
3841 }
3842 crate::MotionKind::FullPageUp => {
3843 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
3846 }
3847 crate::MotionKind::FirstNonBlankLine => {
3848 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
3849 }
3850 crate::MotionKind::SectionBackward => {
3851 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
3852 }
3853 crate::MotionKind::SectionForward => {
3854 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
3855 }
3856 crate::MotionKind::SectionEndBackward => {
3857 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
3858 }
3859 crate::MotionKind::SectionEndForward => {
3860 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
3861 }
3862 }
3863}
3864
3865fn apply_sticky_col<H: crate::types::Host>(
3870 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3871 motion: &Motion,
3872 pre_col: usize,
3873) {
3874 if is_vertical_motion(motion) {
3875 let want = ed.sticky_col.unwrap_or(pre_col);
3876 ed.sticky_col = Some(want);
3879 let (row, _) = ed.cursor();
3880 let line_len = buf_line_chars(&ed.buffer, row);
3881 let max_col = line_len.saturating_sub(1);
3885 let target = want.min(max_col);
3886 buf_set_cursor_rc(&mut ed.buffer, row, target);
3890 } else {
3891 ed.sticky_col = Some(ed.cursor().1);
3894 }
3895}
3896
3897fn is_vertical_motion(motion: &Motion) -> bool {
3898 matches!(
3902 motion,
3903 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
3904 )
3905}
3906
3907fn apply_motion_cursor<H: crate::types::Host>(
3908 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3909 motion: &Motion,
3910 count: usize,
3911) {
3912 apply_motion_cursor_ctx(ed, motion, count, false)
3913}
3914
3915pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
3916 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3917 motion: &Motion,
3918 count: usize,
3919 as_operator: bool,
3920) {
3921 match motion {
3922 Motion::Left => {
3923 crate::motions::move_left(&mut ed.buffer, count);
3925 ed.push_buffer_cursor_to_textarea();
3926 }
3927 Motion::Right => {
3928 if as_operator {
3932 crate::motions::move_right_to_end(&mut ed.buffer, count);
3933 } else {
3934 crate::motions::move_right_in_line(&mut ed.buffer, count);
3935 }
3936 ed.push_buffer_cursor_to_textarea();
3937 }
3938 Motion::Up => {
3939 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3943 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3944 ed.push_buffer_cursor_to_textarea();
3945 }
3946 Motion::Down => {
3947 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3948 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3949 ed.push_buffer_cursor_to_textarea();
3950 }
3951 Motion::ScreenUp => {
3952 let v = *ed.host.viewport();
3953 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3954 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3955 ed.push_buffer_cursor_to_textarea();
3956 }
3957 Motion::ScreenDown => {
3958 let v = *ed.host.viewport();
3959 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3960 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3961 ed.push_buffer_cursor_to_textarea();
3962 }
3963 Motion::WordFwd => {
3964 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3965 ed.push_buffer_cursor_to_textarea();
3966 }
3967 Motion::WordBack => {
3968 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3969 ed.push_buffer_cursor_to_textarea();
3970 }
3971 Motion::WordEnd => {
3972 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3973 ed.push_buffer_cursor_to_textarea();
3974 }
3975 Motion::BigWordFwd => {
3976 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3977 ed.push_buffer_cursor_to_textarea();
3978 }
3979 Motion::BigWordBack => {
3980 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3981 ed.push_buffer_cursor_to_textarea();
3982 }
3983 Motion::BigWordEnd => {
3984 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3985 ed.push_buffer_cursor_to_textarea();
3986 }
3987 Motion::WordEndBack => {
3988 crate::motions::move_word_end_back(
3989 &mut ed.buffer,
3990 false,
3991 count,
3992 &ed.settings.iskeyword,
3993 );
3994 ed.push_buffer_cursor_to_textarea();
3995 }
3996 Motion::BigWordEndBack => {
3997 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3998 ed.push_buffer_cursor_to_textarea();
3999 }
4000 Motion::LineStart => {
4001 crate::motions::move_line_start(&mut ed.buffer);
4002 ed.push_buffer_cursor_to_textarea();
4003 }
4004 Motion::FirstNonBlank => {
4005 crate::motions::move_first_non_blank(&mut ed.buffer);
4006 ed.push_buffer_cursor_to_textarea();
4007 }
4008 Motion::LineEnd => {
4009 crate::motions::move_line_end(&mut ed.buffer);
4011 ed.push_buffer_cursor_to_textarea();
4012 }
4013 Motion::FileTop => {
4014 if count > 1 {
4017 crate::motions::move_bottom(&mut ed.buffer, count);
4018 } else {
4019 crate::motions::move_top(&mut ed.buffer);
4020 }
4021 ed.push_buffer_cursor_to_textarea();
4022 }
4023 Motion::FileBottom => {
4024 if count > 1 {
4027 crate::motions::move_bottom(&mut ed.buffer, count);
4028 } else {
4029 crate::motions::move_bottom(&mut ed.buffer, 0);
4030 }
4031 ed.push_buffer_cursor_to_textarea();
4032 }
4033 Motion::Find { ch, forward, till } => {
4034 for _ in 0..count {
4035 if !find_char_on_line(ed, *ch, *forward, *till) {
4036 break;
4037 }
4038 }
4039 }
4040 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
4042 let _ = matching_bracket(ed);
4043 }
4044 Motion::UnmatchedBracket { forward, open } => {
4045 goto_unmatched_bracket(ed, *forward, *open, count);
4046 }
4047 Motion::WordAtCursor {
4048 forward,
4049 whole_word,
4050 } => {
4051 word_at_cursor_search(ed, *forward, *whole_word, count);
4052 }
4053 Motion::SearchNext { reverse } => {
4054 if let Some(pattern) = ed.vim.last_search.clone() {
4058 ed.push_search_pattern(&pattern);
4059 }
4060 if ed.search_state().pattern.is_none() {
4061 return;
4062 }
4063 let forward = ed.vim.last_search_forward != *reverse;
4067 for _ in 0..count.max(1) {
4068 if forward {
4069 ed.search_advance_forward(true);
4070 } else {
4071 ed.search_advance_backward(true);
4072 }
4073 }
4074 ed.push_buffer_cursor_to_textarea();
4075 }
4076 Motion::ViewportTop => {
4077 let v = *ed.host().viewport();
4078 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
4079 ed.push_buffer_cursor_to_textarea();
4080 }
4081 Motion::ViewportMiddle => {
4082 let v = *ed.host().viewport();
4083 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
4084 ed.push_buffer_cursor_to_textarea();
4085 }
4086 Motion::ViewportBottom => {
4087 let v = *ed.host().viewport();
4088 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
4089 ed.push_buffer_cursor_to_textarea();
4090 }
4091 Motion::LastNonBlank => {
4092 crate::motions::move_last_non_blank(&mut ed.buffer);
4093 ed.push_buffer_cursor_to_textarea();
4094 }
4095 Motion::LineMiddle => {
4096 let row = ed.cursor().0;
4097 let line_chars = buf_line_chars(&ed.buffer, row);
4098 let target = line_chars / 2;
4101 ed.jump_cursor(row, target);
4102 }
4103 Motion::ParagraphPrev => {
4104 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
4105 ed.push_buffer_cursor_to_textarea();
4106 }
4107 Motion::ParagraphNext => {
4108 crate::motions::move_paragraph_next(&mut ed.buffer, count);
4109 ed.push_buffer_cursor_to_textarea();
4110 }
4111 Motion::SentencePrev => {
4112 for _ in 0..count.max(1) {
4113 if let Some((row, col)) = sentence_boundary(ed, false) {
4114 ed.jump_cursor(row, col);
4115 }
4116 }
4117 }
4118 Motion::SentenceNext => {
4119 for _ in 0..count.max(1) {
4120 if let Some((row, col)) = sentence_boundary(ed, true) {
4121 ed.jump_cursor(row, col);
4122 }
4123 }
4124 }
4125 Motion::SectionBackward => {
4126 crate::motions::move_section_backward(&mut ed.buffer, count);
4127 ed.push_buffer_cursor_to_textarea();
4128 }
4129 Motion::SectionForward => {
4130 crate::motions::move_section_forward(&mut ed.buffer, count);
4131 ed.push_buffer_cursor_to_textarea();
4132 }
4133 Motion::SectionEndBackward => {
4134 crate::motions::move_section_end_backward(&mut ed.buffer, count);
4135 ed.push_buffer_cursor_to_textarea();
4136 }
4137 Motion::SectionEndForward => {
4138 crate::motions::move_section_end_forward(&mut ed.buffer, count);
4139 ed.push_buffer_cursor_to_textarea();
4140 }
4141 Motion::FirstNonBlankNextLine => {
4142 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
4143 ed.push_buffer_cursor_to_textarea();
4144 }
4145 Motion::FirstNonBlankPrevLine => {
4146 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
4147 ed.push_buffer_cursor_to_textarea();
4148 }
4149 Motion::FirstNonBlankLine => {
4150 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
4151 ed.push_buffer_cursor_to_textarea();
4152 }
4153 Motion::GotoColumn => {
4154 crate::motions::move_goto_column(&mut ed.buffer, count);
4155 ed.push_buffer_cursor_to_textarea();
4156 }
4157 }
4158}
4159
4160fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4161 ed.sync_buffer_content_from_textarea();
4167 crate::motions::move_first_non_blank(&mut ed.buffer);
4168 ed.push_buffer_cursor_to_textarea();
4169}
4170
4171fn find_char_on_line<H: crate::types::Host>(
4172 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4173 ch: char,
4174 forward: bool,
4175 till: bool,
4176) -> bool {
4177 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
4178 if moved {
4179 ed.push_buffer_cursor_to_textarea();
4180 }
4181 moved
4182}
4183
4184fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
4185 let moved = crate::motions::match_bracket(&mut ed.buffer);
4186 if moved {
4187 ed.push_buffer_cursor_to_textarea();
4188 }
4189 moved
4190}
4191
4192fn goto_unmatched_bracket<H: crate::types::Host>(
4196 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4197 forward: bool,
4198 open: char,
4199 count: usize,
4200) {
4201 let close = match open {
4202 '(' => ')',
4203 '{' => '}',
4204 _ => return,
4205 };
4206 let cursor = buf_cursor_pos(&ed.buffer);
4207 let rows = buf_row_count(&ed.buffer);
4208 let target = count.max(1);
4209 let mut found = 0usize;
4210 let mut depth = 0i32;
4211
4212 if forward {
4213 let mut r = cursor.row;
4214 let mut from_col = cursor.col + 1;
4215 while r < rows {
4216 let line: Vec<char> = buf_line(&ed.buffer, r)
4217 .unwrap_or_default()
4218 .chars()
4219 .collect();
4220 let mut ci = from_col;
4221 while ci < line.len() {
4222 let ch = line[ci];
4223 if ch == open {
4224 depth += 1;
4225 } else if ch == close {
4226 if depth == 0 {
4227 found += 1;
4228 if found == target {
4229 buf_set_cursor_rc(&mut ed.buffer, r, ci);
4230 ed.push_buffer_cursor_to_textarea();
4231 return;
4232 }
4233 } else {
4234 depth -= 1;
4235 }
4236 }
4237 ci += 1;
4238 }
4239 r += 1;
4240 from_col = 0;
4241 }
4242 } else {
4243 let mut r = cursor.row as isize;
4244 let mut from_col = cursor.col as isize - 1;
4247 while r >= 0 {
4248 let line: Vec<char> = buf_line(&ed.buffer, r as usize)
4249 .unwrap_or_default()
4250 .chars()
4251 .collect();
4252 let mut ci = from_col.min(line.len() as isize - 1);
4253 while ci >= 0 {
4254 let ch = line[ci as usize];
4255 if ch == close {
4256 depth += 1;
4257 } else if ch == open {
4258 if depth == 0 {
4259 found += 1;
4260 if found == target {
4261 buf_set_cursor_rc(&mut ed.buffer, r as usize, ci as usize);
4262 ed.push_buffer_cursor_to_textarea();
4263 return;
4264 }
4265 } else {
4266 depth -= 1;
4267 }
4268 }
4269 ci -= 1;
4270 }
4271 r -= 1;
4272 from_col = isize::MAX;
4273 }
4274 }
4275}
4276
4277fn word_at_cursor_search<H: crate::types::Host>(
4278 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4279 forward: bool,
4280 whole_word: bool,
4281 count: usize,
4282) {
4283 let (row, col) = ed.cursor();
4284 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
4285 let chars: Vec<char> = line.chars().collect();
4286 if chars.is_empty() {
4287 return;
4288 }
4289 let spec = ed.settings().iskeyword.clone();
4291 let is_word = |c: char| is_keyword_char(c, &spec);
4292 let mut start = col.min(chars.len().saturating_sub(1));
4293 while start > 0 && is_word(chars[start - 1]) {
4294 start -= 1;
4295 }
4296 let mut end = start;
4297 while end < chars.len() && is_word(chars[end]) {
4298 end += 1;
4299 }
4300 if end <= start {
4301 return;
4302 }
4303 let word: String = chars[start..end].iter().collect();
4304 let escaped = regex_escape(&word);
4305 let pattern = if whole_word {
4306 format!(r"\b{escaped}\b")
4307 } else {
4308 escaped
4309 };
4310 ed.push_search_pattern(&pattern);
4311 if ed.search_state().pattern.is_none() {
4312 return;
4313 }
4314 ed.vim.last_search = Some(pattern);
4316 ed.vim.last_search_forward = forward;
4317 for _ in 0..count.max(1) {
4318 if forward {
4319 ed.search_advance_forward(true);
4320 } else {
4321 ed.search_advance_backward(true);
4322 }
4323 }
4324 ed.push_buffer_cursor_to_textarea();
4325}
4326
4327fn regex_escape(s: &str) -> String {
4328 let mut out = String::with_capacity(s.len());
4329 for c in s.chars() {
4330 if matches!(
4331 c,
4332 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
4333 ) {
4334 out.push('\\');
4335 }
4336 out.push(c);
4337 }
4338 out
4339}
4340
4341pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
4355 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4356 op: Operator,
4357 motion_key: char,
4358 total_count: usize,
4359) {
4360 let input = Input {
4361 key: Key::Char(motion_key),
4362 ctrl: false,
4363 alt: false,
4364 shift: false,
4365 };
4366 let Some(motion) = parse_motion(&input) else {
4367 return;
4368 };
4369 let motion = match motion {
4370 Motion::FindRepeat { reverse } => match ed.vim.last_find {
4371 Some((ch, forward, till)) => Motion::Find {
4372 ch,
4373 forward: if reverse { !forward } else { forward },
4374 till,
4375 },
4376 None => return,
4377 },
4378 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
4380 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
4381 m => m,
4382 };
4383 apply_op_with_motion(ed, op, &motion, total_count);
4384 if let Motion::Find { ch, forward, till } = &motion {
4385 ed.vim.last_find = Some((*ch, *forward, *till));
4386 }
4387 if !ed.vim.replaying && op_is_change(op) {
4388 ed.vim.last_change = Some(LastChange::OpMotion {
4389 op,
4390 motion,
4391 count: total_count,
4392 inserted: None,
4393 });
4394 }
4395}
4396
4397pub(crate) fn apply_op_double<H: crate::types::Host>(
4400 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4401 op: Operator,
4402 total_count: usize,
4403) {
4404 if op == Operator::Comment {
4405 let row = buf_cursor_pos(&ed.buffer).row;
4407 let end_row = (row + total_count.max(1) - 1).min(ed.buffer.row_count().saturating_sub(1));
4408 ed.toggle_comment_range(row, end_row);
4409 ed.vim.mode = Mode::Normal;
4410 if !ed.vim.replaying {
4411 ed.vim.last_change = Some(LastChange::LineOp {
4412 op,
4413 count: total_count,
4414 inserted: None,
4415 });
4416 }
4417 return;
4418 }
4419 execute_line_op(ed, op, total_count);
4420 if !ed.vim.replaying {
4421 ed.vim.last_change = Some(LastChange::LineOp {
4422 op,
4423 count: total_count,
4424 inserted: None,
4425 });
4426 }
4427}
4428
4429fn gn_find_range<H: crate::types::Host>(
4434 ed: &Editor<hjkl_buffer::Buffer, H>,
4435 re: ®ex::Regex,
4436 forward: bool,
4437) -> Option<(crate::types::Pos, crate::types::Pos)> {
4438 use crate::types::{Cursor, Pos, Search};
4439 let cursor = Cursor::cursor(&ed.buffer);
4440 let contains =
4441 Search::find_prev(&ed.buffer, cursor, re).filter(|m| m.start <= cursor && cursor < m.end);
4442 let range = if let Some(m) = contains {
4443 m
4444 } else if forward {
4445 Search::find_next(&ed.buffer, cursor, re)?
4446 } else {
4447 Search::find_prev(&ed.buffer, cursor, re)?
4448 };
4449 let end_incl = if range.end.col > 0 {
4450 Pos::new(range.end.line, range.end.col - 1)
4451 } else {
4452 range.end
4453 };
4454 Some((range.start, end_incl))
4455}
4456
4457pub(crate) fn gn_operate<H: crate::types::Host>(
4462 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4463 op: Option<Operator>,
4464 forward: bool,
4465 count: usize,
4466) {
4467 use crate::types::{Cursor, Pos};
4468 if let Some(p) = ed.vim.last_search.clone() {
4470 ed.push_search_pattern(&p);
4471 }
4472 let Some(re) = ed.search_state().pattern.clone() else {
4473 return;
4474 };
4475 ed.sync_buffer_content_from_textarea();
4476
4477 let Some(mut range) = gn_find_range(ed, &re, forward) else {
4478 return;
4479 };
4480 for _ in 1..count.max(1) {
4482 let past = Pos::new(range.1.line, range.1.col + 1);
4483 Cursor::set_cursor(&mut ed.buffer, past);
4484 match gn_find_range(ed, &re, forward) {
4485 Some(r) => range = r,
4486 None => break,
4487 }
4488 }
4489 let start_t = (range.0.line as usize, range.0.col as usize);
4490 let end_t = (range.1.line as usize, range.1.col as usize);
4491
4492 match op {
4493 None => {
4494 ed.vim.visual_anchor = start_t;
4496 buf_set_cursor_rc(&mut ed.buffer, end_t.0, end_t.1);
4497 ed.vim.mode = Mode::Visual;
4498 ed.vim.current_mode = crate::VimMode::Visual;
4499 ed.push_buffer_cursor_to_textarea();
4500 }
4501 Some(Operator::Delete) => {
4502 ed.push_undo();
4503 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4504 clamp_cursor_to_normal_mode(ed);
4507 ed.push_buffer_cursor_to_textarea();
4508 if !ed.vim.replaying {
4509 ed.vim.last_change = Some(LastChange::GnOp {
4510 op: Operator::Delete,
4511 forward,
4512 inserted: None,
4513 });
4514 }
4515 }
4516 Some(Operator::Change) => {
4517 ed.push_undo();
4518 ed.vim.change_mark_start = Some(start_t);
4519 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4520 if !ed.vim.replaying {
4521 ed.vim.last_change = Some(LastChange::GnOp {
4522 op: Operator::Change,
4523 forward,
4524 inserted: None,
4525 });
4526 }
4527 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4528 }
4529 Some(Operator::Yank) => {
4530 let text = read_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4531 if !text.is_empty() {
4532 ed.record_yank_to_host(text.clone());
4533 ed.record_yank(text, false);
4534 }
4535 buf_set_cursor_rc(&mut ed.buffer, start_t.0, start_t.1);
4536 ed.push_buffer_cursor_to_textarea();
4537 }
4538 Some(other @ (Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase)) => {
4539 ed.push_undo();
4542 apply_case_op_to_selection(ed, other, start_t, end_t, RangeKind::Inclusive);
4543 }
4544 Some(_) => {}
4545 }
4546}
4547
4548pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
4559 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4560 op: Operator,
4561 ch: char,
4562 total_count: usize,
4563) {
4564 if matches!(
4567 op,
4568 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13
4569 ) {
4570 let op_char = match op {
4571 Operator::Uppercase => 'U',
4572 Operator::Lowercase => 'u',
4573 Operator::ToggleCase => '~',
4574 Operator::Rot13 => '?',
4575 _ => unreachable!(),
4576 };
4577 if ch == op_char {
4578 execute_line_op(ed, op, total_count);
4579 if !ed.vim.replaying {
4580 ed.vim.last_change = Some(LastChange::LineOp {
4581 op,
4582 count: total_count,
4583 inserted: None,
4584 });
4585 }
4586 return;
4587 }
4588 }
4589 if ch == 'n' || ch == 'N' {
4591 gn_operate(ed, Some(op), ch == 'n', total_count);
4592 return;
4593 }
4594 let motion = match ch {
4595 'g' => Motion::FileTop,
4596 'e' => Motion::WordEndBack,
4597 'E' => Motion::BigWordEndBack,
4598 'j' => Motion::ScreenDown,
4599 'k' => Motion::ScreenUp,
4600 _ => return, };
4602 apply_op_with_motion(ed, op, &motion, total_count);
4603 if !ed.vim.replaying && op_is_change(op) {
4604 ed.vim.last_change = Some(LastChange::OpMotion {
4605 op,
4606 motion,
4607 count: total_count,
4608 inserted: None,
4609 });
4610 }
4611}
4612
4613pub(crate) fn apply_after_g<H: crate::types::Host>(
4618 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4619 ch: char,
4620 count: usize,
4621) {
4622 match ch {
4623 'g' => {
4624 let pre = ed.cursor();
4626 if count > 1 {
4627 ed.jump_cursor(count - 1, 0);
4628 } else {
4629 ed.jump_cursor(0, 0);
4630 }
4631 move_first_non_whitespace(ed);
4632 ed.sticky_col = Some(ed.cursor().1);
4635 if ed.cursor() != pre {
4636 ed.push_jump(pre);
4637 }
4638 }
4639 'e' => execute_motion(ed, Motion::WordEndBack, count),
4640 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
4641 '_' => execute_motion(ed, Motion::LastNonBlank, count),
4643 'M' => execute_motion(ed, Motion::LineMiddle, count),
4645 'v' => ed.reenter_last_visual(),
4648 'j' => execute_motion(ed, Motion::ScreenDown, count),
4652 'k' => execute_motion(ed, Motion::ScreenUp, count),
4653 'U' => {
4657 ed.vim.pending = Pending::Op {
4658 op: Operator::Uppercase,
4659 count1: count,
4660 };
4661 }
4662 'u' => {
4663 ed.vim.pending = Pending::Op {
4664 op: Operator::Lowercase,
4665 count1: count,
4666 };
4667 }
4668 '~' => {
4669 ed.vim.pending = Pending::Op {
4670 op: Operator::ToggleCase,
4671 count1: count,
4672 };
4673 }
4674 '?' => {
4675 ed.vim.pending = Pending::Op {
4677 op: Operator::Rot13,
4678 count1: count,
4679 };
4680 }
4681 'q' => {
4682 ed.vim.pending = Pending::Op {
4685 op: Operator::Reflow,
4686 count1: count,
4687 };
4688 }
4689 'w' => {
4690 ed.vim.pending = Pending::Op {
4693 op: Operator::ReflowKeepCursor,
4694 count1: count,
4695 };
4696 }
4697 'J' => {
4698 let joins = count.max(2) - 1;
4701 for _ in 0..joins {
4702 ed.push_undo();
4703 join_line_raw(ed);
4704 }
4705 if !ed.vim.replaying {
4706 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
4707 }
4708 }
4709 'd' => {
4710 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
4715 }
4716 'i' => {
4721 if let Some((row, col)) = ed.vim.last_insert_pos {
4722 ed.jump_cursor(row, col);
4723 }
4724 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4725 }
4726 'c' => {
4731 ed.vim.pending = Pending::Op {
4732 op: Operator::Comment,
4733 count1: count,
4734 };
4735 }
4736 'p' => paste_bridge(ed, false, count.max(1), true, false),
4739 'P' => paste_bridge(ed, true, count.max(1), true, false),
4740 'n' => gn_operate(ed, None, true, count.max(1)),
4742 'N' => gn_operate(ed, None, false, count.max(1)),
4743 ';' => walk_change_list(ed, -1, count.max(1)),
4746 ',' => walk_change_list(ed, 1, count.max(1)),
4747 '*' => execute_motion(
4751 ed,
4752 Motion::WordAtCursor {
4753 forward: true,
4754 whole_word: false,
4755 },
4756 count,
4757 ),
4758 '#' => execute_motion(
4759 ed,
4760 Motion::WordAtCursor {
4761 forward: false,
4762 whole_word: false,
4763 },
4764 count,
4765 ),
4766 '&' => {
4769 let cmd = match ed.vim.last_substitute.clone() {
4770 Some(c) => c,
4771 None => {
4772 return;
4777 }
4778 };
4779 let last_row = buf_row_count(&ed.buffer).saturating_sub(1) as u32;
4780 let r = 0u32..=last_row;
4781 let _ = crate::substitute::apply_substitute(ed, &cmd, r);
4784 ed.vim.last_substitute = Some(cmd);
4787 }
4788 _ => {}
4789 }
4790}
4791
4792pub(crate) fn ampersand_repeat<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4796 let Some(mut cmd) = ed.vim.last_substitute.clone() else {
4797 return;
4798 };
4799 cmd.flags = crate::substitute::SubstFlags::default();
4800 let row = buf_cursor_pos(&ed.buffer).row as u32;
4801 let _ = crate::substitute::apply_substitute(ed, &cmd, row..=row);
4802}
4803
4804pub(crate) fn apply_after_z<H: crate::types::Host>(
4809 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4810 ch: char,
4811 count: usize,
4812) {
4813 use crate::editor::CursorScrollTarget;
4814 let row = ed.cursor().0;
4815 match ch {
4816 'z' => {
4817 ed.scroll_cursor_to(CursorScrollTarget::Center);
4818 ed.vim.viewport_pinned = true;
4819 }
4820 't' => {
4821 ed.scroll_cursor_to(CursorScrollTarget::Top);
4822 ed.vim.viewport_pinned = true;
4823 }
4824 'b' => {
4825 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
4826 ed.vim.viewport_pinned = true;
4827 }
4828 'o' => {
4833 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
4834 }
4835 'c' => {
4836 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
4837 }
4838 'a' => {
4839 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
4840 }
4841 'R' => {
4842 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
4843 }
4844 'M' => {
4845 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
4846 }
4847 'E' => {
4848 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
4849 }
4850 'd' => {
4851 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
4852 }
4853 'f' => {
4854 if matches!(
4855 ed.vim.mode,
4856 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
4857 ) {
4858 let anchor_row = match ed.vim.mode {
4861 Mode::VisualLine => ed.vim.visual_line_anchor,
4862 Mode::VisualBlock => ed.vim.block_anchor.0,
4863 _ => ed.vim.visual_anchor.0,
4864 };
4865 let cur = ed.cursor().0;
4866 let top = anchor_row.min(cur);
4867 let bot = anchor_row.max(cur);
4868 ed.apply_fold_op(crate::types::FoldOp::Add {
4869 start_row: top,
4870 end_row: bot,
4871 closed: true,
4872 });
4873 ed.vim.mode = Mode::Normal;
4874 } else {
4875 ed.vim.pending = Pending::Op {
4880 op: Operator::Fold,
4881 count1: count,
4882 };
4883 }
4884 }
4885 _ => {}
4886 }
4887}
4888
4889pub(crate) fn apply_find_char<H: crate::types::Host>(
4895 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4896 ch: char,
4897 forward: bool,
4898 till: bool,
4899 count: usize,
4900) {
4901 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
4902 ed.vim.last_find = Some((ch, forward, till));
4903 ed.vim.last_horizontal_motion = LastHorizontalMotion::FindChar;
4904}
4905
4906pub(crate) fn apply_sneak<H: crate::types::Host>(
4918 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4919 c1: char,
4920 c2: char,
4921 forward: bool,
4922 count: usize,
4923) {
4924 let count = count.max(1);
4925 let (start_row, start_col) = ed.cursor();
4926 let row_count = buf_row_count(&ed.buffer);
4927
4928 let result = if forward {
4929 sneak_scan_forward(ed, start_row, start_col, c1, c2, count)
4930 } else {
4931 sneak_scan_backward(ed, start_row, start_col, c1, c2, count)
4932 };
4933
4934 if let Some((row, col)) = result {
4935 buf_set_cursor_rc(&mut ed.buffer, row, col);
4936 ed.push_buffer_cursor_to_textarea();
4937 let _ = row_count; }
4939
4940 ed.vim.last_sneak = Some(((c1, c2), forward));
4941 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
4942}
4943
4944fn sneak_scan_forward<H: crate::types::Host>(
4947 ed: &Editor<hjkl_buffer::Buffer, H>,
4948 start_row: usize,
4949 start_col: usize,
4950 c1: char,
4951 c2: char,
4952 count: usize,
4953) -> Option<(usize, usize)> {
4954 let row_count = buf_row_count(&ed.buffer);
4955 let mut hits = 0usize;
4956 for row in start_row..row_count {
4957 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4958 let chars: Vec<char> = line.chars().collect();
4959 let col_start = if row == start_row { start_col + 1 } else { 0 };
4961 if col_start + 1 > chars.len() {
4962 continue;
4963 }
4964 for col in col_start..chars.len().saturating_sub(1) {
4965 if chars[col] == c1 && chars[col + 1] == c2 {
4966 hits += 1;
4967 if hits == count {
4968 return Some((row, col));
4969 }
4970 }
4971 }
4972 }
4973 None
4974}
4975
4976fn sneak_scan_backward<H: crate::types::Host>(
4979 ed: &Editor<hjkl_buffer::Buffer, H>,
4980 start_row: usize,
4981 start_col: usize,
4982 c1: char,
4983 c2: char,
4984 count: usize,
4985) -> Option<(usize, usize)> {
4986 let row_count = buf_row_count(&ed.buffer);
4987 let mut hits = 0usize;
4988 let rows_to_scan = (0..row_count).rev().skip(row_count - start_row - 1);
4990 for row in rows_to_scan {
4991 let line = buf_line(&ed.buffer, row).unwrap_or_default();
4992 let chars: Vec<char> = line.chars().collect();
4993 let col_end = if row == start_row {
4995 start_col.saturating_sub(1)
4996 } else if chars.is_empty() {
4997 continue;
4998 } else {
4999 chars.len().saturating_sub(1)
5000 };
5001 if col_end == 0 {
5002 continue;
5003 }
5004 for col in (0..col_end).rev() {
5006 if col + 1 < chars.len() && chars[col] == c1 && chars[col + 1] == c2 {
5007 hits += 1;
5008 if hits == count {
5009 return Some((row, col));
5010 }
5011 }
5012 }
5013 }
5014 None
5015}
5016
5017pub(crate) fn apply_op_sneak<H: crate::types::Host>(
5024 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5025 op: Operator,
5026 c1: char,
5027 c2: char,
5028 forward: bool,
5029 total_count: usize,
5030) {
5031 let start = ed.cursor();
5032 let result = if forward {
5033 sneak_scan_forward(ed, start.0, start.1, c1, c2, total_count)
5034 } else {
5035 sneak_scan_backward(ed, start.0, start.1, c1, c2, total_count)
5036 };
5037 let Some(end) = result else {
5038 return;
5039 };
5040 ed.jump_cursor(end.0, end.1);
5043 let end_cur = ed.cursor();
5044 ed.jump_cursor(start.0, start.1);
5045 run_operator_over_range(ed, op, start, end_cur, RangeKind::Exclusive);
5046 ed.vim.last_sneak = Some(((c1, c2), forward));
5047 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
5048 if !ed.vim.replaying && op_is_change(op) {
5049 }
5053}
5054
5055pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
5061 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5062 op: Operator,
5063 ch: char,
5064 forward: bool,
5065 till: bool,
5066 total_count: usize,
5067) {
5068 let motion = Motion::Find { ch, forward, till };
5069 apply_op_with_motion(ed, op, &motion, total_count);
5070 ed.vim.last_find = Some((ch, forward, till));
5071 if !ed.vim.replaying && op_is_change(op) {
5072 ed.vim.last_change = Some(LastChange::OpMotion {
5073 op,
5074 motion,
5075 count: total_count,
5076 inserted: None,
5077 });
5078 }
5079}
5080
5081pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
5090 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5091 op: Operator,
5092 ch: char,
5093 inner: bool,
5094 total_count: usize,
5095) -> bool {
5096 let obj = match ch {
5099 'w' => TextObject::Word { big: false },
5100 'W' => TextObject::Word { big: true },
5101 '"' | '\'' | '`' => TextObject::Quote(ch),
5102 '(' | ')' | 'b' => TextObject::Bracket('('),
5103 '[' | ']' => TextObject::Bracket('['),
5104 '{' | '}' | 'B' => TextObject::Bracket('{'),
5105 '<' | '>' => TextObject::Bracket('<'),
5106 'p' => TextObject::Paragraph,
5107 't' => TextObject::XmlTag,
5108 's' => TextObject::Sentence,
5109 _ => return false,
5110 };
5111 apply_op_with_text_object(ed, op, obj, inner, total_count.max(1));
5112 if !ed.vim.replaying && op_is_change(op) {
5113 ed.vim.last_change = Some(LastChange::OpTextObj {
5114 op,
5115 obj,
5116 inner,
5117 inserted: None,
5118 });
5119 }
5120 true
5121}
5122
5123pub(crate) fn retreat_one<H: crate::types::Host>(
5125 ed: &Editor<hjkl_buffer::Buffer, H>,
5126 pos: (usize, usize),
5127) -> (usize, usize) {
5128 let (r, c) = pos;
5129 if c > 0 {
5130 (r, c - 1)
5131 } else if r > 0 {
5132 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
5133 (r - 1, prev_len)
5134 } else {
5135 (0, 0)
5136 }
5137}
5138
5139fn begin_insert_noundo<H: crate::types::Host>(
5141 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5142 count: usize,
5143 reason: InsertReason,
5144) {
5145 let reason = if ed.vim.replaying {
5146 InsertReason::ReplayOnly
5147 } else {
5148 reason
5149 };
5150 let (row, col) = ed.cursor();
5151 ed.vim.insert_session = Some(InsertSession {
5152 count,
5153 row_min: row,
5154 row_max: row,
5155 before_rope: crate::types::Query::rope(&ed.buffer),
5156 reason,
5157 start_row: row,
5158 start_col: col,
5159 });
5160 ed.vim.mode = Mode::Insert;
5161 ed.vim.current_mode = crate::VimMode::Insert;
5163 drop_blame_if_left_normal(ed);
5164}
5165
5166pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
5169 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5170 op: Operator,
5171 motion: &Motion,
5172 count: usize,
5173) {
5174 let start = ed.cursor();
5175 apply_motion_cursor_ctx(ed, motion, count, true);
5180 let end = ed.cursor();
5181 let kind = motion_kind(motion);
5182 ed.jump_cursor(start.0, start.1);
5184
5185 if op == Operator::Comment {
5187 let top = start.0.min(end.0);
5188 let bot = start.0.max(end.0);
5189 ed.toggle_comment_range(top, bot);
5190 ed.vim.mode = Mode::Normal;
5191 return;
5192 }
5193
5194 run_operator_over_range(ed, op, start, end, kind);
5195}
5196
5197fn apply_op_with_text_object<H: crate::types::Host>(
5198 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5199 op: Operator,
5200 obj: TextObject,
5201 inner: bool,
5202 count: usize,
5203) {
5204 let Some((mut start, mut end, mut kind)) = text_object_range(ed, obj, inner, count) else {
5205 return;
5206 };
5207 if inner
5216 && matches!(obj, TextObject::Bracket(_))
5217 && kind == RangeKind::Exclusive
5218 && end.0 > start.0
5219 && end.1 == 0
5220 {
5221 let prev = end.0 - 1;
5222 let prev_len = buf_line_chars(&ed.buffer, prev);
5223 let fnb = buf_line(&ed.buffer, start.0)
5224 .unwrap_or_default()
5225 .chars()
5226 .take_while(|c| *c == ' ' || *c == '\t')
5227 .count();
5228 if start.1 <= fnb {
5229 start = (start.0, 0);
5230 end = (prev, prev_len);
5231 kind = RangeKind::Linewise;
5232 } else {
5233 end = (prev, prev_len.saturating_sub(1));
5234 kind = RangeKind::Inclusive;
5235 }
5236 }
5237 ed.jump_cursor(start.0, start.1);
5238 run_operator_over_range(ed, op, start, end, kind);
5239}
5240
5241fn motion_kind(motion: &Motion) -> RangeKind {
5242 match motion {
5243 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
5244 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
5245 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
5246 RangeKind::Linewise
5247 }
5248 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
5249 RangeKind::Inclusive
5250 }
5251 Motion::Find { .. } => RangeKind::Inclusive,
5252 Motion::MatchBracket => RangeKind::Inclusive,
5253 Motion::UnmatchedBracket { .. } => RangeKind::Exclusive,
5256 Motion::LineEnd => RangeKind::Inclusive,
5258 Motion::FirstNonBlankNextLine
5260 | Motion::FirstNonBlankPrevLine
5261 | Motion::FirstNonBlankLine => RangeKind::Linewise,
5262 Motion::SectionBackward
5264 | Motion::SectionForward
5265 | Motion::SectionEndBackward
5266 | Motion::SectionEndForward => RangeKind::Exclusive,
5267 _ => RangeKind::Exclusive,
5268 }
5269}
5270
5271fn change_linewise_rows<H: crate::types::Host>(
5280 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5281 top_row: usize,
5282 end_row: usize,
5283) {
5284 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5285 ed.vim.change_mark_start = Some((top_row, 0));
5287 ed.push_undo();
5288 ed.sync_buffer_content_from_textarea();
5289 let payload = read_vim_range(ed, (top_row, 0), (end_row, 0), RangeKind::Linewise);
5291 if end_row > top_row {
5293 ed.mutate_edit(Edit::DeleteRange {
5294 start: Position::new(top_row + 1, 0),
5295 end: Position::new(end_row, 0),
5296 kind: BufKind::Line,
5297 });
5298 }
5299 let indent_chars = if ed.settings.autoindent {
5302 let line = hjkl_buffer::rope_line_str(&crate::types::Query::rope(&ed.buffer), top_row);
5303 line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
5304 } else {
5305 0
5306 };
5307 let line_chars = buf_line_chars(&ed.buffer, top_row);
5308 if line_chars > indent_chars {
5309 ed.mutate_edit(Edit::DeleteRange {
5310 start: Position::new(top_row, indent_chars),
5311 end: Position::new(top_row, line_chars),
5312 kind: BufKind::Char,
5313 });
5314 }
5315 if !payload.is_empty() {
5316 ed.record_yank_to_host(payload.clone());
5317 ed.record_delete(payload, true);
5318 }
5319 buf_set_cursor_rc(&mut ed.buffer, top_row, indent_chars);
5320 ed.push_buffer_cursor_to_textarea();
5321 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5322}
5323
5324fn run_operator_over_range<H: crate::types::Host>(
5325 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5326 op: Operator,
5327 start: (usize, usize),
5328 end: (usize, usize),
5329 kind: RangeKind,
5330) {
5331 let (top, bot) = order(start, end);
5332 if top == bot && !matches!(kind, RangeKind::Linewise) {
5337 if op == Operator::Change {
5338 ed.vim.change_mark_start = Some(top);
5339 ed.push_undo();
5340 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5341 }
5342 return;
5343 }
5344
5345 match op {
5346 Operator::Yank => {
5347 let text = read_vim_range(ed, top, bot, kind);
5348 if !text.is_empty() {
5349 ed.record_yank_to_host(text.clone());
5350 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
5351 }
5352 let rbr = match kind {
5356 RangeKind::Linewise => {
5357 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
5358 (bot.0, last_col)
5359 }
5360 RangeKind::Inclusive => (bot.0, bot.1),
5361 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
5362 };
5363 ed.set_mark('[', top);
5364 ed.set_mark(']', rbr);
5365 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5366 ed.push_buffer_cursor_to_textarea();
5367 }
5368 Operator::Delete => {
5369 ed.push_undo();
5370 cut_vim_range(ed, top, bot, kind);
5371 if !matches!(kind, RangeKind::Linewise) {
5376 clamp_cursor_to_normal_mode(ed);
5377 }
5378 ed.vim.mode = Mode::Normal;
5379 let pos = ed.cursor();
5383 ed.set_mark('[', pos);
5384 ed.set_mark(']', pos);
5385 }
5386 Operator::Change => {
5387 if matches!(kind, RangeKind::Linewise) {
5392 change_linewise_rows(ed, top.0, bot.0);
5396 } else {
5397 ed.vim.change_mark_start = Some(top);
5399 ed.push_undo();
5400 cut_vim_range(ed, top, bot, kind);
5401 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5402 }
5403 }
5404 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
5405 apply_case_op_to_selection(ed, op, top, bot, kind);
5406 }
5407 Operator::Indent | Operator::Outdent => {
5408 ed.push_undo();
5411 if op == Operator::Indent {
5412 indent_rows(ed, top.0, bot.0, 1);
5413 } else {
5414 outdent_rows(ed, top.0, bot.0, 1);
5415 }
5416 ed.vim.mode = Mode::Normal;
5417 }
5418 Operator::Fold => {
5419 if bot.0 >= top.0 {
5423 ed.apply_fold_op(crate::types::FoldOp::Add {
5424 start_row: top.0,
5425 end_row: bot.0,
5426 closed: true,
5427 });
5428 }
5429 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5430 ed.push_buffer_cursor_to_textarea();
5431 ed.vim.mode = Mode::Normal;
5432 }
5433 Operator::Reflow => {
5434 ed.push_undo();
5435 reflow_rows(ed, top.0, bot.0);
5436 ed.vim.mode = Mode::Normal;
5437 }
5438 Operator::ReflowKeepCursor => {
5439 let saved = ed.cursor();
5442 ed.push_undo();
5443 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
5444 let (new_row, new_col) = reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
5445 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
5446 ed.push_buffer_cursor_to_textarea();
5447 ed.sticky_col = Some(new_col);
5448 ed.vim.mode = Mode::Normal;
5449 }
5450 Operator::AutoIndent => {
5451 ed.push_undo();
5453 auto_indent_rows(ed, top.0, bot.0);
5454 ed.vim.mode = Mode::Normal;
5455 }
5456 Operator::Filter => {
5457 }
5462 Operator::Comment => {
5463 }
5466 }
5467}
5468
5469pub(crate) fn delete_range_bridge<H: crate::types::Host>(
5486 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5487 start: (usize, usize),
5488 end: (usize, usize),
5489 kind: RangeKind,
5490 register: char,
5491) {
5492 ed.vim.pending_register = Some(register);
5493 run_operator_over_range(ed, Operator::Delete, start, end, kind);
5494}
5495
5496pub(crate) fn yank_range_bridge<H: crate::types::Host>(
5499 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5500 start: (usize, usize),
5501 end: (usize, usize),
5502 kind: RangeKind,
5503 register: char,
5504) {
5505 ed.vim.pending_register = Some(register);
5506 run_operator_over_range(ed, Operator::Yank, start, end, kind);
5507}
5508
5509pub(crate) fn change_range_bridge<H: crate::types::Host>(
5514 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5515 start: (usize, usize),
5516 end: (usize, usize),
5517 kind: RangeKind,
5518 register: char,
5519) {
5520 ed.vim.pending_register = Some(register);
5521 run_operator_over_range(ed, Operator::Change, start, end, kind);
5522}
5523
5524pub(crate) fn indent_range_bridge<H: crate::types::Host>(
5529 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5530 start: (usize, usize),
5531 end: (usize, usize),
5532 count: i32,
5533 shiftwidth: u32,
5534) {
5535 if count == 0 {
5536 return;
5537 }
5538 let (top_row, bot_row) = if start.0 <= end.0 {
5539 (start.0, end.0)
5540 } else {
5541 (end.0, start.0)
5542 };
5543 let original_sw = ed.settings().shiftwidth;
5545 if shiftwidth > 0 {
5546 ed.settings_mut().shiftwidth = shiftwidth as usize;
5547 }
5548 ed.push_undo();
5549 let abs_count = count.unsigned_abs() as usize;
5550 if count > 0 {
5551 indent_rows(ed, top_row, bot_row, abs_count);
5552 } else {
5553 outdent_rows(ed, top_row, bot_row, abs_count);
5554 }
5555 if shiftwidth > 0 {
5556 ed.settings_mut().shiftwidth = original_sw;
5557 }
5558 ed.vim.mode = Mode::Normal;
5559}
5560
5561pub(crate) fn case_range_bridge<H: crate::types::Host>(
5565 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5566 start: (usize, usize),
5567 end: (usize, usize),
5568 kind: RangeKind,
5569 op: Operator,
5570) {
5571 match op {
5572 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {}
5573 _ => return,
5574 }
5575 let (top, bot) = order(start, end);
5576 apply_case_op_to_selection(ed, op, top, bot, kind);
5577}
5578
5579pub(crate) fn delete_block_bridge<H: crate::types::Host>(
5600 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5601 top_row: usize,
5602 bot_row: usize,
5603 left_col: usize,
5604 right_col: usize,
5605 register: char,
5606) {
5607 ed.vim.pending_register = Some(register);
5608 let saved_anchor = ed.vim.block_anchor;
5609 let saved_vcol = ed.vim.block_vcol;
5610 ed.vim.block_anchor = (top_row, left_col);
5611 ed.vim.block_vcol = right_col;
5612 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5614 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5616 apply_block_operator(ed, Operator::Delete, 1);
5617 ed.vim.block_anchor = saved_anchor;
5621 ed.vim.block_vcol = saved_vcol;
5622}
5623
5624pub(crate) fn yank_block_bridge<H: crate::types::Host>(
5626 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5627 top_row: usize,
5628 bot_row: usize,
5629 left_col: usize,
5630 right_col: usize,
5631 register: char,
5632) {
5633 ed.vim.pending_register = Some(register);
5634 let saved_anchor = ed.vim.block_anchor;
5635 let saved_vcol = ed.vim.block_vcol;
5636 ed.vim.block_anchor = (top_row, left_col);
5637 ed.vim.block_vcol = right_col;
5638 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5639 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5640 apply_block_operator(ed, Operator::Yank, 1);
5641 ed.vim.block_anchor = saved_anchor;
5642 ed.vim.block_vcol = saved_vcol;
5643}
5644
5645pub(crate) fn change_block_bridge<H: crate::types::Host>(
5648 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5649 top_row: usize,
5650 bot_row: usize,
5651 left_col: usize,
5652 right_col: usize,
5653 register: char,
5654) {
5655 ed.vim.pending_register = Some(register);
5656 let saved_anchor = ed.vim.block_anchor;
5657 let saved_vcol = ed.vim.block_vcol;
5658 ed.vim.block_anchor = (top_row, left_col);
5659 ed.vim.block_vcol = right_col;
5660 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5661 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5662 apply_block_operator(ed, Operator::Change, 1);
5663 ed.vim.block_anchor = saved_anchor;
5664 ed.vim.block_vcol = saved_vcol;
5665}
5666
5667pub(crate) fn indent_block_bridge<H: crate::types::Host>(
5671 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5672 top_row: usize,
5673 bot_row: usize,
5674 count: i32,
5675) {
5676 if count == 0 {
5677 return;
5678 }
5679 ed.push_undo();
5680 let abs = count.unsigned_abs() as usize;
5681 if count > 0 {
5682 indent_rows(ed, top_row, bot_row, abs);
5683 } else {
5684 outdent_rows(ed, top_row, bot_row, abs);
5685 }
5686 ed.vim.mode = Mode::Normal;
5687}
5688
5689pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
5693 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5694 start: (usize, usize),
5695 end: (usize, usize),
5696) {
5697 let (top_row, bot_row) = if start.0 <= end.0 {
5698 (start.0, end.0)
5699 } else {
5700 (end.0, start.0)
5701 };
5702 ed.push_undo();
5703 auto_indent_rows(ed, top_row, bot_row);
5704 ed.vim.mode = Mode::Normal;
5705}
5706
5707pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
5718 ed: &Editor<hjkl_buffer::Buffer, H>,
5719) -> Option<((usize, usize), (usize, usize))> {
5720 word_text_object(ed, true, false)
5721}
5722
5723pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
5726 ed: &Editor<hjkl_buffer::Buffer, H>,
5727) -> Option<((usize, usize), (usize, usize))> {
5728 word_text_object(ed, false, false)
5729}
5730
5731pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
5734 ed: &Editor<hjkl_buffer::Buffer, H>,
5735) -> Option<((usize, usize), (usize, usize))> {
5736 word_text_object(ed, true, true)
5737}
5738
5739pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
5742 ed: &Editor<hjkl_buffer::Buffer, H>,
5743) -> Option<((usize, usize), (usize, usize))> {
5744 word_text_object(ed, false, true)
5745}
5746
5747pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
5763 ed: &Editor<hjkl_buffer::Buffer, H>,
5764 quote: char,
5765) -> Option<((usize, usize), (usize, usize))> {
5766 quote_text_object(ed, quote, true)
5767}
5768
5769pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
5772 ed: &Editor<hjkl_buffer::Buffer, H>,
5773 quote: char,
5774) -> Option<((usize, usize), (usize, usize))> {
5775 quote_text_object(ed, quote, false)
5776}
5777
5778pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
5786 ed: &Editor<hjkl_buffer::Buffer, H>,
5787 open: char,
5788) -> Option<((usize, usize), (usize, usize))> {
5789 bracket_text_object(ed, open, true, 1).map(|(s, e, _kind)| (s, e))
5790}
5791
5792pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
5796 ed: &Editor<hjkl_buffer::Buffer, H>,
5797 open: char,
5798) -> Option<((usize, usize), (usize, usize))> {
5799 bracket_text_object(ed, open, false, 1).map(|(s, e, _kind)| (s, e))
5800}
5801
5802pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
5807 ed: &Editor<hjkl_buffer::Buffer, H>,
5808) -> Option<((usize, usize), (usize, usize))> {
5809 sentence_text_object(ed, true)
5810}
5811
5812pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
5815 ed: &Editor<hjkl_buffer::Buffer, H>,
5816) -> Option<((usize, usize), (usize, usize))> {
5817 sentence_text_object(ed, false)
5818}
5819
5820pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
5825 ed: &Editor<hjkl_buffer::Buffer, H>,
5826) -> Option<((usize, usize), (usize, usize))> {
5827 paragraph_text_object(ed, true)
5828}
5829
5830pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
5833 ed: &Editor<hjkl_buffer::Buffer, H>,
5834) -> Option<((usize, usize), (usize, usize))> {
5835 paragraph_text_object(ed, false)
5836}
5837
5838pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
5844 ed: &Editor<hjkl_buffer::Buffer, H>,
5845) -> Option<((usize, usize), (usize, usize))> {
5846 tag_text_object(ed, true)
5847}
5848
5849pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
5852 ed: &Editor<hjkl_buffer::Buffer, H>,
5853) -> Option<((usize, usize), (usize, usize))> {
5854 tag_text_object(ed, false)
5855}
5856
5857pub(crate) fn rope_line_to_str(rope: &ropey::Rope, r: usize) -> String {
5862 let s = rope.line(r).to_string();
5863 if s.ends_with('\n') {
5865 s[..s.len() - 1].to_string()
5866 } else {
5867 s
5868 }
5869}
5870
5871pub(crate) fn rope_row_range_str(rope: &ropey::Rope, lo: usize, hi: usize) -> String {
5874 let n = rope.len_lines();
5875 let lo = lo.min(n.saturating_sub(1));
5876 let hi = hi.min(n.saturating_sub(1));
5877 if lo > hi {
5878 return String::new();
5879 }
5880 let start_byte = rope.line_to_byte(lo);
5882 let end_byte = if hi + 1 < n {
5885 rope.line_to_byte(hi + 1).saturating_sub(1)
5888 } else {
5889 rope.len_bytes()
5890 };
5891 rope.byte_slice(start_byte..end_byte).to_string()
5892}
5893
5894pub(crate) fn rope_to_lines_vec(rope: &ropey::Rope) -> Vec<String> {
5898 let n = rope.len_lines();
5899 (0..n).map(|r| rope_line_to_str(rope, r)).collect()
5900}
5901
5902fn greedy_wrap(original: &[String], width: usize) -> Vec<String> {
5906 let mut wrapped: Vec<String> = Vec::new();
5907 let mut paragraph: Vec<String> = Vec::new();
5908 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
5909 if para.is_empty() {
5910 return;
5911 }
5912 let words = para.join(" ");
5913 let mut current = String::new();
5914 for word in words.split_whitespace() {
5915 let extra = if current.is_empty() {
5916 word.chars().count()
5917 } else {
5918 current.chars().count() + 1 + word.chars().count()
5919 };
5920 if extra > width && !current.is_empty() {
5921 out.push(std::mem::take(&mut current));
5922 current.push_str(word);
5923 } else if current.is_empty() {
5924 current.push_str(word);
5925 } else {
5926 current.push(' ');
5927 current.push_str(word);
5928 }
5929 }
5930 if !current.is_empty() {
5931 out.push(current);
5932 }
5933 para.clear();
5934 };
5935 for line in original {
5936 if line.trim().is_empty() {
5937 flush(&mut paragraph, &mut wrapped, width);
5938 wrapped.push(String::new());
5939 } else {
5940 paragraph.push(line.clone());
5941 }
5942 }
5943 flush(&mut paragraph, &mut wrapped, width);
5944 wrapped
5945}
5946
5947fn reflow_rows<H: crate::types::Host>(
5953 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5954 top: usize,
5955 bot: usize,
5956) {
5957 let width = ed.settings().textwidth.max(1);
5958 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5959 let bot = bot.min(lines.len().saturating_sub(1));
5960 if top > bot {
5961 return;
5962 }
5963 let original = lines[top..=bot].to_vec();
5964 let wrapped = greedy_wrap(&original, width);
5965
5966 let last_offset = wrapped
5969 .iter()
5970 .rposition(|l| !l.trim().is_empty())
5971 .unwrap_or(0);
5972 let last_row = top + last_offset;
5973
5974 let after: Vec<String> = lines.split_off(bot + 1);
5976 lines.truncate(top);
5977 lines.extend(wrapped);
5978 lines.extend(after);
5979 ed.restore(lines, (last_row, 0));
5980 move_first_non_whitespace(ed);
5981 ed.mark_content_dirty();
5982}
5983
5984fn reflow_rows_keep_cursor<H: crate::types::Host>(
5988 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5989 top: usize,
5990 bot: usize,
5991) -> (Vec<String>, Vec<String>) {
5992 let width = ed.settings().textwidth.max(1);
5993 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5994 let bot = bot.min(lines.len().saturating_sub(1));
5995 if top > bot {
5996 return (Vec::new(), Vec::new());
5997 }
5998 let original = lines[top..=bot].to_vec();
5999 let wrapped = greedy_wrap(&original, width);
6000
6001 let after: Vec<String> = lines.split_off(bot + 1);
6002 lines.truncate(top);
6003 lines.extend(wrapped.clone());
6004 lines.extend(after);
6005 ed.restore(lines, (top, 0));
6006 ed.mark_content_dirty();
6007 (original, wrapped)
6008}
6009
6010fn reflow_keep_cursor(
6022 top: usize,
6023 cursor_row: usize,
6024 cursor_col: usize,
6025 before_lines: &[String],
6026 after_lines: &[String],
6027) -> (usize, usize) {
6028 let relative_row = cursor_row.saturating_sub(top);
6048 let mut char_offset: usize = 0;
6049 for (i, line) in before_lines.iter().enumerate() {
6050 if i == relative_row {
6051 let line_len = line.chars().count();
6053 char_offset += cursor_col.min(line_len);
6054 break;
6055 }
6056 char_offset += line.chars().count() + 1;
6058 }
6059
6060 let mut remaining = char_offset;
6062 for (i, line) in after_lines.iter().enumerate() {
6063 let len = line.chars().count();
6064 if remaining <= len {
6065 let col = remaining.min(if len == 0 { 0 } else { len.saturating_sub(1) });
6067 return (top + i, col);
6068 }
6069 remaining = remaining.saturating_sub(len + 1);
6071 }
6072
6073 let last = after_lines.len().saturating_sub(1);
6075 let last_len = after_lines
6076 .get(last)
6077 .map(|l| l.chars().count())
6078 .unwrap_or(0);
6079 let col = if last_len == 0 { 0 } else { last_len - 1 };
6080 (top + last, col)
6081}
6082
6083fn apply_case_op_to_selection<H: crate::types::Host>(
6089 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6090 op: Operator,
6091 top: (usize, usize),
6092 bot: (usize, usize),
6093 kind: RangeKind,
6094) {
6095 use hjkl_buffer::Edit;
6096 ed.push_undo();
6097 let saved_yank = ed.yank().to_string();
6098 let saved_yank_linewise = ed.vim.yank_linewise;
6099 let selection = cut_vim_range(ed, top, bot, kind);
6100 let transformed = match op {
6101 Operator::Uppercase => selection.to_uppercase(),
6102 Operator::Lowercase => selection.to_lowercase(),
6103 Operator::ToggleCase => toggle_case_str(&selection),
6104 Operator::Rot13 => rot13_str(&selection),
6105 _ => unreachable!(),
6106 };
6107 if !transformed.is_empty() {
6108 let cursor = buf_cursor_pos(&ed.buffer);
6109 ed.mutate_edit(Edit::InsertStr {
6110 at: cursor,
6111 text: transformed,
6112 });
6113 }
6114 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6115 ed.push_buffer_cursor_to_textarea();
6116 ed.set_yank(saved_yank);
6117 ed.vim.yank_linewise = saved_yank_linewise;
6118 ed.vim.mode = Mode::Normal;
6119}
6120
6121fn indent_rows<H: crate::types::Host>(
6126 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6127 top: usize,
6128 bot: usize,
6129 count: usize,
6130) {
6131 ed.sync_buffer_content_from_textarea();
6132 let width = ed.settings().shiftwidth * count.max(1);
6133 let pad: String = " ".repeat(width);
6134 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6135 let bot = bot.min(lines.len().saturating_sub(1));
6136 for line in lines.iter_mut().take(bot + 1).skip(top) {
6137 if !line.is_empty() {
6138 line.insert_str(0, &pad);
6139 }
6140 }
6141 ed.restore(lines, (top, 0));
6144 move_first_non_whitespace(ed);
6145}
6146
6147fn outdent_rows<H: crate::types::Host>(
6151 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6152 top: usize,
6153 bot: usize,
6154 count: usize,
6155) {
6156 ed.sync_buffer_content_from_textarea();
6157 let width = ed.settings().shiftwidth * count.max(1);
6158 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6159 let bot = bot.min(lines.len().saturating_sub(1));
6160 for line in lines.iter_mut().take(bot + 1).skip(top) {
6161 let strip: usize = line
6162 .chars()
6163 .take(width)
6164 .take_while(|c| *c == ' ' || *c == '\t')
6165 .count();
6166 if strip > 0 {
6167 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
6168 line.drain(..byte_len);
6169 }
6170 }
6171 ed.restore(lines, (top, 0));
6172 move_first_non_whitespace(ed);
6173}
6174
6175fn bracket_net(line: &str) -> i32 {
6202 let mut net: i32 = 0;
6203 let mut chars = line.chars().peekable();
6204 while let Some(ch) = chars.next() {
6205 match ch {
6206 '/' if chars.peek() == Some(&'/') => return net,
6208 '"' => {
6209 while let Some(c) = chars.next() {
6211 match c {
6212 '\\' => {
6213 chars.next();
6214 } '"' => break,
6216 _ => {}
6217 }
6218 }
6219 }
6220 '\'' => {
6221 let saved: Vec<char> = chars.clone().take(5).collect();
6230 let close_idx = if saved.first() == Some(&'\\') {
6231 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
6232 } else {
6233 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
6234 };
6235 if let Some(idx) = close_idx {
6236 for _ in 0..=idx {
6237 chars.next();
6238 }
6239 }
6240 }
6242 '{' | '(' | '[' => net += 1,
6243 '}' | ')' | ']' => net -= 1,
6244 _ => {}
6245 }
6246 }
6247 net
6248}
6249
6250fn auto_indent_rows<H: crate::types::Host>(
6272 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6273 top: usize,
6274 bot: usize,
6275) {
6276 ed.sync_buffer_content_from_textarea();
6277 let shiftwidth = ed.settings().shiftwidth;
6278 let expandtab = ed.settings().expandtab;
6279 let indent_unit: String = if expandtab {
6280 " ".repeat(shiftwidth)
6281 } else {
6282 "\t".to_string()
6283 };
6284
6285 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6286 let bot = bot.min(lines.len().saturating_sub(1));
6287
6288 let mut depth: i32 = 0;
6291 for line in lines.iter().take(top) {
6292 depth += bracket_net(line);
6293 if depth < 0 {
6294 depth = 0;
6295 }
6296 }
6297
6298 for line in lines.iter_mut().take(bot + 1).skip(top) {
6299 let trimmed_owned = line.trim_start().to_owned();
6300 if trimmed_owned.is_empty() {
6302 *line = String::new();
6303 continue;
6305 }
6306
6307 let starts_with_close = trimmed_owned
6309 .chars()
6310 .next()
6311 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
6312 let starts_with_dot = trimmed_owned.starts_with('.')
6322 && !trimmed_owned.starts_with("..")
6323 && !trimmed_owned.starts_with(".;");
6324 let effective_depth = if starts_with_close {
6325 depth.saturating_sub(1)
6326 } else if starts_with_dot {
6327 depth.saturating_add(1)
6328 } else {
6329 depth
6330 } as usize;
6331
6332 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
6334
6335 depth += bracket_net(&trimmed_owned);
6337 if depth < 0 {
6338 depth = 0;
6339 }
6340
6341 *line = new_line;
6342 }
6343
6344 ed.restore(lines, (top, 0));
6346 move_first_non_whitespace(ed);
6347 ed.last_indent_range = Some((top, bot));
6349}
6350
6351fn toggle_case_str(s: &str) -> String {
6352 s.chars()
6353 .map(|c| {
6354 if c.is_lowercase() {
6355 c.to_uppercase().next().unwrap_or(c)
6356 } else if c.is_uppercase() {
6357 c.to_lowercase().next().unwrap_or(c)
6358 } else {
6359 c
6360 }
6361 })
6362 .collect()
6363}
6364
6365fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
6366 if a <= b { (a, b) } else { (b, a) }
6367}
6368
6369fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6374 let (row, col) = ed.cursor();
6375 let line_chars = buf_line_chars(&ed.buffer, row);
6376 let max_col = line_chars.saturating_sub(1);
6377 if col > max_col {
6378 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
6379 ed.push_buffer_cursor_to_textarea();
6380 }
6381}
6382
6383fn expand_linewise_over_closed_folds(
6389 buf: &hjkl_buffer::Buffer,
6390 mut start: usize,
6391 mut end: usize,
6392) -> (usize, usize) {
6393 let folds = buf.folds();
6394 if folds.is_empty() {
6395 return (start, end);
6396 }
6397 loop {
6398 let mut changed = false;
6399 for f in &folds {
6400 if !f.closed {
6401 continue;
6402 }
6403 if f.start_row <= end && f.end_row >= start {
6405 if f.start_row < start {
6406 start = f.start_row;
6407 changed = true;
6408 }
6409 if f.end_row > end {
6410 end = f.end_row;
6411 changed = true;
6412 }
6413 }
6414 }
6415 if !changed {
6416 break;
6417 }
6418 }
6419 (start, end)
6420}
6421
6422fn execute_line_op<H: crate::types::Host>(
6423 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6424 op: Operator,
6425 count: usize,
6426) {
6427 let (row, col) = ed.cursor();
6428 let total = buf_row_count(&ed.buffer);
6429 let last_content_row = if total >= 2
6438 && buf_line(&ed.buffer, total - 1)
6439 .map(|s| s.is_empty())
6440 .unwrap_or(false)
6441 {
6442 total - 2
6443 } else {
6444 total.saturating_sub(1)
6445 };
6446 if count >= 2 && row >= last_content_row {
6447 return;
6448 }
6449 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
6450
6451 let (row, end_row) = expand_linewise_over_closed_folds(&ed.buffer, row, end_row);
6456
6457 match op {
6458 Operator::Yank => {
6459 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6461 if !text.is_empty() {
6462 ed.record_yank_to_host(text.clone());
6463 ed.record_yank(text, true);
6464 }
6465 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
6468 ed.set_mark('[', (row, 0));
6469 ed.set_mark(']', (end_row, last_col));
6470 buf_set_cursor_rc(&mut ed.buffer, row, col);
6471 ed.push_buffer_cursor_to_textarea();
6472 ed.vim.mode = Mode::Normal;
6473 }
6474 Operator::Delete => {
6475 ed.push_undo();
6476 let deleted_through_last = end_row + 1 >= total;
6477 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6478 let total_after = buf_row_count(&ed.buffer);
6482 let raw_target = if deleted_through_last {
6483 row.saturating_sub(1).min(total_after.saturating_sub(1))
6484 } else {
6485 row.min(total_after.saturating_sub(1))
6486 };
6487 let target_row = if raw_target > 0
6493 && raw_target + 1 == total_after
6494 && buf_line(&ed.buffer, raw_target)
6495 .map(|s| s.is_empty())
6496 .unwrap_or(false)
6497 {
6498 raw_target - 1
6499 } else {
6500 raw_target
6501 };
6502 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6503 ed.push_buffer_cursor_to_textarea();
6504 move_first_non_whitespace(ed);
6505 ed.sticky_col = Some(ed.cursor().1);
6506 ed.vim.mode = Mode::Normal;
6507 let pos = ed.cursor();
6510 ed.set_mark('[', pos);
6511 ed.set_mark(']', pos);
6512 }
6513 Operator::Change => {
6514 change_linewise_rows(ed, row, end_row);
6518 }
6519 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6520 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
6524 move_first_non_whitespace(ed);
6527 }
6528 Operator::Indent | Operator::Outdent => {
6529 ed.push_undo();
6531 if op == Operator::Indent {
6532 indent_rows(ed, row, end_row, 1);
6533 } else {
6534 outdent_rows(ed, row, end_row, 1);
6535 }
6536 ed.sticky_col = Some(ed.cursor().1);
6537 ed.vim.mode = Mode::Normal;
6538 }
6539 Operator::Fold => unreachable!("Fold has no line-op double"),
6541 Operator::Reflow => {
6542 ed.push_undo();
6544 reflow_rows(ed, row, end_row);
6545 move_first_non_whitespace(ed);
6546 ed.sticky_col = Some(ed.cursor().1);
6547 ed.vim.mode = Mode::Normal;
6548 }
6549 Operator::ReflowKeepCursor => {
6550 let saved = ed.cursor();
6553 ed.push_undo();
6554 let (before, after) = reflow_rows_keep_cursor(ed, row, end_row);
6555 let (new_row, new_col) = reflow_keep_cursor(row, saved.0, saved.1, &before, &after);
6556 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6557 ed.push_buffer_cursor_to_textarea();
6558 ed.sticky_col = Some(new_col);
6559 ed.vim.mode = Mode::Normal;
6560 }
6561 Operator::AutoIndent => {
6562 ed.push_undo();
6564 auto_indent_rows(ed, row, end_row);
6565 ed.sticky_col = Some(ed.cursor().1);
6566 ed.vim.mode = Mode::Normal;
6567 }
6568 Operator::Filter => {
6569 }
6571 Operator::Comment => {
6572 }
6577 }
6578}
6579
6580pub(crate) fn apply_visual_operator<H: crate::types::Host>(
6583 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6584 op: Operator,
6585 count: usize,
6586) {
6587 let levels = count.max(1);
6590 match ed.vim.mode {
6591 Mode::VisualLine => {
6592 let cursor_row = buf_cursor_pos(&ed.buffer).row;
6593 let top = cursor_row.min(ed.vim.visual_line_anchor);
6594 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6595 ed.vim.yank_linewise = true;
6596 match op {
6597 Operator::Yank => {
6598 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6599 if !text.is_empty() {
6600 ed.record_yank_to_host(text.clone());
6601 ed.record_yank(text, true);
6602 }
6603 buf_set_cursor_rc(&mut ed.buffer, top, 0);
6604 ed.push_buffer_cursor_to_textarea();
6605 ed.vim.mode = Mode::Normal;
6606 }
6607 Operator::Delete => {
6608 ed.push_undo();
6609 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6610 ed.vim.mode = Mode::Normal;
6611 }
6612 Operator::Change => {
6613 change_linewise_rows(ed, top, bot);
6616 }
6617 Operator::Uppercase
6618 | Operator::Lowercase
6619 | Operator::ToggleCase
6620 | Operator::Rot13 => {
6621 let bot = buf_cursor_pos(&ed.buffer)
6622 .row
6623 .max(ed.vim.visual_line_anchor);
6624 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
6625 move_first_non_whitespace(ed);
6626 }
6627 Operator::Indent | Operator::Outdent => {
6628 ed.push_undo();
6629 let (cursor_row, _) = ed.cursor();
6630 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6631 if op == Operator::Indent {
6632 indent_rows(ed, top, bot, levels);
6633 } else {
6634 outdent_rows(ed, top, bot, levels);
6635 }
6636 ed.vim.mode = Mode::Normal;
6637 }
6638 Operator::Reflow => {
6639 ed.push_undo();
6640 let (cursor_row, _) = ed.cursor();
6641 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6642 reflow_rows(ed, top, bot);
6643 ed.vim.mode = Mode::Normal;
6644 }
6645 Operator::ReflowKeepCursor => {
6646 let saved = ed.cursor();
6647 ed.push_undo();
6648 let (cursor_row, _) = ed.cursor();
6649 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6650 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6651 let (new_row, new_col) =
6652 reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6653 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6654 ed.push_buffer_cursor_to_textarea();
6655 ed.vim.mode = Mode::Normal;
6656 }
6657 Operator::AutoIndent => {
6658 ed.push_undo();
6659 let (cursor_row, _) = ed.cursor();
6660 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6661 auto_indent_rows(ed, top, bot);
6662 ed.vim.mode = Mode::Normal;
6663 }
6664 Operator::Filter => {}
6666 Operator::Comment => {}
6668 Operator::Fold => unreachable!("Visual zf takes its own path"),
6671 }
6672 }
6673 Mode::Visual => {
6674 ed.vim.yank_linewise = false;
6675 let anchor = ed.vim.visual_anchor;
6676 let cursor = ed.cursor();
6677 let (top, bot) = order(anchor, cursor);
6678 match op {
6679 Operator::Yank => {
6680 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
6681 if !text.is_empty() {
6682 ed.record_yank_to_host(text.clone());
6683 ed.record_yank(text, false);
6684 }
6685 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6686 ed.push_buffer_cursor_to_textarea();
6687 ed.vim.mode = Mode::Normal;
6688 }
6689 Operator::Delete => {
6690 ed.push_undo();
6691 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6692 ed.vim.mode = Mode::Normal;
6693 }
6694 Operator::Change => {
6695 ed.push_undo();
6696 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6697 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
6698 }
6699 Operator::Uppercase
6700 | Operator::Lowercase
6701 | Operator::ToggleCase
6702 | Operator::Rot13 => {
6703 let anchor = ed.vim.visual_anchor;
6705 let cursor = ed.cursor();
6706 let (top, bot) = order(anchor, cursor);
6707 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
6708 }
6709 Operator::Indent | Operator::Outdent => {
6710 ed.push_undo();
6711 let anchor = ed.vim.visual_anchor;
6712 let cursor = ed.cursor();
6713 let (top, bot) = order(anchor, cursor);
6714 if op == Operator::Indent {
6715 indent_rows(ed, top.0, bot.0, levels);
6716 } else {
6717 outdent_rows(ed, top.0, bot.0, levels);
6718 }
6719 ed.vim.mode = Mode::Normal;
6720 }
6721 Operator::Reflow => {
6722 ed.push_undo();
6723 let anchor = ed.vim.visual_anchor;
6724 let cursor = ed.cursor();
6725 let (top, bot) = order(anchor, cursor);
6726 reflow_rows(ed, top.0, bot.0);
6727 ed.vim.mode = Mode::Normal;
6728 }
6729 Operator::ReflowKeepCursor => {
6730 let saved = ed.cursor();
6731 ed.push_undo();
6732 let anchor = ed.vim.visual_anchor;
6733 let cursor = ed.cursor();
6734 let (top, bot) = order(anchor, cursor);
6735 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
6736 let (new_row, new_col) =
6737 reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
6738 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6739 ed.push_buffer_cursor_to_textarea();
6740 ed.vim.mode = Mode::Normal;
6741 }
6742 Operator::AutoIndent => {
6743 ed.push_undo();
6744 let anchor = ed.vim.visual_anchor;
6745 let cursor = ed.cursor();
6746 let (top, bot) = order(anchor, cursor);
6747 auto_indent_rows(ed, top.0, bot.0);
6748 ed.vim.mode = Mode::Normal;
6749 }
6750 Operator::Filter => {}
6752 Operator::Comment => {}
6754 Operator::Fold => unreachable!("Visual zf takes its own path"),
6755 }
6756 }
6757 Mode::VisualBlock => apply_block_operator(ed, op, levels),
6758 _ => {}
6759 }
6760}
6761
6762fn block_bounds<H: crate::types::Host>(
6767 ed: &Editor<hjkl_buffer::Buffer, H>,
6768) -> (usize, usize, usize, usize) {
6769 let (ar, ac) = ed.vim.block_anchor;
6770 let (cr, _) = ed.cursor();
6771 let cc = ed.vim.block_vcol;
6772 let top = ar.min(cr);
6773 let bot = ar.max(cr);
6774 let left = ac.min(cc);
6775 let right = ac.max(cc);
6776 (top, bot, left, right)
6777}
6778
6779pub(crate) fn update_block_vcol<H: crate::types::Host>(
6784 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6785 motion: &Motion,
6786) {
6787 match motion {
6788 Motion::Left
6789 | Motion::Right
6790 | Motion::WordFwd
6791 | Motion::BigWordFwd
6792 | Motion::WordBack
6793 | Motion::BigWordBack
6794 | Motion::WordEnd
6795 | Motion::BigWordEnd
6796 | Motion::WordEndBack
6797 | Motion::BigWordEndBack
6798 | Motion::LineStart
6799 | Motion::FirstNonBlank
6800 | Motion::LineEnd
6801 | Motion::Find { .. }
6802 | Motion::FindRepeat { .. }
6803 | Motion::MatchBracket => {
6804 ed.vim.block_vcol = ed.cursor().1;
6805 }
6806 _ => {}
6808 }
6809}
6810
6811fn apply_block_operator<H: crate::types::Host>(
6816 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6817 op: Operator,
6818 count: usize,
6819) {
6820 let (top, bot, left, right) = block_bounds(ed);
6821 let yank = block_yank(ed, top, bot, left, right);
6823
6824 match op {
6825 Operator::Yank => {
6826 if !yank.is_empty() {
6827 ed.record_yank_to_host(yank.clone());
6828 ed.record_yank(yank, false);
6829 }
6830 ed.vim.mode = Mode::Normal;
6831 ed.jump_cursor(top, left);
6832 }
6833 Operator::Delete => {
6834 ed.push_undo();
6835 delete_block_contents(ed, top, bot, left, right);
6836 if !yank.is_empty() {
6837 ed.record_yank_to_host(yank.clone());
6838 ed.record_delete(yank, false);
6839 }
6840 ed.vim.mode = Mode::Normal;
6841 ed.jump_cursor(top, left);
6842 }
6843 Operator::Change => {
6844 ed.push_undo();
6845 delete_block_contents(ed, top, bot, left, right);
6846 if !yank.is_empty() {
6847 ed.record_yank_to_host(yank.clone());
6848 ed.record_delete(yank, false);
6849 }
6850 ed.jump_cursor(top, left);
6851 begin_insert_noundo(
6852 ed,
6853 1,
6854 InsertReason::BlockChange {
6855 top,
6856 bot,
6857 col: left,
6858 },
6859 );
6860 }
6861 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6862 ed.push_undo();
6863 transform_block_case(ed, op, top, bot, left, right);
6864 ed.vim.mode = Mode::Normal;
6865 ed.jump_cursor(top, left);
6866 }
6867 Operator::Indent | Operator::Outdent => {
6868 ed.push_undo();
6872 if op == Operator::Indent {
6873 indent_rows(ed, top, bot, count.max(1));
6874 } else {
6875 outdent_rows(ed, top, bot, count.max(1));
6876 }
6877 ed.vim.mode = Mode::Normal;
6878 }
6879 Operator::Fold => unreachable!("Visual zf takes its own path"),
6880 Operator::Reflow => {
6881 ed.push_undo();
6885 reflow_rows(ed, top, bot);
6886 ed.vim.mode = Mode::Normal;
6887 }
6888 Operator::ReflowKeepCursor => {
6889 let saved = ed.cursor();
6891 ed.push_undo();
6892 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6893 let (new_row, new_col) = reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6894 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6895 ed.push_buffer_cursor_to_textarea();
6896 ed.vim.mode = Mode::Normal;
6897 }
6898 Operator::AutoIndent => {
6899 ed.push_undo();
6902 auto_indent_rows(ed, top, bot);
6903 ed.vim.mode = Mode::Normal;
6904 }
6905 Operator::Filter => {}
6907 Operator::Comment => {}
6909 }
6910}
6911
6912fn transform_block_case<H: crate::types::Host>(
6916 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6917 op: Operator,
6918 top: usize,
6919 bot: usize,
6920 left: usize,
6921 right: usize,
6922) {
6923 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6924 for r in top..=bot.min(lines.len().saturating_sub(1)) {
6925 let chars: Vec<char> = lines[r].chars().collect();
6926 if left >= chars.len() {
6927 continue;
6928 }
6929 let end = (right + 1).min(chars.len());
6930 let head: String = chars[..left].iter().collect();
6931 let mid: String = chars[left..end].iter().collect();
6932 let tail: String = chars[end..].iter().collect();
6933 let transformed = match op {
6934 Operator::Uppercase => mid.to_uppercase(),
6935 Operator::Lowercase => mid.to_lowercase(),
6936 Operator::ToggleCase => toggle_case_str(&mid),
6937 Operator::Rot13 => rot13_str(&mid),
6938 _ => mid,
6939 };
6940 lines[r] = format!("{head}{transformed}{tail}");
6941 }
6942 let saved_yank = ed.yank().to_string();
6943 let saved_linewise = ed.vim.yank_linewise;
6944 ed.restore(lines, (top, left));
6945 ed.set_yank(saved_yank);
6946 ed.vim.yank_linewise = saved_linewise;
6947}
6948
6949fn block_yank<H: crate::types::Host>(
6950 ed: &Editor<hjkl_buffer::Buffer, H>,
6951 top: usize,
6952 bot: usize,
6953 left: usize,
6954 right: usize,
6955) -> String {
6956 let rope = crate::types::Query::rope(&ed.buffer);
6957 let n = rope.len_lines();
6958 let mut rows: Vec<String> = Vec::new();
6959 for r in top..=bot {
6960 if r >= n {
6961 break;
6962 }
6963 let line = rope_line_to_str(&rope, r);
6964 let chars: Vec<char> = line.chars().collect();
6965 let end = (right + 1).min(chars.len());
6966 if left >= chars.len() {
6967 rows.push(String::new());
6968 } else {
6969 rows.push(chars[left..end].iter().collect());
6970 }
6971 }
6972 rows.join("\n")
6973}
6974
6975fn delete_block_contents<H: crate::types::Host>(
6976 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6977 top: usize,
6978 bot: usize,
6979 left: usize,
6980 right: usize,
6981) {
6982 use hjkl_buffer::{Edit, MotionKind, Position};
6983 ed.sync_buffer_content_from_textarea();
6984 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
6985 if last_row < top {
6986 return;
6987 }
6988 ed.mutate_edit(Edit::DeleteRange {
6989 start: Position::new(top, left),
6990 end: Position::new(last_row, right),
6991 kind: MotionKind::Block,
6992 });
6993 ed.push_buffer_cursor_to_textarea();
6994}
6995
6996pub(crate) fn block_replace<H: crate::types::Host>(
6998 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6999 ch: char,
7000) {
7001 let (top, bot, left, right) = block_bounds(ed);
7002 ed.push_undo();
7003 ed.sync_buffer_content_from_textarea();
7004 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
7005 for r in top..=bot.min(lines.len().saturating_sub(1)) {
7006 let chars: Vec<char> = lines[r].chars().collect();
7007 if left >= chars.len() {
7008 continue;
7009 }
7010 let end = (right + 1).min(chars.len());
7011 let before: String = chars[..left].iter().collect();
7012 let middle: String = std::iter::repeat_n(ch, end - left).collect();
7013 let after: String = chars[end..].iter().collect();
7014 lines[r] = format!("{before}{middle}{after}");
7015 }
7016 reset_textarea_lines(ed, lines);
7017 ed.vim.mode = Mode::Normal;
7018 ed.jump_cursor(top, left);
7019}
7020
7021fn reset_textarea_lines<H: crate::types::Host>(
7025 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7026 lines: Vec<String>,
7027) {
7028 let cursor = ed.cursor();
7029 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
7030 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
7031 ed.mark_content_dirty();
7032}
7033
7034type Pos = (usize, usize);
7040
7041pub(crate) fn text_object_range<H: crate::types::Host>(
7045 ed: &Editor<hjkl_buffer::Buffer, H>,
7046 obj: TextObject,
7047 inner: bool,
7048 count: usize,
7049) -> Option<(Pos, Pos, RangeKind)> {
7050 match obj {
7051 TextObject::Word { big } => {
7052 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
7053 }
7054 TextObject::Quote(q) => {
7055 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
7056 }
7057 TextObject::Bracket(open) => bracket_text_object(ed, open, inner, count),
7058 TextObject::Paragraph => {
7059 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
7060 }
7061 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
7062 TextObject::Sentence => {
7063 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
7064 }
7065 }
7066}
7067
7068fn sentence_boundary<H: crate::types::Host>(
7072 ed: &Editor<hjkl_buffer::Buffer, H>,
7073 forward: bool,
7074) -> Option<(usize, usize)> {
7075 let rope = crate::types::Query::rope(&ed.buffer);
7076 let n_lines = rope.len_lines();
7077 if n_lines == 0 {
7078 return None;
7079 }
7080 let line_lens: Vec<usize> = (0..n_lines)
7082 .map(|r| rope_line_to_str(&rope, r).chars().count())
7083 .collect();
7084 let pos_to_idx = |pos: (usize, usize)| -> usize {
7085 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
7086 idx + pos.1
7087 };
7088 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
7089 for (r, &len) in line_lens.iter().enumerate() {
7090 if idx <= len {
7091 return (r, idx);
7092 }
7093 idx -= len + 1;
7094 }
7095 let last = n_lines.saturating_sub(1);
7096 (last, line_lens[last])
7097 };
7098 let mut chars: Vec<char> = rope.chars().collect();
7101 if chars.last() == Some(&'\n') {
7103 chars.pop();
7104 }
7105 if chars.is_empty() {
7106 return None;
7107 }
7108 let total = chars.len();
7109 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
7110 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
7111
7112 if forward {
7113 let mut i = cursor_idx + 1;
7116 while i < total {
7117 if is_terminator(chars[i]) {
7118 while i + 1 < total && is_terminator(chars[i + 1]) {
7119 i += 1;
7120 }
7121 if i + 1 >= total {
7122 return None;
7123 }
7124 if chars[i + 1].is_whitespace() {
7125 let mut j = i + 1;
7126 while j < total && chars[j].is_whitespace() {
7127 j += 1;
7128 }
7129 if j >= total {
7130 return None;
7131 }
7132 return Some(idx_to_pos(j));
7133 }
7134 }
7135 i += 1;
7136 }
7137 None
7138 } else {
7139 let find_start = |from: usize| -> Option<usize> {
7143 let mut start = from;
7144 while start > 0 {
7145 let prev = chars[start - 1];
7146 if prev.is_whitespace() {
7147 let mut k = start - 1;
7148 while k > 0 && chars[k - 1].is_whitespace() {
7149 k -= 1;
7150 }
7151 if k > 0 && is_terminator(chars[k - 1]) {
7152 break;
7153 }
7154 }
7155 start -= 1;
7156 }
7157 while start < total && chars[start].is_whitespace() {
7158 start += 1;
7159 }
7160 (start < total).then_some(start)
7161 };
7162 let current_start = find_start(cursor_idx)?;
7163 if current_start < cursor_idx {
7164 return Some(idx_to_pos(current_start));
7165 }
7166 let mut k = current_start;
7169 while k > 0 && chars[k - 1].is_whitespace() {
7170 k -= 1;
7171 }
7172 if k == 0 {
7173 return None;
7174 }
7175 let prev_start = find_start(k - 1)?;
7176 Some(idx_to_pos(prev_start))
7177 }
7178}
7179
7180fn sentence_text_object<H: crate::types::Host>(
7186 ed: &Editor<hjkl_buffer::Buffer, H>,
7187 inner: bool,
7188) -> Option<((usize, usize), (usize, usize))> {
7189 let rope = crate::types::Query::rope(&ed.buffer);
7190 let n_lines = rope.len_lines();
7191 if n_lines == 0 {
7192 return None;
7193 }
7194 let line_lens: Vec<usize> = (0..n_lines)
7197 .map(|r| rope_line_to_str(&rope, r).chars().count())
7198 .collect();
7199 let pos_to_idx = |pos: (usize, usize)| -> usize {
7200 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
7201 idx + pos.1
7202 };
7203 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
7204 for (r, &len) in line_lens.iter().enumerate() {
7205 if idx <= len {
7206 return (r, idx);
7207 }
7208 idx -= len + 1;
7209 }
7210 let last = n_lines.saturating_sub(1);
7211 (last, line_lens[last])
7212 };
7213 let mut chars: Vec<char> = rope.chars().collect();
7214 if chars.last() == Some(&'\n') {
7215 chars.pop();
7216 }
7217 if chars.is_empty() {
7218 return None;
7219 }
7220
7221 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
7222 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
7223
7224 let mut start = cursor_idx;
7228 while start > 0 {
7229 let prev = chars[start - 1];
7230 if prev.is_whitespace() {
7231 let mut k = start - 1;
7235 while k > 0 && chars[k - 1].is_whitespace() {
7236 k -= 1;
7237 }
7238 if k > 0 && is_terminator(chars[k - 1]) {
7239 break;
7240 }
7241 }
7242 start -= 1;
7243 }
7244 while start < chars.len() && chars[start].is_whitespace() {
7247 start += 1;
7248 }
7249 if start >= chars.len() {
7250 return None;
7251 }
7252
7253 let mut end = start;
7256 while end < chars.len() {
7257 if is_terminator(chars[end]) {
7258 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
7260 end += 1;
7261 }
7262 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
7265 break;
7266 }
7267 }
7268 end += 1;
7269 }
7270 let end_idx = (end + 1).min(chars.len());
7272
7273 let final_end = if inner {
7274 end_idx
7275 } else {
7276 let mut e = end_idx;
7280 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
7281 e += 1;
7282 }
7283 e
7284 };
7285
7286 Some((idx_to_pos(start), idx_to_pos(final_end)))
7287}
7288
7289fn tag_text_object<H: crate::types::Host>(
7293 ed: &Editor<hjkl_buffer::Buffer, H>,
7294 inner: bool,
7295) -> Option<((usize, usize), (usize, usize))> {
7296 let rope = crate::types::Query::rope(&ed.buffer);
7297 let n_lines = rope.len_lines();
7298 if n_lines == 0 {
7299 return None;
7300 }
7301 let line_lens: Vec<usize> = (0..n_lines)
7305 .map(|r| rope_line_to_str(&rope, r).chars().count())
7306 .collect();
7307 let pos_to_idx = |pos: (usize, usize)| -> usize {
7308 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
7309 idx + pos.1
7310 };
7311 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
7312 for (r, &len) in line_lens.iter().enumerate() {
7313 if idx <= len {
7314 return (r, idx);
7315 }
7316 idx -= len + 1;
7317 }
7318 let last = n_lines.saturating_sub(1);
7319 (last, line_lens[last])
7320 };
7321 let mut chars: Vec<char> = rope.chars().collect();
7322 if chars.last() == Some(&'\n') {
7323 chars.pop();
7324 }
7325 let cursor_idx = pos_to_idx(ed.cursor());
7326
7327 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
7335 let mut next_after: Option<(usize, usize, usize, usize)> = None;
7336 let mut i = 0;
7337 while i < chars.len() {
7338 if chars[i] != '<' {
7339 i += 1;
7340 continue;
7341 }
7342 let mut j = i + 1;
7343 while j < chars.len() && chars[j] != '>' {
7344 j += 1;
7345 }
7346 if j >= chars.len() {
7347 break;
7348 }
7349 let inside: String = chars[i + 1..j].iter().collect();
7350 let close_end = j + 1;
7351 let trimmed = inside.trim();
7352 if trimmed.starts_with('!') || trimmed.starts_with('?') {
7353 i = close_end;
7354 continue;
7355 }
7356 if let Some(rest) = trimmed.strip_prefix('/') {
7357 let name = rest.split_whitespace().next().unwrap_or("").to_string();
7358 if !name.is_empty()
7359 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
7360 {
7361 let (open_start, content_start, _) = stack[stack_idx].clone();
7362 stack.truncate(stack_idx);
7363 let content_end = i;
7364 let candidate = (open_start, content_start, content_end, close_end);
7365 if cursor_idx >= open_start && cursor_idx < close_end {
7372 innermost = match innermost {
7373 Some((os, _, _, ce)) if os <= open_start && close_end <= ce => {
7374 Some(candidate)
7375 }
7376 None => Some(candidate),
7377 existing => existing,
7378 };
7379 } else if open_start >= cursor_idx && next_after.is_none() {
7380 next_after = Some(candidate);
7381 }
7382 }
7383 } else if !trimmed.ends_with('/') {
7384 let name: String = trimmed
7385 .split(|c: char| c.is_whitespace() || c == '/')
7386 .next()
7387 .unwrap_or("")
7388 .to_string();
7389 if !name.is_empty() {
7390 stack.push((i, close_end, name));
7391 }
7392 }
7393 i = close_end;
7394 }
7395
7396 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
7397 if inner {
7398 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
7399 } else {
7400 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
7401 }
7402}
7403
7404fn is_wordchar(c: char) -> bool {
7405 c.is_alphanumeric() || c == '_'
7406}
7407
7408pub(crate) use hjkl_buffer::is_keyword_char;
7412
7413#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7419pub(crate) enum AbbrevKind {
7420 Full,
7422 End,
7424 NonKw,
7426}
7427
7428pub(crate) fn abbrev_kind(lhs: &str, iskeyword: &str) -> AbbrevKind {
7429 let chars: Vec<char> = lhs.chars().collect();
7430 if chars.is_empty() {
7431 return AbbrevKind::NonKw;
7432 }
7433 let last = *chars.last().unwrap();
7434 let last_is_kw = is_keyword_char(last, iskeyword);
7435 if !last_is_kw {
7436 return AbbrevKind::NonKw;
7437 }
7438 let all_kw = chars.iter().all(|&c| is_keyword_char(c, iskeyword));
7440 if all_kw {
7441 AbbrevKind::Full
7442 } else {
7443 AbbrevKind::End
7444 }
7445}
7446
7447pub(crate) fn try_abbrev_expand(
7465 abbrevs: &[Abbrev],
7466 line_before: &str,
7467 mincol: usize,
7468 trigger: AbbrevTrigger,
7469 iskeyword: &str,
7470) -> Option<(usize, String)> {
7471 let chars: Vec<char> = line_before.chars().collect();
7472 let cursor_col = chars.len(); for abbrev in abbrevs {
7475 if !abbrev.insert {
7476 continue;
7477 }
7478 let lhs_chars: Vec<char> = abbrev.lhs.chars().collect();
7479 if lhs_chars.is_empty() {
7480 continue;
7481 }
7482 let lhs_len = lhs_chars.len();
7483
7484 let kind = abbrev_kind(&abbrev.lhs, iskeyword);
7486
7487 match kind {
7489 AbbrevKind::Full | AbbrevKind::End => {
7490 let trigger_char_is_kw = match trigger {
7493 AbbrevTrigger::NonKeyword(c) => is_keyword_char(c, iskeyword),
7494 AbbrevTrigger::CtrlBracket | AbbrevTrigger::Cr | AbbrevTrigger::Esc => false,
7495 };
7496 if trigger_char_is_kw {
7497 continue;
7499 }
7500 }
7501 AbbrevKind::NonKw => {
7502 match trigger {
7504 AbbrevTrigger::Cr | AbbrevTrigger::Esc | AbbrevTrigger::CtrlBracket => {}
7505 AbbrevTrigger::NonKeyword(_) => continue,
7506 }
7507 }
7508 }
7509
7510 if cursor_col < lhs_len {
7512 continue;
7513 }
7514 let lhs_start_col = cursor_col - lhs_len;
7515
7516 if lhs_start_col < mincol {
7518 continue;
7519 }
7520
7521 let text_slice: &[char] = &chars[lhs_start_col..cursor_col];
7523 if text_slice != lhs_chars.as_slice() {
7524 continue;
7525 }
7526
7527 if lhs_start_col > 0 {
7529 let ch_before = chars[lhs_start_col - 1];
7530 match kind {
7531 AbbrevKind::Full => {
7532 if is_keyword_char(ch_before, iskeyword) {
7543 continue; }
7545 if lhs_len == 1 && ch_before != ' ' && ch_before != '\t' {
7546 continue;
7548 }
7549 }
7550 AbbrevKind::End => {
7551 }
7555 AbbrevKind::NonKw => {
7556 if ch_before != ' ' && ch_before != '\t' {
7559 continue;
7560 }
7561 }
7562 }
7563 }
7564 return Some((lhs_len, abbrev.rhs.clone()));
7568 }
7569
7570 None
7571}
7572
7573pub(crate) fn check_and_apply_abbrev<H: crate::types::Host>(
7582 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7583 trigger: AbbrevTrigger,
7584) -> bool {
7585 use hjkl_buffer::{Edit, Position};
7586
7587 let cursor = buf_cursor_pos(&ed.buffer);
7589 let row = cursor.row;
7590 let col = cursor.col;
7591 let line_before: String = {
7592 let line = buf_line(&ed.buffer, row).unwrap_or_default();
7593 line.chars().take(col).collect()
7594 };
7595 let (mincol, on_start_row) = if let Some(ref s) = ed.vim.insert_session {
7596 if row == s.start_row {
7597 (s.start_col, true)
7598 } else {
7599 (0, false)
7600 }
7601 } else {
7602 (0, false)
7603 };
7604 if on_start_row && col <= mincol {
7606 return false;
7607 }
7608
7609 let iskeyword = ed.settings.iskeyword.clone();
7610 let abbrevs = ed.vim.abbrevs.clone();
7611
7612 let Some((lhs_len, rhs)) =
7613 try_abbrev_expand(&abbrevs, &line_before, mincol, trigger, &iskeyword)
7614 else {
7615 return false;
7616 };
7617
7618 let lhs_start = col.saturating_sub(lhs_len);
7620 if lhs_len > 0 {
7621 ed.mutate_edit(Edit::DeleteRange {
7622 start: Position::new(row, lhs_start),
7623 end: Position::new(row, col),
7624 kind: hjkl_buffer::MotionKind::Char,
7625 });
7626 }
7627
7628 let insert_pos = Position::new(row, lhs_start);
7630 if !rhs.is_empty() {
7631 ed.mutate_edit(Edit::InsertStr {
7632 at: insert_pos,
7633 text: rhs.clone(),
7634 });
7635 }
7636
7637 let new_col = lhs_start + rhs.chars().count();
7639 buf_set_cursor_rc(&mut ed.buffer, row, new_col);
7640 ed.push_buffer_cursor_to_textarea();
7641
7642 true
7643}
7644
7645fn word_text_object<H: crate::types::Host>(
7646 ed: &Editor<hjkl_buffer::Buffer, H>,
7647 inner: bool,
7648 big: bool,
7649) -> Option<((usize, usize), (usize, usize))> {
7650 let (row, col) = ed.cursor();
7651 let line = buf_line(&ed.buffer, row)?;
7652 let chars: Vec<char> = line.chars().collect();
7653 if chars.is_empty() {
7654 return None;
7655 }
7656 let at = col.min(chars.len().saturating_sub(1));
7657 let classify = |c: char| -> u8 {
7658 if c.is_whitespace() {
7659 0
7660 } else if big || is_wordchar(c) {
7661 1
7662 } else {
7663 2
7664 }
7665 };
7666 let cls = classify(chars[at]);
7667 let mut start = at;
7668 while start > 0 && classify(chars[start - 1]) == cls {
7669 start -= 1;
7670 }
7671 let mut end = at;
7672 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
7673 end += 1;
7674 }
7675 let char_byte = |i: usize| {
7677 if i >= chars.len() {
7678 line.len()
7679 } else {
7680 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
7681 }
7682 };
7683 let mut start_col = char_byte(start);
7684 let mut end_col = char_byte(end + 1);
7686 if !inner {
7687 let mut t = end + 1;
7689 let mut included_trailing = false;
7690 while t < chars.len() && chars[t].is_whitespace() {
7691 included_trailing = true;
7692 t += 1;
7693 }
7694 if included_trailing {
7695 end_col = char_byte(t);
7696 } else {
7697 let mut s = start;
7698 while s > 0 && chars[s - 1].is_whitespace() {
7699 s -= 1;
7700 }
7701 start_col = char_byte(s);
7702 }
7703 }
7704 Some(((row, start_col), (row, end_col)))
7705}
7706
7707fn quote_text_object<H: crate::types::Host>(
7708 ed: &Editor<hjkl_buffer::Buffer, H>,
7709 q: char,
7710 inner: bool,
7711) -> Option<((usize, usize), (usize, usize))> {
7712 let (row, col) = ed.cursor();
7713 let line = buf_line(&ed.buffer, row)?;
7714 let bytes = line.as_bytes();
7715 let q_byte = q as u8;
7716 let mut positions: Vec<usize> = Vec::new();
7718 for (i, &b) in bytes.iter().enumerate() {
7719 if b == q_byte {
7720 positions.push(i);
7721 }
7722 }
7723 if positions.len() < 2 {
7724 return None;
7725 }
7726 let mut open_idx: Option<usize> = None;
7727 let mut close_idx: Option<usize> = None;
7728 for pair in positions.chunks(2) {
7729 if pair.len() < 2 {
7730 break;
7731 }
7732 if col >= pair[0] && col <= pair[1] {
7733 open_idx = Some(pair[0]);
7734 close_idx = Some(pair[1]);
7735 break;
7736 }
7737 if col < pair[0] {
7738 open_idx = Some(pair[0]);
7739 close_idx = Some(pair[1]);
7740 break;
7741 }
7742 }
7743 let open = open_idx?;
7744 let close = close_idx?;
7745 if inner {
7747 if close <= open + 1 {
7748 return None;
7749 }
7750 Some(((row, open + 1), (row, close)))
7751 } else {
7752 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
7759 let mut end = after_close;
7761 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
7762 end += 1;
7763 }
7764 Some(((row, open), (row, end)))
7765 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
7766 let mut start = open;
7768 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
7769 start -= 1;
7770 }
7771 Some(((row, start), (row, close + 1)))
7772 } else {
7773 Some(((row, open), (row, close + 1)))
7774 }
7775 }
7776}
7777
7778fn bracket_text_object<H: crate::types::Host>(
7779 ed: &Editor<hjkl_buffer::Buffer, H>,
7780 open: char,
7781 inner: bool,
7782 count: usize,
7783) -> Option<(Pos, Pos, RangeKind)> {
7784 let close = match open {
7785 '(' => ')',
7786 '[' => ']',
7787 '{' => '}',
7788 '<' => '>',
7789 _ => return None,
7790 };
7791 let (row, col) = ed.cursor();
7792 let lines = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
7793 let lines = lines.as_slice();
7794 let cursor_char = lines.get(row).and_then(|l| l.chars().nth(col));
7800 let (open_pos, close_pos) = if cursor_char == Some(close) {
7801 let open_pos = if col > 0 {
7802 find_open_bracket(lines, row, col - 1, open, close)
7803 } else if row > 0 {
7804 let pr = row - 1;
7805 let pc = lines[pr].chars().count().saturating_sub(1);
7806 find_open_bracket(lines, pr, pc, open, close)
7807 } else {
7808 None
7809 }?;
7810 (open_pos, (row, col))
7811 } else {
7812 let open_pos = find_open_bracket(lines, row, col, open, close)
7817 .or_else(|| find_next_open(lines, row, col, open))?;
7818 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
7819 (open_pos, close_pos)
7820 };
7821 let (open_pos, close_pos) = {
7825 let (mut op, mut cp) = (open_pos, close_pos);
7826 for _ in 1..count.max(1) {
7827 let outer = if op.1 > 0 {
7828 find_open_bracket(lines, op.0, op.1 - 1, open, close)
7829 } else if op.0 > 0 {
7830 let pr = op.0 - 1;
7831 let pc = lines[pr].chars().count().saturating_sub(1);
7832 find_open_bracket(lines, pr, pc, open, close)
7833 } else {
7834 None
7835 };
7836 let Some(oo) = outer else { break };
7837 let Some(oc) = find_close_bracket(lines, oo.0, oo.1 + 1, open, close) else {
7838 break;
7839 };
7840 op = oo;
7841 cp = oc;
7842 }
7843 (op, cp)
7844 };
7845 if inner {
7847 let open_line_len = lines[open_pos.0].chars().count();
7858 let inner_start = if open_pos.1 + 1 >= open_line_len && open_pos.0 + 1 < lines.len() {
7859 (open_pos.0 + 1, 0)
7860 } else {
7861 advance_pos(lines, open_pos)
7862 };
7863 if inner_start.0 > close_pos.0
7866 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
7867 {
7868 return Some((inner_start, inner_start, RangeKind::Exclusive));
7869 }
7870 if close_pos.0 > open_pos.0 {
7877 let mut saw_ws = false;
7878 let mut saw_other = false;
7879 for r in inner_start.0..=close_pos.0 {
7880 let line: Vec<char> = lines
7881 .get(r)
7882 .map(|l| l.chars().collect())
7883 .unwrap_or_default();
7884 let from = if r == inner_start.0 { inner_start.1 } else { 0 };
7885 let to = if r == close_pos.0 {
7886 close_pos.1
7887 } else {
7888 line.len()
7889 };
7890 for &c in line
7891 .iter()
7892 .take(to.min(line.len()))
7893 .skip(from.min(line.len()))
7894 {
7895 if c == ' ' || c == '\t' {
7896 saw_ws = true;
7897 } else {
7898 saw_other = true;
7899 }
7900 }
7901 }
7902 if saw_ws && !saw_other {
7903 return Some((inner_start, inner_start, RangeKind::Exclusive));
7904 }
7905 }
7906 Some((inner_start, close_pos, RangeKind::Exclusive))
7907 } else {
7908 Some((
7909 open_pos,
7910 advance_pos(lines, close_pos),
7911 RangeKind::Exclusive,
7912 ))
7913 }
7914}
7915
7916fn find_open_bracket(
7917 lines: &[String],
7918 row: usize,
7919 col: usize,
7920 open: char,
7921 close: char,
7922) -> Option<(usize, usize)> {
7923 let mut depth: i32 = 0;
7924 let mut r = row;
7925 let mut c = col as isize;
7926 loop {
7927 let cur = &lines[r];
7928 let chars: Vec<char> = cur.chars().collect();
7929 if (c as usize) >= chars.len() {
7933 c = chars.len() as isize - 1;
7934 }
7935 while c >= 0 {
7936 let ch = chars[c as usize];
7937 if ch == close {
7938 depth += 1;
7939 } else if ch == open {
7940 if depth == 0 {
7941 return Some((r, c as usize));
7942 }
7943 depth -= 1;
7944 }
7945 c -= 1;
7946 }
7947 if r == 0 {
7948 return None;
7949 }
7950 r -= 1;
7951 c = lines[r].chars().count() as isize - 1;
7952 }
7953}
7954
7955fn find_close_bracket(
7956 lines: &[String],
7957 row: usize,
7958 start_col: usize,
7959 open: char,
7960 close: char,
7961) -> Option<(usize, usize)> {
7962 let mut depth: i32 = 0;
7963 let mut r = row;
7964 let mut c = start_col;
7965 loop {
7966 let cur = &lines[r];
7967 let chars: Vec<char> = cur.chars().collect();
7968 while c < chars.len() {
7969 let ch = chars[c];
7970 if ch == open {
7971 depth += 1;
7972 } else if ch == close {
7973 if depth == 0 {
7974 return Some((r, c));
7975 }
7976 depth -= 1;
7977 }
7978 c += 1;
7979 }
7980 if r + 1 >= lines.len() {
7981 return None;
7982 }
7983 r += 1;
7984 c = 0;
7985 }
7986}
7987
7988fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
7992 let mut r = row;
7993 let mut c = col;
7994 while r < lines.len() {
7995 let chars: Vec<char> = lines[r].chars().collect();
7996 while c < chars.len() {
7997 if chars[c] == open {
7998 return Some((r, c));
7999 }
8000 c += 1;
8001 }
8002 r += 1;
8003 c = 0;
8004 }
8005 None
8006}
8007
8008fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
8009 let (r, c) = pos;
8010 let line_len = lines[r].chars().count();
8011 if c < line_len {
8012 (r, c + 1)
8013 } else if r + 1 < lines.len() {
8014 (r + 1, 0)
8015 } else {
8016 pos
8017 }
8018}
8019
8020fn paragraph_text_object<H: crate::types::Host>(
8021 ed: &Editor<hjkl_buffer::Buffer, H>,
8022 inner: bool,
8023) -> Option<((usize, usize), (usize, usize))> {
8024 let (row, _) = ed.cursor();
8025 let rope = crate::types::Query::rope(&ed.buffer);
8026 let n_lines = rope.len_lines();
8027 if n_lines == 0 {
8028 return None;
8029 }
8030 let is_blank = |r: usize| -> bool {
8032 if r >= n_lines {
8033 return true;
8034 }
8035 rope_line_to_str(&rope, r).trim().is_empty()
8036 };
8037 if is_blank(row) {
8038 return None;
8039 }
8040 let mut top = row;
8041 while top > 0 && !is_blank(top - 1) {
8042 top -= 1;
8043 }
8044 let mut bot = row;
8045 while bot + 1 < n_lines && !is_blank(bot + 1) {
8046 bot += 1;
8047 }
8048 if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
8050 bot += 1;
8051 }
8052 let end_col = rope_line_to_str(&rope, bot).chars().count();
8053 Some(((top, 0), (bot, end_col)))
8054}
8055
8056fn read_vim_range<H: crate::types::Host>(
8062 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8063 start: (usize, usize),
8064 end: (usize, usize),
8065 kind: RangeKind,
8066) -> String {
8067 let (top, bot) = order(start, end);
8068 ed.sync_buffer_content_from_textarea();
8069 let rope = crate::types::Query::rope(&ed.buffer);
8070 let n_lines = rope.len_lines();
8071 match kind {
8072 RangeKind::Linewise => {
8073 let lo = top.0;
8074 let hi = bot.0.min(n_lines.saturating_sub(1));
8075 let mut text = rope_row_range_str(&rope, lo, hi);
8076 text.push('\n');
8077 text
8078 }
8079 RangeKind::Inclusive | RangeKind::Exclusive => {
8080 let inclusive = matches!(kind, RangeKind::Inclusive);
8081 let mut out = String::new();
8083 for row in top.0..=bot.0 {
8084 if row >= n_lines {
8085 break;
8086 }
8087 let line = rope_line_to_str(&rope, row);
8088 let lo = if row == top.0 { top.1 } else { 0 };
8089 let hi_unclamped = if row == bot.0 {
8090 if inclusive { bot.1 + 1 } else { bot.1 }
8091 } else {
8092 line.chars().count() + 1
8093 };
8094 let row_chars: Vec<char> = line.chars().collect();
8095 let hi = hi_unclamped.min(row_chars.len());
8096 if lo < hi {
8097 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
8098 }
8099 if row < bot.0 {
8100 out.push('\n');
8101 }
8102 }
8103 out
8104 }
8105 }
8106}
8107
8108fn cut_vim_range<H: crate::types::Host>(
8117 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8118 start: (usize, usize),
8119 end: (usize, usize),
8120 kind: RangeKind,
8121) -> String {
8122 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
8123 let (top, bot) = order(start, end);
8124 ed.sync_buffer_content_from_textarea();
8125 let (buf_start, buf_end, buf_kind) = match kind {
8126 RangeKind::Linewise => (
8127 Position::new(top.0, 0),
8128 Position::new(bot.0, 0),
8129 BufKind::Line,
8130 ),
8131 RangeKind::Inclusive => {
8132 let line_chars = buf_line_chars(&ed.buffer, bot.0);
8133 let next = if bot.1 < line_chars {
8137 Position::new(bot.0, bot.1 + 1)
8138 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
8139 Position::new(bot.0 + 1, 0)
8140 } else {
8141 Position::new(bot.0, line_chars)
8142 };
8143 (Position::new(top.0, top.1), next, BufKind::Char)
8144 }
8145 RangeKind::Exclusive => (
8146 Position::new(top.0, top.1),
8147 Position::new(bot.0, bot.1),
8148 BufKind::Char,
8149 ),
8150 };
8151 let inverse = ed.mutate_edit(Edit::DeleteRange {
8152 start: buf_start,
8153 end: buf_end,
8154 kind: buf_kind,
8155 });
8156 let text = match inverse {
8157 Edit::InsertStr { text, .. } => text,
8158 _ => String::new(),
8159 };
8160 if !text.is_empty() {
8161 ed.record_yank_to_host(text.clone());
8162 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
8163 }
8164 ed.push_buffer_cursor_to_textarea();
8165 text
8166}
8167
8168fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8174 use hjkl_buffer::{Edit, MotionKind, Position};
8175 ed.sync_buffer_content_from_textarea();
8176 let cursor = buf_cursor_pos(&ed.buffer);
8177 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8178 if cursor.col >= line_chars {
8179 return;
8180 }
8181 let inverse = ed.mutate_edit(Edit::DeleteRange {
8182 start: cursor,
8183 end: Position::new(cursor.row, line_chars),
8184 kind: MotionKind::Char,
8185 });
8186 if let Edit::InsertStr { text, .. } = inverse
8187 && !text.is_empty()
8188 {
8189 ed.record_yank_to_host(text.clone());
8190 ed.vim.yank_linewise = false;
8191 ed.set_yank(text);
8192 }
8193 buf_set_cursor_pos(&mut ed.buffer, cursor);
8194 ed.push_buffer_cursor_to_textarea();
8195}
8196
8197fn do_char_delete<H: crate::types::Host>(
8198 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8199 forward: bool,
8200 count: usize,
8201) {
8202 use hjkl_buffer::{Edit, MotionKind, Position};
8203 ed.push_undo();
8204 ed.sync_buffer_content_from_textarea();
8205 let mut deleted = String::new();
8208 for _ in 0..count {
8209 let cursor = buf_cursor_pos(&ed.buffer);
8210 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8211 if forward {
8212 if cursor.col >= line_chars {
8215 continue;
8216 }
8217 let inverse = ed.mutate_edit(Edit::DeleteRange {
8218 start: cursor,
8219 end: Position::new(cursor.row, cursor.col + 1),
8220 kind: MotionKind::Char,
8221 });
8222 if let Edit::InsertStr { text, .. } = inverse {
8223 deleted.push_str(&text);
8224 }
8225 } else {
8226 if cursor.col == 0 {
8228 continue;
8229 }
8230 let inverse = ed.mutate_edit(Edit::DeleteRange {
8231 start: Position::new(cursor.row, cursor.col - 1),
8232 end: cursor,
8233 kind: MotionKind::Char,
8234 });
8235 if let Edit::InsertStr { text, .. } = inverse {
8236 deleted = text + &deleted;
8239 }
8240 }
8241 }
8242 if !deleted.is_empty() {
8243 ed.record_yank_to_host(deleted.clone());
8244 ed.record_delete(deleted, false);
8245 }
8246 ed.push_buffer_cursor_to_textarea();
8247}
8248
8249pub(crate) fn adjust_number<H: crate::types::Host>(
8254 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8255 delta: i64,
8256) -> bool {
8257 use hjkl_buffer::{Edit, MotionKind, Position};
8258 ed.sync_buffer_content_from_textarea();
8259 let cursor = buf_cursor_pos(&ed.buffer);
8260 let row = cursor.row;
8261 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
8262 Some(l) => l.chars().collect(),
8263 None => return false,
8264 };
8265 let len = chars.len();
8266
8267 let is_hex_prefix = |i: usize| {
8270 chars[i] == '0'
8271 && i + 1 < len
8272 && matches!(chars[i + 1], 'x' | 'X')
8273 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
8274 };
8275 let mut i = cursor.col;
8276 let mut hex = false;
8277 loop {
8278 if i >= len {
8279 return false;
8280 }
8281 if is_hex_prefix(i) {
8282 hex = true;
8283 break;
8284 }
8285 if chars[i].is_ascii_digit() {
8286 break;
8287 }
8288 i += 1;
8289 }
8290
8291 let (span_start, span_end, new_s) = if hex {
8292 let digits_start = i + 2;
8294 let mut digits_end = digits_start;
8295 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
8296 digits_end += 1;
8297 }
8298 let hexs: String = chars[digits_start..digits_end].iter().collect();
8299 let Ok(n) = u64::from_str_radix(&hexs, 16) else {
8300 return false;
8301 };
8302 let new_val = (n as i128 + delta as i128).max(0) as u64;
8303 let width = digits_end - digits_start;
8304 let prefix: String = chars[i..digits_start].iter().collect();
8305 (i, digits_end, format!("{prefix}{new_val:0width$x}"))
8306 } else {
8307 let digit_start = i;
8309 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
8310 digit_start - 1
8311 } else {
8312 digit_start
8313 };
8314 let mut span_end = digit_start;
8315 while span_end < len && chars[span_end].is_ascii_digit() {
8316 span_end += 1;
8317 }
8318 let s: String = chars[span_start..span_end].iter().collect();
8319 let Ok(n) = s.parse::<i64>() else {
8320 return false;
8321 };
8322 (span_start, span_end, n.saturating_add(delta).to_string())
8323 };
8324
8325 ed.push_undo();
8326 let span_start_pos = Position::new(row, span_start);
8327 let span_end_pos = Position::new(row, span_end);
8328 ed.mutate_edit(Edit::DeleteRange {
8329 start: span_start_pos,
8330 end: span_end_pos,
8331 kind: MotionKind::Char,
8332 });
8333 ed.mutate_edit(Edit::InsertStr {
8334 at: span_start_pos,
8335 text: new_s.clone(),
8336 });
8337 let new_len = new_s.chars().count();
8338 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
8339 ed.push_buffer_cursor_to_textarea();
8340 true
8341}
8342
8343pub(crate) fn replace_char<H: crate::types::Host>(
8344 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8345 ch: char,
8346 count: usize,
8347) {
8348 use hjkl_buffer::{Edit, MotionKind, Position};
8349 ed.push_undo();
8350 ed.sync_buffer_content_from_textarea();
8351 for _ in 0..count {
8352 let cursor = buf_cursor_pos(&ed.buffer);
8353 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8354 if cursor.col >= line_chars {
8355 break;
8356 }
8357 ed.mutate_edit(Edit::DeleteRange {
8358 start: cursor,
8359 end: Position::new(cursor.row, cursor.col + 1),
8360 kind: MotionKind::Char,
8361 });
8362 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
8363 }
8364 crate::motions::move_left(&mut ed.buffer, 1);
8366 ed.push_buffer_cursor_to_textarea();
8367}
8368
8369fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8370 use hjkl_buffer::{Edit, MotionKind, Position};
8371 ed.sync_buffer_content_from_textarea();
8372 let cursor = buf_cursor_pos(&ed.buffer);
8373 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
8374 return;
8375 };
8376 let toggled = if c.is_uppercase() {
8377 c.to_lowercase().next().unwrap_or(c)
8378 } else {
8379 c.to_uppercase().next().unwrap_or(c)
8380 };
8381 ed.mutate_edit(Edit::DeleteRange {
8382 start: cursor,
8383 end: Position::new(cursor.row, cursor.col + 1),
8384 kind: MotionKind::Char,
8385 });
8386 ed.mutate_edit(Edit::InsertChar {
8387 at: cursor,
8388 ch: toggled,
8389 });
8390}
8391
8392fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8393 use hjkl_buffer::{Edit, Position};
8394 ed.sync_buffer_content_from_textarea();
8395 let row = buf_cursor_pos(&ed.buffer).row;
8396 if row + 1 >= buf_row_count(&ed.buffer) {
8397 return;
8398 }
8399 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8400 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
8401 let next_trimmed = next_raw.trim_start();
8402 let cur_chars = cur_line.chars().count();
8403 let next_chars = next_raw.chars().count();
8404 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
8407 " "
8408 } else {
8409 ""
8410 };
8411 let joined = format!("{cur_line}{separator}{next_trimmed}");
8412 ed.mutate_edit(Edit::Replace {
8413 start: Position::new(row, 0),
8414 end: Position::new(row + 1, next_chars),
8415 with: joined,
8416 });
8417 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
8421 ed.push_buffer_cursor_to_textarea();
8422}
8423
8424fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8427 use hjkl_buffer::Edit;
8428 ed.sync_buffer_content_from_textarea();
8429 let row = buf_cursor_pos(&ed.buffer).row;
8430 if row + 1 >= buf_row_count(&ed.buffer) {
8431 return;
8432 }
8433 let join_col = buf_line_chars(&ed.buffer, row);
8434 ed.mutate_edit(Edit::JoinLines {
8435 row,
8436 count: 1,
8437 with_space: false,
8438 });
8439 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
8441 ed.push_buffer_cursor_to_textarea();
8442}
8443
8444pub(crate) fn visual_join<H: crate::types::Host>(
8448 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8449 with_space: bool,
8450) {
8451 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8452 let (top, bot) = match ed.vim.mode {
8453 Mode::VisualLine => (
8454 cursor_row.min(ed.vim.visual_line_anchor),
8455 cursor_row.max(ed.vim.visual_line_anchor),
8456 ),
8457 Mode::VisualBlock => {
8458 let a = ed.vim.block_anchor.0;
8459 (a.min(cursor_row), a.max(cursor_row))
8460 }
8461 Mode::Visual => {
8462 let a = ed.vim.visual_anchor.0;
8463 (a.min(cursor_row), a.max(cursor_row))
8464 }
8465 _ => return,
8466 };
8467 let joins = (bot - top).max(1);
8470 ed.push_undo();
8471 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8472 ed.push_buffer_cursor_to_textarea();
8473 for _ in 0..joins {
8474 if with_space {
8475 join_line(ed);
8476 } else {
8477 join_line_raw(ed);
8478 }
8479 }
8480 ed.vim.mode = Mode::Normal;
8481 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8482}
8483
8484pub(crate) fn goto_percent<H: crate::types::Host>(
8487 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8488 count: usize,
8489) {
8490 let rows = buf_row_count(&ed.buffer);
8491 if rows == 0 {
8492 return;
8493 }
8494 let total = if rows >= 2
8497 && buf_line(&ed.buffer, rows - 1)
8498 .map(|s| s.is_empty())
8499 .unwrap_or(false)
8500 {
8501 rows - 1
8502 } else {
8503 rows
8504 };
8505 let line = (count * total).div_ceil(100).clamp(1, total);
8507 let pre = ed.cursor();
8508 ed.jump_cursor(line - 1, 0);
8509 move_first_non_whitespace(ed);
8510 ed.sticky_col = Some(ed.cursor().1);
8511 if ed.cursor() != pre {
8512 ed.push_jump(pre);
8513 }
8514}
8515
8516fn indent_width(s: &str, tabstop: usize) -> usize {
8519 let ts = tabstop.max(1);
8520 let mut w = 0usize;
8521 for c in s.chars() {
8522 match c {
8523 ' ' => w += 1,
8524 '\t' => w += ts - (w % ts),
8525 _ => break,
8526 }
8527 }
8528 w
8529}
8530
8531fn build_indent(width: usize, settings: &crate::editor::Settings) -> String {
8534 if settings.expandtab {
8535 return " ".repeat(width);
8536 }
8537 let ts = settings.tabstop.max(1);
8538 let tabs = width / ts;
8539 let spaces = width % ts;
8540 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
8541}
8542
8543fn reindent_block(text: &str, target_width: usize, settings: &crate::editor::Settings) -> String {
8546 let ts = settings.tabstop.max(1);
8547 let lines: Vec<&str> = text.split('\n').collect();
8548 let first_width = lines.first().map(|l| indent_width(l, ts)).unwrap_or(0);
8549 let delta = target_width as isize - first_width as isize;
8550 lines
8551 .iter()
8552 .map(|line| {
8553 let trimmed = line.trim_start_matches([' ', '\t']);
8554 if trimmed.is_empty() {
8555 return String::new();
8557 }
8558 let old_w = indent_width(line, ts) as isize;
8559 let new_w = (old_w + delta).max(0) as usize;
8560 format!("{}{}", build_indent(new_w, settings), trimmed)
8561 })
8562 .collect::<Vec<_>>()
8563 .join("\n")
8564}
8565
8566fn do_paste<H: crate::types::Host>(
8567 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8568 before: bool,
8569 count: usize,
8570 cursor_after: bool,
8571 reindent: bool,
8572) {
8573 use hjkl_buffer::{Edit, Position};
8574 ed.push_undo();
8575 let selector = ed.vim.pending_register.take();
8580 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
8581 Some(slot) => (slot.text.clone(), slot.linewise),
8582 None => {
8588 let s = &ed.registers().unnamed;
8589 (s.text.clone(), s.linewise)
8590 }
8591 };
8592 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
8596 let original_row_for_linewise_after = if linewise && !before {
8602 let r = buf_cursor_pos(&ed.buffer).row;
8605 let (_, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, r, r);
8606 Some(fold_end)
8607 } else {
8608 None
8609 };
8610 for _ in 0..count {
8611 ed.sync_buffer_content_from_textarea();
8612 let yank = yank.clone();
8613 if yank.is_empty() {
8614 continue;
8615 }
8616 if linewise {
8617 let mut text = yank.trim_matches('\n').to_string();
8621 let row = buf_cursor_pos(&ed.buffer).row;
8622 if reindent {
8624 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8625 let target_w = indent_width(&cur_line, ed.settings.tabstop.max(1));
8626 text = reindent_block(&text, target_w, &ed.settings);
8627 }
8628 let (fold_start, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, row, row);
8632 let target_row = if before {
8633 ed.mutate_edit(Edit::InsertStr {
8634 at: Position::new(fold_start, 0),
8635 text: format!("{text}\n"),
8636 });
8637 fold_start
8638 } else {
8639 let line_chars = buf_line_chars(&ed.buffer, fold_end);
8640 ed.mutate_edit(Edit::InsertStr {
8641 at: Position::new(fold_end, line_chars),
8642 text: format!("\n{text}"),
8643 });
8644 fold_end + 1
8645 };
8646 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
8647 crate::motions::move_first_non_blank(&mut ed.buffer);
8648 ed.push_buffer_cursor_to_textarea();
8649 let payload_lines = text.lines().count().max(1);
8651 let bot_row = target_row + payload_lines - 1;
8652 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
8653 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
8654 } else {
8655 let cursor = buf_cursor_pos(&ed.buffer);
8659 let at = if before {
8660 cursor
8661 } else {
8662 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8663 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
8664 };
8665 ed.mutate_edit(Edit::InsertStr {
8666 at,
8667 text: yank.clone(),
8668 });
8669 if !cursor_after && ed.cursor().1 > 0 {
8674 crate::motions::move_left(&mut ed.buffer, 1);
8675 ed.push_buffer_cursor_to_textarea();
8676 }
8677 let lo = (at.row, at.col);
8679 let hi = if cursor_after {
8680 let c = ed.cursor();
8681 (c.0, c.1.saturating_sub(1))
8682 } else {
8683 ed.cursor()
8684 };
8685 paste_mark = Some((lo, hi));
8686 }
8687 }
8688 if let Some((lo, hi)) = paste_mark {
8689 ed.set_mark('[', lo);
8690 ed.set_mark(']', hi);
8691 }
8692 if cursor_after && linewise {
8695 if let Some((_, (bot_row, _))) = paste_mark {
8696 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
8697 let target = (bot_row + 1).min(last_row);
8698 buf_set_cursor_rc(&mut ed.buffer, target, 0);
8699 ed.push_buffer_cursor_to_textarea();
8700 }
8701 } else if let Some(orig_row) = original_row_for_linewise_after {
8702 let first_target = orig_row.saturating_add(1);
8707 buf_set_cursor_rc(&mut ed.buffer, first_target, 0);
8708 crate::motions::move_first_non_blank(&mut ed.buffer);
8709 ed.push_buffer_cursor_to_textarea();
8710 }
8711 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8713}
8714
8715pub(crate) fn visual_paste<H: crate::types::Host>(
8720 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8721 before: bool,
8722) {
8723 use hjkl_buffer::{Edit, Position};
8724 ed.sync_buffer_content_from_textarea();
8725
8726 let selector = ed.vim.pending_register.take();
8729 let (reg_text, reg_linewise) = match selector.and_then(|c| ed.registers().read(c)) {
8730 Some(slot) => (slot.text.clone(), slot.linewise),
8731 None => {
8732 let s = &ed.registers().unnamed;
8733 (s.text.clone(), s.linewise)
8734 }
8735 };
8736 let saved_unnamed = before.then(|| ed.registers().unnamed.clone());
8738
8739 let mode = ed.vim.mode;
8740 ed.push_undo();
8741
8742 match mode {
8743 Mode::VisualLine => {
8744 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8745 let top = cursor_row.min(ed.vim.visual_line_anchor);
8746 let bot = cursor_row.max(ed.vim.visual_line_anchor);
8747 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
8749 let text = reg_text.trim_matches('\n').to_string();
8751 let line_count = buf_row_count(&ed.buffer);
8752 if top >= line_count {
8753 let last = line_count.saturating_sub(1);
8756 let lc = buf_line_chars(&ed.buffer, last);
8757 ed.mutate_edit(Edit::InsertStr {
8758 at: Position::new(last, lc),
8759 text: format!("\n{text}"),
8760 });
8761 buf_set_cursor_rc(&mut ed.buffer, last + 1, 0);
8762 } else {
8763 ed.mutate_edit(Edit::InsertStr {
8764 at: Position::new(top, 0),
8765 text: format!("{text}\n"),
8766 });
8767 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8768 }
8769 crate::motions::move_first_non_blank(&mut ed.buffer);
8770 ed.push_buffer_cursor_to_textarea();
8771 }
8772 Mode::Visual | Mode::VisualBlock => {
8773 let anchor = if mode == Mode::VisualBlock {
8774 ed.vim.block_anchor
8775 } else {
8776 ed.vim.visual_anchor
8777 };
8778 let cursor = ed.cursor();
8779 let (top, bot) = order(anchor, cursor);
8780 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
8782 if reg_linewise {
8784 let text = reg_text.trim_matches('\n').to_string();
8786 let lc = buf_line_chars(&ed.buffer, top.0);
8787 ed.mutate_edit(Edit::InsertStr {
8788 at: Position::new(top.0, lc),
8789 text: format!("\n{text}"),
8790 });
8791 buf_set_cursor_rc(&mut ed.buffer, top.0 + 1, 0);
8792 crate::motions::move_first_non_blank(&mut ed.buffer);
8793 } else {
8794 ed.mutate_edit(Edit::InsertStr {
8795 at: Position::new(top.0, top.1),
8796 text: reg_text.clone(),
8797 });
8798 let inserted_len = reg_text.chars().count();
8800 let last_col = top.1 + inserted_len.saturating_sub(1);
8801 buf_set_cursor_rc(&mut ed.buffer, top.0, last_col);
8802 }
8803 ed.push_buffer_cursor_to_textarea();
8804 }
8805 _ => {}
8806 }
8807
8808 if let Some(slot) = saved_unnamed {
8810 ed.registers_mut().unnamed = slot;
8811 }
8812 ed.vim.mode = Mode::Normal;
8813 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8814}
8815
8816pub(crate) fn adjust_number_visual<H: crate::types::Host>(
8821 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8822 delta: i64,
8823 sequential: bool,
8824) {
8825 use hjkl_buffer::{Edit, MotionKind, Position};
8826 ed.sync_buffer_content_from_textarea();
8827 let mode = ed.vim.mode;
8828 let cursor = buf_cursor_pos(&ed.buffer);
8829
8830 let (top, bot, mut scan_col_first, block_left) = match mode {
8832 Mode::VisualLine => {
8833 let t = cursor.row.min(ed.vim.visual_line_anchor);
8834 let b = cursor.row.max(ed.vim.visual_line_anchor);
8835 (t, b, 0usize, None)
8836 }
8837 Mode::Visual => {
8838 let (a, c) = order(ed.vim.visual_anchor, (cursor.row, cursor.col));
8839 (a.0, c.0, a.1, None)
8840 }
8841 Mode::VisualBlock => {
8842 let (a, c) = order(ed.vim.block_anchor, (cursor.row, cursor.col));
8843 let left = a.1.min(c.1);
8844 (a.0, c.0, left, Some(left))
8845 }
8846 _ => return,
8847 };
8848
8849 ed.push_undo();
8850 let mut found_count: i64 = 0;
8851 for row in top..=bot {
8852 let start_col = match block_left {
8853 Some(left) => left,
8854 None => {
8855 let c = if row == top { scan_col_first } else { 0 };
8858 scan_col_first = 0;
8859 c
8860 }
8861 };
8862 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
8863 Some(l) => l.chars().collect(),
8864 None => continue,
8865 };
8866 let Some(digit_start) =
8867 (start_col.min(chars.len())..chars.len()).find(|&i| chars[i].is_ascii_digit())
8868 else {
8869 continue;
8870 };
8871 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
8872 digit_start - 1
8873 } else {
8874 digit_start
8875 };
8876 let mut span_end = digit_start;
8877 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
8878 span_end += 1;
8879 }
8880 let s: String = chars[span_start..span_end].iter().collect();
8881 let Ok(n) = s.parse::<i64>() else {
8882 continue;
8883 };
8884 found_count += 1;
8885 let this_delta = if sequential {
8886 delta.saturating_mul(found_count)
8887 } else {
8888 delta
8889 };
8890 let new_s = n.saturating_add(this_delta).to_string();
8891 let span_start_pos = Position::new(row, span_start);
8892 let span_end_pos = Position::new(row, span_end);
8893 ed.mutate_edit(Edit::DeleteRange {
8894 start: span_start_pos,
8895 end: span_end_pos,
8896 kind: MotionKind::Char,
8897 });
8898 ed.mutate_edit(Edit::InsertStr {
8899 at: span_start_pos,
8900 text: new_s,
8901 });
8902 }
8903 buf_set_cursor_rc(&mut ed.buffer, top, block_left.unwrap_or(0));
8905 ed.push_buffer_cursor_to_textarea();
8906 ed.vim.mode = Mode::Normal;
8907 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8908}
8909
8910pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8911 if let Some(entry) = ed.undo_stack.pop() {
8912 let (cur_rope, cur_cursor) = ed.snapshot();
8913 ed.redo_stack.push(crate::editor::UndoEntry {
8914 rope: cur_rope,
8915 cursor: cur_cursor,
8916 timestamp: entry.timestamp,
8917 });
8918 ed.restore_rope(entry.rope, entry.cursor);
8919 }
8920 ed.vim.mode = Mode::Normal;
8921 clamp_cursor_to_normal_mode(ed);
8925}
8926
8927pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8928 if let Some(entry) = ed.redo_stack.pop() {
8929 let (cur_rope, cur_cursor) = ed.snapshot();
8930 let before = cur_rope.clone();
8931 ed.undo_stack.push(crate::editor::UndoEntry {
8932 rope: cur_rope,
8933 cursor: cur_cursor,
8934 timestamp: entry.timestamp,
8935 });
8936 ed.cap_undo();
8937 ed.restore_rope(entry.rope, entry.cursor);
8938 let after = crate::types::Query::rope(&ed.buffer);
8942 if let Some((row, col)) = first_diff_pos(&before, &after) {
8943 buf_set_cursor_rc(&mut ed.buffer, row, col);
8944 ed.push_buffer_cursor_to_textarea();
8945 }
8946 }
8947 ed.vim.mode = Mode::Normal;
8948 clamp_cursor_to_normal_mode(ed);
8949}
8950
8951fn first_diff_pos(a: &ropey::Rope, b: &ropey::Rope) -> Option<(usize, usize)> {
8954 let rows = a.len_lines().max(b.len_lines());
8955 for r in 0..rows {
8956 let la = if r < a.len_lines() {
8957 hjkl_buffer::rope_line_str(a, r)
8958 } else {
8959 String::new()
8960 };
8961 let lb = if r < b.len_lines() {
8962 hjkl_buffer::rope_line_str(b, r)
8963 } else {
8964 String::new()
8965 };
8966 if la != lb {
8967 let col = la
8968 .chars()
8969 .zip(lb.chars())
8970 .take_while(|(x, y)| x == y)
8971 .count();
8972 return Some((r, col));
8973 }
8974 }
8975 None
8976}
8977
8978fn replay_insert_and_finish<H: crate::types::Host>(
8985 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8986 text: &str,
8987) {
8988 use hjkl_buffer::{Edit, Position};
8989 let cursor = ed.cursor();
8990 ed.mutate_edit(Edit::InsertStr {
8991 at: Position::new(cursor.0, cursor.1),
8992 text: text.to_string(),
8993 });
8994 if ed.vim.insert_session.take().is_some() {
8995 if ed.cursor().1 > 0 {
8996 crate::motions::move_left(&mut ed.buffer, 1);
8997 ed.push_buffer_cursor_to_textarea();
8998 }
8999 ed.vim.mode = Mode::Normal;
9000 }
9001}
9002
9003pub(crate) fn replay_last_change<H: crate::types::Host>(
9004 ed: &mut Editor<hjkl_buffer::Buffer, H>,
9005 outer_count: usize,
9006) {
9007 let Some(change) = ed.vim.last_change.clone() else {
9008 return;
9009 };
9010 ed.vim.replaying = true;
9011 let scale = if outer_count > 0 { outer_count } else { 1 };
9012 match change {
9013 LastChange::OpMotion {
9014 op,
9015 motion,
9016 count,
9017 inserted,
9018 } => {
9019 let total = count.max(1) * scale;
9020 apply_op_with_motion(ed, op, &motion, total);
9021 if let Some(text) = inserted {
9022 replay_insert_and_finish(ed, &text);
9023 }
9024 }
9025 LastChange::OpTextObj {
9026 op,
9027 obj,
9028 inner,
9029 inserted,
9030 } => {
9031 apply_op_with_text_object(ed, op, obj, inner, 1);
9034 if let Some(text) = inserted {
9035 replay_insert_and_finish(ed, &text);
9036 }
9037 }
9038 LastChange::LineOp {
9039 op,
9040 count,
9041 inserted,
9042 } => {
9043 let total = count.max(1) * scale;
9044 execute_line_op(ed, op, total);
9045 if let Some(text) = inserted {
9046 replay_insert_and_finish(ed, &text);
9047 }
9048 }
9049 LastChange::CharDel { forward, count } => {
9050 do_char_delete(ed, forward, count * scale);
9051 }
9052 LastChange::ReplaceChar { ch, count } => {
9053 replace_char(ed, ch, count * scale);
9054 }
9055 LastChange::ToggleCase { count } => {
9056 for _ in 0..count * scale {
9057 ed.push_undo();
9058 toggle_case_at_cursor(ed);
9059 }
9060 }
9061 LastChange::JoinLine { count } => {
9062 for _ in 0..count * scale {
9063 ed.push_undo();
9064 join_line(ed);
9065 }
9066 }
9067 LastChange::Paste {
9068 before,
9069 count,
9070 cursor_after,
9071 reindent,
9072 } => {
9073 do_paste(ed, before, count * scale, cursor_after, reindent);
9074 }
9075 LastChange::GnOp {
9076 op,
9077 forward,
9078 inserted,
9079 } => {
9080 gn_operate(ed, Some(op), forward, 1);
9081 if let Some(text) = inserted {
9082 replay_insert_and_finish(ed, &text);
9083 }
9084 }
9085 LastChange::ReplaceMode { text } => {
9086 use hjkl_buffer::{Edit, MotionKind, Position};
9087 ed.push_undo();
9088 for ch in text.chars() {
9089 let cursor = buf_cursor_pos(&ed.buffer);
9090 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
9091 if cursor.col < line_chars {
9092 ed.mutate_edit(Edit::DeleteRange {
9094 start: cursor,
9095 end: Position::new(cursor.row, cursor.col + 1),
9096 kind: MotionKind::Char,
9097 });
9098 }
9099 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
9100 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
9101 }
9102 if ed.cursor().1 > 0 {
9104 crate::motions::move_left(&mut ed.buffer, 1);
9105 }
9106 ed.push_buffer_cursor_to_textarea();
9107 }
9108 LastChange::DeleteToEol { inserted } => {
9109 use hjkl_buffer::{Edit, Position};
9110 ed.push_undo();
9111 delete_to_eol(ed);
9112 if let Some(text) = inserted {
9113 let cursor = ed.cursor();
9114 ed.mutate_edit(Edit::InsertStr {
9115 at: Position::new(cursor.0, cursor.1),
9116 text,
9117 });
9118 }
9119 }
9120 LastChange::OpenLine { above, inserted } => {
9121 use hjkl_buffer::{Edit, Position};
9122 ed.push_undo();
9123 ed.sync_buffer_content_from_textarea();
9124 let row = buf_cursor_pos(&ed.buffer).row;
9125 if above {
9126 ed.mutate_edit(Edit::InsertStr {
9127 at: Position::new(row, 0),
9128 text: "\n".to_string(),
9129 });
9130 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
9131 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
9132 } else {
9133 let line_chars = buf_line_chars(&ed.buffer, row);
9134 ed.mutate_edit(Edit::InsertStr {
9135 at: Position::new(row, line_chars),
9136 text: "\n".to_string(),
9137 });
9138 }
9139 ed.push_buffer_cursor_to_textarea();
9140 let cursor = ed.cursor();
9141 ed.mutate_edit(Edit::InsertStr {
9142 at: Position::new(cursor.0, cursor.1),
9143 text: inserted,
9144 });
9145 }
9146 LastChange::InsertAt {
9147 entry,
9148 inserted,
9149 count,
9150 } => {
9151 use hjkl_buffer::{Edit, Position};
9152 ed.push_undo();
9153 match entry {
9154 InsertEntry::I => {}
9155 InsertEntry::ShiftI => move_first_non_whitespace(ed),
9156 InsertEntry::A => {
9157 crate::motions::move_right_to_end(&mut ed.buffer, 1);
9158 ed.push_buffer_cursor_to_textarea();
9159 }
9160 InsertEntry::ShiftA => {
9161 crate::motions::move_line_end(&mut ed.buffer);
9162 crate::motions::move_right_to_end(&mut ed.buffer, 1);
9163 ed.push_buffer_cursor_to_textarea();
9164 }
9165 }
9166 for _ in 0..count.max(1) {
9167 let cursor = ed.cursor();
9168 ed.mutate_edit(Edit::InsertStr {
9169 at: Position::new(cursor.0, cursor.1),
9170 text: inserted.clone(),
9171 });
9172 }
9173 }
9174 }
9175 ed.vim.replaying = false;
9176}
9177
9178fn changed_run(before: &str, after: &str) -> String {
9184 let a: Vec<char> = before.chars().collect();
9185 let b: Vec<char> = after.chars().collect();
9186 let prefix = a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count();
9187 let max_suffix = a.len().min(b.len()) - prefix;
9188 let suffix = a
9189 .iter()
9190 .rev()
9191 .zip(b.iter().rev())
9192 .take(max_suffix)
9193 .take_while(|(x, y)| x == y)
9194 .count();
9195 b[prefix..b.len() - suffix].iter().collect()
9196}
9197
9198fn extract_inserted(before: &str, after: &str) -> String {
9199 let before_chars: Vec<char> = before.chars().collect();
9200 let after_chars: Vec<char> = after.chars().collect();
9201 if after_chars.len() <= before_chars.len() {
9202 return String::new();
9203 }
9204 let prefix = before_chars
9205 .iter()
9206 .zip(after_chars.iter())
9207 .take_while(|(a, b)| a == b)
9208 .count();
9209 let max_suffix = before_chars.len() - prefix;
9210 let suffix = before_chars
9211 .iter()
9212 .rev()
9213 .zip(after_chars.iter().rev())
9214 .take(max_suffix)
9215 .take_while(|(a, b)| a == b)
9216 .count();
9217 after_chars[prefix..after_chars.len() - suffix]
9218 .iter()
9219 .collect()
9220}
9221
9222#[cfg(test)]
9225mod comment_continuation_tests {
9226 use super::*;
9227 use crate::{DefaultHost, Editor, Options};
9228 use hjkl_buffer::Buffer;
9229
9230 fn make_editor_with_lang(lang: &str, content: &str) -> Editor<Buffer, DefaultHost> {
9231 let buf = Buffer::from_str(content);
9232 let host = DefaultHost::new();
9233 let opts = Options {
9234 filetype: lang.to_string(),
9235 formatoptions: "ro".to_string(),
9236 ..Options::default()
9237 };
9238 Editor::new(buf, host, opts)
9239 }
9240
9241 #[test]
9242 fn detect_rust_doc_comment() {
9243 let result = detect_comment_on_line("rust", "/// foo bar");
9244 assert!(result.is_some());
9245 let (indent, prefix) = result.unwrap();
9246 assert_eq!(indent, "");
9247 assert_eq!(prefix, "/// ");
9248 }
9249
9250 #[test]
9251 fn detect_rust_inner_doc_comment() {
9252 let result = detect_comment_on_line("rust", "//! crate docs");
9253 assert!(result.is_some());
9254 let (_, prefix) = result.unwrap();
9255 assert_eq!(prefix, "//! ");
9256 }
9257
9258 #[test]
9259 fn detect_rust_plain_comment() {
9260 let result = detect_comment_on_line("rust", "// normal comment");
9261 assert!(result.is_some());
9262 let (_, prefix) = result.unwrap();
9263 assert_eq!(prefix, "// ");
9264 }
9265
9266 #[test]
9267 fn detect_indented_comment() {
9268 let result = detect_comment_on_line("rust", " // indented");
9269 assert!(result.is_some());
9270 let (indent, prefix) = result.unwrap();
9271 assert_eq!(indent, " ");
9272 assert_eq!(prefix, "// ");
9273 }
9274
9275 #[test]
9276 fn detect_python_hash() {
9277 let result = detect_comment_on_line("python", "# comment");
9278 assert!(result.is_some());
9279 let (_, prefix) = result.unwrap();
9280 assert_eq!(prefix, "# ");
9281 }
9282
9283 #[test]
9284 fn detect_lua_double_dash() {
9285 let result = detect_comment_on_line("lua", "-- a lua comment");
9286 assert!(result.is_some());
9287 let (_, prefix) = result.unwrap();
9288 assert_eq!(prefix, "-- ");
9289 }
9290
9291 #[test]
9292 fn detect_non_comment_is_none() {
9293 assert!(detect_comment_on_line("rust", "let x = 1;").is_none());
9294 assert!(detect_comment_on_line("python", "x = 1").is_none());
9295 }
9296
9297 #[test]
9298 fn detect_bare_double_slash_still_matches() {
9299 assert!(detect_comment_on_line("rust", "//").is_some());
9301 }
9302
9303 #[test]
9304 fn rust_doc_before_plain() {
9305 let result = detect_comment_on_line("rust", "/// outer doc");
9307 let (_, prefix) = result.unwrap();
9308 assert_eq!(prefix, "/// ", "/// must match before //");
9309 }
9310
9311 #[test]
9312 fn continue_comment_returns_prefix_for_comment_row() {
9313 let ed = make_editor_with_lang("rust", "/// hello\n");
9314 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9315 assert_eq!(cont, Some("/// ".to_string()));
9316 }
9317
9318 #[test]
9319 fn continue_comment_returns_none_for_non_comment() {
9320 let ed = make_editor_with_lang("rust", "let x = 1;\n");
9321 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9322 assert!(cont.is_none());
9323 }
9324
9325 #[test]
9326 fn continue_comment_returns_none_when_filetype_empty() {
9327 let buf = Buffer::from_str("// hello\n");
9328 let host = DefaultHost::new();
9329 let ed = Editor::new(buf, host, Options::default());
9331 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9332 assert!(cont.is_none());
9333 }
9334}
9335
9336#[cfg(test)]
9337mod comment_toggle_tests {
9338 use super::*;
9339 use crate::{DefaultHost, Editor, Options};
9340 use hjkl_buffer::Buffer;
9341
9342 fn make_rust_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9343 let buf = Buffer::from_str(content);
9344 let host = DefaultHost::new();
9345 let opts = Options {
9346 filetype: "rust".to_string(),
9347 ..Options::default()
9348 };
9349 Editor::new(buf, host, opts)
9350 }
9351
9352 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9353 buf_line(&ed.buffer, row).unwrap_or_default()
9354 }
9355
9356 #[test]
9359 fn gcc_comments_rust_line() {
9360 let mut ed = make_rust_editor("let x = 1;");
9361 ed.toggle_comment_range(0, 0);
9362 assert_eq!(line(&ed, 0), "// let x = 1;");
9363 }
9364
9365 #[test]
9366 fn gcc_uncomments_rust_line() {
9367 let mut ed = make_rust_editor("// let x = 1;");
9368 ed.toggle_comment_range(0, 0);
9369 assert_eq!(line(&ed, 0), "let x = 1;");
9370 }
9371
9372 #[test]
9373 fn gcc_indent_preserving() {
9374 let mut ed = make_rust_editor(" let x = 1;");
9376 ed.toggle_comment_range(0, 0);
9377 assert_eq!(line(&ed, 0), " // let x = 1;");
9378 }
9379
9380 #[test]
9381 fn gcc_indent_preserving_uncomment() {
9382 let mut ed = make_rust_editor(" // let x = 1;");
9383 ed.toggle_comment_range(0, 0);
9384 assert_eq!(line(&ed, 0), " let x = 1;");
9385 }
9386
9387 #[test]
9390 fn toggle_multi_line_all_uncommented() {
9391 let content = "let a = 1;\nlet b = 2;\nlet c = 3;";
9392 let mut ed = make_rust_editor(content);
9393 ed.toggle_comment_range(0, 2);
9394 assert_eq!(line(&ed, 0), "// let a = 1;");
9395 assert_eq!(line(&ed, 1), "// let b = 2;");
9396 assert_eq!(line(&ed, 2), "// let c = 3;");
9397 }
9398
9399 #[test]
9400 fn toggle_multi_line_all_commented() {
9401 let content = "// let a = 1;\n// let b = 2;\n// let c = 3;";
9402 let mut ed = make_rust_editor(content);
9403 ed.toggle_comment_range(0, 2);
9404 assert_eq!(line(&ed, 0), "let a = 1;");
9405 assert_eq!(line(&ed, 1), "let b = 2;");
9406 assert_eq!(line(&ed, 2), "let c = 3;");
9407 }
9408
9409 #[test]
9412 fn toggle_mixed_state_comments_all() {
9413 let content = "let a = 1;\n// let b = 2;\nlet c = 3;\n// let d = 4;\nlet e = 5;";
9415 let mut ed = make_rust_editor(content);
9416 ed.toggle_comment_range(0, 4);
9417 for r in 0..5 {
9418 assert!(
9419 line(&ed, r).trim_start().starts_with("//"),
9420 "row {r} not commented: {:?}",
9421 line(&ed, r)
9422 );
9423 }
9424 }
9425
9426 #[test]
9429 fn blank_lines_not_commented() {
9430 let content = "let a = 1;\n\nlet b = 2;";
9431 let mut ed = make_rust_editor(content);
9432 ed.toggle_comment_range(0, 2);
9433 assert_eq!(line(&ed, 0), "// let a = 1;");
9434 assert_eq!(line(&ed, 1), ""); assert_eq!(line(&ed, 2), "// let b = 2;");
9436 }
9437
9438 #[test]
9441 fn python_comment_toggle() {
9442 let buf = Buffer::from_str("x = 1\ny = 2");
9443 let host = DefaultHost::new();
9444 let opts = Options {
9445 filetype: "python".to_string(),
9446 ..Options::default()
9447 };
9448 let mut ed = Editor::new(buf, host, opts);
9449 ed.toggle_comment_range(0, 1);
9450 assert_eq!(line(&ed, 0), "# x = 1");
9451 assert_eq!(line(&ed, 1), "# y = 2");
9452 ed.toggle_comment_range(0, 1);
9454 assert_eq!(line(&ed, 0), "x = 1");
9455 assert_eq!(line(&ed, 1), "y = 2");
9456 }
9457
9458 #[test]
9461 fn commentstring_override_via_setting() {
9462 let buf = Buffer::from_str("hello world");
9463 let host = DefaultHost::new();
9464 let opts = Options {
9465 filetype: "rust".to_string(),
9466 ..Options::default()
9467 };
9468 let mut ed = Editor::new(buf, host, opts);
9469 ed.settings_mut().commentstring = "# %s".to_string();
9471 ed.toggle_comment_range(0, 0);
9472 assert_eq!(line(&ed, 0), "# hello world");
9473 }
9474
9475 #[test]
9478 fn unknown_lang_no_op() {
9479 let buf = Buffer::from_str("hello");
9480 let host = DefaultHost::new();
9481 let opts = Options::default(); let mut ed = Editor::new(buf, host, opts);
9483 ed.toggle_comment_range(0, 0);
9484 assert_eq!(line(&ed, 0), "hello");
9486 }
9487}
9488
9489#[cfg(test)]
9492mod g_ampersand_tests {
9493 use super::*;
9494 use crate::{DefaultHost, Editor, Options};
9495 use hjkl_buffer::{Buffer, rope_line_str};
9496
9497 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9498 let buf = Buffer::from_str(content);
9499 let host = DefaultHost::new();
9500 Editor::new(buf, host, Options::default())
9501 }
9502
9503 fn buf_line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9504 let rope = ed.buffer().rope();
9505 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
9506 }
9507
9508 #[test]
9511 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
9512 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
9513 let cmd = crate::substitute::parse_substitute("/foo/bar/").unwrap();
9515 ed.set_last_substitute(cmd);
9516 apply_after_g(&mut ed, '&', 1);
9518 assert_eq!(buf_line(&ed, 0), "bar");
9519 assert_eq!(buf_line(&ed, 1), "bar bar foo");
9521 assert_eq!(buf_line(&ed, 2), "baz");
9522 }
9523
9524 #[test]
9526 fn g_ampersand_with_g_flag_replaces_all_per_line() {
9527 let mut ed = make_editor("foo foo\nfoo");
9528 let cmd = crate::substitute::parse_substitute("/foo/bar/g").unwrap();
9529 ed.set_last_substitute(cmd);
9530 apply_after_g(&mut ed, '&', 1);
9531 assert_eq!(buf_line(&ed, 0), "bar bar");
9532 assert_eq!(buf_line(&ed, 1), "bar");
9533 }
9534
9535 #[test]
9537 fn g_ampersand_noop_when_no_prior_substitute() {
9538 let mut ed = make_editor("foo\nbar");
9539 apply_after_g(&mut ed, '&', 1);
9541 assert_eq!(buf_line(&ed, 0), "foo");
9542 assert_eq!(buf_line(&ed, 1), "bar");
9543 }
9544}
9545
9546#[cfg(test)]
9549mod sneak_tests {
9550 use super::*;
9551 use crate::{DefaultHost, Editor, Options};
9552 use hjkl_buffer::Buffer;
9553
9554 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9555 let buf = Buffer::from_str(content);
9556 let host = DefaultHost::new();
9557 Editor::new(buf, host, Options::default())
9558 }
9559
9560 #[test]
9562 fn sneak_forward_jumps_to_two_char_digraph() {
9563 let mut ed = make_editor("foo bar baz qux\n");
9564 ed.jump_cursor(0, 0);
9565 ed.sneak('b', 'a', true, 1);
9566 assert_eq!(ed.cursor(), (0, 4), "cursor should land on 'ba' in 'bar'");
9567 }
9568
9569 #[test]
9571 fn sneak_backward_jumps_to_prior_match() {
9572 let mut ed = make_editor("foo bar baz qux\n");
9573 ed.jump_cursor(0, 12);
9574 ed.sneak('b', 'a', false, 1);
9575 assert_eq!(
9576 ed.cursor(),
9577 (0, 8),
9578 "backward sneak should find 'ba' in 'baz'"
9579 );
9580 }
9581
9582 #[test]
9584 fn sneak_repeat_semicolon_next_match() {
9585 let mut ed = make_editor("foo bar baz qux\n");
9586 ed.jump_cursor(0, 0);
9587 ed.sneak('b', 'a', true, 1);
9589 assert_eq!(ed.cursor(), (0, 4));
9590 execute_motion(&mut ed, Motion::FindRepeat { reverse: false }, 1);
9592 assert_eq!(ed.cursor(), (0, 8), "semicolon should jump to next 'ba'");
9593 }
9594
9595 #[test]
9597 fn sneak_repeat_comma_prev_match() {
9598 let mut ed = make_editor("foo bar baz qux\n");
9599 ed.jump_cursor(0, 0);
9600 ed.sneak('b', 'a', true, 1);
9601 assert_eq!(ed.cursor(), (0, 4));
9602 let pre = ed.cursor();
9604 execute_motion(&mut ed, Motion::FindRepeat { reverse: true }, 1);
9605 assert_eq!(
9606 ed.cursor(),
9607 pre,
9608 "comma with no prior match should leave cursor unchanged"
9609 );
9610 }
9611
9612 #[test]
9614 fn sneak_s_searches_backward() {
9615 let mut ed = make_editor("foo bar baz qux\n");
9616 ed.jump_cursor(0, 12);
9617 ed.sneak('b', 'a', false, 1);
9618 assert_eq!(ed.cursor(), (0, 8));
9619 }
9620
9621 #[test]
9623 fn sneak_with_count_jumps_to_nth() {
9624 let mut ed = make_editor("foo bar baz qux\n");
9625 ed.jump_cursor(0, 0);
9626 ed.sneak('b', 'a', true, 2);
9627 assert_eq!(ed.cursor(), (0, 8), "count=2 should jump to 2nd 'ba'");
9628 }
9629
9630 #[test]
9632 fn sneak_no_match_cursor_stays() {
9633 let mut ed = make_editor("foo bar baz qux\n");
9634 ed.jump_cursor(0, 0);
9635 let pre = ed.cursor();
9636 ed.sneak('x', 'x', true, 1);
9637 assert_eq!(ed.cursor(), pre, "no match should leave cursor unchanged");
9638 }
9639
9640 #[test]
9642 fn operator_pending_dsab_deletes_to_digraph() {
9643 let mut ed = make_editor("hello ab world\n");
9644 ed.jump_cursor(0, 0);
9645 ed.apply_op_sneak(Operator::Delete, 'a', 'b', true, 1);
9646 let content = ed.content();
9648 assert!(
9649 content.starts_with("ab world"),
9650 "dsab should delete 'hello ' leaving 'ab world'; got: {content:?}"
9651 );
9652 }
9653
9654 #[test]
9656 fn sneak_cross_line_match() {
9657 let mut ed = make_editor("foo\nbar baz\n");
9658 ed.jump_cursor(0, 0);
9659 ed.sneak('b', 'a', true, 1);
9660 assert_eq!(ed.cursor(), (1, 0), "sneak should cross line boundary");
9661 }
9662
9663 #[test]
9665 fn sneak_updates_last_sneak_state() {
9666 let mut ed = make_editor("foo bar baz\n");
9667 ed.jump_cursor(0, 0);
9668 ed.sneak('b', 'a', true, 1);
9669 let ls = ed.last_sneak();
9670 assert_eq!(
9671 ls,
9672 Some((('b', 'a'), true)),
9673 "last_sneak should record the digraph and direction"
9674 );
9675 }
9676}
9677
9678#[cfg(test)]
9687mod indent_count_tests {
9688 use super::*;
9689 use crate::{DefaultHost, Editor, Options};
9690 use hjkl_buffer::Buffer;
9691
9692 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9693 let buf = Buffer::from_str(content);
9694 let mut ed = Editor::new(buf, DefaultHost::new(), Options::default());
9695 ed.settings_mut().expandtab = true;
9696 ed.settings_mut().shiftwidth = 4;
9697 ed
9698 }
9699
9700 fn content(ed: &Editor<Buffer, DefaultHost>) -> String {
9701 (*ed.buffer().content_joined()).clone()
9702 }
9703
9704 #[test]
9705 fn count_indent_operates_on_n_lines() {
9706 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9707 ed.jump_cursor(0, 0);
9708 execute_line_op(&mut ed, Operator::Indent, 3);
9709 assert_eq!(content(&ed), " a\n b\n c\nd\ne\nf\n");
9710 }
9711
9712 #[test]
9713 fn count_indent_clamps_to_buffer_end() {
9714 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9715 ed.jump_cursor(0, 0);
9716 execute_line_op(&mut ed, Operator::Indent, 10);
9717 assert_eq!(content(&ed), " a\n b\n c\n d\n e\n f\n");
9718 }
9719
9720 #[test]
9721 fn count_outdent_clamps_to_buffer_end() {
9722 let mut ed = make_editor(" a\n b\n c\n");
9723 ed.jump_cursor(0, 0);
9724 execute_line_op(&mut ed, Operator::Outdent, 10);
9725 assert_eq!(content(&ed), "a\nb\nc\n");
9726 }
9727
9728 #[test]
9729 fn count_indent_on_last_line_is_noop() {
9730 let mut ed = make_editor("a\nb\nc\n");
9731 ed.jump_cursor(2, 0); execute_line_op(&mut ed, Operator::Indent, 5);
9733 assert_eq!(
9734 content(&ed),
9735 "a\nb\nc\n",
9736 "5>> on last line must abort (E16)"
9737 );
9738 }
9739
9740 #[test]
9741 fn count_indent_on_single_line_is_noop() {
9742 let mut ed = make_editor("x\n");
9743 ed.jump_cursor(0, 0);
9744 execute_line_op(&mut ed, Operator::Indent, 5);
9745 assert_eq!(content(&ed), "x\n", "5>> on the only line must abort (E16)");
9746 }
9747
9748 #[test]
9749 fn count_outdent_on_last_line_is_noop() {
9750 let mut ed = make_editor(" a\n b\n c\n");
9751 ed.jump_cursor(2, 0);
9752 execute_line_op(&mut ed, Operator::Outdent, 5);
9753 assert_eq!(content(&ed), " a\n b\n c\n");
9754 }
9755
9756 #[test]
9757 fn single_indent_on_last_line_still_works() {
9758 let mut ed = make_editor("a\nb\nc\n");
9760 ed.jump_cursor(2, 0);
9761 execute_line_op(&mut ed, Operator::Indent, 1);
9762 assert_eq!(content(&ed), "a\nb\n c\n");
9763 }
9764}
9765
9766#[cfg(test)]
9769mod abbrev_tests {
9770 use super::{Abbrev, AbbrevKind, AbbrevTrigger, abbrev_kind, try_abbrev_expand};
9771 use AbbrevKind::{End, Full, NonKw};
9772
9773 const ISK: &str = "@,48-57,_,192-255"; fn make_abbrev(lhs: &str, rhs: &str) -> Abbrev {
9776 Abbrev {
9777 lhs: lhs.to_string(),
9778 rhs: rhs.to_string(),
9779 insert: true,
9780 cmdline: false,
9781 noremap: false,
9782 }
9783 }
9784
9785 fn expand(
9786 abbrevs: &[Abbrev],
9787 before: &str,
9788 mincol: usize,
9789 trig: AbbrevTrigger,
9790 ) -> Option<(usize, String)> {
9791 try_abbrev_expand(abbrevs, before, mincol, trig, ISK)
9792 }
9793
9794 #[test]
9797 fn fullid_all_keyword_chars() {
9798 assert_eq!(abbrev_kind("teh", ISK), Full);
9799 assert_eq!(abbrev_kind("abc123", ISK), Full);
9800 assert_eq!(abbrev_kind("_foo", ISK), Full);
9801 }
9802
9803 #[test]
9804 fn endid_ends_with_kw_has_nonkw() {
9805 assert_eq!(abbrev_kind("#i", ISK), End);
9806 assert_eq!(abbrev_kind("#include", ISK), End);
9807 }
9808
9809 #[test]
9810 fn nonid_ends_with_nonkw() {
9811 assert_eq!(abbrev_kind(";;", ISK), NonKw);
9812 assert_eq!(abbrev_kind("->", ISK), NonKw);
9813 }
9814
9815 #[test]
9818 fn fullid_expands_on_space_trigger() {
9819 let abbrevs = [make_abbrev("teh", "the")];
9820 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword(' '));
9821 assert_eq!(r, Some((3, "the".to_string())));
9822 }
9823
9824 #[test]
9825 fn fullid_expands_on_esc_trigger() {
9826 let abbrevs = [make_abbrev("teh", "the")];
9827 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Esc);
9828 assert_eq!(r, Some((3, "the".to_string())));
9829 }
9830
9831 #[test]
9832 fn fullid_expands_on_cr_trigger() {
9833 let abbrevs = [make_abbrev("teh", "the")];
9834 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Cr);
9835 assert_eq!(r, Some((3, "the".to_string())));
9836 }
9837
9838 #[test]
9839 fn fullid_expands_on_ctrl_bracket() {
9840 let abbrevs = [make_abbrev("teh", "the")];
9841 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::CtrlBracket);
9842 assert_eq!(r, Some((3, "the".to_string())));
9843 }
9844
9845 #[test]
9846 fn fullid_does_not_expand_on_keyword_trigger() {
9847 let abbrevs = [make_abbrev("teh", "the")];
9849 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword('a'));
9850 assert_eq!(r, None);
9852 }
9853
9854 #[test]
9855 fn fullid_no_expand_when_lhs_not_at_end() {
9856 let abbrevs = [make_abbrev("teh", "the")];
9857 let r = expand(&abbrevs, "ateh", 0, AbbrevTrigger::NonKeyword(' '));
9859 assert_eq!(r, None);
9860 }
9861
9862 #[test]
9863 fn fullid_expands_after_nonkw_prefix() {
9864 let abbrevs = [make_abbrev("teh", "the")];
9865 let r = expand(&abbrevs, "!teh", 0, AbbrevTrigger::NonKeyword(' '));
9867 assert_eq!(r, Some((3, "the".to_string())));
9868 }
9869
9870 #[test]
9871 fn fullid_single_char_no_expand_after_nonblank_nonkw() {
9872 let abbrevs = [make_abbrev("a", "b")];
9873 let r = expand(&abbrevs, "!a", 0, AbbrevTrigger::NonKeyword(' '));
9875 assert_eq!(r, None);
9876 }
9877
9878 #[test]
9879 fn fullid_single_char_expands_after_space() {
9880 let abbrevs = [make_abbrev("a", "b")];
9881 let r = expand(&abbrevs, " a", 0, AbbrevTrigger::NonKeyword(' '));
9883 assert_eq!(r, Some((1, "b".to_string())));
9884 }
9885
9886 #[test]
9889 fn mincol_blocks_consuming_preexisting_text() {
9890 let abbrevs = [make_abbrev("teh", "the")];
9891 let r = expand(&abbrevs, "teh", 3, AbbrevTrigger::NonKeyword(' '));
9893 assert_eq!(r, None);
9894 }
9895
9896 #[test]
9897 fn mincol_allows_match_starting_at_mincol() {
9898 let abbrevs = [make_abbrev("teh", "the")];
9899 let r = expand(&abbrevs, "!! teh", 3, AbbrevTrigger::NonKeyword(' '));
9902 assert_eq!(r, Some((3, "the".to_string())));
9903 }
9904
9905 #[test]
9908 fn endid_expands_on_space_trigger() {
9909 let abbrevs = [make_abbrev("#i", "#include")];
9910 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::NonKeyword(' '));
9911 assert_eq!(r, Some((2, "#include".to_string())));
9912 }
9913
9914 #[test]
9915 fn endid_expands_on_esc_trigger() {
9916 let abbrevs = [make_abbrev("#i", "#include")];
9917 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::Esc);
9918 assert_eq!(r, Some((2, "#include".to_string())));
9919 }
9920
9921 #[test]
9924 fn nonid_expands_on_esc_trigger() {
9925 let abbrevs = [make_abbrev(";;", "std::endl;")];
9926 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Esc);
9927 assert_eq!(r, Some((2, "std::endl;".to_string())));
9928 }
9929
9930 #[test]
9931 fn nonid_expands_on_cr_trigger() {
9932 let abbrevs = [make_abbrev(";;", "std::endl;")];
9933 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Cr);
9934 assert_eq!(r, Some((2, "std::endl;".to_string())));
9935 }
9936
9937 #[test]
9938 fn nonid_does_not_expand_on_nonkw_trigger() {
9939 let abbrevs = [make_abbrev(";;", "std::endl;")];
9941 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::NonKeyword(' '));
9942 assert_eq!(r, None);
9943 }
9944
9945 #[test]
9946 fn nonid_expands_on_ctrl_bracket() {
9947 let abbrevs = [make_abbrev(";;", "std::endl;")];
9948 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::CtrlBracket);
9949 assert_eq!(r, Some((2, "std::endl;".to_string())));
9950 }
9951
9952 #[test]
9955 fn multiword_rhs_expansion() {
9956 let abbrevs = [make_abbrev("hw", "hello world")];
9957 let r = expand(&abbrevs, "hw", 0, AbbrevTrigger::NonKeyword(' '));
9958 assert_eq!(r, Some((2, "hello world".to_string())));
9959 }
9960
9961 #[test]
9964 fn no_match_returns_none() {
9965 let abbrevs = [make_abbrev("teh", "the")];
9966 let r = expand(&abbrevs, "xyz", 0, AbbrevTrigger::NonKeyword(' '));
9967 assert_eq!(r, None);
9968 }
9969
9970 #[test]
9971 fn empty_abbrevs_returns_none() {
9972 let r = expand(&[], "teh", 0, AbbrevTrigger::NonKeyword(' '));
9973 assert_eq!(r, None);
9974 }
9975
9976 #[test]
9977 fn empty_before_text_returns_none() {
9978 let abbrevs = [make_abbrev("teh", "the")];
9979 let r = expand(&abbrevs, "", 0, AbbrevTrigger::NonKeyword(' '));
9980 assert_eq!(r, None);
9981 }
9982}