1use crate::VimMode;
75use crate::input::{Input, Key};
76
77use crate::editor::Editor;
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum Mode {
83 #[default]
84 Normal,
85 Insert,
86 Visual,
87 VisualLine,
88 VisualBlock,
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Default)]
96enum Pending {
97 #[default]
98 None,
99 Op { op: Operator, count1: usize },
102 OpTextObj {
104 op: Operator,
105 count1: usize,
106 inner: bool,
107 },
108 OpG { op: Operator, count1: usize },
110 G,
112 Find { forward: bool, till: bool },
114 OpFind {
116 op: Operator,
117 count1: usize,
118 forward: bool,
119 till: bool,
120 },
121 Replace,
123 VisualTextObj { inner: bool },
126 Z,
128 SetMark,
130 GotoMarkLine,
133 GotoMarkChar,
136 SelectRegister,
139 RecordMacroTarget,
143 PlayMacroTarget { count: usize },
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq)]
152pub enum Operator {
153 Delete,
154 Change,
155 Yank,
156 Uppercase,
159 Lowercase,
161 ToggleCase,
165 Indent,
170 Outdent,
173 Fold,
177 Reflow,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub enum Motion {
186 Left,
187 Right,
188 Up,
189 Down,
190 WordFwd,
191 BigWordFwd,
192 WordBack,
193 BigWordBack,
194 WordEnd,
195 BigWordEnd,
196 WordEndBack,
198 BigWordEndBack,
200 LineStart,
201 FirstNonBlank,
202 LineEnd,
203 FileTop,
204 FileBottom,
205 Find {
206 ch: char,
207 forward: bool,
208 till: bool,
209 },
210 FindRepeat {
211 reverse: bool,
212 },
213 MatchBracket,
214 WordAtCursor {
215 forward: bool,
216 whole_word: bool,
219 },
220 SearchNext {
222 reverse: bool,
223 },
224 ViewportTop,
226 ViewportMiddle,
228 ViewportBottom,
230 LastNonBlank,
232 LineMiddle,
235 ParagraphPrev,
237 ParagraphNext,
239 SentencePrev,
241 SentenceNext,
243 ScreenDown,
246 ScreenUp,
248}
249
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub enum TextObject {
252 Word {
253 big: bool,
254 },
255 Quote(char),
256 Bracket(char),
257 Paragraph,
258 XmlTag,
262 Sentence,
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub enum MotionKind {
272 Exclusive,
274 Inclusive,
276 Linewise,
278}
279
280#[derive(Debug, Clone)]
284enum LastChange {
285 OpMotion {
287 op: Operator,
288 motion: Motion,
289 count: usize,
290 inserted: Option<String>,
291 },
292 OpTextObj {
294 op: Operator,
295 obj: TextObject,
296 inner: bool,
297 inserted: Option<String>,
298 },
299 LineOp {
301 op: Operator,
302 count: usize,
303 inserted: Option<String>,
304 },
305 CharDel { forward: bool, count: usize },
307 ReplaceChar { ch: char, count: usize },
309 ToggleCase { count: usize },
311 JoinLine { count: usize },
313 Paste { before: bool, count: usize },
315 DeleteToEol { inserted: Option<String> },
317 OpenLine { above: bool, inserted: String },
319 InsertAt {
321 entry: InsertEntry,
322 inserted: String,
323 count: usize,
324 },
325}
326
327#[derive(Debug, Clone, Copy, PartialEq, Eq)]
328enum InsertEntry {
329 I,
330 A,
331 ShiftI,
332 ShiftA,
333}
334
335#[derive(Default)]
338pub struct VimState {
339 mode: Mode,
340 pending: Pending,
341 count: usize,
342 last_find: Option<(char, bool, bool)>,
344 last_change: Option<LastChange>,
345 insert_session: Option<InsertSession>,
347 pub(super) visual_anchor: (usize, usize),
351 pub(super) visual_line_anchor: usize,
353 pub(super) block_anchor: (usize, usize),
356 pub(super) block_vcol: usize,
362 pub(super) sticky_col: Option<usize>,
370 pub(super) yank_linewise: bool,
372 pub(super) pending_register: Option<char>,
375 pub(super) recording_macro: Option<char>,
379 pub(super) recording_keys: Vec<crate::input::Input>,
384 pub(super) replaying_macro: bool,
387 pub(super) last_macro: Option<char>,
389 #[doc(hidden)]
392 pub last_edit_pos: Option<(usize, usize)>,
393 pub(super) change_list: Vec<(usize, usize)>,
397 pub(super) change_list_cursor: Option<usize>,
400 pub(super) last_visual: Option<LastVisual>,
403 pub(super) viewport_pinned: bool,
407 replaying: bool,
409 one_shot_normal: bool,
412 pub(super) search_prompt: Option<SearchPrompt>,
414 pub(super) last_search: Option<String>,
418 pub(super) last_search_forward: bool,
422 #[doc(hidden)]
427 pub jump_back: Vec<(usize, usize)>,
428 pub(super) jump_fwd: Vec<(usize, usize)>,
431 #[doc(hidden)]
436 pub marks: std::collections::HashMap<char, (usize, usize)>,
437 pub(super) insert_pending_register: bool,
441 pub(super) search_history: Vec<String>,
445 pub(super) search_history_cursor: Option<usize>,
450}
451
452const SEARCH_HISTORY_MAX: usize = 100;
453pub(crate) const CHANGE_LIST_MAX: usize = 100;
454
455#[derive(Debug, Clone)]
458pub struct SearchPrompt {
459 pub text: String,
460 pub cursor: usize,
461 pub forward: bool,
462}
463
464#[derive(Debug, Clone)]
465struct InsertSession {
466 count: usize,
467 row_min: usize,
469 row_max: usize,
470 before_lines: Vec<String>,
474 reason: InsertReason,
475}
476
477#[derive(Debug, Clone)]
478enum InsertReason {
479 Enter(InsertEntry),
481 Open { above: bool },
483 AfterChange,
486 DeleteToEol,
488 ReplayOnly,
491 BlockEdge { top: usize, bot: usize, col: usize },
495 Replace,
499}
500
501#[derive(Debug, Clone, Copy)]
511pub(super) struct LastVisual {
512 pub mode: Mode,
513 pub anchor: (usize, usize),
514 pub cursor: (usize, usize),
515 pub block_vcol: usize,
516}
517
518impl VimState {
519 pub fn public_mode(&self) -> VimMode {
520 match self.mode {
521 Mode::Normal => VimMode::Normal,
522 Mode::Insert => VimMode::Insert,
523 Mode::Visual => VimMode::Visual,
524 Mode::VisualLine => VimMode::VisualLine,
525 Mode::VisualBlock => VimMode::VisualBlock,
526 }
527 }
528
529 pub fn force_normal(&mut self) {
530 self.mode = Mode::Normal;
531 self.pending = Pending::None;
532 self.count = 0;
533 self.insert_session = None;
534 }
535
536 pub fn is_visual(&self) -> bool {
537 matches!(
538 self.mode,
539 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
540 )
541 }
542
543 pub fn is_visual_char(&self) -> bool {
544 self.mode == Mode::Visual
545 }
546
547 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
548 self.visual_anchor = anchor;
549 self.mode = Mode::Visual;
550 }
551}
552
553fn enter_search(ed: &mut Editor<'_>, forward: bool) {
559 ed.vim.search_prompt = Some(SearchPrompt {
560 text: String::new(),
561 cursor: 0,
562 forward,
563 });
564 ed.vim.search_history_cursor = None;
565 ed.buffer_mut().set_search_pattern(None);
566}
567
568fn push_search_pattern(ed: &mut Editor<'_>, pattern: &str) {
573 let compiled = if pattern.is_empty() {
574 None
575 } else {
576 let effective: std::borrow::Cow<'_, str> = if ed.settings().ignore_case {
580 std::borrow::Cow::Owned(format!("(?i){pattern}"))
581 } else {
582 std::borrow::Cow::Borrowed(pattern)
583 };
584 regex::Regex::new(&effective).ok()
585 };
586 ed.buffer_mut().set_search_pattern(compiled);
587}
588
589fn step_search_prompt(ed: &mut Editor<'_>, input: Input) -> bool {
590 let history_dir = match (input.key, input.ctrl) {
594 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
595 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
596 _ => None,
597 };
598 if let Some(dir) = history_dir {
599 walk_search_history(ed, dir);
600 return true;
601 }
602 match input.key {
603 Key::Esc => {
604 let text = ed
607 .vim
608 .search_prompt
609 .take()
610 .map(|p| p.text)
611 .unwrap_or_default();
612 if !text.is_empty() {
613 ed.vim.last_search = Some(text);
614 }
615 ed.vim.search_history_cursor = None;
616 }
617 Key::Enter => {
618 let prompt = ed.vim.search_prompt.take();
619 if let Some(p) = prompt {
620 let pattern = if p.text.is_empty() {
623 ed.vim.last_search.clone()
624 } else {
625 Some(p.text.clone())
626 };
627 if let Some(pattern) = pattern {
628 push_search_pattern(ed, &pattern);
629 let pre = ed.cursor();
630 if p.forward {
631 ed.buffer_mut().search_forward(true);
632 } else {
633 ed.buffer_mut().search_backward(true);
634 }
635 ed.push_buffer_cursor_to_textarea();
636 if ed.cursor() != pre {
637 push_jump(ed, pre);
638 }
639 record_search_history(ed, &pattern);
640 ed.vim.last_search = Some(pattern);
641 ed.vim.last_search_forward = p.forward;
642 }
643 }
644 ed.vim.search_history_cursor = None;
645 }
646 Key::Backspace => {
647 ed.vim.search_history_cursor = None;
648 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
649 if p.text.pop().is_some() {
650 p.cursor = p.text.chars().count();
651 Some(p.text.clone())
652 } else {
653 None
654 }
655 });
656 if let Some(text) = new_text {
657 push_search_pattern(ed, &text);
658 }
659 }
660 Key::Char(c) => {
661 ed.vim.search_history_cursor = None;
662 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
663 p.text.push(c);
664 p.cursor = p.text.chars().count();
665 p.text.clone()
666 });
667 if let Some(text) = new_text {
668 push_search_pattern(ed, &text);
669 }
670 }
671 _ => {}
672 }
673 true
674}
675
676fn walk_change_list(ed: &mut Editor<'_>, dir: isize, count: usize) {
680 if ed.vim.change_list.is_empty() {
681 return;
682 }
683 let len = ed.vim.change_list.len();
684 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
685 (None, -1) => len as isize - 1,
686 (None, 1) => return, (Some(i), -1) => i as isize - 1,
688 (Some(i), 1) => i as isize + 1,
689 _ => return,
690 };
691 for _ in 1..count {
692 let next = idx + dir;
693 if next < 0 || next >= len as isize {
694 break;
695 }
696 idx = next;
697 }
698 if idx < 0 || idx >= len as isize {
699 return;
700 }
701 let idx = idx as usize;
702 ed.vim.change_list_cursor = Some(idx);
703 let (row, col) = ed.vim.change_list[idx];
704 ed.jump_cursor(row, col);
705}
706
707fn record_search_history(ed: &mut Editor<'_>, pattern: &str) {
711 if pattern.is_empty() {
712 return;
713 }
714 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
715 return;
716 }
717 ed.vim.search_history.push(pattern.to_string());
718 let len = ed.vim.search_history.len();
719 if len > SEARCH_HISTORY_MAX {
720 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
721 }
722}
723
724fn walk_search_history(ed: &mut Editor<'_>, dir: isize) {
730 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
731 return;
732 }
733 let len = ed.vim.search_history.len();
734 let next_idx = match (ed.vim.search_history_cursor, dir) {
735 (None, -1) => Some(len - 1),
736 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
738 (Some(i), 1) if i + 1 < len => Some(i + 1),
739 _ => None,
740 };
741 let Some(idx) = next_idx else {
742 return;
743 };
744 ed.vim.search_history_cursor = Some(idx);
745 let text = ed.vim.search_history[idx].clone();
746 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
747 prompt.cursor = text.chars().count();
748 prompt.text = text.clone();
749 }
750 push_search_pattern(ed, &text);
751}
752
753pub fn step(ed: &mut Editor<'_>, input: Input) -> bool {
754 ed.sync_buffer_content_from_textarea();
759 if ed.vim.recording_macro.is_some()
764 && !ed.vim.replaying_macro
765 && matches!(ed.vim.pending, Pending::None)
766 && ed.vim.mode != Mode::Insert
767 && input.key == Key::Char('q')
768 && !input.ctrl
769 && !input.alt
770 {
771 let reg = ed.vim.recording_macro.take().unwrap();
772 let keys = std::mem::take(&mut ed.vim.recording_keys);
773 let text = crate::input::encode_macro(&keys);
774 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
775 return true;
776 }
777 if ed.vim.search_prompt.is_some() {
779 return step_search_prompt(ed, input);
780 }
781 let pending_was_macro_chord = matches!(
785 ed.vim.pending,
786 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
787 );
788 let was_insert = ed.vim.mode == Mode::Insert;
789 let pre_visual_snapshot = match ed.vim.mode {
792 Mode::Visual => Some(LastVisual {
793 mode: Mode::Visual,
794 anchor: ed.vim.visual_anchor,
795 cursor: ed.cursor(),
796 block_vcol: 0,
797 }),
798 Mode::VisualLine => Some(LastVisual {
799 mode: Mode::VisualLine,
800 anchor: (ed.vim.visual_line_anchor, 0),
801 cursor: ed.cursor(),
802 block_vcol: 0,
803 }),
804 Mode::VisualBlock => Some(LastVisual {
805 mode: Mode::VisualBlock,
806 anchor: ed.vim.block_anchor,
807 cursor: ed.cursor(),
808 block_vcol: ed.vim.block_vcol,
809 }),
810 _ => None,
811 };
812 let consumed = match ed.vim.mode {
813 Mode::Insert => step_insert(ed, input),
814 _ => step_normal(ed, input),
815 };
816 if let Some(snap) = pre_visual_snapshot
817 && !matches!(
818 ed.vim.mode,
819 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
820 )
821 {
822 ed.vim.last_visual = Some(snap);
823 }
824 if !was_insert
828 && ed.vim.one_shot_normal
829 && ed.vim.mode == Mode::Normal
830 && matches!(ed.vim.pending, Pending::None)
831 {
832 ed.vim.one_shot_normal = false;
833 ed.vim.mode = Mode::Insert;
834 }
835 ed.sync_buffer_content_from_textarea();
841 if !ed.vim.viewport_pinned {
845 ed.ensure_cursor_in_scrolloff();
846 }
847 ed.vim.viewport_pinned = false;
848 if ed.vim.recording_macro.is_some()
853 && !ed.vim.replaying_macro
854 && input.key != Key::Char('q')
855 && !pending_was_macro_chord
856 {
857 ed.vim.recording_keys.push(input);
858 }
859 consumed
860}
861
862fn step_insert(ed: &mut Editor<'_>, input: Input) -> bool {
865 if ed.vim.insert_pending_register {
869 ed.vim.insert_pending_register = false;
870 if let Key::Char(c) = input.key
871 && !input.ctrl
872 {
873 insert_register_text(ed, c);
874 }
875 return true;
876 }
877
878 if input.key == Key::Esc {
879 finish_insert_session(ed);
880 ed.vim.mode = Mode::Normal;
881 let col = ed.cursor().1;
886 if col > 0 {
887 ed.buffer_mut().move_left(1);
888 ed.push_buffer_cursor_to_textarea();
889 }
890 ed.vim.sticky_col = Some(ed.cursor().1);
891 return true;
892 }
893
894 if input.ctrl {
896 match input.key {
897 Key::Char('w') => {
898 use hjkl_buffer::{Edit, MotionKind};
899 ed.sync_buffer_content_from_textarea();
900 let cursor = ed.buffer().cursor();
901 if cursor.row == 0 && cursor.col == 0 {
902 return true;
903 }
904 ed.buffer_mut().move_word_back(false, 1);
907 let word_start = ed.buffer().cursor();
908 if word_start == cursor {
909 return true;
910 }
911 ed.buffer_mut().set_cursor(cursor);
912 ed.mutate_edit(Edit::DeleteRange {
913 start: word_start,
914 end: cursor,
915 kind: MotionKind::Char,
916 });
917 ed.push_buffer_cursor_to_textarea();
918 return true;
919 }
920 Key::Char('u') => {
921 use hjkl_buffer::{Edit, MotionKind, Position};
922 ed.sync_buffer_content_from_textarea();
923 let cursor = ed.buffer().cursor();
924 if cursor.col > 0 {
925 ed.mutate_edit(Edit::DeleteRange {
926 start: Position::new(cursor.row, 0),
927 end: cursor,
928 kind: MotionKind::Char,
929 });
930 ed.push_buffer_cursor_to_textarea();
931 }
932 return true;
933 }
934 Key::Char('h') => {
935 use hjkl_buffer::{Edit, MotionKind, Position};
936 ed.sync_buffer_content_from_textarea();
937 let cursor = ed.buffer().cursor();
938 if cursor.col > 0 {
939 ed.mutate_edit(Edit::DeleteRange {
940 start: Position::new(cursor.row, cursor.col - 1),
941 end: cursor,
942 kind: MotionKind::Char,
943 });
944 } else if cursor.row > 0 {
945 let prev_row = cursor.row - 1;
946 let prev_chars = ed
947 .buffer()
948 .line(prev_row)
949 .map(|l| l.chars().count())
950 .unwrap_or(0);
951 ed.mutate_edit(Edit::JoinLines {
952 row: prev_row,
953 count: 1,
954 with_space: false,
955 });
956 ed.buffer_mut()
957 .set_cursor(Position::new(prev_row, prev_chars));
958 }
959 ed.push_buffer_cursor_to_textarea();
960 return true;
961 }
962 Key::Char('o') => {
963 ed.vim.one_shot_normal = true;
966 ed.vim.mode = Mode::Normal;
967 return true;
968 }
969 Key::Char('r') => {
970 ed.vim.insert_pending_register = true;
973 return true;
974 }
975 Key::Char('t') => {
976 let (row, col) = ed.cursor();
981 let sw = ed.settings().shiftwidth;
982 indent_rows(ed, row, row, 1);
983 ed.jump_cursor(row, col + sw);
984 return true;
985 }
986 Key::Char('d') => {
987 let (row, col) = ed.cursor();
991 let before_len = ed.buffer().lines()[row].len();
992 outdent_rows(ed, row, row, 1);
993 let after_len = ed.buffer().lines()[row].len();
994 let stripped = before_len.saturating_sub(after_len);
995 let new_col = col.saturating_sub(stripped);
996 ed.jump_cursor(row, new_col);
997 return true;
998 }
999 _ => {}
1000 }
1001 }
1002
1003 let (row, _) = ed.cursor();
1006 if let Some(ref mut session) = ed.vim.insert_session {
1007 session.row_min = session.row_min.min(row);
1008 session.row_max = session.row_max.max(row);
1009 }
1010 let mutated = handle_insert_key(ed, input);
1011 if mutated {
1012 ed.mark_content_dirty();
1013 let (row, _) = ed.cursor();
1014 if let Some(ref mut session) = ed.vim.insert_session {
1015 session.row_min = session.row_min.min(row);
1016 session.row_max = session.row_max.max(row);
1017 }
1018 }
1019 true
1020}
1021
1022fn insert_register_text(ed: &mut Editor<'_>, selector: char) {
1027 use hjkl_buffer::{Edit, Position};
1028 let text = match ed.registers().read(selector) {
1029 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1030 _ => return,
1031 };
1032 ed.sync_buffer_content_from_textarea();
1033 let cursor = ed.buffer().cursor();
1034 ed.mutate_edit(Edit::InsertStr {
1035 at: cursor,
1036 text: text.clone(),
1037 });
1038 let mut row = cursor.row;
1041 let mut col = cursor.col;
1042 for ch in text.chars() {
1043 if ch == '\n' {
1044 row += 1;
1045 col = 0;
1046 } else {
1047 col += 1;
1048 }
1049 }
1050 ed.buffer_mut().set_cursor(Position::new(row, col));
1051 ed.push_buffer_cursor_to_textarea();
1052 ed.mark_content_dirty();
1053 if let Some(ref mut session) = ed.vim.insert_session {
1054 session.row_min = session.row_min.min(row);
1055 session.row_max = session.row_max.max(row);
1056 }
1057}
1058
1059fn handle_insert_key(ed: &mut Editor<'_>, input: Input) -> bool {
1066 use hjkl_buffer::{Edit, MotionKind, Position};
1067 ed.sync_buffer_content_from_textarea();
1068 let cursor = ed.buffer().cursor();
1069 let line_chars = ed
1070 .buffer()
1071 .line(cursor.row)
1072 .map(|l| l.chars().count())
1073 .unwrap_or(0);
1074 let in_replace = matches!(
1078 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1079 Some(InsertReason::Replace)
1080 );
1081 let mutated = match input.key {
1082 Key::Char(c) if in_replace && cursor.col < line_chars => {
1083 ed.mutate_edit(Edit::DeleteRange {
1084 start: cursor,
1085 end: Position::new(cursor.row, cursor.col + 1),
1086 kind: MotionKind::Char,
1087 });
1088 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1089 true
1090 }
1091 Key::Char(c) => {
1092 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1093 true
1094 }
1095 Key::Enter => {
1096 ed.mutate_edit(Edit::InsertStr {
1097 at: cursor,
1098 text: "\n".into(),
1099 });
1100 true
1101 }
1102 Key::Tab => {
1103 ed.mutate_edit(Edit::InsertChar {
1104 at: cursor,
1105 ch: '\t',
1106 });
1107 true
1108 }
1109 Key::Backspace => {
1110 if cursor.col > 0 {
1111 ed.mutate_edit(Edit::DeleteRange {
1112 start: Position::new(cursor.row, cursor.col - 1),
1113 end: cursor,
1114 kind: MotionKind::Char,
1115 });
1116 true
1117 } else if cursor.row > 0 {
1118 let prev_row = cursor.row - 1;
1119 let prev_chars = ed
1120 .buffer()
1121 .line(prev_row)
1122 .map(|l| l.chars().count())
1123 .unwrap_or(0);
1124 ed.mutate_edit(Edit::JoinLines {
1125 row: prev_row,
1126 count: 1,
1127 with_space: false,
1128 });
1129 ed.buffer_mut()
1130 .set_cursor(Position::new(prev_row, prev_chars));
1131 true
1132 } else {
1133 false
1134 }
1135 }
1136 Key::Delete => {
1137 if cursor.col < line_chars {
1138 ed.mutate_edit(Edit::DeleteRange {
1139 start: cursor,
1140 end: Position::new(cursor.row, cursor.col + 1),
1141 kind: MotionKind::Char,
1142 });
1143 true
1144 } else if cursor.row + 1 < ed.buffer().row_count() {
1145 ed.mutate_edit(Edit::JoinLines {
1146 row: cursor.row,
1147 count: 1,
1148 with_space: false,
1149 });
1150 ed.buffer_mut().set_cursor(cursor);
1151 true
1152 } else {
1153 false
1154 }
1155 }
1156 Key::Left => {
1157 ed.buffer_mut().move_left(1);
1158 false
1159 }
1160 Key::Right => {
1161 ed.buffer_mut().move_right_to_end(1);
1164 false
1165 }
1166 Key::Up => {
1167 ed.buffer_mut().move_up(1);
1168 false
1169 }
1170 Key::Down => {
1171 ed.buffer_mut().move_down(1);
1172 false
1173 }
1174 Key::Home => {
1175 ed.buffer_mut().move_line_start();
1176 false
1177 }
1178 Key::End => {
1179 ed.buffer_mut().move_line_end();
1180 false
1181 }
1182 Key::PageUp => {
1183 let rows = viewport_full_rows(ed, 1) as isize;
1187 scroll_cursor_rows(ed, -rows);
1188 return false;
1189 }
1190 Key::PageDown => {
1191 let rows = viewport_full_rows(ed, 1) as isize;
1192 scroll_cursor_rows(ed, rows);
1193 return false;
1194 }
1195 _ => false,
1198 };
1199 ed.push_buffer_cursor_to_textarea();
1200 mutated
1201}
1202
1203fn finish_insert_session(ed: &mut Editor<'_>) {
1204 let Some(session) = ed.vim.insert_session.take() else {
1205 return;
1206 };
1207 let lines = ed.buffer().lines();
1208 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1212 let before_end = session
1213 .row_max
1214 .min(session.before_lines.len().saturating_sub(1));
1215 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1216 session.before_lines[session.row_min..=before_end].join("\n")
1217 } else {
1218 String::new()
1219 };
1220 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1221 lines[session.row_min..=after_end].join("\n")
1222 } else {
1223 String::new()
1224 };
1225 let inserted = extract_inserted(&before, &after);
1226 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1227 use hjkl_buffer::{Edit, Position};
1228 for _ in 0..session.count - 1 {
1229 let (row, col) = ed.cursor();
1230 ed.mutate_edit(Edit::InsertStr {
1231 at: Position::new(row, col),
1232 text: inserted.clone(),
1233 });
1234 }
1235 }
1236 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1237 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1238 use hjkl_buffer::{Edit, Position};
1239 for r in (top + 1)..=bot {
1240 let line_len = ed.buffer().line(r).map(|l| l.chars().count()).unwrap_or(0);
1241 if col > line_len {
1242 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1245 ed.mutate_edit(Edit::InsertStr {
1246 at: Position::new(r, line_len),
1247 text: pad,
1248 });
1249 }
1250 ed.mutate_edit(Edit::InsertStr {
1251 at: Position::new(r, col),
1252 text: inserted.clone(),
1253 });
1254 }
1255 ed.buffer_mut().set_cursor(Position::new(top, col));
1256 ed.push_buffer_cursor_to_textarea();
1257 }
1258 return;
1259 }
1260 if ed.vim.replaying {
1261 return;
1262 }
1263 match session.reason {
1264 InsertReason::Enter(entry) => {
1265 ed.vim.last_change = Some(LastChange::InsertAt {
1266 entry,
1267 inserted,
1268 count: session.count,
1269 });
1270 }
1271 InsertReason::Open { above } => {
1272 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1273 }
1274 InsertReason::AfterChange => {
1275 if let Some(
1276 LastChange::OpMotion { inserted: ins, .. }
1277 | LastChange::OpTextObj { inserted: ins, .. }
1278 | LastChange::LineOp { inserted: ins, .. },
1279 ) = ed.vim.last_change.as_mut()
1280 {
1281 *ins = Some(inserted);
1282 }
1283 }
1284 InsertReason::DeleteToEol => {
1285 ed.vim.last_change = Some(LastChange::DeleteToEol {
1286 inserted: Some(inserted),
1287 });
1288 }
1289 InsertReason::ReplayOnly => {}
1290 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1291 InsertReason::Replace => {
1292 ed.vim.last_change = Some(LastChange::DeleteToEol {
1297 inserted: Some(inserted),
1298 });
1299 }
1300 }
1301}
1302
1303fn begin_insert(ed: &mut Editor<'_>, count: usize, reason: InsertReason) {
1304 let record = !matches!(reason, InsertReason::ReplayOnly);
1305 if record {
1306 ed.push_undo();
1307 }
1308 let reason = if ed.vim.replaying {
1309 InsertReason::ReplayOnly
1310 } else {
1311 reason
1312 };
1313 let (row, _) = ed.cursor();
1314 ed.vim.insert_session = Some(InsertSession {
1315 count,
1316 row_min: row,
1317 row_max: row,
1318 before_lines: ed.buffer().lines().to_vec(),
1319 reason,
1320 });
1321 ed.vim.mode = Mode::Insert;
1322}
1323
1324fn step_normal(ed: &mut Editor<'_>, input: Input) -> bool {
1327 if let Key::Char(d @ '0'..='9') = input.key
1329 && !input.ctrl
1330 && !input.alt
1331 && !matches!(
1332 ed.vim.pending,
1333 Pending::Replace
1334 | Pending::Find { .. }
1335 | Pending::OpFind { .. }
1336 | Pending::VisualTextObj { .. }
1337 )
1338 && (d != '0' || ed.vim.count > 0)
1339 {
1340 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1341 return true;
1342 }
1343
1344 match std::mem::take(&mut ed.vim.pending) {
1346 Pending::Replace => return handle_replace(ed, input),
1347 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1348 Pending::OpFind {
1349 op,
1350 count1,
1351 forward,
1352 till,
1353 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1354 Pending::G => return handle_after_g(ed, input),
1355 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1356 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1357 Pending::OpTextObj { op, count1, inner } => {
1358 return handle_text_object(ed, input, op, count1, inner);
1359 }
1360 Pending::VisualTextObj { inner } => {
1361 return handle_visual_text_obj(ed, input, inner);
1362 }
1363 Pending::Z => return handle_after_z(ed, input),
1364 Pending::SetMark => return handle_set_mark(ed, input),
1365 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1366 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1367 Pending::SelectRegister => return handle_select_register(ed, input),
1368 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1369 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1370 Pending::None => {}
1371 }
1372
1373 let count = take_count(&mut ed.vim);
1374
1375 match input.key {
1377 Key::Esc => {
1378 ed.vim.force_normal();
1379 return true;
1380 }
1381 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1382 ed.vim.visual_anchor = ed.cursor();
1383 ed.vim.mode = Mode::Visual;
1384 return true;
1385 }
1386 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1387 let (row, _) = ed.cursor();
1388 ed.vim.visual_line_anchor = row;
1389 ed.vim.mode = Mode::VisualLine;
1390 return true;
1391 }
1392 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1393 ed.vim.visual_anchor = ed.cursor();
1394 ed.vim.mode = Mode::Visual;
1395 return true;
1396 }
1397 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1398 let (row, _) = ed.cursor();
1399 ed.vim.visual_line_anchor = row;
1400 ed.vim.mode = Mode::VisualLine;
1401 return true;
1402 }
1403 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1404 let cur = ed.cursor();
1405 ed.vim.block_anchor = cur;
1406 ed.vim.block_vcol = cur.1;
1407 ed.vim.mode = Mode::VisualBlock;
1408 return true;
1409 }
1410 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1411 ed.vim.mode = Mode::Normal;
1413 return true;
1414 }
1415 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1418 Mode::Visual => {
1419 let cur = ed.cursor();
1420 let anchor = ed.vim.visual_anchor;
1421 ed.vim.visual_anchor = cur;
1422 ed.jump_cursor(anchor.0, anchor.1);
1423 return true;
1424 }
1425 Mode::VisualLine => {
1426 let cur_row = ed.cursor().0;
1427 let anchor_row = ed.vim.visual_line_anchor;
1428 ed.vim.visual_line_anchor = cur_row;
1429 ed.jump_cursor(anchor_row, 0);
1430 return true;
1431 }
1432 Mode::VisualBlock => {
1433 let cur = ed.cursor();
1434 let anchor = ed.vim.block_anchor;
1435 ed.vim.block_anchor = cur;
1436 ed.vim.block_vcol = anchor.1;
1437 ed.jump_cursor(anchor.0, anchor.1);
1438 return true;
1439 }
1440 _ => {}
1441 },
1442 _ => {}
1443 }
1444
1445 if ed.vim.is_visual()
1447 && let Some(op) = visual_operator(&input)
1448 {
1449 apply_visual_operator(ed, op);
1450 return true;
1451 }
1452
1453 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1457 match input.key {
1458 Key::Char('r') => {
1459 ed.vim.pending = Pending::Replace;
1460 return true;
1461 }
1462 Key::Char('I') => {
1463 let (top, bot, left, _right) = block_bounds(ed);
1464 ed.jump_cursor(top, left);
1465 ed.vim.mode = Mode::Normal;
1466 begin_insert(
1467 ed,
1468 1,
1469 InsertReason::BlockEdge {
1470 top,
1471 bot,
1472 col: left,
1473 },
1474 );
1475 return true;
1476 }
1477 Key::Char('A') => {
1478 let (top, bot, _left, right) = block_bounds(ed);
1479 let line_len = ed.buffer().lines()[top].chars().count();
1480 let col = (right + 1).min(line_len);
1481 ed.jump_cursor(top, col);
1482 ed.vim.mode = Mode::Normal;
1483 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1484 return true;
1485 }
1486 _ => {}
1487 }
1488 }
1489
1490 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1492 && !input.ctrl
1493 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1494 {
1495 let inner = matches!(input.key, Key::Char('i'));
1496 ed.vim.pending = Pending::VisualTextObj { inner };
1497 return true;
1498 }
1499
1500 if input.ctrl
1505 && let Key::Char(c) = input.key
1506 {
1507 match c {
1508 'd' => {
1509 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1510 return true;
1511 }
1512 'u' => {
1513 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1514 return true;
1515 }
1516 'f' => {
1517 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1518 return true;
1519 }
1520 'b' => {
1521 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1522 return true;
1523 }
1524 'r' => {
1525 do_redo(ed);
1526 return true;
1527 }
1528 'a' if ed.vim.mode == Mode::Normal => {
1529 adjust_number(ed, count.max(1) as i64);
1530 return true;
1531 }
1532 'x' if ed.vim.mode == Mode::Normal => {
1533 adjust_number(ed, -(count.max(1) as i64));
1534 return true;
1535 }
1536 'o' if ed.vim.mode == Mode::Normal => {
1537 for _ in 0..count.max(1) {
1538 jump_back(ed);
1539 }
1540 return true;
1541 }
1542 'i' if ed.vim.mode == Mode::Normal => {
1543 for _ in 0..count.max(1) {
1544 jump_forward(ed);
1545 }
1546 return true;
1547 }
1548 _ => {}
1549 }
1550 }
1551
1552 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1554 for _ in 0..count.max(1) {
1555 jump_forward(ed);
1556 }
1557 return true;
1558 }
1559
1560 if let Some(motion) = parse_motion(&input) {
1562 execute_motion(ed, motion.clone(), count);
1563 if ed.vim.mode == Mode::VisualBlock {
1565 update_block_vcol(ed, &motion);
1566 }
1567 if let Motion::Find { ch, forward, till } = motion {
1568 ed.vim.last_find = Some((ch, forward, till));
1569 }
1570 return true;
1571 }
1572
1573 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
1575 return true;
1576 }
1577
1578 if ed.vim.mode == Mode::Normal
1580 && let Key::Char(op_ch) = input.key
1581 && !input.ctrl
1582 && let Some(op) = char_to_operator(op_ch)
1583 {
1584 ed.vim.pending = Pending::Op { op, count1: count };
1585 return true;
1586 }
1587
1588 if ed.vim.mode == Mode::Normal
1590 && let Some((forward, till)) = find_entry(&input)
1591 {
1592 ed.vim.count = count;
1593 ed.vim.pending = Pending::Find { forward, till };
1594 return true;
1595 }
1596
1597 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
1599 ed.vim.count = count;
1600 ed.vim.pending = Pending::G;
1601 return true;
1602 }
1603
1604 if !input.ctrl
1606 && input.key == Key::Char('z')
1607 && matches!(
1608 ed.vim.mode,
1609 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
1610 )
1611 {
1612 ed.vim.pending = Pending::Z;
1613 return true;
1614 }
1615
1616 if !input.ctrl && ed.vim.mode == Mode::Normal {
1620 match input.key {
1621 Key::Char('m') => {
1622 ed.vim.pending = Pending::SetMark;
1623 return true;
1624 }
1625 Key::Char('\'') => {
1626 ed.vim.pending = Pending::GotoMarkLine;
1627 return true;
1628 }
1629 Key::Char('`') => {
1630 ed.vim.pending = Pending::GotoMarkChar;
1631 return true;
1632 }
1633 Key::Char('"') => {
1634 ed.vim.pending = Pending::SelectRegister;
1637 return true;
1638 }
1639 Key::Char('@') => {
1640 ed.vim.pending = Pending::PlayMacroTarget { count };
1644 return true;
1645 }
1646 Key::Char('q') if ed.vim.recording_macro.is_none() => {
1647 ed.vim.pending = Pending::RecordMacroTarget;
1652 return true;
1653 }
1654 _ => {}
1655 }
1656 }
1657
1658 true
1660}
1661
1662fn handle_set_mark(ed: &mut Editor<'_>, input: Input) -> bool {
1663 if let Key::Char(c) = input.key {
1664 let pos = ed.cursor();
1665 if c.is_ascii_lowercase() {
1666 ed.vim.marks.insert(c, pos);
1667 } else if c.is_ascii_uppercase() {
1668 ed.file_marks.insert(c, pos);
1671 }
1672 }
1673 true
1674}
1675
1676fn handle_select_register(ed: &mut Editor<'_>, input: Input) -> bool {
1680 if let Key::Char(c) = input.key
1681 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*'))
1682 {
1683 ed.vim.pending_register = Some(c);
1684 }
1685 true
1686}
1687
1688fn handle_record_macro_target(ed: &mut Editor<'_>, input: Input) -> bool {
1693 if let Key::Char(c) = input.key
1694 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
1695 {
1696 ed.vim.recording_macro = Some(c);
1697 if c.is_ascii_uppercase() {
1700 let lower = c.to_ascii_lowercase();
1701 let text = ed
1705 .registers()
1706 .read(lower)
1707 .map(|s| s.text.clone())
1708 .unwrap_or_default();
1709 ed.vim.recording_keys = crate::input::decode_macro(&text);
1710 } else {
1711 ed.vim.recording_keys.clear();
1712 }
1713 }
1714 true
1715}
1716
1717fn handle_play_macro_target(ed: &mut Editor<'_>, input: Input, count: usize) -> bool {
1723 let reg = match input.key {
1724 Key::Char('@') => ed.vim.last_macro,
1725 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
1726 Some(c.to_ascii_lowercase())
1727 }
1728 _ => None,
1729 };
1730 let Some(reg) = reg else {
1731 return true;
1732 };
1733 let text = match ed.registers().read(reg) {
1736 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1737 _ => return true,
1738 };
1739 let keys = crate::input::decode_macro(&text);
1740 ed.vim.last_macro = Some(reg);
1741 let times = count.max(1);
1742 let was_replaying = ed.vim.replaying_macro;
1743 ed.vim.replaying_macro = true;
1744 for _ in 0..times {
1745 for k in keys.iter().copied() {
1746 step(ed, k);
1747 }
1748 }
1749 ed.vim.replaying_macro = was_replaying;
1750 true
1751}
1752
1753fn handle_goto_mark(ed: &mut Editor<'_>, input: Input, linewise: bool) -> bool {
1754 let Key::Char(c) = input.key else {
1755 return true;
1756 };
1757 let target = match c {
1764 'a'..='z' => ed.vim.marks.get(&c).copied(),
1765 'A'..='Z' => ed.file_marks.get(&c).copied(),
1766 '\'' | '`' => ed.vim.jump_back.last().copied(),
1767 '.' => ed.vim.last_edit_pos,
1768 _ => None,
1769 };
1770 let Some((row, col)) = target else {
1771 return true;
1772 };
1773 let pre = ed.cursor();
1774 let (r, c_clamped) = clamp_pos(ed, (row, col));
1775 if linewise {
1776 ed.buffer_mut().set_cursor(hjkl_buffer::Position::new(r, 0));
1777 ed.push_buffer_cursor_to_textarea();
1778 move_first_non_whitespace(ed);
1779 } else {
1780 ed.buffer_mut()
1781 .set_cursor(hjkl_buffer::Position::new(r, c_clamped));
1782 ed.push_buffer_cursor_to_textarea();
1783 }
1784 if ed.cursor() != pre {
1785 push_jump(ed, pre);
1786 }
1787 ed.vim.sticky_col = Some(ed.cursor().1);
1788 true
1789}
1790
1791fn take_count(vim: &mut VimState) -> usize {
1792 if vim.count > 0 {
1793 let n = vim.count;
1794 vim.count = 0;
1795 n
1796 } else {
1797 1
1798 }
1799}
1800
1801fn char_to_operator(c: char) -> Option<Operator> {
1802 match c {
1803 'd' => Some(Operator::Delete),
1804 'c' => Some(Operator::Change),
1805 'y' => Some(Operator::Yank),
1806 '>' => Some(Operator::Indent),
1807 '<' => Some(Operator::Outdent),
1808 _ => None,
1809 }
1810}
1811
1812fn visual_operator(input: &Input) -> Option<Operator> {
1813 if input.ctrl {
1814 return None;
1815 }
1816 match input.key {
1817 Key::Char('y') => Some(Operator::Yank),
1818 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
1819 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
1820 Key::Char('U') => Some(Operator::Uppercase),
1822 Key::Char('u') => Some(Operator::Lowercase),
1823 Key::Char('~') => Some(Operator::ToggleCase),
1824 Key::Char('>') => Some(Operator::Indent),
1826 Key::Char('<') => Some(Operator::Outdent),
1827 _ => None,
1828 }
1829}
1830
1831fn find_entry(input: &Input) -> Option<(bool, bool)> {
1832 if input.ctrl {
1833 return None;
1834 }
1835 match input.key {
1836 Key::Char('f') => Some((true, false)),
1837 Key::Char('F') => Some((false, false)),
1838 Key::Char('t') => Some((true, true)),
1839 Key::Char('T') => Some((false, true)),
1840 _ => None,
1841 }
1842}
1843
1844const JUMPLIST_MAX: usize = 100;
1848
1849fn push_jump(ed: &mut Editor<'_>, from: (usize, usize)) {
1854 ed.vim.jump_back.push(from);
1855 if ed.vim.jump_back.len() > JUMPLIST_MAX {
1856 ed.vim.jump_back.remove(0);
1857 }
1858 ed.vim.jump_fwd.clear();
1859}
1860
1861fn jump_back(ed: &mut Editor<'_>) {
1864 let Some(target) = ed.vim.jump_back.pop() else {
1865 return;
1866 };
1867 let cur = ed.cursor();
1868 ed.vim.jump_fwd.push(cur);
1869 let (r, c) = clamp_pos(ed, target);
1870 ed.jump_cursor(r, c);
1871 ed.vim.sticky_col = Some(c);
1872}
1873
1874fn jump_forward(ed: &mut Editor<'_>) {
1877 let Some(target) = ed.vim.jump_fwd.pop() else {
1878 return;
1879 };
1880 let cur = ed.cursor();
1881 ed.vim.jump_back.push(cur);
1882 if ed.vim.jump_back.len() > JUMPLIST_MAX {
1883 ed.vim.jump_back.remove(0);
1884 }
1885 let (r, c) = clamp_pos(ed, target);
1886 ed.jump_cursor(r, c);
1887 ed.vim.sticky_col = Some(c);
1888}
1889
1890fn clamp_pos(ed: &Editor<'_>, pos: (usize, usize)) -> (usize, usize) {
1893 let last_row = ed.buffer().lines().len().saturating_sub(1);
1894 let r = pos.0.min(last_row);
1895 let line_len = ed.buffer().line(r).map(|l| l.chars().count()).unwrap_or(0);
1896 let c = pos.1.min(line_len.saturating_sub(1));
1897 (r, c)
1898}
1899
1900fn is_big_jump(motion: &Motion) -> bool {
1902 matches!(
1903 motion,
1904 Motion::FileTop
1905 | Motion::FileBottom
1906 | Motion::MatchBracket
1907 | Motion::WordAtCursor { .. }
1908 | Motion::SearchNext { .. }
1909 | Motion::ViewportTop
1910 | Motion::ViewportMiddle
1911 | Motion::ViewportBottom
1912 )
1913}
1914
1915fn viewport_half_rows(ed: &Editor<'_>, count: usize) -> usize {
1920 let h = ed.viewport_height_value() as usize;
1921 (h / 2).max(1).saturating_mul(count.max(1))
1922}
1923
1924fn viewport_full_rows(ed: &Editor<'_>, count: usize) -> usize {
1927 let h = ed.viewport_height_value() as usize;
1928 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
1929}
1930
1931fn scroll_cursor_rows(ed: &mut Editor<'_>, delta: isize) {
1936 if delta == 0 {
1937 return;
1938 }
1939 ed.sync_buffer_content_from_textarea();
1940 let (row, _) = ed.cursor();
1941 let last_row = ed.buffer().row_count().saturating_sub(1);
1942 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
1943 ed.buffer_mut()
1944 .set_cursor(hjkl_buffer::Position::new(target, 0));
1945 ed.buffer_mut().move_first_non_blank();
1946 ed.push_buffer_cursor_to_textarea();
1947 ed.vim.sticky_col = Some(ed.buffer().cursor().col);
1948}
1949
1950fn parse_motion(input: &Input) -> Option<Motion> {
1953 if input.ctrl {
1954 return None;
1955 }
1956 match input.key {
1957 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
1958 Key::Char('l') | Key::Right => Some(Motion::Right),
1959 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
1960 Key::Char('k') | Key::Up => Some(Motion::Up),
1961 Key::Char('w') => Some(Motion::WordFwd),
1962 Key::Char('W') => Some(Motion::BigWordFwd),
1963 Key::Char('b') => Some(Motion::WordBack),
1964 Key::Char('B') => Some(Motion::BigWordBack),
1965 Key::Char('e') => Some(Motion::WordEnd),
1966 Key::Char('E') => Some(Motion::BigWordEnd),
1967 Key::Char('0') | Key::Home => Some(Motion::LineStart),
1968 Key::Char('^') => Some(Motion::FirstNonBlank),
1969 Key::Char('$') | Key::End => Some(Motion::LineEnd),
1970 Key::Char('G') => Some(Motion::FileBottom),
1971 Key::Char('%') => Some(Motion::MatchBracket),
1972 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
1973 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
1974 Key::Char('*') => Some(Motion::WordAtCursor {
1975 forward: true,
1976 whole_word: true,
1977 }),
1978 Key::Char('#') => Some(Motion::WordAtCursor {
1979 forward: false,
1980 whole_word: true,
1981 }),
1982 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
1983 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
1984 Key::Char('H') => Some(Motion::ViewportTop),
1985 Key::Char('M') => Some(Motion::ViewportMiddle),
1986 Key::Char('L') => Some(Motion::ViewportBottom),
1987 Key::Char('{') => Some(Motion::ParagraphPrev),
1988 Key::Char('}') => Some(Motion::ParagraphNext),
1989 Key::Char('(') => Some(Motion::SentencePrev),
1990 Key::Char(')') => Some(Motion::SentenceNext),
1991 _ => None,
1992 }
1993}
1994
1995fn execute_motion(ed: &mut Editor<'_>, motion: Motion, count: usize) {
1998 let count = count.max(1);
1999 let motion = match motion {
2001 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2002 Some((ch, forward, till)) => Motion::Find {
2003 ch,
2004 forward: if reverse { !forward } else { forward },
2005 till,
2006 },
2007 None => return,
2008 },
2009 other => other,
2010 };
2011 let pre_pos = ed.cursor();
2012 let pre_col = pre_pos.1;
2013 apply_motion_cursor(ed, &motion, count);
2014 let post_pos = ed.cursor();
2015 if is_big_jump(&motion) && pre_pos != post_pos {
2016 push_jump(ed, pre_pos);
2017 }
2018 apply_sticky_col(ed, &motion, pre_col);
2019 ed.sync_buffer_from_textarea();
2024}
2025
2026fn apply_sticky_col(ed: &mut Editor<'_>, motion: &Motion, pre_col: usize) {
2031 if is_vertical_motion(motion) {
2032 let want = ed.vim.sticky_col.unwrap_or(pre_col);
2033 ed.vim.sticky_col = Some(want);
2036 let (row, _) = ed.cursor();
2037 let line_len = ed.buffer().lines()[row].chars().count();
2038 let max_col = line_len.saturating_sub(1);
2042 let target = want.min(max_col);
2043 ed.jump_cursor(row, target);
2044 } else {
2045 ed.vim.sticky_col = Some(ed.cursor().1);
2048 }
2049}
2050
2051fn is_vertical_motion(motion: &Motion) -> bool {
2052 matches!(
2056 motion,
2057 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2058 )
2059}
2060
2061fn apply_motion_cursor(ed: &mut Editor<'_>, motion: &Motion, count: usize) {
2062 apply_motion_cursor_ctx(ed, motion, count, false)
2063}
2064
2065fn apply_motion_cursor_ctx(ed: &mut Editor<'_>, motion: &Motion, count: usize, as_operator: bool) {
2066 match motion {
2067 Motion::Left => {
2068 ed.buffer_mut().move_left(count);
2070 ed.push_buffer_cursor_to_textarea();
2071 }
2072 Motion::Right => {
2073 if as_operator {
2077 ed.buffer_mut().move_right_to_end(count);
2078 } else {
2079 ed.buffer_mut().move_right_in_line(count);
2080 }
2081 ed.push_buffer_cursor_to_textarea();
2082 }
2083 Motion::Up => {
2084 ed.buffer_mut().move_up(count);
2088 ed.push_buffer_cursor_to_textarea();
2089 }
2090 Motion::Down => {
2091 ed.buffer_mut().move_down(count);
2092 ed.push_buffer_cursor_to_textarea();
2093 }
2094 Motion::ScreenUp => {
2095 ed.buffer_mut().move_screen_up(count);
2096 ed.push_buffer_cursor_to_textarea();
2097 }
2098 Motion::ScreenDown => {
2099 ed.buffer_mut().move_screen_down(count);
2100 ed.push_buffer_cursor_to_textarea();
2101 }
2102 Motion::WordFwd => {
2103 ed.buffer_mut().move_word_fwd(false, count);
2104 ed.push_buffer_cursor_to_textarea();
2105 }
2106 Motion::WordBack => {
2107 ed.buffer_mut().move_word_back(false, count);
2108 ed.push_buffer_cursor_to_textarea();
2109 }
2110 Motion::WordEnd => {
2111 ed.buffer_mut().move_word_end(false, count);
2112 ed.push_buffer_cursor_to_textarea();
2113 }
2114 Motion::BigWordFwd => {
2115 ed.buffer_mut().move_word_fwd(true, count);
2116 ed.push_buffer_cursor_to_textarea();
2117 }
2118 Motion::BigWordBack => {
2119 ed.buffer_mut().move_word_back(true, count);
2120 ed.push_buffer_cursor_to_textarea();
2121 }
2122 Motion::BigWordEnd => {
2123 ed.buffer_mut().move_word_end(true, count);
2124 ed.push_buffer_cursor_to_textarea();
2125 }
2126 Motion::WordEndBack => {
2127 ed.buffer_mut().move_word_end_back(false, count);
2128 ed.push_buffer_cursor_to_textarea();
2129 }
2130 Motion::BigWordEndBack => {
2131 ed.buffer_mut().move_word_end_back(true, count);
2132 ed.push_buffer_cursor_to_textarea();
2133 }
2134 Motion::LineStart => {
2135 ed.buffer_mut().move_line_start();
2136 ed.push_buffer_cursor_to_textarea();
2137 }
2138 Motion::FirstNonBlank => {
2139 ed.buffer_mut().move_first_non_blank();
2140 ed.push_buffer_cursor_to_textarea();
2141 }
2142 Motion::LineEnd => {
2143 ed.buffer_mut().move_line_end();
2145 ed.push_buffer_cursor_to_textarea();
2146 }
2147 Motion::FileTop => {
2148 if count > 1 {
2151 ed.buffer_mut().move_bottom(count);
2152 } else {
2153 ed.buffer_mut().move_top();
2154 }
2155 ed.push_buffer_cursor_to_textarea();
2156 }
2157 Motion::FileBottom => {
2158 if count > 1 {
2161 ed.buffer_mut().move_bottom(count);
2162 } else {
2163 ed.buffer_mut().move_bottom(0);
2164 }
2165 ed.push_buffer_cursor_to_textarea();
2166 }
2167 Motion::Find { ch, forward, till } => {
2168 for _ in 0..count {
2169 if !find_char_on_line(ed, *ch, *forward, *till) {
2170 break;
2171 }
2172 }
2173 }
2174 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2176 let _ = matching_bracket(ed);
2177 }
2178 Motion::WordAtCursor {
2179 forward,
2180 whole_word,
2181 } => {
2182 word_at_cursor_search(ed, *forward, *whole_word, count);
2183 }
2184 Motion::SearchNext { reverse } => {
2185 if let Some(pattern) = ed.vim.last_search.clone() {
2189 push_search_pattern(ed, &pattern);
2190 }
2191 if ed.buffer().search_pattern().is_none() {
2192 return;
2193 }
2194 let forward = ed.vim.last_search_forward != *reverse;
2198 for _ in 0..count.max(1) {
2199 if forward {
2200 ed.buffer_mut().search_forward(true);
2201 } else {
2202 ed.buffer_mut().search_backward(true);
2203 }
2204 }
2205 ed.push_buffer_cursor_to_textarea();
2206 }
2207 Motion::ViewportTop => {
2208 ed.buffer_mut().move_viewport_top(count.saturating_sub(1));
2209 ed.push_buffer_cursor_to_textarea();
2210 }
2211 Motion::ViewportMiddle => {
2212 ed.buffer_mut().move_viewport_middle();
2213 ed.push_buffer_cursor_to_textarea();
2214 }
2215 Motion::ViewportBottom => {
2216 ed.buffer_mut()
2217 .move_viewport_bottom(count.saturating_sub(1));
2218 ed.push_buffer_cursor_to_textarea();
2219 }
2220 Motion::LastNonBlank => {
2221 ed.buffer_mut().move_last_non_blank();
2222 ed.push_buffer_cursor_to_textarea();
2223 }
2224 Motion::LineMiddle => {
2225 let row = ed.cursor().0;
2226 let line_chars = ed
2227 .buffer()
2228 .line(row)
2229 .map(|l| l.chars().count())
2230 .unwrap_or(0);
2231 let target = line_chars / 2;
2234 ed.jump_cursor(row, target);
2235 }
2236 Motion::ParagraphPrev => {
2237 ed.buffer_mut().move_paragraph_prev(count);
2238 ed.push_buffer_cursor_to_textarea();
2239 }
2240 Motion::ParagraphNext => {
2241 ed.buffer_mut().move_paragraph_next(count);
2242 ed.push_buffer_cursor_to_textarea();
2243 }
2244 Motion::SentencePrev => {
2245 for _ in 0..count.max(1) {
2246 if let Some((row, col)) = sentence_boundary(ed, false) {
2247 ed.jump_cursor(row, col);
2248 }
2249 }
2250 }
2251 Motion::SentenceNext => {
2252 for _ in 0..count.max(1) {
2253 if let Some((row, col)) = sentence_boundary(ed, true) {
2254 ed.jump_cursor(row, col);
2255 }
2256 }
2257 }
2258 }
2259}
2260
2261fn move_first_non_whitespace(ed: &mut Editor<'_>) {
2262 ed.sync_buffer_content_from_textarea();
2268 ed.buffer_mut().move_first_non_blank();
2269 ed.push_buffer_cursor_to_textarea();
2270}
2271
2272fn find_char_on_line(ed: &mut Editor<'_>, ch: char, forward: bool, till: bool) -> bool {
2273 let moved = ed.buffer_mut().find_char_on_line(ch, forward, till);
2274 if moved {
2275 ed.push_buffer_cursor_to_textarea();
2276 }
2277 moved
2278}
2279
2280fn matching_bracket(ed: &mut Editor<'_>) -> bool {
2281 let moved = ed.buffer_mut().match_bracket();
2282 if moved {
2283 ed.push_buffer_cursor_to_textarea();
2284 }
2285 moved
2286}
2287
2288fn word_at_cursor_search(ed: &mut Editor<'_>, forward: bool, whole_word: bool, count: usize) {
2289 let (row, col) = ed.cursor();
2290 let line: String = ed.buffer().line(row).unwrap_or("").to_string();
2291 let chars: Vec<char> = line.chars().collect();
2292 if chars.is_empty() {
2293 return;
2294 }
2295 let is_word = |c: char| c.is_alphanumeric() || c == '_';
2297 let mut start = col.min(chars.len().saturating_sub(1));
2298 while start > 0 && is_word(chars[start - 1]) {
2299 start -= 1;
2300 }
2301 let mut end = start;
2302 while end < chars.len() && is_word(chars[end]) {
2303 end += 1;
2304 }
2305 if end <= start {
2306 return;
2307 }
2308 let word: String = chars[start..end].iter().collect();
2309 let escaped = regex_escape(&word);
2310 let pattern = if whole_word {
2311 format!(r"\b{escaped}\b")
2312 } else {
2313 escaped
2314 };
2315 push_search_pattern(ed, &pattern);
2316 if ed.buffer().search_pattern().is_none() {
2317 return;
2318 }
2319 ed.vim.last_search = Some(pattern);
2321 ed.vim.last_search_forward = forward;
2322 for _ in 0..count.max(1) {
2323 if forward {
2324 ed.buffer_mut().search_forward(true);
2325 } else {
2326 ed.buffer_mut().search_backward(true);
2327 }
2328 }
2329 ed.push_buffer_cursor_to_textarea();
2330}
2331
2332fn regex_escape(s: &str) -> String {
2333 let mut out = String::with_capacity(s.len());
2334 for c in s.chars() {
2335 if matches!(
2336 c,
2337 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2338 ) {
2339 out.push('\\');
2340 }
2341 out.push(c);
2342 }
2343 out
2344}
2345
2346fn handle_after_op(ed: &mut Editor<'_>, input: Input, op: Operator, count1: usize) -> bool {
2349 if let Key::Char(d @ '0'..='9') = input.key
2351 && !input.ctrl
2352 && (d != '0' || ed.vim.count > 0)
2353 {
2354 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2355 ed.vim.pending = Pending::Op { op, count1 };
2356 return true;
2357 }
2358
2359 if input.key == Key::Esc {
2361 ed.vim.count = 0;
2362 return true;
2363 }
2364
2365 let double_ch = match op {
2369 Operator::Delete => Some('d'),
2370 Operator::Change => Some('c'),
2371 Operator::Yank => Some('y'),
2372 Operator::Indent => Some('>'),
2373 Operator::Outdent => Some('<'),
2374 Operator::Uppercase => Some('U'),
2375 Operator::Lowercase => Some('u'),
2376 Operator::ToggleCase => Some('~'),
2377 Operator::Fold => None,
2378 Operator::Reflow => Some('q'),
2381 };
2382 if let Key::Char(c) = input.key
2383 && !input.ctrl
2384 && Some(c) == double_ch
2385 {
2386 let count2 = take_count(&mut ed.vim);
2387 let total = count1.max(1) * count2.max(1);
2388 execute_line_op(ed, op, total);
2389 if !ed.vim.replaying {
2390 ed.vim.last_change = Some(LastChange::LineOp {
2391 op,
2392 count: total,
2393 inserted: None,
2394 });
2395 }
2396 return true;
2397 }
2398
2399 if let Key::Char('i') | Key::Char('a') = input.key
2401 && !input.ctrl
2402 {
2403 let inner = matches!(input.key, Key::Char('i'));
2404 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2405 return true;
2406 }
2407
2408 if input.key == Key::Char('g') && !input.ctrl {
2410 ed.vim.pending = Pending::OpG { op, count1 };
2411 return true;
2412 }
2413
2414 if let Some((forward, till)) = find_entry(&input) {
2416 ed.vim.pending = Pending::OpFind {
2417 op,
2418 count1,
2419 forward,
2420 till,
2421 };
2422 return true;
2423 }
2424
2425 let count2 = take_count(&mut ed.vim);
2427 let total = count1.max(1) * count2.max(1);
2428 if let Some(motion) = parse_motion(&input) {
2429 let motion = match motion {
2430 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2431 Some((ch, forward, till)) => Motion::Find {
2432 ch,
2433 forward: if reverse { !forward } else { forward },
2434 till,
2435 },
2436 None => return true,
2437 },
2438 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2442 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2443 m => m,
2444 };
2445 apply_op_with_motion(ed, op, &motion, total);
2446 if let Motion::Find { ch, forward, till } = &motion {
2447 ed.vim.last_find = Some((*ch, *forward, *till));
2448 }
2449 if !ed.vim.replaying && op_is_change(op) {
2450 ed.vim.last_change = Some(LastChange::OpMotion {
2451 op,
2452 motion,
2453 count: total,
2454 inserted: None,
2455 });
2456 }
2457 return true;
2458 }
2459
2460 true
2462}
2463
2464fn handle_op_after_g(ed: &mut Editor<'_>, input: Input, op: Operator, count1: usize) -> bool {
2465 if input.ctrl {
2466 return true;
2467 }
2468 let count2 = take_count(&mut ed.vim);
2469 let total = count1.max(1) * count2.max(1);
2470 if matches!(
2474 op,
2475 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2476 ) {
2477 let op_char = match op {
2478 Operator::Uppercase => 'U',
2479 Operator::Lowercase => 'u',
2480 Operator::ToggleCase => '~',
2481 _ => unreachable!(),
2482 };
2483 if input.key == Key::Char(op_char) {
2484 execute_line_op(ed, op, total);
2485 if !ed.vim.replaying {
2486 ed.vim.last_change = Some(LastChange::LineOp {
2487 op,
2488 count: total,
2489 inserted: None,
2490 });
2491 }
2492 return true;
2493 }
2494 }
2495 let motion = match input.key {
2496 Key::Char('g') => Motion::FileTop,
2497 Key::Char('e') => Motion::WordEndBack,
2498 Key::Char('E') => Motion::BigWordEndBack,
2499 Key::Char('j') => Motion::ScreenDown,
2500 Key::Char('k') => Motion::ScreenUp,
2501 _ => return true,
2502 };
2503 apply_op_with_motion(ed, op, &motion, total);
2504 if !ed.vim.replaying && op_is_change(op) {
2505 ed.vim.last_change = Some(LastChange::OpMotion {
2506 op,
2507 motion,
2508 count: total,
2509 inserted: None,
2510 });
2511 }
2512 true
2513}
2514
2515fn handle_after_g(ed: &mut Editor<'_>, input: Input) -> bool {
2516 let count = take_count(&mut ed.vim);
2517 match input.key {
2518 Key::Char('g') => {
2519 let pre = ed.cursor();
2521 if count > 1 {
2522 ed.jump_cursor(count - 1, 0);
2523 } else {
2524 ed.jump_cursor(0, 0);
2525 }
2526 move_first_non_whitespace(ed);
2527 if ed.cursor() != pre {
2528 push_jump(ed, pre);
2529 }
2530 }
2531 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
2532 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
2533 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
2535 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
2537 Key::Char('v') => {
2539 if let Some(snap) = ed.vim.last_visual {
2540 match snap.mode {
2541 Mode::Visual => {
2542 ed.vim.visual_anchor = snap.anchor;
2543 ed.vim.mode = Mode::Visual;
2544 }
2545 Mode::VisualLine => {
2546 ed.vim.visual_line_anchor = snap.anchor.0;
2547 ed.vim.mode = Mode::VisualLine;
2548 }
2549 Mode::VisualBlock => {
2550 ed.vim.block_anchor = snap.anchor;
2551 ed.vim.block_vcol = snap.block_vcol;
2552 ed.vim.mode = Mode::VisualBlock;
2553 }
2554 _ => {}
2555 }
2556 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2557 }
2558 }
2559 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
2563 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
2564 Key::Char('U') => {
2568 ed.vim.pending = Pending::Op {
2569 op: Operator::Uppercase,
2570 count1: count,
2571 };
2572 }
2573 Key::Char('u') => {
2574 ed.vim.pending = Pending::Op {
2575 op: Operator::Lowercase,
2576 count1: count,
2577 };
2578 }
2579 Key::Char('~') => {
2580 ed.vim.pending = Pending::Op {
2581 op: Operator::ToggleCase,
2582 count1: count,
2583 };
2584 }
2585 Key::Char('q') => {
2586 ed.vim.pending = Pending::Op {
2589 op: Operator::Reflow,
2590 count1: count,
2591 };
2592 }
2593 Key::Char('J') => {
2594 for _ in 0..count.max(1) {
2596 ed.push_undo();
2597 join_line_raw(ed);
2598 }
2599 if !ed.vim.replaying {
2600 ed.vim.last_change = Some(LastChange::JoinLine {
2601 count: count.max(1),
2602 });
2603 }
2604 }
2605 Key::Char('d') => {
2606 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
2611 }
2612 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
2615 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
2616 Key::Char('*') => execute_motion(
2620 ed,
2621 Motion::WordAtCursor {
2622 forward: true,
2623 whole_word: false,
2624 },
2625 count,
2626 ),
2627 Key::Char('#') => execute_motion(
2628 ed,
2629 Motion::WordAtCursor {
2630 forward: false,
2631 whole_word: false,
2632 },
2633 count,
2634 ),
2635 _ => {}
2636 }
2637 true
2638}
2639
2640fn handle_after_z(ed: &mut Editor<'_>, input: Input) -> bool {
2641 use crate::editor::CursorScrollTarget;
2642 let row = ed.cursor().0;
2643 match input.key {
2644 Key::Char('z') => {
2645 ed.scroll_cursor_to(CursorScrollTarget::Center);
2646 ed.vim.viewport_pinned = true;
2647 }
2648 Key::Char('t') => {
2649 ed.scroll_cursor_to(CursorScrollTarget::Top);
2650 ed.vim.viewport_pinned = true;
2651 }
2652 Key::Char('b') => {
2653 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
2654 ed.vim.viewport_pinned = true;
2655 }
2656 Key::Char('o') => {
2659 ed.buffer_mut().open_fold_at(row);
2660 }
2661 Key::Char('c') => {
2662 ed.buffer_mut().close_fold_at(row);
2663 }
2664 Key::Char('a') => {
2665 ed.buffer_mut().toggle_fold_at(row);
2666 }
2667 Key::Char('R') => {
2668 ed.buffer_mut().open_all_folds();
2669 }
2670 Key::Char('M') => {
2671 ed.buffer_mut().close_all_folds();
2672 }
2673 Key::Char('E') => {
2674 ed.buffer_mut().clear_all_folds();
2675 }
2676 Key::Char('d') => {
2677 ed.buffer_mut().remove_fold_at(row);
2678 }
2679 Key::Char('f') => {
2680 if matches!(
2681 ed.vim.mode,
2682 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2683 ) {
2684 let anchor_row = match ed.vim.mode {
2687 Mode::VisualLine => ed.vim.visual_line_anchor,
2688 Mode::VisualBlock => ed.vim.block_anchor.0,
2689 _ => ed.vim.visual_anchor.0,
2690 };
2691 let cur = ed.cursor().0;
2692 let top = anchor_row.min(cur);
2693 let bot = anchor_row.max(cur);
2694 ed.buffer_mut().add_fold(top, bot, true);
2695 ed.vim.mode = Mode::Normal;
2696 } else {
2697 let count = take_count(&mut ed.vim);
2702 ed.vim.pending = Pending::Op {
2703 op: Operator::Fold,
2704 count1: count,
2705 };
2706 }
2707 }
2708 _ => {}
2709 }
2710 true
2711}
2712
2713fn handle_replace(ed: &mut Editor<'_>, input: Input) -> bool {
2714 if let Key::Char(ch) = input.key {
2715 if ed.vim.mode == Mode::VisualBlock {
2716 block_replace(ed, ch);
2717 return true;
2718 }
2719 let count = take_count(&mut ed.vim);
2720 replace_char(ed, ch, count.max(1));
2721 if !ed.vim.replaying {
2722 ed.vim.last_change = Some(LastChange::ReplaceChar {
2723 ch,
2724 count: count.max(1),
2725 });
2726 }
2727 }
2728 true
2729}
2730
2731fn handle_find_target(ed: &mut Editor<'_>, input: Input, forward: bool, till: bool) -> bool {
2732 let Key::Char(ch) = input.key else {
2733 return true;
2734 };
2735 let count = take_count(&mut ed.vim);
2736 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
2737 ed.vim.last_find = Some((ch, forward, till));
2738 true
2739}
2740
2741fn handle_op_find_target(
2742 ed: &mut Editor<'_>,
2743 input: Input,
2744 op: Operator,
2745 count1: usize,
2746 forward: bool,
2747 till: bool,
2748) -> bool {
2749 let Key::Char(ch) = input.key else {
2750 return true;
2751 };
2752 let count2 = take_count(&mut ed.vim);
2753 let total = count1.max(1) * count2.max(1);
2754 let motion = Motion::Find { ch, forward, till };
2755 apply_op_with_motion(ed, op, &motion, total);
2756 ed.vim.last_find = Some((ch, forward, till));
2757 if !ed.vim.replaying && op_is_change(op) {
2758 ed.vim.last_change = Some(LastChange::OpMotion {
2759 op,
2760 motion,
2761 count: total,
2762 inserted: None,
2763 });
2764 }
2765 true
2766}
2767
2768fn handle_text_object(
2769 ed: &mut Editor<'_>,
2770 input: Input,
2771 op: Operator,
2772 _count1: usize,
2773 inner: bool,
2774) -> bool {
2775 let Key::Char(ch) = input.key else {
2776 return true;
2777 };
2778 let obj = match ch {
2779 'w' => TextObject::Word { big: false },
2780 'W' => TextObject::Word { big: true },
2781 '"' | '\'' | '`' => TextObject::Quote(ch),
2782 '(' | ')' | 'b' => TextObject::Bracket('('),
2783 '[' | ']' => TextObject::Bracket('['),
2784 '{' | '}' | 'B' => TextObject::Bracket('{'),
2785 '<' | '>' => TextObject::Bracket('<'),
2786 'p' => TextObject::Paragraph,
2787 't' => TextObject::XmlTag,
2788 's' => TextObject::Sentence,
2789 _ => return true,
2790 };
2791 apply_op_with_text_object(ed, op, obj, inner);
2792 if !ed.vim.replaying && op_is_change(op) {
2793 ed.vim.last_change = Some(LastChange::OpTextObj {
2794 op,
2795 obj,
2796 inner,
2797 inserted: None,
2798 });
2799 }
2800 true
2801}
2802
2803fn handle_visual_text_obj(ed: &mut Editor<'_>, input: Input, inner: bool) -> bool {
2804 let Key::Char(ch) = input.key else {
2805 return true;
2806 };
2807 let obj = match ch {
2808 'w' => TextObject::Word { big: false },
2809 'W' => TextObject::Word { big: true },
2810 '"' | '\'' | '`' => TextObject::Quote(ch),
2811 '(' | ')' | 'b' => TextObject::Bracket('('),
2812 '[' | ']' => TextObject::Bracket('['),
2813 '{' | '}' | 'B' => TextObject::Bracket('{'),
2814 '<' | '>' => TextObject::Bracket('<'),
2815 'p' => TextObject::Paragraph,
2816 't' => TextObject::XmlTag,
2817 's' => TextObject::Sentence,
2818 _ => return true,
2819 };
2820 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
2821 return true;
2822 };
2823 match kind {
2827 MotionKind::Linewise => {
2828 ed.vim.visual_line_anchor = start.0;
2829 ed.vim.mode = Mode::VisualLine;
2830 ed.jump_cursor(end.0, 0);
2831 }
2832 _ => {
2833 ed.vim.mode = Mode::Visual;
2834 ed.vim.visual_anchor = (start.0, start.1);
2835 let (er, ec) = retreat_one(ed, end);
2836 ed.jump_cursor(er, ec);
2837 }
2838 }
2839 true
2840}
2841
2842fn retreat_one(ed: &Editor<'_>, pos: (usize, usize)) -> (usize, usize) {
2844 let (r, c) = pos;
2845 if c > 0 {
2846 (r, c - 1)
2847 } else if r > 0 {
2848 let prev_len = ed.buffer().lines()[r - 1].len();
2849 (r - 1, prev_len)
2850 } else {
2851 (0, 0)
2852 }
2853}
2854
2855fn op_is_change(op: Operator) -> bool {
2856 matches!(op, Operator::Delete | Operator::Change)
2857}
2858
2859fn handle_normal_only(ed: &mut Editor<'_>, input: &Input, count: usize) -> bool {
2862 if input.ctrl {
2863 return false;
2864 }
2865 match input.key {
2866 Key::Char('i') => {
2867 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
2868 true
2869 }
2870 Key::Char('I') => {
2871 move_first_non_whitespace(ed);
2872 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
2873 true
2874 }
2875 Key::Char('a') => {
2876 ed.buffer_mut().move_right_to_end(1);
2877 ed.push_buffer_cursor_to_textarea();
2878 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
2879 true
2880 }
2881 Key::Char('A') => {
2882 ed.buffer_mut().move_line_end();
2883 ed.buffer_mut().move_right_to_end(1);
2884 ed.push_buffer_cursor_to_textarea();
2885 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
2886 true
2887 }
2888 Key::Char('R') => {
2889 begin_insert(ed, count.max(1), InsertReason::Replace);
2892 true
2893 }
2894 Key::Char('o') => {
2895 use hjkl_buffer::{Edit, Position};
2896 ed.push_undo();
2897 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
2900 ed.sync_buffer_content_from_textarea();
2901 let row = ed.buffer().cursor().row;
2902 let line_chars = ed
2903 .buffer()
2904 .line(row)
2905 .map(|l| l.chars().count())
2906 .unwrap_or(0);
2907 ed.mutate_edit(Edit::InsertStr {
2908 at: Position::new(row, line_chars),
2909 text: "\n".to_string(),
2910 });
2911 ed.push_buffer_cursor_to_textarea();
2912 true
2913 }
2914 Key::Char('O') => {
2915 use hjkl_buffer::{Edit, Position};
2916 ed.push_undo();
2917 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
2918 ed.sync_buffer_content_from_textarea();
2919 let row = ed.buffer().cursor().row;
2920 ed.mutate_edit(Edit::InsertStr {
2921 at: Position::new(row, 0),
2922 text: "\n".to_string(),
2923 });
2924 ed.buffer_mut().move_up(1);
2927 ed.push_buffer_cursor_to_textarea();
2928 true
2929 }
2930 Key::Char('x') => {
2931 do_char_delete(ed, true, count.max(1));
2932 if !ed.vim.replaying {
2933 ed.vim.last_change = Some(LastChange::CharDel {
2934 forward: true,
2935 count: count.max(1),
2936 });
2937 }
2938 true
2939 }
2940 Key::Char('X') => {
2941 do_char_delete(ed, false, count.max(1));
2942 if !ed.vim.replaying {
2943 ed.vim.last_change = Some(LastChange::CharDel {
2944 forward: false,
2945 count: count.max(1),
2946 });
2947 }
2948 true
2949 }
2950 Key::Char('~') => {
2951 for _ in 0..count.max(1) {
2952 ed.push_undo();
2953 toggle_case_at_cursor(ed);
2954 }
2955 if !ed.vim.replaying {
2956 ed.vim.last_change = Some(LastChange::ToggleCase {
2957 count: count.max(1),
2958 });
2959 }
2960 true
2961 }
2962 Key::Char('J') => {
2963 for _ in 0..count.max(1) {
2964 ed.push_undo();
2965 join_line(ed);
2966 }
2967 if !ed.vim.replaying {
2968 ed.vim.last_change = Some(LastChange::JoinLine {
2969 count: count.max(1),
2970 });
2971 }
2972 true
2973 }
2974 Key::Char('D') => {
2975 ed.push_undo();
2976 delete_to_eol(ed);
2977 ed.buffer_mut().move_left(1);
2979 ed.push_buffer_cursor_to_textarea();
2980 if !ed.vim.replaying {
2981 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
2982 }
2983 true
2984 }
2985 Key::Char('Y') => {
2986 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
2988 true
2989 }
2990 Key::Char('C') => {
2991 ed.push_undo();
2992 delete_to_eol(ed);
2993 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
2994 true
2995 }
2996 Key::Char('s') => {
2997 use hjkl_buffer::{Edit, MotionKind, Position};
2998 ed.push_undo();
2999 ed.sync_buffer_content_from_textarea();
3000 for _ in 0..count.max(1) {
3001 let cursor = ed.buffer().cursor();
3002 let line_chars = ed
3003 .buffer()
3004 .line(cursor.row)
3005 .map(|l| l.chars().count())
3006 .unwrap_or(0);
3007 if cursor.col >= line_chars {
3008 break;
3009 }
3010 ed.mutate_edit(Edit::DeleteRange {
3011 start: cursor,
3012 end: Position::new(cursor.row, cursor.col + 1),
3013 kind: MotionKind::Char,
3014 });
3015 }
3016 ed.push_buffer_cursor_to_textarea();
3017 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3018 if !ed.vim.replaying {
3020 ed.vim.last_change = Some(LastChange::OpMotion {
3021 op: Operator::Change,
3022 motion: Motion::Right,
3023 count: count.max(1),
3024 inserted: None,
3025 });
3026 }
3027 true
3028 }
3029 Key::Char('p') => {
3030 do_paste(ed, false, count.max(1));
3031 if !ed.vim.replaying {
3032 ed.vim.last_change = Some(LastChange::Paste {
3033 before: false,
3034 count: count.max(1),
3035 });
3036 }
3037 true
3038 }
3039 Key::Char('P') => {
3040 do_paste(ed, true, count.max(1));
3041 if !ed.vim.replaying {
3042 ed.vim.last_change = Some(LastChange::Paste {
3043 before: true,
3044 count: count.max(1),
3045 });
3046 }
3047 true
3048 }
3049 Key::Char('u') => {
3050 do_undo(ed);
3051 true
3052 }
3053 Key::Char('r') => {
3054 ed.vim.count = count;
3055 ed.vim.pending = Pending::Replace;
3056 true
3057 }
3058 Key::Char('/') => {
3059 enter_search(ed, true);
3060 true
3061 }
3062 Key::Char('?') => {
3063 enter_search(ed, false);
3064 true
3065 }
3066 Key::Char('.') => {
3067 replay_last_change(ed, count);
3068 true
3069 }
3070 _ => false,
3071 }
3072}
3073
3074fn begin_insert_noundo(ed: &mut Editor<'_>, count: usize, reason: InsertReason) {
3076 let reason = if ed.vim.replaying {
3077 InsertReason::ReplayOnly
3078 } else {
3079 reason
3080 };
3081 let (row, _) = ed.cursor();
3082 ed.vim.insert_session = Some(InsertSession {
3083 count,
3084 row_min: row,
3085 row_max: row,
3086 before_lines: ed.buffer().lines().to_vec(),
3087 reason,
3088 });
3089 ed.vim.mode = Mode::Insert;
3090}
3091
3092fn apply_op_with_motion(ed: &mut Editor<'_>, op: Operator, motion: &Motion, count: usize) {
3095 let start = ed.cursor();
3096 apply_motion_cursor_ctx(ed, motion, count, true);
3101 let end = ed.cursor();
3102 let kind = motion_kind(motion);
3103 ed.jump_cursor(start.0, start.1);
3105 run_operator_over_range(ed, op, start, end, kind);
3106}
3107
3108fn apply_op_with_text_object(ed: &mut Editor<'_>, op: Operator, obj: TextObject, inner: bool) {
3109 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3110 return;
3111 };
3112 ed.jump_cursor(start.0, start.1);
3113 run_operator_over_range(ed, op, start, end, kind);
3114}
3115
3116fn motion_kind(motion: &Motion) -> MotionKind {
3117 match motion {
3118 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3119 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3120 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3121 MotionKind::Linewise
3122 }
3123 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3124 MotionKind::Inclusive
3125 }
3126 Motion::Find { .. } => MotionKind::Inclusive,
3127 Motion::MatchBracket => MotionKind::Inclusive,
3128 Motion::LineEnd => MotionKind::Inclusive,
3130 _ => MotionKind::Exclusive,
3131 }
3132}
3133
3134fn run_operator_over_range(
3135 ed: &mut Editor<'_>,
3136 op: Operator,
3137 start: (usize, usize),
3138 end: (usize, usize),
3139 kind: MotionKind,
3140) {
3141 let (top, bot) = order(start, end);
3142 if top == bot {
3143 return;
3144 }
3145
3146 match op {
3147 Operator::Yank => {
3148 let text = read_vim_range(ed, top, bot, kind);
3149 if !text.is_empty() {
3150 ed.last_yank = Some(text.clone());
3151 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3152 }
3153 ed.buffer_mut()
3154 .set_cursor(hjkl_buffer::Position::new(top.0, top.1));
3155 ed.push_buffer_cursor_to_textarea();
3156 }
3157 Operator::Delete => {
3158 ed.push_undo();
3159 cut_vim_range(ed, top, bot, kind);
3160 ed.vim.mode = Mode::Normal;
3161 }
3162 Operator::Change => {
3163 ed.push_undo();
3164 cut_vim_range(ed, top, bot, kind);
3165 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3166 }
3167 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3168 apply_case_op_to_selection(ed, op, top, bot, kind);
3169 }
3170 Operator::Indent | Operator::Outdent => {
3171 ed.push_undo();
3174 if op == Operator::Indent {
3175 indent_rows(ed, top.0, bot.0, 1);
3176 } else {
3177 outdent_rows(ed, top.0, bot.0, 1);
3178 }
3179 ed.vim.mode = Mode::Normal;
3180 }
3181 Operator::Fold => {
3182 if bot.0 >= top.0 {
3186 ed.buffer_mut().add_fold(top.0, bot.0, true);
3187 }
3188 ed.buffer_mut()
3189 .set_cursor(hjkl_buffer::Position::new(top.0, top.1));
3190 ed.push_buffer_cursor_to_textarea();
3191 ed.vim.mode = Mode::Normal;
3192 }
3193 Operator::Reflow => {
3194 ed.push_undo();
3195 reflow_rows(ed, top.0, bot.0);
3196 ed.vim.mode = Mode::Normal;
3197 }
3198 }
3199}
3200
3201fn reflow_rows(ed: &mut Editor<'_>, top: usize, bot: usize) {
3206 let width = ed.settings().textwidth.max(1);
3207 let mut lines: Vec<String> = ed.buffer().lines().to_vec();
3208 let bot = bot.min(lines.len().saturating_sub(1));
3209 if top > bot {
3210 return;
3211 }
3212 let original = lines[top..=bot].to_vec();
3213 let mut wrapped: Vec<String> = Vec::new();
3214 let mut paragraph: Vec<String> = Vec::new();
3215 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3216 if para.is_empty() {
3217 return;
3218 }
3219 let words = para.join(" ");
3220 let mut current = String::new();
3221 for word in words.split_whitespace() {
3222 let extra = if current.is_empty() {
3223 word.chars().count()
3224 } else {
3225 current.chars().count() + 1 + word.chars().count()
3226 };
3227 if extra > width && !current.is_empty() {
3228 out.push(std::mem::take(&mut current));
3229 current.push_str(word);
3230 } else if current.is_empty() {
3231 current.push_str(word);
3232 } else {
3233 current.push(' ');
3234 current.push_str(word);
3235 }
3236 }
3237 if !current.is_empty() {
3238 out.push(current);
3239 }
3240 para.clear();
3241 };
3242 for line in &original {
3243 if line.trim().is_empty() {
3244 flush(&mut paragraph, &mut wrapped, width);
3245 wrapped.push(String::new());
3246 } else {
3247 paragraph.push(line.clone());
3248 }
3249 }
3250 flush(&mut paragraph, &mut wrapped, width);
3251
3252 let after: Vec<String> = lines.split_off(bot + 1);
3254 lines.truncate(top);
3255 lines.extend(wrapped);
3256 lines.extend(after);
3257 ed.restore(lines, (top, 0));
3258 ed.mark_content_dirty();
3259}
3260
3261fn apply_case_op_to_selection(
3267 ed: &mut Editor<'_>,
3268 op: Operator,
3269 top: (usize, usize),
3270 bot: (usize, usize),
3271 kind: MotionKind,
3272) {
3273 use hjkl_buffer::{Edit, Position};
3274 ed.push_undo();
3275 let saved_yank = ed.yank().to_string();
3276 let saved_yank_linewise = ed.vim.yank_linewise;
3277 let selection = cut_vim_range(ed, top, bot, kind);
3278 let transformed = match op {
3279 Operator::Uppercase => selection.to_uppercase(),
3280 Operator::Lowercase => selection.to_lowercase(),
3281 Operator::ToggleCase => toggle_case_str(&selection),
3282 _ => unreachable!(),
3283 };
3284 if !transformed.is_empty() {
3285 let cursor = ed.buffer().cursor();
3286 ed.mutate_edit(Edit::InsertStr {
3287 at: cursor,
3288 text: transformed,
3289 });
3290 }
3291 ed.buffer_mut().set_cursor(Position::new(top.0, top.1));
3292 ed.push_buffer_cursor_to_textarea();
3293 ed.set_yank(saved_yank);
3294 ed.vim.yank_linewise = saved_yank_linewise;
3295 ed.vim.mode = Mode::Normal;
3296}
3297
3298fn indent_rows(ed: &mut Editor<'_>, top: usize, bot: usize, count: usize) {
3303 ed.sync_buffer_content_from_textarea();
3304 let width = ed.settings().shiftwidth * count.max(1);
3305 let pad: String = " ".repeat(width);
3306 let mut lines: Vec<String> = ed.buffer().lines().to_vec();
3307 let bot = bot.min(lines.len().saturating_sub(1));
3308 for line in lines.iter_mut().take(bot + 1).skip(top) {
3309 if !line.is_empty() {
3310 line.insert_str(0, &pad);
3311 }
3312 }
3313 ed.restore(lines, (top, 0));
3316 move_first_non_whitespace(ed);
3317}
3318
3319fn outdent_rows(ed: &mut Editor<'_>, top: usize, bot: usize, count: usize) {
3323 ed.sync_buffer_content_from_textarea();
3324 let width = ed.settings().shiftwidth * count.max(1);
3325 let mut lines: Vec<String> = ed.buffer().lines().to_vec();
3326 let bot = bot.min(lines.len().saturating_sub(1));
3327 for line in lines.iter_mut().take(bot + 1).skip(top) {
3328 let strip: usize = line
3329 .chars()
3330 .take(width)
3331 .take_while(|c| *c == ' ' || *c == '\t')
3332 .count();
3333 if strip > 0 {
3334 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3335 line.drain(..byte_len);
3336 }
3337 }
3338 ed.restore(lines, (top, 0));
3339 move_first_non_whitespace(ed);
3340}
3341
3342fn toggle_case_str(s: &str) -> String {
3343 s.chars()
3344 .map(|c| {
3345 if c.is_lowercase() {
3346 c.to_uppercase().next().unwrap_or(c)
3347 } else if c.is_uppercase() {
3348 c.to_lowercase().next().unwrap_or(c)
3349 } else {
3350 c
3351 }
3352 })
3353 .collect()
3354}
3355
3356fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3357 if a <= b { (a, b) } else { (b, a) }
3358}
3359
3360fn execute_line_op(ed: &mut Editor<'_>, op: Operator, count: usize) {
3363 let (row, col) = ed.cursor();
3364 let total = ed.buffer().lines().len();
3365 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3366
3367 match op {
3368 Operator::Yank => {
3369 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3371 if !text.is_empty() {
3372 ed.last_yank = Some(text.clone());
3373 ed.record_yank(text, true);
3374 }
3375 ed.buffer_mut()
3376 .set_cursor(hjkl_buffer::Position::new(row, col));
3377 ed.push_buffer_cursor_to_textarea();
3378 ed.vim.mode = Mode::Normal;
3379 }
3380 Operator::Delete => {
3381 ed.push_undo();
3382 let deleted_through_last = end_row + 1 >= total;
3383 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3384 let total_after = ed.buffer().row_count();
3388 let target_row = if deleted_through_last {
3389 row.saturating_sub(1).min(total_after.saturating_sub(1))
3390 } else {
3391 row.min(total_after.saturating_sub(1))
3392 };
3393 ed.buffer_mut()
3394 .set_cursor(hjkl_buffer::Position::new(target_row, 0));
3395 ed.push_buffer_cursor_to_textarea();
3396 move_first_non_whitespace(ed);
3397 ed.vim.mode = Mode::Normal;
3398 }
3399 Operator::Change => {
3400 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3404 ed.push_undo();
3405 ed.sync_buffer_content_from_textarea();
3406 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3408 if end_row > row {
3409 ed.mutate_edit(Edit::DeleteRange {
3410 start: Position::new(row + 1, 0),
3411 end: Position::new(end_row, 0),
3412 kind: BufKind::Line,
3413 });
3414 }
3415 let line_chars = ed
3416 .buffer()
3417 .line(row)
3418 .map(|l| l.chars().count())
3419 .unwrap_or(0);
3420 if line_chars > 0 {
3421 ed.mutate_edit(Edit::DeleteRange {
3422 start: Position::new(row, 0),
3423 end: Position::new(row, line_chars),
3424 kind: BufKind::Char,
3425 });
3426 }
3427 if !payload.is_empty() {
3428 ed.last_yank = Some(payload.clone());
3429 ed.record_delete(payload, true);
3430 }
3431 ed.buffer_mut().set_cursor(Position::new(row, 0));
3432 ed.push_buffer_cursor_to_textarea();
3433 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3434 }
3435 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3436 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
3440 move_first_non_whitespace(ed);
3443 }
3444 Operator::Indent | Operator::Outdent => {
3445 ed.push_undo();
3447 if op == Operator::Indent {
3448 indent_rows(ed, row, end_row, 1);
3449 } else {
3450 outdent_rows(ed, row, end_row, 1);
3451 }
3452 ed.vim.mode = Mode::Normal;
3453 }
3454 Operator::Fold => unreachable!("Fold has no line-op double"),
3456 Operator::Reflow => {
3457 ed.push_undo();
3459 reflow_rows(ed, row, end_row);
3460 ed.vim.mode = Mode::Normal;
3461 }
3462 }
3463}
3464
3465fn apply_visual_operator(ed: &mut Editor<'_>, op: Operator) {
3468 match ed.vim.mode {
3469 Mode::VisualLine => {
3470 let cursor_row = ed.buffer().cursor().row;
3471 let top = cursor_row.min(ed.vim.visual_line_anchor);
3472 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3473 ed.vim.yank_linewise = true;
3474 match op {
3475 Operator::Yank => {
3476 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3477 if !text.is_empty() {
3478 ed.last_yank = Some(text.clone());
3479 ed.record_yank(text, true);
3480 }
3481 ed.buffer_mut()
3482 .set_cursor(hjkl_buffer::Position::new(top, 0));
3483 ed.push_buffer_cursor_to_textarea();
3484 ed.vim.mode = Mode::Normal;
3485 }
3486 Operator::Delete => {
3487 ed.push_undo();
3488 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3489 ed.vim.mode = Mode::Normal;
3490 }
3491 Operator::Change => {
3492 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3495 ed.push_undo();
3496 ed.sync_buffer_content_from_textarea();
3497 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3498 if bot > top {
3499 ed.mutate_edit(Edit::DeleteRange {
3500 start: Position::new(top + 1, 0),
3501 end: Position::new(bot, 0),
3502 kind: BufKind::Line,
3503 });
3504 }
3505 let line_chars = ed
3506 .buffer()
3507 .line(top)
3508 .map(|l| l.chars().count())
3509 .unwrap_or(0);
3510 if line_chars > 0 {
3511 ed.mutate_edit(Edit::DeleteRange {
3512 start: Position::new(top, 0),
3513 end: Position::new(top, line_chars),
3514 kind: BufKind::Char,
3515 });
3516 }
3517 if !payload.is_empty() {
3518 ed.last_yank = Some(payload.clone());
3519 ed.record_delete(payload, true);
3520 }
3521 ed.buffer_mut().set_cursor(Position::new(top, 0));
3522 ed.push_buffer_cursor_to_textarea();
3523 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3524 }
3525 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3526 let bot = ed.buffer().cursor().row.max(ed.vim.visual_line_anchor);
3527 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
3528 move_first_non_whitespace(ed);
3529 }
3530 Operator::Indent | Operator::Outdent => {
3531 ed.push_undo();
3532 let (cursor_row, _) = ed.cursor();
3533 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3534 if op == Operator::Indent {
3535 indent_rows(ed, top, bot, 1);
3536 } else {
3537 outdent_rows(ed, top, bot, 1);
3538 }
3539 ed.vim.mode = Mode::Normal;
3540 }
3541 Operator::Reflow => {
3542 ed.push_undo();
3543 let (cursor_row, _) = ed.cursor();
3544 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3545 reflow_rows(ed, top, bot);
3546 ed.vim.mode = Mode::Normal;
3547 }
3548 Operator::Fold => unreachable!("Visual zf takes its own path"),
3551 }
3552 }
3553 Mode::Visual => {
3554 ed.vim.yank_linewise = false;
3555 let anchor = ed.vim.visual_anchor;
3556 let cursor = ed.cursor();
3557 let (top, bot) = order(anchor, cursor);
3558 match op {
3559 Operator::Yank => {
3560 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
3561 if !text.is_empty() {
3562 ed.last_yank = Some(text.clone());
3563 ed.record_yank(text, false);
3564 }
3565 ed.buffer_mut()
3566 .set_cursor(hjkl_buffer::Position::new(top.0, top.1));
3567 ed.push_buffer_cursor_to_textarea();
3568 ed.vim.mode = Mode::Normal;
3569 }
3570 Operator::Delete => {
3571 ed.push_undo();
3572 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
3573 ed.vim.mode = Mode::Normal;
3574 }
3575 Operator::Change => {
3576 ed.push_undo();
3577 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
3578 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3579 }
3580 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3581 let anchor = ed.vim.visual_anchor;
3583 let cursor = ed.cursor();
3584 let (top, bot) = order(anchor, cursor);
3585 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
3586 }
3587 Operator::Indent | Operator::Outdent => {
3588 ed.push_undo();
3589 let anchor = ed.vim.visual_anchor;
3590 let cursor = ed.cursor();
3591 let (top, bot) = order(anchor, cursor);
3592 if op == Operator::Indent {
3593 indent_rows(ed, top.0, bot.0, 1);
3594 } else {
3595 outdent_rows(ed, top.0, bot.0, 1);
3596 }
3597 ed.vim.mode = Mode::Normal;
3598 }
3599 Operator::Reflow => {
3600 ed.push_undo();
3601 let anchor = ed.vim.visual_anchor;
3602 let cursor = ed.cursor();
3603 let (top, bot) = order(anchor, cursor);
3604 reflow_rows(ed, top.0, bot.0);
3605 ed.vim.mode = Mode::Normal;
3606 }
3607 Operator::Fold => unreachable!("Visual zf takes its own path"),
3608 }
3609 }
3610 Mode::VisualBlock => apply_block_operator(ed, op),
3611 _ => {}
3612 }
3613}
3614
3615fn block_bounds(ed: &Editor<'_>) -> (usize, usize, usize, usize) {
3620 let (ar, ac) = ed.vim.block_anchor;
3621 let (cr, _) = ed.cursor();
3622 let cc = ed.vim.block_vcol;
3623 let top = ar.min(cr);
3624 let bot = ar.max(cr);
3625 let left = ac.min(cc);
3626 let right = ac.max(cc);
3627 (top, bot, left, right)
3628}
3629
3630fn update_block_vcol(ed: &mut Editor<'_>, motion: &Motion) {
3635 match motion {
3636 Motion::Left
3637 | Motion::Right
3638 | Motion::WordFwd
3639 | Motion::BigWordFwd
3640 | Motion::WordBack
3641 | Motion::BigWordBack
3642 | Motion::WordEnd
3643 | Motion::BigWordEnd
3644 | Motion::WordEndBack
3645 | Motion::BigWordEndBack
3646 | Motion::LineStart
3647 | Motion::FirstNonBlank
3648 | Motion::LineEnd
3649 | Motion::Find { .. }
3650 | Motion::FindRepeat { .. }
3651 | Motion::MatchBracket => {
3652 ed.vim.block_vcol = ed.cursor().1;
3653 }
3654 _ => {}
3656 }
3657}
3658
3659fn apply_block_operator(ed: &mut Editor<'_>, op: Operator) {
3664 let (top, bot, left, right) = block_bounds(ed);
3665 let yank = block_yank(ed, top, bot, left, right);
3667
3668 match op {
3669 Operator::Yank => {
3670 if !yank.is_empty() {
3671 ed.last_yank = Some(yank.clone());
3672 ed.record_yank(yank, false);
3673 }
3674 ed.vim.mode = Mode::Normal;
3675 ed.jump_cursor(top, left);
3676 }
3677 Operator::Delete => {
3678 ed.push_undo();
3679 delete_block_contents(ed, top, bot, left, right);
3680 if !yank.is_empty() {
3681 ed.last_yank = Some(yank.clone());
3682 ed.record_delete(yank, false);
3683 }
3684 ed.vim.mode = Mode::Normal;
3685 ed.jump_cursor(top, left);
3686 }
3687 Operator::Change => {
3688 ed.push_undo();
3689 delete_block_contents(ed, top, bot, left, right);
3690 if !yank.is_empty() {
3691 ed.last_yank = Some(yank.clone());
3692 ed.record_delete(yank, false);
3693 }
3694 ed.jump_cursor(top, left);
3695 begin_insert_noundo(
3696 ed,
3697 1,
3698 InsertReason::BlockEdge {
3699 top,
3700 bot,
3701 col: left,
3702 },
3703 );
3704 }
3705 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3706 ed.push_undo();
3707 transform_block_case(ed, op, top, bot, left, right);
3708 ed.vim.mode = Mode::Normal;
3709 ed.jump_cursor(top, left);
3710 }
3711 Operator::Indent | Operator::Outdent => {
3712 ed.push_undo();
3716 if op == Operator::Indent {
3717 indent_rows(ed, top, bot, 1);
3718 } else {
3719 outdent_rows(ed, top, bot, 1);
3720 }
3721 ed.vim.mode = Mode::Normal;
3722 }
3723 Operator::Fold => unreachable!("Visual zf takes its own path"),
3724 Operator::Reflow => {
3725 ed.push_undo();
3729 reflow_rows(ed, top, bot);
3730 ed.vim.mode = Mode::Normal;
3731 }
3732 }
3733}
3734
3735fn transform_block_case(
3739 ed: &mut Editor<'_>,
3740 op: Operator,
3741 top: usize,
3742 bot: usize,
3743 left: usize,
3744 right: usize,
3745) {
3746 let mut lines: Vec<String> = ed.buffer().lines().to_vec();
3747 for r in top..=bot.min(lines.len().saturating_sub(1)) {
3748 let chars: Vec<char> = lines[r].chars().collect();
3749 if left >= chars.len() {
3750 continue;
3751 }
3752 let end = (right + 1).min(chars.len());
3753 let head: String = chars[..left].iter().collect();
3754 let mid: String = chars[left..end].iter().collect();
3755 let tail: String = chars[end..].iter().collect();
3756 let transformed = match op {
3757 Operator::Uppercase => mid.to_uppercase(),
3758 Operator::Lowercase => mid.to_lowercase(),
3759 Operator::ToggleCase => toggle_case_str(&mid),
3760 _ => mid,
3761 };
3762 lines[r] = format!("{head}{transformed}{tail}");
3763 }
3764 let saved_yank = ed.yank().to_string();
3765 let saved_linewise = ed.vim.yank_linewise;
3766 ed.restore(lines, (top, left));
3767 ed.set_yank(saved_yank);
3768 ed.vim.yank_linewise = saved_linewise;
3769}
3770
3771fn block_yank(ed: &Editor<'_>, top: usize, bot: usize, left: usize, right: usize) -> String {
3772 let lines = ed.buffer().lines();
3773 let mut rows: Vec<String> = Vec::new();
3774 for r in top..=bot {
3775 let line = match lines.get(r) {
3776 Some(l) => l,
3777 None => break,
3778 };
3779 let chars: Vec<char> = line.chars().collect();
3780 let end = (right + 1).min(chars.len());
3781 if left >= chars.len() {
3782 rows.push(String::new());
3783 } else {
3784 rows.push(chars[left..end].iter().collect());
3785 }
3786 }
3787 rows.join("\n")
3788}
3789
3790fn delete_block_contents(ed: &mut Editor<'_>, top: usize, bot: usize, left: usize, right: usize) {
3791 use hjkl_buffer::{Edit, MotionKind, Position};
3792 ed.sync_buffer_content_from_textarea();
3793 let last_row = bot.min(ed.buffer().row_count().saturating_sub(1));
3794 if last_row < top {
3795 return;
3796 }
3797 ed.mutate_edit(Edit::DeleteRange {
3798 start: Position::new(top, left),
3799 end: Position::new(last_row, right),
3800 kind: MotionKind::Block,
3801 });
3802 ed.push_buffer_cursor_to_textarea();
3803}
3804
3805fn block_replace(ed: &mut Editor<'_>, ch: char) {
3807 let (top, bot, left, right) = block_bounds(ed);
3808 ed.push_undo();
3809 ed.sync_buffer_content_from_textarea();
3810 let mut lines: Vec<String> = ed.buffer().lines().to_vec();
3811 for r in top..=bot.min(lines.len().saturating_sub(1)) {
3812 let chars: Vec<char> = lines[r].chars().collect();
3813 if left >= chars.len() {
3814 continue;
3815 }
3816 let end = (right + 1).min(chars.len());
3817 let before: String = chars[..left].iter().collect();
3818 let middle: String = std::iter::repeat_n(ch, end - left).collect();
3819 let after: String = chars[end..].iter().collect();
3820 lines[r] = format!("{before}{middle}{after}");
3821 }
3822 reset_textarea_lines(ed, lines);
3823 ed.vim.mode = Mode::Normal;
3824 ed.jump_cursor(top, left);
3825}
3826
3827fn reset_textarea_lines(ed: &mut Editor<'_>, lines: Vec<String>) {
3831 let cursor = ed.cursor();
3832 ed.buffer_mut().replace_all(&lines.join("\n"));
3833 ed.buffer_mut()
3834 .set_cursor(hjkl_buffer::Position::new(cursor.0, cursor.1));
3835 ed.mark_content_dirty();
3836}
3837
3838type Pos = (usize, usize);
3844
3845fn text_object_range(
3849 ed: &Editor<'_>,
3850 obj: TextObject,
3851 inner: bool,
3852) -> Option<(Pos, Pos, MotionKind)> {
3853 match obj {
3854 TextObject::Word { big } => {
3855 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
3856 }
3857 TextObject::Quote(q) => {
3858 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
3859 }
3860 TextObject::Bracket(open) => {
3861 bracket_text_object(ed, open, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
3862 }
3863 TextObject::Paragraph => {
3864 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
3865 }
3866 TextObject::XmlTag => {
3867 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
3868 }
3869 TextObject::Sentence => {
3870 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
3871 }
3872 }
3873}
3874
3875fn sentence_boundary(ed: &Editor<'_>, forward: bool) -> Option<(usize, usize)> {
3879 let lines = ed.buffer().lines();
3880 if lines.is_empty() {
3881 return None;
3882 }
3883 let pos_to_idx = |pos: (usize, usize)| -> usize {
3884 let mut idx = 0;
3885 for line in lines.iter().take(pos.0) {
3886 idx += line.chars().count() + 1;
3887 }
3888 idx + pos.1
3889 };
3890 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
3891 for (r, line) in lines.iter().enumerate() {
3892 let len = line.chars().count();
3893 if idx <= len {
3894 return (r, idx);
3895 }
3896 idx -= len + 1;
3897 }
3898 let last = lines.len().saturating_sub(1);
3899 (last, lines[last].chars().count())
3900 };
3901 let mut chars: Vec<char> = Vec::new();
3902 for (r, line) in lines.iter().enumerate() {
3903 chars.extend(line.chars());
3904 if r + 1 < lines.len() {
3905 chars.push('\n');
3906 }
3907 }
3908 if chars.is_empty() {
3909 return None;
3910 }
3911 let total = chars.len();
3912 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
3913 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
3914
3915 if forward {
3916 let mut i = cursor_idx + 1;
3919 while i < total {
3920 if is_terminator(chars[i]) {
3921 while i + 1 < total && is_terminator(chars[i + 1]) {
3922 i += 1;
3923 }
3924 if i + 1 >= total {
3925 return None;
3926 }
3927 if chars[i + 1].is_whitespace() {
3928 let mut j = i + 1;
3929 while j < total && chars[j].is_whitespace() {
3930 j += 1;
3931 }
3932 if j >= total {
3933 return None;
3934 }
3935 return Some(idx_to_pos(j));
3936 }
3937 }
3938 i += 1;
3939 }
3940 None
3941 } else {
3942 let find_start = |from: usize| -> Option<usize> {
3946 let mut start = from;
3947 while start > 0 {
3948 let prev = chars[start - 1];
3949 if prev.is_whitespace() {
3950 let mut k = start - 1;
3951 while k > 0 && chars[k - 1].is_whitespace() {
3952 k -= 1;
3953 }
3954 if k > 0 && is_terminator(chars[k - 1]) {
3955 break;
3956 }
3957 }
3958 start -= 1;
3959 }
3960 while start < total && chars[start].is_whitespace() {
3961 start += 1;
3962 }
3963 (start < total).then_some(start)
3964 };
3965 let current_start = find_start(cursor_idx)?;
3966 if current_start < cursor_idx {
3967 return Some(idx_to_pos(current_start));
3968 }
3969 let mut k = current_start;
3972 while k > 0 && chars[k - 1].is_whitespace() {
3973 k -= 1;
3974 }
3975 if k == 0 {
3976 return None;
3977 }
3978 let prev_start = find_start(k - 1)?;
3979 Some(idx_to_pos(prev_start))
3980 }
3981}
3982
3983fn sentence_text_object(ed: &Editor<'_>, inner: bool) -> Option<((usize, usize), (usize, usize))> {
3989 let lines = ed.buffer().lines();
3990 if lines.is_empty() {
3991 return None;
3992 }
3993 let pos_to_idx = |pos: (usize, usize)| -> usize {
3996 let mut idx = 0;
3997 for line in lines.iter().take(pos.0) {
3998 idx += line.chars().count() + 1;
3999 }
4000 idx + pos.1
4001 };
4002 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4003 for (r, line) in lines.iter().enumerate() {
4004 let len = line.chars().count();
4005 if idx <= len {
4006 return (r, idx);
4007 }
4008 idx -= len + 1;
4009 }
4010 let last = lines.len().saturating_sub(1);
4011 (last, lines[last].chars().count())
4012 };
4013 let mut chars: Vec<char> = Vec::new();
4014 for (r, line) in lines.iter().enumerate() {
4015 chars.extend(line.chars());
4016 if r + 1 < lines.len() {
4017 chars.push('\n');
4018 }
4019 }
4020 if chars.is_empty() {
4021 return None;
4022 }
4023
4024 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4025 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4026
4027 let mut start = cursor_idx;
4031 while start > 0 {
4032 let prev = chars[start - 1];
4033 if prev.is_whitespace() {
4034 let mut k = start - 1;
4038 while k > 0 && chars[k - 1].is_whitespace() {
4039 k -= 1;
4040 }
4041 if k > 0 && is_terminator(chars[k - 1]) {
4042 break;
4043 }
4044 }
4045 start -= 1;
4046 }
4047 while start < chars.len() && chars[start].is_whitespace() {
4050 start += 1;
4051 }
4052 if start >= chars.len() {
4053 return None;
4054 }
4055
4056 let mut end = start;
4059 while end < chars.len() {
4060 if is_terminator(chars[end]) {
4061 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4063 end += 1;
4064 }
4065 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4068 break;
4069 }
4070 }
4071 end += 1;
4072 }
4073 let end_idx = (end + 1).min(chars.len());
4075
4076 let final_end = if inner {
4077 end_idx
4078 } else {
4079 let mut e = end_idx;
4083 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4084 e += 1;
4085 }
4086 e
4087 };
4088
4089 Some((idx_to_pos(start), idx_to_pos(final_end)))
4090}
4091
4092fn tag_text_object(ed: &Editor<'_>, inner: bool) -> Option<((usize, usize), (usize, usize))> {
4096 let lines = ed.buffer().lines();
4097 if lines.is_empty() {
4098 return None;
4099 }
4100 let pos_to_idx = |pos: (usize, usize)| -> usize {
4104 let mut idx = 0;
4105 for line in lines.iter().take(pos.0) {
4106 idx += line.chars().count() + 1;
4107 }
4108 idx + pos.1
4109 };
4110 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4111 for (r, line) in lines.iter().enumerate() {
4112 let len = line.chars().count();
4113 if idx <= len {
4114 return (r, idx);
4115 }
4116 idx -= len + 1;
4117 }
4118 let last = lines.len().saturating_sub(1);
4119 (last, lines[last].chars().count())
4120 };
4121 let mut chars: Vec<char> = Vec::new();
4122 for (r, line) in lines.iter().enumerate() {
4123 chars.extend(line.chars());
4124 if r + 1 < lines.len() {
4125 chars.push('\n');
4126 }
4127 }
4128 let cursor_idx = pos_to_idx(ed.cursor());
4129
4130 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4136 let mut i = 0;
4137 while i < chars.len() {
4138 if chars[i] != '<' {
4139 i += 1;
4140 continue;
4141 }
4142 let mut j = i + 1;
4143 while j < chars.len() && chars[j] != '>' {
4144 j += 1;
4145 }
4146 if j >= chars.len() {
4147 break;
4148 }
4149 let inside: String = chars[i + 1..j].iter().collect();
4150 let close_end = j + 1;
4151 let trimmed = inside.trim();
4152 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4153 i = close_end;
4154 continue;
4155 }
4156 if let Some(rest) = trimmed.strip_prefix('/') {
4157 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4158 if !name.is_empty()
4159 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4160 {
4161 let (open_start, content_start, _) = stack[stack_idx].clone();
4162 stack.truncate(stack_idx);
4163 let content_end = i;
4164 if cursor_idx >= content_start && cursor_idx <= content_end {
4165 let candidate = (open_start, content_start, content_end, close_end);
4166 innermost = match innermost {
4167 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4168 Some(candidate)
4169 }
4170 None => Some(candidate),
4171 existing => existing,
4172 };
4173 }
4174 }
4175 } else if !trimmed.ends_with('/') {
4176 let name: String = trimmed
4177 .split(|c: char| c.is_whitespace() || c == '/')
4178 .next()
4179 .unwrap_or("")
4180 .to_string();
4181 if !name.is_empty() {
4182 stack.push((i, close_end, name));
4183 }
4184 }
4185 i = close_end;
4186 }
4187
4188 let (open_start, content_start, content_end, close_end) = innermost?;
4189 if inner {
4190 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4191 } else {
4192 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4193 }
4194}
4195
4196fn is_wordchar(c: char) -> bool {
4197 c.is_alphanumeric() || c == '_'
4198}
4199
4200fn word_text_object(
4201 ed: &Editor<'_>,
4202 inner: bool,
4203 big: bool,
4204) -> Option<((usize, usize), (usize, usize))> {
4205 let (row, col) = ed.cursor();
4206 let line = ed.buffer().lines().get(row)?;
4207 let chars: Vec<char> = line.chars().collect();
4208 if chars.is_empty() {
4209 return None;
4210 }
4211 let at = col.min(chars.len().saturating_sub(1));
4212 let classify = |c: char| -> u8 {
4213 if c.is_whitespace() {
4214 0
4215 } else if big || is_wordchar(c) {
4216 1
4217 } else {
4218 2
4219 }
4220 };
4221 let cls = classify(chars[at]);
4222 let mut start = at;
4223 while start > 0 && classify(chars[start - 1]) == cls {
4224 start -= 1;
4225 }
4226 let mut end = at;
4227 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4228 end += 1;
4229 }
4230 let char_byte = |i: usize| {
4232 if i >= chars.len() {
4233 line.len()
4234 } else {
4235 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4236 }
4237 };
4238 let mut start_col = char_byte(start);
4239 let mut end_col = char_byte(end + 1);
4241 if !inner {
4242 let mut t = end + 1;
4244 let mut included_trailing = false;
4245 while t < chars.len() && chars[t].is_whitespace() {
4246 included_trailing = true;
4247 t += 1;
4248 }
4249 if included_trailing {
4250 end_col = char_byte(t);
4251 } else {
4252 let mut s = start;
4253 while s > 0 && chars[s - 1].is_whitespace() {
4254 s -= 1;
4255 }
4256 start_col = char_byte(s);
4257 }
4258 }
4259 Some(((row, start_col), (row, end_col)))
4260}
4261
4262fn quote_text_object(
4263 ed: &Editor<'_>,
4264 q: char,
4265 inner: bool,
4266) -> Option<((usize, usize), (usize, usize))> {
4267 let (row, col) = ed.cursor();
4268 let line = ed.buffer().lines().get(row)?;
4269 let bytes = line.as_bytes();
4270 let q_byte = q as u8;
4271 let mut positions: Vec<usize> = Vec::new();
4273 for (i, &b) in bytes.iter().enumerate() {
4274 if b == q_byte {
4275 positions.push(i);
4276 }
4277 }
4278 if positions.len() < 2 {
4279 return None;
4280 }
4281 let mut open_idx: Option<usize> = None;
4282 let mut close_idx: Option<usize> = None;
4283 for pair in positions.chunks(2) {
4284 if pair.len() < 2 {
4285 break;
4286 }
4287 if col >= pair[0] && col <= pair[1] {
4288 open_idx = Some(pair[0]);
4289 close_idx = Some(pair[1]);
4290 break;
4291 }
4292 if col < pair[0] {
4293 open_idx = Some(pair[0]);
4294 close_idx = Some(pair[1]);
4295 break;
4296 }
4297 }
4298 let open = open_idx?;
4299 let close = close_idx?;
4300 if inner {
4302 if close <= open + 1 {
4303 return None;
4304 }
4305 Some(((row, open + 1), (row, close)))
4306 } else {
4307 Some(((row, open), (row, close + 1)))
4308 }
4309}
4310
4311fn bracket_text_object(
4312 ed: &Editor<'_>,
4313 open: char,
4314 inner: bool,
4315) -> Option<((usize, usize), (usize, usize))> {
4316 let close = match open {
4317 '(' => ')',
4318 '[' => ']',
4319 '{' => '}',
4320 '<' => '>',
4321 _ => return None,
4322 };
4323 let (row, col) = ed.cursor();
4324 let lines = ed.buffer().lines();
4325 let open_pos = find_open_bracket(lines, row, col, open, close)?;
4327 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
4328 if inner {
4330 let inner_start = advance_pos(lines, open_pos);
4331 if inner_start.0 > close_pos.0
4332 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
4333 {
4334 return None;
4335 }
4336 Some((inner_start, close_pos))
4337 } else {
4338 Some((open_pos, advance_pos(lines, close_pos)))
4339 }
4340}
4341
4342fn find_open_bracket(
4343 lines: &[String],
4344 row: usize,
4345 col: usize,
4346 open: char,
4347 close: char,
4348) -> Option<(usize, usize)> {
4349 let mut depth: i32 = 0;
4350 let mut r = row;
4351 let mut c = col as isize;
4352 loop {
4353 let cur = &lines[r];
4354 let chars: Vec<char> = cur.chars().collect();
4355 if (c as usize) >= chars.len() {
4359 c = chars.len() as isize - 1;
4360 }
4361 while c >= 0 {
4362 let ch = chars[c as usize];
4363 if ch == close {
4364 depth += 1;
4365 } else if ch == open {
4366 if depth == 0 {
4367 return Some((r, c as usize));
4368 }
4369 depth -= 1;
4370 }
4371 c -= 1;
4372 }
4373 if r == 0 {
4374 return None;
4375 }
4376 r -= 1;
4377 c = lines[r].chars().count() as isize - 1;
4378 }
4379}
4380
4381fn find_close_bracket(
4382 lines: &[String],
4383 row: usize,
4384 start_col: usize,
4385 open: char,
4386 close: char,
4387) -> Option<(usize, usize)> {
4388 let mut depth: i32 = 0;
4389 let mut r = row;
4390 let mut c = start_col;
4391 loop {
4392 let cur = &lines[r];
4393 let chars: Vec<char> = cur.chars().collect();
4394 while c < chars.len() {
4395 let ch = chars[c];
4396 if ch == open {
4397 depth += 1;
4398 } else if ch == close {
4399 if depth == 0 {
4400 return Some((r, c));
4401 }
4402 depth -= 1;
4403 }
4404 c += 1;
4405 }
4406 if r + 1 >= lines.len() {
4407 return None;
4408 }
4409 r += 1;
4410 c = 0;
4411 }
4412}
4413
4414fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
4415 let (r, c) = pos;
4416 let line_len = lines[r].chars().count();
4417 if c < line_len {
4418 (r, c + 1)
4419 } else if r + 1 < lines.len() {
4420 (r + 1, 0)
4421 } else {
4422 pos
4423 }
4424}
4425
4426fn paragraph_text_object(ed: &Editor<'_>, inner: bool) -> Option<((usize, usize), (usize, usize))> {
4427 let (row, _) = ed.cursor();
4428 let lines = ed.buffer().lines();
4429 if lines.is_empty() {
4430 return None;
4431 }
4432 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
4434 if is_blank(row) {
4435 return None;
4436 }
4437 let mut top = row;
4438 while top > 0 && !is_blank(top - 1) {
4439 top -= 1;
4440 }
4441 let mut bot = row;
4442 while bot + 1 < lines.len() && !is_blank(bot + 1) {
4443 bot += 1;
4444 }
4445 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
4447 bot += 1;
4448 }
4449 let end_col = lines[bot].chars().count();
4450 Some(((top, 0), (bot, end_col)))
4451}
4452
4453fn read_vim_range(
4459 ed: &mut Editor<'_>,
4460 start: (usize, usize),
4461 end: (usize, usize),
4462 kind: MotionKind,
4463) -> String {
4464 let (top, bot) = order(start, end);
4465 ed.sync_buffer_content_from_textarea();
4466 let lines = ed.buffer().lines();
4467 match kind {
4468 MotionKind::Linewise => {
4469 let lo = top.0;
4470 let hi = bot.0.min(lines.len().saturating_sub(1));
4471 let mut text = lines[lo..=hi].join("\n");
4472 text.push('\n');
4473 text
4474 }
4475 MotionKind::Inclusive | MotionKind::Exclusive => {
4476 let inclusive = matches!(kind, MotionKind::Inclusive);
4477 let mut out = String::new();
4479 for row in top.0..=bot.0 {
4480 let line = lines.get(row).map(String::as_str).unwrap_or("");
4481 let lo = if row == top.0 { top.1 } else { 0 };
4482 let hi_unclamped = if row == bot.0 {
4483 if inclusive { bot.1 + 1 } else { bot.1 }
4484 } else {
4485 line.chars().count() + 1
4486 };
4487 let row_chars: Vec<char> = line.chars().collect();
4488 let hi = hi_unclamped.min(row_chars.len());
4489 if lo < hi {
4490 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
4491 }
4492 if row < bot.0 {
4493 out.push('\n');
4494 }
4495 }
4496 out
4497 }
4498 }
4499}
4500
4501fn cut_vim_range(
4510 ed: &mut Editor<'_>,
4511 start: (usize, usize),
4512 end: (usize, usize),
4513 kind: MotionKind,
4514) -> String {
4515 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4516 let (top, bot) = order(start, end);
4517 ed.sync_buffer_content_from_textarea();
4518 let (buf_start, buf_end, buf_kind) = match kind {
4519 MotionKind::Linewise => (
4520 Position::new(top.0, 0),
4521 Position::new(bot.0, 0),
4522 BufKind::Line,
4523 ),
4524 MotionKind::Inclusive => {
4525 let line_chars = ed
4526 .buffer()
4527 .line(bot.0)
4528 .map(|l| l.chars().count())
4529 .unwrap_or(0);
4530 let next = if bot.1 < line_chars {
4534 Position::new(bot.0, bot.1 + 1)
4535 } else if bot.0 + 1 < ed.buffer().row_count() {
4536 Position::new(bot.0 + 1, 0)
4537 } else {
4538 Position::new(bot.0, line_chars)
4539 };
4540 (Position::new(top.0, top.1), next, BufKind::Char)
4541 }
4542 MotionKind::Exclusive => (
4543 Position::new(top.0, top.1),
4544 Position::new(bot.0, bot.1),
4545 BufKind::Char,
4546 ),
4547 };
4548 let inverse = ed.mutate_edit(Edit::DeleteRange {
4549 start: buf_start,
4550 end: buf_end,
4551 kind: buf_kind,
4552 });
4553 let text = match inverse {
4554 Edit::InsertStr { text, .. } => text,
4555 _ => String::new(),
4556 };
4557 if !text.is_empty() {
4558 ed.last_yank = Some(text.clone());
4559 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
4560 }
4561 ed.push_buffer_cursor_to_textarea();
4562 text
4563}
4564
4565fn delete_to_eol(ed: &mut Editor<'_>) {
4571 use hjkl_buffer::{Edit, MotionKind, Position};
4572 ed.sync_buffer_content_from_textarea();
4573 let cursor = ed.buffer().cursor();
4574 let line_chars = ed
4575 .buffer()
4576 .line(cursor.row)
4577 .map(|l| l.chars().count())
4578 .unwrap_or(0);
4579 if cursor.col >= line_chars {
4580 return;
4581 }
4582 let inverse = ed.mutate_edit(Edit::DeleteRange {
4583 start: cursor,
4584 end: Position::new(cursor.row, line_chars),
4585 kind: MotionKind::Char,
4586 });
4587 if let Edit::InsertStr { text, .. } = inverse
4588 && !text.is_empty()
4589 {
4590 ed.last_yank = Some(text.clone());
4591 ed.vim.yank_linewise = false;
4592 ed.set_yank(text);
4593 }
4594 ed.buffer_mut().set_cursor(cursor);
4595 ed.push_buffer_cursor_to_textarea();
4596}
4597
4598fn do_char_delete(ed: &mut Editor<'_>, forward: bool, count: usize) {
4599 use hjkl_buffer::{Edit, MotionKind, Position};
4600 ed.push_undo();
4601 ed.sync_buffer_content_from_textarea();
4602 for _ in 0..count {
4603 let cursor = ed.buffer().cursor();
4604 let line_chars = ed
4605 .buffer()
4606 .line(cursor.row)
4607 .map(|l| l.chars().count())
4608 .unwrap_or(0);
4609 if forward {
4610 if cursor.col >= line_chars {
4613 continue;
4614 }
4615 ed.mutate_edit(Edit::DeleteRange {
4616 start: cursor,
4617 end: Position::new(cursor.row, cursor.col + 1),
4618 kind: MotionKind::Char,
4619 });
4620 } else {
4621 if cursor.col == 0 {
4623 continue;
4624 }
4625 ed.mutate_edit(Edit::DeleteRange {
4626 start: Position::new(cursor.row, cursor.col - 1),
4627 end: cursor,
4628 kind: MotionKind::Char,
4629 });
4630 }
4631 }
4632 ed.push_buffer_cursor_to_textarea();
4633}
4634
4635fn adjust_number(ed: &mut Editor<'_>, delta: i64) -> bool {
4639 use hjkl_buffer::{Edit, MotionKind, Position};
4640 ed.sync_buffer_content_from_textarea();
4641 let cursor = ed.buffer().cursor();
4642 let row = cursor.row;
4643 let chars: Vec<char> = match ed.buffer().line(row) {
4644 Some(l) => l.chars().collect(),
4645 None => return false,
4646 };
4647 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
4648 return false;
4649 };
4650 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
4651 digit_start - 1
4652 } else {
4653 digit_start
4654 };
4655 let mut span_end = digit_start;
4656 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
4657 span_end += 1;
4658 }
4659 let s: String = chars[span_start..span_end].iter().collect();
4660 let Ok(n) = s.parse::<i64>() else {
4661 return false;
4662 };
4663 let new_s = n.saturating_add(delta).to_string();
4664
4665 ed.push_undo();
4666 let span_start_pos = Position::new(row, span_start);
4667 let span_end_pos = Position::new(row, span_end);
4668 ed.mutate_edit(Edit::DeleteRange {
4669 start: span_start_pos,
4670 end: span_end_pos,
4671 kind: MotionKind::Char,
4672 });
4673 ed.mutate_edit(Edit::InsertStr {
4674 at: span_start_pos,
4675 text: new_s.clone(),
4676 });
4677 let new_len = new_s.chars().count();
4678 ed.buffer_mut()
4679 .set_cursor(Position::new(row, span_start + new_len.saturating_sub(1)));
4680 ed.push_buffer_cursor_to_textarea();
4681 true
4682}
4683
4684fn replace_char(ed: &mut Editor<'_>, ch: char, count: usize) {
4685 use hjkl_buffer::{Edit, MotionKind, Position};
4686 ed.push_undo();
4687 ed.sync_buffer_content_from_textarea();
4688 for _ in 0..count {
4689 let cursor = ed.buffer().cursor();
4690 let line_chars = ed
4691 .buffer()
4692 .line(cursor.row)
4693 .map(|l| l.chars().count())
4694 .unwrap_or(0);
4695 if cursor.col >= line_chars {
4696 break;
4697 }
4698 ed.mutate_edit(Edit::DeleteRange {
4699 start: cursor,
4700 end: Position::new(cursor.row, cursor.col + 1),
4701 kind: MotionKind::Char,
4702 });
4703 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
4704 }
4705 ed.buffer_mut().move_left(1);
4707 ed.push_buffer_cursor_to_textarea();
4708}
4709
4710fn toggle_case_at_cursor(ed: &mut Editor<'_>) {
4711 use hjkl_buffer::{Edit, MotionKind, Position};
4712 ed.sync_buffer_content_from_textarea();
4713 let cursor = ed.buffer().cursor();
4714 let Some(c) = ed
4715 .buffer()
4716 .line(cursor.row)
4717 .and_then(|l| l.chars().nth(cursor.col))
4718 else {
4719 return;
4720 };
4721 let toggled = if c.is_uppercase() {
4722 c.to_lowercase().next().unwrap_or(c)
4723 } else {
4724 c.to_uppercase().next().unwrap_or(c)
4725 };
4726 ed.mutate_edit(Edit::DeleteRange {
4727 start: cursor,
4728 end: Position::new(cursor.row, cursor.col + 1),
4729 kind: MotionKind::Char,
4730 });
4731 ed.mutate_edit(Edit::InsertChar {
4732 at: cursor,
4733 ch: toggled,
4734 });
4735}
4736
4737fn join_line(ed: &mut Editor<'_>) {
4738 use hjkl_buffer::{Edit, Position};
4739 ed.sync_buffer_content_from_textarea();
4740 let row = ed.buffer().cursor().row;
4741 if row + 1 >= ed.buffer().row_count() {
4742 return;
4743 }
4744 let cur_line = ed.buffer().line(row).unwrap_or("").to_string();
4745 let next_raw = ed.buffer().line(row + 1).unwrap_or("").to_string();
4746 let next_trimmed = next_raw.trim_start();
4747 let cur_chars = cur_line.chars().count();
4748 let next_chars = next_raw.chars().count();
4749 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
4752 " "
4753 } else {
4754 ""
4755 };
4756 let joined = format!("{cur_line}{separator}{next_trimmed}");
4757 ed.mutate_edit(Edit::Replace {
4758 start: Position::new(row, 0),
4759 end: Position::new(row + 1, next_chars),
4760 with: joined,
4761 });
4762 ed.buffer_mut().set_cursor(Position::new(row, cur_chars));
4766 ed.push_buffer_cursor_to_textarea();
4767}
4768
4769fn join_line_raw(ed: &mut Editor<'_>) {
4772 use hjkl_buffer::{Edit, Position};
4773 ed.sync_buffer_content_from_textarea();
4774 let row = ed.buffer().cursor().row;
4775 if row + 1 >= ed.buffer().row_count() {
4776 return;
4777 }
4778 let join_col = ed
4779 .buffer()
4780 .line(row)
4781 .map(|l| l.chars().count())
4782 .unwrap_or(0);
4783 ed.mutate_edit(Edit::JoinLines {
4784 row,
4785 count: 1,
4786 with_space: false,
4787 });
4788 ed.buffer_mut().set_cursor(Position::new(row, join_col));
4790 ed.push_buffer_cursor_to_textarea();
4791}
4792
4793fn do_paste(ed: &mut Editor<'_>, before: bool, count: usize) {
4794 use hjkl_buffer::{Edit, Position};
4795 ed.push_undo();
4796 let selector = ed.vim.pending_register.take();
4801 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
4802 Some(slot) => (slot.text.clone(), slot.linewise),
4803 None => (ed.yank().to_string(), ed.vim.yank_linewise),
4804 };
4805 for _ in 0..count {
4806 ed.sync_buffer_content_from_textarea();
4807 let yank = yank.clone();
4808 if yank.is_empty() {
4809 continue;
4810 }
4811 if linewise {
4812 let text = yank.trim_matches('\n').to_string();
4816 let row = ed.buffer().cursor().row;
4817 let target_row = if before {
4818 ed.mutate_edit(Edit::InsertStr {
4819 at: Position::new(row, 0),
4820 text: format!("{text}\n"),
4821 });
4822 row
4823 } else {
4824 let line_chars = ed
4825 .buffer()
4826 .line(row)
4827 .map(|l| l.chars().count())
4828 .unwrap_or(0);
4829 ed.mutate_edit(Edit::InsertStr {
4830 at: Position::new(row, line_chars),
4831 text: format!("\n{text}"),
4832 });
4833 row + 1
4834 };
4835 ed.buffer_mut().set_cursor(Position::new(target_row, 0));
4836 ed.buffer_mut().move_first_non_blank();
4837 ed.push_buffer_cursor_to_textarea();
4838 } else {
4839 let cursor = ed.buffer().cursor();
4843 let at = if before {
4844 cursor
4845 } else {
4846 let line_chars = ed
4847 .buffer()
4848 .line(cursor.row)
4849 .map(|l| l.chars().count())
4850 .unwrap_or(0);
4851 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
4852 };
4853 ed.mutate_edit(Edit::InsertStr {
4854 at,
4855 text: yank.clone(),
4856 });
4857 ed.buffer_mut().move_left(1);
4860 ed.push_buffer_cursor_to_textarea();
4861 }
4862 }
4863 ed.vim.sticky_col = Some(ed.buffer().cursor().col);
4865}
4866
4867#[doc(hidden)]
4868pub fn do_undo(ed: &mut Editor<'_>) {
4869 if let Some((lines, cursor)) = ed.undo_stack.pop() {
4870 let current = ed.snapshot();
4871 ed.redo_stack.push(current);
4872 ed.restore(lines, cursor);
4873 }
4874 ed.vim.mode = Mode::Normal;
4875}
4876
4877#[doc(hidden)]
4878pub fn do_redo(ed: &mut Editor<'_>) {
4879 if let Some((lines, cursor)) = ed.redo_stack.pop() {
4880 let current = ed.snapshot();
4881 ed.undo_stack.push(current);
4882 ed.restore(lines, cursor);
4883 }
4884 ed.vim.mode = Mode::Normal;
4885}
4886
4887fn replay_insert_and_finish(ed: &mut Editor<'_>, text: &str) {
4894 use hjkl_buffer::{Edit, Position};
4895 let cursor = ed.cursor();
4896 ed.mutate_edit(Edit::InsertStr {
4897 at: Position::new(cursor.0, cursor.1),
4898 text: text.to_string(),
4899 });
4900 if ed.vim.insert_session.take().is_some() {
4901 if ed.cursor().1 > 0 {
4902 ed.buffer_mut().move_left(1);
4903 ed.push_buffer_cursor_to_textarea();
4904 }
4905 ed.vim.mode = Mode::Normal;
4906 }
4907}
4908
4909fn replay_last_change(ed: &mut Editor<'_>, outer_count: usize) {
4910 let Some(change) = ed.vim.last_change.clone() else {
4911 return;
4912 };
4913 ed.vim.replaying = true;
4914 let scale = if outer_count > 0 { outer_count } else { 1 };
4915 match change {
4916 LastChange::OpMotion {
4917 op,
4918 motion,
4919 count,
4920 inserted,
4921 } => {
4922 let total = count.max(1) * scale;
4923 apply_op_with_motion(ed, op, &motion, total);
4924 if let Some(text) = inserted {
4925 replay_insert_and_finish(ed, &text);
4926 }
4927 }
4928 LastChange::OpTextObj {
4929 op,
4930 obj,
4931 inner,
4932 inserted,
4933 } => {
4934 apply_op_with_text_object(ed, op, obj, inner);
4935 if let Some(text) = inserted {
4936 replay_insert_and_finish(ed, &text);
4937 }
4938 }
4939 LastChange::LineOp {
4940 op,
4941 count,
4942 inserted,
4943 } => {
4944 let total = count.max(1) * scale;
4945 execute_line_op(ed, op, total);
4946 if let Some(text) = inserted {
4947 replay_insert_and_finish(ed, &text);
4948 }
4949 }
4950 LastChange::CharDel { forward, count } => {
4951 do_char_delete(ed, forward, count * scale);
4952 }
4953 LastChange::ReplaceChar { ch, count } => {
4954 replace_char(ed, ch, count * scale);
4955 }
4956 LastChange::ToggleCase { count } => {
4957 for _ in 0..count * scale {
4958 ed.push_undo();
4959 toggle_case_at_cursor(ed);
4960 }
4961 }
4962 LastChange::JoinLine { count } => {
4963 for _ in 0..count * scale {
4964 ed.push_undo();
4965 join_line(ed);
4966 }
4967 }
4968 LastChange::Paste { before, count } => {
4969 do_paste(ed, before, count * scale);
4970 }
4971 LastChange::DeleteToEol { inserted } => {
4972 use hjkl_buffer::{Edit, Position};
4973 ed.push_undo();
4974 delete_to_eol(ed);
4975 if let Some(text) = inserted {
4976 let cursor = ed.cursor();
4977 ed.mutate_edit(Edit::InsertStr {
4978 at: Position::new(cursor.0, cursor.1),
4979 text,
4980 });
4981 }
4982 }
4983 LastChange::OpenLine { above, inserted } => {
4984 use hjkl_buffer::{Edit, Position};
4985 ed.push_undo();
4986 ed.sync_buffer_content_from_textarea();
4987 let row = ed.buffer().cursor().row;
4988 if above {
4989 ed.mutate_edit(Edit::InsertStr {
4990 at: Position::new(row, 0),
4991 text: "\n".to_string(),
4992 });
4993 ed.buffer_mut().move_up(1);
4994 } else {
4995 let line_chars = ed
4996 .buffer()
4997 .line(row)
4998 .map(|l| l.chars().count())
4999 .unwrap_or(0);
5000 ed.mutate_edit(Edit::InsertStr {
5001 at: Position::new(row, line_chars),
5002 text: "\n".to_string(),
5003 });
5004 }
5005 ed.push_buffer_cursor_to_textarea();
5006 let cursor = ed.cursor();
5007 ed.mutate_edit(Edit::InsertStr {
5008 at: Position::new(cursor.0, cursor.1),
5009 text: inserted,
5010 });
5011 }
5012 LastChange::InsertAt {
5013 entry,
5014 inserted,
5015 count,
5016 } => {
5017 use hjkl_buffer::{Edit, Position};
5018 ed.push_undo();
5019 match entry {
5020 InsertEntry::I => {}
5021 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5022 InsertEntry::A => {
5023 ed.buffer_mut().move_right_to_end(1);
5024 ed.push_buffer_cursor_to_textarea();
5025 }
5026 InsertEntry::ShiftA => {
5027 ed.buffer_mut().move_line_end();
5028 ed.buffer_mut().move_right_to_end(1);
5029 ed.push_buffer_cursor_to_textarea();
5030 }
5031 }
5032 for _ in 0..count.max(1) {
5033 let cursor = ed.cursor();
5034 ed.mutate_edit(Edit::InsertStr {
5035 at: Position::new(cursor.0, cursor.1),
5036 text: inserted.clone(),
5037 });
5038 }
5039 }
5040 }
5041 ed.vim.replaying = false;
5042}
5043
5044fn extract_inserted(before: &str, after: &str) -> String {
5047 let before_chars: Vec<char> = before.chars().collect();
5048 let after_chars: Vec<char> = after.chars().collect();
5049 if after_chars.len() <= before_chars.len() {
5050 return String::new();
5051 }
5052 let prefix = before_chars
5053 .iter()
5054 .zip(after_chars.iter())
5055 .take_while(|(a, b)| a == b)
5056 .count();
5057 let max_suffix = before_chars.len() - prefix;
5058 let suffix = before_chars
5059 .iter()
5060 .rev()
5061 .zip(after_chars.iter().rev())
5062 .take(max_suffix)
5063 .take_while(|(a, b)| a == b)
5064 .count();
5065 after_chars[prefix..after_chars.len() - suffix]
5066 .iter()
5067 .collect()
5068}
5069
5070#[cfg(test)]
5073mod tests {
5074 use crate::editor::Editor;
5075 use crate::{KeybindingMode, VimMode};
5076 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5077
5078 fn run_keys(e: &mut Editor<'_>, keys: &str) {
5079 let mut iter = keys.chars().peekable();
5083 while let Some(c) = iter.next() {
5084 if c == '<' {
5085 let mut tag = String::new();
5086 for ch in iter.by_ref() {
5087 if ch == '>' {
5088 break;
5089 }
5090 tag.push(ch);
5091 }
5092 let ev = match tag.as_str() {
5093 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5094 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5095 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5096 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5097 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5098 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5099 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5100 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5101 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5105 s if s.starts_with("C-") => {
5106 let ch = s.chars().nth(2).unwrap();
5107 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5108 }
5109 _ => continue,
5110 };
5111 e.handle_key(ev);
5112 } else {
5113 let mods = if c.is_uppercase() {
5114 KeyModifiers::SHIFT
5115 } else {
5116 KeyModifiers::NONE
5117 };
5118 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5119 }
5120 }
5121 }
5122
5123 fn editor_with(content: &str) -> Editor<'static> {
5124 let mut e = Editor::new(KeybindingMode::Vim);
5125 e.set_content(content);
5126 e
5127 }
5128
5129 #[test]
5130 fn f_char_jumps_on_line() {
5131 let mut e = editor_with("hello world");
5132 run_keys(&mut e, "fw");
5133 assert_eq!(e.cursor(), (0, 6));
5134 }
5135
5136 #[test]
5137 fn cap_f_jumps_backward() {
5138 let mut e = editor_with("hello world");
5139 e.jump_cursor(0, 10);
5140 run_keys(&mut e, "Fo");
5141 assert_eq!(e.cursor().1, 7);
5142 }
5143
5144 #[test]
5145 fn t_stops_before_char() {
5146 let mut e = editor_with("hello");
5147 run_keys(&mut e, "tl");
5148 assert_eq!(e.cursor(), (0, 1));
5149 }
5150
5151 #[test]
5152 fn semicolon_repeats_find() {
5153 let mut e = editor_with("aa.bb.cc");
5154 run_keys(&mut e, "f.");
5155 assert_eq!(e.cursor().1, 2);
5156 run_keys(&mut e, ";");
5157 assert_eq!(e.cursor().1, 5);
5158 }
5159
5160 #[test]
5161 fn comma_repeats_find_reverse() {
5162 let mut e = editor_with("aa.bb.cc");
5163 run_keys(&mut e, "f.");
5164 run_keys(&mut e, ";");
5165 run_keys(&mut e, ",");
5166 assert_eq!(e.cursor().1, 2);
5167 }
5168
5169 #[test]
5170 fn di_quote_deletes_content() {
5171 let mut e = editor_with("foo \"bar\" baz");
5172 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5174 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5175 }
5176
5177 #[test]
5178 fn da_quote_deletes_with_quotes() {
5179 let mut e = editor_with("foo \"bar\" baz");
5180 e.jump_cursor(0, 6);
5181 run_keys(&mut e, "da\"");
5182 assert_eq!(e.buffer().lines()[0], "foo baz");
5183 }
5184
5185 #[test]
5186 fn ci_paren_deletes_and_inserts() {
5187 let mut e = editor_with("fn(a, b, c)");
5188 e.jump_cursor(0, 5);
5189 run_keys(&mut e, "ci(");
5190 assert_eq!(e.vim_mode(), VimMode::Insert);
5191 assert_eq!(e.buffer().lines()[0], "fn()");
5192 }
5193
5194 #[test]
5195 fn diw_deletes_inner_word() {
5196 let mut e = editor_with("hello world");
5197 e.jump_cursor(0, 2);
5198 run_keys(&mut e, "diw");
5199 assert_eq!(e.buffer().lines()[0], " world");
5200 }
5201
5202 #[test]
5203 fn daw_deletes_word_with_trailing_space() {
5204 let mut e = editor_with("hello world");
5205 run_keys(&mut e, "daw");
5206 assert_eq!(e.buffer().lines()[0], "world");
5207 }
5208
5209 #[test]
5210 fn percent_jumps_to_matching_bracket() {
5211 let mut e = editor_with("foo(bar)");
5212 e.jump_cursor(0, 3);
5213 run_keys(&mut e, "%");
5214 assert_eq!(e.cursor().1, 7);
5215 run_keys(&mut e, "%");
5216 assert_eq!(e.cursor().1, 3);
5217 }
5218
5219 #[test]
5220 fn dot_repeats_last_change() {
5221 let mut e = editor_with("aaa bbb ccc");
5222 run_keys(&mut e, "dw");
5223 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5224 run_keys(&mut e, ".");
5225 assert_eq!(e.buffer().lines()[0], "ccc");
5226 }
5227
5228 #[test]
5229 fn dot_repeats_change_operator_with_text() {
5230 let mut e = editor_with("foo foo foo");
5231 run_keys(&mut e, "cwbar<Esc>");
5232 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5233 run_keys(&mut e, "w");
5235 run_keys(&mut e, ".");
5236 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5237 }
5238
5239 #[test]
5240 fn dot_repeats_x() {
5241 let mut e = editor_with("abcdef");
5242 run_keys(&mut e, "x");
5243 run_keys(&mut e, "..");
5244 assert_eq!(e.buffer().lines()[0], "def");
5245 }
5246
5247 #[test]
5248 fn count_operator_motion_compose() {
5249 let mut e = editor_with("one two three four five");
5250 run_keys(&mut e, "d3w");
5251 assert_eq!(e.buffer().lines()[0], "four five");
5252 }
5253
5254 #[test]
5255 fn two_dd_deletes_two_lines() {
5256 let mut e = editor_with("a\nb\nc");
5257 run_keys(&mut e, "2dd");
5258 assert_eq!(e.buffer().lines().len(), 1);
5259 assert_eq!(e.buffer().lines()[0], "c");
5260 }
5261
5262 #[test]
5267 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5268 let mut e = editor_with("one\ntwo\n three\nfour");
5269 e.jump_cursor(1, 2);
5270 run_keys(&mut e, "dd");
5271 assert_eq!(e.buffer().lines()[1], " three");
5273 assert_eq!(e.cursor(), (1, 4));
5274 }
5275
5276 #[test]
5277 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5278 let mut e = editor_with("one\n two\nthree");
5279 e.jump_cursor(2, 0);
5280 run_keys(&mut e, "dd");
5281 assert_eq!(e.buffer().lines().len(), 2);
5283 assert_eq!(e.cursor(), (1, 2));
5284 }
5285
5286 #[test]
5287 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5288 let mut e = editor_with("lonely");
5289 run_keys(&mut e, "dd");
5290 assert_eq!(e.buffer().lines().len(), 1);
5291 assert_eq!(e.buffer().lines()[0], "");
5292 assert_eq!(e.cursor(), (0, 0));
5293 }
5294
5295 #[test]
5296 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5297 let mut e = editor_with("a\nb\nc\n d\ne");
5298 e.jump_cursor(1, 0);
5300 run_keys(&mut e, "3dd");
5301 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
5302 assert_eq!(e.cursor(), (1, 0));
5303 }
5304
5305 #[test]
5306 fn gu_lowercases_motion_range() {
5307 let mut e = editor_with("HELLO WORLD");
5308 run_keys(&mut e, "guw");
5309 assert_eq!(e.buffer().lines()[0], "hello WORLD");
5310 assert_eq!(e.cursor(), (0, 0));
5311 }
5312
5313 #[test]
5314 fn g_u_uppercases_text_object() {
5315 let mut e = editor_with("hello world");
5316 run_keys(&mut e, "gUiw");
5318 assert_eq!(e.buffer().lines()[0], "HELLO world");
5319 assert_eq!(e.cursor(), (0, 0));
5320 }
5321
5322 #[test]
5323 fn g_tilde_toggles_case_of_range() {
5324 let mut e = editor_with("Hello World");
5325 run_keys(&mut e, "g~iw");
5326 assert_eq!(e.buffer().lines()[0], "hELLO World");
5327 }
5328
5329 #[test]
5330 fn g_uu_uppercases_current_line() {
5331 let mut e = editor_with("select 1\nselect 2");
5332 run_keys(&mut e, "gUU");
5333 assert_eq!(e.buffer().lines()[0], "SELECT 1");
5334 assert_eq!(e.buffer().lines()[1], "select 2");
5335 }
5336
5337 #[test]
5338 fn gugu_lowercases_current_line() {
5339 let mut e = editor_with("FOO BAR\nBAZ");
5340 run_keys(&mut e, "gugu");
5341 assert_eq!(e.buffer().lines()[0], "foo bar");
5342 }
5343
5344 #[test]
5345 fn visual_u_uppercases_selection() {
5346 let mut e = editor_with("hello world");
5347 run_keys(&mut e, "veU");
5349 assert_eq!(e.buffer().lines()[0], "HELLO world");
5350 }
5351
5352 #[test]
5353 fn visual_line_u_lowercases_line() {
5354 let mut e = editor_with("HELLO WORLD\nOTHER");
5355 run_keys(&mut e, "Vu");
5356 assert_eq!(e.buffer().lines()[0], "hello world");
5357 assert_eq!(e.buffer().lines()[1], "OTHER");
5358 }
5359
5360 #[test]
5361 fn g_uu_with_count_uppercases_multiple_lines() {
5362 let mut e = editor_with("one\ntwo\nthree\nfour");
5363 run_keys(&mut e, "3gUU");
5365 assert_eq!(e.buffer().lines()[0], "ONE");
5366 assert_eq!(e.buffer().lines()[1], "TWO");
5367 assert_eq!(e.buffer().lines()[2], "THREE");
5368 assert_eq!(e.buffer().lines()[3], "four");
5369 }
5370
5371 #[test]
5372 fn double_gt_indents_current_line() {
5373 let mut e = editor_with("hello");
5374 run_keys(&mut e, ">>");
5375 assert_eq!(e.buffer().lines()[0], " hello");
5376 assert_eq!(e.cursor(), (0, 2));
5378 }
5379
5380 #[test]
5381 fn double_lt_outdents_current_line() {
5382 let mut e = editor_with(" hello");
5383 run_keys(&mut e, "<lt><lt>");
5384 assert_eq!(e.buffer().lines()[0], " hello");
5385 assert_eq!(e.cursor(), (0, 2));
5386 }
5387
5388 #[test]
5389 fn count_double_gt_indents_multiple_lines() {
5390 let mut e = editor_with("a\nb\nc\nd");
5391 run_keys(&mut e, "3>>");
5393 assert_eq!(e.buffer().lines()[0], " a");
5394 assert_eq!(e.buffer().lines()[1], " b");
5395 assert_eq!(e.buffer().lines()[2], " c");
5396 assert_eq!(e.buffer().lines()[3], "d");
5397 }
5398
5399 #[test]
5400 fn outdent_clips_ragged_leading_whitespace() {
5401 let mut e = editor_with(" x");
5404 run_keys(&mut e, "<lt><lt>");
5405 assert_eq!(e.buffer().lines()[0], "x");
5406 }
5407
5408 #[test]
5409 fn indent_motion_is_always_linewise() {
5410 let mut e = editor_with("foo bar");
5413 run_keys(&mut e, ">w");
5414 assert_eq!(e.buffer().lines()[0], " foo bar");
5415 }
5416
5417 #[test]
5418 fn indent_text_object_extends_over_paragraph() {
5419 let mut e = editor_with("a\nb\n\nc\nd");
5420 run_keys(&mut e, ">ap");
5422 assert_eq!(e.buffer().lines()[0], " a");
5423 assert_eq!(e.buffer().lines()[1], " b");
5424 assert_eq!(e.buffer().lines()[2], "");
5425 assert_eq!(e.buffer().lines()[3], "c");
5426 }
5427
5428 #[test]
5429 fn visual_line_indent_shifts_selected_rows() {
5430 let mut e = editor_with("x\ny\nz");
5431 run_keys(&mut e, "Vj>");
5433 assert_eq!(e.buffer().lines()[0], " x");
5434 assert_eq!(e.buffer().lines()[1], " y");
5435 assert_eq!(e.buffer().lines()[2], "z");
5436 }
5437
5438 #[test]
5439 fn outdent_empty_line_is_noop() {
5440 let mut e = editor_with("\nfoo");
5441 run_keys(&mut e, "<lt><lt>");
5442 assert_eq!(e.buffer().lines()[0], "");
5443 }
5444
5445 #[test]
5446 fn indent_skips_empty_lines() {
5447 let mut e = editor_with("");
5450 run_keys(&mut e, ">>");
5451 assert_eq!(e.buffer().lines()[0], "");
5452 }
5453
5454 #[test]
5455 fn insert_ctrl_t_indents_current_line() {
5456 let mut e = editor_with("x");
5457 run_keys(&mut e, "i<C-t>");
5459 assert_eq!(e.buffer().lines()[0], " x");
5460 assert_eq!(e.cursor(), (0, 2));
5463 }
5464
5465 #[test]
5466 fn insert_ctrl_d_outdents_current_line() {
5467 let mut e = editor_with(" x");
5468 run_keys(&mut e, "A<C-d>");
5470 assert_eq!(e.buffer().lines()[0], " x");
5471 }
5472
5473 #[test]
5474 fn h_at_col_zero_does_not_wrap_to_prev_line() {
5475 let mut e = editor_with("first\nsecond");
5476 e.jump_cursor(1, 0);
5477 run_keys(&mut e, "h");
5478 assert_eq!(e.cursor(), (1, 0));
5480 }
5481
5482 #[test]
5483 fn l_at_last_char_does_not_wrap_to_next_line() {
5484 let mut e = editor_with("ab\ncd");
5485 e.jump_cursor(0, 1);
5487 run_keys(&mut e, "l");
5488 assert_eq!(e.cursor(), (0, 1));
5490 }
5491
5492 #[test]
5493 fn count_l_clamps_at_line_end() {
5494 let mut e = editor_with("abcde");
5495 run_keys(&mut e, "20l");
5498 assert_eq!(e.cursor(), (0, 4));
5499 }
5500
5501 #[test]
5502 fn count_h_clamps_at_col_zero() {
5503 let mut e = editor_with("abcde");
5504 e.jump_cursor(0, 3);
5505 run_keys(&mut e, "20h");
5506 assert_eq!(e.cursor(), (0, 0));
5507 }
5508
5509 #[test]
5510 fn dl_on_last_char_still_deletes_it() {
5511 let mut e = editor_with("ab");
5515 e.jump_cursor(0, 1);
5516 run_keys(&mut e, "dl");
5517 assert_eq!(e.buffer().lines()[0], "a");
5518 }
5519
5520 #[test]
5521 fn case_op_preserves_yank_register() {
5522 let mut e = editor_with("target");
5523 run_keys(&mut e, "yy");
5524 let yank_before = e.yank().to_string();
5525 run_keys(&mut e, "gUU");
5527 assert_eq!(e.buffer().lines()[0], "TARGET");
5528 assert_eq!(
5529 e.yank(),
5530 yank_before,
5531 "case ops must preserve the yank buffer"
5532 );
5533 }
5534
5535 #[test]
5536 fn dap_deletes_paragraph() {
5537 let mut e = editor_with("a\nb\n\nc\nd");
5538 run_keys(&mut e, "dap");
5539 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
5540 }
5541
5542 #[test]
5543 fn dit_deletes_inner_tag_content() {
5544 let mut e = editor_with("<b>hello</b>");
5545 e.jump_cursor(0, 4);
5547 run_keys(&mut e, "dit");
5548 assert_eq!(e.buffer().lines()[0], "<b></b>");
5549 }
5550
5551 #[test]
5552 fn dat_deletes_around_tag() {
5553 let mut e = editor_with("hi <b>foo</b> bye");
5554 e.jump_cursor(0, 6);
5555 run_keys(&mut e, "dat");
5556 assert_eq!(e.buffer().lines()[0], "hi bye");
5557 }
5558
5559 #[test]
5560 fn dit_picks_innermost_tag() {
5561 let mut e = editor_with("<a><b>x</b></a>");
5562 e.jump_cursor(0, 6);
5564 run_keys(&mut e, "dit");
5565 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
5567 }
5568
5569 #[test]
5570 fn dat_innermost_tag_pair() {
5571 let mut e = editor_with("<a><b>x</b></a>");
5572 e.jump_cursor(0, 6);
5573 run_keys(&mut e, "dat");
5574 assert_eq!(e.buffer().lines()[0], "<a></a>");
5575 }
5576
5577 #[test]
5578 fn dit_outside_any_tag_no_op() {
5579 let mut e = editor_with("plain text");
5580 e.jump_cursor(0, 3);
5581 run_keys(&mut e, "dit");
5582 assert_eq!(e.buffer().lines()[0], "plain text");
5584 }
5585
5586 #[test]
5587 fn cit_changes_inner_tag_content() {
5588 let mut e = editor_with("<b>hello</b>");
5589 e.jump_cursor(0, 4);
5590 run_keys(&mut e, "citNEW<Esc>");
5591 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
5592 }
5593
5594 #[test]
5595 fn cat_changes_around_tag() {
5596 let mut e = editor_with("hi <b>foo</b> bye");
5597 e.jump_cursor(0, 6);
5598 run_keys(&mut e, "catBAR<Esc>");
5599 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
5600 }
5601
5602 #[test]
5603 fn yit_yanks_inner_tag_content() {
5604 let mut e = editor_with("<b>hello</b>");
5605 e.jump_cursor(0, 4);
5606 run_keys(&mut e, "yit");
5607 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5608 }
5609
5610 #[test]
5611 fn yat_yanks_full_tag_pair() {
5612 let mut e = editor_with("hi <b>foo</b> bye");
5613 e.jump_cursor(0, 6);
5614 run_keys(&mut e, "yat");
5615 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5616 }
5617
5618 #[test]
5619 fn vit_visually_selects_inner_tag() {
5620 let mut e = editor_with("<b>hello</b>");
5621 e.jump_cursor(0, 4);
5622 run_keys(&mut e, "vit");
5623 assert_eq!(e.vim_mode(), VimMode::Visual);
5624 run_keys(&mut e, "y");
5625 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5626 }
5627
5628 #[test]
5629 fn vat_visually_selects_around_tag() {
5630 let mut e = editor_with("x<b>foo</b>y");
5631 e.jump_cursor(0, 5);
5632 run_keys(&mut e, "vat");
5633 assert_eq!(e.vim_mode(), VimMode::Visual);
5634 run_keys(&mut e, "y");
5635 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5636 }
5637
5638 #[test]
5641 #[allow(non_snake_case)]
5642 fn diW_deletes_inner_big_word() {
5643 let mut e = editor_with("foo.bar baz");
5644 e.jump_cursor(0, 2);
5645 run_keys(&mut e, "diW");
5646 assert_eq!(e.buffer().lines()[0], " baz");
5648 }
5649
5650 #[test]
5651 #[allow(non_snake_case)]
5652 fn daW_deletes_around_big_word() {
5653 let mut e = editor_with("foo.bar baz");
5654 e.jump_cursor(0, 2);
5655 run_keys(&mut e, "daW");
5656 assert_eq!(e.buffer().lines()[0], "baz");
5657 }
5658
5659 #[test]
5660 fn di_double_quote_deletes_inside() {
5661 let mut e = editor_with("a \"hello\" b");
5662 e.jump_cursor(0, 4);
5663 run_keys(&mut e, "di\"");
5664 assert_eq!(e.buffer().lines()[0], "a \"\" b");
5665 }
5666
5667 #[test]
5668 fn da_double_quote_deletes_around() {
5669 let mut e = editor_with("a \"hello\" b");
5670 e.jump_cursor(0, 4);
5671 run_keys(&mut e, "da\"");
5672 assert_eq!(e.buffer().lines()[0], "a b");
5673 }
5674
5675 #[test]
5676 fn di_single_quote_deletes_inside() {
5677 let mut e = editor_with("x 'foo' y");
5678 e.jump_cursor(0, 4);
5679 run_keys(&mut e, "di'");
5680 assert_eq!(e.buffer().lines()[0], "x '' y");
5681 }
5682
5683 #[test]
5684 fn da_single_quote_deletes_around() {
5685 let mut e = editor_with("x 'foo' y");
5686 e.jump_cursor(0, 4);
5687 run_keys(&mut e, "da'");
5688 assert_eq!(e.buffer().lines()[0], "x y");
5689 }
5690
5691 #[test]
5692 fn di_backtick_deletes_inside() {
5693 let mut e = editor_with("p `q` r");
5694 e.jump_cursor(0, 3);
5695 run_keys(&mut e, "di`");
5696 assert_eq!(e.buffer().lines()[0], "p `` r");
5697 }
5698
5699 #[test]
5700 fn da_backtick_deletes_around() {
5701 let mut e = editor_with("p `q` r");
5702 e.jump_cursor(0, 3);
5703 run_keys(&mut e, "da`");
5704 assert_eq!(e.buffer().lines()[0], "p r");
5705 }
5706
5707 #[test]
5708 fn di_paren_deletes_inside() {
5709 let mut e = editor_with("f(arg)");
5710 e.jump_cursor(0, 3);
5711 run_keys(&mut e, "di(");
5712 assert_eq!(e.buffer().lines()[0], "f()");
5713 }
5714
5715 #[test]
5716 fn di_paren_alias_b_works() {
5717 let mut e = editor_with("f(arg)");
5718 e.jump_cursor(0, 3);
5719 run_keys(&mut e, "dib");
5720 assert_eq!(e.buffer().lines()[0], "f()");
5721 }
5722
5723 #[test]
5724 fn di_bracket_deletes_inside() {
5725 let mut e = editor_with("a[b,c]d");
5726 e.jump_cursor(0, 3);
5727 run_keys(&mut e, "di[");
5728 assert_eq!(e.buffer().lines()[0], "a[]d");
5729 }
5730
5731 #[test]
5732 fn da_bracket_deletes_around() {
5733 let mut e = editor_with("a[b,c]d");
5734 e.jump_cursor(0, 3);
5735 run_keys(&mut e, "da[");
5736 assert_eq!(e.buffer().lines()[0], "ad");
5737 }
5738
5739 #[test]
5740 fn di_brace_deletes_inside() {
5741 let mut e = editor_with("x{y}z");
5742 e.jump_cursor(0, 2);
5743 run_keys(&mut e, "di{");
5744 assert_eq!(e.buffer().lines()[0], "x{}z");
5745 }
5746
5747 #[test]
5748 fn da_brace_deletes_around() {
5749 let mut e = editor_with("x{y}z");
5750 e.jump_cursor(0, 2);
5751 run_keys(&mut e, "da{");
5752 assert_eq!(e.buffer().lines()[0], "xz");
5753 }
5754
5755 #[test]
5756 fn di_brace_alias_capital_b_works() {
5757 let mut e = editor_with("x{y}z");
5758 e.jump_cursor(0, 2);
5759 run_keys(&mut e, "diB");
5760 assert_eq!(e.buffer().lines()[0], "x{}z");
5761 }
5762
5763 #[test]
5764 fn di_angle_deletes_inside() {
5765 let mut e = editor_with("p<q>r");
5766 e.jump_cursor(0, 2);
5767 run_keys(&mut e, "di<lt>");
5769 assert_eq!(e.buffer().lines()[0], "p<>r");
5770 }
5771
5772 #[test]
5773 fn da_angle_deletes_around() {
5774 let mut e = editor_with("p<q>r");
5775 e.jump_cursor(0, 2);
5776 run_keys(&mut e, "da<lt>");
5777 assert_eq!(e.buffer().lines()[0], "pr");
5778 }
5779
5780 #[test]
5781 fn dip_deletes_inner_paragraph() {
5782 let mut e = editor_with("a\nb\nc\n\nd");
5783 e.jump_cursor(1, 0);
5784 run_keys(&mut e, "dip");
5785 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
5788 }
5789
5790 #[test]
5793 fn sentence_motion_close_paren_jumps_forward() {
5794 let mut e = editor_with("Alpha. Beta. Gamma.");
5795 e.jump_cursor(0, 0);
5796 run_keys(&mut e, ")");
5797 assert_eq!(e.cursor(), (0, 7));
5799 run_keys(&mut e, ")");
5800 assert_eq!(e.cursor(), (0, 13));
5801 }
5802
5803 #[test]
5804 fn sentence_motion_open_paren_jumps_backward() {
5805 let mut e = editor_with("Alpha. Beta. Gamma.");
5806 e.jump_cursor(0, 13);
5807 run_keys(&mut e, "(");
5808 assert_eq!(e.cursor(), (0, 7));
5811 run_keys(&mut e, "(");
5812 assert_eq!(e.cursor(), (0, 0));
5813 }
5814
5815 #[test]
5816 fn sentence_motion_count() {
5817 let mut e = editor_with("A. B. C. D.");
5818 e.jump_cursor(0, 0);
5819 run_keys(&mut e, "3)");
5820 assert_eq!(e.cursor(), (0, 9));
5822 }
5823
5824 #[test]
5825 fn dis_deletes_inner_sentence() {
5826 let mut e = editor_with("First one. Second one. Third one.");
5827 e.jump_cursor(0, 13);
5828 run_keys(&mut e, "dis");
5829 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
5831 }
5832
5833 #[test]
5834 fn das_deletes_around_sentence_with_trailing_space() {
5835 let mut e = editor_with("Alpha. Beta. Gamma.");
5836 e.jump_cursor(0, 8);
5837 run_keys(&mut e, "das");
5838 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
5841 }
5842
5843 #[test]
5844 fn dis_handles_double_terminator() {
5845 let mut e = editor_with("Wow!? Next.");
5846 e.jump_cursor(0, 1);
5847 run_keys(&mut e, "dis");
5848 assert_eq!(e.buffer().lines()[0], " Next.");
5851 }
5852
5853 #[test]
5854 fn dis_first_sentence_from_cursor_at_zero() {
5855 let mut e = editor_with("Alpha. Beta.");
5856 e.jump_cursor(0, 0);
5857 run_keys(&mut e, "dis");
5858 assert_eq!(e.buffer().lines()[0], " Beta.");
5859 }
5860
5861 #[test]
5862 fn yis_yanks_inner_sentence() {
5863 let mut e = editor_with("Hello world. Bye.");
5864 e.jump_cursor(0, 5);
5865 run_keys(&mut e, "yis");
5866 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
5867 }
5868
5869 #[test]
5870 fn vis_visually_selects_inner_sentence() {
5871 let mut e = editor_with("First. Second.");
5872 e.jump_cursor(0, 1);
5873 run_keys(&mut e, "vis");
5874 assert_eq!(e.vim_mode(), VimMode::Visual);
5875 run_keys(&mut e, "y");
5876 assert_eq!(e.registers().read('"').unwrap().text, "First.");
5877 }
5878
5879 #[test]
5880 fn ciw_changes_inner_word() {
5881 let mut e = editor_with("hello world");
5882 e.jump_cursor(0, 1);
5883 run_keys(&mut e, "ciwHEY<Esc>");
5884 assert_eq!(e.buffer().lines()[0], "HEY world");
5885 }
5886
5887 #[test]
5888 fn yiw_yanks_inner_word() {
5889 let mut e = editor_with("hello world");
5890 e.jump_cursor(0, 1);
5891 run_keys(&mut e, "yiw");
5892 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5893 }
5894
5895 #[test]
5896 fn viw_selects_inner_word() {
5897 let mut e = editor_with("hello world");
5898 e.jump_cursor(0, 2);
5899 run_keys(&mut e, "viw");
5900 assert_eq!(e.vim_mode(), VimMode::Visual);
5901 run_keys(&mut e, "y");
5902 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5903 }
5904
5905 #[test]
5906 fn ci_paren_changes_inside() {
5907 let mut e = editor_with("f(old)");
5908 e.jump_cursor(0, 3);
5909 run_keys(&mut e, "ci(NEW<Esc>");
5910 assert_eq!(e.buffer().lines()[0], "f(NEW)");
5911 }
5912
5913 #[test]
5914 fn yi_double_quote_yanks_inside() {
5915 let mut e = editor_with("say \"hi there\" then");
5916 e.jump_cursor(0, 6);
5917 run_keys(&mut e, "yi\"");
5918 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
5919 }
5920
5921 #[test]
5922 fn vap_visual_selects_around_paragraph() {
5923 let mut e = editor_with("a\nb\n\nc");
5924 e.jump_cursor(0, 0);
5925 run_keys(&mut e, "vap");
5926 assert_eq!(e.vim_mode(), VimMode::VisualLine);
5927 run_keys(&mut e, "y");
5928 let text = e.registers().read('"').unwrap().text.clone();
5930 assert!(text.starts_with("a\nb"));
5931 }
5932
5933 #[test]
5934 fn star_finds_next_occurrence() {
5935 let mut e = editor_with("foo bar foo baz");
5936 run_keys(&mut e, "*");
5937 assert_eq!(e.cursor().1, 8);
5938 }
5939
5940 #[test]
5941 fn star_skips_substring_match() {
5942 let mut e = editor_with("foo foobar baz");
5945 run_keys(&mut e, "*");
5946 assert_eq!(e.cursor().1, 0);
5947 }
5948
5949 #[test]
5950 fn g_star_matches_substring() {
5951 let mut e = editor_with("foo foobar baz");
5954 run_keys(&mut e, "g*");
5955 assert_eq!(e.cursor().1, 4);
5956 }
5957
5958 #[test]
5959 fn g_pound_matches_substring_backward() {
5960 let mut e = editor_with("foo foobar baz foo");
5963 run_keys(&mut e, "$b");
5964 assert_eq!(e.cursor().1, 15);
5965 run_keys(&mut e, "g#");
5966 assert_eq!(e.cursor().1, 4);
5967 }
5968
5969 #[test]
5970 fn n_repeats_last_search_forward() {
5971 let mut e = editor_with("foo bar foo baz foo");
5972 run_keys(&mut e, "/foo<CR>");
5975 assert_eq!(e.cursor().1, 8);
5976 run_keys(&mut e, "n");
5977 assert_eq!(e.cursor().1, 16);
5978 }
5979
5980 #[test]
5981 fn shift_n_reverses_search() {
5982 let mut e = editor_with("foo bar foo baz foo");
5983 run_keys(&mut e, "/foo<CR>");
5984 run_keys(&mut e, "n");
5985 assert_eq!(e.cursor().1, 16);
5986 run_keys(&mut e, "N");
5987 assert_eq!(e.cursor().1, 8);
5988 }
5989
5990 #[test]
5991 fn n_noop_without_pattern() {
5992 let mut e = editor_with("foo bar");
5993 run_keys(&mut e, "n");
5994 assert_eq!(e.cursor(), (0, 0));
5995 }
5996
5997 #[test]
5998 fn visual_line_preserves_cursor_column() {
5999 let mut e = editor_with("hello world\nanother one\nbye");
6002 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6004 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6005 assert_eq!(e.cursor(), (0, 5));
6006 run_keys(&mut e, "j");
6007 assert_eq!(e.cursor(), (1, 5));
6008 }
6009
6010 #[test]
6011 fn visual_line_yank_includes_trailing_newline() {
6012 let mut e = editor_with("aaa\nbbb\nccc");
6013 run_keys(&mut e, "Vjy");
6014 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6016 }
6017
6018 #[test]
6019 fn visual_line_yank_last_line_trailing_newline() {
6020 let mut e = editor_with("aaa\nbbb\nccc");
6021 run_keys(&mut e, "jj");
6023 run_keys(&mut e, "Vy");
6024 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6025 }
6026
6027 #[test]
6028 fn yy_on_last_line_has_trailing_newline() {
6029 let mut e = editor_with("aaa\nbbb\nccc");
6030 run_keys(&mut e, "jj");
6031 run_keys(&mut e, "yy");
6032 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6033 }
6034
6035 #[test]
6036 fn yy_in_middle_has_trailing_newline() {
6037 let mut e = editor_with("aaa\nbbb\nccc");
6038 run_keys(&mut e, "j");
6039 run_keys(&mut e, "yy");
6040 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6041 }
6042
6043 #[test]
6044 fn di_single_quote() {
6045 let mut e = editor_with("say 'hello world' now");
6046 e.jump_cursor(0, 7);
6047 run_keys(&mut e, "di'");
6048 assert_eq!(e.buffer().lines()[0], "say '' now");
6049 }
6050
6051 #[test]
6052 fn da_single_quote() {
6053 let mut e = editor_with("say 'hello' now");
6054 e.jump_cursor(0, 7);
6055 run_keys(&mut e, "da'");
6056 assert_eq!(e.buffer().lines()[0], "say now");
6057 }
6058
6059 #[test]
6060 fn di_backtick() {
6061 let mut e = editor_with("say `hi` now");
6062 e.jump_cursor(0, 5);
6063 run_keys(&mut e, "di`");
6064 assert_eq!(e.buffer().lines()[0], "say `` now");
6065 }
6066
6067 #[test]
6068 fn di_brace() {
6069 let mut e = editor_with("fn { a; b; c }");
6070 e.jump_cursor(0, 7);
6071 run_keys(&mut e, "di{");
6072 assert_eq!(e.buffer().lines()[0], "fn {}");
6073 }
6074
6075 #[test]
6076 fn di_bracket() {
6077 let mut e = editor_with("arr[1, 2, 3]");
6078 e.jump_cursor(0, 5);
6079 run_keys(&mut e, "di[");
6080 assert_eq!(e.buffer().lines()[0], "arr[]");
6081 }
6082
6083 #[test]
6084 fn dab_deletes_around_paren() {
6085 let mut e = editor_with("fn(a, b) + 1");
6086 e.jump_cursor(0, 4);
6087 run_keys(&mut e, "dab");
6088 assert_eq!(e.buffer().lines()[0], "fn + 1");
6089 }
6090
6091 #[test]
6092 fn da_big_b_deletes_around_brace() {
6093 let mut e = editor_with("x = {a: 1}");
6094 e.jump_cursor(0, 6);
6095 run_keys(&mut e, "daB");
6096 assert_eq!(e.buffer().lines()[0], "x = ");
6097 }
6098
6099 #[test]
6100 fn di_big_w_deletes_bigword() {
6101 let mut e = editor_with("foo-bar baz");
6102 e.jump_cursor(0, 2);
6103 run_keys(&mut e, "diW");
6104 assert_eq!(e.buffer().lines()[0], " baz");
6105 }
6106
6107 #[test]
6108 fn visual_select_inner_word() {
6109 let mut e = editor_with("hello world");
6110 e.jump_cursor(0, 2);
6111 run_keys(&mut e, "viw");
6112 assert_eq!(e.vim_mode(), VimMode::Visual);
6113 run_keys(&mut e, "y");
6114 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6115 }
6116
6117 #[test]
6118 fn visual_select_inner_quote() {
6119 let mut e = editor_with("foo \"bar\" baz");
6120 e.jump_cursor(0, 6);
6121 run_keys(&mut e, "vi\"");
6122 run_keys(&mut e, "y");
6123 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6124 }
6125
6126 #[test]
6127 fn visual_select_inner_paren() {
6128 let mut e = editor_with("fn(a, b)");
6129 e.jump_cursor(0, 4);
6130 run_keys(&mut e, "vi(");
6131 run_keys(&mut e, "y");
6132 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6133 }
6134
6135 #[test]
6136 fn visual_select_outer_brace() {
6137 let mut e = editor_with("{x}");
6138 e.jump_cursor(0, 1);
6139 run_keys(&mut e, "va{");
6140 run_keys(&mut e, "y");
6141 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6142 }
6143
6144 #[test]
6145 fn caw_changes_word_with_trailing_space() {
6146 let mut e = editor_with("hello world");
6147 run_keys(&mut e, "cawfoo<Esc>");
6148 assert_eq!(e.buffer().lines()[0], "fooworld");
6149 }
6150
6151 #[test]
6152 fn visual_char_yank_preserves_raw_text() {
6153 let mut e = editor_with("hello world");
6154 run_keys(&mut e, "vllly");
6155 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6156 }
6157
6158 #[test]
6159 fn single_line_visual_line_selects_full_line_on_yank() {
6160 let mut e = editor_with("hello world\nbye");
6161 run_keys(&mut e, "V");
6162 run_keys(&mut e, "y");
6165 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6166 }
6167
6168 #[test]
6169 fn visual_line_extends_both_directions() {
6170 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6171 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6173 assert_eq!(e.cursor(), (3, 0));
6174 run_keys(&mut e, "k");
6175 assert_eq!(e.cursor(), (2, 0));
6177 run_keys(&mut e, "k");
6178 assert_eq!(e.cursor(), (1, 0));
6179 }
6180
6181 #[test]
6182 fn visual_char_preserves_cursor_column() {
6183 let mut e = editor_with("hello world");
6184 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6186 assert_eq!(e.cursor(), (0, 5));
6187 run_keys(&mut e, "ll");
6188 assert_eq!(e.cursor(), (0, 7));
6189 }
6190
6191 #[test]
6192 fn visual_char_highlight_bounds_order() {
6193 let mut e = editor_with("abcdef");
6194 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
6196 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
6199 }
6200
6201 #[test]
6202 fn visual_line_highlight_bounds() {
6203 let mut e = editor_with("a\nb\nc");
6204 run_keys(&mut e, "V");
6205 assert_eq!(e.line_highlight(), Some((0, 0)));
6206 run_keys(&mut e, "j");
6207 assert_eq!(e.line_highlight(), Some((0, 1)));
6208 run_keys(&mut e, "j");
6209 assert_eq!(e.line_highlight(), Some((0, 2)));
6210 }
6211
6212 #[test]
6215 fn h_moves_left() {
6216 let mut e = editor_with("hello");
6217 e.jump_cursor(0, 3);
6218 run_keys(&mut e, "h");
6219 assert_eq!(e.cursor(), (0, 2));
6220 }
6221
6222 #[test]
6223 fn l_moves_right() {
6224 let mut e = editor_with("hello");
6225 run_keys(&mut e, "l");
6226 assert_eq!(e.cursor(), (0, 1));
6227 }
6228
6229 #[test]
6230 fn k_moves_up() {
6231 let mut e = editor_with("a\nb\nc");
6232 e.jump_cursor(2, 0);
6233 run_keys(&mut e, "k");
6234 assert_eq!(e.cursor(), (1, 0));
6235 }
6236
6237 #[test]
6238 fn zero_moves_to_line_start() {
6239 let mut e = editor_with(" hello");
6240 run_keys(&mut e, "$");
6241 run_keys(&mut e, "0");
6242 assert_eq!(e.cursor().1, 0);
6243 }
6244
6245 #[test]
6246 fn caret_moves_to_first_non_blank() {
6247 let mut e = editor_with(" hello");
6248 run_keys(&mut e, "0");
6249 run_keys(&mut e, "^");
6250 assert_eq!(e.cursor().1, 4);
6251 }
6252
6253 #[test]
6254 fn dollar_moves_to_last_char() {
6255 let mut e = editor_with("hello");
6256 run_keys(&mut e, "$");
6257 assert_eq!(e.cursor().1, 4);
6258 }
6259
6260 #[test]
6261 fn dollar_on_empty_line_stays_at_col_zero() {
6262 let mut e = editor_with("");
6263 run_keys(&mut e, "$");
6264 assert_eq!(e.cursor().1, 0);
6265 }
6266
6267 #[test]
6268 fn w_jumps_to_next_word() {
6269 let mut e = editor_with("foo bar baz");
6270 run_keys(&mut e, "w");
6271 assert_eq!(e.cursor().1, 4);
6272 }
6273
6274 #[test]
6275 fn b_jumps_back_a_word() {
6276 let mut e = editor_with("foo bar");
6277 e.jump_cursor(0, 6);
6278 run_keys(&mut e, "b");
6279 assert_eq!(e.cursor().1, 4);
6280 }
6281
6282 #[test]
6283 fn e_jumps_to_word_end() {
6284 let mut e = editor_with("foo bar");
6285 run_keys(&mut e, "e");
6286 assert_eq!(e.cursor().1, 2);
6287 }
6288
6289 #[test]
6292 fn d_dollar_deletes_to_eol() {
6293 let mut e = editor_with("hello world");
6294 e.jump_cursor(0, 5);
6295 run_keys(&mut e, "d$");
6296 assert_eq!(e.buffer().lines()[0], "hello");
6297 }
6298
6299 #[test]
6300 fn d_zero_deletes_to_line_start() {
6301 let mut e = editor_with("hello world");
6302 e.jump_cursor(0, 6);
6303 run_keys(&mut e, "d0");
6304 assert_eq!(e.buffer().lines()[0], "world");
6305 }
6306
6307 #[test]
6308 fn d_caret_deletes_to_first_non_blank() {
6309 let mut e = editor_with(" hello");
6310 e.jump_cursor(0, 6);
6311 run_keys(&mut e, "d^");
6312 assert_eq!(e.buffer().lines()[0], " llo");
6313 }
6314
6315 #[test]
6316 fn d_capital_g_deletes_to_end_of_file() {
6317 let mut e = editor_with("a\nb\nc\nd");
6318 e.jump_cursor(1, 0);
6319 run_keys(&mut e, "dG");
6320 assert_eq!(e.buffer().lines(), &["a".to_string()]);
6321 }
6322
6323 #[test]
6324 fn d_gg_deletes_to_start_of_file() {
6325 let mut e = editor_with("a\nb\nc\nd");
6326 e.jump_cursor(2, 0);
6327 run_keys(&mut e, "dgg");
6328 assert_eq!(e.buffer().lines(), &["d".to_string()]);
6329 }
6330
6331 #[test]
6332 fn cw_is_ce_quirk() {
6333 let mut e = editor_with("foo bar");
6336 run_keys(&mut e, "cwxyz<Esc>");
6337 assert_eq!(e.buffer().lines()[0], "xyz bar");
6338 }
6339
6340 #[test]
6343 fn big_d_deletes_to_eol() {
6344 let mut e = editor_with("hello world");
6345 e.jump_cursor(0, 5);
6346 run_keys(&mut e, "D");
6347 assert_eq!(e.buffer().lines()[0], "hello");
6348 }
6349
6350 #[test]
6351 fn big_c_deletes_to_eol_and_inserts() {
6352 let mut e = editor_with("hello world");
6353 e.jump_cursor(0, 5);
6354 run_keys(&mut e, "C!<Esc>");
6355 assert_eq!(e.buffer().lines()[0], "hello!");
6356 }
6357
6358 #[test]
6359 fn j_joins_next_line_with_space() {
6360 let mut e = editor_with("hello\nworld");
6361 run_keys(&mut e, "J");
6362 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6363 }
6364
6365 #[test]
6366 fn j_strips_leading_whitespace_on_join() {
6367 let mut e = editor_with("hello\n world");
6368 run_keys(&mut e, "J");
6369 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6370 }
6371
6372 #[test]
6373 fn big_x_deletes_char_before_cursor() {
6374 let mut e = editor_with("hello");
6375 e.jump_cursor(0, 3);
6376 run_keys(&mut e, "X");
6377 assert_eq!(e.buffer().lines()[0], "helo");
6378 }
6379
6380 #[test]
6381 fn s_substitutes_char_and_enters_insert() {
6382 let mut e = editor_with("hello");
6383 run_keys(&mut e, "sX<Esc>");
6384 assert_eq!(e.buffer().lines()[0], "Xello");
6385 }
6386
6387 #[test]
6388 fn count_x_deletes_many() {
6389 let mut e = editor_with("abcdef");
6390 run_keys(&mut e, "3x");
6391 assert_eq!(e.buffer().lines()[0], "def");
6392 }
6393
6394 #[test]
6397 fn p_pastes_charwise_after_cursor() {
6398 let mut e = editor_with("hello");
6399 run_keys(&mut e, "yw");
6400 run_keys(&mut e, "$p");
6401 assert_eq!(e.buffer().lines()[0], "hellohello");
6402 }
6403
6404 #[test]
6405 fn capital_p_pastes_charwise_before_cursor() {
6406 let mut e = editor_with("hello");
6407 run_keys(&mut e, "v");
6409 run_keys(&mut e, "l");
6410 run_keys(&mut e, "y");
6411 run_keys(&mut e, "$P");
6412 assert_eq!(e.buffer().lines()[0], "hellheo");
6415 }
6416
6417 #[test]
6418 fn p_pastes_linewise_below() {
6419 let mut e = editor_with("one\ntwo\nthree");
6420 run_keys(&mut e, "yy");
6421 run_keys(&mut e, "p");
6422 assert_eq!(
6423 e.buffer().lines(),
6424 &[
6425 "one".to_string(),
6426 "one".to_string(),
6427 "two".to_string(),
6428 "three".to_string()
6429 ]
6430 );
6431 }
6432
6433 #[test]
6434 fn capital_p_pastes_linewise_above() {
6435 let mut e = editor_with("one\ntwo");
6436 e.jump_cursor(1, 0);
6437 run_keys(&mut e, "yy");
6438 run_keys(&mut e, "P");
6439 assert_eq!(
6440 e.buffer().lines(),
6441 &["one".to_string(), "two".to_string(), "two".to_string()]
6442 );
6443 }
6444
6445 #[test]
6448 fn hash_finds_previous_occurrence() {
6449 let mut e = editor_with("foo bar foo baz foo");
6450 e.jump_cursor(0, 16);
6452 run_keys(&mut e, "#");
6453 assert_eq!(e.cursor().1, 8);
6454 }
6455
6456 #[test]
6459 fn visual_line_delete_removes_full_lines() {
6460 let mut e = editor_with("a\nb\nc\nd");
6461 run_keys(&mut e, "Vjd");
6462 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
6463 }
6464
6465 #[test]
6466 fn visual_line_change_leaves_blank_line() {
6467 let mut e = editor_with("a\nb\nc");
6468 run_keys(&mut e, "Vjc");
6469 assert_eq!(e.vim_mode(), VimMode::Insert);
6470 run_keys(&mut e, "X<Esc>");
6471 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
6475 }
6476
6477 #[test]
6478 fn cc_leaves_blank_line() {
6479 let mut e = editor_with("a\nb\nc");
6480 e.jump_cursor(1, 0);
6481 run_keys(&mut e, "ccX<Esc>");
6482 assert_eq!(
6483 e.buffer().lines(),
6484 &["a".to_string(), "X".to_string(), "c".to_string()]
6485 );
6486 }
6487
6488 #[test]
6493 fn big_w_skips_hyphens() {
6494 let mut e = editor_with("foo-bar baz");
6496 run_keys(&mut e, "W");
6497 assert_eq!(e.cursor().1, 8);
6498 }
6499
6500 #[test]
6501 fn big_w_crosses_lines() {
6502 let mut e = editor_with("foo-bar\nbaz-qux");
6503 run_keys(&mut e, "W");
6504 assert_eq!(e.cursor(), (1, 0));
6505 }
6506
6507 #[test]
6508 fn big_b_skips_hyphens() {
6509 let mut e = editor_with("foo-bar baz");
6510 e.jump_cursor(0, 9);
6511 run_keys(&mut e, "B");
6512 assert_eq!(e.cursor().1, 8);
6513 run_keys(&mut e, "B");
6514 assert_eq!(e.cursor().1, 0);
6515 }
6516
6517 #[test]
6518 fn big_e_jumps_to_big_word_end() {
6519 let mut e = editor_with("foo-bar baz");
6520 run_keys(&mut e, "E");
6521 assert_eq!(e.cursor().1, 6);
6522 run_keys(&mut e, "E");
6523 assert_eq!(e.cursor().1, 10);
6524 }
6525
6526 #[test]
6527 fn dw_with_big_word_variant() {
6528 let mut e = editor_with("foo-bar baz");
6530 run_keys(&mut e, "dW");
6531 assert_eq!(e.buffer().lines()[0], "baz");
6532 }
6533
6534 #[test]
6537 fn insert_ctrl_w_deletes_word_back() {
6538 let mut e = editor_with("");
6539 run_keys(&mut e, "i");
6540 for c in "hello world".chars() {
6541 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6542 }
6543 run_keys(&mut e, "<C-w>");
6544 assert_eq!(e.buffer().lines()[0], "hello ");
6545 }
6546
6547 #[test]
6548 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
6549 let mut e = editor_with("hello\nworld");
6553 e.jump_cursor(1, 0);
6554 run_keys(&mut e, "i");
6555 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6556 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
6559 assert_eq!(e.cursor(), (0, 0));
6560 }
6561
6562 #[test]
6563 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
6564 let mut e = editor_with("foo bar\nbaz");
6565 e.jump_cursor(1, 0);
6566 run_keys(&mut e, "i");
6567 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6568 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
6570 assert_eq!(e.cursor(), (0, 4));
6571 }
6572
6573 #[test]
6574 fn insert_ctrl_u_deletes_to_line_start() {
6575 let mut e = editor_with("");
6576 run_keys(&mut e, "i");
6577 for c in "hello world".chars() {
6578 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6579 }
6580 run_keys(&mut e, "<C-u>");
6581 assert_eq!(e.buffer().lines()[0], "");
6582 }
6583
6584 #[test]
6585 fn insert_ctrl_o_runs_one_normal_command() {
6586 let mut e = editor_with("hello world");
6587 run_keys(&mut e, "A");
6589 assert_eq!(e.vim_mode(), VimMode::Insert);
6590 e.jump_cursor(0, 0);
6592 run_keys(&mut e, "<C-o>");
6593 assert_eq!(e.vim_mode(), VimMode::Normal);
6594 run_keys(&mut e, "dw");
6595 assert_eq!(e.vim_mode(), VimMode::Insert);
6597 assert_eq!(e.buffer().lines()[0], "world");
6598 }
6599
6600 #[test]
6603 fn j_through_empty_line_preserves_column() {
6604 let mut e = editor_with("hello world\n\nanother line");
6605 run_keys(&mut e, "llllll");
6607 assert_eq!(e.cursor(), (0, 6));
6608 run_keys(&mut e, "j");
6611 assert_eq!(e.cursor(), (1, 0));
6612 run_keys(&mut e, "j");
6614 assert_eq!(e.cursor(), (2, 6));
6615 }
6616
6617 #[test]
6618 fn j_through_shorter_line_preserves_column() {
6619 let mut e = editor_with("hello world\nhi\nanother line");
6620 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
6623 run_keys(&mut e, "j");
6624 assert_eq!(e.cursor(), (2, 7));
6625 }
6626
6627 #[test]
6628 fn esc_from_insert_sticky_matches_visible_cursor() {
6629 let mut e = editor_with(" this is a line\n another one of a similar size");
6633 e.jump_cursor(0, 12);
6634 run_keys(&mut e, "I");
6635 assert_eq!(e.cursor(), (0, 4));
6636 run_keys(&mut e, "X<Esc>");
6637 assert_eq!(e.cursor(), (0, 4));
6638 run_keys(&mut e, "j");
6639 assert_eq!(e.cursor(), (1, 4));
6640 }
6641
6642 #[test]
6643 fn esc_from_insert_sticky_tracks_inserted_chars() {
6644 let mut e = editor_with("xxxxxxx\nyyyyyyy");
6645 run_keys(&mut e, "i");
6646 run_keys(&mut e, "abc<Esc>");
6647 assert_eq!(e.cursor(), (0, 2));
6648 run_keys(&mut e, "j");
6649 assert_eq!(e.cursor(), (1, 2));
6650 }
6651
6652 #[test]
6653 fn esc_from_insert_sticky_tracks_arrow_nav() {
6654 let mut e = editor_with("xxxxxx\nyyyyyy");
6655 run_keys(&mut e, "i");
6656 run_keys(&mut e, "abc");
6657 for _ in 0..2 {
6658 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
6659 }
6660 run_keys(&mut e, "<Esc>");
6661 assert_eq!(e.cursor(), (0, 0));
6662 run_keys(&mut e, "j");
6663 assert_eq!(e.cursor(), (1, 0));
6664 }
6665
6666 #[test]
6667 fn esc_from_insert_at_col_14_followed_by_j() {
6668 let line = "x".repeat(30);
6671 let buf = format!("{line}\n{line}");
6672 let mut e = editor_with(&buf);
6673 e.jump_cursor(0, 14);
6674 run_keys(&mut e, "i");
6675 for c in "test ".chars() {
6676 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6677 }
6678 run_keys(&mut e, "<Esc>");
6679 assert_eq!(e.cursor(), (0, 18));
6680 run_keys(&mut e, "j");
6681 assert_eq!(e.cursor(), (1, 18));
6682 }
6683
6684 #[test]
6685 fn linewise_paste_resets_sticky_column() {
6686 let mut e = editor_with(" hello\naaaaaaaa\nbye");
6690 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
6692 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
6696 run_keys(&mut e, "j");
6698 assert_eq!(e.cursor(), (3, 2));
6699 }
6700
6701 #[test]
6702 fn horizontal_motion_resyncs_sticky_column() {
6703 let mut e = editor_with("hello world\n\nanother line");
6707 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
6710 assert_eq!(e.cursor(), (2, 3));
6711 }
6712
6713 #[test]
6716 fn ctrl_v_enters_visual_block() {
6717 let mut e = editor_with("aaa\nbbb\nccc");
6718 run_keys(&mut e, "<C-v>");
6719 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
6720 }
6721
6722 #[test]
6723 fn visual_block_esc_returns_to_normal() {
6724 let mut e = editor_with("aaa\nbbb\nccc");
6725 run_keys(&mut e, "<C-v>");
6726 run_keys(&mut e, "<Esc>");
6727 assert_eq!(e.vim_mode(), VimMode::Normal);
6728 }
6729
6730 #[test]
6731 fn visual_block_delete_removes_column_range() {
6732 let mut e = editor_with("hello\nworld\nhappy");
6733 run_keys(&mut e, "l");
6735 run_keys(&mut e, "<C-v>");
6736 run_keys(&mut e, "jj");
6737 run_keys(&mut e, "ll");
6738 run_keys(&mut e, "d");
6739 assert_eq!(
6741 e.buffer().lines(),
6742 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
6743 );
6744 }
6745
6746 #[test]
6747 fn visual_block_yank_joins_with_newlines() {
6748 let mut e = editor_with("hello\nworld\nhappy");
6749 run_keys(&mut e, "<C-v>");
6750 run_keys(&mut e, "jj");
6751 run_keys(&mut e, "ll");
6752 run_keys(&mut e, "y");
6753 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
6754 }
6755
6756 #[test]
6757 fn visual_block_replace_fills_block() {
6758 let mut e = editor_with("hello\nworld\nhappy");
6759 run_keys(&mut e, "<C-v>");
6760 run_keys(&mut e, "jj");
6761 run_keys(&mut e, "ll");
6762 run_keys(&mut e, "rx");
6763 assert_eq!(
6764 e.buffer().lines(),
6765 &[
6766 "xxxlo".to_string(),
6767 "xxxld".to_string(),
6768 "xxxpy".to_string()
6769 ]
6770 );
6771 }
6772
6773 #[test]
6774 fn visual_block_insert_repeats_across_rows() {
6775 let mut e = editor_with("hello\nworld\nhappy");
6776 run_keys(&mut e, "<C-v>");
6777 run_keys(&mut e, "jj");
6778 run_keys(&mut e, "I");
6779 run_keys(&mut e, "# <Esc>");
6780 assert_eq!(
6781 e.buffer().lines(),
6782 &[
6783 "# hello".to_string(),
6784 "# world".to_string(),
6785 "# happy".to_string()
6786 ]
6787 );
6788 }
6789
6790 #[test]
6791 fn block_highlight_returns_none_outside_block_mode() {
6792 let mut e = editor_with("abc");
6793 assert!(e.block_highlight().is_none());
6794 run_keys(&mut e, "v");
6795 assert!(e.block_highlight().is_none());
6796 run_keys(&mut e, "<Esc>V");
6797 assert!(e.block_highlight().is_none());
6798 }
6799
6800 #[test]
6801 fn block_highlight_bounds_track_anchor_and_cursor() {
6802 let mut e = editor_with("aaaa\nbbbb\ncccc");
6803 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
6805 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
6808 }
6809
6810 #[test]
6811 fn visual_block_delete_handles_short_lines() {
6812 let mut e = editor_with("hello\nhi\nworld");
6814 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
6816 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
6818 assert_eq!(
6823 e.buffer().lines(),
6824 &["ho".to_string(), "h".to_string(), "wd".to_string()]
6825 );
6826 }
6827
6828 #[test]
6829 fn visual_block_yank_pads_short_lines_with_empties() {
6830 let mut e = editor_with("hello\nhi\nworld");
6831 run_keys(&mut e, "l");
6832 run_keys(&mut e, "<C-v>");
6833 run_keys(&mut e, "jjll");
6834 run_keys(&mut e, "y");
6835 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
6837 }
6838
6839 #[test]
6840 fn visual_block_replace_skips_past_eol() {
6841 let mut e = editor_with("ab\ncd\nef");
6844 run_keys(&mut e, "l");
6846 run_keys(&mut e, "<C-v>");
6847 run_keys(&mut e, "jjllllll");
6848 run_keys(&mut e, "rX");
6849 assert_eq!(
6852 e.buffer().lines(),
6853 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
6854 );
6855 }
6856
6857 #[test]
6858 fn visual_block_with_empty_line_in_middle() {
6859 let mut e = editor_with("abcd\n\nefgh");
6860 run_keys(&mut e, "<C-v>");
6861 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
6863 assert_eq!(
6866 e.buffer().lines(),
6867 &["d".to_string(), "".to_string(), "h".to_string()]
6868 );
6869 }
6870
6871 #[test]
6872 fn block_insert_pads_empty_lines_to_block_column() {
6873 let mut e = editor_with("this is a line\n\nthis is a line");
6876 e.jump_cursor(0, 3);
6877 run_keys(&mut e, "<C-v>");
6878 run_keys(&mut e, "jj");
6879 run_keys(&mut e, "I");
6880 run_keys(&mut e, "XX<Esc>");
6881 assert_eq!(
6882 e.buffer().lines(),
6883 &[
6884 "thiXXs is a line".to_string(),
6885 " XX".to_string(),
6886 "thiXXs is a line".to_string()
6887 ]
6888 );
6889 }
6890
6891 #[test]
6892 fn block_insert_pads_short_lines_to_block_column() {
6893 let mut e = editor_with("aaaaa\nbb\naaaaa");
6894 e.jump_cursor(0, 3);
6895 run_keys(&mut e, "<C-v>");
6896 run_keys(&mut e, "jj");
6897 run_keys(&mut e, "I");
6898 run_keys(&mut e, "Y<Esc>");
6899 assert_eq!(
6901 e.buffer().lines(),
6902 &[
6903 "aaaYaa".to_string(),
6904 "bb Y".to_string(),
6905 "aaaYaa".to_string()
6906 ]
6907 );
6908 }
6909
6910 #[test]
6911 fn visual_block_append_repeats_across_rows() {
6912 let mut e = editor_with("foo\nbar\nbaz");
6913 run_keys(&mut e, "<C-v>");
6914 run_keys(&mut e, "jj");
6915 run_keys(&mut e, "A");
6918 run_keys(&mut e, "!<Esc>");
6919 assert_eq!(
6920 e.buffer().lines(),
6921 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
6922 );
6923 }
6924
6925 #[test]
6928 fn slash_opens_forward_search_prompt() {
6929 let mut e = editor_with("hello world");
6930 run_keys(&mut e, "/");
6931 let p = e.search_prompt().expect("prompt should be active");
6932 assert!(p.text.is_empty());
6933 assert!(p.forward);
6934 }
6935
6936 #[test]
6937 fn question_opens_backward_search_prompt() {
6938 let mut e = editor_with("hello world");
6939 run_keys(&mut e, "?");
6940 let p = e.search_prompt().expect("prompt should be active");
6941 assert!(!p.forward);
6942 }
6943
6944 #[test]
6945 fn search_prompt_typing_updates_pattern_live() {
6946 let mut e = editor_with("foo bar\nbaz");
6947 run_keys(&mut e, "/bar");
6948 assert_eq!(e.search_prompt().unwrap().text, "bar");
6949 assert!(e.buffer().search_pattern().is_some());
6951 }
6952
6953 #[test]
6954 fn search_prompt_backspace_and_enter() {
6955 let mut e = editor_with("hello world\nagain");
6956 run_keys(&mut e, "/worlx");
6957 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
6958 assert_eq!(e.search_prompt().unwrap().text, "worl");
6959 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6960 assert!(e.search_prompt().is_none());
6962 assert_eq!(e.last_search(), Some("worl"));
6963 assert_eq!(e.cursor(), (0, 6));
6964 }
6965
6966 #[test]
6967 fn empty_search_prompt_enter_repeats_last_search() {
6968 let mut e = editor_with("foo bar foo baz foo");
6969 run_keys(&mut e, "/foo");
6970 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6971 assert_eq!(e.cursor().1, 8);
6972 run_keys(&mut e, "/");
6974 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6975 assert_eq!(e.cursor().1, 16);
6976 assert_eq!(e.last_search(), Some("foo"));
6977 }
6978
6979 #[test]
6980 fn search_history_records_committed_patterns() {
6981 let mut e = editor_with("alpha beta gamma");
6982 run_keys(&mut e, "/alpha<CR>");
6983 run_keys(&mut e, "/beta<CR>");
6984 let history = e.vim.search_history.clone();
6986 assert_eq!(history, vec!["alpha", "beta"]);
6987 }
6988
6989 #[test]
6990 fn search_history_dedupes_consecutive_repeats() {
6991 let mut e = editor_with("foo bar foo");
6992 run_keys(&mut e, "/foo<CR>");
6993 run_keys(&mut e, "/foo<CR>");
6994 run_keys(&mut e, "/bar<CR>");
6995 run_keys(&mut e, "/bar<CR>");
6996 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
6998 }
6999
7000 #[test]
7001 fn ctrl_p_walks_history_backward() {
7002 let mut e = editor_with("alpha beta gamma");
7003 run_keys(&mut e, "/alpha<CR>");
7004 run_keys(&mut e, "/beta<CR>");
7005 run_keys(&mut e, "/");
7007 assert_eq!(e.search_prompt().unwrap().text, "");
7008 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7009 assert_eq!(e.search_prompt().unwrap().text, "beta");
7010 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7011 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7012 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7014 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7015 }
7016
7017 #[test]
7018 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7019 let mut e = editor_with("a b c");
7020 run_keys(&mut e, "/a<CR>");
7021 run_keys(&mut e, "/b<CR>");
7022 run_keys(&mut e, "/c<CR>");
7023 run_keys(&mut e, "/");
7024 for _ in 0..3 {
7026 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7027 }
7028 assert_eq!(e.search_prompt().unwrap().text, "a");
7029 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7030 assert_eq!(e.search_prompt().unwrap().text, "b");
7031 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7032 assert_eq!(e.search_prompt().unwrap().text, "c");
7033 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7035 assert_eq!(e.search_prompt().unwrap().text, "c");
7036 }
7037
7038 #[test]
7039 fn typing_after_history_walk_resets_cursor() {
7040 let mut e = editor_with("foo");
7041 run_keys(&mut e, "/foo<CR>");
7042 run_keys(&mut e, "/");
7043 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7044 assert_eq!(e.search_prompt().unwrap().text, "foo");
7045 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7048 assert_eq!(e.search_prompt().unwrap().text, "foox");
7049 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7050 assert_eq!(e.search_prompt().unwrap().text, "foo");
7051 }
7052
7053 #[test]
7054 fn empty_backward_search_prompt_enter_repeats_last_search() {
7055 let mut e = editor_with("foo bar foo baz foo");
7056 run_keys(&mut e, "/foo");
7058 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7059 assert_eq!(e.cursor().1, 8);
7060 run_keys(&mut e, "?");
7061 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7062 assert_eq!(e.cursor().1, 0);
7063 assert_eq!(e.last_search(), Some("foo"));
7064 }
7065
7066 #[test]
7067 fn search_prompt_esc_cancels_but_keeps_last_search() {
7068 let mut e = editor_with("foo bar\nbaz");
7069 run_keys(&mut e, "/bar");
7070 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7071 assert!(e.search_prompt().is_none());
7072 assert_eq!(e.last_search(), Some("bar"));
7073 }
7074
7075 #[test]
7076 fn search_then_n_and_shift_n_navigate() {
7077 let mut e = editor_with("foo bar foo baz foo");
7078 run_keys(&mut e, "/foo");
7079 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7080 assert_eq!(e.cursor().1, 8);
7082 run_keys(&mut e, "n");
7083 assert_eq!(e.cursor().1, 16);
7084 run_keys(&mut e, "N");
7085 assert_eq!(e.cursor().1, 8);
7086 }
7087
7088 #[test]
7089 fn question_mark_searches_backward_on_enter() {
7090 let mut e = editor_with("foo bar foo baz");
7091 e.jump_cursor(0, 10);
7092 run_keys(&mut e, "?foo");
7093 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7094 assert_eq!(e.cursor(), (0, 8));
7096 }
7097
7098 #[test]
7101 fn big_y_yanks_to_end_of_line() {
7102 let mut e = editor_with("hello world");
7103 e.jump_cursor(0, 6);
7104 run_keys(&mut e, "Y");
7105 assert_eq!(e.last_yank.as_deref(), Some("world"));
7106 }
7107
7108 #[test]
7109 fn big_y_from_line_start_yanks_full_line() {
7110 let mut e = editor_with("hello world");
7111 run_keys(&mut e, "Y");
7112 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7113 }
7114
7115 #[test]
7116 fn gj_joins_without_inserting_space() {
7117 let mut e = editor_with("hello\n world");
7118 run_keys(&mut e, "gJ");
7119 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7121 }
7122
7123 #[test]
7124 fn gj_noop_on_last_line() {
7125 let mut e = editor_with("only");
7126 run_keys(&mut e, "gJ");
7127 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7128 }
7129
7130 #[test]
7131 fn ge_jumps_to_previous_word_end() {
7132 let mut e = editor_with("foo bar baz");
7133 e.jump_cursor(0, 5);
7134 run_keys(&mut e, "ge");
7135 assert_eq!(e.cursor(), (0, 2));
7136 }
7137
7138 #[test]
7139 fn ge_respects_word_class() {
7140 let mut e = editor_with("foo-bar baz");
7143 e.jump_cursor(0, 5);
7144 run_keys(&mut e, "ge");
7145 assert_eq!(e.cursor(), (0, 3));
7146 }
7147
7148 #[test]
7149 fn big_ge_treats_hyphens_as_part_of_word() {
7150 let mut e = editor_with("foo-bar baz");
7153 e.jump_cursor(0, 10);
7154 run_keys(&mut e, "gE");
7155 assert_eq!(e.cursor(), (0, 6));
7156 }
7157
7158 #[test]
7159 fn ge_crosses_line_boundary() {
7160 let mut e = editor_with("foo\nbar");
7161 e.jump_cursor(1, 0);
7162 run_keys(&mut e, "ge");
7163 assert_eq!(e.cursor(), (0, 2));
7164 }
7165
7166 #[test]
7167 fn dge_deletes_to_end_of_previous_word() {
7168 let mut e = editor_with("foo bar baz");
7169 e.jump_cursor(0, 8);
7170 run_keys(&mut e, "dge");
7173 assert_eq!(e.buffer().lines()[0], "foo baaz");
7174 }
7175
7176 #[test]
7177 fn ctrl_scroll_keys_do_not_panic() {
7178 let mut e = editor_with(
7181 (0..50)
7182 .map(|i| format!("line{i}"))
7183 .collect::<Vec<_>>()
7184 .join("\n")
7185 .as_str(),
7186 );
7187 run_keys(&mut e, "<C-f>");
7188 run_keys(&mut e, "<C-b>");
7189 assert!(!e.buffer().lines().is_empty());
7191 }
7192
7193 #[test]
7200 fn count_insert_with_arrow_nav_does_not_leak_rows() {
7201 let mut e = Editor::new(KeybindingMode::Vim);
7202 e.set_content("row0\nrow1\nrow2");
7203 run_keys(&mut e, "3iX<Down><Esc>");
7205 assert!(e.buffer().lines()[0].contains('X'));
7207 assert!(
7210 !e.buffer().lines()[1].contains("row0"),
7211 "row1 leaked row0 contents: {:?}",
7212 e.buffer().lines()[1]
7213 );
7214 assert_eq!(e.buffer().lines().len(), 3);
7217 }
7218
7219 fn editor_with_rows(n: usize, viewport: u16) -> Editor<'static> {
7222 let mut e = Editor::new(KeybindingMode::Vim);
7223 let body = (0..n)
7224 .map(|i| format!(" line{}", i))
7225 .collect::<Vec<_>>()
7226 .join("\n");
7227 e.set_content(&body);
7228 e.set_viewport_height(viewport);
7229 e
7230 }
7231
7232 #[test]
7233 fn ctrl_d_moves_cursor_half_page_down() {
7234 let mut e = editor_with_rows(100, 20);
7235 run_keys(&mut e, "<C-d>");
7236 assert_eq!(e.cursor().0, 10);
7237 }
7238
7239 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor<'static> {
7240 let mut e = Editor::new(KeybindingMode::Vim);
7241 e.set_content(&lines.join("\n"));
7242 e.set_viewport_height(viewport);
7243 let v = e.buffer_mut().viewport_mut();
7244 v.height = viewport;
7245 v.width = text_width;
7246 v.text_width = text_width;
7247 v.wrap = hjkl_buffer::Wrap::Char;
7248 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
7249 e
7250 }
7251
7252 #[test]
7253 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
7254 let lines = ["aaaabbbbcccc"; 10];
7258 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7259 e.jump_cursor(4, 0);
7260 e.ensure_cursor_in_scrolloff();
7261 let csr = e.buffer().cursor_screen_row().unwrap();
7262 assert!(csr <= 6, "csr={csr}");
7263 }
7264
7265 #[test]
7266 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
7267 let lines = ["aaaabbbbcccc"; 10];
7268 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7269 e.jump_cursor(7, 0);
7272 e.ensure_cursor_in_scrolloff();
7273 e.jump_cursor(2, 0);
7274 e.ensure_cursor_in_scrolloff();
7275 let csr = e.buffer().cursor_screen_row().unwrap();
7276 assert!(csr >= 5, "csr={csr}");
7278 }
7279
7280 #[test]
7281 fn scrolloff_wrap_clamps_top_at_buffer_end() {
7282 let lines = ["aaaabbbbcccc"; 5];
7283 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7284 e.jump_cursor(4, 11);
7285 e.ensure_cursor_in_scrolloff();
7286 let top = e.buffer().viewport().top_row;
7291 assert_eq!(top, 1);
7292 }
7293
7294 #[test]
7295 fn ctrl_u_moves_cursor_half_page_up() {
7296 let mut e = editor_with_rows(100, 20);
7297 e.jump_cursor(50, 0);
7298 run_keys(&mut e, "<C-u>");
7299 assert_eq!(e.cursor().0, 40);
7300 }
7301
7302 #[test]
7303 fn ctrl_f_moves_cursor_full_page_down() {
7304 let mut e = editor_with_rows(100, 20);
7305 run_keys(&mut e, "<C-f>");
7306 assert_eq!(e.cursor().0, 18);
7308 }
7309
7310 #[test]
7311 fn ctrl_b_moves_cursor_full_page_up() {
7312 let mut e = editor_with_rows(100, 20);
7313 e.jump_cursor(50, 0);
7314 run_keys(&mut e, "<C-b>");
7315 assert_eq!(e.cursor().0, 32);
7316 }
7317
7318 #[test]
7319 fn ctrl_d_lands_on_first_non_blank() {
7320 let mut e = editor_with_rows(100, 20);
7321 run_keys(&mut e, "<C-d>");
7322 assert_eq!(e.cursor().1, 2);
7324 }
7325
7326 #[test]
7327 fn ctrl_d_clamps_at_end_of_buffer() {
7328 let mut e = editor_with_rows(5, 20);
7329 run_keys(&mut e, "<C-d>");
7330 assert_eq!(e.cursor().0, 4);
7331 }
7332
7333 #[test]
7334 fn capital_h_jumps_to_viewport_top() {
7335 let mut e = editor_with_rows(100, 10);
7336 e.jump_cursor(50, 0);
7337 e.set_viewport_top(45);
7338 let top = e.buffer().viewport().top_row;
7339 run_keys(&mut e, "H");
7340 assert_eq!(e.cursor().0, top);
7341 assert_eq!(e.cursor().1, 2);
7342 }
7343
7344 #[test]
7345 fn capital_l_jumps_to_viewport_bottom() {
7346 let mut e = editor_with_rows(100, 10);
7347 e.jump_cursor(50, 0);
7348 e.set_viewport_top(45);
7349 let top = e.buffer().viewport().top_row;
7350 run_keys(&mut e, "L");
7351 assert_eq!(e.cursor().0, top + 9);
7352 }
7353
7354 #[test]
7355 fn capital_m_jumps_to_viewport_middle() {
7356 let mut e = editor_with_rows(100, 10);
7357 e.jump_cursor(50, 0);
7358 e.set_viewport_top(45);
7359 let top = e.buffer().viewport().top_row;
7360 run_keys(&mut e, "M");
7361 assert_eq!(e.cursor().0, top + 4);
7363 }
7364
7365 #[test]
7366 fn g_capital_m_lands_at_line_midpoint() {
7367 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
7369 assert_eq!(e.cursor(), (0, 6));
7371 }
7372
7373 #[test]
7374 fn g_capital_m_on_empty_line_stays_at_zero() {
7375 let mut e = editor_with("");
7376 run_keys(&mut e, "gM");
7377 assert_eq!(e.cursor(), (0, 0));
7378 }
7379
7380 #[test]
7381 fn g_capital_m_uses_current_line_only() {
7382 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
7385 run_keys(&mut e, "gM");
7386 assert_eq!(e.cursor(), (1, 6));
7387 }
7388
7389 #[test]
7390 fn capital_h_count_offsets_from_top() {
7391 let mut e = editor_with_rows(100, 10);
7392 e.jump_cursor(50, 0);
7393 e.set_viewport_top(45);
7394 let top = e.buffer().viewport().top_row;
7395 run_keys(&mut e, "3H");
7396 assert_eq!(e.cursor().0, top + 2);
7397 }
7398
7399 #[test]
7402 fn ctrl_o_returns_to_pre_g_position() {
7403 let mut e = editor_with_rows(50, 20);
7404 e.jump_cursor(5, 2);
7405 run_keys(&mut e, "G");
7406 assert_eq!(e.cursor().0, 49);
7407 run_keys(&mut e, "<C-o>");
7408 assert_eq!(e.cursor(), (5, 2));
7409 }
7410
7411 #[test]
7412 fn ctrl_i_redoes_jump_after_ctrl_o() {
7413 let mut e = editor_with_rows(50, 20);
7414 e.jump_cursor(5, 2);
7415 run_keys(&mut e, "G");
7416 let post = e.cursor();
7417 run_keys(&mut e, "<C-o>");
7418 run_keys(&mut e, "<C-i>");
7419 assert_eq!(e.cursor(), post);
7420 }
7421
7422 #[test]
7423 fn new_jump_clears_forward_stack() {
7424 let mut e = editor_with_rows(50, 20);
7425 e.jump_cursor(5, 2);
7426 run_keys(&mut e, "G");
7427 run_keys(&mut e, "<C-o>");
7428 run_keys(&mut e, "gg");
7429 run_keys(&mut e, "<C-i>");
7430 assert_eq!(e.cursor().0, 0);
7431 }
7432
7433 #[test]
7434 fn ctrl_o_on_empty_stack_is_noop() {
7435 let mut e = editor_with_rows(10, 20);
7436 e.jump_cursor(3, 1);
7437 run_keys(&mut e, "<C-o>");
7438 assert_eq!(e.cursor(), (3, 1));
7439 }
7440
7441 #[test]
7442 fn asterisk_search_pushes_jump() {
7443 let mut e = editor_with("foo bar\nbaz foo end");
7444 e.jump_cursor(0, 0);
7445 run_keys(&mut e, "*");
7446 let after = e.cursor();
7447 assert_ne!(after, (0, 0));
7448 run_keys(&mut e, "<C-o>");
7449 assert_eq!(e.cursor(), (0, 0));
7450 }
7451
7452 #[test]
7453 fn h_viewport_jump_is_recorded() {
7454 let mut e = editor_with_rows(100, 10);
7455 e.jump_cursor(50, 0);
7456 e.set_viewport_top(45);
7457 let pre = e.cursor();
7458 run_keys(&mut e, "H");
7459 assert_ne!(e.cursor(), pre);
7460 run_keys(&mut e, "<C-o>");
7461 assert_eq!(e.cursor(), pre);
7462 }
7463
7464 #[test]
7465 fn j_k_motion_does_not_push_jump() {
7466 let mut e = editor_with_rows(50, 20);
7467 e.jump_cursor(5, 0);
7468 run_keys(&mut e, "jjj");
7469 run_keys(&mut e, "<C-o>");
7470 assert_eq!(e.cursor().0, 8);
7471 }
7472
7473 #[test]
7474 fn jumplist_caps_at_100() {
7475 let mut e = editor_with_rows(200, 20);
7476 for i in 0..101 {
7477 e.jump_cursor(i, 0);
7478 run_keys(&mut e, "G");
7479 }
7480 assert!(e.vim.jump_back.len() <= 100);
7481 }
7482
7483 #[test]
7484 fn tab_acts_as_ctrl_i() {
7485 let mut e = editor_with_rows(50, 20);
7486 e.jump_cursor(5, 2);
7487 run_keys(&mut e, "G");
7488 let post = e.cursor();
7489 run_keys(&mut e, "<C-o>");
7490 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
7491 assert_eq!(e.cursor(), post);
7492 }
7493
7494 #[test]
7497 fn ma_then_backtick_a_jumps_exact() {
7498 let mut e = editor_with_rows(50, 20);
7499 e.jump_cursor(5, 3);
7500 run_keys(&mut e, "ma");
7501 e.jump_cursor(20, 0);
7502 run_keys(&mut e, "`a");
7503 assert_eq!(e.cursor(), (5, 3));
7504 }
7505
7506 #[test]
7507 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
7508 let mut e = editor_with_rows(50, 20);
7509 e.jump_cursor(5, 6);
7511 run_keys(&mut e, "ma");
7512 e.jump_cursor(30, 4);
7513 run_keys(&mut e, "'a");
7514 assert_eq!(e.cursor(), (5, 2));
7515 }
7516
7517 #[test]
7518 fn goto_mark_pushes_jumplist() {
7519 let mut e = editor_with_rows(50, 20);
7520 e.jump_cursor(10, 2);
7521 run_keys(&mut e, "mz");
7522 e.jump_cursor(3, 0);
7523 run_keys(&mut e, "`z");
7524 assert_eq!(e.cursor(), (10, 2));
7525 run_keys(&mut e, "<C-o>");
7526 assert_eq!(e.cursor(), (3, 0));
7527 }
7528
7529 #[test]
7530 fn goto_missing_mark_is_noop() {
7531 let mut e = editor_with_rows(50, 20);
7532 e.jump_cursor(3, 1);
7533 run_keys(&mut e, "`q");
7534 assert_eq!(e.cursor(), (3, 1));
7535 }
7536
7537 #[test]
7538 fn uppercase_mark_letter_ignored() {
7539 let mut e = editor_with_rows(50, 20);
7540 e.jump_cursor(5, 3);
7541 run_keys(&mut e, "mA");
7542 assert!(e.vim.marks.is_empty());
7545 }
7546
7547 #[test]
7548 fn mark_survives_document_shrink_via_clamp() {
7549 let mut e = editor_with_rows(50, 20);
7550 e.jump_cursor(40, 4);
7551 run_keys(&mut e, "mx");
7552 e.set_content("a\nb\nc\nd\ne");
7554 run_keys(&mut e, "`x");
7555 let (r, _) = e.cursor();
7557 assert!(r <= 4);
7558 }
7559
7560 #[test]
7561 fn g_semicolon_walks_back_through_edits() {
7562 let mut e = editor_with("alpha\nbeta\ngamma");
7563 e.jump_cursor(0, 0);
7566 run_keys(&mut e, "iX<Esc>");
7567 e.jump_cursor(2, 0);
7568 run_keys(&mut e, "iY<Esc>");
7569 run_keys(&mut e, "g;");
7571 assert_eq!(e.cursor(), (2, 1));
7572 run_keys(&mut e, "g;");
7574 assert_eq!(e.cursor(), (0, 1));
7575 run_keys(&mut e, "g;");
7577 assert_eq!(e.cursor(), (0, 1));
7578 }
7579
7580 #[test]
7581 fn g_comma_walks_forward_after_g_semicolon() {
7582 let mut e = editor_with("a\nb\nc");
7583 e.jump_cursor(0, 0);
7584 run_keys(&mut e, "iX<Esc>");
7585 e.jump_cursor(2, 0);
7586 run_keys(&mut e, "iY<Esc>");
7587 run_keys(&mut e, "g;");
7588 run_keys(&mut e, "g;");
7589 assert_eq!(e.cursor(), (0, 1));
7590 run_keys(&mut e, "g,");
7591 assert_eq!(e.cursor(), (2, 1));
7592 }
7593
7594 #[test]
7595 fn new_edit_during_walk_trims_forward_entries() {
7596 let mut e = editor_with("a\nb\nc\nd");
7597 e.jump_cursor(0, 0);
7598 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
7600 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
7603 run_keys(&mut e, "g;");
7604 assert_eq!(e.cursor(), (0, 1));
7605 run_keys(&mut e, "iZ<Esc>");
7607 run_keys(&mut e, "g,");
7609 assert_ne!(e.cursor(), (2, 1));
7611 }
7612
7613 #[test]
7619 fn capital_mark_set_and_jump() {
7620 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
7621 e.jump_cursor(2, 1);
7622 run_keys(&mut e, "mA");
7623 e.jump_cursor(0, 0);
7625 run_keys(&mut e, "'A");
7627 assert_eq!(e.cursor().0, 2);
7629 }
7630
7631 #[test]
7632 fn capital_mark_survives_set_content() {
7633 let mut e = editor_with("first buffer line\nsecond");
7634 e.jump_cursor(1, 3);
7635 run_keys(&mut e, "mA");
7636 e.set_content("totally different content\non many\nrows of text");
7638 e.jump_cursor(0, 0);
7640 run_keys(&mut e, "'A");
7641 assert_eq!(e.cursor().0, 1);
7642 }
7643
7644 #[test]
7649 fn capital_mark_shifts_with_edit() {
7650 let mut e = editor_with("a\nb\nc\nd");
7651 e.jump_cursor(3, 0);
7652 run_keys(&mut e, "mA");
7653 e.jump_cursor(0, 0);
7655 run_keys(&mut e, "dd");
7656 e.jump_cursor(0, 0);
7657 run_keys(&mut e, "'A");
7658 assert_eq!(e.cursor().0, 2);
7659 }
7660
7661 #[test]
7662 fn mark_below_delete_shifts_up() {
7663 let mut e = editor_with("a\nb\nc\nd\ne");
7664 e.jump_cursor(3, 0);
7666 run_keys(&mut e, "ma");
7667 e.jump_cursor(0, 0);
7669 run_keys(&mut e, "dd");
7670 e.jump_cursor(0, 0);
7672 run_keys(&mut e, "'a");
7673 assert_eq!(e.cursor().0, 2);
7674 assert_eq!(e.buffer().line(2).unwrap(), "d");
7675 }
7676
7677 #[test]
7678 fn mark_on_deleted_row_is_dropped() {
7679 let mut e = editor_with("a\nb\nc\nd");
7680 e.jump_cursor(1, 0);
7682 run_keys(&mut e, "ma");
7683 run_keys(&mut e, "dd");
7685 e.jump_cursor(2, 0);
7687 run_keys(&mut e, "'a");
7688 assert_eq!(e.cursor().0, 2);
7690 }
7691
7692 #[test]
7693 fn mark_above_edit_unchanged() {
7694 let mut e = editor_with("a\nb\nc\nd\ne");
7695 e.jump_cursor(0, 0);
7697 run_keys(&mut e, "ma");
7698 e.jump_cursor(3, 0);
7700 run_keys(&mut e, "dd");
7701 e.jump_cursor(2, 0);
7703 run_keys(&mut e, "'a");
7704 assert_eq!(e.cursor().0, 0);
7705 }
7706
7707 #[test]
7708 fn mark_shifts_down_after_insert() {
7709 let mut e = editor_with("a\nb\nc");
7710 e.jump_cursor(2, 0);
7712 run_keys(&mut e, "ma");
7713 e.jump_cursor(0, 0);
7715 run_keys(&mut e, "Onew<Esc>");
7716 e.jump_cursor(0, 0);
7719 run_keys(&mut e, "'a");
7720 assert_eq!(e.cursor().0, 3);
7721 assert_eq!(e.buffer().line(3).unwrap(), "c");
7722 }
7723
7724 #[test]
7727 fn forward_search_commit_pushes_jump() {
7728 let mut e = editor_with("alpha beta\nfoo target end\nmore");
7729 e.jump_cursor(0, 0);
7730 run_keys(&mut e, "/target<CR>");
7731 assert_ne!(e.cursor(), (0, 0));
7733 run_keys(&mut e, "<C-o>");
7735 assert_eq!(e.cursor(), (0, 0));
7736 }
7737
7738 #[test]
7739 fn search_commit_no_match_does_not_push_jump() {
7740 let mut e = editor_with("alpha beta\nfoo end");
7741 e.jump_cursor(0, 3);
7742 let pre_len = e.vim.jump_back.len();
7743 run_keys(&mut e, "/zzznotfound<CR>");
7744 assert_eq!(e.vim.jump_back.len(), pre_len);
7746 }
7747
7748 #[test]
7751 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
7752 let mut e = editor_with("hello world");
7753 run_keys(&mut e, "lll");
7754 let (row, col) = e.cursor();
7755 assert_eq!(e.buffer.cursor().row, row);
7756 assert_eq!(e.buffer.cursor().col, col);
7757 }
7758
7759 #[test]
7760 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
7761 let mut e = editor_with("aaaa\nbbbb\ncccc");
7762 run_keys(&mut e, "jj");
7763 let (row, col) = e.cursor();
7764 assert_eq!(e.buffer.cursor().row, row);
7765 assert_eq!(e.buffer.cursor().col, col);
7766 }
7767
7768 #[test]
7769 fn buffer_cursor_mirrors_textarea_after_word_motion() {
7770 let mut e = editor_with("foo bar baz");
7771 run_keys(&mut e, "ww");
7772 let (row, col) = e.cursor();
7773 assert_eq!(e.buffer.cursor().row, row);
7774 assert_eq!(e.buffer.cursor().col, col);
7775 }
7776
7777 #[test]
7778 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
7779 let mut e = editor_with("a\nb\nc\nd\ne");
7780 run_keys(&mut e, "G");
7781 let (row, col) = e.cursor();
7782 assert_eq!(e.buffer.cursor().row, row);
7783 assert_eq!(e.buffer.cursor().col, col);
7784 }
7785
7786 #[test]
7787 fn buffer_sticky_col_mirrors_vim_state() {
7788 let mut e = editor_with("longline\nhi\nlongline");
7789 run_keys(&mut e, "fl");
7790 run_keys(&mut e, "j");
7791 assert_eq!(e.buffer.sticky_col(), e.vim.sticky_col);
7793 }
7794
7795 #[test]
7796 fn buffer_content_mirrors_textarea_after_insert() {
7797 let mut e = editor_with("hello");
7798 run_keys(&mut e, "iXYZ<Esc>");
7799 let text = e.buffer().lines().join("\n");
7800 assert_eq!(e.buffer.as_string(), text);
7801 }
7802
7803 #[test]
7804 fn buffer_content_mirrors_textarea_after_delete() {
7805 let mut e = editor_with("alpha bravo charlie");
7806 run_keys(&mut e, "dw");
7807 let text = e.buffer().lines().join("\n");
7808 assert_eq!(e.buffer.as_string(), text);
7809 }
7810
7811 #[test]
7812 fn buffer_content_mirrors_textarea_after_dd() {
7813 let mut e = editor_with("a\nb\nc\nd");
7814 run_keys(&mut e, "jdd");
7815 let text = e.buffer().lines().join("\n");
7816 assert_eq!(e.buffer.as_string(), text);
7817 }
7818
7819 #[test]
7820 fn buffer_content_mirrors_textarea_after_open_line() {
7821 let mut e = editor_with("foo\nbar");
7822 run_keys(&mut e, "oNEW<Esc>");
7823 let text = e.buffer().lines().join("\n");
7824 assert_eq!(e.buffer.as_string(), text);
7825 }
7826
7827 #[test]
7828 fn buffer_content_mirrors_textarea_after_paste() {
7829 let mut e = editor_with("hello");
7830 run_keys(&mut e, "yy");
7831 run_keys(&mut e, "p");
7832 let text = e.buffer().lines().join("\n");
7833 assert_eq!(e.buffer.as_string(), text);
7834 }
7835
7836 #[test]
7837 fn buffer_selection_none_in_normal_mode() {
7838 let e = editor_with("foo bar");
7839 assert!(e.buffer_selection().is_none());
7840 }
7841
7842 #[test]
7843 fn buffer_selection_char_in_visual_mode() {
7844 use hjkl_buffer::{Position, Selection};
7845 let mut e = editor_with("hello world");
7846 run_keys(&mut e, "vlll");
7847 assert_eq!(
7848 e.buffer_selection(),
7849 Some(Selection::Char {
7850 anchor: Position::new(0, 0),
7851 head: Position::new(0, 3),
7852 })
7853 );
7854 }
7855
7856 #[test]
7857 fn buffer_selection_line_in_visual_line_mode() {
7858 use hjkl_buffer::Selection;
7859 let mut e = editor_with("a\nb\nc\nd");
7860 run_keys(&mut e, "Vj");
7861 assert_eq!(
7862 e.buffer_selection(),
7863 Some(Selection::Line {
7864 anchor_row: 0,
7865 head_row: 1,
7866 })
7867 );
7868 }
7869
7870 #[test]
7871 fn intern_style_dedups_repeated_styles() {
7872 use ratatui::style::{Color, Style};
7873 let mut e = editor_with("");
7874 let red = Style::default().fg(Color::Red);
7875 let blue = Style::default().fg(Color::Blue);
7876 let id_r1 = e.intern_style(red);
7877 let id_r2 = e.intern_style(red);
7878 let id_b = e.intern_style(blue);
7879 assert_eq!(id_r1, id_r2);
7880 assert_ne!(id_r1, id_b);
7881 assert_eq!(e.style_table().len(), 2);
7882 }
7883
7884 #[test]
7885 fn install_syntax_spans_translates_styled_spans() {
7886 use ratatui::style::{Color, Style};
7887 let mut e = editor_with("SELECT foo");
7888 e.install_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
7889 let by_row = e.buffer.spans();
7890 assert_eq!(by_row.len(), 1);
7891 assert_eq!(by_row[0].len(), 1);
7892 assert_eq!(by_row[0][0].start_byte, 0);
7893 assert_eq!(by_row[0][0].end_byte, 6);
7894 let id = by_row[0][0].style;
7895 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
7896 }
7897
7898 #[test]
7899 fn install_syntax_spans_clamps_sentinel_end() {
7900 use ratatui::style::{Color, Style};
7901 let mut e = editor_with("hello");
7902 e.install_syntax_spans(vec![vec![(
7903 0,
7904 usize::MAX,
7905 Style::default().fg(Color::Blue),
7906 )]]);
7907 let by_row = e.buffer.spans();
7908 assert_eq!(by_row[0][0].end_byte, 5);
7909 }
7910
7911 #[test]
7912 fn install_syntax_spans_drops_zero_width() {
7913 use ratatui::style::{Color, Style};
7914 let mut e = editor_with("abc");
7915 e.install_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
7916 assert!(e.buffer.spans()[0].is_empty());
7917 }
7918
7919 #[test]
7920 fn named_register_yank_into_a_then_paste_from_a() {
7921 let mut e = editor_with("hello world\nsecond");
7922 run_keys(&mut e, "\"ayw");
7923 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
7925 run_keys(&mut e, "j0\"aP");
7927 assert_eq!(e.buffer().lines()[1], "hello second");
7928 }
7929
7930 #[test]
7931 fn capital_r_overstrikes_chars() {
7932 let mut e = editor_with("hello");
7933 e.jump_cursor(0, 0);
7934 run_keys(&mut e, "RXY<Esc>");
7935 assert_eq!(e.buffer().lines()[0], "XYllo");
7937 }
7938
7939 #[test]
7940 fn capital_r_at_eol_appends() {
7941 let mut e = editor_with("hi");
7942 e.jump_cursor(0, 1);
7943 run_keys(&mut e, "RXYZ<Esc>");
7945 assert_eq!(e.buffer().lines()[0], "hXYZ");
7946 }
7947
7948 #[test]
7949 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
7950 let mut e = editor_with("abc");
7954 e.jump_cursor(0, 0);
7955 run_keys(&mut e, "RX<Esc>");
7956 assert_eq!(e.buffer().lines()[0], "Xbc");
7957 }
7958
7959 #[test]
7960 fn ctrl_r_in_insert_pastes_named_register() {
7961 let mut e = editor_with("hello world");
7962 run_keys(&mut e, "\"ayw");
7964 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
7965 run_keys(&mut e, "o");
7967 assert_eq!(e.vim_mode(), VimMode::Insert);
7968 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
7969 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
7970 assert_eq!(e.buffer().lines()[1], "hello ");
7971 assert_eq!(e.cursor(), (1, 6));
7973 assert_eq!(e.vim_mode(), VimMode::Insert);
7975 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
7976 assert_eq!(e.buffer().lines()[1], "hello X");
7977 }
7978
7979 #[test]
7980 fn ctrl_r_with_unnamed_register() {
7981 let mut e = editor_with("foo");
7982 run_keys(&mut e, "yiw");
7983 run_keys(&mut e, "A ");
7984 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
7986 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
7987 assert_eq!(e.buffer().lines()[0], "foo foo");
7988 }
7989
7990 #[test]
7991 fn ctrl_r_unknown_selector_is_no_op() {
7992 let mut e = editor_with("abc");
7993 run_keys(&mut e, "A");
7994 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
7995 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
7998 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
7999 assert_eq!(e.buffer().lines()[0], "abcZ");
8000 }
8001
8002 #[test]
8003 fn ctrl_r_multiline_register_pastes_with_newlines() {
8004 let mut e = editor_with("alpha\nbeta\ngamma");
8005 run_keys(&mut e, "\"byy");
8007 run_keys(&mut e, "j\"byy");
8008 run_keys(&mut e, "ggVj\"by");
8012 let payload = e.registers().read('b').unwrap().text.clone();
8013 assert!(payload.contains('\n'));
8014 run_keys(&mut e, "Go");
8015 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8016 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
8017 let total_lines = e.buffer().lines().len();
8020 assert!(total_lines >= 5);
8021 }
8022
8023 #[test]
8024 fn yank_zero_holds_last_yank_after_delete() {
8025 let mut e = editor_with("hello world");
8026 run_keys(&mut e, "yw");
8027 let yanked = e.registers().read('0').unwrap().text.clone();
8028 assert!(!yanked.is_empty());
8029 run_keys(&mut e, "dw");
8031 assert_eq!(e.registers().read('0').unwrap().text, yanked);
8032 assert!(!e.registers().read('1').unwrap().text.is_empty());
8034 }
8035
8036 #[test]
8037 fn delete_ring_rotates_through_one_through_nine() {
8038 let mut e = editor_with("a b c d e f g h i j");
8039 for _ in 0..3 {
8041 run_keys(&mut e, "dw");
8042 }
8043 let r1 = e.registers().read('1').unwrap().text.clone();
8045 let r2 = e.registers().read('2').unwrap().text.clone();
8046 let r3 = e.registers().read('3').unwrap().text.clone();
8047 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
8048 assert_ne!(r1, r2);
8049 assert_ne!(r2, r3);
8050 }
8051
8052 #[test]
8053 fn capital_register_appends_to_lowercase() {
8054 let mut e = editor_with("foo bar");
8055 run_keys(&mut e, "\"ayw");
8056 let first = e.registers().read('a').unwrap().text.clone();
8057 assert!(first.contains("foo"));
8058 run_keys(&mut e, "w\"Ayw");
8060 let combined = e.registers().read('a').unwrap().text.clone();
8061 assert!(combined.starts_with(&first));
8062 assert!(combined.contains("bar"));
8063 }
8064
8065 #[test]
8066 fn zf_in_visual_line_creates_closed_fold() {
8067 let mut e = editor_with("a\nb\nc\nd\ne");
8068 e.jump_cursor(1, 0);
8070 run_keys(&mut e, "Vjjzf");
8071 assert_eq!(e.buffer().folds().len(), 1);
8072 let f = e.buffer().folds()[0];
8073 assert_eq!(f.start_row, 1);
8074 assert_eq!(f.end_row, 3);
8075 assert!(f.closed);
8076 }
8077
8078 #[test]
8079 fn zfj_in_normal_creates_two_row_fold() {
8080 let mut e = editor_with("a\nb\nc\nd\ne");
8081 e.jump_cursor(1, 0);
8082 run_keys(&mut e, "zfj");
8083 assert_eq!(e.buffer().folds().len(), 1);
8084 let f = e.buffer().folds()[0];
8085 assert_eq!(f.start_row, 1);
8086 assert_eq!(f.end_row, 2);
8087 assert!(f.closed);
8088 assert_eq!(e.cursor().0, 1);
8090 }
8091
8092 #[test]
8093 fn zf_with_count_folds_count_rows() {
8094 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8095 e.jump_cursor(0, 0);
8096 run_keys(&mut e, "zf3j");
8098 assert_eq!(e.buffer().folds().len(), 1);
8099 let f = e.buffer().folds()[0];
8100 assert_eq!(f.start_row, 0);
8101 assert_eq!(f.end_row, 3);
8102 }
8103
8104 #[test]
8105 fn zfk_folds_upward_range() {
8106 let mut e = editor_with("a\nb\nc\nd\ne");
8107 e.jump_cursor(3, 0);
8108 run_keys(&mut e, "zfk");
8109 let f = e.buffer().folds()[0];
8110 assert_eq!(f.start_row, 2);
8112 assert_eq!(f.end_row, 3);
8113 }
8114
8115 #[test]
8116 fn zf_capital_g_folds_to_bottom() {
8117 let mut e = editor_with("a\nb\nc\nd\ne");
8118 e.jump_cursor(1, 0);
8119 run_keys(&mut e, "zfG");
8121 let f = e.buffer().folds()[0];
8122 assert_eq!(f.start_row, 1);
8123 assert_eq!(f.end_row, 4);
8124 }
8125
8126 #[test]
8127 fn zfgg_folds_to_top_via_operator_pipeline() {
8128 let mut e = editor_with("a\nb\nc\nd\ne");
8129 e.jump_cursor(3, 0);
8130 run_keys(&mut e, "zfgg");
8134 let f = e.buffer().folds()[0];
8135 assert_eq!(f.start_row, 0);
8136 assert_eq!(f.end_row, 3);
8137 }
8138
8139 #[test]
8140 fn zfip_folds_paragraph_via_text_object() {
8141 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
8142 e.jump_cursor(1, 0);
8143 run_keys(&mut e, "zfip");
8145 assert_eq!(e.buffer().folds().len(), 1);
8146 let f = e.buffer().folds()[0];
8147 assert_eq!(f.start_row, 0);
8148 assert_eq!(f.end_row, 2);
8149 }
8150
8151 #[test]
8152 fn zfap_folds_paragraph_with_trailing_blank() {
8153 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
8154 e.jump_cursor(0, 0);
8155 run_keys(&mut e, "zfap");
8157 let f = e.buffer().folds()[0];
8158 assert_eq!(f.start_row, 0);
8159 assert_eq!(f.end_row, 3);
8160 }
8161
8162 #[test]
8163 fn zf_paragraph_motion_folds_to_blank() {
8164 let mut e = editor_with("alpha\nbeta\n\ngamma");
8165 e.jump_cursor(0, 0);
8166 run_keys(&mut e, "zf}");
8168 let f = e.buffer().folds()[0];
8169 assert_eq!(f.start_row, 0);
8170 assert_eq!(f.end_row, 2);
8171 }
8172
8173 #[test]
8174 fn za_toggles_fold_under_cursor() {
8175 let mut e = editor_with("a\nb\nc\nd");
8176 e.buffer_mut().add_fold(1, 2, true);
8177 e.jump_cursor(1, 0);
8178 run_keys(&mut e, "za");
8179 assert!(!e.buffer().folds()[0].closed);
8180 run_keys(&mut e, "za");
8181 assert!(e.buffer().folds()[0].closed);
8182 }
8183
8184 #[test]
8185 fn zr_opens_all_folds_zm_closes_all() {
8186 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8187 e.buffer_mut().add_fold(0, 1, true);
8188 e.buffer_mut().add_fold(2, 3, true);
8189 e.buffer_mut().add_fold(4, 5, true);
8190 run_keys(&mut e, "zR");
8191 assert!(e.buffer().folds().iter().all(|f| !f.closed));
8192 run_keys(&mut e, "zM");
8193 assert!(e.buffer().folds().iter().all(|f| f.closed));
8194 }
8195
8196 #[test]
8197 fn ze_clears_all_folds() {
8198 let mut e = editor_with("a\nb\nc\nd");
8199 e.buffer_mut().add_fold(0, 1, true);
8200 e.buffer_mut().add_fold(2, 3, false);
8201 run_keys(&mut e, "zE");
8202 assert!(e.buffer().folds().is_empty());
8203 }
8204
8205 #[test]
8206 fn g_underscore_jumps_to_last_non_blank() {
8207 let mut e = editor_with("hello world ");
8208 run_keys(&mut e, "g_");
8209 assert_eq!(e.cursor().1, 10);
8211 }
8212
8213 #[test]
8214 fn gj_and_gk_alias_j_and_k() {
8215 let mut e = editor_with("a\nb\nc");
8216 run_keys(&mut e, "gj");
8217 assert_eq!(e.cursor().0, 1);
8218 run_keys(&mut e, "gk");
8219 assert_eq!(e.cursor().0, 0);
8220 }
8221
8222 #[test]
8223 fn paragraph_motions_walk_blank_lines() {
8224 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
8225 run_keys(&mut e, "}");
8226 assert_eq!(e.cursor().0, 2);
8227 run_keys(&mut e, "}");
8228 assert_eq!(e.cursor().0, 5);
8229 run_keys(&mut e, "{");
8230 assert_eq!(e.cursor().0, 2);
8231 }
8232
8233 #[test]
8234 fn gv_reenters_last_visual_selection() {
8235 let mut e = editor_with("alpha\nbeta\ngamma");
8236 run_keys(&mut e, "Vj");
8237 run_keys(&mut e, "<Esc>");
8239 assert_eq!(e.vim_mode(), VimMode::Normal);
8240 run_keys(&mut e, "gv");
8242 assert_eq!(e.vim_mode(), VimMode::VisualLine);
8243 }
8244
8245 #[test]
8246 fn o_in_visual_swaps_anchor_and_cursor() {
8247 let mut e = editor_with("hello world");
8248 run_keys(&mut e, "vllll");
8250 assert_eq!(e.cursor().1, 4);
8251 run_keys(&mut e, "o");
8253 assert_eq!(e.cursor().1, 0);
8254 assert_eq!(e.vim.visual_anchor, (0, 4));
8256 }
8257
8258 #[test]
8259 fn editing_inside_fold_invalidates_it() {
8260 let mut e = editor_with("a\nb\nc\nd");
8261 e.buffer_mut().add_fold(1, 2, true);
8262 e.jump_cursor(1, 0);
8263 run_keys(&mut e, "iX<Esc>");
8265 assert!(e.buffer().folds().is_empty());
8267 }
8268
8269 #[test]
8270 fn zd_removes_fold_under_cursor() {
8271 let mut e = editor_with("a\nb\nc\nd");
8272 e.buffer_mut().add_fold(1, 2, true);
8273 e.jump_cursor(2, 0);
8274 run_keys(&mut e, "zd");
8275 assert!(e.buffer().folds().is_empty());
8276 }
8277
8278 #[test]
8279 fn dot_mark_jumps_to_last_edit_position() {
8280 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8281 e.jump_cursor(2, 0);
8282 run_keys(&mut e, "iX<Esc>");
8284 let after_edit = e.cursor();
8285 run_keys(&mut e, "gg");
8287 assert_eq!(e.cursor().0, 0);
8288 run_keys(&mut e, "'.");
8290 assert_eq!(e.cursor().0, after_edit.0);
8291 }
8292
8293 #[test]
8294 fn quote_quote_returns_to_pre_jump_position() {
8295 let mut e = editor_with_rows(50, 20);
8296 e.jump_cursor(10, 2);
8297 let before = e.cursor();
8298 run_keys(&mut e, "G");
8300 assert_ne!(e.cursor(), before);
8301 run_keys(&mut e, "''");
8303 assert_eq!(e.cursor().0, before.0);
8304 }
8305
8306 #[test]
8307 fn backtick_backtick_restores_exact_pre_jump_pos() {
8308 let mut e = editor_with_rows(50, 20);
8309 e.jump_cursor(7, 3);
8310 let before = e.cursor();
8311 run_keys(&mut e, "G");
8312 run_keys(&mut e, "``");
8313 assert_eq!(e.cursor(), before);
8314 }
8315
8316 #[test]
8317 fn macro_record_and_replay_basic() {
8318 let mut e = editor_with("foo\nbar\nbaz");
8319 run_keys(&mut e, "qaIX<Esc>jq");
8321 assert_eq!(e.buffer().lines()[0], "Xfoo");
8322 run_keys(&mut e, "@a");
8324 assert_eq!(e.buffer().lines()[1], "Xbar");
8325 run_keys(&mut e, "j@@");
8327 assert_eq!(e.buffer().lines()[2], "Xbaz");
8328 }
8329
8330 #[test]
8331 fn macro_count_replays_n_times() {
8332 let mut e = editor_with("a\nb\nc\nd\ne");
8333 run_keys(&mut e, "qajq");
8335 assert_eq!(e.cursor().0, 1);
8336 run_keys(&mut e, "3@a");
8338 assert_eq!(e.cursor().0, 4);
8339 }
8340
8341 #[test]
8342 fn macro_capital_q_appends_to_lowercase_register() {
8343 let mut e = editor_with("hello");
8344 run_keys(&mut e, "qall<Esc>q");
8345 run_keys(&mut e, "qAhh<Esc>q");
8346 let text = e.registers().read('a').unwrap().text.clone();
8349 assert!(text.contains("ll<Esc>"));
8350 assert!(text.contains("hh<Esc>"));
8351 }
8352
8353 #[test]
8354 fn buffer_selection_block_in_visual_block_mode() {
8355 use hjkl_buffer::{Position, Selection};
8356 let mut e = editor_with("aaaa\nbbbb\ncccc");
8357 run_keys(&mut e, "<C-v>jl");
8358 assert_eq!(
8359 e.buffer_selection(),
8360 Some(Selection::Block {
8361 anchor: Position::new(0, 0),
8362 head: Position::new(1, 1),
8363 })
8364 );
8365 }
8366
8367 #[test]
8370 fn n_after_question_mark_keeps_walking_backward() {
8371 let mut e = editor_with("foo bar foo baz foo end");
8374 e.jump_cursor(0, 22);
8375 run_keys(&mut e, "?foo<CR>");
8376 assert_eq!(e.cursor().1, 16);
8377 run_keys(&mut e, "n");
8378 assert_eq!(e.cursor().1, 8);
8379 run_keys(&mut e, "N");
8380 assert_eq!(e.cursor().1, 16);
8381 }
8382
8383 #[test]
8384 fn nested_macro_chord_records_literal_keys() {
8385 let mut e = editor_with("alpha\nbeta\ngamma");
8388 run_keys(&mut e, "qblq");
8390 run_keys(&mut e, "qaIX<Esc>q");
8393 e.jump_cursor(1, 0);
8395 run_keys(&mut e, "@a");
8396 assert_eq!(e.buffer().lines()[1], "Xbeta");
8397 }
8398
8399 #[test]
8400 fn shift_gt_motion_indents_one_line() {
8401 let mut e = editor_with("hello world");
8405 run_keys(&mut e, ">w");
8406 assert_eq!(e.buffer().lines()[0], " hello world");
8407 }
8408
8409 #[test]
8410 fn shift_lt_motion_outdents_one_line() {
8411 let mut e = editor_with(" hello world");
8412 run_keys(&mut e, "<lt>w");
8413 assert_eq!(e.buffer().lines()[0], " hello world");
8415 }
8416
8417 #[test]
8418 fn shift_gt_text_object_indents_paragraph() {
8419 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
8420 e.jump_cursor(0, 0);
8421 run_keys(&mut e, ">ip");
8422 assert_eq!(e.buffer().lines()[0], " alpha");
8423 assert_eq!(e.buffer().lines()[1], " beta");
8424 assert_eq!(e.buffer().lines()[2], " gamma");
8425 assert_eq!(e.buffer().lines()[4], "rest");
8427 }
8428
8429 #[test]
8430 fn ctrl_o_runs_exactly_one_normal_command() {
8431 let mut e = editor_with("alpha beta gamma");
8434 e.jump_cursor(0, 0);
8435 run_keys(&mut e, "i");
8436 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
8437 run_keys(&mut e, "dw");
8438 assert_eq!(e.vim_mode(), VimMode::Insert);
8440 run_keys(&mut e, "X");
8442 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
8443 }
8444
8445 #[test]
8446 fn macro_replay_respects_mode_switching() {
8447 let mut e = editor_with("hi");
8451 run_keys(&mut e, "qaiX<Esc>0q");
8452 assert_eq!(e.vim_mode(), VimMode::Normal);
8453 e.set_content("yo");
8455 run_keys(&mut e, "@a");
8456 assert_eq!(e.vim_mode(), VimMode::Normal);
8457 assert_eq!(e.cursor().1, 0);
8458 assert_eq!(e.buffer().lines()[0], "Xyo");
8459 }
8460
8461 #[test]
8462 fn macro_recorded_text_round_trips_through_register() {
8463 let mut e = editor_with("");
8467 run_keys(&mut e, "qaiX<Esc>q");
8468 let text = e.registers().read('a').unwrap().text.clone();
8469 assert!(text.starts_with("iX"));
8470 run_keys(&mut e, "@a");
8472 assert_eq!(e.buffer().lines()[0], "XX");
8473 }
8474
8475 #[test]
8476 fn dot_after_macro_replays_macros_last_change() {
8477 let mut e = editor_with("ab\ncd\nef");
8480 run_keys(&mut e, "qaIX<Esc>jq");
8483 assert_eq!(e.buffer().lines()[0], "Xab");
8484 run_keys(&mut e, "@a");
8485 assert_eq!(e.buffer().lines()[1], "Xcd");
8486 let row_before_dot = e.cursor().0;
8489 run_keys(&mut e, ".");
8490 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
8491 }
8492}