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 pub(super) 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 pub(super) jump_back: Vec<(usize, usize)>,
428 pub(super) jump_fwd: Vec<(usize, usize)>,
431 pub(super) 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
4867pub(crate) fn do_undo(ed: &mut Editor<'_>) {
4868 if let Some((lines, cursor)) = ed.undo_stack.pop() {
4869 let current = ed.snapshot();
4870 ed.redo_stack.push(current);
4871 ed.restore(lines, cursor);
4872 }
4873 ed.vim.mode = Mode::Normal;
4874}
4875
4876pub(crate) fn do_redo(ed: &mut Editor<'_>) {
4877 if let Some((lines, cursor)) = ed.redo_stack.pop() {
4878 let current = ed.snapshot();
4879 ed.undo_stack.push(current);
4880 ed.restore(lines, cursor);
4881 }
4882 ed.vim.mode = Mode::Normal;
4883}
4884
4885fn replay_insert_and_finish(ed: &mut Editor<'_>, text: &str) {
4892 use hjkl_buffer::{Edit, Position};
4893 let cursor = ed.cursor();
4894 ed.mutate_edit(Edit::InsertStr {
4895 at: Position::new(cursor.0, cursor.1),
4896 text: text.to_string(),
4897 });
4898 if ed.vim.insert_session.take().is_some() {
4899 if ed.cursor().1 > 0 {
4900 ed.buffer_mut().move_left(1);
4901 ed.push_buffer_cursor_to_textarea();
4902 }
4903 ed.vim.mode = Mode::Normal;
4904 }
4905}
4906
4907fn replay_last_change(ed: &mut Editor<'_>, outer_count: usize) {
4908 let Some(change) = ed.vim.last_change.clone() else {
4909 return;
4910 };
4911 ed.vim.replaying = true;
4912 let scale = if outer_count > 0 { outer_count } else { 1 };
4913 match change {
4914 LastChange::OpMotion {
4915 op,
4916 motion,
4917 count,
4918 inserted,
4919 } => {
4920 let total = count.max(1) * scale;
4921 apply_op_with_motion(ed, op, &motion, total);
4922 if let Some(text) = inserted {
4923 replay_insert_and_finish(ed, &text);
4924 }
4925 }
4926 LastChange::OpTextObj {
4927 op,
4928 obj,
4929 inner,
4930 inserted,
4931 } => {
4932 apply_op_with_text_object(ed, op, obj, inner);
4933 if let Some(text) = inserted {
4934 replay_insert_and_finish(ed, &text);
4935 }
4936 }
4937 LastChange::LineOp {
4938 op,
4939 count,
4940 inserted,
4941 } => {
4942 let total = count.max(1) * scale;
4943 execute_line_op(ed, op, total);
4944 if let Some(text) = inserted {
4945 replay_insert_and_finish(ed, &text);
4946 }
4947 }
4948 LastChange::CharDel { forward, count } => {
4949 do_char_delete(ed, forward, count * scale);
4950 }
4951 LastChange::ReplaceChar { ch, count } => {
4952 replace_char(ed, ch, count * scale);
4953 }
4954 LastChange::ToggleCase { count } => {
4955 for _ in 0..count * scale {
4956 ed.push_undo();
4957 toggle_case_at_cursor(ed);
4958 }
4959 }
4960 LastChange::JoinLine { count } => {
4961 for _ in 0..count * scale {
4962 ed.push_undo();
4963 join_line(ed);
4964 }
4965 }
4966 LastChange::Paste { before, count } => {
4967 do_paste(ed, before, count * scale);
4968 }
4969 LastChange::DeleteToEol { inserted } => {
4970 use hjkl_buffer::{Edit, Position};
4971 ed.push_undo();
4972 delete_to_eol(ed);
4973 if let Some(text) = inserted {
4974 let cursor = ed.cursor();
4975 ed.mutate_edit(Edit::InsertStr {
4976 at: Position::new(cursor.0, cursor.1),
4977 text,
4978 });
4979 }
4980 }
4981 LastChange::OpenLine { above, inserted } => {
4982 use hjkl_buffer::{Edit, Position};
4983 ed.push_undo();
4984 ed.sync_buffer_content_from_textarea();
4985 let row = ed.buffer().cursor().row;
4986 if above {
4987 ed.mutate_edit(Edit::InsertStr {
4988 at: Position::new(row, 0),
4989 text: "\n".to_string(),
4990 });
4991 ed.buffer_mut().move_up(1);
4992 } else {
4993 let line_chars = ed
4994 .buffer()
4995 .line(row)
4996 .map(|l| l.chars().count())
4997 .unwrap_or(0);
4998 ed.mutate_edit(Edit::InsertStr {
4999 at: Position::new(row, line_chars),
5000 text: "\n".to_string(),
5001 });
5002 }
5003 ed.push_buffer_cursor_to_textarea();
5004 let cursor = ed.cursor();
5005 ed.mutate_edit(Edit::InsertStr {
5006 at: Position::new(cursor.0, cursor.1),
5007 text: inserted,
5008 });
5009 }
5010 LastChange::InsertAt {
5011 entry,
5012 inserted,
5013 count,
5014 } => {
5015 use hjkl_buffer::{Edit, Position};
5016 ed.push_undo();
5017 match entry {
5018 InsertEntry::I => {}
5019 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5020 InsertEntry::A => {
5021 ed.buffer_mut().move_right_to_end(1);
5022 ed.push_buffer_cursor_to_textarea();
5023 }
5024 InsertEntry::ShiftA => {
5025 ed.buffer_mut().move_line_end();
5026 ed.buffer_mut().move_right_to_end(1);
5027 ed.push_buffer_cursor_to_textarea();
5028 }
5029 }
5030 for _ in 0..count.max(1) {
5031 let cursor = ed.cursor();
5032 ed.mutate_edit(Edit::InsertStr {
5033 at: Position::new(cursor.0, cursor.1),
5034 text: inserted.clone(),
5035 });
5036 }
5037 }
5038 }
5039 ed.vim.replaying = false;
5040}
5041
5042fn extract_inserted(before: &str, after: &str) -> String {
5045 let before_chars: Vec<char> = before.chars().collect();
5046 let after_chars: Vec<char> = after.chars().collect();
5047 if after_chars.len() <= before_chars.len() {
5048 return String::new();
5049 }
5050 let prefix = before_chars
5051 .iter()
5052 .zip(after_chars.iter())
5053 .take_while(|(a, b)| a == b)
5054 .count();
5055 let max_suffix = before_chars.len() - prefix;
5056 let suffix = before_chars
5057 .iter()
5058 .rev()
5059 .zip(after_chars.iter().rev())
5060 .take(max_suffix)
5061 .take_while(|(a, b)| a == b)
5062 .count();
5063 after_chars[prefix..after_chars.len() - suffix]
5064 .iter()
5065 .collect()
5066}
5067
5068#[cfg(test)]
5071mod tests {
5072 use crate::editor::Editor;
5073 use crate::{KeybindingMode, VimMode};
5074 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5075
5076 fn run_keys(e: &mut Editor<'_>, keys: &str) {
5077 let mut iter = keys.chars().peekable();
5081 while let Some(c) = iter.next() {
5082 if c == '<' {
5083 let mut tag = String::new();
5084 for ch in iter.by_ref() {
5085 if ch == '>' {
5086 break;
5087 }
5088 tag.push(ch);
5089 }
5090 let ev = match tag.as_str() {
5091 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5092 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5093 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5094 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5095 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5096 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5097 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5098 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5099 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5103 s if s.starts_with("C-") => {
5104 let ch = s.chars().nth(2).unwrap();
5105 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5106 }
5107 _ => continue,
5108 };
5109 e.handle_key(ev);
5110 } else {
5111 let mods = if c.is_uppercase() {
5112 KeyModifiers::SHIFT
5113 } else {
5114 KeyModifiers::NONE
5115 };
5116 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5117 }
5118 }
5119 }
5120
5121 fn editor_with(content: &str) -> Editor<'static> {
5122 let mut e = Editor::new(KeybindingMode::Vim);
5123 e.set_content(content);
5124 e
5125 }
5126
5127 #[test]
5128 fn f_char_jumps_on_line() {
5129 let mut e = editor_with("hello world");
5130 run_keys(&mut e, "fw");
5131 assert_eq!(e.cursor(), (0, 6));
5132 }
5133
5134 #[test]
5135 fn cap_f_jumps_backward() {
5136 let mut e = editor_with("hello world");
5137 e.jump_cursor(0, 10);
5138 run_keys(&mut e, "Fo");
5139 assert_eq!(e.cursor().1, 7);
5140 }
5141
5142 #[test]
5143 fn t_stops_before_char() {
5144 let mut e = editor_with("hello");
5145 run_keys(&mut e, "tl");
5146 assert_eq!(e.cursor(), (0, 1));
5147 }
5148
5149 #[test]
5150 fn semicolon_repeats_find() {
5151 let mut e = editor_with("aa.bb.cc");
5152 run_keys(&mut e, "f.");
5153 assert_eq!(e.cursor().1, 2);
5154 run_keys(&mut e, ";");
5155 assert_eq!(e.cursor().1, 5);
5156 }
5157
5158 #[test]
5159 fn comma_repeats_find_reverse() {
5160 let mut e = editor_with("aa.bb.cc");
5161 run_keys(&mut e, "f.");
5162 run_keys(&mut e, ";");
5163 run_keys(&mut e, ",");
5164 assert_eq!(e.cursor().1, 2);
5165 }
5166
5167 #[test]
5168 fn di_quote_deletes_content() {
5169 let mut e = editor_with("foo \"bar\" baz");
5170 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5172 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5173 }
5174
5175 #[test]
5176 fn da_quote_deletes_with_quotes() {
5177 let mut e = editor_with("foo \"bar\" baz");
5178 e.jump_cursor(0, 6);
5179 run_keys(&mut e, "da\"");
5180 assert_eq!(e.buffer().lines()[0], "foo baz");
5181 }
5182
5183 #[test]
5184 fn ci_paren_deletes_and_inserts() {
5185 let mut e = editor_with("fn(a, b, c)");
5186 e.jump_cursor(0, 5);
5187 run_keys(&mut e, "ci(");
5188 assert_eq!(e.vim_mode(), VimMode::Insert);
5189 assert_eq!(e.buffer().lines()[0], "fn()");
5190 }
5191
5192 #[test]
5193 fn diw_deletes_inner_word() {
5194 let mut e = editor_with("hello world");
5195 e.jump_cursor(0, 2);
5196 run_keys(&mut e, "diw");
5197 assert_eq!(e.buffer().lines()[0], " world");
5198 }
5199
5200 #[test]
5201 fn daw_deletes_word_with_trailing_space() {
5202 let mut e = editor_with("hello world");
5203 run_keys(&mut e, "daw");
5204 assert_eq!(e.buffer().lines()[0], "world");
5205 }
5206
5207 #[test]
5208 fn percent_jumps_to_matching_bracket() {
5209 let mut e = editor_with("foo(bar)");
5210 e.jump_cursor(0, 3);
5211 run_keys(&mut e, "%");
5212 assert_eq!(e.cursor().1, 7);
5213 run_keys(&mut e, "%");
5214 assert_eq!(e.cursor().1, 3);
5215 }
5216
5217 #[test]
5218 fn dot_repeats_last_change() {
5219 let mut e = editor_with("aaa bbb ccc");
5220 run_keys(&mut e, "dw");
5221 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5222 run_keys(&mut e, ".");
5223 assert_eq!(e.buffer().lines()[0], "ccc");
5224 }
5225
5226 #[test]
5227 fn dot_repeats_change_operator_with_text() {
5228 let mut e = editor_with("foo foo foo");
5229 run_keys(&mut e, "cwbar<Esc>");
5230 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5231 run_keys(&mut e, "w");
5233 run_keys(&mut e, ".");
5234 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5235 }
5236
5237 #[test]
5238 fn dot_repeats_x() {
5239 let mut e = editor_with("abcdef");
5240 run_keys(&mut e, "x");
5241 run_keys(&mut e, "..");
5242 assert_eq!(e.buffer().lines()[0], "def");
5243 }
5244
5245 #[test]
5246 fn count_operator_motion_compose() {
5247 let mut e = editor_with("one two three four five");
5248 run_keys(&mut e, "d3w");
5249 assert_eq!(e.buffer().lines()[0], "four five");
5250 }
5251
5252 #[test]
5253 fn two_dd_deletes_two_lines() {
5254 let mut e = editor_with("a\nb\nc");
5255 run_keys(&mut e, "2dd");
5256 assert_eq!(e.buffer().lines().len(), 1);
5257 assert_eq!(e.buffer().lines()[0], "c");
5258 }
5259
5260 #[test]
5265 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5266 let mut e = editor_with("one\ntwo\n three\nfour");
5267 e.jump_cursor(1, 2);
5268 run_keys(&mut e, "dd");
5269 assert_eq!(e.buffer().lines()[1], " three");
5271 assert_eq!(e.cursor(), (1, 4));
5272 }
5273
5274 #[test]
5275 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5276 let mut e = editor_with("one\n two\nthree");
5277 e.jump_cursor(2, 0);
5278 run_keys(&mut e, "dd");
5279 assert_eq!(e.buffer().lines().len(), 2);
5281 assert_eq!(e.cursor(), (1, 2));
5282 }
5283
5284 #[test]
5285 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5286 let mut e = editor_with("lonely");
5287 run_keys(&mut e, "dd");
5288 assert_eq!(e.buffer().lines().len(), 1);
5289 assert_eq!(e.buffer().lines()[0], "");
5290 assert_eq!(e.cursor(), (0, 0));
5291 }
5292
5293 #[test]
5294 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5295 let mut e = editor_with("a\nb\nc\n d\ne");
5296 e.jump_cursor(1, 0);
5298 run_keys(&mut e, "3dd");
5299 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
5300 assert_eq!(e.cursor(), (1, 0));
5301 }
5302
5303 #[test]
5304 fn gu_lowercases_motion_range() {
5305 let mut e = editor_with("HELLO WORLD");
5306 run_keys(&mut e, "guw");
5307 assert_eq!(e.buffer().lines()[0], "hello WORLD");
5308 assert_eq!(e.cursor(), (0, 0));
5309 }
5310
5311 #[test]
5312 fn g_u_uppercases_text_object() {
5313 let mut e = editor_with("hello world");
5314 run_keys(&mut e, "gUiw");
5316 assert_eq!(e.buffer().lines()[0], "HELLO world");
5317 assert_eq!(e.cursor(), (0, 0));
5318 }
5319
5320 #[test]
5321 fn g_tilde_toggles_case_of_range() {
5322 let mut e = editor_with("Hello World");
5323 run_keys(&mut e, "g~iw");
5324 assert_eq!(e.buffer().lines()[0], "hELLO World");
5325 }
5326
5327 #[test]
5328 fn g_uu_uppercases_current_line() {
5329 let mut e = editor_with("select 1\nselect 2");
5330 run_keys(&mut e, "gUU");
5331 assert_eq!(e.buffer().lines()[0], "SELECT 1");
5332 assert_eq!(e.buffer().lines()[1], "select 2");
5333 }
5334
5335 #[test]
5336 fn gugu_lowercases_current_line() {
5337 let mut e = editor_with("FOO BAR\nBAZ");
5338 run_keys(&mut e, "gugu");
5339 assert_eq!(e.buffer().lines()[0], "foo bar");
5340 }
5341
5342 #[test]
5343 fn visual_u_uppercases_selection() {
5344 let mut e = editor_with("hello world");
5345 run_keys(&mut e, "veU");
5347 assert_eq!(e.buffer().lines()[0], "HELLO world");
5348 }
5349
5350 #[test]
5351 fn visual_line_u_lowercases_line() {
5352 let mut e = editor_with("HELLO WORLD\nOTHER");
5353 run_keys(&mut e, "Vu");
5354 assert_eq!(e.buffer().lines()[0], "hello world");
5355 assert_eq!(e.buffer().lines()[1], "OTHER");
5356 }
5357
5358 #[test]
5359 fn g_uu_with_count_uppercases_multiple_lines() {
5360 let mut e = editor_with("one\ntwo\nthree\nfour");
5361 run_keys(&mut e, "3gUU");
5363 assert_eq!(e.buffer().lines()[0], "ONE");
5364 assert_eq!(e.buffer().lines()[1], "TWO");
5365 assert_eq!(e.buffer().lines()[2], "THREE");
5366 assert_eq!(e.buffer().lines()[3], "four");
5367 }
5368
5369 #[test]
5370 fn double_gt_indents_current_line() {
5371 let mut e = editor_with("hello");
5372 run_keys(&mut e, ">>");
5373 assert_eq!(e.buffer().lines()[0], " hello");
5374 assert_eq!(e.cursor(), (0, 2));
5376 }
5377
5378 #[test]
5379 fn double_lt_outdents_current_line() {
5380 let mut e = editor_with(" hello");
5381 run_keys(&mut e, "<lt><lt>");
5382 assert_eq!(e.buffer().lines()[0], " hello");
5383 assert_eq!(e.cursor(), (0, 2));
5384 }
5385
5386 #[test]
5387 fn count_double_gt_indents_multiple_lines() {
5388 let mut e = editor_with("a\nb\nc\nd");
5389 run_keys(&mut e, "3>>");
5391 assert_eq!(e.buffer().lines()[0], " a");
5392 assert_eq!(e.buffer().lines()[1], " b");
5393 assert_eq!(e.buffer().lines()[2], " c");
5394 assert_eq!(e.buffer().lines()[3], "d");
5395 }
5396
5397 #[test]
5398 fn outdent_clips_ragged_leading_whitespace() {
5399 let mut e = editor_with(" x");
5402 run_keys(&mut e, "<lt><lt>");
5403 assert_eq!(e.buffer().lines()[0], "x");
5404 }
5405
5406 #[test]
5407 fn indent_motion_is_always_linewise() {
5408 let mut e = editor_with("foo bar");
5411 run_keys(&mut e, ">w");
5412 assert_eq!(e.buffer().lines()[0], " foo bar");
5413 }
5414
5415 #[test]
5416 fn indent_text_object_extends_over_paragraph() {
5417 let mut e = editor_with("a\nb\n\nc\nd");
5418 run_keys(&mut e, ">ap");
5420 assert_eq!(e.buffer().lines()[0], " a");
5421 assert_eq!(e.buffer().lines()[1], " b");
5422 assert_eq!(e.buffer().lines()[2], "");
5423 assert_eq!(e.buffer().lines()[3], "c");
5424 }
5425
5426 #[test]
5427 fn visual_line_indent_shifts_selected_rows() {
5428 let mut e = editor_with("x\ny\nz");
5429 run_keys(&mut e, "Vj>");
5431 assert_eq!(e.buffer().lines()[0], " x");
5432 assert_eq!(e.buffer().lines()[1], " y");
5433 assert_eq!(e.buffer().lines()[2], "z");
5434 }
5435
5436 #[test]
5437 fn outdent_empty_line_is_noop() {
5438 let mut e = editor_with("\nfoo");
5439 run_keys(&mut e, "<lt><lt>");
5440 assert_eq!(e.buffer().lines()[0], "");
5441 }
5442
5443 #[test]
5444 fn indent_skips_empty_lines() {
5445 let mut e = editor_with("");
5448 run_keys(&mut e, ">>");
5449 assert_eq!(e.buffer().lines()[0], "");
5450 }
5451
5452 #[test]
5453 fn insert_ctrl_t_indents_current_line() {
5454 let mut e = editor_with("x");
5455 run_keys(&mut e, "i<C-t>");
5457 assert_eq!(e.buffer().lines()[0], " x");
5458 assert_eq!(e.cursor(), (0, 2));
5461 }
5462
5463 #[test]
5464 fn insert_ctrl_d_outdents_current_line() {
5465 let mut e = editor_with(" x");
5466 run_keys(&mut e, "A<C-d>");
5468 assert_eq!(e.buffer().lines()[0], " x");
5469 }
5470
5471 #[test]
5472 fn h_at_col_zero_does_not_wrap_to_prev_line() {
5473 let mut e = editor_with("first\nsecond");
5474 e.jump_cursor(1, 0);
5475 run_keys(&mut e, "h");
5476 assert_eq!(e.cursor(), (1, 0));
5478 }
5479
5480 #[test]
5481 fn l_at_last_char_does_not_wrap_to_next_line() {
5482 let mut e = editor_with("ab\ncd");
5483 e.jump_cursor(0, 1);
5485 run_keys(&mut e, "l");
5486 assert_eq!(e.cursor(), (0, 1));
5488 }
5489
5490 #[test]
5491 fn count_l_clamps_at_line_end() {
5492 let mut e = editor_with("abcde");
5493 run_keys(&mut e, "20l");
5496 assert_eq!(e.cursor(), (0, 4));
5497 }
5498
5499 #[test]
5500 fn count_h_clamps_at_col_zero() {
5501 let mut e = editor_with("abcde");
5502 e.jump_cursor(0, 3);
5503 run_keys(&mut e, "20h");
5504 assert_eq!(e.cursor(), (0, 0));
5505 }
5506
5507 #[test]
5508 fn dl_on_last_char_still_deletes_it() {
5509 let mut e = editor_with("ab");
5513 e.jump_cursor(0, 1);
5514 run_keys(&mut e, "dl");
5515 assert_eq!(e.buffer().lines()[0], "a");
5516 }
5517
5518 #[test]
5519 fn case_op_preserves_yank_register() {
5520 let mut e = editor_with("target");
5521 run_keys(&mut e, "yy");
5522 let yank_before = e.yank().to_string();
5523 run_keys(&mut e, "gUU");
5525 assert_eq!(e.buffer().lines()[0], "TARGET");
5526 assert_eq!(
5527 e.yank(),
5528 yank_before,
5529 "case ops must preserve the yank buffer"
5530 );
5531 }
5532
5533 #[test]
5534 fn dap_deletes_paragraph() {
5535 let mut e = editor_with("a\nb\n\nc\nd");
5536 run_keys(&mut e, "dap");
5537 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
5538 }
5539
5540 #[test]
5541 fn dit_deletes_inner_tag_content() {
5542 let mut e = editor_with("<b>hello</b>");
5543 e.jump_cursor(0, 4);
5545 run_keys(&mut e, "dit");
5546 assert_eq!(e.buffer().lines()[0], "<b></b>");
5547 }
5548
5549 #[test]
5550 fn dat_deletes_around_tag() {
5551 let mut e = editor_with("hi <b>foo</b> bye");
5552 e.jump_cursor(0, 6);
5553 run_keys(&mut e, "dat");
5554 assert_eq!(e.buffer().lines()[0], "hi bye");
5555 }
5556
5557 #[test]
5558 fn dit_picks_innermost_tag() {
5559 let mut e = editor_with("<a><b>x</b></a>");
5560 e.jump_cursor(0, 6);
5562 run_keys(&mut e, "dit");
5563 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
5565 }
5566
5567 #[test]
5568 fn dat_innermost_tag_pair() {
5569 let mut e = editor_with("<a><b>x</b></a>");
5570 e.jump_cursor(0, 6);
5571 run_keys(&mut e, "dat");
5572 assert_eq!(e.buffer().lines()[0], "<a></a>");
5573 }
5574
5575 #[test]
5576 fn dit_outside_any_tag_no_op() {
5577 let mut e = editor_with("plain text");
5578 e.jump_cursor(0, 3);
5579 run_keys(&mut e, "dit");
5580 assert_eq!(e.buffer().lines()[0], "plain text");
5582 }
5583
5584 #[test]
5585 fn cit_changes_inner_tag_content() {
5586 let mut e = editor_with("<b>hello</b>");
5587 e.jump_cursor(0, 4);
5588 run_keys(&mut e, "citNEW<Esc>");
5589 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
5590 }
5591
5592 #[test]
5593 fn cat_changes_around_tag() {
5594 let mut e = editor_with("hi <b>foo</b> bye");
5595 e.jump_cursor(0, 6);
5596 run_keys(&mut e, "catBAR<Esc>");
5597 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
5598 }
5599
5600 #[test]
5601 fn yit_yanks_inner_tag_content() {
5602 let mut e = editor_with("<b>hello</b>");
5603 e.jump_cursor(0, 4);
5604 run_keys(&mut e, "yit");
5605 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5606 }
5607
5608 #[test]
5609 fn yat_yanks_full_tag_pair() {
5610 let mut e = editor_with("hi <b>foo</b> bye");
5611 e.jump_cursor(0, 6);
5612 run_keys(&mut e, "yat");
5613 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5614 }
5615
5616 #[test]
5617 fn vit_visually_selects_inner_tag() {
5618 let mut e = editor_with("<b>hello</b>");
5619 e.jump_cursor(0, 4);
5620 run_keys(&mut e, "vit");
5621 assert_eq!(e.vim_mode(), VimMode::Visual);
5622 run_keys(&mut e, "y");
5623 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5624 }
5625
5626 #[test]
5627 fn vat_visually_selects_around_tag() {
5628 let mut e = editor_with("x<b>foo</b>y");
5629 e.jump_cursor(0, 5);
5630 run_keys(&mut e, "vat");
5631 assert_eq!(e.vim_mode(), VimMode::Visual);
5632 run_keys(&mut e, "y");
5633 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5634 }
5635
5636 #[test]
5639 #[allow(non_snake_case)]
5640 fn diW_deletes_inner_big_word() {
5641 let mut e = editor_with("foo.bar baz");
5642 e.jump_cursor(0, 2);
5643 run_keys(&mut e, "diW");
5644 assert_eq!(e.buffer().lines()[0], " baz");
5646 }
5647
5648 #[test]
5649 #[allow(non_snake_case)]
5650 fn daW_deletes_around_big_word() {
5651 let mut e = editor_with("foo.bar baz");
5652 e.jump_cursor(0, 2);
5653 run_keys(&mut e, "daW");
5654 assert_eq!(e.buffer().lines()[0], "baz");
5655 }
5656
5657 #[test]
5658 fn di_double_quote_deletes_inside() {
5659 let mut e = editor_with("a \"hello\" b");
5660 e.jump_cursor(0, 4);
5661 run_keys(&mut e, "di\"");
5662 assert_eq!(e.buffer().lines()[0], "a \"\" b");
5663 }
5664
5665 #[test]
5666 fn da_double_quote_deletes_around() {
5667 let mut e = editor_with("a \"hello\" b");
5668 e.jump_cursor(0, 4);
5669 run_keys(&mut e, "da\"");
5670 assert_eq!(e.buffer().lines()[0], "a b");
5671 }
5672
5673 #[test]
5674 fn di_single_quote_deletes_inside() {
5675 let mut e = editor_with("x 'foo' y");
5676 e.jump_cursor(0, 4);
5677 run_keys(&mut e, "di'");
5678 assert_eq!(e.buffer().lines()[0], "x '' y");
5679 }
5680
5681 #[test]
5682 fn da_single_quote_deletes_around() {
5683 let mut e = editor_with("x 'foo' y");
5684 e.jump_cursor(0, 4);
5685 run_keys(&mut e, "da'");
5686 assert_eq!(e.buffer().lines()[0], "x y");
5687 }
5688
5689 #[test]
5690 fn di_backtick_deletes_inside() {
5691 let mut e = editor_with("p `q` r");
5692 e.jump_cursor(0, 3);
5693 run_keys(&mut e, "di`");
5694 assert_eq!(e.buffer().lines()[0], "p `` r");
5695 }
5696
5697 #[test]
5698 fn da_backtick_deletes_around() {
5699 let mut e = editor_with("p `q` r");
5700 e.jump_cursor(0, 3);
5701 run_keys(&mut e, "da`");
5702 assert_eq!(e.buffer().lines()[0], "p r");
5703 }
5704
5705 #[test]
5706 fn di_paren_deletes_inside() {
5707 let mut e = editor_with("f(arg)");
5708 e.jump_cursor(0, 3);
5709 run_keys(&mut e, "di(");
5710 assert_eq!(e.buffer().lines()[0], "f()");
5711 }
5712
5713 #[test]
5714 fn di_paren_alias_b_works() {
5715 let mut e = editor_with("f(arg)");
5716 e.jump_cursor(0, 3);
5717 run_keys(&mut e, "dib");
5718 assert_eq!(e.buffer().lines()[0], "f()");
5719 }
5720
5721 #[test]
5722 fn di_bracket_deletes_inside() {
5723 let mut e = editor_with("a[b,c]d");
5724 e.jump_cursor(0, 3);
5725 run_keys(&mut e, "di[");
5726 assert_eq!(e.buffer().lines()[0], "a[]d");
5727 }
5728
5729 #[test]
5730 fn da_bracket_deletes_around() {
5731 let mut e = editor_with("a[b,c]d");
5732 e.jump_cursor(0, 3);
5733 run_keys(&mut e, "da[");
5734 assert_eq!(e.buffer().lines()[0], "ad");
5735 }
5736
5737 #[test]
5738 fn di_brace_deletes_inside() {
5739 let mut e = editor_with("x{y}z");
5740 e.jump_cursor(0, 2);
5741 run_keys(&mut e, "di{");
5742 assert_eq!(e.buffer().lines()[0], "x{}z");
5743 }
5744
5745 #[test]
5746 fn da_brace_deletes_around() {
5747 let mut e = editor_with("x{y}z");
5748 e.jump_cursor(0, 2);
5749 run_keys(&mut e, "da{");
5750 assert_eq!(e.buffer().lines()[0], "xz");
5751 }
5752
5753 #[test]
5754 fn di_brace_alias_capital_b_works() {
5755 let mut e = editor_with("x{y}z");
5756 e.jump_cursor(0, 2);
5757 run_keys(&mut e, "diB");
5758 assert_eq!(e.buffer().lines()[0], "x{}z");
5759 }
5760
5761 #[test]
5762 fn di_angle_deletes_inside() {
5763 let mut e = editor_with("p<q>r");
5764 e.jump_cursor(0, 2);
5765 run_keys(&mut e, "di<lt>");
5767 assert_eq!(e.buffer().lines()[0], "p<>r");
5768 }
5769
5770 #[test]
5771 fn da_angle_deletes_around() {
5772 let mut e = editor_with("p<q>r");
5773 e.jump_cursor(0, 2);
5774 run_keys(&mut e, "da<lt>");
5775 assert_eq!(e.buffer().lines()[0], "pr");
5776 }
5777
5778 #[test]
5779 fn dip_deletes_inner_paragraph() {
5780 let mut e = editor_with("a\nb\nc\n\nd");
5781 e.jump_cursor(1, 0);
5782 run_keys(&mut e, "dip");
5783 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
5786 }
5787
5788 #[test]
5791 fn sentence_motion_close_paren_jumps_forward() {
5792 let mut e = editor_with("Alpha. Beta. Gamma.");
5793 e.jump_cursor(0, 0);
5794 run_keys(&mut e, ")");
5795 assert_eq!(e.cursor(), (0, 7));
5797 run_keys(&mut e, ")");
5798 assert_eq!(e.cursor(), (0, 13));
5799 }
5800
5801 #[test]
5802 fn sentence_motion_open_paren_jumps_backward() {
5803 let mut e = editor_with("Alpha. Beta. Gamma.");
5804 e.jump_cursor(0, 13);
5805 run_keys(&mut e, "(");
5806 assert_eq!(e.cursor(), (0, 7));
5809 run_keys(&mut e, "(");
5810 assert_eq!(e.cursor(), (0, 0));
5811 }
5812
5813 #[test]
5814 fn sentence_motion_count() {
5815 let mut e = editor_with("A. B. C. D.");
5816 e.jump_cursor(0, 0);
5817 run_keys(&mut e, "3)");
5818 assert_eq!(e.cursor(), (0, 9));
5820 }
5821
5822 #[test]
5823 fn dis_deletes_inner_sentence() {
5824 let mut e = editor_with("First one. Second one. Third one.");
5825 e.jump_cursor(0, 13);
5826 run_keys(&mut e, "dis");
5827 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
5829 }
5830
5831 #[test]
5832 fn das_deletes_around_sentence_with_trailing_space() {
5833 let mut e = editor_with("Alpha. Beta. Gamma.");
5834 e.jump_cursor(0, 8);
5835 run_keys(&mut e, "das");
5836 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
5839 }
5840
5841 #[test]
5842 fn dis_handles_double_terminator() {
5843 let mut e = editor_with("Wow!? Next.");
5844 e.jump_cursor(0, 1);
5845 run_keys(&mut e, "dis");
5846 assert_eq!(e.buffer().lines()[0], " Next.");
5849 }
5850
5851 #[test]
5852 fn dis_first_sentence_from_cursor_at_zero() {
5853 let mut e = editor_with("Alpha. Beta.");
5854 e.jump_cursor(0, 0);
5855 run_keys(&mut e, "dis");
5856 assert_eq!(e.buffer().lines()[0], " Beta.");
5857 }
5858
5859 #[test]
5860 fn yis_yanks_inner_sentence() {
5861 let mut e = editor_with("Hello world. Bye.");
5862 e.jump_cursor(0, 5);
5863 run_keys(&mut e, "yis");
5864 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
5865 }
5866
5867 #[test]
5868 fn vis_visually_selects_inner_sentence() {
5869 let mut e = editor_with("First. Second.");
5870 e.jump_cursor(0, 1);
5871 run_keys(&mut e, "vis");
5872 assert_eq!(e.vim_mode(), VimMode::Visual);
5873 run_keys(&mut e, "y");
5874 assert_eq!(e.registers().read('"').unwrap().text, "First.");
5875 }
5876
5877 #[test]
5878 fn ciw_changes_inner_word() {
5879 let mut e = editor_with("hello world");
5880 e.jump_cursor(0, 1);
5881 run_keys(&mut e, "ciwHEY<Esc>");
5882 assert_eq!(e.buffer().lines()[0], "HEY world");
5883 }
5884
5885 #[test]
5886 fn yiw_yanks_inner_word() {
5887 let mut e = editor_with("hello world");
5888 e.jump_cursor(0, 1);
5889 run_keys(&mut e, "yiw");
5890 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5891 }
5892
5893 #[test]
5894 fn viw_selects_inner_word() {
5895 let mut e = editor_with("hello world");
5896 e.jump_cursor(0, 2);
5897 run_keys(&mut e, "viw");
5898 assert_eq!(e.vim_mode(), VimMode::Visual);
5899 run_keys(&mut e, "y");
5900 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5901 }
5902
5903 #[test]
5904 fn ci_paren_changes_inside() {
5905 let mut e = editor_with("f(old)");
5906 e.jump_cursor(0, 3);
5907 run_keys(&mut e, "ci(NEW<Esc>");
5908 assert_eq!(e.buffer().lines()[0], "f(NEW)");
5909 }
5910
5911 #[test]
5912 fn yi_double_quote_yanks_inside() {
5913 let mut e = editor_with("say \"hi there\" then");
5914 e.jump_cursor(0, 6);
5915 run_keys(&mut e, "yi\"");
5916 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
5917 }
5918
5919 #[test]
5920 fn vap_visual_selects_around_paragraph() {
5921 let mut e = editor_with("a\nb\n\nc");
5922 e.jump_cursor(0, 0);
5923 run_keys(&mut e, "vap");
5924 assert_eq!(e.vim_mode(), VimMode::VisualLine);
5925 run_keys(&mut e, "y");
5926 let text = e.registers().read('"').unwrap().text.clone();
5928 assert!(text.starts_with("a\nb"));
5929 }
5930
5931 #[test]
5932 fn star_finds_next_occurrence() {
5933 let mut e = editor_with("foo bar foo baz");
5934 run_keys(&mut e, "*");
5935 assert_eq!(e.cursor().1, 8);
5936 }
5937
5938 #[test]
5939 fn star_skips_substring_match() {
5940 let mut e = editor_with("foo foobar baz");
5943 run_keys(&mut e, "*");
5944 assert_eq!(e.cursor().1, 0);
5945 }
5946
5947 #[test]
5948 fn g_star_matches_substring() {
5949 let mut e = editor_with("foo foobar baz");
5952 run_keys(&mut e, "g*");
5953 assert_eq!(e.cursor().1, 4);
5954 }
5955
5956 #[test]
5957 fn g_pound_matches_substring_backward() {
5958 let mut e = editor_with("foo foobar baz foo");
5961 run_keys(&mut e, "$b");
5962 assert_eq!(e.cursor().1, 15);
5963 run_keys(&mut e, "g#");
5964 assert_eq!(e.cursor().1, 4);
5965 }
5966
5967 #[test]
5968 fn n_repeats_last_search_forward() {
5969 let mut e = editor_with("foo bar foo baz foo");
5970 run_keys(&mut e, "/foo<CR>");
5973 assert_eq!(e.cursor().1, 8);
5974 run_keys(&mut e, "n");
5975 assert_eq!(e.cursor().1, 16);
5976 }
5977
5978 #[test]
5979 fn shift_n_reverses_search() {
5980 let mut e = editor_with("foo bar foo baz foo");
5981 run_keys(&mut e, "/foo<CR>");
5982 run_keys(&mut e, "n");
5983 assert_eq!(e.cursor().1, 16);
5984 run_keys(&mut e, "N");
5985 assert_eq!(e.cursor().1, 8);
5986 }
5987
5988 #[test]
5989 fn n_noop_without_pattern() {
5990 let mut e = editor_with("foo bar");
5991 run_keys(&mut e, "n");
5992 assert_eq!(e.cursor(), (0, 0));
5993 }
5994
5995 #[test]
5996 fn visual_line_preserves_cursor_column() {
5997 let mut e = editor_with("hello world\nanother one\nbye");
6000 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6002 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6003 assert_eq!(e.cursor(), (0, 5));
6004 run_keys(&mut e, "j");
6005 assert_eq!(e.cursor(), (1, 5));
6006 }
6007
6008 #[test]
6009 fn visual_line_yank_includes_trailing_newline() {
6010 let mut e = editor_with("aaa\nbbb\nccc");
6011 run_keys(&mut e, "Vjy");
6012 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6014 }
6015
6016 #[test]
6017 fn visual_line_yank_last_line_trailing_newline() {
6018 let mut e = editor_with("aaa\nbbb\nccc");
6019 run_keys(&mut e, "jj");
6021 run_keys(&mut e, "Vy");
6022 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6023 }
6024
6025 #[test]
6026 fn yy_on_last_line_has_trailing_newline() {
6027 let mut e = editor_with("aaa\nbbb\nccc");
6028 run_keys(&mut e, "jj");
6029 run_keys(&mut e, "yy");
6030 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6031 }
6032
6033 #[test]
6034 fn yy_in_middle_has_trailing_newline() {
6035 let mut e = editor_with("aaa\nbbb\nccc");
6036 run_keys(&mut e, "j");
6037 run_keys(&mut e, "yy");
6038 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6039 }
6040
6041 #[test]
6042 fn di_single_quote() {
6043 let mut e = editor_with("say 'hello world' now");
6044 e.jump_cursor(0, 7);
6045 run_keys(&mut e, "di'");
6046 assert_eq!(e.buffer().lines()[0], "say '' now");
6047 }
6048
6049 #[test]
6050 fn da_single_quote() {
6051 let mut e = editor_with("say 'hello' now");
6052 e.jump_cursor(0, 7);
6053 run_keys(&mut e, "da'");
6054 assert_eq!(e.buffer().lines()[0], "say now");
6055 }
6056
6057 #[test]
6058 fn di_backtick() {
6059 let mut e = editor_with("say `hi` now");
6060 e.jump_cursor(0, 5);
6061 run_keys(&mut e, "di`");
6062 assert_eq!(e.buffer().lines()[0], "say `` now");
6063 }
6064
6065 #[test]
6066 fn di_brace() {
6067 let mut e = editor_with("fn { a; b; c }");
6068 e.jump_cursor(0, 7);
6069 run_keys(&mut e, "di{");
6070 assert_eq!(e.buffer().lines()[0], "fn {}");
6071 }
6072
6073 #[test]
6074 fn di_bracket() {
6075 let mut e = editor_with("arr[1, 2, 3]");
6076 e.jump_cursor(0, 5);
6077 run_keys(&mut e, "di[");
6078 assert_eq!(e.buffer().lines()[0], "arr[]");
6079 }
6080
6081 #[test]
6082 fn dab_deletes_around_paren() {
6083 let mut e = editor_with("fn(a, b) + 1");
6084 e.jump_cursor(0, 4);
6085 run_keys(&mut e, "dab");
6086 assert_eq!(e.buffer().lines()[0], "fn + 1");
6087 }
6088
6089 #[test]
6090 fn da_big_b_deletes_around_brace() {
6091 let mut e = editor_with("x = {a: 1}");
6092 e.jump_cursor(0, 6);
6093 run_keys(&mut e, "daB");
6094 assert_eq!(e.buffer().lines()[0], "x = ");
6095 }
6096
6097 #[test]
6098 fn di_big_w_deletes_bigword() {
6099 let mut e = editor_with("foo-bar baz");
6100 e.jump_cursor(0, 2);
6101 run_keys(&mut e, "diW");
6102 assert_eq!(e.buffer().lines()[0], " baz");
6103 }
6104
6105 #[test]
6106 fn visual_select_inner_word() {
6107 let mut e = editor_with("hello world");
6108 e.jump_cursor(0, 2);
6109 run_keys(&mut e, "viw");
6110 assert_eq!(e.vim_mode(), VimMode::Visual);
6111 run_keys(&mut e, "y");
6112 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6113 }
6114
6115 #[test]
6116 fn visual_select_inner_quote() {
6117 let mut e = editor_with("foo \"bar\" baz");
6118 e.jump_cursor(0, 6);
6119 run_keys(&mut e, "vi\"");
6120 run_keys(&mut e, "y");
6121 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6122 }
6123
6124 #[test]
6125 fn visual_select_inner_paren() {
6126 let mut e = editor_with("fn(a, b)");
6127 e.jump_cursor(0, 4);
6128 run_keys(&mut e, "vi(");
6129 run_keys(&mut e, "y");
6130 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6131 }
6132
6133 #[test]
6134 fn visual_select_outer_brace() {
6135 let mut e = editor_with("{x}");
6136 e.jump_cursor(0, 1);
6137 run_keys(&mut e, "va{");
6138 run_keys(&mut e, "y");
6139 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6140 }
6141
6142 #[test]
6143 fn caw_changes_word_with_trailing_space() {
6144 let mut e = editor_with("hello world");
6145 run_keys(&mut e, "cawfoo<Esc>");
6146 assert_eq!(e.buffer().lines()[0], "fooworld");
6147 }
6148
6149 #[test]
6150 fn visual_char_yank_preserves_raw_text() {
6151 let mut e = editor_with("hello world");
6152 run_keys(&mut e, "vllly");
6153 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6154 }
6155
6156 #[test]
6157 fn single_line_visual_line_selects_full_line_on_yank() {
6158 let mut e = editor_with("hello world\nbye");
6159 run_keys(&mut e, "V");
6160 run_keys(&mut e, "y");
6163 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6164 }
6165
6166 #[test]
6167 fn visual_line_extends_both_directions() {
6168 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6169 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6171 assert_eq!(e.cursor(), (3, 0));
6172 run_keys(&mut e, "k");
6173 assert_eq!(e.cursor(), (2, 0));
6175 run_keys(&mut e, "k");
6176 assert_eq!(e.cursor(), (1, 0));
6177 }
6178
6179 #[test]
6180 fn visual_char_preserves_cursor_column() {
6181 let mut e = editor_with("hello world");
6182 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6184 assert_eq!(e.cursor(), (0, 5));
6185 run_keys(&mut e, "ll");
6186 assert_eq!(e.cursor(), (0, 7));
6187 }
6188
6189 #[test]
6190 fn visual_char_highlight_bounds_order() {
6191 let mut e = editor_with("abcdef");
6192 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
6194 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
6197 }
6198
6199 #[test]
6200 fn visual_line_highlight_bounds() {
6201 let mut e = editor_with("a\nb\nc");
6202 run_keys(&mut e, "V");
6203 assert_eq!(e.line_highlight(), Some((0, 0)));
6204 run_keys(&mut e, "j");
6205 assert_eq!(e.line_highlight(), Some((0, 1)));
6206 run_keys(&mut e, "j");
6207 assert_eq!(e.line_highlight(), Some((0, 2)));
6208 }
6209
6210 #[test]
6213 fn h_moves_left() {
6214 let mut e = editor_with("hello");
6215 e.jump_cursor(0, 3);
6216 run_keys(&mut e, "h");
6217 assert_eq!(e.cursor(), (0, 2));
6218 }
6219
6220 #[test]
6221 fn l_moves_right() {
6222 let mut e = editor_with("hello");
6223 run_keys(&mut e, "l");
6224 assert_eq!(e.cursor(), (0, 1));
6225 }
6226
6227 #[test]
6228 fn k_moves_up() {
6229 let mut e = editor_with("a\nb\nc");
6230 e.jump_cursor(2, 0);
6231 run_keys(&mut e, "k");
6232 assert_eq!(e.cursor(), (1, 0));
6233 }
6234
6235 #[test]
6236 fn zero_moves_to_line_start() {
6237 let mut e = editor_with(" hello");
6238 run_keys(&mut e, "$");
6239 run_keys(&mut e, "0");
6240 assert_eq!(e.cursor().1, 0);
6241 }
6242
6243 #[test]
6244 fn caret_moves_to_first_non_blank() {
6245 let mut e = editor_with(" hello");
6246 run_keys(&mut e, "0");
6247 run_keys(&mut e, "^");
6248 assert_eq!(e.cursor().1, 4);
6249 }
6250
6251 #[test]
6252 fn dollar_moves_to_last_char() {
6253 let mut e = editor_with("hello");
6254 run_keys(&mut e, "$");
6255 assert_eq!(e.cursor().1, 4);
6256 }
6257
6258 #[test]
6259 fn dollar_on_empty_line_stays_at_col_zero() {
6260 let mut e = editor_with("");
6261 run_keys(&mut e, "$");
6262 assert_eq!(e.cursor().1, 0);
6263 }
6264
6265 #[test]
6266 fn w_jumps_to_next_word() {
6267 let mut e = editor_with("foo bar baz");
6268 run_keys(&mut e, "w");
6269 assert_eq!(e.cursor().1, 4);
6270 }
6271
6272 #[test]
6273 fn b_jumps_back_a_word() {
6274 let mut e = editor_with("foo bar");
6275 e.jump_cursor(0, 6);
6276 run_keys(&mut e, "b");
6277 assert_eq!(e.cursor().1, 4);
6278 }
6279
6280 #[test]
6281 fn e_jumps_to_word_end() {
6282 let mut e = editor_with("foo bar");
6283 run_keys(&mut e, "e");
6284 assert_eq!(e.cursor().1, 2);
6285 }
6286
6287 #[test]
6290 fn d_dollar_deletes_to_eol() {
6291 let mut e = editor_with("hello world");
6292 e.jump_cursor(0, 5);
6293 run_keys(&mut e, "d$");
6294 assert_eq!(e.buffer().lines()[0], "hello");
6295 }
6296
6297 #[test]
6298 fn d_zero_deletes_to_line_start() {
6299 let mut e = editor_with("hello world");
6300 e.jump_cursor(0, 6);
6301 run_keys(&mut e, "d0");
6302 assert_eq!(e.buffer().lines()[0], "world");
6303 }
6304
6305 #[test]
6306 fn d_caret_deletes_to_first_non_blank() {
6307 let mut e = editor_with(" hello");
6308 e.jump_cursor(0, 6);
6309 run_keys(&mut e, "d^");
6310 assert_eq!(e.buffer().lines()[0], " llo");
6311 }
6312
6313 #[test]
6314 fn d_capital_g_deletes_to_end_of_file() {
6315 let mut e = editor_with("a\nb\nc\nd");
6316 e.jump_cursor(1, 0);
6317 run_keys(&mut e, "dG");
6318 assert_eq!(e.buffer().lines(), &["a".to_string()]);
6319 }
6320
6321 #[test]
6322 fn d_gg_deletes_to_start_of_file() {
6323 let mut e = editor_with("a\nb\nc\nd");
6324 e.jump_cursor(2, 0);
6325 run_keys(&mut e, "dgg");
6326 assert_eq!(e.buffer().lines(), &["d".to_string()]);
6327 }
6328
6329 #[test]
6330 fn cw_is_ce_quirk() {
6331 let mut e = editor_with("foo bar");
6334 run_keys(&mut e, "cwxyz<Esc>");
6335 assert_eq!(e.buffer().lines()[0], "xyz bar");
6336 }
6337
6338 #[test]
6341 fn big_d_deletes_to_eol() {
6342 let mut e = editor_with("hello world");
6343 e.jump_cursor(0, 5);
6344 run_keys(&mut e, "D");
6345 assert_eq!(e.buffer().lines()[0], "hello");
6346 }
6347
6348 #[test]
6349 fn big_c_deletes_to_eol_and_inserts() {
6350 let mut e = editor_with("hello world");
6351 e.jump_cursor(0, 5);
6352 run_keys(&mut e, "C!<Esc>");
6353 assert_eq!(e.buffer().lines()[0], "hello!");
6354 }
6355
6356 #[test]
6357 fn j_joins_next_line_with_space() {
6358 let mut e = editor_with("hello\nworld");
6359 run_keys(&mut e, "J");
6360 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6361 }
6362
6363 #[test]
6364 fn j_strips_leading_whitespace_on_join() {
6365 let mut e = editor_with("hello\n world");
6366 run_keys(&mut e, "J");
6367 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6368 }
6369
6370 #[test]
6371 fn big_x_deletes_char_before_cursor() {
6372 let mut e = editor_with("hello");
6373 e.jump_cursor(0, 3);
6374 run_keys(&mut e, "X");
6375 assert_eq!(e.buffer().lines()[0], "helo");
6376 }
6377
6378 #[test]
6379 fn s_substitutes_char_and_enters_insert() {
6380 let mut e = editor_with("hello");
6381 run_keys(&mut e, "sX<Esc>");
6382 assert_eq!(e.buffer().lines()[0], "Xello");
6383 }
6384
6385 #[test]
6386 fn count_x_deletes_many() {
6387 let mut e = editor_with("abcdef");
6388 run_keys(&mut e, "3x");
6389 assert_eq!(e.buffer().lines()[0], "def");
6390 }
6391
6392 #[test]
6395 fn p_pastes_charwise_after_cursor() {
6396 let mut e = editor_with("hello");
6397 run_keys(&mut e, "yw");
6398 run_keys(&mut e, "$p");
6399 assert_eq!(e.buffer().lines()[0], "hellohello");
6400 }
6401
6402 #[test]
6403 fn capital_p_pastes_charwise_before_cursor() {
6404 let mut e = editor_with("hello");
6405 run_keys(&mut e, "v");
6407 run_keys(&mut e, "l");
6408 run_keys(&mut e, "y");
6409 run_keys(&mut e, "$P");
6410 assert_eq!(e.buffer().lines()[0], "hellheo");
6413 }
6414
6415 #[test]
6416 fn p_pastes_linewise_below() {
6417 let mut e = editor_with("one\ntwo\nthree");
6418 run_keys(&mut e, "yy");
6419 run_keys(&mut e, "p");
6420 assert_eq!(
6421 e.buffer().lines(),
6422 &[
6423 "one".to_string(),
6424 "one".to_string(),
6425 "two".to_string(),
6426 "three".to_string()
6427 ]
6428 );
6429 }
6430
6431 #[test]
6432 fn capital_p_pastes_linewise_above() {
6433 let mut e = editor_with("one\ntwo");
6434 e.jump_cursor(1, 0);
6435 run_keys(&mut e, "yy");
6436 run_keys(&mut e, "P");
6437 assert_eq!(
6438 e.buffer().lines(),
6439 &["one".to_string(), "two".to_string(), "two".to_string()]
6440 );
6441 }
6442
6443 #[test]
6446 fn hash_finds_previous_occurrence() {
6447 let mut e = editor_with("foo bar foo baz foo");
6448 e.jump_cursor(0, 16);
6450 run_keys(&mut e, "#");
6451 assert_eq!(e.cursor().1, 8);
6452 }
6453
6454 #[test]
6457 fn visual_line_delete_removes_full_lines() {
6458 let mut e = editor_with("a\nb\nc\nd");
6459 run_keys(&mut e, "Vjd");
6460 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
6461 }
6462
6463 #[test]
6464 fn visual_line_change_leaves_blank_line() {
6465 let mut e = editor_with("a\nb\nc");
6466 run_keys(&mut e, "Vjc");
6467 assert_eq!(e.vim_mode(), VimMode::Insert);
6468 run_keys(&mut e, "X<Esc>");
6469 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
6473 }
6474
6475 #[test]
6476 fn cc_leaves_blank_line() {
6477 let mut e = editor_with("a\nb\nc");
6478 e.jump_cursor(1, 0);
6479 run_keys(&mut e, "ccX<Esc>");
6480 assert_eq!(
6481 e.buffer().lines(),
6482 &["a".to_string(), "X".to_string(), "c".to_string()]
6483 );
6484 }
6485
6486 #[test]
6491 fn big_w_skips_hyphens() {
6492 let mut e = editor_with("foo-bar baz");
6494 run_keys(&mut e, "W");
6495 assert_eq!(e.cursor().1, 8);
6496 }
6497
6498 #[test]
6499 fn big_w_crosses_lines() {
6500 let mut e = editor_with("foo-bar\nbaz-qux");
6501 run_keys(&mut e, "W");
6502 assert_eq!(e.cursor(), (1, 0));
6503 }
6504
6505 #[test]
6506 fn big_b_skips_hyphens() {
6507 let mut e = editor_with("foo-bar baz");
6508 e.jump_cursor(0, 9);
6509 run_keys(&mut e, "B");
6510 assert_eq!(e.cursor().1, 8);
6511 run_keys(&mut e, "B");
6512 assert_eq!(e.cursor().1, 0);
6513 }
6514
6515 #[test]
6516 fn big_e_jumps_to_big_word_end() {
6517 let mut e = editor_with("foo-bar baz");
6518 run_keys(&mut e, "E");
6519 assert_eq!(e.cursor().1, 6);
6520 run_keys(&mut e, "E");
6521 assert_eq!(e.cursor().1, 10);
6522 }
6523
6524 #[test]
6525 fn dw_with_big_word_variant() {
6526 let mut e = editor_with("foo-bar baz");
6528 run_keys(&mut e, "dW");
6529 assert_eq!(e.buffer().lines()[0], "baz");
6530 }
6531
6532 #[test]
6535 fn insert_ctrl_w_deletes_word_back() {
6536 let mut e = editor_with("");
6537 run_keys(&mut e, "i");
6538 for c in "hello world".chars() {
6539 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6540 }
6541 run_keys(&mut e, "<C-w>");
6542 assert_eq!(e.buffer().lines()[0], "hello ");
6543 }
6544
6545 #[test]
6546 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
6547 let mut e = editor_with("hello\nworld");
6551 e.jump_cursor(1, 0);
6552 run_keys(&mut e, "i");
6553 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6554 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
6557 assert_eq!(e.cursor(), (0, 0));
6558 }
6559
6560 #[test]
6561 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
6562 let mut e = editor_with("foo bar\nbaz");
6563 e.jump_cursor(1, 0);
6564 run_keys(&mut e, "i");
6565 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6566 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
6568 assert_eq!(e.cursor(), (0, 4));
6569 }
6570
6571 #[test]
6572 fn insert_ctrl_u_deletes_to_line_start() {
6573 let mut e = editor_with("");
6574 run_keys(&mut e, "i");
6575 for c in "hello world".chars() {
6576 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6577 }
6578 run_keys(&mut e, "<C-u>");
6579 assert_eq!(e.buffer().lines()[0], "");
6580 }
6581
6582 #[test]
6583 fn insert_ctrl_o_runs_one_normal_command() {
6584 let mut e = editor_with("hello world");
6585 run_keys(&mut e, "A");
6587 assert_eq!(e.vim_mode(), VimMode::Insert);
6588 e.jump_cursor(0, 0);
6590 run_keys(&mut e, "<C-o>");
6591 assert_eq!(e.vim_mode(), VimMode::Normal);
6592 run_keys(&mut e, "dw");
6593 assert_eq!(e.vim_mode(), VimMode::Insert);
6595 assert_eq!(e.buffer().lines()[0], "world");
6596 }
6597
6598 #[test]
6601 fn j_through_empty_line_preserves_column() {
6602 let mut e = editor_with("hello world\n\nanother line");
6603 run_keys(&mut e, "llllll");
6605 assert_eq!(e.cursor(), (0, 6));
6606 run_keys(&mut e, "j");
6609 assert_eq!(e.cursor(), (1, 0));
6610 run_keys(&mut e, "j");
6612 assert_eq!(e.cursor(), (2, 6));
6613 }
6614
6615 #[test]
6616 fn j_through_shorter_line_preserves_column() {
6617 let mut e = editor_with("hello world\nhi\nanother line");
6618 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
6621 run_keys(&mut e, "j");
6622 assert_eq!(e.cursor(), (2, 7));
6623 }
6624
6625 #[test]
6626 fn esc_from_insert_sticky_matches_visible_cursor() {
6627 let mut e = editor_with(" this is a line\n another one of a similar size");
6631 e.jump_cursor(0, 12);
6632 run_keys(&mut e, "I");
6633 assert_eq!(e.cursor(), (0, 4));
6634 run_keys(&mut e, "X<Esc>");
6635 assert_eq!(e.cursor(), (0, 4));
6636 run_keys(&mut e, "j");
6637 assert_eq!(e.cursor(), (1, 4));
6638 }
6639
6640 #[test]
6641 fn esc_from_insert_sticky_tracks_inserted_chars() {
6642 let mut e = editor_with("xxxxxxx\nyyyyyyy");
6643 run_keys(&mut e, "i");
6644 run_keys(&mut e, "abc<Esc>");
6645 assert_eq!(e.cursor(), (0, 2));
6646 run_keys(&mut e, "j");
6647 assert_eq!(e.cursor(), (1, 2));
6648 }
6649
6650 #[test]
6651 fn esc_from_insert_sticky_tracks_arrow_nav() {
6652 let mut e = editor_with("xxxxxx\nyyyyyy");
6653 run_keys(&mut e, "i");
6654 run_keys(&mut e, "abc");
6655 for _ in 0..2 {
6656 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
6657 }
6658 run_keys(&mut e, "<Esc>");
6659 assert_eq!(e.cursor(), (0, 0));
6660 run_keys(&mut e, "j");
6661 assert_eq!(e.cursor(), (1, 0));
6662 }
6663
6664 #[test]
6665 fn esc_from_insert_at_col_14_followed_by_j() {
6666 let line = "x".repeat(30);
6669 let buf = format!("{line}\n{line}");
6670 let mut e = editor_with(&buf);
6671 e.jump_cursor(0, 14);
6672 run_keys(&mut e, "i");
6673 for c in "test ".chars() {
6674 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6675 }
6676 run_keys(&mut e, "<Esc>");
6677 assert_eq!(e.cursor(), (0, 18));
6678 run_keys(&mut e, "j");
6679 assert_eq!(e.cursor(), (1, 18));
6680 }
6681
6682 #[test]
6683 fn linewise_paste_resets_sticky_column() {
6684 let mut e = editor_with(" hello\naaaaaaaa\nbye");
6688 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
6690 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
6694 run_keys(&mut e, "j");
6696 assert_eq!(e.cursor(), (3, 2));
6697 }
6698
6699 #[test]
6700 fn horizontal_motion_resyncs_sticky_column() {
6701 let mut e = editor_with("hello world\n\nanother line");
6705 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
6708 assert_eq!(e.cursor(), (2, 3));
6709 }
6710
6711 #[test]
6714 fn ctrl_v_enters_visual_block() {
6715 let mut e = editor_with("aaa\nbbb\nccc");
6716 run_keys(&mut e, "<C-v>");
6717 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
6718 }
6719
6720 #[test]
6721 fn visual_block_esc_returns_to_normal() {
6722 let mut e = editor_with("aaa\nbbb\nccc");
6723 run_keys(&mut e, "<C-v>");
6724 run_keys(&mut e, "<Esc>");
6725 assert_eq!(e.vim_mode(), VimMode::Normal);
6726 }
6727
6728 #[test]
6729 fn visual_block_delete_removes_column_range() {
6730 let mut e = editor_with("hello\nworld\nhappy");
6731 run_keys(&mut e, "l");
6733 run_keys(&mut e, "<C-v>");
6734 run_keys(&mut e, "jj");
6735 run_keys(&mut e, "ll");
6736 run_keys(&mut e, "d");
6737 assert_eq!(
6739 e.buffer().lines(),
6740 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
6741 );
6742 }
6743
6744 #[test]
6745 fn visual_block_yank_joins_with_newlines() {
6746 let mut e = editor_with("hello\nworld\nhappy");
6747 run_keys(&mut e, "<C-v>");
6748 run_keys(&mut e, "jj");
6749 run_keys(&mut e, "ll");
6750 run_keys(&mut e, "y");
6751 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
6752 }
6753
6754 #[test]
6755 fn visual_block_replace_fills_block() {
6756 let mut e = editor_with("hello\nworld\nhappy");
6757 run_keys(&mut e, "<C-v>");
6758 run_keys(&mut e, "jj");
6759 run_keys(&mut e, "ll");
6760 run_keys(&mut e, "rx");
6761 assert_eq!(
6762 e.buffer().lines(),
6763 &[
6764 "xxxlo".to_string(),
6765 "xxxld".to_string(),
6766 "xxxpy".to_string()
6767 ]
6768 );
6769 }
6770
6771 #[test]
6772 fn visual_block_insert_repeats_across_rows() {
6773 let mut e = editor_with("hello\nworld\nhappy");
6774 run_keys(&mut e, "<C-v>");
6775 run_keys(&mut e, "jj");
6776 run_keys(&mut e, "I");
6777 run_keys(&mut e, "# <Esc>");
6778 assert_eq!(
6779 e.buffer().lines(),
6780 &[
6781 "# hello".to_string(),
6782 "# world".to_string(),
6783 "# happy".to_string()
6784 ]
6785 );
6786 }
6787
6788 #[test]
6789 fn block_highlight_returns_none_outside_block_mode() {
6790 let mut e = editor_with("abc");
6791 assert!(e.block_highlight().is_none());
6792 run_keys(&mut e, "v");
6793 assert!(e.block_highlight().is_none());
6794 run_keys(&mut e, "<Esc>V");
6795 assert!(e.block_highlight().is_none());
6796 }
6797
6798 #[test]
6799 fn block_highlight_bounds_track_anchor_and_cursor() {
6800 let mut e = editor_with("aaaa\nbbbb\ncccc");
6801 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
6803 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
6806 }
6807
6808 #[test]
6809 fn visual_block_delete_handles_short_lines() {
6810 let mut e = editor_with("hello\nhi\nworld");
6812 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
6814 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
6816 assert_eq!(
6821 e.buffer().lines(),
6822 &["ho".to_string(), "h".to_string(), "wd".to_string()]
6823 );
6824 }
6825
6826 #[test]
6827 fn visual_block_yank_pads_short_lines_with_empties() {
6828 let mut e = editor_with("hello\nhi\nworld");
6829 run_keys(&mut e, "l");
6830 run_keys(&mut e, "<C-v>");
6831 run_keys(&mut e, "jjll");
6832 run_keys(&mut e, "y");
6833 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
6835 }
6836
6837 #[test]
6838 fn visual_block_replace_skips_past_eol() {
6839 let mut e = editor_with("ab\ncd\nef");
6842 run_keys(&mut e, "l");
6844 run_keys(&mut e, "<C-v>");
6845 run_keys(&mut e, "jjllllll");
6846 run_keys(&mut e, "rX");
6847 assert_eq!(
6850 e.buffer().lines(),
6851 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
6852 );
6853 }
6854
6855 #[test]
6856 fn visual_block_with_empty_line_in_middle() {
6857 let mut e = editor_with("abcd\n\nefgh");
6858 run_keys(&mut e, "<C-v>");
6859 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
6861 assert_eq!(
6864 e.buffer().lines(),
6865 &["d".to_string(), "".to_string(), "h".to_string()]
6866 );
6867 }
6868
6869 #[test]
6870 fn block_insert_pads_empty_lines_to_block_column() {
6871 let mut e = editor_with("this is a line\n\nthis is a line");
6874 e.jump_cursor(0, 3);
6875 run_keys(&mut e, "<C-v>");
6876 run_keys(&mut e, "jj");
6877 run_keys(&mut e, "I");
6878 run_keys(&mut e, "XX<Esc>");
6879 assert_eq!(
6880 e.buffer().lines(),
6881 &[
6882 "thiXXs is a line".to_string(),
6883 " XX".to_string(),
6884 "thiXXs is a line".to_string()
6885 ]
6886 );
6887 }
6888
6889 #[test]
6890 fn block_insert_pads_short_lines_to_block_column() {
6891 let mut e = editor_with("aaaaa\nbb\naaaaa");
6892 e.jump_cursor(0, 3);
6893 run_keys(&mut e, "<C-v>");
6894 run_keys(&mut e, "jj");
6895 run_keys(&mut e, "I");
6896 run_keys(&mut e, "Y<Esc>");
6897 assert_eq!(
6899 e.buffer().lines(),
6900 &[
6901 "aaaYaa".to_string(),
6902 "bb Y".to_string(),
6903 "aaaYaa".to_string()
6904 ]
6905 );
6906 }
6907
6908 #[test]
6909 fn visual_block_append_repeats_across_rows() {
6910 let mut e = editor_with("foo\nbar\nbaz");
6911 run_keys(&mut e, "<C-v>");
6912 run_keys(&mut e, "jj");
6913 run_keys(&mut e, "A");
6916 run_keys(&mut e, "!<Esc>");
6917 assert_eq!(
6918 e.buffer().lines(),
6919 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
6920 );
6921 }
6922
6923 #[test]
6926 fn slash_opens_forward_search_prompt() {
6927 let mut e = editor_with("hello world");
6928 run_keys(&mut e, "/");
6929 let p = e.search_prompt().expect("prompt should be active");
6930 assert!(p.text.is_empty());
6931 assert!(p.forward);
6932 }
6933
6934 #[test]
6935 fn question_opens_backward_search_prompt() {
6936 let mut e = editor_with("hello world");
6937 run_keys(&mut e, "?");
6938 let p = e.search_prompt().expect("prompt should be active");
6939 assert!(!p.forward);
6940 }
6941
6942 #[test]
6943 fn search_prompt_typing_updates_pattern_live() {
6944 let mut e = editor_with("foo bar\nbaz");
6945 run_keys(&mut e, "/bar");
6946 assert_eq!(e.search_prompt().unwrap().text, "bar");
6947 assert!(e.buffer().search_pattern().is_some());
6949 }
6950
6951 #[test]
6952 fn search_prompt_backspace_and_enter() {
6953 let mut e = editor_with("hello world\nagain");
6954 run_keys(&mut e, "/worlx");
6955 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
6956 assert_eq!(e.search_prompt().unwrap().text, "worl");
6957 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6958 assert!(e.search_prompt().is_none());
6960 assert_eq!(e.last_search(), Some("worl"));
6961 assert_eq!(e.cursor(), (0, 6));
6962 }
6963
6964 #[test]
6965 fn empty_search_prompt_enter_repeats_last_search() {
6966 let mut e = editor_with("foo bar foo baz foo");
6967 run_keys(&mut e, "/foo");
6968 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6969 assert_eq!(e.cursor().1, 8);
6970 run_keys(&mut e, "/");
6972 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6973 assert_eq!(e.cursor().1, 16);
6974 assert_eq!(e.last_search(), Some("foo"));
6975 }
6976
6977 #[test]
6978 fn search_history_records_committed_patterns() {
6979 let mut e = editor_with("alpha beta gamma");
6980 run_keys(&mut e, "/alpha<CR>");
6981 run_keys(&mut e, "/beta<CR>");
6982 let history = e.vim.search_history.clone();
6984 assert_eq!(history, vec!["alpha", "beta"]);
6985 }
6986
6987 #[test]
6988 fn search_history_dedupes_consecutive_repeats() {
6989 let mut e = editor_with("foo bar foo");
6990 run_keys(&mut e, "/foo<CR>");
6991 run_keys(&mut e, "/foo<CR>");
6992 run_keys(&mut e, "/bar<CR>");
6993 run_keys(&mut e, "/bar<CR>");
6994 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
6996 }
6997
6998 #[test]
6999 fn ctrl_p_walks_history_backward() {
7000 let mut e = editor_with("alpha beta gamma");
7001 run_keys(&mut e, "/alpha<CR>");
7002 run_keys(&mut e, "/beta<CR>");
7003 run_keys(&mut e, "/");
7005 assert_eq!(e.search_prompt().unwrap().text, "");
7006 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7007 assert_eq!(e.search_prompt().unwrap().text, "beta");
7008 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7009 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7010 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7012 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7013 }
7014
7015 #[test]
7016 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7017 let mut e = editor_with("a b c");
7018 run_keys(&mut e, "/a<CR>");
7019 run_keys(&mut e, "/b<CR>");
7020 run_keys(&mut e, "/c<CR>");
7021 run_keys(&mut e, "/");
7022 for _ in 0..3 {
7024 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7025 }
7026 assert_eq!(e.search_prompt().unwrap().text, "a");
7027 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7028 assert_eq!(e.search_prompt().unwrap().text, "b");
7029 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7030 assert_eq!(e.search_prompt().unwrap().text, "c");
7031 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7033 assert_eq!(e.search_prompt().unwrap().text, "c");
7034 }
7035
7036 #[test]
7037 fn typing_after_history_walk_resets_cursor() {
7038 let mut e = editor_with("foo");
7039 run_keys(&mut e, "/foo<CR>");
7040 run_keys(&mut e, "/");
7041 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7042 assert_eq!(e.search_prompt().unwrap().text, "foo");
7043 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7046 assert_eq!(e.search_prompt().unwrap().text, "foox");
7047 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7048 assert_eq!(e.search_prompt().unwrap().text, "foo");
7049 }
7050
7051 #[test]
7052 fn empty_backward_search_prompt_enter_repeats_last_search() {
7053 let mut e = editor_with("foo bar foo baz foo");
7054 run_keys(&mut e, "/foo");
7056 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7057 assert_eq!(e.cursor().1, 8);
7058 run_keys(&mut e, "?");
7059 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7060 assert_eq!(e.cursor().1, 0);
7061 assert_eq!(e.last_search(), Some("foo"));
7062 }
7063
7064 #[test]
7065 fn search_prompt_esc_cancels_but_keeps_last_search() {
7066 let mut e = editor_with("foo bar\nbaz");
7067 run_keys(&mut e, "/bar");
7068 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7069 assert!(e.search_prompt().is_none());
7070 assert_eq!(e.last_search(), Some("bar"));
7071 }
7072
7073 #[test]
7074 fn search_then_n_and_shift_n_navigate() {
7075 let mut e = editor_with("foo bar foo baz foo");
7076 run_keys(&mut e, "/foo");
7077 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7078 assert_eq!(e.cursor().1, 8);
7080 run_keys(&mut e, "n");
7081 assert_eq!(e.cursor().1, 16);
7082 run_keys(&mut e, "N");
7083 assert_eq!(e.cursor().1, 8);
7084 }
7085
7086 #[test]
7087 fn question_mark_searches_backward_on_enter() {
7088 let mut e = editor_with("foo bar foo baz");
7089 e.jump_cursor(0, 10);
7090 run_keys(&mut e, "?foo");
7091 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7092 assert_eq!(e.cursor(), (0, 8));
7094 }
7095
7096 #[test]
7099 fn big_y_yanks_to_end_of_line() {
7100 let mut e = editor_with("hello world");
7101 e.jump_cursor(0, 6);
7102 run_keys(&mut e, "Y");
7103 assert_eq!(e.last_yank.as_deref(), Some("world"));
7104 }
7105
7106 #[test]
7107 fn big_y_from_line_start_yanks_full_line() {
7108 let mut e = editor_with("hello world");
7109 run_keys(&mut e, "Y");
7110 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7111 }
7112
7113 #[test]
7114 fn gj_joins_without_inserting_space() {
7115 let mut e = editor_with("hello\n world");
7116 run_keys(&mut e, "gJ");
7117 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7119 }
7120
7121 #[test]
7122 fn gj_noop_on_last_line() {
7123 let mut e = editor_with("only");
7124 run_keys(&mut e, "gJ");
7125 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7126 }
7127
7128 #[test]
7129 fn ge_jumps_to_previous_word_end() {
7130 let mut e = editor_with("foo bar baz");
7131 e.jump_cursor(0, 5);
7132 run_keys(&mut e, "ge");
7133 assert_eq!(e.cursor(), (0, 2));
7134 }
7135
7136 #[test]
7137 fn ge_respects_word_class() {
7138 let mut e = editor_with("foo-bar baz");
7141 e.jump_cursor(0, 5);
7142 run_keys(&mut e, "ge");
7143 assert_eq!(e.cursor(), (0, 3));
7144 }
7145
7146 #[test]
7147 fn big_ge_treats_hyphens_as_part_of_word() {
7148 let mut e = editor_with("foo-bar baz");
7151 e.jump_cursor(0, 10);
7152 run_keys(&mut e, "gE");
7153 assert_eq!(e.cursor(), (0, 6));
7154 }
7155
7156 #[test]
7157 fn ge_crosses_line_boundary() {
7158 let mut e = editor_with("foo\nbar");
7159 e.jump_cursor(1, 0);
7160 run_keys(&mut e, "ge");
7161 assert_eq!(e.cursor(), (0, 2));
7162 }
7163
7164 #[test]
7165 fn dge_deletes_to_end_of_previous_word() {
7166 let mut e = editor_with("foo bar baz");
7167 e.jump_cursor(0, 8);
7168 run_keys(&mut e, "dge");
7171 assert_eq!(e.buffer().lines()[0], "foo baaz");
7172 }
7173
7174 #[test]
7175 fn ctrl_scroll_keys_do_not_panic() {
7176 let mut e = editor_with(
7179 (0..50)
7180 .map(|i| format!("line{i}"))
7181 .collect::<Vec<_>>()
7182 .join("\n")
7183 .as_str(),
7184 );
7185 run_keys(&mut e, "<C-f>");
7186 run_keys(&mut e, "<C-b>");
7187 assert!(!e.buffer().lines().is_empty());
7189 }
7190
7191 #[test]
7198 fn count_insert_with_arrow_nav_does_not_leak_rows() {
7199 let mut e = Editor::new(KeybindingMode::Vim);
7200 e.set_content("row0\nrow1\nrow2");
7201 run_keys(&mut e, "3iX<Down><Esc>");
7203 assert!(e.buffer().lines()[0].contains('X'));
7205 assert!(
7208 !e.buffer().lines()[1].contains("row0"),
7209 "row1 leaked row0 contents: {:?}",
7210 e.buffer().lines()[1]
7211 );
7212 assert_eq!(e.buffer().lines().len(), 3);
7215 }
7216
7217 fn editor_with_rows(n: usize, viewport: u16) -> Editor<'static> {
7220 let mut e = Editor::new(KeybindingMode::Vim);
7221 let body = (0..n)
7222 .map(|i| format!(" line{}", i))
7223 .collect::<Vec<_>>()
7224 .join("\n");
7225 e.set_content(&body);
7226 e.set_viewport_height(viewport);
7227 e
7228 }
7229
7230 #[test]
7231 fn ctrl_d_moves_cursor_half_page_down() {
7232 let mut e = editor_with_rows(100, 20);
7233 run_keys(&mut e, "<C-d>");
7234 assert_eq!(e.cursor().0, 10);
7235 }
7236
7237 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor<'static> {
7238 let mut e = Editor::new(KeybindingMode::Vim);
7239 e.set_content(&lines.join("\n"));
7240 e.set_viewport_height(viewport);
7241 let v = e.buffer_mut().viewport_mut();
7242 v.height = viewport;
7243 v.width = text_width;
7244 v.text_width = text_width;
7245 v.wrap = hjkl_buffer::Wrap::Char;
7246 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
7247 e
7248 }
7249
7250 #[test]
7251 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
7252 let lines = ["aaaabbbbcccc"; 10];
7256 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7257 e.jump_cursor(4, 0);
7258 e.ensure_cursor_in_scrolloff();
7259 let csr = e.buffer().cursor_screen_row().unwrap();
7260 assert!(csr <= 6, "csr={csr}");
7261 }
7262
7263 #[test]
7264 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
7265 let lines = ["aaaabbbbcccc"; 10];
7266 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7267 e.jump_cursor(7, 0);
7270 e.ensure_cursor_in_scrolloff();
7271 e.jump_cursor(2, 0);
7272 e.ensure_cursor_in_scrolloff();
7273 let csr = e.buffer().cursor_screen_row().unwrap();
7274 assert!(csr >= 5, "csr={csr}");
7276 }
7277
7278 #[test]
7279 fn scrolloff_wrap_clamps_top_at_buffer_end() {
7280 let lines = ["aaaabbbbcccc"; 5];
7281 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7282 e.jump_cursor(4, 11);
7283 e.ensure_cursor_in_scrolloff();
7284 let top = e.buffer().viewport().top_row;
7289 assert_eq!(top, 1);
7290 }
7291
7292 #[test]
7293 fn ctrl_u_moves_cursor_half_page_up() {
7294 let mut e = editor_with_rows(100, 20);
7295 e.jump_cursor(50, 0);
7296 run_keys(&mut e, "<C-u>");
7297 assert_eq!(e.cursor().0, 40);
7298 }
7299
7300 #[test]
7301 fn ctrl_f_moves_cursor_full_page_down() {
7302 let mut e = editor_with_rows(100, 20);
7303 run_keys(&mut e, "<C-f>");
7304 assert_eq!(e.cursor().0, 18);
7306 }
7307
7308 #[test]
7309 fn ctrl_b_moves_cursor_full_page_up() {
7310 let mut e = editor_with_rows(100, 20);
7311 e.jump_cursor(50, 0);
7312 run_keys(&mut e, "<C-b>");
7313 assert_eq!(e.cursor().0, 32);
7314 }
7315
7316 #[test]
7317 fn ctrl_d_lands_on_first_non_blank() {
7318 let mut e = editor_with_rows(100, 20);
7319 run_keys(&mut e, "<C-d>");
7320 assert_eq!(e.cursor().1, 2);
7322 }
7323
7324 #[test]
7325 fn ctrl_d_clamps_at_end_of_buffer() {
7326 let mut e = editor_with_rows(5, 20);
7327 run_keys(&mut e, "<C-d>");
7328 assert_eq!(e.cursor().0, 4);
7329 }
7330
7331 #[test]
7332 fn capital_h_jumps_to_viewport_top() {
7333 let mut e = editor_with_rows(100, 10);
7334 e.jump_cursor(50, 0);
7335 e.set_viewport_top(45);
7336 let top = e.buffer().viewport().top_row;
7337 run_keys(&mut e, "H");
7338 assert_eq!(e.cursor().0, top);
7339 assert_eq!(e.cursor().1, 2);
7340 }
7341
7342 #[test]
7343 fn capital_l_jumps_to_viewport_bottom() {
7344 let mut e = editor_with_rows(100, 10);
7345 e.jump_cursor(50, 0);
7346 e.set_viewport_top(45);
7347 let top = e.buffer().viewport().top_row;
7348 run_keys(&mut e, "L");
7349 assert_eq!(e.cursor().0, top + 9);
7350 }
7351
7352 #[test]
7353 fn capital_m_jumps_to_viewport_middle() {
7354 let mut e = editor_with_rows(100, 10);
7355 e.jump_cursor(50, 0);
7356 e.set_viewport_top(45);
7357 let top = e.buffer().viewport().top_row;
7358 run_keys(&mut e, "M");
7359 assert_eq!(e.cursor().0, top + 4);
7361 }
7362
7363 #[test]
7364 fn g_capital_m_lands_at_line_midpoint() {
7365 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
7367 assert_eq!(e.cursor(), (0, 6));
7369 }
7370
7371 #[test]
7372 fn g_capital_m_on_empty_line_stays_at_zero() {
7373 let mut e = editor_with("");
7374 run_keys(&mut e, "gM");
7375 assert_eq!(e.cursor(), (0, 0));
7376 }
7377
7378 #[test]
7379 fn g_capital_m_uses_current_line_only() {
7380 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
7383 run_keys(&mut e, "gM");
7384 assert_eq!(e.cursor(), (1, 6));
7385 }
7386
7387 #[test]
7388 fn capital_h_count_offsets_from_top() {
7389 let mut e = editor_with_rows(100, 10);
7390 e.jump_cursor(50, 0);
7391 e.set_viewport_top(45);
7392 let top = e.buffer().viewport().top_row;
7393 run_keys(&mut e, "3H");
7394 assert_eq!(e.cursor().0, top + 2);
7395 }
7396
7397 #[test]
7400 fn ctrl_o_returns_to_pre_g_position() {
7401 let mut e = editor_with_rows(50, 20);
7402 e.jump_cursor(5, 2);
7403 run_keys(&mut e, "G");
7404 assert_eq!(e.cursor().0, 49);
7405 run_keys(&mut e, "<C-o>");
7406 assert_eq!(e.cursor(), (5, 2));
7407 }
7408
7409 #[test]
7410 fn ctrl_i_redoes_jump_after_ctrl_o() {
7411 let mut e = editor_with_rows(50, 20);
7412 e.jump_cursor(5, 2);
7413 run_keys(&mut e, "G");
7414 let post = e.cursor();
7415 run_keys(&mut e, "<C-o>");
7416 run_keys(&mut e, "<C-i>");
7417 assert_eq!(e.cursor(), post);
7418 }
7419
7420 #[test]
7421 fn new_jump_clears_forward_stack() {
7422 let mut e = editor_with_rows(50, 20);
7423 e.jump_cursor(5, 2);
7424 run_keys(&mut e, "G");
7425 run_keys(&mut e, "<C-o>");
7426 run_keys(&mut e, "gg");
7427 run_keys(&mut e, "<C-i>");
7428 assert_eq!(e.cursor().0, 0);
7429 }
7430
7431 #[test]
7432 fn ctrl_o_on_empty_stack_is_noop() {
7433 let mut e = editor_with_rows(10, 20);
7434 e.jump_cursor(3, 1);
7435 run_keys(&mut e, "<C-o>");
7436 assert_eq!(e.cursor(), (3, 1));
7437 }
7438
7439 #[test]
7440 fn asterisk_search_pushes_jump() {
7441 let mut e = editor_with("foo bar\nbaz foo end");
7442 e.jump_cursor(0, 0);
7443 run_keys(&mut e, "*");
7444 let after = e.cursor();
7445 assert_ne!(after, (0, 0));
7446 run_keys(&mut e, "<C-o>");
7447 assert_eq!(e.cursor(), (0, 0));
7448 }
7449
7450 #[test]
7451 fn h_viewport_jump_is_recorded() {
7452 let mut e = editor_with_rows(100, 10);
7453 e.jump_cursor(50, 0);
7454 e.set_viewport_top(45);
7455 let pre = e.cursor();
7456 run_keys(&mut e, "H");
7457 assert_ne!(e.cursor(), pre);
7458 run_keys(&mut e, "<C-o>");
7459 assert_eq!(e.cursor(), pre);
7460 }
7461
7462 #[test]
7463 fn j_k_motion_does_not_push_jump() {
7464 let mut e = editor_with_rows(50, 20);
7465 e.jump_cursor(5, 0);
7466 run_keys(&mut e, "jjj");
7467 run_keys(&mut e, "<C-o>");
7468 assert_eq!(e.cursor().0, 8);
7469 }
7470
7471 #[test]
7472 fn jumplist_caps_at_100() {
7473 let mut e = editor_with_rows(200, 20);
7474 for i in 0..101 {
7475 e.jump_cursor(i, 0);
7476 run_keys(&mut e, "G");
7477 }
7478 assert!(e.vim.jump_back.len() <= 100);
7479 }
7480
7481 #[test]
7482 fn tab_acts_as_ctrl_i() {
7483 let mut e = editor_with_rows(50, 20);
7484 e.jump_cursor(5, 2);
7485 run_keys(&mut e, "G");
7486 let post = e.cursor();
7487 run_keys(&mut e, "<C-o>");
7488 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
7489 assert_eq!(e.cursor(), post);
7490 }
7491
7492 #[test]
7495 fn ma_then_backtick_a_jumps_exact() {
7496 let mut e = editor_with_rows(50, 20);
7497 e.jump_cursor(5, 3);
7498 run_keys(&mut e, "ma");
7499 e.jump_cursor(20, 0);
7500 run_keys(&mut e, "`a");
7501 assert_eq!(e.cursor(), (5, 3));
7502 }
7503
7504 #[test]
7505 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
7506 let mut e = editor_with_rows(50, 20);
7507 e.jump_cursor(5, 6);
7509 run_keys(&mut e, "ma");
7510 e.jump_cursor(30, 4);
7511 run_keys(&mut e, "'a");
7512 assert_eq!(e.cursor(), (5, 2));
7513 }
7514
7515 #[test]
7516 fn goto_mark_pushes_jumplist() {
7517 let mut e = editor_with_rows(50, 20);
7518 e.jump_cursor(10, 2);
7519 run_keys(&mut e, "mz");
7520 e.jump_cursor(3, 0);
7521 run_keys(&mut e, "`z");
7522 assert_eq!(e.cursor(), (10, 2));
7523 run_keys(&mut e, "<C-o>");
7524 assert_eq!(e.cursor(), (3, 0));
7525 }
7526
7527 #[test]
7528 fn goto_missing_mark_is_noop() {
7529 let mut e = editor_with_rows(50, 20);
7530 e.jump_cursor(3, 1);
7531 run_keys(&mut e, "`q");
7532 assert_eq!(e.cursor(), (3, 1));
7533 }
7534
7535 #[test]
7536 fn uppercase_mark_letter_ignored() {
7537 let mut e = editor_with_rows(50, 20);
7538 e.jump_cursor(5, 3);
7539 run_keys(&mut e, "mA");
7540 assert!(e.vim.marks.is_empty());
7543 }
7544
7545 #[test]
7546 fn mark_survives_document_shrink_via_clamp() {
7547 let mut e = editor_with_rows(50, 20);
7548 e.jump_cursor(40, 4);
7549 run_keys(&mut e, "mx");
7550 e.set_content("a\nb\nc\nd\ne");
7552 run_keys(&mut e, "`x");
7553 let (r, _) = e.cursor();
7555 assert!(r <= 4);
7556 }
7557
7558 #[test]
7559 fn g_semicolon_walks_back_through_edits() {
7560 let mut e = editor_with("alpha\nbeta\ngamma");
7561 e.jump_cursor(0, 0);
7564 run_keys(&mut e, "iX<Esc>");
7565 e.jump_cursor(2, 0);
7566 run_keys(&mut e, "iY<Esc>");
7567 run_keys(&mut e, "g;");
7569 assert_eq!(e.cursor(), (2, 1));
7570 run_keys(&mut e, "g;");
7572 assert_eq!(e.cursor(), (0, 1));
7573 run_keys(&mut e, "g;");
7575 assert_eq!(e.cursor(), (0, 1));
7576 }
7577
7578 #[test]
7579 fn g_comma_walks_forward_after_g_semicolon() {
7580 let mut e = editor_with("a\nb\nc");
7581 e.jump_cursor(0, 0);
7582 run_keys(&mut e, "iX<Esc>");
7583 e.jump_cursor(2, 0);
7584 run_keys(&mut e, "iY<Esc>");
7585 run_keys(&mut e, "g;");
7586 run_keys(&mut e, "g;");
7587 assert_eq!(e.cursor(), (0, 1));
7588 run_keys(&mut e, "g,");
7589 assert_eq!(e.cursor(), (2, 1));
7590 }
7591
7592 #[test]
7593 fn new_edit_during_walk_trims_forward_entries() {
7594 let mut e = editor_with("a\nb\nc\nd");
7595 e.jump_cursor(0, 0);
7596 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
7598 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
7601 run_keys(&mut e, "g;");
7602 assert_eq!(e.cursor(), (0, 1));
7603 run_keys(&mut e, "iZ<Esc>");
7605 run_keys(&mut e, "g,");
7607 assert_ne!(e.cursor(), (2, 1));
7609 }
7610
7611 #[test]
7617 fn capital_mark_set_and_jump() {
7618 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
7619 e.jump_cursor(2, 1);
7620 run_keys(&mut e, "mA");
7621 e.jump_cursor(0, 0);
7623 run_keys(&mut e, "'A");
7625 assert_eq!(e.cursor().0, 2);
7627 }
7628
7629 #[test]
7630 fn capital_mark_survives_set_content() {
7631 let mut e = editor_with("first buffer line\nsecond");
7632 e.jump_cursor(1, 3);
7633 run_keys(&mut e, "mA");
7634 e.set_content("totally different content\non many\nrows of text");
7636 e.jump_cursor(0, 0);
7638 run_keys(&mut e, "'A");
7639 assert_eq!(e.cursor().0, 1);
7640 }
7641
7642 #[test]
7647 fn capital_mark_shifts_with_edit() {
7648 let mut e = editor_with("a\nb\nc\nd");
7649 e.jump_cursor(3, 0);
7650 run_keys(&mut e, "mA");
7651 e.jump_cursor(0, 0);
7653 run_keys(&mut e, "dd");
7654 e.jump_cursor(0, 0);
7655 run_keys(&mut e, "'A");
7656 assert_eq!(e.cursor().0, 2);
7657 }
7658
7659 #[test]
7660 fn mark_below_delete_shifts_up() {
7661 let mut e = editor_with("a\nb\nc\nd\ne");
7662 e.jump_cursor(3, 0);
7664 run_keys(&mut e, "ma");
7665 e.jump_cursor(0, 0);
7667 run_keys(&mut e, "dd");
7668 e.jump_cursor(0, 0);
7670 run_keys(&mut e, "'a");
7671 assert_eq!(e.cursor().0, 2);
7672 assert_eq!(e.buffer().line(2).unwrap(), "d");
7673 }
7674
7675 #[test]
7676 fn mark_on_deleted_row_is_dropped() {
7677 let mut e = editor_with("a\nb\nc\nd");
7678 e.jump_cursor(1, 0);
7680 run_keys(&mut e, "ma");
7681 run_keys(&mut e, "dd");
7683 e.jump_cursor(2, 0);
7685 run_keys(&mut e, "'a");
7686 assert_eq!(e.cursor().0, 2);
7688 }
7689
7690 #[test]
7691 fn mark_above_edit_unchanged() {
7692 let mut e = editor_with("a\nb\nc\nd\ne");
7693 e.jump_cursor(0, 0);
7695 run_keys(&mut e, "ma");
7696 e.jump_cursor(3, 0);
7698 run_keys(&mut e, "dd");
7699 e.jump_cursor(2, 0);
7701 run_keys(&mut e, "'a");
7702 assert_eq!(e.cursor().0, 0);
7703 }
7704
7705 #[test]
7706 fn mark_shifts_down_after_insert() {
7707 let mut e = editor_with("a\nb\nc");
7708 e.jump_cursor(2, 0);
7710 run_keys(&mut e, "ma");
7711 e.jump_cursor(0, 0);
7713 run_keys(&mut e, "Onew<Esc>");
7714 e.jump_cursor(0, 0);
7717 run_keys(&mut e, "'a");
7718 assert_eq!(e.cursor().0, 3);
7719 assert_eq!(e.buffer().line(3).unwrap(), "c");
7720 }
7721
7722 #[test]
7725 fn forward_search_commit_pushes_jump() {
7726 let mut e = editor_with("alpha beta\nfoo target end\nmore");
7727 e.jump_cursor(0, 0);
7728 run_keys(&mut e, "/target<CR>");
7729 assert_ne!(e.cursor(), (0, 0));
7731 run_keys(&mut e, "<C-o>");
7733 assert_eq!(e.cursor(), (0, 0));
7734 }
7735
7736 #[test]
7737 fn search_commit_no_match_does_not_push_jump() {
7738 let mut e = editor_with("alpha beta\nfoo end");
7739 e.jump_cursor(0, 3);
7740 let pre_len = e.vim.jump_back.len();
7741 run_keys(&mut e, "/zzznotfound<CR>");
7742 assert_eq!(e.vim.jump_back.len(), pre_len);
7744 }
7745
7746 #[test]
7749 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
7750 let mut e = editor_with("hello world");
7751 run_keys(&mut e, "lll");
7752 let (row, col) = e.cursor();
7753 assert_eq!(e.buffer.cursor().row, row);
7754 assert_eq!(e.buffer.cursor().col, col);
7755 }
7756
7757 #[test]
7758 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
7759 let mut e = editor_with("aaaa\nbbbb\ncccc");
7760 run_keys(&mut e, "jj");
7761 let (row, col) = e.cursor();
7762 assert_eq!(e.buffer.cursor().row, row);
7763 assert_eq!(e.buffer.cursor().col, col);
7764 }
7765
7766 #[test]
7767 fn buffer_cursor_mirrors_textarea_after_word_motion() {
7768 let mut e = editor_with("foo bar baz");
7769 run_keys(&mut e, "ww");
7770 let (row, col) = e.cursor();
7771 assert_eq!(e.buffer.cursor().row, row);
7772 assert_eq!(e.buffer.cursor().col, col);
7773 }
7774
7775 #[test]
7776 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
7777 let mut e = editor_with("a\nb\nc\nd\ne");
7778 run_keys(&mut e, "G");
7779 let (row, col) = e.cursor();
7780 assert_eq!(e.buffer.cursor().row, row);
7781 assert_eq!(e.buffer.cursor().col, col);
7782 }
7783
7784 #[test]
7785 fn buffer_sticky_col_mirrors_vim_state() {
7786 let mut e = editor_with("longline\nhi\nlongline");
7787 run_keys(&mut e, "fl");
7788 run_keys(&mut e, "j");
7789 assert_eq!(e.buffer.sticky_col(), e.vim.sticky_col);
7791 }
7792
7793 #[test]
7794 fn buffer_content_mirrors_textarea_after_insert() {
7795 let mut e = editor_with("hello");
7796 run_keys(&mut e, "iXYZ<Esc>");
7797 let text = e.buffer().lines().join("\n");
7798 assert_eq!(e.buffer.as_string(), text);
7799 }
7800
7801 #[test]
7802 fn buffer_content_mirrors_textarea_after_delete() {
7803 let mut e = editor_with("alpha bravo charlie");
7804 run_keys(&mut e, "dw");
7805 let text = e.buffer().lines().join("\n");
7806 assert_eq!(e.buffer.as_string(), text);
7807 }
7808
7809 #[test]
7810 fn buffer_content_mirrors_textarea_after_dd() {
7811 let mut e = editor_with("a\nb\nc\nd");
7812 run_keys(&mut e, "jdd");
7813 let text = e.buffer().lines().join("\n");
7814 assert_eq!(e.buffer.as_string(), text);
7815 }
7816
7817 #[test]
7818 fn buffer_content_mirrors_textarea_after_open_line() {
7819 let mut e = editor_with("foo\nbar");
7820 run_keys(&mut e, "oNEW<Esc>");
7821 let text = e.buffer().lines().join("\n");
7822 assert_eq!(e.buffer.as_string(), text);
7823 }
7824
7825 #[test]
7826 fn buffer_content_mirrors_textarea_after_paste() {
7827 let mut e = editor_with("hello");
7828 run_keys(&mut e, "yy");
7829 run_keys(&mut e, "p");
7830 let text = e.buffer().lines().join("\n");
7831 assert_eq!(e.buffer.as_string(), text);
7832 }
7833
7834 #[test]
7835 fn buffer_selection_none_in_normal_mode() {
7836 let e = editor_with("foo bar");
7837 assert!(e.buffer_selection().is_none());
7838 }
7839
7840 #[test]
7841 fn buffer_selection_char_in_visual_mode() {
7842 use hjkl_buffer::{Position, Selection};
7843 let mut e = editor_with("hello world");
7844 run_keys(&mut e, "vlll");
7845 assert_eq!(
7846 e.buffer_selection(),
7847 Some(Selection::Char {
7848 anchor: Position::new(0, 0),
7849 head: Position::new(0, 3),
7850 })
7851 );
7852 }
7853
7854 #[test]
7855 fn buffer_selection_line_in_visual_line_mode() {
7856 use hjkl_buffer::Selection;
7857 let mut e = editor_with("a\nb\nc\nd");
7858 run_keys(&mut e, "Vj");
7859 assert_eq!(
7860 e.buffer_selection(),
7861 Some(Selection::Line {
7862 anchor_row: 0,
7863 head_row: 1,
7864 })
7865 );
7866 }
7867
7868 #[test]
7869 fn intern_style_dedups_repeated_styles() {
7870 use ratatui::style::{Color, Style};
7871 let mut e = editor_with("");
7872 let red = Style::default().fg(Color::Red);
7873 let blue = Style::default().fg(Color::Blue);
7874 let id_r1 = e.intern_style(red);
7875 let id_r2 = e.intern_style(red);
7876 let id_b = e.intern_style(blue);
7877 assert_eq!(id_r1, id_r2);
7878 assert_ne!(id_r1, id_b);
7879 assert_eq!(e.style_table().len(), 2);
7880 }
7881
7882 #[test]
7883 fn install_syntax_spans_translates_styled_spans() {
7884 use ratatui::style::{Color, Style};
7885 let mut e = editor_with("SELECT foo");
7886 e.install_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
7887 let by_row = e.buffer.spans();
7888 assert_eq!(by_row.len(), 1);
7889 assert_eq!(by_row[0].len(), 1);
7890 assert_eq!(by_row[0][0].start_byte, 0);
7891 assert_eq!(by_row[0][0].end_byte, 6);
7892 let id = by_row[0][0].style;
7893 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
7894 }
7895
7896 #[test]
7897 fn install_syntax_spans_clamps_sentinel_end() {
7898 use ratatui::style::{Color, Style};
7899 let mut e = editor_with("hello");
7900 e.install_syntax_spans(vec![vec![(
7901 0,
7902 usize::MAX,
7903 Style::default().fg(Color::Blue),
7904 )]]);
7905 let by_row = e.buffer.spans();
7906 assert_eq!(by_row[0][0].end_byte, 5);
7907 }
7908
7909 #[test]
7910 fn install_syntax_spans_drops_zero_width() {
7911 use ratatui::style::{Color, Style};
7912 let mut e = editor_with("abc");
7913 e.install_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
7914 assert!(e.buffer.spans()[0].is_empty());
7915 }
7916
7917 #[test]
7918 fn named_register_yank_into_a_then_paste_from_a() {
7919 let mut e = editor_with("hello world\nsecond");
7920 run_keys(&mut e, "\"ayw");
7921 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
7923 run_keys(&mut e, "j0\"aP");
7925 assert_eq!(e.buffer().lines()[1], "hello second");
7926 }
7927
7928 #[test]
7929 fn capital_r_overstrikes_chars() {
7930 let mut e = editor_with("hello");
7931 e.jump_cursor(0, 0);
7932 run_keys(&mut e, "RXY<Esc>");
7933 assert_eq!(e.buffer().lines()[0], "XYllo");
7935 }
7936
7937 #[test]
7938 fn capital_r_at_eol_appends() {
7939 let mut e = editor_with("hi");
7940 e.jump_cursor(0, 1);
7941 run_keys(&mut e, "RXYZ<Esc>");
7943 assert_eq!(e.buffer().lines()[0], "hXYZ");
7944 }
7945
7946 #[test]
7947 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
7948 let mut e = editor_with("abc");
7952 e.jump_cursor(0, 0);
7953 run_keys(&mut e, "RX<Esc>");
7954 assert_eq!(e.buffer().lines()[0], "Xbc");
7955 }
7956
7957 #[test]
7958 fn ctrl_r_in_insert_pastes_named_register() {
7959 let mut e = editor_with("hello world");
7960 run_keys(&mut e, "\"ayw");
7962 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
7963 run_keys(&mut e, "o");
7965 assert_eq!(e.vim_mode(), VimMode::Insert);
7966 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
7967 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
7968 assert_eq!(e.buffer().lines()[1], "hello ");
7969 assert_eq!(e.cursor(), (1, 6));
7971 assert_eq!(e.vim_mode(), VimMode::Insert);
7973 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
7974 assert_eq!(e.buffer().lines()[1], "hello X");
7975 }
7976
7977 #[test]
7978 fn ctrl_r_with_unnamed_register() {
7979 let mut e = editor_with("foo");
7980 run_keys(&mut e, "yiw");
7981 run_keys(&mut e, "A ");
7982 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
7984 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
7985 assert_eq!(e.buffer().lines()[0], "foo foo");
7986 }
7987
7988 #[test]
7989 fn ctrl_r_unknown_selector_is_no_op() {
7990 let mut e = editor_with("abc");
7991 run_keys(&mut e, "A");
7992 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
7993 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
7996 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
7997 assert_eq!(e.buffer().lines()[0], "abcZ");
7998 }
7999
8000 #[test]
8001 fn ctrl_r_multiline_register_pastes_with_newlines() {
8002 let mut e = editor_with("alpha\nbeta\ngamma");
8003 run_keys(&mut e, "\"byy");
8005 run_keys(&mut e, "j\"byy");
8006 run_keys(&mut e, "ggVj\"by");
8010 let payload = e.registers().read('b').unwrap().text.clone();
8011 assert!(payload.contains('\n'));
8012 run_keys(&mut e, "Go");
8013 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8014 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
8015 let total_lines = e.buffer().lines().len();
8018 assert!(total_lines >= 5);
8019 }
8020
8021 #[test]
8022 fn yank_zero_holds_last_yank_after_delete() {
8023 let mut e = editor_with("hello world");
8024 run_keys(&mut e, "yw");
8025 let yanked = e.registers().read('0').unwrap().text.clone();
8026 assert!(!yanked.is_empty());
8027 run_keys(&mut e, "dw");
8029 assert_eq!(e.registers().read('0').unwrap().text, yanked);
8030 assert!(!e.registers().read('1').unwrap().text.is_empty());
8032 }
8033
8034 #[test]
8035 fn delete_ring_rotates_through_one_through_nine() {
8036 let mut e = editor_with("a b c d e f g h i j");
8037 for _ in 0..3 {
8039 run_keys(&mut e, "dw");
8040 }
8041 let r1 = e.registers().read('1').unwrap().text.clone();
8043 let r2 = e.registers().read('2').unwrap().text.clone();
8044 let r3 = e.registers().read('3').unwrap().text.clone();
8045 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
8046 assert_ne!(r1, r2);
8047 assert_ne!(r2, r3);
8048 }
8049
8050 #[test]
8051 fn capital_register_appends_to_lowercase() {
8052 let mut e = editor_with("foo bar");
8053 run_keys(&mut e, "\"ayw");
8054 let first = e.registers().read('a').unwrap().text.clone();
8055 assert!(first.contains("foo"));
8056 run_keys(&mut e, "w\"Ayw");
8058 let combined = e.registers().read('a').unwrap().text.clone();
8059 assert!(combined.starts_with(&first));
8060 assert!(combined.contains("bar"));
8061 }
8062
8063 #[test]
8064 fn zf_in_visual_line_creates_closed_fold() {
8065 let mut e = editor_with("a\nb\nc\nd\ne");
8066 e.jump_cursor(1, 0);
8068 run_keys(&mut e, "Vjjzf");
8069 assert_eq!(e.buffer().folds().len(), 1);
8070 let f = e.buffer().folds()[0];
8071 assert_eq!(f.start_row, 1);
8072 assert_eq!(f.end_row, 3);
8073 assert!(f.closed);
8074 }
8075
8076 #[test]
8077 fn zfj_in_normal_creates_two_row_fold() {
8078 let mut e = editor_with("a\nb\nc\nd\ne");
8079 e.jump_cursor(1, 0);
8080 run_keys(&mut e, "zfj");
8081 assert_eq!(e.buffer().folds().len(), 1);
8082 let f = e.buffer().folds()[0];
8083 assert_eq!(f.start_row, 1);
8084 assert_eq!(f.end_row, 2);
8085 assert!(f.closed);
8086 assert_eq!(e.cursor().0, 1);
8088 }
8089
8090 #[test]
8091 fn zf_with_count_folds_count_rows() {
8092 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8093 e.jump_cursor(0, 0);
8094 run_keys(&mut e, "zf3j");
8096 assert_eq!(e.buffer().folds().len(), 1);
8097 let f = e.buffer().folds()[0];
8098 assert_eq!(f.start_row, 0);
8099 assert_eq!(f.end_row, 3);
8100 }
8101
8102 #[test]
8103 fn zfk_folds_upward_range() {
8104 let mut e = editor_with("a\nb\nc\nd\ne");
8105 e.jump_cursor(3, 0);
8106 run_keys(&mut e, "zfk");
8107 let f = e.buffer().folds()[0];
8108 assert_eq!(f.start_row, 2);
8110 assert_eq!(f.end_row, 3);
8111 }
8112
8113 #[test]
8114 fn zf_capital_g_folds_to_bottom() {
8115 let mut e = editor_with("a\nb\nc\nd\ne");
8116 e.jump_cursor(1, 0);
8117 run_keys(&mut e, "zfG");
8119 let f = e.buffer().folds()[0];
8120 assert_eq!(f.start_row, 1);
8121 assert_eq!(f.end_row, 4);
8122 }
8123
8124 #[test]
8125 fn zfgg_folds_to_top_via_operator_pipeline() {
8126 let mut e = editor_with("a\nb\nc\nd\ne");
8127 e.jump_cursor(3, 0);
8128 run_keys(&mut e, "zfgg");
8132 let f = e.buffer().folds()[0];
8133 assert_eq!(f.start_row, 0);
8134 assert_eq!(f.end_row, 3);
8135 }
8136
8137 #[test]
8138 fn zfip_folds_paragraph_via_text_object() {
8139 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
8140 e.jump_cursor(1, 0);
8141 run_keys(&mut e, "zfip");
8143 assert_eq!(e.buffer().folds().len(), 1);
8144 let f = e.buffer().folds()[0];
8145 assert_eq!(f.start_row, 0);
8146 assert_eq!(f.end_row, 2);
8147 }
8148
8149 #[test]
8150 fn zfap_folds_paragraph_with_trailing_blank() {
8151 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
8152 e.jump_cursor(0, 0);
8153 run_keys(&mut e, "zfap");
8155 let f = e.buffer().folds()[0];
8156 assert_eq!(f.start_row, 0);
8157 assert_eq!(f.end_row, 3);
8158 }
8159
8160 #[test]
8161 fn zf_paragraph_motion_folds_to_blank() {
8162 let mut e = editor_with("alpha\nbeta\n\ngamma");
8163 e.jump_cursor(0, 0);
8164 run_keys(&mut e, "zf}");
8166 let f = e.buffer().folds()[0];
8167 assert_eq!(f.start_row, 0);
8168 assert_eq!(f.end_row, 2);
8169 }
8170
8171 #[test]
8172 fn za_toggles_fold_under_cursor() {
8173 let mut e = editor_with("a\nb\nc\nd");
8174 e.buffer_mut().add_fold(1, 2, true);
8175 e.jump_cursor(1, 0);
8176 run_keys(&mut e, "za");
8177 assert!(!e.buffer().folds()[0].closed);
8178 run_keys(&mut e, "za");
8179 assert!(e.buffer().folds()[0].closed);
8180 }
8181
8182 #[test]
8183 fn zr_opens_all_folds_zm_closes_all() {
8184 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8185 e.buffer_mut().add_fold(0, 1, true);
8186 e.buffer_mut().add_fold(2, 3, true);
8187 e.buffer_mut().add_fold(4, 5, true);
8188 run_keys(&mut e, "zR");
8189 assert!(e.buffer().folds().iter().all(|f| !f.closed));
8190 run_keys(&mut e, "zM");
8191 assert!(e.buffer().folds().iter().all(|f| f.closed));
8192 }
8193
8194 #[test]
8195 fn ze_clears_all_folds() {
8196 let mut e = editor_with("a\nb\nc\nd");
8197 e.buffer_mut().add_fold(0, 1, true);
8198 e.buffer_mut().add_fold(2, 3, false);
8199 run_keys(&mut e, "zE");
8200 assert!(e.buffer().folds().is_empty());
8201 }
8202
8203 #[test]
8204 fn g_underscore_jumps_to_last_non_blank() {
8205 let mut e = editor_with("hello world ");
8206 run_keys(&mut e, "g_");
8207 assert_eq!(e.cursor().1, 10);
8209 }
8210
8211 #[test]
8212 fn gj_and_gk_alias_j_and_k() {
8213 let mut e = editor_with("a\nb\nc");
8214 run_keys(&mut e, "gj");
8215 assert_eq!(e.cursor().0, 1);
8216 run_keys(&mut e, "gk");
8217 assert_eq!(e.cursor().0, 0);
8218 }
8219
8220 #[test]
8221 fn paragraph_motions_walk_blank_lines() {
8222 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
8223 run_keys(&mut e, "}");
8224 assert_eq!(e.cursor().0, 2);
8225 run_keys(&mut e, "}");
8226 assert_eq!(e.cursor().0, 5);
8227 run_keys(&mut e, "{");
8228 assert_eq!(e.cursor().0, 2);
8229 }
8230
8231 #[test]
8232 fn gv_reenters_last_visual_selection() {
8233 let mut e = editor_with("alpha\nbeta\ngamma");
8234 run_keys(&mut e, "Vj");
8235 run_keys(&mut e, "<Esc>");
8237 assert_eq!(e.vim_mode(), VimMode::Normal);
8238 run_keys(&mut e, "gv");
8240 assert_eq!(e.vim_mode(), VimMode::VisualLine);
8241 }
8242
8243 #[test]
8244 fn o_in_visual_swaps_anchor_and_cursor() {
8245 let mut e = editor_with("hello world");
8246 run_keys(&mut e, "vllll");
8248 assert_eq!(e.cursor().1, 4);
8249 run_keys(&mut e, "o");
8251 assert_eq!(e.cursor().1, 0);
8252 assert_eq!(e.vim.visual_anchor, (0, 4));
8254 }
8255
8256 #[test]
8257 fn editing_inside_fold_invalidates_it() {
8258 let mut e = editor_with("a\nb\nc\nd");
8259 e.buffer_mut().add_fold(1, 2, true);
8260 e.jump_cursor(1, 0);
8261 run_keys(&mut e, "iX<Esc>");
8263 assert!(e.buffer().folds().is_empty());
8265 }
8266
8267 #[test]
8268 fn zd_removes_fold_under_cursor() {
8269 let mut e = editor_with("a\nb\nc\nd");
8270 e.buffer_mut().add_fold(1, 2, true);
8271 e.jump_cursor(2, 0);
8272 run_keys(&mut e, "zd");
8273 assert!(e.buffer().folds().is_empty());
8274 }
8275
8276 #[test]
8277 fn dot_mark_jumps_to_last_edit_position() {
8278 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8279 e.jump_cursor(2, 0);
8280 run_keys(&mut e, "iX<Esc>");
8282 let after_edit = e.cursor();
8283 run_keys(&mut e, "gg");
8285 assert_eq!(e.cursor().0, 0);
8286 run_keys(&mut e, "'.");
8288 assert_eq!(e.cursor().0, after_edit.0);
8289 }
8290
8291 #[test]
8292 fn quote_quote_returns_to_pre_jump_position() {
8293 let mut e = editor_with_rows(50, 20);
8294 e.jump_cursor(10, 2);
8295 let before = e.cursor();
8296 run_keys(&mut e, "G");
8298 assert_ne!(e.cursor(), before);
8299 run_keys(&mut e, "''");
8301 assert_eq!(e.cursor().0, before.0);
8302 }
8303
8304 #[test]
8305 fn backtick_backtick_restores_exact_pre_jump_pos() {
8306 let mut e = editor_with_rows(50, 20);
8307 e.jump_cursor(7, 3);
8308 let before = e.cursor();
8309 run_keys(&mut e, "G");
8310 run_keys(&mut e, "``");
8311 assert_eq!(e.cursor(), before);
8312 }
8313
8314 #[test]
8315 fn macro_record_and_replay_basic() {
8316 let mut e = editor_with("foo\nbar\nbaz");
8317 run_keys(&mut e, "qaIX<Esc>jq");
8319 assert_eq!(e.buffer().lines()[0], "Xfoo");
8320 run_keys(&mut e, "@a");
8322 assert_eq!(e.buffer().lines()[1], "Xbar");
8323 run_keys(&mut e, "j@@");
8325 assert_eq!(e.buffer().lines()[2], "Xbaz");
8326 }
8327
8328 #[test]
8329 fn macro_count_replays_n_times() {
8330 let mut e = editor_with("a\nb\nc\nd\ne");
8331 run_keys(&mut e, "qajq");
8333 assert_eq!(e.cursor().0, 1);
8334 run_keys(&mut e, "3@a");
8336 assert_eq!(e.cursor().0, 4);
8337 }
8338
8339 #[test]
8340 fn macro_capital_q_appends_to_lowercase_register() {
8341 let mut e = editor_with("hello");
8342 run_keys(&mut e, "qall<Esc>q");
8343 run_keys(&mut e, "qAhh<Esc>q");
8344 let text = e.registers().read('a').unwrap().text.clone();
8347 assert!(text.contains("ll<Esc>"));
8348 assert!(text.contains("hh<Esc>"));
8349 }
8350
8351 #[test]
8352 fn buffer_selection_block_in_visual_block_mode() {
8353 use hjkl_buffer::{Position, Selection};
8354 let mut e = editor_with("aaaa\nbbbb\ncccc");
8355 run_keys(&mut e, "<C-v>jl");
8356 assert_eq!(
8357 e.buffer_selection(),
8358 Some(Selection::Block {
8359 anchor: Position::new(0, 0),
8360 head: Position::new(1, 1),
8361 })
8362 );
8363 }
8364
8365 #[test]
8368 fn n_after_question_mark_keeps_walking_backward() {
8369 let mut e = editor_with("foo bar foo baz foo end");
8372 e.jump_cursor(0, 22);
8373 run_keys(&mut e, "?foo<CR>");
8374 assert_eq!(e.cursor().1, 16);
8375 run_keys(&mut e, "n");
8376 assert_eq!(e.cursor().1, 8);
8377 run_keys(&mut e, "N");
8378 assert_eq!(e.cursor().1, 16);
8379 }
8380
8381 #[test]
8382 fn nested_macro_chord_records_literal_keys() {
8383 let mut e = editor_with("alpha\nbeta\ngamma");
8386 run_keys(&mut e, "qblq");
8388 run_keys(&mut e, "qaIX<Esc>q");
8391 e.jump_cursor(1, 0);
8393 run_keys(&mut e, "@a");
8394 assert_eq!(e.buffer().lines()[1], "Xbeta");
8395 }
8396
8397 #[test]
8398 fn shift_gt_motion_indents_one_line() {
8399 let mut e = editor_with("hello world");
8403 run_keys(&mut e, ">w");
8404 assert_eq!(e.buffer().lines()[0], " hello world");
8405 }
8406
8407 #[test]
8408 fn shift_lt_motion_outdents_one_line() {
8409 let mut e = editor_with(" hello world");
8410 run_keys(&mut e, "<lt>w");
8411 assert_eq!(e.buffer().lines()[0], " hello world");
8413 }
8414
8415 #[test]
8416 fn shift_gt_text_object_indents_paragraph() {
8417 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
8418 e.jump_cursor(0, 0);
8419 run_keys(&mut e, ">ip");
8420 assert_eq!(e.buffer().lines()[0], " alpha");
8421 assert_eq!(e.buffer().lines()[1], " beta");
8422 assert_eq!(e.buffer().lines()[2], " gamma");
8423 assert_eq!(e.buffer().lines()[4], "rest");
8425 }
8426
8427 #[test]
8428 fn ctrl_o_runs_exactly_one_normal_command() {
8429 let mut e = editor_with("alpha beta gamma");
8432 e.jump_cursor(0, 0);
8433 run_keys(&mut e, "i");
8434 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
8435 run_keys(&mut e, "dw");
8436 assert_eq!(e.vim_mode(), VimMode::Insert);
8438 run_keys(&mut e, "X");
8440 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
8441 }
8442
8443 #[test]
8444 fn macro_replay_respects_mode_switching() {
8445 let mut e = editor_with("hi");
8449 run_keys(&mut e, "qaiX<Esc>0q");
8450 assert_eq!(e.vim_mode(), VimMode::Normal);
8451 e.set_content("yo");
8453 run_keys(&mut e, "@a");
8454 assert_eq!(e.vim_mode(), VimMode::Normal);
8455 assert_eq!(e.cursor().1, 0);
8456 assert_eq!(e.buffer().lines()[0], "Xyo");
8457 }
8458
8459 #[test]
8460 fn macro_recorded_text_round_trips_through_register() {
8461 let mut e = editor_with("");
8465 run_keys(&mut e, "qaiX<Esc>q");
8466 let text = e.registers().read('a').unwrap().text.clone();
8467 assert!(text.starts_with("iX"));
8468 run_keys(&mut e, "@a");
8470 assert_eq!(e.buffer().lines()[0], "XX");
8471 }
8472
8473 #[test]
8474 fn dot_after_macro_replays_macros_last_change() {
8475 let mut e = editor_with("ab\ncd\nef");
8478 run_keys(&mut e, "qaIX<Esc>jq");
8481 assert_eq!(e.buffer().lines()[0], "Xab");
8482 run_keys(&mut e, "@a");
8483 assert_eq!(e.buffer().lines()[1], "Xcd");
8484 let row_before_dot = e.cursor().0;
8487 run_keys(&mut e, ".");
8488 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
8489 }
8490}