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 while c >= 0 {
4353 let ch = chars[c as usize];
4354 if ch == close {
4355 depth += 1;
4356 } else if ch == open {
4357 if depth == 0 {
4358 return Some((r, c as usize));
4359 }
4360 depth -= 1;
4361 }
4362 c -= 1;
4363 }
4364 if r == 0 {
4365 return None;
4366 }
4367 r -= 1;
4368 c = lines[r].chars().count() as isize - 1;
4369 }
4370}
4371
4372fn find_close_bracket(
4373 lines: &[String],
4374 row: usize,
4375 start_col: usize,
4376 open: char,
4377 close: char,
4378) -> Option<(usize, usize)> {
4379 let mut depth: i32 = 0;
4380 let mut r = row;
4381 let mut c = start_col;
4382 loop {
4383 let cur = &lines[r];
4384 let chars: Vec<char> = cur.chars().collect();
4385 while c < chars.len() {
4386 let ch = chars[c];
4387 if ch == open {
4388 depth += 1;
4389 } else if ch == close {
4390 if depth == 0 {
4391 return Some((r, c));
4392 }
4393 depth -= 1;
4394 }
4395 c += 1;
4396 }
4397 if r + 1 >= lines.len() {
4398 return None;
4399 }
4400 r += 1;
4401 c = 0;
4402 }
4403}
4404
4405fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
4406 let (r, c) = pos;
4407 let line_len = lines[r].chars().count();
4408 if c < line_len {
4409 (r, c + 1)
4410 } else if r + 1 < lines.len() {
4411 (r + 1, 0)
4412 } else {
4413 pos
4414 }
4415}
4416
4417fn paragraph_text_object(ed: &Editor<'_>, inner: bool) -> Option<((usize, usize), (usize, usize))> {
4418 let (row, _) = ed.cursor();
4419 let lines = ed.buffer().lines();
4420 if lines.is_empty() {
4421 return None;
4422 }
4423 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
4425 if is_blank(row) {
4426 return None;
4427 }
4428 let mut top = row;
4429 while top > 0 && !is_blank(top - 1) {
4430 top -= 1;
4431 }
4432 let mut bot = row;
4433 while bot + 1 < lines.len() && !is_blank(bot + 1) {
4434 bot += 1;
4435 }
4436 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
4438 bot += 1;
4439 }
4440 let end_col = lines[bot].chars().count();
4441 Some(((top, 0), (bot, end_col)))
4442}
4443
4444fn read_vim_range(
4450 ed: &mut Editor<'_>,
4451 start: (usize, usize),
4452 end: (usize, usize),
4453 kind: MotionKind,
4454) -> String {
4455 let (top, bot) = order(start, end);
4456 ed.sync_buffer_content_from_textarea();
4457 let lines = ed.buffer().lines();
4458 match kind {
4459 MotionKind::Linewise => {
4460 let lo = top.0;
4461 let hi = bot.0.min(lines.len().saturating_sub(1));
4462 let mut text = lines[lo..=hi].join("\n");
4463 text.push('\n');
4464 text
4465 }
4466 MotionKind::Inclusive | MotionKind::Exclusive => {
4467 let inclusive = matches!(kind, MotionKind::Inclusive);
4468 let mut out = String::new();
4470 for row in top.0..=bot.0 {
4471 let line = lines.get(row).map(String::as_str).unwrap_or("");
4472 let lo = if row == top.0 { top.1 } else { 0 };
4473 let hi_unclamped = if row == bot.0 {
4474 if inclusive { bot.1 + 1 } else { bot.1 }
4475 } else {
4476 line.chars().count() + 1
4477 };
4478 let row_chars: Vec<char> = line.chars().collect();
4479 let hi = hi_unclamped.min(row_chars.len());
4480 if lo < hi {
4481 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
4482 }
4483 if row < bot.0 {
4484 out.push('\n');
4485 }
4486 }
4487 out
4488 }
4489 }
4490}
4491
4492fn cut_vim_range(
4501 ed: &mut Editor<'_>,
4502 start: (usize, usize),
4503 end: (usize, usize),
4504 kind: MotionKind,
4505) -> String {
4506 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4507 let (top, bot) = order(start, end);
4508 ed.sync_buffer_content_from_textarea();
4509 let (buf_start, buf_end, buf_kind) = match kind {
4510 MotionKind::Linewise => (
4511 Position::new(top.0, 0),
4512 Position::new(bot.0, 0),
4513 BufKind::Line,
4514 ),
4515 MotionKind::Inclusive => {
4516 let line_chars = ed
4517 .buffer()
4518 .line(bot.0)
4519 .map(|l| l.chars().count())
4520 .unwrap_or(0);
4521 let next = if bot.1 < line_chars {
4525 Position::new(bot.0, bot.1 + 1)
4526 } else if bot.0 + 1 < ed.buffer().row_count() {
4527 Position::new(bot.0 + 1, 0)
4528 } else {
4529 Position::new(bot.0, line_chars)
4530 };
4531 (Position::new(top.0, top.1), next, BufKind::Char)
4532 }
4533 MotionKind::Exclusive => (
4534 Position::new(top.0, top.1),
4535 Position::new(bot.0, bot.1),
4536 BufKind::Char,
4537 ),
4538 };
4539 let inverse = ed.mutate_edit(Edit::DeleteRange {
4540 start: buf_start,
4541 end: buf_end,
4542 kind: buf_kind,
4543 });
4544 let text = match inverse {
4545 Edit::InsertStr { text, .. } => text,
4546 _ => String::new(),
4547 };
4548 if !text.is_empty() {
4549 ed.last_yank = Some(text.clone());
4550 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
4551 }
4552 ed.push_buffer_cursor_to_textarea();
4553 text
4554}
4555
4556fn delete_to_eol(ed: &mut Editor<'_>) {
4562 use hjkl_buffer::{Edit, MotionKind, Position};
4563 ed.sync_buffer_content_from_textarea();
4564 let cursor = ed.buffer().cursor();
4565 let line_chars = ed
4566 .buffer()
4567 .line(cursor.row)
4568 .map(|l| l.chars().count())
4569 .unwrap_or(0);
4570 if cursor.col >= line_chars {
4571 return;
4572 }
4573 let inverse = ed.mutate_edit(Edit::DeleteRange {
4574 start: cursor,
4575 end: Position::new(cursor.row, line_chars),
4576 kind: MotionKind::Char,
4577 });
4578 if let Edit::InsertStr { text, .. } = inverse
4579 && !text.is_empty()
4580 {
4581 ed.last_yank = Some(text.clone());
4582 ed.vim.yank_linewise = false;
4583 ed.set_yank(text);
4584 }
4585 ed.buffer_mut().set_cursor(cursor);
4586 ed.push_buffer_cursor_to_textarea();
4587}
4588
4589fn do_char_delete(ed: &mut Editor<'_>, forward: bool, count: usize) {
4590 use hjkl_buffer::{Edit, MotionKind, Position};
4591 ed.push_undo();
4592 ed.sync_buffer_content_from_textarea();
4593 for _ in 0..count {
4594 let cursor = ed.buffer().cursor();
4595 let line_chars = ed
4596 .buffer()
4597 .line(cursor.row)
4598 .map(|l| l.chars().count())
4599 .unwrap_or(0);
4600 if forward {
4601 if cursor.col >= line_chars {
4604 continue;
4605 }
4606 ed.mutate_edit(Edit::DeleteRange {
4607 start: cursor,
4608 end: Position::new(cursor.row, cursor.col + 1),
4609 kind: MotionKind::Char,
4610 });
4611 } else {
4612 if cursor.col == 0 {
4614 continue;
4615 }
4616 ed.mutate_edit(Edit::DeleteRange {
4617 start: Position::new(cursor.row, cursor.col - 1),
4618 end: cursor,
4619 kind: MotionKind::Char,
4620 });
4621 }
4622 }
4623 ed.push_buffer_cursor_to_textarea();
4624}
4625
4626fn adjust_number(ed: &mut Editor<'_>, delta: i64) -> bool {
4630 use hjkl_buffer::{Edit, MotionKind, Position};
4631 ed.sync_buffer_content_from_textarea();
4632 let cursor = ed.buffer().cursor();
4633 let row = cursor.row;
4634 let chars: Vec<char> = match ed.buffer().line(row) {
4635 Some(l) => l.chars().collect(),
4636 None => return false,
4637 };
4638 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
4639 return false;
4640 };
4641 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
4642 digit_start - 1
4643 } else {
4644 digit_start
4645 };
4646 let mut span_end = digit_start;
4647 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
4648 span_end += 1;
4649 }
4650 let s: String = chars[span_start..span_end].iter().collect();
4651 let Ok(n) = s.parse::<i64>() else {
4652 return false;
4653 };
4654 let new_s = n.saturating_add(delta).to_string();
4655
4656 ed.push_undo();
4657 let span_start_pos = Position::new(row, span_start);
4658 let span_end_pos = Position::new(row, span_end);
4659 ed.mutate_edit(Edit::DeleteRange {
4660 start: span_start_pos,
4661 end: span_end_pos,
4662 kind: MotionKind::Char,
4663 });
4664 ed.mutate_edit(Edit::InsertStr {
4665 at: span_start_pos,
4666 text: new_s.clone(),
4667 });
4668 let new_len = new_s.chars().count();
4669 ed.buffer_mut()
4670 .set_cursor(Position::new(row, span_start + new_len.saturating_sub(1)));
4671 ed.push_buffer_cursor_to_textarea();
4672 true
4673}
4674
4675fn replace_char(ed: &mut Editor<'_>, ch: char, count: usize) {
4676 use hjkl_buffer::{Edit, MotionKind, Position};
4677 ed.push_undo();
4678 ed.sync_buffer_content_from_textarea();
4679 for _ in 0..count {
4680 let cursor = ed.buffer().cursor();
4681 let line_chars = ed
4682 .buffer()
4683 .line(cursor.row)
4684 .map(|l| l.chars().count())
4685 .unwrap_or(0);
4686 if cursor.col >= line_chars {
4687 break;
4688 }
4689 ed.mutate_edit(Edit::DeleteRange {
4690 start: cursor,
4691 end: Position::new(cursor.row, cursor.col + 1),
4692 kind: MotionKind::Char,
4693 });
4694 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
4695 }
4696 ed.buffer_mut().move_left(1);
4698 ed.push_buffer_cursor_to_textarea();
4699}
4700
4701fn toggle_case_at_cursor(ed: &mut Editor<'_>) {
4702 use hjkl_buffer::{Edit, MotionKind, Position};
4703 ed.sync_buffer_content_from_textarea();
4704 let cursor = ed.buffer().cursor();
4705 let Some(c) = ed
4706 .buffer()
4707 .line(cursor.row)
4708 .and_then(|l| l.chars().nth(cursor.col))
4709 else {
4710 return;
4711 };
4712 let toggled = if c.is_uppercase() {
4713 c.to_lowercase().next().unwrap_or(c)
4714 } else {
4715 c.to_uppercase().next().unwrap_or(c)
4716 };
4717 ed.mutate_edit(Edit::DeleteRange {
4718 start: cursor,
4719 end: Position::new(cursor.row, cursor.col + 1),
4720 kind: MotionKind::Char,
4721 });
4722 ed.mutate_edit(Edit::InsertChar {
4723 at: cursor,
4724 ch: toggled,
4725 });
4726}
4727
4728fn join_line(ed: &mut Editor<'_>) {
4729 use hjkl_buffer::{Edit, Position};
4730 ed.sync_buffer_content_from_textarea();
4731 let row = ed.buffer().cursor().row;
4732 if row + 1 >= ed.buffer().row_count() {
4733 return;
4734 }
4735 let cur_line = ed.buffer().line(row).unwrap_or("").to_string();
4736 let next_raw = ed.buffer().line(row + 1).unwrap_or("").to_string();
4737 let next_trimmed = next_raw.trim_start();
4738 let cur_chars = cur_line.chars().count();
4739 let next_chars = next_raw.chars().count();
4740 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
4743 " "
4744 } else {
4745 ""
4746 };
4747 let joined = format!("{cur_line}{separator}{next_trimmed}");
4748 ed.mutate_edit(Edit::Replace {
4749 start: Position::new(row, 0),
4750 end: Position::new(row + 1, next_chars),
4751 with: joined,
4752 });
4753 ed.buffer_mut().set_cursor(Position::new(row, cur_chars));
4757 ed.push_buffer_cursor_to_textarea();
4758}
4759
4760fn join_line_raw(ed: &mut Editor<'_>) {
4763 use hjkl_buffer::{Edit, Position};
4764 ed.sync_buffer_content_from_textarea();
4765 let row = ed.buffer().cursor().row;
4766 if row + 1 >= ed.buffer().row_count() {
4767 return;
4768 }
4769 let join_col = ed
4770 .buffer()
4771 .line(row)
4772 .map(|l| l.chars().count())
4773 .unwrap_or(0);
4774 ed.mutate_edit(Edit::JoinLines {
4775 row,
4776 count: 1,
4777 with_space: false,
4778 });
4779 ed.buffer_mut().set_cursor(Position::new(row, join_col));
4781 ed.push_buffer_cursor_to_textarea();
4782}
4783
4784fn do_paste(ed: &mut Editor<'_>, before: bool, count: usize) {
4785 use hjkl_buffer::{Edit, Position};
4786 ed.push_undo();
4787 let selector = ed.vim.pending_register.take();
4792 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
4793 Some(slot) => (slot.text.clone(), slot.linewise),
4794 None => (ed.yank().to_string(), ed.vim.yank_linewise),
4795 };
4796 for _ in 0..count {
4797 ed.sync_buffer_content_from_textarea();
4798 let yank = yank.clone();
4799 if yank.is_empty() {
4800 continue;
4801 }
4802 if linewise {
4803 let text = yank.trim_matches('\n').to_string();
4807 let row = ed.buffer().cursor().row;
4808 let target_row = if before {
4809 ed.mutate_edit(Edit::InsertStr {
4810 at: Position::new(row, 0),
4811 text: format!("{text}\n"),
4812 });
4813 row
4814 } else {
4815 let line_chars = ed
4816 .buffer()
4817 .line(row)
4818 .map(|l| l.chars().count())
4819 .unwrap_or(0);
4820 ed.mutate_edit(Edit::InsertStr {
4821 at: Position::new(row, line_chars),
4822 text: format!("\n{text}"),
4823 });
4824 row + 1
4825 };
4826 ed.buffer_mut().set_cursor(Position::new(target_row, 0));
4827 ed.buffer_mut().move_first_non_blank();
4828 ed.push_buffer_cursor_to_textarea();
4829 } else {
4830 let cursor = ed.buffer().cursor();
4834 let at = if before {
4835 cursor
4836 } else {
4837 let line_chars = ed
4838 .buffer()
4839 .line(cursor.row)
4840 .map(|l| l.chars().count())
4841 .unwrap_or(0);
4842 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
4843 };
4844 ed.mutate_edit(Edit::InsertStr {
4845 at,
4846 text: yank.clone(),
4847 });
4848 ed.buffer_mut().move_left(1);
4851 ed.push_buffer_cursor_to_textarea();
4852 }
4853 }
4854 ed.vim.sticky_col = Some(ed.buffer().cursor().col);
4856}
4857
4858pub(crate) fn do_undo(ed: &mut Editor<'_>) {
4859 if let Some((lines, cursor)) = ed.undo_stack.pop() {
4860 let current = ed.snapshot();
4861 ed.redo_stack.push(current);
4862 ed.restore(lines, cursor);
4863 }
4864 ed.vim.mode = Mode::Normal;
4865}
4866
4867pub(crate) fn do_redo(ed: &mut Editor<'_>) {
4868 if let Some((lines, cursor)) = ed.redo_stack.pop() {
4869 let current = ed.snapshot();
4870 ed.undo_stack.push(current);
4871 ed.restore(lines, cursor);
4872 }
4873 ed.vim.mode = Mode::Normal;
4874}
4875
4876fn replay_insert_and_finish(ed: &mut Editor<'_>, text: &str) {
4883 use hjkl_buffer::{Edit, Position};
4884 let cursor = ed.cursor();
4885 ed.mutate_edit(Edit::InsertStr {
4886 at: Position::new(cursor.0, cursor.1),
4887 text: text.to_string(),
4888 });
4889 if ed.vim.insert_session.take().is_some() {
4890 if ed.cursor().1 > 0 {
4891 ed.buffer_mut().move_left(1);
4892 ed.push_buffer_cursor_to_textarea();
4893 }
4894 ed.vim.mode = Mode::Normal;
4895 }
4896}
4897
4898fn replay_last_change(ed: &mut Editor<'_>, outer_count: usize) {
4899 let Some(change) = ed.vim.last_change.clone() else {
4900 return;
4901 };
4902 ed.vim.replaying = true;
4903 let scale = if outer_count > 0 { outer_count } else { 1 };
4904 match change {
4905 LastChange::OpMotion {
4906 op,
4907 motion,
4908 count,
4909 inserted,
4910 } => {
4911 let total = count.max(1) * scale;
4912 apply_op_with_motion(ed, op, &motion, total);
4913 if let Some(text) = inserted {
4914 replay_insert_and_finish(ed, &text);
4915 }
4916 }
4917 LastChange::OpTextObj {
4918 op,
4919 obj,
4920 inner,
4921 inserted,
4922 } => {
4923 apply_op_with_text_object(ed, op, obj, inner);
4924 if let Some(text) = inserted {
4925 replay_insert_and_finish(ed, &text);
4926 }
4927 }
4928 LastChange::LineOp {
4929 op,
4930 count,
4931 inserted,
4932 } => {
4933 let total = count.max(1) * scale;
4934 execute_line_op(ed, op, total);
4935 if let Some(text) = inserted {
4936 replay_insert_and_finish(ed, &text);
4937 }
4938 }
4939 LastChange::CharDel { forward, count } => {
4940 do_char_delete(ed, forward, count * scale);
4941 }
4942 LastChange::ReplaceChar { ch, count } => {
4943 replace_char(ed, ch, count * scale);
4944 }
4945 LastChange::ToggleCase { count } => {
4946 for _ in 0..count * scale {
4947 ed.push_undo();
4948 toggle_case_at_cursor(ed);
4949 }
4950 }
4951 LastChange::JoinLine { count } => {
4952 for _ in 0..count * scale {
4953 ed.push_undo();
4954 join_line(ed);
4955 }
4956 }
4957 LastChange::Paste { before, count } => {
4958 do_paste(ed, before, count * scale);
4959 }
4960 LastChange::DeleteToEol { inserted } => {
4961 use hjkl_buffer::{Edit, Position};
4962 ed.push_undo();
4963 delete_to_eol(ed);
4964 if let Some(text) = inserted {
4965 let cursor = ed.cursor();
4966 ed.mutate_edit(Edit::InsertStr {
4967 at: Position::new(cursor.0, cursor.1),
4968 text,
4969 });
4970 }
4971 }
4972 LastChange::OpenLine { above, inserted } => {
4973 use hjkl_buffer::{Edit, Position};
4974 ed.push_undo();
4975 ed.sync_buffer_content_from_textarea();
4976 let row = ed.buffer().cursor().row;
4977 if above {
4978 ed.mutate_edit(Edit::InsertStr {
4979 at: Position::new(row, 0),
4980 text: "\n".to_string(),
4981 });
4982 ed.buffer_mut().move_up(1);
4983 } else {
4984 let line_chars = ed
4985 .buffer()
4986 .line(row)
4987 .map(|l| l.chars().count())
4988 .unwrap_or(0);
4989 ed.mutate_edit(Edit::InsertStr {
4990 at: Position::new(row, line_chars),
4991 text: "\n".to_string(),
4992 });
4993 }
4994 ed.push_buffer_cursor_to_textarea();
4995 let cursor = ed.cursor();
4996 ed.mutate_edit(Edit::InsertStr {
4997 at: Position::new(cursor.0, cursor.1),
4998 text: inserted,
4999 });
5000 }
5001 LastChange::InsertAt {
5002 entry,
5003 inserted,
5004 count,
5005 } => {
5006 use hjkl_buffer::{Edit, Position};
5007 ed.push_undo();
5008 match entry {
5009 InsertEntry::I => {}
5010 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5011 InsertEntry::A => {
5012 ed.buffer_mut().move_right_to_end(1);
5013 ed.push_buffer_cursor_to_textarea();
5014 }
5015 InsertEntry::ShiftA => {
5016 ed.buffer_mut().move_line_end();
5017 ed.buffer_mut().move_right_to_end(1);
5018 ed.push_buffer_cursor_to_textarea();
5019 }
5020 }
5021 for _ in 0..count.max(1) {
5022 let cursor = ed.cursor();
5023 ed.mutate_edit(Edit::InsertStr {
5024 at: Position::new(cursor.0, cursor.1),
5025 text: inserted.clone(),
5026 });
5027 }
5028 }
5029 }
5030 ed.vim.replaying = false;
5031}
5032
5033fn extract_inserted(before: &str, after: &str) -> String {
5036 let before_chars: Vec<char> = before.chars().collect();
5037 let after_chars: Vec<char> = after.chars().collect();
5038 if after_chars.len() <= before_chars.len() {
5039 return String::new();
5040 }
5041 let prefix = before_chars
5042 .iter()
5043 .zip(after_chars.iter())
5044 .take_while(|(a, b)| a == b)
5045 .count();
5046 let max_suffix = before_chars.len() - prefix;
5047 let suffix = before_chars
5048 .iter()
5049 .rev()
5050 .zip(after_chars.iter().rev())
5051 .take(max_suffix)
5052 .take_while(|(a, b)| a == b)
5053 .count();
5054 after_chars[prefix..after_chars.len() - suffix]
5055 .iter()
5056 .collect()
5057}
5058
5059#[cfg(test)]
5062mod tests {
5063 use crate::editor::Editor;
5064 use crate::{KeybindingMode, VimMode};
5065 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5066
5067 fn run_keys(e: &mut Editor<'_>, keys: &str) {
5068 let mut iter = keys.chars().peekable();
5072 while let Some(c) = iter.next() {
5073 if c == '<' {
5074 let mut tag = String::new();
5075 for ch in iter.by_ref() {
5076 if ch == '>' {
5077 break;
5078 }
5079 tag.push(ch);
5080 }
5081 let ev = match tag.as_str() {
5082 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5083 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5084 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5085 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5086 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5087 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5088 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5089 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5090 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5094 s if s.starts_with("C-") => {
5095 let ch = s.chars().nth(2).unwrap();
5096 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5097 }
5098 _ => continue,
5099 };
5100 e.handle_key(ev);
5101 } else {
5102 let mods = if c.is_uppercase() {
5103 KeyModifiers::SHIFT
5104 } else {
5105 KeyModifiers::NONE
5106 };
5107 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5108 }
5109 }
5110 }
5111
5112 fn editor_with(content: &str) -> Editor<'static> {
5113 let mut e = Editor::new(KeybindingMode::Vim);
5114 e.set_content(content);
5115 e
5116 }
5117
5118 #[test]
5119 fn f_char_jumps_on_line() {
5120 let mut e = editor_with("hello world");
5121 run_keys(&mut e, "fw");
5122 assert_eq!(e.cursor(), (0, 6));
5123 }
5124
5125 #[test]
5126 fn cap_f_jumps_backward() {
5127 let mut e = editor_with("hello world");
5128 e.jump_cursor(0, 10);
5129 run_keys(&mut e, "Fo");
5130 assert_eq!(e.cursor().1, 7);
5131 }
5132
5133 #[test]
5134 fn t_stops_before_char() {
5135 let mut e = editor_with("hello");
5136 run_keys(&mut e, "tl");
5137 assert_eq!(e.cursor(), (0, 1));
5138 }
5139
5140 #[test]
5141 fn semicolon_repeats_find() {
5142 let mut e = editor_with("aa.bb.cc");
5143 run_keys(&mut e, "f.");
5144 assert_eq!(e.cursor().1, 2);
5145 run_keys(&mut e, ";");
5146 assert_eq!(e.cursor().1, 5);
5147 }
5148
5149 #[test]
5150 fn comma_repeats_find_reverse() {
5151 let mut e = editor_with("aa.bb.cc");
5152 run_keys(&mut e, "f.");
5153 run_keys(&mut e, ";");
5154 run_keys(&mut e, ",");
5155 assert_eq!(e.cursor().1, 2);
5156 }
5157
5158 #[test]
5159 fn di_quote_deletes_content() {
5160 let mut e = editor_with("foo \"bar\" baz");
5161 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5163 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5164 }
5165
5166 #[test]
5167 fn da_quote_deletes_with_quotes() {
5168 let mut e = editor_with("foo \"bar\" baz");
5169 e.jump_cursor(0, 6);
5170 run_keys(&mut e, "da\"");
5171 assert_eq!(e.buffer().lines()[0], "foo baz");
5172 }
5173
5174 #[test]
5175 fn ci_paren_deletes_and_inserts() {
5176 let mut e = editor_with("fn(a, b, c)");
5177 e.jump_cursor(0, 5);
5178 run_keys(&mut e, "ci(");
5179 assert_eq!(e.vim_mode(), VimMode::Insert);
5180 assert_eq!(e.buffer().lines()[0], "fn()");
5181 }
5182
5183 #[test]
5184 fn diw_deletes_inner_word() {
5185 let mut e = editor_with("hello world");
5186 e.jump_cursor(0, 2);
5187 run_keys(&mut e, "diw");
5188 assert_eq!(e.buffer().lines()[0], " world");
5189 }
5190
5191 #[test]
5192 fn daw_deletes_word_with_trailing_space() {
5193 let mut e = editor_with("hello world");
5194 run_keys(&mut e, "daw");
5195 assert_eq!(e.buffer().lines()[0], "world");
5196 }
5197
5198 #[test]
5199 fn percent_jumps_to_matching_bracket() {
5200 let mut e = editor_with("foo(bar)");
5201 e.jump_cursor(0, 3);
5202 run_keys(&mut e, "%");
5203 assert_eq!(e.cursor().1, 7);
5204 run_keys(&mut e, "%");
5205 assert_eq!(e.cursor().1, 3);
5206 }
5207
5208 #[test]
5209 fn dot_repeats_last_change() {
5210 let mut e = editor_with("aaa bbb ccc");
5211 run_keys(&mut e, "dw");
5212 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5213 run_keys(&mut e, ".");
5214 assert_eq!(e.buffer().lines()[0], "ccc");
5215 }
5216
5217 #[test]
5218 fn dot_repeats_change_operator_with_text() {
5219 let mut e = editor_with("foo foo foo");
5220 run_keys(&mut e, "cwbar<Esc>");
5221 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5222 run_keys(&mut e, "w");
5224 run_keys(&mut e, ".");
5225 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5226 }
5227
5228 #[test]
5229 fn dot_repeats_x() {
5230 let mut e = editor_with("abcdef");
5231 run_keys(&mut e, "x");
5232 run_keys(&mut e, "..");
5233 assert_eq!(e.buffer().lines()[0], "def");
5234 }
5235
5236 #[test]
5237 fn count_operator_motion_compose() {
5238 let mut e = editor_with("one two three four five");
5239 run_keys(&mut e, "d3w");
5240 assert_eq!(e.buffer().lines()[0], "four five");
5241 }
5242
5243 #[test]
5244 fn two_dd_deletes_two_lines() {
5245 let mut e = editor_with("a\nb\nc");
5246 run_keys(&mut e, "2dd");
5247 assert_eq!(e.buffer().lines().len(), 1);
5248 assert_eq!(e.buffer().lines()[0], "c");
5249 }
5250
5251 #[test]
5256 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5257 let mut e = editor_with("one\ntwo\n three\nfour");
5258 e.jump_cursor(1, 2);
5259 run_keys(&mut e, "dd");
5260 assert_eq!(e.buffer().lines()[1], " three");
5262 assert_eq!(e.cursor(), (1, 4));
5263 }
5264
5265 #[test]
5266 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5267 let mut e = editor_with("one\n two\nthree");
5268 e.jump_cursor(2, 0);
5269 run_keys(&mut e, "dd");
5270 assert_eq!(e.buffer().lines().len(), 2);
5272 assert_eq!(e.cursor(), (1, 2));
5273 }
5274
5275 #[test]
5276 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5277 let mut e = editor_with("lonely");
5278 run_keys(&mut e, "dd");
5279 assert_eq!(e.buffer().lines().len(), 1);
5280 assert_eq!(e.buffer().lines()[0], "");
5281 assert_eq!(e.cursor(), (0, 0));
5282 }
5283
5284 #[test]
5285 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5286 let mut e = editor_with("a\nb\nc\n d\ne");
5287 e.jump_cursor(1, 0);
5289 run_keys(&mut e, "3dd");
5290 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
5291 assert_eq!(e.cursor(), (1, 0));
5292 }
5293
5294 #[test]
5295 fn gu_lowercases_motion_range() {
5296 let mut e = editor_with("HELLO WORLD");
5297 run_keys(&mut e, "guw");
5298 assert_eq!(e.buffer().lines()[0], "hello WORLD");
5299 assert_eq!(e.cursor(), (0, 0));
5300 }
5301
5302 #[test]
5303 fn g_u_uppercases_text_object() {
5304 let mut e = editor_with("hello world");
5305 run_keys(&mut e, "gUiw");
5307 assert_eq!(e.buffer().lines()[0], "HELLO world");
5308 assert_eq!(e.cursor(), (0, 0));
5309 }
5310
5311 #[test]
5312 fn g_tilde_toggles_case_of_range() {
5313 let mut e = editor_with("Hello World");
5314 run_keys(&mut e, "g~iw");
5315 assert_eq!(e.buffer().lines()[0], "hELLO World");
5316 }
5317
5318 #[test]
5319 fn g_uu_uppercases_current_line() {
5320 let mut e = editor_with("select 1\nselect 2");
5321 run_keys(&mut e, "gUU");
5322 assert_eq!(e.buffer().lines()[0], "SELECT 1");
5323 assert_eq!(e.buffer().lines()[1], "select 2");
5324 }
5325
5326 #[test]
5327 fn gugu_lowercases_current_line() {
5328 let mut e = editor_with("FOO BAR\nBAZ");
5329 run_keys(&mut e, "gugu");
5330 assert_eq!(e.buffer().lines()[0], "foo bar");
5331 }
5332
5333 #[test]
5334 fn visual_u_uppercases_selection() {
5335 let mut e = editor_with("hello world");
5336 run_keys(&mut e, "veU");
5338 assert_eq!(e.buffer().lines()[0], "HELLO world");
5339 }
5340
5341 #[test]
5342 fn visual_line_u_lowercases_line() {
5343 let mut e = editor_with("HELLO WORLD\nOTHER");
5344 run_keys(&mut e, "Vu");
5345 assert_eq!(e.buffer().lines()[0], "hello world");
5346 assert_eq!(e.buffer().lines()[1], "OTHER");
5347 }
5348
5349 #[test]
5350 fn g_uu_with_count_uppercases_multiple_lines() {
5351 let mut e = editor_with("one\ntwo\nthree\nfour");
5352 run_keys(&mut e, "3gUU");
5354 assert_eq!(e.buffer().lines()[0], "ONE");
5355 assert_eq!(e.buffer().lines()[1], "TWO");
5356 assert_eq!(e.buffer().lines()[2], "THREE");
5357 assert_eq!(e.buffer().lines()[3], "four");
5358 }
5359
5360 #[test]
5361 fn double_gt_indents_current_line() {
5362 let mut e = editor_with("hello");
5363 run_keys(&mut e, ">>");
5364 assert_eq!(e.buffer().lines()[0], " hello");
5365 assert_eq!(e.cursor(), (0, 2));
5367 }
5368
5369 #[test]
5370 fn double_lt_outdents_current_line() {
5371 let mut e = editor_with(" hello");
5372 run_keys(&mut e, "<lt><lt>");
5373 assert_eq!(e.buffer().lines()[0], " hello");
5374 assert_eq!(e.cursor(), (0, 2));
5375 }
5376
5377 #[test]
5378 fn count_double_gt_indents_multiple_lines() {
5379 let mut e = editor_with("a\nb\nc\nd");
5380 run_keys(&mut e, "3>>");
5382 assert_eq!(e.buffer().lines()[0], " a");
5383 assert_eq!(e.buffer().lines()[1], " b");
5384 assert_eq!(e.buffer().lines()[2], " c");
5385 assert_eq!(e.buffer().lines()[3], "d");
5386 }
5387
5388 #[test]
5389 fn outdent_clips_ragged_leading_whitespace() {
5390 let mut e = editor_with(" x");
5393 run_keys(&mut e, "<lt><lt>");
5394 assert_eq!(e.buffer().lines()[0], "x");
5395 }
5396
5397 #[test]
5398 fn indent_motion_is_always_linewise() {
5399 let mut e = editor_with("foo bar");
5402 run_keys(&mut e, ">w");
5403 assert_eq!(e.buffer().lines()[0], " foo bar");
5404 }
5405
5406 #[test]
5407 fn indent_text_object_extends_over_paragraph() {
5408 let mut e = editor_with("a\nb\n\nc\nd");
5409 run_keys(&mut e, ">ap");
5411 assert_eq!(e.buffer().lines()[0], " a");
5412 assert_eq!(e.buffer().lines()[1], " b");
5413 assert_eq!(e.buffer().lines()[2], "");
5414 assert_eq!(e.buffer().lines()[3], "c");
5415 }
5416
5417 #[test]
5418 fn visual_line_indent_shifts_selected_rows() {
5419 let mut e = editor_with("x\ny\nz");
5420 run_keys(&mut e, "Vj>");
5422 assert_eq!(e.buffer().lines()[0], " x");
5423 assert_eq!(e.buffer().lines()[1], " y");
5424 assert_eq!(e.buffer().lines()[2], "z");
5425 }
5426
5427 #[test]
5428 fn outdent_empty_line_is_noop() {
5429 let mut e = editor_with("\nfoo");
5430 run_keys(&mut e, "<lt><lt>");
5431 assert_eq!(e.buffer().lines()[0], "");
5432 }
5433
5434 #[test]
5435 fn indent_skips_empty_lines() {
5436 let mut e = editor_with("");
5439 run_keys(&mut e, ">>");
5440 assert_eq!(e.buffer().lines()[0], "");
5441 }
5442
5443 #[test]
5444 fn insert_ctrl_t_indents_current_line() {
5445 let mut e = editor_with("x");
5446 run_keys(&mut e, "i<C-t>");
5448 assert_eq!(e.buffer().lines()[0], " x");
5449 assert_eq!(e.cursor(), (0, 2));
5452 }
5453
5454 #[test]
5455 fn insert_ctrl_d_outdents_current_line() {
5456 let mut e = editor_with(" x");
5457 run_keys(&mut e, "A<C-d>");
5459 assert_eq!(e.buffer().lines()[0], " x");
5460 }
5461
5462 #[test]
5463 fn h_at_col_zero_does_not_wrap_to_prev_line() {
5464 let mut e = editor_with("first\nsecond");
5465 e.jump_cursor(1, 0);
5466 run_keys(&mut e, "h");
5467 assert_eq!(e.cursor(), (1, 0));
5469 }
5470
5471 #[test]
5472 fn l_at_last_char_does_not_wrap_to_next_line() {
5473 let mut e = editor_with("ab\ncd");
5474 e.jump_cursor(0, 1);
5476 run_keys(&mut e, "l");
5477 assert_eq!(e.cursor(), (0, 1));
5479 }
5480
5481 #[test]
5482 fn count_l_clamps_at_line_end() {
5483 let mut e = editor_with("abcde");
5484 run_keys(&mut e, "20l");
5487 assert_eq!(e.cursor(), (0, 4));
5488 }
5489
5490 #[test]
5491 fn count_h_clamps_at_col_zero() {
5492 let mut e = editor_with("abcde");
5493 e.jump_cursor(0, 3);
5494 run_keys(&mut e, "20h");
5495 assert_eq!(e.cursor(), (0, 0));
5496 }
5497
5498 #[test]
5499 fn dl_on_last_char_still_deletes_it() {
5500 let mut e = editor_with("ab");
5504 e.jump_cursor(0, 1);
5505 run_keys(&mut e, "dl");
5506 assert_eq!(e.buffer().lines()[0], "a");
5507 }
5508
5509 #[test]
5510 fn case_op_preserves_yank_register() {
5511 let mut e = editor_with("target");
5512 run_keys(&mut e, "yy");
5513 let yank_before = e.yank().to_string();
5514 run_keys(&mut e, "gUU");
5516 assert_eq!(e.buffer().lines()[0], "TARGET");
5517 assert_eq!(
5518 e.yank(),
5519 yank_before,
5520 "case ops must preserve the yank buffer"
5521 );
5522 }
5523
5524 #[test]
5525 fn dap_deletes_paragraph() {
5526 let mut e = editor_with("a\nb\n\nc\nd");
5527 run_keys(&mut e, "dap");
5528 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
5529 }
5530
5531 #[test]
5532 fn dit_deletes_inner_tag_content() {
5533 let mut e = editor_with("<b>hello</b>");
5534 e.jump_cursor(0, 4);
5536 run_keys(&mut e, "dit");
5537 assert_eq!(e.buffer().lines()[0], "<b></b>");
5538 }
5539
5540 #[test]
5541 fn dat_deletes_around_tag() {
5542 let mut e = editor_with("hi <b>foo</b> bye");
5543 e.jump_cursor(0, 6);
5544 run_keys(&mut e, "dat");
5545 assert_eq!(e.buffer().lines()[0], "hi bye");
5546 }
5547
5548 #[test]
5549 fn dit_picks_innermost_tag() {
5550 let mut e = editor_with("<a><b>x</b></a>");
5551 e.jump_cursor(0, 6);
5553 run_keys(&mut e, "dit");
5554 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
5556 }
5557
5558 #[test]
5559 fn dat_innermost_tag_pair() {
5560 let mut e = editor_with("<a><b>x</b></a>");
5561 e.jump_cursor(0, 6);
5562 run_keys(&mut e, "dat");
5563 assert_eq!(e.buffer().lines()[0], "<a></a>");
5564 }
5565
5566 #[test]
5567 fn dit_outside_any_tag_no_op() {
5568 let mut e = editor_with("plain text");
5569 e.jump_cursor(0, 3);
5570 run_keys(&mut e, "dit");
5571 assert_eq!(e.buffer().lines()[0], "plain text");
5573 }
5574
5575 #[test]
5576 fn cit_changes_inner_tag_content() {
5577 let mut e = editor_with("<b>hello</b>");
5578 e.jump_cursor(0, 4);
5579 run_keys(&mut e, "citNEW<Esc>");
5580 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
5581 }
5582
5583 #[test]
5584 fn cat_changes_around_tag() {
5585 let mut e = editor_with("hi <b>foo</b> bye");
5586 e.jump_cursor(0, 6);
5587 run_keys(&mut e, "catBAR<Esc>");
5588 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
5589 }
5590
5591 #[test]
5592 fn yit_yanks_inner_tag_content() {
5593 let mut e = editor_with("<b>hello</b>");
5594 e.jump_cursor(0, 4);
5595 run_keys(&mut e, "yit");
5596 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5597 }
5598
5599 #[test]
5600 fn yat_yanks_full_tag_pair() {
5601 let mut e = editor_with("hi <b>foo</b> bye");
5602 e.jump_cursor(0, 6);
5603 run_keys(&mut e, "yat");
5604 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5605 }
5606
5607 #[test]
5608 fn vit_visually_selects_inner_tag() {
5609 let mut e = editor_with("<b>hello</b>");
5610 e.jump_cursor(0, 4);
5611 run_keys(&mut e, "vit");
5612 assert_eq!(e.vim_mode(), VimMode::Visual);
5613 run_keys(&mut e, "y");
5614 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5615 }
5616
5617 #[test]
5618 fn vat_visually_selects_around_tag() {
5619 let mut e = editor_with("x<b>foo</b>y");
5620 e.jump_cursor(0, 5);
5621 run_keys(&mut e, "vat");
5622 assert_eq!(e.vim_mode(), VimMode::Visual);
5623 run_keys(&mut e, "y");
5624 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5625 }
5626
5627 #[test]
5630 #[allow(non_snake_case)]
5631 fn diW_deletes_inner_big_word() {
5632 let mut e = editor_with("foo.bar baz");
5633 e.jump_cursor(0, 2);
5634 run_keys(&mut e, "diW");
5635 assert_eq!(e.buffer().lines()[0], " baz");
5637 }
5638
5639 #[test]
5640 #[allow(non_snake_case)]
5641 fn daW_deletes_around_big_word() {
5642 let mut e = editor_with("foo.bar baz");
5643 e.jump_cursor(0, 2);
5644 run_keys(&mut e, "daW");
5645 assert_eq!(e.buffer().lines()[0], "baz");
5646 }
5647
5648 #[test]
5649 fn di_double_quote_deletes_inside() {
5650 let mut e = editor_with("a \"hello\" b");
5651 e.jump_cursor(0, 4);
5652 run_keys(&mut e, "di\"");
5653 assert_eq!(e.buffer().lines()[0], "a \"\" b");
5654 }
5655
5656 #[test]
5657 fn da_double_quote_deletes_around() {
5658 let mut e = editor_with("a \"hello\" b");
5659 e.jump_cursor(0, 4);
5660 run_keys(&mut e, "da\"");
5661 assert_eq!(e.buffer().lines()[0], "a b");
5662 }
5663
5664 #[test]
5665 fn di_single_quote_deletes_inside() {
5666 let mut e = editor_with("x 'foo' y");
5667 e.jump_cursor(0, 4);
5668 run_keys(&mut e, "di'");
5669 assert_eq!(e.buffer().lines()[0], "x '' y");
5670 }
5671
5672 #[test]
5673 fn da_single_quote_deletes_around() {
5674 let mut e = editor_with("x 'foo' y");
5675 e.jump_cursor(0, 4);
5676 run_keys(&mut e, "da'");
5677 assert_eq!(e.buffer().lines()[0], "x y");
5678 }
5679
5680 #[test]
5681 fn di_backtick_deletes_inside() {
5682 let mut e = editor_with("p `q` r");
5683 e.jump_cursor(0, 3);
5684 run_keys(&mut e, "di`");
5685 assert_eq!(e.buffer().lines()[0], "p `` r");
5686 }
5687
5688 #[test]
5689 fn da_backtick_deletes_around() {
5690 let mut e = editor_with("p `q` r");
5691 e.jump_cursor(0, 3);
5692 run_keys(&mut e, "da`");
5693 assert_eq!(e.buffer().lines()[0], "p r");
5694 }
5695
5696 #[test]
5697 fn di_paren_deletes_inside() {
5698 let mut e = editor_with("f(arg)");
5699 e.jump_cursor(0, 3);
5700 run_keys(&mut e, "di(");
5701 assert_eq!(e.buffer().lines()[0], "f()");
5702 }
5703
5704 #[test]
5705 fn di_paren_alias_b_works() {
5706 let mut e = editor_with("f(arg)");
5707 e.jump_cursor(0, 3);
5708 run_keys(&mut e, "dib");
5709 assert_eq!(e.buffer().lines()[0], "f()");
5710 }
5711
5712 #[test]
5713 fn di_bracket_deletes_inside() {
5714 let mut e = editor_with("a[b,c]d");
5715 e.jump_cursor(0, 3);
5716 run_keys(&mut e, "di[");
5717 assert_eq!(e.buffer().lines()[0], "a[]d");
5718 }
5719
5720 #[test]
5721 fn da_bracket_deletes_around() {
5722 let mut e = editor_with("a[b,c]d");
5723 e.jump_cursor(0, 3);
5724 run_keys(&mut e, "da[");
5725 assert_eq!(e.buffer().lines()[0], "ad");
5726 }
5727
5728 #[test]
5729 fn di_brace_deletes_inside() {
5730 let mut e = editor_with("x{y}z");
5731 e.jump_cursor(0, 2);
5732 run_keys(&mut e, "di{");
5733 assert_eq!(e.buffer().lines()[0], "x{}z");
5734 }
5735
5736 #[test]
5737 fn da_brace_deletes_around() {
5738 let mut e = editor_with("x{y}z");
5739 e.jump_cursor(0, 2);
5740 run_keys(&mut e, "da{");
5741 assert_eq!(e.buffer().lines()[0], "xz");
5742 }
5743
5744 #[test]
5745 fn di_brace_alias_capital_b_works() {
5746 let mut e = editor_with("x{y}z");
5747 e.jump_cursor(0, 2);
5748 run_keys(&mut e, "diB");
5749 assert_eq!(e.buffer().lines()[0], "x{}z");
5750 }
5751
5752 #[test]
5753 fn di_angle_deletes_inside() {
5754 let mut e = editor_with("p<q>r");
5755 e.jump_cursor(0, 2);
5756 run_keys(&mut e, "di<lt>");
5758 assert_eq!(e.buffer().lines()[0], "p<>r");
5759 }
5760
5761 #[test]
5762 fn da_angle_deletes_around() {
5763 let mut e = editor_with("p<q>r");
5764 e.jump_cursor(0, 2);
5765 run_keys(&mut e, "da<lt>");
5766 assert_eq!(e.buffer().lines()[0], "pr");
5767 }
5768
5769 #[test]
5770 fn dip_deletes_inner_paragraph() {
5771 let mut e = editor_with("a\nb\nc\n\nd");
5772 e.jump_cursor(1, 0);
5773 run_keys(&mut e, "dip");
5774 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
5777 }
5778
5779 #[test]
5782 fn sentence_motion_close_paren_jumps_forward() {
5783 let mut e = editor_with("Alpha. Beta. Gamma.");
5784 e.jump_cursor(0, 0);
5785 run_keys(&mut e, ")");
5786 assert_eq!(e.cursor(), (0, 7));
5788 run_keys(&mut e, ")");
5789 assert_eq!(e.cursor(), (0, 13));
5790 }
5791
5792 #[test]
5793 fn sentence_motion_open_paren_jumps_backward() {
5794 let mut e = editor_with("Alpha. Beta. Gamma.");
5795 e.jump_cursor(0, 13);
5796 run_keys(&mut e, "(");
5797 assert_eq!(e.cursor(), (0, 7));
5800 run_keys(&mut e, "(");
5801 assert_eq!(e.cursor(), (0, 0));
5802 }
5803
5804 #[test]
5805 fn sentence_motion_count() {
5806 let mut e = editor_with("A. B. C. D.");
5807 e.jump_cursor(0, 0);
5808 run_keys(&mut e, "3)");
5809 assert_eq!(e.cursor(), (0, 9));
5811 }
5812
5813 #[test]
5814 fn dis_deletes_inner_sentence() {
5815 let mut e = editor_with("First one. Second one. Third one.");
5816 e.jump_cursor(0, 13);
5817 run_keys(&mut e, "dis");
5818 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
5820 }
5821
5822 #[test]
5823 fn das_deletes_around_sentence_with_trailing_space() {
5824 let mut e = editor_with("Alpha. Beta. Gamma.");
5825 e.jump_cursor(0, 8);
5826 run_keys(&mut e, "das");
5827 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
5830 }
5831
5832 #[test]
5833 fn dis_handles_double_terminator() {
5834 let mut e = editor_with("Wow!? Next.");
5835 e.jump_cursor(0, 1);
5836 run_keys(&mut e, "dis");
5837 assert_eq!(e.buffer().lines()[0], " Next.");
5840 }
5841
5842 #[test]
5843 fn dis_first_sentence_from_cursor_at_zero() {
5844 let mut e = editor_with("Alpha. Beta.");
5845 e.jump_cursor(0, 0);
5846 run_keys(&mut e, "dis");
5847 assert_eq!(e.buffer().lines()[0], " Beta.");
5848 }
5849
5850 #[test]
5851 fn yis_yanks_inner_sentence() {
5852 let mut e = editor_with("Hello world. Bye.");
5853 e.jump_cursor(0, 5);
5854 run_keys(&mut e, "yis");
5855 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
5856 }
5857
5858 #[test]
5859 fn vis_visually_selects_inner_sentence() {
5860 let mut e = editor_with("First. Second.");
5861 e.jump_cursor(0, 1);
5862 run_keys(&mut e, "vis");
5863 assert_eq!(e.vim_mode(), VimMode::Visual);
5864 run_keys(&mut e, "y");
5865 assert_eq!(e.registers().read('"').unwrap().text, "First.");
5866 }
5867
5868 #[test]
5869 fn ciw_changes_inner_word() {
5870 let mut e = editor_with("hello world");
5871 e.jump_cursor(0, 1);
5872 run_keys(&mut e, "ciwHEY<Esc>");
5873 assert_eq!(e.buffer().lines()[0], "HEY world");
5874 }
5875
5876 #[test]
5877 fn yiw_yanks_inner_word() {
5878 let mut e = editor_with("hello world");
5879 e.jump_cursor(0, 1);
5880 run_keys(&mut e, "yiw");
5881 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5882 }
5883
5884 #[test]
5885 fn viw_selects_inner_word() {
5886 let mut e = editor_with("hello world");
5887 e.jump_cursor(0, 2);
5888 run_keys(&mut e, "viw");
5889 assert_eq!(e.vim_mode(), VimMode::Visual);
5890 run_keys(&mut e, "y");
5891 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5892 }
5893
5894 #[test]
5895 fn ci_paren_changes_inside() {
5896 let mut e = editor_with("f(old)");
5897 e.jump_cursor(0, 3);
5898 run_keys(&mut e, "ci(NEW<Esc>");
5899 assert_eq!(e.buffer().lines()[0], "f(NEW)");
5900 }
5901
5902 #[test]
5903 fn yi_double_quote_yanks_inside() {
5904 let mut e = editor_with("say \"hi there\" then");
5905 e.jump_cursor(0, 6);
5906 run_keys(&mut e, "yi\"");
5907 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
5908 }
5909
5910 #[test]
5911 fn vap_visual_selects_around_paragraph() {
5912 let mut e = editor_with("a\nb\n\nc");
5913 e.jump_cursor(0, 0);
5914 run_keys(&mut e, "vap");
5915 assert_eq!(e.vim_mode(), VimMode::VisualLine);
5916 run_keys(&mut e, "y");
5917 let text = e.registers().read('"').unwrap().text.clone();
5919 assert!(text.starts_with("a\nb"));
5920 }
5921
5922 #[test]
5923 fn star_finds_next_occurrence() {
5924 let mut e = editor_with("foo bar foo baz");
5925 run_keys(&mut e, "*");
5926 assert_eq!(e.cursor().1, 8);
5927 }
5928
5929 #[test]
5930 fn star_skips_substring_match() {
5931 let mut e = editor_with("foo foobar baz");
5934 run_keys(&mut e, "*");
5935 assert_eq!(e.cursor().1, 0);
5936 }
5937
5938 #[test]
5939 fn g_star_matches_substring() {
5940 let mut e = editor_with("foo foobar baz");
5943 run_keys(&mut e, "g*");
5944 assert_eq!(e.cursor().1, 4);
5945 }
5946
5947 #[test]
5948 fn g_pound_matches_substring_backward() {
5949 let mut e = editor_with("foo foobar baz foo");
5952 run_keys(&mut e, "$b");
5953 assert_eq!(e.cursor().1, 15);
5954 run_keys(&mut e, "g#");
5955 assert_eq!(e.cursor().1, 4);
5956 }
5957
5958 #[test]
5959 fn n_repeats_last_search_forward() {
5960 let mut e = editor_with("foo bar foo baz foo");
5961 run_keys(&mut e, "/foo<CR>");
5964 assert_eq!(e.cursor().1, 8);
5965 run_keys(&mut e, "n");
5966 assert_eq!(e.cursor().1, 16);
5967 }
5968
5969 #[test]
5970 fn shift_n_reverses_search() {
5971 let mut e = editor_with("foo bar foo baz foo");
5972 run_keys(&mut e, "/foo<CR>");
5973 run_keys(&mut e, "n");
5974 assert_eq!(e.cursor().1, 16);
5975 run_keys(&mut e, "N");
5976 assert_eq!(e.cursor().1, 8);
5977 }
5978
5979 #[test]
5980 fn n_noop_without_pattern() {
5981 let mut e = editor_with("foo bar");
5982 run_keys(&mut e, "n");
5983 assert_eq!(e.cursor(), (0, 0));
5984 }
5985
5986 #[test]
5987 fn visual_line_preserves_cursor_column() {
5988 let mut e = editor_with("hello world\nanother one\nbye");
5991 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
5993 assert_eq!(e.vim_mode(), VimMode::VisualLine);
5994 assert_eq!(e.cursor(), (0, 5));
5995 run_keys(&mut e, "j");
5996 assert_eq!(e.cursor(), (1, 5));
5997 }
5998
5999 #[test]
6000 fn visual_line_yank_includes_trailing_newline() {
6001 let mut e = editor_with("aaa\nbbb\nccc");
6002 run_keys(&mut e, "Vjy");
6003 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6005 }
6006
6007 #[test]
6008 fn visual_line_yank_last_line_trailing_newline() {
6009 let mut e = editor_with("aaa\nbbb\nccc");
6010 run_keys(&mut e, "jj");
6012 run_keys(&mut e, "Vy");
6013 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6014 }
6015
6016 #[test]
6017 fn yy_on_last_line_has_trailing_newline() {
6018 let mut e = editor_with("aaa\nbbb\nccc");
6019 run_keys(&mut e, "jj");
6020 run_keys(&mut e, "yy");
6021 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6022 }
6023
6024 #[test]
6025 fn yy_in_middle_has_trailing_newline() {
6026 let mut e = editor_with("aaa\nbbb\nccc");
6027 run_keys(&mut e, "j");
6028 run_keys(&mut e, "yy");
6029 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6030 }
6031
6032 #[test]
6033 fn di_single_quote() {
6034 let mut e = editor_with("say 'hello world' now");
6035 e.jump_cursor(0, 7);
6036 run_keys(&mut e, "di'");
6037 assert_eq!(e.buffer().lines()[0], "say '' now");
6038 }
6039
6040 #[test]
6041 fn da_single_quote() {
6042 let mut e = editor_with("say 'hello' now");
6043 e.jump_cursor(0, 7);
6044 run_keys(&mut e, "da'");
6045 assert_eq!(e.buffer().lines()[0], "say now");
6046 }
6047
6048 #[test]
6049 fn di_backtick() {
6050 let mut e = editor_with("say `hi` now");
6051 e.jump_cursor(0, 5);
6052 run_keys(&mut e, "di`");
6053 assert_eq!(e.buffer().lines()[0], "say `` now");
6054 }
6055
6056 #[test]
6057 fn di_brace() {
6058 let mut e = editor_with("fn { a; b; c }");
6059 e.jump_cursor(0, 7);
6060 run_keys(&mut e, "di{");
6061 assert_eq!(e.buffer().lines()[0], "fn {}");
6062 }
6063
6064 #[test]
6065 fn di_bracket() {
6066 let mut e = editor_with("arr[1, 2, 3]");
6067 e.jump_cursor(0, 5);
6068 run_keys(&mut e, "di[");
6069 assert_eq!(e.buffer().lines()[0], "arr[]");
6070 }
6071
6072 #[test]
6073 fn dab_deletes_around_paren() {
6074 let mut e = editor_with("fn(a, b) + 1");
6075 e.jump_cursor(0, 4);
6076 run_keys(&mut e, "dab");
6077 assert_eq!(e.buffer().lines()[0], "fn + 1");
6078 }
6079
6080 #[test]
6081 fn da_big_b_deletes_around_brace() {
6082 let mut e = editor_with("x = {a: 1}");
6083 e.jump_cursor(0, 6);
6084 run_keys(&mut e, "daB");
6085 assert_eq!(e.buffer().lines()[0], "x = ");
6086 }
6087
6088 #[test]
6089 fn di_big_w_deletes_bigword() {
6090 let mut e = editor_with("foo-bar baz");
6091 e.jump_cursor(0, 2);
6092 run_keys(&mut e, "diW");
6093 assert_eq!(e.buffer().lines()[0], " baz");
6094 }
6095
6096 #[test]
6097 fn visual_select_inner_word() {
6098 let mut e = editor_with("hello world");
6099 e.jump_cursor(0, 2);
6100 run_keys(&mut e, "viw");
6101 assert_eq!(e.vim_mode(), VimMode::Visual);
6102 run_keys(&mut e, "y");
6103 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6104 }
6105
6106 #[test]
6107 fn visual_select_inner_quote() {
6108 let mut e = editor_with("foo \"bar\" baz");
6109 e.jump_cursor(0, 6);
6110 run_keys(&mut e, "vi\"");
6111 run_keys(&mut e, "y");
6112 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6113 }
6114
6115 #[test]
6116 fn visual_select_inner_paren() {
6117 let mut e = editor_with("fn(a, b)");
6118 e.jump_cursor(0, 4);
6119 run_keys(&mut e, "vi(");
6120 run_keys(&mut e, "y");
6121 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6122 }
6123
6124 #[test]
6125 fn visual_select_outer_brace() {
6126 let mut e = editor_with("{x}");
6127 e.jump_cursor(0, 1);
6128 run_keys(&mut e, "va{");
6129 run_keys(&mut e, "y");
6130 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6131 }
6132
6133 #[test]
6134 fn caw_changes_word_with_trailing_space() {
6135 let mut e = editor_with("hello world");
6136 run_keys(&mut e, "cawfoo<Esc>");
6137 assert_eq!(e.buffer().lines()[0], "fooworld");
6138 }
6139
6140 #[test]
6141 fn visual_char_yank_preserves_raw_text() {
6142 let mut e = editor_with("hello world");
6143 run_keys(&mut e, "vllly");
6144 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6145 }
6146
6147 #[test]
6148 fn single_line_visual_line_selects_full_line_on_yank() {
6149 let mut e = editor_with("hello world\nbye");
6150 run_keys(&mut e, "V");
6151 run_keys(&mut e, "y");
6154 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6155 }
6156
6157 #[test]
6158 fn visual_line_extends_both_directions() {
6159 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6160 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6162 assert_eq!(e.cursor(), (3, 0));
6163 run_keys(&mut e, "k");
6164 assert_eq!(e.cursor(), (2, 0));
6166 run_keys(&mut e, "k");
6167 assert_eq!(e.cursor(), (1, 0));
6168 }
6169
6170 #[test]
6171 fn visual_char_preserves_cursor_column() {
6172 let mut e = editor_with("hello world");
6173 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6175 assert_eq!(e.cursor(), (0, 5));
6176 run_keys(&mut e, "ll");
6177 assert_eq!(e.cursor(), (0, 7));
6178 }
6179
6180 #[test]
6181 fn visual_char_highlight_bounds_order() {
6182 let mut e = editor_with("abcdef");
6183 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
6185 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
6188 }
6189
6190 #[test]
6191 fn visual_line_highlight_bounds() {
6192 let mut e = editor_with("a\nb\nc");
6193 run_keys(&mut e, "V");
6194 assert_eq!(e.line_highlight(), Some((0, 0)));
6195 run_keys(&mut e, "j");
6196 assert_eq!(e.line_highlight(), Some((0, 1)));
6197 run_keys(&mut e, "j");
6198 assert_eq!(e.line_highlight(), Some((0, 2)));
6199 }
6200
6201 #[test]
6204 fn h_moves_left() {
6205 let mut e = editor_with("hello");
6206 e.jump_cursor(0, 3);
6207 run_keys(&mut e, "h");
6208 assert_eq!(e.cursor(), (0, 2));
6209 }
6210
6211 #[test]
6212 fn l_moves_right() {
6213 let mut e = editor_with("hello");
6214 run_keys(&mut e, "l");
6215 assert_eq!(e.cursor(), (0, 1));
6216 }
6217
6218 #[test]
6219 fn k_moves_up() {
6220 let mut e = editor_with("a\nb\nc");
6221 e.jump_cursor(2, 0);
6222 run_keys(&mut e, "k");
6223 assert_eq!(e.cursor(), (1, 0));
6224 }
6225
6226 #[test]
6227 fn zero_moves_to_line_start() {
6228 let mut e = editor_with(" hello");
6229 run_keys(&mut e, "$");
6230 run_keys(&mut e, "0");
6231 assert_eq!(e.cursor().1, 0);
6232 }
6233
6234 #[test]
6235 fn caret_moves_to_first_non_blank() {
6236 let mut e = editor_with(" hello");
6237 run_keys(&mut e, "0");
6238 run_keys(&mut e, "^");
6239 assert_eq!(e.cursor().1, 4);
6240 }
6241
6242 #[test]
6243 fn dollar_moves_to_last_char() {
6244 let mut e = editor_with("hello");
6245 run_keys(&mut e, "$");
6246 assert_eq!(e.cursor().1, 4);
6247 }
6248
6249 #[test]
6250 fn dollar_on_empty_line_stays_at_col_zero() {
6251 let mut e = editor_with("");
6252 run_keys(&mut e, "$");
6253 assert_eq!(e.cursor().1, 0);
6254 }
6255
6256 #[test]
6257 fn w_jumps_to_next_word() {
6258 let mut e = editor_with("foo bar baz");
6259 run_keys(&mut e, "w");
6260 assert_eq!(e.cursor().1, 4);
6261 }
6262
6263 #[test]
6264 fn b_jumps_back_a_word() {
6265 let mut e = editor_with("foo bar");
6266 e.jump_cursor(0, 6);
6267 run_keys(&mut e, "b");
6268 assert_eq!(e.cursor().1, 4);
6269 }
6270
6271 #[test]
6272 fn e_jumps_to_word_end() {
6273 let mut e = editor_with("foo bar");
6274 run_keys(&mut e, "e");
6275 assert_eq!(e.cursor().1, 2);
6276 }
6277
6278 #[test]
6281 fn d_dollar_deletes_to_eol() {
6282 let mut e = editor_with("hello world");
6283 e.jump_cursor(0, 5);
6284 run_keys(&mut e, "d$");
6285 assert_eq!(e.buffer().lines()[0], "hello");
6286 }
6287
6288 #[test]
6289 fn d_zero_deletes_to_line_start() {
6290 let mut e = editor_with("hello world");
6291 e.jump_cursor(0, 6);
6292 run_keys(&mut e, "d0");
6293 assert_eq!(e.buffer().lines()[0], "world");
6294 }
6295
6296 #[test]
6297 fn d_caret_deletes_to_first_non_blank() {
6298 let mut e = editor_with(" hello");
6299 e.jump_cursor(0, 6);
6300 run_keys(&mut e, "d^");
6301 assert_eq!(e.buffer().lines()[0], " llo");
6302 }
6303
6304 #[test]
6305 fn d_capital_g_deletes_to_end_of_file() {
6306 let mut e = editor_with("a\nb\nc\nd");
6307 e.jump_cursor(1, 0);
6308 run_keys(&mut e, "dG");
6309 assert_eq!(e.buffer().lines(), &["a".to_string()]);
6310 }
6311
6312 #[test]
6313 fn d_gg_deletes_to_start_of_file() {
6314 let mut e = editor_with("a\nb\nc\nd");
6315 e.jump_cursor(2, 0);
6316 run_keys(&mut e, "dgg");
6317 assert_eq!(e.buffer().lines(), &["d".to_string()]);
6318 }
6319
6320 #[test]
6321 fn cw_is_ce_quirk() {
6322 let mut e = editor_with("foo bar");
6325 run_keys(&mut e, "cwxyz<Esc>");
6326 assert_eq!(e.buffer().lines()[0], "xyz bar");
6327 }
6328
6329 #[test]
6332 fn big_d_deletes_to_eol() {
6333 let mut e = editor_with("hello world");
6334 e.jump_cursor(0, 5);
6335 run_keys(&mut e, "D");
6336 assert_eq!(e.buffer().lines()[0], "hello");
6337 }
6338
6339 #[test]
6340 fn big_c_deletes_to_eol_and_inserts() {
6341 let mut e = editor_with("hello world");
6342 e.jump_cursor(0, 5);
6343 run_keys(&mut e, "C!<Esc>");
6344 assert_eq!(e.buffer().lines()[0], "hello!");
6345 }
6346
6347 #[test]
6348 fn j_joins_next_line_with_space() {
6349 let mut e = editor_with("hello\nworld");
6350 run_keys(&mut e, "J");
6351 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6352 }
6353
6354 #[test]
6355 fn j_strips_leading_whitespace_on_join() {
6356 let mut e = editor_with("hello\n world");
6357 run_keys(&mut e, "J");
6358 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6359 }
6360
6361 #[test]
6362 fn big_x_deletes_char_before_cursor() {
6363 let mut e = editor_with("hello");
6364 e.jump_cursor(0, 3);
6365 run_keys(&mut e, "X");
6366 assert_eq!(e.buffer().lines()[0], "helo");
6367 }
6368
6369 #[test]
6370 fn s_substitutes_char_and_enters_insert() {
6371 let mut e = editor_with("hello");
6372 run_keys(&mut e, "sX<Esc>");
6373 assert_eq!(e.buffer().lines()[0], "Xello");
6374 }
6375
6376 #[test]
6377 fn count_x_deletes_many() {
6378 let mut e = editor_with("abcdef");
6379 run_keys(&mut e, "3x");
6380 assert_eq!(e.buffer().lines()[0], "def");
6381 }
6382
6383 #[test]
6386 fn p_pastes_charwise_after_cursor() {
6387 let mut e = editor_with("hello");
6388 run_keys(&mut e, "yw");
6389 run_keys(&mut e, "$p");
6390 assert_eq!(e.buffer().lines()[0], "hellohello");
6391 }
6392
6393 #[test]
6394 fn capital_p_pastes_charwise_before_cursor() {
6395 let mut e = editor_with("hello");
6396 run_keys(&mut e, "v");
6398 run_keys(&mut e, "l");
6399 run_keys(&mut e, "y");
6400 run_keys(&mut e, "$P");
6401 assert_eq!(e.buffer().lines()[0], "hellheo");
6404 }
6405
6406 #[test]
6407 fn p_pastes_linewise_below() {
6408 let mut e = editor_with("one\ntwo\nthree");
6409 run_keys(&mut e, "yy");
6410 run_keys(&mut e, "p");
6411 assert_eq!(
6412 e.buffer().lines(),
6413 &[
6414 "one".to_string(),
6415 "one".to_string(),
6416 "two".to_string(),
6417 "three".to_string()
6418 ]
6419 );
6420 }
6421
6422 #[test]
6423 fn capital_p_pastes_linewise_above() {
6424 let mut e = editor_with("one\ntwo");
6425 e.jump_cursor(1, 0);
6426 run_keys(&mut e, "yy");
6427 run_keys(&mut e, "P");
6428 assert_eq!(
6429 e.buffer().lines(),
6430 &["one".to_string(), "two".to_string(), "two".to_string()]
6431 );
6432 }
6433
6434 #[test]
6437 fn hash_finds_previous_occurrence() {
6438 let mut e = editor_with("foo bar foo baz foo");
6439 e.jump_cursor(0, 16);
6441 run_keys(&mut e, "#");
6442 assert_eq!(e.cursor().1, 8);
6443 }
6444
6445 #[test]
6448 fn visual_line_delete_removes_full_lines() {
6449 let mut e = editor_with("a\nb\nc\nd");
6450 run_keys(&mut e, "Vjd");
6451 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
6452 }
6453
6454 #[test]
6455 fn visual_line_change_leaves_blank_line() {
6456 let mut e = editor_with("a\nb\nc");
6457 run_keys(&mut e, "Vjc");
6458 assert_eq!(e.vim_mode(), VimMode::Insert);
6459 run_keys(&mut e, "X<Esc>");
6460 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
6464 }
6465
6466 #[test]
6467 fn cc_leaves_blank_line() {
6468 let mut e = editor_with("a\nb\nc");
6469 e.jump_cursor(1, 0);
6470 run_keys(&mut e, "ccX<Esc>");
6471 assert_eq!(
6472 e.buffer().lines(),
6473 &["a".to_string(), "X".to_string(), "c".to_string()]
6474 );
6475 }
6476
6477 #[test]
6482 fn big_w_skips_hyphens() {
6483 let mut e = editor_with("foo-bar baz");
6485 run_keys(&mut e, "W");
6486 assert_eq!(e.cursor().1, 8);
6487 }
6488
6489 #[test]
6490 fn big_w_crosses_lines() {
6491 let mut e = editor_with("foo-bar\nbaz-qux");
6492 run_keys(&mut e, "W");
6493 assert_eq!(e.cursor(), (1, 0));
6494 }
6495
6496 #[test]
6497 fn big_b_skips_hyphens() {
6498 let mut e = editor_with("foo-bar baz");
6499 e.jump_cursor(0, 9);
6500 run_keys(&mut e, "B");
6501 assert_eq!(e.cursor().1, 8);
6502 run_keys(&mut e, "B");
6503 assert_eq!(e.cursor().1, 0);
6504 }
6505
6506 #[test]
6507 fn big_e_jumps_to_big_word_end() {
6508 let mut e = editor_with("foo-bar baz");
6509 run_keys(&mut e, "E");
6510 assert_eq!(e.cursor().1, 6);
6511 run_keys(&mut e, "E");
6512 assert_eq!(e.cursor().1, 10);
6513 }
6514
6515 #[test]
6516 fn dw_with_big_word_variant() {
6517 let mut e = editor_with("foo-bar baz");
6519 run_keys(&mut e, "dW");
6520 assert_eq!(e.buffer().lines()[0], "baz");
6521 }
6522
6523 #[test]
6526 fn insert_ctrl_w_deletes_word_back() {
6527 let mut e = editor_with("");
6528 run_keys(&mut e, "i");
6529 for c in "hello world".chars() {
6530 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6531 }
6532 run_keys(&mut e, "<C-w>");
6533 assert_eq!(e.buffer().lines()[0], "hello ");
6534 }
6535
6536 #[test]
6537 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
6538 let mut e = editor_with("hello\nworld");
6542 e.jump_cursor(1, 0);
6543 run_keys(&mut e, "i");
6544 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6545 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
6548 assert_eq!(e.cursor(), (0, 0));
6549 }
6550
6551 #[test]
6552 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
6553 let mut e = editor_with("foo bar\nbaz");
6554 e.jump_cursor(1, 0);
6555 run_keys(&mut e, "i");
6556 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6557 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
6559 assert_eq!(e.cursor(), (0, 4));
6560 }
6561
6562 #[test]
6563 fn insert_ctrl_u_deletes_to_line_start() {
6564 let mut e = editor_with("");
6565 run_keys(&mut e, "i");
6566 for c in "hello world".chars() {
6567 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6568 }
6569 run_keys(&mut e, "<C-u>");
6570 assert_eq!(e.buffer().lines()[0], "");
6571 }
6572
6573 #[test]
6574 fn insert_ctrl_o_runs_one_normal_command() {
6575 let mut e = editor_with("hello world");
6576 run_keys(&mut e, "A");
6578 assert_eq!(e.vim_mode(), VimMode::Insert);
6579 e.jump_cursor(0, 0);
6581 run_keys(&mut e, "<C-o>");
6582 assert_eq!(e.vim_mode(), VimMode::Normal);
6583 run_keys(&mut e, "dw");
6584 assert_eq!(e.vim_mode(), VimMode::Insert);
6586 assert_eq!(e.buffer().lines()[0], "world");
6587 }
6588
6589 #[test]
6592 fn j_through_empty_line_preserves_column() {
6593 let mut e = editor_with("hello world\n\nanother line");
6594 run_keys(&mut e, "llllll");
6596 assert_eq!(e.cursor(), (0, 6));
6597 run_keys(&mut e, "j");
6600 assert_eq!(e.cursor(), (1, 0));
6601 run_keys(&mut e, "j");
6603 assert_eq!(e.cursor(), (2, 6));
6604 }
6605
6606 #[test]
6607 fn j_through_shorter_line_preserves_column() {
6608 let mut e = editor_with("hello world\nhi\nanother line");
6609 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
6612 run_keys(&mut e, "j");
6613 assert_eq!(e.cursor(), (2, 7));
6614 }
6615
6616 #[test]
6617 fn esc_from_insert_sticky_matches_visible_cursor() {
6618 let mut e = editor_with(" this is a line\n another one of a similar size");
6622 e.jump_cursor(0, 12);
6623 run_keys(&mut e, "I");
6624 assert_eq!(e.cursor(), (0, 4));
6625 run_keys(&mut e, "X<Esc>");
6626 assert_eq!(e.cursor(), (0, 4));
6627 run_keys(&mut e, "j");
6628 assert_eq!(e.cursor(), (1, 4));
6629 }
6630
6631 #[test]
6632 fn esc_from_insert_sticky_tracks_inserted_chars() {
6633 let mut e = editor_with("xxxxxxx\nyyyyyyy");
6634 run_keys(&mut e, "i");
6635 run_keys(&mut e, "abc<Esc>");
6636 assert_eq!(e.cursor(), (0, 2));
6637 run_keys(&mut e, "j");
6638 assert_eq!(e.cursor(), (1, 2));
6639 }
6640
6641 #[test]
6642 fn esc_from_insert_sticky_tracks_arrow_nav() {
6643 let mut e = editor_with("xxxxxx\nyyyyyy");
6644 run_keys(&mut e, "i");
6645 run_keys(&mut e, "abc");
6646 for _ in 0..2 {
6647 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
6648 }
6649 run_keys(&mut e, "<Esc>");
6650 assert_eq!(e.cursor(), (0, 0));
6651 run_keys(&mut e, "j");
6652 assert_eq!(e.cursor(), (1, 0));
6653 }
6654
6655 #[test]
6656 fn esc_from_insert_at_col_14_followed_by_j() {
6657 let line = "x".repeat(30);
6660 let buf = format!("{line}\n{line}");
6661 let mut e = editor_with(&buf);
6662 e.jump_cursor(0, 14);
6663 run_keys(&mut e, "i");
6664 for c in "test ".chars() {
6665 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6666 }
6667 run_keys(&mut e, "<Esc>");
6668 assert_eq!(e.cursor(), (0, 18));
6669 run_keys(&mut e, "j");
6670 assert_eq!(e.cursor(), (1, 18));
6671 }
6672
6673 #[test]
6674 fn linewise_paste_resets_sticky_column() {
6675 let mut e = editor_with(" hello\naaaaaaaa\nbye");
6679 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
6681 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
6685 run_keys(&mut e, "j");
6687 assert_eq!(e.cursor(), (3, 2));
6688 }
6689
6690 #[test]
6691 fn horizontal_motion_resyncs_sticky_column() {
6692 let mut e = editor_with("hello world\n\nanother line");
6696 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
6699 assert_eq!(e.cursor(), (2, 3));
6700 }
6701
6702 #[test]
6705 fn ctrl_v_enters_visual_block() {
6706 let mut e = editor_with("aaa\nbbb\nccc");
6707 run_keys(&mut e, "<C-v>");
6708 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
6709 }
6710
6711 #[test]
6712 fn visual_block_esc_returns_to_normal() {
6713 let mut e = editor_with("aaa\nbbb\nccc");
6714 run_keys(&mut e, "<C-v>");
6715 run_keys(&mut e, "<Esc>");
6716 assert_eq!(e.vim_mode(), VimMode::Normal);
6717 }
6718
6719 #[test]
6720 fn visual_block_delete_removes_column_range() {
6721 let mut e = editor_with("hello\nworld\nhappy");
6722 run_keys(&mut e, "l");
6724 run_keys(&mut e, "<C-v>");
6725 run_keys(&mut e, "jj");
6726 run_keys(&mut e, "ll");
6727 run_keys(&mut e, "d");
6728 assert_eq!(
6730 e.buffer().lines(),
6731 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
6732 );
6733 }
6734
6735 #[test]
6736 fn visual_block_yank_joins_with_newlines() {
6737 let mut e = editor_with("hello\nworld\nhappy");
6738 run_keys(&mut e, "<C-v>");
6739 run_keys(&mut e, "jj");
6740 run_keys(&mut e, "ll");
6741 run_keys(&mut e, "y");
6742 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
6743 }
6744
6745 #[test]
6746 fn visual_block_replace_fills_block() {
6747 let mut e = editor_with("hello\nworld\nhappy");
6748 run_keys(&mut e, "<C-v>");
6749 run_keys(&mut e, "jj");
6750 run_keys(&mut e, "ll");
6751 run_keys(&mut e, "rx");
6752 assert_eq!(
6753 e.buffer().lines(),
6754 &[
6755 "xxxlo".to_string(),
6756 "xxxld".to_string(),
6757 "xxxpy".to_string()
6758 ]
6759 );
6760 }
6761
6762 #[test]
6763 fn visual_block_insert_repeats_across_rows() {
6764 let mut e = editor_with("hello\nworld\nhappy");
6765 run_keys(&mut e, "<C-v>");
6766 run_keys(&mut e, "jj");
6767 run_keys(&mut e, "I");
6768 run_keys(&mut e, "# <Esc>");
6769 assert_eq!(
6770 e.buffer().lines(),
6771 &[
6772 "# hello".to_string(),
6773 "# world".to_string(),
6774 "# happy".to_string()
6775 ]
6776 );
6777 }
6778
6779 #[test]
6780 fn block_highlight_returns_none_outside_block_mode() {
6781 let mut e = editor_with("abc");
6782 assert!(e.block_highlight().is_none());
6783 run_keys(&mut e, "v");
6784 assert!(e.block_highlight().is_none());
6785 run_keys(&mut e, "<Esc>V");
6786 assert!(e.block_highlight().is_none());
6787 }
6788
6789 #[test]
6790 fn block_highlight_bounds_track_anchor_and_cursor() {
6791 let mut e = editor_with("aaaa\nbbbb\ncccc");
6792 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
6794 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
6797 }
6798
6799 #[test]
6800 fn visual_block_delete_handles_short_lines() {
6801 let mut e = editor_with("hello\nhi\nworld");
6803 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
6805 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
6807 assert_eq!(
6812 e.buffer().lines(),
6813 &["ho".to_string(), "h".to_string(), "wd".to_string()]
6814 );
6815 }
6816
6817 #[test]
6818 fn visual_block_yank_pads_short_lines_with_empties() {
6819 let mut e = editor_with("hello\nhi\nworld");
6820 run_keys(&mut e, "l");
6821 run_keys(&mut e, "<C-v>");
6822 run_keys(&mut e, "jjll");
6823 run_keys(&mut e, "y");
6824 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
6826 }
6827
6828 #[test]
6829 fn visual_block_replace_skips_past_eol() {
6830 let mut e = editor_with("ab\ncd\nef");
6833 run_keys(&mut e, "l");
6835 run_keys(&mut e, "<C-v>");
6836 run_keys(&mut e, "jjllllll");
6837 run_keys(&mut e, "rX");
6838 assert_eq!(
6841 e.buffer().lines(),
6842 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
6843 );
6844 }
6845
6846 #[test]
6847 fn visual_block_with_empty_line_in_middle() {
6848 let mut e = editor_with("abcd\n\nefgh");
6849 run_keys(&mut e, "<C-v>");
6850 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
6852 assert_eq!(
6855 e.buffer().lines(),
6856 &["d".to_string(), "".to_string(), "h".to_string()]
6857 );
6858 }
6859
6860 #[test]
6861 fn block_insert_pads_empty_lines_to_block_column() {
6862 let mut e = editor_with("this is a line\n\nthis is a line");
6865 e.jump_cursor(0, 3);
6866 run_keys(&mut e, "<C-v>");
6867 run_keys(&mut e, "jj");
6868 run_keys(&mut e, "I");
6869 run_keys(&mut e, "XX<Esc>");
6870 assert_eq!(
6871 e.buffer().lines(),
6872 &[
6873 "thiXXs is a line".to_string(),
6874 " XX".to_string(),
6875 "thiXXs is a line".to_string()
6876 ]
6877 );
6878 }
6879
6880 #[test]
6881 fn block_insert_pads_short_lines_to_block_column() {
6882 let mut e = editor_with("aaaaa\nbb\naaaaa");
6883 e.jump_cursor(0, 3);
6884 run_keys(&mut e, "<C-v>");
6885 run_keys(&mut e, "jj");
6886 run_keys(&mut e, "I");
6887 run_keys(&mut e, "Y<Esc>");
6888 assert_eq!(
6890 e.buffer().lines(),
6891 &[
6892 "aaaYaa".to_string(),
6893 "bb Y".to_string(),
6894 "aaaYaa".to_string()
6895 ]
6896 );
6897 }
6898
6899 #[test]
6900 fn visual_block_append_repeats_across_rows() {
6901 let mut e = editor_with("foo\nbar\nbaz");
6902 run_keys(&mut e, "<C-v>");
6903 run_keys(&mut e, "jj");
6904 run_keys(&mut e, "A");
6907 run_keys(&mut e, "!<Esc>");
6908 assert_eq!(
6909 e.buffer().lines(),
6910 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
6911 );
6912 }
6913
6914 #[test]
6917 fn slash_opens_forward_search_prompt() {
6918 let mut e = editor_with("hello world");
6919 run_keys(&mut e, "/");
6920 let p = e.search_prompt().expect("prompt should be active");
6921 assert!(p.text.is_empty());
6922 assert!(p.forward);
6923 }
6924
6925 #[test]
6926 fn question_opens_backward_search_prompt() {
6927 let mut e = editor_with("hello world");
6928 run_keys(&mut e, "?");
6929 let p = e.search_prompt().expect("prompt should be active");
6930 assert!(!p.forward);
6931 }
6932
6933 #[test]
6934 fn search_prompt_typing_updates_pattern_live() {
6935 let mut e = editor_with("foo bar\nbaz");
6936 run_keys(&mut e, "/bar");
6937 assert_eq!(e.search_prompt().unwrap().text, "bar");
6938 assert!(e.buffer().search_pattern().is_some());
6940 }
6941
6942 #[test]
6943 fn search_prompt_backspace_and_enter() {
6944 let mut e = editor_with("hello world\nagain");
6945 run_keys(&mut e, "/worlx");
6946 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
6947 assert_eq!(e.search_prompt().unwrap().text, "worl");
6948 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6949 assert!(e.search_prompt().is_none());
6951 assert_eq!(e.last_search(), Some("worl"));
6952 assert_eq!(e.cursor(), (0, 6));
6953 }
6954
6955 #[test]
6956 fn empty_search_prompt_enter_repeats_last_search() {
6957 let mut e = editor_with("foo bar foo baz foo");
6958 run_keys(&mut e, "/foo");
6959 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6960 assert_eq!(e.cursor().1, 8);
6961 run_keys(&mut e, "/");
6963 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
6964 assert_eq!(e.cursor().1, 16);
6965 assert_eq!(e.last_search(), Some("foo"));
6966 }
6967
6968 #[test]
6969 fn search_history_records_committed_patterns() {
6970 let mut e = editor_with("alpha beta gamma");
6971 run_keys(&mut e, "/alpha<CR>");
6972 run_keys(&mut e, "/beta<CR>");
6973 let history = e.vim.search_history.clone();
6975 assert_eq!(history, vec!["alpha", "beta"]);
6976 }
6977
6978 #[test]
6979 fn search_history_dedupes_consecutive_repeats() {
6980 let mut e = editor_with("foo bar foo");
6981 run_keys(&mut e, "/foo<CR>");
6982 run_keys(&mut e, "/foo<CR>");
6983 run_keys(&mut e, "/bar<CR>");
6984 run_keys(&mut e, "/bar<CR>");
6985 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
6987 }
6988
6989 #[test]
6990 fn ctrl_p_walks_history_backward() {
6991 let mut e = editor_with("alpha beta gamma");
6992 run_keys(&mut e, "/alpha<CR>");
6993 run_keys(&mut e, "/beta<CR>");
6994 run_keys(&mut e, "/");
6996 assert_eq!(e.search_prompt().unwrap().text, "");
6997 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
6998 assert_eq!(e.search_prompt().unwrap().text, "beta");
6999 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7000 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7001 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7003 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7004 }
7005
7006 #[test]
7007 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7008 let mut e = editor_with("a b c");
7009 run_keys(&mut e, "/a<CR>");
7010 run_keys(&mut e, "/b<CR>");
7011 run_keys(&mut e, "/c<CR>");
7012 run_keys(&mut e, "/");
7013 for _ in 0..3 {
7015 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7016 }
7017 assert_eq!(e.search_prompt().unwrap().text, "a");
7018 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7019 assert_eq!(e.search_prompt().unwrap().text, "b");
7020 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7021 assert_eq!(e.search_prompt().unwrap().text, "c");
7022 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7024 assert_eq!(e.search_prompt().unwrap().text, "c");
7025 }
7026
7027 #[test]
7028 fn typing_after_history_walk_resets_cursor() {
7029 let mut e = editor_with("foo");
7030 run_keys(&mut e, "/foo<CR>");
7031 run_keys(&mut e, "/");
7032 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7033 assert_eq!(e.search_prompt().unwrap().text, "foo");
7034 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7037 assert_eq!(e.search_prompt().unwrap().text, "foox");
7038 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7039 assert_eq!(e.search_prompt().unwrap().text, "foo");
7040 }
7041
7042 #[test]
7043 fn empty_backward_search_prompt_enter_repeats_last_search() {
7044 let mut e = editor_with("foo bar foo baz foo");
7045 run_keys(&mut e, "/foo");
7047 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7048 assert_eq!(e.cursor().1, 8);
7049 run_keys(&mut e, "?");
7050 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7051 assert_eq!(e.cursor().1, 0);
7052 assert_eq!(e.last_search(), Some("foo"));
7053 }
7054
7055 #[test]
7056 fn search_prompt_esc_cancels_but_keeps_last_search() {
7057 let mut e = editor_with("foo bar\nbaz");
7058 run_keys(&mut e, "/bar");
7059 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7060 assert!(e.search_prompt().is_none());
7061 assert_eq!(e.last_search(), Some("bar"));
7062 }
7063
7064 #[test]
7065 fn search_then_n_and_shift_n_navigate() {
7066 let mut e = editor_with("foo bar foo baz foo");
7067 run_keys(&mut e, "/foo");
7068 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7069 assert_eq!(e.cursor().1, 8);
7071 run_keys(&mut e, "n");
7072 assert_eq!(e.cursor().1, 16);
7073 run_keys(&mut e, "N");
7074 assert_eq!(e.cursor().1, 8);
7075 }
7076
7077 #[test]
7078 fn question_mark_searches_backward_on_enter() {
7079 let mut e = editor_with("foo bar foo baz");
7080 e.jump_cursor(0, 10);
7081 run_keys(&mut e, "?foo");
7082 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7083 assert_eq!(e.cursor(), (0, 8));
7085 }
7086
7087 #[test]
7090 fn big_y_yanks_to_end_of_line() {
7091 let mut e = editor_with("hello world");
7092 e.jump_cursor(0, 6);
7093 run_keys(&mut e, "Y");
7094 assert_eq!(e.last_yank.as_deref(), Some("world"));
7095 }
7096
7097 #[test]
7098 fn big_y_from_line_start_yanks_full_line() {
7099 let mut e = editor_with("hello world");
7100 run_keys(&mut e, "Y");
7101 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7102 }
7103
7104 #[test]
7105 fn gj_joins_without_inserting_space() {
7106 let mut e = editor_with("hello\n world");
7107 run_keys(&mut e, "gJ");
7108 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7110 }
7111
7112 #[test]
7113 fn gj_noop_on_last_line() {
7114 let mut e = editor_with("only");
7115 run_keys(&mut e, "gJ");
7116 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7117 }
7118
7119 #[test]
7120 fn ge_jumps_to_previous_word_end() {
7121 let mut e = editor_with("foo bar baz");
7122 e.jump_cursor(0, 5);
7123 run_keys(&mut e, "ge");
7124 assert_eq!(e.cursor(), (0, 2));
7125 }
7126
7127 #[test]
7128 fn ge_respects_word_class() {
7129 let mut e = editor_with("foo-bar baz");
7132 e.jump_cursor(0, 5);
7133 run_keys(&mut e, "ge");
7134 assert_eq!(e.cursor(), (0, 3));
7135 }
7136
7137 #[test]
7138 fn big_ge_treats_hyphens_as_part_of_word() {
7139 let mut e = editor_with("foo-bar baz");
7142 e.jump_cursor(0, 10);
7143 run_keys(&mut e, "gE");
7144 assert_eq!(e.cursor(), (0, 6));
7145 }
7146
7147 #[test]
7148 fn ge_crosses_line_boundary() {
7149 let mut e = editor_with("foo\nbar");
7150 e.jump_cursor(1, 0);
7151 run_keys(&mut e, "ge");
7152 assert_eq!(e.cursor(), (0, 2));
7153 }
7154
7155 #[test]
7156 fn dge_deletes_to_end_of_previous_word() {
7157 let mut e = editor_with("foo bar baz");
7158 e.jump_cursor(0, 8);
7159 run_keys(&mut e, "dge");
7162 assert_eq!(e.buffer().lines()[0], "foo baaz");
7163 }
7164
7165 #[test]
7166 fn ctrl_scroll_keys_do_not_panic() {
7167 let mut e = editor_with(
7170 (0..50)
7171 .map(|i| format!("line{i}"))
7172 .collect::<Vec<_>>()
7173 .join("\n")
7174 .as_str(),
7175 );
7176 run_keys(&mut e, "<C-f>");
7177 run_keys(&mut e, "<C-b>");
7178 assert!(!e.buffer().lines().is_empty());
7180 }
7181
7182 #[test]
7189 fn count_insert_with_arrow_nav_does_not_leak_rows() {
7190 let mut e = Editor::new(KeybindingMode::Vim);
7191 e.set_content("row0\nrow1\nrow2");
7192 run_keys(&mut e, "3iX<Down><Esc>");
7194 assert!(e.buffer().lines()[0].contains('X'));
7196 assert!(
7199 !e.buffer().lines()[1].contains("row0"),
7200 "row1 leaked row0 contents: {:?}",
7201 e.buffer().lines()[1]
7202 );
7203 assert_eq!(e.buffer().lines().len(), 3);
7206 }
7207
7208 fn editor_with_rows(n: usize, viewport: u16) -> Editor<'static> {
7211 let mut e = Editor::new(KeybindingMode::Vim);
7212 let body = (0..n)
7213 .map(|i| format!(" line{}", i))
7214 .collect::<Vec<_>>()
7215 .join("\n");
7216 e.set_content(&body);
7217 e.set_viewport_height(viewport);
7218 e
7219 }
7220
7221 #[test]
7222 fn ctrl_d_moves_cursor_half_page_down() {
7223 let mut e = editor_with_rows(100, 20);
7224 run_keys(&mut e, "<C-d>");
7225 assert_eq!(e.cursor().0, 10);
7226 }
7227
7228 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor<'static> {
7229 let mut e = Editor::new(KeybindingMode::Vim);
7230 e.set_content(&lines.join("\n"));
7231 e.set_viewport_height(viewport);
7232 let v = e.buffer_mut().viewport_mut();
7233 v.height = viewport;
7234 v.width = text_width;
7235 v.text_width = text_width;
7236 v.wrap = hjkl_buffer::Wrap::Char;
7237 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
7238 e
7239 }
7240
7241 #[test]
7242 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
7243 let lines = ["aaaabbbbcccc"; 10];
7247 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7248 e.jump_cursor(4, 0);
7249 e.ensure_cursor_in_scrolloff();
7250 let csr = e.buffer().cursor_screen_row().unwrap();
7251 assert!(csr <= 6, "csr={csr}");
7252 }
7253
7254 #[test]
7255 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
7256 let lines = ["aaaabbbbcccc"; 10];
7257 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7258 e.jump_cursor(7, 0);
7261 e.ensure_cursor_in_scrolloff();
7262 e.jump_cursor(2, 0);
7263 e.ensure_cursor_in_scrolloff();
7264 let csr = e.buffer().cursor_screen_row().unwrap();
7265 assert!(csr >= 5, "csr={csr}");
7267 }
7268
7269 #[test]
7270 fn scrolloff_wrap_clamps_top_at_buffer_end() {
7271 let lines = ["aaaabbbbcccc"; 5];
7272 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7273 e.jump_cursor(4, 11);
7274 e.ensure_cursor_in_scrolloff();
7275 let top = e.buffer().viewport().top_row;
7280 assert_eq!(top, 1);
7281 }
7282
7283 #[test]
7284 fn ctrl_u_moves_cursor_half_page_up() {
7285 let mut e = editor_with_rows(100, 20);
7286 e.jump_cursor(50, 0);
7287 run_keys(&mut e, "<C-u>");
7288 assert_eq!(e.cursor().0, 40);
7289 }
7290
7291 #[test]
7292 fn ctrl_f_moves_cursor_full_page_down() {
7293 let mut e = editor_with_rows(100, 20);
7294 run_keys(&mut e, "<C-f>");
7295 assert_eq!(e.cursor().0, 18);
7297 }
7298
7299 #[test]
7300 fn ctrl_b_moves_cursor_full_page_up() {
7301 let mut e = editor_with_rows(100, 20);
7302 e.jump_cursor(50, 0);
7303 run_keys(&mut e, "<C-b>");
7304 assert_eq!(e.cursor().0, 32);
7305 }
7306
7307 #[test]
7308 fn ctrl_d_lands_on_first_non_blank() {
7309 let mut e = editor_with_rows(100, 20);
7310 run_keys(&mut e, "<C-d>");
7311 assert_eq!(e.cursor().1, 2);
7313 }
7314
7315 #[test]
7316 fn ctrl_d_clamps_at_end_of_buffer() {
7317 let mut e = editor_with_rows(5, 20);
7318 run_keys(&mut e, "<C-d>");
7319 assert_eq!(e.cursor().0, 4);
7320 }
7321
7322 #[test]
7323 fn capital_h_jumps_to_viewport_top() {
7324 let mut e = editor_with_rows(100, 10);
7325 e.jump_cursor(50, 0);
7326 e.set_viewport_top(45);
7327 let top = e.buffer().viewport().top_row;
7328 run_keys(&mut e, "H");
7329 assert_eq!(e.cursor().0, top);
7330 assert_eq!(e.cursor().1, 2);
7331 }
7332
7333 #[test]
7334 fn capital_l_jumps_to_viewport_bottom() {
7335 let mut e = editor_with_rows(100, 10);
7336 e.jump_cursor(50, 0);
7337 e.set_viewport_top(45);
7338 let top = e.buffer().viewport().top_row;
7339 run_keys(&mut e, "L");
7340 assert_eq!(e.cursor().0, top + 9);
7341 }
7342
7343 #[test]
7344 fn capital_m_jumps_to_viewport_middle() {
7345 let mut e = editor_with_rows(100, 10);
7346 e.jump_cursor(50, 0);
7347 e.set_viewport_top(45);
7348 let top = e.buffer().viewport().top_row;
7349 run_keys(&mut e, "M");
7350 assert_eq!(e.cursor().0, top + 4);
7352 }
7353
7354 #[test]
7355 fn g_capital_m_lands_at_line_midpoint() {
7356 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
7358 assert_eq!(e.cursor(), (0, 6));
7360 }
7361
7362 #[test]
7363 fn g_capital_m_on_empty_line_stays_at_zero() {
7364 let mut e = editor_with("");
7365 run_keys(&mut e, "gM");
7366 assert_eq!(e.cursor(), (0, 0));
7367 }
7368
7369 #[test]
7370 fn g_capital_m_uses_current_line_only() {
7371 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
7374 run_keys(&mut e, "gM");
7375 assert_eq!(e.cursor(), (1, 6));
7376 }
7377
7378 #[test]
7379 fn capital_h_count_offsets_from_top() {
7380 let mut e = editor_with_rows(100, 10);
7381 e.jump_cursor(50, 0);
7382 e.set_viewport_top(45);
7383 let top = e.buffer().viewport().top_row;
7384 run_keys(&mut e, "3H");
7385 assert_eq!(e.cursor().0, top + 2);
7386 }
7387
7388 #[test]
7391 fn ctrl_o_returns_to_pre_g_position() {
7392 let mut e = editor_with_rows(50, 20);
7393 e.jump_cursor(5, 2);
7394 run_keys(&mut e, "G");
7395 assert_eq!(e.cursor().0, 49);
7396 run_keys(&mut e, "<C-o>");
7397 assert_eq!(e.cursor(), (5, 2));
7398 }
7399
7400 #[test]
7401 fn ctrl_i_redoes_jump_after_ctrl_o() {
7402 let mut e = editor_with_rows(50, 20);
7403 e.jump_cursor(5, 2);
7404 run_keys(&mut e, "G");
7405 let post = e.cursor();
7406 run_keys(&mut e, "<C-o>");
7407 run_keys(&mut e, "<C-i>");
7408 assert_eq!(e.cursor(), post);
7409 }
7410
7411 #[test]
7412 fn new_jump_clears_forward_stack() {
7413 let mut e = editor_with_rows(50, 20);
7414 e.jump_cursor(5, 2);
7415 run_keys(&mut e, "G");
7416 run_keys(&mut e, "<C-o>");
7417 run_keys(&mut e, "gg");
7418 run_keys(&mut e, "<C-i>");
7419 assert_eq!(e.cursor().0, 0);
7420 }
7421
7422 #[test]
7423 fn ctrl_o_on_empty_stack_is_noop() {
7424 let mut e = editor_with_rows(10, 20);
7425 e.jump_cursor(3, 1);
7426 run_keys(&mut e, "<C-o>");
7427 assert_eq!(e.cursor(), (3, 1));
7428 }
7429
7430 #[test]
7431 fn asterisk_search_pushes_jump() {
7432 let mut e = editor_with("foo bar\nbaz foo end");
7433 e.jump_cursor(0, 0);
7434 run_keys(&mut e, "*");
7435 let after = e.cursor();
7436 assert_ne!(after, (0, 0));
7437 run_keys(&mut e, "<C-o>");
7438 assert_eq!(e.cursor(), (0, 0));
7439 }
7440
7441 #[test]
7442 fn h_viewport_jump_is_recorded() {
7443 let mut e = editor_with_rows(100, 10);
7444 e.jump_cursor(50, 0);
7445 e.set_viewport_top(45);
7446 let pre = e.cursor();
7447 run_keys(&mut e, "H");
7448 assert_ne!(e.cursor(), pre);
7449 run_keys(&mut e, "<C-o>");
7450 assert_eq!(e.cursor(), pre);
7451 }
7452
7453 #[test]
7454 fn j_k_motion_does_not_push_jump() {
7455 let mut e = editor_with_rows(50, 20);
7456 e.jump_cursor(5, 0);
7457 run_keys(&mut e, "jjj");
7458 run_keys(&mut e, "<C-o>");
7459 assert_eq!(e.cursor().0, 8);
7460 }
7461
7462 #[test]
7463 fn jumplist_caps_at_100() {
7464 let mut e = editor_with_rows(200, 20);
7465 for i in 0..101 {
7466 e.jump_cursor(i, 0);
7467 run_keys(&mut e, "G");
7468 }
7469 assert!(e.vim.jump_back.len() <= 100);
7470 }
7471
7472 #[test]
7473 fn tab_acts_as_ctrl_i() {
7474 let mut e = editor_with_rows(50, 20);
7475 e.jump_cursor(5, 2);
7476 run_keys(&mut e, "G");
7477 let post = e.cursor();
7478 run_keys(&mut e, "<C-o>");
7479 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
7480 assert_eq!(e.cursor(), post);
7481 }
7482
7483 #[test]
7486 fn ma_then_backtick_a_jumps_exact() {
7487 let mut e = editor_with_rows(50, 20);
7488 e.jump_cursor(5, 3);
7489 run_keys(&mut e, "ma");
7490 e.jump_cursor(20, 0);
7491 run_keys(&mut e, "`a");
7492 assert_eq!(e.cursor(), (5, 3));
7493 }
7494
7495 #[test]
7496 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
7497 let mut e = editor_with_rows(50, 20);
7498 e.jump_cursor(5, 6);
7500 run_keys(&mut e, "ma");
7501 e.jump_cursor(30, 4);
7502 run_keys(&mut e, "'a");
7503 assert_eq!(e.cursor(), (5, 2));
7504 }
7505
7506 #[test]
7507 fn goto_mark_pushes_jumplist() {
7508 let mut e = editor_with_rows(50, 20);
7509 e.jump_cursor(10, 2);
7510 run_keys(&mut e, "mz");
7511 e.jump_cursor(3, 0);
7512 run_keys(&mut e, "`z");
7513 assert_eq!(e.cursor(), (10, 2));
7514 run_keys(&mut e, "<C-o>");
7515 assert_eq!(e.cursor(), (3, 0));
7516 }
7517
7518 #[test]
7519 fn goto_missing_mark_is_noop() {
7520 let mut e = editor_with_rows(50, 20);
7521 e.jump_cursor(3, 1);
7522 run_keys(&mut e, "`q");
7523 assert_eq!(e.cursor(), (3, 1));
7524 }
7525
7526 #[test]
7527 fn uppercase_mark_letter_ignored() {
7528 let mut e = editor_with_rows(50, 20);
7529 e.jump_cursor(5, 3);
7530 run_keys(&mut e, "mA");
7531 assert!(e.vim.marks.is_empty());
7534 }
7535
7536 #[test]
7537 fn mark_survives_document_shrink_via_clamp() {
7538 let mut e = editor_with_rows(50, 20);
7539 e.jump_cursor(40, 4);
7540 run_keys(&mut e, "mx");
7541 e.set_content("a\nb\nc\nd\ne");
7543 run_keys(&mut e, "`x");
7544 let (r, _) = e.cursor();
7546 assert!(r <= 4);
7547 }
7548
7549 #[test]
7550 fn g_semicolon_walks_back_through_edits() {
7551 let mut e = editor_with("alpha\nbeta\ngamma");
7552 e.jump_cursor(0, 0);
7555 run_keys(&mut e, "iX<Esc>");
7556 e.jump_cursor(2, 0);
7557 run_keys(&mut e, "iY<Esc>");
7558 run_keys(&mut e, "g;");
7560 assert_eq!(e.cursor(), (2, 1));
7561 run_keys(&mut e, "g;");
7563 assert_eq!(e.cursor(), (0, 1));
7564 run_keys(&mut e, "g;");
7566 assert_eq!(e.cursor(), (0, 1));
7567 }
7568
7569 #[test]
7570 fn g_comma_walks_forward_after_g_semicolon() {
7571 let mut e = editor_with("a\nb\nc");
7572 e.jump_cursor(0, 0);
7573 run_keys(&mut e, "iX<Esc>");
7574 e.jump_cursor(2, 0);
7575 run_keys(&mut e, "iY<Esc>");
7576 run_keys(&mut e, "g;");
7577 run_keys(&mut e, "g;");
7578 assert_eq!(e.cursor(), (0, 1));
7579 run_keys(&mut e, "g,");
7580 assert_eq!(e.cursor(), (2, 1));
7581 }
7582
7583 #[test]
7584 fn new_edit_during_walk_trims_forward_entries() {
7585 let mut e = editor_with("a\nb\nc\nd");
7586 e.jump_cursor(0, 0);
7587 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
7589 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
7592 run_keys(&mut e, "g;");
7593 assert_eq!(e.cursor(), (0, 1));
7594 run_keys(&mut e, "iZ<Esc>");
7596 run_keys(&mut e, "g,");
7598 assert_ne!(e.cursor(), (2, 1));
7600 }
7601
7602 #[test]
7603 fn gqq_reflows_current_line_to_textwidth() {
7604 let mut e = editor_with("alpha beta gamma delta epsilon zeta eta theta iota");
7605 crate::ex::run(&mut e, "set tw=20");
7606 assert_eq!(e.settings().textwidth, 20);
7607 run_keys(&mut e, "gqq");
7608 for line in e.buffer().lines() {
7610 assert!(line.chars().count() <= 20, "line too long: {line:?}");
7611 }
7612 assert!(e.buffer().lines().len() > 1);
7614 }
7615
7616 #[test]
7617 fn gq_motion_reflows_paragraph() {
7618 let mut e = editor_with("one two three\nfour five six\nseven eight\n\ntail");
7619 crate::ex::run(&mut e, "set tw=15");
7620 e.jump_cursor(0, 0);
7621 run_keys(&mut e, "gq}");
7623 assert_eq!(e.buffer().lines().last().unwrap(), "tail");
7625 }
7626
7627 #[test]
7628 fn gq_preserves_paragraph_breaks() {
7629 let mut e = editor_with("alpha beta gamma\n\ndelta epsilon zeta");
7630 crate::ex::run(&mut e, "set tw=10");
7631 run_keys(&mut e, "ggVGgq");
7632 let blanks = e.buffer().lines().iter().filter(|l| l.is_empty()).count();
7635 assert_eq!(blanks, 1);
7636 }
7637
7638 #[test]
7639 fn gqq_undo_restores_original_line() {
7640 let mut e = editor_with("a b c d e f g h i j k l m n o p");
7641 crate::ex::run(&mut e, "set tw=10");
7642 let before: Vec<String> = e.buffer().lines().to_vec();
7643 run_keys(&mut e, "gqq");
7644 crate::vim::do_undo(&mut e);
7645 assert_eq!(e.buffer().lines(), before);
7646 }
7647
7648 #[test]
7649 fn capital_mark_set_and_jump() {
7650 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
7651 e.jump_cursor(2, 1);
7652 run_keys(&mut e, "mA");
7653 e.jump_cursor(0, 0);
7655 run_keys(&mut e, "'A");
7657 assert_eq!(e.cursor().0, 2);
7659 }
7660
7661 #[test]
7662 fn capital_mark_survives_set_content() {
7663 let mut e = editor_with("first buffer line\nsecond");
7664 e.jump_cursor(1, 3);
7665 run_keys(&mut e, "mA");
7666 e.set_content("totally different content\non many\nrows of text");
7668 e.jump_cursor(0, 0);
7670 run_keys(&mut e, "'A");
7671 assert_eq!(e.cursor().0, 1);
7672 }
7673
7674 #[test]
7675 fn capital_mark_shows_in_marks_listing() {
7676 let mut e = editor_with("a\nb\nc");
7677 e.jump_cursor(2, 0);
7678 run_keys(&mut e, "mZ");
7679 e.jump_cursor(0, 0);
7680 run_keys(&mut e, "ma");
7681 let info = match crate::ex::run(&mut e, "marks") {
7682 crate::ex::ExEffect::Info(s) => s,
7683 other => panic!("expected Info, got {other:?}"),
7684 };
7685 assert!(info.contains(" a "));
7686 assert!(info.contains(" Z "));
7687 }
7688
7689 #[test]
7690 fn capital_mark_shifts_with_edit() {
7691 let mut e = editor_with("a\nb\nc\nd");
7692 e.jump_cursor(3, 0);
7693 run_keys(&mut e, "mA");
7694 e.jump_cursor(0, 0);
7696 run_keys(&mut e, "dd");
7697 e.jump_cursor(0, 0);
7698 run_keys(&mut e, "'A");
7699 assert_eq!(e.cursor().0, 2);
7700 }
7701
7702 #[test]
7703 fn mark_below_delete_shifts_up() {
7704 let mut e = editor_with("a\nb\nc\nd\ne");
7705 e.jump_cursor(3, 0);
7707 run_keys(&mut e, "ma");
7708 e.jump_cursor(0, 0);
7710 run_keys(&mut e, "dd");
7711 e.jump_cursor(0, 0);
7713 run_keys(&mut e, "'a");
7714 assert_eq!(e.cursor().0, 2);
7715 assert_eq!(e.buffer().line(2).unwrap(), "d");
7716 }
7717
7718 #[test]
7719 fn mark_on_deleted_row_is_dropped() {
7720 let mut e = editor_with("a\nb\nc\nd");
7721 e.jump_cursor(1, 0);
7723 run_keys(&mut e, "ma");
7724 run_keys(&mut e, "dd");
7726 e.jump_cursor(2, 0);
7728 run_keys(&mut e, "'a");
7729 assert_eq!(e.cursor().0, 2);
7731 }
7732
7733 #[test]
7734 fn mark_above_edit_unchanged() {
7735 let mut e = editor_with("a\nb\nc\nd\ne");
7736 e.jump_cursor(0, 0);
7738 run_keys(&mut e, "ma");
7739 e.jump_cursor(3, 0);
7741 run_keys(&mut e, "dd");
7742 e.jump_cursor(2, 0);
7744 run_keys(&mut e, "'a");
7745 assert_eq!(e.cursor().0, 0);
7746 }
7747
7748 #[test]
7749 fn mark_shifts_down_after_insert() {
7750 let mut e = editor_with("a\nb\nc");
7751 e.jump_cursor(2, 0);
7753 run_keys(&mut e, "ma");
7754 e.jump_cursor(0, 0);
7756 run_keys(&mut e, "Onew<Esc>");
7757 e.jump_cursor(0, 0);
7760 run_keys(&mut e, "'a");
7761 assert_eq!(e.cursor().0, 3);
7762 assert_eq!(e.buffer().line(3).unwrap(), "c");
7763 }
7764
7765 #[test]
7768 fn forward_search_commit_pushes_jump() {
7769 let mut e = editor_with("alpha beta\nfoo target end\nmore");
7770 e.jump_cursor(0, 0);
7771 run_keys(&mut e, "/target<CR>");
7772 assert_ne!(e.cursor(), (0, 0));
7774 run_keys(&mut e, "<C-o>");
7776 assert_eq!(e.cursor(), (0, 0));
7777 }
7778
7779 #[test]
7780 fn search_commit_no_match_does_not_push_jump() {
7781 let mut e = editor_with("alpha beta\nfoo end");
7782 e.jump_cursor(0, 3);
7783 let pre_len = e.vim.jump_back.len();
7784 run_keys(&mut e, "/zzznotfound<CR>");
7785 assert_eq!(e.vim.jump_back.len(), pre_len);
7787 }
7788
7789 #[test]
7792 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
7793 let mut e = editor_with("hello world");
7794 run_keys(&mut e, "lll");
7795 let (row, col) = e.cursor();
7796 assert_eq!(e.buffer.cursor().row, row);
7797 assert_eq!(e.buffer.cursor().col, col);
7798 }
7799
7800 #[test]
7801 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
7802 let mut e = editor_with("aaaa\nbbbb\ncccc");
7803 run_keys(&mut e, "jj");
7804 let (row, col) = e.cursor();
7805 assert_eq!(e.buffer.cursor().row, row);
7806 assert_eq!(e.buffer.cursor().col, col);
7807 }
7808
7809 #[test]
7810 fn buffer_cursor_mirrors_textarea_after_word_motion() {
7811 let mut e = editor_with("foo bar baz");
7812 run_keys(&mut e, "ww");
7813 let (row, col) = e.cursor();
7814 assert_eq!(e.buffer.cursor().row, row);
7815 assert_eq!(e.buffer.cursor().col, col);
7816 }
7817
7818 #[test]
7819 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
7820 let mut e = editor_with("a\nb\nc\nd\ne");
7821 run_keys(&mut e, "G");
7822 let (row, col) = e.cursor();
7823 assert_eq!(e.buffer.cursor().row, row);
7824 assert_eq!(e.buffer.cursor().col, col);
7825 }
7826
7827 #[test]
7828 fn buffer_sticky_col_mirrors_vim_state() {
7829 let mut e = editor_with("longline\nhi\nlongline");
7830 run_keys(&mut e, "fl");
7831 run_keys(&mut e, "j");
7832 assert_eq!(e.buffer.sticky_col(), e.vim.sticky_col);
7834 }
7835
7836 #[test]
7837 fn buffer_content_mirrors_textarea_after_insert() {
7838 let mut e = editor_with("hello");
7839 run_keys(&mut e, "iXYZ<Esc>");
7840 let text = e.buffer().lines().join("\n");
7841 assert_eq!(e.buffer.as_string(), text);
7842 }
7843
7844 #[test]
7845 fn buffer_content_mirrors_textarea_after_delete() {
7846 let mut e = editor_with("alpha bravo charlie");
7847 run_keys(&mut e, "dw");
7848 let text = e.buffer().lines().join("\n");
7849 assert_eq!(e.buffer.as_string(), text);
7850 }
7851
7852 #[test]
7853 fn buffer_content_mirrors_textarea_after_dd() {
7854 let mut e = editor_with("a\nb\nc\nd");
7855 run_keys(&mut e, "jdd");
7856 let text = e.buffer().lines().join("\n");
7857 assert_eq!(e.buffer.as_string(), text);
7858 }
7859
7860 #[test]
7861 fn buffer_content_mirrors_textarea_after_open_line() {
7862 let mut e = editor_with("foo\nbar");
7863 run_keys(&mut e, "oNEW<Esc>");
7864 let text = e.buffer().lines().join("\n");
7865 assert_eq!(e.buffer.as_string(), text);
7866 }
7867
7868 #[test]
7869 fn buffer_content_mirrors_textarea_after_paste() {
7870 let mut e = editor_with("hello");
7871 run_keys(&mut e, "yy");
7872 run_keys(&mut e, "p");
7873 let text = e.buffer().lines().join("\n");
7874 assert_eq!(e.buffer.as_string(), text);
7875 }
7876
7877 #[test]
7878 fn buffer_selection_none_in_normal_mode() {
7879 let e = editor_with("foo bar");
7880 assert!(e.buffer_selection().is_none());
7881 }
7882
7883 #[test]
7884 fn buffer_selection_char_in_visual_mode() {
7885 use hjkl_buffer::{Position, Selection};
7886 let mut e = editor_with("hello world");
7887 run_keys(&mut e, "vlll");
7888 assert_eq!(
7889 e.buffer_selection(),
7890 Some(Selection::Char {
7891 anchor: Position::new(0, 0),
7892 head: Position::new(0, 3),
7893 })
7894 );
7895 }
7896
7897 #[test]
7898 fn buffer_selection_line_in_visual_line_mode() {
7899 use hjkl_buffer::Selection;
7900 let mut e = editor_with("a\nb\nc\nd");
7901 run_keys(&mut e, "Vj");
7902 assert_eq!(
7903 e.buffer_selection(),
7904 Some(Selection::Line {
7905 anchor_row: 0,
7906 head_row: 1,
7907 })
7908 );
7909 }
7910
7911 #[test]
7912 fn intern_style_dedups_repeated_styles() {
7913 use ratatui::style::{Color, Style};
7914 let mut e = editor_with("");
7915 let red = Style::default().fg(Color::Red);
7916 let blue = Style::default().fg(Color::Blue);
7917 let id_r1 = e.intern_style(red);
7918 let id_r2 = e.intern_style(red);
7919 let id_b = e.intern_style(blue);
7920 assert_eq!(id_r1, id_r2);
7921 assert_ne!(id_r1, id_b);
7922 assert_eq!(e.style_table().len(), 2);
7923 }
7924
7925 #[test]
7926 fn install_syntax_spans_translates_styled_spans() {
7927 use ratatui::style::{Color, Style};
7928 let mut e = editor_with("SELECT foo");
7929 e.install_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
7930 let by_row = e.buffer.spans();
7931 assert_eq!(by_row.len(), 1);
7932 assert_eq!(by_row[0].len(), 1);
7933 assert_eq!(by_row[0][0].start_byte, 0);
7934 assert_eq!(by_row[0][0].end_byte, 6);
7935 let id = by_row[0][0].style;
7936 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
7937 }
7938
7939 #[test]
7940 fn install_syntax_spans_clamps_sentinel_end() {
7941 use ratatui::style::{Color, Style};
7942 let mut e = editor_with("hello");
7943 e.install_syntax_spans(vec![vec![(
7944 0,
7945 usize::MAX,
7946 Style::default().fg(Color::Blue),
7947 )]]);
7948 let by_row = e.buffer.spans();
7949 assert_eq!(by_row[0][0].end_byte, 5);
7950 }
7951
7952 #[test]
7953 fn install_syntax_spans_drops_zero_width() {
7954 use ratatui::style::{Color, Style};
7955 let mut e = editor_with("abc");
7956 e.install_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
7957 assert!(e.buffer.spans()[0].is_empty());
7958 }
7959
7960 #[test]
7961 fn named_register_yank_into_a_then_paste_from_a() {
7962 let mut e = editor_with("hello world\nsecond");
7963 run_keys(&mut e, "\"ayw");
7964 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
7966 run_keys(&mut e, "j0\"aP");
7968 assert_eq!(e.buffer().lines()[1], "hello second");
7969 }
7970
7971 #[test]
7972 fn capital_r_overstrikes_chars() {
7973 let mut e = editor_with("hello");
7974 e.jump_cursor(0, 0);
7975 run_keys(&mut e, "RXY<Esc>");
7976 assert_eq!(e.buffer().lines()[0], "XYllo");
7978 }
7979
7980 #[test]
7981 fn capital_r_at_eol_appends() {
7982 let mut e = editor_with("hi");
7983 e.jump_cursor(0, 1);
7984 run_keys(&mut e, "RXYZ<Esc>");
7986 assert_eq!(e.buffer().lines()[0], "hXYZ");
7987 }
7988
7989 #[test]
7990 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
7991 let mut e = editor_with("abc");
7995 e.jump_cursor(0, 0);
7996 run_keys(&mut e, "RX<Esc>");
7997 assert_eq!(e.buffer().lines()[0], "Xbc");
7998 }
7999
8000 #[test]
8001 fn ctrl_r_in_insert_pastes_named_register() {
8002 let mut e = editor_with("hello world");
8003 run_keys(&mut e, "\"ayw");
8005 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8006 run_keys(&mut e, "o");
8008 assert_eq!(e.vim_mode(), VimMode::Insert);
8009 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8010 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
8011 assert_eq!(e.buffer().lines()[1], "hello ");
8012 assert_eq!(e.cursor(), (1, 6));
8014 assert_eq!(e.vim_mode(), VimMode::Insert);
8016 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
8017 assert_eq!(e.buffer().lines()[1], "hello X");
8018 }
8019
8020 #[test]
8021 fn ctrl_r_with_unnamed_register() {
8022 let mut e = editor_with("foo");
8023 run_keys(&mut e, "yiw");
8024 run_keys(&mut e, "A ");
8025 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8027 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
8028 assert_eq!(e.buffer().lines()[0], "foo foo");
8029 }
8030
8031 #[test]
8032 fn ctrl_r_unknown_selector_is_no_op() {
8033 let mut e = editor_with("abc");
8034 run_keys(&mut e, "A");
8035 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8036 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
8039 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
8040 assert_eq!(e.buffer().lines()[0], "abcZ");
8041 }
8042
8043 #[test]
8044 fn ctrl_r_multiline_register_pastes_with_newlines() {
8045 let mut e = editor_with("alpha\nbeta\ngamma");
8046 run_keys(&mut e, "\"byy");
8048 run_keys(&mut e, "j\"byy");
8049 run_keys(&mut e, "ggVj\"by");
8053 let payload = e.registers().read('b').unwrap().text.clone();
8054 assert!(payload.contains('\n'));
8055 run_keys(&mut e, "Go");
8056 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8057 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
8058 let total_lines = e.buffer().lines().len();
8061 assert!(total_lines >= 5);
8062 }
8063
8064 #[test]
8065 fn yank_zero_holds_last_yank_after_delete() {
8066 let mut e = editor_with("hello world");
8067 run_keys(&mut e, "yw");
8068 let yanked = e.registers().read('0').unwrap().text.clone();
8069 assert!(!yanked.is_empty());
8070 run_keys(&mut e, "dw");
8072 assert_eq!(e.registers().read('0').unwrap().text, yanked);
8073 assert!(!e.registers().read('1').unwrap().text.is_empty());
8075 }
8076
8077 #[test]
8078 fn delete_ring_rotates_through_one_through_nine() {
8079 let mut e = editor_with("a b c d e f g h i j");
8080 for _ in 0..3 {
8082 run_keys(&mut e, "dw");
8083 }
8084 let r1 = e.registers().read('1').unwrap().text.clone();
8086 let r2 = e.registers().read('2').unwrap().text.clone();
8087 let r3 = e.registers().read('3').unwrap().text.clone();
8088 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
8089 assert_ne!(r1, r2);
8090 assert_ne!(r2, r3);
8091 }
8092
8093 #[test]
8094 fn capital_register_appends_to_lowercase() {
8095 let mut e = editor_with("foo bar");
8096 run_keys(&mut e, "\"ayw");
8097 let first = e.registers().read('a').unwrap().text.clone();
8098 assert!(first.contains("foo"));
8099 run_keys(&mut e, "w\"Ayw");
8101 let combined = e.registers().read('a').unwrap().text.clone();
8102 assert!(combined.starts_with(&first));
8103 assert!(combined.contains("bar"));
8104 }
8105
8106 #[test]
8107 fn zf_in_visual_line_creates_closed_fold() {
8108 let mut e = editor_with("a\nb\nc\nd\ne");
8109 e.jump_cursor(1, 0);
8111 run_keys(&mut e, "Vjjzf");
8112 assert_eq!(e.buffer().folds().len(), 1);
8113 let f = e.buffer().folds()[0];
8114 assert_eq!(f.start_row, 1);
8115 assert_eq!(f.end_row, 3);
8116 assert!(f.closed);
8117 }
8118
8119 #[test]
8120 fn zfj_in_normal_creates_two_row_fold() {
8121 let mut e = editor_with("a\nb\nc\nd\ne");
8122 e.jump_cursor(1, 0);
8123 run_keys(&mut e, "zfj");
8124 assert_eq!(e.buffer().folds().len(), 1);
8125 let f = e.buffer().folds()[0];
8126 assert_eq!(f.start_row, 1);
8127 assert_eq!(f.end_row, 2);
8128 assert!(f.closed);
8129 assert_eq!(e.cursor().0, 1);
8131 }
8132
8133 #[test]
8134 fn zf_with_count_folds_count_rows() {
8135 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8136 e.jump_cursor(0, 0);
8137 run_keys(&mut e, "zf3j");
8139 assert_eq!(e.buffer().folds().len(), 1);
8140 let f = e.buffer().folds()[0];
8141 assert_eq!(f.start_row, 0);
8142 assert_eq!(f.end_row, 3);
8143 }
8144
8145 #[test]
8146 fn zfk_folds_upward_range() {
8147 let mut e = editor_with("a\nb\nc\nd\ne");
8148 e.jump_cursor(3, 0);
8149 run_keys(&mut e, "zfk");
8150 let f = e.buffer().folds()[0];
8151 assert_eq!(f.start_row, 2);
8153 assert_eq!(f.end_row, 3);
8154 }
8155
8156 #[test]
8157 fn zf_capital_g_folds_to_bottom() {
8158 let mut e = editor_with("a\nb\nc\nd\ne");
8159 e.jump_cursor(1, 0);
8160 run_keys(&mut e, "zfG");
8162 let f = e.buffer().folds()[0];
8163 assert_eq!(f.start_row, 1);
8164 assert_eq!(f.end_row, 4);
8165 }
8166
8167 #[test]
8168 fn zfgg_folds_to_top_via_operator_pipeline() {
8169 let mut e = editor_with("a\nb\nc\nd\ne");
8170 e.jump_cursor(3, 0);
8171 run_keys(&mut e, "zfgg");
8175 let f = e.buffer().folds()[0];
8176 assert_eq!(f.start_row, 0);
8177 assert_eq!(f.end_row, 3);
8178 }
8179
8180 #[test]
8181 fn zfip_folds_paragraph_via_text_object() {
8182 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
8183 e.jump_cursor(1, 0);
8184 run_keys(&mut e, "zfip");
8186 assert_eq!(e.buffer().folds().len(), 1);
8187 let f = e.buffer().folds()[0];
8188 assert_eq!(f.start_row, 0);
8189 assert_eq!(f.end_row, 2);
8190 }
8191
8192 #[test]
8193 fn zfap_folds_paragraph_with_trailing_blank() {
8194 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
8195 e.jump_cursor(0, 0);
8196 run_keys(&mut e, "zfap");
8198 let f = e.buffer().folds()[0];
8199 assert_eq!(f.start_row, 0);
8200 assert_eq!(f.end_row, 3);
8201 }
8202
8203 #[test]
8204 fn zf_paragraph_motion_folds_to_blank() {
8205 let mut e = editor_with("alpha\nbeta\n\ngamma");
8206 e.jump_cursor(0, 0);
8207 run_keys(&mut e, "zf}");
8209 let f = e.buffer().folds()[0];
8210 assert_eq!(f.start_row, 0);
8211 assert_eq!(f.end_row, 2);
8212 }
8213
8214 #[test]
8215 fn za_toggles_fold_under_cursor() {
8216 let mut e = editor_with("a\nb\nc\nd");
8217 e.buffer_mut().add_fold(1, 2, true);
8218 e.jump_cursor(1, 0);
8219 run_keys(&mut e, "za");
8220 assert!(!e.buffer().folds()[0].closed);
8221 run_keys(&mut e, "za");
8222 assert!(e.buffer().folds()[0].closed);
8223 }
8224
8225 #[test]
8226 fn zr_opens_all_folds_zm_closes_all() {
8227 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8228 e.buffer_mut().add_fold(0, 1, true);
8229 e.buffer_mut().add_fold(2, 3, true);
8230 e.buffer_mut().add_fold(4, 5, true);
8231 run_keys(&mut e, "zR");
8232 assert!(e.buffer().folds().iter().all(|f| !f.closed));
8233 run_keys(&mut e, "zM");
8234 assert!(e.buffer().folds().iter().all(|f| f.closed));
8235 }
8236
8237 #[test]
8238 fn ze_clears_all_folds() {
8239 let mut e = editor_with("a\nb\nc\nd");
8240 e.buffer_mut().add_fold(0, 1, true);
8241 e.buffer_mut().add_fold(2, 3, false);
8242 run_keys(&mut e, "zE");
8243 assert!(e.buffer().folds().is_empty());
8244 }
8245
8246 #[test]
8247 fn g_underscore_jumps_to_last_non_blank() {
8248 let mut e = editor_with("hello world ");
8249 run_keys(&mut e, "g_");
8250 assert_eq!(e.cursor().1, 10);
8252 }
8253
8254 #[test]
8255 fn gj_and_gk_alias_j_and_k() {
8256 let mut e = editor_with("a\nb\nc");
8257 run_keys(&mut e, "gj");
8258 assert_eq!(e.cursor().0, 1);
8259 run_keys(&mut e, "gk");
8260 assert_eq!(e.cursor().0, 0);
8261 }
8262
8263 #[test]
8264 fn paragraph_motions_walk_blank_lines() {
8265 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
8266 run_keys(&mut e, "}");
8267 assert_eq!(e.cursor().0, 2);
8268 run_keys(&mut e, "}");
8269 assert_eq!(e.cursor().0, 5);
8270 run_keys(&mut e, "{");
8271 assert_eq!(e.cursor().0, 2);
8272 }
8273
8274 #[test]
8275 fn gv_reenters_last_visual_selection() {
8276 let mut e = editor_with("alpha\nbeta\ngamma");
8277 run_keys(&mut e, "Vj");
8278 run_keys(&mut e, "<Esc>");
8280 assert_eq!(e.vim_mode(), VimMode::Normal);
8281 run_keys(&mut e, "gv");
8283 assert_eq!(e.vim_mode(), VimMode::VisualLine);
8284 }
8285
8286 #[test]
8287 fn o_in_visual_swaps_anchor_and_cursor() {
8288 let mut e = editor_with("hello world");
8289 run_keys(&mut e, "vllll");
8291 assert_eq!(e.cursor().1, 4);
8292 run_keys(&mut e, "o");
8294 assert_eq!(e.cursor().1, 0);
8295 assert_eq!(e.vim.visual_anchor, (0, 4));
8297 }
8298
8299 #[test]
8300 fn editing_inside_fold_invalidates_it() {
8301 let mut e = editor_with("a\nb\nc\nd");
8302 e.buffer_mut().add_fold(1, 2, true);
8303 e.jump_cursor(1, 0);
8304 run_keys(&mut e, "iX<Esc>");
8306 assert!(e.buffer().folds().is_empty());
8308 }
8309
8310 #[test]
8311 fn zd_removes_fold_under_cursor() {
8312 let mut e = editor_with("a\nb\nc\nd");
8313 e.buffer_mut().add_fold(1, 2, true);
8314 e.jump_cursor(2, 0);
8315 run_keys(&mut e, "zd");
8316 assert!(e.buffer().folds().is_empty());
8317 }
8318
8319 #[test]
8320 fn dot_mark_jumps_to_last_edit_position() {
8321 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8322 e.jump_cursor(2, 0);
8323 run_keys(&mut e, "iX<Esc>");
8325 let after_edit = e.cursor();
8326 run_keys(&mut e, "gg");
8328 assert_eq!(e.cursor().0, 0);
8329 run_keys(&mut e, "'.");
8331 assert_eq!(e.cursor().0, after_edit.0);
8332 }
8333
8334 #[test]
8335 fn quote_quote_returns_to_pre_jump_position() {
8336 let mut e = editor_with_rows(50, 20);
8337 e.jump_cursor(10, 2);
8338 let before = e.cursor();
8339 run_keys(&mut e, "G");
8341 assert_ne!(e.cursor(), before);
8342 run_keys(&mut e, "''");
8344 assert_eq!(e.cursor().0, before.0);
8345 }
8346
8347 #[test]
8348 fn backtick_backtick_restores_exact_pre_jump_pos() {
8349 let mut e = editor_with_rows(50, 20);
8350 e.jump_cursor(7, 3);
8351 let before = e.cursor();
8352 run_keys(&mut e, "G");
8353 run_keys(&mut e, "``");
8354 assert_eq!(e.cursor(), before);
8355 }
8356
8357 #[test]
8358 fn macro_record_and_replay_basic() {
8359 let mut e = editor_with("foo\nbar\nbaz");
8360 run_keys(&mut e, "qaIX<Esc>jq");
8362 assert_eq!(e.buffer().lines()[0], "Xfoo");
8363 run_keys(&mut e, "@a");
8365 assert_eq!(e.buffer().lines()[1], "Xbar");
8366 run_keys(&mut e, "j@@");
8368 assert_eq!(e.buffer().lines()[2], "Xbaz");
8369 }
8370
8371 #[test]
8372 fn macro_count_replays_n_times() {
8373 let mut e = editor_with("a\nb\nc\nd\ne");
8374 run_keys(&mut e, "qajq");
8376 assert_eq!(e.cursor().0, 1);
8377 run_keys(&mut e, "3@a");
8379 assert_eq!(e.cursor().0, 4);
8380 }
8381
8382 #[test]
8383 fn macro_capital_q_appends_to_lowercase_register() {
8384 let mut e = editor_with("hello");
8385 run_keys(&mut e, "qall<Esc>q");
8386 run_keys(&mut e, "qAhh<Esc>q");
8387 let text = e.registers().read('a').unwrap().text.clone();
8390 assert!(text.contains("ll<Esc>"));
8391 assert!(text.contains("hh<Esc>"));
8392 }
8393
8394 #[test]
8395 fn buffer_selection_block_in_visual_block_mode() {
8396 use hjkl_buffer::{Position, Selection};
8397 let mut e = editor_with("aaaa\nbbbb\ncccc");
8398 run_keys(&mut e, "<C-v>jl");
8399 assert_eq!(
8400 e.buffer_selection(),
8401 Some(Selection::Block {
8402 anchor: Position::new(0, 0),
8403 head: Position::new(1, 1),
8404 })
8405 );
8406 }
8407
8408 #[test]
8411 fn n_after_question_mark_keeps_walking_backward() {
8412 let mut e = editor_with("foo bar foo baz foo end");
8415 e.jump_cursor(0, 22);
8416 run_keys(&mut e, "?foo<CR>");
8417 assert_eq!(e.cursor().1, 16);
8418 run_keys(&mut e, "n");
8419 assert_eq!(e.cursor().1, 8);
8420 run_keys(&mut e, "N");
8421 assert_eq!(e.cursor().1, 16);
8422 }
8423
8424 #[test]
8425 fn nested_macro_chord_records_literal_keys() {
8426 let mut e = editor_with("alpha\nbeta\ngamma");
8429 run_keys(&mut e, "qblq");
8431 run_keys(&mut e, "qaIX<Esc>q");
8434 e.jump_cursor(1, 0);
8436 run_keys(&mut e, "@a");
8437 assert_eq!(e.buffer().lines()[1], "Xbeta");
8438 }
8439
8440 #[test]
8441 fn shift_gt_motion_indents_one_line() {
8442 let mut e = editor_with("hello world");
8446 run_keys(&mut e, ">w");
8447 assert_eq!(e.buffer().lines()[0], " hello world");
8448 }
8449
8450 #[test]
8451 fn shift_lt_motion_outdents_one_line() {
8452 let mut e = editor_with(" hello world");
8453 run_keys(&mut e, "<lt>w");
8454 assert_eq!(e.buffer().lines()[0], " hello world");
8456 }
8457
8458 #[test]
8459 fn shift_gt_text_object_indents_paragraph() {
8460 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
8461 e.jump_cursor(0, 0);
8462 run_keys(&mut e, ">ip");
8463 assert_eq!(e.buffer().lines()[0], " alpha");
8464 assert_eq!(e.buffer().lines()[1], " beta");
8465 assert_eq!(e.buffer().lines()[2], " gamma");
8466 assert_eq!(e.buffer().lines()[4], "rest");
8468 }
8469
8470 #[test]
8471 fn ctrl_o_runs_exactly_one_normal_command() {
8472 let mut e = editor_with("alpha beta gamma");
8475 e.jump_cursor(0, 0);
8476 run_keys(&mut e, "i");
8477 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
8478 run_keys(&mut e, "dw");
8479 assert_eq!(e.vim_mode(), VimMode::Insert);
8481 run_keys(&mut e, "X");
8483 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
8484 }
8485
8486 #[test]
8487 fn macro_replay_respects_mode_switching() {
8488 let mut e = editor_with("hi");
8492 run_keys(&mut e, "qaiX<Esc>0q");
8493 assert_eq!(e.vim_mode(), VimMode::Normal);
8494 e.set_content("yo");
8496 run_keys(&mut e, "@a");
8497 assert_eq!(e.vim_mode(), VimMode::Normal);
8498 assert_eq!(e.cursor().1, 0);
8499 assert_eq!(e.buffer().lines()[0], "Xyo");
8500 }
8501
8502 #[test]
8503 fn macro_recorded_text_round_trips_through_register() {
8504 let mut e = editor_with("");
8508 run_keys(&mut e, "qaiX<Esc>q");
8509 let text = e.registers().read('a').unwrap().text.clone();
8510 assert!(text.starts_with("iX"));
8511 run_keys(&mut e, "@a");
8513 assert_eq!(e.buffer().lines()[0], "XX");
8514 }
8515
8516 #[test]
8517 fn dot_after_macro_replays_macros_last_change() {
8518 let mut e = editor_with("ab\ncd\nef");
8521 run_keys(&mut e, "qaIX<Esc>jq");
8524 assert_eq!(e.buffer().lines()[0], "Xab");
8525 run_keys(&mut e, "@a");
8526 assert_eq!(e.buffer().lines()[1], "Xcd");
8527 let row_before_dot = e.cursor().0;
8530 run_keys(&mut e, ".");
8531 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
8532 }
8533}