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