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}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
166pub enum Operator {
167 Delete,
168 Change,
169 Yank,
170 Uppercase,
173 Lowercase,
175 ToggleCase,
179 Indent,
184 Outdent,
187 Fold,
191 Reflow,
196 AutoIndent,
200 Filter,
205 Comment,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub enum Motion {
213 Left,
214 Right,
215 Up,
216 Down,
217 WordFwd,
218 BigWordFwd,
219 WordBack,
220 BigWordBack,
221 WordEnd,
222 BigWordEnd,
223 WordEndBack,
225 BigWordEndBack,
227 LineStart,
228 FirstNonBlank,
229 LineEnd,
230 FileTop,
231 FileBottom,
232 Find {
233 ch: char,
234 forward: bool,
235 till: bool,
236 },
237 FindRepeat {
238 reverse: bool,
239 },
240 MatchBracket,
241 WordAtCursor {
242 forward: bool,
243 whole_word: bool,
246 },
247 SearchNext {
249 reverse: bool,
250 },
251 ViewportTop,
253 ViewportMiddle,
255 ViewportBottom,
257 LastNonBlank,
259 LineMiddle,
262 ParagraphPrev,
264 ParagraphNext,
266 SentencePrev,
268 SentenceNext,
270 ScreenDown,
273 ScreenUp,
275 SectionBackward,
278 SectionForward,
280 SectionEndBackward,
283 SectionEndForward,
285 FirstNonBlankNextLine,
287 FirstNonBlankPrevLine,
289 FirstNonBlankLine,
291 GotoColumn,
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
297pub enum TextObject {
298 Word {
299 big: bool,
300 },
301 Quote(char),
302 Bracket(char),
303 Paragraph,
304 XmlTag,
308 Sentence,
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq)]
317pub enum RangeKind {
318 Exclusive,
320 Inclusive,
322 Linewise,
324}
325
326#[derive(Debug, Clone)]
330pub enum LastChange {
331 OpMotion {
333 op: Operator,
334 motion: Motion,
335 count: usize,
336 inserted: Option<String>,
337 },
338 OpTextObj {
340 op: Operator,
341 obj: TextObject,
342 inner: bool,
343 inserted: Option<String>,
344 },
345 LineOp {
347 op: Operator,
348 count: usize,
349 inserted: Option<String>,
350 },
351 CharDel { forward: bool, count: usize },
353 ReplaceChar { ch: char, count: usize },
355 ToggleCase { count: usize },
357 JoinLine { count: usize },
359 Paste { before: bool, count: usize },
361 DeleteToEol { inserted: Option<String> },
363 OpenLine { above: bool, inserted: String },
365 InsertAt {
367 entry: InsertEntry,
368 inserted: String,
369 count: usize,
370 },
371}
372
373#[derive(Debug, Clone, Copy, PartialEq, Eq)]
374pub enum InsertEntry {
375 I,
376 A,
377 ShiftI,
378 ShiftA,
379}
380
381#[derive(Default)]
384pub struct VimState {
385 pub mode: Mode,
390 pub pending: Pending,
392 pub count: usize,
395 pub last_find: Option<(char, bool, bool)>,
397 pub last_change: Option<LastChange>,
399 pub insert_session: Option<InsertSession>,
401 pub visual_anchor: (usize, usize),
405 pub visual_line_anchor: usize,
407 pub block_anchor: (usize, usize),
410 pub block_vcol: usize,
416 pub yank_linewise: bool,
418 pub pending_register: Option<char>,
421 pub recording_macro: Option<char>,
425 pub recording_keys: Vec<crate::input::Input>,
430 pub replaying_macro: bool,
433 pub last_macro: Option<char>,
435 pub last_edit_pos: Option<(usize, usize)>,
438 pub last_insert_pos: Option<(usize, usize)>,
442 pub change_list: Vec<(usize, usize)>,
446 pub change_list_cursor: Option<usize>,
449 pub last_visual: Option<LastVisual>,
452 pub viewport_pinned: bool,
456 pub replaying: bool,
458 pub one_shot_normal: bool,
461 pub search_prompt: Option<SearchPrompt>,
463 pub last_search: Option<String>,
467 pub last_search_forward: bool,
471 pub jump_back: Vec<(usize, usize)>,
476 pub jump_fwd: Vec<(usize, usize)>,
479 pub insert_pending_register: bool,
483 pub change_mark_start: Option<(usize, usize)>,
489 pub search_history: Vec<String>,
493 pub search_history_cursor: Option<usize>,
498 pub last_input_at: Option<std::time::Instant>,
507 pub last_input_host_at: Option<core::time::Duration>,
511 pub(crate) current_mode: crate::VimMode,
517 pub last_substitute: Option<crate::substitute::SubstituteCmd>,
519 pub pending_closes: Vec<(usize, usize, char)>,
527}
528
529pub(crate) const SEARCH_HISTORY_MAX: usize = 100;
530pub(crate) const CHANGE_LIST_MAX: usize = 100;
531
532#[derive(Debug, Clone)]
535pub struct SearchPrompt {
536 pub text: String,
537 pub cursor: usize,
538 pub forward: bool,
539}
540
541#[derive(Debug, Clone)]
542pub struct InsertSession {
543 pub count: usize,
544 pub row_min: usize,
546 pub row_max: usize,
547 pub before_rope: ropey::Rope,
552 pub reason: InsertReason,
553}
554
555#[derive(Debug, Clone)]
556pub enum InsertReason {
557 Enter(InsertEntry),
559 Open { above: bool },
561 AfterChange,
564 DeleteToEol,
566 ReplayOnly,
569 BlockEdge { top: usize, bot: usize, col: usize },
573 BlockChange { top: usize, bot: usize, col: usize },
578 Replace,
582}
583
584#[derive(Debug, Clone, Copy)]
594pub struct LastVisual {
595 pub mode: Mode,
596 pub anchor: (usize, usize),
597 pub cursor: (usize, usize),
598 pub block_vcol: usize,
599}
600
601impl VimState {
602 pub fn public_mode(&self) -> VimMode {
603 match self.mode {
604 Mode::Normal => VimMode::Normal,
605 Mode::Insert => VimMode::Insert,
606 Mode::Visual => VimMode::Visual,
607 Mode::VisualLine => VimMode::VisualLine,
608 Mode::VisualBlock => VimMode::VisualBlock,
609 }
610 }
611
612 pub fn force_normal(&mut self) {
613 self.mode = Mode::Normal;
614 self.pending = Pending::None;
615 self.count = 0;
616 self.insert_session = None;
617 self.current_mode = crate::VimMode::Normal;
619 }
620
621 pub(crate) fn clear_pending_prefix(&mut self) {
631 self.pending = Pending::None;
632 self.count = 0;
633 self.pending_register = None;
634 self.insert_pending_register = false;
635 }
636
637 pub(crate) fn widen_insert_row(&mut self, row: usize) {
642 if let Some(ref mut session) = self.insert_session {
643 session.row_min = session.row_min.min(row);
644 session.row_max = session.row_max.max(row);
645 }
646 }
647
648 pub fn is_visual(&self) -> bool {
649 matches!(
650 self.mode,
651 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
652 )
653 }
654
655 pub fn is_visual_char(&self) -> bool {
656 self.mode == Mode::Visual
657 }
658
659 pub(crate) fn pending_count_val(&self) -> Option<u32> {
662 if self.count == 0 {
663 None
664 } else {
665 Some(self.count as u32)
666 }
667 }
668
669 pub(crate) fn is_chord_pending(&self) -> bool {
672 !matches!(self.pending, Pending::None)
673 }
674
675 pub(crate) fn pending_op_char(&self) -> Option<char> {
679 let op = match &self.pending {
680 Pending::Op { op, .. }
681 | Pending::OpTextObj { op, .. }
682 | Pending::OpG { op, .. }
683 | Pending::OpFind { op, .. }
684 | Pending::OpSquareBracketOpen { op, .. }
685 | Pending::OpSquareBracketClose { op, .. } => Some(*op),
686 _ => None,
687 };
688 op.map(|o| match o {
689 Operator::Delete => 'd',
690 Operator::Change => 'c',
691 Operator::Yank => 'y',
692 Operator::Uppercase => 'U',
693 Operator::Lowercase => 'u',
694 Operator::ToggleCase => '~',
695 Operator::Indent => '>',
696 Operator::Outdent => '<',
697 Operator::Fold => 'z',
698 Operator::Reflow => 'q',
699 Operator::AutoIndent => '=',
700 Operator::Filter => '!',
701 Operator::Comment => 'c',
703 })
704 }
705}
706
707pub(crate) fn enter_search<H: crate::types::Host>(
713 ed: &mut Editor<hjkl_buffer::Buffer, H>,
714 forward: bool,
715) {
716 ed.vim.search_prompt = Some(SearchPrompt {
717 text: String::new(),
718 cursor: 0,
719 forward,
720 });
721 ed.vim.search_history_cursor = None;
722 ed.set_search_pattern(None);
726}
727
728fn walk_change_list<H: crate::types::Host>(
732 ed: &mut Editor<hjkl_buffer::Buffer, H>,
733 dir: isize,
734 count: usize,
735) {
736 if ed.vim.change_list.is_empty() {
737 return;
738 }
739 let len = ed.vim.change_list.len();
740 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
741 (None, -1) => len as isize - 1,
742 (None, 1) => return, (Some(i), -1) => i as isize - 1,
744 (Some(i), 1) => i as isize + 1,
745 _ => return,
746 };
747 for _ in 1..count {
748 let next = idx + dir;
749 if next < 0 || next >= len as isize {
750 break;
751 }
752 idx = next;
753 }
754 if idx < 0 || idx >= len as isize {
755 return;
756 }
757 let idx = idx as usize;
758 ed.vim.change_list_cursor = Some(idx);
759 let (row, col) = ed.vim.change_list[idx];
760 ed.jump_cursor(row, col);
761}
762
763fn insert_register_text<H: crate::types::Host>(
768 ed: &mut Editor<hjkl_buffer::Buffer, H>,
769 selector: char,
770) {
771 use hjkl_buffer::Edit;
772 let text = match ed.registers().read(selector) {
773 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
774 _ => return,
775 };
776 ed.sync_buffer_content_from_textarea();
777 let cursor = buf_cursor_pos(&ed.buffer);
778 ed.mutate_edit(Edit::InsertStr {
779 at: cursor,
780 text: text.clone(),
781 });
782 let mut row = cursor.row;
785 let mut col = cursor.col;
786 for ch in text.chars() {
787 if ch == '\n' {
788 row += 1;
789 col = 0;
790 } else {
791 col += 1;
792 }
793 }
794 buf_set_cursor_rc(&mut ed.buffer, row, col);
795 ed.push_buffer_cursor_to_textarea();
796 ed.mark_content_dirty();
797 if let Some(ref mut session) = ed.vim.insert_session {
798 session.row_min = session.row_min.min(row);
799 session.row_max = session.row_max.max(row);
800 }
801}
802
803pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
822 if !settings.autoindent {
823 return String::new();
824 }
825 let base: String = prev_line
827 .chars()
828 .take_while(|c| *c == ' ' || *c == '\t')
829 .collect();
830
831 if settings.smartindent {
832 let unit = if settings.expandtab {
833 if settings.softtabstop > 0 {
834 " ".repeat(settings.softtabstop)
835 } else {
836 " ".repeat(settings.shiftwidth)
837 }
838 } else {
839 "\t".to_string()
840 };
841
842 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
844 if matches!(last_non_ws, Some('{' | '(' | '[')) {
845 return format!("{base}{unit}");
846 }
847
848 if is_html_filetype(&settings.filetype) {
853 let trimmed_end_len = prev_line
854 .trim_end_matches(|c: char| c.is_whitespace())
855 .len();
856 let trimmed = &prev_line[..trimmed_end_len];
857 if let Some(stripped) = trimmed.strip_suffix('>')
858 && scan_tag_opener(trimmed, stripped.len()).is_some()
859 {
860 return format!("{base}{unit}");
861 }
862 }
863 }
864
865 base
866}
867
868fn comment_prefixes_for_lang(lang: &str) -> &'static [&'static str] {
874 match lang {
875 "rust" => &["/// ", "//! ", "// "],
876 "c" | "cpp" => &["// "],
877 "python" | "sh" | "bash" | "zsh" | "fish" | "toml" | "yaml" => &["# "],
878 "lua" => &["-- "],
879 "sql" => &["-- "],
880 "vim" | "viml" => &["\" "],
881 _ => &[],
882 }
883}
884
885pub(crate) fn detect_comment_on_line(lang: &str, line: &str) -> Option<(String, &'static str)> {
891 let indent_end = line
892 .char_indices()
893 .find(|(_, c)| *c != ' ' && *c != '\t')
894 .map(|(i, _)| i)
895 .unwrap_or(line.len());
896 let indent = line[..indent_end].to_string();
897 let rest = &line[indent_end..];
898 for &prefix in comment_prefixes_for_lang(lang) {
899 if rest.starts_with(prefix) {
900 return Some((indent, prefix));
901 }
902 let bare = prefix.trim_end_matches(' ');
905 if rest == bare || rest.starts_with(&format!("{bare} ")) {
906 return Some((indent, prefix));
907 }
908 }
909 None
910}
911
912pub(crate) fn continue_comment(
919 buffer: &hjkl_buffer::Buffer,
920 settings: &crate::editor::Settings,
921 row: usize,
922) -> Option<String> {
923 if settings.filetype.is_empty() {
924 return None;
925 }
926 let line = crate::buf_helpers::buf_line(buffer, row)?;
927 let (indent, prefix) = detect_comment_on_line(&settings.filetype, &line)?;
928 Some(format!("{indent}{prefix}"))
929}
930
931fn try_dedent_close_bracket<H: crate::types::Host>(
941 ed: &mut Editor<hjkl_buffer::Buffer, H>,
942 cursor: hjkl_buffer::Position,
943 ch: char,
944) -> bool {
945 use hjkl_buffer::{Edit, MotionKind, Position};
946
947 if !ed.settings.smartindent {
948 return false;
949 }
950 if !matches!(ch, '}' | ')' | ']') {
951 return false;
952 }
953
954 let line = match buf_line(&ed.buffer, cursor.row) {
955 Some(l) => l.to_string(),
956 None => return false,
957 };
958
959 let before: String = line.chars().take(cursor.col).collect();
961 if !before.chars().all(|c| c == ' ' || c == '\t') {
962 return false;
963 }
964 if before.is_empty() {
965 return false;
967 }
968
969 let unit_len: usize = if ed.settings.expandtab {
971 if ed.settings.softtabstop > 0 {
972 ed.settings.softtabstop
973 } else {
974 ed.settings.shiftwidth
975 }
976 } else {
977 1
979 };
980
981 let strip_len = if ed.settings.expandtab {
983 let spaces = before.chars().filter(|c| *c == ' ').count();
985 if spaces < unit_len {
986 return false;
987 }
988 unit_len
989 } else {
990 if !before.starts_with('\t') {
992 return false;
993 }
994 1
995 };
996
997 ed.mutate_edit(Edit::DeleteRange {
999 start: Position::new(cursor.row, 0),
1000 end: Position::new(cursor.row, strip_len),
1001 kind: MotionKind::Char,
1002 });
1003 let new_col = cursor.col.saturating_sub(strip_len);
1008 ed.mutate_edit(Edit::InsertChar {
1009 at: Position::new(cursor.row, new_col),
1010 ch,
1011 });
1012 true
1013}
1014
1015fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1016 let Some(session) = ed.vim.insert_session.take() else {
1017 return;
1018 };
1019 let after_rope = crate::types::Query::rope(&ed.buffer);
1020 let before_n = session.before_rope.len_lines();
1024 let after_n = after_rope.len_lines();
1025 let after_end = session.row_max.min(after_n.saturating_sub(1));
1026 let before_end = session.row_max.min(before_n.saturating_sub(1));
1027 let before = if before_end >= session.row_min && session.row_min < before_n {
1028 rope_row_range_str(&session.before_rope, session.row_min, before_end)
1029 } else {
1030 String::new()
1031 };
1032 let after = if after_end >= session.row_min && session.row_min < after_n {
1033 rope_row_range_str(&after_rope, session.row_min, after_end)
1034 } else {
1035 String::new()
1036 };
1037 let inserted = extract_inserted(&before, &after);
1038 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1039 use hjkl_buffer::{Edit, Position};
1040 for _ in 0..session.count - 1 {
1041 let (row, col) = ed.cursor();
1042 ed.mutate_edit(Edit::InsertStr {
1043 at: Position::new(row, col),
1044 text: inserted.clone(),
1045 });
1046 }
1047 }
1048 fn replicate_block_text<H: crate::types::Host>(
1052 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1053 inserted: &str,
1054 top: usize,
1055 bot: usize,
1056 col: usize,
1057 ) {
1058 use hjkl_buffer::{Edit, Position};
1059 for r in (top + 1)..=bot {
1060 let line_len = buf_line_chars(&ed.buffer, r);
1061 if col > line_len {
1062 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1063 ed.mutate_edit(Edit::InsertStr {
1064 at: Position::new(r, line_len),
1065 text: pad,
1066 });
1067 }
1068 ed.mutate_edit(Edit::InsertStr {
1069 at: Position::new(r, col),
1070 text: inserted.to_string(),
1071 });
1072 }
1073 }
1074
1075 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1076 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1079 replicate_block_text(ed, &inserted, top, bot, col);
1080 buf_set_cursor_rc(&mut ed.buffer, top, col);
1081 ed.push_buffer_cursor_to_textarea();
1082 }
1083 return;
1084 }
1085 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1086 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1090 replicate_block_text(ed, &inserted, top, bot, col);
1091 let ins_chars = inserted.chars().count();
1092 let line_len = buf_line_chars(&ed.buffer, top);
1093 let target_col = (col + ins_chars).min(line_len);
1094 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1095 ed.push_buffer_cursor_to_textarea();
1096 }
1097 return;
1098 }
1099 if ed.vim.replaying {
1100 return;
1101 }
1102 match session.reason {
1103 InsertReason::Enter(entry) => {
1104 ed.vim.last_change = Some(LastChange::InsertAt {
1105 entry,
1106 inserted,
1107 count: session.count,
1108 });
1109 }
1110 InsertReason::Open { above } => {
1111 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1112 }
1113 InsertReason::AfterChange => {
1114 if let Some(
1115 LastChange::OpMotion { inserted: ins, .. }
1116 | LastChange::OpTextObj { inserted: ins, .. }
1117 | LastChange::LineOp { inserted: ins, .. },
1118 ) = ed.vim.last_change.as_mut()
1119 {
1120 *ins = Some(inserted);
1121 }
1122 if let Some(start) = ed.vim.change_mark_start.take() {
1128 let end = ed.cursor();
1129 ed.set_mark('[', start);
1130 ed.set_mark(']', end);
1131 }
1132 }
1133 InsertReason::DeleteToEol => {
1134 ed.vim.last_change = Some(LastChange::DeleteToEol {
1135 inserted: Some(inserted),
1136 });
1137 }
1138 InsertReason::ReplayOnly => {}
1139 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1140 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1141 InsertReason::Replace => {
1142 ed.vim.last_change = Some(LastChange::DeleteToEol {
1147 inserted: Some(inserted),
1148 });
1149 }
1150 }
1151}
1152
1153pub(crate) fn begin_insert<H: crate::types::Host>(
1154 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1155 count: usize,
1156 reason: InsertReason,
1157) {
1158 let record = !matches!(reason, InsertReason::ReplayOnly);
1159 if record {
1160 ed.push_undo();
1161 }
1162 let reason = if ed.vim.replaying {
1163 InsertReason::ReplayOnly
1164 } else {
1165 reason
1166 };
1167 let (row, _) = ed.cursor();
1168 ed.vim.insert_session = Some(InsertSession {
1169 count,
1170 row_min: row,
1171 row_max: row,
1172 before_rope: crate::types::Query::rope(&ed.buffer),
1173 reason,
1174 });
1175 ed.vim.mode = Mode::Insert;
1176 ed.vim.current_mode = crate::VimMode::Insert;
1178}
1179
1180pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1195 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1196) {
1197 if !ed.settings.undo_break_on_motion {
1198 return;
1199 }
1200 if ed.vim.replaying {
1201 return;
1202 }
1203 if ed.vim.insert_session.is_none() {
1204 return;
1205 }
1206 ed.push_undo();
1207 let before_rope = crate::types::Query::rope(&ed.buffer);
1208 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1209 if let Some(ref mut session) = ed.vim.insert_session {
1210 session.before_rope = before_rope;
1211 session.row_min = row;
1212 session.row_max = row;
1213 }
1214}
1215
1216fn autopair_close_for(
1248 ch: char,
1249 filetype: &str,
1250 prev_char: Option<char>,
1251 prev2_char: Option<char>,
1252) -> Option<char> {
1253 let is_triple_quote_third =
1259 matches!(ch, '"' | '`' | '\'') && prev_char == Some(ch) && prev2_char == Some(ch);
1260
1261 match ch {
1262 '(' => Some(')'),
1263 '[' => Some(']'),
1264 '{' => Some('}'),
1265 '"' => {
1266 if is_triple_quote_third {
1267 None
1268 } else {
1269 Some('"')
1270 }
1271 }
1272 '`' => {
1273 if is_triple_quote_third {
1274 None
1275 } else {
1276 Some('`')
1277 }
1278 }
1279 '<' => {
1280 if is_html_filetype(filetype) {
1281 Some('>')
1282 } else {
1283 None
1284 }
1285 }
1286 '\'' => {
1287 if is_triple_quote_third {
1288 return None;
1289 }
1290 if prev_char.map(|c| c.is_ascii_alphabetic()).unwrap_or(false) {
1293 None
1294 } else {
1295 Some('\'')
1296 }
1297 }
1298 _ => None,
1299 }
1300}
1301
1302fn detect_code_fence_opener(line: &str, cursor_col: usize) -> Option<String> {
1317 if cursor_col != line.chars().count() {
1318 return None;
1319 }
1320 let trimmed = line.trim_start();
1321 let backtick_run = trimmed.chars().take_while(|c| *c == '`').count();
1322 if backtick_run < 3 {
1323 return None;
1324 }
1325 let rest = &trimmed[backtick_run..];
1326 if rest.is_empty() {
1327 return None;
1328 }
1329 let all_lang_chars = rest
1330 .chars()
1331 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '+' || c == '-');
1332 if !all_lang_chars {
1333 return None;
1334 }
1335 Some("`".repeat(backtick_run))
1336}
1337
1338fn is_html_filetype(ft: &str) -> bool {
1340 matches!(
1341 ft,
1342 "html" | "xml" | "svg" | "jsx" | "tsx" | "vue" | "svelte"
1343 )
1344}
1345
1346#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1362enum TagKind {
1363 Open,
1364 Close,
1365}
1366
1367#[derive(Debug, Clone, PartialEq, Eq)]
1369struct TagSpan {
1370 kind: TagKind,
1371 name: String,
1372 row: usize,
1374 name_start_col: usize,
1376 name_end_col: usize,
1377}
1378
1379fn detect_tag_at_cursor(line: &str, row: usize, col: usize) -> Option<TagSpan> {
1383 let chars: Vec<char> = line.chars().collect();
1384 let mut lt = None;
1386 let mut i = col.min(chars.len());
1387 while i > 0 {
1388 i -= 1;
1389 let c = chars[i];
1390 if c == '<' {
1391 lt = Some(i);
1392 break;
1393 }
1394 if c == '>' {
1396 return None;
1397 }
1398 }
1399 let lt = lt?;
1400 let (kind, name_start) = if chars.get(lt + 1) == Some(&'/') {
1402 (TagKind::Close, lt + 2)
1403 } else {
1404 (TagKind::Open, lt + 1)
1405 };
1406 let first = chars.get(name_start)?;
1408 if !first.is_ascii_alphabetic() {
1409 return None;
1410 }
1411 let mut name_end = name_start;
1413 while name_end < chars.len()
1414 && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-')
1415 {
1416 name_end += 1;
1417 }
1418 if col < name_start || col > name_end {
1422 return None;
1423 }
1424 let name: String = chars[name_start..name_end].iter().collect();
1425 Some(TagSpan {
1426 kind,
1427 name,
1428 row,
1429 name_start_col: name_start,
1430 name_end_col: name_end,
1431 })
1432}
1433
1434fn find_matching_tag(buffer: &hjkl_buffer::Buffer, anchor: &TagSpan) -> Option<TagSpan> {
1448 let row_count = buffer.row_count();
1449 let scan_forward = anchor.kind == TagKind::Open;
1450 let row_iter: Box<dyn Iterator<Item = usize>> = if scan_forward {
1451 Box::new(anchor.row..row_count)
1452 } else {
1453 Box::new((0..=anchor.row).rev())
1454 };
1455 let push_kind = if scan_forward {
1456 TagKind::Open
1457 } else {
1458 TagKind::Close
1459 };
1460 let mut depth: usize = 1;
1461
1462 for r in row_iter {
1463 let line = buf_line(buffer, r)?;
1464 let chars: Vec<char> = line.chars().collect();
1465 let tags = scan_line_tags(&chars, r);
1466 let tags_iter: Box<dyn Iterator<Item = TagSpan>> = if scan_forward {
1467 Box::new(tags.into_iter())
1468 } else {
1469 Box::new(tags.into_iter().rev())
1470 };
1471 for tag in tags_iter {
1472 if r == anchor.row
1474 && tag.name_start_col == anchor.name_start_col
1475 && tag.kind == anchor.kind
1476 {
1477 continue;
1478 }
1479 if r == anchor.row {
1483 if scan_forward && tag.name_start_col < anchor.name_start_col {
1484 continue;
1485 }
1486 if !scan_forward && tag.name_start_col > anchor.name_start_col {
1487 continue;
1488 }
1489 }
1490 if tag.kind == push_kind {
1491 depth += 1;
1492 } else {
1493 depth -= 1;
1494 if depth == 0 {
1495 return Some(tag);
1496 }
1497 }
1498 }
1499 }
1500 None
1501}
1502
1503fn scan_line_tags(chars: &[char], row: usize) -> Vec<TagSpan> {
1507 let mut out = Vec::new();
1508 let n = chars.len();
1509 let mut i = 0;
1510 while i < n {
1511 if chars[i] != '<' {
1512 i += 1;
1513 continue;
1514 }
1515 if chars[i..].starts_with(&['<', '!', '-', '-']) {
1517 let mut j = i + 4;
1518 while j + 2 < n && !(chars[j] == '-' && chars[j + 1] == '-' && chars[j + 2] == '>') {
1519 j += 1;
1520 }
1521 i = (j + 3).min(n);
1522 continue;
1523 }
1524 let (kind, name_start) = if chars.get(i + 1) == Some(&'/') {
1525 (TagKind::Close, i + 2)
1526 } else {
1527 (TagKind::Open, i + 1)
1528 };
1529 if chars
1531 .get(name_start)
1532 .is_none_or(|c| !c.is_ascii_alphabetic())
1533 {
1534 i += 1;
1535 continue;
1536 }
1537 let mut name_end = name_start;
1538 while name_end < n && (chars[name_end].is_ascii_alphanumeric() || chars[name_end] == '-') {
1539 name_end += 1;
1540 }
1541 let mut k = name_end;
1543 let mut self_closing = false;
1544 while k < n {
1545 if chars[k] == '>' {
1546 if k > name_end && chars[k - 1] == '/' {
1547 self_closing = true;
1548 }
1549 break;
1550 }
1551 k += 1;
1552 }
1553 if k >= n {
1554 break;
1556 }
1557 let name: String = chars[name_start..name_end].iter().collect();
1558 if !(self_closing || kind == TagKind::Open && is_void_element(&name)) {
1560 out.push(TagSpan {
1561 kind,
1562 name,
1563 row,
1564 name_start_col: name_start,
1565 name_end_col: name_end,
1566 });
1567 }
1568 i = k + 1;
1569 }
1570 out
1571}
1572
1573pub(crate) fn sync_paired_tag_on_exit<H: crate::types::Host>(
1578 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1579) {
1580 if !is_html_filetype(&ed.settings.filetype) {
1581 return;
1582 }
1583 let (row, col) = ed.cursor();
1584 let line = match buf_line(&ed.buffer, row) {
1585 Some(l) => l,
1586 None => return,
1587 };
1588 let anchor = match detect_tag_at_cursor(&line, row, col) {
1589 Some(t) => t,
1590 None => return,
1591 };
1592 let partner = match find_matching_tag(&ed.buffer, &anchor) {
1593 Some(t) => t,
1594 None => return,
1595 };
1596 if partner.name == anchor.name {
1597 return;
1598 }
1599 use hjkl_buffer::{Edit, MotionKind, Position};
1601 let start = Position::new(partner.row, partner.name_start_col);
1602 let end = Position::new(partner.row, partner.name_end_col);
1603 ed.mutate_edit(Edit::DeleteRange {
1604 start,
1605 end,
1606 kind: MotionKind::Char,
1607 });
1608 ed.mutate_edit(Edit::InsertStr {
1609 at: start,
1610 text: anchor.name.clone(),
1611 });
1612 buf_set_cursor_rc(&mut ed.buffer, row, col);
1615 ed.push_buffer_cursor_to_textarea();
1616}
1617
1618fn is_void_element(tag: &str) -> bool {
1620 matches!(
1621 tag.to_ascii_lowercase().as_str(),
1622 "area"
1623 | "base"
1624 | "br"
1625 | "col"
1626 | "embed"
1627 | "hr"
1628 | "img"
1629 | "input"
1630 | "link"
1631 | "meta"
1632 | "param"
1633 | "source"
1634 | "track"
1635 | "wbr"
1636 )
1637}
1638
1639fn scan_tag_opener(line: &str, col: usize) -> Option<String> {
1649 let before = if col > 0 { &line[..col] } else { return None };
1652
1653 let lt_pos = before.rfind('<')?;
1655 let inner = &before[lt_pos + 1..]; if inner.starts_with('!') {
1659 return None;
1660 }
1661 if inner.trim_end().ends_with('/') {
1663 return None;
1664 }
1665
1666 let tag: String = inner
1668 .chars()
1669 .take_while(|c| c.is_ascii_alphanumeric() || *c == '-')
1670 .collect();
1671 if tag.is_empty() {
1672 return None;
1673 }
1674 if !tag
1676 .chars()
1677 .next()
1678 .map(|c| c.is_ascii_alphabetic())
1679 .unwrap_or(false)
1680 {
1681 return None;
1682 }
1683 if is_void_element(&tag) {
1684 return None;
1685 }
1686 Some(tag)
1687}
1688
1689pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1694 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1695 ch: char,
1696) -> bool {
1697 use hjkl_buffer::{Edit, MotionKind, Position};
1698 ed.sync_buffer_content_from_textarea();
1699 let cursor = buf_cursor_pos(&ed.buffer);
1700 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1701 let in_replace = matches!(
1702 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1703 Some(InsertReason::Replace)
1704 );
1705
1706 if !in_replace
1715 && !ed.vim.pending_closes.is_empty()
1716 && let Some(&(pr, _pc, pch)) = ed.vim.pending_closes.last()
1717 && ch == pch
1718 && cursor.row == pr
1719 {
1720 let char_at_cursor =
1721 buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col));
1722 if char_at_cursor == Some(ch) {
1723 ed.vim.pending_closes.pop();
1724 let filetype = ed.settings.filetype.clone();
1726 let autoclose_tag = ed.settings.autoclose_tag;
1727 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1728 let new_col = cursor.col + 1;
1730 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1731 if let Some(line) = buf_line(&ed.buffer, cursor.row)
1733 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
1734 {
1735 let close_tag = format!("</{tag}>");
1736 let insert_pos = Position::new(cursor.row, new_col);
1737 ed.mutate_edit(Edit::InsertStr {
1738 at: insert_pos,
1739 text: close_tag,
1740 });
1741 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1743 }
1744 } else {
1745 buf_set_cursor_rc(&mut ed.buffer, cursor.row, cursor.col + 1);
1746 }
1747 ed.push_buffer_cursor_to_textarea();
1748 return true;
1749 }
1750 }
1751
1752 if in_replace && cursor.col < line_chars {
1753 ed.vim.pending_closes.clear();
1755 ed.mutate_edit(Edit::DeleteRange {
1756 start: cursor,
1757 end: Position::new(cursor.row, cursor.col + 1),
1758 kind: MotionKind::Char,
1759 });
1760 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1761 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1762 let autopair = ed.settings.autopair;
1764 let filetype = ed.settings.filetype.clone();
1765 let autoclose_tag = ed.settings.autoclose_tag;
1766
1767 let (prev_char, prev2_char) = {
1768 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1769 let chars: Vec<char> = line.chars().collect();
1770 let p1 = if cursor.col > 0 {
1771 chars.get(cursor.col - 1).copied()
1772 } else {
1773 None
1774 };
1775 let p2 = if cursor.col > 1 {
1776 chars.get(cursor.col - 2).copied()
1777 } else {
1778 None
1779 };
1780 (p1, p2)
1781 };
1782
1783 if autopair {
1784 if let Some(close) = autopair_close_for(ch, &filetype, prev_char, prev2_char) {
1785 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1787 let after = Position::new(cursor.row, cursor.col + 1);
1790 ed.mutate_edit(Edit::InsertChar {
1791 at: after,
1792 ch: close,
1793 });
1794 let between_col = cursor.col + 1;
1797 buf_set_cursor_rc(&mut ed.buffer, cursor.row, between_col);
1798 ed.vim.pending_closes.push((cursor.row, between_col, close));
1803 ed.push_buffer_cursor_to_textarea();
1804 return true;
1805 }
1806
1807 if ch == '>' && autoclose_tag && is_html_filetype(&filetype) {
1811 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1812 let new_col = cursor.col + 1;
1813 if let Some(line) = buf_line(&ed.buffer, cursor.row)
1816 && let Some(tag) = scan_tag_opener(&line, new_col.saturating_sub(1))
1817 {
1818 let close_tag = format!("</{tag}>");
1819 let insert_pos = Position::new(cursor.row, new_col);
1820 ed.mutate_edit(Edit::InsertStr {
1821 at: insert_pos,
1822 text: close_tag,
1823 });
1824 buf_set_cursor_rc(&mut ed.buffer, cursor.row, new_col);
1826 }
1827 ed.push_buffer_cursor_to_textarea();
1828 return true;
1829 }
1830 }
1831
1832 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1837 }
1838 ed.push_buffer_cursor_to_textarea();
1839 true
1840}
1841
1842pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1848 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1849) -> bool {
1850 use hjkl_buffer::Edit;
1851 ed.sync_buffer_content_from_textarea();
1852 let cursor = buf_cursor_pos(&ed.buffer);
1853 let prev_line = buf_line(&ed.buffer, cursor.row)
1854 .unwrap_or_default()
1855 .to_string();
1856
1857 if ed.settings.autopair && !ed.vim.pending_closes.is_empty() {
1861 let prev_char = if cursor.col > 0 {
1864 prev_line.chars().nth(cursor.col - 1)
1865 } else {
1866 None
1867 };
1868 let next_char = prev_line.chars().nth(cursor.col);
1869 let is_open_pair = matches!(
1870 (prev_char, next_char),
1871 (Some('{'), Some('}')) | (Some('('), Some(')')) | (Some('['), Some(']'))
1872 );
1873 if is_open_pair {
1874 ed.vim.pending_closes.clear();
1877 let base_indent: String = prev_line
1879 .chars()
1880 .take_while(|c| *c == ' ' || *c == '\t')
1881 .collect();
1882 let inner_indent = if ed.settings.expandtab {
1883 let unit = if ed.settings.softtabstop > 0 {
1884 ed.settings.softtabstop
1885 } else {
1886 ed.settings.shiftwidth
1887 };
1888 format!("{base_indent}{}", " ".repeat(unit))
1889 } else {
1890 format!("{base_indent}\t")
1891 };
1892 let text = format!("\n{inner_indent}\n{base_indent}");
1895 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1896 let new_row = cursor.row + 1;
1898 let new_col = inner_indent.len();
1899 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1900 ed.push_buffer_cursor_to_textarea();
1901 return true;
1902 }
1903 }
1904
1905 if ed.settings.autopair
1914 && let Some(fence) = detect_code_fence_opener(&prev_line, cursor.col)
1915 {
1916 ed.vim.pending_closes.clear();
1917 let base_indent: String = prev_line
1918 .chars()
1919 .take_while(|c| *c == ' ' || *c == '\t')
1920 .collect();
1921 let text = format!("\n{base_indent}\n{base_indent}{fence}");
1922 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1923 let new_row = cursor.row + 1;
1924 let new_col = base_indent.chars().count();
1925 buf_set_cursor_rc(&mut ed.buffer, new_row, new_col);
1926 ed.push_buffer_cursor_to_textarea();
1927 return true;
1928 }
1929
1930 let comment_cont = if ed.settings.formatoptions.contains('r') {
1932 continue_comment(&ed.buffer, &ed.settings, cursor.row)
1933 } else {
1934 None
1935 };
1936
1937 ed.vim.pending_closes.clear();
1939
1940 let text = if let Some(cont) = comment_cont {
1941 format!("\n{cont}")
1944 } else {
1945 let indent = compute_enter_indent(&ed.settings, &prev_line);
1946 format!("\n{indent}")
1947 };
1948 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1949 ed.push_buffer_cursor_to_textarea();
1950 true
1951}
1952
1953pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1956 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1957) -> bool {
1958 use hjkl_buffer::Edit;
1959 ed.sync_buffer_content_from_textarea();
1960 let cursor = buf_cursor_pos(&ed.buffer);
1961 if ed.settings.expandtab {
1962 let sts = ed.settings.softtabstop;
1963 let n = if sts > 0 {
1964 sts - (cursor.col % sts)
1965 } else {
1966 ed.settings.tabstop.max(1)
1967 };
1968 ed.mutate_edit(Edit::InsertStr {
1969 at: cursor,
1970 text: " ".repeat(n),
1971 });
1972 } else {
1973 ed.mutate_edit(Edit::InsertChar {
1974 at: cursor,
1975 ch: '\t',
1976 });
1977 }
1978 ed.push_buffer_cursor_to_textarea();
1979 true
1980}
1981
1982pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1993 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1994) -> bool {
1995 use hjkl_buffer::{Edit, MotionKind, Position};
1996 ed.sync_buffer_content_from_textarea();
1997 let cursor = buf_cursor_pos(&ed.buffer);
1998
1999 if cursor.col > 0 {
2002 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2003 if let Some((indent, prefix)) = detect_comment_on_line(&ed.settings.filetype, &line) {
2004 let full_prefix = format!("{indent}{prefix}");
2005 let line_trimmed = line.trim_end_matches(' ');
2008 let prefix_trimmed = full_prefix.trim_end_matches(' ');
2009 if line_trimmed == prefix_trimmed && cursor.col == full_prefix.chars().count() {
2010 ed.mutate_edit(Edit::DeleteRange {
2012 start: Position::new(cursor.row, 0),
2013 end: cursor,
2014 kind: MotionKind::Char,
2015 });
2016 ed.push_buffer_cursor_to_textarea();
2017 return true;
2018 }
2019 }
2020 }
2021
2022 let sts = ed.settings.softtabstop;
2023 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
2024 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
2025 let chars: Vec<char> = line.chars().collect();
2026 let run_start = cursor.col - sts;
2027 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
2028 ed.mutate_edit(Edit::DeleteRange {
2029 start: Position::new(cursor.row, run_start),
2030 end: cursor,
2031 kind: MotionKind::Char,
2032 });
2033 ed.push_buffer_cursor_to_textarea();
2034 return true;
2035 }
2036 }
2037 let result = if cursor.col > 0 {
2038 ed.mutate_edit(Edit::DeleteRange {
2039 start: Position::new(cursor.row, cursor.col - 1),
2040 end: cursor,
2041 kind: MotionKind::Char,
2042 });
2043 true
2044 } else if cursor.row > 0 {
2045 let prev_row = cursor.row - 1;
2046 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2047 ed.mutate_edit(Edit::JoinLines {
2048 row: prev_row,
2049 count: 1,
2050 with_space: false,
2051 });
2052 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2053 true
2054 } else {
2055 false
2056 };
2057 ed.push_buffer_cursor_to_textarea();
2058 result
2059}
2060
2061pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
2064 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2065) -> bool {
2066 use hjkl_buffer::{Edit, MotionKind, Position};
2067 ed.sync_buffer_content_from_textarea();
2068 let cursor = buf_cursor_pos(&ed.buffer);
2069 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2070 let result = if cursor.col < line_chars {
2071 ed.mutate_edit(Edit::DeleteRange {
2072 start: cursor,
2073 end: Position::new(cursor.row, cursor.col + 1),
2074 kind: MotionKind::Char,
2075 });
2076 buf_set_cursor_pos(&mut ed.buffer, cursor);
2077 true
2078 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
2079 ed.mutate_edit(Edit::JoinLines {
2080 row: cursor.row,
2081 count: 1,
2082 with_space: false,
2083 });
2084 buf_set_cursor_pos(&mut ed.buffer, cursor);
2085 true
2086 } else {
2087 false
2088 };
2089 ed.push_buffer_cursor_to_textarea();
2090 result
2091}
2092
2093#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2095pub enum InsertDir {
2096 Left,
2097 Right,
2098 Up,
2099 Down,
2100}
2101
2102pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
2106 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2107 dir: InsertDir,
2108) -> bool {
2109 ed.sync_buffer_content_from_textarea();
2110 ed.vim.pending_closes.clear();
2111 match dir {
2112 InsertDir::Left => {
2113 crate::motions::move_left(&mut ed.buffer, 1);
2114 }
2115 InsertDir::Right => {
2116 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2117 }
2118 InsertDir::Up => {
2119 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2120 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2121 }
2122 InsertDir::Down => {
2123 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2124 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2125 }
2126 }
2127 break_undo_group_in_insert(ed);
2128 ed.push_buffer_cursor_to_textarea();
2129 false
2130}
2131
2132pub(crate) fn insert_home_bridge<H: crate::types::Host>(
2135 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2136) -> bool {
2137 ed.sync_buffer_content_from_textarea();
2138 ed.vim.pending_closes.clear();
2139 crate::motions::move_line_start(&mut ed.buffer);
2140 break_undo_group_in_insert(ed);
2141 ed.push_buffer_cursor_to_textarea();
2142 false
2143}
2144
2145pub(crate) fn insert_end_bridge<H: crate::types::Host>(
2148 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2149) -> bool {
2150 ed.sync_buffer_content_from_textarea();
2151 ed.vim.pending_closes.clear();
2152 crate::motions::move_line_end(&mut ed.buffer);
2153 break_undo_group_in_insert(ed);
2154 ed.push_buffer_cursor_to_textarea();
2155 false
2156}
2157
2158pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
2161 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2162 viewport_h: u16,
2163) -> bool {
2164 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2165 scroll_cursor_rows(ed, -rows);
2166 false
2167}
2168
2169pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
2172 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2173 viewport_h: u16,
2174) -> bool {
2175 let rows = viewport_h.saturating_sub(2).max(1) as isize;
2176 scroll_cursor_rows(ed, rows);
2177 false
2178}
2179
2180pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
2184 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2185) -> bool {
2186 use hjkl_buffer::{Edit, MotionKind};
2187 ed.sync_buffer_content_from_textarea();
2188 let cursor = buf_cursor_pos(&ed.buffer);
2189 if cursor.row == 0 && cursor.col == 0 {
2190 return true;
2191 }
2192 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
2193 let word_start = buf_cursor_pos(&ed.buffer);
2194 if word_start == cursor {
2195 return true;
2196 }
2197 buf_set_cursor_pos(&mut ed.buffer, cursor);
2198 ed.mutate_edit(Edit::DeleteRange {
2199 start: word_start,
2200 end: cursor,
2201 kind: MotionKind::Char,
2202 });
2203 ed.push_buffer_cursor_to_textarea();
2204 true
2205}
2206
2207pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
2210 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2211) -> bool {
2212 use hjkl_buffer::{Edit, MotionKind, Position};
2213 ed.sync_buffer_content_from_textarea();
2214 let cursor = buf_cursor_pos(&ed.buffer);
2215 if cursor.col > 0 {
2216 ed.mutate_edit(Edit::DeleteRange {
2217 start: Position::new(cursor.row, 0),
2218 end: cursor,
2219 kind: MotionKind::Char,
2220 });
2221 ed.push_buffer_cursor_to_textarea();
2222 }
2223 true
2224}
2225
2226pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
2230 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2231) -> bool {
2232 use hjkl_buffer::{Edit, MotionKind, Position};
2233 ed.sync_buffer_content_from_textarea();
2234 let cursor = buf_cursor_pos(&ed.buffer);
2235 if cursor.col > 0 {
2236 ed.mutate_edit(Edit::DeleteRange {
2237 start: Position::new(cursor.row, cursor.col - 1),
2238 end: cursor,
2239 kind: MotionKind::Char,
2240 });
2241 } else if cursor.row > 0 {
2242 let prev_row = cursor.row - 1;
2243 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
2244 ed.mutate_edit(Edit::JoinLines {
2245 row: prev_row,
2246 count: 1,
2247 with_space: false,
2248 });
2249 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
2250 }
2251 ed.push_buffer_cursor_to_textarea();
2252 true
2253}
2254
2255pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
2258 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2259) -> bool {
2260 let (row, col) = ed.cursor();
2261 let sw = ed.settings().shiftwidth;
2262 indent_rows(ed, row, row, 1);
2263 ed.jump_cursor(row, col + sw);
2264 true
2265}
2266
2267pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
2270 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2271) -> bool {
2272 let (row, col) = ed.cursor();
2273 let before_len = buf_line_bytes(&ed.buffer, row);
2274 outdent_rows(ed, row, row, 1);
2275 let after_len = buf_line_bytes(&ed.buffer, row);
2276 let stripped = before_len.saturating_sub(after_len);
2277 let new_col = col.saturating_sub(stripped);
2278 ed.jump_cursor(row, new_col);
2279 true
2280}
2281
2282pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
2286 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2287) -> bool {
2288 ed.vim.one_shot_normal = true;
2289 ed.vim.mode = Mode::Normal;
2290 ed.vim.current_mode = crate::VimMode::Normal;
2292 false
2293}
2294
2295pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
2299 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2300) -> bool {
2301 ed.vim.insert_pending_register = true;
2302 false
2303}
2304
2305pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
2309 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2310 reg: char,
2311) -> bool {
2312 insert_register_text(ed, reg);
2313 true
2316}
2317
2318pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
2324 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2325) -> bool {
2326 ed.vim.pending_closes.clear();
2327 finish_insert_session(ed);
2328 sync_paired_tag_on_exit(ed);
2332 ed.vim.mode = Mode::Normal;
2333 ed.vim.current_mode = crate::VimMode::Normal;
2335 let col = ed.cursor().1;
2336 ed.vim.last_insert_pos = Some(ed.cursor());
2337 if col > 0 {
2338 crate::motions::move_left(&mut ed.buffer, 1);
2339 ed.push_buffer_cursor_to_textarea();
2340 }
2341 ed.sticky_col = Some(ed.cursor().1);
2342 true
2343}
2344
2345#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2350pub enum ScrollDir {
2351 Down,
2353 Up,
2355}
2356
2357pub(crate) fn enter_insert_i_bridge<H: crate::types::Host>(
2362 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2363 count: usize,
2364) {
2365 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2366}
2367
2368pub(crate) fn enter_insert_shift_i_bridge<H: crate::types::Host>(
2370 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2371 count: usize,
2372) {
2373 move_first_non_whitespace(ed);
2374 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2375}
2376
2377pub(crate) fn enter_insert_a_bridge<H: crate::types::Host>(
2379 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2380 count: usize,
2381) {
2382 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2383 ed.push_buffer_cursor_to_textarea();
2384 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2385}
2386
2387pub(crate) fn enter_insert_shift_a_bridge<H: crate::types::Host>(
2389 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2390 count: usize,
2391) {
2392 crate::motions::move_line_end(&mut ed.buffer);
2393 crate::motions::move_right_to_end(&mut ed.buffer, 1);
2394 ed.push_buffer_cursor_to_textarea();
2395 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2396}
2397
2398pub(crate) fn open_line_below_bridge<H: crate::types::Host>(
2402 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2403 count: usize,
2404) {
2405 use hjkl_buffer::{Edit, Position};
2406 ed.push_undo();
2407 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2408 ed.sync_buffer_content_from_textarea();
2409 let row = buf_cursor_pos(&ed.buffer).row;
2410 let line_chars = buf_line_chars(&ed.buffer, row);
2411 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
2412
2413 let comment_cont = if ed.settings.formatoptions.contains('o') {
2415 continue_comment(&ed.buffer, &ed.settings, row)
2416 } else {
2417 None
2418 };
2419
2420 let suffix = if let Some(cont) = comment_cont {
2421 format!("\n{cont}")
2422 } else {
2423 let indent = compute_enter_indent(&ed.settings, &prev_line);
2424 format!("\n{indent}")
2425 };
2426 ed.mutate_edit(Edit::InsertStr {
2427 at: Position::new(row, line_chars),
2428 text: suffix,
2429 });
2430 ed.push_buffer_cursor_to_textarea();
2431}
2432
2433pub(crate) fn open_line_above_bridge<H: crate::types::Host>(
2437 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2438 count: usize,
2439) {
2440 use hjkl_buffer::{Edit, Position};
2441 ed.push_undo();
2442 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2443 ed.sync_buffer_content_from_textarea();
2444 let row = buf_cursor_pos(&ed.buffer).row;
2445
2446 let comment_cont = if ed.settings.formatoptions.contains('o') {
2448 continue_comment(&ed.buffer, &ed.settings, row)
2449 } else {
2450 None
2451 };
2452
2453 let (insert_text, new_line_content) = if let Some(cont) = comment_cont {
2456 let content = cont.clone();
2457 (format!("{cont}\n"), content)
2458 } else {
2459 let indent = if row > 0 {
2460 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
2461 compute_enter_indent(&ed.settings, &above)
2462 } else {
2463 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
2464 cur.chars()
2465 .take_while(|c| *c == ' ' || *c == '\t')
2466 .collect::<String>()
2467 };
2468 let content = indent.clone();
2469 (format!("{indent}\n"), content)
2470 };
2471 ed.mutate_edit(Edit::InsertStr {
2472 at: Position::new(row, 0),
2473 text: insert_text,
2474 });
2475 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2476 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
2477 let new_row = buf_cursor_pos(&ed.buffer).row;
2478 buf_set_cursor_rc(&mut ed.buffer, new_row, new_line_content.chars().count());
2479 ed.push_buffer_cursor_to_textarea();
2480}
2481
2482pub(crate) fn enter_replace_mode_bridge<H: crate::types::Host>(
2484 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2485 count: usize,
2486) {
2487 begin_insert(ed, count.max(1), InsertReason::Replace);
2488}
2489
2490pub(crate) fn delete_char_forward_bridge<H: crate::types::Host>(
2495 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2496 count: usize,
2497) {
2498 do_char_delete(ed, true, count.max(1));
2499 if !ed.vim.replaying {
2500 ed.vim.last_change = Some(LastChange::CharDel {
2501 forward: true,
2502 count: count.max(1),
2503 });
2504 }
2505}
2506
2507pub(crate) fn delete_char_backward_bridge<H: crate::types::Host>(
2510 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2511 count: usize,
2512) {
2513 do_char_delete(ed, false, count.max(1));
2514 if !ed.vim.replaying {
2515 ed.vim.last_change = Some(LastChange::CharDel {
2516 forward: false,
2517 count: count.max(1),
2518 });
2519 }
2520}
2521
2522pub(crate) fn substitute_char_bridge<H: crate::types::Host>(
2525 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2526 count: usize,
2527) {
2528 use hjkl_buffer::{Edit, MotionKind, Position};
2529 ed.push_undo();
2530 ed.sync_buffer_content_from_textarea();
2531 for _ in 0..count.max(1) {
2532 let cursor = buf_cursor_pos(&ed.buffer);
2533 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
2534 if cursor.col >= line_chars {
2535 break;
2536 }
2537 ed.mutate_edit(Edit::DeleteRange {
2538 start: cursor,
2539 end: Position::new(cursor.row, cursor.col + 1),
2540 kind: MotionKind::Char,
2541 });
2542 }
2543 ed.push_buffer_cursor_to_textarea();
2544 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
2545 if !ed.vim.replaying {
2546 ed.vim.last_change = Some(LastChange::OpMotion {
2547 op: Operator::Change,
2548 motion: Motion::Right,
2549 count: count.max(1),
2550 inserted: None,
2551 });
2552 }
2553}
2554
2555pub(crate) fn substitute_line_bridge<H: crate::types::Host>(
2558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2559 count: usize,
2560) {
2561 execute_line_op(ed, Operator::Change, count.max(1));
2562 if !ed.vim.replaying {
2563 ed.vim.last_change = Some(LastChange::LineOp {
2564 op: Operator::Change,
2565 count: count.max(1),
2566 inserted: None,
2567 });
2568 }
2569}
2570
2571pub(crate) fn delete_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2574 ed.push_undo();
2575 delete_to_eol(ed);
2576 crate::motions::move_left(&mut ed.buffer, 1);
2577 ed.push_buffer_cursor_to_textarea();
2578 if !ed.vim.replaying {
2579 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2580 }
2581}
2582
2583pub(crate) fn change_to_eol_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2586 ed.push_undo();
2587 delete_to_eol(ed);
2588 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2589}
2590
2591pub(crate) fn yank_to_eol_bridge<H: crate::types::Host>(
2593 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2594 count: usize,
2595) {
2596 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2597}
2598
2599pub(crate) fn join_line_bridge<H: crate::types::Host>(
2602 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2603 count: usize,
2604) {
2605 for _ in 0..count.max(1) {
2606 ed.push_undo();
2607 join_line(ed);
2608 }
2609 if !ed.vim.replaying {
2610 ed.vim.last_change = Some(LastChange::JoinLine {
2611 count: count.max(1),
2612 });
2613 }
2614}
2615
2616pub(crate) fn toggle_case_at_cursor_bridge<H: crate::types::Host>(
2619 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2620 count: usize,
2621) {
2622 for _ in 0..count.max(1) {
2623 ed.push_undo();
2624 toggle_case_at_cursor(ed);
2625 }
2626 if !ed.vim.replaying {
2627 ed.vim.last_change = Some(LastChange::ToggleCase {
2628 count: count.max(1),
2629 });
2630 }
2631}
2632
2633pub(crate) fn paste_after_bridge<H: crate::types::Host>(
2637 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2638 count: usize,
2639) {
2640 do_paste(ed, false, count.max(1));
2641 if !ed.vim.replaying {
2642 ed.vim.last_change = Some(LastChange::Paste {
2643 before: false,
2644 count: count.max(1),
2645 });
2646 }
2647}
2648
2649pub(crate) fn paste_before_bridge<H: crate::types::Host>(
2653 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2654 count: usize,
2655) {
2656 do_paste(ed, true, count.max(1));
2657 if !ed.vim.replaying {
2658 ed.vim.last_change = Some(LastChange::Paste {
2659 before: true,
2660 count: count.max(1),
2661 });
2662 }
2663}
2664
2665pub(crate) fn jump_back_bridge<H: crate::types::Host>(
2670 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2671 count: usize,
2672) {
2673 for _ in 0..count.max(1) {
2674 jump_back(ed);
2675 }
2676}
2677
2678pub(crate) fn jump_forward_bridge<H: crate::types::Host>(
2681 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2682 count: usize,
2683) {
2684 for _ in 0..count.max(1) {
2685 jump_forward(ed);
2686 }
2687}
2688
2689pub(crate) fn scroll_full_page_bridge<H: crate::types::Host>(
2694 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2695 dir: ScrollDir,
2696 count: usize,
2697) {
2698 let rows = viewport_full_rows(ed, count) as isize;
2699 match dir {
2700 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2701 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2702 }
2703}
2704
2705pub(crate) fn scroll_half_page_bridge<H: crate::types::Host>(
2708 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2709 dir: ScrollDir,
2710 count: usize,
2711) {
2712 let rows = viewport_half_rows(ed, count) as isize;
2713 match dir {
2714 ScrollDir::Down => scroll_cursor_rows(ed, rows),
2715 ScrollDir::Up => scroll_cursor_rows(ed, -rows),
2716 }
2717}
2718
2719pub(crate) fn scroll_line_bridge<H: crate::types::Host>(
2723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2724 dir: ScrollDir,
2725 count: usize,
2726) {
2727 let n = count.max(1);
2728 let total = buf_row_count(&ed.buffer);
2729 let last = total.saturating_sub(1);
2730 let h = ed.viewport_height_value() as usize;
2731 let vp = ed.host().viewport();
2732 let cur_top = vp.top_row;
2733 let new_top = match dir {
2734 ScrollDir::Down => (cur_top + n).min(last),
2735 ScrollDir::Up => cur_top.saturating_sub(n),
2736 };
2737 ed.set_viewport_top(new_top);
2738 let (row, col) = ed.cursor();
2740 let bot = (new_top + h).saturating_sub(1).min(last);
2741 let clamped = row.max(new_top).min(bot);
2742 if clamped != row {
2743 buf_set_cursor_rc(&mut ed.buffer, clamped, col);
2744 ed.push_buffer_cursor_to_textarea();
2745 }
2746}
2747
2748pub(crate) fn search_repeat_bridge<H: crate::types::Host>(
2753 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2754 forward: bool,
2755 count: usize,
2756) {
2757 if let Some(pattern) = ed.vim.last_search.clone() {
2758 ed.push_search_pattern(&pattern);
2759 }
2760 if ed.search_state().pattern.is_none() {
2761 return;
2762 }
2763 let go_forward = ed.vim.last_search_forward == forward;
2764 for _ in 0..count.max(1) {
2765 if go_forward {
2766 ed.search_advance_forward(true);
2767 } else {
2768 ed.search_advance_backward(true);
2769 }
2770 }
2771 ed.push_buffer_cursor_to_textarea();
2772}
2773
2774pub(crate) fn word_search_bridge<H: crate::types::Host>(
2778 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2779 forward: bool,
2780 whole_word: bool,
2781 count: usize,
2782) {
2783 word_at_cursor_search(ed, forward, whole_word, count.max(1));
2784}
2785
2786#[allow(dead_code)]
2791#[inline]
2792pub(crate) fn do_undo_bridge<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2793 do_undo(ed);
2794}
2795
2796#[inline]
2811pub(crate) fn set_vim_mode_bridge<H: crate::types::Host>(
2812 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2813 mode: Mode,
2814) {
2815 ed.vim.mode = mode;
2816 ed.vim.current_mode = ed.vim.public_mode();
2817}
2818
2819pub(crate) fn enter_visual_char_bridge<H: crate::types::Host>(
2822 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2823) {
2824 let cur = ed.cursor();
2825 ed.vim.visual_anchor = cur;
2826 set_vim_mode_bridge(ed, Mode::Visual);
2827}
2828
2829pub(crate) fn enter_visual_line_bridge<H: crate::types::Host>(
2832 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2833) {
2834 let (row, _) = ed.cursor();
2835 ed.vim.visual_line_anchor = row;
2836 set_vim_mode_bridge(ed, Mode::VisualLine);
2837}
2838
2839pub(crate) fn enter_visual_block_bridge<H: crate::types::Host>(
2843 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2844) {
2845 let cur = ed.cursor();
2846 ed.vim.block_anchor = cur;
2847 ed.vim.block_vcol = cur.1;
2848 set_vim_mode_bridge(ed, Mode::VisualBlock);
2849}
2850
2851pub(crate) fn exit_visual_to_normal_bridge<H: crate::types::Host>(
2856 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2857) {
2858 let snap: Option<LastVisual> = match ed.vim.mode {
2860 Mode::Visual => Some(LastVisual {
2861 mode: Mode::Visual,
2862 anchor: ed.vim.visual_anchor,
2863 cursor: ed.cursor(),
2864 block_vcol: 0,
2865 }),
2866 Mode::VisualLine => Some(LastVisual {
2867 mode: Mode::VisualLine,
2868 anchor: (ed.vim.visual_line_anchor, 0),
2869 cursor: ed.cursor(),
2870 block_vcol: 0,
2871 }),
2872 Mode::VisualBlock => Some(LastVisual {
2873 mode: Mode::VisualBlock,
2874 anchor: ed.vim.block_anchor,
2875 cursor: ed.cursor(),
2876 block_vcol: ed.vim.block_vcol,
2877 }),
2878 _ => None,
2879 };
2880 ed.vim.pending = Pending::None;
2882 ed.vim.count = 0;
2883 ed.vim.insert_session = None;
2884 set_vim_mode_bridge(ed, Mode::Normal);
2885 if let Some(snap) = snap {
2889 let (lo, hi) = match snap.mode {
2890 Mode::Visual => {
2891 if snap.anchor <= snap.cursor {
2892 (snap.anchor, snap.cursor)
2893 } else {
2894 (snap.cursor, snap.anchor)
2895 }
2896 }
2897 Mode::VisualLine => {
2898 let r_lo = snap.anchor.0.min(snap.cursor.0);
2899 let r_hi = snap.anchor.0.max(snap.cursor.0);
2900 let vl_rope = ed.buffer().rope();
2901 let r_hi_clamped = r_hi.min(vl_rope.len_lines().saturating_sub(1));
2902 let last_col = hjkl_buffer::rope_line_str(&vl_rope, r_hi_clamped)
2903 .chars()
2904 .count()
2905 .saturating_sub(1);
2906 ((r_lo, 0), (r_hi, last_col))
2907 }
2908 Mode::VisualBlock => {
2909 let (r1, c1) = snap.anchor;
2910 let (r2, c2) = snap.cursor;
2911 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
2912 }
2913 _ => {
2914 if snap.anchor <= snap.cursor {
2915 (snap.anchor, snap.cursor)
2916 } else {
2917 (snap.cursor, snap.anchor)
2918 }
2919 }
2920 };
2921 ed.set_mark('<', lo);
2922 ed.set_mark('>', hi);
2923 ed.vim.last_visual = Some(snap);
2924 }
2925}
2926
2927pub(crate) fn visual_o_toggle_bridge<H: crate::types::Host>(
2933 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2934) {
2935 match ed.vim.mode {
2936 Mode::Visual => {
2937 let cur = ed.cursor();
2938 let anchor = ed.vim.visual_anchor;
2939 ed.vim.visual_anchor = cur;
2940 ed.jump_cursor(anchor.0, anchor.1);
2941 }
2942 Mode::VisualLine => {
2943 let cur_row = ed.cursor().0;
2944 let anchor_row = ed.vim.visual_line_anchor;
2945 ed.vim.visual_line_anchor = cur_row;
2946 ed.jump_cursor(anchor_row, 0);
2947 }
2948 Mode::VisualBlock => {
2949 let cur = ed.cursor();
2950 let anchor = ed.vim.block_anchor;
2951 ed.vim.block_anchor = cur;
2952 ed.vim.block_vcol = anchor.1;
2953 ed.jump_cursor(anchor.0, anchor.1);
2954 }
2955 _ => {}
2956 }
2957}
2958
2959pub(crate) fn reenter_last_visual_bridge<H: crate::types::Host>(
2963 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2964) {
2965 if let Some(snap) = ed.vim.last_visual {
2966 match snap.mode {
2967 Mode::Visual => {
2968 ed.vim.visual_anchor = snap.anchor;
2969 set_vim_mode_bridge(ed, Mode::Visual);
2970 }
2971 Mode::VisualLine => {
2972 ed.vim.visual_line_anchor = snap.anchor.0;
2973 set_vim_mode_bridge(ed, Mode::VisualLine);
2974 }
2975 Mode::VisualBlock => {
2976 ed.vim.block_anchor = snap.anchor;
2977 ed.vim.block_vcol = snap.block_vcol;
2978 set_vim_mode_bridge(ed, Mode::VisualBlock);
2979 }
2980 _ => {}
2981 }
2982 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2983 }
2984}
2985
2986pub(crate) fn set_mode_bridge<H: crate::types::Host>(
2992 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2993 mode: crate::VimMode,
2994) {
2995 let internal = match mode {
2996 crate::VimMode::Normal => Mode::Normal,
2997 crate::VimMode::Insert => Mode::Insert,
2998 crate::VimMode::Visual => Mode::Visual,
2999 crate::VimMode::VisualLine => Mode::VisualLine,
3000 crate::VimMode::VisualBlock => Mode::VisualBlock,
3001 };
3002 ed.vim.mode = internal;
3003 ed.vim.current_mode = mode;
3004}
3005
3006pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
3023 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3024 ch: char,
3025) {
3026 if ch.is_ascii_lowercase() {
3027 let pos = ed.cursor();
3028 ed.set_mark(ch, pos);
3029 } else if ch.is_ascii_uppercase() {
3030 let pos = ed.cursor();
3031 let bid = ed.current_buffer_id();
3032 ed.set_global_mark(ch, bid, pos);
3033 tracing::debug!(
3034 mark = ch as u32,
3035 buffer_id = bid,
3036 row = pos.0,
3037 col = pos.1,
3038 "global mark set"
3039 );
3040 }
3041 }
3043
3044pub(crate) fn goto_mark<H: crate::types::Host>(
3053 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3054 ch: char,
3055 linewise: bool,
3056) {
3057 let target = match ch {
3058 'a'..='z' => ed.mark(ch),
3059 '\'' | '`' => ed.vim.jump_back.last().copied(),
3060 '.' => ed.vim.last_edit_pos,
3061 '[' | ']' | '<' | '>' => ed.mark(ch),
3062 _ => None,
3063 };
3064 let Some((row, col)) = target else {
3065 return;
3066 };
3067 let pre = ed.cursor();
3068 let (r, c_clamped) = clamp_pos(ed, (row, col));
3069 if linewise {
3070 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3071 ed.push_buffer_cursor_to_textarea();
3072 move_first_non_whitespace(ed);
3073 } else {
3074 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3075 ed.push_buffer_cursor_to_textarea();
3076 }
3077 if ed.cursor() != pre {
3078 ed.push_jump(pre);
3079 }
3080 ed.sticky_col = Some(ed.cursor().1);
3081}
3082
3083pub(crate) fn try_goto_mark<H: crate::types::Host>(
3092 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3093 ch: char,
3094 linewise: bool,
3095) -> crate::editor::MarkJump {
3096 use crate::editor::MarkJump;
3097 match ch {
3098 'A'..='Z' => {
3099 let Some((bid, row, col)) = ed.global_mark(ch) else {
3100 return MarkJump::Unset;
3101 };
3102 if bid != ed.current_buffer_id() {
3103 tracing::debug!(
3104 mark = ch as u32,
3105 buffer_id = bid,
3106 row,
3107 col,
3108 "global mark cross-buffer jump"
3109 );
3110 return MarkJump::CrossBuffer {
3111 buffer_id: bid,
3112 row,
3113 col,
3114 };
3115 }
3116 let pre = ed.cursor();
3118 let (r, c_clamped) = clamp_pos(ed, (row, col));
3119 if linewise {
3120 buf_set_cursor_rc(&mut ed.buffer, r, 0);
3121 ed.push_buffer_cursor_to_textarea();
3122 move_first_non_whitespace(ed);
3123 } else {
3124 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
3125 ed.push_buffer_cursor_to_textarea();
3126 }
3127 if ed.cursor() != pre {
3128 ed.push_jump(pre);
3129 }
3130 ed.sticky_col = Some(ed.cursor().1);
3131 MarkJump::SameBuffer
3132 }
3133 'a'..='z' | '\'' | '`' | '.' | '[' | ']' | '<' | '>' => {
3134 goto_mark(ed, ch, linewise);
3135 MarkJump::SameBuffer
3136 }
3137 _ => MarkJump::Unset,
3138 }
3139}
3140
3141pub fn op_is_change(op: Operator) -> bool {
3145 matches!(op, Operator::Delete | Operator::Change)
3146}
3147
3148pub(crate) const JUMPLIST_MAX: usize = 100;
3152
3153fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3156 let Some(target) = ed.vim.jump_back.pop() else {
3157 return;
3158 };
3159 let cur = ed.cursor();
3160 ed.vim.jump_fwd.push(cur);
3161 let (r, c) = clamp_pos(ed, target);
3162 ed.jump_cursor(r, c);
3163 ed.sticky_col = Some(c);
3164}
3165
3166fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3169 let Some(target) = ed.vim.jump_fwd.pop() else {
3170 return;
3171 };
3172 let cur = ed.cursor();
3173 ed.vim.jump_back.push(cur);
3174 if ed.vim.jump_back.len() > JUMPLIST_MAX {
3175 ed.vim.jump_back.remove(0);
3176 }
3177 let (r, c) = clamp_pos(ed, target);
3178 ed.jump_cursor(r, c);
3179 ed.sticky_col = Some(c);
3180}
3181
3182fn clamp_pos<H: crate::types::Host>(
3185 ed: &Editor<hjkl_buffer::Buffer, H>,
3186 pos: (usize, usize),
3187) -> (usize, usize) {
3188 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3189 let r = pos.0.min(last_row);
3190 let line_len = buf_line_chars(&ed.buffer, r);
3191 let c = pos.1.min(line_len.saturating_sub(1));
3192 (r, c)
3193}
3194
3195fn is_big_jump(motion: &Motion) -> bool {
3197 matches!(
3198 motion,
3199 Motion::FileTop
3200 | Motion::FileBottom
3201 | Motion::MatchBracket
3202 | Motion::WordAtCursor { .. }
3203 | Motion::SearchNext { .. }
3204 | Motion::ViewportTop
3205 | Motion::ViewportMiddle
3206 | Motion::ViewportBottom
3207 )
3208}
3209
3210fn viewport_half_rows<H: crate::types::Host>(
3215 ed: &Editor<hjkl_buffer::Buffer, H>,
3216 count: usize,
3217) -> usize {
3218 let h = ed.viewport_height_value() as usize;
3219 (h / 2).max(1).saturating_mul(count.max(1))
3220}
3221
3222fn viewport_full_rows<H: crate::types::Host>(
3225 ed: &Editor<hjkl_buffer::Buffer, H>,
3226 count: usize,
3227) -> usize {
3228 let h = ed.viewport_height_value() as usize;
3229 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
3230}
3231
3232fn scroll_cursor_rows<H: crate::types::Host>(
3237 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3238 delta: isize,
3239) {
3240 if delta == 0 {
3241 return;
3242 }
3243 ed.sync_buffer_content_from_textarea();
3244 let (row, _) = ed.cursor();
3245 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
3246 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
3247 buf_set_cursor_rc(&mut ed.buffer, target, 0);
3248 crate::motions::move_first_non_blank(&mut ed.buffer);
3249 ed.push_buffer_cursor_to_textarea();
3250 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3251}
3252
3253pub fn parse_motion(input: &Input) -> Option<Motion> {
3259 if input.ctrl {
3260 return None;
3261 }
3262 match input.key {
3263 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
3264 Key::Char('l') | Key::Right => Some(Motion::Right),
3265 Key::Char('j') | Key::Down => Some(Motion::Down),
3266 Key::Char('+') | Key::Enter => Some(Motion::FirstNonBlankNextLine),
3268 Key::Char('-') => Some(Motion::FirstNonBlankPrevLine),
3270 Key::Char('_') => Some(Motion::FirstNonBlankLine),
3272 Key::Char('k') | Key::Up => Some(Motion::Up),
3273 Key::Char('w') => Some(Motion::WordFwd),
3274 Key::Char('W') => Some(Motion::BigWordFwd),
3275 Key::Char('b') => Some(Motion::WordBack),
3276 Key::Char('B') => Some(Motion::BigWordBack),
3277 Key::Char('e') => Some(Motion::WordEnd),
3278 Key::Char('E') => Some(Motion::BigWordEnd),
3279 Key::Char('0') | Key::Home => Some(Motion::LineStart),
3280 Key::Char('^') => Some(Motion::FirstNonBlank),
3281 Key::Char('$') | Key::End => Some(Motion::LineEnd),
3282 Key::Char('G') => Some(Motion::FileBottom),
3283 Key::Char('%') => Some(Motion::MatchBracket),
3284 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
3285 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
3286 Key::Char('*') => Some(Motion::WordAtCursor {
3287 forward: true,
3288 whole_word: true,
3289 }),
3290 Key::Char('#') => Some(Motion::WordAtCursor {
3291 forward: false,
3292 whole_word: true,
3293 }),
3294 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
3295 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
3296 Key::Char('H') => Some(Motion::ViewportTop),
3297 Key::Char('M') => Some(Motion::ViewportMiddle),
3298 Key::Char('L') => Some(Motion::ViewportBottom),
3299 Key::Char('{') => Some(Motion::ParagraphPrev),
3300 Key::Char('}') => Some(Motion::ParagraphNext),
3301 Key::Char('(') => Some(Motion::SentencePrev),
3302 Key::Char(')') => Some(Motion::SentenceNext),
3303 Key::Char('|') => Some(Motion::GotoColumn),
3304 _ => None,
3305 }
3306}
3307
3308pub(crate) fn execute_motion<H: crate::types::Host>(
3311 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3312 motion: Motion,
3313 count: usize,
3314) {
3315 let count = count.max(1);
3316 let motion = match motion {
3318 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3319 Some((ch, forward, till)) => Motion::Find {
3320 ch,
3321 forward: if reverse { !forward } else { forward },
3322 till,
3323 },
3324 None => return,
3325 },
3326 other => other,
3327 };
3328 let pre_pos = ed.cursor();
3329 let pre_col = pre_pos.1;
3330 apply_motion_cursor(ed, &motion, count);
3331 let post_pos = ed.cursor();
3332 if is_big_jump(&motion) && pre_pos != post_pos {
3333 ed.push_jump(pre_pos);
3334 }
3335 apply_sticky_col(ed, &motion, pre_col);
3336 ed.sync_buffer_from_textarea();
3341}
3342
3343fn execute_motion_with_block_vcol<H: crate::types::Host>(
3354 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3355 motion: Motion,
3356 count: usize,
3357) {
3358 let motion_copy = motion.clone();
3359 execute_motion(ed, motion, count);
3360 if ed.vim.mode == Mode::VisualBlock {
3361 update_block_vcol(ed, &motion_copy);
3362 }
3363}
3364
3365pub(crate) fn apply_motion_kind<H: crate::types::Host>(
3393 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3394 kind: crate::MotionKind,
3395 count: usize,
3396) {
3397 let count = count.max(1);
3398 match kind {
3399 crate::MotionKind::CharLeft => {
3400 execute_motion_with_block_vcol(ed, Motion::Left, count);
3401 }
3402 crate::MotionKind::CharRight => {
3403 execute_motion_with_block_vcol(ed, Motion::Right, count);
3404 }
3405 crate::MotionKind::LineDown => {
3406 execute_motion_with_block_vcol(ed, Motion::Down, count);
3407 }
3408 crate::MotionKind::LineUp => {
3409 execute_motion_with_block_vcol(ed, Motion::Up, count);
3410 }
3411 crate::MotionKind::FirstNonBlankDown => {
3412 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3417 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3418 crate::motions::move_first_non_blank(&mut ed.buffer);
3419 ed.push_buffer_cursor_to_textarea();
3420 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3421 ed.sync_buffer_from_textarea();
3422 }
3423 crate::MotionKind::FirstNonBlankUp => {
3424 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3427 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3428 crate::motions::move_first_non_blank(&mut ed.buffer);
3429 ed.push_buffer_cursor_to_textarea();
3430 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
3431 ed.sync_buffer_from_textarea();
3432 }
3433 crate::MotionKind::WordForward => {
3434 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
3435 }
3436 crate::MotionKind::BigWordForward => {
3437 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
3438 }
3439 crate::MotionKind::WordBackward => {
3440 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
3441 }
3442 crate::MotionKind::BigWordBackward => {
3443 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
3444 }
3445 crate::MotionKind::WordEnd => {
3446 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
3447 }
3448 crate::MotionKind::BigWordEnd => {
3449 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
3450 }
3451 crate::MotionKind::LineStart => {
3452 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
3455 }
3456 crate::MotionKind::FirstNonBlank => {
3457 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
3460 }
3461 crate::MotionKind::GotoLine => {
3462 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
3471 }
3472 crate::MotionKind::LineEnd => {
3473 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
3477 }
3478 crate::MotionKind::FindRepeat => {
3479 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
3483 }
3484 crate::MotionKind::FindRepeatReverse => {
3485 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
3489 }
3490 crate::MotionKind::BracketMatch => {
3491 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
3496 }
3497 crate::MotionKind::ViewportTop => {
3498 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
3501 }
3502 crate::MotionKind::ViewportMiddle => {
3503 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
3506 }
3507 crate::MotionKind::ViewportBottom => {
3508 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
3511 }
3512 crate::MotionKind::HalfPageDown => {
3513 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
3517 }
3518 crate::MotionKind::HalfPageUp => {
3519 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
3522 }
3523 crate::MotionKind::FullPageDown => {
3524 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
3527 }
3528 crate::MotionKind::FullPageUp => {
3529 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
3532 }
3533 crate::MotionKind::FirstNonBlankLine => {
3534 execute_motion_with_block_vcol(ed, Motion::FirstNonBlankLine, count);
3535 }
3536 crate::MotionKind::SectionBackward => {
3537 execute_motion_with_block_vcol(ed, Motion::SectionBackward, count);
3538 }
3539 crate::MotionKind::SectionForward => {
3540 execute_motion_with_block_vcol(ed, Motion::SectionForward, count);
3541 }
3542 crate::MotionKind::SectionEndBackward => {
3543 execute_motion_with_block_vcol(ed, Motion::SectionEndBackward, count);
3544 }
3545 crate::MotionKind::SectionEndForward => {
3546 execute_motion_with_block_vcol(ed, Motion::SectionEndForward, count);
3547 }
3548 }
3549}
3550
3551fn apply_sticky_col<H: crate::types::Host>(
3556 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3557 motion: &Motion,
3558 pre_col: usize,
3559) {
3560 if is_vertical_motion(motion) {
3561 let want = ed.sticky_col.unwrap_or(pre_col);
3562 ed.sticky_col = Some(want);
3565 let (row, _) = ed.cursor();
3566 let line_len = buf_line_chars(&ed.buffer, row);
3567 let max_col = line_len.saturating_sub(1);
3571 let target = want.min(max_col);
3572 buf_set_cursor_rc(&mut ed.buffer, row, target);
3576 } else {
3577 ed.sticky_col = Some(ed.cursor().1);
3580 }
3581}
3582
3583fn is_vertical_motion(motion: &Motion) -> bool {
3584 matches!(
3588 motion,
3589 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
3590 )
3591}
3592
3593fn apply_motion_cursor<H: crate::types::Host>(
3594 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3595 motion: &Motion,
3596 count: usize,
3597) {
3598 apply_motion_cursor_ctx(ed, motion, count, false)
3599}
3600
3601pub(crate) fn apply_motion_cursor_ctx<H: crate::types::Host>(
3602 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3603 motion: &Motion,
3604 count: usize,
3605 as_operator: bool,
3606) {
3607 match motion {
3608 Motion::Left => {
3609 crate::motions::move_left(&mut ed.buffer, count);
3611 ed.push_buffer_cursor_to_textarea();
3612 }
3613 Motion::Right => {
3614 if as_operator {
3618 crate::motions::move_right_to_end(&mut ed.buffer, count);
3619 } else {
3620 crate::motions::move_right_in_line(&mut ed.buffer, count);
3621 }
3622 ed.push_buffer_cursor_to_textarea();
3623 }
3624 Motion::Up => {
3625 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3629 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3630 ed.push_buffer_cursor_to_textarea();
3631 }
3632 Motion::Down => {
3633 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3634 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3635 ed.push_buffer_cursor_to_textarea();
3636 }
3637 Motion::ScreenUp => {
3638 let v = *ed.host.viewport();
3639 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3640 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3641 ed.push_buffer_cursor_to_textarea();
3642 }
3643 Motion::ScreenDown => {
3644 let v = *ed.host.viewport();
3645 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3646 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3647 ed.push_buffer_cursor_to_textarea();
3648 }
3649 Motion::WordFwd => {
3650 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3651 ed.push_buffer_cursor_to_textarea();
3652 }
3653 Motion::WordBack => {
3654 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3655 ed.push_buffer_cursor_to_textarea();
3656 }
3657 Motion::WordEnd => {
3658 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3659 ed.push_buffer_cursor_to_textarea();
3660 }
3661 Motion::BigWordFwd => {
3662 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3663 ed.push_buffer_cursor_to_textarea();
3664 }
3665 Motion::BigWordBack => {
3666 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3667 ed.push_buffer_cursor_to_textarea();
3668 }
3669 Motion::BigWordEnd => {
3670 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3671 ed.push_buffer_cursor_to_textarea();
3672 }
3673 Motion::WordEndBack => {
3674 crate::motions::move_word_end_back(
3675 &mut ed.buffer,
3676 false,
3677 count,
3678 &ed.settings.iskeyword,
3679 );
3680 ed.push_buffer_cursor_to_textarea();
3681 }
3682 Motion::BigWordEndBack => {
3683 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3684 ed.push_buffer_cursor_to_textarea();
3685 }
3686 Motion::LineStart => {
3687 crate::motions::move_line_start(&mut ed.buffer);
3688 ed.push_buffer_cursor_to_textarea();
3689 }
3690 Motion::FirstNonBlank => {
3691 crate::motions::move_first_non_blank(&mut ed.buffer);
3692 ed.push_buffer_cursor_to_textarea();
3693 }
3694 Motion::LineEnd => {
3695 crate::motions::move_line_end(&mut ed.buffer);
3697 ed.push_buffer_cursor_to_textarea();
3698 }
3699 Motion::FileTop => {
3700 if count > 1 {
3703 crate::motions::move_bottom(&mut ed.buffer, count);
3704 } else {
3705 crate::motions::move_top(&mut ed.buffer);
3706 }
3707 ed.push_buffer_cursor_to_textarea();
3708 }
3709 Motion::FileBottom => {
3710 if count > 1 {
3713 crate::motions::move_bottom(&mut ed.buffer, count);
3714 } else {
3715 crate::motions::move_bottom(&mut ed.buffer, 0);
3716 }
3717 ed.push_buffer_cursor_to_textarea();
3718 }
3719 Motion::Find { ch, forward, till } => {
3720 for _ in 0..count {
3721 if !find_char_on_line(ed, *ch, *forward, *till) {
3722 break;
3723 }
3724 }
3725 }
3726 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
3728 let _ = matching_bracket(ed);
3729 }
3730 Motion::WordAtCursor {
3731 forward,
3732 whole_word,
3733 } => {
3734 word_at_cursor_search(ed, *forward, *whole_word, count);
3735 }
3736 Motion::SearchNext { reverse } => {
3737 if let Some(pattern) = ed.vim.last_search.clone() {
3741 ed.push_search_pattern(&pattern);
3742 }
3743 if ed.search_state().pattern.is_none() {
3744 return;
3745 }
3746 let forward = ed.vim.last_search_forward != *reverse;
3750 for _ in 0..count.max(1) {
3751 if forward {
3752 ed.search_advance_forward(true);
3753 } else {
3754 ed.search_advance_backward(true);
3755 }
3756 }
3757 ed.push_buffer_cursor_to_textarea();
3758 }
3759 Motion::ViewportTop => {
3760 let v = *ed.host().viewport();
3761 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
3762 ed.push_buffer_cursor_to_textarea();
3763 }
3764 Motion::ViewportMiddle => {
3765 let v = *ed.host().viewport();
3766 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
3767 ed.push_buffer_cursor_to_textarea();
3768 }
3769 Motion::ViewportBottom => {
3770 let v = *ed.host().viewport();
3771 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
3772 ed.push_buffer_cursor_to_textarea();
3773 }
3774 Motion::LastNonBlank => {
3775 crate::motions::move_last_non_blank(&mut ed.buffer);
3776 ed.push_buffer_cursor_to_textarea();
3777 }
3778 Motion::LineMiddle => {
3779 let row = ed.cursor().0;
3780 let line_chars = buf_line_chars(&ed.buffer, row);
3781 let target = line_chars / 2;
3784 ed.jump_cursor(row, target);
3785 }
3786 Motion::ParagraphPrev => {
3787 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
3788 ed.push_buffer_cursor_to_textarea();
3789 }
3790 Motion::ParagraphNext => {
3791 crate::motions::move_paragraph_next(&mut ed.buffer, count);
3792 ed.push_buffer_cursor_to_textarea();
3793 }
3794 Motion::SentencePrev => {
3795 for _ in 0..count.max(1) {
3796 if let Some((row, col)) = sentence_boundary(ed, false) {
3797 ed.jump_cursor(row, col);
3798 }
3799 }
3800 }
3801 Motion::SentenceNext => {
3802 for _ in 0..count.max(1) {
3803 if let Some((row, col)) = sentence_boundary(ed, true) {
3804 ed.jump_cursor(row, col);
3805 }
3806 }
3807 }
3808 Motion::SectionBackward => {
3809 crate::motions::move_section_backward(&mut ed.buffer, count);
3810 ed.push_buffer_cursor_to_textarea();
3811 }
3812 Motion::SectionForward => {
3813 crate::motions::move_section_forward(&mut ed.buffer, count);
3814 ed.push_buffer_cursor_to_textarea();
3815 }
3816 Motion::SectionEndBackward => {
3817 crate::motions::move_section_end_backward(&mut ed.buffer, count);
3818 ed.push_buffer_cursor_to_textarea();
3819 }
3820 Motion::SectionEndForward => {
3821 crate::motions::move_section_end_forward(&mut ed.buffer, count);
3822 ed.push_buffer_cursor_to_textarea();
3823 }
3824 Motion::FirstNonBlankNextLine => {
3825 crate::motions::move_first_non_blank_next_line(&mut ed.buffer, count);
3826 ed.push_buffer_cursor_to_textarea();
3827 }
3828 Motion::FirstNonBlankPrevLine => {
3829 crate::motions::move_first_non_blank_prev_line(&mut ed.buffer, count);
3830 ed.push_buffer_cursor_to_textarea();
3831 }
3832 Motion::FirstNonBlankLine => {
3833 crate::motions::move_first_non_blank_line(&mut ed.buffer, count);
3834 ed.push_buffer_cursor_to_textarea();
3835 }
3836 Motion::GotoColumn => {
3837 crate::motions::move_goto_column(&mut ed.buffer, count);
3838 ed.push_buffer_cursor_to_textarea();
3839 }
3840 }
3841}
3842
3843fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3844 ed.sync_buffer_content_from_textarea();
3850 crate::motions::move_first_non_blank(&mut ed.buffer);
3851 ed.push_buffer_cursor_to_textarea();
3852}
3853
3854fn find_char_on_line<H: crate::types::Host>(
3855 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3856 ch: char,
3857 forward: bool,
3858 till: bool,
3859) -> bool {
3860 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
3861 if moved {
3862 ed.push_buffer_cursor_to_textarea();
3863 }
3864 moved
3865}
3866
3867fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3868 let moved = crate::motions::match_bracket(&mut ed.buffer);
3869 if moved {
3870 ed.push_buffer_cursor_to_textarea();
3871 }
3872 moved
3873}
3874
3875fn word_at_cursor_search<H: crate::types::Host>(
3876 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3877 forward: bool,
3878 whole_word: bool,
3879 count: usize,
3880) {
3881 let (row, col) = ed.cursor();
3882 let line: String = buf_line(&ed.buffer, row).unwrap_or_default();
3883 let chars: Vec<char> = line.chars().collect();
3884 if chars.is_empty() {
3885 return;
3886 }
3887 let spec = ed.settings().iskeyword.clone();
3889 let is_word = |c: char| is_keyword_char(c, &spec);
3890 let mut start = col.min(chars.len().saturating_sub(1));
3891 while start > 0 && is_word(chars[start - 1]) {
3892 start -= 1;
3893 }
3894 let mut end = start;
3895 while end < chars.len() && is_word(chars[end]) {
3896 end += 1;
3897 }
3898 if end <= start {
3899 return;
3900 }
3901 let word: String = chars[start..end].iter().collect();
3902 let escaped = regex_escape(&word);
3903 let pattern = if whole_word {
3904 format!(r"\b{escaped}\b")
3905 } else {
3906 escaped
3907 };
3908 ed.push_search_pattern(&pattern);
3909 if ed.search_state().pattern.is_none() {
3910 return;
3911 }
3912 ed.vim.last_search = Some(pattern);
3914 ed.vim.last_search_forward = forward;
3915 for _ in 0..count.max(1) {
3916 if forward {
3917 ed.search_advance_forward(true);
3918 } else {
3919 ed.search_advance_backward(true);
3920 }
3921 }
3922 ed.push_buffer_cursor_to_textarea();
3923}
3924
3925fn regex_escape(s: &str) -> String {
3926 let mut out = String::with_capacity(s.len());
3927 for c in s.chars() {
3928 if matches!(
3929 c,
3930 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
3931 ) {
3932 out.push('\\');
3933 }
3934 out.push(c);
3935 }
3936 out
3937}
3938
3939pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
3953 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3954 op: Operator,
3955 motion_key: char,
3956 total_count: usize,
3957) {
3958 let input = Input {
3959 key: Key::Char(motion_key),
3960 ctrl: false,
3961 alt: false,
3962 shift: false,
3963 };
3964 let Some(motion) = parse_motion(&input) else {
3965 return;
3966 };
3967 let motion = match motion {
3968 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3969 Some((ch, forward, till)) => Motion::Find {
3970 ch,
3971 forward: if reverse { !forward } else { forward },
3972 till,
3973 },
3974 None => return,
3975 },
3976 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3978 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3979 m => m,
3980 };
3981 apply_op_with_motion(ed, op, &motion, total_count);
3982 if let Motion::Find { ch, forward, till } = &motion {
3983 ed.vim.last_find = Some((*ch, *forward, *till));
3984 }
3985 if !ed.vim.replaying && op_is_change(op) {
3986 ed.vim.last_change = Some(LastChange::OpMotion {
3987 op,
3988 motion,
3989 count: total_count,
3990 inserted: None,
3991 });
3992 }
3993}
3994
3995pub(crate) fn apply_op_double<H: crate::types::Host>(
3998 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3999 op: Operator,
4000 total_count: usize,
4001) {
4002 if op == Operator::Comment {
4003 let row = buf_cursor_pos(&ed.buffer).row;
4005 let end_row = (row + total_count.max(1) - 1).min(ed.buffer.row_count().saturating_sub(1));
4006 ed.toggle_comment_range(row, end_row);
4007 ed.vim.mode = Mode::Normal;
4008 if !ed.vim.replaying {
4009 ed.vim.last_change = Some(LastChange::LineOp {
4010 op,
4011 count: total_count,
4012 inserted: None,
4013 });
4014 }
4015 return;
4016 }
4017 execute_line_op(ed, op, total_count);
4018 if !ed.vim.replaying {
4019 ed.vim.last_change = Some(LastChange::LineOp {
4020 op,
4021 count: total_count,
4022 inserted: None,
4023 });
4024 }
4025}
4026
4027pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
4037 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4038 op: Operator,
4039 ch: char,
4040 total_count: usize,
4041) {
4042 if matches!(
4045 op,
4046 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
4047 ) {
4048 let op_char = match op {
4049 Operator::Uppercase => 'U',
4050 Operator::Lowercase => 'u',
4051 Operator::ToggleCase => '~',
4052 _ => unreachable!(),
4053 };
4054 if ch == op_char {
4055 execute_line_op(ed, op, total_count);
4056 if !ed.vim.replaying {
4057 ed.vim.last_change = Some(LastChange::LineOp {
4058 op,
4059 count: total_count,
4060 inserted: None,
4061 });
4062 }
4063 return;
4064 }
4065 }
4066 let motion = match ch {
4067 'g' => Motion::FileTop,
4068 'e' => Motion::WordEndBack,
4069 'E' => Motion::BigWordEndBack,
4070 'j' => Motion::ScreenDown,
4071 'k' => Motion::ScreenUp,
4072 _ => return, };
4074 apply_op_with_motion(ed, op, &motion, total_count);
4075 if !ed.vim.replaying && op_is_change(op) {
4076 ed.vim.last_change = Some(LastChange::OpMotion {
4077 op,
4078 motion,
4079 count: total_count,
4080 inserted: None,
4081 });
4082 }
4083}
4084
4085pub(crate) fn apply_after_g<H: crate::types::Host>(
4090 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4091 ch: char,
4092 count: usize,
4093) {
4094 match ch {
4095 'g' => {
4096 let pre = ed.cursor();
4098 if count > 1 {
4099 ed.jump_cursor(count - 1, 0);
4100 } else {
4101 ed.jump_cursor(0, 0);
4102 }
4103 move_first_non_whitespace(ed);
4104 ed.sticky_col = Some(ed.cursor().1);
4107 if ed.cursor() != pre {
4108 ed.push_jump(pre);
4109 }
4110 }
4111 'e' => execute_motion(ed, Motion::WordEndBack, count),
4112 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
4113 '_' => execute_motion(ed, Motion::LastNonBlank, count),
4115 'M' => execute_motion(ed, Motion::LineMiddle, count),
4117 'v' => ed.reenter_last_visual(),
4120 'j' => execute_motion(ed, Motion::ScreenDown, count),
4124 'k' => execute_motion(ed, Motion::ScreenUp, count),
4125 'U' => {
4129 ed.vim.pending = Pending::Op {
4130 op: Operator::Uppercase,
4131 count1: count,
4132 };
4133 }
4134 'u' => {
4135 ed.vim.pending = Pending::Op {
4136 op: Operator::Lowercase,
4137 count1: count,
4138 };
4139 }
4140 '~' => {
4141 ed.vim.pending = Pending::Op {
4142 op: Operator::ToggleCase,
4143 count1: count,
4144 };
4145 }
4146 'q' => {
4147 ed.vim.pending = Pending::Op {
4150 op: Operator::Reflow,
4151 count1: count,
4152 };
4153 }
4154 'J' => {
4155 for _ in 0..count.max(1) {
4157 ed.push_undo();
4158 join_line_raw(ed);
4159 }
4160 if !ed.vim.replaying {
4161 ed.vim.last_change = Some(LastChange::JoinLine {
4162 count: count.max(1),
4163 });
4164 }
4165 }
4166 'd' => {
4167 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
4172 }
4173 'i' => {
4178 if let Some((row, col)) = ed.vim.last_insert_pos {
4179 ed.jump_cursor(row, col);
4180 }
4181 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4182 }
4183 'c' => {
4188 ed.vim.pending = Pending::Op {
4189 op: Operator::Comment,
4190 count1: count,
4191 };
4192 }
4193 ';' => walk_change_list(ed, -1, count.max(1)),
4196 ',' => walk_change_list(ed, 1, count.max(1)),
4197 '*' => execute_motion(
4201 ed,
4202 Motion::WordAtCursor {
4203 forward: true,
4204 whole_word: false,
4205 },
4206 count,
4207 ),
4208 '#' => execute_motion(
4209 ed,
4210 Motion::WordAtCursor {
4211 forward: false,
4212 whole_word: false,
4213 },
4214 count,
4215 ),
4216 _ => {}
4217 }
4218}
4219
4220pub(crate) fn apply_after_z<H: crate::types::Host>(
4225 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4226 ch: char,
4227 count: usize,
4228) {
4229 use crate::editor::CursorScrollTarget;
4230 let row = ed.cursor().0;
4231 match ch {
4232 'z' => {
4233 ed.scroll_cursor_to(CursorScrollTarget::Center);
4234 ed.vim.viewport_pinned = true;
4235 }
4236 't' => {
4237 ed.scroll_cursor_to(CursorScrollTarget::Top);
4238 ed.vim.viewport_pinned = true;
4239 }
4240 'b' => {
4241 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
4242 ed.vim.viewport_pinned = true;
4243 }
4244 'o' => {
4249 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
4250 }
4251 'c' => {
4252 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
4253 }
4254 'a' => {
4255 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
4256 }
4257 'R' => {
4258 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
4259 }
4260 'M' => {
4261 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
4262 }
4263 'E' => {
4264 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
4265 }
4266 'd' => {
4267 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
4268 }
4269 'f' => {
4270 if matches!(
4271 ed.vim.mode,
4272 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
4273 ) {
4274 let anchor_row = match ed.vim.mode {
4277 Mode::VisualLine => ed.vim.visual_line_anchor,
4278 Mode::VisualBlock => ed.vim.block_anchor.0,
4279 _ => ed.vim.visual_anchor.0,
4280 };
4281 let cur = ed.cursor().0;
4282 let top = anchor_row.min(cur);
4283 let bot = anchor_row.max(cur);
4284 ed.apply_fold_op(crate::types::FoldOp::Add {
4285 start_row: top,
4286 end_row: bot,
4287 closed: true,
4288 });
4289 ed.vim.mode = Mode::Normal;
4290 } else {
4291 ed.vim.pending = Pending::Op {
4296 op: Operator::Fold,
4297 count1: count,
4298 };
4299 }
4300 }
4301 _ => {}
4302 }
4303}
4304
4305pub(crate) fn apply_find_char<H: crate::types::Host>(
4311 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4312 ch: char,
4313 forward: bool,
4314 till: bool,
4315 count: usize,
4316) {
4317 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
4318 ed.vim.last_find = Some((ch, forward, till));
4319}
4320
4321pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
4327 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4328 op: Operator,
4329 ch: char,
4330 forward: bool,
4331 till: bool,
4332 total_count: usize,
4333) {
4334 let motion = Motion::Find { ch, forward, till };
4335 apply_op_with_motion(ed, op, &motion, total_count);
4336 ed.vim.last_find = Some((ch, forward, till));
4337 if !ed.vim.replaying && op_is_change(op) {
4338 ed.vim.last_change = Some(LastChange::OpMotion {
4339 op,
4340 motion,
4341 count: total_count,
4342 inserted: None,
4343 });
4344 }
4345}
4346
4347pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
4356 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4357 op: Operator,
4358 ch: char,
4359 inner: bool,
4360 _total_count: usize,
4361) -> bool {
4362 let obj = match ch {
4365 'w' => TextObject::Word { big: false },
4366 'W' => TextObject::Word { big: true },
4367 '"' | '\'' | '`' => TextObject::Quote(ch),
4368 '(' | ')' | 'b' => TextObject::Bracket('('),
4369 '[' | ']' => TextObject::Bracket('['),
4370 '{' | '}' | 'B' => TextObject::Bracket('{'),
4371 '<' | '>' => TextObject::Bracket('<'),
4372 'p' => TextObject::Paragraph,
4373 't' => TextObject::XmlTag,
4374 's' => TextObject::Sentence,
4375 _ => return false,
4376 };
4377 apply_op_with_text_object(ed, op, obj, inner);
4378 if !ed.vim.replaying && op_is_change(op) {
4379 ed.vim.last_change = Some(LastChange::OpTextObj {
4380 op,
4381 obj,
4382 inner,
4383 inserted: None,
4384 });
4385 }
4386 true
4387}
4388
4389pub(crate) fn retreat_one<H: crate::types::Host>(
4391 ed: &Editor<hjkl_buffer::Buffer, H>,
4392 pos: (usize, usize),
4393) -> (usize, usize) {
4394 let (r, c) = pos;
4395 if c > 0 {
4396 (r, c - 1)
4397 } else if r > 0 {
4398 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
4399 (r - 1, prev_len)
4400 } else {
4401 (0, 0)
4402 }
4403}
4404
4405fn begin_insert_noundo<H: crate::types::Host>(
4407 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4408 count: usize,
4409 reason: InsertReason,
4410) {
4411 let reason = if ed.vim.replaying {
4412 InsertReason::ReplayOnly
4413 } else {
4414 reason
4415 };
4416 let (row, _) = ed.cursor();
4417 ed.vim.insert_session = Some(InsertSession {
4418 count,
4419 row_min: row,
4420 row_max: row,
4421 before_rope: crate::types::Query::rope(&ed.buffer),
4422 reason,
4423 });
4424 ed.vim.mode = Mode::Insert;
4425 ed.vim.current_mode = crate::VimMode::Insert;
4427}
4428
4429pub(crate) fn apply_op_with_motion<H: crate::types::Host>(
4432 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4433 op: Operator,
4434 motion: &Motion,
4435 count: usize,
4436) {
4437 let start = ed.cursor();
4438 apply_motion_cursor_ctx(ed, motion, count, true);
4443 let end = ed.cursor();
4444 let kind = motion_kind(motion);
4445 ed.jump_cursor(start.0, start.1);
4447
4448 if op == Operator::Comment {
4450 let top = start.0.min(end.0);
4451 let bot = start.0.max(end.0);
4452 ed.toggle_comment_range(top, bot);
4453 ed.vim.mode = Mode::Normal;
4454 return;
4455 }
4456
4457 run_operator_over_range(ed, op, start, end, kind);
4458}
4459
4460fn apply_op_with_text_object<H: crate::types::Host>(
4461 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4462 op: Operator,
4463 obj: TextObject,
4464 inner: bool,
4465) {
4466 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
4467 return;
4468 };
4469 ed.jump_cursor(start.0, start.1);
4470 run_operator_over_range(ed, op, start, end, kind);
4471}
4472
4473fn motion_kind(motion: &Motion) -> RangeKind {
4474 match motion {
4475 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => RangeKind::Linewise,
4476 Motion::FileTop | Motion::FileBottom => RangeKind::Linewise,
4477 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
4478 RangeKind::Linewise
4479 }
4480 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
4481 RangeKind::Inclusive
4482 }
4483 Motion::Find { .. } => RangeKind::Inclusive,
4484 Motion::MatchBracket => RangeKind::Inclusive,
4485 Motion::LineEnd => RangeKind::Inclusive,
4487 Motion::FirstNonBlankNextLine
4489 | Motion::FirstNonBlankPrevLine
4490 | Motion::FirstNonBlankLine => RangeKind::Linewise,
4491 Motion::SectionBackward
4493 | Motion::SectionForward
4494 | Motion::SectionEndBackward
4495 | Motion::SectionEndForward => RangeKind::Exclusive,
4496 _ => RangeKind::Exclusive,
4497 }
4498}
4499
4500fn run_operator_over_range<H: crate::types::Host>(
4501 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4502 op: Operator,
4503 start: (usize, usize),
4504 end: (usize, usize),
4505 kind: RangeKind,
4506) {
4507 let (top, bot) = order(start, end);
4508 if top == bot && !matches!(kind, RangeKind::Linewise) {
4512 return;
4513 }
4514
4515 match op {
4516 Operator::Yank => {
4517 let text = read_vim_range(ed, top, bot, kind);
4518 if !text.is_empty() {
4519 ed.record_yank_to_host(text.clone());
4520 ed.record_yank(text, matches!(kind, RangeKind::Linewise));
4521 }
4522 let rbr = match kind {
4526 RangeKind::Linewise => {
4527 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
4528 (bot.0, last_col)
4529 }
4530 RangeKind::Inclusive => (bot.0, bot.1),
4531 RangeKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
4532 };
4533 ed.set_mark('[', top);
4534 ed.set_mark(']', rbr);
4535 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4536 ed.push_buffer_cursor_to_textarea();
4537 }
4538 Operator::Delete => {
4539 ed.push_undo();
4540 cut_vim_range(ed, top, bot, kind);
4541 if !matches!(kind, RangeKind::Linewise) {
4546 clamp_cursor_to_normal_mode(ed);
4547 }
4548 ed.vim.mode = Mode::Normal;
4549 let pos = ed.cursor();
4553 ed.set_mark('[', pos);
4554 ed.set_mark(']', pos);
4555 }
4556 Operator::Change => {
4557 ed.vim.change_mark_start = Some(top);
4562 ed.push_undo();
4563 cut_vim_range(ed, top, bot, kind);
4564 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4565 }
4566 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4567 apply_case_op_to_selection(ed, op, top, bot, kind);
4568 }
4569 Operator::Indent | Operator::Outdent => {
4570 ed.push_undo();
4573 if op == Operator::Indent {
4574 indent_rows(ed, top.0, bot.0, 1);
4575 } else {
4576 outdent_rows(ed, top.0, bot.0, 1);
4577 }
4578 ed.vim.mode = Mode::Normal;
4579 }
4580 Operator::Fold => {
4581 if bot.0 >= top.0 {
4585 ed.apply_fold_op(crate::types::FoldOp::Add {
4586 start_row: top.0,
4587 end_row: bot.0,
4588 closed: true,
4589 });
4590 }
4591 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4592 ed.push_buffer_cursor_to_textarea();
4593 ed.vim.mode = Mode::Normal;
4594 }
4595 Operator::Reflow => {
4596 ed.push_undo();
4597 reflow_rows(ed, top.0, bot.0);
4598 ed.vim.mode = Mode::Normal;
4599 }
4600 Operator::AutoIndent => {
4601 ed.push_undo();
4603 auto_indent_rows(ed, top.0, bot.0);
4604 ed.vim.mode = Mode::Normal;
4605 }
4606 Operator::Filter => {
4607 }
4612 Operator::Comment => {
4613 }
4616 }
4617}
4618
4619pub(crate) fn delete_range_bridge<H: crate::types::Host>(
4636 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4637 start: (usize, usize),
4638 end: (usize, usize),
4639 kind: RangeKind,
4640 register: char,
4641) {
4642 ed.vim.pending_register = Some(register);
4643 run_operator_over_range(ed, Operator::Delete, start, end, kind);
4644}
4645
4646pub(crate) fn yank_range_bridge<H: crate::types::Host>(
4649 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4650 start: (usize, usize),
4651 end: (usize, usize),
4652 kind: RangeKind,
4653 register: char,
4654) {
4655 ed.vim.pending_register = Some(register);
4656 run_operator_over_range(ed, Operator::Yank, start, end, kind);
4657}
4658
4659pub(crate) fn change_range_bridge<H: crate::types::Host>(
4664 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4665 start: (usize, usize),
4666 end: (usize, usize),
4667 kind: RangeKind,
4668 register: char,
4669) {
4670 ed.vim.pending_register = Some(register);
4671 run_operator_over_range(ed, Operator::Change, start, end, kind);
4672}
4673
4674pub(crate) fn indent_range_bridge<H: crate::types::Host>(
4679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4680 start: (usize, usize),
4681 end: (usize, usize),
4682 count: i32,
4683 shiftwidth: u32,
4684) {
4685 if count == 0 {
4686 return;
4687 }
4688 let (top_row, bot_row) = if start.0 <= end.0 {
4689 (start.0, end.0)
4690 } else {
4691 (end.0, start.0)
4692 };
4693 let original_sw = ed.settings().shiftwidth;
4695 if shiftwidth > 0 {
4696 ed.settings_mut().shiftwidth = shiftwidth as usize;
4697 }
4698 ed.push_undo();
4699 let abs_count = count.unsigned_abs() as usize;
4700 if count > 0 {
4701 indent_rows(ed, top_row, bot_row, abs_count);
4702 } else {
4703 outdent_rows(ed, top_row, bot_row, abs_count);
4704 }
4705 if shiftwidth > 0 {
4706 ed.settings_mut().shiftwidth = original_sw;
4707 }
4708 ed.vim.mode = Mode::Normal;
4709}
4710
4711pub(crate) fn case_range_bridge<H: crate::types::Host>(
4715 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4716 start: (usize, usize),
4717 end: (usize, usize),
4718 kind: RangeKind,
4719 op: Operator,
4720) {
4721 match op {
4722 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
4723 _ => return,
4724 }
4725 let (top, bot) = order(start, end);
4726 apply_case_op_to_selection(ed, op, top, bot, kind);
4727}
4728
4729pub(crate) fn delete_block_bridge<H: crate::types::Host>(
4750 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4751 top_row: usize,
4752 bot_row: usize,
4753 left_col: usize,
4754 right_col: usize,
4755 register: char,
4756) {
4757 ed.vim.pending_register = Some(register);
4758 let saved_anchor = ed.vim.block_anchor;
4759 let saved_vcol = ed.vim.block_vcol;
4760 ed.vim.block_anchor = (top_row, left_col);
4761 ed.vim.block_vcol = right_col;
4762 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4764 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4766 apply_block_operator(ed, Operator::Delete);
4767 ed.vim.block_anchor = saved_anchor;
4771 ed.vim.block_vcol = saved_vcol;
4772}
4773
4774pub(crate) fn yank_block_bridge<H: crate::types::Host>(
4776 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4777 top_row: usize,
4778 bot_row: usize,
4779 left_col: usize,
4780 right_col: usize,
4781 register: char,
4782) {
4783 ed.vim.pending_register = Some(register);
4784 let saved_anchor = ed.vim.block_anchor;
4785 let saved_vcol = ed.vim.block_vcol;
4786 ed.vim.block_anchor = (top_row, left_col);
4787 ed.vim.block_vcol = right_col;
4788 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4789 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4790 apply_block_operator(ed, Operator::Yank);
4791 ed.vim.block_anchor = saved_anchor;
4792 ed.vim.block_vcol = saved_vcol;
4793}
4794
4795pub(crate) fn change_block_bridge<H: crate::types::Host>(
4798 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4799 top_row: usize,
4800 bot_row: usize,
4801 left_col: usize,
4802 right_col: usize,
4803 register: char,
4804) {
4805 ed.vim.pending_register = Some(register);
4806 let saved_anchor = ed.vim.block_anchor;
4807 let saved_vcol = ed.vim.block_vcol;
4808 ed.vim.block_anchor = (top_row, left_col);
4809 ed.vim.block_vcol = right_col;
4810 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4811 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4812 apply_block_operator(ed, Operator::Change);
4813 ed.vim.block_anchor = saved_anchor;
4814 ed.vim.block_vcol = saved_vcol;
4815}
4816
4817pub(crate) fn indent_block_bridge<H: crate::types::Host>(
4821 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4822 top_row: usize,
4823 bot_row: usize,
4824 count: i32,
4825) {
4826 if count == 0 {
4827 return;
4828 }
4829 ed.push_undo();
4830 let abs = count.unsigned_abs() as usize;
4831 if count > 0 {
4832 indent_rows(ed, top_row, bot_row, abs);
4833 } else {
4834 outdent_rows(ed, top_row, bot_row, abs);
4835 }
4836 ed.vim.mode = Mode::Normal;
4837}
4838
4839pub(crate) fn auto_indent_range_bridge<H: crate::types::Host>(
4843 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4844 start: (usize, usize),
4845 end: (usize, usize),
4846) {
4847 let (top_row, bot_row) = if start.0 <= end.0 {
4848 (start.0, end.0)
4849 } else {
4850 (end.0, start.0)
4851 };
4852 ed.push_undo();
4853 auto_indent_rows(ed, top_row, bot_row);
4854 ed.vim.mode = Mode::Normal;
4855}
4856
4857pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
4868 ed: &Editor<hjkl_buffer::Buffer, H>,
4869) -> Option<((usize, usize), (usize, usize))> {
4870 word_text_object(ed, true, false)
4871}
4872
4873pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
4876 ed: &Editor<hjkl_buffer::Buffer, H>,
4877) -> Option<((usize, usize), (usize, usize))> {
4878 word_text_object(ed, false, false)
4879}
4880
4881pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
4884 ed: &Editor<hjkl_buffer::Buffer, H>,
4885) -> Option<((usize, usize), (usize, usize))> {
4886 word_text_object(ed, true, true)
4887}
4888
4889pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
4892 ed: &Editor<hjkl_buffer::Buffer, H>,
4893) -> Option<((usize, usize), (usize, usize))> {
4894 word_text_object(ed, false, true)
4895}
4896
4897pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
4913 ed: &Editor<hjkl_buffer::Buffer, H>,
4914 quote: char,
4915) -> Option<((usize, usize), (usize, usize))> {
4916 quote_text_object(ed, quote, true)
4917}
4918
4919pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
4922 ed: &Editor<hjkl_buffer::Buffer, H>,
4923 quote: char,
4924) -> Option<((usize, usize), (usize, usize))> {
4925 quote_text_object(ed, quote, false)
4926}
4927
4928pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
4936 ed: &Editor<hjkl_buffer::Buffer, H>,
4937 open: char,
4938) -> Option<((usize, usize), (usize, usize))> {
4939 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
4940}
4941
4942pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
4946 ed: &Editor<hjkl_buffer::Buffer, H>,
4947 open: char,
4948) -> Option<((usize, usize), (usize, usize))> {
4949 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
4950}
4951
4952pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
4957 ed: &Editor<hjkl_buffer::Buffer, H>,
4958) -> Option<((usize, usize), (usize, usize))> {
4959 sentence_text_object(ed, true)
4960}
4961
4962pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
4965 ed: &Editor<hjkl_buffer::Buffer, H>,
4966) -> Option<((usize, usize), (usize, usize))> {
4967 sentence_text_object(ed, false)
4968}
4969
4970pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
4975 ed: &Editor<hjkl_buffer::Buffer, H>,
4976) -> Option<((usize, usize), (usize, usize))> {
4977 paragraph_text_object(ed, true)
4978}
4979
4980pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
4983 ed: &Editor<hjkl_buffer::Buffer, H>,
4984) -> Option<((usize, usize), (usize, usize))> {
4985 paragraph_text_object(ed, false)
4986}
4987
4988pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
4994 ed: &Editor<hjkl_buffer::Buffer, H>,
4995) -> Option<((usize, usize), (usize, usize))> {
4996 tag_text_object(ed, true)
4997}
4998
4999pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
5002 ed: &Editor<hjkl_buffer::Buffer, H>,
5003) -> Option<((usize, usize), (usize, usize))> {
5004 tag_text_object(ed, false)
5005}
5006
5007pub(crate) fn rope_line_to_str(rope: &ropey::Rope, r: usize) -> String {
5012 let s = rope.line(r).to_string();
5013 if s.ends_with('\n') {
5015 s[..s.len() - 1].to_string()
5016 } else {
5017 s
5018 }
5019}
5020
5021pub(crate) fn rope_row_range_str(rope: &ropey::Rope, lo: usize, hi: usize) -> String {
5024 let n = rope.len_lines();
5025 let lo = lo.min(n.saturating_sub(1));
5026 let hi = hi.min(n.saturating_sub(1));
5027 if lo > hi {
5028 return String::new();
5029 }
5030 let start_byte = rope.line_to_byte(lo);
5032 let end_byte = if hi + 1 < n {
5035 rope.line_to_byte(hi + 1).saturating_sub(1)
5038 } else {
5039 rope.len_bytes()
5040 };
5041 rope.byte_slice(start_byte..end_byte).to_string()
5042}
5043
5044pub(crate) fn rope_to_lines_vec(rope: &ropey::Rope) -> Vec<String> {
5048 let n = rope.len_lines();
5049 (0..n).map(|r| rope_line_to_str(rope, r)).collect()
5050}
5051
5052fn reflow_rows<H: crate::types::Host>(
5057 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5058 top: usize,
5059 bot: usize,
5060) {
5061 let width = ed.settings().textwidth.max(1);
5062 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5063 let bot = bot.min(lines.len().saturating_sub(1));
5064 if top > bot {
5065 return;
5066 }
5067 let original = lines[top..=bot].to_vec();
5068 let mut wrapped: Vec<String> = Vec::new();
5069 let mut paragraph: Vec<String> = Vec::new();
5070 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
5071 if para.is_empty() {
5072 return;
5073 }
5074 let words = para.join(" ");
5075 let mut current = String::new();
5076 for word in words.split_whitespace() {
5077 let extra = if current.is_empty() {
5078 word.chars().count()
5079 } else {
5080 current.chars().count() + 1 + word.chars().count()
5081 };
5082 if extra > width && !current.is_empty() {
5083 out.push(std::mem::take(&mut current));
5084 current.push_str(word);
5085 } else if current.is_empty() {
5086 current.push_str(word);
5087 } else {
5088 current.push(' ');
5089 current.push_str(word);
5090 }
5091 }
5092 if !current.is_empty() {
5093 out.push(current);
5094 }
5095 para.clear();
5096 };
5097 for line in &original {
5098 if line.trim().is_empty() {
5099 flush(&mut paragraph, &mut wrapped, width);
5100 wrapped.push(String::new());
5101 } else {
5102 paragraph.push(line.clone());
5103 }
5104 }
5105 flush(&mut paragraph, &mut wrapped, width);
5106
5107 let after: Vec<String> = lines.split_off(bot + 1);
5109 lines.truncate(top);
5110 lines.extend(wrapped);
5111 lines.extend(after);
5112 ed.restore(lines, (top, 0));
5113 ed.mark_content_dirty();
5114}
5115
5116fn apply_case_op_to_selection<H: crate::types::Host>(
5122 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5123 op: Operator,
5124 top: (usize, usize),
5125 bot: (usize, usize),
5126 kind: RangeKind,
5127) {
5128 use hjkl_buffer::Edit;
5129 ed.push_undo();
5130 let saved_yank = ed.yank().to_string();
5131 let saved_yank_linewise = ed.vim.yank_linewise;
5132 let selection = cut_vim_range(ed, top, bot, kind);
5133 let transformed = match op {
5134 Operator::Uppercase => selection.to_uppercase(),
5135 Operator::Lowercase => selection.to_lowercase(),
5136 Operator::ToggleCase => toggle_case_str(&selection),
5137 _ => unreachable!(),
5138 };
5139 if !transformed.is_empty() {
5140 let cursor = buf_cursor_pos(&ed.buffer);
5141 ed.mutate_edit(Edit::InsertStr {
5142 at: cursor,
5143 text: transformed,
5144 });
5145 }
5146 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5147 ed.push_buffer_cursor_to_textarea();
5148 ed.set_yank(saved_yank);
5149 ed.vim.yank_linewise = saved_yank_linewise;
5150 ed.vim.mode = Mode::Normal;
5151}
5152
5153fn indent_rows<H: crate::types::Host>(
5158 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5159 top: usize,
5160 bot: usize,
5161 count: usize,
5162) {
5163 ed.sync_buffer_content_from_textarea();
5164 let width = ed.settings().shiftwidth * count.max(1);
5165 let pad: String = " ".repeat(width);
5166 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5167 let bot = bot.min(lines.len().saturating_sub(1));
5168 for line in lines.iter_mut().take(bot + 1).skip(top) {
5169 if !line.is_empty() {
5170 line.insert_str(0, &pad);
5171 }
5172 }
5173 ed.restore(lines, (top, 0));
5176 move_first_non_whitespace(ed);
5177}
5178
5179fn outdent_rows<H: crate::types::Host>(
5183 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5184 top: usize,
5185 bot: usize,
5186 count: usize,
5187) {
5188 ed.sync_buffer_content_from_textarea();
5189 let width = ed.settings().shiftwidth * count.max(1);
5190 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5191 let bot = bot.min(lines.len().saturating_sub(1));
5192 for line in lines.iter_mut().take(bot + 1).skip(top) {
5193 let strip: usize = line
5194 .chars()
5195 .take(width)
5196 .take_while(|c| *c == ' ' || *c == '\t')
5197 .count();
5198 if strip > 0 {
5199 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
5200 line.drain(..byte_len);
5201 }
5202 }
5203 ed.restore(lines, (top, 0));
5204 move_first_non_whitespace(ed);
5205}
5206
5207fn bracket_net(line: &str) -> i32 {
5234 let mut net: i32 = 0;
5235 let mut chars = line.chars().peekable();
5236 while let Some(ch) = chars.next() {
5237 match ch {
5238 '/' if chars.peek() == Some(&'/') => return net,
5240 '"' => {
5241 while let Some(c) = chars.next() {
5243 match c {
5244 '\\' => {
5245 chars.next();
5246 } '"' => break,
5248 _ => {}
5249 }
5250 }
5251 }
5252 '\'' => {
5253 let saved: Vec<char> = chars.clone().take(5).collect();
5262 let close_idx = if saved.first() == Some(&'\\') {
5263 saved.iter().skip(2).position(|&c| c == '\'').map(|p| p + 2)
5264 } else {
5265 saved.iter().skip(1).position(|&c| c == '\'').map(|p| p + 1)
5266 };
5267 if let Some(idx) = close_idx {
5268 for _ in 0..=idx {
5269 chars.next();
5270 }
5271 }
5272 }
5274 '{' | '(' | '[' => net += 1,
5275 '}' | ')' | ']' => net -= 1,
5276 _ => {}
5277 }
5278 }
5279 net
5280}
5281
5282fn auto_indent_rows<H: crate::types::Host>(
5304 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5305 top: usize,
5306 bot: usize,
5307) {
5308 ed.sync_buffer_content_from_textarea();
5309 let shiftwidth = ed.settings().shiftwidth;
5310 let expandtab = ed.settings().expandtab;
5311 let indent_unit: String = if expandtab {
5312 " ".repeat(shiftwidth)
5313 } else {
5314 "\t".to_string()
5315 };
5316
5317 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5318 let bot = bot.min(lines.len().saturating_sub(1));
5319
5320 let mut depth: i32 = 0;
5323 for line in lines.iter().take(top) {
5324 depth += bracket_net(line);
5325 if depth < 0 {
5326 depth = 0;
5327 }
5328 }
5329
5330 for line in lines.iter_mut().take(bot + 1).skip(top) {
5331 let trimmed_owned = line.trim_start().to_owned();
5332 if trimmed_owned.is_empty() {
5334 *line = String::new();
5335 continue;
5337 }
5338
5339 let starts_with_close = trimmed_owned
5341 .chars()
5342 .next()
5343 .is_some_and(|c| matches!(c, '}' | ')' | ']'));
5344 let starts_with_dot = trimmed_owned.starts_with('.')
5354 && !trimmed_owned.starts_with("..")
5355 && !trimmed_owned.starts_with(".;");
5356 let effective_depth = if starts_with_close {
5357 depth.saturating_sub(1)
5358 } else if starts_with_dot {
5359 depth.saturating_add(1)
5360 } else {
5361 depth
5362 } as usize;
5363
5364 let new_line = format!("{}{}", indent_unit.repeat(effective_depth), trimmed_owned);
5366
5367 depth += bracket_net(&trimmed_owned);
5369 if depth < 0 {
5370 depth = 0;
5371 }
5372
5373 *line = new_line;
5374 }
5375
5376 ed.restore(lines, (top, 0));
5378 move_first_non_whitespace(ed);
5379 ed.last_indent_range = Some((top, bot));
5381}
5382
5383fn toggle_case_str(s: &str) -> String {
5384 s.chars()
5385 .map(|c| {
5386 if c.is_lowercase() {
5387 c.to_uppercase().next().unwrap_or(c)
5388 } else if c.is_uppercase() {
5389 c.to_lowercase().next().unwrap_or(c)
5390 } else {
5391 c
5392 }
5393 })
5394 .collect()
5395}
5396
5397fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
5398 if a <= b { (a, b) } else { (b, a) }
5399}
5400
5401fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5406 let (row, col) = ed.cursor();
5407 let line_chars = buf_line_chars(&ed.buffer, row);
5408 let max_col = line_chars.saturating_sub(1);
5409 if col > max_col {
5410 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
5411 ed.push_buffer_cursor_to_textarea();
5412 }
5413}
5414
5415fn execute_line_op<H: crate::types::Host>(
5418 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5419 op: Operator,
5420 count: usize,
5421) {
5422 let (row, col) = ed.cursor();
5423 let total = buf_row_count(&ed.buffer);
5424 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
5425
5426 match op {
5427 Operator::Yank => {
5428 let text = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
5430 if !text.is_empty() {
5431 ed.record_yank_to_host(text.clone());
5432 ed.record_yank(text, true);
5433 }
5434 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
5437 ed.set_mark('[', (row, 0));
5438 ed.set_mark(']', (end_row, last_col));
5439 buf_set_cursor_rc(&mut ed.buffer, row, col);
5440 ed.push_buffer_cursor_to_textarea();
5441 ed.vim.mode = Mode::Normal;
5442 }
5443 Operator::Delete => {
5444 ed.push_undo();
5445 let deleted_through_last = end_row + 1 >= total;
5446 cut_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
5447 let total_after = buf_row_count(&ed.buffer);
5451 let raw_target = if deleted_through_last {
5452 row.saturating_sub(1).min(total_after.saturating_sub(1))
5453 } else {
5454 row.min(total_after.saturating_sub(1))
5455 };
5456 let target_row = if raw_target > 0
5462 && raw_target + 1 == total_after
5463 && buf_line(&ed.buffer, raw_target)
5464 .map(|s| s.is_empty())
5465 .unwrap_or(false)
5466 {
5467 raw_target - 1
5468 } else {
5469 raw_target
5470 };
5471 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5472 ed.push_buffer_cursor_to_textarea();
5473 move_first_non_whitespace(ed);
5474 ed.sticky_col = Some(ed.cursor().1);
5475 ed.vim.mode = Mode::Normal;
5476 let pos = ed.cursor();
5479 ed.set_mark('[', pos);
5480 ed.set_mark(']', pos);
5481 }
5482 Operator::Change => {
5483 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5487 ed.vim.change_mark_start = Some((row, 0));
5489 ed.push_undo();
5490 ed.sync_buffer_content_from_textarea();
5491 let payload = read_vim_range(ed, (row, col), (end_row, 0), RangeKind::Linewise);
5493 if end_row > row {
5494 ed.mutate_edit(Edit::DeleteRange {
5495 start: Position::new(row + 1, 0),
5496 end: Position::new(end_row, 0),
5497 kind: BufKind::Line,
5498 });
5499 }
5500 let line_chars = buf_line_chars(&ed.buffer, row);
5501 if line_chars > 0 {
5502 ed.mutate_edit(Edit::DeleteRange {
5503 start: Position::new(row, 0),
5504 end: Position::new(row, line_chars),
5505 kind: BufKind::Char,
5506 });
5507 }
5508 if !payload.is_empty() {
5509 ed.record_yank_to_host(payload.clone());
5510 ed.record_delete(payload, true);
5511 }
5512 buf_set_cursor_rc(&mut ed.buffer, row, 0);
5513 ed.push_buffer_cursor_to_textarea();
5514 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5515 }
5516 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5517 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), RangeKind::Linewise);
5521 move_first_non_whitespace(ed);
5524 }
5525 Operator::Indent | Operator::Outdent => {
5526 ed.push_undo();
5528 if op == Operator::Indent {
5529 indent_rows(ed, row, end_row, 1);
5530 } else {
5531 outdent_rows(ed, row, end_row, 1);
5532 }
5533 ed.sticky_col = Some(ed.cursor().1);
5534 ed.vim.mode = Mode::Normal;
5535 }
5536 Operator::Fold => unreachable!("Fold has no line-op double"),
5538 Operator::Reflow => {
5539 ed.push_undo();
5541 reflow_rows(ed, row, end_row);
5542 move_first_non_whitespace(ed);
5543 ed.sticky_col = Some(ed.cursor().1);
5544 ed.vim.mode = Mode::Normal;
5545 }
5546 Operator::AutoIndent => {
5547 ed.push_undo();
5549 auto_indent_rows(ed, row, end_row);
5550 ed.sticky_col = Some(ed.cursor().1);
5551 ed.vim.mode = Mode::Normal;
5552 }
5553 Operator::Filter => {
5554 }
5556 Operator::Comment => {
5557 }
5562 }
5563}
5564
5565pub(crate) fn apply_visual_operator<H: crate::types::Host>(
5568 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5569 op: Operator,
5570) {
5571 match ed.vim.mode {
5572 Mode::VisualLine => {
5573 let cursor_row = buf_cursor_pos(&ed.buffer).row;
5574 let top = cursor_row.min(ed.vim.visual_line_anchor);
5575 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5576 ed.vim.yank_linewise = true;
5577 match op {
5578 Operator::Yank => {
5579 let text = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
5580 if !text.is_empty() {
5581 ed.record_yank_to_host(text.clone());
5582 ed.record_yank(text, true);
5583 }
5584 buf_set_cursor_rc(&mut ed.buffer, top, 0);
5585 ed.push_buffer_cursor_to_textarea();
5586 ed.vim.mode = Mode::Normal;
5587 }
5588 Operator::Delete => {
5589 ed.push_undo();
5590 cut_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
5591 ed.vim.mode = Mode::Normal;
5592 }
5593 Operator::Change => {
5594 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5597 ed.push_undo();
5598 ed.sync_buffer_content_from_textarea();
5599 let payload = read_vim_range(ed, (top, 0), (bot, 0), RangeKind::Linewise);
5600 if bot > top {
5601 ed.mutate_edit(Edit::DeleteRange {
5602 start: Position::new(top + 1, 0),
5603 end: Position::new(bot, 0),
5604 kind: BufKind::Line,
5605 });
5606 }
5607 let line_chars = buf_line_chars(&ed.buffer, top);
5608 if line_chars > 0 {
5609 ed.mutate_edit(Edit::DeleteRange {
5610 start: Position::new(top, 0),
5611 end: Position::new(top, line_chars),
5612 kind: BufKind::Char,
5613 });
5614 }
5615 if !payload.is_empty() {
5616 ed.record_yank_to_host(payload.clone());
5617 ed.record_delete(payload, true);
5618 }
5619 buf_set_cursor_rc(&mut ed.buffer, top, 0);
5620 ed.push_buffer_cursor_to_textarea();
5621 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5622 }
5623 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5624 let bot = buf_cursor_pos(&ed.buffer)
5625 .row
5626 .max(ed.vim.visual_line_anchor);
5627 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), RangeKind::Linewise);
5628 move_first_non_whitespace(ed);
5629 }
5630 Operator::Indent | Operator::Outdent => {
5631 ed.push_undo();
5632 let (cursor_row, _) = ed.cursor();
5633 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5634 if op == Operator::Indent {
5635 indent_rows(ed, top, bot, 1);
5636 } else {
5637 outdent_rows(ed, top, bot, 1);
5638 }
5639 ed.vim.mode = Mode::Normal;
5640 }
5641 Operator::Reflow => {
5642 ed.push_undo();
5643 let (cursor_row, _) = ed.cursor();
5644 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5645 reflow_rows(ed, top, bot);
5646 ed.vim.mode = Mode::Normal;
5647 }
5648 Operator::AutoIndent => {
5649 ed.push_undo();
5650 let (cursor_row, _) = ed.cursor();
5651 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5652 auto_indent_rows(ed, top, bot);
5653 ed.vim.mode = Mode::Normal;
5654 }
5655 Operator::Filter => {}
5657 Operator::Comment => {}
5659 Operator::Fold => unreachable!("Visual zf takes its own path"),
5662 }
5663 }
5664 Mode::Visual => {
5665 ed.vim.yank_linewise = false;
5666 let anchor = ed.vim.visual_anchor;
5667 let cursor = ed.cursor();
5668 let (top, bot) = order(anchor, cursor);
5669 match op {
5670 Operator::Yank => {
5671 let text = read_vim_range(ed, top, bot, RangeKind::Inclusive);
5672 if !text.is_empty() {
5673 ed.record_yank_to_host(text.clone());
5674 ed.record_yank(text, false);
5675 }
5676 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5677 ed.push_buffer_cursor_to_textarea();
5678 ed.vim.mode = Mode::Normal;
5679 }
5680 Operator::Delete => {
5681 ed.push_undo();
5682 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
5683 ed.vim.mode = Mode::Normal;
5684 }
5685 Operator::Change => {
5686 ed.push_undo();
5687 cut_vim_range(ed, top, bot, RangeKind::Inclusive);
5688 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5689 }
5690 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5691 let anchor = ed.vim.visual_anchor;
5693 let cursor = ed.cursor();
5694 let (top, bot) = order(anchor, cursor);
5695 apply_case_op_to_selection(ed, op, top, bot, RangeKind::Inclusive);
5696 }
5697 Operator::Indent | Operator::Outdent => {
5698 ed.push_undo();
5699 let anchor = ed.vim.visual_anchor;
5700 let cursor = ed.cursor();
5701 let (top, bot) = order(anchor, cursor);
5702 if op == Operator::Indent {
5703 indent_rows(ed, top.0, bot.0, 1);
5704 } else {
5705 outdent_rows(ed, top.0, bot.0, 1);
5706 }
5707 ed.vim.mode = Mode::Normal;
5708 }
5709 Operator::Reflow => {
5710 ed.push_undo();
5711 let anchor = ed.vim.visual_anchor;
5712 let cursor = ed.cursor();
5713 let (top, bot) = order(anchor, cursor);
5714 reflow_rows(ed, top.0, bot.0);
5715 ed.vim.mode = Mode::Normal;
5716 }
5717 Operator::AutoIndent => {
5718 ed.push_undo();
5719 let anchor = ed.vim.visual_anchor;
5720 let cursor = ed.cursor();
5721 let (top, bot) = order(anchor, cursor);
5722 auto_indent_rows(ed, top.0, bot.0);
5723 ed.vim.mode = Mode::Normal;
5724 }
5725 Operator::Filter => {}
5727 Operator::Comment => {}
5729 Operator::Fold => unreachable!("Visual zf takes its own path"),
5730 }
5731 }
5732 Mode::VisualBlock => apply_block_operator(ed, op),
5733 _ => {}
5734 }
5735}
5736
5737fn block_bounds<H: crate::types::Host>(
5742 ed: &Editor<hjkl_buffer::Buffer, H>,
5743) -> (usize, usize, usize, usize) {
5744 let (ar, ac) = ed.vim.block_anchor;
5745 let (cr, _) = ed.cursor();
5746 let cc = ed.vim.block_vcol;
5747 let top = ar.min(cr);
5748 let bot = ar.max(cr);
5749 let left = ac.min(cc);
5750 let right = ac.max(cc);
5751 (top, bot, left, right)
5752}
5753
5754pub(crate) fn update_block_vcol<H: crate::types::Host>(
5759 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5760 motion: &Motion,
5761) {
5762 match motion {
5763 Motion::Left
5764 | Motion::Right
5765 | Motion::WordFwd
5766 | Motion::BigWordFwd
5767 | Motion::WordBack
5768 | Motion::BigWordBack
5769 | Motion::WordEnd
5770 | Motion::BigWordEnd
5771 | Motion::WordEndBack
5772 | Motion::BigWordEndBack
5773 | Motion::LineStart
5774 | Motion::FirstNonBlank
5775 | Motion::LineEnd
5776 | Motion::Find { .. }
5777 | Motion::FindRepeat { .. }
5778 | Motion::MatchBracket => {
5779 ed.vim.block_vcol = ed.cursor().1;
5780 }
5781 _ => {}
5783 }
5784}
5785
5786fn apply_block_operator<H: crate::types::Host>(
5791 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5792 op: Operator,
5793) {
5794 let (top, bot, left, right) = block_bounds(ed);
5795 let yank = block_yank(ed, top, bot, left, right);
5797
5798 match op {
5799 Operator::Yank => {
5800 if !yank.is_empty() {
5801 ed.record_yank_to_host(yank.clone());
5802 ed.record_yank(yank, false);
5803 }
5804 ed.vim.mode = Mode::Normal;
5805 ed.jump_cursor(top, left);
5806 }
5807 Operator::Delete => {
5808 ed.push_undo();
5809 delete_block_contents(ed, top, bot, left, right);
5810 if !yank.is_empty() {
5811 ed.record_yank_to_host(yank.clone());
5812 ed.record_delete(yank, false);
5813 }
5814 ed.vim.mode = Mode::Normal;
5815 ed.jump_cursor(top, left);
5816 }
5817 Operator::Change => {
5818 ed.push_undo();
5819 delete_block_contents(ed, top, bot, left, right);
5820 if !yank.is_empty() {
5821 ed.record_yank_to_host(yank.clone());
5822 ed.record_delete(yank, false);
5823 }
5824 ed.jump_cursor(top, left);
5825 begin_insert_noundo(
5826 ed,
5827 1,
5828 InsertReason::BlockChange {
5829 top,
5830 bot,
5831 col: left,
5832 },
5833 );
5834 }
5835 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5836 ed.push_undo();
5837 transform_block_case(ed, op, top, bot, left, right);
5838 ed.vim.mode = Mode::Normal;
5839 ed.jump_cursor(top, left);
5840 }
5841 Operator::Indent | Operator::Outdent => {
5842 ed.push_undo();
5846 if op == Operator::Indent {
5847 indent_rows(ed, top, bot, 1);
5848 } else {
5849 outdent_rows(ed, top, bot, 1);
5850 }
5851 ed.vim.mode = Mode::Normal;
5852 }
5853 Operator::Fold => unreachable!("Visual zf takes its own path"),
5854 Operator::Reflow => {
5855 ed.push_undo();
5859 reflow_rows(ed, top, bot);
5860 ed.vim.mode = Mode::Normal;
5861 }
5862 Operator::AutoIndent => {
5863 ed.push_undo();
5866 auto_indent_rows(ed, top, bot);
5867 ed.vim.mode = Mode::Normal;
5868 }
5869 Operator::Filter => {}
5871 Operator::Comment => {}
5873 }
5874}
5875
5876fn transform_block_case<H: crate::types::Host>(
5880 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5881 op: Operator,
5882 top: usize,
5883 bot: usize,
5884 left: usize,
5885 right: usize,
5886) {
5887 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5888 for r in top..=bot.min(lines.len().saturating_sub(1)) {
5889 let chars: Vec<char> = lines[r].chars().collect();
5890 if left >= chars.len() {
5891 continue;
5892 }
5893 let end = (right + 1).min(chars.len());
5894 let head: String = chars[..left].iter().collect();
5895 let mid: String = chars[left..end].iter().collect();
5896 let tail: String = chars[end..].iter().collect();
5897 let transformed = match op {
5898 Operator::Uppercase => mid.to_uppercase(),
5899 Operator::Lowercase => mid.to_lowercase(),
5900 Operator::ToggleCase => toggle_case_str(&mid),
5901 _ => mid,
5902 };
5903 lines[r] = format!("{head}{transformed}{tail}");
5904 }
5905 let saved_yank = ed.yank().to_string();
5906 let saved_linewise = ed.vim.yank_linewise;
5907 ed.restore(lines, (top, left));
5908 ed.set_yank(saved_yank);
5909 ed.vim.yank_linewise = saved_linewise;
5910}
5911
5912fn block_yank<H: crate::types::Host>(
5913 ed: &Editor<hjkl_buffer::Buffer, H>,
5914 top: usize,
5915 bot: usize,
5916 left: usize,
5917 right: usize,
5918) -> String {
5919 let rope = crate::types::Query::rope(&ed.buffer);
5920 let n = rope.len_lines();
5921 let mut rows: Vec<String> = Vec::new();
5922 for r in top..=bot {
5923 if r >= n {
5924 break;
5925 }
5926 let line = rope_line_to_str(&rope, r);
5927 let chars: Vec<char> = line.chars().collect();
5928 let end = (right + 1).min(chars.len());
5929 if left >= chars.len() {
5930 rows.push(String::new());
5931 } else {
5932 rows.push(chars[left..end].iter().collect());
5933 }
5934 }
5935 rows.join("\n")
5936}
5937
5938fn delete_block_contents<H: crate::types::Host>(
5939 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5940 top: usize,
5941 bot: usize,
5942 left: usize,
5943 right: usize,
5944) {
5945 use hjkl_buffer::{Edit, MotionKind, Position};
5946 ed.sync_buffer_content_from_textarea();
5947 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
5948 if last_row < top {
5949 return;
5950 }
5951 ed.mutate_edit(Edit::DeleteRange {
5952 start: Position::new(top, left),
5953 end: Position::new(last_row, right),
5954 kind: MotionKind::Block,
5955 });
5956 ed.push_buffer_cursor_to_textarea();
5957}
5958
5959pub(crate) fn block_replace<H: crate::types::Host>(
5961 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5962 ch: char,
5963) {
5964 let (top, bot, left, right) = block_bounds(ed);
5965 ed.push_undo();
5966 ed.sync_buffer_content_from_textarea();
5967 let mut lines: Vec<String> = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
5968 for r in top..=bot.min(lines.len().saturating_sub(1)) {
5969 let chars: Vec<char> = lines[r].chars().collect();
5970 if left >= chars.len() {
5971 continue;
5972 }
5973 let end = (right + 1).min(chars.len());
5974 let before: String = chars[..left].iter().collect();
5975 let middle: String = std::iter::repeat_n(ch, end - left).collect();
5976 let after: String = chars[end..].iter().collect();
5977 lines[r] = format!("{before}{middle}{after}");
5978 }
5979 reset_textarea_lines(ed, lines);
5980 ed.vim.mode = Mode::Normal;
5981 ed.jump_cursor(top, left);
5982}
5983
5984fn reset_textarea_lines<H: crate::types::Host>(
5988 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5989 lines: Vec<String>,
5990) {
5991 let cursor = ed.cursor();
5992 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
5993 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
5994 ed.mark_content_dirty();
5995}
5996
5997type Pos = (usize, usize);
6003
6004pub(crate) fn text_object_range<H: crate::types::Host>(
6008 ed: &Editor<hjkl_buffer::Buffer, H>,
6009 obj: TextObject,
6010 inner: bool,
6011) -> Option<(Pos, Pos, RangeKind)> {
6012 match obj {
6013 TextObject::Word { big } => {
6014 word_text_object(ed, inner, big).map(|(s, e)| (s, e, RangeKind::Exclusive))
6015 }
6016 TextObject::Quote(q) => {
6017 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6018 }
6019 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
6020 TextObject::Paragraph => {
6021 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Linewise))
6022 }
6023 TextObject::XmlTag => tag_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive)),
6024 TextObject::Sentence => {
6025 sentence_text_object(ed, inner).map(|(s, e)| (s, e, RangeKind::Exclusive))
6026 }
6027 }
6028}
6029
6030fn sentence_boundary<H: crate::types::Host>(
6034 ed: &Editor<hjkl_buffer::Buffer, H>,
6035 forward: bool,
6036) -> Option<(usize, usize)> {
6037 let rope = crate::types::Query::rope(&ed.buffer);
6038 let n_lines = rope.len_lines();
6039 if n_lines == 0 {
6040 return None;
6041 }
6042 let line_lens: Vec<usize> = (0..n_lines)
6044 .map(|r| rope_line_to_str(&rope, r).chars().count())
6045 .collect();
6046 let pos_to_idx = |pos: (usize, usize)| -> usize {
6047 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6048 idx + pos.1
6049 };
6050 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6051 for (r, &len) in line_lens.iter().enumerate() {
6052 if idx <= len {
6053 return (r, idx);
6054 }
6055 idx -= len + 1;
6056 }
6057 let last = n_lines.saturating_sub(1);
6058 (last, line_lens[last])
6059 };
6060 let mut chars: Vec<char> = rope.chars().collect();
6063 if chars.last() == Some(&'\n') {
6065 chars.pop();
6066 }
6067 if chars.is_empty() {
6068 return None;
6069 }
6070 let total = chars.len();
6071 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
6072 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6073
6074 if forward {
6075 let mut i = cursor_idx + 1;
6078 while i < total {
6079 if is_terminator(chars[i]) {
6080 while i + 1 < total && is_terminator(chars[i + 1]) {
6081 i += 1;
6082 }
6083 if i + 1 >= total {
6084 return None;
6085 }
6086 if chars[i + 1].is_whitespace() {
6087 let mut j = i + 1;
6088 while j < total && chars[j].is_whitespace() {
6089 j += 1;
6090 }
6091 if j >= total {
6092 return None;
6093 }
6094 return Some(idx_to_pos(j));
6095 }
6096 }
6097 i += 1;
6098 }
6099 None
6100 } else {
6101 let find_start = |from: usize| -> Option<usize> {
6105 let mut start = from;
6106 while start > 0 {
6107 let prev = chars[start - 1];
6108 if prev.is_whitespace() {
6109 let mut k = start - 1;
6110 while k > 0 && chars[k - 1].is_whitespace() {
6111 k -= 1;
6112 }
6113 if k > 0 && is_terminator(chars[k - 1]) {
6114 break;
6115 }
6116 }
6117 start -= 1;
6118 }
6119 while start < total && chars[start].is_whitespace() {
6120 start += 1;
6121 }
6122 (start < total).then_some(start)
6123 };
6124 let current_start = find_start(cursor_idx)?;
6125 if current_start < cursor_idx {
6126 return Some(idx_to_pos(current_start));
6127 }
6128 let mut k = current_start;
6131 while k > 0 && chars[k - 1].is_whitespace() {
6132 k -= 1;
6133 }
6134 if k == 0 {
6135 return None;
6136 }
6137 let prev_start = find_start(k - 1)?;
6138 Some(idx_to_pos(prev_start))
6139 }
6140}
6141
6142fn sentence_text_object<H: crate::types::Host>(
6148 ed: &Editor<hjkl_buffer::Buffer, H>,
6149 inner: bool,
6150) -> Option<((usize, usize), (usize, usize))> {
6151 let rope = crate::types::Query::rope(&ed.buffer);
6152 let n_lines = rope.len_lines();
6153 if n_lines == 0 {
6154 return None;
6155 }
6156 let line_lens: Vec<usize> = (0..n_lines)
6159 .map(|r| rope_line_to_str(&rope, r).chars().count())
6160 .collect();
6161 let pos_to_idx = |pos: (usize, usize)| -> usize {
6162 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6163 idx + pos.1
6164 };
6165 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6166 for (r, &len) in line_lens.iter().enumerate() {
6167 if idx <= len {
6168 return (r, idx);
6169 }
6170 idx -= len + 1;
6171 }
6172 let last = n_lines.saturating_sub(1);
6173 (last, line_lens[last])
6174 };
6175 let mut chars: Vec<char> = rope.chars().collect();
6176 if chars.last() == Some(&'\n') {
6177 chars.pop();
6178 }
6179 if chars.is_empty() {
6180 return None;
6181 }
6182
6183 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
6184 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
6185
6186 let mut start = cursor_idx;
6190 while start > 0 {
6191 let prev = chars[start - 1];
6192 if prev.is_whitespace() {
6193 let mut k = start - 1;
6197 while k > 0 && chars[k - 1].is_whitespace() {
6198 k -= 1;
6199 }
6200 if k > 0 && is_terminator(chars[k - 1]) {
6201 break;
6202 }
6203 }
6204 start -= 1;
6205 }
6206 while start < chars.len() && chars[start].is_whitespace() {
6209 start += 1;
6210 }
6211 if start >= chars.len() {
6212 return None;
6213 }
6214
6215 let mut end = start;
6218 while end < chars.len() {
6219 if is_terminator(chars[end]) {
6220 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
6222 end += 1;
6223 }
6224 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
6227 break;
6228 }
6229 }
6230 end += 1;
6231 }
6232 let end_idx = (end + 1).min(chars.len());
6234
6235 let final_end = if inner {
6236 end_idx
6237 } else {
6238 let mut e = end_idx;
6242 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
6243 e += 1;
6244 }
6245 e
6246 };
6247
6248 Some((idx_to_pos(start), idx_to_pos(final_end)))
6249}
6250
6251fn tag_text_object<H: crate::types::Host>(
6255 ed: &Editor<hjkl_buffer::Buffer, H>,
6256 inner: bool,
6257) -> Option<((usize, usize), (usize, usize))> {
6258 let rope = crate::types::Query::rope(&ed.buffer);
6259 let n_lines = rope.len_lines();
6260 if n_lines == 0 {
6261 return None;
6262 }
6263 let line_lens: Vec<usize> = (0..n_lines)
6267 .map(|r| rope_line_to_str(&rope, r).chars().count())
6268 .collect();
6269 let pos_to_idx = |pos: (usize, usize)| -> usize {
6270 let idx: usize = line_lens.iter().take(pos.0).map(|&len| len + 1).sum();
6271 idx + pos.1
6272 };
6273 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
6274 for (r, &len) in line_lens.iter().enumerate() {
6275 if idx <= len {
6276 return (r, idx);
6277 }
6278 idx -= len + 1;
6279 }
6280 let last = n_lines.saturating_sub(1);
6281 (last, line_lens[last])
6282 };
6283 let mut chars: Vec<char> = rope.chars().collect();
6284 if chars.last() == Some(&'\n') {
6285 chars.pop();
6286 }
6287 let cursor_idx = pos_to_idx(ed.cursor());
6288
6289 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
6297 let mut next_after: Option<(usize, usize, usize, usize)> = None;
6298 let mut i = 0;
6299 while i < chars.len() {
6300 if chars[i] != '<' {
6301 i += 1;
6302 continue;
6303 }
6304 let mut j = i + 1;
6305 while j < chars.len() && chars[j] != '>' {
6306 j += 1;
6307 }
6308 if j >= chars.len() {
6309 break;
6310 }
6311 let inside: String = chars[i + 1..j].iter().collect();
6312 let close_end = j + 1;
6313 let trimmed = inside.trim();
6314 if trimmed.starts_with('!') || trimmed.starts_with('?') {
6315 i = close_end;
6316 continue;
6317 }
6318 if let Some(rest) = trimmed.strip_prefix('/') {
6319 let name = rest.split_whitespace().next().unwrap_or("").to_string();
6320 if !name.is_empty()
6321 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
6322 {
6323 let (open_start, content_start, _) = stack[stack_idx].clone();
6324 stack.truncate(stack_idx);
6325 let content_end = i;
6326 let candidate = (open_start, content_start, content_end, close_end);
6327 if cursor_idx >= content_start && cursor_idx <= content_end {
6328 innermost = match innermost {
6329 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
6330 Some(candidate)
6331 }
6332 None => Some(candidate),
6333 existing => existing,
6334 };
6335 } else if open_start >= cursor_idx && next_after.is_none() {
6336 next_after = Some(candidate);
6337 }
6338 }
6339 } else if !trimmed.ends_with('/') {
6340 let name: String = trimmed
6341 .split(|c: char| c.is_whitespace() || c == '/')
6342 .next()
6343 .unwrap_or("")
6344 .to_string();
6345 if !name.is_empty() {
6346 stack.push((i, close_end, name));
6347 }
6348 }
6349 i = close_end;
6350 }
6351
6352 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
6353 if inner {
6354 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
6355 } else {
6356 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
6357 }
6358}
6359
6360fn is_wordchar(c: char) -> bool {
6361 c.is_alphanumeric() || c == '_'
6362}
6363
6364pub(crate) use hjkl_buffer::is_keyword_char;
6368
6369fn word_text_object<H: crate::types::Host>(
6370 ed: &Editor<hjkl_buffer::Buffer, H>,
6371 inner: bool,
6372 big: bool,
6373) -> Option<((usize, usize), (usize, usize))> {
6374 let (row, col) = ed.cursor();
6375 let line = buf_line(&ed.buffer, row)?;
6376 let chars: Vec<char> = line.chars().collect();
6377 if chars.is_empty() {
6378 return None;
6379 }
6380 let at = col.min(chars.len().saturating_sub(1));
6381 let classify = |c: char| -> u8 {
6382 if c.is_whitespace() {
6383 0
6384 } else if big || is_wordchar(c) {
6385 1
6386 } else {
6387 2
6388 }
6389 };
6390 let cls = classify(chars[at]);
6391 let mut start = at;
6392 while start > 0 && classify(chars[start - 1]) == cls {
6393 start -= 1;
6394 }
6395 let mut end = at;
6396 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
6397 end += 1;
6398 }
6399 let char_byte = |i: usize| {
6401 if i >= chars.len() {
6402 line.len()
6403 } else {
6404 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
6405 }
6406 };
6407 let mut start_col = char_byte(start);
6408 let mut end_col = char_byte(end + 1);
6410 if !inner {
6411 let mut t = end + 1;
6413 let mut included_trailing = false;
6414 while t < chars.len() && chars[t].is_whitespace() {
6415 included_trailing = true;
6416 t += 1;
6417 }
6418 if included_trailing {
6419 end_col = char_byte(t);
6420 } else {
6421 let mut s = start;
6422 while s > 0 && chars[s - 1].is_whitespace() {
6423 s -= 1;
6424 }
6425 start_col = char_byte(s);
6426 }
6427 }
6428 Some(((row, start_col), (row, end_col)))
6429}
6430
6431fn quote_text_object<H: crate::types::Host>(
6432 ed: &Editor<hjkl_buffer::Buffer, H>,
6433 q: char,
6434 inner: bool,
6435) -> Option<((usize, usize), (usize, usize))> {
6436 let (row, col) = ed.cursor();
6437 let line = buf_line(&ed.buffer, row)?;
6438 let bytes = line.as_bytes();
6439 let q_byte = q as u8;
6440 let mut positions: Vec<usize> = Vec::new();
6442 for (i, &b) in bytes.iter().enumerate() {
6443 if b == q_byte {
6444 positions.push(i);
6445 }
6446 }
6447 if positions.len() < 2 {
6448 return None;
6449 }
6450 let mut open_idx: Option<usize> = None;
6451 let mut close_idx: Option<usize> = None;
6452 for pair in positions.chunks(2) {
6453 if pair.len() < 2 {
6454 break;
6455 }
6456 if col >= pair[0] && col <= pair[1] {
6457 open_idx = Some(pair[0]);
6458 close_idx = Some(pair[1]);
6459 break;
6460 }
6461 if col < pair[0] {
6462 open_idx = Some(pair[0]);
6463 close_idx = Some(pair[1]);
6464 break;
6465 }
6466 }
6467 let open = open_idx?;
6468 let close = close_idx?;
6469 if inner {
6471 if close <= open + 1 {
6472 return None;
6473 }
6474 Some(((row, open + 1), (row, close)))
6475 } else {
6476 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
6483 let mut end = after_close;
6485 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
6486 end += 1;
6487 }
6488 Some(((row, open), (row, end)))
6489 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
6490 let mut start = open;
6492 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
6493 start -= 1;
6494 }
6495 Some(((row, start), (row, close + 1)))
6496 } else {
6497 Some(((row, open), (row, close + 1)))
6498 }
6499 }
6500}
6501
6502fn bracket_text_object<H: crate::types::Host>(
6503 ed: &Editor<hjkl_buffer::Buffer, H>,
6504 open: char,
6505 inner: bool,
6506) -> Option<(Pos, Pos, RangeKind)> {
6507 let close = match open {
6508 '(' => ')',
6509 '[' => ']',
6510 '{' => '}',
6511 '<' => '>',
6512 _ => return None,
6513 };
6514 let (row, col) = ed.cursor();
6515 let lines = rope_to_lines_vec(&crate::types::Query::rope(&ed.buffer));
6516 let lines = lines.as_slice();
6517 let open_pos = find_open_bracket(lines, row, col, open, close)
6522 .or_else(|| find_next_open(lines, row, col, open))?;
6523 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
6524 if inner {
6526 if close_pos.0 > open_pos.0 + 1 {
6532 let inner_row_start = open_pos.0 + 1;
6534 let inner_row_end = close_pos.0 - 1;
6535 let end_col = lines
6536 .get(inner_row_end)
6537 .map(|l| l.chars().count())
6538 .unwrap_or(0);
6539 return Some((
6540 (inner_row_start, 0),
6541 (inner_row_end, end_col),
6542 RangeKind::Linewise,
6543 ));
6544 }
6545 let inner_start = advance_pos(lines, open_pos);
6546 if inner_start.0 > close_pos.0
6547 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
6548 {
6549 return None;
6550 }
6551 Some((inner_start, close_pos, RangeKind::Exclusive))
6552 } else {
6553 Some((
6554 open_pos,
6555 advance_pos(lines, close_pos),
6556 RangeKind::Exclusive,
6557 ))
6558 }
6559}
6560
6561fn find_open_bracket(
6562 lines: &[String],
6563 row: usize,
6564 col: usize,
6565 open: char,
6566 close: char,
6567) -> Option<(usize, usize)> {
6568 let mut depth: i32 = 0;
6569 let mut r = row;
6570 let mut c = col as isize;
6571 loop {
6572 let cur = &lines[r];
6573 let chars: Vec<char> = cur.chars().collect();
6574 if (c as usize) >= chars.len() {
6578 c = chars.len() as isize - 1;
6579 }
6580 while c >= 0 {
6581 let ch = chars[c as usize];
6582 if ch == close {
6583 depth += 1;
6584 } else if ch == open {
6585 if depth == 0 {
6586 return Some((r, c as usize));
6587 }
6588 depth -= 1;
6589 }
6590 c -= 1;
6591 }
6592 if r == 0 {
6593 return None;
6594 }
6595 r -= 1;
6596 c = lines[r].chars().count() as isize - 1;
6597 }
6598}
6599
6600fn find_close_bracket(
6601 lines: &[String],
6602 row: usize,
6603 start_col: usize,
6604 open: char,
6605 close: char,
6606) -> Option<(usize, usize)> {
6607 let mut depth: i32 = 0;
6608 let mut r = row;
6609 let mut c = start_col;
6610 loop {
6611 let cur = &lines[r];
6612 let chars: Vec<char> = cur.chars().collect();
6613 while c < chars.len() {
6614 let ch = chars[c];
6615 if ch == open {
6616 depth += 1;
6617 } else if ch == close {
6618 if depth == 0 {
6619 return Some((r, c));
6620 }
6621 depth -= 1;
6622 }
6623 c += 1;
6624 }
6625 if r + 1 >= lines.len() {
6626 return None;
6627 }
6628 r += 1;
6629 c = 0;
6630 }
6631}
6632
6633fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
6637 let mut r = row;
6638 let mut c = col;
6639 while r < lines.len() {
6640 let chars: Vec<char> = lines[r].chars().collect();
6641 while c < chars.len() {
6642 if chars[c] == open {
6643 return Some((r, c));
6644 }
6645 c += 1;
6646 }
6647 r += 1;
6648 c = 0;
6649 }
6650 None
6651}
6652
6653fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
6654 let (r, c) = pos;
6655 let line_len = lines[r].chars().count();
6656 if c < line_len {
6657 (r, c + 1)
6658 } else if r + 1 < lines.len() {
6659 (r + 1, 0)
6660 } else {
6661 pos
6662 }
6663}
6664
6665fn paragraph_text_object<H: crate::types::Host>(
6666 ed: &Editor<hjkl_buffer::Buffer, H>,
6667 inner: bool,
6668) -> Option<((usize, usize), (usize, usize))> {
6669 let (row, _) = ed.cursor();
6670 let rope = crate::types::Query::rope(&ed.buffer);
6671 let n_lines = rope.len_lines();
6672 if n_lines == 0 {
6673 return None;
6674 }
6675 let is_blank = |r: usize| -> bool {
6677 if r >= n_lines {
6678 return true;
6679 }
6680 rope_line_to_str(&rope, r).trim().is_empty()
6681 };
6682 if is_blank(row) {
6683 return None;
6684 }
6685 let mut top = row;
6686 while top > 0 && !is_blank(top - 1) {
6687 top -= 1;
6688 }
6689 let mut bot = row;
6690 while bot + 1 < n_lines && !is_blank(bot + 1) {
6691 bot += 1;
6692 }
6693 if !inner && bot + 1 < n_lines && is_blank(bot + 1) {
6695 bot += 1;
6696 }
6697 let end_col = rope_line_to_str(&rope, bot).chars().count();
6698 Some(((top, 0), (bot, end_col)))
6699}
6700
6701fn read_vim_range<H: crate::types::Host>(
6707 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6708 start: (usize, usize),
6709 end: (usize, usize),
6710 kind: RangeKind,
6711) -> String {
6712 let (top, bot) = order(start, end);
6713 ed.sync_buffer_content_from_textarea();
6714 let rope = crate::types::Query::rope(&ed.buffer);
6715 let n_lines = rope.len_lines();
6716 match kind {
6717 RangeKind::Linewise => {
6718 let lo = top.0;
6719 let hi = bot.0.min(n_lines.saturating_sub(1));
6720 let mut text = rope_row_range_str(&rope, lo, hi);
6721 text.push('\n');
6722 text
6723 }
6724 RangeKind::Inclusive | RangeKind::Exclusive => {
6725 let inclusive = matches!(kind, RangeKind::Inclusive);
6726 let mut out = String::new();
6728 for row in top.0..=bot.0 {
6729 if row >= n_lines {
6730 break;
6731 }
6732 let line = rope_line_to_str(&rope, row);
6733 let lo = if row == top.0 { top.1 } else { 0 };
6734 let hi_unclamped = if row == bot.0 {
6735 if inclusive { bot.1 + 1 } else { bot.1 }
6736 } else {
6737 line.chars().count() + 1
6738 };
6739 let row_chars: Vec<char> = line.chars().collect();
6740 let hi = hi_unclamped.min(row_chars.len());
6741 if lo < hi {
6742 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
6743 }
6744 if row < bot.0 {
6745 out.push('\n');
6746 }
6747 }
6748 out
6749 }
6750 }
6751}
6752
6753fn cut_vim_range<H: crate::types::Host>(
6762 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6763 start: (usize, usize),
6764 end: (usize, usize),
6765 kind: RangeKind,
6766) -> String {
6767 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
6768 let (top, bot) = order(start, end);
6769 ed.sync_buffer_content_from_textarea();
6770 let (buf_start, buf_end, buf_kind) = match kind {
6771 RangeKind::Linewise => (
6772 Position::new(top.0, 0),
6773 Position::new(bot.0, 0),
6774 BufKind::Line,
6775 ),
6776 RangeKind::Inclusive => {
6777 let line_chars = buf_line_chars(&ed.buffer, bot.0);
6778 let next = if bot.1 < line_chars {
6782 Position::new(bot.0, bot.1 + 1)
6783 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
6784 Position::new(bot.0 + 1, 0)
6785 } else {
6786 Position::new(bot.0, line_chars)
6787 };
6788 (Position::new(top.0, top.1), next, BufKind::Char)
6789 }
6790 RangeKind::Exclusive => (
6791 Position::new(top.0, top.1),
6792 Position::new(bot.0, bot.1),
6793 BufKind::Char,
6794 ),
6795 };
6796 let inverse = ed.mutate_edit(Edit::DeleteRange {
6797 start: buf_start,
6798 end: buf_end,
6799 kind: buf_kind,
6800 });
6801 let text = match inverse {
6802 Edit::InsertStr { text, .. } => text,
6803 _ => String::new(),
6804 };
6805 if !text.is_empty() {
6806 ed.record_yank_to_host(text.clone());
6807 ed.record_delete(text.clone(), matches!(kind, RangeKind::Linewise));
6808 }
6809 ed.push_buffer_cursor_to_textarea();
6810 text
6811}
6812
6813fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6819 use hjkl_buffer::{Edit, MotionKind, Position};
6820 ed.sync_buffer_content_from_textarea();
6821 let cursor = buf_cursor_pos(&ed.buffer);
6822 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6823 if cursor.col >= line_chars {
6824 return;
6825 }
6826 let inverse = ed.mutate_edit(Edit::DeleteRange {
6827 start: cursor,
6828 end: Position::new(cursor.row, line_chars),
6829 kind: MotionKind::Char,
6830 });
6831 if let Edit::InsertStr { text, .. } = inverse
6832 && !text.is_empty()
6833 {
6834 ed.record_yank_to_host(text.clone());
6835 ed.vim.yank_linewise = false;
6836 ed.set_yank(text);
6837 }
6838 buf_set_cursor_pos(&mut ed.buffer, cursor);
6839 ed.push_buffer_cursor_to_textarea();
6840}
6841
6842fn do_char_delete<H: crate::types::Host>(
6843 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6844 forward: bool,
6845 count: usize,
6846) {
6847 use hjkl_buffer::{Edit, MotionKind, Position};
6848 ed.push_undo();
6849 ed.sync_buffer_content_from_textarea();
6850 let mut deleted = String::new();
6853 for _ in 0..count {
6854 let cursor = buf_cursor_pos(&ed.buffer);
6855 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6856 if forward {
6857 if cursor.col >= line_chars {
6860 continue;
6861 }
6862 let inverse = ed.mutate_edit(Edit::DeleteRange {
6863 start: cursor,
6864 end: Position::new(cursor.row, cursor.col + 1),
6865 kind: MotionKind::Char,
6866 });
6867 if let Edit::InsertStr { text, .. } = inverse {
6868 deleted.push_str(&text);
6869 }
6870 } else {
6871 if cursor.col == 0 {
6873 continue;
6874 }
6875 let inverse = ed.mutate_edit(Edit::DeleteRange {
6876 start: Position::new(cursor.row, cursor.col - 1),
6877 end: cursor,
6878 kind: MotionKind::Char,
6879 });
6880 if let Edit::InsertStr { text, .. } = inverse {
6881 deleted = text + &deleted;
6884 }
6885 }
6886 }
6887 if !deleted.is_empty() {
6888 ed.record_yank_to_host(deleted.clone());
6889 ed.record_delete(deleted, false);
6890 }
6891 ed.push_buffer_cursor_to_textarea();
6892}
6893
6894pub(crate) fn adjust_number<H: crate::types::Host>(
6898 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6899 delta: i64,
6900) -> bool {
6901 use hjkl_buffer::{Edit, MotionKind, Position};
6902 ed.sync_buffer_content_from_textarea();
6903 let cursor = buf_cursor_pos(&ed.buffer);
6904 let row = cursor.row;
6905 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
6906 Some(l) => l.chars().collect(),
6907 None => return false,
6908 };
6909 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
6910 return false;
6911 };
6912 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
6913 digit_start - 1
6914 } else {
6915 digit_start
6916 };
6917 let mut span_end = digit_start;
6918 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
6919 span_end += 1;
6920 }
6921 let s: String = chars[span_start..span_end].iter().collect();
6922 let Ok(n) = s.parse::<i64>() else {
6923 return false;
6924 };
6925 let new_s = n.saturating_add(delta).to_string();
6926
6927 ed.push_undo();
6928 let span_start_pos = Position::new(row, span_start);
6929 let span_end_pos = Position::new(row, span_end);
6930 ed.mutate_edit(Edit::DeleteRange {
6931 start: span_start_pos,
6932 end: span_end_pos,
6933 kind: MotionKind::Char,
6934 });
6935 ed.mutate_edit(Edit::InsertStr {
6936 at: span_start_pos,
6937 text: new_s.clone(),
6938 });
6939 let new_len = new_s.chars().count();
6940 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
6941 ed.push_buffer_cursor_to_textarea();
6942 true
6943}
6944
6945pub(crate) fn replace_char<H: crate::types::Host>(
6946 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6947 ch: char,
6948 count: usize,
6949) {
6950 use hjkl_buffer::{Edit, MotionKind, Position};
6951 ed.push_undo();
6952 ed.sync_buffer_content_from_textarea();
6953 for _ in 0..count {
6954 let cursor = buf_cursor_pos(&ed.buffer);
6955 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6956 if cursor.col >= line_chars {
6957 break;
6958 }
6959 ed.mutate_edit(Edit::DeleteRange {
6960 start: cursor,
6961 end: Position::new(cursor.row, cursor.col + 1),
6962 kind: MotionKind::Char,
6963 });
6964 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
6965 }
6966 crate::motions::move_left(&mut ed.buffer, 1);
6968 ed.push_buffer_cursor_to_textarea();
6969}
6970
6971fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6972 use hjkl_buffer::{Edit, MotionKind, Position};
6973 ed.sync_buffer_content_from_textarea();
6974 let cursor = buf_cursor_pos(&ed.buffer);
6975 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
6976 return;
6977 };
6978 let toggled = if c.is_uppercase() {
6979 c.to_lowercase().next().unwrap_or(c)
6980 } else {
6981 c.to_uppercase().next().unwrap_or(c)
6982 };
6983 ed.mutate_edit(Edit::DeleteRange {
6984 start: cursor,
6985 end: Position::new(cursor.row, cursor.col + 1),
6986 kind: MotionKind::Char,
6987 });
6988 ed.mutate_edit(Edit::InsertChar {
6989 at: cursor,
6990 ch: toggled,
6991 });
6992}
6993
6994fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6995 use hjkl_buffer::{Edit, Position};
6996 ed.sync_buffer_content_from_textarea();
6997 let row = buf_cursor_pos(&ed.buffer).row;
6998 if row + 1 >= buf_row_count(&ed.buffer) {
6999 return;
7000 }
7001 let cur_line = buf_line(&ed.buffer, row).unwrap_or_default();
7002 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or_default();
7003 let next_trimmed = next_raw.trim_start();
7004 let cur_chars = cur_line.chars().count();
7005 let next_chars = next_raw.chars().count();
7006 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
7009 " "
7010 } else {
7011 ""
7012 };
7013 let joined = format!("{cur_line}{separator}{next_trimmed}");
7014 ed.mutate_edit(Edit::Replace {
7015 start: Position::new(row, 0),
7016 end: Position::new(row + 1, next_chars),
7017 with: joined,
7018 });
7019 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
7023 ed.push_buffer_cursor_to_textarea();
7024}
7025
7026fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7029 use hjkl_buffer::Edit;
7030 ed.sync_buffer_content_from_textarea();
7031 let row = buf_cursor_pos(&ed.buffer).row;
7032 if row + 1 >= buf_row_count(&ed.buffer) {
7033 return;
7034 }
7035 let join_col = buf_line_chars(&ed.buffer, row);
7036 ed.mutate_edit(Edit::JoinLines {
7037 row,
7038 count: 1,
7039 with_space: false,
7040 });
7041 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
7043 ed.push_buffer_cursor_to_textarea();
7044}
7045
7046fn do_paste<H: crate::types::Host>(
7047 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7048 before: bool,
7049 count: usize,
7050) {
7051 use hjkl_buffer::{Edit, Position};
7052 ed.push_undo();
7053 let selector = ed.vim.pending_register.take();
7058 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
7059 Some(slot) => (slot.text.clone(), slot.linewise),
7060 None => {
7066 let s = &ed.registers().unnamed;
7067 (s.text.clone(), s.linewise)
7068 }
7069 };
7070 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
7074 let original_row_for_linewise_after = if linewise && !before {
7080 Some(buf_cursor_pos(&ed.buffer).row)
7081 } else {
7082 None
7083 };
7084 for _ in 0..count {
7085 ed.sync_buffer_content_from_textarea();
7086 let yank = yank.clone();
7087 if yank.is_empty() {
7088 continue;
7089 }
7090 if linewise {
7091 let text = yank.trim_matches('\n').to_string();
7095 let row = buf_cursor_pos(&ed.buffer).row;
7096 let target_row = if before {
7097 ed.mutate_edit(Edit::InsertStr {
7098 at: Position::new(row, 0),
7099 text: format!("{text}\n"),
7100 });
7101 row
7102 } else {
7103 let line_chars = buf_line_chars(&ed.buffer, row);
7104 ed.mutate_edit(Edit::InsertStr {
7105 at: Position::new(row, line_chars),
7106 text: format!("\n{text}"),
7107 });
7108 row + 1
7109 };
7110 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
7111 crate::motions::move_first_non_blank(&mut ed.buffer);
7112 ed.push_buffer_cursor_to_textarea();
7113 let payload_lines = text.lines().count().max(1);
7115 let bot_row = target_row + payload_lines - 1;
7116 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
7117 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
7118 } else {
7119 let cursor = buf_cursor_pos(&ed.buffer);
7123 let at = if before {
7124 cursor
7125 } else {
7126 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
7127 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
7128 };
7129 ed.mutate_edit(Edit::InsertStr {
7130 at,
7131 text: yank.clone(),
7132 });
7133 crate::motions::move_left(&mut ed.buffer, 1);
7136 ed.push_buffer_cursor_to_textarea();
7137 let lo = (at.row, at.col);
7139 let hi = ed.cursor();
7140 paste_mark = Some((lo, hi));
7141 }
7142 }
7143 if let Some((lo, hi)) = paste_mark {
7144 ed.set_mark('[', lo);
7145 ed.set_mark(']', hi);
7146 }
7147 if let Some(orig_row) = original_row_for_linewise_after {
7152 let first_target = orig_row.saturating_add(1);
7153 buf_set_cursor_rc(&mut ed.buffer, first_target, 0);
7154 crate::motions::move_first_non_blank(&mut ed.buffer);
7155 ed.push_buffer_cursor_to_textarea();
7156 }
7157 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
7159}
7160
7161pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7162 if let Some((rope, cursor)) = ed.undo_stack.pop() {
7163 let current = ed.snapshot();
7164 ed.redo_stack.push(current);
7165 ed.restore_rope(rope, cursor);
7166 }
7167 ed.vim.mode = Mode::Normal;
7168 clamp_cursor_to_normal_mode(ed);
7172}
7173
7174pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
7175 if let Some((rope, cursor)) = ed.redo_stack.pop() {
7176 let current = ed.snapshot();
7177 ed.undo_stack.push(current);
7178 ed.cap_undo();
7179 ed.restore_rope(rope, cursor);
7180 }
7181 ed.vim.mode = Mode::Normal;
7182}
7183
7184fn replay_insert_and_finish<H: crate::types::Host>(
7191 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7192 text: &str,
7193) {
7194 use hjkl_buffer::{Edit, Position};
7195 let cursor = ed.cursor();
7196 ed.mutate_edit(Edit::InsertStr {
7197 at: Position::new(cursor.0, cursor.1),
7198 text: text.to_string(),
7199 });
7200 if ed.vim.insert_session.take().is_some() {
7201 if ed.cursor().1 > 0 {
7202 crate::motions::move_left(&mut ed.buffer, 1);
7203 ed.push_buffer_cursor_to_textarea();
7204 }
7205 ed.vim.mode = Mode::Normal;
7206 }
7207}
7208
7209pub(crate) fn replay_last_change<H: crate::types::Host>(
7210 ed: &mut Editor<hjkl_buffer::Buffer, H>,
7211 outer_count: usize,
7212) {
7213 let Some(change) = ed.vim.last_change.clone() else {
7214 return;
7215 };
7216 ed.vim.replaying = true;
7217 let scale = if outer_count > 0 { outer_count } else { 1 };
7218 match change {
7219 LastChange::OpMotion {
7220 op,
7221 motion,
7222 count,
7223 inserted,
7224 } => {
7225 let total = count.max(1) * scale;
7226 apply_op_with_motion(ed, op, &motion, total);
7227 if let Some(text) = inserted {
7228 replay_insert_and_finish(ed, &text);
7229 }
7230 }
7231 LastChange::OpTextObj {
7232 op,
7233 obj,
7234 inner,
7235 inserted,
7236 } => {
7237 apply_op_with_text_object(ed, op, obj, inner);
7238 if let Some(text) = inserted {
7239 replay_insert_and_finish(ed, &text);
7240 }
7241 }
7242 LastChange::LineOp {
7243 op,
7244 count,
7245 inserted,
7246 } => {
7247 let total = count.max(1) * scale;
7248 execute_line_op(ed, op, total);
7249 if let Some(text) = inserted {
7250 replay_insert_and_finish(ed, &text);
7251 }
7252 }
7253 LastChange::CharDel { forward, count } => {
7254 do_char_delete(ed, forward, count * scale);
7255 }
7256 LastChange::ReplaceChar { ch, count } => {
7257 replace_char(ed, ch, count * scale);
7258 }
7259 LastChange::ToggleCase { count } => {
7260 for _ in 0..count * scale {
7261 ed.push_undo();
7262 toggle_case_at_cursor(ed);
7263 }
7264 }
7265 LastChange::JoinLine { count } => {
7266 for _ in 0..count * scale {
7267 ed.push_undo();
7268 join_line(ed);
7269 }
7270 }
7271 LastChange::Paste { before, count } => {
7272 do_paste(ed, before, count * scale);
7273 }
7274 LastChange::DeleteToEol { inserted } => {
7275 use hjkl_buffer::{Edit, Position};
7276 ed.push_undo();
7277 delete_to_eol(ed);
7278 if let Some(text) = inserted {
7279 let cursor = ed.cursor();
7280 ed.mutate_edit(Edit::InsertStr {
7281 at: Position::new(cursor.0, cursor.1),
7282 text,
7283 });
7284 }
7285 }
7286 LastChange::OpenLine { above, inserted } => {
7287 use hjkl_buffer::{Edit, Position};
7288 ed.push_undo();
7289 ed.sync_buffer_content_from_textarea();
7290 let row = buf_cursor_pos(&ed.buffer).row;
7291 if above {
7292 ed.mutate_edit(Edit::InsertStr {
7293 at: Position::new(row, 0),
7294 text: "\n".to_string(),
7295 });
7296 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
7297 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
7298 } else {
7299 let line_chars = buf_line_chars(&ed.buffer, row);
7300 ed.mutate_edit(Edit::InsertStr {
7301 at: Position::new(row, line_chars),
7302 text: "\n".to_string(),
7303 });
7304 }
7305 ed.push_buffer_cursor_to_textarea();
7306 let cursor = ed.cursor();
7307 ed.mutate_edit(Edit::InsertStr {
7308 at: Position::new(cursor.0, cursor.1),
7309 text: inserted,
7310 });
7311 }
7312 LastChange::InsertAt {
7313 entry,
7314 inserted,
7315 count,
7316 } => {
7317 use hjkl_buffer::{Edit, Position};
7318 ed.push_undo();
7319 match entry {
7320 InsertEntry::I => {}
7321 InsertEntry::ShiftI => move_first_non_whitespace(ed),
7322 InsertEntry::A => {
7323 crate::motions::move_right_to_end(&mut ed.buffer, 1);
7324 ed.push_buffer_cursor_to_textarea();
7325 }
7326 InsertEntry::ShiftA => {
7327 crate::motions::move_line_end(&mut ed.buffer);
7328 crate::motions::move_right_to_end(&mut ed.buffer, 1);
7329 ed.push_buffer_cursor_to_textarea();
7330 }
7331 }
7332 for _ in 0..count.max(1) {
7333 let cursor = ed.cursor();
7334 ed.mutate_edit(Edit::InsertStr {
7335 at: Position::new(cursor.0, cursor.1),
7336 text: inserted.clone(),
7337 });
7338 }
7339 }
7340 }
7341 ed.vim.replaying = false;
7342}
7343
7344fn extract_inserted(before: &str, after: &str) -> String {
7347 let before_chars: Vec<char> = before.chars().collect();
7348 let after_chars: Vec<char> = after.chars().collect();
7349 if after_chars.len() <= before_chars.len() {
7350 return String::new();
7351 }
7352 let prefix = before_chars
7353 .iter()
7354 .zip(after_chars.iter())
7355 .take_while(|(a, b)| a == b)
7356 .count();
7357 let max_suffix = before_chars.len() - prefix;
7358 let suffix = before_chars
7359 .iter()
7360 .rev()
7361 .zip(after_chars.iter().rev())
7362 .take(max_suffix)
7363 .take_while(|(a, b)| a == b)
7364 .count();
7365 after_chars[prefix..after_chars.len() - suffix]
7366 .iter()
7367 .collect()
7368}
7369
7370#[cfg(test)]
7373mod comment_continuation_tests {
7374 use super::*;
7375 use crate::{DefaultHost, Editor, Options};
7376 use hjkl_buffer::Buffer;
7377
7378 fn make_editor_with_lang(lang: &str, content: &str) -> Editor<Buffer, DefaultHost> {
7379 let buf = Buffer::from_str(content);
7380 let host = DefaultHost::new();
7381 let opts = Options {
7382 filetype: lang.to_string(),
7383 formatoptions: "ro".to_string(),
7384 ..Options::default()
7385 };
7386 Editor::new(buf, host, opts)
7387 }
7388
7389 #[test]
7390 fn detect_rust_doc_comment() {
7391 let result = detect_comment_on_line("rust", "/// foo bar");
7392 assert!(result.is_some());
7393 let (indent, prefix) = result.unwrap();
7394 assert_eq!(indent, "");
7395 assert_eq!(prefix, "/// ");
7396 }
7397
7398 #[test]
7399 fn detect_rust_inner_doc_comment() {
7400 let result = detect_comment_on_line("rust", "//! crate docs");
7401 assert!(result.is_some());
7402 let (_, prefix) = result.unwrap();
7403 assert_eq!(prefix, "//! ");
7404 }
7405
7406 #[test]
7407 fn detect_rust_plain_comment() {
7408 let result = detect_comment_on_line("rust", "// normal comment");
7409 assert!(result.is_some());
7410 let (_, prefix) = result.unwrap();
7411 assert_eq!(prefix, "// ");
7412 }
7413
7414 #[test]
7415 fn detect_indented_comment() {
7416 let result = detect_comment_on_line("rust", " // indented");
7417 assert!(result.is_some());
7418 let (indent, prefix) = result.unwrap();
7419 assert_eq!(indent, " ");
7420 assert_eq!(prefix, "// ");
7421 }
7422
7423 #[test]
7424 fn detect_python_hash() {
7425 let result = detect_comment_on_line("python", "# comment");
7426 assert!(result.is_some());
7427 let (_, prefix) = result.unwrap();
7428 assert_eq!(prefix, "# ");
7429 }
7430
7431 #[test]
7432 fn detect_lua_double_dash() {
7433 let result = detect_comment_on_line("lua", "-- a lua comment");
7434 assert!(result.is_some());
7435 let (_, prefix) = result.unwrap();
7436 assert_eq!(prefix, "-- ");
7437 }
7438
7439 #[test]
7440 fn detect_non_comment_is_none() {
7441 assert!(detect_comment_on_line("rust", "let x = 1;").is_none());
7442 assert!(detect_comment_on_line("python", "x = 1").is_none());
7443 }
7444
7445 #[test]
7446 fn detect_bare_double_slash_still_matches() {
7447 assert!(detect_comment_on_line("rust", "//").is_some());
7449 }
7450
7451 #[test]
7452 fn rust_doc_before_plain() {
7453 let result = detect_comment_on_line("rust", "/// outer doc");
7455 let (_, prefix) = result.unwrap();
7456 assert_eq!(prefix, "/// ", "/// must match before //");
7457 }
7458
7459 #[test]
7460 fn continue_comment_returns_prefix_for_comment_row() {
7461 let ed = make_editor_with_lang("rust", "/// hello\n");
7462 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7463 assert_eq!(cont, Some("/// ".to_string()));
7464 }
7465
7466 #[test]
7467 fn continue_comment_returns_none_for_non_comment() {
7468 let ed = make_editor_with_lang("rust", "let x = 1;\n");
7469 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7470 assert!(cont.is_none());
7471 }
7472
7473 #[test]
7474 fn continue_comment_returns_none_when_filetype_empty() {
7475 let buf = Buffer::from_str("// hello\n");
7476 let host = DefaultHost::new();
7477 let ed = Editor::new(buf, host, Options::default());
7479 let cont = continue_comment(&ed.buffer, &ed.settings, 0);
7480 assert!(cont.is_none());
7481 }
7482}
7483
7484#[cfg(test)]
7485mod comment_toggle_tests {
7486 use super::*;
7487 use crate::{DefaultHost, Editor, Options};
7488 use hjkl_buffer::Buffer;
7489
7490 fn make_rust_editor(content: &str) -> Editor<Buffer, DefaultHost> {
7491 let buf = Buffer::from_str(content);
7492 let host = DefaultHost::new();
7493 let opts = Options {
7494 filetype: "rust".to_string(),
7495 ..Options::default()
7496 };
7497 Editor::new(buf, host, opts)
7498 }
7499
7500 fn line(ed: &Editor<Buffer, DefaultHost>, row: usize) -> String {
7501 buf_line(&ed.buffer, row).unwrap_or_default()
7502 }
7503
7504 #[test]
7507 fn gcc_comments_rust_line() {
7508 let mut ed = make_rust_editor("let x = 1;");
7509 ed.toggle_comment_range(0, 0);
7510 assert_eq!(line(&ed, 0), "// let x = 1;");
7511 }
7512
7513 #[test]
7514 fn gcc_uncomments_rust_line() {
7515 let mut ed = make_rust_editor("// let x = 1;");
7516 ed.toggle_comment_range(0, 0);
7517 assert_eq!(line(&ed, 0), "let x = 1;");
7518 }
7519
7520 #[test]
7521 fn gcc_indent_preserving() {
7522 let mut ed = make_rust_editor(" let x = 1;");
7524 ed.toggle_comment_range(0, 0);
7525 assert_eq!(line(&ed, 0), " // let x = 1;");
7526 }
7527
7528 #[test]
7529 fn gcc_indent_preserving_uncomment() {
7530 let mut ed = make_rust_editor(" // let x = 1;");
7531 ed.toggle_comment_range(0, 0);
7532 assert_eq!(line(&ed, 0), " let x = 1;");
7533 }
7534
7535 #[test]
7538 fn toggle_multi_line_all_uncommented() {
7539 let content = "let a = 1;\nlet b = 2;\nlet c = 3;";
7540 let mut ed = make_rust_editor(content);
7541 ed.toggle_comment_range(0, 2);
7542 assert_eq!(line(&ed, 0), "// let a = 1;");
7543 assert_eq!(line(&ed, 1), "// let b = 2;");
7544 assert_eq!(line(&ed, 2), "// let c = 3;");
7545 }
7546
7547 #[test]
7548 fn toggle_multi_line_all_commented() {
7549 let content = "// let a = 1;\n// let b = 2;\n// let c = 3;";
7550 let mut ed = make_rust_editor(content);
7551 ed.toggle_comment_range(0, 2);
7552 assert_eq!(line(&ed, 0), "let a = 1;");
7553 assert_eq!(line(&ed, 1), "let b = 2;");
7554 assert_eq!(line(&ed, 2), "let c = 3;");
7555 }
7556
7557 #[test]
7560 fn toggle_mixed_state_comments_all() {
7561 let content = "let a = 1;\n// let b = 2;\nlet c = 3;\n// let d = 4;\nlet e = 5;";
7563 let mut ed = make_rust_editor(content);
7564 ed.toggle_comment_range(0, 4);
7565 for r in 0..5 {
7566 assert!(
7567 line(&ed, r).trim_start().starts_with("//"),
7568 "row {r} not commented: {:?}",
7569 line(&ed, r)
7570 );
7571 }
7572 }
7573
7574 #[test]
7577 fn blank_lines_not_commented() {
7578 let content = "let a = 1;\n\nlet b = 2;";
7579 let mut ed = make_rust_editor(content);
7580 ed.toggle_comment_range(0, 2);
7581 assert_eq!(line(&ed, 0), "// let a = 1;");
7582 assert_eq!(line(&ed, 1), ""); assert_eq!(line(&ed, 2), "// let b = 2;");
7584 }
7585
7586 #[test]
7589 fn python_comment_toggle() {
7590 let buf = Buffer::from_str("x = 1\ny = 2");
7591 let host = DefaultHost::new();
7592 let opts = Options {
7593 filetype: "python".to_string(),
7594 ..Options::default()
7595 };
7596 let mut ed = Editor::new(buf, host, opts);
7597 ed.toggle_comment_range(0, 1);
7598 assert_eq!(line(&ed, 0), "# x = 1");
7599 assert_eq!(line(&ed, 1), "# y = 2");
7600 ed.toggle_comment_range(0, 1);
7602 assert_eq!(line(&ed, 0), "x = 1");
7603 assert_eq!(line(&ed, 1), "y = 2");
7604 }
7605
7606 #[test]
7609 fn commentstring_override_via_setting() {
7610 let buf = Buffer::from_str("hello world");
7611 let host = DefaultHost::new();
7612 let opts = Options {
7613 filetype: "rust".to_string(),
7614 ..Options::default()
7615 };
7616 let mut ed = Editor::new(buf, host, opts);
7617 ed.settings_mut().commentstring = "# %s".to_string();
7619 ed.toggle_comment_range(0, 0);
7620 assert_eq!(line(&ed, 0), "# hello world");
7621 }
7622
7623 #[test]
7626 fn unknown_lang_no_op() {
7627 let buf = Buffer::from_str("hello");
7628 let host = DefaultHost::new();
7629 let opts = Options::default(); let mut ed = Editor::new(buf, host, opts);
7631 ed.toggle_comment_range(0, 0);
7632 assert_eq!(line(&ed, 0), "hello");
7634 }
7635}