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 SpaceFwd,
262 BackspaceBack,
265 Up,
266 Down,
267 WordFwd,
268 BigWordFwd,
269 WordBack,
270 BigWordBack,
271 WordEnd,
272 BigWordEnd,
273 WordEndBack,
275 BigWordEndBack,
277 LineStart,
278 FirstNonBlank,
279 LineEnd,
280 FileTop,
281 FileBottom,
282 Find {
283 ch: char,
284 forward: bool,
285 till: bool,
286 },
287 FindRepeat {
288 reverse: bool,
289 },
290 MatchBracket,
291 UnmatchedBracket {
295 forward: bool,
296 open: char,
297 },
298 WordAtCursor {
299 forward: bool,
300 whole_word: bool,
303 },
304 SearchNext {
306 reverse: bool,
307 },
308 ViewportTop,
310 ViewportMiddle,
312 ViewportBottom,
314 LastNonBlank,
316 LineMiddle,
319 ScreenLineMiddle,
322 ParagraphPrev,
324 ParagraphNext,
326 SentencePrev,
328 SentenceNext,
330 ScreenDown,
333 ScreenUp,
335 SectionBackward,
338 SectionForward,
340 SectionEndBackward,
343 SectionEndForward,
345 FirstNonBlankNextLine,
347 FirstNonBlankPrevLine,
349 FirstNonBlankLine,
351 GotoColumn,
354}
355
356#[derive(Debug, Clone, Copy, PartialEq, Eq)]
357pub enum TextObject {
358 Word {
359 big: bool,
360 },
361 Quote(char),
362 Bracket(char),
363 Paragraph,
364 XmlTag,
368 Sentence,
373}
374
375#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum RangeKind {
378 Exclusive,
380 Inclusive,
382 Linewise,
384}
385
386#[derive(Debug, Clone)]
390pub enum LastChange {
391 OpMotion {
393 op: Operator,
394 motion: Motion,
395 count: usize,
396 inserted: Option<String>,
397 },
398 OpTextObj {
400 op: Operator,
401 obj: TextObject,
402 inner: bool,
403 inserted: Option<String>,
404 },
405 LineOp {
407 op: Operator,
408 count: usize,
409 inserted: Option<String>,
410 },
411 CharDel { forward: bool, count: usize },
413 ReplaceChar { ch: char, count: usize },
415 ToggleCase { count: usize },
417 JoinLine { count: usize },
419 Paste {
421 before: bool,
422 count: usize,
423 cursor_after: bool,
425 reindent: bool,
427 },
428 DeleteToEol { inserted: Option<String> },
430 OpenLine { above: bool, inserted: String },
432 InsertAt {
434 entry: InsertEntry,
435 inserted: String,
436 count: usize,
437 },
438 GnOp {
441 op: Operator,
442 forward: bool,
443 inserted: Option<String>,
444 },
445 ReplaceMode { text: String },
447}
448
449#[derive(Debug, Clone, Copy, PartialEq, Eq)]
450pub enum InsertEntry {
451 I,
452 A,
453 ShiftI,
454 ShiftA,
455}
456
457#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
468pub enum LastHorizontalMotion {
469 #[default]
470 None,
471 FindChar,
472 Sneak,
473}
474
475#[derive(Debug, Clone)]
485pub struct Abbrev {
486 pub lhs: String,
487 pub rhs: String,
488 pub insert: bool,
489 pub cmdline: bool,
490 pub noremap: bool,
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub enum AbbrevTrigger {
496 NonKeyword(char),
498 CtrlBracket,
500 Cr,
502 Esc,
504}
505
506#[derive(Default)]
507pub struct VimState {
508 pub mode: Mode,
513 pub pending: Pending,
515 pub count: usize,
518 pub last_find: Option<(char, bool, bool)>,
520 pub last_change: Option<LastChange>,
522 pub insert_session: Option<InsertSession>,
524 pub visual_anchor: (usize, usize),
528 pub visual_line_anchor: usize,
530 pub block_anchor: (usize, usize),
533 pub block_vcol: usize,
539 pub yank_linewise: bool,
541 pub pending_register: Option<char>,
544 pub recording_macro: Option<char>,
548 pub recording_keys: Vec<crate::input::Input>,
553 pub replaying_macro: bool,
556 pub last_macro: Option<char>,
558 pub last_edit_pos: Option<(usize, usize)>,
561 pub last_insert_pos: Option<(usize, usize)>,
565 pub change_list: Vec<(usize, usize)>,
569 pub change_list_cursor: Option<usize>,
572 pub last_visual: Option<LastVisual>,
575 pub viewport_pinned: bool,
579 pub scroll_anim_hint: bool,
582 pub replaying: bool,
584 pub one_shot_normal: bool,
587 pub search_prompt: Option<SearchPrompt>,
589 pub last_search: Option<String>,
593 pub last_search_forward: bool,
597 pub last_insert_text: Option<String>,
600 pub jump_back: Vec<(usize, usize)>,
605 pub jump_fwd: Vec<(usize, usize)>,
608 pub insert_pending_register: bool,
612 pub change_mark_start: Option<(usize, usize)>,
618 pub search_history: Vec<String>,
622 pub search_history_cursor: Option<usize>,
627 pub last_input_at: Option<std::time::Instant>,
636 pub last_input_host_at: Option<core::time::Duration>,
640 pub(crate) current_mode: crate::VimMode,
646 pub(crate) view: crate::ViewMode,
652 pub last_substitute: Option<crate::substitute::SubstituteCmd>,
654 pub pending_closes: Vec<(usize, usize, char)>,
662 pub last_sneak: Option<((char, char), bool)>,
665 pub last_horizontal_motion: LastHorizontalMotion,
668 pub abbrevs: Vec<Abbrev>,
671}
672
673pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
674pub(crate) const CHANGE_LIST_MAX: usize = 100;
675
676#[derive(Debug, Clone)]
679pub struct SearchPrompt {
680 pub text: String,
681 pub cursor: usize,
682 pub forward: bool,
683 pub operator: Option<(Operator, usize, (usize, usize))>,
688}
689
690#[derive(Debug, Clone)]
691pub struct InsertSession {
692 pub count: usize,
693 pub row_min: usize,
695 pub row_max: usize,
696 pub before_rope: ropey::Rope,
701 pub reason: InsertReason,
702 pub start_row: usize,
707 pub start_col: usize,
708}
709
710#[derive(Debug, Clone)]
711pub enum InsertReason {
712 Enter(InsertEntry),
714 Open { above: bool },
716 AfterChange,
719 DeleteToEol,
721 ReplayOnly,
724 BlockEdge { top: usize, bot: usize, col: usize },
728 BlockChange { top: usize, bot: usize, col: usize },
733 Replace,
737}
738
739#[derive(Debug, Clone, Copy)]
749pub struct LastVisual {
750 pub mode: Mode,
751 pub anchor: (usize, usize),
752 pub cursor: (usize, usize),
753 pub block_vcol: usize,
754}
755
756impl VimState {
757 pub fn public_mode(&self) -> VimMode {
758 match self.mode {
759 Mode::Normal => VimMode::Normal,
760 Mode::Insert => VimMode::Insert,
761 Mode::Visual => VimMode::Visual,
762 Mode::VisualLine => VimMode::VisualLine,
763 Mode::VisualBlock => VimMode::VisualBlock,
764 }
765 }
766
767 pub fn force_normal(&mut self) {
768 self.mode = Mode::Normal;
769 self.pending = Pending::None;
770 self.count = 0;
771 self.insert_session = None;
772 self.current_mode = crate::VimMode::Normal;
774 }
775
776 pub(crate) fn clear_pending_prefix(&mut self) {
786 self.pending = Pending::None;
787 self.count = 0;
788 self.pending_register = None;
789 self.insert_pending_register = false;
790 }
791
792 pub(crate) fn widen_insert_row(&mut self, row: usize) {
797 if let Some(ref mut session) = self.insert_session {
798 session.row_min = session.row_min.min(row);
799 session.row_max = session.row_max.max(row);
800 }
801 }
802
803 pub fn is_visual(&self) -> bool {
804 matches!(
805 self.mode,
806 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
807 )
808 }
809
810 pub fn is_visual_char(&self) -> bool {
811 self.mode == Mode::Visual
812 }
813
814 pub(crate) fn pending_count_val(&self) -> Option<u32> {
817 if self.count == 0 {
818 None
819 } else {
820 Some(self.count as u32)
821 }
822 }
823
824 pub(crate) fn is_chord_pending(&self) -> bool {
827 !matches!(self.pending, Pending::None)
828 }
829
830 pub(crate) fn pending_op_char(&self) -> Option<char> {
834 let op = match &self.pending {
835 Pending::Op { op, .. }
836 | Pending::OpTextObj { op, .. }
837 | Pending::OpG { op, .. }
838 | Pending::OpFind { op, .. }
839 | Pending::OpSquareBracketOpen { op, .. }
840 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
841 _ => None,
842 };
843 op.map(|o| match o {
844 Operator::Delete => 'd',
845 Operator::Change => 'c',
846 Operator::Yank => 'y',
847 Operator::Uppercase => 'U',
848 Operator::Lowercase => 'u',
849 Operator::ToggleCase => '~',
850 Operator::Indent => '>',
851 Operator::Outdent => '<',
852 Operator::Fold => 'z',
853 Operator::Reflow => 'q',
854 Operator::ReflowKeepCursor => 'w',
855 Operator::AutoIndent => '=',
856 Operator::Filter => '!',
857 Operator::Comment => 'c',
859 Operator::Rot13 => '?',
861 })
862 }
863}
864
865pub(crate) fn enter_search<H: crate::types::Host>(
871 ed: &mut Editor<hjkl_buffer::Buffer, H>,
872 forward: bool,
873) {
874 ed.vim.search_prompt = Some(SearchPrompt {
875 text: String::new(),
876 cursor: 0,
877 forward,
878 operator: None,
879 });
880 ed.vim.search_history_cursor = None;
881 ed.set_search_pattern(None);
885}
886
887pub(crate) fn enter_search_op<H: crate::types::Host>(
891 ed: &mut Editor<hjkl_buffer::Buffer, H>,
892 forward: bool,
893 op: Operator,
894 count: usize,
895) {
896 let origin = ed.cursor();
897 ed.vim.search_prompt = Some(SearchPrompt {
898 text: String::new(),
899 cursor: 0,
900 forward,
901 operator: Some((op, count.max(1), origin)),
902 });
903 ed.vim.search_history_cursor = None;
904 ed.set_search_pattern(None);
905}
906
907pub(crate) fn apply_op_search_range<H: crate::types::Host>(
911 ed: &mut Editor<hjkl_buffer::Buffer, H>,
912 op: Operator,
913 origin: (usize, usize),
914) {
915 let target = ed.cursor();
916 run_operator_over_range(ed, op, origin, target, RangeKind::Exclusive);
917}
918
919fn walk_change_list<H: crate::types::Host>(
923 ed: &mut Editor<hjkl_buffer::Buffer, H>,
924 dir: isize,
925 count: usize,
926) {
927 if ed.vim.change_list.is_empty() {
928 return;
929 }
930 let len = ed.vim.change_list.len();
931 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
932 (None, -1) => len as isize - 1,
933 (None, 1) => return, (Some(i), -1) => i as isize - 1,
935 (Some(i), 1) => i as isize + 1,
936 _ => return,
937 };
938 for _ in 1..count {
939 let next = idx + dir;
940 if next < 0 || next >= len as isize {
941 break;
942 }
943 idx = next;
944 }
945 if idx < 0 || idx >= len as isize {
946 return;
947 }
948 let idx = idx as usize;
949 ed.vim.change_list_cursor = Some(idx);
950 let (row, col) = ed.vim.change_list[idx];
951 ed.jump_cursor(row, col);
952}
953
954fn insert_register_text<H: crate::types::Host>(
959 ed: &mut Editor<hjkl_buffer::Buffer, H>,
960 selector: char,
961) {
962 use hjkl_buffer::Edit;
963 let text = match selector {
966 '/' => match &ed.vim.last_search {
967 Some(s) if !s.is_empty() => s.clone(),
968 _ => return,
969 },
970 '.' => match &ed.vim.last_insert_text {
971 Some(s) if !s.is_empty() => s.clone(),
972 _ => return,
973 },
974 _ => match ed.registers().read(selector) {
975 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
976 _ => return,
977 },
978 };
979 ed.sync_buffer_content_from_textarea();
980 let cursor = buf_cursor_pos(&ed.buffer);
981 ed.mutate_edit(Edit::InsertStr {
982 at: cursor,
983 text: text.clone(),
984 });
985 let mut row = cursor.row;
988 let mut col = cursor.col;
989 for ch in text.chars() {
990 if ch == '\n' {
991 row += 1;
992 col = 0;
993 } else {
994 col += 1;
995 }
996 }
997 buf_set_cursor_rc(&mut ed.buffer, row, col);
998 ed.push_buffer_cursor_to_textarea();
999 ed.mark_content_dirty();
1000 if let Some(ref mut session) = ed.vim.insert_session {
1001 session.row_min = session.row_min.min(row);
1002 session.row_max = session.row_max.max(row);
1003 }
1004}
1005
1006pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1025 if !settings.autoindent {
1026 return String::new();
1027 }
1028 let base: String = prev_line
1030 .chars()
1031 .take_while(|c| *c == ' ' || *c == '\t')
1032 .collect();
1033
1034 if settings.smartindent {
1035 let unit = if settings.expandtab {
1036 if settings.softtabstop > 0 {
1037 " ".repeat(settings.softtabstop)
1038 } else {
1039 " ".repeat(settings.shiftwidth)
1040 }
1041 } else {
1042 "\t".to_string()
1043 };
1044
1045 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1047 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1048 return format!("{base}{unit}");
1049 }
1050
1051 if is_html_filetype(&settings.filetype) {
1056 let trimmed_end_len = prev_line
1057 .trim_end_matches(|c: char| c.is_whitespace())
1058 .len();
1059 let trimmed = &prev_line[..trimmed_end_len];
1060 if let Some(stripped) = trimmed.strip_suffix('>')
1061 && scan_tag_opener(trimmed, stripped.len()).is_some()
1062 {
1063 return format!("{base}{unit}");
1064 }
1065 }
1066 }
1067
1068 base
1069}
1070
1071fn comment_prefixes_for_lang(lang: &str) -> &'static [&'static str] {
1077 match lang {
1078 "rust" => &["/// ", "//! ", "// "],
1079 "c" | "cpp" => &["// "],
1080 "python" | "sh" | "bash" | "zsh" | "fish" | "toml" | "yaml" => &["# "],
1081 "lua" => &["-- "],
1082 "sql" => &["-- "],
1083 "vim" | "viml" => &["\" "],
1084 _ => &[],
1085 }
1086}
1087
1088pub(crate) fn detect_comment_on_line(lang: &str, line: &str) -> Option<(String, &'static str)> {
1094 let indent_end = line
1095 .char_indices()
1096 .find(|(_, c)| *c != ' ' && *c != '\t')
1097 .map(|(i, _)| i)
1098 .unwrap_or(line.len());
1099 let indent = line[..indent_end].to_string();
1100 let rest = &line[indent_end..];
1101 for &prefix in comment_prefixes_for_lang(lang) {
1102 if rest.starts_with(prefix) {
1103 return Some((indent, prefix));
1104 }
1105 let bare = prefix.trim_end_matches(' ');
1108 if rest == bare || rest.starts_with(&format!("{bare} ")) {
1109 return Some((indent, prefix));
1110 }
1111 }
1112 None
1113}
1114
1115pub(crate) fn continue_comment(
1122 buffer: &hjkl_buffer::Buffer,
1123 settings: &crate::editor::Settings,
1124 row: usize,
1125) -> Option<String> {
1126 if settings.filetype.is_empty() {
1127 return None;
1128 }
1129 let line = crate::buf_helpers::buf_line(buffer, row)?;
1130 let (indent, prefix) = detect_comment_on_line(&settings.filetype, &line)?;
1131 Some(format!("{indent}{prefix}"))
1132}
1133
1134fn try_dedent_close_bracket<H: crate::types::Host>(
1144 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1145 cursor: hjkl_buffer::Position,
1146 ch: char,
1147) -> bool {
1148 use hjkl_buffer::{Edit, MotionKind, Position};
1149
1150 if !ed.settings.smartindent {
1151 return false;
1152 }
1153 if !matches!(ch, '}' | ')' | ']') {
1154 return false;
1155 }
1156
1157 let line = match buf_line(&ed.buffer, cursor.row) {
1158 Some(l) => l.to_string(),
1159 None => return false,
1160 };
1161
1162 let before: String = line.chars().take(cursor.col).collect();
1164 if !before.chars().all(|c| c == ' ' || c == '\t') {
1165 return false;
1166 }
1167 if before.is_empty() {
1168 return false;
1170 }
1171
1172 let unit_len: usize = if ed.settings.expandtab {
1174 if ed.settings.softtabstop > 0 {
1175 ed.settings.softtabstop
1176 } else {
1177 ed.settings.shiftwidth
1178 }
1179 } else {
1180 1
1182 };
1183
1184 let strip_len = if ed.settings.expandtab {
1186 let spaces = before.chars().filter(|c| *c == ' ').count();
1188 if spaces < unit_len {
1189 return false;
1190 }
1191 unit_len
1192 } else {
1193 if !before.starts_with('\t') {
1195 return false;
1196 }
1197 1
1198 };
1199
1200 ed.mutate_edit(Edit::DeleteRange {
1202 start: Position::new(cursor.row, 0),
1203 end: Position::new(cursor.row, strip_len),
1204 kind: MotionKind::Char,
1205 });
1206 let new_col = cursor.col.saturating_sub(strip_len);
1211 ed.mutate_edit(Edit::InsertChar {
1212 at: Position::new(cursor.row, new_col),
1213 ch,
1214 });
1215 true
1216}
1217
1218fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1219 let Some(session) = ed.vim.insert_session.take() else {
1220 return;
1221 };
1222 let after_rope = crate::types::Query::rope(&ed.buffer);
1223 let before_n = session.before_rope.len_lines();
1227 let after_n = after_rope.len_lines();
1228 let after_end = session.row_max.min(after_n.saturating_sub(1));
1229 let before_end = session.row_max.min(before_n.saturating_sub(1));
1230 let before = if before_end >= session.row_min && session.row_min < before_n {
1231 rope_row_range_str(&session.before_rope, session.row_min, before_end)
1232 } else {
1233 String::new()
1234 };
1235 let after = if after_end >= session.row_min && session.row_min < after_n {
1236 rope_row_range_str(&after_rope, session.row_min, after_end)
1237 } else {
1238 String::new()
1239 };
1240 let inserted = if matches!(session.reason, InsertReason::Replace) {
1244 changed_run(&before, &after)
1245 } else {
1246 extract_inserted(&before, &after)
1247 };
1248 if !ed.vim.replaying && !inserted.is_empty() {
1250 ed.vim.last_insert_text = Some(inserted.clone());
1251 }
1252 let open_line = matches!(session.reason, InsertReason::Open { .. });
1253 if session.count > 1 && !ed.vim.replaying {
1254 use hjkl_buffer::{Edit, Position};
1255 if open_line {
1256 let (start_row, _) = ed.cursor();
1261 let typed = buf_line(&ed.buffer, start_row).unwrap_or_default();
1262 for at_row in start_row..start_row + (session.count - 1) {
1263 let end = buf_line_chars(&ed.buffer, at_row);
1264 ed.mutate_edit(Edit::InsertStr {
1265 at: Position::new(at_row, end),
1266 text: format!("\n{typed}"),
1267 });
1268 }
1269 } else if !inserted.is_empty() {
1270 for _ in 0..session.count - 1 {
1272 let (row, col) = ed.cursor();
1273 ed.mutate_edit(Edit::InsertStr {
1274 at: Position::new(row, col),
1275 text: inserted.clone(),
1276 });
1277 }
1278 }
1279 }
1280 fn replicate_block_text<H: crate::types::Host>(
1284 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1285 inserted: &str,
1286 top: usize,
1287 bot: usize,
1288 col: usize,
1289 ) {
1290 use hjkl_buffer::{Edit, Position};
1291 for r in (top + 1)..=bot {
1292 let line_len = buf_line_chars(&ed.buffer, r);
1293 if col > line_len {
1294 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1295 ed.mutate_edit(Edit::InsertStr {
1296 at: Position::new(r, line_len),
1297 text: pad,
1298 });
1299 }
1300 ed.mutate_edit(Edit::InsertStr {
1301 at: Position::new(r, col),
1302 text: inserted.to_string(),
1303 });
1304 }
1305 }
1306
1307 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1308 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1311 replicate_block_text(ed, &inserted, top, bot, col);
1312 buf_set_cursor_rc(&mut ed.buffer, top, col);
1313 ed.push_buffer_cursor_to_textarea();
1314 }
1315 return;
1316 }
1317 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1318 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1322 replicate_block_text(ed, &inserted, top, bot, col);
1323 let ins_chars = inserted.chars().count();
1324 let line_len = buf_line_chars(&ed.buffer, top);
1325 let target_col = (col + ins_chars).min(line_len);
1326 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1327 ed.push_buffer_cursor_to_textarea();
1328 }
1329 return;
1330 }
1331 if ed.vim.replaying {
1332 return;
1333 }
1334 match session.reason {
1335 InsertReason::Enter(entry) => {
1336 ed.vim.last_change = Some(LastChange::InsertAt {
1337 entry,
1338 inserted,
1339 count: session.count,
1340 });
1341 }
1342 InsertReason::Open { above } => {
1343 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1344 }
1345 InsertReason::AfterChange => {
1346 if let Some(
1347 LastChange::OpMotion { inserted: ins, .. }
1348 | LastChange::OpTextObj { inserted: ins, .. }
1349 | LastChange::LineOp { inserted: ins, .. }
1350 | LastChange::GnOp { inserted: ins, .. },
1351 ) = ed.vim.last_change.as_mut()
1352 {
1353 *ins = Some(inserted);
1354 }
1355 if let Some(start) = ed.vim.change_mark_start.take() {
1361 let end = ed.cursor();
1362 ed.set_mark('[', start);
1363 ed.set_mark(']', end);
1364 }
1365 }
1366 InsertReason::DeleteToEol => {
1367 ed.vim.last_change = Some(LastChange::DeleteToEol {
1368 inserted: Some(inserted),
1369 });
1370 }
1371 InsertReason::ReplayOnly => {}
1372 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1373 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1374 InsertReason::Replace => {
1375 ed.vim.last_change = Some(LastChange::ReplaceMode { text: inserted });
1378 }
1379 }
1380}
1381
1382pub(crate) fn begin_insert<H: crate::types::Host>(
1383 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1384 count: usize,
1385 reason: InsertReason,
1386) {
1387 if !ed.settings.modifiable {
1389 return;
1390 }
1391 if ed.vim.view == crate::ViewMode::Blame {
1393 ed.vim.view = crate::ViewMode::Normal;
1394 return;
1395 }
1396 let record = !matches!(reason, InsertReason::ReplayOnly);
1397 if record {
1398 ed.push_undo();
1399 }
1400 let reason = if ed.vim.replaying {
1401 InsertReason::ReplayOnly
1402 } else {
1403 reason
1404 };
1405 let (row, col) = ed.cursor();
1406 ed.vim.insert_session = Some(InsertSession {
1407 count,
1408 row_min: row,
1409 row_max: row,
1410 before_rope: crate::types::Query::rope(&ed.buffer),
1411 reason,
1412 start_row: row,
1413 start_col: col,
1414 });
1415 ed.vim.mode = Mode::Insert;
1416 ed.vim.current_mode = crate::VimMode::Insert;
1418 drop_blame_if_left_normal(ed);
1419}
1420
1421pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1436 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1437) {
1438 if !ed.settings.undo_break_on_motion {
1439 return;
1440 }
1441 if ed.vim.replaying {
1442 return;
1443 }
1444 if ed.vim.insert_session.is_none() {
1445 return;
1446 }
1447 ed.push_undo();
1448 let before_rope = crate::types::Query::rope(&ed.buffer);
1449 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1450 if let Some(ref mut session) = ed.vim.insert_session {
1451 session.before_rope = before_rope;
1452 session.row_min = row;
1453 session.row_max = row;
1454 }
1455}
1456
1457fn autopair_close_for(
1489 ch: char,
1490 filetype: &str,
1491 prev_char: Option<char>,
1492 prev2_char: Option<char>,
1493) -> Option<char> {
1494 let is_triple_quote_third =
1500 matches!(ch, '"' | '`' | '\'') && prev_char == Some(ch) && prev2_char == Some(ch);
1501
1502 match ch {
1503 '(' => Some(')'),
1504 '[' => Some(']'),
1505 '{' => Some('}'),
1506 '"' => {
1507 if is_triple_quote_third {
1508 None
1509 } else {
1510 Some('"')
1511 }
1512 }
1513 '`' => {
1514 if is_triple_quote_third {
1515 None
1516 } else {
1517 Some('`')
1518 }
1519 }
1520 '<' => {
1521 if is_html_filetype(filetype) {
1522 Some('>')
1523 } else {
1524 None
1525 }
1526 }
1527 '\'' => {
1528 if is_triple_quote_third {
1529 return None;
1530 }
1531 if prev_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
1534 None
1535 } else {
1536 Some('\'')
1537 }
1538 }
1539 _ => None,
1540 }
1541}
1542
1543fn detect_code_fence_opener(line: &str, cursor_col: usize) -> Option<String> {
1558 if cursor_col != line.chars().count() {
1559 return None;
1560 }
1561 let trimmed = line.trim_start();
1562 let backtick_run = trimmed.chars().take_while(|c| *c == '`').count();
1563 if backtick_run < 3 {
1564 return None;
1565 }
1566 let rest = &trimmed[backtick_run..];
1567 if rest.is_empty() {
1568 return None;
1569 }
1570 let all_lang_chars = rest
1571 .chars()
1572 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-');
1573 if !all_lang_chars {
1574 return None;
1575 }
1576 Some("`".repeat(backtick_run))
1577}
1578
1579fn is_html_filetype(ft: &str) -> bool {
1581 matches!(
1582 ft,
1583 "html" | "xml" | "svg" | "jsx" | "tsx" | "vue" | "svelte"
1584 )
1585}
1586
1587#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1603enum TagKind {
1604 Open,
1605 Close,
1606}
1607
1608#[derive(Debug, Clone, PartialEq, Eq)]
1610struct TagSpan {
1611 kind: TagKind,
1612 name: String,
1613 row: usize,
1615 name_start_col: usize,
1617 name_end_col: usize,
1618}
1619
1620fn detect_tag_at_cursor(line: &str, row: usize, col: usize) -> Option<TagSpan> {
1624 let chars: Vec<char> = line.chars().collect();
1625 let mut lt = None;
1627 let mut i = col.min(chars.len());
1628 while i > 0 {
1629 i -= 1;
1630 let c = chars[i];
1631 if c == '<' {
1632 lt = Some(i);
1633 break;
1634 }
1635 if c == '>' {
1637 return None;
1638 }
1639 }
1640 let lt = lt?;
1641 let (kind, name_start) = if chars.get(lt + 1) == Some(&'/') {
1643 (TagKind::Close, lt + 2)
1644 } else {
1645 (TagKind::Open, lt + 1)
1646 };
1647 let first = chars.get(name_start)?;
1649 if !first.is_ascii_alphabetic() {
1650 return None;
1651 }
1652 let mut name_end = name_start;
1654 while name_end < chars.len()
1655 && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-')
1656 {
1657 name_end += 1;
1658 }
1659 if col < name_start || col > name_end {
1663 return None;
1664 }
1665 let name: String = chars[name_start..name_end].iter().collect();
1666 Some(TagSpan {
1667 kind,
1668 name,
1669 row,
1670 name_start_col: name_start,
1671 name_end_col: name_end,
1672 })
1673}
1674
1675fn find_matching_tag(buffer: &hjkl_buffer::Buffer, anchor: &TagSpan) -> Option<TagSpan> {
1689 let row_count = buffer.row_count();
1690 let scan_forward = anchor.kind == TagKind::Open;
1691 let row_iter: Box<dyn Iterator<Item = usize>> = if scan_forward {
1692 Box::new(anchor.row..row_count)
1693 } else {
1694 Box::new((0..=anchor.row).rev())
1695 };
1696 let push_kind = if scan_forward {
1697 TagKind::Open
1698 } else {
1699 TagKind::Close
1700 };
1701 let mut depth: usize = 1;
1702
1703 for r in row_iter {
1704 let line = buf_line(buffer, r)?;
1705 let chars: Vec<char> = line.chars().collect();
1706 let tags = scan_line_tags(&chars, r);
1707 let tags_iter: Box<dyn Iterator<Item = TagSpan>> = if scan_forward {
1708 Box::new(tags.into_iter())
1709 } else {
1710 Box::new(tags.into_iter().rev())
1711 };
1712 for tag in tags_iter {
1713 if r == anchor.row
1715 && tag.name_start_col == anchor.name_start_col
1716 && tag.kind == anchor.kind
1717 {
1718 continue;
1719 }
1720 if r == anchor.row {
1724 if scan_forward && tag.name_start_col < anchor.name_start_col {
1725 continue;
1726 }
1727 if !scan_forward && tag.name_start_col > anchor.name_start_col {
1728 continue;
1729 }
1730 }
1731 if tag.kind == push_kind {
1732 depth += 1;
1733 } else {
1734 depth -= 1;
1735 if depth == 0 {
1736 return Some(tag);
1737 }
1738 }
1739 }
1740 }
1741 None
1742}
1743
1744fn scan_line_tags(chars: &[char], row: usize) -> Vec<TagSpan> {
1748 let mut out = Vec::new();
1749 let n = chars.len();
1750 let mut i = 0;
1751 while i < n {
1752 if chars[i] != '<' {
1753 i += 1;
1754 continue;
1755 }
1756 if chars[i..].starts_with(&['<', '!', '-', '-']) {
1758 let mut j = i + 4;
1759 while j + 2 < n && !(chars[j] == '-' && chars[j + 1] == '-' && chars[j + 2] == '>') {
1760 j += 1;
1761 }
1762 i = (j + 3).min(n);
1763 continue;
1764 }
1765 let (kind, name_start) = if chars.get(i + 1) == Some(&'/') {
1766 (TagKind::Close, i + 2)
1767 } else {
1768 (TagKind::Open, i + 1)
1769 };
1770 if chars
1772 .get(name_start)
1773 .is_none_or(|c| !c.is_ascii_alphabetic())
1774 {
1775 i += 1;
1776 continue;
1777 }
1778 let mut name_end = name_start;
1779 while name_end < n && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-') {
1780 name_end += 1;
1781 }
1782 let mut k = name_end;
1784 let mut self_closing = false;
1785 while k < n {
1786 if chars[k] == '>' {
1787 if k > name_end && chars[k - 1] == '/' {
1788 self_closing = true;
1789 }
1790 break;
1791 }
1792 k += 1;
1793 }
1794 if k >= n {
1795 break;
1797 }
1798 let name: String = chars[name_start..name_end].iter().collect();
1799 if !(self_closing || kind == TagKind::Open && is_void_element(&name)) {
1801 out.push(TagSpan {
1802 kind,
1803 name,
1804 row,
1805 name_start_col: name_start,
1806 name_end_col: name_end,
1807 });
1808 }
1809 i = k + 1;
1810 }
1811 out
1812}
1813
1814pub(crate) fn sync_paired_tag_on_exit<H: crate::types::Host>(
1819 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1820) {
1821 if !is_html_filetype(&ed.settings.filetype) {
1822 return;
1823 }
1824 let (row, col) = ed.cursor();
1825 let line = match buf_line(&ed.buffer, row) {
1826 Some(l) => l,
1827 None => return,
1828 };
1829 let anchor = match detect_tag_at_cursor(&line, row, col) {
1830 Some(t) => t,
1831 None => return,
1832 };
1833 let partner = match find_matching_tag(&ed.buffer, &anchor) {
1834 Some(t) => t,
1835 None => return,
1836 };
1837 if partner.name == anchor.name {
1838 return;
1839 }
1840 use hjkl_buffer::{Edit, MotionKind, Position};
1842 let start = Position::new(partner.row, partner.name_start_col);
1843 let end = Position::new(partner.row, partner.name_end_col);
1844 ed.mutate_edit(Edit::DeleteRange {
1845 start,
1846 end,
1847 kind: MotionKind::Char,
1848 });
1849 ed.mutate_edit(Edit::InsertStr {
1850 at: start,
1851 text: anchor.name.clone(),
1852 });
1853 buf_set_cursor_rc(&mut ed.buffer, row, col);
1856 ed.push_buffer_cursor_to_textarea();
1857}
1858
1859pub fn matching_tag_pair(
1865 buffer: &hjkl_buffer::Buffer,
1866 row: usize,
1867 col: usize,
1868) -> Option<[(usize, usize, usize); 2]> {
1869 let line = buf_line(buffer, row)?;
1870 let anchor = detect_tag_at_cursor(&line, row, col)?;
1871 let partner = find_matching_tag(buffer, &anchor)?;
1872 Some([
1873 (anchor.row, anchor.name_start_col, anchor.name_end_col),
1874 (partner.row, partner.name_start_col, partner.name_end_col),
1875 ])
1876}
1877
1878fn is_void_element(tag: &str) -> bool {
1880 matches!(
1881 tag.to_ascii_lowercase().as_str(),
1882 "area"
1883 | "base"
1884 | "br"
1885 | "col"
1886 | "embed"
1887 | "hr"
1888 | "img"
1889 | "input"
1890 | "link"
1891 | "meta"
1892 | "param"
1893 | "source"
1894 | "track"
1895 | "wbr"
1896 )
1897}
1898
1899fn scan_tag_opener(line: &str, col: usize) -> Option<String> {
1909 let before = if col > 0 { &line[..col] } else { return None };
1912
1913 let lt_pos = before.rfind('<')?;
1915 let inner = &before[lt_pos + 1..]; if inner.starts_with('!') {
1919 return None;
1920 }
1921 if inner.trim_end().ends_with('/') {
1923 return None;
1924 }
1925
1926 let tag: String = inner
1928 .chars()
1929 .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1930 .collect();
1931 if tag.is_empty() {
1932 return None;
1933 }
1934 if !tag
1936 .chars()
1937 .next()
1938 .map(|c| c.is_ascii_alphabetic())
1939 .unwrap_or(false)
1940 {
1941 return None;
1942 }
1943 if is_void_element(&tag) {
1944 return None;
1945 }
1946 Some(tag)
1947}
1948
1949pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1954 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1955 ch: char,
1956) -> bool {
1957 use hjkl_buffer::{Edit, MotionKind, Position};
1958 ed.sync_buffer_content_from_textarea();
1959 let in_replace = matches!(
1960 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1961 Some(InsertReason::Replace)
1962 );
1963
1964 if !in_replace && !ed.vim.abbrevs.is_empty() {
1971 let iskeyword = ed.settings.iskeyword.clone();
1972 if !is_keyword_char(ch, &iskeyword) {
1973 check_and_apply_abbrev(ed, AbbrevTrigger::NonKeyword(ch));
1975 }
1977 }
1978 let cursor = buf_cursor_pos(&ed.buffer);
1980 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1981
1982 if !in_replace
1991 && !ed.vim.pending_closes.is_empty()
1992 && let Some(&(pr, _pc, pch)) = ed.vim.pending_closes.last()
1993 && ch == pch
1994 && cursor.row == pr
1995 {
1996 let char_at_cursor =
1997 buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col));
1998 if char_at_cursor == Some(ch) {
1999 ed.vim.pending_closes.pop();
2000 let filetype = ed.settings.filetype.clone();
2002 let autoclose_tag = ed.settings.autoclose_tag;
2003 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
2004 let new_col = cursor.col + 1;
2006 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
2007 if let Some(line) = buf_line(&ed.buffer, cursor.row) {
2011 let char_col = new_col.saturating_sub(1);
2012 let byte_col = line
2013 .char_indices()
2014 .nth(char_col)
2015 .map(|(b, _)| b)
2016 .unwrap_or(line.len());
2017 if let Some(tag) = scan_tag_opener(&line, byte_col) {
2018 let close_tag = format!("</{tag}>");
2019 let insert_pos = Position::new(cursor.row, new_col);
2020 ed.mutate_edit(Edit::InsertStr {
2021 at: insert_pos,
2022 text: close_tag,
2023 });
2024 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
2026 }
2027 }
2028 } else {
2029 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
2030 }
2031 ed.push_buffer_cursor_to_textarea();
2032 return true;
2033 }
2034 }
2035
2036 if in_replace && cursor.col < line_chars {
2037 ed.vim.pending_closes.clear();
2039 ed.mutate_edit(Edit::DeleteRange {
2040 start: cursor,
2041 end: Position::new(cursor.row, cursor.col + 1),
2042 kind: MotionKind::Char,
2043 });
2044 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2045 } else if !try_dedent_close_bracket(ed, cursor, ch) {
2046 let autopair = ed.settings.autopair;
2048 let filetype = ed.settings.filetype.clone();
2049 let autoclose_tag = ed.settings.autoclose_tag;
2050
2051 let (prev_char, prev2_char) = {
2052 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2053 let chars: Vec<char> = line.chars().collect();
2054 let p1 = if cursor.col > 0 {
2055 chars.get(cursor.col - 1).copied()
2056 } else {
2057 None
2058 };
2059 let p2 = if cursor.col > 1 {
2060 chars.get(cursor.col - 2).copied()
2061 } else {
2062 None
2063 };
2064 (p1, p2)
2065 };
2066
2067 if autopair {
2068 if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
2069 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2071 let after = Position::new(cursor.row, cursor.col + 1);
2074 ed.mutate_edit(Edit::InsertChar {
2075 at: after,
2076 ch: close,
2077 });
2078 let between_col = cursor.col + 1;
2081 buf_set_cursor_rc(&mut ed.buffer, cursor.row, between_col);
2082 ed.vim.pending_closes.push((cursor.row, between_col, close));
2087 ed.push_buffer_cursor_to_textarea();
2088 return true;
2089 }
2090
2091 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
2095 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2096 let new_col = cursor.col + 1;
2097 if let Some(line) = buf_line(&ed.buffer, cursor.row) {
2102 let char_col = new_col.saturating_sub(1);
2103 let byte_col = line
2104 .char_indices()
2105 .nth(char_col)
2106 .map(|(b, _)| b)
2107 .unwrap_or(line.len());
2108 if let Some(tag) = scan_tag_opener(&line, byte_col) {
2109 let close_tag = format!("</{tag}>");
2110 let insert_pos = Position::new(cursor.row, new_col);
2111 ed.mutate_edit(Edit::InsertStr {
2112 at: insert_pos,
2113 text: close_tag,
2114 });
2115 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
2117 }
2118 }
2119 ed.push_buffer_cursor_to_textarea();
2120 return true;
2121 }
2122 }
2123
2124 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
2129 }
2130 ed.push_buffer_cursor_to_textarea();
2131 true
2132}
2133
2134pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
2140 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2141) -> bool {
2142 use hjkl_buffer::Edit;
2143 ed.sync_buffer_content_from_textarea();
2144
2145 if !ed.vim.abbrevs.is_empty() {
2149 check_and_apply_abbrev(ed, AbbrevTrigger::Cr);
2150 }
2151
2152 let cursor = buf_cursor_pos(&ed.buffer);
2153 let prev_line = buf_line(&ed.buffer, cursor.row)
2154 .unwrap_or_default()
2155 .to_string();
2156
2157 if ed.settings.autopair && !ed.vim.pending_closes.is_empty() {
2161 let prev_char = if cursor.col > 0 {
2164 prev_line.chars().nth(cursor.col - 1)
2165 } else {
2166 None
2167 };
2168 let next_char = prev_line.chars().nth(cursor.col);
2169 let is_open_pair = matches!(
2170 (prev_char, next_char),
2171 (Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
2172 );
2173 if is_open_pair {
2174 ed.vim.pending_closes.clear();
2177 let base_indent: String = prev_line
2179 .chars()
2180 .take_while(|c| *c == ' ' || *c == '\t')
2181 .collect();
2182 let inner_indent = if ed.settings.expandtab {
2183 let unit = if ed.settings.softtabstop > 0 {
2184 ed.settings.softtabstop
2185 } else {
2186 ed.settings.shiftwidth
2187 };
2188 format!("{base_indent}{}", " ".repeat(unit))
2189 } else {
2190 format!("{base_indent}\t")
2191 };
2192 let text = format!("\n{inner_indent}\n{base_indent}");
2195 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2196 let new_row = cursor.row + 1;
2198 let new_col = inner_indent.len();
2199 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
2200 ed.push_buffer_cursor_to_textarea();
2201 return true;
2202 }
2203 }
2204
2205 if ed.settings.autopair
2214 && let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
2215 {
2216 ed.vim.pending_closes.clear();
2217 let base_indent: String = prev_line
2218 .chars()
2219 .take_while(|c| *c == ' ' || *c == '\t')
2220 .collect();
2221 let text = format!("\n{base_indent}\n{base_indent}{fence}");
2222 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2223 let new_row = cursor.row + 1;
2224 let new_col = base_indent.chars().count();
2225 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
2226 ed.push_buffer_cursor_to_textarea();
2227 return true;
2228 }
2229
2230 let comment_cont = if ed.settings.formatoptions.contains('r') {
2232 continue_comment(&ed.buffer, &ed.settings, cursor.row)
2233 } else {
2234 None
2235 };
2236
2237 ed.vim.pending_closes.clear();
2239
2240 let text = if let Some(cont) = comment_cont {
2241 format!("\n{cont}")
2244 } else {
2245 let indent = compute_enter_indent(&ed.settings, &prev_line);
2246 format!("\n{indent}")
2247 };
2248 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
2249 ed.push_buffer_cursor_to_textarea();
2250 true
2251}
2252
2253pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
2256 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2257) -> bool {
2258 use hjkl_buffer::Edit;
2259 ed.sync_buffer_content_from_textarea();
2260 let cursor = buf_cursor_pos(&ed.buffer);
2261 if ed.settings.expandtab {
2262 let sts = ed.settings.softtabstop;
2263 let n = if sts > 0 {
2264 sts - (cursor.col % sts)
2265 } else {
2266 ed.settings.tabstop.max(1)
2267 };
2268 ed.mutate_edit(Edit::InsertStr {
2269 at: cursor,
2270 text: " ".repeat(n),
2271 });
2272 } else {
2273 ed.mutate_edit(Edit::InsertChar {
2274 at: cursor,
2275 ch: '\t',
2276 });
2277 }
2278 ed.push_buffer_cursor_to_textarea();
2279 true
2280}
2281
2282pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
2293 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2294) -> bool {
2295 use hjkl_buffer::{Edit, MotionKind, Position};
2296 ed.sync_buffer_content_from_textarea();
2297 let cursor = buf_cursor_pos(&ed.buffer);
2298
2299 if cursor.col > 0 {
2302 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2303 if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings.filetype, &line) {
2304 let full_prefix = format!("{indent}{prefix}");
2305 let line_trimmed = line.trim_end_matches(' ');
2308 let prefix_trimmed = full_prefix.trim_end_matches(' ');
2309 if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
2310 ed.mutate_edit(Edit::DeleteRange {
2312 start: Position::new(cursor.row, 0),
2313 end: cursor,
2314 kind: MotionKind::Char,
2315 });
2316 ed.push_buffer_cursor_to_textarea();
2317 return true;
2318 }
2319 }
2320 }
2321
2322 let sts = ed.settings.softtabstop;
2323 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
2324 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2325 let chars: Vec<char> = line.chars().collect();
2326 let run_start = cursor.col - sts;
2327 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
2328 ed.mutate_edit(Edit::DeleteRange {
2329 start: Position::new(cursor.row, run_start),
2330 end: cursor,
2331 kind: MotionKind::Char,
2332 });
2333 ed.push_buffer_cursor_to_textarea();
2334 return true;
2335 }
2336 }
2337 let result = if cursor.col > 0 {
2338 ed.mutate_edit(Edit::DeleteRange {
2339 start: Position::new(cursor.row, cursor.col - 1),
2340 end: cursor,
2341 kind: MotionKind::Char,
2342 });
2343 true
2344 } else if cursor.row > 0 {
2345 let prev_row = cursor.row - 1;
2346 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2347 ed.mutate_edit(Edit::JoinLines {
2348 row: prev_row,
2349 count: 1,
2350 with_space: false,
2351 });
2352 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2353 true
2354 } else {
2355 false
2356 };
2357 ed.push_buffer_cursor_to_textarea();
2358 result
2359}
2360
2361pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
2364 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2365) -> bool {
2366 use hjkl_buffer::{Edit, MotionKind, Position};
2367 ed.sync_buffer_content_from_textarea();
2368 let cursor = buf_cursor_pos(&ed.buffer);
2369 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2370 let result = if cursor.col < line_chars {
2371 ed.mutate_edit(Edit::DeleteRange {
2372 start: cursor,
2373 end: Position::new(cursor.row, cursor.col + 1),
2374 kind: MotionKind::Char,
2375 });
2376 buf_set_cursor_pos(&mut ed.buffer, cursor);
2377 true
2378 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
2379 ed.mutate_edit(Edit::JoinLines {
2380 row: cursor.row,
2381 count: 1,
2382 with_space: false,
2383 });
2384 buf_set_cursor_pos(&mut ed.buffer, cursor);
2385 true
2386 } else {
2387 false
2388 };
2389 ed.push_buffer_cursor_to_textarea();
2390 result
2391}
2392
2393#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2395pub enum InsertDir {
2396 Left,
2397 Right,
2398 Up,
2399 Down,
2400}
2401
2402pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
2406 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2407 dir: InsertDir,
2408) -> bool {
2409 ed.sync_buffer_content_from_textarea();
2410 ed.vim.pending_closes.clear();
2411 match dir {
2412 InsertDir::Left => {
2413 crate::motions::move_left(&mut ed.buffer, 1);
2414 }
2415 InsertDir::Right => {
2416 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2417 }
2418 InsertDir::Up => {
2419 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2420 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2421 }
2422 InsertDir::Down => {
2423 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2424 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2425 }
2426 }
2427 break_undo_group_in_insert(ed);
2428 ed.push_buffer_cursor_to_textarea();
2429 false
2430}
2431
2432pub(crate) fn insert_home_bridge<H: crate::types::Host>(
2435 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2436) -> bool {
2437 ed.sync_buffer_content_from_textarea();
2438 ed.vim.pending_closes.clear();
2439 crate::motions::move_line_start(&mut ed.buffer);
2440 break_undo_group_in_insert(ed);
2441 ed.push_buffer_cursor_to_textarea();
2442 false
2443}
2444
2445pub(crate) fn insert_end_bridge<H: crate::types::Host>(
2448 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2449) -> bool {
2450 ed.sync_buffer_content_from_textarea();
2451 ed.vim.pending_closes.clear();
2452 crate::motions::move_line_end(&mut ed.buffer);
2453 break_undo_group_in_insert(ed);
2454 ed.push_buffer_cursor_to_textarea();
2455 false
2456}
2457
2458pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
2461 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2462 viewport_h: u16,
2463) -> bool {
2464 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2465 scroll_cursor_rows(ed, -rows);
2466 false
2467}
2468
2469pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
2472 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2473 viewport_h: u16,
2474) -> bool {
2475 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2476 scroll_cursor_rows(ed, rows);
2477 false
2478}
2479
2480pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
2484 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2485) -> bool {
2486 use hjkl_buffer::{Edit, MotionKind};
2487 ed.sync_buffer_content_from_textarea();
2488 let cursor = buf_cursor_pos(&ed.buffer);
2489 if cursor.row == 0 && cursor.col == 0 {
2490 return true;
2491 }
2492 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
2493 let word_start = buf_cursor_pos(&ed.buffer);
2494 if word_start == cursor {
2495 return true;
2496 }
2497 buf_set_cursor_pos(&mut ed.buffer, cursor);
2498 ed.mutate_edit(Edit::DeleteRange {
2499 start: word_start,
2500 end: cursor,
2501 kind: MotionKind::Char,
2502 });
2503 ed.push_buffer_cursor_to_textarea();
2504 true
2505}
2506
2507pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
2510 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2511) -> bool {
2512 use hjkl_buffer::{Edit, MotionKind, Position};
2513 ed.sync_buffer_content_from_textarea();
2514 let cursor = buf_cursor_pos(&ed.buffer);
2515 if cursor.col > 0 {
2516 ed.mutate_edit(Edit::DeleteRange {
2517 start: Position::new(cursor.row, 0),
2518 end: cursor,
2519 kind: MotionKind::Char,
2520 });
2521 ed.push_buffer_cursor_to_textarea();
2522 }
2523 true
2524}
2525
2526pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
2530 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2531) -> bool {
2532 use hjkl_buffer::{Edit, MotionKind, Position};
2533 ed.sync_buffer_content_from_textarea();
2534 let cursor = buf_cursor_pos(&ed.buffer);
2535 if cursor.col > 0 {
2536 ed.mutate_edit(Edit::DeleteRange {
2537 start: Position::new(cursor.row, cursor.col - 1),
2538 end: cursor,
2539 kind: MotionKind::Char,
2540 });
2541 } else if cursor.row > 0 {
2542 let prev_row = cursor.row - 1;
2543 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2544 ed.mutate_edit(Edit::JoinLines {
2545 row: prev_row,
2546 count: 1,
2547 with_space: false,
2548 });
2549 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2550 }
2551 ed.push_buffer_cursor_to_textarea();
2552 true
2553}
2554
2555pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
2558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2559) -> bool {
2560 let (row, col) = ed.cursor();
2561 let sw = ed.settings().shiftwidth;
2562 indent_rows(ed, row, row, 1);
2563 ed.jump_cursor(row, col + sw);
2564 true
2565}
2566
2567pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
2570 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2571) -> bool {
2572 let (row, col) = ed.cursor();
2573 let before_len = buf_line_bytes(&ed.buffer, row);
2574 outdent_rows(ed, row, row, 1);
2575 let after_len = buf_line_bytes(&ed.buffer, row);
2576 let stripped = before_len.saturating_sub(after_len);
2577 let new_col = col.saturating_sub(stripped);
2578 ed.jump_cursor(row, new_col);
2579 true
2580}
2581
2582pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
2586 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2587) -> bool {
2588 ed.vim.one_shot_normal = true;
2589 ed.vim.mode = Mode::Normal;
2590 ed.vim.current_mode = crate::VimMode::Normal;
2592 false
2593}
2594
2595pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
2599 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2600) -> bool {
2601 ed.vim.insert_pending_register = true;
2602 false
2603}
2604
2605pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
2609 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2610 reg: char,
2611) -> bool {
2612 insert_register_text(ed, reg);
2613 true
2616}
2617
2618pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
2624 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2625) -> bool {
2626 ed.vim.pending_closes.clear();
2627
2628 if !ed.vim.abbrevs.is_empty() {
2631 check_and_apply_abbrev(ed, AbbrevTrigger::Esc);
2632 }
2633
2634 finish_insert_session(ed);
2635 sync_paired_tag_on_exit(ed);
2639 ed.vim.mode = Mode::Normal;
2640 ed.vim.current_mode = crate::VimMode::Normal;
2642 let col = ed.cursor().1;
2643 ed.vim.last_insert_pos = Some(ed.cursor());
2644 if col > 0 {
2645 crate::motions::move_left(&mut ed.buffer, 1);
2646 ed.push_buffer_cursor_to_textarea();
2647 }
2648 ed.sticky_col = Some(ed.cursor().1);
2649 true
2650}
2651
2652#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2657pub enum ScrollDir {
2658 Down,
2660 Up,
2662}
2663
2664pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
2669 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2670 count: usize,
2671) {
2672 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2673}
2674
2675pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
2677 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2678 count: usize,
2679) {
2680 move_first_non_whitespace(ed);
2681 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2682}
2683
2684pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
2686 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2687 count: usize,
2688) {
2689 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2690 ed.push_buffer_cursor_to_textarea();
2691 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2692}
2693
2694pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
2696 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2697 count: usize,
2698) {
2699 crate::motions::move_line_end(&mut ed.buffer);
2700 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2701 ed.push_buffer_cursor_to_textarea();
2702 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2703}
2704
2705pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
2709 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2710 count: usize,
2711) {
2712 use hjkl_buffer::{Edit, Position};
2713 ed.push_undo();
2714 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2715 ed.sync_buffer_content_from_textarea();
2716 let row = buf_cursor_pos(&ed.buffer).row;
2717 let line_chars = buf_line_chars(&ed.buffer, row);
2718 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
2719
2720 let comment_cont = if ed.settings.formatoptions.contains('o') {
2722 continue_comment(&ed.buffer, &ed.settings, row)
2723 } else {
2724 None
2725 };
2726
2727 let suffix = if let Some(cont) = comment_cont {
2728 format!("\n{cont}")
2729 } else {
2730 let indent = compute_enter_indent(&ed.settings, &prev_line);
2731 format!("\n{indent}")
2732 };
2733 ed.mutate_edit(Edit::InsertStr {
2734 at: Position::new(row, line_chars),
2735 text: suffix,
2736 });
2737 ed.push_buffer_cursor_to_textarea();
2738}
2739
2740pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
2744 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2745 count: usize,
2746) {
2747 use hjkl_buffer::{Edit, Position};
2748 ed.push_undo();
2749 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2750 ed.sync_buffer_content_from_textarea();
2751 let row = buf_cursor_pos(&ed.buffer).row;
2752
2753 let comment_cont = if ed.settings.formatoptions.contains('o') {
2755 continue_comment(&ed.buffer, &ed.settings, row)
2756 } else {
2757 None
2758 };
2759
2760 let (insert_text, new_line_content) = if let Some(cont) = comment_cont {
2763 let content = cont.clone();
2764 (format!("{cont}\n"), content)
2765 } else {
2766 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
2772 let indent = compute_enter_indent(&ed.settings, &cur);
2773 let content = indent.clone();
2774 (format!("{indent}\n"), content)
2775 };
2776 ed.mutate_edit(Edit::InsertStr {
2777 at: Position::new(row, 0),
2778 text: insert_text,
2779 });
2780 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2781 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2782 let new_row = buf_cursor_pos(&ed.buffer).row;
2783 buf_set_cursor_rc(&mut ed.buffer, new_row, new_line_content.chars().count());
2784 ed.push_buffer_cursor_to_textarea();
2785}
2786
2787pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
2789 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2790 count: usize,
2791) {
2792 begin_insert(ed, count.max(1), InsertReason::Replace);
2794}
2795
2796pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
2801 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2802 count: usize,
2803) {
2804 do_char_delete(ed, true, count.max(1));
2805 if !ed.vim.replaying {
2806 ed.vim.last_change = Some(LastChange::CharDel {
2807 forward: true,
2808 count: count.max(1),
2809 });
2810 }
2811}
2812
2813pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
2816 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2817 count: usize,
2818) {
2819 do_char_delete(ed, false, count.max(1));
2820 if !ed.vim.replaying {
2821 ed.vim.last_change = Some(LastChange::CharDel {
2822 forward: false,
2823 count: count.max(1),
2824 });
2825 }
2826}
2827
2828pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
2831 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2832 count: usize,
2833) {
2834 use hjkl_buffer::{Edit, MotionKind, Position};
2835 ed.push_undo();
2836 ed.sync_buffer_content_from_textarea();
2837 for _ in 0..count.max(1) {
2838 let cursor = buf_cursor_pos(&ed.buffer);
2839 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2840 if cursor.col >= line_chars {
2841 break;
2842 }
2843 ed.mutate_edit(Edit::DeleteRange {
2844 start: cursor,
2845 end: Position::new(cursor.row, cursor.col + 1),
2846 kind: MotionKind::Char,
2847 });
2848 }
2849 ed.push_buffer_cursor_to_textarea();
2850 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
2851 if !ed.vim.replaying {
2852 ed.vim.last_change = Some(LastChange::OpMotion {
2853 op: Operator::Change,
2854 motion: Motion::Right,
2855 count: count.max(1),
2856 inserted: None,
2857 });
2858 }
2859}
2860
2861pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
2864 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2865 count: usize,
2866) {
2867 execute_line_op(ed, Operator::Change, count.max(1));
2868 if !ed.vim.replaying {
2869 ed.vim.last_change = Some(LastChange::LineOp {
2870 op: Operator::Change,
2871 count: count.max(1),
2872 inserted: None,
2873 });
2874 }
2875}
2876
2877pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2880 ed.push_undo();
2881 delete_to_eol(ed);
2882 crate::motions::move_left(&mut ed.buffer, 1);
2883 ed.push_buffer_cursor_to_textarea();
2884 if !ed.vim.replaying {
2885 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2886 }
2887}
2888
2889pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2892 ed.push_undo();
2893 delete_to_eol(ed);
2894 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2895}
2896
2897pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
2899 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2900 count: usize,
2901) {
2902 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2903}
2904
2905pub(crate) fn join_line_bridge<H: crate::types::Host>(
2908 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2909 count: usize,
2910) {
2911 let joins = count.max(2) - 1;
2914 for _ in 0..joins {
2915 ed.push_undo();
2916 join_line(ed);
2917 }
2918 if !ed.vim.replaying {
2919 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
2920 }
2921}
2922
2923pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
2926 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2927 count: usize,
2928) {
2929 for _ in 0..count.max(1) {
2930 ed.push_undo();
2931 toggle_case_at_cursor(ed);
2932 }
2933 if !ed.vim.replaying {
2934 ed.vim.last_change = Some(LastChange::ToggleCase {
2935 count: count.max(1),
2936 });
2937 }
2938}
2939
2940pub(crate) fn paste_after_bridge<H: crate::types::Host>(
2944 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2945 count: usize,
2946) {
2947 paste_bridge(ed, false, count, false, false);
2948}
2949
2950pub(crate) fn paste_before_bridge<H: crate::types::Host>(
2954 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2955 count: usize,
2956) {
2957 paste_bridge(ed, true, count, false, false);
2958}
2959
2960pub(crate) fn paste_bridge<H: crate::types::Host>(
2963 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2964 before: bool,
2965 count: usize,
2966 cursor_after: bool,
2967 reindent: bool,
2968) {
2969 do_paste(ed, before, count.max(1), cursor_after, reindent);
2970 if !ed.vim.replaying {
2971 ed.vim.last_change = Some(LastChange::Paste {
2972 before,
2973 count: count.max(1),
2974 cursor_after,
2975 reindent,
2976 });
2977 }
2978}
2979
2980pub(crate) fn jump_back_bridge<H: crate::types::Host>(
2985 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2986 count: usize,
2987) {
2988 for _ in 0..count.max(1) {
2989 jump_back(ed);
2990 }
2991}
2992
2993pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
2996 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2997 count: usize,
2998) {
2999 for _ in 0..count.max(1) {
3000 jump_forward(ed);
3001 }
3002}
3003
3004pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
3009 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3010 dir: ScrollDir,
3011 count: usize,
3012) {
3013 ed.vim.scroll_anim_hint = true;
3014 let rows = viewport_full_rows(ed, count) as isize;
3015 match dir {
3016 ScrollDir::Down => scroll_cursor_rows(ed, rows),
3017 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
3018 }
3019}
3020
3021pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
3024 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3025 dir: ScrollDir,
3026 count: usize,
3027) {
3028 ed.vim.scroll_anim_hint = true;
3029 let rows = viewport_half_rows(ed, count) as isize;
3030 match dir {
3031 ScrollDir::Down => scroll_cursor_rows(ed, rows),
3032 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
3033 }
3034}
3035
3036pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
3040 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3041 dir: ScrollDir,
3042 count: usize,
3043) {
3044 let n = count.max(1);
3045 let total = buf_row_count(&ed.buffer);
3046 let last = total.saturating_sub(1);
3047 let h = ed.viewport_height_value() as usize;
3048 let vp = ed.host().viewport();
3049 let cur_top = vp.top_row;
3050 let new_top = match dir {
3051 ScrollDir::Down => (cur_top + n).min(last),
3052 ScrollDir::Up => cur_top.saturating_sub(n),
3053 };
3054 ed.set_viewport_top(new_top);
3055 let (row, col) = ed.cursor();
3057 let bot = (new_top + h).saturating_sub(1).min(last);
3058 let clamped = row.max(new_top).min(bot);
3059 if clamped != row {
3060 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
3061 ed.push_buffer_cursor_to_textarea();
3062 }
3063}
3064
3065pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
3070 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3071 forward: bool,
3072 count: usize,
3073) {
3074 if let Some(pattern) = ed.vim.last_search.clone() {
3075 ed.push_search_pattern(&pattern);
3076 }
3077 if ed.search_state().pattern.is_none() {
3078 return;
3079 }
3080 let go_forward = ed.vim.last_search_forward == forward;
3081 for _ in 0..count.max(1) {
3082 if go_forward {
3083 ed.search_advance_forward(true);
3084 } else {
3085 ed.search_advance_backward(true);
3086 }
3087 }
3088 ed.push_buffer_cursor_to_textarea();
3089}
3090
3091pub(crate) fn word_search_bridge<H: crate::types::Host>(
3095 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3096 forward: bool,
3097 whole_word: bool,
3098 count: usize,
3099) {
3100 word_at_cursor_search(ed, forward, whole_word, count.max(1));
3101}
3102
3103#[allow(dead_code)]
3108#[inline]
3109pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3110 do_undo(ed);
3111}
3112
3113#[inline]
3130pub(crate) fn drop_blame_if_left_normal<H: crate::types::Host>(
3131 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3132) {
3133 if ed.vim.current_mode != crate::VimMode::Normal {
3134 ed.vim.view = crate::ViewMode::Normal;
3135 }
3136}
3137
3138#[inline]
3142pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
3143 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3144 mode: Mode,
3145) {
3146 ed.vim.mode = mode;
3147 ed.vim.current_mode = ed.vim.public_mode();
3148 drop_blame_if_left_normal(ed);
3149}
3150
3151pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
3154 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3155) {
3156 let cur = ed.cursor();
3157 ed.vim.visual_anchor = cur;
3158 set_vim_mode_bridge(ed, Mode::Visual);
3159}
3160
3161pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
3164 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3165) {
3166 let (row, _) = ed.cursor();
3167 ed.vim.visual_line_anchor = row;
3168 set_vim_mode_bridge(ed, Mode::VisualLine);
3169}
3170
3171pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
3175 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3176) {
3177 let cur = ed.cursor();
3178 ed.vim.block_anchor = cur;
3179 ed.vim.block_vcol = cur.1;
3180 set_vim_mode_bridge(ed, Mode::VisualBlock);
3181}
3182
3183pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
3188 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3189) {
3190 let snap: Option<LastVisual> = match ed.vim.mode {
3192 Mode::Visual => Some(LastVisual {
3193 mode: Mode::Visual,
3194 anchor: ed.vim.visual_anchor,
3195 cursor: ed.cursor(),
3196 block_vcol: 0,
3197 }),
3198 Mode::VisualLine => Some(LastVisual {
3199 mode: Mode::VisualLine,
3200 anchor: (ed.vim.visual_line_anchor, 0),
3201 cursor: ed.cursor(),
3202 block_vcol: 0,
3203 }),
3204 Mode::VisualBlock => Some(LastVisual {
3205 mode: Mode::VisualBlock,
3206 anchor: ed.vim.block_anchor,
3207 cursor: ed.cursor(),
3208 block_vcol: ed.vim.block_vcol,
3209 }),
3210 _ => None,
3211 };
3212 ed.vim.pending = Pending::None;
3214 ed.vim.count = 0;
3215 ed.vim.insert_session = None;
3216 set_vim_mode_bridge(ed, Mode::Normal);
3217 if let Some(snap) = snap {
3221 let (lo, hi) = match snap.mode {
3222 Mode::Visual => {
3223 if snap.anchor <= snap.cursor {
3224 (snap.anchor, snap.cursor)
3225 } else {
3226 (snap.cursor, snap.anchor)
3227 }
3228 }
3229 Mode::VisualLine => {
3230 let r_lo = snap.anchor.0.min(snap.cursor.0);
3231 let r_hi = snap.anchor.0.max(snap.cursor.0);
3232 let vl_rope = ed.buffer().rope();
3233 let r_hi_clamped = r_hi.min(vl_rope.len_lines().saturating_sub(1));
3234 let last_col = hjkl_buffer::rope_line_str(&vl_rope, r_hi_clamped)
3235 .chars()
3236 .count()
3237 .saturating_sub(1);
3238 ((r_lo, 0), (r_hi, last_col))
3239 }
3240 Mode::VisualBlock => {
3241 let (r1, c1) = snap.anchor;
3242 let (r2, c2) = snap.cursor;
3243 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
3244 }
3245 _ => {
3246 if snap.anchor <= snap.cursor {
3247 (snap.anchor, snap.cursor)
3248 } else {
3249 (snap.cursor, snap.anchor)
3250 }
3251 }
3252 };
3253 ed.set_mark('<', lo);
3254 ed.set_mark('>', hi);
3255 ed.vim.last_visual = Some(snap);
3256 }
3257}
3258
3259pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
3265 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3266) {
3267 match ed.vim.mode {
3268 Mode::Visual => {
3269 let cur = ed.cursor();
3270 let anchor = ed.vim.visual_anchor;
3271 ed.vim.visual_anchor = cur;
3272 ed.jump_cursor(anchor.0, anchor.1);
3273 }
3274 Mode::VisualLine => {
3275 let cur_row = ed.cursor().0;
3276 let anchor_row = ed.vim.visual_line_anchor;
3277 ed.vim.visual_line_anchor = cur_row;
3278 ed.jump_cursor(anchor_row, 0);
3279 }
3280 Mode::VisualBlock => {
3281 let cur = ed.cursor();
3282 let anchor = ed.vim.block_anchor;
3283 ed.vim.block_anchor = cur;
3284 ed.vim.block_vcol = anchor.1;
3285 ed.jump_cursor(anchor.0, anchor.1);
3286 }
3287 _ => {}
3288 }
3289}
3290
3291pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
3295 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3296) {
3297 if let Some(snap) = ed.vim.last_visual {
3298 match snap.mode {
3299 Mode::Visual => {
3300 ed.vim.visual_anchor = snap.anchor;
3301 set_vim_mode_bridge(ed, Mode::Visual);
3302 }
3303 Mode::VisualLine => {
3304 ed.vim.visual_line_anchor = snap.anchor.0;
3305 set_vim_mode_bridge(ed, Mode::VisualLine);
3306 }
3307 Mode::VisualBlock => {
3308 ed.vim.block_anchor = snap.anchor;
3309 ed.vim.block_vcol = snap.block_vcol;
3310 set_vim_mode_bridge(ed, Mode::VisualBlock);
3311 }
3312 _ => {}
3313 }
3314 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3315 }
3316}
3317
3318pub(crate) fn set_mode_bridge<H: crate::types::Host>(
3324 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3325 mode: crate::VimMode,
3326) {
3327 let internal = match mode {
3328 crate::VimMode::Normal => Mode::Normal,
3329 crate::VimMode::Insert => Mode::Insert,
3330 crate::VimMode::Visual => Mode::Visual,
3331 crate::VimMode::VisualLine => Mode::VisualLine,
3332 crate::VimMode::VisualBlock => Mode::VisualBlock,
3333 };
3334 ed.vim.mode = internal;
3335 ed.vim.current_mode = mode;
3336 drop_blame_if_left_normal(ed);
3337}
3338
3339pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
3356 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3357 ch: char,
3358) {
3359 if ch.is_ascii_lowercase() {
3360 let pos = ed.cursor();
3361 ed.set_mark(ch, pos);
3362 } else if ch.is_ascii_uppercase() {
3363 let pos = ed.cursor();
3364 let bid = ed.current_buffer_id();
3365 ed.set_global_mark(ch, bid, pos);
3366 tracing::debug!(
3367 mark = ch as u32,
3368 buffer_id = bid,
3369 row = pos.0,
3370 col = pos.1,
3371 "global mark set"
3372 );
3373 }
3374 }
3376
3377pub(crate) fn goto_mark<H: crate::types::Host>(
3386 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3387 ch: char,
3388 linewise: bool,
3389) {
3390 let target = match ch {
3391 'a'..='z' => ed.mark(ch),
3392 '\'' | '`' => ed.vim.jump_back.last().copied(),
3393 '.' => ed.vim.last_edit_pos,
3394 '[' | ']' | '<' | '>' => ed.mark(ch),
3395 _ => None,
3396 };
3397 let Some((row, col)) = target else {
3398 return;
3399 };
3400 let pre = ed.cursor();
3401 let (r, c_clamped) = clamp_pos(ed, (row, col));
3402 if linewise {
3403 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3404 ed.push_buffer_cursor_to_textarea();
3405 move_first_non_whitespace(ed);
3406 } else {
3407 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3408 ed.push_buffer_cursor_to_textarea();
3409 }
3410 if ed.cursor() != pre {
3411 ed.push_jump(pre);
3412 }
3413 ed.sticky_col = Some(ed.cursor().1);
3414}
3415
3416pub(crate) fn try_goto_mark<H: crate::types::Host>(
3425 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3426 ch: char,
3427 linewise: bool,
3428) -> crate::editor::MarkJump {
3429 use crate::editor::MarkJump;
3430 match ch {
3431 'A'..='Z' => {
3432 let Some((bid, row, col)) = ed.global_mark(ch) else {
3433 return MarkJump::Unset;
3434 };
3435 if bid != ed.current_buffer_id() {
3436 tracing::debug!(
3437 mark = ch as u32,
3438 buffer_id = bid,
3439 row,
3440 col,
3441 "global mark cross-buffer jump"
3442 );
3443 return MarkJump::CrossBuffer {
3444 buffer_id: bid,
3445 row,
3446 col,
3447 };
3448 }
3449 let pre = ed.cursor();
3451 let (r, c_clamped) = clamp_pos(ed, (row, col));
3452 if linewise {
3453 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3454 ed.push_buffer_cursor_to_textarea();
3455 move_first_non_whitespace(ed);
3456 } else {
3457 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3458 ed.push_buffer_cursor_to_textarea();
3459 }
3460 if ed.cursor() != pre {
3461 ed.push_jump(pre);
3462 }
3463 ed.sticky_col = Some(ed.cursor().1);
3464 MarkJump::SameBuffer
3465 }
3466 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
3467 goto_mark(ed, ch, linewise);
3468 MarkJump::SameBuffer
3469 }
3470 _ => MarkJump::Unset,
3471 }
3472}
3473
3474pub fn op_is_change(op: Operator) -> bool {
3478 matches!(op, Operator::Delete | Operator::Change)
3479}
3480
3481pub(crate) const JUMPLIST_MAX: usize = 100;
3485
3486fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3489 let Some(target) = ed.vim.jump_back.pop() else {
3490 return;
3491 };
3492 let cur = ed.cursor();
3493 ed.vim.jump_fwd.push(cur);
3494 let (r, c) = clamp_pos(ed, target);
3495 ed.jump_cursor(r, c);
3496 ed.sticky_col = Some(c);
3497}
3498
3499fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3502 let Some(target) = ed.vim.jump_fwd.pop() else {
3503 return;
3504 };
3505 let cur = ed.cursor();
3506 ed.vim.jump_back.push(cur);
3507 if ed.vim.jump_back.len() > JUMPLIST_MAX {
3508 ed.vim.jump_back.remove(0);
3509 }
3510 let (r, c) = clamp_pos(ed, target);
3511 ed.jump_cursor(r, c);
3512 ed.sticky_col = Some(c);
3513}
3514
3515fn clamp_pos<H: crate::types::Host>(
3518 ed: &Editor<hjkl_buffer::Buffer, H>,
3519 pos: (usize, usize),
3520) -> (usize, usize) {
3521 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3522 let r = pos.0.min(last_row);
3523 let line_len = buf_line_chars(&ed.buffer, r);
3524 let c = pos.1.min(line_len.saturating_sub(1));
3525 (r, c)
3526}
3527
3528fn is_big_jump(motion: &Motion) -> bool {
3530 matches!(
3531 motion,
3532 Motion::FileTop
3533 | Motion::FileBottom
3534 | Motion::MatchBracket
3535 | Motion::WordAtCursor { .. }
3536 | Motion::SearchNext { .. }
3537 | Motion::ViewportTop
3538 | Motion::ViewportMiddle
3539 | Motion::ViewportBottom
3540 )
3541}
3542
3543fn viewport_half_rows<H: crate::types::Host>(
3548 ed: &Editor<hjkl_buffer::Buffer, H>,
3549 count: usize,
3550) -> usize {
3551 let h = ed.viewport_height_value() as usize;
3552 (h / 2).max(1).saturating_mul(count.max(1))
3553}
3554
3555fn viewport_full_rows<H: crate::types::Host>(
3558 ed: &Editor<hjkl_buffer::Buffer, H>,
3559 count: usize,
3560) -> usize {
3561 let h = ed.viewport_height_value() as usize;
3562 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
3563}
3564
3565fn scroll_cursor_rows<H: crate::types::Host>(
3570 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3571 delta: isize,
3572) {
3573 if delta == 0 {
3574 return;
3575 }
3576 ed.sync_buffer_content_from_textarea();
3577 let (row, _) = ed.cursor();
3578 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3579 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
3580 buf_set_cursor_rc(&mut ed.buffer, target, 0);
3581 crate::motions::move_first_non_blank(&mut ed.buffer);
3582 ed.push_buffer_cursor_to_textarea();
3583 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3584}
3585
3586pub fn parse_motion(input: &Input) -> Option<Motion> {
3592 if input.ctrl {
3593 if input.key == Key::Char('h') {
3597 return Some(Motion::BackspaceBack);
3598 }
3599 return None;
3600 }
3601 match input.key {
3602 Key::Char('h') | Key::Left => Some(Motion::Left),
3603 Key::Char('l') | Key::Right => Some(Motion::Right),
3604 Key::Char(' ') => Some(Motion::SpaceFwd),
3608 Key::Backspace => Some(Motion::BackspaceBack),
3609 Key::Char('j') | Key::Down => Some(Motion::Down),
3610 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
3612 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
3614 Key::Char('_') => Some(Motion::FirstNonBlankLine),
3616 Key::Char('k') | Key::Up => Some(Motion::Up),
3617 Key::Char('w') => Some(Motion::WordFwd),
3618 Key::Char('W') => Some(Motion::BigWordFwd),
3619 Key::Char('b') => Some(Motion::WordBack),
3620 Key::Char('B') => Some(Motion::BigWordBack),
3621 Key::Char('e') => Some(Motion::WordEnd),
3622 Key::Char('E') => Some(Motion::BigWordEnd),
3623 Key::Char('0') | Key::Home => Some(Motion::LineStart),
3624 Key::Char('^') => Some(Motion::FirstNonBlank),
3625 Key::Char('$') | Key::End => Some(Motion::LineEnd),
3626 Key::Char('G') => Some(Motion::FileBottom),
3627 Key::Char('%') => Some(Motion::MatchBracket),
3628 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
3629 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
3630 Key::Char('*') => Some(Motion::WordAtCursor {
3631 forward: true,
3632 whole_word: true,
3633 }),
3634 Key::Char('#') => Some(Motion::WordAtCursor {
3635 forward: false,
3636 whole_word: true,
3637 }),
3638 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
3639 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
3640 Key::Char('H') => Some(Motion::ViewportTop),
3641 Key::Char('M') => Some(Motion::ViewportMiddle),
3642 Key::Char('L') => Some(Motion::ViewportBottom),
3643 Key::Char('{') => Some(Motion::ParagraphPrev),
3644 Key::Char('}') => Some(Motion::ParagraphNext),
3645 Key::Char('(') => Some(Motion::SentencePrev),
3646 Key::Char(')') => Some(Motion::SentenceNext),
3647 Key::Char('|') => Some(Motion::GotoColumn),
3648 _ => None,
3649 }
3650}
3651
3652pub(crate) fn execute_motion<H: crate::types::Host>(
3655 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3656 motion: Motion,
3657 count: usize,
3658) {
3659 let count = count.max(1);
3660 if let Motion::FindRepeat { reverse } = motion
3663 && ed.vim.last_horizontal_motion == LastHorizontalMotion::Sneak
3664 {
3665 if let Some(((c1, c2), fwd)) = ed.vim.last_sneak {
3666 let effective_fwd = if reverse { !fwd } else { fwd };
3667 apply_sneak(ed, c1, c2, effective_fwd, count);
3668 }
3669 return;
3670 }
3671 let motion = match motion {
3673 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3674 Some((ch, forward, till)) => Motion::Find {
3675 ch,
3676 forward: if reverse { !forward } else { forward },
3677 till,
3678 },
3679 None => return,
3680 },
3681 other => other,
3682 };
3683 let pre_pos = ed.cursor();
3684 let pre_col = pre_pos.1;
3685 apply_motion_cursor(ed, &motion, count);
3686 let post_pos = ed.cursor();
3687 if is_big_jump(&motion) && pre_pos != post_pos {
3688 ed.push_jump(pre_pos);
3689 }
3690 apply_sticky_col(ed, &motion, pre_col);
3691 ed.sync_buffer_from_textarea();
3696}
3697
3698fn execute_motion_with_block_vcol<H: crate::types::Host>(
3709 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3710 motion: Motion,
3711 count: usize,
3712) {
3713 let motion_copy = motion.clone();
3714 execute_motion(ed, motion, count);
3715 if ed.vim.mode == Mode::VisualBlock {
3716 update_block_vcol(ed, &motion_copy);
3717 }
3718}
3719
3720pub(crate) fn apply_motion_kind<H: crate::types::Host>(
3748 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3749 kind: crate::MotionKind,
3750 count: usize,
3751) {
3752 let count = count.max(1);
3753 match kind {
3754 crate::MotionKind::CharLeft => {
3755 execute_motion_with_block_vcol(ed, Motion::Left, count);
3756 }
3757 crate::MotionKind::CharRight => {
3758 execute_motion_with_block_vcol(ed, Motion::Right, count);
3759 }
3760 crate::MotionKind::LineDown => {
3761 execute_motion_with_block_vcol(ed, Motion::Down, count);
3762 }
3763 crate::MotionKind::LineUp => {
3764 execute_motion_with_block_vcol(ed, Motion::Up, count);
3765 }
3766 crate::MotionKind::FirstNonBlankDown => {
3767 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3772 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3773 crate::motions::move_first_non_blank(&mut ed.buffer);
3774 ed.push_buffer_cursor_to_textarea();
3775 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3776 ed.sync_buffer_from_textarea();
3777 }
3778 crate::MotionKind::FirstNonBlankUp => {
3779 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3782 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3783 crate::motions::move_first_non_blank(&mut ed.buffer);
3784 ed.push_buffer_cursor_to_textarea();
3785 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3786 ed.sync_buffer_from_textarea();
3787 }
3788 crate::MotionKind::WordForward => {
3789 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
3790 }
3791 crate::MotionKind::BigWordForward => {
3792 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
3793 }
3794 crate::MotionKind::WordBackward => {
3795 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
3796 }
3797 crate::MotionKind::BigWordBackward => {
3798 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
3799 }
3800 crate::MotionKind::WordEnd => {
3801 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
3802 }
3803 crate::MotionKind::BigWordEnd => {
3804 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
3805 }
3806 crate::MotionKind::LineStart => {
3807 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
3810 }
3811 crate::MotionKind::FirstNonBlank => {
3812 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
3815 }
3816 crate::MotionKind::GotoLine => {
3817 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
3826 }
3827 crate::MotionKind::LineEnd => {
3828 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
3832 }
3833 crate::MotionKind::FindRepeat => {
3834 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
3838 }
3839 crate::MotionKind::FindRepeatReverse => {
3840 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
3844 }
3845 crate::MotionKind::BracketMatch => {
3846 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
3851 }
3852 crate::MotionKind::ViewportTop => {
3853 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
3856 }
3857 crate::MotionKind::ViewportMiddle => {
3858 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
3861 }
3862 crate::MotionKind::ViewportBottom => {
3863 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
3866 }
3867 crate::MotionKind::HalfPageDown => {
3868 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
3872 }
3873 crate::MotionKind::HalfPageUp => {
3874 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
3877 }
3878 crate::MotionKind::FullPageDown => {
3879 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
3882 }
3883 crate::MotionKind::FullPageUp => {
3884 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
3887 }
3888 crate::MotionKind::FirstNonBlankLine => {
3889 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
3890 }
3891 crate::MotionKind::SectionBackward => {
3892 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
3893 }
3894 crate::MotionKind::SectionForward => {
3895 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
3896 }
3897 crate::MotionKind::SectionEndBackward => {
3898 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
3899 }
3900 crate::MotionKind::SectionEndForward => {
3901 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
3902 }
3903 }
3904}
3905
3906fn apply_sticky_col<H: crate::types::Host>(
3911 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3912 motion: &Motion,
3913 pre_col: usize,
3914) {
3915 if is_vertical_motion(motion) {
3916 let want = ed.sticky_col.unwrap_or(pre_col);
3917 ed.sticky_col = Some(want);
3920 let (row, _) = ed.cursor();
3921 let line_len = buf_line_chars(&ed.buffer, row);
3922 let max_col = line_len.saturating_sub(1);
3926 let target = want.min(max_col);
3927 buf_set_cursor_rc(&mut ed.buffer, row, target);
3931 } else {
3932 ed.sticky_col = Some(ed.cursor().1);
3935 }
3936}
3937
3938fn is_vertical_motion(motion: &Motion) -> bool {
3939 matches!(
3943 motion,
3944 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
3945 )
3946}
3947
3948fn apply_motion_cursor<H: crate::types::Host>(
3949 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3950 motion: &Motion,
3951 count: usize,
3952) {
3953 apply_motion_cursor_ctx(ed, motion, count, false)
3954}
3955
3956pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
3957 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3958 motion: &Motion,
3959 count: usize,
3960 as_operator: bool,
3961) {
3962 match motion {
3963 Motion::Left => {
3964 crate::motions::move_left(&mut ed.buffer, count);
3966 ed.push_buffer_cursor_to_textarea();
3967 }
3968 Motion::Right => {
3969 if as_operator {
3973 crate::motions::move_right_to_end(&mut ed.buffer, count);
3974 } else {
3975 crate::motions::move_right_in_line(&mut ed.buffer, count);
3976 }
3977 ed.push_buffer_cursor_to_textarea();
3978 }
3979 Motion::SpaceFwd => {
3980 if as_operator {
3983 crate::motions::move_right_to_end(&mut ed.buffer, count);
3984 } else {
3985 crate::motions::move_space_fwd(&mut ed.buffer, count);
3986 }
3987 ed.push_buffer_cursor_to_textarea();
3988 }
3989 Motion::BackspaceBack => {
3990 if as_operator {
3993 crate::motions::move_left(&mut ed.buffer, count);
3994 } else {
3995 crate::motions::move_backspace_back(&mut ed.buffer, count);
3996 }
3997 ed.push_buffer_cursor_to_textarea();
3998 }
3999 Motion::Up => {
4000 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
4004 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
4005 ed.push_buffer_cursor_to_textarea();
4006 }
4007 Motion::Down => {
4008 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
4009 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
4010 ed.push_buffer_cursor_to_textarea();
4011 }
4012 Motion::ScreenUp => {
4013 let v = *ed.host.viewport();
4014 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
4015 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
4016 ed.push_buffer_cursor_to_textarea();
4017 }
4018 Motion::ScreenDown => {
4019 let v = *ed.host.viewport();
4020 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
4021 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
4022 ed.push_buffer_cursor_to_textarea();
4023 }
4024 Motion::WordFwd => {
4025 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
4026 ed.push_buffer_cursor_to_textarea();
4027 }
4028 Motion::WordBack => {
4029 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
4030 ed.push_buffer_cursor_to_textarea();
4031 }
4032 Motion::WordEnd => {
4033 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
4034 ed.push_buffer_cursor_to_textarea();
4035 }
4036 Motion::BigWordFwd => {
4037 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
4038 ed.push_buffer_cursor_to_textarea();
4039 }
4040 Motion::BigWordBack => {
4041 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
4042 ed.push_buffer_cursor_to_textarea();
4043 }
4044 Motion::BigWordEnd => {
4045 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
4046 ed.push_buffer_cursor_to_textarea();
4047 }
4048 Motion::WordEndBack => {
4049 crate::motions::move_word_end_back(
4050 &mut ed.buffer,
4051 false,
4052 count,
4053 &ed.settings.iskeyword,
4054 );
4055 ed.push_buffer_cursor_to_textarea();
4056 }
4057 Motion::BigWordEndBack => {
4058 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
4059 ed.push_buffer_cursor_to_textarea();
4060 }
4061 Motion::LineStart => {
4062 crate::motions::move_line_start(&mut ed.buffer);
4063 ed.push_buffer_cursor_to_textarea();
4064 }
4065 Motion::FirstNonBlank => {
4066 crate::motions::move_first_non_blank(&mut ed.buffer);
4067 ed.push_buffer_cursor_to_textarea();
4068 }
4069 Motion::LineEnd => {
4070 crate::motions::move_line_end(&mut ed.buffer);
4072 ed.push_buffer_cursor_to_textarea();
4073 }
4074 Motion::FileTop => {
4075 if count > 1 {
4078 crate::motions::move_bottom(&mut ed.buffer, count);
4079 } else {
4080 crate::motions::move_top(&mut ed.buffer);
4081 }
4082 ed.push_buffer_cursor_to_textarea();
4083 }
4084 Motion::FileBottom => {
4085 if count > 1 {
4088 crate::motions::move_bottom(&mut ed.buffer, count);
4089 } else {
4090 crate::motions::move_bottom(&mut ed.buffer, 0);
4091 }
4092 ed.push_buffer_cursor_to_textarea();
4093 }
4094 Motion::Find { ch, forward, till } => {
4095 for _ in 0..count {
4096 if !find_char_on_line(ed, *ch, *forward, *till) {
4097 break;
4098 }
4099 }
4100 }
4101 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
4103 let _ = matching_bracket(ed);
4104 }
4105 Motion::UnmatchedBracket { forward, open } => {
4106 goto_unmatched_bracket(ed, *forward, *open, count);
4107 }
4108 Motion::WordAtCursor {
4109 forward,
4110 whole_word,
4111 } => {
4112 word_at_cursor_search(ed, *forward, *whole_word, count);
4113 }
4114 Motion::SearchNext { reverse } => {
4115 if let Some(pattern) = ed.vim.last_search.clone() {
4119 ed.push_search_pattern(&pattern);
4120 }
4121 if ed.search_state().pattern.is_none() {
4122 return;
4123 }
4124 let forward = ed.vim.last_search_forward != *reverse;
4128 for _ in 0..count.max(1) {
4129 if forward {
4130 ed.search_advance_forward(true);
4131 } else {
4132 ed.search_advance_backward(true);
4133 }
4134 }
4135 ed.push_buffer_cursor_to_textarea();
4136 }
4137 Motion::ViewportTop => {
4138 let v = *ed.host().viewport();
4139 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
4140 ed.push_buffer_cursor_to_textarea();
4141 }
4142 Motion::ViewportMiddle => {
4143 let v = *ed.host().viewport();
4144 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
4145 ed.push_buffer_cursor_to_textarea();
4146 }
4147 Motion::ViewportBottom => {
4148 let v = *ed.host().viewport();
4149 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
4150 ed.push_buffer_cursor_to_textarea();
4151 }
4152 Motion::LastNonBlank => {
4153 crate::motions::move_last_non_blank(&mut ed.buffer);
4154 ed.push_buffer_cursor_to_textarea();
4155 }
4156 Motion::LineMiddle => {
4157 let row = ed.cursor().0;
4158 let line_chars = buf_line_chars(&ed.buffer, row);
4159 let target = line_chars / 2;
4162 ed.jump_cursor(row, target);
4163 }
4164 Motion::ScreenLineMiddle => {
4165 let row = ed.cursor().0;
4168 let width = ed.host().viewport().width as usize;
4169 let last = buf_line_chars(&ed.buffer, row).saturating_sub(1);
4170 let target = (width / 2).min(last);
4171 ed.jump_cursor(row, target);
4172 }
4173 Motion::ParagraphPrev => {
4174 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
4175 ed.push_buffer_cursor_to_textarea();
4176 }
4177 Motion::ParagraphNext => {
4178 crate::motions::move_paragraph_next(&mut ed.buffer, count);
4179 ed.push_buffer_cursor_to_textarea();
4180 }
4181 Motion::SentencePrev => {
4182 for _ in 0..count.max(1) {
4183 if let Some((row, col)) = sentence_boundary(ed, false) {
4184 ed.jump_cursor(row, col);
4185 }
4186 }
4187 }
4188 Motion::SentenceNext => {
4189 for _ in 0..count.max(1) {
4190 if let Some((row, col)) = sentence_boundary(ed, true) {
4191 ed.jump_cursor(row, col);
4192 }
4193 }
4194 }
4195 Motion::SectionBackward => {
4196 crate::motions::move_section_backward(&mut ed.buffer, count);
4197 ed.push_buffer_cursor_to_textarea();
4198 }
4199 Motion::SectionForward => {
4200 crate::motions::move_section_forward(&mut ed.buffer, count);
4201 ed.push_buffer_cursor_to_textarea();
4202 }
4203 Motion::SectionEndBackward => {
4204 crate::motions::move_section_end_backward(&mut ed.buffer, count);
4205 ed.push_buffer_cursor_to_textarea();
4206 }
4207 Motion::SectionEndForward => {
4208 crate::motions::move_section_end_forward(&mut ed.buffer, count);
4209 ed.push_buffer_cursor_to_textarea();
4210 }
4211 Motion::FirstNonBlankNextLine => {
4212 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
4213 ed.push_buffer_cursor_to_textarea();
4214 }
4215 Motion::FirstNonBlankPrevLine => {
4216 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
4217 ed.push_buffer_cursor_to_textarea();
4218 }
4219 Motion::FirstNonBlankLine => {
4220 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
4221 ed.push_buffer_cursor_to_textarea();
4222 }
4223 Motion::GotoColumn => {
4224 crate::motions::move_goto_column(&mut ed.buffer, count);
4225 ed.push_buffer_cursor_to_textarea();
4226 }
4227 }
4228}
4229
4230fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4231 ed.sync_buffer_content_from_textarea();
4237 crate::motions::move_first_non_blank(&mut ed.buffer);
4238 ed.push_buffer_cursor_to_textarea();
4239}
4240
4241fn find_char_on_line<H: crate::types::Host>(
4242 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4243 ch: char,
4244 forward: bool,
4245 till: bool,
4246) -> bool {
4247 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
4248 if moved {
4249 ed.push_buffer_cursor_to_textarea();
4250 }
4251 moved
4252}
4253
4254fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
4255 let moved = crate::motions::match_bracket(&mut ed.buffer);
4256 if moved {
4257 ed.push_buffer_cursor_to_textarea();
4258 }
4259 moved
4260}
4261
4262fn goto_unmatched_bracket<H: crate::types::Host>(
4266 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4267 forward: bool,
4268 open: char,
4269 count: usize,
4270) {
4271 let close = match open {
4272 '(' => ')',
4273 '{' => '}',
4274 _ => return,
4275 };
4276 let cursor = buf_cursor_pos(&ed.buffer);
4277 let rows = buf_row_count(&ed.buffer);
4278 let target = count.max(1);
4279 let mut found = 0usize;
4280 let mut depth = 0i32;
4281
4282 if forward {
4283 let mut r = cursor.row;
4284 let mut from_col = cursor.col + 1;
4285 while r < rows {
4286 let line: Vec<char> = buf_line(&ed.buffer, r)
4287 .unwrap_or_default()
4288 .chars()
4289 .collect();
4290 let mut ci = from_col;
4291 while ci < line.len() {
4292 let ch = line[ci];
4293 if ch == open {
4294 depth += 1;
4295 } else if ch == close {
4296 if depth == 0 {
4297 found += 1;
4298 if found == target {
4299 buf_set_cursor_rc(&mut ed.buffer, r, ci);
4300 ed.push_buffer_cursor_to_textarea();
4301 return;
4302 }
4303 } else {
4304 depth -= 1;
4305 }
4306 }
4307 ci += 1;
4308 }
4309 r += 1;
4310 from_col = 0;
4311 }
4312 } else {
4313 let mut r = cursor.row as isize;
4314 let mut from_col = cursor.col as isize - 1;
4317 while r >= 0 {
4318 let line: Vec<char> = buf_line(&ed.buffer, r as usize)
4319 .unwrap_or_default()
4320 .chars()
4321 .collect();
4322 let mut ci = from_col.min(line.len() as isize - 1);
4323 while ci >= 0 {
4324 let ch = line[ci as usize];
4325 if ch == close {
4326 depth += 1;
4327 } else if ch == open {
4328 if depth == 0 {
4329 found += 1;
4330 if found == target {
4331 buf_set_cursor_rc(&mut ed.buffer, r as usize, ci as usize);
4332 ed.push_buffer_cursor_to_textarea();
4333 return;
4334 }
4335 } else {
4336 depth -= 1;
4337 }
4338 }
4339 ci -= 1;
4340 }
4341 r -= 1;
4342 from_col = isize::MAX;
4343 }
4344 }
4345}
4346
4347fn word_at_cursor_search<H: crate::types::Host>(
4348 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4349 forward: bool,
4350 whole_word: bool,
4351 count: usize,
4352) {
4353 let (row, col) = ed.cursor();
4354 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
4355 let chars: Vec<char> = line.chars().collect();
4356 if chars.is_empty() {
4357 return;
4358 }
4359 let spec = ed.settings().iskeyword.clone();
4361 let is_word = |c: char| is_keyword_char(c, &spec);
4362 let mut start = col.min(chars.len().saturating_sub(1));
4363 while start > 0 && is_word(chars[start - 1]) {
4364 start -= 1;
4365 }
4366 let mut end = start;
4367 while end < chars.len() && is_word(chars[end]) {
4368 end += 1;
4369 }
4370 if end <= start {
4371 return;
4372 }
4373 let word: String = chars[start..end].iter().collect();
4374 let escaped = regex_escape(&word);
4375 let pattern = if whole_word {
4376 format!(r"\b{escaped}\b")
4377 } else {
4378 escaped
4379 };
4380 ed.push_search_pattern(&pattern);
4381 if ed.search_state().pattern.is_none() {
4382 return;
4383 }
4384 ed.vim.last_search = Some(pattern);
4386 ed.vim.last_search_forward = forward;
4387 for _ in 0..count.max(1) {
4388 if forward {
4389 ed.search_advance_forward(true);
4390 } else {
4391 ed.search_advance_backward(true);
4392 }
4393 }
4394 ed.push_buffer_cursor_to_textarea();
4395}
4396
4397fn regex_escape(s: &str) -> String {
4398 let mut out = String::with_capacity(s.len());
4399 for c in s.chars() {
4400 if matches!(
4401 c,
4402 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
4403 ) {
4404 out.push('\\');
4405 }
4406 out.push(c);
4407 }
4408 out
4409}
4410
4411pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
4425 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4426 op: Operator,
4427 motion_key: char,
4428 total_count: usize,
4429) {
4430 let input = Input {
4431 key: Key::Char(motion_key),
4432 ctrl: false,
4433 alt: false,
4434 shift: false,
4435 };
4436 let Some(motion) = parse_motion(&input) else {
4437 return;
4438 };
4439 let motion = match motion {
4440 Motion::FindRepeat { reverse } => match ed.vim.last_find {
4441 Some((ch, forward, till)) => Motion::Find {
4442 ch,
4443 forward: if reverse { !forward } else { forward },
4444 till,
4445 },
4446 None => return,
4447 },
4448 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
4450 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
4451 m => m,
4452 };
4453 apply_op_with_motion(ed, op, &motion, total_count);
4454 if let Motion::Find { ch, forward, till } = &motion {
4455 ed.vim.last_find = Some((*ch, *forward, *till));
4456 }
4457 if !ed.vim.replaying && op_is_change(op) {
4458 ed.vim.last_change = Some(LastChange::OpMotion {
4459 op,
4460 motion,
4461 count: total_count,
4462 inserted: None,
4463 });
4464 }
4465}
4466
4467pub(crate) fn apply_op_double<H: crate::types::Host>(
4470 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4471 op: Operator,
4472 total_count: usize,
4473) {
4474 if op == Operator::Comment {
4475 let row = buf_cursor_pos(&ed.buffer).row;
4477 let end_row = (row + total_count.max(1) - 1).min(ed.buffer.row_count().saturating_sub(1));
4478 ed.toggle_comment_range(row, end_row);
4479 ed.vim.mode = Mode::Normal;
4480 if !ed.vim.replaying {
4481 ed.vim.last_change = Some(LastChange::LineOp {
4482 op,
4483 count: total_count,
4484 inserted: None,
4485 });
4486 }
4487 return;
4488 }
4489 execute_line_op(ed, op, total_count);
4490 if !ed.vim.replaying {
4491 ed.vim.last_change = Some(LastChange::LineOp {
4492 op,
4493 count: total_count,
4494 inserted: None,
4495 });
4496 }
4497}
4498
4499fn gn_find_range<H: crate::types::Host>(
4504 ed: &Editor<hjkl_buffer::Buffer, H>,
4505 re: ®ex::Regex,
4506 forward: bool,
4507) -> Option<(crate::types::Pos, crate::types::Pos)> {
4508 use crate::types::{Cursor, Pos, Search};
4509 let cursor = Cursor::cursor(&ed.buffer);
4510 let contains =
4511 Search::find_prev(&ed.buffer, cursor, re).filter(|m| m.start <= cursor && cursor < m.end);
4512 let range = if let Some(m) = contains {
4513 m
4514 } else if forward {
4515 Search::find_next(&ed.buffer, cursor, re)?
4516 } else {
4517 Search::find_prev(&ed.buffer, cursor, re)?
4518 };
4519 let end_incl = if range.end.col > 0 {
4520 Pos::new(range.end.line, range.end.col - 1)
4521 } else {
4522 range.end
4523 };
4524 Some((range.start, end_incl))
4525}
4526
4527pub(crate) fn gn_operate<H: crate::types::Host>(
4532 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4533 op: Option<Operator>,
4534 forward: bool,
4535 count: usize,
4536) {
4537 use crate::types::{Cursor, Pos};
4538 if let Some(p) = ed.vim.last_search.clone() {
4540 ed.push_search_pattern(&p);
4541 }
4542 let Some(re) = ed.search_state().pattern.clone() else {
4543 return;
4544 };
4545 ed.sync_buffer_content_from_textarea();
4546
4547 let Some(mut range) = gn_find_range(ed, &re, forward) else {
4548 return;
4549 };
4550 for _ in 1..count.max(1) {
4552 let past = Pos::new(range.1.line, range.1.col + 1);
4553 Cursor::set_cursor(&mut ed.buffer, past);
4554 match gn_find_range(ed, &re, forward) {
4555 Some(r) => range = r,
4556 None => break,
4557 }
4558 }
4559 let start_t = (range.0.line as usize, range.0.col as usize);
4560 let end_t = (range.1.line as usize, range.1.col as usize);
4561
4562 match op {
4563 None => {
4564 ed.vim.visual_anchor = start_t;
4566 buf_set_cursor_rc(&mut ed.buffer, end_t.0, end_t.1);
4567 ed.vim.mode = Mode::Visual;
4568 ed.vim.current_mode = crate::VimMode::Visual;
4569 ed.push_buffer_cursor_to_textarea();
4570 }
4571 Some(Operator::Delete) => {
4572 ed.push_undo();
4573 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4574 clamp_cursor_to_normal_mode(ed);
4577 ed.push_buffer_cursor_to_textarea();
4578 if !ed.vim.replaying {
4579 ed.vim.last_change = Some(LastChange::GnOp {
4580 op: Operator::Delete,
4581 forward,
4582 inserted: None,
4583 });
4584 }
4585 }
4586 Some(Operator::Change) => {
4587 ed.push_undo();
4588 ed.vim.change_mark_start = Some(start_t);
4589 cut_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4590 if !ed.vim.replaying {
4591 ed.vim.last_change = Some(LastChange::GnOp {
4592 op: Operator::Change,
4593 forward,
4594 inserted: None,
4595 });
4596 }
4597 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4598 }
4599 Some(Operator::Yank) => {
4600 let text = read_vim_range(ed, start_t, end_t, RangeKind::Inclusive);
4601 if !text.is_empty() {
4602 ed.record_yank_to_host(text.clone());
4603 ed.record_yank(text, false);
4604 }
4605 buf_set_cursor_rc(&mut ed.buffer, start_t.0, start_t.1);
4606 ed.push_buffer_cursor_to_textarea();
4607 }
4608 Some(other @ (Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase)) => {
4609 ed.push_undo();
4612 apply_case_op_to_selection(ed, other, start_t, end_t, RangeKind::Inclusive);
4613 }
4614 Some(_) => {}
4615 }
4616}
4617
4618pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
4629 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4630 op: Operator,
4631 ch: char,
4632 total_count: usize,
4633) {
4634 if matches!(
4637 op,
4638 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13
4639 ) {
4640 let op_char = match op {
4641 Operator::Uppercase => 'U',
4642 Operator::Lowercase => 'u',
4643 Operator::ToggleCase => '~',
4644 Operator::Rot13 => '?',
4645 _ => unreachable!(),
4646 };
4647 if ch == op_char {
4648 execute_line_op(ed, op, total_count);
4649 if !ed.vim.replaying {
4650 ed.vim.last_change = Some(LastChange::LineOp {
4651 op,
4652 count: total_count,
4653 inserted: None,
4654 });
4655 }
4656 return;
4657 }
4658 }
4659 if ch == 'n' || ch == 'N' {
4661 gn_operate(ed, Some(op), ch == 'n', total_count);
4662 return;
4663 }
4664 let motion = match ch {
4665 'g' => Motion::FileTop,
4666 'e' => Motion::WordEndBack,
4667 'E' => Motion::BigWordEndBack,
4668 'j' => Motion::ScreenDown,
4669 'k' => Motion::ScreenUp,
4670 _ => return, };
4672 apply_op_with_motion(ed, op, &motion, total_count);
4673 if !ed.vim.replaying && op_is_change(op) {
4674 ed.vim.last_change = Some(LastChange::OpMotion {
4675 op,
4676 motion,
4677 count: total_count,
4678 inserted: None,
4679 });
4680 }
4681}
4682
4683pub(crate) fn apply_after_g<H: crate::types::Host>(
4688 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4689 ch: char,
4690 count: usize,
4691) {
4692 match ch {
4693 'g' => {
4694 let pre = ed.cursor();
4696 if count > 1 {
4697 ed.jump_cursor(count - 1, 0);
4698 } else {
4699 ed.jump_cursor(0, 0);
4700 }
4701 move_first_non_whitespace(ed);
4702 ed.sticky_col = Some(ed.cursor().1);
4705 if ed.cursor() != pre {
4706 ed.push_jump(pre);
4707 }
4708 }
4709 'e' => execute_motion(ed, Motion::WordEndBack, count),
4710 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
4711 '_' => execute_motion(ed, Motion::LastNonBlank, count),
4713 'M' => execute_motion(ed, Motion::LineMiddle, count),
4715 'm' => execute_motion(ed, Motion::ScreenLineMiddle, count),
4717 'v' => ed.reenter_last_visual(),
4720 'j' => execute_motion(ed, Motion::ScreenDown, count),
4724 'k' => execute_motion(ed, Motion::ScreenUp, count),
4725 'U' => {
4729 ed.vim.pending = Pending::Op {
4730 op: Operator::Uppercase,
4731 count1: count,
4732 };
4733 }
4734 'u' => {
4735 ed.vim.pending = Pending::Op {
4736 op: Operator::Lowercase,
4737 count1: count,
4738 };
4739 }
4740 '~' => {
4741 ed.vim.pending = Pending::Op {
4742 op: Operator::ToggleCase,
4743 count1: count,
4744 };
4745 }
4746 '?' => {
4747 ed.vim.pending = Pending::Op {
4749 op: Operator::Rot13,
4750 count1: count,
4751 };
4752 }
4753 'q' => {
4754 ed.vim.pending = Pending::Op {
4757 op: Operator::Reflow,
4758 count1: count,
4759 };
4760 }
4761 'w' => {
4762 ed.vim.pending = Pending::Op {
4765 op: Operator::ReflowKeepCursor,
4766 count1: count,
4767 };
4768 }
4769 'J' => {
4770 let joins = count.max(2) - 1;
4773 for _ in 0..joins {
4774 ed.push_undo();
4775 join_line_raw(ed);
4776 }
4777 if !ed.vim.replaying {
4778 ed.vim.last_change = Some(LastChange::JoinLine { count: joins });
4779 }
4780 }
4781 'd' => {
4782 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
4787 }
4788 'i' => {
4793 if let Some((row, col)) = ed.vim.last_insert_pos {
4794 ed.jump_cursor(row, col);
4795 }
4796 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4797 }
4798 'c' => {
4803 ed.vim.pending = Pending::Op {
4804 op: Operator::Comment,
4805 count1: count,
4806 };
4807 }
4808 'p' => paste_bridge(ed, false, count.max(1), true, false),
4811 'P' => paste_bridge(ed, true, count.max(1), true, false),
4812 'n' => gn_operate(ed, None, true, count.max(1)),
4814 'N' => gn_operate(ed, None, false, count.max(1)),
4815 ';' => walk_change_list(ed, -1, count.max(1)),
4818 ',' => walk_change_list(ed, 1, count.max(1)),
4819 '*' => execute_motion(
4823 ed,
4824 Motion::WordAtCursor {
4825 forward: true,
4826 whole_word: false,
4827 },
4828 count,
4829 ),
4830 '#' => execute_motion(
4831 ed,
4832 Motion::WordAtCursor {
4833 forward: false,
4834 whole_word: false,
4835 },
4836 count,
4837 ),
4838 '&' => {
4841 let cmd = match ed.vim.last_substitute.clone() {
4842 Some(c) => c,
4843 None => {
4844 return;
4849 }
4850 };
4851 let last_row = buf_row_count(&ed.buffer).saturating_sub(1) as u32;
4852 let r = 0u32..=last_row;
4853 let _ = crate::substitute::apply_substitute(ed, &cmd, r);
4856 ed.vim.last_substitute = Some(cmd);
4859 }
4860 _ => {}
4861 }
4862}
4863
4864pub(crate) fn ampersand_repeat<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4868 let Some(mut cmd) = ed.vim.last_substitute.clone() else {
4869 return;
4870 };
4871 cmd.flags = crate::substitute::SubstFlags::default();
4872 let row = buf_cursor_pos(&ed.buffer).row as u32;
4873 let _ = crate::substitute::apply_substitute(ed, &cmd, row..=row);
4874}
4875
4876pub(crate) fn apply_after_z<H: crate::types::Host>(
4881 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4882 ch: char,
4883 count: usize,
4884) {
4885 use crate::editor::CursorScrollTarget;
4886 let row = ed.cursor().0;
4887 match ch {
4888 'z' => {
4889 ed.scroll_cursor_to(CursorScrollTarget::Center);
4890 ed.vim.viewport_pinned = true;
4891 ed.vim.scroll_anim_hint = true;
4892 }
4893 't' => {
4894 ed.scroll_cursor_to(CursorScrollTarget::Top);
4895 ed.vim.viewport_pinned = true;
4896 ed.vim.scroll_anim_hint = true;
4897 }
4898 'b' => {
4899 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
4900 ed.vim.viewport_pinned = true;
4901 ed.vim.scroll_anim_hint = true;
4902 }
4903 'o' => {
4908 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
4909 }
4910 'c' => {
4911 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
4912 }
4913 'a' => {
4914 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
4915 }
4916 'R' => {
4917 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
4918 }
4919 'M' => {
4920 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
4921 }
4922 'E' => {
4923 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
4924 }
4925 'd' => {
4926 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
4927 }
4928 'f' => {
4929 if matches!(
4930 ed.vim.mode,
4931 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
4932 ) {
4933 let anchor_row = match ed.vim.mode {
4936 Mode::VisualLine => ed.vim.visual_line_anchor,
4937 Mode::VisualBlock => ed.vim.block_anchor.0,
4938 _ => ed.vim.visual_anchor.0,
4939 };
4940 let cur = ed.cursor().0;
4941 let top = anchor_row.min(cur);
4942 let bot = anchor_row.max(cur);
4943 ed.apply_fold_op(crate::types::FoldOp::Add {
4944 start_row: top,
4945 end_row: bot,
4946 closed: true,
4947 });
4948 ed.vim.mode = Mode::Normal;
4949 } else {
4950 ed.vim.pending = Pending::Op {
4955 op: Operator::Fold,
4956 count1: count,
4957 };
4958 }
4959 }
4960 _ => {}
4961 }
4962}
4963
4964pub(crate) fn apply_find_char<H: crate::types::Host>(
4970 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4971 ch: char,
4972 forward: bool,
4973 till: bool,
4974 count: usize,
4975) {
4976 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
4977 ed.vim.last_find = Some((ch, forward, till));
4978 ed.vim.last_horizontal_motion = LastHorizontalMotion::FindChar;
4979}
4980
4981pub(crate) fn apply_sneak<H: crate::types::Host>(
4993 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4994 c1: char,
4995 c2: char,
4996 forward: bool,
4997 count: usize,
4998) {
4999 let count = count.max(1);
5000 let (start_row, start_col) = ed.cursor();
5001 let row_count = buf_row_count(&ed.buffer);
5002
5003 let result = if forward {
5004 sneak_scan_forward(ed, start_row, start_col, c1, c2, count)
5005 } else {
5006 sneak_scan_backward(ed, start_row, start_col, c1, c2, count)
5007 };
5008
5009 if let Some((row, col)) = result {
5010 buf_set_cursor_rc(&mut ed.buffer, row, col);
5011 ed.push_buffer_cursor_to_textarea();
5012 let _ = row_count; }
5014
5015 ed.vim.last_sneak = Some(((c1, c2), forward));
5016 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
5017}
5018
5019fn sneak_scan_forward<H: crate::types::Host>(
5022 ed: &Editor<hjkl_buffer::Buffer, H>,
5023 start_row: usize,
5024 start_col: usize,
5025 c1: char,
5026 c2: char,
5027 count: usize,
5028) -> Option<(usize, usize)> {
5029 let row_count = buf_row_count(&ed.buffer);
5030 let mut hits = 0usize;
5031 for row in start_row..row_count {
5032 let line = buf_line(&ed.buffer, row).unwrap_or_default();
5033 let chars: Vec<char> = line.chars().collect();
5034 let col_start = if row == start_row { start_col + 1 } else { 0 };
5036 if col_start + 1 > chars.len() {
5037 continue;
5038 }
5039 for col in col_start..chars.len().saturating_sub(1) {
5040 if chars[col] == c1 && chars[col + 1] == c2 {
5041 hits += 1;
5042 if hits == count {
5043 return Some((row, col));
5044 }
5045 }
5046 }
5047 }
5048 None
5049}
5050
5051fn sneak_scan_backward<H: crate::types::Host>(
5054 ed: &Editor<hjkl_buffer::Buffer, H>,
5055 start_row: usize,
5056 start_col: usize,
5057 c1: char,
5058 c2: char,
5059 count: usize,
5060) -> Option<(usize, usize)> {
5061 let row_count = buf_row_count(&ed.buffer);
5062 let mut hits = 0usize;
5063 let rows_to_scan = (0..row_count).rev().skip(row_count - start_row - 1);
5065 for row in rows_to_scan {
5066 let line = buf_line(&ed.buffer, row).unwrap_or_default();
5067 let chars: Vec<char> = line.chars().collect();
5068 let col_end = if row == start_row {
5070 start_col.saturating_sub(1)
5071 } else if chars.is_empty() {
5072 continue;
5073 } else {
5074 chars.len().saturating_sub(1)
5075 };
5076 if col_end == 0 {
5077 continue;
5078 }
5079 for col in (0..col_end).rev() {
5081 if col + 1 < chars.len() && chars[col] == c1 && chars[col + 1] == c2 {
5082 hits += 1;
5083 if hits == count {
5084 return Some((row, col));
5085 }
5086 }
5087 }
5088 }
5089 None
5090}
5091
5092pub(crate) fn apply_op_sneak<H: crate::types::Host>(
5099 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5100 op: Operator,
5101 c1: char,
5102 c2: char,
5103 forward: bool,
5104 total_count: usize,
5105) {
5106 let start = ed.cursor();
5107 let result = if forward {
5108 sneak_scan_forward(ed, start.0, start.1, c1, c2, total_count)
5109 } else {
5110 sneak_scan_backward(ed, start.0, start.1, c1, c2, total_count)
5111 };
5112 let Some(end) = result else {
5113 return;
5114 };
5115 ed.jump_cursor(end.0, end.1);
5118 let end_cur = ed.cursor();
5119 ed.jump_cursor(start.0, start.1);
5120 run_operator_over_range(ed, op, start, end_cur, RangeKind::Exclusive);
5121 ed.vim.last_sneak = Some(((c1, c2), forward));
5122 ed.vim.last_horizontal_motion = LastHorizontalMotion::Sneak;
5123 if !ed.vim.replaying && op_is_change(op) {
5124 }
5128}
5129
5130pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
5136 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5137 op: Operator,
5138 ch: char,
5139 forward: bool,
5140 till: bool,
5141 total_count: usize,
5142) {
5143 let motion = Motion::Find { ch, forward, till };
5144 apply_op_with_motion(ed, op, &motion, total_count);
5145 ed.vim.last_find = Some((ch, forward, till));
5146 if !ed.vim.replaying && op_is_change(op) {
5147 ed.vim.last_change = Some(LastChange::OpMotion {
5148 op,
5149 motion,
5150 count: total_count,
5151 inserted: None,
5152 });
5153 }
5154}
5155
5156pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
5165 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5166 op: Operator,
5167 ch: char,
5168 inner: bool,
5169 total_count: usize,
5170) -> bool {
5171 let obj = match ch {
5174 'w' => TextObject::Word { big: false },
5175 'W' => TextObject::Word { big: true },
5176 '"' | '\'' | '`' => TextObject::Quote(ch),
5177 '(' | ')' | 'b' => TextObject::Bracket('('),
5178 '[' | ']' => TextObject::Bracket('['),
5179 '{' | '}' | 'B' => TextObject::Bracket('{'),
5180 '<' | '>' => TextObject::Bracket('<'),
5181 'p' => TextObject::Paragraph,
5182 't' => TextObject::XmlTag,
5183 's' => TextObject::Sentence,
5184 _ => return false,
5185 };
5186 apply_op_with_text_object(ed, op, obj, inner, total_count.max(1));
5187 if !ed.vim.replaying && op_is_change(op) {
5188 ed.vim.last_change = Some(LastChange::OpTextObj {
5189 op,
5190 obj,
5191 inner,
5192 inserted: None,
5193 });
5194 }
5195 true
5196}
5197
5198pub(crate) fn retreat_one<H: crate::types::Host>(
5200 ed: &Editor<hjkl_buffer::Buffer, H>,
5201 pos: (usize, usize),
5202) -> (usize, usize) {
5203 let (r, c) = pos;
5204 if c > 0 {
5205 (r, c - 1)
5206 } else if r > 0 {
5207 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
5208 (r - 1, prev_len)
5209 } else {
5210 (0, 0)
5211 }
5212}
5213
5214fn begin_insert_noundo<H: crate::types::Host>(
5216 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5217 count: usize,
5218 reason: InsertReason,
5219) {
5220 let reason = if ed.vim.replaying {
5221 InsertReason::ReplayOnly
5222 } else {
5223 reason
5224 };
5225 let (row, col) = ed.cursor();
5226 ed.vim.insert_session = Some(InsertSession {
5227 count,
5228 row_min: row,
5229 row_max: row,
5230 before_rope: crate::types::Query::rope(&ed.buffer),
5231 reason,
5232 start_row: row,
5233 start_col: col,
5234 });
5235 ed.vim.mode = Mode::Insert;
5236 ed.vim.current_mode = crate::VimMode::Insert;
5238 drop_blame_if_left_normal(ed);
5239}
5240
5241pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
5244 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5245 op: Operator,
5246 motion: &Motion,
5247 count: usize,
5248) {
5249 let start = ed.cursor();
5250 apply_motion_cursor_ctx(ed, motion, count, true);
5255 let end = ed.cursor();
5256 let kind = motion_kind(motion);
5257 ed.jump_cursor(start.0, start.1);
5259
5260 if op == Operator::Comment {
5262 let top = start.0.min(end.0);
5263 let bot = start.0.max(end.0);
5264 ed.toggle_comment_range(top, bot);
5265 ed.vim.mode = Mode::Normal;
5266 return;
5267 }
5268
5269 run_operator_over_range(ed, op, start, end, kind);
5270}
5271
5272fn apply_op_with_text_object<H: crate::types::Host>(
5273 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5274 op: Operator,
5275 obj: TextObject,
5276 inner: bool,
5277 count: usize,
5278) {
5279 let Some((mut start, mut end, mut kind)) = text_object_range(ed, obj, inner, count) else {
5280 return;
5281 };
5282 if inner
5291 && matches!(obj, TextObject::Bracket(_))
5292 && kind == RangeKind::Exclusive
5293 && end.0 > start.0
5294 && end.1 == 0
5295 {
5296 let prev = end.0 - 1;
5297 let prev_len = buf_line_chars(&ed.buffer, prev);
5298 let fnb = buf_line(&ed.buffer, start.0)
5299 .unwrap_or_default()
5300 .chars()
5301 .take_while(|c| *c == ' ' || *c == '\t')
5302 .count();
5303 if start.1 <= fnb {
5304 start = (start.0, 0);
5305 end = (prev, prev_len);
5306 kind = RangeKind::Linewise;
5307 } else {
5308 end = (prev, prev_len.saturating_sub(1));
5309 kind = RangeKind::Inclusive;
5310 }
5311 }
5312 ed.jump_cursor(start.0, start.1);
5313 run_operator_over_range(ed, op, start, end, kind);
5314}
5315
5316fn motion_kind(motion: &Motion) -> RangeKind {
5317 match motion {
5318 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
5319 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
5320 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
5321 RangeKind::Linewise
5322 }
5323 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
5324 RangeKind::Inclusive
5325 }
5326 Motion::Find { .. } => RangeKind::Inclusive,
5327 Motion::MatchBracket => RangeKind::Inclusive,
5328 Motion::UnmatchedBracket { .. } => RangeKind::Exclusive,
5331 Motion::LineEnd => RangeKind::Inclusive,
5333 Motion::FirstNonBlankNextLine
5335 | Motion::FirstNonBlankPrevLine
5336 | Motion::FirstNonBlankLine => RangeKind::Linewise,
5337 Motion::SectionBackward
5339 | Motion::SectionForward
5340 | Motion::SectionEndBackward
5341 | Motion::SectionEndForward => RangeKind::Exclusive,
5342 _ => RangeKind::Exclusive,
5343 }
5344}
5345
5346fn change_linewise_rows<H: crate::types::Host>(
5355 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5356 top_row: usize,
5357 end_row: usize,
5358) {
5359 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5360 ed.vim.change_mark_start = Some((top_row, 0));
5362 ed.push_undo();
5363 ed.sync_buffer_content_from_textarea();
5364 let payload = read_vim_range(ed, (top_row, 0), (end_row, 0), RangeKind::Linewise);
5366 if end_row > top_row {
5368 ed.mutate_edit(Edit::DeleteRange {
5369 start: Position::new(top_row + 1, 0),
5370 end: Position::new(end_row, 0),
5371 kind: BufKind::Line,
5372 });
5373 }
5374 let indent_chars = if ed.settings.autoindent {
5377 let line = hjkl_buffer::rope_line_str(&crate::types::Query::rope(&ed.buffer), top_row);
5378 line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
5379 } else {
5380 0
5381 };
5382 let line_chars = buf_line_chars(&ed.buffer, top_row);
5383 if line_chars > indent_chars {
5384 ed.mutate_edit(Edit::DeleteRange {
5385 start: Position::new(top_row, indent_chars),
5386 end: Position::new(top_row, line_chars),
5387 kind: BufKind::Char,
5388 });
5389 }
5390 if !payload.is_empty() {
5391 ed.record_yank_to_host(payload.clone());
5392 ed.record_delete(payload, true);
5393 }
5394 buf_set_cursor_rc(&mut ed.buffer, top_row, indent_chars);
5395 ed.push_buffer_cursor_to_textarea();
5396 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5397}
5398
5399fn run_operator_over_range<H: crate::types::Host>(
5400 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5401 op: Operator,
5402 start: (usize, usize),
5403 end: (usize, usize),
5404 kind: RangeKind,
5405) {
5406 let (top, bot) = order(start, end);
5407 if top == bot && !matches!(kind, RangeKind::Linewise) {
5412 if op == Operator::Change {
5413 ed.vim.change_mark_start = Some(top);
5414 ed.push_undo();
5415 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5416 }
5417 return;
5418 }
5419
5420 match op {
5421 Operator::Yank => {
5422 let text = read_vim_range(ed, top, bot, kind);
5423 if !text.is_empty() {
5424 ed.record_yank_to_host(text.clone());
5425 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
5426 }
5427 let rbr = match kind {
5431 RangeKind::Linewise => {
5432 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
5433 (bot.0, last_col)
5434 }
5435 RangeKind::Inclusive => (bot.0, bot.1),
5436 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
5437 };
5438 ed.set_mark('[', top);
5439 ed.set_mark(']', rbr);
5440 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5441 ed.push_buffer_cursor_to_textarea();
5442 }
5443 Operator::Delete => {
5444 ed.push_undo();
5445 cut_vim_range(ed, top, bot, kind);
5446 if !matches!(kind, RangeKind::Linewise) {
5451 clamp_cursor_to_normal_mode(ed);
5452 }
5453 ed.vim.mode = Mode::Normal;
5454 let pos = ed.cursor();
5458 ed.set_mark('[', pos);
5459 ed.set_mark(']', pos);
5460 }
5461 Operator::Change => {
5462 if matches!(kind, RangeKind::Linewise) {
5467 change_linewise_rows(ed, top.0, bot.0);
5471 } else {
5472 ed.vim.change_mark_start = Some(top);
5474 ed.push_undo();
5475 cut_vim_range(ed, top, bot, kind);
5476 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5477 }
5478 }
5479 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
5480 apply_case_op_to_selection(ed, op, top, bot, kind);
5481 }
5482 Operator::Indent | Operator::Outdent => {
5483 ed.push_undo();
5486 if op == Operator::Indent {
5487 indent_rows(ed, top.0, bot.0, 1);
5488 } else {
5489 outdent_rows(ed, top.0, bot.0, 1);
5490 }
5491 ed.vim.mode = Mode::Normal;
5492 }
5493 Operator::Fold => {
5494 if bot.0 >= top.0 {
5498 ed.apply_fold_op(crate::types::FoldOp::Add {
5499 start_row: top.0,
5500 end_row: bot.0,
5501 closed: true,
5502 });
5503 }
5504 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5505 ed.push_buffer_cursor_to_textarea();
5506 ed.vim.mode = Mode::Normal;
5507 }
5508 Operator::Reflow => {
5509 ed.push_undo();
5510 reflow_rows(ed, top.0, bot.0);
5511 ed.vim.mode = Mode::Normal;
5512 }
5513 Operator::ReflowKeepCursor => {
5514 let saved = ed.cursor();
5517 ed.push_undo();
5518 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
5519 let (new_row, new_col) = reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
5520 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
5521 ed.push_buffer_cursor_to_textarea();
5522 ed.sticky_col = Some(new_col);
5523 ed.vim.mode = Mode::Normal;
5524 }
5525 Operator::AutoIndent => {
5526 ed.push_undo();
5528 auto_indent_rows(ed, top.0, bot.0);
5529 ed.vim.mode = Mode::Normal;
5530 }
5531 Operator::Filter => {
5532 }
5537 Operator::Comment => {
5538 }
5541 }
5542}
5543
5544pub(crate) fn delete_range_bridge<H: crate::types::Host>(
5561 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5562 start: (usize, usize),
5563 end: (usize, usize),
5564 kind: RangeKind,
5565 register: char,
5566) {
5567 ed.vim.pending_register = Some(register);
5568 run_operator_over_range(ed, Operator::Delete, start, end, kind);
5569}
5570
5571pub(crate) fn yank_range_bridge<H: crate::types::Host>(
5574 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5575 start: (usize, usize),
5576 end: (usize, usize),
5577 kind: RangeKind,
5578 register: char,
5579) {
5580 ed.vim.pending_register = Some(register);
5581 run_operator_over_range(ed, Operator::Yank, start, end, kind);
5582}
5583
5584pub(crate) fn change_range_bridge<H: crate::types::Host>(
5589 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5590 start: (usize, usize),
5591 end: (usize, usize),
5592 kind: RangeKind,
5593 register: char,
5594) {
5595 ed.vim.pending_register = Some(register);
5596 run_operator_over_range(ed, Operator::Change, start, end, kind);
5597}
5598
5599pub(crate) fn indent_range_bridge<H: crate::types::Host>(
5604 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5605 start: (usize, usize),
5606 end: (usize, usize),
5607 count: i32,
5608 shiftwidth: u32,
5609) {
5610 if count == 0 {
5611 return;
5612 }
5613 let (top_row, bot_row) = if start.0 <= end.0 {
5614 (start.0, end.0)
5615 } else {
5616 (end.0, start.0)
5617 };
5618 let original_sw = ed.settings().shiftwidth;
5620 if shiftwidth > 0 {
5621 ed.settings_mut().shiftwidth = shiftwidth as usize;
5622 }
5623 ed.push_undo();
5624 let abs_count = count.unsigned_abs() as usize;
5625 if count > 0 {
5626 indent_rows(ed, top_row, bot_row, abs_count);
5627 } else {
5628 outdent_rows(ed, top_row, bot_row, abs_count);
5629 }
5630 if shiftwidth > 0 {
5631 ed.settings_mut().shiftwidth = original_sw;
5632 }
5633 ed.vim.mode = Mode::Normal;
5634}
5635
5636pub(crate) fn case_range_bridge<H: crate::types::Host>(
5640 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5641 start: (usize, usize),
5642 end: (usize, usize),
5643 kind: RangeKind,
5644 op: Operator,
5645) {
5646 match op {
5647 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {}
5648 _ => return,
5649 }
5650 let (top, bot) = order(start, end);
5651 apply_case_op_to_selection(ed, op, top, bot, kind);
5652}
5653
5654pub(crate) fn delete_block_bridge<H: crate::types::Host>(
5675 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5676 top_row: usize,
5677 bot_row: usize,
5678 left_col: usize,
5679 right_col: usize,
5680 register: char,
5681) {
5682 ed.vim.pending_register = Some(register);
5683 let saved_anchor = ed.vim.block_anchor;
5684 let saved_vcol = ed.vim.block_vcol;
5685 ed.vim.block_anchor = (top_row, left_col);
5686 ed.vim.block_vcol = right_col;
5687 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5689 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5691 apply_block_operator(ed, Operator::Delete, 1);
5692 ed.vim.block_anchor = saved_anchor;
5696 ed.vim.block_vcol = saved_vcol;
5697}
5698
5699pub(crate) fn yank_block_bridge<H: crate::types::Host>(
5701 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5702 top_row: usize,
5703 bot_row: usize,
5704 left_col: usize,
5705 right_col: usize,
5706 register: char,
5707) {
5708 ed.vim.pending_register = Some(register);
5709 let saved_anchor = ed.vim.block_anchor;
5710 let saved_vcol = ed.vim.block_vcol;
5711 ed.vim.block_anchor = (top_row, left_col);
5712 ed.vim.block_vcol = right_col;
5713 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5714 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5715 apply_block_operator(ed, Operator::Yank, 1);
5716 ed.vim.block_anchor = saved_anchor;
5717 ed.vim.block_vcol = saved_vcol;
5718}
5719
5720pub(crate) fn change_block_bridge<H: crate::types::Host>(
5723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5724 top_row: usize,
5725 bot_row: usize,
5726 left_col: usize,
5727 right_col: usize,
5728 register: char,
5729) {
5730 ed.vim.pending_register = Some(register);
5731 let saved_anchor = ed.vim.block_anchor;
5732 let saved_vcol = ed.vim.block_vcol;
5733 ed.vim.block_anchor = (top_row, left_col);
5734 ed.vim.block_vcol = right_col;
5735 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
5736 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
5737 apply_block_operator(ed, Operator::Change, 1);
5738 ed.vim.block_anchor = saved_anchor;
5739 ed.vim.block_vcol = saved_vcol;
5740}
5741
5742pub(crate) fn indent_block_bridge<H: crate::types::Host>(
5746 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5747 top_row: usize,
5748 bot_row: usize,
5749 count: i32,
5750) {
5751 if count == 0 {
5752 return;
5753 }
5754 ed.push_undo();
5755 let abs = count.unsigned_abs() as usize;
5756 if count > 0 {
5757 indent_rows(ed, top_row, bot_row, abs);
5758 } else {
5759 outdent_rows(ed, top_row, bot_row, abs);
5760 }
5761 ed.vim.mode = Mode::Normal;
5762}
5763
5764pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
5768 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5769 start: (usize, usize),
5770 end: (usize, usize),
5771) {
5772 let (top_row, bot_row) = if start.0 <= end.0 {
5773 (start.0, end.0)
5774 } else {
5775 (end.0, start.0)
5776 };
5777 ed.push_undo();
5778 auto_indent_rows(ed, top_row, bot_row);
5779 ed.vim.mode = Mode::Normal;
5780}
5781
5782pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
5793 ed: &Editor<hjkl_buffer::Buffer, H>,
5794) -> Option<((usize, usize), (usize, usize))> {
5795 word_text_object(ed, true, false)
5796}
5797
5798pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
5801 ed: &Editor<hjkl_buffer::Buffer, H>,
5802) -> Option<((usize, usize), (usize, usize))> {
5803 word_text_object(ed, false, false)
5804}
5805
5806pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
5809 ed: &Editor<hjkl_buffer::Buffer, H>,
5810) -> Option<((usize, usize), (usize, usize))> {
5811 word_text_object(ed, true, true)
5812}
5813
5814pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
5817 ed: &Editor<hjkl_buffer::Buffer, H>,
5818) -> Option<((usize, usize), (usize, usize))> {
5819 word_text_object(ed, false, true)
5820}
5821
5822pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
5838 ed: &Editor<hjkl_buffer::Buffer, H>,
5839 quote: char,
5840) -> Option<((usize, usize), (usize, usize))> {
5841 quote_text_object(ed, quote, true)
5842}
5843
5844pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
5847 ed: &Editor<hjkl_buffer::Buffer, H>,
5848 quote: char,
5849) -> Option<((usize, usize), (usize, usize))> {
5850 quote_text_object(ed, quote, false)
5851}
5852
5853pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
5861 ed: &Editor<hjkl_buffer::Buffer, H>,
5862 open: char,
5863) -> Option<((usize, usize), (usize, usize))> {
5864 bracket_text_object(ed, open, true, 1).map(|(s, e, _kind)| (s, e))
5865}
5866
5867pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
5871 ed: &Editor<hjkl_buffer::Buffer, H>,
5872 open: char,
5873) -> Option<((usize, usize), (usize, usize))> {
5874 bracket_text_object(ed, open, false, 1).map(|(s, e, _kind)| (s, e))
5875}
5876
5877pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
5882 ed: &Editor<hjkl_buffer::Buffer, H>,
5883) -> Option<((usize, usize), (usize, usize))> {
5884 sentence_text_object(ed, true)
5885}
5886
5887pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
5890 ed: &Editor<hjkl_buffer::Buffer, H>,
5891) -> Option<((usize, usize), (usize, usize))> {
5892 sentence_text_object(ed, false)
5893}
5894
5895pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
5900 ed: &Editor<hjkl_buffer::Buffer, H>,
5901) -> Option<((usize, usize), (usize, usize))> {
5902 paragraph_text_object(ed, true)
5903}
5904
5905pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
5908 ed: &Editor<hjkl_buffer::Buffer, H>,
5909) -> Option<((usize, usize), (usize, usize))> {
5910 paragraph_text_object(ed, false)
5911}
5912
5913pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
5919 ed: &Editor<hjkl_buffer::Buffer, H>,
5920) -> Option<((usize, usize), (usize, usize))> {
5921 tag_text_object(ed, true)
5922}
5923
5924pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
5927 ed: &Editor<hjkl_buffer::Buffer, H>,
5928) -> Option<((usize, usize), (usize, usize))> {
5929 tag_text_object(ed, false)
5930}
5931
5932pub(crate) fn rope_line_to_str(rope: &ropey::Rope, r: usize) -> String {
5937 let s = rope.line(r).to_string();
5938 if s.ends_with('\n') {
5940 s[..s.len() - 1].to_string()
5941 } else {
5942 s
5943 }
5944}
5945
5946pub(crate) fn rope_row_range_str(rope: &ropey::Rope, lo: usize, hi: usize) -> String {
5949 let n = rope.len_lines();
5950 let lo = lo.min(n.saturating_sub(1));
5951 let hi = hi.min(n.saturating_sub(1));
5952 if lo > hi {
5953 return String::new();
5954 }
5955 let start_byte = rope.line_to_byte(lo);
5957 let end_byte = if hi + 1 < n {
5960 rope.line_to_byte(hi + 1).saturating_sub(1)
5963 } else {
5964 rope.len_bytes()
5965 };
5966 rope.byte_slice(start_byte..end_byte).to_string()
5967}
5968
5969pub(crate) fn rope_to_lines_vec(rope: &ropey::Rope) -> Vec<String> {
5973 let n = rope.len_lines();
5974 (0..n).map(|r| rope_line_to_str(rope, r)).collect()
5975}
5976
5977fn greedy_wrap(original: &[String], width: usize) -> Vec<String> {
5981 let mut wrapped: Vec<String> = Vec::new();
5982 let mut paragraph: Vec<String> = Vec::new();
5983 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
5984 if para.is_empty() {
5985 return;
5986 }
5987 let words = para.join(" ");
5988 let mut current = String::new();
5989 for word in words.split_whitespace() {
5990 let extra = if current.is_empty() {
5991 word.chars().count()
5992 } else {
5993 current.chars().count() + 1 + word.chars().count()
5994 };
5995 if extra > width && !current.is_empty() {
5996 out.push(std::mem::take(&mut current));
5997 current.push_str(word);
5998 } else if current.is_empty() {
5999 current.push_str(word);
6000 } else {
6001 current.push(' ');
6002 current.push_str(word);
6003 }
6004 }
6005 if !current.is_empty() {
6006 out.push(current);
6007 }
6008 para.clear();
6009 };
6010 for line in original {
6011 if line.trim().is_empty() {
6012 flush(&mut paragraph, &mut wrapped, width);
6013 wrapped.push(String::new());
6014 } else {
6015 paragraph.push(line.clone());
6016 }
6017 }
6018 flush(&mut paragraph, &mut wrapped, width);
6019 wrapped
6020}
6021
6022fn reflow_rows<H: crate::types::Host>(
6028 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6029 top: usize,
6030 bot: usize,
6031) {
6032 let width = ed.settings().textwidth.max(1);
6033 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6034 let bot = bot.min(lines.len().saturating_sub(1));
6035 if top > bot {
6036 return;
6037 }
6038 let original = lines[top..=bot].to_vec();
6039 let wrapped = greedy_wrap(&original, width);
6040
6041 let last_offset = wrapped
6044 .iter()
6045 .rposition(|l| !l.trim().is_empty())
6046 .unwrap_or(0);
6047 let last_row = top + last_offset;
6048
6049 let after: Vec<String> = lines.split_off(bot + 1);
6051 lines.truncate(top);
6052 lines.extend(wrapped);
6053 lines.extend(after);
6054 ed.restore(lines, (last_row, 0));
6055 move_first_non_whitespace(ed);
6056 ed.mark_content_dirty();
6057}
6058
6059fn reflow_rows_keep_cursor<H: crate::types::Host>(
6063 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6064 top: usize,
6065 bot: usize,
6066) -> (Vec<String>, Vec<String>) {
6067 let width = ed.settings().textwidth.max(1);
6068 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6069 let bot = bot.min(lines.len().saturating_sub(1));
6070 if top > bot {
6071 return (Vec::new(), Vec::new());
6072 }
6073 let original = lines[top..=bot].to_vec();
6074 let wrapped = greedy_wrap(&original, width);
6075
6076 let after: Vec<String> = lines.split_off(bot + 1);
6077 lines.truncate(top);
6078 lines.extend(wrapped.clone());
6079 lines.extend(after);
6080 ed.restore(lines, (top, 0));
6081 ed.mark_content_dirty();
6082 (original, wrapped)
6083}
6084
6085fn reflow_keep_cursor(
6097 top: usize,
6098 cursor_row: usize,
6099 cursor_col: usize,
6100 before_lines: &[String],
6101 after_lines: &[String],
6102) -> (usize, usize) {
6103 let relative_row = cursor_row.saturating_sub(top);
6123 let mut char_offset: usize = 0;
6124 for (i, line) in before_lines.iter().enumerate() {
6125 if i == relative_row {
6126 let line_len = line.chars().count();
6128 char_offset += cursor_col.min(line_len);
6129 break;
6130 }
6131 char_offset += line.chars().count() + 1;
6133 }
6134
6135 let mut remaining = char_offset;
6137 for (i, line) in after_lines.iter().enumerate() {
6138 let len = line.chars().count();
6139 if remaining <= len {
6140 let col = remaining.min(if len == 0 { 0 } else { len.saturating_sub(1) });
6142 return (top + i, col);
6143 }
6144 remaining = remaining.saturating_sub(len + 1);
6146 }
6147
6148 let last = after_lines.len().saturating_sub(1);
6150 let last_len = after_lines
6151 .get(last)
6152 .map(|l| l.chars().count())
6153 .unwrap_or(0);
6154 let col = if last_len == 0 { 0 } else { last_len - 1 };
6155 (top + last, col)
6156}
6157
6158fn apply_case_op_to_selection<H: crate::types::Host>(
6164 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6165 op: Operator,
6166 top: (usize, usize),
6167 bot: (usize, usize),
6168 kind: RangeKind,
6169) {
6170 use hjkl_buffer::Edit;
6171 ed.push_undo();
6172 let saved_yank = ed.yank().to_string();
6173 let saved_yank_linewise = ed.vim.yank_linewise;
6174 let selection = cut_vim_range(ed, top, bot, kind);
6175 let transformed = match op {
6176 Operator::Uppercase => selection.to_uppercase(),
6177 Operator::Lowercase => selection.to_lowercase(),
6178 Operator::ToggleCase => toggle_case_str(&selection),
6179 Operator::Rot13 => rot13_str(&selection),
6180 _ => unreachable!(),
6181 };
6182 if !transformed.is_empty() {
6183 let cursor = buf_cursor_pos(&ed.buffer);
6184 ed.mutate_edit(Edit::InsertStr {
6185 at: cursor,
6186 text: transformed,
6187 });
6188 }
6189 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6190 ed.push_buffer_cursor_to_textarea();
6191 ed.set_yank(saved_yank);
6192 ed.vim.yank_linewise = saved_yank_linewise;
6193 ed.vim.mode = Mode::Normal;
6194}
6195
6196fn indent_rows<H: crate::types::Host>(
6201 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6202 top: usize,
6203 bot: usize,
6204 count: usize,
6205) {
6206 ed.sync_buffer_content_from_textarea();
6207 let width = ed.settings().shiftwidth * count.max(1);
6208 let pad: String = " ".repeat(width);
6209 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6210 let bot = bot.min(lines.len().saturating_sub(1));
6211 for line in lines.iter_mut().take(bot + 1).skip(top) {
6212 if !line.is_empty() {
6213 line.insert_str(0, &pad);
6214 }
6215 }
6216 ed.restore(lines, (top, 0));
6219 move_first_non_whitespace(ed);
6220}
6221
6222fn outdent_rows<H: crate::types::Host>(
6226 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6227 top: usize,
6228 bot: usize,
6229 count: usize,
6230) {
6231 ed.sync_buffer_content_from_textarea();
6232 let width = ed.settings().shiftwidth * count.max(1);
6233 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6234 let bot = bot.min(lines.len().saturating_sub(1));
6235 for line in lines.iter_mut().take(bot + 1).skip(top) {
6236 let strip: usize = line
6237 .chars()
6238 .take(width)
6239 .take_while(|c| *c == ' ' || *c == '\t')
6240 .count();
6241 if strip > 0 {
6242 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
6243 line.drain(..byte_len);
6244 }
6245 }
6246 ed.restore(lines, (top, 0));
6247 move_first_non_whitespace(ed);
6248}
6249
6250fn bracket_net(line: &str) -> i32 {
6277 let mut net: i32 = 0;
6278 let mut chars = line.chars().peekable();
6279 while let Some(ch) = chars.next() {
6280 match ch {
6281 '/' if chars.peek() == Some(&'/') => return net,
6283 '"' => {
6284 while let Some(c) = chars.next() {
6286 match c {
6287 '\\' => {
6288 chars.next();
6289 } '"' => break,
6291 _ => {}
6292 }
6293 }
6294 }
6295 '\'' => {
6296 let saved: Vec<char> = chars.clone().take(5).collect();
6305 let close_idx = if saved.first() == Some(&'\\') {
6306 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
6307 } else {
6308 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
6309 };
6310 if let Some(idx) = close_idx {
6311 for _ in 0..=idx {
6312 chars.next();
6313 }
6314 }
6315 }
6317 '{' | '(' | '[' => net += 1,
6318 '}' | ')' | ']' => net -= 1,
6319 _ => {}
6320 }
6321 }
6322 net
6323}
6324
6325fn auto_indent_rows<H: crate::types::Host>(
6347 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6348 top: usize,
6349 bot: usize,
6350) {
6351 ed.sync_buffer_content_from_textarea();
6352 let shiftwidth = ed.settings().shiftwidth;
6353 let expandtab = ed.settings().expandtab;
6354 let indent_unit: String = if expandtab {
6355 " ".repeat(shiftwidth)
6356 } else {
6357 "\t".to_string()
6358 };
6359
6360 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6361 let bot = bot.min(lines.len().saturating_sub(1));
6362
6363 let mut depth: i32 = 0;
6366 for line in lines.iter().take(top) {
6367 depth += bracket_net(line);
6368 if depth < 0 {
6369 depth = 0;
6370 }
6371 }
6372
6373 for line in lines.iter_mut().take(bot + 1).skip(top) {
6374 let trimmed_owned = line.trim_start().to_owned();
6375 if trimmed_owned.is_empty() {
6377 *line = String::new();
6378 continue;
6380 }
6381
6382 let starts_with_close = trimmed_owned
6384 .chars()
6385 .next()
6386 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
6387 let starts_with_dot = trimmed_owned.starts_with('.')
6397 && !trimmed_owned.starts_with("..")
6398 && !trimmed_owned.starts_with(".;");
6399 let effective_depth = if starts_with_close {
6400 depth.saturating_sub(1)
6401 } else if starts_with_dot {
6402 depth.saturating_add(1)
6403 } else {
6404 depth
6405 } as usize;
6406
6407 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
6409
6410 depth += bracket_net(&trimmed_owned);
6412 if depth < 0 {
6413 depth = 0;
6414 }
6415
6416 *line = new_line;
6417 }
6418
6419 ed.restore(lines, (top, 0));
6421 move_first_non_whitespace(ed);
6422 ed.last_indent_range = Some((top, bot));
6424}
6425
6426fn toggle_case_str(s: &str) -> String {
6427 s.chars()
6428 .map(|c| {
6429 if c.is_lowercase() {
6430 c.to_uppercase().next().unwrap_or(c)
6431 } else if c.is_uppercase() {
6432 c.to_lowercase().next().unwrap_or(c)
6433 } else {
6434 c
6435 }
6436 })
6437 .collect()
6438}
6439
6440fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
6441 if a <= b { (a, b) } else { (b, a) }
6442}
6443
6444fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6449 let (row, col) = ed.cursor();
6450 let line_chars = buf_line_chars(&ed.buffer, row);
6451 let max_col = line_chars.saturating_sub(1);
6452 if col > max_col {
6453 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
6454 ed.push_buffer_cursor_to_textarea();
6455 }
6456}
6457
6458fn expand_linewise_over_closed_folds(
6464 buf: &hjkl_buffer::Buffer,
6465 mut start: usize,
6466 mut end: usize,
6467) -> (usize, usize) {
6468 let folds = buf.folds();
6469 if folds.is_empty() {
6470 return (start, end);
6471 }
6472 loop {
6473 let mut changed = false;
6474 for f in &folds {
6475 if !f.closed {
6476 continue;
6477 }
6478 if f.start_row <= end && f.end_row >= start {
6480 if f.start_row < start {
6481 start = f.start_row;
6482 changed = true;
6483 }
6484 if f.end_row > end {
6485 end = f.end_row;
6486 changed = true;
6487 }
6488 }
6489 }
6490 if !changed {
6491 break;
6492 }
6493 }
6494 (start, end)
6495}
6496
6497fn execute_line_op<H: crate::types::Host>(
6498 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6499 op: Operator,
6500 count: usize,
6501) {
6502 let (row, col) = ed.cursor();
6503 let total = buf_row_count(&ed.buffer);
6504 let last_content_row = if total >= 2
6513 && buf_line(&ed.buffer, total - 1)
6514 .map(|s| s.is_empty())
6515 .unwrap_or(false)
6516 {
6517 total - 2
6518 } else {
6519 total.saturating_sub(1)
6520 };
6521 if count >= 2 && row >= last_content_row {
6522 return;
6523 }
6524 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
6525
6526 let (row, end_row) = expand_linewise_over_closed_folds(&ed.buffer, row, end_row);
6531
6532 match op {
6533 Operator::Yank => {
6534 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6536 if !text.is_empty() {
6537 ed.record_yank_to_host(text.clone());
6538 ed.record_yank(text, true);
6539 }
6540 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
6543 ed.set_mark('[', (row, 0));
6544 ed.set_mark(']', (end_row, last_col));
6545 buf_set_cursor_rc(&mut ed.buffer, row, col);
6546 ed.push_buffer_cursor_to_textarea();
6547 ed.vim.mode = Mode::Normal;
6548 }
6549 Operator::Delete => {
6550 ed.push_undo();
6551 let deleted_through_last = end_row + 1 >= total;
6552 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
6553 let total_after = buf_row_count(&ed.buffer);
6557 let raw_target = if deleted_through_last {
6558 row.saturating_sub(1).min(total_after.saturating_sub(1))
6559 } else {
6560 row.min(total_after.saturating_sub(1))
6561 };
6562 let target_row = if raw_target > 0
6568 && raw_target + 1 == total_after
6569 && buf_line(&ed.buffer, raw_target)
6570 .map(|s| s.is_empty())
6571 .unwrap_or(false)
6572 {
6573 raw_target - 1
6574 } else {
6575 raw_target
6576 };
6577 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6578 ed.push_buffer_cursor_to_textarea();
6579 move_first_non_whitespace(ed);
6580 ed.sticky_col = Some(ed.cursor().1);
6581 ed.vim.mode = Mode::Normal;
6582 let pos = ed.cursor();
6585 ed.set_mark('[', pos);
6586 ed.set_mark(']', pos);
6587 }
6588 Operator::Change => {
6589 change_linewise_rows(ed, row, end_row);
6593 }
6594 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6595 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
6599 move_first_non_whitespace(ed);
6602 }
6603 Operator::Indent | Operator::Outdent => {
6604 ed.push_undo();
6606 if op == Operator::Indent {
6607 indent_rows(ed, row, end_row, 1);
6608 } else {
6609 outdent_rows(ed, row, end_row, 1);
6610 }
6611 ed.sticky_col = Some(ed.cursor().1);
6612 ed.vim.mode = Mode::Normal;
6613 }
6614 Operator::Fold => unreachable!("Fold has no line-op double"),
6616 Operator::Reflow => {
6617 ed.push_undo();
6619 reflow_rows(ed, row, end_row);
6620 move_first_non_whitespace(ed);
6621 ed.sticky_col = Some(ed.cursor().1);
6622 ed.vim.mode = Mode::Normal;
6623 }
6624 Operator::ReflowKeepCursor => {
6625 let saved = ed.cursor();
6628 ed.push_undo();
6629 let (before, after) = reflow_rows_keep_cursor(ed, row, end_row);
6630 let (new_row, new_col) = reflow_keep_cursor(row, saved.0, saved.1, &before, &after);
6631 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6632 ed.push_buffer_cursor_to_textarea();
6633 ed.sticky_col = Some(new_col);
6634 ed.vim.mode = Mode::Normal;
6635 }
6636 Operator::AutoIndent => {
6637 ed.push_undo();
6639 auto_indent_rows(ed, row, end_row);
6640 ed.sticky_col = Some(ed.cursor().1);
6641 ed.vim.mode = Mode::Normal;
6642 }
6643 Operator::Filter => {
6644 }
6646 Operator::Comment => {
6647 }
6652 }
6653}
6654
6655pub(crate) fn apply_visual_operator<H: crate::types::Host>(
6658 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6659 op: Operator,
6660 count: usize,
6661) {
6662 let levels = count.max(1);
6665 match ed.vim.mode {
6666 Mode::VisualLine => {
6667 let cursor_row = buf_cursor_pos(&ed.buffer).row;
6668 let top = cursor_row.min(ed.vim.visual_line_anchor);
6669 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6670 ed.vim.yank_linewise = true;
6671 match op {
6672 Operator::Yank => {
6673 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6674 if !text.is_empty() {
6675 ed.record_yank_to_host(text.clone());
6676 ed.record_yank(text, true);
6677 }
6678 buf_set_cursor_rc(&mut ed.buffer, top, 0);
6679 ed.push_buffer_cursor_to_textarea();
6680 ed.vim.mode = Mode::Normal;
6681 }
6682 Operator::Delete => {
6683 ed.push_undo();
6684 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
6685 ed.vim.mode = Mode::Normal;
6686 }
6687 Operator::Change => {
6688 change_linewise_rows(ed, top, bot);
6691 }
6692 Operator::Uppercase
6693 | Operator::Lowercase
6694 | Operator::ToggleCase
6695 | Operator::Rot13 => {
6696 let bot = buf_cursor_pos(&ed.buffer)
6697 .row
6698 .max(ed.vim.visual_line_anchor);
6699 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
6700 move_first_non_whitespace(ed);
6701 }
6702 Operator::Indent | Operator::Outdent => {
6703 ed.push_undo();
6704 let (cursor_row, _) = ed.cursor();
6705 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6706 if op == Operator::Indent {
6707 indent_rows(ed, top, bot, levels);
6708 } else {
6709 outdent_rows(ed, top, bot, levels);
6710 }
6711 ed.vim.mode = Mode::Normal;
6712 }
6713 Operator::Reflow => {
6714 ed.push_undo();
6715 let (cursor_row, _) = ed.cursor();
6716 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6717 reflow_rows(ed, top, bot);
6718 ed.vim.mode = Mode::Normal;
6719 }
6720 Operator::ReflowKeepCursor => {
6721 let saved = ed.cursor();
6722 ed.push_undo();
6723 let (cursor_row, _) = ed.cursor();
6724 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6725 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6726 let (new_row, new_col) =
6727 reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6728 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6729 ed.push_buffer_cursor_to_textarea();
6730 ed.vim.mode = Mode::Normal;
6731 }
6732 Operator::AutoIndent => {
6733 ed.push_undo();
6734 let (cursor_row, _) = ed.cursor();
6735 let bot = cursor_row.max(ed.vim.visual_line_anchor);
6736 auto_indent_rows(ed, top, bot);
6737 ed.vim.mode = Mode::Normal;
6738 }
6739 Operator::Filter => {}
6741 Operator::Comment => {}
6743 Operator::Fold => unreachable!("Visual zf takes its own path"),
6746 }
6747 }
6748 Mode::Visual => {
6749 ed.vim.yank_linewise = false;
6750 let anchor = ed.vim.visual_anchor;
6751 let cursor = ed.cursor();
6752 let (top, bot) = order(anchor, cursor);
6753 match op {
6754 Operator::Yank => {
6755 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
6756 if !text.is_empty() {
6757 ed.record_yank_to_host(text.clone());
6758 ed.record_yank(text, false);
6759 }
6760 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
6761 ed.push_buffer_cursor_to_textarea();
6762 ed.vim.mode = Mode::Normal;
6763 }
6764 Operator::Delete => {
6765 ed.push_undo();
6766 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6767 ed.vim.mode = Mode::Normal;
6768 }
6769 Operator::Change => {
6770 ed.push_undo();
6771 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
6772 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
6773 }
6774 Operator::Uppercase
6775 | Operator::Lowercase
6776 | Operator::ToggleCase
6777 | Operator::Rot13 => {
6778 let anchor = ed.vim.visual_anchor;
6780 let cursor = ed.cursor();
6781 let (top, bot) = order(anchor, cursor);
6782 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
6783 }
6784 Operator::Indent | Operator::Outdent => {
6785 ed.push_undo();
6786 let anchor = ed.vim.visual_anchor;
6787 let cursor = ed.cursor();
6788 let (top, bot) = order(anchor, cursor);
6789 if op == Operator::Indent {
6790 indent_rows(ed, top.0, bot.0, levels);
6791 } else {
6792 outdent_rows(ed, top.0, bot.0, levels);
6793 }
6794 ed.vim.mode = Mode::Normal;
6795 }
6796 Operator::Reflow => {
6797 ed.push_undo();
6798 let anchor = ed.vim.visual_anchor;
6799 let cursor = ed.cursor();
6800 let (top, bot) = order(anchor, cursor);
6801 reflow_rows(ed, top.0, bot.0);
6802 ed.vim.mode = Mode::Normal;
6803 }
6804 Operator::ReflowKeepCursor => {
6805 let saved = ed.cursor();
6806 ed.push_undo();
6807 let anchor = ed.vim.visual_anchor;
6808 let cursor = ed.cursor();
6809 let (top, bot) = order(anchor, cursor);
6810 let (before, after) = reflow_rows_keep_cursor(ed, top.0, bot.0);
6811 let (new_row, new_col) =
6812 reflow_keep_cursor(top.0, saved.0, saved.1, &before, &after);
6813 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6814 ed.push_buffer_cursor_to_textarea();
6815 ed.vim.mode = Mode::Normal;
6816 }
6817 Operator::AutoIndent => {
6818 ed.push_undo();
6819 let anchor = ed.vim.visual_anchor;
6820 let cursor = ed.cursor();
6821 let (top, bot) = order(anchor, cursor);
6822 auto_indent_rows(ed, top.0, bot.0);
6823 ed.vim.mode = Mode::Normal;
6824 }
6825 Operator::Filter => {}
6827 Operator::Comment => {}
6829 Operator::Fold => unreachable!("Visual zf takes its own path"),
6830 }
6831 }
6832 Mode::VisualBlock => apply_block_operator(ed, op, levels),
6833 _ => {}
6834 }
6835}
6836
6837fn block_bounds<H: crate::types::Host>(
6842 ed: &Editor<hjkl_buffer::Buffer, H>,
6843) -> (usize, usize, usize, usize) {
6844 let (ar, ac) = ed.vim.block_anchor;
6845 let (cr, _) = ed.cursor();
6846 let cc = ed.vim.block_vcol;
6847 let top = ar.min(cr);
6848 let bot = ar.max(cr);
6849 let left = ac.min(cc);
6850 let right = ac.max(cc);
6851 (top, bot, left, right)
6852}
6853
6854pub(crate) fn update_block_vcol<H: crate::types::Host>(
6859 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6860 motion: &Motion,
6861) {
6862 match motion {
6863 Motion::Left
6864 | Motion::Right
6865 | Motion::SpaceFwd
6866 | Motion::BackspaceBack
6867 | Motion::WordFwd
6868 | Motion::BigWordFwd
6869 | Motion::WordBack
6870 | Motion::BigWordBack
6871 | Motion::WordEnd
6872 | Motion::BigWordEnd
6873 | Motion::WordEndBack
6874 | Motion::BigWordEndBack
6875 | Motion::LineStart
6876 | Motion::FirstNonBlank
6877 | Motion::LineEnd
6878 | Motion::Find { .. }
6879 | Motion::FindRepeat { .. }
6880 | Motion::MatchBracket => {
6881 ed.vim.block_vcol = ed.cursor().1;
6882 }
6883 _ => {}
6885 }
6886}
6887
6888fn apply_block_operator<H: crate::types::Host>(
6893 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6894 op: Operator,
6895 count: usize,
6896) {
6897 let (top, bot, left, right) = block_bounds(ed);
6898 let yank = block_yank(ed, top, bot, left, right);
6900
6901 match op {
6902 Operator::Yank => {
6903 if !yank.is_empty() {
6904 ed.record_yank_to_host(yank.clone());
6905 ed.record_yank(yank, false);
6906 }
6907 ed.vim.mode = Mode::Normal;
6908 ed.jump_cursor(top, left);
6909 }
6910 Operator::Delete => {
6911 ed.push_undo();
6912 delete_block_contents(ed, top, bot, left, right);
6913 if !yank.is_empty() {
6914 ed.record_yank_to_host(yank.clone());
6915 ed.record_delete(yank, false);
6916 }
6917 ed.vim.mode = Mode::Normal;
6918 ed.jump_cursor(top, left);
6919 }
6920 Operator::Change => {
6921 ed.push_undo();
6922 delete_block_contents(ed, top, bot, left, right);
6923 if !yank.is_empty() {
6924 ed.record_yank_to_host(yank.clone());
6925 ed.record_delete(yank, false);
6926 }
6927 ed.jump_cursor(top, left);
6928 begin_insert_noundo(
6929 ed,
6930 1,
6931 InsertReason::BlockChange {
6932 top,
6933 bot,
6934 col: left,
6935 },
6936 );
6937 }
6938 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase | Operator::Rot13 => {
6939 ed.push_undo();
6940 transform_block_case(ed, op, top, bot, left, right);
6941 ed.vim.mode = Mode::Normal;
6942 ed.jump_cursor(top, left);
6943 }
6944 Operator::Indent | Operator::Outdent => {
6945 ed.push_undo();
6949 if op == Operator::Indent {
6950 indent_rows(ed, top, bot, count.max(1));
6951 } else {
6952 outdent_rows(ed, top, bot, count.max(1));
6953 }
6954 ed.vim.mode = Mode::Normal;
6955 }
6956 Operator::Fold => unreachable!("Visual zf takes its own path"),
6957 Operator::Reflow => {
6958 ed.push_undo();
6962 reflow_rows(ed, top, bot);
6963 ed.vim.mode = Mode::Normal;
6964 }
6965 Operator::ReflowKeepCursor => {
6966 let saved = ed.cursor();
6968 ed.push_undo();
6969 let (before, after) = reflow_rows_keep_cursor(ed, top, bot);
6970 let (new_row, new_col) = reflow_keep_cursor(top, saved.0, saved.1, &before, &after);
6971 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
6972 ed.push_buffer_cursor_to_textarea();
6973 ed.vim.mode = Mode::Normal;
6974 }
6975 Operator::AutoIndent => {
6976 ed.push_undo();
6979 auto_indent_rows(ed, top, bot);
6980 ed.vim.mode = Mode::Normal;
6981 }
6982 Operator::Filter => {}
6984 Operator::Comment => {}
6986 }
6987}
6988
6989fn transform_block_case<H: crate::types::Host>(
6993 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6994 op: Operator,
6995 top: usize,
6996 bot: usize,
6997 left: usize,
6998 right: usize,
6999) {
7000 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
7001 for r in top..=bot.min(lines.len().saturating_sub(1)) {
7002 let chars: Vec<char> = lines[r].chars().collect();
7003 if left >= chars.len() {
7004 continue;
7005 }
7006 let end = (right + 1).min(chars.len());
7007 let head: String = chars[..left].iter().collect();
7008 let mid: String = chars[left..end].iter().collect();
7009 let tail: String = chars[end..].iter().collect();
7010 let transformed = match op {
7011 Operator::Uppercase => mid.to_uppercase(),
7012 Operator::Lowercase => mid.to_lowercase(),
7013 Operator::ToggleCase => toggle_case_str(&mid),
7014 Operator::Rot13 => rot13_str(&mid),
7015 _ => mid,
7016 };
7017 lines[r] = format!("{head}{transformed}{tail}");
7018 }
7019 let saved_yank = ed.yank().to_string();
7020 let saved_linewise = ed.vim.yank_linewise;
7021 ed.restore(lines, (top, left));
7022 ed.set_yank(saved_yank);
7023 ed.vim.yank_linewise = saved_linewise;
7024}
7025
7026fn block_yank<H: crate::types::Host>(
7027 ed: &Editor<hjkl_buffer::Buffer, H>,
7028 top: usize,
7029 bot: usize,
7030 left: usize,
7031 right: usize,
7032) -> String {
7033 let rope = crate::types::Query::rope(&ed.buffer);
7034 let n = rope.len_lines();
7035 let mut rows: Vec<String> = Vec::new();
7036 for r in top..=bot {
7037 if r >= n {
7038 break;
7039 }
7040 let line = rope_line_to_str(&rope, r);
7041 let chars: Vec<char> = line.chars().collect();
7042 let end = (right + 1).min(chars.len());
7043 if left >= chars.len() {
7044 rows.push(String::new());
7045 } else {
7046 rows.push(chars[left..end].iter().collect());
7047 }
7048 }
7049 rows.join("\n")
7050}
7051
7052fn delete_block_contents<H: crate::types::Host>(
7053 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7054 top: usize,
7055 bot: usize,
7056 left: usize,
7057 right: usize,
7058) {
7059 use hjkl_buffer::{Edit, MotionKind, Position};
7060 ed.sync_buffer_content_from_textarea();
7061 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
7062 if last_row < top {
7063 return;
7064 }
7065 ed.mutate_edit(Edit::DeleteRange {
7066 start: Position::new(top, left),
7067 end: Position::new(last_row, right),
7068 kind: MotionKind::Block,
7069 });
7070 ed.push_buffer_cursor_to_textarea();
7071}
7072
7073pub(crate) fn block_replace<H: crate::types::Host>(
7075 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7076 ch: char,
7077) {
7078 let (top, bot, left, right) = block_bounds(ed);
7079 ed.push_undo();
7080 ed.sync_buffer_content_from_textarea();
7081 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
7082 for r in top..=bot.min(lines.len().saturating_sub(1)) {
7083 let chars: Vec<char> = lines[r].chars().collect();
7084 if left >= chars.len() {
7085 continue;
7086 }
7087 let end = (right + 1).min(chars.len());
7088 let before: String = chars[..left].iter().collect();
7089 let middle: String = std::iter::repeat_n(ch, end - left).collect();
7090 let after: String = chars[end..].iter().collect();
7091 lines[r] = format!("{before}{middle}{after}");
7092 }
7093 reset_textarea_lines(ed, lines);
7094 ed.vim.mode = Mode::Normal;
7095 ed.jump_cursor(top, left);
7096}
7097
7098fn reset_textarea_lines<H: crate::types::Host>(
7102 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7103 lines: Vec<String>,
7104) {
7105 let cursor = ed.cursor();
7106 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
7107 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
7108 ed.mark_content_dirty();
7109}
7110
7111type Pos = (usize, usize);
7117
7118pub(crate) fn text_object_range<H: crate::types::Host>(
7122 ed: &Editor<hjkl_buffer::Buffer, H>,
7123 obj: TextObject,
7124 inner: bool,
7125 count: usize,
7126) -> Option<(Pos, Pos, RangeKind)> {
7127 match obj {
7128 TextObject::Word { big } => {
7129 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
7130 }
7131 TextObject::Quote(q) => {
7132 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
7133 }
7134 TextObject::Bracket(open) => bracket_text_object(ed, open, inner, count),
7135 TextObject::Paragraph => {
7136 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
7137 }
7138 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
7139 TextObject::Sentence => {
7140 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
7141 }
7142 }
7143}
7144
7145fn sentence_boundary<H: crate::types::Host>(
7149 ed: &Editor<hjkl_buffer::Buffer, H>,
7150 forward: bool,
7151) -> Option<(usize, usize)> {
7152 let rope = crate::types::Query::rope(&ed.buffer);
7153 let n_lines = rope.len_lines();
7154 if n_lines == 0 {
7155 return None;
7156 }
7157 let line_lens: Vec<usize> = (0..n_lines)
7159 .map(|r| rope_line_to_str(&rope, r).chars().count())
7160 .collect();
7161 let pos_to_idx = |pos: (usize, usize)| -> usize {
7162 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
7163 idx + pos.1
7164 };
7165 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
7166 for (r, &len) in line_lens.iter().enumerate() {
7167 if idx <= len {
7168 return (r, idx);
7169 }
7170 idx -= len + 1;
7171 }
7172 let last = n_lines.saturating_sub(1);
7173 (last, line_lens[last])
7174 };
7175 let mut chars: Vec<char> = rope.chars().collect();
7178 if chars.last() == Some(&'\n') {
7180 chars.pop();
7181 }
7182 if chars.is_empty() {
7183 return None;
7184 }
7185 let total = chars.len();
7186 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
7187 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
7188
7189 if forward {
7190 let mut i = cursor_idx + 1;
7193 while i < total {
7194 if is_terminator(chars[i]) {
7195 while i + 1 < total && is_terminator(chars[i + 1]) {
7196 i += 1;
7197 }
7198 if i + 1 >= total {
7199 return None;
7200 }
7201 if chars[i + 1].is_whitespace() {
7202 let mut j = i + 1;
7203 while j < total && chars[j].is_whitespace() {
7204 j += 1;
7205 }
7206 if j >= total {
7207 return None;
7208 }
7209 return Some(idx_to_pos(j));
7210 }
7211 }
7212 i += 1;
7213 }
7214 None
7215 } else {
7216 let find_start = |from: usize| -> Option<usize> {
7220 let mut start = from;
7221 while start > 0 {
7222 let prev = chars[start - 1];
7223 if prev.is_whitespace() {
7224 let mut k = start - 1;
7225 while k > 0 && chars[k - 1].is_whitespace() {
7226 k -= 1;
7227 }
7228 if k > 0 && is_terminator(chars[k - 1]) {
7229 break;
7230 }
7231 }
7232 start -= 1;
7233 }
7234 while start < total && chars[start].is_whitespace() {
7235 start += 1;
7236 }
7237 (start < total).then_some(start)
7238 };
7239 let current_start = find_start(cursor_idx)?;
7240 if current_start < cursor_idx {
7241 return Some(idx_to_pos(current_start));
7242 }
7243 let mut k = current_start;
7246 while k > 0 && chars[k - 1].is_whitespace() {
7247 k -= 1;
7248 }
7249 if k == 0 {
7250 return None;
7251 }
7252 let prev_start = find_start(k - 1)?;
7253 Some(idx_to_pos(prev_start))
7254 }
7255}
7256
7257fn sentence_text_object<H: crate::types::Host>(
7263 ed: &Editor<hjkl_buffer::Buffer, H>,
7264 inner: bool,
7265) -> Option<((usize, usize), (usize, usize))> {
7266 let rope = crate::types::Query::rope(&ed.buffer);
7267 let n_lines = rope.len_lines();
7268 if n_lines == 0 {
7269 return None;
7270 }
7271 let line_lens: Vec<usize> = (0..n_lines)
7274 .map(|r| rope_line_to_str(&rope, r).chars().count())
7275 .collect();
7276 let pos_to_idx = |pos: (usize, usize)| -> usize {
7277 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
7278 idx + pos.1
7279 };
7280 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
7281 for (r, &len) in line_lens.iter().enumerate() {
7282 if idx <= len {
7283 return (r, idx);
7284 }
7285 idx -= len + 1;
7286 }
7287 let last = n_lines.saturating_sub(1);
7288 (last, line_lens[last])
7289 };
7290 let mut chars: Vec<char> = rope.chars().collect();
7291 if chars.last() == Some(&'\n') {
7292 chars.pop();
7293 }
7294 if chars.is_empty() {
7295 return None;
7296 }
7297
7298 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
7299 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
7300
7301 let mut start = cursor_idx;
7305 while start > 0 {
7306 let prev = chars[start - 1];
7307 if prev.is_whitespace() {
7308 let mut k = start - 1;
7312 while k > 0 && chars[k - 1].is_whitespace() {
7313 k -= 1;
7314 }
7315 if k > 0 && is_terminator(chars[k - 1]) {
7316 break;
7317 }
7318 }
7319 start -= 1;
7320 }
7321 while start < chars.len() && chars[start].is_whitespace() {
7324 start += 1;
7325 }
7326 if start >= chars.len() {
7327 return None;
7328 }
7329
7330 let mut end = start;
7333 while end < chars.len() {
7334 if is_terminator(chars[end]) {
7335 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
7337 end += 1;
7338 }
7339 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
7342 break;
7343 }
7344 }
7345 end += 1;
7346 }
7347 let end_idx = (end + 1).min(chars.len());
7349
7350 let final_end = if inner {
7351 end_idx
7352 } else {
7353 let mut e = end_idx;
7357 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
7358 e += 1;
7359 }
7360 e
7361 };
7362
7363 Some((idx_to_pos(start), idx_to_pos(final_end)))
7364}
7365
7366fn tag_text_object<H: crate::types::Host>(
7370 ed: &Editor<hjkl_buffer::Buffer, H>,
7371 inner: bool,
7372) -> Option<((usize, usize), (usize, usize))> {
7373 let rope = crate::types::Query::rope(&ed.buffer);
7374 let n_lines = rope.len_lines();
7375 if n_lines == 0 {
7376 return None;
7377 }
7378 let line_lens: Vec<usize> = (0..n_lines)
7382 .map(|r| rope_line_to_str(&rope, r).chars().count())
7383 .collect();
7384 let pos_to_idx = |pos: (usize, usize)| -> usize {
7385 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
7386 idx + pos.1
7387 };
7388 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
7389 for (r, &len) in line_lens.iter().enumerate() {
7390 if idx <= len {
7391 return (r, idx);
7392 }
7393 idx -= len + 1;
7394 }
7395 let last = n_lines.saturating_sub(1);
7396 (last, line_lens[last])
7397 };
7398 let mut chars: Vec<char> = rope.chars().collect();
7399 if chars.last() == Some(&'\n') {
7400 chars.pop();
7401 }
7402 let cursor_idx = pos_to_idx(ed.cursor());
7403
7404 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
7412 let mut next_after: Option<(usize, usize, usize, usize)> = None;
7413 let mut i = 0;
7414 while i < chars.len() {
7415 if chars[i] != '<' {
7416 i += 1;
7417 continue;
7418 }
7419 let mut j = i + 1;
7420 while j < chars.len() && chars[j] != '>' {
7421 j += 1;
7422 }
7423 if j >= chars.len() {
7424 break;
7425 }
7426 let inside: String = chars[i + 1..j].iter().collect();
7427 let close_end = j + 1;
7428 let trimmed = inside.trim();
7429 if trimmed.starts_with('!') || trimmed.starts_with('?') {
7430 i = close_end;
7431 continue;
7432 }
7433 if let Some(rest) = trimmed.strip_prefix('/') {
7434 let name = rest.split_whitespace().next().unwrap_or("").to_string();
7435 if !name.is_empty()
7436 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
7437 {
7438 let (open_start, content_start, _) = stack[stack_idx].clone();
7439 stack.truncate(stack_idx);
7440 let content_end = i;
7441 let candidate = (open_start, content_start, content_end, close_end);
7442 if cursor_idx >= open_start && cursor_idx < close_end {
7449 innermost = match innermost {
7450 Some((os, _, _, ce)) if os <= open_start && close_end <= ce => {
7451 Some(candidate)
7452 }
7453 None => Some(candidate),
7454 existing => existing,
7455 };
7456 } else if open_start >= cursor_idx && next_after.is_none() {
7457 next_after = Some(candidate);
7458 }
7459 }
7460 } else if !trimmed.ends_with('/') {
7461 let name: String = trimmed
7462 .split(|c: char| c.is_whitespace() || c == '/')
7463 .next()
7464 .unwrap_or("")
7465 .to_string();
7466 if !name.is_empty() {
7467 stack.push((i, close_end, name));
7468 }
7469 }
7470 i = close_end;
7471 }
7472
7473 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
7474 if inner {
7475 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
7476 } else {
7477 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
7478 }
7479}
7480
7481fn is_wordchar(c: char) -> bool {
7482 c.is_alphanumeric() || c == '_'
7483}
7484
7485pub(crate) use hjkl_buffer::is_keyword_char;
7489
7490#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7496pub(crate) enum AbbrevKind {
7497 Full,
7499 End,
7501 NonKw,
7503}
7504
7505pub(crate) fn abbrev_kind(lhs: &str, iskeyword: &str) -> AbbrevKind {
7506 let chars: Vec<char> = lhs.chars().collect();
7507 if chars.is_empty() {
7508 return AbbrevKind::NonKw;
7509 }
7510 let last = *chars.last().unwrap();
7511 let last_is_kw = is_keyword_char(last, iskeyword);
7512 if !last_is_kw {
7513 return AbbrevKind::NonKw;
7514 }
7515 let all_kw = chars.iter().all(|&c| is_keyword_char(c, iskeyword));
7517 if all_kw {
7518 AbbrevKind::Full
7519 } else {
7520 AbbrevKind::End
7521 }
7522}
7523
7524pub(crate) fn try_abbrev_expand(
7542 abbrevs: &[Abbrev],
7543 line_before: &str,
7544 mincol: usize,
7545 trigger: AbbrevTrigger,
7546 iskeyword: &str,
7547) -> Option<(usize, String)> {
7548 let chars: Vec<char> = line_before.chars().collect();
7549 let cursor_col = chars.len(); for abbrev in abbrevs {
7552 if !abbrev.insert {
7553 continue;
7554 }
7555 let lhs_chars: Vec<char> = abbrev.lhs.chars().collect();
7556 if lhs_chars.is_empty() {
7557 continue;
7558 }
7559 let lhs_len = lhs_chars.len();
7560
7561 let kind = abbrev_kind(&abbrev.lhs, iskeyword);
7563
7564 match kind {
7566 AbbrevKind::Full | AbbrevKind::End => {
7567 let trigger_char_is_kw = match trigger {
7570 AbbrevTrigger::NonKeyword(c) => is_keyword_char(c, iskeyword),
7571 AbbrevTrigger::CtrlBracket | AbbrevTrigger::Cr | AbbrevTrigger::Esc => false,
7572 };
7573 if trigger_char_is_kw {
7574 continue;
7576 }
7577 }
7578 AbbrevKind::NonKw => {
7579 match trigger {
7581 AbbrevTrigger::Cr | AbbrevTrigger::Esc | AbbrevTrigger::CtrlBracket => {}
7582 AbbrevTrigger::NonKeyword(_) => continue,
7583 }
7584 }
7585 }
7586
7587 if cursor_col < lhs_len {
7589 continue;
7590 }
7591 let lhs_start_col = cursor_col - lhs_len;
7592
7593 if lhs_start_col < mincol {
7595 continue;
7596 }
7597
7598 let text_slice: &[char] = &chars[lhs_start_col..cursor_col];
7600 if text_slice != lhs_chars.as_slice() {
7601 continue;
7602 }
7603
7604 if lhs_start_col > 0 {
7606 let ch_before = chars[lhs_start_col - 1];
7607 match kind {
7608 AbbrevKind::Full => {
7609 if is_keyword_char(ch_before, iskeyword) {
7620 continue; }
7622 if lhs_len == 1 && ch_before != ' ' && ch_before != '\t' {
7623 continue;
7625 }
7626 }
7627 AbbrevKind::End => {
7628 }
7632 AbbrevKind::NonKw => {
7633 if ch_before != ' ' && ch_before != '\t' {
7636 continue;
7637 }
7638 }
7639 }
7640 }
7641 return Some((lhs_len, abbrev.rhs.clone()));
7645 }
7646
7647 None
7648}
7649
7650pub(crate) fn check_and_apply_abbrev<H: crate::types::Host>(
7659 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7660 trigger: AbbrevTrigger,
7661) -> bool {
7662 use hjkl_buffer::{Edit, Position};
7663
7664 let cursor = buf_cursor_pos(&ed.buffer);
7666 let row = cursor.row;
7667 let col = cursor.col;
7668 let line_before: String = {
7669 let line = buf_line(&ed.buffer, row).unwrap_or_default();
7670 line.chars().take(col).collect()
7671 };
7672 let (mincol, on_start_row) = if let Some(ref s) = ed.vim.insert_session {
7673 if row == s.start_row {
7674 (s.start_col, true)
7675 } else {
7676 (0, false)
7677 }
7678 } else {
7679 (0, false)
7680 };
7681 if on_start_row && col <= mincol {
7683 return false;
7684 }
7685
7686 let iskeyword = ed.settings.iskeyword.clone();
7687 let abbrevs = ed.vim.abbrevs.clone();
7688
7689 let Some((lhs_len, rhs)) =
7690 try_abbrev_expand(&abbrevs, &line_before, mincol, trigger, &iskeyword)
7691 else {
7692 return false;
7693 };
7694
7695 let lhs_start = col.saturating_sub(lhs_len);
7697 if lhs_len > 0 {
7698 ed.mutate_edit(Edit::DeleteRange {
7699 start: Position::new(row, lhs_start),
7700 end: Position::new(row, col),
7701 kind: hjkl_buffer::MotionKind::Char,
7702 });
7703 }
7704
7705 let insert_pos = Position::new(row, lhs_start);
7707 if !rhs.is_empty() {
7708 ed.mutate_edit(Edit::InsertStr {
7709 at: insert_pos,
7710 text: rhs.clone(),
7711 });
7712 }
7713
7714 let new_col = lhs_start + rhs.chars().count();
7716 buf_set_cursor_rc(&mut ed.buffer, row, new_col);
7717 ed.push_buffer_cursor_to_textarea();
7718
7719 true
7720}
7721
7722fn word_text_object<H: crate::types::Host>(
7723 ed: &Editor<hjkl_buffer::Buffer, H>,
7724 inner: bool,
7725 big: bool,
7726) -> Option<((usize, usize), (usize, usize))> {
7727 let (row, col) = ed.cursor();
7728 let line = buf_line(&ed.buffer, row)?;
7729 let chars: Vec<char> = line.chars().collect();
7730 if chars.is_empty() {
7731 return None;
7732 }
7733 let at = col.min(chars.len().saturating_sub(1));
7734 let classify = |c: char| -> u8 {
7735 if c.is_whitespace() {
7736 0
7737 } else if big || is_wordchar(c) {
7738 1
7739 } else {
7740 2
7741 }
7742 };
7743 let cls = classify(chars[at]);
7744 let mut start = at;
7745 while start > 0 && classify(chars[start - 1]) == cls {
7746 start -= 1;
7747 }
7748 let mut end = at;
7749 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
7750 end += 1;
7751 }
7752 let char_byte = |i: usize| {
7754 if i >= chars.len() {
7755 line.len()
7756 } else {
7757 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
7758 }
7759 };
7760 let mut start_col = char_byte(start);
7761 let mut end_col = char_byte(end + 1);
7763 if !inner {
7764 let mut t = end + 1;
7766 let mut included_trailing = false;
7767 while t < chars.len() && chars[t].is_whitespace() {
7768 included_trailing = true;
7769 t += 1;
7770 }
7771 if included_trailing {
7772 end_col = char_byte(t);
7773 } else {
7774 let mut s = start;
7775 while s > 0 && chars[s - 1].is_whitespace() {
7776 s -= 1;
7777 }
7778 start_col = char_byte(s);
7779 }
7780 }
7781 Some(((row, start_col), (row, end_col)))
7782}
7783
7784fn quote_text_object<H: crate::types::Host>(
7785 ed: &Editor<hjkl_buffer::Buffer, H>,
7786 q: char,
7787 inner: bool,
7788) -> Option<((usize, usize), (usize, usize))> {
7789 let (row, col) = ed.cursor();
7790 let line = buf_line(&ed.buffer, row)?;
7791 let bytes = line.as_bytes();
7792 let q_byte = q as u8;
7793 let mut positions: Vec<usize> = Vec::new();
7795 for (i, &b) in bytes.iter().enumerate() {
7796 if b == q_byte {
7797 positions.push(i);
7798 }
7799 }
7800 if positions.len() < 2 {
7801 return None;
7802 }
7803 let mut open_idx: Option<usize> = None;
7804 let mut close_idx: Option<usize> = None;
7805 for pair in positions.chunks(2) {
7806 if pair.len() < 2 {
7807 break;
7808 }
7809 if col >= pair[0] && col <= pair[1] {
7810 open_idx = Some(pair[0]);
7811 close_idx = Some(pair[1]);
7812 break;
7813 }
7814 if col < pair[0] {
7815 open_idx = Some(pair[0]);
7816 close_idx = Some(pair[1]);
7817 break;
7818 }
7819 }
7820 let open = open_idx?;
7821 let close = close_idx?;
7822 if inner {
7824 if close <= open + 1 {
7825 return None;
7826 }
7827 Some(((row, open + 1), (row, close)))
7828 } else {
7829 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
7836 let mut end = after_close;
7838 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
7839 end += 1;
7840 }
7841 Some(((row, open), (row, end)))
7842 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
7843 let mut start = open;
7845 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
7846 start -= 1;
7847 }
7848 Some(((row, start), (row, close + 1)))
7849 } else {
7850 Some(((row, open), (row, close + 1)))
7851 }
7852 }
7853}
7854
7855fn bracket_text_object<H: crate::types::Host>(
7856 ed: &Editor<hjkl_buffer::Buffer, H>,
7857 open: char,
7858 inner: bool,
7859 count: usize,
7860) -> Option<(Pos, Pos, RangeKind)> {
7861 let close = match open {
7862 '(' => ')',
7863 '[' => ']',
7864 '{' => '}',
7865 '<' => '>',
7866 _ => return None,
7867 };
7868 let (row, col) = ed.cursor();
7869 let lines = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
7870 let lines = lines.as_slice();
7871 let cursor_char = lines.get(row).and_then(|l| l.chars().nth(col));
7877 let (open_pos, close_pos) = if cursor_char == Some(close) {
7878 let open_pos = if col > 0 {
7879 find_open_bracket(lines, row, col - 1, open, close)
7880 } else if row > 0 {
7881 let pr = row - 1;
7882 let pc = lines[pr].chars().count().saturating_sub(1);
7883 find_open_bracket(lines, pr, pc, open, close)
7884 } else {
7885 None
7886 }?;
7887 (open_pos, (row, col))
7888 } else {
7889 let open_pos = find_open_bracket(lines, row, col, open, close)
7894 .or_else(|| find_next_open(lines, row, col, open))?;
7895 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
7896 (open_pos, close_pos)
7897 };
7898 let (open_pos, close_pos) = {
7902 let (mut op, mut cp) = (open_pos, close_pos);
7903 for _ in 1..count.max(1) {
7904 let outer = if op.1 > 0 {
7905 find_open_bracket(lines, op.0, op.1 - 1, open, close)
7906 } else if op.0 > 0 {
7907 let pr = op.0 - 1;
7908 let pc = lines[pr].chars().count().saturating_sub(1);
7909 find_open_bracket(lines, pr, pc, open, close)
7910 } else {
7911 None
7912 };
7913 let Some(oo) = outer else { break };
7914 let Some(oc) = find_close_bracket(lines, oo.0, oo.1 + 1, open, close) else {
7915 break;
7916 };
7917 op = oo;
7918 cp = oc;
7919 }
7920 (op, cp)
7921 };
7922 if inner {
7924 let open_line_len = lines[open_pos.0].chars().count();
7935 let inner_start = if open_pos.1 + 1 >= open_line_len && open_pos.0 + 1 < lines.len() {
7936 (open_pos.0 + 1, 0)
7937 } else {
7938 advance_pos(lines, open_pos)
7939 };
7940 if inner_start.0 > close_pos.0
7943 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
7944 {
7945 return Some((inner_start, inner_start, RangeKind::Exclusive));
7946 }
7947 if close_pos.0 > open_pos.0 {
7954 let mut saw_ws = false;
7955 let mut saw_other = false;
7956 for r in inner_start.0..=close_pos.0 {
7957 let line: Vec<char> = lines
7958 .get(r)
7959 .map(|l| l.chars().collect())
7960 .unwrap_or_default();
7961 let from = if r == inner_start.0 { inner_start.1 } else { 0 };
7962 let to = if r == close_pos.0 {
7963 close_pos.1
7964 } else {
7965 line.len()
7966 };
7967 for &c in line
7968 .iter()
7969 .take(to.min(line.len()))
7970 .skip(from.min(line.len()))
7971 {
7972 if c == ' ' || c == '\t' {
7973 saw_ws = true;
7974 } else {
7975 saw_other = true;
7976 }
7977 }
7978 }
7979 if saw_ws && !saw_other {
7980 return Some((inner_start, inner_start, RangeKind::Exclusive));
7981 }
7982 }
7983 Some((inner_start, close_pos, RangeKind::Exclusive))
7984 } else {
7985 Some((
7986 open_pos,
7987 advance_pos(lines, close_pos),
7988 RangeKind::Exclusive,
7989 ))
7990 }
7991}
7992
7993fn find_open_bracket(
7994 lines: &[String],
7995 row: usize,
7996 col: usize,
7997 open: char,
7998 close: char,
7999) -> Option<(usize, usize)> {
8000 let mut depth: i32 = 0;
8001 let mut r = row;
8002 let mut c = col as isize;
8003 loop {
8004 let cur = &lines[r];
8005 let chars: Vec<char> = cur.chars().collect();
8006 if (c as usize) >= chars.len() {
8010 c = chars.len() as isize - 1;
8011 }
8012 while c >= 0 {
8013 let ch = chars[c as usize];
8014 if ch == close {
8015 depth += 1;
8016 } else if ch == open {
8017 if depth == 0 {
8018 return Some((r, c as usize));
8019 }
8020 depth -= 1;
8021 }
8022 c -= 1;
8023 }
8024 if r == 0 {
8025 return None;
8026 }
8027 r -= 1;
8028 c = lines[r].chars().count() as isize - 1;
8029 }
8030}
8031
8032fn find_close_bracket(
8033 lines: &[String],
8034 row: usize,
8035 start_col: usize,
8036 open: char,
8037 close: char,
8038) -> Option<(usize, usize)> {
8039 let mut depth: i32 = 0;
8040 let mut r = row;
8041 let mut c = start_col;
8042 loop {
8043 let cur = &lines[r];
8044 let chars: Vec<char> = cur.chars().collect();
8045 while c < chars.len() {
8046 let ch = chars[c];
8047 if ch == open {
8048 depth += 1;
8049 } else if ch == close {
8050 if depth == 0 {
8051 return Some((r, c));
8052 }
8053 depth -= 1;
8054 }
8055 c += 1;
8056 }
8057 if r + 1 >= lines.len() {
8058 return None;
8059 }
8060 r += 1;
8061 c = 0;
8062 }
8063}
8064
8065fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
8069 let mut r = row;
8070 let mut c = col;
8071 while r < lines.len() {
8072 let chars: Vec<char> = lines[r].chars().collect();
8073 while c < chars.len() {
8074 if chars[c] == open {
8075 return Some((r, c));
8076 }
8077 c += 1;
8078 }
8079 r += 1;
8080 c = 0;
8081 }
8082 None
8083}
8084
8085fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
8086 let (r, c) = pos;
8087 let line_len = lines[r].chars().count();
8088 if c < line_len {
8089 (r, c + 1)
8090 } else if r + 1 < lines.len() {
8091 (r + 1, 0)
8092 } else {
8093 pos
8094 }
8095}
8096
8097fn paragraph_text_object<H: crate::types::Host>(
8098 ed: &Editor<hjkl_buffer::Buffer, H>,
8099 inner: bool,
8100) -> Option<((usize, usize), (usize, usize))> {
8101 let (row, _) = ed.cursor();
8102 let rope = crate::types::Query::rope(&ed.buffer);
8103 let n_lines = rope.len_lines();
8104 if n_lines == 0 {
8105 return None;
8106 }
8107 let is_blank = |r: usize| -> bool {
8109 if r >= n_lines {
8110 return true;
8111 }
8112 rope_line_to_str(&rope, r).trim().is_empty()
8113 };
8114 if is_blank(row) {
8115 return None;
8116 }
8117 let mut top = row;
8118 while top > 0 && !is_blank(top - 1) {
8119 top -= 1;
8120 }
8121 let mut bot = row;
8122 while bot + 1 < n_lines && !is_blank(bot + 1) {
8123 bot += 1;
8124 }
8125 if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
8127 bot += 1;
8128 }
8129 let end_col = rope_line_to_str(&rope, bot).chars().count();
8130 Some(((top, 0), (bot, end_col)))
8131}
8132
8133fn read_vim_range<H: crate::types::Host>(
8139 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8140 start: (usize, usize),
8141 end: (usize, usize),
8142 kind: RangeKind,
8143) -> String {
8144 let (top, bot) = order(start, end);
8145 ed.sync_buffer_content_from_textarea();
8146 let rope = crate::types::Query::rope(&ed.buffer);
8147 let n_lines = rope.len_lines();
8148 match kind {
8149 RangeKind::Linewise => {
8150 let lo = top.0;
8151 let hi = bot.0.min(n_lines.saturating_sub(1));
8152 let mut text = rope_row_range_str(&rope, lo, hi);
8153 text.push('\n');
8154 text
8155 }
8156 RangeKind::Inclusive | RangeKind::Exclusive => {
8157 let inclusive = matches!(kind, RangeKind::Inclusive);
8158 let mut out = String::new();
8160 for row in top.0..=bot.0 {
8161 if row >= n_lines {
8162 break;
8163 }
8164 let line = rope_line_to_str(&rope, row);
8165 let lo = if row == top.0 { top.1 } else { 0 };
8166 let hi_unclamped = if row == bot.0 {
8167 if inclusive { bot.1 + 1 } else { bot.1 }
8168 } else {
8169 line.chars().count() + 1
8170 };
8171 let row_chars: Vec<char> = line.chars().collect();
8172 let hi = hi_unclamped.min(row_chars.len());
8173 if lo < hi {
8174 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
8175 }
8176 if row < bot.0 {
8177 out.push('\n');
8178 }
8179 }
8180 out
8181 }
8182 }
8183}
8184
8185fn cut_vim_range<H: crate::types::Host>(
8194 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8195 start: (usize, usize),
8196 end: (usize, usize),
8197 kind: RangeKind,
8198) -> String {
8199 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
8200 let (top, bot) = order(start, end);
8201 ed.sync_buffer_content_from_textarea();
8202 let (buf_start, buf_end, buf_kind) = match kind {
8203 RangeKind::Linewise => (
8204 Position::new(top.0, 0),
8205 Position::new(bot.0, 0),
8206 BufKind::Line,
8207 ),
8208 RangeKind::Inclusive => {
8209 let line_chars = buf_line_chars(&ed.buffer, bot.0);
8210 let next = if bot.1 < line_chars {
8214 Position::new(bot.0, bot.1 + 1)
8215 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
8216 Position::new(bot.0 + 1, 0)
8217 } else {
8218 Position::new(bot.0, line_chars)
8219 };
8220 (Position::new(top.0, top.1), next, BufKind::Char)
8221 }
8222 RangeKind::Exclusive => (
8223 Position::new(top.0, top.1),
8224 Position::new(bot.0, bot.1),
8225 BufKind::Char,
8226 ),
8227 };
8228 let inverse = ed.mutate_edit(Edit::DeleteRange {
8229 start: buf_start,
8230 end: buf_end,
8231 kind: buf_kind,
8232 });
8233 let text = match inverse {
8234 Edit::InsertStr { text, .. } => text,
8235 _ => String::new(),
8236 };
8237 if !text.is_empty() {
8238 ed.record_yank_to_host(text.clone());
8239 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
8240 }
8241 ed.push_buffer_cursor_to_textarea();
8242 text
8243}
8244
8245fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8251 use hjkl_buffer::{Edit, MotionKind, Position};
8252 ed.sync_buffer_content_from_textarea();
8253 let cursor = buf_cursor_pos(&ed.buffer);
8254 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8255 if cursor.col >= line_chars {
8256 return;
8257 }
8258 let inverse = ed.mutate_edit(Edit::DeleteRange {
8259 start: cursor,
8260 end: Position::new(cursor.row, line_chars),
8261 kind: MotionKind::Char,
8262 });
8263 if let Edit::InsertStr { text, .. } = inverse
8264 && !text.is_empty()
8265 {
8266 ed.record_yank_to_host(text.clone());
8267 ed.vim.yank_linewise = false;
8268 ed.set_yank(text);
8269 }
8270 buf_set_cursor_pos(&mut ed.buffer, cursor);
8271 ed.push_buffer_cursor_to_textarea();
8272}
8273
8274fn do_char_delete<H: crate::types::Host>(
8275 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8276 forward: bool,
8277 count: usize,
8278) {
8279 use hjkl_buffer::{Edit, MotionKind, Position};
8280 ed.push_undo();
8281 ed.sync_buffer_content_from_textarea();
8282 let mut deleted = String::new();
8285 for _ in 0..count {
8286 let cursor = buf_cursor_pos(&ed.buffer);
8287 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8288 if forward {
8289 if cursor.col >= line_chars {
8292 continue;
8293 }
8294 let inverse = ed.mutate_edit(Edit::DeleteRange {
8295 start: cursor,
8296 end: Position::new(cursor.row, cursor.col + 1),
8297 kind: MotionKind::Char,
8298 });
8299 if let Edit::InsertStr { text, .. } = inverse {
8300 deleted.push_str(&text);
8301 }
8302 } else {
8303 if cursor.col == 0 {
8305 continue;
8306 }
8307 let inverse = ed.mutate_edit(Edit::DeleteRange {
8308 start: Position::new(cursor.row, cursor.col - 1),
8309 end: cursor,
8310 kind: MotionKind::Char,
8311 });
8312 if let Edit::InsertStr { text, .. } = inverse {
8313 deleted = text + &deleted;
8316 }
8317 }
8318 }
8319 if !deleted.is_empty() {
8320 ed.record_yank_to_host(deleted.clone());
8321 ed.record_delete(deleted, false);
8322 }
8323 ed.push_buffer_cursor_to_textarea();
8324}
8325
8326pub(crate) fn adjust_number<H: crate::types::Host>(
8331 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8332 delta: i64,
8333) -> bool {
8334 use hjkl_buffer::{Edit, MotionKind, Position};
8335 ed.sync_buffer_content_from_textarea();
8336 let cursor = buf_cursor_pos(&ed.buffer);
8337 let row = cursor.row;
8338 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
8339 Some(l) => l.chars().collect(),
8340 None => return false,
8341 };
8342 let len = chars.len();
8343
8344 let is_hex_prefix = |i: usize| {
8347 chars[i] == '0'
8348 && i + 1 < len
8349 && matches!(chars[i + 1], 'x' | 'X')
8350 && chars.get(i + 2).is_some_and(|c| c.is_ascii_hexdigit())
8351 };
8352 let mut i = cursor.col;
8353 let mut hex = false;
8354 loop {
8355 if i >= len {
8356 return false;
8357 }
8358 if is_hex_prefix(i) {
8359 hex = true;
8360 break;
8361 }
8362 if chars[i].is_ascii_digit() {
8363 break;
8364 }
8365 i += 1;
8366 }
8367
8368 let (span_start, span_end, new_s) = if hex {
8369 let digits_start = i + 2;
8371 let mut digits_end = digits_start;
8372 while digits_end < len && chars[digits_end].is_ascii_hexdigit() {
8373 digits_end += 1;
8374 }
8375 let hexs: String = chars[digits_start..digits_end].iter().collect();
8376 let Ok(n) = u64::from_str_radix(&hexs, 16) else {
8377 return false;
8378 };
8379 let new_val = (n as i128 + delta as i128).max(0) as u64;
8380 let width = digits_end - digits_start;
8381 let prefix: String = chars[i..digits_start].iter().collect();
8382 (i, digits_end, format!("{prefix}{new_val:0width$x}"))
8383 } else {
8384 let digit_start = i;
8386 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
8387 digit_start - 1
8388 } else {
8389 digit_start
8390 };
8391 let mut span_end = digit_start;
8392 while span_end < len && chars[span_end].is_ascii_digit() {
8393 span_end += 1;
8394 }
8395 let s: String = chars[span_start..span_end].iter().collect();
8396 let Ok(n) = s.parse::<i64>() else {
8397 return false;
8398 };
8399 (span_start, span_end, n.saturating_add(delta).to_string())
8400 };
8401
8402 ed.push_undo();
8403 let span_start_pos = Position::new(row, span_start);
8404 let span_end_pos = Position::new(row, span_end);
8405 ed.mutate_edit(Edit::DeleteRange {
8406 start: span_start_pos,
8407 end: span_end_pos,
8408 kind: MotionKind::Char,
8409 });
8410 ed.mutate_edit(Edit::InsertStr {
8411 at: span_start_pos,
8412 text: new_s.clone(),
8413 });
8414 let new_len = new_s.chars().count();
8415 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
8416 ed.push_buffer_cursor_to_textarea();
8417 true
8418}
8419
8420pub(crate) fn replace_char<H: crate::types::Host>(
8421 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8422 ch: char,
8423 count: usize,
8424) {
8425 use hjkl_buffer::{Edit, MotionKind, Position};
8426 ed.push_undo();
8427 ed.sync_buffer_content_from_textarea();
8428 for _ in 0..count {
8429 let cursor = buf_cursor_pos(&ed.buffer);
8430 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8431 if cursor.col >= line_chars {
8432 break;
8433 }
8434 ed.mutate_edit(Edit::DeleteRange {
8435 start: cursor,
8436 end: Position::new(cursor.row, cursor.col + 1),
8437 kind: MotionKind::Char,
8438 });
8439 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
8440 }
8441 crate::motions::move_left(&mut ed.buffer, 1);
8443 ed.push_buffer_cursor_to_textarea();
8444}
8445
8446fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8447 use hjkl_buffer::{Edit, MotionKind, Position};
8448 ed.sync_buffer_content_from_textarea();
8449 let cursor = buf_cursor_pos(&ed.buffer);
8450 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
8451 return;
8452 };
8453 let toggled = if c.is_uppercase() {
8454 c.to_lowercase().next().unwrap_or(c)
8455 } else {
8456 c.to_uppercase().next().unwrap_or(c)
8457 };
8458 ed.mutate_edit(Edit::DeleteRange {
8459 start: cursor,
8460 end: Position::new(cursor.row, cursor.col + 1),
8461 kind: MotionKind::Char,
8462 });
8463 ed.mutate_edit(Edit::InsertChar {
8464 at: cursor,
8465 ch: toggled,
8466 });
8467}
8468
8469fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8470 use hjkl_buffer::{Edit, Position};
8471 ed.sync_buffer_content_from_textarea();
8472 let row = buf_cursor_pos(&ed.buffer).row;
8473 if row + 1 >= buf_row_count(&ed.buffer) {
8474 return;
8475 }
8476 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8477 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
8478 let next_trimmed = next_raw.trim_start();
8479 let cur_chars = cur_line.chars().count();
8480 let next_chars = next_raw.chars().count();
8481 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
8484 " "
8485 } else {
8486 ""
8487 };
8488 let joined = format!("{cur_line}{separator}{next_trimmed}");
8489 ed.mutate_edit(Edit::Replace {
8490 start: Position::new(row, 0),
8491 end: Position::new(row + 1, next_chars),
8492 with: joined,
8493 });
8494 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
8498 ed.push_buffer_cursor_to_textarea();
8499}
8500
8501fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8504 use hjkl_buffer::Edit;
8505 ed.sync_buffer_content_from_textarea();
8506 let row = buf_cursor_pos(&ed.buffer).row;
8507 if row + 1 >= buf_row_count(&ed.buffer) {
8508 return;
8509 }
8510 let join_col = buf_line_chars(&ed.buffer, row);
8511 ed.mutate_edit(Edit::JoinLines {
8512 row,
8513 count: 1,
8514 with_space: false,
8515 });
8516 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
8518 ed.push_buffer_cursor_to_textarea();
8519}
8520
8521pub(crate) fn visual_join<H: crate::types::Host>(
8525 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8526 with_space: bool,
8527) {
8528 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8529 let (top, bot) = match ed.vim.mode {
8530 Mode::VisualLine => (
8531 cursor_row.min(ed.vim.visual_line_anchor),
8532 cursor_row.max(ed.vim.visual_line_anchor),
8533 ),
8534 Mode::VisualBlock => {
8535 let a = ed.vim.block_anchor.0;
8536 (a.min(cursor_row), a.max(cursor_row))
8537 }
8538 Mode::Visual => {
8539 let a = ed.vim.visual_anchor.0;
8540 (a.min(cursor_row), a.max(cursor_row))
8541 }
8542 _ => return,
8543 };
8544 let joins = (bot - top).max(1);
8547 ed.push_undo();
8548 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8549 ed.push_buffer_cursor_to_textarea();
8550 for _ in 0..joins {
8551 if with_space {
8552 join_line(ed);
8553 } else {
8554 join_line_raw(ed);
8555 }
8556 }
8557 ed.vim.mode = Mode::Normal;
8558 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8559}
8560
8561pub(crate) fn goto_percent<H: crate::types::Host>(
8564 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8565 count: usize,
8566) {
8567 let rows = buf_row_count(&ed.buffer);
8568 if rows == 0 {
8569 return;
8570 }
8571 let total = if rows >= 2
8574 && buf_line(&ed.buffer, rows - 1)
8575 .map(|s| s.is_empty())
8576 .unwrap_or(false)
8577 {
8578 rows - 1
8579 } else {
8580 rows
8581 };
8582 let line = (count * total).div_ceil(100).clamp(1, total);
8584 let pre = ed.cursor();
8585 ed.jump_cursor(line - 1, 0);
8586 move_first_non_whitespace(ed);
8587 ed.sticky_col = Some(ed.cursor().1);
8588 if ed.cursor() != pre {
8589 ed.push_jump(pre);
8590 }
8591}
8592
8593fn indent_width(s: &str, tabstop: usize) -> usize {
8596 let ts = tabstop.max(1);
8597 let mut w = 0usize;
8598 for c in s.chars() {
8599 match c {
8600 ' ' => w += 1,
8601 '\t' => w += ts - (w % ts),
8602 _ => break,
8603 }
8604 }
8605 w
8606}
8607
8608fn build_indent(width: usize, settings: &crate::editor::Settings) -> String {
8611 if settings.expandtab {
8612 return " ".repeat(width);
8613 }
8614 let ts = settings.tabstop.max(1);
8615 let tabs = width / ts;
8616 let spaces = width % ts;
8617 format!("{}{}", "\t".repeat(tabs), " ".repeat(spaces))
8618}
8619
8620fn reindent_block(text: &str, target_width: usize, settings: &crate::editor::Settings) -> String {
8623 let ts = settings.tabstop.max(1);
8624 let lines: Vec<&str> = text.split('\n').collect();
8625 let first_width = lines.first().map(|l| indent_width(l, ts)).unwrap_or(0);
8626 let delta = target_width as isize - first_width as isize;
8627 lines
8628 .iter()
8629 .map(|line| {
8630 let trimmed = line.trim_start_matches([' ', '\t']);
8631 if trimmed.is_empty() {
8632 return String::new();
8634 }
8635 let old_w = indent_width(line, ts) as isize;
8636 let new_w = (old_w + delta).max(0) as usize;
8637 format!("{}{}", build_indent(new_w, settings), trimmed)
8638 })
8639 .collect::<Vec<_>>()
8640 .join("\n")
8641}
8642
8643fn do_paste<H: crate::types::Host>(
8644 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8645 before: bool,
8646 count: usize,
8647 cursor_after: bool,
8648 reindent: bool,
8649) {
8650 use hjkl_buffer::{Edit, Position};
8651 ed.push_undo();
8652 let selector = ed.vim.pending_register.take();
8657 let (yank, linewise) = {
8658 let regs = ed.registers();
8659 match selector.and_then(|c| regs.read(c)) {
8660 Some(slot) => (slot.text.clone(), slot.linewise),
8661 None => (regs.unnamed.text.clone(), regs.unnamed.linewise),
8667 }
8668 };
8669 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
8673 let original_row_for_linewise_after = if linewise && !before {
8679 let r = buf_cursor_pos(&ed.buffer).row;
8682 let (_, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, r, r);
8683 Some(fold_end)
8684 } else {
8685 None
8686 };
8687 for _ in 0..count {
8688 ed.sync_buffer_content_from_textarea();
8689 let yank = yank.clone();
8690 if yank.is_empty() {
8691 continue;
8692 }
8693 if linewise {
8694 let mut text = yank.trim_matches('\n').to_string();
8698 let row = buf_cursor_pos(&ed.buffer).row;
8699 if reindent {
8701 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
8702 let target_w = indent_width(&cur_line, ed.settings.tabstop.max(1));
8703 text = reindent_block(&text, target_w, &ed.settings);
8704 }
8705 let (fold_start, fold_end) = expand_linewise_over_closed_folds(&ed.buffer, row, row);
8709 let target_row = if before {
8710 ed.mutate_edit(Edit::InsertStr {
8711 at: Position::new(fold_start, 0),
8712 text: format!("{text}\n"),
8713 });
8714 fold_start
8715 } else {
8716 let line_chars = buf_line_chars(&ed.buffer, fold_end);
8717 ed.mutate_edit(Edit::InsertStr {
8718 at: Position::new(fold_end, line_chars),
8719 text: format!("\n{text}"),
8720 });
8721 fold_end + 1
8722 };
8723 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
8724 crate::motions::move_first_non_blank(&mut ed.buffer);
8725 ed.push_buffer_cursor_to_textarea();
8726 let payload_lines = text.lines().count().max(1);
8728 let bot_row = target_row + payload_lines - 1;
8729 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
8730 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
8731 } else {
8732 let cursor = buf_cursor_pos(&ed.buffer);
8736 let at = if before {
8737 cursor
8738 } else {
8739 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
8740 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
8741 };
8742 ed.mutate_edit(Edit::InsertStr {
8743 at,
8744 text: yank.clone(),
8745 });
8746 if !cursor_after && ed.cursor().1 > 0 {
8751 crate::motions::move_left(&mut ed.buffer, 1);
8752 ed.push_buffer_cursor_to_textarea();
8753 }
8754 let lo = (at.row, at.col);
8756 let hi = if cursor_after {
8757 let c = ed.cursor();
8758 (c.0, c.1.saturating_sub(1))
8759 } else {
8760 ed.cursor()
8761 };
8762 paste_mark = Some((lo, hi));
8763 }
8764 }
8765 if let Some((lo, hi)) = paste_mark {
8766 ed.set_mark('[', lo);
8767 ed.set_mark(']', hi);
8768 }
8769 if cursor_after && linewise {
8772 if let Some((_, (bot_row, _))) = paste_mark {
8773 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
8774 let target = (bot_row + 1).min(last_row);
8775 buf_set_cursor_rc(&mut ed.buffer, target, 0);
8776 ed.push_buffer_cursor_to_textarea();
8777 }
8778 } else if let Some(orig_row) = original_row_for_linewise_after {
8779 let first_target = orig_row.saturating_add(1);
8784 buf_set_cursor_rc(&mut ed.buffer, first_target, 0);
8785 crate::motions::move_first_non_blank(&mut ed.buffer);
8786 ed.push_buffer_cursor_to_textarea();
8787 }
8788 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8790}
8791
8792pub(crate) fn visual_paste<H: crate::types::Host>(
8797 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8798 before: bool,
8799) {
8800 use hjkl_buffer::{Edit, Position};
8801 ed.sync_buffer_content_from_textarea();
8802
8803 let selector = ed.vim.pending_register.take();
8806 let (reg_text, reg_linewise) = {
8807 let regs = ed.registers();
8808 match selector.and_then(|c| regs.read(c)) {
8809 Some(slot) => (slot.text.clone(), slot.linewise),
8810 None => (regs.unnamed.text.clone(), regs.unnamed.linewise),
8811 }
8812 };
8813 let saved_unnamed = before.then(|| ed.registers().unnamed.clone());
8815
8816 let mode = ed.vim.mode;
8817 ed.push_undo();
8818
8819 match mode {
8820 Mode::VisualLine => {
8821 let cursor_row = buf_cursor_pos(&ed.buffer).row;
8822 let top = cursor_row.min(ed.vim.visual_line_anchor);
8823 let bot = cursor_row.max(ed.vim.visual_line_anchor);
8824 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
8826 let text = reg_text.trim_matches('\n').to_string();
8828 let line_count = buf_row_count(&ed.buffer);
8829 if top >= line_count {
8830 let last = line_count.saturating_sub(1);
8833 let lc = buf_line_chars(&ed.buffer, last);
8834 ed.mutate_edit(Edit::InsertStr {
8835 at: Position::new(last, lc),
8836 text: format!("\n{text}"),
8837 });
8838 buf_set_cursor_rc(&mut ed.buffer, last + 1, 0);
8839 } else {
8840 ed.mutate_edit(Edit::InsertStr {
8841 at: Position::new(top, 0),
8842 text: format!("{text}\n"),
8843 });
8844 buf_set_cursor_rc(&mut ed.buffer, top, 0);
8845 }
8846 crate::motions::move_first_non_blank(&mut ed.buffer);
8847 ed.push_buffer_cursor_to_textarea();
8848 }
8849 Mode::Visual | Mode::VisualBlock => {
8850 let anchor = if mode == Mode::VisualBlock {
8851 ed.vim.block_anchor
8852 } else {
8853 ed.vim.visual_anchor
8854 };
8855 let cursor = ed.cursor();
8856 let (top, bot) = order(anchor, cursor);
8857 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
8859 if reg_linewise {
8861 let text = reg_text.trim_matches('\n').to_string();
8863 let lc = buf_line_chars(&ed.buffer, top.0);
8864 ed.mutate_edit(Edit::InsertStr {
8865 at: Position::new(top.0, lc),
8866 text: format!("\n{text}"),
8867 });
8868 buf_set_cursor_rc(&mut ed.buffer, top.0 + 1, 0);
8869 crate::motions::move_first_non_blank(&mut ed.buffer);
8870 } else {
8871 ed.mutate_edit(Edit::InsertStr {
8872 at: Position::new(top.0, top.1),
8873 text: reg_text.clone(),
8874 });
8875 let inserted_len = reg_text.chars().count();
8877 let last_col = top.1 + inserted_len.saturating_sub(1);
8878 buf_set_cursor_rc(&mut ed.buffer, top.0, last_col);
8879 }
8880 ed.push_buffer_cursor_to_textarea();
8881 }
8882 _ => {}
8883 }
8884
8885 if let Some(slot) = saved_unnamed {
8887 ed.registers_mut().unnamed = slot;
8888 }
8889 ed.vim.mode = Mode::Normal;
8890 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8891}
8892
8893pub(crate) fn adjust_number_visual<H: crate::types::Host>(
8898 ed: &mut Editor<hjkl_buffer::Buffer, H>,
8899 delta: i64,
8900 sequential: bool,
8901) {
8902 use hjkl_buffer::{Edit, MotionKind, Position};
8903 ed.sync_buffer_content_from_textarea();
8904 let mode = ed.vim.mode;
8905 let cursor = buf_cursor_pos(&ed.buffer);
8906
8907 let (top, bot, mut scan_col_first, block_left) = match mode {
8909 Mode::VisualLine => {
8910 let t = cursor.row.min(ed.vim.visual_line_anchor);
8911 let b = cursor.row.max(ed.vim.visual_line_anchor);
8912 (t, b, 0usize, None)
8913 }
8914 Mode::Visual => {
8915 let (a, c) = order(ed.vim.visual_anchor, (cursor.row, cursor.col));
8916 (a.0, c.0, a.1, None)
8917 }
8918 Mode::VisualBlock => {
8919 let (a, c) = order(ed.vim.block_anchor, (cursor.row, cursor.col));
8920 let left = a.1.min(c.1);
8921 (a.0, c.0, left, Some(left))
8922 }
8923 _ => return,
8924 };
8925
8926 ed.push_undo();
8927 let mut found_count: i64 = 0;
8928 for row in top..=bot {
8929 let start_col = match block_left {
8930 Some(left) => left,
8931 None => {
8932 let c = if row == top { scan_col_first } else { 0 };
8935 scan_col_first = 0;
8936 c
8937 }
8938 };
8939 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
8940 Some(l) => l.chars().collect(),
8941 None => continue,
8942 };
8943 let Some(digit_start) =
8944 (start_col.min(chars.len())..chars.len()).find(|&i| chars[i].is_ascii_digit())
8945 else {
8946 continue;
8947 };
8948 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
8949 digit_start - 1
8950 } else {
8951 digit_start
8952 };
8953 let mut span_end = digit_start;
8954 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
8955 span_end += 1;
8956 }
8957 let s: String = chars[span_start..span_end].iter().collect();
8958 let Ok(n) = s.parse::<i64>() else {
8959 continue;
8960 };
8961 found_count += 1;
8962 let this_delta = if sequential {
8963 delta.saturating_mul(found_count)
8964 } else {
8965 delta
8966 };
8967 let new_s = n.saturating_add(this_delta).to_string();
8968 let span_start_pos = Position::new(row, span_start);
8969 let span_end_pos = Position::new(row, span_end);
8970 ed.mutate_edit(Edit::DeleteRange {
8971 start: span_start_pos,
8972 end: span_end_pos,
8973 kind: MotionKind::Char,
8974 });
8975 ed.mutate_edit(Edit::InsertStr {
8976 at: span_start_pos,
8977 text: new_s,
8978 });
8979 }
8980 buf_set_cursor_rc(&mut ed.buffer, top, block_left.unwrap_or(0));
8982 ed.push_buffer_cursor_to_textarea();
8983 ed.vim.mode = Mode::Normal;
8984 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
8985}
8986
8987pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
8988 if let Some(entry) = ed.buffer.pop_undo_entry() {
8989 let (cur_rope, cur_cursor) = ed.snapshot();
8990 ed.buffer.push_redo_entry(hjkl_buffer::UndoEntry {
8991 rope: cur_rope,
8992 cursor: cur_cursor,
8993 timestamp: entry.timestamp,
8994 });
8995 ed.restore_rope(entry.rope, entry.cursor);
8996 }
8997 ed.vim.mode = Mode::Normal;
8998 clamp_cursor_to_normal_mode(ed);
9002}
9003
9004pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
9005 if let Some(entry) = ed.buffer.pop_redo_entry() {
9006 let (cur_rope, cur_cursor) = ed.snapshot();
9007 let before = cur_rope.clone();
9008 ed.buffer.push_undo_entry(hjkl_buffer::UndoEntry {
9009 rope: cur_rope,
9010 cursor: cur_cursor,
9011 timestamp: entry.timestamp,
9012 });
9013 ed.cap_undo();
9014 ed.restore_rope(entry.rope, entry.cursor);
9015 let after = crate::types::Query::rope(&ed.buffer);
9019 if let Some((row, col)) = first_diff_pos(&before, &after) {
9020 buf_set_cursor_rc(&mut ed.buffer, row, col);
9021 ed.push_buffer_cursor_to_textarea();
9022 }
9023 }
9024 ed.vim.mode = Mode::Normal;
9025 clamp_cursor_to_normal_mode(ed);
9026}
9027
9028fn first_diff_pos(a: &ropey::Rope, b: &ropey::Rope) -> Option<(usize, usize)> {
9031 let rows = a.len_lines().max(b.len_lines());
9032 for r in 0..rows {
9033 let la = if r < a.len_lines() {
9034 hjkl_buffer::rope_line_str(a, r)
9035 } else {
9036 String::new()
9037 };
9038 let lb = if r < b.len_lines() {
9039 hjkl_buffer::rope_line_str(b, r)
9040 } else {
9041 String::new()
9042 };
9043 if la != lb {
9044 let col = la
9045 .chars()
9046 .zip(lb.chars())
9047 .take_while(|(x, y)| x == y)
9048 .count();
9049 return Some((r, col));
9050 }
9051 }
9052 None
9053}
9054
9055fn replay_insert_and_finish<H: crate::types::Host>(
9062 ed: &mut Editor<hjkl_buffer::Buffer, H>,
9063 text: &str,
9064) {
9065 use hjkl_buffer::{Edit, Position};
9066 let cursor = ed.cursor();
9067 ed.mutate_edit(Edit::InsertStr {
9068 at: Position::new(cursor.0, cursor.1),
9069 text: text.to_string(),
9070 });
9071 if ed.vim.insert_session.take().is_some() {
9072 if ed.cursor().1 > 0 {
9073 crate::motions::move_left(&mut ed.buffer, 1);
9074 ed.push_buffer_cursor_to_textarea();
9075 }
9076 ed.vim.mode = Mode::Normal;
9077 }
9078}
9079
9080pub(crate) fn replay_last_change<H: crate::types::Host>(
9081 ed: &mut Editor<hjkl_buffer::Buffer, H>,
9082 outer_count: usize,
9083) {
9084 let Some(change) = ed.vim.last_change.clone() else {
9085 return;
9086 };
9087 ed.vim.replaying = true;
9088 let scale = if outer_count > 0 { outer_count } else { 1 };
9089 match change {
9090 LastChange::OpMotion {
9091 op,
9092 motion,
9093 count,
9094 inserted,
9095 } => {
9096 let total = count.max(1) * scale;
9097 apply_op_with_motion(ed, op, &motion, total);
9098 if let Some(text) = inserted {
9099 replay_insert_and_finish(ed, &text);
9100 }
9101 }
9102 LastChange::OpTextObj {
9103 op,
9104 obj,
9105 inner,
9106 inserted,
9107 } => {
9108 apply_op_with_text_object(ed, op, obj, inner, 1);
9111 if let Some(text) = inserted {
9112 replay_insert_and_finish(ed, &text);
9113 }
9114 }
9115 LastChange::LineOp {
9116 op,
9117 count,
9118 inserted,
9119 } => {
9120 let total = count.max(1) * scale;
9121 execute_line_op(ed, op, total);
9122 if let Some(text) = inserted {
9123 replay_insert_and_finish(ed, &text);
9124 }
9125 }
9126 LastChange::CharDel { forward, count } => {
9127 do_char_delete(ed, forward, count * scale);
9128 }
9129 LastChange::ReplaceChar { ch, count } => {
9130 replace_char(ed, ch, count * scale);
9131 }
9132 LastChange::ToggleCase { count } => {
9133 for _ in 0..count * scale {
9134 ed.push_undo();
9135 toggle_case_at_cursor(ed);
9136 }
9137 }
9138 LastChange::JoinLine { count } => {
9139 for _ in 0..count * scale {
9140 ed.push_undo();
9141 join_line(ed);
9142 }
9143 }
9144 LastChange::Paste {
9145 before,
9146 count,
9147 cursor_after,
9148 reindent,
9149 } => {
9150 do_paste(ed, before, count * scale, cursor_after, reindent);
9151 }
9152 LastChange::GnOp {
9153 op,
9154 forward,
9155 inserted,
9156 } => {
9157 gn_operate(ed, Some(op), forward, 1);
9158 if let Some(text) = inserted {
9159 replay_insert_and_finish(ed, &text);
9160 }
9161 }
9162 LastChange::ReplaceMode { text } => {
9163 use hjkl_buffer::{Edit, MotionKind, Position};
9164 ed.push_undo();
9165 for ch in text.chars() {
9166 let cursor = buf_cursor_pos(&ed.buffer);
9167 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
9168 if cursor.col < line_chars {
9169 ed.mutate_edit(Edit::DeleteRange {
9171 start: cursor,
9172 end: Position::new(cursor.row, cursor.col + 1),
9173 kind: MotionKind::Char,
9174 });
9175 }
9176 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
9177 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
9178 }
9179 if ed.cursor().1 > 0 {
9181 crate::motions::move_left(&mut ed.buffer, 1);
9182 }
9183 ed.push_buffer_cursor_to_textarea();
9184 }
9185 LastChange::DeleteToEol { inserted } => {
9186 use hjkl_buffer::{Edit, Position};
9187 ed.push_undo();
9188 delete_to_eol(ed);
9189 if let Some(text) = inserted {
9190 let cursor = ed.cursor();
9191 ed.mutate_edit(Edit::InsertStr {
9192 at: Position::new(cursor.0, cursor.1),
9193 text,
9194 });
9195 }
9196 }
9197 LastChange::OpenLine { above, inserted } => {
9198 use hjkl_buffer::{Edit, Position};
9199 ed.push_undo();
9200 ed.sync_buffer_content_from_textarea();
9201 let row = buf_cursor_pos(&ed.buffer).row;
9202 if above {
9203 ed.mutate_edit(Edit::InsertStr {
9204 at: Position::new(row, 0),
9205 text: "\n".to_string(),
9206 });
9207 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
9208 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
9209 } else {
9210 let line_chars = buf_line_chars(&ed.buffer, row);
9211 ed.mutate_edit(Edit::InsertStr {
9212 at: Position::new(row, line_chars),
9213 text: "\n".to_string(),
9214 });
9215 }
9216 ed.push_buffer_cursor_to_textarea();
9217 let cursor = ed.cursor();
9218 ed.mutate_edit(Edit::InsertStr {
9219 at: Position::new(cursor.0, cursor.1),
9220 text: inserted,
9221 });
9222 }
9223 LastChange::InsertAt {
9224 entry,
9225 inserted,
9226 count,
9227 } => {
9228 use hjkl_buffer::{Edit, Position};
9229 ed.push_undo();
9230 match entry {
9231 InsertEntry::I => {}
9232 InsertEntry::ShiftI => move_first_non_whitespace(ed),
9233 InsertEntry::A => {
9234 crate::motions::move_right_to_end(&mut ed.buffer, 1);
9235 ed.push_buffer_cursor_to_textarea();
9236 }
9237 InsertEntry::ShiftA => {
9238 crate::motions::move_line_end(&mut ed.buffer);
9239 crate::motions::move_right_to_end(&mut ed.buffer, 1);
9240 ed.push_buffer_cursor_to_textarea();
9241 }
9242 }
9243 for _ in 0..count.max(1) {
9244 let cursor = ed.cursor();
9245 ed.mutate_edit(Edit::InsertStr {
9246 at: Position::new(cursor.0, cursor.1),
9247 text: inserted.clone(),
9248 });
9249 }
9250 }
9251 }
9252 ed.vim.replaying = false;
9253}
9254
9255fn changed_run(before: &str, after: &str) -> String {
9261 let a: Vec<char> = before.chars().collect();
9262 let b: Vec<char> = after.chars().collect();
9263 let prefix = a.iter().zip(b.iter()).take_while(|(x, y)| x == y).count();
9264 let max_suffix = a.len().min(b.len()) - prefix;
9265 let suffix = a
9266 .iter()
9267 .rev()
9268 .zip(b.iter().rev())
9269 .take(max_suffix)
9270 .take_while(|(x, y)| x == y)
9271 .count();
9272 b[prefix..b.len() - suffix].iter().collect()
9273}
9274
9275fn extract_inserted(before: &str, after: &str) -> String {
9276 let before_chars: Vec<char> = before.chars().collect();
9277 let after_chars: Vec<char> = after.chars().collect();
9278 if after_chars.len() <= before_chars.len() {
9279 return String::new();
9280 }
9281 let prefix = before_chars
9282 .iter()
9283 .zip(after_chars.iter())
9284 .take_while(|(a, b)| a == b)
9285 .count();
9286 let max_suffix = before_chars.len() - prefix;
9287 let suffix = before_chars
9288 .iter()
9289 .rev()
9290 .zip(after_chars.iter().rev())
9291 .take(max_suffix)
9292 .take_while(|(a, b)| a == b)
9293 .count();
9294 after_chars[prefix..after_chars.len() - suffix]
9295 .iter()
9296 .collect()
9297}
9298
9299#[cfg(test)]
9302mod comment_continuation_tests {
9303 use super::*;
9304 use crate::{DefaultHost, Editor, Options};
9305 use hjkl_buffer::Buffer;
9306
9307 fn make_editor_with_lang(lang: &str, content: &str) -> Editor<Buffer, DefaultHost> {
9308 let buf = Buffer::from_str(content);
9309 let host = DefaultHost::new();
9310 let opts = Options {
9311 filetype: lang.to_string(),
9312 formatoptions: "ro".to_string(),
9313 ..Options::default()
9314 };
9315 Editor::new(buf, host, opts)
9316 }
9317
9318 #[test]
9319 fn detect_rust_doc_comment() {
9320 let result = detect_comment_on_line("rust", "/// foo bar");
9321 assert!(result.is_some());
9322 let (indent, prefix) = result.unwrap();
9323 assert_eq!(indent, "");
9324 assert_eq!(prefix, "/// ");
9325 }
9326
9327 #[test]
9328 fn detect_rust_inner_doc_comment() {
9329 let result = detect_comment_on_line("rust", "//! crate docs");
9330 assert!(result.is_some());
9331 let (_, prefix) = result.unwrap();
9332 assert_eq!(prefix, "//! ");
9333 }
9334
9335 #[test]
9336 fn detect_rust_plain_comment() {
9337 let result = detect_comment_on_line("rust", "// normal comment");
9338 assert!(result.is_some());
9339 let (_, prefix) = result.unwrap();
9340 assert_eq!(prefix, "// ");
9341 }
9342
9343 #[test]
9344 fn detect_indented_comment() {
9345 let result = detect_comment_on_line("rust", " // indented");
9346 assert!(result.is_some());
9347 let (indent, prefix) = result.unwrap();
9348 assert_eq!(indent, " ");
9349 assert_eq!(prefix, "// ");
9350 }
9351
9352 #[test]
9353 fn detect_python_hash() {
9354 let result = detect_comment_on_line("python", "# comment");
9355 assert!(result.is_some());
9356 let (_, prefix) = result.unwrap();
9357 assert_eq!(prefix, "# ");
9358 }
9359
9360 #[test]
9361 fn detect_lua_double_dash() {
9362 let result = detect_comment_on_line("lua", "-- a lua comment");
9363 assert!(result.is_some());
9364 let (_, prefix) = result.unwrap();
9365 assert_eq!(prefix, "-- ");
9366 }
9367
9368 #[test]
9369 fn detect_non_comment_is_none() {
9370 assert!(detect_comment_on_line("rust", "let x = 1;").is_none());
9371 assert!(detect_comment_on_line("python", "x = 1").is_none());
9372 }
9373
9374 #[test]
9375 fn detect_bare_double_slash_still_matches() {
9376 assert!(detect_comment_on_line("rust", "//").is_some());
9378 }
9379
9380 #[test]
9381 fn rust_doc_before_plain() {
9382 let result = detect_comment_on_line("rust", "/// outer doc");
9384 let (_, prefix) = result.unwrap();
9385 assert_eq!(prefix, "/// ", "/// must match before //");
9386 }
9387
9388 #[test]
9389 fn continue_comment_returns_prefix_for_comment_row() {
9390 let ed = make_editor_with_lang("rust", "/// hello\n");
9391 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9392 assert_eq!(cont, Some("/// ".to_string()));
9393 }
9394
9395 #[test]
9396 fn continue_comment_returns_none_for_non_comment() {
9397 let ed = make_editor_with_lang("rust", "let x = 1;\n");
9398 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9399 assert!(cont.is_none());
9400 }
9401
9402 #[test]
9403 fn continue_comment_returns_none_when_filetype_empty() {
9404 let buf = Buffer::from_str("// hello\n");
9405 let host = DefaultHost::new();
9406 let ed = Editor::new(buf, host, Options::default());
9408 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
9409 assert!(cont.is_none());
9410 }
9411}
9412
9413#[cfg(test)]
9414mod comment_toggle_tests {
9415 use super::*;
9416 use crate::{DefaultHost, Editor, Options};
9417 use hjkl_buffer::Buffer;
9418
9419 fn make_rust_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9420 let buf = Buffer::from_str(content);
9421 let host = DefaultHost::new();
9422 let opts = Options {
9423 filetype: "rust".to_string(),
9424 ..Options::default()
9425 };
9426 Editor::new(buf, host, opts)
9427 }
9428
9429 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9430 buf_line(&ed.buffer, row).unwrap_or_default()
9431 }
9432
9433 #[test]
9436 fn gcc_comments_rust_line() {
9437 let mut ed = make_rust_editor("let x = 1;");
9438 ed.toggle_comment_range(0, 0);
9439 assert_eq!(line(&ed, 0), "// let x = 1;");
9440 }
9441
9442 #[test]
9443 fn gcc_uncomments_rust_line() {
9444 let mut ed = make_rust_editor("// let x = 1;");
9445 ed.toggle_comment_range(0, 0);
9446 assert_eq!(line(&ed, 0), "let x = 1;");
9447 }
9448
9449 #[test]
9450 fn gcc_indent_preserving() {
9451 let mut ed = make_rust_editor(" let x = 1;");
9453 ed.toggle_comment_range(0, 0);
9454 assert_eq!(line(&ed, 0), " // let x = 1;");
9455 }
9456
9457 #[test]
9458 fn gcc_indent_preserving_uncomment() {
9459 let mut ed = make_rust_editor(" // let x = 1;");
9460 ed.toggle_comment_range(0, 0);
9461 assert_eq!(line(&ed, 0), " let x = 1;");
9462 }
9463
9464 #[test]
9467 fn toggle_multi_line_all_uncommented() {
9468 let content = "let a = 1;\nlet b = 2;\nlet c = 3;";
9469 let mut ed = make_rust_editor(content);
9470 ed.toggle_comment_range(0, 2);
9471 assert_eq!(line(&ed, 0), "// let a = 1;");
9472 assert_eq!(line(&ed, 1), "// let b = 2;");
9473 assert_eq!(line(&ed, 2), "// let c = 3;");
9474 }
9475
9476 #[test]
9477 fn toggle_multi_line_all_commented() {
9478 let content = "// let a = 1;\n// let b = 2;\n// let c = 3;";
9479 let mut ed = make_rust_editor(content);
9480 ed.toggle_comment_range(0, 2);
9481 assert_eq!(line(&ed, 0), "let a = 1;");
9482 assert_eq!(line(&ed, 1), "let b = 2;");
9483 assert_eq!(line(&ed, 2), "let c = 3;");
9484 }
9485
9486 #[test]
9489 fn toggle_mixed_state_comments_all() {
9490 let content = "let a = 1;\n// let b = 2;\nlet c = 3;\n// let d = 4;\nlet e = 5;";
9492 let mut ed = make_rust_editor(content);
9493 ed.toggle_comment_range(0, 4);
9494 for r in 0..5 {
9495 assert!(
9496 line(&ed, r).trim_start().starts_with("//"),
9497 "row {r} not commented: {:?}",
9498 line(&ed, r)
9499 );
9500 }
9501 }
9502
9503 #[test]
9506 fn blank_lines_not_commented() {
9507 let content = "let a = 1;\n\nlet b = 2;";
9508 let mut ed = make_rust_editor(content);
9509 ed.toggle_comment_range(0, 2);
9510 assert_eq!(line(&ed, 0), "// let a = 1;");
9511 assert_eq!(line(&ed, 1), ""); assert_eq!(line(&ed, 2), "// let b = 2;");
9513 }
9514
9515 #[test]
9518 fn python_comment_toggle() {
9519 let buf = Buffer::from_str("x = 1\ny = 2");
9520 let host = DefaultHost::new();
9521 let opts = Options {
9522 filetype: "python".to_string(),
9523 ..Options::default()
9524 };
9525 let mut ed = Editor::new(buf, host, opts);
9526 ed.toggle_comment_range(0, 1);
9527 assert_eq!(line(&ed, 0), "# x = 1");
9528 assert_eq!(line(&ed, 1), "# y = 2");
9529 ed.toggle_comment_range(0, 1);
9531 assert_eq!(line(&ed, 0), "x = 1");
9532 assert_eq!(line(&ed, 1), "y = 2");
9533 }
9534
9535 #[test]
9538 fn commentstring_override_via_setting() {
9539 let buf = Buffer::from_str("hello world");
9540 let host = DefaultHost::new();
9541 let opts = Options {
9542 filetype: "rust".to_string(),
9543 ..Options::default()
9544 };
9545 let mut ed = Editor::new(buf, host, opts);
9546 ed.settings_mut().commentstring = "# %s".to_string();
9548 ed.toggle_comment_range(0, 0);
9549 assert_eq!(line(&ed, 0), "# hello world");
9550 }
9551
9552 #[test]
9555 fn unknown_lang_no_op() {
9556 let buf = Buffer::from_str("hello");
9557 let host = DefaultHost::new();
9558 let opts = Options::default(); let mut ed = Editor::new(buf, host, opts);
9560 ed.toggle_comment_range(0, 0);
9561 assert_eq!(line(&ed, 0), "hello");
9563 }
9564}
9565
9566#[cfg(test)]
9569mod g_ampersand_tests {
9570 use super::*;
9571 use crate::{DefaultHost, Editor, Options};
9572 use hjkl_buffer::{Buffer, rope_line_str};
9573
9574 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9575 let buf = Buffer::from_str(content);
9576 let host = DefaultHost::new();
9577 Editor::new(buf, host, Options::default())
9578 }
9579
9580 fn buf_line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
9581 let rope = ed.buffer().rope();
9582 rope_line_str(&rope, row).trim_end_matches('\n').to_string()
9583 }
9584
9585 #[test]
9588 fn g_ampersand_repeats_last_substitute_on_whole_buffer() {
9589 let mut ed = make_editor("foo\nfoo bar foo\nbaz");
9590 let cmd = crate::substitute::parse_substitute("/foo/bar/").unwrap();
9592 ed.set_last_substitute(cmd);
9593 apply_after_g(&mut ed, '&', 1);
9595 assert_eq!(buf_line(&ed, 0), "bar");
9596 assert_eq!(buf_line(&ed, 1), "bar bar foo");
9598 assert_eq!(buf_line(&ed, 2), "baz");
9599 }
9600
9601 #[test]
9603 fn g_ampersand_with_g_flag_replaces_all_per_line() {
9604 let mut ed = make_editor("foo foo\nfoo");
9605 let cmd = crate::substitute::parse_substitute("/foo/bar/g").unwrap();
9606 ed.set_last_substitute(cmd);
9607 apply_after_g(&mut ed, '&', 1);
9608 assert_eq!(buf_line(&ed, 0), "bar bar");
9609 assert_eq!(buf_line(&ed, 1), "bar");
9610 }
9611
9612 #[test]
9614 fn g_ampersand_noop_when_no_prior_substitute() {
9615 let mut ed = make_editor("foo\nbar");
9616 apply_after_g(&mut ed, '&', 1);
9618 assert_eq!(buf_line(&ed, 0), "foo");
9619 assert_eq!(buf_line(&ed, 1), "bar");
9620 }
9621}
9622
9623#[cfg(test)]
9626mod sneak_tests {
9627 use super::*;
9628 use crate::{DefaultHost, Editor, Options};
9629 use hjkl_buffer::Buffer;
9630
9631 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9632 let buf = Buffer::from_str(content);
9633 let host = DefaultHost::new();
9634 Editor::new(buf, host, Options::default())
9635 }
9636
9637 #[test]
9639 fn sneak_forward_jumps_to_two_char_digraph() {
9640 let mut ed = make_editor("foo bar baz qux\n");
9641 ed.jump_cursor(0, 0);
9642 ed.sneak('b', 'a', true, 1);
9643 assert_eq!(ed.cursor(), (0, 4), "cursor should land on 'ba' in 'bar'");
9644 }
9645
9646 #[test]
9648 fn sneak_backward_jumps_to_prior_match() {
9649 let mut ed = make_editor("foo bar baz qux\n");
9650 ed.jump_cursor(0, 12);
9651 ed.sneak('b', 'a', false, 1);
9652 assert_eq!(
9653 ed.cursor(),
9654 (0, 8),
9655 "backward sneak should find 'ba' in 'baz'"
9656 );
9657 }
9658
9659 #[test]
9661 fn sneak_repeat_semicolon_next_match() {
9662 let mut ed = make_editor("foo bar baz qux\n");
9663 ed.jump_cursor(0, 0);
9664 ed.sneak('b', 'a', true, 1);
9666 assert_eq!(ed.cursor(), (0, 4));
9667 execute_motion(&mut ed, Motion::FindRepeat { reverse: false }, 1);
9669 assert_eq!(ed.cursor(), (0, 8), "semicolon should jump to next 'ba'");
9670 }
9671
9672 #[test]
9674 fn sneak_repeat_comma_prev_match() {
9675 let mut ed = make_editor("foo bar baz qux\n");
9676 ed.jump_cursor(0, 0);
9677 ed.sneak('b', 'a', true, 1);
9678 assert_eq!(ed.cursor(), (0, 4));
9679 let pre = ed.cursor();
9681 execute_motion(&mut ed, Motion::FindRepeat { reverse: true }, 1);
9682 assert_eq!(
9683 ed.cursor(),
9684 pre,
9685 "comma with no prior match should leave cursor unchanged"
9686 );
9687 }
9688
9689 #[test]
9691 fn sneak_s_searches_backward() {
9692 let mut ed = make_editor("foo bar baz qux\n");
9693 ed.jump_cursor(0, 12);
9694 ed.sneak('b', 'a', false, 1);
9695 assert_eq!(ed.cursor(), (0, 8));
9696 }
9697
9698 #[test]
9700 fn sneak_with_count_jumps_to_nth() {
9701 let mut ed = make_editor("foo bar baz qux\n");
9702 ed.jump_cursor(0, 0);
9703 ed.sneak('b', 'a', true, 2);
9704 assert_eq!(ed.cursor(), (0, 8), "count=2 should jump to 2nd 'ba'");
9705 }
9706
9707 #[test]
9709 fn sneak_no_match_cursor_stays() {
9710 let mut ed = make_editor("foo bar baz qux\n");
9711 ed.jump_cursor(0, 0);
9712 let pre = ed.cursor();
9713 ed.sneak('x', 'x', true, 1);
9714 assert_eq!(ed.cursor(), pre, "no match should leave cursor unchanged");
9715 }
9716
9717 #[test]
9719 fn operator_pending_dsab_deletes_to_digraph() {
9720 let mut ed = make_editor("hello ab world\n");
9721 ed.jump_cursor(0, 0);
9722 ed.apply_op_sneak(Operator::Delete, 'a', 'b', true, 1);
9723 let content = ed.content();
9725 assert!(
9726 content.starts_with("ab world"),
9727 "dsab should delete 'hello ' leaving 'ab world'; got: {content:?}"
9728 );
9729 }
9730
9731 #[test]
9733 fn sneak_cross_line_match() {
9734 let mut ed = make_editor("foo\nbar baz\n");
9735 ed.jump_cursor(0, 0);
9736 ed.sneak('b', 'a', true, 1);
9737 assert_eq!(ed.cursor(), (1, 0), "sneak should cross line boundary");
9738 }
9739
9740 #[test]
9742 fn sneak_updates_last_sneak_state() {
9743 let mut ed = make_editor("foo bar baz\n");
9744 ed.jump_cursor(0, 0);
9745 ed.sneak('b', 'a', true, 1);
9746 let ls = ed.last_sneak();
9747 assert_eq!(
9748 ls,
9749 Some((('b', 'a'), true)),
9750 "last_sneak should record the digraph and direction"
9751 );
9752 }
9753}
9754
9755#[cfg(test)]
9764mod indent_count_tests {
9765 use super::*;
9766 use crate::{DefaultHost, Editor, Options};
9767 use hjkl_buffer::Buffer;
9768
9769 fn make_editor(content: &str) -> Editor<Buffer, DefaultHost> {
9770 let buf = Buffer::from_str(content);
9771 let mut ed = Editor::new(buf, DefaultHost::new(), Options::default());
9772 ed.settings_mut().expandtab = true;
9773 ed.settings_mut().shiftwidth = 4;
9774 ed
9775 }
9776
9777 fn content(ed: &Editor<Buffer, DefaultHost>) -> String {
9778 (*ed.buffer().content_joined()).clone()
9779 }
9780
9781 #[test]
9782 fn count_indent_operates_on_n_lines() {
9783 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9784 ed.jump_cursor(0, 0);
9785 execute_line_op(&mut ed, Operator::Indent, 3);
9786 assert_eq!(content(&ed), " a\n b\n c\nd\ne\nf\n");
9787 }
9788
9789 #[test]
9790 fn count_indent_clamps_to_buffer_end() {
9791 let mut ed = make_editor("a\nb\nc\nd\ne\nf\n");
9792 ed.jump_cursor(0, 0);
9793 execute_line_op(&mut ed, Operator::Indent, 10);
9794 assert_eq!(content(&ed), " a\n b\n c\n d\n e\n f\n");
9795 }
9796
9797 #[test]
9798 fn count_outdent_clamps_to_buffer_end() {
9799 let mut ed = make_editor(" a\n b\n c\n");
9800 ed.jump_cursor(0, 0);
9801 execute_line_op(&mut ed, Operator::Outdent, 10);
9802 assert_eq!(content(&ed), "a\nb\nc\n");
9803 }
9804
9805 #[test]
9806 fn count_indent_on_last_line_is_noop() {
9807 let mut ed = make_editor("a\nb\nc\n");
9808 ed.jump_cursor(2, 0); execute_line_op(&mut ed, Operator::Indent, 5);
9810 assert_eq!(
9811 content(&ed),
9812 "a\nb\nc\n",
9813 "5>> on last line must abort (E16)"
9814 );
9815 }
9816
9817 #[test]
9818 fn count_indent_on_single_line_is_noop() {
9819 let mut ed = make_editor("x\n");
9820 ed.jump_cursor(0, 0);
9821 execute_line_op(&mut ed, Operator::Indent, 5);
9822 assert_eq!(content(&ed), "x\n", "5>> on the only line must abort (E16)");
9823 }
9824
9825 #[test]
9826 fn count_outdent_on_last_line_is_noop() {
9827 let mut ed = make_editor(" a\n b\n c\n");
9828 ed.jump_cursor(2, 0);
9829 execute_line_op(&mut ed, Operator::Outdent, 5);
9830 assert_eq!(content(&ed), " a\n b\n c\n");
9831 }
9832
9833 #[test]
9834 fn single_indent_on_last_line_still_works() {
9835 let mut ed = make_editor("a\nb\nc\n");
9837 ed.jump_cursor(2, 0);
9838 execute_line_op(&mut ed, Operator::Indent, 1);
9839 assert_eq!(content(&ed), "a\nb\n c\n");
9840 }
9841}
9842
9843#[cfg(test)]
9846mod abbrev_tests {
9847 use super::{Abbrev, AbbrevKind, AbbrevTrigger, abbrev_kind, try_abbrev_expand};
9848 use AbbrevKind::{End, Full, NonKw};
9849
9850 const ISK: &str = "@,48-57,_,192-255"; fn make_abbrev(lhs: &str, rhs: &str) -> Abbrev {
9853 Abbrev {
9854 lhs: lhs.to_string(),
9855 rhs: rhs.to_string(),
9856 insert: true,
9857 cmdline: false,
9858 noremap: false,
9859 }
9860 }
9861
9862 fn expand(
9863 abbrevs: &[Abbrev],
9864 before: &str,
9865 mincol: usize,
9866 trig: AbbrevTrigger,
9867 ) -> Option<(usize, String)> {
9868 try_abbrev_expand(abbrevs, before, mincol, trig, ISK)
9869 }
9870
9871 #[test]
9874 fn fullid_all_keyword_chars() {
9875 assert_eq!(abbrev_kind("teh", ISK), Full);
9876 assert_eq!(abbrev_kind("abc123", ISK), Full);
9877 assert_eq!(abbrev_kind("_foo", ISK), Full);
9878 }
9879
9880 #[test]
9881 fn endid_ends_with_kw_has_nonkw() {
9882 assert_eq!(abbrev_kind("#i", ISK), End);
9883 assert_eq!(abbrev_kind("#include", ISK), End);
9884 }
9885
9886 #[test]
9887 fn nonid_ends_with_nonkw() {
9888 assert_eq!(abbrev_kind(";;", ISK), NonKw);
9889 assert_eq!(abbrev_kind("->", ISK), NonKw);
9890 }
9891
9892 #[test]
9895 fn fullid_expands_on_space_trigger() {
9896 let abbrevs = [make_abbrev("teh", "the")];
9897 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword(' '));
9898 assert_eq!(r, Some((3, "the".to_string())));
9899 }
9900
9901 #[test]
9902 fn fullid_expands_on_esc_trigger() {
9903 let abbrevs = [make_abbrev("teh", "the")];
9904 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Esc);
9905 assert_eq!(r, Some((3, "the".to_string())));
9906 }
9907
9908 #[test]
9909 fn fullid_expands_on_cr_trigger() {
9910 let abbrevs = [make_abbrev("teh", "the")];
9911 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::Cr);
9912 assert_eq!(r, Some((3, "the".to_string())));
9913 }
9914
9915 #[test]
9916 fn fullid_expands_on_ctrl_bracket() {
9917 let abbrevs = [make_abbrev("teh", "the")];
9918 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::CtrlBracket);
9919 assert_eq!(r, Some((3, "the".to_string())));
9920 }
9921
9922 #[test]
9923 fn fullid_does_not_expand_on_keyword_trigger() {
9924 let abbrevs = [make_abbrev("teh", "the")];
9926 let r = expand(&abbrevs, "teh", 0, AbbrevTrigger::NonKeyword('a'));
9927 assert_eq!(r, None);
9929 }
9930
9931 #[test]
9932 fn fullid_no_expand_when_lhs_not_at_end() {
9933 let abbrevs = [make_abbrev("teh", "the")];
9934 let r = expand(&abbrevs, "ateh", 0, AbbrevTrigger::NonKeyword(' '));
9936 assert_eq!(r, None);
9937 }
9938
9939 #[test]
9940 fn fullid_expands_after_nonkw_prefix() {
9941 let abbrevs = [make_abbrev("teh", "the")];
9942 let r = expand(&abbrevs, "!teh", 0, AbbrevTrigger::NonKeyword(' '));
9944 assert_eq!(r, Some((3, "the".to_string())));
9945 }
9946
9947 #[test]
9948 fn fullid_single_char_no_expand_after_nonblank_nonkw() {
9949 let abbrevs = [make_abbrev("a", "b")];
9950 let r = expand(&abbrevs, "!a", 0, AbbrevTrigger::NonKeyword(' '));
9952 assert_eq!(r, None);
9953 }
9954
9955 #[test]
9956 fn fullid_single_char_expands_after_space() {
9957 let abbrevs = [make_abbrev("a", "b")];
9958 let r = expand(&abbrevs, " a", 0, AbbrevTrigger::NonKeyword(' '));
9960 assert_eq!(r, Some((1, "b".to_string())));
9961 }
9962
9963 #[test]
9966 fn mincol_blocks_consuming_preexisting_text() {
9967 let abbrevs = [make_abbrev("teh", "the")];
9968 let r = expand(&abbrevs, "teh", 3, AbbrevTrigger::NonKeyword(' '));
9970 assert_eq!(r, None);
9971 }
9972
9973 #[test]
9974 fn mincol_allows_match_starting_at_mincol() {
9975 let abbrevs = [make_abbrev("teh", "the")];
9976 let r = expand(&abbrevs, "!! teh", 3, AbbrevTrigger::NonKeyword(' '));
9979 assert_eq!(r, Some((3, "the".to_string())));
9980 }
9981
9982 #[test]
9985 fn endid_expands_on_space_trigger() {
9986 let abbrevs = [make_abbrev("#i", "#include")];
9987 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::NonKeyword(' '));
9988 assert_eq!(r, Some((2, "#include".to_string())));
9989 }
9990
9991 #[test]
9992 fn endid_expands_on_esc_trigger() {
9993 let abbrevs = [make_abbrev("#i", "#include")];
9994 let r = expand(&abbrevs, "#i", 0, AbbrevTrigger::Esc);
9995 assert_eq!(r, Some((2, "#include".to_string())));
9996 }
9997
9998 #[test]
10001 fn nonid_expands_on_esc_trigger() {
10002 let abbrevs = [make_abbrev(";;", "std::endl;")];
10003 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Esc);
10004 assert_eq!(r, Some((2, "std::endl;".to_string())));
10005 }
10006
10007 #[test]
10008 fn nonid_expands_on_cr_trigger() {
10009 let abbrevs = [make_abbrev(";;", "std::endl;")];
10010 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::Cr);
10011 assert_eq!(r, Some((2, "std::endl;".to_string())));
10012 }
10013
10014 #[test]
10015 fn nonid_does_not_expand_on_nonkw_trigger() {
10016 let abbrevs = [make_abbrev(";;", "std::endl;")];
10018 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::NonKeyword(' '));
10019 assert_eq!(r, None);
10020 }
10021
10022 #[test]
10023 fn nonid_expands_on_ctrl_bracket() {
10024 let abbrevs = [make_abbrev(";;", "std::endl;")];
10025 let r = expand(&abbrevs, ";;", 0, AbbrevTrigger::CtrlBracket);
10026 assert_eq!(r, Some((2, "std::endl;".to_string())));
10027 }
10028
10029 #[test]
10032 fn multiword_rhs_expansion() {
10033 let abbrevs = [make_abbrev("hw", "hello world")];
10034 let r = expand(&abbrevs, "hw", 0, AbbrevTrigger::NonKeyword(' '));
10035 assert_eq!(r, Some((2, "hello world".to_string())));
10036 }
10037
10038 #[test]
10041 fn no_match_returns_none() {
10042 let abbrevs = [make_abbrev("teh", "the")];
10043 let r = expand(&abbrevs, "xyz", 0, AbbrevTrigger::NonKeyword(' '));
10044 assert_eq!(r, None);
10045 }
10046
10047 #[test]
10048 fn empty_abbrevs_returns_none() {
10049 let r = expand(&[], "teh", 0, AbbrevTrigger::NonKeyword(' '));
10050 assert_eq!(r, None);
10051 }
10052
10053 #[test]
10054 fn empty_before_text_returns_none() {
10055 let abbrevs = [make_abbrev("teh", "the")];
10056 let r = expand(&abbrevs, "", 0, AbbrevTrigger::NonKeyword(' '));
10057 assert_eq!(r, None);
10058 }
10059}
10060
10061#[cfg(test)]
10064mod scan_tag_opener_multibyte_tests {
10065 use crate::types::Options;
10066 use crate::{DefaultHost, Editor};
10067 use hjkl_buffer::Buffer;
10068
10069 fn html_editor(content: &str) -> Editor<Buffer, DefaultHost> {
10070 let buf = Buffer::from_str(content);
10071 let host = DefaultHost::new();
10072 let mut ed = Editor::new(buf, host, Options::default());
10073 ed.settings.filetype = "html".to_string();
10074 ed.settings.autoclose_tag = true;
10075 ed.settings.autopair = true;
10076 ed
10077 }
10078
10079 #[test]
10086 fn autoclose_gt_after_multibyte_no_panic() {
10087 let mut ed = html_editor("ñ");
10088 ed.enter_insert_i(1);
10090 ed.jump_cursor(0, 1);
10092 ed.insert_char('>');
10094 let rope = ed.buffer().rope();
10096 let line = hjkl_buffer::rope_line_str(&rope, 0);
10097 assert!(line.contains('>'), "inserted > must appear in buffer");
10098 }
10099
10100 #[test]
10117 fn autoclose_gt_direct_after_multibyte_no_panic() {
10118 let mut ed = html_editor("ñ");
10121 ed.enter_insert_i(1);
10122 ed.jump_cursor(0, 1); ed.insert_char('>');
10125 let rope = ed.buffer().rope();
10126 let line = hjkl_buffer::rope_line_str(&rope, 0);
10127 assert!(
10128 line.contains('>'),
10129 "inserted > must appear in buffer, got: {line:?}"
10130 );
10131 }
10132}