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_lines_to_vec, buf_row_count,
79 buf_set_cursor_pos, 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)]
100enum 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}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum Operator {
157 Delete,
158 Change,
159 Yank,
160 Uppercase,
163 Lowercase,
165 ToggleCase,
169 Indent,
174 Outdent,
177 Fold,
181 Reflow,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum Motion {
190 Left,
191 Right,
192 Up,
193 Down,
194 WordFwd,
195 BigWordFwd,
196 WordBack,
197 BigWordBack,
198 WordEnd,
199 BigWordEnd,
200 WordEndBack,
202 BigWordEndBack,
204 LineStart,
205 FirstNonBlank,
206 LineEnd,
207 FileTop,
208 FileBottom,
209 Find {
210 ch: char,
211 forward: bool,
212 till: bool,
213 },
214 FindRepeat {
215 reverse: bool,
216 },
217 MatchBracket,
218 WordAtCursor {
219 forward: bool,
220 whole_word: bool,
223 },
224 SearchNext {
226 reverse: bool,
227 },
228 ViewportTop,
230 ViewportMiddle,
232 ViewportBottom,
234 LastNonBlank,
236 LineMiddle,
239 ParagraphPrev,
241 ParagraphNext,
243 SentencePrev,
245 SentenceNext,
247 ScreenDown,
250 ScreenUp,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum TextObject {
256 Word {
257 big: bool,
258 },
259 Quote(char),
260 Bracket(char),
261 Paragraph,
262 XmlTag,
266 Sentence,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum MotionKind {
276 Exclusive,
278 Inclusive,
280 Linewise,
282}
283
284#[derive(Debug, Clone)]
288enum LastChange {
289 OpMotion {
291 op: Operator,
292 motion: Motion,
293 count: usize,
294 inserted: Option<String>,
295 },
296 OpTextObj {
298 op: Operator,
299 obj: TextObject,
300 inner: bool,
301 inserted: Option<String>,
302 },
303 LineOp {
305 op: Operator,
306 count: usize,
307 inserted: Option<String>,
308 },
309 CharDel { forward: bool, count: usize },
311 ReplaceChar { ch: char, count: usize },
313 ToggleCase { count: usize },
315 JoinLine { count: usize },
317 Paste { before: bool, count: usize },
319 DeleteToEol { inserted: Option<String> },
321 OpenLine { above: bool, inserted: String },
323 InsertAt {
325 entry: InsertEntry,
326 inserted: String,
327 count: usize,
328 },
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332enum InsertEntry {
333 I,
334 A,
335 ShiftI,
336 ShiftA,
337}
338
339#[derive(Default)]
342pub struct VimState {
343 mode: Mode,
344 pending: Pending,
345 count: usize,
346 last_find: Option<(char, bool, bool)>,
348 last_change: Option<LastChange>,
349 insert_session: Option<InsertSession>,
351 pub(super) visual_anchor: (usize, usize),
355 pub(super) visual_line_anchor: usize,
357 pub(super) block_anchor: (usize, usize),
360 pub(super) block_vcol: usize,
366 pub(super) yank_linewise: bool,
368 pub(super) pending_register: Option<char>,
371 pub(super) recording_macro: Option<char>,
375 pub(super) recording_keys: Vec<crate::input::Input>,
380 pub(super) replaying_macro: bool,
383 pub(super) last_macro: Option<char>,
385 pub(super) last_edit_pos: Option<(usize, usize)>,
388 pub(super) last_insert_pos: Option<(usize, usize)>,
392 pub(super) change_list: Vec<(usize, usize)>,
396 pub(super) change_list_cursor: Option<usize>,
399 pub(super) last_visual: Option<LastVisual>,
402 pub(super) viewport_pinned: bool,
406 replaying: bool,
408 one_shot_normal: bool,
411 pub(super) search_prompt: Option<SearchPrompt>,
413 pub(super) last_search: Option<String>,
417 pub(super) last_search_forward: bool,
421 pub(super) jump_back: Vec<(usize, usize)>,
426 pub(super) jump_fwd: Vec<(usize, usize)>,
429 pub(super) insert_pending_register: bool,
433 pub(super) change_mark_start: Option<(usize, usize)>,
439 pub(super) search_history: Vec<String>,
443 pub(super) search_history_cursor: Option<usize>,
448 pub(super) last_input_at: Option<std::time::Instant>,
457 pub(super) last_input_host_at: Option<core::time::Duration>,
461}
462
463const SEARCH_HISTORY_MAX: usize = 100;
464pub(crate) const CHANGE_LIST_MAX: usize = 100;
465
466#[derive(Debug, Clone)]
469pub struct SearchPrompt {
470 pub text: String,
471 pub cursor: usize,
472 pub forward: bool,
473}
474
475#[derive(Debug, Clone)]
476struct InsertSession {
477 count: usize,
478 row_min: usize,
480 row_max: usize,
481 before_lines: Vec<String>,
485 reason: InsertReason,
486}
487
488#[derive(Debug, Clone)]
489enum InsertReason {
490 Enter(InsertEntry),
492 Open { above: bool },
494 AfterChange,
497 DeleteToEol,
499 ReplayOnly,
502 BlockEdge { top: usize, bot: usize, col: usize },
506 BlockChange { top: usize, bot: usize, col: usize },
511 Replace,
515}
516
517#[derive(Debug, Clone, Copy)]
527pub(super) struct LastVisual {
528 pub mode: Mode,
529 pub anchor: (usize, usize),
530 pub cursor: (usize, usize),
531 pub block_vcol: usize,
532}
533
534impl VimState {
535 pub fn public_mode(&self) -> VimMode {
536 match self.mode {
537 Mode::Normal => VimMode::Normal,
538 Mode::Insert => VimMode::Insert,
539 Mode::Visual => VimMode::Visual,
540 Mode::VisualLine => VimMode::VisualLine,
541 Mode::VisualBlock => VimMode::VisualBlock,
542 }
543 }
544
545 pub fn force_normal(&mut self) {
546 self.mode = Mode::Normal;
547 self.pending = Pending::None;
548 self.count = 0;
549 self.insert_session = None;
550 }
551
552 pub(crate) fn clear_pending_prefix(&mut self) {
562 self.pending = Pending::None;
563 self.count = 0;
564 self.pending_register = None;
565 self.insert_pending_register = false;
566 }
567
568 pub(crate) fn widen_insert_row(&mut self, row: usize) {
573 if let Some(ref mut session) = self.insert_session {
574 session.row_min = session.row_min.min(row);
575 session.row_max = session.row_max.max(row);
576 }
577 }
578
579 pub fn is_visual(&self) -> bool {
580 matches!(
581 self.mode,
582 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
583 )
584 }
585
586 pub fn is_visual_char(&self) -> bool {
587 self.mode == Mode::Visual
588 }
589
590 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
591 self.visual_anchor = anchor;
592 self.mode = Mode::Visual;
593 }
594
595 pub(crate) fn pending_count_val(&self) -> Option<u32> {
598 if self.count == 0 {
599 None
600 } else {
601 Some(self.count as u32)
602 }
603 }
604
605 pub(crate) fn is_chord_pending(&self) -> bool {
608 !matches!(self.pending, Pending::None)
609 }
610
611 pub(crate) fn pending_op_char(&self) -> Option<char> {
615 let op = match &self.pending {
616 Pending::Op { op, .. }
617 | Pending::OpTextObj { op, .. }
618 | Pending::OpG { op, .. }
619 | Pending::OpFind { op, .. } => Some(*op),
620 _ => None,
621 };
622 op.map(|o| match o {
623 Operator::Delete => 'd',
624 Operator::Change => 'c',
625 Operator::Yank => 'y',
626 Operator::Uppercase => 'U',
627 Operator::Lowercase => 'u',
628 Operator::ToggleCase => '~',
629 Operator::Indent => '>',
630 Operator::Outdent => '<',
631 Operator::Fold => 'z',
632 Operator::Reflow => 'q',
633 })
634 }
635}
636
637fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
643 ed.vim.search_prompt = Some(SearchPrompt {
644 text: String::new(),
645 cursor: 0,
646 forward,
647 });
648 ed.vim.search_history_cursor = None;
649 ed.set_search_pattern(None);
653}
654
655fn push_search_pattern<H: crate::types::Host>(
660 ed: &mut Editor<hjkl_buffer::Buffer, H>,
661 pattern: &str,
662) {
663 let compiled = if pattern.is_empty() {
664 None
665 } else {
666 let case_insensitive = ed.settings().ignore_case
673 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
674 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
675 std::borrow::Cow::Owned(format!("(?i){pattern}"))
676 } else {
677 std::borrow::Cow::Borrowed(pattern)
678 };
679 regex::Regex::new(&effective).ok()
680 };
681 let wrap = ed.settings().wrapscan;
682 ed.set_search_pattern(compiled);
686 ed.search_state_mut().wrap_around = wrap;
687}
688
689fn step_search_prompt<H: crate::types::Host>(
690 ed: &mut Editor<hjkl_buffer::Buffer, H>,
691 input: Input,
692) -> bool {
693 let history_dir = match (input.key, input.ctrl) {
697 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
698 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
699 _ => None,
700 };
701 if let Some(dir) = history_dir {
702 walk_search_history(ed, dir);
703 return true;
704 }
705 match input.key {
706 Key::Esc => {
707 let text = ed
710 .vim
711 .search_prompt
712 .take()
713 .map(|p| p.text)
714 .unwrap_or_default();
715 if !text.is_empty() {
716 ed.vim.last_search = Some(text);
717 }
718 ed.vim.search_history_cursor = None;
719 }
720 Key::Enter => {
721 let prompt = ed.vim.search_prompt.take();
722 if let Some(p) = prompt {
723 let pattern = if p.text.is_empty() {
726 ed.vim.last_search.clone()
727 } else {
728 Some(p.text.clone())
729 };
730 if let Some(pattern) = pattern {
731 push_search_pattern(ed, &pattern);
732 let pre = ed.cursor();
733 if p.forward {
734 ed.search_advance_forward(true);
735 } else {
736 ed.search_advance_backward(true);
737 }
738 ed.push_buffer_cursor_to_textarea();
739 if ed.cursor() != pre {
740 push_jump(ed, pre);
741 }
742 record_search_history(ed, &pattern);
743 ed.vim.last_search = Some(pattern);
744 ed.vim.last_search_forward = p.forward;
745 }
746 }
747 ed.vim.search_history_cursor = None;
748 }
749 Key::Backspace => {
750 ed.vim.search_history_cursor = None;
751 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
752 if p.text.pop().is_some() {
753 p.cursor = p.text.chars().count();
754 Some(p.text.clone())
755 } else {
756 None
757 }
758 });
759 if let Some(text) = new_text {
760 push_search_pattern(ed, &text);
761 }
762 }
763 Key::Char(c) => {
764 ed.vim.search_history_cursor = None;
765 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
766 p.text.push(c);
767 p.cursor = p.text.chars().count();
768 p.text.clone()
769 });
770 if let Some(text) = new_text {
771 push_search_pattern(ed, &text);
772 }
773 }
774 _ => {}
775 }
776 true
777}
778
779fn walk_change_list<H: crate::types::Host>(
783 ed: &mut Editor<hjkl_buffer::Buffer, H>,
784 dir: isize,
785 count: usize,
786) {
787 if ed.vim.change_list.is_empty() {
788 return;
789 }
790 let len = ed.vim.change_list.len();
791 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
792 (None, -1) => len as isize - 1,
793 (None, 1) => return, (Some(i), -1) => i as isize - 1,
795 (Some(i), 1) => i as isize + 1,
796 _ => return,
797 };
798 for _ in 1..count {
799 let next = idx + dir;
800 if next < 0 || next >= len as isize {
801 break;
802 }
803 idx = next;
804 }
805 if idx < 0 || idx >= len as isize {
806 return;
807 }
808 let idx = idx as usize;
809 ed.vim.change_list_cursor = Some(idx);
810 let (row, col) = ed.vim.change_list[idx];
811 ed.jump_cursor(row, col);
812}
813
814fn record_search_history<H: crate::types::Host>(
818 ed: &mut Editor<hjkl_buffer::Buffer, H>,
819 pattern: &str,
820) {
821 if pattern.is_empty() {
822 return;
823 }
824 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
825 return;
826 }
827 ed.vim.search_history.push(pattern.to_string());
828 let len = ed.vim.search_history.len();
829 if len > SEARCH_HISTORY_MAX {
830 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
831 }
832}
833
834fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
840 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
841 return;
842 }
843 let len = ed.vim.search_history.len();
844 let next_idx = match (ed.vim.search_history_cursor, dir) {
845 (None, -1) => Some(len - 1),
846 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
848 (Some(i), 1) if i + 1 < len => Some(i + 1),
849 _ => None,
850 };
851 let Some(idx) = next_idx else {
852 return;
853 };
854 ed.vim.search_history_cursor = Some(idx);
855 let text = ed.vim.search_history[idx].clone();
856 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
857 prompt.cursor = text.chars().count();
858 prompt.text = text.clone();
859 }
860 push_search_pattern(ed, &text);
861}
862
863pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
864 ed.sync_buffer_content_from_textarea();
869 let now = std::time::Instant::now();
877 let host_now = ed.host.now();
878 let timed_out = match ed.vim.last_input_host_at {
879 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
880 None => false,
881 };
882 if timed_out {
883 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
884 || ed.vim.count != 0
885 || ed.vim.pending_register.is_some()
886 || ed.vim.insert_pending_register;
887 if chord_in_flight {
888 ed.vim.clear_pending_prefix();
889 }
890 }
891 ed.vim.last_input_at = Some(now);
892 ed.vim.last_input_host_at = Some(host_now);
893 if ed.vim.recording_macro.is_some()
898 && !ed.vim.replaying_macro
899 && matches!(ed.vim.pending, Pending::None)
900 && ed.vim.mode != Mode::Insert
901 && input.key == Key::Char('q')
902 && !input.ctrl
903 && !input.alt
904 {
905 let reg = ed.vim.recording_macro.take().unwrap();
906 let keys = std::mem::take(&mut ed.vim.recording_keys);
907 let text = crate::input::encode_macro(&keys);
908 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
909 return true;
910 }
911 if ed.vim.search_prompt.is_some() {
913 return step_search_prompt(ed, input);
914 }
915 let pending_was_macro_chord = matches!(
919 ed.vim.pending,
920 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
921 );
922 let was_insert = ed.vim.mode == Mode::Insert;
923 let pre_visual_snapshot = match ed.vim.mode {
926 Mode::Visual => Some(LastVisual {
927 mode: Mode::Visual,
928 anchor: ed.vim.visual_anchor,
929 cursor: ed.cursor(),
930 block_vcol: 0,
931 }),
932 Mode::VisualLine => Some(LastVisual {
933 mode: Mode::VisualLine,
934 anchor: (ed.vim.visual_line_anchor, 0),
935 cursor: ed.cursor(),
936 block_vcol: 0,
937 }),
938 Mode::VisualBlock => Some(LastVisual {
939 mode: Mode::VisualBlock,
940 anchor: ed.vim.block_anchor,
941 cursor: ed.cursor(),
942 block_vcol: ed.vim.block_vcol,
943 }),
944 _ => None,
945 };
946 let consumed = match ed.vim.mode {
947 Mode::Insert => step_insert(ed, input),
948 _ => step_normal(ed, input),
949 };
950 if let Some(snap) = pre_visual_snapshot
951 && !matches!(
952 ed.vim.mode,
953 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
954 )
955 {
956 let (lo, hi) = match snap.mode {
972 Mode::Visual => {
973 if snap.anchor <= snap.cursor {
974 (snap.anchor, snap.cursor)
975 } else {
976 (snap.cursor, snap.anchor)
977 }
978 }
979 Mode::VisualLine => {
980 let r_lo = snap.anchor.0.min(snap.cursor.0);
981 let r_hi = snap.anchor.0.max(snap.cursor.0);
982 let last_col = ed
983 .buffer()
984 .lines()
985 .get(r_hi)
986 .map(|l| l.chars().count().saturating_sub(1))
987 .unwrap_or(0);
988 ((r_lo, 0), (r_hi, last_col))
989 }
990 Mode::VisualBlock => {
991 let (r1, c1) = snap.anchor;
992 let (r2, c2) = snap.cursor;
993 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
994 }
995 _ => {
996 if snap.anchor <= snap.cursor {
999 (snap.anchor, snap.cursor)
1000 } else {
1001 (snap.cursor, snap.anchor)
1002 }
1003 }
1004 };
1005 ed.set_mark('<', lo);
1006 ed.set_mark('>', hi);
1007 ed.vim.last_visual = Some(snap);
1008 }
1009 if !was_insert
1013 && ed.vim.one_shot_normal
1014 && ed.vim.mode == Mode::Normal
1015 && matches!(ed.vim.pending, Pending::None)
1016 {
1017 ed.vim.one_shot_normal = false;
1018 ed.vim.mode = Mode::Insert;
1019 }
1020 ed.sync_buffer_content_from_textarea();
1026 if !ed.vim.viewport_pinned {
1030 ed.ensure_cursor_in_scrolloff();
1031 }
1032 ed.vim.viewport_pinned = false;
1033 if ed.vim.recording_macro.is_some()
1038 && !ed.vim.replaying_macro
1039 && input.key != Key::Char('q')
1040 && !pending_was_macro_chord
1041 {
1042 ed.vim.recording_keys.push(input);
1043 }
1044 consumed
1045}
1046
1047fn step_insert<H: crate::types::Host>(
1050 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1051 input: Input,
1052) -> bool {
1053 if ed.vim.insert_pending_register {
1057 ed.vim.insert_pending_register = false;
1058 if let Key::Char(c) = input.key
1059 && !input.ctrl
1060 {
1061 insert_paste_register_bridge(ed, c);
1062 }
1063 return true;
1064 }
1065
1066 if input.key == Key::Esc {
1067 leave_insert_to_normal_bridge(ed);
1068 return true;
1069 }
1070
1071 if input.ctrl {
1073 match input.key {
1074 Key::Char('w') => {
1075 insert_ctrl_w_bridge(ed);
1076 return true;
1077 }
1078 Key::Char('u') => {
1079 insert_ctrl_u_bridge(ed);
1080 return true;
1081 }
1082 Key::Char('h') => {
1083 insert_ctrl_h_bridge(ed);
1084 return true;
1085 }
1086 Key::Char('o') => {
1087 insert_ctrl_o_bridge(ed);
1088 return true;
1089 }
1090 Key::Char('r') => {
1091 insert_ctrl_r_bridge(ed);
1092 return true;
1093 }
1094 Key::Char('t') => {
1095 insert_ctrl_t_bridge(ed);
1096 return true;
1097 }
1098 Key::Char('d') => {
1099 insert_ctrl_d_bridge(ed);
1100 return true;
1101 }
1102 _ => {}
1103 }
1104 }
1105
1106 let (row, _) = ed.cursor();
1109 if let Some(ref mut session) = ed.vim.insert_session {
1110 session.row_min = session.row_min.min(row);
1111 session.row_max = session.row_max.max(row);
1112 }
1113 let mutated = handle_insert_key(ed, input);
1114 if mutated {
1115 ed.mark_content_dirty();
1116 let (row, _) = ed.cursor();
1117 if let Some(ref mut session) = ed.vim.insert_session {
1118 session.row_min = session.row_min.min(row);
1119 session.row_max = session.row_max.max(row);
1120 }
1121 }
1122 true
1123}
1124
1125fn insert_register_text<H: crate::types::Host>(
1130 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1131 selector: char,
1132) {
1133 use hjkl_buffer::Edit;
1134 let text = match ed.registers().read(selector) {
1135 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1136 _ => return,
1137 };
1138 ed.sync_buffer_content_from_textarea();
1139 let cursor = buf_cursor_pos(&ed.buffer);
1140 ed.mutate_edit(Edit::InsertStr {
1141 at: cursor,
1142 text: text.clone(),
1143 });
1144 let mut row = cursor.row;
1147 let mut col = cursor.col;
1148 for ch in text.chars() {
1149 if ch == '\n' {
1150 row += 1;
1151 col = 0;
1152 } else {
1153 col += 1;
1154 }
1155 }
1156 buf_set_cursor_rc(&mut ed.buffer, row, col);
1157 ed.push_buffer_cursor_to_textarea();
1158 ed.mark_content_dirty();
1159 if let Some(ref mut session) = ed.vim.insert_session {
1160 session.row_min = session.row_min.min(row);
1161 session.row_max = session.row_max.max(row);
1162 }
1163}
1164
1165pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1184 if !settings.autoindent {
1185 return String::new();
1186 }
1187 let base: String = prev_line
1189 .chars()
1190 .take_while(|c| *c == ' ' || *c == '\t')
1191 .collect();
1192
1193 if settings.smartindent {
1194 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1198 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1199 let unit = if settings.expandtab {
1200 if settings.softtabstop > 0 {
1201 " ".repeat(settings.softtabstop)
1202 } else {
1203 " ".repeat(settings.shiftwidth)
1204 }
1205 } else {
1206 "\t".to_string()
1207 };
1208 return format!("{base}{unit}");
1209 }
1210 }
1211
1212 base
1213}
1214
1215fn try_dedent_close_bracket<H: crate::types::Host>(
1225 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1226 cursor: hjkl_buffer::Position,
1227 ch: char,
1228) -> bool {
1229 use hjkl_buffer::{Edit, MotionKind, Position};
1230
1231 if !ed.settings.smartindent {
1232 return false;
1233 }
1234 if !matches!(ch, '}' | ')' | ']') {
1235 return false;
1236 }
1237
1238 let line = match buf_line(&ed.buffer, cursor.row) {
1239 Some(l) => l.to_string(),
1240 None => return false,
1241 };
1242
1243 let before: String = line.chars().take(cursor.col).collect();
1245 if !before.chars().all(|c| c == ' ' || c == '\t') {
1246 return false;
1247 }
1248 if before.is_empty() {
1249 return false;
1251 }
1252
1253 let unit_len: usize = if ed.settings.expandtab {
1255 if ed.settings.softtabstop > 0 {
1256 ed.settings.softtabstop
1257 } else {
1258 ed.settings.shiftwidth
1259 }
1260 } else {
1261 1
1263 };
1264
1265 let strip_len = if ed.settings.expandtab {
1267 let spaces = before.chars().filter(|c| *c == ' ').count();
1269 if spaces < unit_len {
1270 return false;
1271 }
1272 unit_len
1273 } else {
1274 if !before.starts_with('\t') {
1276 return false;
1277 }
1278 1
1279 };
1280
1281 ed.mutate_edit(Edit::DeleteRange {
1283 start: Position::new(cursor.row, 0),
1284 end: Position::new(cursor.row, strip_len),
1285 kind: MotionKind::Char,
1286 });
1287 let new_col = cursor.col.saturating_sub(strip_len);
1292 ed.mutate_edit(Edit::InsertChar {
1293 at: Position::new(cursor.row, new_col),
1294 ch,
1295 });
1296 true
1297}
1298
1299fn handle_insert_key<H: crate::types::Host>(
1305 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1306 input: Input,
1307) -> bool {
1308 match input.key {
1309 Key::Char(c) => insert_char_bridge(ed, c),
1310 Key::Enter => insert_newline_bridge(ed),
1311 Key::Tab => insert_tab_bridge(ed),
1312 Key::Backspace => insert_backspace_bridge(ed),
1313 Key::Delete => insert_delete_bridge(ed),
1314 Key::Left => insert_arrow_bridge(ed, InsertDir::Left),
1315 Key::Right => insert_arrow_bridge(ed, InsertDir::Right),
1316 Key::Up => insert_arrow_bridge(ed, InsertDir::Up),
1317 Key::Down => insert_arrow_bridge(ed, InsertDir::Down),
1318 Key::Home => insert_home_bridge(ed),
1319 Key::End => insert_end_bridge(ed),
1320 Key::PageUp => {
1321 let h = ed.viewport_height_value();
1322 insert_pageup_bridge(ed, h)
1323 }
1324 Key::PageDown => {
1325 let h = ed.viewport_height_value();
1326 insert_pagedown_bridge(ed, h)
1327 }
1328 _ => false,
1331 }
1332}
1333
1334fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1335 let Some(session) = ed.vim.insert_session.take() else {
1336 return;
1337 };
1338 let lines = buf_lines_to_vec(&ed.buffer);
1339 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1343 let before_end = session
1344 .row_max
1345 .min(session.before_lines.len().saturating_sub(1));
1346 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1347 session.before_lines[session.row_min..=before_end].join("\n")
1348 } else {
1349 String::new()
1350 };
1351 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1352 lines[session.row_min..=after_end].join("\n")
1353 } else {
1354 String::new()
1355 };
1356 let inserted = extract_inserted(&before, &after);
1357 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1358 use hjkl_buffer::{Edit, Position};
1359 for _ in 0..session.count - 1 {
1360 let (row, col) = ed.cursor();
1361 ed.mutate_edit(Edit::InsertStr {
1362 at: Position::new(row, col),
1363 text: inserted.clone(),
1364 });
1365 }
1366 }
1367 fn replicate_block_text<H: crate::types::Host>(
1371 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1372 inserted: &str,
1373 top: usize,
1374 bot: usize,
1375 col: usize,
1376 ) {
1377 use hjkl_buffer::{Edit, Position};
1378 for r in (top + 1)..=bot {
1379 let line_len = buf_line_chars(&ed.buffer, r);
1380 if col > line_len {
1381 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1382 ed.mutate_edit(Edit::InsertStr {
1383 at: Position::new(r, line_len),
1384 text: pad,
1385 });
1386 }
1387 ed.mutate_edit(Edit::InsertStr {
1388 at: Position::new(r, col),
1389 text: inserted.to_string(),
1390 });
1391 }
1392 }
1393
1394 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1395 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1398 replicate_block_text(ed, &inserted, top, bot, col);
1399 buf_set_cursor_rc(&mut ed.buffer, top, col);
1400 ed.push_buffer_cursor_to_textarea();
1401 }
1402 return;
1403 }
1404 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1405 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1409 replicate_block_text(ed, &inserted, top, bot, col);
1410 let ins_chars = inserted.chars().count();
1411 let line_len = buf_line_chars(&ed.buffer, top);
1412 let target_col = (col + ins_chars).min(line_len);
1413 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1414 ed.push_buffer_cursor_to_textarea();
1415 }
1416 return;
1417 }
1418 if ed.vim.replaying {
1419 return;
1420 }
1421 match session.reason {
1422 InsertReason::Enter(entry) => {
1423 ed.vim.last_change = Some(LastChange::InsertAt {
1424 entry,
1425 inserted,
1426 count: session.count,
1427 });
1428 }
1429 InsertReason::Open { above } => {
1430 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1431 }
1432 InsertReason::AfterChange => {
1433 if let Some(
1434 LastChange::OpMotion { inserted: ins, .. }
1435 | LastChange::OpTextObj { inserted: ins, .. }
1436 | LastChange::LineOp { inserted: ins, .. },
1437 ) = ed.vim.last_change.as_mut()
1438 {
1439 *ins = Some(inserted);
1440 }
1441 if let Some(start) = ed.vim.change_mark_start.take() {
1447 let end = ed.cursor();
1448 ed.set_mark('[', start);
1449 ed.set_mark(']', end);
1450 }
1451 }
1452 InsertReason::DeleteToEol => {
1453 ed.vim.last_change = Some(LastChange::DeleteToEol {
1454 inserted: Some(inserted),
1455 });
1456 }
1457 InsertReason::ReplayOnly => {}
1458 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1459 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1460 InsertReason::Replace => {
1461 ed.vim.last_change = Some(LastChange::DeleteToEol {
1466 inserted: Some(inserted),
1467 });
1468 }
1469 }
1470}
1471
1472fn begin_insert<H: crate::types::Host>(
1473 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1474 count: usize,
1475 reason: InsertReason,
1476) {
1477 let record = !matches!(reason, InsertReason::ReplayOnly);
1478 if record {
1479 ed.push_undo();
1480 }
1481 let reason = if ed.vim.replaying {
1482 InsertReason::ReplayOnly
1483 } else {
1484 reason
1485 };
1486 let (row, _) = ed.cursor();
1487 ed.vim.insert_session = Some(InsertSession {
1488 count,
1489 row_min: row,
1490 row_max: row,
1491 before_lines: buf_lines_to_vec(&ed.buffer),
1492 reason,
1493 });
1494 ed.vim.mode = Mode::Insert;
1495}
1496
1497pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1512 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1513) {
1514 if !ed.settings.undo_break_on_motion {
1515 return;
1516 }
1517 if ed.vim.replaying {
1518 return;
1519 }
1520 if ed.vim.insert_session.is_none() {
1521 return;
1522 }
1523 ed.push_undo();
1524 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1525 let mut lines: Vec<String> = Vec::with_capacity(n);
1526 for r in 0..n {
1527 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1528 }
1529 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1530 if let Some(ref mut session) = ed.vim.insert_session {
1531 session.before_lines = lines;
1532 session.row_min = row;
1533 session.row_max = row;
1534 }
1535}
1536
1537pub(crate) fn insert_char_bridge<H: crate::types::Host>(
1559 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1560 ch: char,
1561) -> bool {
1562 use hjkl_buffer::{Edit, MotionKind, Position};
1563 ed.sync_buffer_content_from_textarea();
1564 let cursor = buf_cursor_pos(&ed.buffer);
1565 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1566 let in_replace = matches!(
1567 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1568 Some(InsertReason::Replace)
1569 );
1570 if in_replace && cursor.col < line_chars {
1571 ed.mutate_edit(Edit::DeleteRange {
1572 start: cursor,
1573 end: Position::new(cursor.row, cursor.col + 1),
1574 kind: MotionKind::Char,
1575 });
1576 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1577 } else if !try_dedent_close_bracket(ed, cursor, ch) {
1578 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
1579 }
1580 ed.push_buffer_cursor_to_textarea();
1581 true
1582}
1583
1584pub(crate) fn insert_newline_bridge<H: crate::types::Host>(
1587 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1588) -> bool {
1589 use hjkl_buffer::Edit;
1590 ed.sync_buffer_content_from_textarea();
1591 let cursor = buf_cursor_pos(&ed.buffer);
1592 let prev_line = buf_line(&ed.buffer, cursor.row)
1593 .unwrap_or_default()
1594 .to_string();
1595 let indent = compute_enter_indent(&ed.settings, &prev_line);
1596 let text = format!("\n{indent}");
1597 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1598 ed.push_buffer_cursor_to_textarea();
1599 true
1600}
1601
1602pub(crate) fn insert_tab_bridge<H: crate::types::Host>(
1605 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1606) -> bool {
1607 use hjkl_buffer::Edit;
1608 ed.sync_buffer_content_from_textarea();
1609 let cursor = buf_cursor_pos(&ed.buffer);
1610 if ed.settings.expandtab {
1611 let sts = ed.settings.softtabstop;
1612 let n = if sts > 0 {
1613 sts - (cursor.col % sts)
1614 } else {
1615 ed.settings.tabstop.max(1)
1616 };
1617 ed.mutate_edit(Edit::InsertStr {
1618 at: cursor,
1619 text: " ".repeat(n),
1620 });
1621 } else {
1622 ed.mutate_edit(Edit::InsertChar {
1623 at: cursor,
1624 ch: '\t',
1625 });
1626 }
1627 ed.push_buffer_cursor_to_textarea();
1628 true
1629}
1630
1631pub(crate) fn insert_backspace_bridge<H: crate::types::Host>(
1637 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1638) -> bool {
1639 use hjkl_buffer::{Edit, MotionKind, Position};
1640 ed.sync_buffer_content_from_textarea();
1641 let cursor = buf_cursor_pos(&ed.buffer);
1642 let sts = ed.settings.softtabstop;
1643 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1644 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1645 let chars: Vec<char> = line.chars().collect();
1646 let run_start = cursor.col - sts;
1647 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1648 ed.mutate_edit(Edit::DeleteRange {
1649 start: Position::new(cursor.row, run_start),
1650 end: cursor,
1651 kind: MotionKind::Char,
1652 });
1653 ed.push_buffer_cursor_to_textarea();
1654 return true;
1655 }
1656 }
1657 let result = if cursor.col > 0 {
1658 ed.mutate_edit(Edit::DeleteRange {
1659 start: Position::new(cursor.row, cursor.col - 1),
1660 end: cursor,
1661 kind: MotionKind::Char,
1662 });
1663 true
1664 } else if cursor.row > 0 {
1665 let prev_row = cursor.row - 1;
1666 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1667 ed.mutate_edit(Edit::JoinLines {
1668 row: prev_row,
1669 count: 1,
1670 with_space: false,
1671 });
1672 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1673 true
1674 } else {
1675 false
1676 };
1677 ed.push_buffer_cursor_to_textarea();
1678 result
1679}
1680
1681pub(crate) fn insert_delete_bridge<H: crate::types::Host>(
1684 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1685) -> bool {
1686 use hjkl_buffer::{Edit, MotionKind, Position};
1687 ed.sync_buffer_content_from_textarea();
1688 let cursor = buf_cursor_pos(&ed.buffer);
1689 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1690 let result = if cursor.col < line_chars {
1691 ed.mutate_edit(Edit::DeleteRange {
1692 start: cursor,
1693 end: Position::new(cursor.row, cursor.col + 1),
1694 kind: MotionKind::Char,
1695 });
1696 buf_set_cursor_pos(&mut ed.buffer, cursor);
1697 true
1698 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1699 ed.mutate_edit(Edit::JoinLines {
1700 row: cursor.row,
1701 count: 1,
1702 with_space: false,
1703 });
1704 buf_set_cursor_pos(&mut ed.buffer, cursor);
1705 true
1706 } else {
1707 false
1708 };
1709 ed.push_buffer_cursor_to_textarea();
1710 result
1711}
1712
1713#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1715pub enum InsertDir {
1716 Left,
1717 Right,
1718 Up,
1719 Down,
1720}
1721
1722pub(crate) fn insert_arrow_bridge<H: crate::types::Host>(
1725 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1726 dir: InsertDir,
1727) -> bool {
1728 ed.sync_buffer_content_from_textarea();
1729 match dir {
1730 InsertDir::Left => {
1731 crate::motions::move_left(&mut ed.buffer, 1);
1732 }
1733 InsertDir::Right => {
1734 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1735 }
1736 InsertDir::Up => {
1737 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1738 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1739 }
1740 InsertDir::Down => {
1741 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1742 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1743 }
1744 }
1745 break_undo_group_in_insert(ed);
1746 ed.push_buffer_cursor_to_textarea();
1747 false
1748}
1749
1750pub(crate) fn insert_home_bridge<H: crate::types::Host>(
1753 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1754) -> bool {
1755 ed.sync_buffer_content_from_textarea();
1756 crate::motions::move_line_start(&mut ed.buffer);
1757 break_undo_group_in_insert(ed);
1758 ed.push_buffer_cursor_to_textarea();
1759 false
1760}
1761
1762pub(crate) fn insert_end_bridge<H: crate::types::Host>(
1765 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1766) -> bool {
1767 ed.sync_buffer_content_from_textarea();
1768 crate::motions::move_line_end(&mut ed.buffer);
1769 break_undo_group_in_insert(ed);
1770 ed.push_buffer_cursor_to_textarea();
1771 false
1772}
1773
1774pub(crate) fn insert_pageup_bridge<H: crate::types::Host>(
1777 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1778 viewport_h: u16,
1779) -> bool {
1780 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1781 scroll_cursor_rows(ed, -rows);
1782 false
1783}
1784
1785pub(crate) fn insert_pagedown_bridge<H: crate::types::Host>(
1788 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1789 viewport_h: u16,
1790) -> bool {
1791 let rows = viewport_h.saturating_sub(2).max(1) as isize;
1792 scroll_cursor_rows(ed, rows);
1793 false
1794}
1795
1796pub(crate) fn insert_ctrl_w_bridge<H: crate::types::Host>(
1800 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1801) -> bool {
1802 use hjkl_buffer::{Edit, MotionKind};
1803 ed.sync_buffer_content_from_textarea();
1804 let cursor = buf_cursor_pos(&ed.buffer);
1805 if cursor.row == 0 && cursor.col == 0 {
1806 return true;
1807 }
1808 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1809 let word_start = buf_cursor_pos(&ed.buffer);
1810 if word_start == cursor {
1811 return true;
1812 }
1813 buf_set_cursor_pos(&mut ed.buffer, cursor);
1814 ed.mutate_edit(Edit::DeleteRange {
1815 start: word_start,
1816 end: cursor,
1817 kind: MotionKind::Char,
1818 });
1819 ed.push_buffer_cursor_to_textarea();
1820 true
1821}
1822
1823pub(crate) fn insert_ctrl_u_bridge<H: crate::types::Host>(
1826 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1827) -> bool {
1828 use hjkl_buffer::{Edit, MotionKind, Position};
1829 ed.sync_buffer_content_from_textarea();
1830 let cursor = buf_cursor_pos(&ed.buffer);
1831 if cursor.col > 0 {
1832 ed.mutate_edit(Edit::DeleteRange {
1833 start: Position::new(cursor.row, 0),
1834 end: cursor,
1835 kind: MotionKind::Char,
1836 });
1837 ed.push_buffer_cursor_to_textarea();
1838 }
1839 true
1840}
1841
1842pub(crate) fn insert_ctrl_h_bridge<H: crate::types::Host>(
1846 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1847) -> bool {
1848 use hjkl_buffer::{Edit, MotionKind, Position};
1849 ed.sync_buffer_content_from_textarea();
1850 let cursor = buf_cursor_pos(&ed.buffer);
1851 if cursor.col > 0 {
1852 ed.mutate_edit(Edit::DeleteRange {
1853 start: Position::new(cursor.row, cursor.col - 1),
1854 end: cursor,
1855 kind: MotionKind::Char,
1856 });
1857 } else if cursor.row > 0 {
1858 let prev_row = cursor.row - 1;
1859 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1860 ed.mutate_edit(Edit::JoinLines {
1861 row: prev_row,
1862 count: 1,
1863 with_space: false,
1864 });
1865 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1866 }
1867 ed.push_buffer_cursor_to_textarea();
1868 true
1869}
1870
1871pub(crate) fn insert_ctrl_t_bridge<H: crate::types::Host>(
1874 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1875) -> bool {
1876 let (row, col) = ed.cursor();
1877 let sw = ed.settings().shiftwidth;
1878 indent_rows(ed, row, row, 1);
1879 ed.jump_cursor(row, col + sw);
1880 true
1881}
1882
1883pub(crate) fn insert_ctrl_d_bridge<H: crate::types::Host>(
1886 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1887) -> bool {
1888 let (row, col) = ed.cursor();
1889 let before_len = buf_line_bytes(&ed.buffer, row);
1890 outdent_rows(ed, row, row, 1);
1891 let after_len = buf_line_bytes(&ed.buffer, row);
1892 let stripped = before_len.saturating_sub(after_len);
1893 let new_col = col.saturating_sub(stripped);
1894 ed.jump_cursor(row, new_col);
1895 true
1896}
1897
1898pub(crate) fn insert_ctrl_o_bridge<H: crate::types::Host>(
1902 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1903) -> bool {
1904 ed.vim.one_shot_normal = true;
1905 ed.vim.mode = Mode::Normal;
1906 false
1907}
1908
1909pub(crate) fn insert_ctrl_r_bridge<H: crate::types::Host>(
1913 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1914) -> bool {
1915 ed.vim.insert_pending_register = true;
1916 false
1917}
1918
1919pub(crate) fn insert_paste_register_bridge<H: crate::types::Host>(
1923 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1924 reg: char,
1925) -> bool {
1926 insert_register_text(ed, reg);
1927 true
1930}
1931
1932pub(crate) fn leave_insert_to_normal_bridge<H: crate::types::Host>(
1937 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1938) -> bool {
1939 finish_insert_session(ed);
1940 ed.vim.mode = Mode::Normal;
1941 let col = ed.cursor().1;
1942 ed.vim.last_insert_pos = Some(ed.cursor());
1943 if col > 0 {
1944 crate::motions::move_left(&mut ed.buffer, 1);
1945 ed.push_buffer_cursor_to_textarea();
1946 }
1947 ed.sticky_col = Some(ed.cursor().1);
1948 true
1949}
1950
1951fn step_normal<H: crate::types::Host>(
1954 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1955 input: Input,
1956) -> bool {
1957 if let Key::Char(d @ '0'..='9') = input.key
1959 && !input.ctrl
1960 && !input.alt
1961 && !matches!(
1962 ed.vim.pending,
1963 Pending::Replace
1964 | Pending::Find { .. }
1965 | Pending::OpFind { .. }
1966 | Pending::VisualTextObj { .. }
1967 )
1968 && (d != '0' || ed.vim.count > 0)
1969 {
1970 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1971 return true;
1972 }
1973
1974 match std::mem::take(&mut ed.vim.pending) {
1976 Pending::Replace => return handle_replace(ed, input),
1977 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1978 Pending::OpFind {
1979 op,
1980 count1,
1981 forward,
1982 till,
1983 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1984 Pending::G => return handle_after_g(ed, input),
1985 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1986 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1987 Pending::OpTextObj { op, count1, inner } => {
1988 return handle_text_object(ed, input, op, count1, inner);
1989 }
1990 Pending::VisualTextObj { inner } => {
1991 return handle_visual_text_obj(ed, input, inner);
1992 }
1993 Pending::Z => return handle_after_z(ed, input),
1994 Pending::SetMark => return handle_set_mark(ed, input),
1995 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1996 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1997 Pending::SelectRegister => return handle_select_register(ed, input),
1998 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1999 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
2000 Pending::None => {}
2001 }
2002
2003 let count = take_count(&mut ed.vim);
2004
2005 match input.key {
2007 Key::Esc => {
2008 ed.vim.force_normal();
2009 return true;
2010 }
2011 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
2012 ed.vim.visual_anchor = ed.cursor();
2013 ed.vim.mode = Mode::Visual;
2014 return true;
2015 }
2016 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
2017 let (row, _) = ed.cursor();
2018 ed.vim.visual_line_anchor = row;
2019 ed.vim.mode = Mode::VisualLine;
2020 return true;
2021 }
2022 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
2023 ed.vim.visual_anchor = ed.cursor();
2024 ed.vim.mode = Mode::Visual;
2025 return true;
2026 }
2027 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
2028 let (row, _) = ed.cursor();
2029 ed.vim.visual_line_anchor = row;
2030 ed.vim.mode = Mode::VisualLine;
2031 return true;
2032 }
2033 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
2034 let cur = ed.cursor();
2035 ed.vim.block_anchor = cur;
2036 ed.vim.block_vcol = cur.1;
2037 ed.vim.mode = Mode::VisualBlock;
2038 return true;
2039 }
2040 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
2041 ed.vim.mode = Mode::Normal;
2043 return true;
2044 }
2045 Key::Char('o') if !input.ctrl => match ed.vim.mode {
2048 Mode::Visual => {
2049 let cur = ed.cursor();
2050 let anchor = ed.vim.visual_anchor;
2051 ed.vim.visual_anchor = cur;
2052 ed.jump_cursor(anchor.0, anchor.1);
2053 return true;
2054 }
2055 Mode::VisualLine => {
2056 let cur_row = ed.cursor().0;
2057 let anchor_row = ed.vim.visual_line_anchor;
2058 ed.vim.visual_line_anchor = cur_row;
2059 ed.jump_cursor(anchor_row, 0);
2060 return true;
2061 }
2062 Mode::VisualBlock => {
2063 let cur = ed.cursor();
2064 let anchor = ed.vim.block_anchor;
2065 ed.vim.block_anchor = cur;
2066 ed.vim.block_vcol = anchor.1;
2067 ed.jump_cursor(anchor.0, anchor.1);
2068 return true;
2069 }
2070 _ => {}
2071 },
2072 _ => {}
2073 }
2074
2075 if ed.vim.is_visual()
2077 && let Some(op) = visual_operator(&input)
2078 {
2079 apply_visual_operator(ed, op);
2080 return true;
2081 }
2082
2083 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
2087 match input.key {
2088 Key::Char('r') => {
2089 ed.vim.pending = Pending::Replace;
2090 return true;
2091 }
2092 Key::Char('I') => {
2093 let (top, bot, left, _right) = block_bounds(ed);
2094 ed.jump_cursor(top, left);
2095 ed.vim.mode = Mode::Normal;
2096 begin_insert(
2097 ed,
2098 1,
2099 InsertReason::BlockEdge {
2100 top,
2101 bot,
2102 col: left,
2103 },
2104 );
2105 return true;
2106 }
2107 Key::Char('A') => {
2108 let (top, bot, _left, right) = block_bounds(ed);
2109 let line_len = buf_line_chars(&ed.buffer, top);
2110 let col = (right + 1).min(line_len);
2111 ed.jump_cursor(top, col);
2112 ed.vim.mode = Mode::Normal;
2113 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
2114 return true;
2115 }
2116 _ => {}
2117 }
2118 }
2119
2120 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
2122 && !input.ctrl
2123 && matches!(input.key, Key::Char('i') | Key::Char('a'))
2124 {
2125 let inner = matches!(input.key, Key::Char('i'));
2126 ed.vim.pending = Pending::VisualTextObj { inner };
2127 return true;
2128 }
2129
2130 if input.ctrl
2135 && let Key::Char(c) = input.key
2136 {
2137 match c {
2138 'd' => {
2139 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
2140 return true;
2141 }
2142 'u' => {
2143 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
2144 return true;
2145 }
2146 'f' => {
2147 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
2148 return true;
2149 }
2150 'b' => {
2151 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
2152 return true;
2153 }
2154 'r' => {
2155 do_redo(ed);
2156 return true;
2157 }
2158 'a' if ed.vim.mode == Mode::Normal => {
2159 adjust_number(ed, count.max(1) as i64);
2160 return true;
2161 }
2162 'x' if ed.vim.mode == Mode::Normal => {
2163 adjust_number(ed, -(count.max(1) as i64));
2164 return true;
2165 }
2166 'o' if ed.vim.mode == Mode::Normal => {
2167 for _ in 0..count.max(1) {
2168 jump_back(ed);
2169 }
2170 return true;
2171 }
2172 'i' if ed.vim.mode == Mode::Normal => {
2173 for _ in 0..count.max(1) {
2174 jump_forward(ed);
2175 }
2176 return true;
2177 }
2178 _ => {}
2179 }
2180 }
2181
2182 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
2184 for _ in 0..count.max(1) {
2185 jump_forward(ed);
2186 }
2187 return true;
2188 }
2189
2190 if let Some(motion) = parse_motion(&input) {
2192 execute_motion(ed, motion.clone(), count);
2193 if ed.vim.mode == Mode::VisualBlock {
2195 update_block_vcol(ed, &motion);
2196 }
2197 if let Motion::Find { ch, forward, till } = motion {
2198 ed.vim.last_find = Some((ch, forward, till));
2199 }
2200 return true;
2201 }
2202
2203 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
2205 return true;
2206 }
2207
2208 if ed.vim.mode == Mode::Normal
2210 && let Key::Char(op_ch) = input.key
2211 && !input.ctrl
2212 && let Some(op) = char_to_operator(op_ch)
2213 {
2214 ed.vim.pending = Pending::Op { op, count1: count };
2215 return true;
2216 }
2217
2218 if ed.vim.mode == Mode::Normal
2220 && let Some((forward, till)) = find_entry(&input)
2221 {
2222 ed.vim.count = count;
2223 ed.vim.pending = Pending::Find { forward, till };
2224 return true;
2225 }
2226
2227 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
2229 ed.vim.count = count;
2230 ed.vim.pending = Pending::G;
2231 return true;
2232 }
2233
2234 if !input.ctrl
2236 && input.key == Key::Char('z')
2237 && matches!(
2238 ed.vim.mode,
2239 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2240 )
2241 {
2242 ed.vim.pending = Pending::Z;
2243 return true;
2244 }
2245
2246 if !input.ctrl
2252 && matches!(
2253 ed.vim.mode,
2254 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2255 )
2256 && input.key == Key::Char('`')
2257 {
2258 ed.vim.pending = Pending::GotoMarkChar;
2259 return true;
2260 }
2261 if !input.ctrl && ed.vim.mode == Mode::Normal {
2262 match input.key {
2263 Key::Char('m') => {
2264 ed.vim.pending = Pending::SetMark;
2265 return true;
2266 }
2267 Key::Char('\'') => {
2268 ed.vim.pending = Pending::GotoMarkLine;
2269 return true;
2270 }
2271 Key::Char('`') => {
2272 ed.vim.pending = Pending::GotoMarkChar;
2274 return true;
2275 }
2276 Key::Char('"') => {
2277 ed.vim.pending = Pending::SelectRegister;
2280 return true;
2281 }
2282 Key::Char('@') => {
2283 ed.vim.pending = Pending::PlayMacroTarget { count };
2287 return true;
2288 }
2289 Key::Char('q') if ed.vim.recording_macro.is_none() => {
2290 ed.vim.pending = Pending::RecordMacroTarget;
2295 return true;
2296 }
2297 _ => {}
2298 }
2299 }
2300
2301 true
2303}
2304
2305pub(crate) fn set_mark_at_cursor<H: crate::types::Host>(
2312 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2313 ch: char,
2314) {
2315 if ch.is_ascii_lowercase() || ch.is_ascii_uppercase() {
2316 let pos = ed.cursor();
2321 ed.set_mark(ch, pos);
2322 }
2323 }
2325
2326fn handle_set_mark<H: crate::types::Host>(
2327 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2328 input: Input,
2329) -> bool {
2330 if let Key::Char(c) = input.key {
2331 set_mark_at_cursor(ed, c);
2332 }
2333 true
2334}
2335
2336pub(crate) fn goto_mark<H: crate::types::Host>(
2345 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2346 ch: char,
2347 linewise: bool,
2348) {
2349 let target = match ch {
2351 'a'..='z' | 'A'..='Z' => ed.mark(ch),
2352 '\'' | '`' => ed.vim.jump_back.last().copied(),
2353 '.' => ed.vim.last_edit_pos,
2354 '[' | ']' | '<' | '>' => ed.mark(ch),
2355 _ => None,
2356 };
2357 let Some((row, col)) = target else {
2358 return;
2359 };
2360 let pre = ed.cursor();
2361 let (r, c_clamped) = clamp_pos(ed, (row, col));
2362 if linewise {
2363 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2364 ed.push_buffer_cursor_to_textarea();
2365 move_first_non_whitespace(ed);
2366 } else {
2367 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2368 ed.push_buffer_cursor_to_textarea();
2369 }
2370 if ed.cursor() != pre {
2371 push_jump(ed, pre);
2372 }
2373 ed.sticky_col = Some(ed.cursor().1);
2374}
2375
2376fn handle_select_register<H: crate::types::Host>(
2382 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2383 input: Input,
2384) -> bool {
2385 if let Key::Char(c) = input.key {
2386 ed.set_pending_register(c);
2387 }
2388 true
2389}
2390
2391fn handle_record_macro_target<H: crate::types::Host>(
2396 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2397 input: Input,
2398) -> bool {
2399 if let Key::Char(c) = input.key
2400 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2401 {
2402 ed.vim.recording_macro = Some(c);
2403 if c.is_ascii_uppercase() {
2406 let lower = c.to_ascii_lowercase();
2407 let text = ed
2411 .registers()
2412 .read(lower)
2413 .map(|s| s.text.clone())
2414 .unwrap_or_default();
2415 ed.vim.recording_keys = crate::input::decode_macro(&text);
2416 } else {
2417 ed.vim.recording_keys.clear();
2418 }
2419 }
2420 true
2421}
2422
2423fn handle_play_macro_target<H: crate::types::Host>(
2429 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2430 input: Input,
2431 count: usize,
2432) -> bool {
2433 let reg = match input.key {
2434 Key::Char('@') => ed.vim.last_macro,
2435 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2436 Some(c.to_ascii_lowercase())
2437 }
2438 _ => None,
2439 };
2440 let Some(reg) = reg else {
2441 return true;
2442 };
2443 let text = match ed.registers().read(reg) {
2446 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2447 _ => return true,
2448 };
2449 let keys = crate::input::decode_macro(&text);
2450 ed.vim.last_macro = Some(reg);
2451 let times = count.max(1);
2452 let was_replaying = ed.vim.replaying_macro;
2453 ed.vim.replaying_macro = true;
2454 for _ in 0..times {
2455 for k in keys.iter().copied() {
2456 step(ed, k);
2457 }
2458 }
2459 ed.vim.replaying_macro = was_replaying;
2460 true
2461}
2462
2463fn handle_goto_mark<H: crate::types::Host>(
2464 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2465 input: Input,
2466 linewise: bool,
2467) -> bool {
2468 let Key::Char(c) = input.key else {
2469 return true;
2470 };
2471 goto_mark(ed, c, linewise);
2475 true
2476}
2477
2478fn take_count(vim: &mut VimState) -> usize {
2479 if vim.count > 0 {
2480 let n = vim.count;
2481 vim.count = 0;
2482 n
2483 } else {
2484 1
2485 }
2486}
2487
2488fn char_to_operator(c: char) -> Option<Operator> {
2489 match c {
2490 'd' => Some(Operator::Delete),
2491 'c' => Some(Operator::Change),
2492 'y' => Some(Operator::Yank),
2493 '>' => Some(Operator::Indent),
2494 '<' => Some(Operator::Outdent),
2495 _ => None,
2496 }
2497}
2498
2499fn visual_operator(input: &Input) -> Option<Operator> {
2500 if input.ctrl {
2501 return None;
2502 }
2503 match input.key {
2504 Key::Char('y') => Some(Operator::Yank),
2505 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2506 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2507 Key::Char('U') => Some(Operator::Uppercase),
2509 Key::Char('u') => Some(Operator::Lowercase),
2510 Key::Char('~') => Some(Operator::ToggleCase),
2511 Key::Char('>') => Some(Operator::Indent),
2513 Key::Char('<') => Some(Operator::Outdent),
2514 _ => None,
2515 }
2516}
2517
2518fn find_entry(input: &Input) -> Option<(bool, bool)> {
2519 if input.ctrl {
2520 return None;
2521 }
2522 match input.key {
2523 Key::Char('f') => Some((true, false)),
2524 Key::Char('F') => Some((false, false)),
2525 Key::Char('t') => Some((true, true)),
2526 Key::Char('T') => Some((false, true)),
2527 _ => None,
2528 }
2529}
2530
2531const JUMPLIST_MAX: usize = 100;
2535
2536fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2541 ed.vim.jump_back.push(from);
2542 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2543 ed.vim.jump_back.remove(0);
2544 }
2545 ed.vim.jump_fwd.clear();
2546}
2547
2548fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2551 let Some(target) = ed.vim.jump_back.pop() else {
2552 return;
2553 };
2554 let cur = ed.cursor();
2555 ed.vim.jump_fwd.push(cur);
2556 let (r, c) = clamp_pos(ed, target);
2557 ed.jump_cursor(r, c);
2558 ed.sticky_col = Some(c);
2559}
2560
2561fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2564 let Some(target) = ed.vim.jump_fwd.pop() else {
2565 return;
2566 };
2567 let cur = ed.cursor();
2568 ed.vim.jump_back.push(cur);
2569 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2570 ed.vim.jump_back.remove(0);
2571 }
2572 let (r, c) = clamp_pos(ed, target);
2573 ed.jump_cursor(r, c);
2574 ed.sticky_col = Some(c);
2575}
2576
2577fn clamp_pos<H: crate::types::Host>(
2580 ed: &Editor<hjkl_buffer::Buffer, H>,
2581 pos: (usize, usize),
2582) -> (usize, usize) {
2583 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2584 let r = pos.0.min(last_row);
2585 let line_len = buf_line_chars(&ed.buffer, r);
2586 let c = pos.1.min(line_len.saturating_sub(1));
2587 (r, c)
2588}
2589
2590fn is_big_jump(motion: &Motion) -> bool {
2592 matches!(
2593 motion,
2594 Motion::FileTop
2595 | Motion::FileBottom
2596 | Motion::MatchBracket
2597 | Motion::WordAtCursor { .. }
2598 | Motion::SearchNext { .. }
2599 | Motion::ViewportTop
2600 | Motion::ViewportMiddle
2601 | Motion::ViewportBottom
2602 )
2603}
2604
2605fn viewport_half_rows<H: crate::types::Host>(
2610 ed: &Editor<hjkl_buffer::Buffer, H>,
2611 count: usize,
2612) -> usize {
2613 let h = ed.viewport_height_value() as usize;
2614 (h / 2).max(1).saturating_mul(count.max(1))
2615}
2616
2617fn viewport_full_rows<H: crate::types::Host>(
2620 ed: &Editor<hjkl_buffer::Buffer, H>,
2621 count: usize,
2622) -> usize {
2623 let h = ed.viewport_height_value() as usize;
2624 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2625}
2626
2627fn scroll_cursor_rows<H: crate::types::Host>(
2632 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2633 delta: isize,
2634) {
2635 if delta == 0 {
2636 return;
2637 }
2638 ed.sync_buffer_content_from_textarea();
2639 let (row, _) = ed.cursor();
2640 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2641 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2642 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2643 crate::motions::move_first_non_blank(&mut ed.buffer);
2644 ed.push_buffer_cursor_to_textarea();
2645 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2646}
2647
2648fn parse_motion(input: &Input) -> Option<Motion> {
2651 if input.ctrl {
2652 return None;
2653 }
2654 match input.key {
2655 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2656 Key::Char('l') | Key::Right => Some(Motion::Right),
2657 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2658 Key::Char('k') | Key::Up => Some(Motion::Up),
2659 Key::Char('w') => Some(Motion::WordFwd),
2660 Key::Char('W') => Some(Motion::BigWordFwd),
2661 Key::Char('b') => Some(Motion::WordBack),
2662 Key::Char('B') => Some(Motion::BigWordBack),
2663 Key::Char('e') => Some(Motion::WordEnd),
2664 Key::Char('E') => Some(Motion::BigWordEnd),
2665 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2666 Key::Char('^') => Some(Motion::FirstNonBlank),
2667 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2668 Key::Char('G') => Some(Motion::FileBottom),
2669 Key::Char('%') => Some(Motion::MatchBracket),
2670 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2671 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2672 Key::Char('*') => Some(Motion::WordAtCursor {
2673 forward: true,
2674 whole_word: true,
2675 }),
2676 Key::Char('#') => Some(Motion::WordAtCursor {
2677 forward: false,
2678 whole_word: true,
2679 }),
2680 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2681 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2682 Key::Char('H') => Some(Motion::ViewportTop),
2683 Key::Char('M') => Some(Motion::ViewportMiddle),
2684 Key::Char('L') => Some(Motion::ViewportBottom),
2685 Key::Char('{') => Some(Motion::ParagraphPrev),
2686 Key::Char('}') => Some(Motion::ParagraphNext),
2687 Key::Char('(') => Some(Motion::SentencePrev),
2688 Key::Char(')') => Some(Motion::SentenceNext),
2689 _ => None,
2690 }
2691}
2692
2693pub(crate) fn execute_motion<H: crate::types::Host>(
2696 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2697 motion: Motion,
2698 count: usize,
2699) {
2700 let count = count.max(1);
2701 let motion = match motion {
2703 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2704 Some((ch, forward, till)) => Motion::Find {
2705 ch,
2706 forward: if reverse { !forward } else { forward },
2707 till,
2708 },
2709 None => return,
2710 },
2711 other => other,
2712 };
2713 let pre_pos = ed.cursor();
2714 let pre_col = pre_pos.1;
2715 apply_motion_cursor(ed, &motion, count);
2716 let post_pos = ed.cursor();
2717 if is_big_jump(&motion) && pre_pos != post_pos {
2718 push_jump(ed, pre_pos);
2719 }
2720 apply_sticky_col(ed, &motion, pre_col);
2721 ed.sync_buffer_from_textarea();
2726}
2727
2728fn execute_motion_with_block_vcol<H: crate::types::Host>(
2739 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2740 motion: Motion,
2741 count: usize,
2742) {
2743 let motion_copy = motion.clone();
2744 execute_motion(ed, motion, count);
2745 if ed.vim.mode == Mode::VisualBlock {
2746 update_block_vcol(ed, &motion_copy);
2747 }
2748}
2749
2750pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2782 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2783 kind: hjkl_vim::MotionKind,
2784 count: usize,
2785) {
2786 let count = count.max(1);
2787 match kind {
2788 hjkl_vim::MotionKind::CharLeft => {
2789 execute_motion_with_block_vcol(ed, Motion::Left, count);
2790 }
2791 hjkl_vim::MotionKind::CharRight => {
2792 execute_motion_with_block_vcol(ed, Motion::Right, count);
2793 }
2794 hjkl_vim::MotionKind::LineDown => {
2795 execute_motion_with_block_vcol(ed, Motion::Down, count);
2796 }
2797 hjkl_vim::MotionKind::LineUp => {
2798 execute_motion_with_block_vcol(ed, Motion::Up, count);
2799 }
2800 hjkl_vim::MotionKind::FirstNonBlankDown => {
2801 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2806 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2807 crate::motions::move_first_non_blank(&mut ed.buffer);
2808 ed.push_buffer_cursor_to_textarea();
2809 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2810 ed.sync_buffer_from_textarea();
2811 }
2812 hjkl_vim::MotionKind::FirstNonBlankUp => {
2813 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2816 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2817 crate::motions::move_first_non_blank(&mut ed.buffer);
2818 ed.push_buffer_cursor_to_textarea();
2819 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2820 ed.sync_buffer_from_textarea();
2821 }
2822 hjkl_vim::MotionKind::WordForward => {
2823 execute_motion_with_block_vcol(ed, Motion::WordFwd, count);
2824 }
2825 hjkl_vim::MotionKind::BigWordForward => {
2826 execute_motion_with_block_vcol(ed, Motion::BigWordFwd, count);
2827 }
2828 hjkl_vim::MotionKind::WordBackward => {
2829 execute_motion_with_block_vcol(ed, Motion::WordBack, count);
2830 }
2831 hjkl_vim::MotionKind::BigWordBackward => {
2832 execute_motion_with_block_vcol(ed, Motion::BigWordBack, count);
2833 }
2834 hjkl_vim::MotionKind::WordEnd => {
2835 execute_motion_with_block_vcol(ed, Motion::WordEnd, count);
2836 }
2837 hjkl_vim::MotionKind::BigWordEnd => {
2838 execute_motion_with_block_vcol(ed, Motion::BigWordEnd, count);
2839 }
2840 hjkl_vim::MotionKind::LineStart => {
2841 execute_motion_with_block_vcol(ed, Motion::LineStart, 1);
2844 }
2845 hjkl_vim::MotionKind::FirstNonBlank => {
2846 execute_motion_with_block_vcol(ed, Motion::FirstNonBlank, 1);
2849 }
2850 hjkl_vim::MotionKind::GotoLine => {
2851 execute_motion_with_block_vcol(ed, Motion::FileBottom, count);
2860 }
2861 hjkl_vim::MotionKind::LineEnd => {
2862 execute_motion_with_block_vcol(ed, Motion::LineEnd, 1);
2866 }
2867 hjkl_vim::MotionKind::FindRepeat => {
2868 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: false }, count);
2872 }
2873 hjkl_vim::MotionKind::FindRepeatReverse => {
2874 execute_motion_with_block_vcol(ed, Motion::FindRepeat { reverse: true }, count);
2878 }
2879 hjkl_vim::MotionKind::BracketMatch => {
2880 execute_motion_with_block_vcol(ed, Motion::MatchBracket, count);
2885 }
2886 hjkl_vim::MotionKind::ViewportTop => {
2887 execute_motion_with_block_vcol(ed, Motion::ViewportTop, count);
2890 }
2891 hjkl_vim::MotionKind::ViewportMiddle => {
2892 execute_motion_with_block_vcol(ed, Motion::ViewportMiddle, count);
2895 }
2896 hjkl_vim::MotionKind::ViewportBottom => {
2897 execute_motion_with_block_vcol(ed, Motion::ViewportBottom, count);
2900 }
2901 hjkl_vim::MotionKind::HalfPageDown => {
2902 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
2908 }
2909 hjkl_vim::MotionKind::HalfPageUp => {
2910 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
2913 }
2914 hjkl_vim::MotionKind::FullPageDown => {
2915 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
2918 }
2919 hjkl_vim::MotionKind::FullPageUp => {
2920 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
2923 }
2924 _ => {
2925 }
2929 }
2930}
2931
2932fn apply_sticky_col<H: crate::types::Host>(
2937 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2938 motion: &Motion,
2939 pre_col: usize,
2940) {
2941 if is_vertical_motion(motion) {
2942 let want = ed.sticky_col.unwrap_or(pre_col);
2943 ed.sticky_col = Some(want);
2946 let (row, _) = ed.cursor();
2947 let line_len = buf_line_chars(&ed.buffer, row);
2948 let max_col = line_len.saturating_sub(1);
2952 let target = want.min(max_col);
2953 ed.jump_cursor(row, target);
2954 } else {
2955 ed.sticky_col = Some(ed.cursor().1);
2958 }
2959}
2960
2961fn is_vertical_motion(motion: &Motion) -> bool {
2962 matches!(
2966 motion,
2967 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2968 )
2969}
2970
2971fn apply_motion_cursor<H: crate::types::Host>(
2972 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2973 motion: &Motion,
2974 count: usize,
2975) {
2976 apply_motion_cursor_ctx(ed, motion, count, false)
2977}
2978
2979fn apply_motion_cursor_ctx<H: crate::types::Host>(
2980 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2981 motion: &Motion,
2982 count: usize,
2983 as_operator: bool,
2984) {
2985 match motion {
2986 Motion::Left => {
2987 crate::motions::move_left(&mut ed.buffer, count);
2989 ed.push_buffer_cursor_to_textarea();
2990 }
2991 Motion::Right => {
2992 if as_operator {
2996 crate::motions::move_right_to_end(&mut ed.buffer, count);
2997 } else {
2998 crate::motions::move_right_in_line(&mut ed.buffer, count);
2999 }
3000 ed.push_buffer_cursor_to_textarea();
3001 }
3002 Motion::Up => {
3003 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3007 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3008 ed.push_buffer_cursor_to_textarea();
3009 }
3010 Motion::Down => {
3011 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3012 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
3013 ed.push_buffer_cursor_to_textarea();
3014 }
3015 Motion::ScreenUp => {
3016 let v = *ed.host.viewport();
3017 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3018 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3019 ed.push_buffer_cursor_to_textarea();
3020 }
3021 Motion::ScreenDown => {
3022 let v = *ed.host.viewport();
3023 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3024 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
3025 ed.push_buffer_cursor_to_textarea();
3026 }
3027 Motion::WordFwd => {
3028 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3029 ed.push_buffer_cursor_to_textarea();
3030 }
3031 Motion::WordBack => {
3032 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3033 ed.push_buffer_cursor_to_textarea();
3034 }
3035 Motion::WordEnd => {
3036 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
3037 ed.push_buffer_cursor_to_textarea();
3038 }
3039 Motion::BigWordFwd => {
3040 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3041 ed.push_buffer_cursor_to_textarea();
3042 }
3043 Motion::BigWordBack => {
3044 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3045 ed.push_buffer_cursor_to_textarea();
3046 }
3047 Motion::BigWordEnd => {
3048 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3049 ed.push_buffer_cursor_to_textarea();
3050 }
3051 Motion::WordEndBack => {
3052 crate::motions::move_word_end_back(
3053 &mut ed.buffer,
3054 false,
3055 count,
3056 &ed.settings.iskeyword,
3057 );
3058 ed.push_buffer_cursor_to_textarea();
3059 }
3060 Motion::BigWordEndBack => {
3061 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
3062 ed.push_buffer_cursor_to_textarea();
3063 }
3064 Motion::LineStart => {
3065 crate::motions::move_line_start(&mut ed.buffer);
3066 ed.push_buffer_cursor_to_textarea();
3067 }
3068 Motion::FirstNonBlank => {
3069 crate::motions::move_first_non_blank(&mut ed.buffer);
3070 ed.push_buffer_cursor_to_textarea();
3071 }
3072 Motion::LineEnd => {
3073 crate::motions::move_line_end(&mut ed.buffer);
3075 ed.push_buffer_cursor_to_textarea();
3076 }
3077 Motion::FileTop => {
3078 if count > 1 {
3081 crate::motions::move_bottom(&mut ed.buffer, count);
3082 } else {
3083 crate::motions::move_top(&mut ed.buffer);
3084 }
3085 ed.push_buffer_cursor_to_textarea();
3086 }
3087 Motion::FileBottom => {
3088 if count > 1 {
3091 crate::motions::move_bottom(&mut ed.buffer, count);
3092 } else {
3093 crate::motions::move_bottom(&mut ed.buffer, 0);
3094 }
3095 ed.push_buffer_cursor_to_textarea();
3096 }
3097 Motion::Find { ch, forward, till } => {
3098 for _ in 0..count {
3099 if !find_char_on_line(ed, *ch, *forward, *till) {
3100 break;
3101 }
3102 }
3103 }
3104 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
3106 let _ = matching_bracket(ed);
3107 }
3108 Motion::WordAtCursor {
3109 forward,
3110 whole_word,
3111 } => {
3112 word_at_cursor_search(ed, *forward, *whole_word, count);
3113 }
3114 Motion::SearchNext { reverse } => {
3115 if let Some(pattern) = ed.vim.last_search.clone() {
3119 push_search_pattern(ed, &pattern);
3120 }
3121 if ed.search_state().pattern.is_none() {
3122 return;
3123 }
3124 let forward = ed.vim.last_search_forward != *reverse;
3128 for _ in 0..count.max(1) {
3129 if forward {
3130 ed.search_advance_forward(true);
3131 } else {
3132 ed.search_advance_backward(true);
3133 }
3134 }
3135 ed.push_buffer_cursor_to_textarea();
3136 }
3137 Motion::ViewportTop => {
3138 let v = *ed.host().viewport();
3139 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
3140 ed.push_buffer_cursor_to_textarea();
3141 }
3142 Motion::ViewportMiddle => {
3143 let v = *ed.host().viewport();
3144 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
3145 ed.push_buffer_cursor_to_textarea();
3146 }
3147 Motion::ViewportBottom => {
3148 let v = *ed.host().viewport();
3149 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
3150 ed.push_buffer_cursor_to_textarea();
3151 }
3152 Motion::LastNonBlank => {
3153 crate::motions::move_last_non_blank(&mut ed.buffer);
3154 ed.push_buffer_cursor_to_textarea();
3155 }
3156 Motion::LineMiddle => {
3157 let row = ed.cursor().0;
3158 let line_chars = buf_line_chars(&ed.buffer, row);
3159 let target = line_chars / 2;
3162 ed.jump_cursor(row, target);
3163 }
3164 Motion::ParagraphPrev => {
3165 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
3166 ed.push_buffer_cursor_to_textarea();
3167 }
3168 Motion::ParagraphNext => {
3169 crate::motions::move_paragraph_next(&mut ed.buffer, count);
3170 ed.push_buffer_cursor_to_textarea();
3171 }
3172 Motion::SentencePrev => {
3173 for _ in 0..count.max(1) {
3174 if let Some((row, col)) = sentence_boundary(ed, false) {
3175 ed.jump_cursor(row, col);
3176 }
3177 }
3178 }
3179 Motion::SentenceNext => {
3180 for _ in 0..count.max(1) {
3181 if let Some((row, col)) = sentence_boundary(ed, true) {
3182 ed.jump_cursor(row, col);
3183 }
3184 }
3185 }
3186 }
3187}
3188
3189fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3190 ed.sync_buffer_content_from_textarea();
3196 crate::motions::move_first_non_blank(&mut ed.buffer);
3197 ed.push_buffer_cursor_to_textarea();
3198}
3199
3200fn find_char_on_line<H: crate::types::Host>(
3201 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3202 ch: char,
3203 forward: bool,
3204 till: bool,
3205) -> bool {
3206 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
3207 if moved {
3208 ed.push_buffer_cursor_to_textarea();
3209 }
3210 moved
3211}
3212
3213fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
3214 let moved = crate::motions::match_bracket(&mut ed.buffer);
3215 if moved {
3216 ed.push_buffer_cursor_to_textarea();
3217 }
3218 moved
3219}
3220
3221fn word_at_cursor_search<H: crate::types::Host>(
3222 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3223 forward: bool,
3224 whole_word: bool,
3225 count: usize,
3226) {
3227 let (row, col) = ed.cursor();
3228 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
3229 let chars: Vec<char> = line.chars().collect();
3230 if chars.is_empty() {
3231 return;
3232 }
3233 let spec = ed.settings().iskeyword.clone();
3235 let is_word = |c: char| is_keyword_char(c, &spec);
3236 let mut start = col.min(chars.len().saturating_sub(1));
3237 while start > 0 && is_word(chars[start - 1]) {
3238 start -= 1;
3239 }
3240 let mut end = start;
3241 while end < chars.len() && is_word(chars[end]) {
3242 end += 1;
3243 }
3244 if end <= start {
3245 return;
3246 }
3247 let word: String = chars[start..end].iter().collect();
3248 let escaped = regex_escape(&word);
3249 let pattern = if whole_word {
3250 format!(r"\b{escaped}\b")
3251 } else {
3252 escaped
3253 };
3254 push_search_pattern(ed, &pattern);
3255 if ed.search_state().pattern.is_none() {
3256 return;
3257 }
3258 ed.vim.last_search = Some(pattern);
3260 ed.vim.last_search_forward = forward;
3261 for _ in 0..count.max(1) {
3262 if forward {
3263 ed.search_advance_forward(true);
3264 } else {
3265 ed.search_advance_backward(true);
3266 }
3267 }
3268 ed.push_buffer_cursor_to_textarea();
3269}
3270
3271fn regex_escape(s: &str) -> String {
3272 let mut out = String::with_capacity(s.len());
3273 for c in s.chars() {
3274 if matches!(
3275 c,
3276 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
3277 ) {
3278 out.push('\\');
3279 }
3280 out.push(c);
3281 }
3282 out
3283}
3284
3285pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
3299 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3300 op: Operator,
3301 motion_key: char,
3302 total_count: usize,
3303) {
3304 let input = Input {
3305 key: Key::Char(motion_key),
3306 ctrl: false,
3307 alt: false,
3308 shift: false,
3309 };
3310 let Some(motion) = parse_motion(&input) else {
3311 return;
3312 };
3313 let motion = match motion {
3314 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3315 Some((ch, forward, till)) => Motion::Find {
3316 ch,
3317 forward: if reverse { !forward } else { forward },
3318 till,
3319 },
3320 None => return,
3321 },
3322 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3324 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3325 m => m,
3326 };
3327 apply_op_with_motion(ed, op, &motion, total_count);
3328 if let Motion::Find { ch, forward, till } = &motion {
3329 ed.vim.last_find = Some((*ch, *forward, *till));
3330 }
3331 if !ed.vim.replaying && op_is_change(op) {
3332 ed.vim.last_change = Some(LastChange::OpMotion {
3333 op,
3334 motion,
3335 count: total_count,
3336 inserted: None,
3337 });
3338 }
3339}
3340
3341pub(crate) fn apply_op_double<H: crate::types::Host>(
3344 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3345 op: Operator,
3346 total_count: usize,
3347) {
3348 execute_line_op(ed, op, total_count);
3349 if !ed.vim.replaying {
3350 ed.vim.last_change = Some(LastChange::LineOp {
3351 op,
3352 count: total_count,
3353 inserted: None,
3354 });
3355 }
3356}
3357
3358fn handle_after_op<H: crate::types::Host>(
3359 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3360 input: Input,
3361 op: Operator,
3362 count1: usize,
3363) -> bool {
3364 if let Key::Char(d @ '0'..='9') = input.key
3366 && !input.ctrl
3367 && (d != '0' || ed.vim.count > 0)
3368 {
3369 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
3370 ed.vim.pending = Pending::Op { op, count1 };
3371 return true;
3372 }
3373
3374 if input.key == Key::Esc {
3376 ed.vim.count = 0;
3377 return true;
3378 }
3379
3380 let double_ch = match op {
3384 Operator::Delete => Some('d'),
3385 Operator::Change => Some('c'),
3386 Operator::Yank => Some('y'),
3387 Operator::Indent => Some('>'),
3388 Operator::Outdent => Some('<'),
3389 Operator::Uppercase => Some('U'),
3390 Operator::Lowercase => Some('u'),
3391 Operator::ToggleCase => Some('~'),
3392 Operator::Fold => None,
3393 Operator::Reflow => Some('q'),
3396 };
3397 if let Key::Char(c) = input.key
3398 && !input.ctrl
3399 && Some(c) == double_ch
3400 {
3401 let count2 = take_count(&mut ed.vim);
3402 let total = count1.max(1) * count2.max(1);
3403 execute_line_op(ed, op, total);
3404 if !ed.vim.replaying {
3405 ed.vim.last_change = Some(LastChange::LineOp {
3406 op,
3407 count: total,
3408 inserted: None,
3409 });
3410 }
3411 return true;
3412 }
3413
3414 if let Key::Char('i') | Key::Char('a') = input.key
3416 && !input.ctrl
3417 {
3418 let inner = matches!(input.key, Key::Char('i'));
3419 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
3420 return true;
3421 }
3422
3423 if input.key == Key::Char('g') && !input.ctrl {
3425 ed.vim.pending = Pending::OpG { op, count1 };
3426 return true;
3427 }
3428
3429 if let Some((forward, till)) = find_entry(&input) {
3431 ed.vim.pending = Pending::OpFind {
3432 op,
3433 count1,
3434 forward,
3435 till,
3436 };
3437 return true;
3438 }
3439
3440 let count2 = take_count(&mut ed.vim);
3442 let total = count1.max(1) * count2.max(1);
3443 if let Some(motion) = parse_motion(&input) {
3444 let motion = match motion {
3445 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3446 Some((ch, forward, till)) => Motion::Find {
3447 ch,
3448 forward: if reverse { !forward } else { forward },
3449 till,
3450 },
3451 None => return true,
3452 },
3453 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3457 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3458 m => m,
3459 };
3460 apply_op_with_motion(ed, op, &motion, total);
3461 if let Motion::Find { ch, forward, till } = &motion {
3462 ed.vim.last_find = Some((*ch, *forward, *till));
3463 }
3464 if !ed.vim.replaying && op_is_change(op) {
3465 ed.vim.last_change = Some(LastChange::OpMotion {
3466 op,
3467 motion,
3468 count: total,
3469 inserted: None,
3470 });
3471 }
3472 return true;
3473 }
3474
3475 true
3477}
3478
3479pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3489 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3490 op: Operator,
3491 ch: char,
3492 total_count: usize,
3493) {
3494 if matches!(
3497 op,
3498 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3499 ) {
3500 let op_char = match op {
3501 Operator::Uppercase => 'U',
3502 Operator::Lowercase => 'u',
3503 Operator::ToggleCase => '~',
3504 _ => unreachable!(),
3505 };
3506 if ch == op_char {
3507 execute_line_op(ed, op, total_count);
3508 if !ed.vim.replaying {
3509 ed.vim.last_change = Some(LastChange::LineOp {
3510 op,
3511 count: total_count,
3512 inserted: None,
3513 });
3514 }
3515 return;
3516 }
3517 }
3518 let motion = match ch {
3519 'g' => Motion::FileTop,
3520 'e' => Motion::WordEndBack,
3521 'E' => Motion::BigWordEndBack,
3522 'j' => Motion::ScreenDown,
3523 'k' => Motion::ScreenUp,
3524 _ => return, };
3526 apply_op_with_motion(ed, op, &motion, total_count);
3527 if !ed.vim.replaying && op_is_change(op) {
3528 ed.vim.last_change = Some(LastChange::OpMotion {
3529 op,
3530 motion,
3531 count: total_count,
3532 inserted: None,
3533 });
3534 }
3535}
3536
3537fn handle_op_after_g<H: crate::types::Host>(
3538 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3539 input: Input,
3540 op: Operator,
3541 count1: usize,
3542) -> bool {
3543 if input.ctrl {
3544 return true;
3545 }
3546 let count2 = take_count(&mut ed.vim);
3547 let total = count1.max(1) * count2.max(1);
3548 if let Key::Char(ch) = input.key {
3549 apply_op_g_inner(ed, op, ch, total);
3550 }
3551 true
3552}
3553
3554fn handle_after_g<H: crate::types::Host>(
3555 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3556 input: Input,
3557) -> bool {
3558 let count = take_count(&mut ed.vim);
3559 if let Key::Char(ch) = input.key {
3562 apply_after_g(ed, ch, count);
3563 }
3564 true
3565}
3566
3567pub(crate) fn apply_after_g<H: crate::types::Host>(
3572 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3573 ch: char,
3574 count: usize,
3575) {
3576 match ch {
3577 'g' => {
3578 let pre = ed.cursor();
3580 if count > 1 {
3581 ed.jump_cursor(count - 1, 0);
3582 } else {
3583 ed.jump_cursor(0, 0);
3584 }
3585 move_first_non_whitespace(ed);
3586 if ed.cursor() != pre {
3587 push_jump(ed, pre);
3588 }
3589 }
3590 'e' => execute_motion(ed, Motion::WordEndBack, count),
3591 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3592 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3594 'M' => execute_motion(ed, Motion::LineMiddle, count),
3596 'v' => {
3598 if let Some(snap) = ed.vim.last_visual {
3599 match snap.mode {
3600 Mode::Visual => {
3601 ed.vim.visual_anchor = snap.anchor;
3602 ed.vim.mode = Mode::Visual;
3603 }
3604 Mode::VisualLine => {
3605 ed.vim.visual_line_anchor = snap.anchor.0;
3606 ed.vim.mode = Mode::VisualLine;
3607 }
3608 Mode::VisualBlock => {
3609 ed.vim.block_anchor = snap.anchor;
3610 ed.vim.block_vcol = snap.block_vcol;
3611 ed.vim.mode = Mode::VisualBlock;
3612 }
3613 _ => {}
3614 }
3615 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3616 }
3617 }
3618 'j' => execute_motion(ed, Motion::ScreenDown, count),
3622 'k' => execute_motion(ed, Motion::ScreenUp, count),
3623 'U' => {
3627 ed.vim.pending = Pending::Op {
3628 op: Operator::Uppercase,
3629 count1: count,
3630 };
3631 }
3632 'u' => {
3633 ed.vim.pending = Pending::Op {
3634 op: Operator::Lowercase,
3635 count1: count,
3636 };
3637 }
3638 '~' => {
3639 ed.vim.pending = Pending::Op {
3640 op: Operator::ToggleCase,
3641 count1: count,
3642 };
3643 }
3644 'q' => {
3645 ed.vim.pending = Pending::Op {
3648 op: Operator::Reflow,
3649 count1: count,
3650 };
3651 }
3652 'J' => {
3653 for _ in 0..count.max(1) {
3655 ed.push_undo();
3656 join_line_raw(ed);
3657 }
3658 if !ed.vim.replaying {
3659 ed.vim.last_change = Some(LastChange::JoinLine {
3660 count: count.max(1),
3661 });
3662 }
3663 }
3664 'd' => {
3665 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3670 }
3671 'i' => {
3676 if let Some((row, col)) = ed.vim.last_insert_pos {
3677 ed.jump_cursor(row, col);
3678 }
3679 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3680 }
3681 ';' => walk_change_list(ed, -1, count.max(1)),
3684 ',' => walk_change_list(ed, 1, count.max(1)),
3685 '*' => execute_motion(
3689 ed,
3690 Motion::WordAtCursor {
3691 forward: true,
3692 whole_word: false,
3693 },
3694 count,
3695 ),
3696 '#' => execute_motion(
3697 ed,
3698 Motion::WordAtCursor {
3699 forward: false,
3700 whole_word: false,
3701 },
3702 count,
3703 ),
3704 _ => {}
3705 }
3706}
3707
3708fn handle_after_z<H: crate::types::Host>(
3709 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3710 input: Input,
3711) -> bool {
3712 let count = take_count(&mut ed.vim);
3713 if let Key::Char(ch) = input.key {
3716 apply_after_z(ed, ch, count);
3717 }
3718 true
3719}
3720
3721pub(crate) fn apply_after_z<H: crate::types::Host>(
3726 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3727 ch: char,
3728 count: usize,
3729) {
3730 use crate::editor::CursorScrollTarget;
3731 let row = ed.cursor().0;
3732 match ch {
3733 'z' => {
3734 ed.scroll_cursor_to(CursorScrollTarget::Center);
3735 ed.vim.viewport_pinned = true;
3736 }
3737 't' => {
3738 ed.scroll_cursor_to(CursorScrollTarget::Top);
3739 ed.vim.viewport_pinned = true;
3740 }
3741 'b' => {
3742 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3743 ed.vim.viewport_pinned = true;
3744 }
3745 'o' => {
3750 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3751 }
3752 'c' => {
3753 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3754 }
3755 'a' => {
3756 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3757 }
3758 'R' => {
3759 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3760 }
3761 'M' => {
3762 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3763 }
3764 'E' => {
3765 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3766 }
3767 'd' => {
3768 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3769 }
3770 'f' => {
3771 if matches!(
3772 ed.vim.mode,
3773 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3774 ) {
3775 let anchor_row = match ed.vim.mode {
3778 Mode::VisualLine => ed.vim.visual_line_anchor,
3779 Mode::VisualBlock => ed.vim.block_anchor.0,
3780 _ => ed.vim.visual_anchor.0,
3781 };
3782 let cur = ed.cursor().0;
3783 let top = anchor_row.min(cur);
3784 let bot = anchor_row.max(cur);
3785 ed.apply_fold_op(crate::types::FoldOp::Add {
3786 start_row: top,
3787 end_row: bot,
3788 closed: true,
3789 });
3790 ed.vim.mode = Mode::Normal;
3791 } else {
3792 ed.vim.pending = Pending::Op {
3797 op: Operator::Fold,
3798 count1: count,
3799 };
3800 }
3801 }
3802 _ => {}
3803 }
3804}
3805
3806fn handle_replace<H: crate::types::Host>(
3807 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3808 input: Input,
3809) -> bool {
3810 if let Key::Char(ch) = input.key {
3811 if ed.vim.mode == Mode::VisualBlock {
3812 block_replace(ed, ch);
3813 return true;
3814 }
3815 let count = take_count(&mut ed.vim);
3816 replace_char(ed, ch, count.max(1));
3817 if !ed.vim.replaying {
3818 ed.vim.last_change = Some(LastChange::ReplaceChar {
3819 ch,
3820 count: count.max(1),
3821 });
3822 }
3823 }
3824 true
3825}
3826
3827fn handle_find_target<H: crate::types::Host>(
3828 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3829 input: Input,
3830 forward: bool,
3831 till: bool,
3832) -> bool {
3833 let Key::Char(ch) = input.key else {
3834 return true;
3835 };
3836 let count = take_count(&mut ed.vim);
3837 apply_find_char(ed, ch, forward, till, count.max(1));
3838 true
3839}
3840
3841pub(crate) fn apply_find_char<H: crate::types::Host>(
3847 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3848 ch: char,
3849 forward: bool,
3850 till: bool,
3851 count: usize,
3852) {
3853 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3854 ed.vim.last_find = Some((ch, forward, till));
3855}
3856
3857pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3863 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3864 op: Operator,
3865 ch: char,
3866 forward: bool,
3867 till: bool,
3868 total_count: usize,
3869) {
3870 let motion = Motion::Find { ch, forward, till };
3871 apply_op_with_motion(ed, op, &motion, total_count);
3872 ed.vim.last_find = Some((ch, forward, till));
3873 if !ed.vim.replaying && op_is_change(op) {
3874 ed.vim.last_change = Some(LastChange::OpMotion {
3875 op,
3876 motion,
3877 count: total_count,
3878 inserted: None,
3879 });
3880 }
3881}
3882
3883fn handle_op_find_target<H: crate::types::Host>(
3884 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3885 input: Input,
3886 op: Operator,
3887 count1: usize,
3888 forward: bool,
3889 till: bool,
3890) -> bool {
3891 let Key::Char(ch) = input.key else {
3892 return true;
3893 };
3894 let count2 = take_count(&mut ed.vim);
3895 let total = count1.max(1) * count2.max(1);
3896 apply_op_find_motion(ed, op, ch, forward, till, total);
3897 true
3898}
3899
3900pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3910 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3911 op: Operator,
3912 ch: char,
3913 inner: bool,
3914 _total_count: usize,
3915) -> bool {
3916 let obj = match ch {
3919 'w' => TextObject::Word { big: false },
3920 'W' => TextObject::Word { big: true },
3921 '"' | '\'' | '`' => TextObject::Quote(ch),
3922 '(' | ')' | 'b' => TextObject::Bracket('('),
3923 '[' | ']' => TextObject::Bracket('['),
3924 '{' | '}' | 'B' => TextObject::Bracket('{'),
3925 '<' | '>' => TextObject::Bracket('<'),
3926 'p' => TextObject::Paragraph,
3927 't' => TextObject::XmlTag,
3928 's' => TextObject::Sentence,
3929 _ => return false,
3930 };
3931 apply_op_with_text_object(ed, op, obj, inner);
3932 if !ed.vim.replaying && op_is_change(op) {
3933 ed.vim.last_change = Some(LastChange::OpTextObj {
3934 op,
3935 obj,
3936 inner,
3937 inserted: None,
3938 });
3939 }
3940 true
3941}
3942
3943fn handle_text_object<H: crate::types::Host>(
3944 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3945 input: Input,
3946 op: Operator,
3947 _count1: usize,
3948 inner: bool,
3949) -> bool {
3950 let Key::Char(ch) = input.key else {
3951 return true;
3952 };
3953 apply_op_text_obj_inner(ed, op, ch, inner, 1);
3956 true
3957}
3958
3959fn handle_visual_text_obj<H: crate::types::Host>(
3960 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3961 input: Input,
3962 inner: bool,
3963) -> bool {
3964 let Key::Char(ch) = input.key else {
3965 return true;
3966 };
3967 let obj = match ch {
3968 'w' => TextObject::Word { big: false },
3969 'W' => TextObject::Word { big: true },
3970 '"' | '\'' | '`' => TextObject::Quote(ch),
3971 '(' | ')' | 'b' => TextObject::Bracket('('),
3972 '[' | ']' => TextObject::Bracket('['),
3973 '{' | '}' | 'B' => TextObject::Bracket('{'),
3974 '<' | '>' => TextObject::Bracket('<'),
3975 'p' => TextObject::Paragraph,
3976 't' => TextObject::XmlTag,
3977 's' => TextObject::Sentence,
3978 _ => return true,
3979 };
3980 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3981 return true;
3982 };
3983 match kind {
3987 MotionKind::Linewise => {
3988 ed.vim.visual_line_anchor = start.0;
3989 ed.vim.mode = Mode::VisualLine;
3990 ed.jump_cursor(end.0, 0);
3991 }
3992 _ => {
3993 ed.vim.mode = Mode::Visual;
3994 ed.vim.visual_anchor = (start.0, start.1);
3995 let (er, ec) = retreat_one(ed, end);
3996 ed.jump_cursor(er, ec);
3997 }
3998 }
3999 true
4000}
4001
4002fn retreat_one<H: crate::types::Host>(
4004 ed: &Editor<hjkl_buffer::Buffer, H>,
4005 pos: (usize, usize),
4006) -> (usize, usize) {
4007 let (r, c) = pos;
4008 if c > 0 {
4009 (r, c - 1)
4010 } else if r > 0 {
4011 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
4012 (r - 1, prev_len)
4013 } else {
4014 (0, 0)
4015 }
4016}
4017
4018fn op_is_change(op: Operator) -> bool {
4019 matches!(op, Operator::Delete | Operator::Change)
4020}
4021
4022fn handle_normal_only<H: crate::types::Host>(
4025 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4026 input: &Input,
4027 count: usize,
4028) -> bool {
4029 if input.ctrl {
4030 return false;
4031 }
4032 match input.key {
4033 Key::Char('i') => {
4034 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
4035 true
4036 }
4037 Key::Char('I') => {
4038 move_first_non_whitespace(ed);
4039 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
4040 true
4041 }
4042 Key::Char('a') => {
4043 crate::motions::move_right_to_end(&mut ed.buffer, 1);
4044 ed.push_buffer_cursor_to_textarea();
4045 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
4046 true
4047 }
4048 Key::Char('A') => {
4049 crate::motions::move_line_end(&mut ed.buffer);
4050 crate::motions::move_right_to_end(&mut ed.buffer, 1);
4051 ed.push_buffer_cursor_to_textarea();
4052 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
4053 true
4054 }
4055 Key::Char('R') => {
4056 begin_insert(ed, count.max(1), InsertReason::Replace);
4059 true
4060 }
4061 Key::Char('o') => {
4062 use hjkl_buffer::{Edit, Position};
4063 ed.push_undo();
4064 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
4067 ed.sync_buffer_content_from_textarea();
4068 let row = buf_cursor_pos(&ed.buffer).row;
4069 let line_chars = buf_line_chars(&ed.buffer, row);
4070 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
4073 let indent = compute_enter_indent(&ed.settings, prev_line);
4074 ed.mutate_edit(Edit::InsertStr {
4075 at: Position::new(row, line_chars),
4076 text: format!("\n{indent}"),
4077 });
4078 ed.push_buffer_cursor_to_textarea();
4079 true
4080 }
4081 Key::Char('O') => {
4082 use hjkl_buffer::{Edit, Position};
4083 ed.push_undo();
4084 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
4085 ed.sync_buffer_content_from_textarea();
4086 let row = buf_cursor_pos(&ed.buffer).row;
4087 let indent = if row > 0 {
4091 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
4092 compute_enter_indent(&ed.settings, above)
4093 } else {
4094 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
4095 cur.chars()
4096 .take_while(|c| *c == ' ' || *c == '\t')
4097 .collect::<String>()
4098 };
4099 ed.mutate_edit(Edit::InsertStr {
4100 at: Position::new(row, 0),
4101 text: format!("{indent}\n"),
4102 });
4103 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
4108 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
4109 let new_row = buf_cursor_pos(&ed.buffer).row;
4110 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
4111 ed.push_buffer_cursor_to_textarea();
4112 true
4113 }
4114 Key::Char('x') => {
4115 do_char_delete(ed, true, count.max(1));
4116 if !ed.vim.replaying {
4117 ed.vim.last_change = Some(LastChange::CharDel {
4118 forward: true,
4119 count: count.max(1),
4120 });
4121 }
4122 true
4123 }
4124 Key::Char('X') => {
4125 do_char_delete(ed, false, count.max(1));
4126 if !ed.vim.replaying {
4127 ed.vim.last_change = Some(LastChange::CharDel {
4128 forward: false,
4129 count: count.max(1),
4130 });
4131 }
4132 true
4133 }
4134 Key::Char('~') => {
4135 for _ in 0..count.max(1) {
4136 ed.push_undo();
4137 toggle_case_at_cursor(ed);
4138 }
4139 if !ed.vim.replaying {
4140 ed.vim.last_change = Some(LastChange::ToggleCase {
4141 count: count.max(1),
4142 });
4143 }
4144 true
4145 }
4146 Key::Char('J') => {
4147 for _ in 0..count.max(1) {
4148 ed.push_undo();
4149 join_line(ed);
4150 }
4151 if !ed.vim.replaying {
4152 ed.vim.last_change = Some(LastChange::JoinLine {
4153 count: count.max(1),
4154 });
4155 }
4156 true
4157 }
4158 Key::Char('D') => {
4159 ed.push_undo();
4160 delete_to_eol(ed);
4161 crate::motions::move_left(&mut ed.buffer, 1);
4163 ed.push_buffer_cursor_to_textarea();
4164 if !ed.vim.replaying {
4165 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
4166 }
4167 true
4168 }
4169 Key::Char('Y') => {
4170 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
4172 true
4173 }
4174 Key::Char('C') => {
4175 ed.push_undo();
4176 delete_to_eol(ed);
4177 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
4178 true
4179 }
4180 Key::Char('s') => {
4181 use hjkl_buffer::{Edit, MotionKind, Position};
4182 ed.push_undo();
4183 ed.sync_buffer_content_from_textarea();
4184 for _ in 0..count.max(1) {
4185 let cursor = buf_cursor_pos(&ed.buffer);
4186 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
4187 if cursor.col >= line_chars {
4188 break;
4189 }
4190 ed.mutate_edit(Edit::DeleteRange {
4191 start: cursor,
4192 end: Position::new(cursor.row, cursor.col + 1),
4193 kind: MotionKind::Char,
4194 });
4195 }
4196 ed.push_buffer_cursor_to_textarea();
4197 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4198 if !ed.vim.replaying {
4200 ed.vim.last_change = Some(LastChange::OpMotion {
4201 op: Operator::Change,
4202 motion: Motion::Right,
4203 count: count.max(1),
4204 inserted: None,
4205 });
4206 }
4207 true
4208 }
4209 Key::Char('p') => {
4210 do_paste(ed, false, count.max(1));
4211 if !ed.vim.replaying {
4212 ed.vim.last_change = Some(LastChange::Paste {
4213 before: false,
4214 count: count.max(1),
4215 });
4216 }
4217 true
4218 }
4219 Key::Char('P') => {
4220 do_paste(ed, true, count.max(1));
4221 if !ed.vim.replaying {
4222 ed.vim.last_change = Some(LastChange::Paste {
4223 before: true,
4224 count: count.max(1),
4225 });
4226 }
4227 true
4228 }
4229 Key::Char('u') => {
4230 do_undo(ed);
4231 true
4232 }
4233 Key::Char('r') => {
4234 ed.vim.count = count;
4235 ed.vim.pending = Pending::Replace;
4236 true
4237 }
4238 Key::Char('/') => {
4239 enter_search(ed, true);
4240 true
4241 }
4242 Key::Char('?') => {
4243 enter_search(ed, false);
4244 true
4245 }
4246 Key::Char('.') => {
4247 replay_last_change(ed, count);
4248 true
4249 }
4250 _ => false,
4251 }
4252}
4253
4254fn begin_insert_noundo<H: crate::types::Host>(
4256 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4257 count: usize,
4258 reason: InsertReason,
4259) {
4260 let reason = if ed.vim.replaying {
4261 InsertReason::ReplayOnly
4262 } else {
4263 reason
4264 };
4265 let (row, _) = ed.cursor();
4266 ed.vim.insert_session = Some(InsertSession {
4267 count,
4268 row_min: row,
4269 row_max: row,
4270 before_lines: buf_lines_to_vec(&ed.buffer),
4271 reason,
4272 });
4273 ed.vim.mode = Mode::Insert;
4274}
4275
4276fn apply_op_with_motion<H: crate::types::Host>(
4279 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4280 op: Operator,
4281 motion: &Motion,
4282 count: usize,
4283) {
4284 let start = ed.cursor();
4285 apply_motion_cursor_ctx(ed, motion, count, true);
4290 let end = ed.cursor();
4291 let kind = motion_kind(motion);
4292 ed.jump_cursor(start.0, start.1);
4294 run_operator_over_range(ed, op, start, end, kind);
4295}
4296
4297fn apply_op_with_text_object<H: crate::types::Host>(
4298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4299 op: Operator,
4300 obj: TextObject,
4301 inner: bool,
4302) {
4303 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
4304 return;
4305 };
4306 ed.jump_cursor(start.0, start.1);
4307 run_operator_over_range(ed, op, start, end, kind);
4308}
4309
4310fn motion_kind(motion: &Motion) -> MotionKind {
4311 match motion {
4312 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
4313 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
4314 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
4315 MotionKind::Linewise
4316 }
4317 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
4318 MotionKind::Inclusive
4319 }
4320 Motion::Find { .. } => MotionKind::Inclusive,
4321 Motion::MatchBracket => MotionKind::Inclusive,
4322 Motion::LineEnd => MotionKind::Inclusive,
4324 _ => MotionKind::Exclusive,
4325 }
4326}
4327
4328fn run_operator_over_range<H: crate::types::Host>(
4329 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4330 op: Operator,
4331 start: (usize, usize),
4332 end: (usize, usize),
4333 kind: MotionKind,
4334) {
4335 let (top, bot) = order(start, end);
4336 if top == bot && !matches!(kind, MotionKind::Linewise) {
4340 return;
4341 }
4342
4343 match op {
4344 Operator::Yank => {
4345 let text = read_vim_range(ed, top, bot, kind);
4346 if !text.is_empty() {
4347 ed.record_yank_to_host(text.clone());
4348 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
4349 }
4350 let rbr = match kind {
4354 MotionKind::Linewise => {
4355 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
4356 (bot.0, last_col)
4357 }
4358 MotionKind::Inclusive => (bot.0, bot.1),
4359 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
4360 };
4361 ed.set_mark('[', top);
4362 ed.set_mark(']', rbr);
4363 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4364 ed.push_buffer_cursor_to_textarea();
4365 }
4366 Operator::Delete => {
4367 ed.push_undo();
4368 cut_vim_range(ed, top, bot, kind);
4369 if !matches!(kind, MotionKind::Linewise) {
4374 clamp_cursor_to_normal_mode(ed);
4375 }
4376 ed.vim.mode = Mode::Normal;
4377 let pos = ed.cursor();
4381 ed.set_mark('[', pos);
4382 ed.set_mark(']', pos);
4383 }
4384 Operator::Change => {
4385 ed.vim.change_mark_start = Some(top);
4390 ed.push_undo();
4391 cut_vim_range(ed, top, bot, kind);
4392 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4393 }
4394 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4395 apply_case_op_to_selection(ed, op, top, bot, kind);
4396 }
4397 Operator::Indent | Operator::Outdent => {
4398 ed.push_undo();
4401 if op == Operator::Indent {
4402 indent_rows(ed, top.0, bot.0, 1);
4403 } else {
4404 outdent_rows(ed, top.0, bot.0, 1);
4405 }
4406 ed.vim.mode = Mode::Normal;
4407 }
4408 Operator::Fold => {
4409 if bot.0 >= top.0 {
4413 ed.apply_fold_op(crate::types::FoldOp::Add {
4414 start_row: top.0,
4415 end_row: bot.0,
4416 closed: true,
4417 });
4418 }
4419 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4420 ed.push_buffer_cursor_to_textarea();
4421 ed.vim.mode = Mode::Normal;
4422 }
4423 Operator::Reflow => {
4424 ed.push_undo();
4425 reflow_rows(ed, top.0, bot.0);
4426 ed.vim.mode = Mode::Normal;
4427 }
4428 }
4429}
4430
4431pub(crate) fn delete_range_bridge<H: crate::types::Host>(
4448 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4449 start: (usize, usize),
4450 end: (usize, usize),
4451 kind: MotionKind,
4452 register: char,
4453) {
4454 ed.vim.pending_register = Some(register);
4455 run_operator_over_range(ed, Operator::Delete, start, end, kind);
4456}
4457
4458pub(crate) fn yank_range_bridge<H: crate::types::Host>(
4461 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4462 start: (usize, usize),
4463 end: (usize, usize),
4464 kind: MotionKind,
4465 register: char,
4466) {
4467 ed.vim.pending_register = Some(register);
4468 run_operator_over_range(ed, Operator::Yank, start, end, kind);
4469}
4470
4471pub(crate) fn change_range_bridge<H: crate::types::Host>(
4476 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4477 start: (usize, usize),
4478 end: (usize, usize),
4479 kind: MotionKind,
4480 register: char,
4481) {
4482 ed.vim.pending_register = Some(register);
4483 run_operator_over_range(ed, Operator::Change, start, end, kind);
4484}
4485
4486pub(crate) fn indent_range_bridge<H: crate::types::Host>(
4491 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4492 start: (usize, usize),
4493 end: (usize, usize),
4494 count: i32,
4495 shiftwidth: u32,
4496) {
4497 if count == 0 {
4498 return;
4499 }
4500 let (top_row, bot_row) = if start.0 <= end.0 {
4501 (start.0, end.0)
4502 } else {
4503 (end.0, start.0)
4504 };
4505 let original_sw = ed.settings().shiftwidth;
4507 if shiftwidth > 0 {
4508 ed.settings_mut().shiftwidth = shiftwidth as usize;
4509 }
4510 ed.push_undo();
4511 let abs_count = count.unsigned_abs() as usize;
4512 if count > 0 {
4513 indent_rows(ed, top_row, bot_row, abs_count);
4514 } else {
4515 outdent_rows(ed, top_row, bot_row, abs_count);
4516 }
4517 if shiftwidth > 0 {
4518 ed.settings_mut().shiftwidth = original_sw;
4519 }
4520 ed.vim.mode = Mode::Normal;
4521}
4522
4523pub(crate) fn case_range_bridge<H: crate::types::Host>(
4527 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4528 start: (usize, usize),
4529 end: (usize, usize),
4530 kind: MotionKind,
4531 op: Operator,
4532) {
4533 match op {
4534 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {}
4535 _ => return,
4536 }
4537 let (top, bot) = order(start, end);
4538 apply_case_op_to_selection(ed, op, top, bot, kind);
4539}
4540
4541pub(crate) fn delete_block_bridge<H: crate::types::Host>(
4562 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4563 top_row: usize,
4564 bot_row: usize,
4565 left_col: usize,
4566 right_col: usize,
4567 register: char,
4568) {
4569 ed.vim.pending_register = Some(register);
4570 let saved_anchor = ed.vim.block_anchor;
4571 let saved_vcol = ed.vim.block_vcol;
4572 ed.vim.block_anchor = (top_row, left_col);
4573 ed.vim.block_vcol = right_col;
4574 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4576 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4578 apply_block_operator(ed, Operator::Delete);
4579 ed.vim.block_anchor = saved_anchor;
4583 ed.vim.block_vcol = saved_vcol;
4584}
4585
4586pub(crate) fn yank_block_bridge<H: crate::types::Host>(
4588 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4589 top_row: usize,
4590 bot_row: usize,
4591 left_col: usize,
4592 right_col: usize,
4593 register: char,
4594) {
4595 ed.vim.pending_register = Some(register);
4596 let saved_anchor = ed.vim.block_anchor;
4597 let saved_vcol = ed.vim.block_vcol;
4598 ed.vim.block_anchor = (top_row, left_col);
4599 ed.vim.block_vcol = right_col;
4600 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4601 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4602 apply_block_operator(ed, Operator::Yank);
4603 ed.vim.block_anchor = saved_anchor;
4604 ed.vim.block_vcol = saved_vcol;
4605}
4606
4607pub(crate) fn change_block_bridge<H: crate::types::Host>(
4610 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4611 top_row: usize,
4612 bot_row: usize,
4613 left_col: usize,
4614 right_col: usize,
4615 register: char,
4616) {
4617 ed.vim.pending_register = Some(register);
4618 let saved_anchor = ed.vim.block_anchor;
4619 let saved_vcol = ed.vim.block_vcol;
4620 ed.vim.block_anchor = (top_row, left_col);
4621 ed.vim.block_vcol = right_col;
4622 let clamped = right_col.min(buf_line_chars(&ed.buffer, bot_row).saturating_sub(1));
4623 buf_set_cursor_rc(&mut ed.buffer, bot_row, clamped);
4624 apply_block_operator(ed, Operator::Change);
4625 ed.vim.block_anchor = saved_anchor;
4626 ed.vim.block_vcol = saved_vcol;
4627}
4628
4629pub(crate) fn indent_block_bridge<H: crate::types::Host>(
4633 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4634 top_row: usize,
4635 bot_row: usize,
4636 count: i32,
4637) {
4638 if count == 0 {
4639 return;
4640 }
4641 ed.push_undo();
4642 let abs = count.unsigned_abs() as usize;
4643 if count > 0 {
4644 indent_rows(ed, top_row, bot_row, abs);
4645 } else {
4646 outdent_rows(ed, top_row, bot_row, abs);
4647 }
4648 ed.vim.mode = Mode::Normal;
4649}
4650
4651pub(crate) fn text_object_inner_word_bridge<H: crate::types::Host>(
4662 ed: &Editor<hjkl_buffer::Buffer, H>,
4663) -> Option<((usize, usize), (usize, usize))> {
4664 word_text_object(ed, true, false)
4665}
4666
4667pub(crate) fn text_object_around_word_bridge<H: crate::types::Host>(
4670 ed: &Editor<hjkl_buffer::Buffer, H>,
4671) -> Option<((usize, usize), (usize, usize))> {
4672 word_text_object(ed, false, false)
4673}
4674
4675pub(crate) fn text_object_inner_big_word_bridge<H: crate::types::Host>(
4678 ed: &Editor<hjkl_buffer::Buffer, H>,
4679) -> Option<((usize, usize), (usize, usize))> {
4680 word_text_object(ed, true, true)
4681}
4682
4683pub(crate) fn text_object_around_big_word_bridge<H: crate::types::Host>(
4686 ed: &Editor<hjkl_buffer::Buffer, H>,
4687) -> Option<((usize, usize), (usize, usize))> {
4688 word_text_object(ed, false, true)
4689}
4690
4691pub(crate) fn text_object_inner_quote_bridge<H: crate::types::Host>(
4707 ed: &Editor<hjkl_buffer::Buffer, H>,
4708 quote: char,
4709) -> Option<((usize, usize), (usize, usize))> {
4710 quote_text_object(ed, quote, true)
4711}
4712
4713pub(crate) fn text_object_around_quote_bridge<H: crate::types::Host>(
4716 ed: &Editor<hjkl_buffer::Buffer, H>,
4717 quote: char,
4718) -> Option<((usize, usize), (usize, usize))> {
4719 quote_text_object(ed, quote, false)
4720}
4721
4722pub(crate) fn text_object_inner_bracket_bridge<H: crate::types::Host>(
4730 ed: &Editor<hjkl_buffer::Buffer, H>,
4731 open: char,
4732) -> Option<((usize, usize), (usize, usize))> {
4733 bracket_text_object(ed, open, true).map(|(s, e, _kind)| (s, e))
4734}
4735
4736pub(crate) fn text_object_around_bracket_bridge<H: crate::types::Host>(
4740 ed: &Editor<hjkl_buffer::Buffer, H>,
4741 open: char,
4742) -> Option<((usize, usize), (usize, usize))> {
4743 bracket_text_object(ed, open, false).map(|(s, e, _kind)| (s, e))
4744}
4745
4746pub(crate) fn text_object_inner_sentence_bridge<H: crate::types::Host>(
4751 ed: &Editor<hjkl_buffer::Buffer, H>,
4752) -> Option<((usize, usize), (usize, usize))> {
4753 sentence_text_object(ed, true)
4754}
4755
4756pub(crate) fn text_object_around_sentence_bridge<H: crate::types::Host>(
4759 ed: &Editor<hjkl_buffer::Buffer, H>,
4760) -> Option<((usize, usize), (usize, usize))> {
4761 sentence_text_object(ed, false)
4762}
4763
4764pub(crate) fn text_object_inner_paragraph_bridge<H: crate::types::Host>(
4769 ed: &Editor<hjkl_buffer::Buffer, H>,
4770) -> Option<((usize, usize), (usize, usize))> {
4771 paragraph_text_object(ed, true)
4772}
4773
4774pub(crate) fn text_object_around_paragraph_bridge<H: crate::types::Host>(
4777 ed: &Editor<hjkl_buffer::Buffer, H>,
4778) -> Option<((usize, usize), (usize, usize))> {
4779 paragraph_text_object(ed, false)
4780}
4781
4782pub(crate) fn text_object_inner_tag_bridge<H: crate::types::Host>(
4788 ed: &Editor<hjkl_buffer::Buffer, H>,
4789) -> Option<((usize, usize), (usize, usize))> {
4790 tag_text_object(ed, true)
4791}
4792
4793pub(crate) fn text_object_around_tag_bridge<H: crate::types::Host>(
4796 ed: &Editor<hjkl_buffer::Buffer, H>,
4797) -> Option<((usize, usize), (usize, usize))> {
4798 tag_text_object(ed, false)
4799}
4800
4801fn reflow_rows<H: crate::types::Host>(
4806 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4807 top: usize,
4808 bot: usize,
4809) {
4810 let width = ed.settings().textwidth.max(1);
4811 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4812 let bot = bot.min(lines.len().saturating_sub(1));
4813 if top > bot {
4814 return;
4815 }
4816 let original = lines[top..=bot].to_vec();
4817 let mut wrapped: Vec<String> = Vec::new();
4818 let mut paragraph: Vec<String> = Vec::new();
4819 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4820 if para.is_empty() {
4821 return;
4822 }
4823 let words = para.join(" ");
4824 let mut current = String::new();
4825 for word in words.split_whitespace() {
4826 let extra = if current.is_empty() {
4827 word.chars().count()
4828 } else {
4829 current.chars().count() + 1 + word.chars().count()
4830 };
4831 if extra > width && !current.is_empty() {
4832 out.push(std::mem::take(&mut current));
4833 current.push_str(word);
4834 } else if current.is_empty() {
4835 current.push_str(word);
4836 } else {
4837 current.push(' ');
4838 current.push_str(word);
4839 }
4840 }
4841 if !current.is_empty() {
4842 out.push(current);
4843 }
4844 para.clear();
4845 };
4846 for line in &original {
4847 if line.trim().is_empty() {
4848 flush(&mut paragraph, &mut wrapped, width);
4849 wrapped.push(String::new());
4850 } else {
4851 paragraph.push(line.clone());
4852 }
4853 }
4854 flush(&mut paragraph, &mut wrapped, width);
4855
4856 let after: Vec<String> = lines.split_off(bot + 1);
4858 lines.truncate(top);
4859 lines.extend(wrapped);
4860 lines.extend(after);
4861 ed.restore(lines, (top, 0));
4862 ed.mark_content_dirty();
4863}
4864
4865fn apply_case_op_to_selection<H: crate::types::Host>(
4871 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4872 op: Operator,
4873 top: (usize, usize),
4874 bot: (usize, usize),
4875 kind: MotionKind,
4876) {
4877 use hjkl_buffer::Edit;
4878 ed.push_undo();
4879 let saved_yank = ed.yank().to_string();
4880 let saved_yank_linewise = ed.vim.yank_linewise;
4881 let selection = cut_vim_range(ed, top, bot, kind);
4882 let transformed = match op {
4883 Operator::Uppercase => selection.to_uppercase(),
4884 Operator::Lowercase => selection.to_lowercase(),
4885 Operator::ToggleCase => toggle_case_str(&selection),
4886 _ => unreachable!(),
4887 };
4888 if !transformed.is_empty() {
4889 let cursor = buf_cursor_pos(&ed.buffer);
4890 ed.mutate_edit(Edit::InsertStr {
4891 at: cursor,
4892 text: transformed,
4893 });
4894 }
4895 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4896 ed.push_buffer_cursor_to_textarea();
4897 ed.set_yank(saved_yank);
4898 ed.vim.yank_linewise = saved_yank_linewise;
4899 ed.vim.mode = Mode::Normal;
4900}
4901
4902fn indent_rows<H: crate::types::Host>(
4907 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4908 top: usize,
4909 bot: usize,
4910 count: usize,
4911) {
4912 ed.sync_buffer_content_from_textarea();
4913 let width = ed.settings().shiftwidth * count.max(1);
4914 let pad: String = " ".repeat(width);
4915 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4916 let bot = bot.min(lines.len().saturating_sub(1));
4917 for line in lines.iter_mut().take(bot + 1).skip(top) {
4918 if !line.is_empty() {
4919 line.insert_str(0, &pad);
4920 }
4921 }
4922 ed.restore(lines, (top, 0));
4925 move_first_non_whitespace(ed);
4926}
4927
4928fn outdent_rows<H: crate::types::Host>(
4932 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4933 top: usize,
4934 bot: usize,
4935 count: usize,
4936) {
4937 ed.sync_buffer_content_from_textarea();
4938 let width = ed.settings().shiftwidth * count.max(1);
4939 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4940 let bot = bot.min(lines.len().saturating_sub(1));
4941 for line in lines.iter_mut().take(bot + 1).skip(top) {
4942 let strip: usize = line
4943 .chars()
4944 .take(width)
4945 .take_while(|c| *c == ' ' || *c == '\t')
4946 .count();
4947 if strip > 0 {
4948 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4949 line.drain(..byte_len);
4950 }
4951 }
4952 ed.restore(lines, (top, 0));
4953 move_first_non_whitespace(ed);
4954}
4955
4956fn toggle_case_str(s: &str) -> String {
4957 s.chars()
4958 .map(|c| {
4959 if c.is_lowercase() {
4960 c.to_uppercase().next().unwrap_or(c)
4961 } else if c.is_uppercase() {
4962 c.to_lowercase().next().unwrap_or(c)
4963 } else {
4964 c
4965 }
4966 })
4967 .collect()
4968}
4969
4970fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4971 if a <= b { (a, b) } else { (b, a) }
4972}
4973
4974fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4979 let (row, col) = ed.cursor();
4980 let line_chars = buf_line_chars(&ed.buffer, row);
4981 let max_col = line_chars.saturating_sub(1);
4982 if col > max_col {
4983 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4984 ed.push_buffer_cursor_to_textarea();
4985 }
4986}
4987
4988fn execute_line_op<H: crate::types::Host>(
4991 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4992 op: Operator,
4993 count: usize,
4994) {
4995 let (row, col) = ed.cursor();
4996 let total = buf_row_count(&ed.buffer);
4997 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4998
4999 match op {
5000 Operator::Yank => {
5001 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
5003 if !text.is_empty() {
5004 ed.record_yank_to_host(text.clone());
5005 ed.record_yank(text, true);
5006 }
5007 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
5010 ed.set_mark('[', (row, 0));
5011 ed.set_mark(']', (end_row, last_col));
5012 buf_set_cursor_rc(&mut ed.buffer, row, col);
5013 ed.push_buffer_cursor_to_textarea();
5014 ed.vim.mode = Mode::Normal;
5015 }
5016 Operator::Delete => {
5017 ed.push_undo();
5018 let deleted_through_last = end_row + 1 >= total;
5019 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
5020 let total_after = buf_row_count(&ed.buffer);
5024 let raw_target = if deleted_through_last {
5025 row.saturating_sub(1).min(total_after.saturating_sub(1))
5026 } else {
5027 row.min(total_after.saturating_sub(1))
5028 };
5029 let target_row = if raw_target > 0
5035 && raw_target + 1 == total_after
5036 && buf_line(&ed.buffer, raw_target)
5037 .map(str::is_empty)
5038 .unwrap_or(false)
5039 {
5040 raw_target - 1
5041 } else {
5042 raw_target
5043 };
5044 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5045 ed.push_buffer_cursor_to_textarea();
5046 move_first_non_whitespace(ed);
5047 ed.sticky_col = Some(ed.cursor().1);
5048 ed.vim.mode = Mode::Normal;
5049 let pos = ed.cursor();
5052 ed.set_mark('[', pos);
5053 ed.set_mark(']', pos);
5054 }
5055 Operator::Change => {
5056 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5060 ed.vim.change_mark_start = Some((row, 0));
5062 ed.push_undo();
5063 ed.sync_buffer_content_from_textarea();
5064 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
5066 if end_row > row {
5067 ed.mutate_edit(Edit::DeleteRange {
5068 start: Position::new(row + 1, 0),
5069 end: Position::new(end_row, 0),
5070 kind: BufKind::Line,
5071 });
5072 }
5073 let line_chars = buf_line_chars(&ed.buffer, row);
5074 if line_chars > 0 {
5075 ed.mutate_edit(Edit::DeleteRange {
5076 start: Position::new(row, 0),
5077 end: Position::new(row, line_chars),
5078 kind: BufKind::Char,
5079 });
5080 }
5081 if !payload.is_empty() {
5082 ed.record_yank_to_host(payload.clone());
5083 ed.record_delete(payload, true);
5084 }
5085 buf_set_cursor_rc(&mut ed.buffer, row, 0);
5086 ed.push_buffer_cursor_to_textarea();
5087 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5088 }
5089 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5090 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
5094 move_first_non_whitespace(ed);
5097 }
5098 Operator::Indent | Operator::Outdent => {
5099 ed.push_undo();
5101 if op == Operator::Indent {
5102 indent_rows(ed, row, end_row, 1);
5103 } else {
5104 outdent_rows(ed, row, end_row, 1);
5105 }
5106 ed.sticky_col = Some(ed.cursor().1);
5107 ed.vim.mode = Mode::Normal;
5108 }
5109 Operator::Fold => unreachable!("Fold has no line-op double"),
5111 Operator::Reflow => {
5112 ed.push_undo();
5114 reflow_rows(ed, row, end_row);
5115 move_first_non_whitespace(ed);
5116 ed.sticky_col = Some(ed.cursor().1);
5117 ed.vim.mode = Mode::Normal;
5118 }
5119 }
5120}
5121
5122fn apply_visual_operator<H: crate::types::Host>(
5125 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5126 op: Operator,
5127) {
5128 match ed.vim.mode {
5129 Mode::VisualLine => {
5130 let cursor_row = buf_cursor_pos(&ed.buffer).row;
5131 let top = cursor_row.min(ed.vim.visual_line_anchor);
5132 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5133 ed.vim.yank_linewise = true;
5134 match op {
5135 Operator::Yank => {
5136 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
5137 if !text.is_empty() {
5138 ed.record_yank_to_host(text.clone());
5139 ed.record_yank(text, true);
5140 }
5141 buf_set_cursor_rc(&mut ed.buffer, top, 0);
5142 ed.push_buffer_cursor_to_textarea();
5143 ed.vim.mode = Mode::Normal;
5144 }
5145 Operator::Delete => {
5146 ed.push_undo();
5147 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
5148 ed.vim.mode = Mode::Normal;
5149 }
5150 Operator::Change => {
5151 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5154 ed.push_undo();
5155 ed.sync_buffer_content_from_textarea();
5156 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
5157 if bot > top {
5158 ed.mutate_edit(Edit::DeleteRange {
5159 start: Position::new(top + 1, 0),
5160 end: Position::new(bot, 0),
5161 kind: BufKind::Line,
5162 });
5163 }
5164 let line_chars = buf_line_chars(&ed.buffer, top);
5165 if line_chars > 0 {
5166 ed.mutate_edit(Edit::DeleteRange {
5167 start: Position::new(top, 0),
5168 end: Position::new(top, line_chars),
5169 kind: BufKind::Char,
5170 });
5171 }
5172 if !payload.is_empty() {
5173 ed.record_yank_to_host(payload.clone());
5174 ed.record_delete(payload, true);
5175 }
5176 buf_set_cursor_rc(&mut ed.buffer, top, 0);
5177 ed.push_buffer_cursor_to_textarea();
5178 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5179 }
5180 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5181 let bot = buf_cursor_pos(&ed.buffer)
5182 .row
5183 .max(ed.vim.visual_line_anchor);
5184 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
5185 move_first_non_whitespace(ed);
5186 }
5187 Operator::Indent | Operator::Outdent => {
5188 ed.push_undo();
5189 let (cursor_row, _) = ed.cursor();
5190 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5191 if op == Operator::Indent {
5192 indent_rows(ed, top, bot, 1);
5193 } else {
5194 outdent_rows(ed, top, bot, 1);
5195 }
5196 ed.vim.mode = Mode::Normal;
5197 }
5198 Operator::Reflow => {
5199 ed.push_undo();
5200 let (cursor_row, _) = ed.cursor();
5201 let bot = cursor_row.max(ed.vim.visual_line_anchor);
5202 reflow_rows(ed, top, bot);
5203 ed.vim.mode = Mode::Normal;
5204 }
5205 Operator::Fold => unreachable!("Visual zf takes its own path"),
5208 }
5209 }
5210 Mode::Visual => {
5211 ed.vim.yank_linewise = false;
5212 let anchor = ed.vim.visual_anchor;
5213 let cursor = ed.cursor();
5214 let (top, bot) = order(anchor, cursor);
5215 match op {
5216 Operator::Yank => {
5217 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
5218 if !text.is_empty() {
5219 ed.record_yank_to_host(text.clone());
5220 ed.record_yank(text, false);
5221 }
5222 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
5223 ed.push_buffer_cursor_to_textarea();
5224 ed.vim.mode = Mode::Normal;
5225 }
5226 Operator::Delete => {
5227 ed.push_undo();
5228 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
5229 ed.vim.mode = Mode::Normal;
5230 }
5231 Operator::Change => {
5232 ed.push_undo();
5233 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
5234 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
5235 }
5236 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5237 let anchor = ed.vim.visual_anchor;
5239 let cursor = ed.cursor();
5240 let (top, bot) = order(anchor, cursor);
5241 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
5242 }
5243 Operator::Indent | Operator::Outdent => {
5244 ed.push_undo();
5245 let anchor = ed.vim.visual_anchor;
5246 let cursor = ed.cursor();
5247 let (top, bot) = order(anchor, cursor);
5248 if op == Operator::Indent {
5249 indent_rows(ed, top.0, bot.0, 1);
5250 } else {
5251 outdent_rows(ed, top.0, bot.0, 1);
5252 }
5253 ed.vim.mode = Mode::Normal;
5254 }
5255 Operator::Reflow => {
5256 ed.push_undo();
5257 let anchor = ed.vim.visual_anchor;
5258 let cursor = ed.cursor();
5259 let (top, bot) = order(anchor, cursor);
5260 reflow_rows(ed, top.0, bot.0);
5261 ed.vim.mode = Mode::Normal;
5262 }
5263 Operator::Fold => unreachable!("Visual zf takes its own path"),
5264 }
5265 }
5266 Mode::VisualBlock => apply_block_operator(ed, op),
5267 _ => {}
5268 }
5269}
5270
5271fn block_bounds<H: crate::types::Host>(
5276 ed: &Editor<hjkl_buffer::Buffer, H>,
5277) -> (usize, usize, usize, usize) {
5278 let (ar, ac) = ed.vim.block_anchor;
5279 let (cr, _) = ed.cursor();
5280 let cc = ed.vim.block_vcol;
5281 let top = ar.min(cr);
5282 let bot = ar.max(cr);
5283 let left = ac.min(cc);
5284 let right = ac.max(cc);
5285 (top, bot, left, right)
5286}
5287
5288fn update_block_vcol<H: crate::types::Host>(
5293 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5294 motion: &Motion,
5295) {
5296 match motion {
5297 Motion::Left
5298 | Motion::Right
5299 | Motion::WordFwd
5300 | Motion::BigWordFwd
5301 | Motion::WordBack
5302 | Motion::BigWordBack
5303 | Motion::WordEnd
5304 | Motion::BigWordEnd
5305 | Motion::WordEndBack
5306 | Motion::BigWordEndBack
5307 | Motion::LineStart
5308 | Motion::FirstNonBlank
5309 | Motion::LineEnd
5310 | Motion::Find { .. }
5311 | Motion::FindRepeat { .. }
5312 | Motion::MatchBracket => {
5313 ed.vim.block_vcol = ed.cursor().1;
5314 }
5315 _ => {}
5317 }
5318}
5319
5320fn apply_block_operator<H: crate::types::Host>(
5325 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5326 op: Operator,
5327) {
5328 let (top, bot, left, right) = block_bounds(ed);
5329 let yank = block_yank(ed, top, bot, left, right);
5331
5332 match op {
5333 Operator::Yank => {
5334 if !yank.is_empty() {
5335 ed.record_yank_to_host(yank.clone());
5336 ed.record_yank(yank, false);
5337 }
5338 ed.vim.mode = Mode::Normal;
5339 ed.jump_cursor(top, left);
5340 }
5341 Operator::Delete => {
5342 ed.push_undo();
5343 delete_block_contents(ed, top, bot, left, right);
5344 if !yank.is_empty() {
5345 ed.record_yank_to_host(yank.clone());
5346 ed.record_delete(yank, false);
5347 }
5348 ed.vim.mode = Mode::Normal;
5349 ed.jump_cursor(top, left);
5350 }
5351 Operator::Change => {
5352 ed.push_undo();
5353 delete_block_contents(ed, top, bot, left, right);
5354 if !yank.is_empty() {
5355 ed.record_yank_to_host(yank.clone());
5356 ed.record_delete(yank, false);
5357 }
5358 ed.jump_cursor(top, left);
5359 begin_insert_noundo(
5360 ed,
5361 1,
5362 InsertReason::BlockChange {
5363 top,
5364 bot,
5365 col: left,
5366 },
5367 );
5368 }
5369 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
5370 ed.push_undo();
5371 transform_block_case(ed, op, top, bot, left, right);
5372 ed.vim.mode = Mode::Normal;
5373 ed.jump_cursor(top, left);
5374 }
5375 Operator::Indent | Operator::Outdent => {
5376 ed.push_undo();
5380 if op == Operator::Indent {
5381 indent_rows(ed, top, bot, 1);
5382 } else {
5383 outdent_rows(ed, top, bot, 1);
5384 }
5385 ed.vim.mode = Mode::Normal;
5386 }
5387 Operator::Fold => unreachable!("Visual zf takes its own path"),
5388 Operator::Reflow => {
5389 ed.push_undo();
5393 reflow_rows(ed, top, bot);
5394 ed.vim.mode = Mode::Normal;
5395 }
5396 }
5397}
5398
5399fn transform_block_case<H: crate::types::Host>(
5403 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5404 op: Operator,
5405 top: usize,
5406 bot: usize,
5407 left: usize,
5408 right: usize,
5409) {
5410 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
5411 for r in top..=bot.min(lines.len().saturating_sub(1)) {
5412 let chars: Vec<char> = lines[r].chars().collect();
5413 if left >= chars.len() {
5414 continue;
5415 }
5416 let end = (right + 1).min(chars.len());
5417 let head: String = chars[..left].iter().collect();
5418 let mid: String = chars[left..end].iter().collect();
5419 let tail: String = chars[end..].iter().collect();
5420 let transformed = match op {
5421 Operator::Uppercase => mid.to_uppercase(),
5422 Operator::Lowercase => mid.to_lowercase(),
5423 Operator::ToggleCase => toggle_case_str(&mid),
5424 _ => mid,
5425 };
5426 lines[r] = format!("{head}{transformed}{tail}");
5427 }
5428 let saved_yank = ed.yank().to_string();
5429 let saved_linewise = ed.vim.yank_linewise;
5430 ed.restore(lines, (top, left));
5431 ed.set_yank(saved_yank);
5432 ed.vim.yank_linewise = saved_linewise;
5433}
5434
5435fn block_yank<H: crate::types::Host>(
5436 ed: &Editor<hjkl_buffer::Buffer, H>,
5437 top: usize,
5438 bot: usize,
5439 left: usize,
5440 right: usize,
5441) -> String {
5442 let lines = buf_lines_to_vec(&ed.buffer);
5443 let mut rows: Vec<String> = Vec::new();
5444 for r in top..=bot {
5445 let line = match lines.get(r) {
5446 Some(l) => l,
5447 None => break,
5448 };
5449 let chars: Vec<char> = line.chars().collect();
5450 let end = (right + 1).min(chars.len());
5451 if left >= chars.len() {
5452 rows.push(String::new());
5453 } else {
5454 rows.push(chars[left..end].iter().collect());
5455 }
5456 }
5457 rows.join("\n")
5458}
5459
5460fn delete_block_contents<H: crate::types::Host>(
5461 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5462 top: usize,
5463 bot: usize,
5464 left: usize,
5465 right: usize,
5466) {
5467 use hjkl_buffer::{Edit, MotionKind, Position};
5468 ed.sync_buffer_content_from_textarea();
5469 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
5470 if last_row < top {
5471 return;
5472 }
5473 ed.mutate_edit(Edit::DeleteRange {
5474 start: Position::new(top, left),
5475 end: Position::new(last_row, right),
5476 kind: MotionKind::Block,
5477 });
5478 ed.push_buffer_cursor_to_textarea();
5479}
5480
5481fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
5483 let (top, bot, left, right) = block_bounds(ed);
5484 ed.push_undo();
5485 ed.sync_buffer_content_from_textarea();
5486 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
5487 for r in top..=bot.min(lines.len().saturating_sub(1)) {
5488 let chars: Vec<char> = lines[r].chars().collect();
5489 if left >= chars.len() {
5490 continue;
5491 }
5492 let end = (right + 1).min(chars.len());
5493 let before: String = chars[..left].iter().collect();
5494 let middle: String = std::iter::repeat_n(ch, end - left).collect();
5495 let after: String = chars[end..].iter().collect();
5496 lines[r] = format!("{before}{middle}{after}");
5497 }
5498 reset_textarea_lines(ed, lines);
5499 ed.vim.mode = Mode::Normal;
5500 ed.jump_cursor(top, left);
5501}
5502
5503fn reset_textarea_lines<H: crate::types::Host>(
5507 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5508 lines: Vec<String>,
5509) {
5510 let cursor = ed.cursor();
5511 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
5512 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
5513 ed.mark_content_dirty();
5514}
5515
5516type Pos = (usize, usize);
5522
5523fn text_object_range<H: crate::types::Host>(
5527 ed: &Editor<hjkl_buffer::Buffer, H>,
5528 obj: TextObject,
5529 inner: bool,
5530) -> Option<(Pos, Pos, MotionKind)> {
5531 match obj {
5532 TextObject::Word { big } => {
5533 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
5534 }
5535 TextObject::Quote(q) => {
5536 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
5537 }
5538 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
5539 TextObject::Paragraph => {
5540 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
5541 }
5542 TextObject::XmlTag => {
5543 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
5544 }
5545 TextObject::Sentence => {
5546 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
5547 }
5548 }
5549}
5550
5551fn sentence_boundary<H: crate::types::Host>(
5555 ed: &Editor<hjkl_buffer::Buffer, H>,
5556 forward: bool,
5557) -> Option<(usize, usize)> {
5558 let lines = buf_lines_to_vec(&ed.buffer);
5559 if lines.is_empty() {
5560 return None;
5561 }
5562 let pos_to_idx = |pos: (usize, usize)| -> usize {
5563 let mut idx = 0;
5564 for line in lines.iter().take(pos.0) {
5565 idx += line.chars().count() + 1;
5566 }
5567 idx + pos.1
5568 };
5569 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5570 for (r, line) in lines.iter().enumerate() {
5571 let len = line.chars().count();
5572 if idx <= len {
5573 return (r, idx);
5574 }
5575 idx -= len + 1;
5576 }
5577 let last = lines.len().saturating_sub(1);
5578 (last, lines[last].chars().count())
5579 };
5580 let mut chars: Vec<char> = Vec::new();
5581 for (r, line) in lines.iter().enumerate() {
5582 chars.extend(line.chars());
5583 if r + 1 < lines.len() {
5584 chars.push('\n');
5585 }
5586 }
5587 if chars.is_empty() {
5588 return None;
5589 }
5590 let total = chars.len();
5591 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
5592 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5593
5594 if forward {
5595 let mut i = cursor_idx + 1;
5598 while i < total {
5599 if is_terminator(chars[i]) {
5600 while i + 1 < total && is_terminator(chars[i + 1]) {
5601 i += 1;
5602 }
5603 if i + 1 >= total {
5604 return None;
5605 }
5606 if chars[i + 1].is_whitespace() {
5607 let mut j = i + 1;
5608 while j < total && chars[j].is_whitespace() {
5609 j += 1;
5610 }
5611 if j >= total {
5612 return None;
5613 }
5614 return Some(idx_to_pos(j));
5615 }
5616 }
5617 i += 1;
5618 }
5619 None
5620 } else {
5621 let find_start = |from: usize| -> Option<usize> {
5625 let mut start = from;
5626 while start > 0 {
5627 let prev = chars[start - 1];
5628 if prev.is_whitespace() {
5629 let mut k = start - 1;
5630 while k > 0 && chars[k - 1].is_whitespace() {
5631 k -= 1;
5632 }
5633 if k > 0 && is_terminator(chars[k - 1]) {
5634 break;
5635 }
5636 }
5637 start -= 1;
5638 }
5639 while start < total && chars[start].is_whitespace() {
5640 start += 1;
5641 }
5642 (start < total).then_some(start)
5643 };
5644 let current_start = find_start(cursor_idx)?;
5645 if current_start < cursor_idx {
5646 return Some(idx_to_pos(current_start));
5647 }
5648 let mut k = current_start;
5651 while k > 0 && chars[k - 1].is_whitespace() {
5652 k -= 1;
5653 }
5654 if k == 0 {
5655 return None;
5656 }
5657 let prev_start = find_start(k - 1)?;
5658 Some(idx_to_pos(prev_start))
5659 }
5660}
5661
5662fn sentence_text_object<H: crate::types::Host>(
5668 ed: &Editor<hjkl_buffer::Buffer, H>,
5669 inner: bool,
5670) -> Option<((usize, usize), (usize, usize))> {
5671 let lines = buf_lines_to_vec(&ed.buffer);
5672 if lines.is_empty() {
5673 return None;
5674 }
5675 let pos_to_idx = |pos: (usize, usize)| -> usize {
5678 let mut idx = 0;
5679 for line in lines.iter().take(pos.0) {
5680 idx += line.chars().count() + 1;
5681 }
5682 idx + pos.1
5683 };
5684 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5685 for (r, line) in lines.iter().enumerate() {
5686 let len = line.chars().count();
5687 if idx <= len {
5688 return (r, idx);
5689 }
5690 idx -= len + 1;
5691 }
5692 let last = lines.len().saturating_sub(1);
5693 (last, lines[last].chars().count())
5694 };
5695 let mut chars: Vec<char> = Vec::new();
5696 for (r, line) in lines.iter().enumerate() {
5697 chars.extend(line.chars());
5698 if r + 1 < lines.len() {
5699 chars.push('\n');
5700 }
5701 }
5702 if chars.is_empty() {
5703 return None;
5704 }
5705
5706 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
5707 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5708
5709 let mut start = cursor_idx;
5713 while start > 0 {
5714 let prev = chars[start - 1];
5715 if prev.is_whitespace() {
5716 let mut k = start - 1;
5720 while k > 0 && chars[k - 1].is_whitespace() {
5721 k -= 1;
5722 }
5723 if k > 0 && is_terminator(chars[k - 1]) {
5724 break;
5725 }
5726 }
5727 start -= 1;
5728 }
5729 while start < chars.len() && chars[start].is_whitespace() {
5732 start += 1;
5733 }
5734 if start >= chars.len() {
5735 return None;
5736 }
5737
5738 let mut end = start;
5741 while end < chars.len() {
5742 if is_terminator(chars[end]) {
5743 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5745 end += 1;
5746 }
5747 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5750 break;
5751 }
5752 }
5753 end += 1;
5754 }
5755 let end_idx = (end + 1).min(chars.len());
5757
5758 let final_end = if inner {
5759 end_idx
5760 } else {
5761 let mut e = end_idx;
5765 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5766 e += 1;
5767 }
5768 e
5769 };
5770
5771 Some((idx_to_pos(start), idx_to_pos(final_end)))
5772}
5773
5774fn tag_text_object<H: crate::types::Host>(
5778 ed: &Editor<hjkl_buffer::Buffer, H>,
5779 inner: bool,
5780) -> Option<((usize, usize), (usize, usize))> {
5781 let lines = buf_lines_to_vec(&ed.buffer);
5782 if lines.is_empty() {
5783 return None;
5784 }
5785 let pos_to_idx = |pos: (usize, usize)| -> usize {
5789 let mut idx = 0;
5790 for line in lines.iter().take(pos.0) {
5791 idx += line.chars().count() + 1;
5792 }
5793 idx + pos.1
5794 };
5795 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5796 for (r, line) in lines.iter().enumerate() {
5797 let len = line.chars().count();
5798 if idx <= len {
5799 return (r, idx);
5800 }
5801 idx -= len + 1;
5802 }
5803 let last = lines.len().saturating_sub(1);
5804 (last, lines[last].chars().count())
5805 };
5806 let mut chars: Vec<char> = Vec::new();
5807 for (r, line) in lines.iter().enumerate() {
5808 chars.extend(line.chars());
5809 if r + 1 < lines.len() {
5810 chars.push('\n');
5811 }
5812 }
5813 let cursor_idx = pos_to_idx(ed.cursor());
5814
5815 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5823 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5824 let mut i = 0;
5825 while i < chars.len() {
5826 if chars[i] != '<' {
5827 i += 1;
5828 continue;
5829 }
5830 let mut j = i + 1;
5831 while j < chars.len() && chars[j] != '>' {
5832 j += 1;
5833 }
5834 if j >= chars.len() {
5835 break;
5836 }
5837 let inside: String = chars[i + 1..j].iter().collect();
5838 let close_end = j + 1;
5839 let trimmed = inside.trim();
5840 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5841 i = close_end;
5842 continue;
5843 }
5844 if let Some(rest) = trimmed.strip_prefix('/') {
5845 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5846 if !name.is_empty()
5847 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5848 {
5849 let (open_start, content_start, _) = stack[stack_idx].clone();
5850 stack.truncate(stack_idx);
5851 let content_end = i;
5852 let candidate = (open_start, content_start, content_end, close_end);
5853 if cursor_idx >= content_start && cursor_idx <= content_end {
5854 innermost = match innermost {
5855 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5856 Some(candidate)
5857 }
5858 None => Some(candidate),
5859 existing => existing,
5860 };
5861 } else if open_start >= cursor_idx && next_after.is_none() {
5862 next_after = Some(candidate);
5863 }
5864 }
5865 } else if !trimmed.ends_with('/') {
5866 let name: String = trimmed
5867 .split(|c: char| c.is_whitespace() || c == '/')
5868 .next()
5869 .unwrap_or("")
5870 .to_string();
5871 if !name.is_empty() {
5872 stack.push((i, close_end, name));
5873 }
5874 }
5875 i = close_end;
5876 }
5877
5878 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5879 if inner {
5880 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5881 } else {
5882 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5883 }
5884}
5885
5886fn is_wordchar(c: char) -> bool {
5887 c.is_alphanumeric() || c == '_'
5888}
5889
5890pub(crate) use hjkl_buffer::is_keyword_char;
5894
5895fn word_text_object<H: crate::types::Host>(
5896 ed: &Editor<hjkl_buffer::Buffer, H>,
5897 inner: bool,
5898 big: bool,
5899) -> Option<((usize, usize), (usize, usize))> {
5900 let (row, col) = ed.cursor();
5901 let line = buf_line(&ed.buffer, row)?;
5902 let chars: Vec<char> = line.chars().collect();
5903 if chars.is_empty() {
5904 return None;
5905 }
5906 let at = col.min(chars.len().saturating_sub(1));
5907 let classify = |c: char| -> u8 {
5908 if c.is_whitespace() {
5909 0
5910 } else if big || is_wordchar(c) {
5911 1
5912 } else {
5913 2
5914 }
5915 };
5916 let cls = classify(chars[at]);
5917 let mut start = at;
5918 while start > 0 && classify(chars[start - 1]) == cls {
5919 start -= 1;
5920 }
5921 let mut end = at;
5922 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5923 end += 1;
5924 }
5925 let char_byte = |i: usize| {
5927 if i >= chars.len() {
5928 line.len()
5929 } else {
5930 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5931 }
5932 };
5933 let mut start_col = char_byte(start);
5934 let mut end_col = char_byte(end + 1);
5936 if !inner {
5937 let mut t = end + 1;
5939 let mut included_trailing = false;
5940 while t < chars.len() && chars[t].is_whitespace() {
5941 included_trailing = true;
5942 t += 1;
5943 }
5944 if included_trailing {
5945 end_col = char_byte(t);
5946 } else {
5947 let mut s = start;
5948 while s > 0 && chars[s - 1].is_whitespace() {
5949 s -= 1;
5950 }
5951 start_col = char_byte(s);
5952 }
5953 }
5954 Some(((row, start_col), (row, end_col)))
5955}
5956
5957fn quote_text_object<H: crate::types::Host>(
5958 ed: &Editor<hjkl_buffer::Buffer, H>,
5959 q: char,
5960 inner: bool,
5961) -> Option<((usize, usize), (usize, usize))> {
5962 let (row, col) = ed.cursor();
5963 let line = buf_line(&ed.buffer, row)?;
5964 let bytes = line.as_bytes();
5965 let q_byte = q as u8;
5966 let mut positions: Vec<usize> = Vec::new();
5968 for (i, &b) in bytes.iter().enumerate() {
5969 if b == q_byte {
5970 positions.push(i);
5971 }
5972 }
5973 if positions.len() < 2 {
5974 return None;
5975 }
5976 let mut open_idx: Option<usize> = None;
5977 let mut close_idx: Option<usize> = None;
5978 for pair in positions.chunks(2) {
5979 if pair.len() < 2 {
5980 break;
5981 }
5982 if col >= pair[0] && col <= pair[1] {
5983 open_idx = Some(pair[0]);
5984 close_idx = Some(pair[1]);
5985 break;
5986 }
5987 if col < pair[0] {
5988 open_idx = Some(pair[0]);
5989 close_idx = Some(pair[1]);
5990 break;
5991 }
5992 }
5993 let open = open_idx?;
5994 let close = close_idx?;
5995 if inner {
5997 if close <= open + 1 {
5998 return None;
5999 }
6000 Some(((row, open + 1), (row, close)))
6001 } else {
6002 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
6009 let mut end = after_close;
6011 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
6012 end += 1;
6013 }
6014 Some(((row, open), (row, end)))
6015 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
6016 let mut start = open;
6018 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
6019 start -= 1;
6020 }
6021 Some(((row, start), (row, close + 1)))
6022 } else {
6023 Some(((row, open), (row, close + 1)))
6024 }
6025 }
6026}
6027
6028fn bracket_text_object<H: crate::types::Host>(
6029 ed: &Editor<hjkl_buffer::Buffer, H>,
6030 open: char,
6031 inner: bool,
6032) -> Option<(Pos, Pos, MotionKind)> {
6033 let close = match open {
6034 '(' => ')',
6035 '[' => ']',
6036 '{' => '}',
6037 '<' => '>',
6038 _ => return None,
6039 };
6040 let (row, col) = ed.cursor();
6041 let lines = buf_lines_to_vec(&ed.buffer);
6042 let lines = lines.as_slice();
6043 let open_pos = find_open_bracket(lines, row, col, open, close)
6048 .or_else(|| find_next_open(lines, row, col, open))?;
6049 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
6050 if inner {
6052 if close_pos.0 > open_pos.0 + 1 {
6058 let inner_row_start = open_pos.0 + 1;
6060 let inner_row_end = close_pos.0 - 1;
6061 let end_col = lines
6062 .get(inner_row_end)
6063 .map(|l| l.chars().count())
6064 .unwrap_or(0);
6065 return Some((
6066 (inner_row_start, 0),
6067 (inner_row_end, end_col),
6068 MotionKind::Linewise,
6069 ));
6070 }
6071 let inner_start = advance_pos(lines, open_pos);
6072 if inner_start.0 > close_pos.0
6073 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
6074 {
6075 return None;
6076 }
6077 Some((inner_start, close_pos, MotionKind::Exclusive))
6078 } else {
6079 Some((
6080 open_pos,
6081 advance_pos(lines, close_pos),
6082 MotionKind::Exclusive,
6083 ))
6084 }
6085}
6086
6087fn find_open_bracket(
6088 lines: &[String],
6089 row: usize,
6090 col: usize,
6091 open: char,
6092 close: char,
6093) -> Option<(usize, usize)> {
6094 let mut depth: i32 = 0;
6095 let mut r = row;
6096 let mut c = col as isize;
6097 loop {
6098 let cur = &lines[r];
6099 let chars: Vec<char> = cur.chars().collect();
6100 if (c as usize) >= chars.len() {
6104 c = chars.len() as isize - 1;
6105 }
6106 while c >= 0 {
6107 let ch = chars[c as usize];
6108 if ch == close {
6109 depth += 1;
6110 } else if ch == open {
6111 if depth == 0 {
6112 return Some((r, c as usize));
6113 }
6114 depth -= 1;
6115 }
6116 c -= 1;
6117 }
6118 if r == 0 {
6119 return None;
6120 }
6121 r -= 1;
6122 c = lines[r].chars().count() as isize - 1;
6123 }
6124}
6125
6126fn find_close_bracket(
6127 lines: &[String],
6128 row: usize,
6129 start_col: usize,
6130 open: char,
6131 close: char,
6132) -> Option<(usize, usize)> {
6133 let mut depth: i32 = 0;
6134 let mut r = row;
6135 let mut c = start_col;
6136 loop {
6137 let cur = &lines[r];
6138 let chars: Vec<char> = cur.chars().collect();
6139 while c < chars.len() {
6140 let ch = chars[c];
6141 if ch == open {
6142 depth += 1;
6143 } else if ch == close {
6144 if depth == 0 {
6145 return Some((r, c));
6146 }
6147 depth -= 1;
6148 }
6149 c += 1;
6150 }
6151 if r + 1 >= lines.len() {
6152 return None;
6153 }
6154 r += 1;
6155 c = 0;
6156 }
6157}
6158
6159fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
6163 let mut r = row;
6164 let mut c = col;
6165 while r < lines.len() {
6166 let chars: Vec<char> = lines[r].chars().collect();
6167 while c < chars.len() {
6168 if chars[c] == open {
6169 return Some((r, c));
6170 }
6171 c += 1;
6172 }
6173 r += 1;
6174 c = 0;
6175 }
6176 None
6177}
6178
6179fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
6180 let (r, c) = pos;
6181 let line_len = lines[r].chars().count();
6182 if c < line_len {
6183 (r, c + 1)
6184 } else if r + 1 < lines.len() {
6185 (r + 1, 0)
6186 } else {
6187 pos
6188 }
6189}
6190
6191fn paragraph_text_object<H: crate::types::Host>(
6192 ed: &Editor<hjkl_buffer::Buffer, H>,
6193 inner: bool,
6194) -> Option<((usize, usize), (usize, usize))> {
6195 let (row, _) = ed.cursor();
6196 let lines = buf_lines_to_vec(&ed.buffer);
6197 if lines.is_empty() {
6198 return None;
6199 }
6200 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
6202 if is_blank(row) {
6203 return None;
6204 }
6205 let mut top = row;
6206 while top > 0 && !is_blank(top - 1) {
6207 top -= 1;
6208 }
6209 let mut bot = row;
6210 while bot + 1 < lines.len() && !is_blank(bot + 1) {
6211 bot += 1;
6212 }
6213 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
6215 bot += 1;
6216 }
6217 let end_col = lines[bot].chars().count();
6218 Some(((top, 0), (bot, end_col)))
6219}
6220
6221fn read_vim_range<H: crate::types::Host>(
6227 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6228 start: (usize, usize),
6229 end: (usize, usize),
6230 kind: MotionKind,
6231) -> String {
6232 let (top, bot) = order(start, end);
6233 ed.sync_buffer_content_from_textarea();
6234 let lines = buf_lines_to_vec(&ed.buffer);
6235 match kind {
6236 MotionKind::Linewise => {
6237 let lo = top.0;
6238 let hi = bot.0.min(lines.len().saturating_sub(1));
6239 let mut text = lines[lo..=hi].join("\n");
6240 text.push('\n');
6241 text
6242 }
6243 MotionKind::Inclusive | MotionKind::Exclusive => {
6244 let inclusive = matches!(kind, MotionKind::Inclusive);
6245 let mut out = String::new();
6247 for row in top.0..=bot.0 {
6248 let line = lines.get(row).map(String::as_str).unwrap_or("");
6249 let lo = if row == top.0 { top.1 } else { 0 };
6250 let hi_unclamped = if row == bot.0 {
6251 if inclusive { bot.1 + 1 } else { bot.1 }
6252 } else {
6253 line.chars().count() + 1
6254 };
6255 let row_chars: Vec<char> = line.chars().collect();
6256 let hi = hi_unclamped.min(row_chars.len());
6257 if lo < hi {
6258 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
6259 }
6260 if row < bot.0 {
6261 out.push('\n');
6262 }
6263 }
6264 out
6265 }
6266 }
6267}
6268
6269fn cut_vim_range<H: crate::types::Host>(
6278 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6279 start: (usize, usize),
6280 end: (usize, usize),
6281 kind: MotionKind,
6282) -> String {
6283 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
6284 let (top, bot) = order(start, end);
6285 ed.sync_buffer_content_from_textarea();
6286 let (buf_start, buf_end, buf_kind) = match kind {
6287 MotionKind::Linewise => (
6288 Position::new(top.0, 0),
6289 Position::new(bot.0, 0),
6290 BufKind::Line,
6291 ),
6292 MotionKind::Inclusive => {
6293 let line_chars = buf_line_chars(&ed.buffer, bot.0);
6294 let next = if bot.1 < line_chars {
6298 Position::new(bot.0, bot.1 + 1)
6299 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
6300 Position::new(bot.0 + 1, 0)
6301 } else {
6302 Position::new(bot.0, line_chars)
6303 };
6304 (Position::new(top.0, top.1), next, BufKind::Char)
6305 }
6306 MotionKind::Exclusive => (
6307 Position::new(top.0, top.1),
6308 Position::new(bot.0, bot.1),
6309 BufKind::Char,
6310 ),
6311 };
6312 let inverse = ed.mutate_edit(Edit::DeleteRange {
6313 start: buf_start,
6314 end: buf_end,
6315 kind: buf_kind,
6316 });
6317 let text = match inverse {
6318 Edit::InsertStr { text, .. } => text,
6319 _ => String::new(),
6320 };
6321 if !text.is_empty() {
6322 ed.record_yank_to_host(text.clone());
6323 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
6324 }
6325 ed.push_buffer_cursor_to_textarea();
6326 text
6327}
6328
6329fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6335 use hjkl_buffer::{Edit, MotionKind, Position};
6336 ed.sync_buffer_content_from_textarea();
6337 let cursor = buf_cursor_pos(&ed.buffer);
6338 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6339 if cursor.col >= line_chars {
6340 return;
6341 }
6342 let inverse = ed.mutate_edit(Edit::DeleteRange {
6343 start: cursor,
6344 end: Position::new(cursor.row, line_chars),
6345 kind: MotionKind::Char,
6346 });
6347 if let Edit::InsertStr { text, .. } = inverse
6348 && !text.is_empty()
6349 {
6350 ed.record_yank_to_host(text.clone());
6351 ed.vim.yank_linewise = false;
6352 ed.set_yank(text);
6353 }
6354 buf_set_cursor_pos(&mut ed.buffer, cursor);
6355 ed.push_buffer_cursor_to_textarea();
6356}
6357
6358fn do_char_delete<H: crate::types::Host>(
6359 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6360 forward: bool,
6361 count: usize,
6362) {
6363 use hjkl_buffer::{Edit, MotionKind, Position};
6364 ed.push_undo();
6365 ed.sync_buffer_content_from_textarea();
6366 let mut deleted = String::new();
6369 for _ in 0..count {
6370 let cursor = buf_cursor_pos(&ed.buffer);
6371 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6372 if forward {
6373 if cursor.col >= line_chars {
6376 continue;
6377 }
6378 let inverse = ed.mutate_edit(Edit::DeleteRange {
6379 start: cursor,
6380 end: Position::new(cursor.row, cursor.col + 1),
6381 kind: MotionKind::Char,
6382 });
6383 if let Edit::InsertStr { text, .. } = inverse {
6384 deleted.push_str(&text);
6385 }
6386 } else {
6387 if cursor.col == 0 {
6389 continue;
6390 }
6391 let inverse = ed.mutate_edit(Edit::DeleteRange {
6392 start: Position::new(cursor.row, cursor.col - 1),
6393 end: cursor,
6394 kind: MotionKind::Char,
6395 });
6396 if let Edit::InsertStr { text, .. } = inverse {
6397 deleted = text + &deleted;
6400 }
6401 }
6402 }
6403 if !deleted.is_empty() {
6404 ed.record_yank_to_host(deleted.clone());
6405 ed.record_delete(deleted, false);
6406 }
6407 ed.push_buffer_cursor_to_textarea();
6408}
6409
6410fn adjust_number<H: crate::types::Host>(
6414 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6415 delta: i64,
6416) -> bool {
6417 use hjkl_buffer::{Edit, MotionKind, Position};
6418 ed.sync_buffer_content_from_textarea();
6419 let cursor = buf_cursor_pos(&ed.buffer);
6420 let row = cursor.row;
6421 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
6422 Some(l) => l.chars().collect(),
6423 None => return false,
6424 };
6425 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
6426 return false;
6427 };
6428 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
6429 digit_start - 1
6430 } else {
6431 digit_start
6432 };
6433 let mut span_end = digit_start;
6434 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
6435 span_end += 1;
6436 }
6437 let s: String = chars[span_start..span_end].iter().collect();
6438 let Ok(n) = s.parse::<i64>() else {
6439 return false;
6440 };
6441 let new_s = n.saturating_add(delta).to_string();
6442
6443 ed.push_undo();
6444 let span_start_pos = Position::new(row, span_start);
6445 let span_end_pos = Position::new(row, span_end);
6446 ed.mutate_edit(Edit::DeleteRange {
6447 start: span_start_pos,
6448 end: span_end_pos,
6449 kind: MotionKind::Char,
6450 });
6451 ed.mutate_edit(Edit::InsertStr {
6452 at: span_start_pos,
6453 text: new_s.clone(),
6454 });
6455 let new_len = new_s.chars().count();
6456 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
6457 ed.push_buffer_cursor_to_textarea();
6458 true
6459}
6460
6461pub(crate) fn replace_char<H: crate::types::Host>(
6462 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6463 ch: char,
6464 count: usize,
6465) {
6466 use hjkl_buffer::{Edit, MotionKind, Position};
6467 ed.push_undo();
6468 ed.sync_buffer_content_from_textarea();
6469 for _ in 0..count {
6470 let cursor = buf_cursor_pos(&ed.buffer);
6471 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6472 if cursor.col >= line_chars {
6473 break;
6474 }
6475 ed.mutate_edit(Edit::DeleteRange {
6476 start: cursor,
6477 end: Position::new(cursor.row, cursor.col + 1),
6478 kind: MotionKind::Char,
6479 });
6480 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
6481 }
6482 crate::motions::move_left(&mut ed.buffer, 1);
6484 ed.push_buffer_cursor_to_textarea();
6485}
6486
6487fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6488 use hjkl_buffer::{Edit, MotionKind, Position};
6489 ed.sync_buffer_content_from_textarea();
6490 let cursor = buf_cursor_pos(&ed.buffer);
6491 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
6492 return;
6493 };
6494 let toggled = if c.is_uppercase() {
6495 c.to_lowercase().next().unwrap_or(c)
6496 } else {
6497 c.to_uppercase().next().unwrap_or(c)
6498 };
6499 ed.mutate_edit(Edit::DeleteRange {
6500 start: cursor,
6501 end: Position::new(cursor.row, cursor.col + 1),
6502 kind: MotionKind::Char,
6503 });
6504 ed.mutate_edit(Edit::InsertChar {
6505 at: cursor,
6506 ch: toggled,
6507 });
6508}
6509
6510fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6511 use hjkl_buffer::{Edit, Position};
6512 ed.sync_buffer_content_from_textarea();
6513 let row = buf_cursor_pos(&ed.buffer).row;
6514 if row + 1 >= buf_row_count(&ed.buffer) {
6515 return;
6516 }
6517 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
6518 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
6519 let next_trimmed = next_raw.trim_start();
6520 let cur_chars = cur_line.chars().count();
6521 let next_chars = next_raw.chars().count();
6522 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
6525 " "
6526 } else {
6527 ""
6528 };
6529 let joined = format!("{cur_line}{separator}{next_trimmed}");
6530 ed.mutate_edit(Edit::Replace {
6531 start: Position::new(row, 0),
6532 end: Position::new(row + 1, next_chars),
6533 with: joined,
6534 });
6535 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
6539 ed.push_buffer_cursor_to_textarea();
6540}
6541
6542fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6545 use hjkl_buffer::Edit;
6546 ed.sync_buffer_content_from_textarea();
6547 let row = buf_cursor_pos(&ed.buffer).row;
6548 if row + 1 >= buf_row_count(&ed.buffer) {
6549 return;
6550 }
6551 let join_col = buf_line_chars(&ed.buffer, row);
6552 ed.mutate_edit(Edit::JoinLines {
6553 row,
6554 count: 1,
6555 with_space: false,
6556 });
6557 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
6559 ed.push_buffer_cursor_to_textarea();
6560}
6561
6562fn do_paste<H: crate::types::Host>(
6563 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6564 before: bool,
6565 count: usize,
6566) {
6567 use hjkl_buffer::{Edit, Position};
6568 ed.push_undo();
6569 let selector = ed.vim.pending_register.take();
6574 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
6575 Some(slot) => (slot.text.clone(), slot.linewise),
6576 None => {
6582 let s = &ed.registers().unnamed;
6583 (s.text.clone(), s.linewise)
6584 }
6585 };
6586 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
6590 for _ in 0..count {
6591 ed.sync_buffer_content_from_textarea();
6592 let yank = yank.clone();
6593 if yank.is_empty() {
6594 continue;
6595 }
6596 if linewise {
6597 let text = yank.trim_matches('\n').to_string();
6601 let row = buf_cursor_pos(&ed.buffer).row;
6602 let target_row = if before {
6603 ed.mutate_edit(Edit::InsertStr {
6604 at: Position::new(row, 0),
6605 text: format!("{text}\n"),
6606 });
6607 row
6608 } else {
6609 let line_chars = buf_line_chars(&ed.buffer, row);
6610 ed.mutate_edit(Edit::InsertStr {
6611 at: Position::new(row, line_chars),
6612 text: format!("\n{text}"),
6613 });
6614 row + 1
6615 };
6616 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
6617 crate::motions::move_first_non_blank(&mut ed.buffer);
6618 ed.push_buffer_cursor_to_textarea();
6619 let payload_lines = text.lines().count().max(1);
6621 let bot_row = target_row + payload_lines - 1;
6622 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
6623 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
6624 } else {
6625 let cursor = buf_cursor_pos(&ed.buffer);
6629 let at = if before {
6630 cursor
6631 } else {
6632 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
6633 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
6634 };
6635 ed.mutate_edit(Edit::InsertStr {
6636 at,
6637 text: yank.clone(),
6638 });
6639 crate::motions::move_left(&mut ed.buffer, 1);
6642 ed.push_buffer_cursor_to_textarea();
6643 let lo = (at.row, at.col);
6645 let hi = ed.cursor();
6646 paste_mark = Some((lo, hi));
6647 }
6648 }
6649 if let Some((lo, hi)) = paste_mark {
6650 ed.set_mark('[', lo);
6651 ed.set_mark(']', hi);
6652 }
6653 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
6655}
6656
6657pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6658 if let Some((lines, cursor)) = ed.undo_stack.pop() {
6659 let current = ed.snapshot();
6660 ed.redo_stack.push(current);
6661 ed.restore(lines, cursor);
6662 }
6663 ed.vim.mode = Mode::Normal;
6664 clamp_cursor_to_normal_mode(ed);
6668}
6669
6670pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
6671 if let Some((lines, cursor)) = ed.redo_stack.pop() {
6672 let current = ed.snapshot();
6673 ed.undo_stack.push(current);
6674 ed.cap_undo();
6675 ed.restore(lines, cursor);
6676 }
6677 ed.vim.mode = Mode::Normal;
6678}
6679
6680fn replay_insert_and_finish<H: crate::types::Host>(
6687 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6688 text: &str,
6689) {
6690 use hjkl_buffer::{Edit, Position};
6691 let cursor = ed.cursor();
6692 ed.mutate_edit(Edit::InsertStr {
6693 at: Position::new(cursor.0, cursor.1),
6694 text: text.to_string(),
6695 });
6696 if ed.vim.insert_session.take().is_some() {
6697 if ed.cursor().1 > 0 {
6698 crate::motions::move_left(&mut ed.buffer, 1);
6699 ed.push_buffer_cursor_to_textarea();
6700 }
6701 ed.vim.mode = Mode::Normal;
6702 }
6703}
6704
6705pub(crate) fn replay_last_change<H: crate::types::Host>(
6706 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6707 outer_count: usize,
6708) {
6709 let Some(change) = ed.vim.last_change.clone() else {
6710 return;
6711 };
6712 ed.vim.replaying = true;
6713 let scale = if outer_count > 0 { outer_count } else { 1 };
6714 match change {
6715 LastChange::OpMotion {
6716 op,
6717 motion,
6718 count,
6719 inserted,
6720 } => {
6721 let total = count.max(1) * scale;
6722 apply_op_with_motion(ed, op, &motion, total);
6723 if let Some(text) = inserted {
6724 replay_insert_and_finish(ed, &text);
6725 }
6726 }
6727 LastChange::OpTextObj {
6728 op,
6729 obj,
6730 inner,
6731 inserted,
6732 } => {
6733 apply_op_with_text_object(ed, op, obj, inner);
6734 if let Some(text) = inserted {
6735 replay_insert_and_finish(ed, &text);
6736 }
6737 }
6738 LastChange::LineOp {
6739 op,
6740 count,
6741 inserted,
6742 } => {
6743 let total = count.max(1) * scale;
6744 execute_line_op(ed, op, total);
6745 if let Some(text) = inserted {
6746 replay_insert_and_finish(ed, &text);
6747 }
6748 }
6749 LastChange::CharDel { forward, count } => {
6750 do_char_delete(ed, forward, count * scale);
6751 }
6752 LastChange::ReplaceChar { ch, count } => {
6753 replace_char(ed, ch, count * scale);
6754 }
6755 LastChange::ToggleCase { count } => {
6756 for _ in 0..count * scale {
6757 ed.push_undo();
6758 toggle_case_at_cursor(ed);
6759 }
6760 }
6761 LastChange::JoinLine { count } => {
6762 for _ in 0..count * scale {
6763 ed.push_undo();
6764 join_line(ed);
6765 }
6766 }
6767 LastChange::Paste { before, count } => {
6768 do_paste(ed, before, count * scale);
6769 }
6770 LastChange::DeleteToEol { inserted } => {
6771 use hjkl_buffer::{Edit, Position};
6772 ed.push_undo();
6773 delete_to_eol(ed);
6774 if let Some(text) = inserted {
6775 let cursor = ed.cursor();
6776 ed.mutate_edit(Edit::InsertStr {
6777 at: Position::new(cursor.0, cursor.1),
6778 text,
6779 });
6780 }
6781 }
6782 LastChange::OpenLine { above, inserted } => {
6783 use hjkl_buffer::{Edit, Position};
6784 ed.push_undo();
6785 ed.sync_buffer_content_from_textarea();
6786 let row = buf_cursor_pos(&ed.buffer).row;
6787 if above {
6788 ed.mutate_edit(Edit::InsertStr {
6789 at: Position::new(row, 0),
6790 text: "\n".to_string(),
6791 });
6792 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6793 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6794 } else {
6795 let line_chars = buf_line_chars(&ed.buffer, row);
6796 ed.mutate_edit(Edit::InsertStr {
6797 at: Position::new(row, line_chars),
6798 text: "\n".to_string(),
6799 });
6800 }
6801 ed.push_buffer_cursor_to_textarea();
6802 let cursor = ed.cursor();
6803 ed.mutate_edit(Edit::InsertStr {
6804 at: Position::new(cursor.0, cursor.1),
6805 text: inserted,
6806 });
6807 }
6808 LastChange::InsertAt {
6809 entry,
6810 inserted,
6811 count,
6812 } => {
6813 use hjkl_buffer::{Edit, Position};
6814 ed.push_undo();
6815 match entry {
6816 InsertEntry::I => {}
6817 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6818 InsertEntry::A => {
6819 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6820 ed.push_buffer_cursor_to_textarea();
6821 }
6822 InsertEntry::ShiftA => {
6823 crate::motions::move_line_end(&mut ed.buffer);
6824 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6825 ed.push_buffer_cursor_to_textarea();
6826 }
6827 }
6828 for _ in 0..count.max(1) {
6829 let cursor = ed.cursor();
6830 ed.mutate_edit(Edit::InsertStr {
6831 at: Position::new(cursor.0, cursor.1),
6832 text: inserted.clone(),
6833 });
6834 }
6835 }
6836 }
6837 ed.vim.replaying = false;
6838}
6839
6840fn extract_inserted(before: &str, after: &str) -> String {
6843 let before_chars: Vec<char> = before.chars().collect();
6844 let after_chars: Vec<char> = after.chars().collect();
6845 if after_chars.len() <= before_chars.len() {
6846 return String::new();
6847 }
6848 let prefix = before_chars
6849 .iter()
6850 .zip(after_chars.iter())
6851 .take_while(|(a, b)| a == b)
6852 .count();
6853 let max_suffix = before_chars.len() - prefix;
6854 let suffix = before_chars
6855 .iter()
6856 .rev()
6857 .zip(after_chars.iter().rev())
6858 .take(max_suffix)
6859 .take_while(|(a, b)| a == b)
6860 .count();
6861 after_chars[prefix..after_chars.len() - suffix]
6862 .iter()
6863 .collect()
6864}
6865
6866#[cfg(all(test, feature = "crossterm"))]
6869mod tests {
6870 use crate::VimMode;
6871 use crate::editor::Editor;
6872 use crate::types::Host;
6873 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6874
6875 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6876 let mut iter = keys.chars().peekable();
6880 while let Some(c) = iter.next() {
6881 if c == '<' {
6882 let mut tag = String::new();
6883 for ch in iter.by_ref() {
6884 if ch == '>' {
6885 break;
6886 }
6887 tag.push(ch);
6888 }
6889 let ev = match tag.as_str() {
6890 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6891 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6892 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6893 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6894 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6895 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6896 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6897 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6898 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6902 s if s.starts_with("C-") => {
6903 let ch = s.chars().nth(2).unwrap();
6904 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6905 }
6906 _ => continue,
6907 };
6908 e.handle_key(ev);
6909 } else {
6910 let mods = if c.is_uppercase() {
6911 KeyModifiers::SHIFT
6912 } else {
6913 KeyModifiers::NONE
6914 };
6915 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6916 }
6917 }
6918 }
6919
6920 fn editor_with(content: &str) -> Editor {
6921 let opts = crate::types::Options {
6926 shiftwidth: 2,
6927 ..crate::types::Options::default()
6928 };
6929 let mut e = Editor::new(
6930 hjkl_buffer::Buffer::new(),
6931 crate::types::DefaultHost::new(),
6932 opts,
6933 );
6934 e.set_content(content);
6935 e
6936 }
6937
6938 #[test]
6939 fn f_char_jumps_on_line() {
6940 let mut e = editor_with("hello world");
6941 run_keys(&mut e, "fw");
6942 assert_eq!(e.cursor(), (0, 6));
6943 }
6944
6945 #[test]
6946 fn cap_f_jumps_backward() {
6947 let mut e = editor_with("hello world");
6948 e.jump_cursor(0, 10);
6949 run_keys(&mut e, "Fo");
6950 assert_eq!(e.cursor().1, 7);
6951 }
6952
6953 #[test]
6954 fn t_stops_before_char() {
6955 let mut e = editor_with("hello");
6956 run_keys(&mut e, "tl");
6957 assert_eq!(e.cursor(), (0, 1));
6958 }
6959
6960 #[test]
6961 fn semicolon_repeats_find() {
6962 let mut e = editor_with("aa.bb.cc");
6963 run_keys(&mut e, "f.");
6964 assert_eq!(e.cursor().1, 2);
6965 run_keys(&mut e, ";");
6966 assert_eq!(e.cursor().1, 5);
6967 }
6968
6969 #[test]
6970 fn comma_repeats_find_reverse() {
6971 let mut e = editor_with("aa.bb.cc");
6972 run_keys(&mut e, "f.");
6973 run_keys(&mut e, ";");
6974 run_keys(&mut e, ",");
6975 assert_eq!(e.cursor().1, 2);
6976 }
6977
6978 #[test]
6979 fn di_quote_deletes_content() {
6980 let mut e = editor_with("foo \"bar\" baz");
6981 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6983 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6984 }
6985
6986 #[test]
6987 fn da_quote_deletes_with_quotes() {
6988 let mut e = editor_with("foo \"bar\" baz");
6991 e.jump_cursor(0, 6);
6992 run_keys(&mut e, "da\"");
6993 assert_eq!(e.buffer().lines()[0], "foo baz");
6994 }
6995
6996 #[test]
6997 fn ci_paren_deletes_and_inserts() {
6998 let mut e = editor_with("fn(a, b, c)");
6999 e.jump_cursor(0, 5);
7000 run_keys(&mut e, "ci(");
7001 assert_eq!(e.vim_mode(), VimMode::Insert);
7002 assert_eq!(e.buffer().lines()[0], "fn()");
7003 }
7004
7005 #[test]
7006 fn diw_deletes_inner_word() {
7007 let mut e = editor_with("hello world");
7008 e.jump_cursor(0, 2);
7009 run_keys(&mut e, "diw");
7010 assert_eq!(e.buffer().lines()[0], " world");
7011 }
7012
7013 #[test]
7014 fn daw_deletes_word_with_trailing_space() {
7015 let mut e = editor_with("hello world");
7016 run_keys(&mut e, "daw");
7017 assert_eq!(e.buffer().lines()[0], "world");
7018 }
7019
7020 #[test]
7021 fn percent_jumps_to_matching_bracket() {
7022 let mut e = editor_with("foo(bar)");
7023 e.jump_cursor(0, 3);
7024 run_keys(&mut e, "%");
7025 assert_eq!(e.cursor().1, 7);
7026 run_keys(&mut e, "%");
7027 assert_eq!(e.cursor().1, 3);
7028 }
7029
7030 #[test]
7031 fn dot_repeats_last_change() {
7032 let mut e = editor_with("aaa bbb ccc");
7033 run_keys(&mut e, "dw");
7034 assert_eq!(e.buffer().lines()[0], "bbb ccc");
7035 run_keys(&mut e, ".");
7036 assert_eq!(e.buffer().lines()[0], "ccc");
7037 }
7038
7039 #[test]
7040 fn dot_repeats_change_operator_with_text() {
7041 let mut e = editor_with("foo foo foo");
7042 run_keys(&mut e, "cwbar<Esc>");
7043 assert_eq!(e.buffer().lines()[0], "bar foo foo");
7044 run_keys(&mut e, "w");
7046 run_keys(&mut e, ".");
7047 assert_eq!(e.buffer().lines()[0], "bar bar foo");
7048 }
7049
7050 #[test]
7051 fn dot_repeats_x() {
7052 let mut e = editor_with("abcdef");
7053 run_keys(&mut e, "x");
7054 run_keys(&mut e, "..");
7055 assert_eq!(e.buffer().lines()[0], "def");
7056 }
7057
7058 #[test]
7059 fn count_operator_motion_compose() {
7060 let mut e = editor_with("one two three four five");
7061 run_keys(&mut e, "d3w");
7062 assert_eq!(e.buffer().lines()[0], "four five");
7063 }
7064
7065 #[test]
7066 fn two_dd_deletes_two_lines() {
7067 let mut e = editor_with("a\nb\nc");
7068 run_keys(&mut e, "2dd");
7069 assert_eq!(e.buffer().lines().len(), 1);
7070 assert_eq!(e.buffer().lines()[0], "c");
7071 }
7072
7073 #[test]
7078 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
7079 let mut e = editor_with("one\ntwo\n three\nfour");
7080 e.jump_cursor(1, 2);
7081 run_keys(&mut e, "dd");
7082 assert_eq!(e.buffer().lines()[1], " three");
7084 assert_eq!(e.cursor(), (1, 4));
7085 }
7086
7087 #[test]
7088 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
7089 let mut e = editor_with("one\n two\nthree");
7090 e.jump_cursor(2, 0);
7091 run_keys(&mut e, "dd");
7092 assert_eq!(e.buffer().lines().len(), 2);
7094 assert_eq!(e.cursor(), (1, 2));
7095 }
7096
7097 #[test]
7098 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
7099 let mut e = editor_with("lonely");
7100 run_keys(&mut e, "dd");
7101 assert_eq!(e.buffer().lines().len(), 1);
7102 assert_eq!(e.buffer().lines()[0], "");
7103 assert_eq!(e.cursor(), (0, 0));
7104 }
7105
7106 #[test]
7107 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
7108 let mut e = editor_with("a\nb\nc\n d\ne");
7109 e.jump_cursor(1, 0);
7111 run_keys(&mut e, "3dd");
7112 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
7113 assert_eq!(e.cursor(), (1, 0));
7114 }
7115
7116 #[test]
7117 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
7118 let mut e = editor_with(" line one\n line two\n xyz!");
7137 e.jump_cursor(0, 8);
7139 assert_eq!(e.cursor(), (0, 8));
7140 run_keys(&mut e, "dd");
7143 assert_eq!(
7144 e.cursor(),
7145 (0, 4),
7146 "dd must place cursor on first-non-blank"
7147 );
7148 run_keys(&mut e, "j");
7152 let (row, col) = e.cursor();
7153 assert_eq!(row, 1);
7154 assert_eq!(
7155 col, 4,
7156 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
7157 );
7158 }
7159
7160 #[test]
7161 fn gu_lowercases_motion_range() {
7162 let mut e = editor_with("HELLO WORLD");
7163 run_keys(&mut e, "guw");
7164 assert_eq!(e.buffer().lines()[0], "hello WORLD");
7165 assert_eq!(e.cursor(), (0, 0));
7166 }
7167
7168 #[test]
7169 fn g_u_uppercases_text_object() {
7170 let mut e = editor_with("hello world");
7171 run_keys(&mut e, "gUiw");
7173 assert_eq!(e.buffer().lines()[0], "HELLO world");
7174 assert_eq!(e.cursor(), (0, 0));
7175 }
7176
7177 #[test]
7178 fn g_tilde_toggles_case_of_range() {
7179 let mut e = editor_with("Hello World");
7180 run_keys(&mut e, "g~iw");
7181 assert_eq!(e.buffer().lines()[0], "hELLO World");
7182 }
7183
7184 #[test]
7185 fn g_uu_uppercases_current_line() {
7186 let mut e = editor_with("select 1\nselect 2");
7187 run_keys(&mut e, "gUU");
7188 assert_eq!(e.buffer().lines()[0], "SELECT 1");
7189 assert_eq!(e.buffer().lines()[1], "select 2");
7190 }
7191
7192 #[test]
7193 fn gugu_lowercases_current_line() {
7194 let mut e = editor_with("FOO BAR\nBAZ");
7195 run_keys(&mut e, "gugu");
7196 assert_eq!(e.buffer().lines()[0], "foo bar");
7197 }
7198
7199 #[test]
7200 fn visual_u_uppercases_selection() {
7201 let mut e = editor_with("hello world");
7202 run_keys(&mut e, "veU");
7204 assert_eq!(e.buffer().lines()[0], "HELLO world");
7205 }
7206
7207 #[test]
7208 fn visual_line_u_lowercases_line() {
7209 let mut e = editor_with("HELLO WORLD\nOTHER");
7210 run_keys(&mut e, "Vu");
7211 assert_eq!(e.buffer().lines()[0], "hello world");
7212 assert_eq!(e.buffer().lines()[1], "OTHER");
7213 }
7214
7215 #[test]
7216 fn g_uu_with_count_uppercases_multiple_lines() {
7217 let mut e = editor_with("one\ntwo\nthree\nfour");
7218 run_keys(&mut e, "3gUU");
7220 assert_eq!(e.buffer().lines()[0], "ONE");
7221 assert_eq!(e.buffer().lines()[1], "TWO");
7222 assert_eq!(e.buffer().lines()[2], "THREE");
7223 assert_eq!(e.buffer().lines()[3], "four");
7224 }
7225
7226 #[test]
7227 fn double_gt_indents_current_line() {
7228 let mut e = editor_with("hello");
7229 run_keys(&mut e, ">>");
7230 assert_eq!(e.buffer().lines()[0], " hello");
7231 assert_eq!(e.cursor(), (0, 2));
7233 }
7234
7235 #[test]
7236 fn double_lt_outdents_current_line() {
7237 let mut e = editor_with(" hello");
7238 run_keys(&mut e, "<lt><lt>");
7239 assert_eq!(e.buffer().lines()[0], " hello");
7240 assert_eq!(e.cursor(), (0, 2));
7241 }
7242
7243 #[test]
7244 fn count_double_gt_indents_multiple_lines() {
7245 let mut e = editor_with("a\nb\nc\nd");
7246 run_keys(&mut e, "3>>");
7248 assert_eq!(e.buffer().lines()[0], " a");
7249 assert_eq!(e.buffer().lines()[1], " b");
7250 assert_eq!(e.buffer().lines()[2], " c");
7251 assert_eq!(e.buffer().lines()[3], "d");
7252 }
7253
7254 #[test]
7255 fn outdent_clips_ragged_leading_whitespace() {
7256 let mut e = editor_with(" x");
7259 run_keys(&mut e, "<lt><lt>");
7260 assert_eq!(e.buffer().lines()[0], "x");
7261 }
7262
7263 #[test]
7264 fn indent_motion_is_always_linewise() {
7265 let mut e = editor_with("foo bar");
7268 run_keys(&mut e, ">w");
7269 assert_eq!(e.buffer().lines()[0], " foo bar");
7270 }
7271
7272 #[test]
7273 fn indent_text_object_extends_over_paragraph() {
7274 let mut e = editor_with("a\nb\n\nc\nd");
7275 run_keys(&mut e, ">ap");
7277 assert_eq!(e.buffer().lines()[0], " a");
7278 assert_eq!(e.buffer().lines()[1], " b");
7279 assert_eq!(e.buffer().lines()[2], "");
7280 assert_eq!(e.buffer().lines()[3], "c");
7281 }
7282
7283 #[test]
7284 fn visual_line_indent_shifts_selected_rows() {
7285 let mut e = editor_with("x\ny\nz");
7286 run_keys(&mut e, "Vj>");
7288 assert_eq!(e.buffer().lines()[0], " x");
7289 assert_eq!(e.buffer().lines()[1], " y");
7290 assert_eq!(e.buffer().lines()[2], "z");
7291 }
7292
7293 #[test]
7294 fn outdent_empty_line_is_noop() {
7295 let mut e = editor_with("\nfoo");
7296 run_keys(&mut e, "<lt><lt>");
7297 assert_eq!(e.buffer().lines()[0], "");
7298 }
7299
7300 #[test]
7301 fn indent_skips_empty_lines() {
7302 let mut e = editor_with("");
7305 run_keys(&mut e, ">>");
7306 assert_eq!(e.buffer().lines()[0], "");
7307 }
7308
7309 #[test]
7310 fn insert_ctrl_t_indents_current_line() {
7311 let mut e = editor_with("x");
7312 run_keys(&mut e, "i<C-t>");
7314 assert_eq!(e.buffer().lines()[0], " x");
7315 assert_eq!(e.cursor(), (0, 2));
7318 }
7319
7320 #[test]
7321 fn insert_ctrl_d_outdents_current_line() {
7322 let mut e = editor_with(" x");
7323 run_keys(&mut e, "A<C-d>");
7325 assert_eq!(e.buffer().lines()[0], " x");
7326 }
7327
7328 #[test]
7329 fn h_at_col_zero_does_not_wrap_to_prev_line() {
7330 let mut e = editor_with("first\nsecond");
7331 e.jump_cursor(1, 0);
7332 run_keys(&mut e, "h");
7333 assert_eq!(e.cursor(), (1, 0));
7335 }
7336
7337 #[test]
7338 fn l_at_last_char_does_not_wrap_to_next_line() {
7339 let mut e = editor_with("ab\ncd");
7340 e.jump_cursor(0, 1);
7342 run_keys(&mut e, "l");
7343 assert_eq!(e.cursor(), (0, 1));
7345 }
7346
7347 #[test]
7348 fn count_l_clamps_at_line_end() {
7349 let mut e = editor_with("abcde");
7350 run_keys(&mut e, "20l");
7353 assert_eq!(e.cursor(), (0, 4));
7354 }
7355
7356 #[test]
7357 fn count_h_clamps_at_col_zero() {
7358 let mut e = editor_with("abcde");
7359 e.jump_cursor(0, 3);
7360 run_keys(&mut e, "20h");
7361 assert_eq!(e.cursor(), (0, 0));
7362 }
7363
7364 #[test]
7365 fn dl_on_last_char_still_deletes_it() {
7366 let mut e = editor_with("ab");
7370 e.jump_cursor(0, 1);
7371 run_keys(&mut e, "dl");
7372 assert_eq!(e.buffer().lines()[0], "a");
7373 }
7374
7375 #[test]
7376 fn case_op_preserves_yank_register() {
7377 let mut e = editor_with("target");
7378 run_keys(&mut e, "yy");
7379 let yank_before = e.yank().to_string();
7380 run_keys(&mut e, "gUU");
7382 assert_eq!(e.buffer().lines()[0], "TARGET");
7383 assert_eq!(
7384 e.yank(),
7385 yank_before,
7386 "case ops must preserve the yank buffer"
7387 );
7388 }
7389
7390 #[test]
7391 fn dap_deletes_paragraph() {
7392 let mut e = editor_with("a\nb\n\nc\nd");
7393 run_keys(&mut e, "dap");
7394 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
7395 }
7396
7397 #[test]
7398 fn dit_deletes_inner_tag_content() {
7399 let mut e = editor_with("<b>hello</b>");
7400 e.jump_cursor(0, 4);
7402 run_keys(&mut e, "dit");
7403 assert_eq!(e.buffer().lines()[0], "<b></b>");
7404 }
7405
7406 #[test]
7407 fn dat_deletes_around_tag() {
7408 let mut e = editor_with("hi <b>foo</b> bye");
7409 e.jump_cursor(0, 6);
7410 run_keys(&mut e, "dat");
7411 assert_eq!(e.buffer().lines()[0], "hi bye");
7412 }
7413
7414 #[test]
7415 fn dit_picks_innermost_tag() {
7416 let mut e = editor_with("<a><b>x</b></a>");
7417 e.jump_cursor(0, 6);
7419 run_keys(&mut e, "dit");
7420 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
7422 }
7423
7424 #[test]
7425 fn dat_innermost_tag_pair() {
7426 let mut e = editor_with("<a><b>x</b></a>");
7427 e.jump_cursor(0, 6);
7428 run_keys(&mut e, "dat");
7429 assert_eq!(e.buffer().lines()[0], "<a></a>");
7430 }
7431
7432 #[test]
7433 fn dit_outside_any_tag_no_op() {
7434 let mut e = editor_with("plain text");
7435 e.jump_cursor(0, 3);
7436 run_keys(&mut e, "dit");
7437 assert_eq!(e.buffer().lines()[0], "plain text");
7439 }
7440
7441 #[test]
7442 fn cit_changes_inner_tag_content() {
7443 let mut e = editor_with("<b>hello</b>");
7444 e.jump_cursor(0, 4);
7445 run_keys(&mut e, "citNEW<Esc>");
7446 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
7447 }
7448
7449 #[test]
7450 fn cat_changes_around_tag() {
7451 let mut e = editor_with("hi <b>foo</b> bye");
7452 e.jump_cursor(0, 6);
7453 run_keys(&mut e, "catBAR<Esc>");
7454 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
7455 }
7456
7457 #[test]
7458 fn yit_yanks_inner_tag_content() {
7459 let mut e = editor_with("<b>hello</b>");
7460 e.jump_cursor(0, 4);
7461 run_keys(&mut e, "yit");
7462 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7463 }
7464
7465 #[test]
7466 fn yat_yanks_full_tag_pair() {
7467 let mut e = editor_with("hi <b>foo</b> bye");
7468 e.jump_cursor(0, 6);
7469 run_keys(&mut e, "yat");
7470 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
7471 }
7472
7473 #[test]
7474 fn vit_visually_selects_inner_tag() {
7475 let mut e = editor_with("<b>hello</b>");
7476 e.jump_cursor(0, 4);
7477 run_keys(&mut e, "vit");
7478 assert_eq!(e.vim_mode(), VimMode::Visual);
7479 run_keys(&mut e, "y");
7480 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7481 }
7482
7483 #[test]
7484 fn vat_visually_selects_around_tag() {
7485 let mut e = editor_with("x<b>foo</b>y");
7486 e.jump_cursor(0, 5);
7487 run_keys(&mut e, "vat");
7488 assert_eq!(e.vim_mode(), VimMode::Visual);
7489 run_keys(&mut e, "y");
7490 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
7491 }
7492
7493 #[test]
7496 #[allow(non_snake_case)]
7497 fn diW_deletes_inner_big_word() {
7498 let mut e = editor_with("foo.bar baz");
7499 e.jump_cursor(0, 2);
7500 run_keys(&mut e, "diW");
7501 assert_eq!(e.buffer().lines()[0], " baz");
7503 }
7504
7505 #[test]
7506 #[allow(non_snake_case)]
7507 fn daW_deletes_around_big_word() {
7508 let mut e = editor_with("foo.bar baz");
7509 e.jump_cursor(0, 2);
7510 run_keys(&mut e, "daW");
7511 assert_eq!(e.buffer().lines()[0], "baz");
7512 }
7513
7514 #[test]
7515 fn di_double_quote_deletes_inside() {
7516 let mut e = editor_with("a \"hello\" b");
7517 e.jump_cursor(0, 4);
7518 run_keys(&mut e, "di\"");
7519 assert_eq!(e.buffer().lines()[0], "a \"\" b");
7520 }
7521
7522 #[test]
7523 fn da_double_quote_deletes_around() {
7524 let mut e = editor_with("a \"hello\" b");
7526 e.jump_cursor(0, 4);
7527 run_keys(&mut e, "da\"");
7528 assert_eq!(e.buffer().lines()[0], "a b");
7529 }
7530
7531 #[test]
7532 fn di_single_quote_deletes_inside() {
7533 let mut e = editor_with("x 'foo' y");
7534 e.jump_cursor(0, 4);
7535 run_keys(&mut e, "di'");
7536 assert_eq!(e.buffer().lines()[0], "x '' y");
7537 }
7538
7539 #[test]
7540 fn da_single_quote_deletes_around() {
7541 let mut e = editor_with("x 'foo' y");
7543 e.jump_cursor(0, 4);
7544 run_keys(&mut e, "da'");
7545 assert_eq!(e.buffer().lines()[0], "x y");
7546 }
7547
7548 #[test]
7549 fn di_backtick_deletes_inside() {
7550 let mut e = editor_with("p `q` r");
7551 e.jump_cursor(0, 3);
7552 run_keys(&mut e, "di`");
7553 assert_eq!(e.buffer().lines()[0], "p `` r");
7554 }
7555
7556 #[test]
7557 fn da_backtick_deletes_around() {
7558 let mut e = editor_with("p `q` r");
7560 e.jump_cursor(0, 3);
7561 run_keys(&mut e, "da`");
7562 assert_eq!(e.buffer().lines()[0], "p r");
7563 }
7564
7565 #[test]
7566 fn di_paren_deletes_inside() {
7567 let mut e = editor_with("f(arg)");
7568 e.jump_cursor(0, 3);
7569 run_keys(&mut e, "di(");
7570 assert_eq!(e.buffer().lines()[0], "f()");
7571 }
7572
7573 #[test]
7574 fn di_paren_alias_b_works() {
7575 let mut e = editor_with("f(arg)");
7576 e.jump_cursor(0, 3);
7577 run_keys(&mut e, "dib");
7578 assert_eq!(e.buffer().lines()[0], "f()");
7579 }
7580
7581 #[test]
7582 fn di_bracket_deletes_inside() {
7583 let mut e = editor_with("a[b,c]d");
7584 e.jump_cursor(0, 3);
7585 run_keys(&mut e, "di[");
7586 assert_eq!(e.buffer().lines()[0], "a[]d");
7587 }
7588
7589 #[test]
7590 fn da_bracket_deletes_around() {
7591 let mut e = editor_with("a[b,c]d");
7592 e.jump_cursor(0, 3);
7593 run_keys(&mut e, "da[");
7594 assert_eq!(e.buffer().lines()[0], "ad");
7595 }
7596
7597 #[test]
7598 fn di_brace_deletes_inside() {
7599 let mut e = editor_with("x{y}z");
7600 e.jump_cursor(0, 2);
7601 run_keys(&mut e, "di{");
7602 assert_eq!(e.buffer().lines()[0], "x{}z");
7603 }
7604
7605 #[test]
7606 fn da_brace_deletes_around() {
7607 let mut e = editor_with("x{y}z");
7608 e.jump_cursor(0, 2);
7609 run_keys(&mut e, "da{");
7610 assert_eq!(e.buffer().lines()[0], "xz");
7611 }
7612
7613 #[test]
7614 fn di_brace_alias_capital_b_works() {
7615 let mut e = editor_with("x{y}z");
7616 e.jump_cursor(0, 2);
7617 run_keys(&mut e, "diB");
7618 assert_eq!(e.buffer().lines()[0], "x{}z");
7619 }
7620
7621 #[test]
7622 fn di_angle_deletes_inside() {
7623 let mut e = editor_with("p<q>r");
7624 e.jump_cursor(0, 2);
7625 run_keys(&mut e, "di<lt>");
7627 assert_eq!(e.buffer().lines()[0], "p<>r");
7628 }
7629
7630 #[test]
7631 fn da_angle_deletes_around() {
7632 let mut e = editor_with("p<q>r");
7633 e.jump_cursor(0, 2);
7634 run_keys(&mut e, "da<lt>");
7635 assert_eq!(e.buffer().lines()[0], "pr");
7636 }
7637
7638 #[test]
7639 fn dip_deletes_inner_paragraph() {
7640 let mut e = editor_with("a\nb\nc\n\nd");
7641 e.jump_cursor(1, 0);
7642 run_keys(&mut e, "dip");
7643 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
7646 }
7647
7648 #[test]
7651 fn sentence_motion_close_paren_jumps_forward() {
7652 let mut e = editor_with("Alpha. Beta. Gamma.");
7653 e.jump_cursor(0, 0);
7654 run_keys(&mut e, ")");
7655 assert_eq!(e.cursor(), (0, 7));
7657 run_keys(&mut e, ")");
7658 assert_eq!(e.cursor(), (0, 13));
7659 }
7660
7661 #[test]
7662 fn sentence_motion_open_paren_jumps_backward() {
7663 let mut e = editor_with("Alpha. Beta. Gamma.");
7664 e.jump_cursor(0, 13);
7665 run_keys(&mut e, "(");
7666 assert_eq!(e.cursor(), (0, 7));
7669 run_keys(&mut e, "(");
7670 assert_eq!(e.cursor(), (0, 0));
7671 }
7672
7673 #[test]
7674 fn sentence_motion_count() {
7675 let mut e = editor_with("A. B. C. D.");
7676 e.jump_cursor(0, 0);
7677 run_keys(&mut e, "3)");
7678 assert_eq!(e.cursor(), (0, 9));
7680 }
7681
7682 #[test]
7683 fn dis_deletes_inner_sentence() {
7684 let mut e = editor_with("First one. Second one. Third one.");
7685 e.jump_cursor(0, 13);
7686 run_keys(&mut e, "dis");
7687 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
7689 }
7690
7691 #[test]
7692 fn das_deletes_around_sentence_with_trailing_space() {
7693 let mut e = editor_with("Alpha. Beta. Gamma.");
7694 e.jump_cursor(0, 8);
7695 run_keys(&mut e, "das");
7696 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
7699 }
7700
7701 #[test]
7702 fn dis_handles_double_terminator() {
7703 let mut e = editor_with("Wow!? Next.");
7704 e.jump_cursor(0, 1);
7705 run_keys(&mut e, "dis");
7706 assert_eq!(e.buffer().lines()[0], " Next.");
7709 }
7710
7711 #[test]
7712 fn dis_first_sentence_from_cursor_at_zero() {
7713 let mut e = editor_with("Alpha. Beta.");
7714 e.jump_cursor(0, 0);
7715 run_keys(&mut e, "dis");
7716 assert_eq!(e.buffer().lines()[0], " Beta.");
7717 }
7718
7719 #[test]
7720 fn yis_yanks_inner_sentence() {
7721 let mut e = editor_with("Hello world. Bye.");
7722 e.jump_cursor(0, 5);
7723 run_keys(&mut e, "yis");
7724 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
7725 }
7726
7727 #[test]
7728 fn vis_visually_selects_inner_sentence() {
7729 let mut e = editor_with("First. Second.");
7730 e.jump_cursor(0, 1);
7731 run_keys(&mut e, "vis");
7732 assert_eq!(e.vim_mode(), VimMode::Visual);
7733 run_keys(&mut e, "y");
7734 assert_eq!(e.registers().read('"').unwrap().text, "First.");
7735 }
7736
7737 #[test]
7738 fn ciw_changes_inner_word() {
7739 let mut e = editor_with("hello world");
7740 e.jump_cursor(0, 1);
7741 run_keys(&mut e, "ciwHEY<Esc>");
7742 assert_eq!(e.buffer().lines()[0], "HEY world");
7743 }
7744
7745 #[test]
7746 fn yiw_yanks_inner_word() {
7747 let mut e = editor_with("hello world");
7748 e.jump_cursor(0, 1);
7749 run_keys(&mut e, "yiw");
7750 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7751 }
7752
7753 #[test]
7754 fn viw_selects_inner_word() {
7755 let mut e = editor_with("hello world");
7756 e.jump_cursor(0, 2);
7757 run_keys(&mut e, "viw");
7758 assert_eq!(e.vim_mode(), VimMode::Visual);
7759 run_keys(&mut e, "y");
7760 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7761 }
7762
7763 #[test]
7764 fn ci_paren_changes_inside() {
7765 let mut e = editor_with("f(old)");
7766 e.jump_cursor(0, 3);
7767 run_keys(&mut e, "ci(NEW<Esc>");
7768 assert_eq!(e.buffer().lines()[0], "f(NEW)");
7769 }
7770
7771 #[test]
7772 fn yi_double_quote_yanks_inside() {
7773 let mut e = editor_with("say \"hi there\" then");
7774 e.jump_cursor(0, 6);
7775 run_keys(&mut e, "yi\"");
7776 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
7777 }
7778
7779 #[test]
7780 fn vap_visual_selects_around_paragraph() {
7781 let mut e = editor_with("a\nb\n\nc");
7782 e.jump_cursor(0, 0);
7783 run_keys(&mut e, "vap");
7784 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7785 run_keys(&mut e, "y");
7786 let text = e.registers().read('"').unwrap().text.clone();
7788 assert!(text.starts_with("a\nb"));
7789 }
7790
7791 #[test]
7792 fn star_finds_next_occurrence() {
7793 let mut e = editor_with("foo bar foo baz");
7794 run_keys(&mut e, "*");
7795 assert_eq!(e.cursor().1, 8);
7796 }
7797
7798 #[test]
7799 fn star_skips_substring_match() {
7800 let mut e = editor_with("foo foobar baz");
7803 run_keys(&mut e, "*");
7804 assert_eq!(e.cursor().1, 0);
7805 }
7806
7807 #[test]
7808 fn g_star_matches_substring() {
7809 let mut e = editor_with("foo foobar baz");
7812 run_keys(&mut e, "g*");
7813 assert_eq!(e.cursor().1, 4);
7814 }
7815
7816 #[test]
7817 fn g_pound_matches_substring_backward() {
7818 let mut e = editor_with("foo foobar baz foo");
7821 run_keys(&mut e, "$b");
7822 assert_eq!(e.cursor().1, 15);
7823 run_keys(&mut e, "g#");
7824 assert_eq!(e.cursor().1, 4);
7825 }
7826
7827 #[test]
7828 fn n_repeats_last_search_forward() {
7829 let mut e = editor_with("foo bar foo baz foo");
7830 run_keys(&mut e, "/foo<CR>");
7833 assert_eq!(e.cursor().1, 8);
7834 run_keys(&mut e, "n");
7835 assert_eq!(e.cursor().1, 16);
7836 }
7837
7838 #[test]
7839 fn shift_n_reverses_search() {
7840 let mut e = editor_with("foo bar foo baz foo");
7841 run_keys(&mut e, "/foo<CR>");
7842 run_keys(&mut e, "n");
7843 assert_eq!(e.cursor().1, 16);
7844 run_keys(&mut e, "N");
7845 assert_eq!(e.cursor().1, 8);
7846 }
7847
7848 #[test]
7849 fn n_noop_without_pattern() {
7850 let mut e = editor_with("foo bar");
7851 run_keys(&mut e, "n");
7852 assert_eq!(e.cursor(), (0, 0));
7853 }
7854
7855 #[test]
7856 fn visual_line_preserves_cursor_column() {
7857 let mut e = editor_with("hello world\nanother one\nbye");
7860 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7862 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7863 assert_eq!(e.cursor(), (0, 5));
7864 run_keys(&mut e, "j");
7865 assert_eq!(e.cursor(), (1, 5));
7866 }
7867
7868 #[test]
7869 fn visual_line_yank_includes_trailing_newline() {
7870 let mut e = editor_with("aaa\nbbb\nccc");
7871 run_keys(&mut e, "Vjy");
7872 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7874 }
7875
7876 #[test]
7877 fn visual_line_yank_last_line_trailing_newline() {
7878 let mut e = editor_with("aaa\nbbb\nccc");
7879 run_keys(&mut e, "jj");
7881 run_keys(&mut e, "Vy");
7882 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7883 }
7884
7885 #[test]
7886 fn yy_on_last_line_has_trailing_newline() {
7887 let mut e = editor_with("aaa\nbbb\nccc");
7888 run_keys(&mut e, "jj");
7889 run_keys(&mut e, "yy");
7890 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7891 }
7892
7893 #[test]
7894 fn yy_in_middle_has_trailing_newline() {
7895 let mut e = editor_with("aaa\nbbb\nccc");
7896 run_keys(&mut e, "j");
7897 run_keys(&mut e, "yy");
7898 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7899 }
7900
7901 #[test]
7902 fn di_single_quote() {
7903 let mut e = editor_with("say 'hello world' now");
7904 e.jump_cursor(0, 7);
7905 run_keys(&mut e, "di'");
7906 assert_eq!(e.buffer().lines()[0], "say '' now");
7907 }
7908
7909 #[test]
7910 fn da_single_quote() {
7911 let mut e = editor_with("say 'hello' now");
7913 e.jump_cursor(0, 7);
7914 run_keys(&mut e, "da'");
7915 assert_eq!(e.buffer().lines()[0], "say now");
7916 }
7917
7918 #[test]
7919 fn di_backtick() {
7920 let mut e = editor_with("say `hi` now");
7921 e.jump_cursor(0, 5);
7922 run_keys(&mut e, "di`");
7923 assert_eq!(e.buffer().lines()[0], "say `` now");
7924 }
7925
7926 #[test]
7927 fn di_brace() {
7928 let mut e = editor_with("fn { a; b; c }");
7929 e.jump_cursor(0, 7);
7930 run_keys(&mut e, "di{");
7931 assert_eq!(e.buffer().lines()[0], "fn {}");
7932 }
7933
7934 #[test]
7935 fn di_bracket() {
7936 let mut e = editor_with("arr[1, 2, 3]");
7937 e.jump_cursor(0, 5);
7938 run_keys(&mut e, "di[");
7939 assert_eq!(e.buffer().lines()[0], "arr[]");
7940 }
7941
7942 #[test]
7943 fn dab_deletes_around_paren() {
7944 let mut e = editor_with("fn(a, b) + 1");
7945 e.jump_cursor(0, 4);
7946 run_keys(&mut e, "dab");
7947 assert_eq!(e.buffer().lines()[0], "fn + 1");
7948 }
7949
7950 #[test]
7951 fn da_big_b_deletes_around_brace() {
7952 let mut e = editor_with("x = {a: 1}");
7953 e.jump_cursor(0, 6);
7954 run_keys(&mut e, "daB");
7955 assert_eq!(e.buffer().lines()[0], "x = ");
7956 }
7957
7958 #[test]
7959 fn di_big_w_deletes_bigword() {
7960 let mut e = editor_with("foo-bar baz");
7961 e.jump_cursor(0, 2);
7962 run_keys(&mut e, "diW");
7963 assert_eq!(e.buffer().lines()[0], " baz");
7964 }
7965
7966 #[test]
7967 fn visual_select_inner_word() {
7968 let mut e = editor_with("hello world");
7969 e.jump_cursor(0, 2);
7970 run_keys(&mut e, "viw");
7971 assert_eq!(e.vim_mode(), VimMode::Visual);
7972 run_keys(&mut e, "y");
7973 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7974 }
7975
7976 #[test]
7977 fn visual_select_inner_quote() {
7978 let mut e = editor_with("foo \"bar\" baz");
7979 e.jump_cursor(0, 6);
7980 run_keys(&mut e, "vi\"");
7981 run_keys(&mut e, "y");
7982 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7983 }
7984
7985 #[test]
7986 fn visual_select_inner_paren() {
7987 let mut e = editor_with("fn(a, b)");
7988 e.jump_cursor(0, 4);
7989 run_keys(&mut e, "vi(");
7990 run_keys(&mut e, "y");
7991 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7992 }
7993
7994 #[test]
7995 fn visual_select_outer_brace() {
7996 let mut e = editor_with("{x}");
7997 e.jump_cursor(0, 1);
7998 run_keys(&mut e, "va{");
7999 run_keys(&mut e, "y");
8000 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
8001 }
8002
8003 #[test]
8004 fn ci_paren_forward_scans_when_cursor_before_pair() {
8005 let mut e = editor_with("foo(bar)");
8008 e.jump_cursor(0, 0);
8009 run_keys(&mut e, "ci(NEW<Esc>");
8010 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
8011 }
8012
8013 #[test]
8014 fn ci_paren_forward_scans_across_lines() {
8015 let mut e = editor_with("first\nfoo(bar)\nlast");
8016 e.jump_cursor(0, 0);
8017 run_keys(&mut e, "ci(NEW<Esc>");
8018 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
8019 }
8020
8021 #[test]
8022 fn ci_brace_forward_scans_when_cursor_before_pair() {
8023 let mut e = editor_with("let x = {y};");
8024 e.jump_cursor(0, 0);
8025 run_keys(&mut e, "ci{NEW<Esc>");
8026 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
8027 }
8028
8029 #[test]
8030 fn cit_forward_scans_when_cursor_before_tag() {
8031 let mut e = editor_with("text <b>hello</b> rest");
8034 e.jump_cursor(0, 0);
8035 run_keys(&mut e, "citNEW<Esc>");
8036 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
8037 }
8038
8039 #[test]
8040 fn dat_forward_scans_when_cursor_before_tag() {
8041 let mut e = editor_with("text <b>hello</b> rest");
8043 e.jump_cursor(0, 0);
8044 run_keys(&mut e, "dat");
8045 assert_eq!(e.buffer().lines()[0], "text rest");
8046 }
8047
8048 #[test]
8049 fn ci_paren_still_works_when_cursor_inside() {
8050 let mut e = editor_with("fn(a, b)");
8053 e.jump_cursor(0, 4);
8054 run_keys(&mut e, "ci(NEW<Esc>");
8055 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
8056 }
8057
8058 #[test]
8059 fn caw_changes_word_with_trailing_space() {
8060 let mut e = editor_with("hello world");
8061 run_keys(&mut e, "cawfoo<Esc>");
8062 assert_eq!(e.buffer().lines()[0], "fooworld");
8063 }
8064
8065 #[test]
8066 fn visual_char_yank_preserves_raw_text() {
8067 let mut e = editor_with("hello world");
8068 run_keys(&mut e, "vllly");
8069 assert_eq!(e.last_yank.as_deref(), Some("hell"));
8070 }
8071
8072 #[test]
8073 fn single_line_visual_line_selects_full_line_on_yank() {
8074 let mut e = editor_with("hello world\nbye");
8075 run_keys(&mut e, "V");
8076 run_keys(&mut e, "y");
8079 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
8080 }
8081
8082 #[test]
8083 fn visual_line_extends_both_directions() {
8084 let mut e = editor_with("aaa\nbbb\nccc\nddd");
8085 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
8087 assert_eq!(e.cursor(), (3, 0));
8088 run_keys(&mut e, "k");
8089 assert_eq!(e.cursor(), (2, 0));
8091 run_keys(&mut e, "k");
8092 assert_eq!(e.cursor(), (1, 0));
8093 }
8094
8095 #[test]
8096 fn visual_char_preserves_cursor_column() {
8097 let mut e = editor_with("hello world");
8098 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
8100 assert_eq!(e.cursor(), (0, 5));
8101 run_keys(&mut e, "ll");
8102 assert_eq!(e.cursor(), (0, 7));
8103 }
8104
8105 #[test]
8106 fn visual_char_highlight_bounds_order() {
8107 let mut e = editor_with("abcdef");
8108 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
8110 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
8113 }
8114
8115 #[test]
8116 fn visual_line_highlight_bounds() {
8117 let mut e = editor_with("a\nb\nc");
8118 run_keys(&mut e, "V");
8119 assert_eq!(e.line_highlight(), Some((0, 0)));
8120 run_keys(&mut e, "j");
8121 assert_eq!(e.line_highlight(), Some((0, 1)));
8122 run_keys(&mut e, "j");
8123 assert_eq!(e.line_highlight(), Some((0, 2)));
8124 }
8125
8126 #[test]
8129 fn h_moves_left() {
8130 let mut e = editor_with("hello");
8131 e.jump_cursor(0, 3);
8132 run_keys(&mut e, "h");
8133 assert_eq!(e.cursor(), (0, 2));
8134 }
8135
8136 #[test]
8137 fn l_moves_right() {
8138 let mut e = editor_with("hello");
8139 run_keys(&mut e, "l");
8140 assert_eq!(e.cursor(), (0, 1));
8141 }
8142
8143 #[test]
8144 fn k_moves_up() {
8145 let mut e = editor_with("a\nb\nc");
8146 e.jump_cursor(2, 0);
8147 run_keys(&mut e, "k");
8148 assert_eq!(e.cursor(), (1, 0));
8149 }
8150
8151 #[test]
8152 fn zero_moves_to_line_start() {
8153 let mut e = editor_with(" hello");
8154 run_keys(&mut e, "$");
8155 run_keys(&mut e, "0");
8156 assert_eq!(e.cursor().1, 0);
8157 }
8158
8159 #[test]
8160 fn caret_moves_to_first_non_blank() {
8161 let mut e = editor_with(" hello");
8162 run_keys(&mut e, "0");
8163 run_keys(&mut e, "^");
8164 assert_eq!(e.cursor().1, 4);
8165 }
8166
8167 #[test]
8168 fn dollar_moves_to_last_char() {
8169 let mut e = editor_with("hello");
8170 run_keys(&mut e, "$");
8171 assert_eq!(e.cursor().1, 4);
8172 }
8173
8174 #[test]
8175 fn dollar_on_empty_line_stays_at_col_zero() {
8176 let mut e = editor_with("");
8177 run_keys(&mut e, "$");
8178 assert_eq!(e.cursor().1, 0);
8179 }
8180
8181 #[test]
8182 fn w_jumps_to_next_word() {
8183 let mut e = editor_with("foo bar baz");
8184 run_keys(&mut e, "w");
8185 assert_eq!(e.cursor().1, 4);
8186 }
8187
8188 #[test]
8189 fn b_jumps_back_a_word() {
8190 let mut e = editor_with("foo bar");
8191 e.jump_cursor(0, 6);
8192 run_keys(&mut e, "b");
8193 assert_eq!(e.cursor().1, 4);
8194 }
8195
8196 #[test]
8197 fn e_jumps_to_word_end() {
8198 let mut e = editor_with("foo bar");
8199 run_keys(&mut e, "e");
8200 assert_eq!(e.cursor().1, 2);
8201 }
8202
8203 #[test]
8206 fn d_dollar_deletes_to_eol() {
8207 let mut e = editor_with("hello world");
8208 e.jump_cursor(0, 5);
8209 run_keys(&mut e, "d$");
8210 assert_eq!(e.buffer().lines()[0], "hello");
8211 }
8212
8213 #[test]
8214 fn d_zero_deletes_to_line_start() {
8215 let mut e = editor_with("hello world");
8216 e.jump_cursor(0, 6);
8217 run_keys(&mut e, "d0");
8218 assert_eq!(e.buffer().lines()[0], "world");
8219 }
8220
8221 #[test]
8222 fn d_caret_deletes_to_first_non_blank() {
8223 let mut e = editor_with(" hello");
8224 e.jump_cursor(0, 6);
8225 run_keys(&mut e, "d^");
8226 assert_eq!(e.buffer().lines()[0], " llo");
8227 }
8228
8229 #[test]
8230 fn d_capital_g_deletes_to_end_of_file() {
8231 let mut e = editor_with("a\nb\nc\nd");
8232 e.jump_cursor(1, 0);
8233 run_keys(&mut e, "dG");
8234 assert_eq!(e.buffer().lines(), &["a".to_string()]);
8235 }
8236
8237 #[test]
8238 fn d_gg_deletes_to_start_of_file() {
8239 let mut e = editor_with("a\nb\nc\nd");
8240 e.jump_cursor(2, 0);
8241 run_keys(&mut e, "dgg");
8242 assert_eq!(e.buffer().lines(), &["d".to_string()]);
8243 }
8244
8245 #[test]
8246 fn cw_is_ce_quirk() {
8247 let mut e = editor_with("foo bar");
8250 run_keys(&mut e, "cwxyz<Esc>");
8251 assert_eq!(e.buffer().lines()[0], "xyz bar");
8252 }
8253
8254 #[test]
8257 fn big_d_deletes_to_eol() {
8258 let mut e = editor_with("hello world");
8259 e.jump_cursor(0, 5);
8260 run_keys(&mut e, "D");
8261 assert_eq!(e.buffer().lines()[0], "hello");
8262 }
8263
8264 #[test]
8265 fn big_c_deletes_to_eol_and_inserts() {
8266 let mut e = editor_with("hello world");
8267 e.jump_cursor(0, 5);
8268 run_keys(&mut e, "C!<Esc>");
8269 assert_eq!(e.buffer().lines()[0], "hello!");
8270 }
8271
8272 #[test]
8273 fn j_joins_next_line_with_space() {
8274 let mut e = editor_with("hello\nworld");
8275 run_keys(&mut e, "J");
8276 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8277 }
8278
8279 #[test]
8280 fn j_strips_leading_whitespace_on_join() {
8281 let mut e = editor_with("hello\n world");
8282 run_keys(&mut e, "J");
8283 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8284 }
8285
8286 #[test]
8287 fn big_x_deletes_char_before_cursor() {
8288 let mut e = editor_with("hello");
8289 e.jump_cursor(0, 3);
8290 run_keys(&mut e, "X");
8291 assert_eq!(e.buffer().lines()[0], "helo");
8292 }
8293
8294 #[test]
8295 fn s_substitutes_char_and_enters_insert() {
8296 let mut e = editor_with("hello");
8297 run_keys(&mut e, "sX<Esc>");
8298 assert_eq!(e.buffer().lines()[0], "Xello");
8299 }
8300
8301 #[test]
8302 fn count_x_deletes_many() {
8303 let mut e = editor_with("abcdef");
8304 run_keys(&mut e, "3x");
8305 assert_eq!(e.buffer().lines()[0], "def");
8306 }
8307
8308 #[test]
8311 fn p_pastes_charwise_after_cursor() {
8312 let mut e = editor_with("hello");
8313 run_keys(&mut e, "yw");
8314 run_keys(&mut e, "$p");
8315 assert_eq!(e.buffer().lines()[0], "hellohello");
8316 }
8317
8318 #[test]
8319 fn capital_p_pastes_charwise_before_cursor() {
8320 let mut e = editor_with("hello");
8321 run_keys(&mut e, "v");
8323 run_keys(&mut e, "l");
8324 run_keys(&mut e, "y");
8325 run_keys(&mut e, "$P");
8326 assert_eq!(e.buffer().lines()[0], "hellheo");
8329 }
8330
8331 #[test]
8332 fn p_pastes_linewise_below() {
8333 let mut e = editor_with("one\ntwo\nthree");
8334 run_keys(&mut e, "yy");
8335 run_keys(&mut e, "p");
8336 assert_eq!(
8337 e.buffer().lines(),
8338 &[
8339 "one".to_string(),
8340 "one".to_string(),
8341 "two".to_string(),
8342 "three".to_string()
8343 ]
8344 );
8345 }
8346
8347 #[test]
8348 fn capital_p_pastes_linewise_above() {
8349 let mut e = editor_with("one\ntwo");
8350 e.jump_cursor(1, 0);
8351 run_keys(&mut e, "yy");
8352 run_keys(&mut e, "P");
8353 assert_eq!(
8354 e.buffer().lines(),
8355 &["one".to_string(), "two".to_string(), "two".to_string()]
8356 );
8357 }
8358
8359 #[test]
8362 fn hash_finds_previous_occurrence() {
8363 let mut e = editor_with("foo bar foo baz foo");
8364 e.jump_cursor(0, 16);
8366 run_keys(&mut e, "#");
8367 assert_eq!(e.cursor().1, 8);
8368 }
8369
8370 #[test]
8373 fn visual_line_delete_removes_full_lines() {
8374 let mut e = editor_with("a\nb\nc\nd");
8375 run_keys(&mut e, "Vjd");
8376 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
8377 }
8378
8379 #[test]
8380 fn visual_line_change_leaves_blank_line() {
8381 let mut e = editor_with("a\nb\nc");
8382 run_keys(&mut e, "Vjc");
8383 assert_eq!(e.vim_mode(), VimMode::Insert);
8384 run_keys(&mut e, "X<Esc>");
8385 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
8389 }
8390
8391 #[test]
8392 fn cc_leaves_blank_line() {
8393 let mut e = editor_with("a\nb\nc");
8394 e.jump_cursor(1, 0);
8395 run_keys(&mut e, "ccX<Esc>");
8396 assert_eq!(
8397 e.buffer().lines(),
8398 &["a".to_string(), "X".to_string(), "c".to_string()]
8399 );
8400 }
8401
8402 #[test]
8407 fn big_w_skips_hyphens() {
8408 let mut e = editor_with("foo-bar baz");
8410 run_keys(&mut e, "W");
8411 assert_eq!(e.cursor().1, 8);
8412 }
8413
8414 #[test]
8415 fn big_w_crosses_lines() {
8416 let mut e = editor_with("foo-bar\nbaz-qux");
8417 run_keys(&mut e, "W");
8418 assert_eq!(e.cursor(), (1, 0));
8419 }
8420
8421 #[test]
8422 fn big_b_skips_hyphens() {
8423 let mut e = editor_with("foo-bar baz");
8424 e.jump_cursor(0, 9);
8425 run_keys(&mut e, "B");
8426 assert_eq!(e.cursor().1, 8);
8427 run_keys(&mut e, "B");
8428 assert_eq!(e.cursor().1, 0);
8429 }
8430
8431 #[test]
8432 fn big_e_jumps_to_big_word_end() {
8433 let mut e = editor_with("foo-bar baz");
8434 run_keys(&mut e, "E");
8435 assert_eq!(e.cursor().1, 6);
8436 run_keys(&mut e, "E");
8437 assert_eq!(e.cursor().1, 10);
8438 }
8439
8440 #[test]
8441 fn dw_with_big_word_variant() {
8442 let mut e = editor_with("foo-bar baz");
8444 run_keys(&mut e, "dW");
8445 assert_eq!(e.buffer().lines()[0], "baz");
8446 }
8447
8448 #[test]
8451 fn insert_ctrl_w_deletes_word_back() {
8452 let mut e = editor_with("");
8453 run_keys(&mut e, "i");
8454 for c in "hello world".chars() {
8455 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
8456 }
8457 run_keys(&mut e, "<C-w>");
8458 assert_eq!(e.buffer().lines()[0], "hello ");
8459 }
8460
8461 #[test]
8462 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
8463 let mut e = editor_with("hello\nworld");
8467 e.jump_cursor(1, 0);
8468 run_keys(&mut e, "i");
8469 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
8470 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
8473 assert_eq!(e.cursor(), (0, 0));
8474 }
8475
8476 #[test]
8477 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
8478 let mut e = editor_with("foo bar\nbaz");
8479 e.jump_cursor(1, 0);
8480 run_keys(&mut e, "i");
8481 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
8482 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
8484 assert_eq!(e.cursor(), (0, 4));
8485 }
8486
8487 #[test]
8488 fn insert_ctrl_u_deletes_to_line_start() {
8489 let mut e = editor_with("");
8490 run_keys(&mut e, "i");
8491 for c in "hello world".chars() {
8492 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
8493 }
8494 run_keys(&mut e, "<C-u>");
8495 assert_eq!(e.buffer().lines()[0], "");
8496 }
8497
8498 #[test]
8499 fn insert_ctrl_o_runs_one_normal_command() {
8500 let mut e = editor_with("hello world");
8501 run_keys(&mut e, "A");
8503 assert_eq!(e.vim_mode(), VimMode::Insert);
8504 e.jump_cursor(0, 0);
8506 run_keys(&mut e, "<C-o>");
8507 assert_eq!(e.vim_mode(), VimMode::Normal);
8508 run_keys(&mut e, "dw");
8509 assert_eq!(e.vim_mode(), VimMode::Insert);
8511 assert_eq!(e.buffer().lines()[0], "world");
8512 }
8513
8514 #[test]
8517 fn j_through_empty_line_preserves_column() {
8518 let mut e = editor_with("hello world\n\nanother line");
8519 run_keys(&mut e, "llllll");
8521 assert_eq!(e.cursor(), (0, 6));
8522 run_keys(&mut e, "j");
8525 assert_eq!(e.cursor(), (1, 0));
8526 run_keys(&mut e, "j");
8528 assert_eq!(e.cursor(), (2, 6));
8529 }
8530
8531 #[test]
8532 fn j_through_shorter_line_preserves_column() {
8533 let mut e = editor_with("hello world\nhi\nanother line");
8534 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
8537 run_keys(&mut e, "j");
8538 assert_eq!(e.cursor(), (2, 7));
8539 }
8540
8541 #[test]
8542 fn esc_from_insert_sticky_matches_visible_cursor() {
8543 let mut e = editor_with(" this is a line\n another one of a similar size");
8547 e.jump_cursor(0, 12);
8548 run_keys(&mut e, "I");
8549 assert_eq!(e.cursor(), (0, 4));
8550 run_keys(&mut e, "X<Esc>");
8551 assert_eq!(e.cursor(), (0, 4));
8552 run_keys(&mut e, "j");
8553 assert_eq!(e.cursor(), (1, 4));
8554 }
8555
8556 #[test]
8557 fn esc_from_insert_sticky_tracks_inserted_chars() {
8558 let mut e = editor_with("xxxxxxx\nyyyyyyy");
8559 run_keys(&mut e, "i");
8560 run_keys(&mut e, "abc<Esc>");
8561 assert_eq!(e.cursor(), (0, 2));
8562 run_keys(&mut e, "j");
8563 assert_eq!(e.cursor(), (1, 2));
8564 }
8565
8566 #[test]
8567 fn esc_from_insert_sticky_tracks_arrow_nav() {
8568 let mut e = editor_with("xxxxxx\nyyyyyy");
8569 run_keys(&mut e, "i");
8570 run_keys(&mut e, "abc");
8571 for _ in 0..2 {
8572 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
8573 }
8574 run_keys(&mut e, "<Esc>");
8575 assert_eq!(e.cursor(), (0, 0));
8576 run_keys(&mut e, "j");
8577 assert_eq!(e.cursor(), (1, 0));
8578 }
8579
8580 #[test]
8581 fn esc_from_insert_at_col_14_followed_by_j() {
8582 let line = "x".repeat(30);
8585 let buf = format!("{line}\n{line}");
8586 let mut e = editor_with(&buf);
8587 e.jump_cursor(0, 14);
8588 run_keys(&mut e, "i");
8589 for c in "test ".chars() {
8590 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
8591 }
8592 run_keys(&mut e, "<Esc>");
8593 assert_eq!(e.cursor(), (0, 18));
8594 run_keys(&mut e, "j");
8595 assert_eq!(e.cursor(), (1, 18));
8596 }
8597
8598 #[test]
8599 fn linewise_paste_resets_sticky_column() {
8600 let mut e = editor_with(" hello\naaaaaaaa\nbye");
8604 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
8606 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
8610 run_keys(&mut e, "j");
8612 assert_eq!(e.cursor(), (3, 2));
8613 }
8614
8615 #[test]
8616 fn horizontal_motion_resyncs_sticky_column() {
8617 let mut e = editor_with("hello world\n\nanother line");
8621 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
8624 assert_eq!(e.cursor(), (2, 3));
8625 }
8626
8627 #[test]
8630 fn ctrl_v_enters_visual_block() {
8631 let mut e = editor_with("aaa\nbbb\nccc");
8632 run_keys(&mut e, "<C-v>");
8633 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
8634 }
8635
8636 #[test]
8637 fn visual_block_esc_returns_to_normal() {
8638 let mut e = editor_with("aaa\nbbb\nccc");
8639 run_keys(&mut e, "<C-v>");
8640 run_keys(&mut e, "<Esc>");
8641 assert_eq!(e.vim_mode(), VimMode::Normal);
8642 }
8643
8644 #[test]
8645 fn backtick_lt_jumps_to_visual_start_mark() {
8646 let mut e = editor_with("foo bar baz\n");
8650 run_keys(&mut e, "v");
8651 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
8654 run_keys(&mut e, "`<lt>");
8656 assert_eq!(e.cursor(), (0, 0));
8657 }
8658
8659 #[test]
8660 fn backtick_gt_jumps_to_visual_end_mark() {
8661 let mut e = editor_with("foo bar baz\n");
8662 run_keys(&mut e, "v");
8663 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
8665 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
8667 assert_eq!(e.cursor(), (0, 4));
8668 }
8669
8670 #[test]
8671 fn visual_exit_sets_lt_gt_marks() {
8672 let mut e = editor_with("aaa\nbbb\nccc\nddd");
8675 run_keys(&mut e, "V");
8677 run_keys(&mut e, "j");
8678 run_keys(&mut e, "<Esc>");
8679 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
8680 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
8681 assert_eq!(lt.0, 0, "'< row should be the lower bound");
8682 assert_eq!(gt.0, 1, "'> row should be the upper bound");
8683 }
8684
8685 #[test]
8686 fn visual_exit_marks_use_lower_higher_order() {
8687 let mut e = editor_with("aaa\nbbb\nccc\nddd");
8691 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
8693 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
8695 let lt = e.mark('<').unwrap();
8696 let gt = e.mark('>').unwrap();
8697 assert_eq!(lt.0, 2);
8698 assert_eq!(gt.0, 3);
8699 }
8700
8701 #[test]
8702 fn visualline_exit_marks_snap_to_line_edges() {
8703 let mut e = editor_with("aaaaa\nbbbbb\ncc");
8705 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
8707 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
8709 let lt = e.mark('<').unwrap();
8710 let gt = e.mark('>').unwrap();
8711 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
8712 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
8714 }
8715
8716 #[test]
8717 fn visualblock_exit_marks_use_block_corners() {
8718 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
8722 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
8724 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
8727 let lt = e.mark('<').unwrap();
8728 let gt = e.mark('>').unwrap();
8729 assert_eq!(lt, (0, 2), "'< should be top-left corner");
8731 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
8732 }
8733
8734 #[test]
8735 fn visual_block_delete_removes_column_range() {
8736 let mut e = editor_with("hello\nworld\nhappy");
8737 run_keys(&mut e, "l");
8739 run_keys(&mut e, "<C-v>");
8740 run_keys(&mut e, "jj");
8741 run_keys(&mut e, "ll");
8742 run_keys(&mut e, "d");
8743 assert_eq!(
8745 e.buffer().lines(),
8746 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
8747 );
8748 }
8749
8750 #[test]
8751 fn visual_block_yank_joins_with_newlines() {
8752 let mut e = editor_with("hello\nworld\nhappy");
8753 run_keys(&mut e, "<C-v>");
8754 run_keys(&mut e, "jj");
8755 run_keys(&mut e, "ll");
8756 run_keys(&mut e, "y");
8757 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
8758 }
8759
8760 #[test]
8761 fn visual_block_replace_fills_block() {
8762 let mut e = editor_with("hello\nworld\nhappy");
8763 run_keys(&mut e, "<C-v>");
8764 run_keys(&mut e, "jj");
8765 run_keys(&mut e, "ll");
8766 run_keys(&mut e, "rx");
8767 assert_eq!(
8768 e.buffer().lines(),
8769 &[
8770 "xxxlo".to_string(),
8771 "xxxld".to_string(),
8772 "xxxpy".to_string()
8773 ]
8774 );
8775 }
8776
8777 #[test]
8778 fn visual_block_insert_repeats_across_rows() {
8779 let mut e = editor_with("hello\nworld\nhappy");
8780 run_keys(&mut e, "<C-v>");
8781 run_keys(&mut e, "jj");
8782 run_keys(&mut e, "I");
8783 run_keys(&mut e, "# <Esc>");
8784 assert_eq!(
8785 e.buffer().lines(),
8786 &[
8787 "# hello".to_string(),
8788 "# world".to_string(),
8789 "# happy".to_string()
8790 ]
8791 );
8792 }
8793
8794 #[test]
8795 fn block_highlight_returns_none_outside_block_mode() {
8796 let mut e = editor_with("abc");
8797 assert!(e.block_highlight().is_none());
8798 run_keys(&mut e, "v");
8799 assert!(e.block_highlight().is_none());
8800 run_keys(&mut e, "<Esc>V");
8801 assert!(e.block_highlight().is_none());
8802 }
8803
8804 #[test]
8805 fn block_highlight_bounds_track_anchor_and_cursor() {
8806 let mut e = editor_with("aaaa\nbbbb\ncccc");
8807 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8809 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8812 }
8813
8814 #[test]
8815 fn visual_block_delete_handles_short_lines() {
8816 let mut e = editor_with("hello\nhi\nworld");
8818 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8820 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8822 assert_eq!(
8827 e.buffer().lines(),
8828 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8829 );
8830 }
8831
8832 #[test]
8833 fn visual_block_yank_pads_short_lines_with_empties() {
8834 let mut e = editor_with("hello\nhi\nworld");
8835 run_keys(&mut e, "l");
8836 run_keys(&mut e, "<C-v>");
8837 run_keys(&mut e, "jjll");
8838 run_keys(&mut e, "y");
8839 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8841 }
8842
8843 #[test]
8844 fn visual_block_replace_skips_past_eol() {
8845 let mut e = editor_with("ab\ncd\nef");
8848 run_keys(&mut e, "l");
8850 run_keys(&mut e, "<C-v>");
8851 run_keys(&mut e, "jjllllll");
8852 run_keys(&mut e, "rX");
8853 assert_eq!(
8856 e.buffer().lines(),
8857 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8858 );
8859 }
8860
8861 #[test]
8862 fn visual_block_with_empty_line_in_middle() {
8863 let mut e = editor_with("abcd\n\nefgh");
8864 run_keys(&mut e, "<C-v>");
8865 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8867 assert_eq!(
8870 e.buffer().lines(),
8871 &["d".to_string(), "".to_string(), "h".to_string()]
8872 );
8873 }
8874
8875 #[test]
8876 fn block_insert_pads_empty_lines_to_block_column() {
8877 let mut e = editor_with("this is a line\n\nthis is a line");
8880 e.jump_cursor(0, 3);
8881 run_keys(&mut e, "<C-v>");
8882 run_keys(&mut e, "jj");
8883 run_keys(&mut e, "I");
8884 run_keys(&mut e, "XX<Esc>");
8885 assert_eq!(
8886 e.buffer().lines(),
8887 &[
8888 "thiXXs is a line".to_string(),
8889 " XX".to_string(),
8890 "thiXXs is a line".to_string()
8891 ]
8892 );
8893 }
8894
8895 #[test]
8896 fn block_insert_pads_short_lines_to_block_column() {
8897 let mut e = editor_with("aaaaa\nbb\naaaaa");
8898 e.jump_cursor(0, 3);
8899 run_keys(&mut e, "<C-v>");
8900 run_keys(&mut e, "jj");
8901 run_keys(&mut e, "I");
8902 run_keys(&mut e, "Y<Esc>");
8903 assert_eq!(
8905 e.buffer().lines(),
8906 &[
8907 "aaaYaa".to_string(),
8908 "bb Y".to_string(),
8909 "aaaYaa".to_string()
8910 ]
8911 );
8912 }
8913
8914 #[test]
8915 fn visual_block_append_repeats_across_rows() {
8916 let mut e = editor_with("foo\nbar\nbaz");
8917 run_keys(&mut e, "<C-v>");
8918 run_keys(&mut e, "jj");
8919 run_keys(&mut e, "A");
8922 run_keys(&mut e, "!<Esc>");
8923 assert_eq!(
8924 e.buffer().lines(),
8925 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8926 );
8927 }
8928
8929 #[test]
8932 fn slash_opens_forward_search_prompt() {
8933 let mut e = editor_with("hello world");
8934 run_keys(&mut e, "/");
8935 let p = e.search_prompt().expect("prompt should be active");
8936 assert!(p.text.is_empty());
8937 assert!(p.forward);
8938 }
8939
8940 #[test]
8941 fn question_opens_backward_search_prompt() {
8942 let mut e = editor_with("hello world");
8943 run_keys(&mut e, "?");
8944 let p = e.search_prompt().expect("prompt should be active");
8945 assert!(!p.forward);
8946 }
8947
8948 #[test]
8949 fn search_prompt_typing_updates_pattern_live() {
8950 let mut e = editor_with("foo bar\nbaz");
8951 run_keys(&mut e, "/bar");
8952 assert_eq!(e.search_prompt().unwrap().text, "bar");
8953 assert!(e.search_state().pattern.is_some());
8955 }
8956
8957 #[test]
8958 fn search_prompt_backspace_and_enter() {
8959 let mut e = editor_with("hello world\nagain");
8960 run_keys(&mut e, "/worlx");
8961 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8962 assert_eq!(e.search_prompt().unwrap().text, "worl");
8963 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8964 assert!(e.search_prompt().is_none());
8966 assert_eq!(e.last_search(), Some("worl"));
8967 assert_eq!(e.cursor(), (0, 6));
8968 }
8969
8970 #[test]
8971 fn empty_search_prompt_enter_repeats_last_search() {
8972 let mut e = editor_with("foo bar foo baz foo");
8973 run_keys(&mut e, "/foo");
8974 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8975 assert_eq!(e.cursor().1, 8);
8976 run_keys(&mut e, "/");
8978 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8979 assert_eq!(e.cursor().1, 16);
8980 assert_eq!(e.last_search(), Some("foo"));
8981 }
8982
8983 #[test]
8984 fn search_history_records_committed_patterns() {
8985 let mut e = editor_with("alpha beta gamma");
8986 run_keys(&mut e, "/alpha<CR>");
8987 run_keys(&mut e, "/beta<CR>");
8988 let history = e.vim.search_history.clone();
8990 assert_eq!(history, vec!["alpha", "beta"]);
8991 }
8992
8993 #[test]
8994 fn search_history_dedupes_consecutive_repeats() {
8995 let mut e = editor_with("foo bar foo");
8996 run_keys(&mut e, "/foo<CR>");
8997 run_keys(&mut e, "/foo<CR>");
8998 run_keys(&mut e, "/bar<CR>");
8999 run_keys(&mut e, "/bar<CR>");
9000 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
9002 }
9003
9004 #[test]
9005 fn ctrl_p_walks_history_backward() {
9006 let mut e = editor_with("alpha beta gamma");
9007 run_keys(&mut e, "/alpha<CR>");
9008 run_keys(&mut e, "/beta<CR>");
9009 run_keys(&mut e, "/");
9011 assert_eq!(e.search_prompt().unwrap().text, "");
9012 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
9013 assert_eq!(e.search_prompt().unwrap().text, "beta");
9014 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
9015 assert_eq!(e.search_prompt().unwrap().text, "alpha");
9016 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
9018 assert_eq!(e.search_prompt().unwrap().text, "alpha");
9019 }
9020
9021 #[test]
9022 fn ctrl_n_walks_history_forward_after_ctrl_p() {
9023 let mut e = editor_with("a b c");
9024 run_keys(&mut e, "/a<CR>");
9025 run_keys(&mut e, "/b<CR>");
9026 run_keys(&mut e, "/c<CR>");
9027 run_keys(&mut e, "/");
9028 for _ in 0..3 {
9030 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
9031 }
9032 assert_eq!(e.search_prompt().unwrap().text, "a");
9033 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
9034 assert_eq!(e.search_prompt().unwrap().text, "b");
9035 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
9036 assert_eq!(e.search_prompt().unwrap().text, "c");
9037 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
9039 assert_eq!(e.search_prompt().unwrap().text, "c");
9040 }
9041
9042 #[test]
9043 fn typing_after_history_walk_resets_cursor() {
9044 let mut e = editor_with("foo");
9045 run_keys(&mut e, "/foo<CR>");
9046 run_keys(&mut e, "/");
9047 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
9048 assert_eq!(e.search_prompt().unwrap().text, "foo");
9049 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
9052 assert_eq!(e.search_prompt().unwrap().text, "foox");
9053 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
9054 assert_eq!(e.search_prompt().unwrap().text, "foo");
9055 }
9056
9057 #[test]
9058 fn empty_backward_search_prompt_enter_repeats_last_search() {
9059 let mut e = editor_with("foo bar foo baz foo");
9060 run_keys(&mut e, "/foo");
9062 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
9063 assert_eq!(e.cursor().1, 8);
9064 run_keys(&mut e, "?");
9065 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
9066 assert_eq!(e.cursor().1, 0);
9067 assert_eq!(e.last_search(), Some("foo"));
9068 }
9069
9070 #[test]
9071 fn search_prompt_esc_cancels_but_keeps_last_search() {
9072 let mut e = editor_with("foo bar\nbaz");
9073 run_keys(&mut e, "/bar");
9074 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
9075 assert!(e.search_prompt().is_none());
9076 assert_eq!(e.last_search(), Some("bar"));
9077 }
9078
9079 #[test]
9080 fn search_then_n_and_shift_n_navigate() {
9081 let mut e = editor_with("foo bar foo baz foo");
9082 run_keys(&mut e, "/foo");
9083 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
9084 assert_eq!(e.cursor().1, 8);
9086 run_keys(&mut e, "n");
9087 assert_eq!(e.cursor().1, 16);
9088 run_keys(&mut e, "N");
9089 assert_eq!(e.cursor().1, 8);
9090 }
9091
9092 #[test]
9093 fn question_mark_searches_backward_on_enter() {
9094 let mut e = editor_with("foo bar foo baz");
9095 e.jump_cursor(0, 10);
9096 run_keys(&mut e, "?foo");
9097 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
9098 assert_eq!(e.cursor(), (0, 8));
9100 }
9101
9102 #[test]
9105 fn big_y_yanks_to_end_of_line() {
9106 let mut e = editor_with("hello world");
9107 e.jump_cursor(0, 6);
9108 run_keys(&mut e, "Y");
9109 assert_eq!(e.last_yank.as_deref(), Some("world"));
9110 }
9111
9112 #[test]
9113 fn big_y_from_line_start_yanks_full_line() {
9114 let mut e = editor_with("hello world");
9115 run_keys(&mut e, "Y");
9116 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
9117 }
9118
9119 #[test]
9120 fn gj_joins_without_inserting_space() {
9121 let mut e = editor_with("hello\n world");
9122 run_keys(&mut e, "gJ");
9123 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
9125 }
9126
9127 #[test]
9128 fn gj_noop_on_last_line() {
9129 let mut e = editor_with("only");
9130 run_keys(&mut e, "gJ");
9131 assert_eq!(e.buffer().lines(), &["only".to_string()]);
9132 }
9133
9134 #[test]
9135 fn ge_jumps_to_previous_word_end() {
9136 let mut e = editor_with("foo bar baz");
9137 e.jump_cursor(0, 5);
9138 run_keys(&mut e, "ge");
9139 assert_eq!(e.cursor(), (0, 2));
9140 }
9141
9142 #[test]
9143 fn ge_respects_word_class() {
9144 let mut e = editor_with("foo-bar baz");
9147 e.jump_cursor(0, 5);
9148 run_keys(&mut e, "ge");
9149 assert_eq!(e.cursor(), (0, 3));
9150 }
9151
9152 #[test]
9153 fn big_ge_treats_hyphens_as_part_of_word() {
9154 let mut e = editor_with("foo-bar baz");
9157 e.jump_cursor(0, 10);
9158 run_keys(&mut e, "gE");
9159 assert_eq!(e.cursor(), (0, 6));
9160 }
9161
9162 #[test]
9163 fn ge_crosses_line_boundary() {
9164 let mut e = editor_with("foo\nbar");
9165 e.jump_cursor(1, 0);
9166 run_keys(&mut e, "ge");
9167 assert_eq!(e.cursor(), (0, 2));
9168 }
9169
9170 #[test]
9171 fn dge_deletes_to_end_of_previous_word() {
9172 let mut e = editor_with("foo bar baz");
9173 e.jump_cursor(0, 8);
9174 run_keys(&mut e, "dge");
9177 assert_eq!(e.buffer().lines()[0], "foo baaz");
9178 }
9179
9180 #[test]
9181 fn ctrl_scroll_keys_do_not_panic() {
9182 let mut e = editor_with(
9185 (0..50)
9186 .map(|i| format!("line{i}"))
9187 .collect::<Vec<_>>()
9188 .join("\n")
9189 .as_str(),
9190 );
9191 run_keys(&mut e, "<C-f>");
9192 run_keys(&mut e, "<C-b>");
9193 assert!(!e.buffer().lines().is_empty());
9195 }
9196
9197 #[test]
9204 fn count_insert_with_arrow_nav_does_not_leak_rows() {
9205 let mut e = Editor::new(
9206 hjkl_buffer::Buffer::new(),
9207 crate::types::DefaultHost::new(),
9208 crate::types::Options::default(),
9209 );
9210 e.set_content("row0\nrow1\nrow2");
9211 run_keys(&mut e, "3iX<Down><Esc>");
9213 assert!(e.buffer().lines()[0].contains('X'));
9215 assert!(
9218 !e.buffer().lines()[1].contains("row0"),
9219 "row1 leaked row0 contents: {:?}",
9220 e.buffer().lines()[1]
9221 );
9222 assert_eq!(e.buffer().lines().len(), 3);
9225 }
9226
9227 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
9230 let mut e = Editor::new(
9231 hjkl_buffer::Buffer::new(),
9232 crate::types::DefaultHost::new(),
9233 crate::types::Options::default(),
9234 );
9235 let body = (0..n)
9236 .map(|i| format!(" line{}", i))
9237 .collect::<Vec<_>>()
9238 .join("\n");
9239 e.set_content(&body);
9240 e.set_viewport_height(viewport);
9241 e
9242 }
9243
9244 #[test]
9245 fn ctrl_d_moves_cursor_half_page_down() {
9246 let mut e = editor_with_rows(100, 20);
9247 run_keys(&mut e, "<C-d>");
9248 assert_eq!(e.cursor().0, 10);
9249 }
9250
9251 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
9252 let mut e = Editor::new(
9253 hjkl_buffer::Buffer::new(),
9254 crate::types::DefaultHost::new(),
9255 crate::types::Options::default(),
9256 );
9257 e.set_content(&lines.join("\n"));
9258 e.set_viewport_height(viewport);
9259 let v = e.host_mut().viewport_mut();
9260 v.height = viewport;
9261 v.width = text_width;
9262 v.text_width = text_width;
9263 v.wrap = hjkl_buffer::Wrap::Char;
9264 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
9265 e
9266 }
9267
9268 #[test]
9269 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
9270 let lines = ["aaaabbbbcccc"; 10];
9274 let mut e = editor_with_wrap_lines(&lines, 12, 4);
9275 e.jump_cursor(4, 0);
9276 e.ensure_cursor_in_scrolloff();
9277 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
9278 assert!(csr <= 6, "csr={csr}");
9279 }
9280
9281 #[test]
9282 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
9283 let lines = ["aaaabbbbcccc"; 10];
9284 let mut e = editor_with_wrap_lines(&lines, 12, 4);
9285 e.jump_cursor(7, 0);
9288 e.ensure_cursor_in_scrolloff();
9289 e.jump_cursor(2, 0);
9290 e.ensure_cursor_in_scrolloff();
9291 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
9292 assert!(csr >= 5, "csr={csr}");
9294 }
9295
9296 #[test]
9297 fn scrolloff_wrap_clamps_top_at_buffer_end() {
9298 let lines = ["aaaabbbbcccc"; 5];
9299 let mut e = editor_with_wrap_lines(&lines, 12, 4);
9300 e.jump_cursor(4, 11);
9301 e.ensure_cursor_in_scrolloff();
9302 let top = e.host().viewport().top_row;
9307 assert_eq!(top, 1);
9308 }
9309
9310 #[test]
9311 fn ctrl_u_moves_cursor_half_page_up() {
9312 let mut e = editor_with_rows(100, 20);
9313 e.jump_cursor(50, 0);
9314 run_keys(&mut e, "<C-u>");
9315 assert_eq!(e.cursor().0, 40);
9316 }
9317
9318 #[test]
9319 fn ctrl_f_moves_cursor_full_page_down() {
9320 let mut e = editor_with_rows(100, 20);
9321 run_keys(&mut e, "<C-f>");
9322 assert_eq!(e.cursor().0, 18);
9324 }
9325
9326 #[test]
9327 fn ctrl_b_moves_cursor_full_page_up() {
9328 let mut e = editor_with_rows(100, 20);
9329 e.jump_cursor(50, 0);
9330 run_keys(&mut e, "<C-b>");
9331 assert_eq!(e.cursor().0, 32);
9332 }
9333
9334 #[test]
9335 fn ctrl_d_lands_on_first_non_blank() {
9336 let mut e = editor_with_rows(100, 20);
9337 run_keys(&mut e, "<C-d>");
9338 assert_eq!(e.cursor().1, 2);
9340 }
9341
9342 #[test]
9343 fn ctrl_d_clamps_at_end_of_buffer() {
9344 let mut e = editor_with_rows(5, 20);
9345 run_keys(&mut e, "<C-d>");
9346 assert_eq!(e.cursor().0, 4);
9347 }
9348
9349 #[test]
9350 fn capital_h_jumps_to_viewport_top() {
9351 let mut e = editor_with_rows(100, 10);
9352 e.jump_cursor(50, 0);
9353 e.set_viewport_top(45);
9354 let top = e.host().viewport().top_row;
9355 run_keys(&mut e, "H");
9356 assert_eq!(e.cursor().0, top);
9357 assert_eq!(e.cursor().1, 2);
9358 }
9359
9360 #[test]
9361 fn capital_l_jumps_to_viewport_bottom() {
9362 let mut e = editor_with_rows(100, 10);
9363 e.jump_cursor(50, 0);
9364 e.set_viewport_top(45);
9365 let top = e.host().viewport().top_row;
9366 run_keys(&mut e, "L");
9367 assert_eq!(e.cursor().0, top + 9);
9368 }
9369
9370 #[test]
9371 fn capital_m_jumps_to_viewport_middle() {
9372 let mut e = editor_with_rows(100, 10);
9373 e.jump_cursor(50, 0);
9374 e.set_viewport_top(45);
9375 let top = e.host().viewport().top_row;
9376 run_keys(&mut e, "M");
9377 assert_eq!(e.cursor().0, top + 4);
9379 }
9380
9381 #[test]
9382 fn g_capital_m_lands_at_line_midpoint() {
9383 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
9385 assert_eq!(e.cursor(), (0, 6));
9387 }
9388
9389 #[test]
9390 fn g_capital_m_on_empty_line_stays_at_zero() {
9391 let mut e = editor_with("");
9392 run_keys(&mut e, "gM");
9393 assert_eq!(e.cursor(), (0, 0));
9394 }
9395
9396 #[test]
9397 fn g_capital_m_uses_current_line_only() {
9398 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
9401 run_keys(&mut e, "gM");
9402 assert_eq!(e.cursor(), (1, 6));
9403 }
9404
9405 #[test]
9406 fn capital_h_count_offsets_from_top() {
9407 let mut e = editor_with_rows(100, 10);
9408 e.jump_cursor(50, 0);
9409 e.set_viewport_top(45);
9410 let top = e.host().viewport().top_row;
9411 run_keys(&mut e, "3H");
9412 assert_eq!(e.cursor().0, top + 2);
9413 }
9414
9415 #[test]
9418 fn ctrl_o_returns_to_pre_g_position() {
9419 let mut e = editor_with_rows(50, 20);
9420 e.jump_cursor(5, 2);
9421 run_keys(&mut e, "G");
9422 assert_eq!(e.cursor().0, 49);
9423 run_keys(&mut e, "<C-o>");
9424 assert_eq!(e.cursor(), (5, 2));
9425 }
9426
9427 #[test]
9428 fn ctrl_i_redoes_jump_after_ctrl_o() {
9429 let mut e = editor_with_rows(50, 20);
9430 e.jump_cursor(5, 2);
9431 run_keys(&mut e, "G");
9432 let post = e.cursor();
9433 run_keys(&mut e, "<C-o>");
9434 run_keys(&mut e, "<C-i>");
9435 assert_eq!(e.cursor(), post);
9436 }
9437
9438 #[test]
9439 fn new_jump_clears_forward_stack() {
9440 let mut e = editor_with_rows(50, 20);
9441 e.jump_cursor(5, 2);
9442 run_keys(&mut e, "G");
9443 run_keys(&mut e, "<C-o>");
9444 run_keys(&mut e, "gg");
9445 run_keys(&mut e, "<C-i>");
9446 assert_eq!(e.cursor().0, 0);
9447 }
9448
9449 #[test]
9450 fn ctrl_o_on_empty_stack_is_noop() {
9451 let mut e = editor_with_rows(10, 20);
9452 e.jump_cursor(3, 1);
9453 run_keys(&mut e, "<C-o>");
9454 assert_eq!(e.cursor(), (3, 1));
9455 }
9456
9457 #[test]
9458 fn asterisk_search_pushes_jump() {
9459 let mut e = editor_with("foo bar\nbaz foo end");
9460 e.jump_cursor(0, 0);
9461 run_keys(&mut e, "*");
9462 let after = e.cursor();
9463 assert_ne!(after, (0, 0));
9464 run_keys(&mut e, "<C-o>");
9465 assert_eq!(e.cursor(), (0, 0));
9466 }
9467
9468 #[test]
9469 fn h_viewport_jump_is_recorded() {
9470 let mut e = editor_with_rows(100, 10);
9471 e.jump_cursor(50, 0);
9472 e.set_viewport_top(45);
9473 let pre = e.cursor();
9474 run_keys(&mut e, "H");
9475 assert_ne!(e.cursor(), pre);
9476 run_keys(&mut e, "<C-o>");
9477 assert_eq!(e.cursor(), pre);
9478 }
9479
9480 #[test]
9481 fn j_k_motion_does_not_push_jump() {
9482 let mut e = editor_with_rows(50, 20);
9483 e.jump_cursor(5, 0);
9484 run_keys(&mut e, "jjj");
9485 run_keys(&mut e, "<C-o>");
9486 assert_eq!(e.cursor().0, 8);
9487 }
9488
9489 #[test]
9490 fn jumplist_caps_at_100() {
9491 let mut e = editor_with_rows(200, 20);
9492 for i in 0..101 {
9493 e.jump_cursor(i, 0);
9494 run_keys(&mut e, "G");
9495 }
9496 assert!(e.vim.jump_back.len() <= 100);
9497 }
9498
9499 #[test]
9500 fn tab_acts_as_ctrl_i() {
9501 let mut e = editor_with_rows(50, 20);
9502 e.jump_cursor(5, 2);
9503 run_keys(&mut e, "G");
9504 let post = e.cursor();
9505 run_keys(&mut e, "<C-o>");
9506 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9507 assert_eq!(e.cursor(), post);
9508 }
9509
9510 #[test]
9513 fn ma_then_backtick_a_jumps_exact() {
9514 let mut e = editor_with_rows(50, 20);
9515 e.jump_cursor(5, 3);
9516 run_keys(&mut e, "ma");
9517 e.jump_cursor(20, 0);
9518 run_keys(&mut e, "`a");
9519 assert_eq!(e.cursor(), (5, 3));
9520 }
9521
9522 #[test]
9523 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
9524 let mut e = editor_with_rows(50, 20);
9525 e.jump_cursor(5, 6);
9527 run_keys(&mut e, "ma");
9528 e.jump_cursor(30, 4);
9529 run_keys(&mut e, "'a");
9530 assert_eq!(e.cursor(), (5, 2));
9531 }
9532
9533 #[test]
9534 fn goto_mark_pushes_jumplist() {
9535 let mut e = editor_with_rows(50, 20);
9536 e.jump_cursor(10, 2);
9537 run_keys(&mut e, "mz");
9538 e.jump_cursor(3, 0);
9539 run_keys(&mut e, "`z");
9540 assert_eq!(e.cursor(), (10, 2));
9541 run_keys(&mut e, "<C-o>");
9542 assert_eq!(e.cursor(), (3, 0));
9543 }
9544
9545 #[test]
9546 fn goto_missing_mark_is_noop() {
9547 let mut e = editor_with_rows(50, 20);
9548 e.jump_cursor(3, 1);
9549 run_keys(&mut e, "`q");
9550 assert_eq!(e.cursor(), (3, 1));
9551 }
9552
9553 #[test]
9554 fn uppercase_mark_stored_under_uppercase_key() {
9555 let mut e = editor_with_rows(50, 20);
9556 e.jump_cursor(5, 3);
9557 run_keys(&mut e, "mA");
9558 assert_eq!(e.mark('A'), Some((5, 3)));
9561 assert!(e.mark('a').is_none());
9562 }
9563
9564 #[test]
9565 fn mark_survives_document_shrink_via_clamp() {
9566 let mut e = editor_with_rows(50, 20);
9567 e.jump_cursor(40, 4);
9568 run_keys(&mut e, "mx");
9569 e.set_content("a\nb\nc\nd\ne");
9571 run_keys(&mut e, "`x");
9572 let (r, _) = e.cursor();
9574 assert!(r <= 4);
9575 }
9576
9577 #[test]
9578 fn g_semicolon_walks_back_through_edits() {
9579 let mut e = editor_with("alpha\nbeta\ngamma");
9580 e.jump_cursor(0, 0);
9583 run_keys(&mut e, "iX<Esc>");
9584 e.jump_cursor(2, 0);
9585 run_keys(&mut e, "iY<Esc>");
9586 run_keys(&mut e, "g;");
9588 assert_eq!(e.cursor(), (2, 1));
9589 run_keys(&mut e, "g;");
9591 assert_eq!(e.cursor(), (0, 1));
9592 run_keys(&mut e, "g;");
9594 assert_eq!(e.cursor(), (0, 1));
9595 }
9596
9597 #[test]
9598 fn g_comma_walks_forward_after_g_semicolon() {
9599 let mut e = editor_with("a\nb\nc");
9600 e.jump_cursor(0, 0);
9601 run_keys(&mut e, "iX<Esc>");
9602 e.jump_cursor(2, 0);
9603 run_keys(&mut e, "iY<Esc>");
9604 run_keys(&mut e, "g;");
9605 run_keys(&mut e, "g;");
9606 assert_eq!(e.cursor(), (0, 1));
9607 run_keys(&mut e, "g,");
9608 assert_eq!(e.cursor(), (2, 1));
9609 }
9610
9611 #[test]
9612 fn new_edit_during_walk_trims_forward_entries() {
9613 let mut e = editor_with("a\nb\nc\nd");
9614 e.jump_cursor(0, 0);
9615 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
9617 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
9620 run_keys(&mut e, "g;");
9621 assert_eq!(e.cursor(), (0, 1));
9622 run_keys(&mut e, "iZ<Esc>");
9624 run_keys(&mut e, "g,");
9626 assert_ne!(e.cursor(), (2, 1));
9628 }
9629
9630 #[test]
9636 fn capital_mark_set_and_jump() {
9637 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9638 e.jump_cursor(2, 1);
9639 run_keys(&mut e, "mA");
9640 e.jump_cursor(0, 0);
9642 run_keys(&mut e, "'A");
9644 assert_eq!(e.cursor().0, 2);
9646 }
9647
9648 #[test]
9649 fn capital_mark_survives_set_content() {
9650 let mut e = editor_with("first buffer line\nsecond");
9651 e.jump_cursor(1, 3);
9652 run_keys(&mut e, "mA");
9653 e.set_content("totally different content\non many\nrows of text");
9655 e.jump_cursor(0, 0);
9657 run_keys(&mut e, "'A");
9658 assert_eq!(e.cursor().0, 1);
9659 }
9660
9661 #[test]
9666 fn capital_mark_shifts_with_edit() {
9667 let mut e = editor_with("a\nb\nc\nd");
9668 e.jump_cursor(3, 0);
9669 run_keys(&mut e, "mA");
9670 e.jump_cursor(0, 0);
9672 run_keys(&mut e, "dd");
9673 e.jump_cursor(0, 0);
9674 run_keys(&mut e, "'A");
9675 assert_eq!(e.cursor().0, 2);
9676 }
9677
9678 #[test]
9679 fn mark_below_delete_shifts_up() {
9680 let mut e = editor_with("a\nb\nc\nd\ne");
9681 e.jump_cursor(3, 0);
9683 run_keys(&mut e, "ma");
9684 e.jump_cursor(0, 0);
9686 run_keys(&mut e, "dd");
9687 e.jump_cursor(0, 0);
9689 run_keys(&mut e, "'a");
9690 assert_eq!(e.cursor().0, 2);
9691 assert_eq!(e.buffer().line(2).unwrap(), "d");
9692 }
9693
9694 #[test]
9695 fn mark_on_deleted_row_is_dropped() {
9696 let mut e = editor_with("a\nb\nc\nd");
9697 e.jump_cursor(1, 0);
9699 run_keys(&mut e, "ma");
9700 run_keys(&mut e, "dd");
9702 e.jump_cursor(2, 0);
9704 run_keys(&mut e, "'a");
9705 assert_eq!(e.cursor().0, 2);
9707 }
9708
9709 #[test]
9710 fn mark_above_edit_unchanged() {
9711 let mut e = editor_with("a\nb\nc\nd\ne");
9712 e.jump_cursor(0, 0);
9714 run_keys(&mut e, "ma");
9715 e.jump_cursor(3, 0);
9717 run_keys(&mut e, "dd");
9718 e.jump_cursor(2, 0);
9720 run_keys(&mut e, "'a");
9721 assert_eq!(e.cursor().0, 0);
9722 }
9723
9724 #[test]
9725 fn mark_shifts_down_after_insert() {
9726 let mut e = editor_with("a\nb\nc");
9727 e.jump_cursor(2, 0);
9729 run_keys(&mut e, "ma");
9730 e.jump_cursor(0, 0);
9732 run_keys(&mut e, "Onew<Esc>");
9733 e.jump_cursor(0, 0);
9736 run_keys(&mut e, "'a");
9737 assert_eq!(e.cursor().0, 3);
9738 assert_eq!(e.buffer().line(3).unwrap(), "c");
9739 }
9740
9741 #[test]
9744 fn forward_search_commit_pushes_jump() {
9745 let mut e = editor_with("alpha beta\nfoo target end\nmore");
9746 e.jump_cursor(0, 0);
9747 run_keys(&mut e, "/target<CR>");
9748 assert_ne!(e.cursor(), (0, 0));
9750 run_keys(&mut e, "<C-o>");
9752 assert_eq!(e.cursor(), (0, 0));
9753 }
9754
9755 #[test]
9756 fn search_commit_no_match_does_not_push_jump() {
9757 let mut e = editor_with("alpha beta\nfoo end");
9758 e.jump_cursor(0, 3);
9759 let pre_len = e.vim.jump_back.len();
9760 run_keys(&mut e, "/zzznotfound<CR>");
9761 assert_eq!(e.vim.jump_back.len(), pre_len);
9763 }
9764
9765 #[test]
9768 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
9769 let mut e = editor_with("hello world");
9770 run_keys(&mut e, "lll");
9771 let (row, col) = e.cursor();
9772 assert_eq!(e.buffer.cursor().row, row);
9773 assert_eq!(e.buffer.cursor().col, col);
9774 }
9775
9776 #[test]
9777 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
9778 let mut e = editor_with("aaaa\nbbbb\ncccc");
9779 run_keys(&mut e, "jj");
9780 let (row, col) = e.cursor();
9781 assert_eq!(e.buffer.cursor().row, row);
9782 assert_eq!(e.buffer.cursor().col, col);
9783 }
9784
9785 #[test]
9786 fn buffer_cursor_mirrors_textarea_after_word_motion() {
9787 let mut e = editor_with("foo bar baz");
9788 run_keys(&mut e, "ww");
9789 let (row, col) = e.cursor();
9790 assert_eq!(e.buffer.cursor().row, row);
9791 assert_eq!(e.buffer.cursor().col, col);
9792 }
9793
9794 #[test]
9795 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
9796 let mut e = editor_with("a\nb\nc\nd\ne");
9797 run_keys(&mut e, "G");
9798 let (row, col) = e.cursor();
9799 assert_eq!(e.buffer.cursor().row, row);
9800 assert_eq!(e.buffer.cursor().col, col);
9801 }
9802
9803 #[test]
9804 fn editor_sticky_col_tracks_horizontal_motion() {
9805 let mut e = editor_with("longline\nhi\nlongline");
9806 run_keys(&mut e, "fl");
9811 let landed = e.cursor().1;
9812 assert!(landed > 0, "fl should have moved");
9813 run_keys(&mut e, "j");
9814 assert_eq!(e.sticky_col(), Some(landed));
9817 }
9818
9819 #[test]
9820 fn buffer_content_mirrors_textarea_after_insert() {
9821 let mut e = editor_with("hello");
9822 run_keys(&mut e, "iXYZ<Esc>");
9823 let text = e.buffer().lines().join("\n");
9824 assert_eq!(e.buffer.as_string(), text);
9825 }
9826
9827 #[test]
9828 fn buffer_content_mirrors_textarea_after_delete() {
9829 let mut e = editor_with("alpha bravo charlie");
9830 run_keys(&mut e, "dw");
9831 let text = e.buffer().lines().join("\n");
9832 assert_eq!(e.buffer.as_string(), text);
9833 }
9834
9835 #[test]
9836 fn buffer_content_mirrors_textarea_after_dd() {
9837 let mut e = editor_with("a\nb\nc\nd");
9838 run_keys(&mut e, "jdd");
9839 let text = e.buffer().lines().join("\n");
9840 assert_eq!(e.buffer.as_string(), text);
9841 }
9842
9843 #[test]
9844 fn buffer_content_mirrors_textarea_after_open_line() {
9845 let mut e = editor_with("foo\nbar");
9846 run_keys(&mut e, "oNEW<Esc>");
9847 let text = e.buffer().lines().join("\n");
9848 assert_eq!(e.buffer.as_string(), text);
9849 }
9850
9851 #[test]
9852 fn buffer_content_mirrors_textarea_after_paste() {
9853 let mut e = editor_with("hello");
9854 run_keys(&mut e, "yy");
9855 run_keys(&mut e, "p");
9856 let text = e.buffer().lines().join("\n");
9857 assert_eq!(e.buffer.as_string(), text);
9858 }
9859
9860 #[test]
9861 fn buffer_selection_none_in_normal_mode() {
9862 let e = editor_with("foo bar");
9863 assert!(e.buffer_selection().is_none());
9864 }
9865
9866 #[test]
9867 fn buffer_selection_char_in_visual_mode() {
9868 use hjkl_buffer::{Position, Selection};
9869 let mut e = editor_with("hello world");
9870 run_keys(&mut e, "vlll");
9871 assert_eq!(
9872 e.buffer_selection(),
9873 Some(Selection::Char {
9874 anchor: Position::new(0, 0),
9875 head: Position::new(0, 3),
9876 })
9877 );
9878 }
9879
9880 #[test]
9881 fn buffer_selection_line_in_visual_line_mode() {
9882 use hjkl_buffer::Selection;
9883 let mut e = editor_with("a\nb\nc\nd");
9884 run_keys(&mut e, "Vj");
9885 assert_eq!(
9886 e.buffer_selection(),
9887 Some(Selection::Line {
9888 anchor_row: 0,
9889 head_row: 1,
9890 })
9891 );
9892 }
9893
9894 #[test]
9895 fn wrapscan_off_blocks_wrap_around() {
9896 let mut e = editor_with("first\nsecond\nthird\n");
9897 e.settings_mut().wrapscan = false;
9898 e.jump_cursor(2, 0);
9900 run_keys(&mut e, "/first<CR>");
9901 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9903 e.settings_mut().wrapscan = true;
9905 run_keys(&mut e, "/first<CR>");
9906 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9907 }
9908
9909 #[test]
9910 fn smartcase_uppercase_pattern_stays_sensitive() {
9911 let mut e = editor_with("foo\nFoo\nBAR\n");
9912 e.settings_mut().ignore_case = true;
9913 e.settings_mut().smartcase = true;
9914 run_keys(&mut e, "/foo<CR>");
9917 let r1 = e
9918 .search_state()
9919 .pattern
9920 .as_ref()
9921 .unwrap()
9922 .as_str()
9923 .to_string();
9924 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9925 run_keys(&mut e, "/Foo<CR>");
9927 let r2 = e
9928 .search_state()
9929 .pattern
9930 .as_ref()
9931 .unwrap()
9932 .as_str()
9933 .to_string();
9934 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9935 }
9936
9937 #[test]
9938 fn enter_with_autoindent_copies_leading_whitespace() {
9939 let mut e = editor_with(" foo");
9940 e.jump_cursor(0, 7);
9941 run_keys(&mut e, "i<CR>");
9942 assert_eq!(e.buffer.line(1).unwrap(), " ");
9943 }
9944
9945 #[test]
9946 fn enter_without_autoindent_inserts_bare_newline() {
9947 let mut e = editor_with(" foo");
9948 e.settings_mut().autoindent = false;
9949 e.jump_cursor(0, 7);
9950 run_keys(&mut e, "i<CR>");
9951 assert_eq!(e.buffer.line(1).unwrap(), "");
9952 }
9953
9954 #[test]
9955 fn iskeyword_default_treats_alnum_underscore_as_word() {
9956 let mut e = editor_with("foo_bar baz");
9957 e.jump_cursor(0, 0);
9961 run_keys(&mut e, "*");
9962 let p = e
9963 .search_state()
9964 .pattern
9965 .as_ref()
9966 .unwrap()
9967 .as_str()
9968 .to_string();
9969 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9970 }
9971
9972 #[test]
9973 fn w_motion_respects_custom_iskeyword() {
9974 let mut e = editor_with("foo-bar baz");
9978 run_keys(&mut e, "w");
9979 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9980 let mut e2 = editor_with("foo-bar baz");
9983 e2.set_iskeyword("@,_,45");
9984 run_keys(&mut e2, "w");
9985 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9986 }
9987
9988 #[test]
9989 fn iskeyword_with_dash_treats_dash_as_word_char() {
9990 let mut e = editor_with("foo-bar baz");
9991 e.settings_mut().iskeyword = "@,_,45".to_string();
9992 e.jump_cursor(0, 0);
9993 run_keys(&mut e, "*");
9994 let p = e
9995 .search_state()
9996 .pattern
9997 .as_ref()
9998 .unwrap()
9999 .as_str()
10000 .to_string();
10001 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
10002 }
10003
10004 #[test]
10005 fn timeoutlen_drops_pending_g_prefix() {
10006 use std::time::{Duration, Instant};
10007 let mut e = editor_with("a\nb\nc");
10008 e.jump_cursor(2, 0);
10009 run_keys(&mut e, "g");
10011 assert!(matches!(e.vim.pending, super::Pending::G));
10012 e.settings.timeout_len = Duration::from_nanos(0);
10020 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
10021 e.vim.last_input_host_at = Some(Duration::ZERO);
10022 run_keys(&mut e, "g");
10026 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
10028 }
10029
10030 #[test]
10031 fn undobreak_on_breaks_group_at_arrow_motion() {
10032 let mut e = editor_with("");
10033 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
10035 let line = e.buffer.line(0).unwrap_or("").to_string();
10038 assert!(line.contains("aaa"), "after undobreak: {line:?}");
10039 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
10040 }
10041
10042 #[test]
10043 fn undobreak_off_keeps_full_run_in_one_group() {
10044 let mut e = editor_with("");
10045 e.settings_mut().undo_break_on_motion = false;
10046 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
10047 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
10050 }
10051
10052 #[test]
10053 fn undobreak_round_trips_through_options() {
10054 let e = editor_with("");
10055 let opts = e.current_options();
10056 assert!(opts.undo_break_on_motion);
10057 let mut e2 = editor_with("");
10058 let mut new_opts = opts.clone();
10059 new_opts.undo_break_on_motion = false;
10060 e2.apply_options(&new_opts);
10061 assert!(!e2.current_options().undo_break_on_motion);
10062 }
10063
10064 #[test]
10065 fn undo_levels_cap_drops_oldest() {
10066 let mut e = editor_with("abcde");
10067 e.settings_mut().undo_levels = 3;
10068 run_keys(&mut e, "ra");
10069 run_keys(&mut e, "lrb");
10070 run_keys(&mut e, "lrc");
10071 run_keys(&mut e, "lrd");
10072 run_keys(&mut e, "lre");
10073 assert_eq!(e.undo_stack_len(), 3);
10074 }
10075
10076 #[test]
10077 fn tab_inserts_literal_tab_when_noexpandtab() {
10078 let mut e = editor_with("");
10079 e.settings_mut().expandtab = false;
10082 e.settings_mut().softtabstop = 0;
10083 run_keys(&mut e, "i");
10084 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
10085 assert_eq!(e.buffer.line(0).unwrap(), "\t");
10086 }
10087
10088 #[test]
10089 fn tab_inserts_spaces_when_expandtab() {
10090 let mut e = editor_with("");
10091 e.settings_mut().expandtab = true;
10092 e.settings_mut().tabstop = 4;
10093 run_keys(&mut e, "i");
10094 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
10095 assert_eq!(e.buffer.line(0).unwrap(), " ");
10096 }
10097
10098 #[test]
10099 fn tab_with_softtabstop_fills_to_next_boundary() {
10100 let mut e = editor_with("ab");
10102 e.settings_mut().expandtab = true;
10103 e.settings_mut().tabstop = 8;
10104 e.settings_mut().softtabstop = 4;
10105 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
10107 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
10108 }
10109
10110 #[test]
10111 fn backspace_deletes_softtab_run() {
10112 let mut e = editor_with(" x");
10115 e.settings_mut().softtabstop = 4;
10116 run_keys(&mut e, "fxi");
10118 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
10119 assert_eq!(e.buffer.line(0).unwrap(), "x");
10120 }
10121
10122 #[test]
10123 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
10124 let mut e = editor_with(" x");
10127 e.settings_mut().softtabstop = 4;
10128 run_keys(&mut e, "fxi");
10129 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
10130 assert_eq!(e.buffer.line(0).unwrap(), " x");
10131 }
10132
10133 #[test]
10134 fn readonly_blocks_insert_mutation() {
10135 let mut e = editor_with("hello");
10136 e.settings_mut().readonly = true;
10137 run_keys(&mut e, "iX<Esc>");
10138 assert_eq!(e.buffer.line(0).unwrap(), "hello");
10139 }
10140
10141 #[cfg(feature = "ratatui")]
10142 #[test]
10143 fn intern_ratatui_style_dedups_repeated_styles() {
10144 use ratatui::style::{Color, Style};
10145 let mut e = editor_with("");
10146 let red = Style::default().fg(Color::Red);
10147 let blue = Style::default().fg(Color::Blue);
10148 let id_r1 = e.intern_ratatui_style(red);
10149 let id_r2 = e.intern_ratatui_style(red);
10150 let id_b = e.intern_ratatui_style(blue);
10151 assert_eq!(id_r1, id_r2);
10152 assert_ne!(id_r1, id_b);
10153 assert_eq!(e.style_table().len(), 2);
10154 }
10155
10156 #[cfg(feature = "ratatui")]
10157 #[test]
10158 fn install_ratatui_syntax_spans_translates_styled_spans() {
10159 use ratatui::style::{Color, Style};
10160 let mut e = editor_with("SELECT foo");
10161 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
10162 let by_row = e.buffer_spans();
10163 assert_eq!(by_row.len(), 1);
10164 assert_eq!(by_row[0].len(), 1);
10165 assert_eq!(by_row[0][0].start_byte, 0);
10166 assert_eq!(by_row[0][0].end_byte, 6);
10167 let id = by_row[0][0].style;
10168 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
10169 }
10170
10171 #[cfg(feature = "ratatui")]
10172 #[test]
10173 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
10174 use ratatui::style::{Color, Style};
10175 let mut e = editor_with("hello");
10176 e.install_ratatui_syntax_spans(vec![vec![(
10177 0,
10178 usize::MAX,
10179 Style::default().fg(Color::Blue),
10180 )]]);
10181 let by_row = e.buffer_spans();
10182 assert_eq!(by_row[0][0].end_byte, 5);
10183 }
10184
10185 #[cfg(feature = "ratatui")]
10186 #[test]
10187 fn install_ratatui_syntax_spans_drops_zero_width() {
10188 use ratatui::style::{Color, Style};
10189 let mut e = editor_with("abc");
10190 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
10191 assert!(e.buffer_spans()[0].is_empty());
10192 }
10193
10194 #[test]
10195 fn named_register_yank_into_a_then_paste_from_a() {
10196 let mut e = editor_with("hello world\nsecond");
10197 run_keys(&mut e, "\"ayw");
10198 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
10200 run_keys(&mut e, "j0\"aP");
10202 assert_eq!(e.buffer().lines()[1], "hello second");
10203 }
10204
10205 #[test]
10206 fn capital_r_overstrikes_chars() {
10207 let mut e = editor_with("hello");
10208 e.jump_cursor(0, 0);
10209 run_keys(&mut e, "RXY<Esc>");
10210 assert_eq!(e.buffer().lines()[0], "XYllo");
10212 }
10213
10214 #[test]
10215 fn capital_r_at_eol_appends() {
10216 let mut e = editor_with("hi");
10217 e.jump_cursor(0, 1);
10218 run_keys(&mut e, "RXYZ<Esc>");
10220 assert_eq!(e.buffer().lines()[0], "hXYZ");
10221 }
10222
10223 #[test]
10224 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
10225 let mut e = editor_with("abc");
10229 e.jump_cursor(0, 0);
10230 run_keys(&mut e, "RX<Esc>");
10231 assert_eq!(e.buffer().lines()[0], "Xbc");
10232 }
10233
10234 #[test]
10235 fn ctrl_r_in_insert_pastes_named_register() {
10236 let mut e = editor_with("hello world");
10237 run_keys(&mut e, "\"ayw");
10239 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
10240 run_keys(&mut e, "o");
10242 assert_eq!(e.vim_mode(), VimMode::Insert);
10243 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10244 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
10245 assert_eq!(e.buffer().lines()[1], "hello ");
10246 assert_eq!(e.cursor(), (1, 6));
10248 assert_eq!(e.vim_mode(), VimMode::Insert);
10250 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
10251 assert_eq!(e.buffer().lines()[1], "hello X");
10252 }
10253
10254 #[test]
10255 fn ctrl_r_with_unnamed_register() {
10256 let mut e = editor_with("foo");
10257 run_keys(&mut e, "yiw");
10258 run_keys(&mut e, "A ");
10259 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10261 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
10262 assert_eq!(e.buffer().lines()[0], "foo foo");
10263 }
10264
10265 #[test]
10266 fn ctrl_r_unknown_selector_is_no_op() {
10267 let mut e = editor_with("abc");
10268 run_keys(&mut e, "A");
10269 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10270 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
10273 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
10274 assert_eq!(e.buffer().lines()[0], "abcZ");
10275 }
10276
10277 #[test]
10278 fn ctrl_r_multiline_register_pastes_with_newlines() {
10279 let mut e = editor_with("alpha\nbeta\ngamma");
10280 run_keys(&mut e, "\"byy");
10282 run_keys(&mut e, "j\"byy");
10283 run_keys(&mut e, "ggVj\"by");
10287 let payload = e.registers().read('b').unwrap().text.clone();
10288 assert!(payload.contains('\n'));
10289 run_keys(&mut e, "Go");
10290 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
10291 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
10292 let total_lines = e.buffer().lines().len();
10295 assert!(total_lines >= 5);
10296 }
10297
10298 #[test]
10299 fn yank_zero_holds_last_yank_after_delete() {
10300 let mut e = editor_with("hello world");
10301 run_keys(&mut e, "yw");
10302 let yanked = e.registers().read('0').unwrap().text.clone();
10303 assert!(!yanked.is_empty());
10304 run_keys(&mut e, "dw");
10306 assert_eq!(e.registers().read('0').unwrap().text, yanked);
10307 assert!(!e.registers().read('1').unwrap().text.is_empty());
10309 }
10310
10311 #[test]
10312 fn delete_ring_rotates_through_one_through_nine() {
10313 let mut e = editor_with("a b c d e f g h i j");
10314 for _ in 0..3 {
10316 run_keys(&mut e, "dw");
10317 }
10318 let r1 = e.registers().read('1').unwrap().text.clone();
10320 let r2 = e.registers().read('2').unwrap().text.clone();
10321 let r3 = e.registers().read('3').unwrap().text.clone();
10322 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
10323 assert_ne!(r1, r2);
10324 assert_ne!(r2, r3);
10325 }
10326
10327 #[test]
10328 fn capital_register_appends_to_lowercase() {
10329 let mut e = editor_with("foo bar");
10330 run_keys(&mut e, "\"ayw");
10331 let first = e.registers().read('a').unwrap().text.clone();
10332 assert!(first.contains("foo"));
10333 run_keys(&mut e, "w\"Ayw");
10335 let combined = e.registers().read('a').unwrap().text.clone();
10336 assert!(combined.starts_with(&first));
10337 assert!(combined.contains("bar"));
10338 }
10339
10340 #[test]
10341 fn zf_in_visual_line_creates_closed_fold() {
10342 let mut e = editor_with("a\nb\nc\nd\ne");
10343 e.jump_cursor(1, 0);
10345 run_keys(&mut e, "Vjjzf");
10346 assert_eq!(e.buffer().folds().len(), 1);
10347 let f = e.buffer().folds()[0];
10348 assert_eq!(f.start_row, 1);
10349 assert_eq!(f.end_row, 3);
10350 assert!(f.closed);
10351 }
10352
10353 #[test]
10354 fn zfj_in_normal_creates_two_row_fold() {
10355 let mut e = editor_with("a\nb\nc\nd\ne");
10356 e.jump_cursor(1, 0);
10357 run_keys(&mut e, "zfj");
10358 assert_eq!(e.buffer().folds().len(), 1);
10359 let f = e.buffer().folds()[0];
10360 assert_eq!(f.start_row, 1);
10361 assert_eq!(f.end_row, 2);
10362 assert!(f.closed);
10363 assert_eq!(e.cursor().0, 1);
10365 }
10366
10367 #[test]
10368 fn zf_with_count_folds_count_rows() {
10369 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10370 e.jump_cursor(0, 0);
10371 run_keys(&mut e, "zf3j");
10373 assert_eq!(e.buffer().folds().len(), 1);
10374 let f = e.buffer().folds()[0];
10375 assert_eq!(f.start_row, 0);
10376 assert_eq!(f.end_row, 3);
10377 }
10378
10379 #[test]
10380 fn zfk_folds_upward_range() {
10381 let mut e = editor_with("a\nb\nc\nd\ne");
10382 e.jump_cursor(3, 0);
10383 run_keys(&mut e, "zfk");
10384 let f = e.buffer().folds()[0];
10385 assert_eq!(f.start_row, 2);
10387 assert_eq!(f.end_row, 3);
10388 }
10389
10390 #[test]
10391 fn zf_capital_g_folds_to_bottom() {
10392 let mut e = editor_with("a\nb\nc\nd\ne");
10393 e.jump_cursor(1, 0);
10394 run_keys(&mut e, "zfG");
10396 let f = e.buffer().folds()[0];
10397 assert_eq!(f.start_row, 1);
10398 assert_eq!(f.end_row, 4);
10399 }
10400
10401 #[test]
10402 fn zfgg_folds_to_top_via_operator_pipeline() {
10403 let mut e = editor_with("a\nb\nc\nd\ne");
10404 e.jump_cursor(3, 0);
10405 run_keys(&mut e, "zfgg");
10409 let f = e.buffer().folds()[0];
10410 assert_eq!(f.start_row, 0);
10411 assert_eq!(f.end_row, 3);
10412 }
10413
10414 #[test]
10415 fn zfip_folds_paragraph_via_text_object() {
10416 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
10417 e.jump_cursor(1, 0);
10418 run_keys(&mut e, "zfip");
10420 assert_eq!(e.buffer().folds().len(), 1);
10421 let f = e.buffer().folds()[0];
10422 assert_eq!(f.start_row, 0);
10423 assert_eq!(f.end_row, 2);
10424 }
10425
10426 #[test]
10427 fn zfap_folds_paragraph_with_trailing_blank() {
10428 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
10429 e.jump_cursor(0, 0);
10430 run_keys(&mut e, "zfap");
10432 let f = e.buffer().folds()[0];
10433 assert_eq!(f.start_row, 0);
10434 assert_eq!(f.end_row, 3);
10435 }
10436
10437 #[test]
10438 fn zf_paragraph_motion_folds_to_blank() {
10439 let mut e = editor_with("alpha\nbeta\n\ngamma");
10440 e.jump_cursor(0, 0);
10441 run_keys(&mut e, "zf}");
10443 let f = e.buffer().folds()[0];
10444 assert_eq!(f.start_row, 0);
10445 assert_eq!(f.end_row, 2);
10446 }
10447
10448 #[test]
10449 fn za_toggles_fold_under_cursor() {
10450 let mut e = editor_with("a\nb\nc\nd");
10451 e.buffer_mut().add_fold(1, 2, true);
10452 e.jump_cursor(1, 0);
10453 run_keys(&mut e, "za");
10454 assert!(!e.buffer().folds()[0].closed);
10455 run_keys(&mut e, "za");
10456 assert!(e.buffer().folds()[0].closed);
10457 }
10458
10459 #[test]
10460 fn zr_opens_all_folds_zm_closes_all() {
10461 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10462 e.buffer_mut().add_fold(0, 1, true);
10463 e.buffer_mut().add_fold(2, 3, true);
10464 e.buffer_mut().add_fold(4, 5, true);
10465 run_keys(&mut e, "zR");
10466 assert!(e.buffer().folds().iter().all(|f| !f.closed));
10467 run_keys(&mut e, "zM");
10468 assert!(e.buffer().folds().iter().all(|f| f.closed));
10469 }
10470
10471 #[test]
10472 fn ze_clears_all_folds() {
10473 let mut e = editor_with("a\nb\nc\nd");
10474 e.buffer_mut().add_fold(0, 1, true);
10475 e.buffer_mut().add_fold(2, 3, false);
10476 run_keys(&mut e, "zE");
10477 assert!(e.buffer().folds().is_empty());
10478 }
10479
10480 #[test]
10481 fn g_underscore_jumps_to_last_non_blank() {
10482 let mut e = editor_with("hello world ");
10483 run_keys(&mut e, "g_");
10484 assert_eq!(e.cursor().1, 10);
10486 }
10487
10488 #[test]
10489 fn gj_and_gk_alias_j_and_k() {
10490 let mut e = editor_with("a\nb\nc");
10491 run_keys(&mut e, "gj");
10492 assert_eq!(e.cursor().0, 1);
10493 run_keys(&mut e, "gk");
10494 assert_eq!(e.cursor().0, 0);
10495 }
10496
10497 #[test]
10498 fn paragraph_motions_walk_blank_lines() {
10499 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
10500 run_keys(&mut e, "}");
10501 assert_eq!(e.cursor().0, 2);
10502 run_keys(&mut e, "}");
10503 assert_eq!(e.cursor().0, 5);
10504 run_keys(&mut e, "{");
10505 assert_eq!(e.cursor().0, 2);
10506 }
10507
10508 #[test]
10509 fn gv_reenters_last_visual_selection() {
10510 let mut e = editor_with("alpha\nbeta\ngamma");
10511 run_keys(&mut e, "Vj");
10512 run_keys(&mut e, "<Esc>");
10514 assert_eq!(e.vim_mode(), VimMode::Normal);
10515 run_keys(&mut e, "gv");
10517 assert_eq!(e.vim_mode(), VimMode::VisualLine);
10518 }
10519
10520 #[test]
10521 fn o_in_visual_swaps_anchor_and_cursor() {
10522 let mut e = editor_with("hello world");
10523 run_keys(&mut e, "vllll");
10525 assert_eq!(e.cursor().1, 4);
10526 run_keys(&mut e, "o");
10528 assert_eq!(e.cursor().1, 0);
10529 assert_eq!(e.vim.visual_anchor, (0, 4));
10531 }
10532
10533 #[test]
10534 fn editing_inside_fold_invalidates_it() {
10535 let mut e = editor_with("a\nb\nc\nd");
10536 e.buffer_mut().add_fold(1, 2, true);
10537 e.jump_cursor(1, 0);
10538 run_keys(&mut e, "iX<Esc>");
10540 assert!(e.buffer().folds().is_empty());
10542 }
10543
10544 #[test]
10545 fn zd_removes_fold_under_cursor() {
10546 let mut e = editor_with("a\nb\nc\nd");
10547 e.buffer_mut().add_fold(1, 2, true);
10548 e.jump_cursor(2, 0);
10549 run_keys(&mut e, "zd");
10550 assert!(e.buffer().folds().is_empty());
10551 }
10552
10553 #[test]
10554 fn take_fold_ops_observes_z_keystroke_dispatch() {
10555 use crate::types::FoldOp;
10560 let mut e = editor_with("a\nb\nc\nd");
10561 e.buffer_mut().add_fold(1, 2, true);
10562 e.jump_cursor(1, 0);
10563 let _ = e.take_fold_ops();
10566 run_keys(&mut e, "zo");
10567 run_keys(&mut e, "zM");
10568 let ops = e.take_fold_ops();
10569 assert_eq!(ops.len(), 2);
10570 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
10571 assert!(matches!(ops[1], FoldOp::CloseAll));
10572 assert!(e.take_fold_ops().is_empty());
10574 }
10575
10576 #[test]
10577 fn edit_pipeline_emits_invalidate_fold_op() {
10578 use crate::types::FoldOp;
10581 let mut e = editor_with("a\nb\nc\nd");
10582 e.buffer_mut().add_fold(1, 2, true);
10583 e.jump_cursor(1, 0);
10584 let _ = e.take_fold_ops();
10585 run_keys(&mut e, "iX<Esc>");
10586 let ops = e.take_fold_ops();
10587 assert!(
10588 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
10589 "expected at least one Invalidate op, got {ops:?}"
10590 );
10591 }
10592
10593 #[test]
10594 fn dot_mark_jumps_to_last_edit_position() {
10595 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
10596 e.jump_cursor(2, 0);
10597 run_keys(&mut e, "iX<Esc>");
10599 let after_edit = e.cursor();
10600 run_keys(&mut e, "gg");
10602 assert_eq!(e.cursor().0, 0);
10603 run_keys(&mut e, "'.");
10605 assert_eq!(e.cursor().0, after_edit.0);
10606 }
10607
10608 #[test]
10609 fn quote_quote_returns_to_pre_jump_position() {
10610 let mut e = editor_with_rows(50, 20);
10611 e.jump_cursor(10, 2);
10612 let before = e.cursor();
10613 run_keys(&mut e, "G");
10615 assert_ne!(e.cursor(), before);
10616 run_keys(&mut e, "''");
10618 assert_eq!(e.cursor().0, before.0);
10619 }
10620
10621 #[test]
10622 fn backtick_backtick_restores_exact_pre_jump_pos() {
10623 let mut e = editor_with_rows(50, 20);
10624 e.jump_cursor(7, 3);
10625 let before = e.cursor();
10626 run_keys(&mut e, "G");
10627 run_keys(&mut e, "``");
10628 assert_eq!(e.cursor(), before);
10629 }
10630
10631 #[test]
10632 fn macro_record_and_replay_basic() {
10633 let mut e = editor_with("foo\nbar\nbaz");
10634 run_keys(&mut e, "qaIX<Esc>jq");
10636 assert_eq!(e.buffer().lines()[0], "Xfoo");
10637 run_keys(&mut e, "@a");
10639 assert_eq!(e.buffer().lines()[1], "Xbar");
10640 run_keys(&mut e, "j@@");
10642 assert_eq!(e.buffer().lines()[2], "Xbaz");
10643 }
10644
10645 #[test]
10646 fn macro_count_replays_n_times() {
10647 let mut e = editor_with("a\nb\nc\nd\ne");
10648 run_keys(&mut e, "qajq");
10650 assert_eq!(e.cursor().0, 1);
10651 run_keys(&mut e, "3@a");
10653 assert_eq!(e.cursor().0, 4);
10654 }
10655
10656 #[test]
10657 fn macro_capital_q_appends_to_lowercase_register() {
10658 let mut e = editor_with("hello");
10659 run_keys(&mut e, "qall<Esc>q");
10660 run_keys(&mut e, "qAhh<Esc>q");
10661 let text = e.registers().read('a').unwrap().text.clone();
10664 assert!(text.contains("ll<Esc>"));
10665 assert!(text.contains("hh<Esc>"));
10666 }
10667
10668 #[test]
10669 fn buffer_selection_block_in_visual_block_mode() {
10670 use hjkl_buffer::{Position, Selection};
10671 let mut e = editor_with("aaaa\nbbbb\ncccc");
10672 run_keys(&mut e, "<C-v>jl");
10673 assert_eq!(
10674 e.buffer_selection(),
10675 Some(Selection::Block {
10676 anchor: Position::new(0, 0),
10677 head: Position::new(1, 1),
10678 })
10679 );
10680 }
10681
10682 #[test]
10685 fn n_after_question_mark_keeps_walking_backward() {
10686 let mut e = editor_with("foo bar foo baz foo end");
10689 e.jump_cursor(0, 22);
10690 run_keys(&mut e, "?foo<CR>");
10691 assert_eq!(e.cursor().1, 16);
10692 run_keys(&mut e, "n");
10693 assert_eq!(e.cursor().1, 8);
10694 run_keys(&mut e, "N");
10695 assert_eq!(e.cursor().1, 16);
10696 }
10697
10698 #[test]
10699 fn nested_macro_chord_records_literal_keys() {
10700 let mut e = editor_with("alpha\nbeta\ngamma");
10703 run_keys(&mut e, "qblq");
10705 run_keys(&mut e, "qaIX<Esc>q");
10708 e.jump_cursor(1, 0);
10710 run_keys(&mut e, "@a");
10711 assert_eq!(e.buffer().lines()[1], "Xbeta");
10712 }
10713
10714 #[test]
10715 fn shift_gt_motion_indents_one_line() {
10716 let mut e = editor_with("hello world");
10720 run_keys(&mut e, ">w");
10721 assert_eq!(e.buffer().lines()[0], " hello world");
10722 }
10723
10724 #[test]
10725 fn shift_lt_motion_outdents_one_line() {
10726 let mut e = editor_with(" hello world");
10727 run_keys(&mut e, "<lt>w");
10728 assert_eq!(e.buffer().lines()[0], " hello world");
10730 }
10731
10732 #[test]
10733 fn shift_gt_text_object_indents_paragraph() {
10734 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
10735 e.jump_cursor(0, 0);
10736 run_keys(&mut e, ">ip");
10737 assert_eq!(e.buffer().lines()[0], " alpha");
10738 assert_eq!(e.buffer().lines()[1], " beta");
10739 assert_eq!(e.buffer().lines()[2], " gamma");
10740 assert_eq!(e.buffer().lines()[4], "rest");
10742 }
10743
10744 #[test]
10745 fn ctrl_o_runs_exactly_one_normal_command() {
10746 let mut e = editor_with("alpha beta gamma");
10749 e.jump_cursor(0, 0);
10750 run_keys(&mut e, "i");
10751 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
10752 run_keys(&mut e, "dw");
10753 assert_eq!(e.vim_mode(), VimMode::Insert);
10755 run_keys(&mut e, "X");
10757 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
10758 }
10759
10760 #[test]
10761 fn macro_replay_respects_mode_switching() {
10762 let mut e = editor_with("hi");
10766 run_keys(&mut e, "qaiX<Esc>0q");
10767 assert_eq!(e.vim_mode(), VimMode::Normal);
10768 e.set_content("yo");
10770 run_keys(&mut e, "@a");
10771 assert_eq!(e.vim_mode(), VimMode::Normal);
10772 assert_eq!(e.cursor().1, 0);
10773 assert_eq!(e.buffer().lines()[0], "Xyo");
10774 }
10775
10776 #[test]
10777 fn macro_recorded_text_round_trips_through_register() {
10778 let mut e = editor_with("");
10782 run_keys(&mut e, "qaiX<Esc>q");
10783 let text = e.registers().read('a').unwrap().text.clone();
10784 assert!(text.starts_with("iX"));
10785 run_keys(&mut e, "@a");
10787 assert_eq!(e.buffer().lines()[0], "XX");
10788 }
10789
10790 #[test]
10791 fn dot_after_macro_replays_macros_last_change() {
10792 let mut e = editor_with("ab\ncd\nef");
10795 run_keys(&mut e, "qaIX<Esc>jq");
10798 assert_eq!(e.buffer().lines()[0], "Xab");
10799 run_keys(&mut e, "@a");
10800 assert_eq!(e.buffer().lines()[1], "Xcd");
10801 let row_before_dot = e.cursor().0;
10804 run_keys(&mut e, ".");
10805 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10806 }
10807
10808 fn si_editor(content: &str) -> Editor {
10814 let opts = crate::types::Options {
10815 shiftwidth: 4,
10816 softtabstop: 4,
10817 expandtab: true,
10818 smartindent: true,
10819 autoindent: true,
10820 ..crate::types::Options::default()
10821 };
10822 let mut e = Editor::new(
10823 hjkl_buffer::Buffer::new(),
10824 crate::types::DefaultHost::new(),
10825 opts,
10826 );
10827 e.set_content(content);
10828 e
10829 }
10830
10831 #[test]
10832 fn smartindent_bumps_indent_after_open_brace() {
10833 let mut e = si_editor("fn foo() {");
10835 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10837 assert_eq!(
10838 e.buffer().lines()[1],
10839 " ",
10840 "smartindent should bump one shiftwidth after {{"
10841 );
10842 }
10843
10844 #[test]
10845 fn smartindent_no_bump_when_off() {
10846 let mut e = si_editor("fn foo() {");
10849 e.settings_mut().smartindent = false;
10850 e.jump_cursor(0, 10);
10851 run_keys(&mut e, "i<CR>");
10852 assert_eq!(
10853 e.buffer().lines()[1],
10854 "",
10855 "without smartindent, no bump: new line copies empty leading ws"
10856 );
10857 }
10858
10859 #[test]
10860 fn smartindent_uses_tab_when_noexpandtab() {
10861 let opts = crate::types::Options {
10863 shiftwidth: 4,
10864 softtabstop: 0,
10865 expandtab: false,
10866 smartindent: true,
10867 autoindent: true,
10868 ..crate::types::Options::default()
10869 };
10870 let mut e = Editor::new(
10871 hjkl_buffer::Buffer::new(),
10872 crate::types::DefaultHost::new(),
10873 opts,
10874 );
10875 e.set_content("fn foo() {");
10876 e.jump_cursor(0, 10);
10877 run_keys(&mut e, "i<CR>");
10878 assert_eq!(
10879 e.buffer().lines()[1],
10880 "\t",
10881 "noexpandtab: smartindent bump inserts a literal tab"
10882 );
10883 }
10884
10885 #[test]
10886 fn smartindent_dedent_on_close_brace() {
10887 let mut e = si_editor("fn foo() {");
10890 e.set_content("fn foo() {\n ");
10892 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10894 assert_eq!(
10895 e.buffer().lines()[1],
10896 "}",
10897 "close brace on whitespace-only line should dedent"
10898 );
10899 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10900 }
10901
10902 #[test]
10903 fn smartindent_no_dedent_when_off() {
10904 let mut e = si_editor("fn foo() {\n ");
10906 e.settings_mut().smartindent = false;
10907 e.jump_cursor(1, 4);
10908 run_keys(&mut e, "i}");
10909 assert_eq!(
10910 e.buffer().lines()[1],
10911 " }",
10912 "without smartindent, `}}` just appends at cursor"
10913 );
10914 }
10915
10916 #[test]
10917 fn smartindent_no_dedent_mid_line() {
10918 let mut e = si_editor(" let x = 1");
10921 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10923 assert_eq!(
10924 e.buffer().lines()[0],
10925 " let x = 1}",
10926 "mid-line `}}` should not dedent"
10927 );
10928 }
10929
10930 #[test]
10934 fn count_5x_fills_unnamed_register() {
10935 let mut e = editor_with("hello world\n");
10936 e.jump_cursor(0, 0);
10937 run_keys(&mut e, "5x");
10938 assert_eq!(e.buffer().lines()[0], " world");
10939 assert_eq!(e.cursor(), (0, 0));
10940 assert_eq!(e.yank(), "hello");
10941 }
10942
10943 #[test]
10944 fn x_fills_unnamed_register_single_char() {
10945 let mut e = editor_with("abc\n");
10946 e.jump_cursor(0, 0);
10947 run_keys(&mut e, "x");
10948 assert_eq!(e.buffer().lines()[0], "bc");
10949 assert_eq!(e.yank(), "a");
10950 }
10951
10952 #[test]
10953 fn big_x_fills_unnamed_register() {
10954 let mut e = editor_with("hello\n");
10955 e.jump_cursor(0, 3);
10956 run_keys(&mut e, "X");
10957 assert_eq!(e.buffer().lines()[0], "helo");
10958 assert_eq!(e.yank(), "l");
10959 }
10960
10961 #[test]
10963 fn g_motion_trailing_newline_lands_on_last_content_row() {
10964 let mut e = editor_with("foo\nbar\nbaz\n");
10965 e.jump_cursor(0, 0);
10966 run_keys(&mut e, "G");
10967 assert_eq!(
10969 e.cursor().0,
10970 2,
10971 "G should land on row 2 (baz), not row 3 (phantom empty)"
10972 );
10973 }
10974
10975 #[test]
10977 fn dd_last_line_clamps_cursor_to_new_last_row() {
10978 let mut e = editor_with("foo\nbar\n");
10979 e.jump_cursor(1, 0);
10980 run_keys(&mut e, "dd");
10981 assert_eq!(e.buffer().lines()[0], "foo");
10982 assert_eq!(
10983 e.cursor(),
10984 (0, 0),
10985 "cursor should clamp to row 0 after dd on last content line"
10986 );
10987 }
10988
10989 #[test]
10991 fn d_dollar_cursor_on_last_char() {
10992 let mut e = editor_with("hello world\n");
10993 e.jump_cursor(0, 5);
10994 run_keys(&mut e, "d$");
10995 assert_eq!(e.buffer().lines()[0], "hello");
10996 assert_eq!(
10997 e.cursor(),
10998 (0, 4),
10999 "d$ should leave cursor on col 4, not col 5"
11000 );
11001 }
11002
11003 #[test]
11005 fn undo_insert_clamps_cursor_to_last_valid_col() {
11006 let mut e = editor_with("hello\n");
11007 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
11009 assert_eq!(e.buffer().lines()[0], "hello");
11010 assert_eq!(
11011 e.cursor(),
11012 (0, 4),
11013 "undo should clamp cursor to col 4 on 'hello'"
11014 );
11015 }
11016
11017 #[test]
11019 fn da_doublequote_eats_trailing_whitespace() {
11020 let mut e = editor_with("say \"hello\" there\n");
11021 e.jump_cursor(0, 6);
11022 run_keys(&mut e, "da\"");
11023 assert_eq!(e.buffer().lines()[0], "say there");
11024 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
11025 }
11026
11027 #[test]
11029 fn dab_cursor_col_clamped_after_delete() {
11030 let mut e = editor_with("fn x() {\n body\n}\n");
11031 e.jump_cursor(1, 4);
11032 run_keys(&mut e, "daB");
11033 assert_eq!(e.buffer().lines()[0], "fn x() ");
11034 assert_eq!(
11035 e.cursor(),
11036 (0, 6),
11037 "daB should leave cursor at col 6, not 7"
11038 );
11039 }
11040
11041 #[test]
11043 fn dib_preserves_surrounding_newlines() {
11044 let mut e = editor_with("{\n body\n}\n");
11045 e.jump_cursor(1, 4);
11046 run_keys(&mut e, "diB");
11047 assert_eq!(e.buffer().lines()[0], "{");
11048 assert_eq!(e.buffer().lines()[1], "}");
11049 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
11050 }
11051
11052 #[test]
11053 fn is_chord_pending_tracks_replace_state() {
11054 let mut e = editor_with("abc\n");
11055 assert!(!e.is_chord_pending());
11056 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
11058 assert!(e.is_chord_pending(), "engine should be pending after r");
11059 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
11061 assert!(
11062 !e.is_chord_pending(),
11063 "engine pending should clear after replace"
11064 );
11065 }
11066
11067 #[test]
11070 fn yiw_sets_lbr_rbr_marks_around_word() {
11071 let mut e = editor_with("hello world");
11074 run_keys(&mut e, "yiw");
11075 let lo = e.mark('[').expect("'[' must be set after yiw");
11076 let hi = e.mark(']').expect("']' must be set after yiw");
11077 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
11078 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
11079 }
11080
11081 #[test]
11082 fn yj_linewise_sets_marks_at_line_edges() {
11083 let mut e = editor_with("aaaaa\nbbbbb\nccc");
11086 run_keys(&mut e, "yj");
11087 let lo = e.mark('[').expect("'[' must be set after yj");
11088 let hi = e.mark(']').expect("']' must be set after yj");
11089 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
11090 assert_eq!(
11091 hi,
11092 (1, 4),
11093 "'] snaps to (bot_row, last_col) for linewise yank"
11094 );
11095 }
11096
11097 #[test]
11098 fn dd_sets_lbr_rbr_marks_to_cursor() {
11099 let mut e = editor_with("aaa\nbbb");
11102 run_keys(&mut e, "dd");
11103 let lo = e.mark('[').expect("'[' must be set after dd");
11104 let hi = e.mark(']').expect("']' must be set after dd");
11105 assert_eq!(lo, hi, "after delete both marks are at the same position");
11106 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
11107 }
11108
11109 #[test]
11110 fn dw_sets_lbr_rbr_marks_to_cursor() {
11111 let mut e = editor_with("hello world");
11114 run_keys(&mut e, "dw");
11115 let lo = e.mark('[').expect("'[' must be set after dw");
11116 let hi = e.mark(']').expect("']' must be set after dw");
11117 assert_eq!(lo, hi, "after delete both marks are at the same position");
11118 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
11119 }
11120
11121 #[test]
11122 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
11123 let mut e = editor_with("hello world");
11128 run_keys(&mut e, "cwfoo<Esc>");
11129 let lo = e.mark('[').expect("'[' must be set after cw");
11130 let hi = e.mark(']').expect("']' must be set after cw");
11131 assert_eq!(lo, (0, 0), "'[ should be start of change");
11132 assert_eq!(hi.0, 0, "'] should be on row 0");
11135 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
11136 }
11137
11138 #[test]
11139 fn cw_with_no_insertion_sets_marks_at_change_start() {
11140 let mut e = editor_with("hello world");
11143 run_keys(&mut e, "cw<Esc>");
11144 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
11145 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
11146 assert_eq!(lo.0, 0, "'[ should be on row 0");
11147 assert_eq!(hi.0, 0, "'] should be on row 0");
11148 assert_eq!(lo, hi, "marks coincide when insert is empty");
11150 }
11151
11152 #[test]
11153 fn p_charwise_sets_marks_around_pasted_text() {
11154 let mut e = editor_with("abc xyz");
11157 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
11160 let hi = e.mark(']').expect("']' set after charwise paste");
11161 assert!(lo <= hi, "'[ must not exceed ']'");
11162 assert_eq!(
11164 hi.1.wrapping_sub(lo.1),
11165 2,
11166 "'] - '[ should span 2 cols for a 3-char paste"
11167 );
11168 }
11169
11170 #[test]
11171 fn p_linewise_sets_marks_at_line_edges() {
11172 let mut e = editor_with("aaa\nbbb\nccc");
11175 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
11179 let hi = e.mark(']').expect("']' set after linewise paste");
11180 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
11181 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
11182 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
11183 }
11184
11185 #[test]
11186 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
11187 let mut e = editor_with("hello world");
11191 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
11195 assert_eq!(
11197 e.cursor(),
11198 (0, 4),
11199 "visual `[v`] should land on last yanked char"
11200 );
11201 assert_eq!(
11203 e.vim_mode(),
11204 crate::VimMode::Visual,
11205 "should be in Visual mode"
11206 );
11207 }
11208
11209 #[test]
11215 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
11216 let mut e = editor_with("hello\nworld\n");
11219 e.jump_cursor(0, 0);
11220 run_keys(&mut e, "iX<Esc>j`.");
11221 assert_eq!(
11222 e.cursor(),
11223 (0, 0),
11224 "dot mark should jump to the change-start (col 0), not post-insert col"
11225 );
11226 }
11227
11228 #[test]
11231 fn count_100g_clamps_to_last_content_row() {
11232 let mut e = editor_with("foo\nbar\nbaz\n");
11235 e.jump_cursor(0, 0);
11236 run_keys(&mut e, "100G");
11237 assert_eq!(
11238 e.cursor(),
11239 (2, 0),
11240 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
11241 );
11242 }
11243
11244 #[test]
11247 fn gi_resumes_last_insert_position() {
11248 let mut e = editor_with("world\nhello\n");
11254 e.jump_cursor(0, 0);
11255 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
11256 assert_eq!(
11257 e.vim_mode(),
11258 crate::VimMode::Normal,
11259 "should be in Normal mode after gi<Esc>"
11260 );
11261 assert_eq!(
11262 e.cursor(),
11263 (0, 1),
11264 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
11265 );
11266 }
11267
11268 #[test]
11272 fn visual_block_change_cursor_on_last_inserted_char() {
11273 let mut e = editor_with("foo\nbar\nbaz\n");
11277 e.jump_cursor(0, 0);
11278 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
11279 let lines = e.buffer().lines().to_vec();
11280 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
11281 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
11282 assert_eq!(
11283 e.cursor(),
11284 (0, 1),
11285 "cursor should be on last char of inserted 'ZZ' (col 1)"
11286 );
11287 }
11288
11289 #[test]
11294 fn register_blackhole_delete_preserves_unnamed_register() {
11295 let mut e = editor_with("foo bar baz\n");
11302 e.jump_cursor(0, 0);
11303 run_keys(&mut e, "yiww\"_dwbp");
11304 let lines = e.buffer().lines().to_vec();
11305 assert_eq!(
11306 lines[0], "ffoooo baz",
11307 "black-hole delete must not corrupt unnamed register"
11308 );
11309 assert_eq!(
11310 e.cursor(),
11311 (0, 3),
11312 "cursor should be on last pasted char (col 3)"
11313 );
11314 }
11315
11316 #[test]
11319 fn after_z_zz_sets_viewport_pinned() {
11320 let mut e = editor_with("a\nb\nc\nd\ne");
11321 e.jump_cursor(2, 0);
11322 e.after_z('z', 1);
11323 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
11324 }
11325
11326 #[test]
11327 fn after_z_zo_opens_fold_at_cursor() {
11328 let mut e = editor_with("a\nb\nc\nd");
11329 e.buffer_mut().add_fold(1, 2, true);
11330 e.jump_cursor(1, 0);
11331 e.after_z('o', 1);
11332 assert!(
11333 !e.buffer().folds()[0].closed,
11334 "zo must open the fold at the cursor row"
11335 );
11336 }
11337
11338 #[test]
11339 fn after_z_zm_closes_all_folds() {
11340 let mut e = editor_with("a\nb\nc\nd\ne\nf");
11341 e.buffer_mut().add_fold(0, 1, false);
11342 e.buffer_mut().add_fold(4, 5, false);
11343 e.after_z('M', 1);
11344 assert!(
11345 e.buffer().folds().iter().all(|f| f.closed),
11346 "zM must close all folds"
11347 );
11348 }
11349
11350 #[test]
11351 fn after_z_zd_removes_fold_at_cursor() {
11352 let mut e = editor_with("a\nb\nc\nd");
11353 e.buffer_mut().add_fold(1, 2, true);
11354 e.jump_cursor(1, 0);
11355 e.after_z('d', 1);
11356 assert!(
11357 e.buffer().folds().is_empty(),
11358 "zd must remove the fold at the cursor row"
11359 );
11360 }
11361
11362 #[test]
11363 fn after_z_zf_in_visual_creates_fold() {
11364 let mut e = editor_with("a\nb\nc\nd\ne");
11365 e.jump_cursor(1, 0);
11367 run_keys(&mut e, "V2j");
11368 e.after_z('f', 1);
11370 let folds = e.buffer().folds();
11371 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
11372 assert_eq!(folds[0].start_row, 1);
11373 assert_eq!(folds[0].end_row, 3);
11374 assert!(folds[0].closed);
11375 }
11376
11377 #[test]
11380 fn apply_op_motion_dw_deletes_word() {
11381 let mut e = editor_with("hello world");
11383 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
11384 assert_eq!(
11385 e.buffer().lines().first().cloned().unwrap_or_default(),
11386 "world"
11387 );
11388 }
11389
11390 #[test]
11391 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
11392 let mut e = editor_with("hello world");
11394 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
11395 let line = e.buffer().lines().first().cloned().unwrap_or_default();
11398 assert!(
11399 line.starts_with(' ') || line == " world",
11400 "cw quirk: got {line:?}"
11401 );
11402 assert_eq!(e.vim_mode(), VimMode::Insert);
11403 }
11404
11405 #[test]
11406 fn apply_op_double_dd_deletes_line() {
11407 let mut e = editor_with("line1\nline2\nline3");
11408 e.apply_op_double(crate::vim::Operator::Delete, 1);
11410 let lines: Vec<_> = e.buffer().lines().to_vec();
11411 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
11412 }
11413
11414 #[test]
11415 fn apply_op_double_yy_does_not_modify_buffer() {
11416 let mut e = editor_with("hello");
11417 e.apply_op_double(crate::vim::Operator::Yank, 1);
11418 assert_eq!(
11419 e.buffer().lines().first().cloned().unwrap_or_default(),
11420 "hello"
11421 );
11422 }
11423
11424 #[test]
11425 fn apply_op_double_dd_count2_deletes_two_lines() {
11426 let mut e = editor_with("line1\nline2\nline3");
11427 e.apply_op_double(crate::vim::Operator::Delete, 2);
11428 let lines: Vec<_> = e.buffer().lines().to_vec();
11429 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
11430 }
11431
11432 #[test]
11433 fn apply_op_motion_unknown_key_is_noop() {
11434 let mut e = editor_with("hello");
11436 let before = e.cursor();
11437 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
11439 assert_eq!(
11440 e.buffer().lines().first().cloned().unwrap_or_default(),
11441 "hello"
11442 );
11443 }
11444
11445 #[test]
11448 fn apply_op_find_dfx_deletes_to_x() {
11449 let mut e = editor_with("hello x world");
11451 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
11452 assert_eq!(
11453 e.buffer().lines().first().cloned().unwrap_or_default(),
11454 " world",
11455 "dfx must delete 'hello x'"
11456 );
11457 }
11458
11459 #[test]
11460 fn apply_op_find_dtx_deletes_up_to_x() {
11461 let mut e = editor_with("hello x world");
11463 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
11464 assert_eq!(
11465 e.buffer().lines().first().cloned().unwrap_or_default(),
11466 "x world",
11467 "dtx must delete 'hello ' leaving 'x world'"
11468 );
11469 }
11470
11471 #[test]
11472 fn apply_op_find_records_last_find() {
11473 let mut e = editor_with("hello x world");
11475 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
11476 let _ = e.cursor(); }
11483
11484 #[test]
11487 fn apply_op_text_obj_diw_deletes_word() {
11488 let mut e = editor_with("hello world");
11490 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', true, 1);
11491 let line = e.buffer().lines().first().cloned().unwrap_or_default();
11492 assert!(
11497 !line.contains("hello"),
11498 "diw must delete 'hello', remaining: {line:?}"
11499 );
11500 }
11501
11502 #[test]
11503 fn apply_op_text_obj_daw_deletes_around_word() {
11504 let mut e = editor_with("hello world");
11506 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', false, 1);
11507 let line = e.buffer().lines().first().cloned().unwrap_or_default();
11508 assert!(
11509 !line.contains("hello"),
11510 "daw must delete 'hello' and surrounding space, remaining: {line:?}"
11511 );
11512 }
11513
11514 #[test]
11515 fn apply_op_text_obj_invalid_char_no_op() {
11516 let mut e = editor_with("hello world");
11518 let before = e.buffer().as_string();
11519 e.apply_op_text_obj(crate::vim::Operator::Delete, 'X', true, 1);
11520 assert_eq!(
11521 e.buffer().as_string(),
11522 before,
11523 "unknown text-object char must be a no-op"
11524 );
11525 }
11526
11527 #[test]
11530 fn apply_op_g_dgg_deletes_to_top() {
11531 let mut e = editor_with("line1\nline2\nline3");
11544 e.jump_cursor(1, 0);
11546 e.apply_op_g(crate::vim::Operator::Delete, 'g', 1);
11549 let lines: Vec<_> = e.buffer().lines().to_vec();
11550 assert_eq!(lines, vec!["line3"], "dgg must delete to file top");
11551 }
11552
11553 #[test]
11554 fn apply_op_g_dge_deletes_word_end_back() {
11555 let mut e = editor_with("hello world");
11568 let before = e.buffer().as_string();
11569 e.apply_op_g(crate::vim::Operator::Delete, 'X', 1);
11571 assert_eq!(
11572 e.buffer().as_string(),
11573 before,
11574 "apply_op_g with unknown char must be a no-op"
11575 );
11576 e.apply_op_g(crate::vim::Operator::Delete, 'e', 1);
11578 }
11580
11581 #[test]
11582 fn apply_op_g_dgj_deletes_screen_down() {
11583 let mut e = editor_with("line1\nline2\nline3");
11586 e.apply_op_g(crate::vim::Operator::Delete, 'j', 1);
11587 let lines: Vec<_> = e.buffer().lines().to_vec();
11588 assert_eq!(lines, vec!["line3"], "dgj must delete current+next line");
11590 }
11591
11592 fn blank_editor() -> Editor {
11595 Editor::new(
11596 hjkl_buffer::Buffer::new(),
11597 crate::types::DefaultHost::new(),
11598 crate::types::Options::default(),
11599 )
11600 }
11601
11602 #[test]
11603 fn set_pending_register_valid_letter_sets_field() {
11604 let mut e = blank_editor();
11605 assert!(e.vim.pending_register.is_none());
11606 e.set_pending_register('a');
11607 assert_eq!(e.vim.pending_register, Some('a'));
11608 }
11609
11610 #[test]
11611 fn set_pending_register_invalid_char_no_op() {
11612 let mut e = blank_editor();
11613 e.set_pending_register('!');
11614 assert!(
11615 e.vim.pending_register.is_none(),
11616 "invalid register char must not set pending_register"
11617 );
11618 }
11619
11620 #[test]
11621 fn set_pending_register_special_plus_sets_field() {
11622 let mut e = blank_editor();
11624 e.set_pending_register('+');
11625 assert_eq!(e.vim.pending_register, Some('+'));
11626 }
11627
11628 #[test]
11629 fn set_pending_register_star_sets_field() {
11630 let mut e = blank_editor();
11632 e.set_pending_register('*');
11633 assert_eq!(e.vim.pending_register, Some('*'));
11634 }
11635
11636 #[test]
11637 fn set_pending_register_underscore_sets_field() {
11638 let mut e = blank_editor();
11640 e.set_pending_register('_');
11641 assert_eq!(e.vim.pending_register, Some('_'));
11642 }
11643}