1use crate::VimMode;
75use crate::input::{Input, Key};
76
77use crate::buf_helpers::{
78 buf_cursor_pos, buf_line, buf_line_bytes, buf_line_chars, buf_lines_to_vec, buf_row_count,
79 buf_set_cursor_pos, buf_set_cursor_rc,
80};
81use crate::editor::Editor;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum Mode {
87 #[default]
88 Normal,
89 Insert,
90 Visual,
91 VisualLine,
92 VisualBlock,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Default)]
100enum Pending {
101 #[default]
102 None,
103 Op { op: Operator, count1: usize },
106 OpTextObj {
108 op: Operator,
109 count1: usize,
110 inner: bool,
111 },
112 OpG { op: Operator, count1: usize },
114 G,
116 Find { forward: bool, till: bool },
118 OpFind {
120 op: Operator,
121 count1: usize,
122 forward: bool,
123 till: bool,
124 },
125 Replace,
127 VisualTextObj { inner: bool },
130 Z,
132 SetMark,
134 GotoMarkLine,
137 GotoMarkChar,
140 SelectRegister,
143 RecordMacroTarget,
147 PlayMacroTarget { count: usize },
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum Operator {
157 Delete,
158 Change,
159 Yank,
160 Uppercase,
163 Lowercase,
165 ToggleCase,
169 Indent,
174 Outdent,
177 Fold,
181 Reflow,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum Motion {
190 Left,
191 Right,
192 Up,
193 Down,
194 WordFwd,
195 BigWordFwd,
196 WordBack,
197 BigWordBack,
198 WordEnd,
199 BigWordEnd,
200 WordEndBack,
202 BigWordEndBack,
204 LineStart,
205 FirstNonBlank,
206 LineEnd,
207 FileTop,
208 FileBottom,
209 Find {
210 ch: char,
211 forward: bool,
212 till: bool,
213 },
214 FindRepeat {
215 reverse: bool,
216 },
217 MatchBracket,
218 WordAtCursor {
219 forward: bool,
220 whole_word: bool,
223 },
224 SearchNext {
226 reverse: bool,
227 },
228 ViewportTop,
230 ViewportMiddle,
232 ViewportBottom,
234 LastNonBlank,
236 LineMiddle,
239 ParagraphPrev,
241 ParagraphNext,
243 SentencePrev,
245 SentenceNext,
247 ScreenDown,
250 ScreenUp,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum TextObject {
256 Word {
257 big: bool,
258 },
259 Quote(char),
260 Bracket(char),
261 Paragraph,
262 XmlTag,
266 Sentence,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum MotionKind {
276 Exclusive,
278 Inclusive,
280 Linewise,
282}
283
284#[derive(Debug, Clone)]
288enum LastChange {
289 OpMotion {
291 op: Operator,
292 motion: Motion,
293 count: usize,
294 inserted: Option<String>,
295 },
296 OpTextObj {
298 op: Operator,
299 obj: TextObject,
300 inner: bool,
301 inserted: Option<String>,
302 },
303 LineOp {
305 op: Operator,
306 count: usize,
307 inserted: Option<String>,
308 },
309 CharDel { forward: bool, count: usize },
311 ReplaceChar { ch: char, count: usize },
313 ToggleCase { count: usize },
315 JoinLine { count: usize },
317 Paste { before: bool, count: usize },
319 DeleteToEol { inserted: Option<String> },
321 OpenLine { above: bool, inserted: String },
323 InsertAt {
325 entry: InsertEntry,
326 inserted: String,
327 count: usize,
328 },
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332enum InsertEntry {
333 I,
334 A,
335 ShiftI,
336 ShiftA,
337}
338
339#[derive(Default)]
342pub struct VimState {
343 mode: Mode,
344 pending: Pending,
345 count: usize,
346 last_find: Option<(char, bool, bool)>,
348 last_change: Option<LastChange>,
349 insert_session: Option<InsertSession>,
351 pub(super) visual_anchor: (usize, usize),
355 pub(super) visual_line_anchor: usize,
357 pub(super) block_anchor: (usize, usize),
360 pub(super) block_vcol: usize,
366 pub(super) yank_linewise: bool,
368 pub(super) pending_register: Option<char>,
371 pub(super) recording_macro: Option<char>,
375 pub(super) recording_keys: Vec<crate::input::Input>,
380 pub(super) replaying_macro: bool,
383 pub(super) last_macro: Option<char>,
385 pub(super) last_edit_pos: Option<(usize, usize)>,
388 pub(super) last_insert_pos: Option<(usize, usize)>,
392 pub(super) change_list: Vec<(usize, usize)>,
396 pub(super) change_list_cursor: Option<usize>,
399 pub(super) last_visual: Option<LastVisual>,
402 pub(super) viewport_pinned: bool,
406 replaying: bool,
408 one_shot_normal: bool,
411 pub(super) search_prompt: Option<SearchPrompt>,
413 pub(super) last_search: Option<String>,
417 pub(super) last_search_forward: bool,
421 pub(super) jump_back: Vec<(usize, usize)>,
426 pub(super) jump_fwd: Vec<(usize, usize)>,
429 pub(super) insert_pending_register: bool,
433 pub(super) change_mark_start: Option<(usize, usize)>,
439 pub(super) search_history: Vec<String>,
443 pub(super) search_history_cursor: Option<usize>,
448 pub(super) last_input_at: Option<std::time::Instant>,
457 pub(super) last_input_host_at: Option<core::time::Duration>,
461}
462
463const SEARCH_HISTORY_MAX: usize = 100;
464pub(crate) const CHANGE_LIST_MAX: usize = 100;
465
466#[derive(Debug, Clone)]
469pub struct SearchPrompt {
470 pub text: String,
471 pub cursor: usize,
472 pub forward: bool,
473}
474
475#[derive(Debug, Clone)]
476struct InsertSession {
477 count: usize,
478 row_min: usize,
480 row_max: usize,
481 before_lines: Vec<String>,
485 reason: InsertReason,
486}
487
488#[derive(Debug, Clone)]
489enum InsertReason {
490 Enter(InsertEntry),
492 Open { above: bool },
494 AfterChange,
497 DeleteToEol,
499 ReplayOnly,
502 BlockEdge { top: usize, bot: usize, col: usize },
506 BlockChange { top: usize, bot: usize, col: usize },
511 Replace,
515}
516
517#[derive(Debug, Clone, Copy)]
527pub(super) struct LastVisual {
528 pub mode: Mode,
529 pub anchor: (usize, usize),
530 pub cursor: (usize, usize),
531 pub block_vcol: usize,
532}
533
534impl VimState {
535 pub fn public_mode(&self) -> VimMode {
536 match self.mode {
537 Mode::Normal => VimMode::Normal,
538 Mode::Insert => VimMode::Insert,
539 Mode::Visual => VimMode::Visual,
540 Mode::VisualLine => VimMode::VisualLine,
541 Mode::VisualBlock => VimMode::VisualBlock,
542 }
543 }
544
545 pub fn force_normal(&mut self) {
546 self.mode = Mode::Normal;
547 self.pending = Pending::None;
548 self.count = 0;
549 self.insert_session = None;
550 }
551
552 pub(crate) fn clear_pending_prefix(&mut self) {
562 self.pending = Pending::None;
563 self.count = 0;
564 self.pending_register = None;
565 self.insert_pending_register = false;
566 }
567
568 pub fn is_visual(&self) -> bool {
569 matches!(
570 self.mode,
571 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
572 )
573 }
574
575 pub fn is_visual_char(&self) -> bool {
576 self.mode == Mode::Visual
577 }
578
579 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
580 self.visual_anchor = anchor;
581 self.mode = Mode::Visual;
582 }
583
584 pub(crate) fn pending_count_val(&self) -> Option<u32> {
587 if self.count == 0 {
588 None
589 } else {
590 Some(self.count as u32)
591 }
592 }
593
594 pub(crate) fn is_chord_pending(&self) -> bool {
597 !matches!(self.pending, Pending::None)
598 }
599
600 pub(crate) fn pending_op_char(&self) -> Option<char> {
604 let op = match &self.pending {
605 Pending::Op { op, .. }
606 | Pending::OpTextObj { op, .. }
607 | Pending::OpG { op, .. }
608 | Pending::OpFind { op, .. } => Some(*op),
609 _ => None,
610 };
611 op.map(|o| match o {
612 Operator::Delete => 'd',
613 Operator::Change => 'c',
614 Operator::Yank => 'y',
615 Operator::Uppercase => 'U',
616 Operator::Lowercase => 'u',
617 Operator::ToggleCase => '~',
618 Operator::Indent => '>',
619 Operator::Outdent => '<',
620 Operator::Fold => 'z',
621 Operator::Reflow => 'q',
622 })
623 }
624}
625
626fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
632 ed.vim.search_prompt = Some(SearchPrompt {
633 text: String::new(),
634 cursor: 0,
635 forward,
636 });
637 ed.vim.search_history_cursor = None;
638 ed.set_search_pattern(None);
642}
643
644fn push_search_pattern<H: crate::types::Host>(
649 ed: &mut Editor<hjkl_buffer::Buffer, H>,
650 pattern: &str,
651) {
652 let compiled = if pattern.is_empty() {
653 None
654 } else {
655 let case_insensitive = ed.settings().ignore_case
662 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
663 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
664 std::borrow::Cow::Owned(format!("(?i){pattern}"))
665 } else {
666 std::borrow::Cow::Borrowed(pattern)
667 };
668 regex::Regex::new(&effective).ok()
669 };
670 let wrap = ed.settings().wrapscan;
671 ed.set_search_pattern(compiled);
675 ed.search_state_mut().wrap_around = wrap;
676}
677
678fn step_search_prompt<H: crate::types::Host>(
679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
680 input: Input,
681) -> bool {
682 let history_dir = match (input.key, input.ctrl) {
686 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
687 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
688 _ => None,
689 };
690 if let Some(dir) = history_dir {
691 walk_search_history(ed, dir);
692 return true;
693 }
694 match input.key {
695 Key::Esc => {
696 let text = ed
699 .vim
700 .search_prompt
701 .take()
702 .map(|p| p.text)
703 .unwrap_or_default();
704 if !text.is_empty() {
705 ed.vim.last_search = Some(text);
706 }
707 ed.vim.search_history_cursor = None;
708 }
709 Key::Enter => {
710 let prompt = ed.vim.search_prompt.take();
711 if let Some(p) = prompt {
712 let pattern = if p.text.is_empty() {
715 ed.vim.last_search.clone()
716 } else {
717 Some(p.text.clone())
718 };
719 if let Some(pattern) = pattern {
720 push_search_pattern(ed, &pattern);
721 let pre = ed.cursor();
722 if p.forward {
723 ed.search_advance_forward(true);
724 } else {
725 ed.search_advance_backward(true);
726 }
727 ed.push_buffer_cursor_to_textarea();
728 if ed.cursor() != pre {
729 push_jump(ed, pre);
730 }
731 record_search_history(ed, &pattern);
732 ed.vim.last_search = Some(pattern);
733 ed.vim.last_search_forward = p.forward;
734 }
735 }
736 ed.vim.search_history_cursor = None;
737 }
738 Key::Backspace => {
739 ed.vim.search_history_cursor = None;
740 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
741 if p.text.pop().is_some() {
742 p.cursor = p.text.chars().count();
743 Some(p.text.clone())
744 } else {
745 None
746 }
747 });
748 if let Some(text) = new_text {
749 push_search_pattern(ed, &text);
750 }
751 }
752 Key::Char(c) => {
753 ed.vim.search_history_cursor = None;
754 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
755 p.text.push(c);
756 p.cursor = p.text.chars().count();
757 p.text.clone()
758 });
759 if let Some(text) = new_text {
760 push_search_pattern(ed, &text);
761 }
762 }
763 _ => {}
764 }
765 true
766}
767
768fn walk_change_list<H: crate::types::Host>(
772 ed: &mut Editor<hjkl_buffer::Buffer, H>,
773 dir: isize,
774 count: usize,
775) {
776 if ed.vim.change_list.is_empty() {
777 return;
778 }
779 let len = ed.vim.change_list.len();
780 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
781 (None, -1) => len as isize - 1,
782 (None, 1) => return, (Some(i), -1) => i as isize - 1,
784 (Some(i), 1) => i as isize + 1,
785 _ => return,
786 };
787 for _ in 1..count {
788 let next = idx + dir;
789 if next < 0 || next >= len as isize {
790 break;
791 }
792 idx = next;
793 }
794 if idx < 0 || idx >= len as isize {
795 return;
796 }
797 let idx = idx as usize;
798 ed.vim.change_list_cursor = Some(idx);
799 let (row, col) = ed.vim.change_list[idx];
800 ed.jump_cursor(row, col);
801}
802
803fn record_search_history<H: crate::types::Host>(
807 ed: &mut Editor<hjkl_buffer::Buffer, H>,
808 pattern: &str,
809) {
810 if pattern.is_empty() {
811 return;
812 }
813 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
814 return;
815 }
816 ed.vim.search_history.push(pattern.to_string());
817 let len = ed.vim.search_history.len();
818 if len > SEARCH_HISTORY_MAX {
819 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
820 }
821}
822
823fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
829 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
830 return;
831 }
832 let len = ed.vim.search_history.len();
833 let next_idx = match (ed.vim.search_history_cursor, dir) {
834 (None, -1) => Some(len - 1),
835 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
837 (Some(i), 1) if i + 1 < len => Some(i + 1),
838 _ => None,
839 };
840 let Some(idx) = next_idx else {
841 return;
842 };
843 ed.vim.search_history_cursor = Some(idx);
844 let text = ed.vim.search_history[idx].clone();
845 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
846 prompt.cursor = text.chars().count();
847 prompt.text = text.clone();
848 }
849 push_search_pattern(ed, &text);
850}
851
852pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
853 ed.sync_buffer_content_from_textarea();
858 let now = std::time::Instant::now();
866 let host_now = ed.host.now();
867 let timed_out = match ed.vim.last_input_host_at {
868 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
869 None => false,
870 };
871 if timed_out {
872 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
873 || ed.vim.count != 0
874 || ed.vim.pending_register.is_some()
875 || ed.vim.insert_pending_register;
876 if chord_in_flight {
877 ed.vim.clear_pending_prefix();
878 }
879 }
880 ed.vim.last_input_at = Some(now);
881 ed.vim.last_input_host_at = Some(host_now);
882 if ed.vim.recording_macro.is_some()
887 && !ed.vim.replaying_macro
888 && matches!(ed.vim.pending, Pending::None)
889 && ed.vim.mode != Mode::Insert
890 && input.key == Key::Char('q')
891 && !input.ctrl
892 && !input.alt
893 {
894 let reg = ed.vim.recording_macro.take().unwrap();
895 let keys = std::mem::take(&mut ed.vim.recording_keys);
896 let text = crate::input::encode_macro(&keys);
897 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
898 return true;
899 }
900 if ed.vim.search_prompt.is_some() {
902 return step_search_prompt(ed, input);
903 }
904 let pending_was_macro_chord = matches!(
908 ed.vim.pending,
909 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
910 );
911 let was_insert = ed.vim.mode == Mode::Insert;
912 let pre_visual_snapshot = match ed.vim.mode {
915 Mode::Visual => Some(LastVisual {
916 mode: Mode::Visual,
917 anchor: ed.vim.visual_anchor,
918 cursor: ed.cursor(),
919 block_vcol: 0,
920 }),
921 Mode::VisualLine => Some(LastVisual {
922 mode: Mode::VisualLine,
923 anchor: (ed.vim.visual_line_anchor, 0),
924 cursor: ed.cursor(),
925 block_vcol: 0,
926 }),
927 Mode::VisualBlock => Some(LastVisual {
928 mode: Mode::VisualBlock,
929 anchor: ed.vim.block_anchor,
930 cursor: ed.cursor(),
931 block_vcol: ed.vim.block_vcol,
932 }),
933 _ => None,
934 };
935 let consumed = match ed.vim.mode {
936 Mode::Insert => step_insert(ed, input),
937 _ => step_normal(ed, input),
938 };
939 if let Some(snap) = pre_visual_snapshot
940 && !matches!(
941 ed.vim.mode,
942 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
943 )
944 {
945 let (lo, hi) = match snap.mode {
961 Mode::Visual => {
962 if snap.anchor <= snap.cursor {
963 (snap.anchor, snap.cursor)
964 } else {
965 (snap.cursor, snap.anchor)
966 }
967 }
968 Mode::VisualLine => {
969 let r_lo = snap.anchor.0.min(snap.cursor.0);
970 let r_hi = snap.anchor.0.max(snap.cursor.0);
971 let last_col = ed
972 .buffer()
973 .lines()
974 .get(r_hi)
975 .map(|l| l.chars().count().saturating_sub(1))
976 .unwrap_or(0);
977 ((r_lo, 0), (r_hi, last_col))
978 }
979 Mode::VisualBlock => {
980 let (r1, c1) = snap.anchor;
981 let (r2, c2) = snap.cursor;
982 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
983 }
984 _ => {
985 if snap.anchor <= snap.cursor {
988 (snap.anchor, snap.cursor)
989 } else {
990 (snap.cursor, snap.anchor)
991 }
992 }
993 };
994 ed.set_mark('<', lo);
995 ed.set_mark('>', hi);
996 ed.vim.last_visual = Some(snap);
997 }
998 if !was_insert
1002 && ed.vim.one_shot_normal
1003 && ed.vim.mode == Mode::Normal
1004 && matches!(ed.vim.pending, Pending::None)
1005 {
1006 ed.vim.one_shot_normal = false;
1007 ed.vim.mode = Mode::Insert;
1008 }
1009 ed.sync_buffer_content_from_textarea();
1015 if !ed.vim.viewport_pinned {
1019 ed.ensure_cursor_in_scrolloff();
1020 }
1021 ed.vim.viewport_pinned = false;
1022 if ed.vim.recording_macro.is_some()
1027 && !ed.vim.replaying_macro
1028 && input.key != Key::Char('q')
1029 && !pending_was_macro_chord
1030 {
1031 ed.vim.recording_keys.push(input);
1032 }
1033 consumed
1034}
1035
1036fn step_insert<H: crate::types::Host>(
1039 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1040 input: Input,
1041) -> bool {
1042 if ed.vim.insert_pending_register {
1046 ed.vim.insert_pending_register = false;
1047 if let Key::Char(c) = input.key
1048 && !input.ctrl
1049 {
1050 insert_register_text(ed, c);
1051 }
1052 return true;
1053 }
1054
1055 if input.key == Key::Esc {
1056 finish_insert_session(ed);
1057 ed.vim.mode = Mode::Normal;
1058 let col = ed.cursor().1;
1063 ed.vim.last_insert_pos = Some(ed.cursor());
1067 if col > 0 {
1068 crate::motions::move_left(&mut ed.buffer, 1);
1069 ed.push_buffer_cursor_to_textarea();
1070 }
1071 ed.sticky_col = Some(ed.cursor().1);
1072 return true;
1073 }
1074
1075 if input.ctrl {
1077 match input.key {
1078 Key::Char('w') => {
1079 use hjkl_buffer::{Edit, MotionKind};
1080 ed.sync_buffer_content_from_textarea();
1081 let cursor = buf_cursor_pos(&ed.buffer);
1082 if cursor.row == 0 && cursor.col == 0 {
1083 return true;
1084 }
1085 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1088 let word_start = buf_cursor_pos(&ed.buffer);
1089 if word_start == cursor {
1090 return true;
1091 }
1092 buf_set_cursor_pos(&mut ed.buffer, cursor);
1093 ed.mutate_edit(Edit::DeleteRange {
1094 start: word_start,
1095 end: cursor,
1096 kind: MotionKind::Char,
1097 });
1098 ed.push_buffer_cursor_to_textarea();
1099 return true;
1100 }
1101 Key::Char('u') => {
1102 use hjkl_buffer::{Edit, MotionKind, Position};
1103 ed.sync_buffer_content_from_textarea();
1104 let cursor = buf_cursor_pos(&ed.buffer);
1105 if cursor.col > 0 {
1106 ed.mutate_edit(Edit::DeleteRange {
1107 start: Position::new(cursor.row, 0),
1108 end: cursor,
1109 kind: MotionKind::Char,
1110 });
1111 ed.push_buffer_cursor_to_textarea();
1112 }
1113 return true;
1114 }
1115 Key::Char('h') => {
1116 use hjkl_buffer::{Edit, MotionKind, Position};
1117 ed.sync_buffer_content_from_textarea();
1118 let cursor = buf_cursor_pos(&ed.buffer);
1119 if cursor.col > 0 {
1120 ed.mutate_edit(Edit::DeleteRange {
1121 start: Position::new(cursor.row, cursor.col - 1),
1122 end: cursor,
1123 kind: MotionKind::Char,
1124 });
1125 } else if cursor.row > 0 {
1126 let prev_row = cursor.row - 1;
1127 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1128 ed.mutate_edit(Edit::JoinLines {
1129 row: prev_row,
1130 count: 1,
1131 with_space: false,
1132 });
1133 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1134 }
1135 ed.push_buffer_cursor_to_textarea();
1136 return true;
1137 }
1138 Key::Char('o') => {
1139 ed.vim.one_shot_normal = true;
1142 ed.vim.mode = Mode::Normal;
1143 return true;
1144 }
1145 Key::Char('r') => {
1146 ed.vim.insert_pending_register = true;
1149 return true;
1150 }
1151 Key::Char('t') => {
1152 let (row, col) = ed.cursor();
1157 let sw = ed.settings().shiftwidth;
1158 indent_rows(ed, row, row, 1);
1159 ed.jump_cursor(row, col + sw);
1160 return true;
1161 }
1162 Key::Char('d') => {
1163 let (row, col) = ed.cursor();
1167 let before_len = buf_line_bytes(&ed.buffer, row);
1168 outdent_rows(ed, row, row, 1);
1169 let after_len = buf_line_bytes(&ed.buffer, row);
1170 let stripped = before_len.saturating_sub(after_len);
1171 let new_col = col.saturating_sub(stripped);
1172 ed.jump_cursor(row, new_col);
1173 return true;
1174 }
1175 _ => {}
1176 }
1177 }
1178
1179 let (row, _) = ed.cursor();
1182 if let Some(ref mut session) = ed.vim.insert_session {
1183 session.row_min = session.row_min.min(row);
1184 session.row_max = session.row_max.max(row);
1185 }
1186 let mutated = handle_insert_key(ed, input);
1187 if mutated {
1188 ed.mark_content_dirty();
1189 let (row, _) = ed.cursor();
1190 if let Some(ref mut session) = ed.vim.insert_session {
1191 session.row_min = session.row_min.min(row);
1192 session.row_max = session.row_max.max(row);
1193 }
1194 }
1195 true
1196}
1197
1198fn insert_register_text<H: crate::types::Host>(
1203 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1204 selector: char,
1205) {
1206 use hjkl_buffer::Edit;
1207 let text = match ed.registers().read(selector) {
1208 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1209 _ => return,
1210 };
1211 ed.sync_buffer_content_from_textarea();
1212 let cursor = buf_cursor_pos(&ed.buffer);
1213 ed.mutate_edit(Edit::InsertStr {
1214 at: cursor,
1215 text: text.clone(),
1216 });
1217 let mut row = cursor.row;
1220 let mut col = cursor.col;
1221 for ch in text.chars() {
1222 if ch == '\n' {
1223 row += 1;
1224 col = 0;
1225 } else {
1226 col += 1;
1227 }
1228 }
1229 buf_set_cursor_rc(&mut ed.buffer, row, col);
1230 ed.push_buffer_cursor_to_textarea();
1231 ed.mark_content_dirty();
1232 if let Some(ref mut session) = ed.vim.insert_session {
1233 session.row_min = session.row_min.min(row);
1234 session.row_max = session.row_max.max(row);
1235 }
1236}
1237
1238pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1257 if !settings.autoindent {
1258 return String::new();
1259 }
1260 let base: String = prev_line
1262 .chars()
1263 .take_while(|c| *c == ' ' || *c == '\t')
1264 .collect();
1265
1266 if settings.smartindent {
1267 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1271 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1272 let unit = if settings.expandtab {
1273 if settings.softtabstop > 0 {
1274 " ".repeat(settings.softtabstop)
1275 } else {
1276 " ".repeat(settings.shiftwidth)
1277 }
1278 } else {
1279 "\t".to_string()
1280 };
1281 return format!("{base}{unit}");
1282 }
1283 }
1284
1285 base
1286}
1287
1288fn try_dedent_close_bracket<H: crate::types::Host>(
1298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1299 cursor: hjkl_buffer::Position,
1300 ch: char,
1301) -> bool {
1302 use hjkl_buffer::{Edit, MotionKind, Position};
1303
1304 if !ed.settings.smartindent {
1305 return false;
1306 }
1307 if !matches!(ch, '}' | ')' | ']') {
1308 return false;
1309 }
1310
1311 let line = match buf_line(&ed.buffer, cursor.row) {
1312 Some(l) => l.to_string(),
1313 None => return false,
1314 };
1315
1316 let before: String = line.chars().take(cursor.col).collect();
1318 if !before.chars().all(|c| c == ' ' || c == '\t') {
1319 return false;
1320 }
1321 if before.is_empty() {
1322 return false;
1324 }
1325
1326 let unit_len: usize = if ed.settings.expandtab {
1328 if ed.settings.softtabstop > 0 {
1329 ed.settings.softtabstop
1330 } else {
1331 ed.settings.shiftwidth
1332 }
1333 } else {
1334 1
1336 };
1337
1338 let strip_len = if ed.settings.expandtab {
1340 let spaces = before.chars().filter(|c| *c == ' ').count();
1342 if spaces < unit_len {
1343 return false;
1344 }
1345 unit_len
1346 } else {
1347 if !before.starts_with('\t') {
1349 return false;
1350 }
1351 1
1352 };
1353
1354 ed.mutate_edit(Edit::DeleteRange {
1356 start: Position::new(cursor.row, 0),
1357 end: Position::new(cursor.row, strip_len),
1358 kind: MotionKind::Char,
1359 });
1360 let new_col = cursor.col.saturating_sub(strip_len);
1365 ed.mutate_edit(Edit::InsertChar {
1366 at: Position::new(cursor.row, new_col),
1367 ch,
1368 });
1369 true
1370}
1371
1372fn handle_insert_key<H: crate::types::Host>(
1379 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1380 input: Input,
1381) -> bool {
1382 use hjkl_buffer::{Edit, MotionKind, Position};
1383 ed.sync_buffer_content_from_textarea();
1384 let cursor = buf_cursor_pos(&ed.buffer);
1385 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1386 let in_replace = matches!(
1390 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1391 Some(InsertReason::Replace)
1392 );
1393 let mutated = match input.key {
1394 Key::Char(c) if in_replace && cursor.col < line_chars => {
1395 ed.mutate_edit(Edit::DeleteRange {
1396 start: cursor,
1397 end: Position::new(cursor.row, cursor.col + 1),
1398 kind: MotionKind::Char,
1399 });
1400 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1401 true
1402 }
1403 Key::Char(c) => {
1404 if !try_dedent_close_bracket(ed, cursor, c) {
1405 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1406 }
1407 true
1408 }
1409 Key::Enter => {
1410 let prev_line = buf_line(&ed.buffer, cursor.row)
1411 .unwrap_or_default()
1412 .to_string();
1413 let indent = compute_enter_indent(&ed.settings, &prev_line);
1414 let text = format!("\n{indent}");
1415 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1416 true
1417 }
1418 Key::Tab => {
1419 if ed.settings.expandtab {
1420 let sts = ed.settings.softtabstop;
1423 let n = if sts > 0 {
1424 sts - (cursor.col % sts)
1425 } else {
1426 ed.settings.tabstop.max(1)
1427 };
1428 ed.mutate_edit(Edit::InsertStr {
1429 at: cursor,
1430 text: " ".repeat(n),
1431 });
1432 } else {
1433 ed.mutate_edit(Edit::InsertChar {
1434 at: cursor,
1435 ch: '\t',
1436 });
1437 }
1438 true
1439 }
1440 Key::Backspace => {
1441 let sts = ed.settings.softtabstop;
1445 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1446 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1447 let chars: Vec<char> = line.chars().collect();
1448 let run_start = cursor.col - sts;
1449 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1450 ed.mutate_edit(Edit::DeleteRange {
1451 start: Position::new(cursor.row, run_start),
1452 end: cursor,
1453 kind: MotionKind::Char,
1454 });
1455 return true;
1456 }
1457 }
1458 if cursor.col > 0 {
1459 ed.mutate_edit(Edit::DeleteRange {
1460 start: Position::new(cursor.row, cursor.col - 1),
1461 end: cursor,
1462 kind: MotionKind::Char,
1463 });
1464 true
1465 } else if cursor.row > 0 {
1466 let prev_row = cursor.row - 1;
1467 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1468 ed.mutate_edit(Edit::JoinLines {
1469 row: prev_row,
1470 count: 1,
1471 with_space: false,
1472 });
1473 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1474 true
1475 } else {
1476 false
1477 }
1478 }
1479 Key::Delete => {
1480 if cursor.col < line_chars {
1481 ed.mutate_edit(Edit::DeleteRange {
1482 start: cursor,
1483 end: Position::new(cursor.row, cursor.col + 1),
1484 kind: MotionKind::Char,
1485 });
1486 true
1487 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1488 ed.mutate_edit(Edit::JoinLines {
1489 row: cursor.row,
1490 count: 1,
1491 with_space: false,
1492 });
1493 buf_set_cursor_pos(&mut ed.buffer, cursor);
1494 true
1495 } else {
1496 false
1497 }
1498 }
1499 Key::Left => {
1500 crate::motions::move_left(&mut ed.buffer, 1);
1501 break_undo_group_in_insert(ed);
1502 false
1503 }
1504 Key::Right => {
1505 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1508 break_undo_group_in_insert(ed);
1509 false
1510 }
1511 Key::Up => {
1512 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1513 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1514 break_undo_group_in_insert(ed);
1515 false
1516 }
1517 Key::Down => {
1518 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1519 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1520 break_undo_group_in_insert(ed);
1521 false
1522 }
1523 Key::Home => {
1524 crate::motions::move_line_start(&mut ed.buffer);
1525 break_undo_group_in_insert(ed);
1526 false
1527 }
1528 Key::End => {
1529 crate::motions::move_line_end(&mut ed.buffer);
1530 break_undo_group_in_insert(ed);
1531 false
1532 }
1533 Key::PageUp => {
1534 let rows = viewport_full_rows(ed, 1) as isize;
1538 scroll_cursor_rows(ed, -rows);
1539 return false;
1540 }
1541 Key::PageDown => {
1542 let rows = viewport_full_rows(ed, 1) as isize;
1543 scroll_cursor_rows(ed, rows);
1544 return false;
1545 }
1546 _ => false,
1549 };
1550 ed.push_buffer_cursor_to_textarea();
1551 mutated
1552}
1553
1554fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1555 let Some(session) = ed.vim.insert_session.take() else {
1556 return;
1557 };
1558 let lines = buf_lines_to_vec(&ed.buffer);
1559 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1563 let before_end = session
1564 .row_max
1565 .min(session.before_lines.len().saturating_sub(1));
1566 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1567 session.before_lines[session.row_min..=before_end].join("\n")
1568 } else {
1569 String::new()
1570 };
1571 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1572 lines[session.row_min..=after_end].join("\n")
1573 } else {
1574 String::new()
1575 };
1576 let inserted = extract_inserted(&before, &after);
1577 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1578 use hjkl_buffer::{Edit, Position};
1579 for _ in 0..session.count - 1 {
1580 let (row, col) = ed.cursor();
1581 ed.mutate_edit(Edit::InsertStr {
1582 at: Position::new(row, col),
1583 text: inserted.clone(),
1584 });
1585 }
1586 }
1587 fn replicate_block_text<H: crate::types::Host>(
1591 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1592 inserted: &str,
1593 top: usize,
1594 bot: usize,
1595 col: usize,
1596 ) {
1597 use hjkl_buffer::{Edit, Position};
1598 for r in (top + 1)..=bot {
1599 let line_len = buf_line_chars(&ed.buffer, r);
1600 if col > line_len {
1601 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1602 ed.mutate_edit(Edit::InsertStr {
1603 at: Position::new(r, line_len),
1604 text: pad,
1605 });
1606 }
1607 ed.mutate_edit(Edit::InsertStr {
1608 at: Position::new(r, col),
1609 text: inserted.to_string(),
1610 });
1611 }
1612 }
1613
1614 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1615 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1618 replicate_block_text(ed, &inserted, top, bot, col);
1619 buf_set_cursor_rc(&mut ed.buffer, top, col);
1620 ed.push_buffer_cursor_to_textarea();
1621 }
1622 return;
1623 }
1624 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1625 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1629 replicate_block_text(ed, &inserted, top, bot, col);
1630 let ins_chars = inserted.chars().count();
1631 let line_len = buf_line_chars(&ed.buffer, top);
1632 let target_col = (col + ins_chars).min(line_len);
1633 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1634 ed.push_buffer_cursor_to_textarea();
1635 }
1636 return;
1637 }
1638 if ed.vim.replaying {
1639 return;
1640 }
1641 match session.reason {
1642 InsertReason::Enter(entry) => {
1643 ed.vim.last_change = Some(LastChange::InsertAt {
1644 entry,
1645 inserted,
1646 count: session.count,
1647 });
1648 }
1649 InsertReason::Open { above } => {
1650 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1651 }
1652 InsertReason::AfterChange => {
1653 if let Some(
1654 LastChange::OpMotion { inserted: ins, .. }
1655 | LastChange::OpTextObj { inserted: ins, .. }
1656 | LastChange::LineOp { inserted: ins, .. },
1657 ) = ed.vim.last_change.as_mut()
1658 {
1659 *ins = Some(inserted);
1660 }
1661 if let Some(start) = ed.vim.change_mark_start.take() {
1667 let end = ed.cursor();
1668 ed.set_mark('[', start);
1669 ed.set_mark(']', end);
1670 }
1671 }
1672 InsertReason::DeleteToEol => {
1673 ed.vim.last_change = Some(LastChange::DeleteToEol {
1674 inserted: Some(inserted),
1675 });
1676 }
1677 InsertReason::ReplayOnly => {}
1678 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1679 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1680 InsertReason::Replace => {
1681 ed.vim.last_change = Some(LastChange::DeleteToEol {
1686 inserted: Some(inserted),
1687 });
1688 }
1689 }
1690}
1691
1692fn begin_insert<H: crate::types::Host>(
1693 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1694 count: usize,
1695 reason: InsertReason,
1696) {
1697 let record = !matches!(reason, InsertReason::ReplayOnly);
1698 if record {
1699 ed.push_undo();
1700 }
1701 let reason = if ed.vim.replaying {
1702 InsertReason::ReplayOnly
1703 } else {
1704 reason
1705 };
1706 let (row, _) = ed.cursor();
1707 ed.vim.insert_session = Some(InsertSession {
1708 count,
1709 row_min: row,
1710 row_max: row,
1711 before_lines: buf_lines_to_vec(&ed.buffer),
1712 reason,
1713 });
1714 ed.vim.mode = Mode::Insert;
1715}
1716
1717pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1732 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1733) {
1734 if !ed.settings.undo_break_on_motion {
1735 return;
1736 }
1737 if ed.vim.replaying {
1738 return;
1739 }
1740 if ed.vim.insert_session.is_none() {
1741 return;
1742 }
1743 ed.push_undo();
1744 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1745 let mut lines: Vec<String> = Vec::with_capacity(n);
1746 for r in 0..n {
1747 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1748 }
1749 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1750 if let Some(ref mut session) = ed.vim.insert_session {
1751 session.before_lines = lines;
1752 session.row_min = row;
1753 session.row_max = row;
1754 }
1755}
1756
1757fn step_normal<H: crate::types::Host>(
1760 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1761 input: Input,
1762) -> bool {
1763 if let Key::Char(d @ '0'..='9') = input.key
1765 && !input.ctrl
1766 && !input.alt
1767 && !matches!(
1768 ed.vim.pending,
1769 Pending::Replace
1770 | Pending::Find { .. }
1771 | Pending::OpFind { .. }
1772 | Pending::VisualTextObj { .. }
1773 )
1774 && (d != '0' || ed.vim.count > 0)
1775 {
1776 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1777 return true;
1778 }
1779
1780 match std::mem::take(&mut ed.vim.pending) {
1782 Pending::Replace => return handle_replace(ed, input),
1783 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1784 Pending::OpFind {
1785 op,
1786 count1,
1787 forward,
1788 till,
1789 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1790 Pending::G => return handle_after_g(ed, input),
1791 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1792 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1793 Pending::OpTextObj { op, count1, inner } => {
1794 return handle_text_object(ed, input, op, count1, inner);
1795 }
1796 Pending::VisualTextObj { inner } => {
1797 return handle_visual_text_obj(ed, input, inner);
1798 }
1799 Pending::Z => return handle_after_z(ed, input),
1800 Pending::SetMark => return handle_set_mark(ed, input),
1801 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1802 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1803 Pending::SelectRegister => return handle_select_register(ed, input),
1804 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1805 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1806 Pending::None => {}
1807 }
1808
1809 let count = take_count(&mut ed.vim);
1810
1811 match input.key {
1813 Key::Esc => {
1814 ed.vim.force_normal();
1815 return true;
1816 }
1817 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1818 ed.vim.visual_anchor = ed.cursor();
1819 ed.vim.mode = Mode::Visual;
1820 return true;
1821 }
1822 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1823 let (row, _) = ed.cursor();
1824 ed.vim.visual_line_anchor = row;
1825 ed.vim.mode = Mode::VisualLine;
1826 return true;
1827 }
1828 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1829 ed.vim.visual_anchor = ed.cursor();
1830 ed.vim.mode = Mode::Visual;
1831 return true;
1832 }
1833 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1834 let (row, _) = ed.cursor();
1835 ed.vim.visual_line_anchor = row;
1836 ed.vim.mode = Mode::VisualLine;
1837 return true;
1838 }
1839 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1840 let cur = ed.cursor();
1841 ed.vim.block_anchor = cur;
1842 ed.vim.block_vcol = cur.1;
1843 ed.vim.mode = Mode::VisualBlock;
1844 return true;
1845 }
1846 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1847 ed.vim.mode = Mode::Normal;
1849 return true;
1850 }
1851 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1854 Mode::Visual => {
1855 let cur = ed.cursor();
1856 let anchor = ed.vim.visual_anchor;
1857 ed.vim.visual_anchor = cur;
1858 ed.jump_cursor(anchor.0, anchor.1);
1859 return true;
1860 }
1861 Mode::VisualLine => {
1862 let cur_row = ed.cursor().0;
1863 let anchor_row = ed.vim.visual_line_anchor;
1864 ed.vim.visual_line_anchor = cur_row;
1865 ed.jump_cursor(anchor_row, 0);
1866 return true;
1867 }
1868 Mode::VisualBlock => {
1869 let cur = ed.cursor();
1870 let anchor = ed.vim.block_anchor;
1871 ed.vim.block_anchor = cur;
1872 ed.vim.block_vcol = anchor.1;
1873 ed.jump_cursor(anchor.0, anchor.1);
1874 return true;
1875 }
1876 _ => {}
1877 },
1878 _ => {}
1879 }
1880
1881 if ed.vim.is_visual()
1883 && let Some(op) = visual_operator(&input)
1884 {
1885 apply_visual_operator(ed, op);
1886 return true;
1887 }
1888
1889 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1893 match input.key {
1894 Key::Char('r') => {
1895 ed.vim.pending = Pending::Replace;
1896 return true;
1897 }
1898 Key::Char('I') => {
1899 let (top, bot, left, _right) = block_bounds(ed);
1900 ed.jump_cursor(top, left);
1901 ed.vim.mode = Mode::Normal;
1902 begin_insert(
1903 ed,
1904 1,
1905 InsertReason::BlockEdge {
1906 top,
1907 bot,
1908 col: left,
1909 },
1910 );
1911 return true;
1912 }
1913 Key::Char('A') => {
1914 let (top, bot, _left, right) = block_bounds(ed);
1915 let line_len = buf_line_chars(&ed.buffer, top);
1916 let col = (right + 1).min(line_len);
1917 ed.jump_cursor(top, col);
1918 ed.vim.mode = Mode::Normal;
1919 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1920 return true;
1921 }
1922 _ => {}
1923 }
1924 }
1925
1926 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1928 && !input.ctrl
1929 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1930 {
1931 let inner = matches!(input.key, Key::Char('i'));
1932 ed.vim.pending = Pending::VisualTextObj { inner };
1933 return true;
1934 }
1935
1936 if input.ctrl
1941 && let Key::Char(c) = input.key
1942 {
1943 match c {
1944 'd' => {
1945 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1946 return true;
1947 }
1948 'u' => {
1949 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1950 return true;
1951 }
1952 'f' => {
1953 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1954 return true;
1955 }
1956 'b' => {
1957 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1958 return true;
1959 }
1960 'r' => {
1961 do_redo(ed);
1962 return true;
1963 }
1964 'a' if ed.vim.mode == Mode::Normal => {
1965 adjust_number(ed, count.max(1) as i64);
1966 return true;
1967 }
1968 'x' if ed.vim.mode == Mode::Normal => {
1969 adjust_number(ed, -(count.max(1) as i64));
1970 return true;
1971 }
1972 'o' if ed.vim.mode == Mode::Normal => {
1973 for _ in 0..count.max(1) {
1974 jump_back(ed);
1975 }
1976 return true;
1977 }
1978 'i' if ed.vim.mode == Mode::Normal => {
1979 for _ in 0..count.max(1) {
1980 jump_forward(ed);
1981 }
1982 return true;
1983 }
1984 _ => {}
1985 }
1986 }
1987
1988 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1990 for _ in 0..count.max(1) {
1991 jump_forward(ed);
1992 }
1993 return true;
1994 }
1995
1996 if let Some(motion) = parse_motion(&input) {
1998 execute_motion(ed, motion.clone(), count);
1999 if ed.vim.mode == Mode::VisualBlock {
2001 update_block_vcol(ed, &motion);
2002 }
2003 if let Motion::Find { ch, forward, till } = motion {
2004 ed.vim.last_find = Some((ch, forward, till));
2005 }
2006 return true;
2007 }
2008
2009 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
2011 return true;
2012 }
2013
2014 if ed.vim.mode == Mode::Normal
2016 && let Key::Char(op_ch) = input.key
2017 && !input.ctrl
2018 && let Some(op) = char_to_operator(op_ch)
2019 {
2020 ed.vim.pending = Pending::Op { op, count1: count };
2021 return true;
2022 }
2023
2024 if ed.vim.mode == Mode::Normal
2026 && let Some((forward, till)) = find_entry(&input)
2027 {
2028 ed.vim.count = count;
2029 ed.vim.pending = Pending::Find { forward, till };
2030 return true;
2031 }
2032
2033 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
2035 ed.vim.count = count;
2036 ed.vim.pending = Pending::G;
2037 return true;
2038 }
2039
2040 if !input.ctrl
2042 && input.key == Key::Char('z')
2043 && matches!(
2044 ed.vim.mode,
2045 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2046 )
2047 {
2048 ed.vim.pending = Pending::Z;
2049 return true;
2050 }
2051
2052 if !input.ctrl
2058 && matches!(
2059 ed.vim.mode,
2060 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2061 )
2062 && input.key == Key::Char('`')
2063 {
2064 ed.vim.pending = Pending::GotoMarkChar;
2065 return true;
2066 }
2067 if !input.ctrl && ed.vim.mode == Mode::Normal {
2068 match input.key {
2069 Key::Char('m') => {
2070 ed.vim.pending = Pending::SetMark;
2071 return true;
2072 }
2073 Key::Char('\'') => {
2074 ed.vim.pending = Pending::GotoMarkLine;
2075 return true;
2076 }
2077 Key::Char('`') => {
2078 ed.vim.pending = Pending::GotoMarkChar;
2080 return true;
2081 }
2082 Key::Char('"') => {
2083 ed.vim.pending = Pending::SelectRegister;
2086 return true;
2087 }
2088 Key::Char('@') => {
2089 ed.vim.pending = Pending::PlayMacroTarget { count };
2093 return true;
2094 }
2095 Key::Char('q') if ed.vim.recording_macro.is_none() => {
2096 ed.vim.pending = Pending::RecordMacroTarget;
2101 return true;
2102 }
2103 _ => {}
2104 }
2105 }
2106
2107 true
2109}
2110
2111fn handle_set_mark<H: crate::types::Host>(
2112 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2113 input: Input,
2114) -> bool {
2115 if let Key::Char(c) = input.key
2116 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
2117 {
2118 let pos = ed.cursor();
2123 ed.set_mark(c, pos);
2124 }
2125 true
2126}
2127
2128fn handle_select_register<H: crate::types::Host>(
2134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2135 input: Input,
2136) -> bool {
2137 if let Key::Char(c) = input.key {
2138 ed.set_pending_register(c);
2139 }
2140 true
2141}
2142
2143fn handle_record_macro_target<H: crate::types::Host>(
2148 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2149 input: Input,
2150) -> bool {
2151 if let Key::Char(c) = input.key
2152 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2153 {
2154 ed.vim.recording_macro = Some(c);
2155 if c.is_ascii_uppercase() {
2158 let lower = c.to_ascii_lowercase();
2159 let text = ed
2163 .registers()
2164 .read(lower)
2165 .map(|s| s.text.clone())
2166 .unwrap_or_default();
2167 ed.vim.recording_keys = crate::input::decode_macro(&text);
2168 } else {
2169 ed.vim.recording_keys.clear();
2170 }
2171 }
2172 true
2173}
2174
2175fn handle_play_macro_target<H: crate::types::Host>(
2181 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2182 input: Input,
2183 count: usize,
2184) -> bool {
2185 let reg = match input.key {
2186 Key::Char('@') => ed.vim.last_macro,
2187 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2188 Some(c.to_ascii_lowercase())
2189 }
2190 _ => None,
2191 };
2192 let Some(reg) = reg else {
2193 return true;
2194 };
2195 let text = match ed.registers().read(reg) {
2198 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2199 _ => return true,
2200 };
2201 let keys = crate::input::decode_macro(&text);
2202 ed.vim.last_macro = Some(reg);
2203 let times = count.max(1);
2204 let was_replaying = ed.vim.replaying_macro;
2205 ed.vim.replaying_macro = true;
2206 for _ in 0..times {
2207 for k in keys.iter().copied() {
2208 step(ed, k);
2209 }
2210 }
2211 ed.vim.replaying_macro = was_replaying;
2212 true
2213}
2214
2215fn handle_goto_mark<H: crate::types::Host>(
2216 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2217 input: Input,
2218 linewise: bool,
2219) -> bool {
2220 let Key::Char(c) = input.key else {
2221 return true;
2222 };
2223 let target = match c {
2230 'a'..='z' | 'A'..='Z' => ed.mark(c),
2231 '\'' | '`' => ed.vim.jump_back.last().copied(),
2232 '.' => ed.vim.last_edit_pos,
2233 '[' | ']' | '<' | '>' => ed.mark(c),
2238 _ => None,
2239 };
2240 let Some((row, col)) = target else {
2241 return true;
2242 };
2243 let pre = ed.cursor();
2244 let (r, c_clamped) = clamp_pos(ed, (row, col));
2245 if linewise {
2246 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2247 ed.push_buffer_cursor_to_textarea();
2248 move_first_non_whitespace(ed);
2249 } else {
2250 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2251 ed.push_buffer_cursor_to_textarea();
2252 }
2253 if ed.cursor() != pre {
2254 push_jump(ed, pre);
2255 }
2256 ed.sticky_col = Some(ed.cursor().1);
2257 true
2258}
2259
2260fn take_count(vim: &mut VimState) -> usize {
2261 if vim.count > 0 {
2262 let n = vim.count;
2263 vim.count = 0;
2264 n
2265 } else {
2266 1
2267 }
2268}
2269
2270fn char_to_operator(c: char) -> Option<Operator> {
2271 match c {
2272 'd' => Some(Operator::Delete),
2273 'c' => Some(Operator::Change),
2274 'y' => Some(Operator::Yank),
2275 '>' => Some(Operator::Indent),
2276 '<' => Some(Operator::Outdent),
2277 _ => None,
2278 }
2279}
2280
2281fn visual_operator(input: &Input) -> Option<Operator> {
2282 if input.ctrl {
2283 return None;
2284 }
2285 match input.key {
2286 Key::Char('y') => Some(Operator::Yank),
2287 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2288 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2289 Key::Char('U') => Some(Operator::Uppercase),
2291 Key::Char('u') => Some(Operator::Lowercase),
2292 Key::Char('~') => Some(Operator::ToggleCase),
2293 Key::Char('>') => Some(Operator::Indent),
2295 Key::Char('<') => Some(Operator::Outdent),
2296 _ => None,
2297 }
2298}
2299
2300fn find_entry(input: &Input) -> Option<(bool, bool)> {
2301 if input.ctrl {
2302 return None;
2303 }
2304 match input.key {
2305 Key::Char('f') => Some((true, false)),
2306 Key::Char('F') => Some((false, false)),
2307 Key::Char('t') => Some((true, true)),
2308 Key::Char('T') => Some((false, true)),
2309 _ => None,
2310 }
2311}
2312
2313const JUMPLIST_MAX: usize = 100;
2317
2318fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2323 ed.vim.jump_back.push(from);
2324 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2325 ed.vim.jump_back.remove(0);
2326 }
2327 ed.vim.jump_fwd.clear();
2328}
2329
2330fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2333 let Some(target) = ed.vim.jump_back.pop() else {
2334 return;
2335 };
2336 let cur = ed.cursor();
2337 ed.vim.jump_fwd.push(cur);
2338 let (r, c) = clamp_pos(ed, target);
2339 ed.jump_cursor(r, c);
2340 ed.sticky_col = Some(c);
2341}
2342
2343fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2346 let Some(target) = ed.vim.jump_fwd.pop() else {
2347 return;
2348 };
2349 let cur = ed.cursor();
2350 ed.vim.jump_back.push(cur);
2351 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2352 ed.vim.jump_back.remove(0);
2353 }
2354 let (r, c) = clamp_pos(ed, target);
2355 ed.jump_cursor(r, c);
2356 ed.sticky_col = Some(c);
2357}
2358
2359fn clamp_pos<H: crate::types::Host>(
2362 ed: &Editor<hjkl_buffer::Buffer, H>,
2363 pos: (usize, usize),
2364) -> (usize, usize) {
2365 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2366 let r = pos.0.min(last_row);
2367 let line_len = buf_line_chars(&ed.buffer, r);
2368 let c = pos.1.min(line_len.saturating_sub(1));
2369 (r, c)
2370}
2371
2372fn is_big_jump(motion: &Motion) -> bool {
2374 matches!(
2375 motion,
2376 Motion::FileTop
2377 | Motion::FileBottom
2378 | Motion::MatchBracket
2379 | Motion::WordAtCursor { .. }
2380 | Motion::SearchNext { .. }
2381 | Motion::ViewportTop
2382 | Motion::ViewportMiddle
2383 | Motion::ViewportBottom
2384 )
2385}
2386
2387fn viewport_half_rows<H: crate::types::Host>(
2392 ed: &Editor<hjkl_buffer::Buffer, H>,
2393 count: usize,
2394) -> usize {
2395 let h = ed.viewport_height_value() as usize;
2396 (h / 2).max(1).saturating_mul(count.max(1))
2397}
2398
2399fn viewport_full_rows<H: crate::types::Host>(
2402 ed: &Editor<hjkl_buffer::Buffer, H>,
2403 count: usize,
2404) -> usize {
2405 let h = ed.viewport_height_value() as usize;
2406 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2407}
2408
2409fn scroll_cursor_rows<H: crate::types::Host>(
2414 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2415 delta: isize,
2416) {
2417 if delta == 0 {
2418 return;
2419 }
2420 ed.sync_buffer_content_from_textarea();
2421 let (row, _) = ed.cursor();
2422 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2423 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2424 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2425 crate::motions::move_first_non_blank(&mut ed.buffer);
2426 ed.push_buffer_cursor_to_textarea();
2427 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2428}
2429
2430fn parse_motion(input: &Input) -> Option<Motion> {
2433 if input.ctrl {
2434 return None;
2435 }
2436 match input.key {
2437 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2438 Key::Char('l') | Key::Right => Some(Motion::Right),
2439 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2440 Key::Char('k') | Key::Up => Some(Motion::Up),
2441 Key::Char('w') => Some(Motion::WordFwd),
2442 Key::Char('W') => Some(Motion::BigWordFwd),
2443 Key::Char('b') => Some(Motion::WordBack),
2444 Key::Char('B') => Some(Motion::BigWordBack),
2445 Key::Char('e') => Some(Motion::WordEnd),
2446 Key::Char('E') => Some(Motion::BigWordEnd),
2447 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2448 Key::Char('^') => Some(Motion::FirstNonBlank),
2449 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2450 Key::Char('G') => Some(Motion::FileBottom),
2451 Key::Char('%') => Some(Motion::MatchBracket),
2452 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2453 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2454 Key::Char('*') => Some(Motion::WordAtCursor {
2455 forward: true,
2456 whole_word: true,
2457 }),
2458 Key::Char('#') => Some(Motion::WordAtCursor {
2459 forward: false,
2460 whole_word: true,
2461 }),
2462 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2463 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2464 Key::Char('H') => Some(Motion::ViewportTop),
2465 Key::Char('M') => Some(Motion::ViewportMiddle),
2466 Key::Char('L') => Some(Motion::ViewportBottom),
2467 Key::Char('{') => Some(Motion::ParagraphPrev),
2468 Key::Char('}') => Some(Motion::ParagraphNext),
2469 Key::Char('(') => Some(Motion::SentencePrev),
2470 Key::Char(')') => Some(Motion::SentenceNext),
2471 _ => None,
2472 }
2473}
2474
2475pub(crate) fn execute_motion<H: crate::types::Host>(
2478 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2479 motion: Motion,
2480 count: usize,
2481) {
2482 let count = count.max(1);
2483 let motion = match motion {
2485 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2486 Some((ch, forward, till)) => Motion::Find {
2487 ch,
2488 forward: if reverse { !forward } else { forward },
2489 till,
2490 },
2491 None => return,
2492 },
2493 other => other,
2494 };
2495 let pre_pos = ed.cursor();
2496 let pre_col = pre_pos.1;
2497 apply_motion_cursor(ed, &motion, count);
2498 let post_pos = ed.cursor();
2499 if is_big_jump(&motion) && pre_pos != post_pos {
2500 push_jump(ed, pre_pos);
2501 }
2502 apply_sticky_col(ed, &motion, pre_col);
2503 ed.sync_buffer_from_textarea();
2508}
2509
2510pub(crate) fn apply_motion_kind<H: crate::types::Host>(
2519 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2520 kind: hjkl_vim::MotionKind,
2521 count: usize,
2522) {
2523 let count = count.max(1);
2524 match kind {
2525 hjkl_vim::MotionKind::CharLeft => {
2526 execute_motion(ed, Motion::Left, count);
2527 }
2528 hjkl_vim::MotionKind::CharRight => {
2529 execute_motion(ed, Motion::Right, count);
2530 }
2531 hjkl_vim::MotionKind::LineDown => {
2532 execute_motion(ed, Motion::Down, count);
2533 }
2534 hjkl_vim::MotionKind::LineUp => {
2535 execute_motion(ed, Motion::Up, count);
2536 }
2537 hjkl_vim::MotionKind::FirstNonBlankDown => {
2538 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2543 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2544 crate::motions::move_first_non_blank(&mut ed.buffer);
2545 ed.push_buffer_cursor_to_textarea();
2546 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2547 ed.sync_buffer_from_textarea();
2548 }
2549 hjkl_vim::MotionKind::FirstNonBlankUp => {
2550 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2553 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2554 crate::motions::move_first_non_blank(&mut ed.buffer);
2555 ed.push_buffer_cursor_to_textarea();
2556 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2557 ed.sync_buffer_from_textarea();
2558 }
2559 _ => {
2560 }
2564 }
2565}
2566
2567fn apply_sticky_col<H: crate::types::Host>(
2572 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2573 motion: &Motion,
2574 pre_col: usize,
2575) {
2576 if is_vertical_motion(motion) {
2577 let want = ed.sticky_col.unwrap_or(pre_col);
2578 ed.sticky_col = Some(want);
2581 let (row, _) = ed.cursor();
2582 let line_len = buf_line_chars(&ed.buffer, row);
2583 let max_col = line_len.saturating_sub(1);
2587 let target = want.min(max_col);
2588 ed.jump_cursor(row, target);
2589 } else {
2590 ed.sticky_col = Some(ed.cursor().1);
2593 }
2594}
2595
2596fn is_vertical_motion(motion: &Motion) -> bool {
2597 matches!(
2601 motion,
2602 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2603 )
2604}
2605
2606fn apply_motion_cursor<H: crate::types::Host>(
2607 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2608 motion: &Motion,
2609 count: usize,
2610) {
2611 apply_motion_cursor_ctx(ed, motion, count, false)
2612}
2613
2614fn apply_motion_cursor_ctx<H: crate::types::Host>(
2615 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2616 motion: &Motion,
2617 count: usize,
2618 as_operator: bool,
2619) {
2620 match motion {
2621 Motion::Left => {
2622 crate::motions::move_left(&mut ed.buffer, count);
2624 ed.push_buffer_cursor_to_textarea();
2625 }
2626 Motion::Right => {
2627 if as_operator {
2631 crate::motions::move_right_to_end(&mut ed.buffer, count);
2632 } else {
2633 crate::motions::move_right_in_line(&mut ed.buffer, count);
2634 }
2635 ed.push_buffer_cursor_to_textarea();
2636 }
2637 Motion::Up => {
2638 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2642 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2643 ed.push_buffer_cursor_to_textarea();
2644 }
2645 Motion::Down => {
2646 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2647 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2648 ed.push_buffer_cursor_to_textarea();
2649 }
2650 Motion::ScreenUp => {
2651 let v = *ed.host.viewport();
2652 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2653 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2654 ed.push_buffer_cursor_to_textarea();
2655 }
2656 Motion::ScreenDown => {
2657 let v = *ed.host.viewport();
2658 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2659 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2660 ed.push_buffer_cursor_to_textarea();
2661 }
2662 Motion::WordFwd => {
2663 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2664 ed.push_buffer_cursor_to_textarea();
2665 }
2666 Motion::WordBack => {
2667 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2668 ed.push_buffer_cursor_to_textarea();
2669 }
2670 Motion::WordEnd => {
2671 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2672 ed.push_buffer_cursor_to_textarea();
2673 }
2674 Motion::BigWordFwd => {
2675 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2676 ed.push_buffer_cursor_to_textarea();
2677 }
2678 Motion::BigWordBack => {
2679 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2680 ed.push_buffer_cursor_to_textarea();
2681 }
2682 Motion::BigWordEnd => {
2683 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2684 ed.push_buffer_cursor_to_textarea();
2685 }
2686 Motion::WordEndBack => {
2687 crate::motions::move_word_end_back(
2688 &mut ed.buffer,
2689 false,
2690 count,
2691 &ed.settings.iskeyword,
2692 );
2693 ed.push_buffer_cursor_to_textarea();
2694 }
2695 Motion::BigWordEndBack => {
2696 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2697 ed.push_buffer_cursor_to_textarea();
2698 }
2699 Motion::LineStart => {
2700 crate::motions::move_line_start(&mut ed.buffer);
2701 ed.push_buffer_cursor_to_textarea();
2702 }
2703 Motion::FirstNonBlank => {
2704 crate::motions::move_first_non_blank(&mut ed.buffer);
2705 ed.push_buffer_cursor_to_textarea();
2706 }
2707 Motion::LineEnd => {
2708 crate::motions::move_line_end(&mut ed.buffer);
2710 ed.push_buffer_cursor_to_textarea();
2711 }
2712 Motion::FileTop => {
2713 if count > 1 {
2716 crate::motions::move_bottom(&mut ed.buffer, count);
2717 } else {
2718 crate::motions::move_top(&mut ed.buffer);
2719 }
2720 ed.push_buffer_cursor_to_textarea();
2721 }
2722 Motion::FileBottom => {
2723 if count > 1 {
2726 crate::motions::move_bottom(&mut ed.buffer, count);
2727 } else {
2728 crate::motions::move_bottom(&mut ed.buffer, 0);
2729 }
2730 ed.push_buffer_cursor_to_textarea();
2731 }
2732 Motion::Find { ch, forward, till } => {
2733 for _ in 0..count {
2734 if !find_char_on_line(ed, *ch, *forward, *till) {
2735 break;
2736 }
2737 }
2738 }
2739 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2741 let _ = matching_bracket(ed);
2742 }
2743 Motion::WordAtCursor {
2744 forward,
2745 whole_word,
2746 } => {
2747 word_at_cursor_search(ed, *forward, *whole_word, count);
2748 }
2749 Motion::SearchNext { reverse } => {
2750 if let Some(pattern) = ed.vim.last_search.clone() {
2754 push_search_pattern(ed, &pattern);
2755 }
2756 if ed.search_state().pattern.is_none() {
2757 return;
2758 }
2759 let forward = ed.vim.last_search_forward != *reverse;
2763 for _ in 0..count.max(1) {
2764 if forward {
2765 ed.search_advance_forward(true);
2766 } else {
2767 ed.search_advance_backward(true);
2768 }
2769 }
2770 ed.push_buffer_cursor_to_textarea();
2771 }
2772 Motion::ViewportTop => {
2773 let v = *ed.host().viewport();
2774 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2775 ed.push_buffer_cursor_to_textarea();
2776 }
2777 Motion::ViewportMiddle => {
2778 let v = *ed.host().viewport();
2779 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2780 ed.push_buffer_cursor_to_textarea();
2781 }
2782 Motion::ViewportBottom => {
2783 let v = *ed.host().viewport();
2784 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2785 ed.push_buffer_cursor_to_textarea();
2786 }
2787 Motion::LastNonBlank => {
2788 crate::motions::move_last_non_blank(&mut ed.buffer);
2789 ed.push_buffer_cursor_to_textarea();
2790 }
2791 Motion::LineMiddle => {
2792 let row = ed.cursor().0;
2793 let line_chars = buf_line_chars(&ed.buffer, row);
2794 let target = line_chars / 2;
2797 ed.jump_cursor(row, target);
2798 }
2799 Motion::ParagraphPrev => {
2800 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2801 ed.push_buffer_cursor_to_textarea();
2802 }
2803 Motion::ParagraphNext => {
2804 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2805 ed.push_buffer_cursor_to_textarea();
2806 }
2807 Motion::SentencePrev => {
2808 for _ in 0..count.max(1) {
2809 if let Some((row, col)) = sentence_boundary(ed, false) {
2810 ed.jump_cursor(row, col);
2811 }
2812 }
2813 }
2814 Motion::SentenceNext => {
2815 for _ in 0..count.max(1) {
2816 if let Some((row, col)) = sentence_boundary(ed, true) {
2817 ed.jump_cursor(row, col);
2818 }
2819 }
2820 }
2821 }
2822}
2823
2824fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2825 ed.sync_buffer_content_from_textarea();
2831 crate::motions::move_first_non_blank(&mut ed.buffer);
2832 ed.push_buffer_cursor_to_textarea();
2833}
2834
2835fn find_char_on_line<H: crate::types::Host>(
2836 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2837 ch: char,
2838 forward: bool,
2839 till: bool,
2840) -> bool {
2841 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2842 if moved {
2843 ed.push_buffer_cursor_to_textarea();
2844 }
2845 moved
2846}
2847
2848fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2849 let moved = crate::motions::match_bracket(&mut ed.buffer);
2850 if moved {
2851 ed.push_buffer_cursor_to_textarea();
2852 }
2853 moved
2854}
2855
2856fn word_at_cursor_search<H: crate::types::Host>(
2857 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2858 forward: bool,
2859 whole_word: bool,
2860 count: usize,
2861) {
2862 let (row, col) = ed.cursor();
2863 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2864 let chars: Vec<char> = line.chars().collect();
2865 if chars.is_empty() {
2866 return;
2867 }
2868 let spec = ed.settings().iskeyword.clone();
2870 let is_word = |c: char| is_keyword_char(c, &spec);
2871 let mut start = col.min(chars.len().saturating_sub(1));
2872 while start > 0 && is_word(chars[start - 1]) {
2873 start -= 1;
2874 }
2875 let mut end = start;
2876 while end < chars.len() && is_word(chars[end]) {
2877 end += 1;
2878 }
2879 if end <= start {
2880 return;
2881 }
2882 let word: String = chars[start..end].iter().collect();
2883 let escaped = regex_escape(&word);
2884 let pattern = if whole_word {
2885 format!(r"\b{escaped}\b")
2886 } else {
2887 escaped
2888 };
2889 push_search_pattern(ed, &pattern);
2890 if ed.search_state().pattern.is_none() {
2891 return;
2892 }
2893 ed.vim.last_search = Some(pattern);
2895 ed.vim.last_search_forward = forward;
2896 for _ in 0..count.max(1) {
2897 if forward {
2898 ed.search_advance_forward(true);
2899 } else {
2900 ed.search_advance_backward(true);
2901 }
2902 }
2903 ed.push_buffer_cursor_to_textarea();
2904}
2905
2906fn regex_escape(s: &str) -> String {
2907 let mut out = String::with_capacity(s.len());
2908 for c in s.chars() {
2909 if matches!(
2910 c,
2911 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2912 ) {
2913 out.push('\\');
2914 }
2915 out.push(c);
2916 }
2917 out
2918}
2919
2920pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2934 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2935 op: Operator,
2936 motion_key: char,
2937 total_count: usize,
2938) {
2939 let input = Input {
2940 key: Key::Char(motion_key),
2941 ctrl: false,
2942 alt: false,
2943 shift: false,
2944 };
2945 let Some(motion) = parse_motion(&input) else {
2946 return;
2947 };
2948 let motion = match motion {
2949 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2950 Some((ch, forward, till)) => Motion::Find {
2951 ch,
2952 forward: if reverse { !forward } else { forward },
2953 till,
2954 },
2955 None => return,
2956 },
2957 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2959 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2960 m => m,
2961 };
2962 apply_op_with_motion(ed, op, &motion, total_count);
2963 if let Motion::Find { ch, forward, till } = &motion {
2964 ed.vim.last_find = Some((*ch, *forward, *till));
2965 }
2966 if !ed.vim.replaying && op_is_change(op) {
2967 ed.vim.last_change = Some(LastChange::OpMotion {
2968 op,
2969 motion,
2970 count: total_count,
2971 inserted: None,
2972 });
2973 }
2974}
2975
2976pub(crate) fn apply_op_double<H: crate::types::Host>(
2979 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2980 op: Operator,
2981 total_count: usize,
2982) {
2983 execute_line_op(ed, op, total_count);
2984 if !ed.vim.replaying {
2985 ed.vim.last_change = Some(LastChange::LineOp {
2986 op,
2987 count: total_count,
2988 inserted: None,
2989 });
2990 }
2991}
2992
2993fn handle_after_op<H: crate::types::Host>(
2994 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2995 input: Input,
2996 op: Operator,
2997 count1: usize,
2998) -> bool {
2999 if let Key::Char(d @ '0'..='9') = input.key
3001 && !input.ctrl
3002 && (d != '0' || ed.vim.count > 0)
3003 {
3004 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
3005 ed.vim.pending = Pending::Op { op, count1 };
3006 return true;
3007 }
3008
3009 if input.key == Key::Esc {
3011 ed.vim.count = 0;
3012 return true;
3013 }
3014
3015 let double_ch = match op {
3019 Operator::Delete => Some('d'),
3020 Operator::Change => Some('c'),
3021 Operator::Yank => Some('y'),
3022 Operator::Indent => Some('>'),
3023 Operator::Outdent => Some('<'),
3024 Operator::Uppercase => Some('U'),
3025 Operator::Lowercase => Some('u'),
3026 Operator::ToggleCase => Some('~'),
3027 Operator::Fold => None,
3028 Operator::Reflow => Some('q'),
3031 };
3032 if let Key::Char(c) = input.key
3033 && !input.ctrl
3034 && Some(c) == double_ch
3035 {
3036 let count2 = take_count(&mut ed.vim);
3037 let total = count1.max(1) * count2.max(1);
3038 execute_line_op(ed, op, total);
3039 if !ed.vim.replaying {
3040 ed.vim.last_change = Some(LastChange::LineOp {
3041 op,
3042 count: total,
3043 inserted: None,
3044 });
3045 }
3046 return true;
3047 }
3048
3049 if let Key::Char('i') | Key::Char('a') = input.key
3051 && !input.ctrl
3052 {
3053 let inner = matches!(input.key, Key::Char('i'));
3054 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
3055 return true;
3056 }
3057
3058 if input.key == Key::Char('g') && !input.ctrl {
3060 ed.vim.pending = Pending::OpG { op, count1 };
3061 return true;
3062 }
3063
3064 if let Some((forward, till)) = find_entry(&input) {
3066 ed.vim.pending = Pending::OpFind {
3067 op,
3068 count1,
3069 forward,
3070 till,
3071 };
3072 return true;
3073 }
3074
3075 let count2 = take_count(&mut ed.vim);
3077 let total = count1.max(1) * count2.max(1);
3078 if let Some(motion) = parse_motion(&input) {
3079 let motion = match motion {
3080 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3081 Some((ch, forward, till)) => Motion::Find {
3082 ch,
3083 forward: if reverse { !forward } else { forward },
3084 till,
3085 },
3086 None => return true,
3087 },
3088 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3092 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3093 m => m,
3094 };
3095 apply_op_with_motion(ed, op, &motion, total);
3096 if let Motion::Find { ch, forward, till } = &motion {
3097 ed.vim.last_find = Some((*ch, *forward, *till));
3098 }
3099 if !ed.vim.replaying && op_is_change(op) {
3100 ed.vim.last_change = Some(LastChange::OpMotion {
3101 op,
3102 motion,
3103 count: total,
3104 inserted: None,
3105 });
3106 }
3107 return true;
3108 }
3109
3110 true
3112}
3113
3114pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3124 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3125 op: Operator,
3126 ch: char,
3127 total_count: usize,
3128) {
3129 if matches!(
3132 op,
3133 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3134 ) {
3135 let op_char = match op {
3136 Operator::Uppercase => 'U',
3137 Operator::Lowercase => 'u',
3138 Operator::ToggleCase => '~',
3139 _ => unreachable!(),
3140 };
3141 if ch == op_char {
3142 execute_line_op(ed, op, total_count);
3143 if !ed.vim.replaying {
3144 ed.vim.last_change = Some(LastChange::LineOp {
3145 op,
3146 count: total_count,
3147 inserted: None,
3148 });
3149 }
3150 return;
3151 }
3152 }
3153 let motion = match ch {
3154 'g' => Motion::FileTop,
3155 'e' => Motion::WordEndBack,
3156 'E' => Motion::BigWordEndBack,
3157 'j' => Motion::ScreenDown,
3158 'k' => Motion::ScreenUp,
3159 _ => return, };
3161 apply_op_with_motion(ed, op, &motion, total_count);
3162 if !ed.vim.replaying && op_is_change(op) {
3163 ed.vim.last_change = Some(LastChange::OpMotion {
3164 op,
3165 motion,
3166 count: total_count,
3167 inserted: None,
3168 });
3169 }
3170}
3171
3172fn handle_op_after_g<H: crate::types::Host>(
3173 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3174 input: Input,
3175 op: Operator,
3176 count1: usize,
3177) -> bool {
3178 if input.ctrl {
3179 return true;
3180 }
3181 let count2 = take_count(&mut ed.vim);
3182 let total = count1.max(1) * count2.max(1);
3183 if let Key::Char(ch) = input.key {
3184 apply_op_g_inner(ed, op, ch, total);
3185 }
3186 true
3187}
3188
3189fn handle_after_g<H: crate::types::Host>(
3190 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3191 input: Input,
3192) -> bool {
3193 let count = take_count(&mut ed.vim);
3194 if let Key::Char(ch) = input.key {
3197 apply_after_g(ed, ch, count);
3198 }
3199 true
3200}
3201
3202pub(crate) fn apply_after_g<H: crate::types::Host>(
3207 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3208 ch: char,
3209 count: usize,
3210) {
3211 match ch {
3212 'g' => {
3213 let pre = ed.cursor();
3215 if count > 1 {
3216 ed.jump_cursor(count - 1, 0);
3217 } else {
3218 ed.jump_cursor(0, 0);
3219 }
3220 move_first_non_whitespace(ed);
3221 if ed.cursor() != pre {
3222 push_jump(ed, pre);
3223 }
3224 }
3225 'e' => execute_motion(ed, Motion::WordEndBack, count),
3226 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3227 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3229 'M' => execute_motion(ed, Motion::LineMiddle, count),
3231 'v' => {
3233 if let Some(snap) = ed.vim.last_visual {
3234 match snap.mode {
3235 Mode::Visual => {
3236 ed.vim.visual_anchor = snap.anchor;
3237 ed.vim.mode = Mode::Visual;
3238 }
3239 Mode::VisualLine => {
3240 ed.vim.visual_line_anchor = snap.anchor.0;
3241 ed.vim.mode = Mode::VisualLine;
3242 }
3243 Mode::VisualBlock => {
3244 ed.vim.block_anchor = snap.anchor;
3245 ed.vim.block_vcol = snap.block_vcol;
3246 ed.vim.mode = Mode::VisualBlock;
3247 }
3248 _ => {}
3249 }
3250 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3251 }
3252 }
3253 'j' => execute_motion(ed, Motion::ScreenDown, count),
3257 'k' => execute_motion(ed, Motion::ScreenUp, count),
3258 'U' => {
3262 ed.vim.pending = Pending::Op {
3263 op: Operator::Uppercase,
3264 count1: count,
3265 };
3266 }
3267 'u' => {
3268 ed.vim.pending = Pending::Op {
3269 op: Operator::Lowercase,
3270 count1: count,
3271 };
3272 }
3273 '~' => {
3274 ed.vim.pending = Pending::Op {
3275 op: Operator::ToggleCase,
3276 count1: count,
3277 };
3278 }
3279 'q' => {
3280 ed.vim.pending = Pending::Op {
3283 op: Operator::Reflow,
3284 count1: count,
3285 };
3286 }
3287 'J' => {
3288 for _ in 0..count.max(1) {
3290 ed.push_undo();
3291 join_line_raw(ed);
3292 }
3293 if !ed.vim.replaying {
3294 ed.vim.last_change = Some(LastChange::JoinLine {
3295 count: count.max(1),
3296 });
3297 }
3298 }
3299 'd' => {
3300 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3305 }
3306 'i' => {
3311 if let Some((row, col)) = ed.vim.last_insert_pos {
3312 ed.jump_cursor(row, col);
3313 }
3314 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3315 }
3316 ';' => walk_change_list(ed, -1, count.max(1)),
3319 ',' => walk_change_list(ed, 1, count.max(1)),
3320 '*' => execute_motion(
3324 ed,
3325 Motion::WordAtCursor {
3326 forward: true,
3327 whole_word: false,
3328 },
3329 count,
3330 ),
3331 '#' => execute_motion(
3332 ed,
3333 Motion::WordAtCursor {
3334 forward: false,
3335 whole_word: false,
3336 },
3337 count,
3338 ),
3339 _ => {}
3340 }
3341}
3342
3343fn handle_after_z<H: crate::types::Host>(
3344 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3345 input: Input,
3346) -> bool {
3347 let count = take_count(&mut ed.vim);
3348 if let Key::Char(ch) = input.key {
3351 apply_after_z(ed, ch, count);
3352 }
3353 true
3354}
3355
3356pub(crate) fn apply_after_z<H: crate::types::Host>(
3361 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3362 ch: char,
3363 count: usize,
3364) {
3365 use crate::editor::CursorScrollTarget;
3366 let row = ed.cursor().0;
3367 match ch {
3368 'z' => {
3369 ed.scroll_cursor_to(CursorScrollTarget::Center);
3370 ed.vim.viewport_pinned = true;
3371 }
3372 't' => {
3373 ed.scroll_cursor_to(CursorScrollTarget::Top);
3374 ed.vim.viewport_pinned = true;
3375 }
3376 'b' => {
3377 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3378 ed.vim.viewport_pinned = true;
3379 }
3380 'o' => {
3385 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3386 }
3387 'c' => {
3388 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3389 }
3390 'a' => {
3391 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3392 }
3393 'R' => {
3394 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3395 }
3396 'M' => {
3397 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3398 }
3399 'E' => {
3400 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3401 }
3402 'd' => {
3403 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3404 }
3405 'f' => {
3406 if matches!(
3407 ed.vim.mode,
3408 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3409 ) {
3410 let anchor_row = match ed.vim.mode {
3413 Mode::VisualLine => ed.vim.visual_line_anchor,
3414 Mode::VisualBlock => ed.vim.block_anchor.0,
3415 _ => ed.vim.visual_anchor.0,
3416 };
3417 let cur = ed.cursor().0;
3418 let top = anchor_row.min(cur);
3419 let bot = anchor_row.max(cur);
3420 ed.apply_fold_op(crate::types::FoldOp::Add {
3421 start_row: top,
3422 end_row: bot,
3423 closed: true,
3424 });
3425 ed.vim.mode = Mode::Normal;
3426 } else {
3427 ed.vim.pending = Pending::Op {
3432 op: Operator::Fold,
3433 count1: count,
3434 };
3435 }
3436 }
3437 _ => {}
3438 }
3439}
3440
3441fn handle_replace<H: crate::types::Host>(
3442 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3443 input: Input,
3444) -> bool {
3445 if let Key::Char(ch) = input.key {
3446 if ed.vim.mode == Mode::VisualBlock {
3447 block_replace(ed, ch);
3448 return true;
3449 }
3450 let count = take_count(&mut ed.vim);
3451 replace_char(ed, ch, count.max(1));
3452 if !ed.vim.replaying {
3453 ed.vim.last_change = Some(LastChange::ReplaceChar {
3454 ch,
3455 count: count.max(1),
3456 });
3457 }
3458 }
3459 true
3460}
3461
3462fn handle_find_target<H: crate::types::Host>(
3463 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3464 input: Input,
3465 forward: bool,
3466 till: bool,
3467) -> bool {
3468 let Key::Char(ch) = input.key else {
3469 return true;
3470 };
3471 let count = take_count(&mut ed.vim);
3472 apply_find_char(ed, ch, forward, till, count.max(1));
3473 true
3474}
3475
3476pub(crate) fn apply_find_char<H: crate::types::Host>(
3482 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3483 ch: char,
3484 forward: bool,
3485 till: bool,
3486 count: usize,
3487) {
3488 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3489 ed.vim.last_find = Some((ch, forward, till));
3490}
3491
3492pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3498 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3499 op: Operator,
3500 ch: char,
3501 forward: bool,
3502 till: bool,
3503 total_count: usize,
3504) {
3505 let motion = Motion::Find { ch, forward, till };
3506 apply_op_with_motion(ed, op, &motion, total_count);
3507 ed.vim.last_find = Some((ch, forward, till));
3508 if !ed.vim.replaying && op_is_change(op) {
3509 ed.vim.last_change = Some(LastChange::OpMotion {
3510 op,
3511 motion,
3512 count: total_count,
3513 inserted: None,
3514 });
3515 }
3516}
3517
3518fn handle_op_find_target<H: crate::types::Host>(
3519 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3520 input: Input,
3521 op: Operator,
3522 count1: usize,
3523 forward: bool,
3524 till: bool,
3525) -> bool {
3526 let Key::Char(ch) = input.key else {
3527 return true;
3528 };
3529 let count2 = take_count(&mut ed.vim);
3530 let total = count1.max(1) * count2.max(1);
3531 apply_op_find_motion(ed, op, ch, forward, till, total);
3532 true
3533}
3534
3535pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3545 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3546 op: Operator,
3547 ch: char,
3548 inner: bool,
3549 _total_count: usize,
3550) -> bool {
3551 let obj = match ch {
3554 'w' => TextObject::Word { big: false },
3555 'W' => TextObject::Word { big: true },
3556 '"' | '\'' | '`' => TextObject::Quote(ch),
3557 '(' | ')' | 'b' => TextObject::Bracket('('),
3558 '[' | ']' => TextObject::Bracket('['),
3559 '{' | '}' | 'B' => TextObject::Bracket('{'),
3560 '<' | '>' => TextObject::Bracket('<'),
3561 'p' => TextObject::Paragraph,
3562 't' => TextObject::XmlTag,
3563 's' => TextObject::Sentence,
3564 _ => return false,
3565 };
3566 apply_op_with_text_object(ed, op, obj, inner);
3567 if !ed.vim.replaying && op_is_change(op) {
3568 ed.vim.last_change = Some(LastChange::OpTextObj {
3569 op,
3570 obj,
3571 inner,
3572 inserted: None,
3573 });
3574 }
3575 true
3576}
3577
3578fn handle_text_object<H: crate::types::Host>(
3579 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3580 input: Input,
3581 op: Operator,
3582 _count1: usize,
3583 inner: bool,
3584) -> bool {
3585 let Key::Char(ch) = input.key else {
3586 return true;
3587 };
3588 apply_op_text_obj_inner(ed, op, ch, inner, 1);
3591 true
3592}
3593
3594fn handle_visual_text_obj<H: crate::types::Host>(
3595 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3596 input: Input,
3597 inner: bool,
3598) -> bool {
3599 let Key::Char(ch) = input.key else {
3600 return true;
3601 };
3602 let obj = match ch {
3603 'w' => TextObject::Word { big: false },
3604 'W' => TextObject::Word { big: true },
3605 '"' | '\'' | '`' => TextObject::Quote(ch),
3606 '(' | ')' | 'b' => TextObject::Bracket('('),
3607 '[' | ']' => TextObject::Bracket('['),
3608 '{' | '}' | 'B' => TextObject::Bracket('{'),
3609 '<' | '>' => TextObject::Bracket('<'),
3610 'p' => TextObject::Paragraph,
3611 't' => TextObject::XmlTag,
3612 's' => TextObject::Sentence,
3613 _ => return true,
3614 };
3615 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3616 return true;
3617 };
3618 match kind {
3622 MotionKind::Linewise => {
3623 ed.vim.visual_line_anchor = start.0;
3624 ed.vim.mode = Mode::VisualLine;
3625 ed.jump_cursor(end.0, 0);
3626 }
3627 _ => {
3628 ed.vim.mode = Mode::Visual;
3629 ed.vim.visual_anchor = (start.0, start.1);
3630 let (er, ec) = retreat_one(ed, end);
3631 ed.jump_cursor(er, ec);
3632 }
3633 }
3634 true
3635}
3636
3637fn retreat_one<H: crate::types::Host>(
3639 ed: &Editor<hjkl_buffer::Buffer, H>,
3640 pos: (usize, usize),
3641) -> (usize, usize) {
3642 let (r, c) = pos;
3643 if c > 0 {
3644 (r, c - 1)
3645 } else if r > 0 {
3646 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3647 (r - 1, prev_len)
3648 } else {
3649 (0, 0)
3650 }
3651}
3652
3653fn op_is_change(op: Operator) -> bool {
3654 matches!(op, Operator::Delete | Operator::Change)
3655}
3656
3657fn handle_normal_only<H: crate::types::Host>(
3660 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3661 input: &Input,
3662 count: usize,
3663) -> bool {
3664 if input.ctrl {
3665 return false;
3666 }
3667 match input.key {
3668 Key::Char('i') => {
3669 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3670 true
3671 }
3672 Key::Char('I') => {
3673 move_first_non_whitespace(ed);
3674 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3675 true
3676 }
3677 Key::Char('a') => {
3678 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3679 ed.push_buffer_cursor_to_textarea();
3680 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3681 true
3682 }
3683 Key::Char('A') => {
3684 crate::motions::move_line_end(&mut ed.buffer);
3685 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3686 ed.push_buffer_cursor_to_textarea();
3687 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3688 true
3689 }
3690 Key::Char('R') => {
3691 begin_insert(ed, count.max(1), InsertReason::Replace);
3694 true
3695 }
3696 Key::Char('o') => {
3697 use hjkl_buffer::{Edit, Position};
3698 ed.push_undo();
3699 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3702 ed.sync_buffer_content_from_textarea();
3703 let row = buf_cursor_pos(&ed.buffer).row;
3704 let line_chars = buf_line_chars(&ed.buffer, row);
3705 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3708 let indent = compute_enter_indent(&ed.settings, prev_line);
3709 ed.mutate_edit(Edit::InsertStr {
3710 at: Position::new(row, line_chars),
3711 text: format!("\n{indent}"),
3712 });
3713 ed.push_buffer_cursor_to_textarea();
3714 true
3715 }
3716 Key::Char('O') => {
3717 use hjkl_buffer::{Edit, Position};
3718 ed.push_undo();
3719 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3720 ed.sync_buffer_content_from_textarea();
3721 let row = buf_cursor_pos(&ed.buffer).row;
3722 let indent = if row > 0 {
3726 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3727 compute_enter_indent(&ed.settings, above)
3728 } else {
3729 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3730 cur.chars()
3731 .take_while(|c| *c == ' ' || *c == '\t')
3732 .collect::<String>()
3733 };
3734 ed.mutate_edit(Edit::InsertStr {
3735 at: Position::new(row, 0),
3736 text: format!("{indent}\n"),
3737 });
3738 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3743 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3744 let new_row = buf_cursor_pos(&ed.buffer).row;
3745 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3746 ed.push_buffer_cursor_to_textarea();
3747 true
3748 }
3749 Key::Char('x') => {
3750 do_char_delete(ed, true, count.max(1));
3751 if !ed.vim.replaying {
3752 ed.vim.last_change = Some(LastChange::CharDel {
3753 forward: true,
3754 count: count.max(1),
3755 });
3756 }
3757 true
3758 }
3759 Key::Char('X') => {
3760 do_char_delete(ed, false, count.max(1));
3761 if !ed.vim.replaying {
3762 ed.vim.last_change = Some(LastChange::CharDel {
3763 forward: false,
3764 count: count.max(1),
3765 });
3766 }
3767 true
3768 }
3769 Key::Char('~') => {
3770 for _ in 0..count.max(1) {
3771 ed.push_undo();
3772 toggle_case_at_cursor(ed);
3773 }
3774 if !ed.vim.replaying {
3775 ed.vim.last_change = Some(LastChange::ToggleCase {
3776 count: count.max(1),
3777 });
3778 }
3779 true
3780 }
3781 Key::Char('J') => {
3782 for _ in 0..count.max(1) {
3783 ed.push_undo();
3784 join_line(ed);
3785 }
3786 if !ed.vim.replaying {
3787 ed.vim.last_change = Some(LastChange::JoinLine {
3788 count: count.max(1),
3789 });
3790 }
3791 true
3792 }
3793 Key::Char('D') => {
3794 ed.push_undo();
3795 delete_to_eol(ed);
3796 crate::motions::move_left(&mut ed.buffer, 1);
3798 ed.push_buffer_cursor_to_textarea();
3799 if !ed.vim.replaying {
3800 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3801 }
3802 true
3803 }
3804 Key::Char('Y') => {
3805 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3807 true
3808 }
3809 Key::Char('C') => {
3810 ed.push_undo();
3811 delete_to_eol(ed);
3812 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3813 true
3814 }
3815 Key::Char('s') => {
3816 use hjkl_buffer::{Edit, MotionKind, Position};
3817 ed.push_undo();
3818 ed.sync_buffer_content_from_textarea();
3819 for _ in 0..count.max(1) {
3820 let cursor = buf_cursor_pos(&ed.buffer);
3821 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3822 if cursor.col >= line_chars {
3823 break;
3824 }
3825 ed.mutate_edit(Edit::DeleteRange {
3826 start: cursor,
3827 end: Position::new(cursor.row, cursor.col + 1),
3828 kind: MotionKind::Char,
3829 });
3830 }
3831 ed.push_buffer_cursor_to_textarea();
3832 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3833 if !ed.vim.replaying {
3835 ed.vim.last_change = Some(LastChange::OpMotion {
3836 op: Operator::Change,
3837 motion: Motion::Right,
3838 count: count.max(1),
3839 inserted: None,
3840 });
3841 }
3842 true
3843 }
3844 Key::Char('p') => {
3845 do_paste(ed, false, count.max(1));
3846 if !ed.vim.replaying {
3847 ed.vim.last_change = Some(LastChange::Paste {
3848 before: false,
3849 count: count.max(1),
3850 });
3851 }
3852 true
3853 }
3854 Key::Char('P') => {
3855 do_paste(ed, true, count.max(1));
3856 if !ed.vim.replaying {
3857 ed.vim.last_change = Some(LastChange::Paste {
3858 before: true,
3859 count: count.max(1),
3860 });
3861 }
3862 true
3863 }
3864 Key::Char('u') => {
3865 do_undo(ed);
3866 true
3867 }
3868 Key::Char('r') => {
3869 ed.vim.count = count;
3870 ed.vim.pending = Pending::Replace;
3871 true
3872 }
3873 Key::Char('/') => {
3874 enter_search(ed, true);
3875 true
3876 }
3877 Key::Char('?') => {
3878 enter_search(ed, false);
3879 true
3880 }
3881 Key::Char('.') => {
3882 replay_last_change(ed, count);
3883 true
3884 }
3885 _ => false,
3886 }
3887}
3888
3889fn begin_insert_noundo<H: crate::types::Host>(
3891 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3892 count: usize,
3893 reason: InsertReason,
3894) {
3895 let reason = if ed.vim.replaying {
3896 InsertReason::ReplayOnly
3897 } else {
3898 reason
3899 };
3900 let (row, _) = ed.cursor();
3901 ed.vim.insert_session = Some(InsertSession {
3902 count,
3903 row_min: row,
3904 row_max: row,
3905 before_lines: buf_lines_to_vec(&ed.buffer),
3906 reason,
3907 });
3908 ed.vim.mode = Mode::Insert;
3909}
3910
3911fn apply_op_with_motion<H: crate::types::Host>(
3914 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3915 op: Operator,
3916 motion: &Motion,
3917 count: usize,
3918) {
3919 let start = ed.cursor();
3920 apply_motion_cursor_ctx(ed, motion, count, true);
3925 let end = ed.cursor();
3926 let kind = motion_kind(motion);
3927 ed.jump_cursor(start.0, start.1);
3929 run_operator_over_range(ed, op, start, end, kind);
3930}
3931
3932fn apply_op_with_text_object<H: crate::types::Host>(
3933 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3934 op: Operator,
3935 obj: TextObject,
3936 inner: bool,
3937) {
3938 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3939 return;
3940 };
3941 ed.jump_cursor(start.0, start.1);
3942 run_operator_over_range(ed, op, start, end, kind);
3943}
3944
3945fn motion_kind(motion: &Motion) -> MotionKind {
3946 match motion {
3947 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3948 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3949 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3950 MotionKind::Linewise
3951 }
3952 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3953 MotionKind::Inclusive
3954 }
3955 Motion::Find { .. } => MotionKind::Inclusive,
3956 Motion::MatchBracket => MotionKind::Inclusive,
3957 Motion::LineEnd => MotionKind::Inclusive,
3959 _ => MotionKind::Exclusive,
3960 }
3961}
3962
3963fn run_operator_over_range<H: crate::types::Host>(
3964 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3965 op: Operator,
3966 start: (usize, usize),
3967 end: (usize, usize),
3968 kind: MotionKind,
3969) {
3970 let (top, bot) = order(start, end);
3971 if top == bot {
3972 return;
3973 }
3974
3975 match op {
3976 Operator::Yank => {
3977 let text = read_vim_range(ed, top, bot, kind);
3978 if !text.is_empty() {
3979 ed.record_yank_to_host(text.clone());
3980 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3981 }
3982 let rbr = match kind {
3986 MotionKind::Linewise => {
3987 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3988 (bot.0, last_col)
3989 }
3990 MotionKind::Inclusive => (bot.0, bot.1),
3991 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3992 };
3993 ed.set_mark('[', top);
3994 ed.set_mark(']', rbr);
3995 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3996 ed.push_buffer_cursor_to_textarea();
3997 }
3998 Operator::Delete => {
3999 ed.push_undo();
4000 cut_vim_range(ed, top, bot, kind);
4001 if !matches!(kind, MotionKind::Linewise) {
4006 clamp_cursor_to_normal_mode(ed);
4007 }
4008 ed.vim.mode = Mode::Normal;
4009 let pos = ed.cursor();
4013 ed.set_mark('[', pos);
4014 ed.set_mark(']', pos);
4015 }
4016 Operator::Change => {
4017 ed.vim.change_mark_start = Some(top);
4022 ed.push_undo();
4023 cut_vim_range(ed, top, bot, kind);
4024 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4025 }
4026 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4027 apply_case_op_to_selection(ed, op, top, bot, kind);
4028 }
4029 Operator::Indent | Operator::Outdent => {
4030 ed.push_undo();
4033 if op == Operator::Indent {
4034 indent_rows(ed, top.0, bot.0, 1);
4035 } else {
4036 outdent_rows(ed, top.0, bot.0, 1);
4037 }
4038 ed.vim.mode = Mode::Normal;
4039 }
4040 Operator::Fold => {
4041 if bot.0 >= top.0 {
4045 ed.apply_fold_op(crate::types::FoldOp::Add {
4046 start_row: top.0,
4047 end_row: bot.0,
4048 closed: true,
4049 });
4050 }
4051 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4052 ed.push_buffer_cursor_to_textarea();
4053 ed.vim.mode = Mode::Normal;
4054 }
4055 Operator::Reflow => {
4056 ed.push_undo();
4057 reflow_rows(ed, top.0, bot.0);
4058 ed.vim.mode = Mode::Normal;
4059 }
4060 }
4061}
4062
4063fn reflow_rows<H: crate::types::Host>(
4068 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4069 top: usize,
4070 bot: usize,
4071) {
4072 let width = ed.settings().textwidth.max(1);
4073 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4074 let bot = bot.min(lines.len().saturating_sub(1));
4075 if top > bot {
4076 return;
4077 }
4078 let original = lines[top..=bot].to_vec();
4079 let mut wrapped: Vec<String> = Vec::new();
4080 let mut paragraph: Vec<String> = Vec::new();
4081 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4082 if para.is_empty() {
4083 return;
4084 }
4085 let words = para.join(" ");
4086 let mut current = String::new();
4087 for word in words.split_whitespace() {
4088 let extra = if current.is_empty() {
4089 word.chars().count()
4090 } else {
4091 current.chars().count() + 1 + word.chars().count()
4092 };
4093 if extra > width && !current.is_empty() {
4094 out.push(std::mem::take(&mut current));
4095 current.push_str(word);
4096 } else if current.is_empty() {
4097 current.push_str(word);
4098 } else {
4099 current.push(' ');
4100 current.push_str(word);
4101 }
4102 }
4103 if !current.is_empty() {
4104 out.push(current);
4105 }
4106 para.clear();
4107 };
4108 for line in &original {
4109 if line.trim().is_empty() {
4110 flush(&mut paragraph, &mut wrapped, width);
4111 wrapped.push(String::new());
4112 } else {
4113 paragraph.push(line.clone());
4114 }
4115 }
4116 flush(&mut paragraph, &mut wrapped, width);
4117
4118 let after: Vec<String> = lines.split_off(bot + 1);
4120 lines.truncate(top);
4121 lines.extend(wrapped);
4122 lines.extend(after);
4123 ed.restore(lines, (top, 0));
4124 ed.mark_content_dirty();
4125}
4126
4127fn apply_case_op_to_selection<H: crate::types::Host>(
4133 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4134 op: Operator,
4135 top: (usize, usize),
4136 bot: (usize, usize),
4137 kind: MotionKind,
4138) {
4139 use hjkl_buffer::Edit;
4140 ed.push_undo();
4141 let saved_yank = ed.yank().to_string();
4142 let saved_yank_linewise = ed.vim.yank_linewise;
4143 let selection = cut_vim_range(ed, top, bot, kind);
4144 let transformed = match op {
4145 Operator::Uppercase => selection.to_uppercase(),
4146 Operator::Lowercase => selection.to_lowercase(),
4147 Operator::ToggleCase => toggle_case_str(&selection),
4148 _ => unreachable!(),
4149 };
4150 if !transformed.is_empty() {
4151 let cursor = buf_cursor_pos(&ed.buffer);
4152 ed.mutate_edit(Edit::InsertStr {
4153 at: cursor,
4154 text: transformed,
4155 });
4156 }
4157 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4158 ed.push_buffer_cursor_to_textarea();
4159 ed.set_yank(saved_yank);
4160 ed.vim.yank_linewise = saved_yank_linewise;
4161 ed.vim.mode = Mode::Normal;
4162}
4163
4164fn indent_rows<H: crate::types::Host>(
4169 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4170 top: usize,
4171 bot: usize,
4172 count: usize,
4173) {
4174 ed.sync_buffer_content_from_textarea();
4175 let width = ed.settings().shiftwidth * count.max(1);
4176 let pad: String = " ".repeat(width);
4177 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4178 let bot = bot.min(lines.len().saturating_sub(1));
4179 for line in lines.iter_mut().take(bot + 1).skip(top) {
4180 if !line.is_empty() {
4181 line.insert_str(0, &pad);
4182 }
4183 }
4184 ed.restore(lines, (top, 0));
4187 move_first_non_whitespace(ed);
4188}
4189
4190fn outdent_rows<H: crate::types::Host>(
4194 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4195 top: usize,
4196 bot: usize,
4197 count: usize,
4198) {
4199 ed.sync_buffer_content_from_textarea();
4200 let width = ed.settings().shiftwidth * count.max(1);
4201 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4202 let bot = bot.min(lines.len().saturating_sub(1));
4203 for line in lines.iter_mut().take(bot + 1).skip(top) {
4204 let strip: usize = line
4205 .chars()
4206 .take(width)
4207 .take_while(|c| *c == ' ' || *c == '\t')
4208 .count();
4209 if strip > 0 {
4210 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4211 line.drain(..byte_len);
4212 }
4213 }
4214 ed.restore(lines, (top, 0));
4215 move_first_non_whitespace(ed);
4216}
4217
4218fn toggle_case_str(s: &str) -> String {
4219 s.chars()
4220 .map(|c| {
4221 if c.is_lowercase() {
4222 c.to_uppercase().next().unwrap_or(c)
4223 } else if c.is_uppercase() {
4224 c.to_lowercase().next().unwrap_or(c)
4225 } else {
4226 c
4227 }
4228 })
4229 .collect()
4230}
4231
4232fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4233 if a <= b { (a, b) } else { (b, a) }
4234}
4235
4236fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4241 let (row, col) = ed.cursor();
4242 let line_chars = buf_line_chars(&ed.buffer, row);
4243 let max_col = line_chars.saturating_sub(1);
4244 if col > max_col {
4245 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4246 ed.push_buffer_cursor_to_textarea();
4247 }
4248}
4249
4250fn execute_line_op<H: crate::types::Host>(
4253 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4254 op: Operator,
4255 count: usize,
4256) {
4257 let (row, col) = ed.cursor();
4258 let total = buf_row_count(&ed.buffer);
4259 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4260
4261 match op {
4262 Operator::Yank => {
4263 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4265 if !text.is_empty() {
4266 ed.record_yank_to_host(text.clone());
4267 ed.record_yank(text, true);
4268 }
4269 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4272 ed.set_mark('[', (row, 0));
4273 ed.set_mark(']', (end_row, last_col));
4274 buf_set_cursor_rc(&mut ed.buffer, row, col);
4275 ed.push_buffer_cursor_to_textarea();
4276 ed.vim.mode = Mode::Normal;
4277 }
4278 Operator::Delete => {
4279 ed.push_undo();
4280 let deleted_through_last = end_row + 1 >= total;
4281 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4282 let total_after = buf_row_count(&ed.buffer);
4286 let raw_target = if deleted_through_last {
4287 row.saturating_sub(1).min(total_after.saturating_sub(1))
4288 } else {
4289 row.min(total_after.saturating_sub(1))
4290 };
4291 let target_row = if raw_target > 0
4297 && raw_target + 1 == total_after
4298 && buf_line(&ed.buffer, raw_target)
4299 .map(str::is_empty)
4300 .unwrap_or(false)
4301 {
4302 raw_target - 1
4303 } else {
4304 raw_target
4305 };
4306 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4307 ed.push_buffer_cursor_to_textarea();
4308 move_first_non_whitespace(ed);
4309 ed.sticky_col = Some(ed.cursor().1);
4310 ed.vim.mode = Mode::Normal;
4311 let pos = ed.cursor();
4314 ed.set_mark('[', pos);
4315 ed.set_mark(']', pos);
4316 }
4317 Operator::Change => {
4318 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4322 ed.vim.change_mark_start = Some((row, 0));
4324 ed.push_undo();
4325 ed.sync_buffer_content_from_textarea();
4326 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4328 if end_row > row {
4329 ed.mutate_edit(Edit::DeleteRange {
4330 start: Position::new(row + 1, 0),
4331 end: Position::new(end_row, 0),
4332 kind: BufKind::Line,
4333 });
4334 }
4335 let line_chars = buf_line_chars(&ed.buffer, row);
4336 if line_chars > 0 {
4337 ed.mutate_edit(Edit::DeleteRange {
4338 start: Position::new(row, 0),
4339 end: Position::new(row, line_chars),
4340 kind: BufKind::Char,
4341 });
4342 }
4343 if !payload.is_empty() {
4344 ed.record_yank_to_host(payload.clone());
4345 ed.record_delete(payload, true);
4346 }
4347 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4348 ed.push_buffer_cursor_to_textarea();
4349 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4350 }
4351 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4352 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4356 move_first_non_whitespace(ed);
4359 }
4360 Operator::Indent | Operator::Outdent => {
4361 ed.push_undo();
4363 if op == Operator::Indent {
4364 indent_rows(ed, row, end_row, 1);
4365 } else {
4366 outdent_rows(ed, row, end_row, 1);
4367 }
4368 ed.sticky_col = Some(ed.cursor().1);
4369 ed.vim.mode = Mode::Normal;
4370 }
4371 Operator::Fold => unreachable!("Fold has no line-op double"),
4373 Operator::Reflow => {
4374 ed.push_undo();
4376 reflow_rows(ed, row, end_row);
4377 move_first_non_whitespace(ed);
4378 ed.sticky_col = Some(ed.cursor().1);
4379 ed.vim.mode = Mode::Normal;
4380 }
4381 }
4382}
4383
4384fn apply_visual_operator<H: crate::types::Host>(
4387 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4388 op: Operator,
4389) {
4390 match ed.vim.mode {
4391 Mode::VisualLine => {
4392 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4393 let top = cursor_row.min(ed.vim.visual_line_anchor);
4394 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4395 ed.vim.yank_linewise = true;
4396 match op {
4397 Operator::Yank => {
4398 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4399 if !text.is_empty() {
4400 ed.record_yank_to_host(text.clone());
4401 ed.record_yank(text, true);
4402 }
4403 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4404 ed.push_buffer_cursor_to_textarea();
4405 ed.vim.mode = Mode::Normal;
4406 }
4407 Operator::Delete => {
4408 ed.push_undo();
4409 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4410 ed.vim.mode = Mode::Normal;
4411 }
4412 Operator::Change => {
4413 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4416 ed.push_undo();
4417 ed.sync_buffer_content_from_textarea();
4418 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4419 if bot > top {
4420 ed.mutate_edit(Edit::DeleteRange {
4421 start: Position::new(top + 1, 0),
4422 end: Position::new(bot, 0),
4423 kind: BufKind::Line,
4424 });
4425 }
4426 let line_chars = buf_line_chars(&ed.buffer, top);
4427 if line_chars > 0 {
4428 ed.mutate_edit(Edit::DeleteRange {
4429 start: Position::new(top, 0),
4430 end: Position::new(top, line_chars),
4431 kind: BufKind::Char,
4432 });
4433 }
4434 if !payload.is_empty() {
4435 ed.record_yank_to_host(payload.clone());
4436 ed.record_delete(payload, true);
4437 }
4438 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4439 ed.push_buffer_cursor_to_textarea();
4440 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4441 }
4442 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4443 let bot = buf_cursor_pos(&ed.buffer)
4444 .row
4445 .max(ed.vim.visual_line_anchor);
4446 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4447 move_first_non_whitespace(ed);
4448 }
4449 Operator::Indent | Operator::Outdent => {
4450 ed.push_undo();
4451 let (cursor_row, _) = ed.cursor();
4452 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4453 if op == Operator::Indent {
4454 indent_rows(ed, top, bot, 1);
4455 } else {
4456 outdent_rows(ed, top, bot, 1);
4457 }
4458 ed.vim.mode = Mode::Normal;
4459 }
4460 Operator::Reflow => {
4461 ed.push_undo();
4462 let (cursor_row, _) = ed.cursor();
4463 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4464 reflow_rows(ed, top, bot);
4465 ed.vim.mode = Mode::Normal;
4466 }
4467 Operator::Fold => unreachable!("Visual zf takes its own path"),
4470 }
4471 }
4472 Mode::Visual => {
4473 ed.vim.yank_linewise = false;
4474 let anchor = ed.vim.visual_anchor;
4475 let cursor = ed.cursor();
4476 let (top, bot) = order(anchor, cursor);
4477 match op {
4478 Operator::Yank => {
4479 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4480 if !text.is_empty() {
4481 ed.record_yank_to_host(text.clone());
4482 ed.record_yank(text, false);
4483 }
4484 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4485 ed.push_buffer_cursor_to_textarea();
4486 ed.vim.mode = Mode::Normal;
4487 }
4488 Operator::Delete => {
4489 ed.push_undo();
4490 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4491 ed.vim.mode = Mode::Normal;
4492 }
4493 Operator::Change => {
4494 ed.push_undo();
4495 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4496 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4497 }
4498 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4499 let anchor = ed.vim.visual_anchor;
4501 let cursor = ed.cursor();
4502 let (top, bot) = order(anchor, cursor);
4503 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4504 }
4505 Operator::Indent | Operator::Outdent => {
4506 ed.push_undo();
4507 let anchor = ed.vim.visual_anchor;
4508 let cursor = ed.cursor();
4509 let (top, bot) = order(anchor, cursor);
4510 if op == Operator::Indent {
4511 indent_rows(ed, top.0, bot.0, 1);
4512 } else {
4513 outdent_rows(ed, top.0, bot.0, 1);
4514 }
4515 ed.vim.mode = Mode::Normal;
4516 }
4517 Operator::Reflow => {
4518 ed.push_undo();
4519 let anchor = ed.vim.visual_anchor;
4520 let cursor = ed.cursor();
4521 let (top, bot) = order(anchor, cursor);
4522 reflow_rows(ed, top.0, bot.0);
4523 ed.vim.mode = Mode::Normal;
4524 }
4525 Operator::Fold => unreachable!("Visual zf takes its own path"),
4526 }
4527 }
4528 Mode::VisualBlock => apply_block_operator(ed, op),
4529 _ => {}
4530 }
4531}
4532
4533fn block_bounds<H: crate::types::Host>(
4538 ed: &Editor<hjkl_buffer::Buffer, H>,
4539) -> (usize, usize, usize, usize) {
4540 let (ar, ac) = ed.vim.block_anchor;
4541 let (cr, _) = ed.cursor();
4542 let cc = ed.vim.block_vcol;
4543 let top = ar.min(cr);
4544 let bot = ar.max(cr);
4545 let left = ac.min(cc);
4546 let right = ac.max(cc);
4547 (top, bot, left, right)
4548}
4549
4550fn update_block_vcol<H: crate::types::Host>(
4555 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4556 motion: &Motion,
4557) {
4558 match motion {
4559 Motion::Left
4560 | Motion::Right
4561 | Motion::WordFwd
4562 | Motion::BigWordFwd
4563 | Motion::WordBack
4564 | Motion::BigWordBack
4565 | Motion::WordEnd
4566 | Motion::BigWordEnd
4567 | Motion::WordEndBack
4568 | Motion::BigWordEndBack
4569 | Motion::LineStart
4570 | Motion::FirstNonBlank
4571 | Motion::LineEnd
4572 | Motion::Find { .. }
4573 | Motion::FindRepeat { .. }
4574 | Motion::MatchBracket => {
4575 ed.vim.block_vcol = ed.cursor().1;
4576 }
4577 _ => {}
4579 }
4580}
4581
4582fn apply_block_operator<H: crate::types::Host>(
4587 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4588 op: Operator,
4589) {
4590 let (top, bot, left, right) = block_bounds(ed);
4591 let yank = block_yank(ed, top, bot, left, right);
4593
4594 match op {
4595 Operator::Yank => {
4596 if !yank.is_empty() {
4597 ed.record_yank_to_host(yank.clone());
4598 ed.record_yank(yank, false);
4599 }
4600 ed.vim.mode = Mode::Normal;
4601 ed.jump_cursor(top, left);
4602 }
4603 Operator::Delete => {
4604 ed.push_undo();
4605 delete_block_contents(ed, top, bot, left, right);
4606 if !yank.is_empty() {
4607 ed.record_yank_to_host(yank.clone());
4608 ed.record_delete(yank, false);
4609 }
4610 ed.vim.mode = Mode::Normal;
4611 ed.jump_cursor(top, left);
4612 }
4613 Operator::Change => {
4614 ed.push_undo();
4615 delete_block_contents(ed, top, bot, left, right);
4616 if !yank.is_empty() {
4617 ed.record_yank_to_host(yank.clone());
4618 ed.record_delete(yank, false);
4619 }
4620 ed.jump_cursor(top, left);
4621 begin_insert_noundo(
4622 ed,
4623 1,
4624 InsertReason::BlockChange {
4625 top,
4626 bot,
4627 col: left,
4628 },
4629 );
4630 }
4631 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4632 ed.push_undo();
4633 transform_block_case(ed, op, top, bot, left, right);
4634 ed.vim.mode = Mode::Normal;
4635 ed.jump_cursor(top, left);
4636 }
4637 Operator::Indent | Operator::Outdent => {
4638 ed.push_undo();
4642 if op == Operator::Indent {
4643 indent_rows(ed, top, bot, 1);
4644 } else {
4645 outdent_rows(ed, top, bot, 1);
4646 }
4647 ed.vim.mode = Mode::Normal;
4648 }
4649 Operator::Fold => unreachable!("Visual zf takes its own path"),
4650 Operator::Reflow => {
4651 ed.push_undo();
4655 reflow_rows(ed, top, bot);
4656 ed.vim.mode = Mode::Normal;
4657 }
4658 }
4659}
4660
4661fn transform_block_case<H: crate::types::Host>(
4665 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4666 op: Operator,
4667 top: usize,
4668 bot: usize,
4669 left: usize,
4670 right: usize,
4671) {
4672 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4673 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4674 let chars: Vec<char> = lines[r].chars().collect();
4675 if left >= chars.len() {
4676 continue;
4677 }
4678 let end = (right + 1).min(chars.len());
4679 let head: String = chars[..left].iter().collect();
4680 let mid: String = chars[left..end].iter().collect();
4681 let tail: String = chars[end..].iter().collect();
4682 let transformed = match op {
4683 Operator::Uppercase => mid.to_uppercase(),
4684 Operator::Lowercase => mid.to_lowercase(),
4685 Operator::ToggleCase => toggle_case_str(&mid),
4686 _ => mid,
4687 };
4688 lines[r] = format!("{head}{transformed}{tail}");
4689 }
4690 let saved_yank = ed.yank().to_string();
4691 let saved_linewise = ed.vim.yank_linewise;
4692 ed.restore(lines, (top, left));
4693 ed.set_yank(saved_yank);
4694 ed.vim.yank_linewise = saved_linewise;
4695}
4696
4697fn block_yank<H: crate::types::Host>(
4698 ed: &Editor<hjkl_buffer::Buffer, H>,
4699 top: usize,
4700 bot: usize,
4701 left: usize,
4702 right: usize,
4703) -> String {
4704 let lines = buf_lines_to_vec(&ed.buffer);
4705 let mut rows: Vec<String> = Vec::new();
4706 for r in top..=bot {
4707 let line = match lines.get(r) {
4708 Some(l) => l,
4709 None => break,
4710 };
4711 let chars: Vec<char> = line.chars().collect();
4712 let end = (right + 1).min(chars.len());
4713 if left >= chars.len() {
4714 rows.push(String::new());
4715 } else {
4716 rows.push(chars[left..end].iter().collect());
4717 }
4718 }
4719 rows.join("\n")
4720}
4721
4722fn delete_block_contents<H: crate::types::Host>(
4723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4724 top: usize,
4725 bot: usize,
4726 left: usize,
4727 right: usize,
4728) {
4729 use hjkl_buffer::{Edit, MotionKind, Position};
4730 ed.sync_buffer_content_from_textarea();
4731 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4732 if last_row < top {
4733 return;
4734 }
4735 ed.mutate_edit(Edit::DeleteRange {
4736 start: Position::new(top, left),
4737 end: Position::new(last_row, right),
4738 kind: MotionKind::Block,
4739 });
4740 ed.push_buffer_cursor_to_textarea();
4741}
4742
4743fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4745 let (top, bot, left, right) = block_bounds(ed);
4746 ed.push_undo();
4747 ed.sync_buffer_content_from_textarea();
4748 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4749 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4750 let chars: Vec<char> = lines[r].chars().collect();
4751 if left >= chars.len() {
4752 continue;
4753 }
4754 let end = (right + 1).min(chars.len());
4755 let before: String = chars[..left].iter().collect();
4756 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4757 let after: String = chars[end..].iter().collect();
4758 lines[r] = format!("{before}{middle}{after}");
4759 }
4760 reset_textarea_lines(ed, lines);
4761 ed.vim.mode = Mode::Normal;
4762 ed.jump_cursor(top, left);
4763}
4764
4765fn reset_textarea_lines<H: crate::types::Host>(
4769 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4770 lines: Vec<String>,
4771) {
4772 let cursor = ed.cursor();
4773 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4774 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4775 ed.mark_content_dirty();
4776}
4777
4778type Pos = (usize, usize);
4784
4785fn text_object_range<H: crate::types::Host>(
4789 ed: &Editor<hjkl_buffer::Buffer, H>,
4790 obj: TextObject,
4791 inner: bool,
4792) -> Option<(Pos, Pos, MotionKind)> {
4793 match obj {
4794 TextObject::Word { big } => {
4795 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4796 }
4797 TextObject::Quote(q) => {
4798 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4799 }
4800 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4801 TextObject::Paragraph => {
4802 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4803 }
4804 TextObject::XmlTag => {
4805 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4806 }
4807 TextObject::Sentence => {
4808 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4809 }
4810 }
4811}
4812
4813fn sentence_boundary<H: crate::types::Host>(
4817 ed: &Editor<hjkl_buffer::Buffer, H>,
4818 forward: bool,
4819) -> Option<(usize, usize)> {
4820 let lines = buf_lines_to_vec(&ed.buffer);
4821 if lines.is_empty() {
4822 return None;
4823 }
4824 let pos_to_idx = |pos: (usize, usize)| -> usize {
4825 let mut idx = 0;
4826 for line in lines.iter().take(pos.0) {
4827 idx += line.chars().count() + 1;
4828 }
4829 idx + pos.1
4830 };
4831 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4832 for (r, line) in lines.iter().enumerate() {
4833 let len = line.chars().count();
4834 if idx <= len {
4835 return (r, idx);
4836 }
4837 idx -= len + 1;
4838 }
4839 let last = lines.len().saturating_sub(1);
4840 (last, lines[last].chars().count())
4841 };
4842 let mut chars: Vec<char> = Vec::new();
4843 for (r, line) in lines.iter().enumerate() {
4844 chars.extend(line.chars());
4845 if r + 1 < lines.len() {
4846 chars.push('\n');
4847 }
4848 }
4849 if chars.is_empty() {
4850 return None;
4851 }
4852 let total = chars.len();
4853 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4854 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4855
4856 if forward {
4857 let mut i = cursor_idx + 1;
4860 while i < total {
4861 if is_terminator(chars[i]) {
4862 while i + 1 < total && is_terminator(chars[i + 1]) {
4863 i += 1;
4864 }
4865 if i + 1 >= total {
4866 return None;
4867 }
4868 if chars[i + 1].is_whitespace() {
4869 let mut j = i + 1;
4870 while j < total && chars[j].is_whitespace() {
4871 j += 1;
4872 }
4873 if j >= total {
4874 return None;
4875 }
4876 return Some(idx_to_pos(j));
4877 }
4878 }
4879 i += 1;
4880 }
4881 None
4882 } else {
4883 let find_start = |from: usize| -> Option<usize> {
4887 let mut start = from;
4888 while start > 0 {
4889 let prev = chars[start - 1];
4890 if prev.is_whitespace() {
4891 let mut k = start - 1;
4892 while k > 0 && chars[k - 1].is_whitespace() {
4893 k -= 1;
4894 }
4895 if k > 0 && is_terminator(chars[k - 1]) {
4896 break;
4897 }
4898 }
4899 start -= 1;
4900 }
4901 while start < total && chars[start].is_whitespace() {
4902 start += 1;
4903 }
4904 (start < total).then_some(start)
4905 };
4906 let current_start = find_start(cursor_idx)?;
4907 if current_start < cursor_idx {
4908 return Some(idx_to_pos(current_start));
4909 }
4910 let mut k = current_start;
4913 while k > 0 && chars[k - 1].is_whitespace() {
4914 k -= 1;
4915 }
4916 if k == 0 {
4917 return None;
4918 }
4919 let prev_start = find_start(k - 1)?;
4920 Some(idx_to_pos(prev_start))
4921 }
4922}
4923
4924fn sentence_text_object<H: crate::types::Host>(
4930 ed: &Editor<hjkl_buffer::Buffer, H>,
4931 inner: bool,
4932) -> Option<((usize, usize), (usize, usize))> {
4933 let lines = buf_lines_to_vec(&ed.buffer);
4934 if lines.is_empty() {
4935 return None;
4936 }
4937 let pos_to_idx = |pos: (usize, usize)| -> usize {
4940 let mut idx = 0;
4941 for line in lines.iter().take(pos.0) {
4942 idx += line.chars().count() + 1;
4943 }
4944 idx + pos.1
4945 };
4946 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4947 for (r, line) in lines.iter().enumerate() {
4948 let len = line.chars().count();
4949 if idx <= len {
4950 return (r, idx);
4951 }
4952 idx -= len + 1;
4953 }
4954 let last = lines.len().saturating_sub(1);
4955 (last, lines[last].chars().count())
4956 };
4957 let mut chars: Vec<char> = Vec::new();
4958 for (r, line) in lines.iter().enumerate() {
4959 chars.extend(line.chars());
4960 if r + 1 < lines.len() {
4961 chars.push('\n');
4962 }
4963 }
4964 if chars.is_empty() {
4965 return None;
4966 }
4967
4968 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4969 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4970
4971 let mut start = cursor_idx;
4975 while start > 0 {
4976 let prev = chars[start - 1];
4977 if prev.is_whitespace() {
4978 let mut k = start - 1;
4982 while k > 0 && chars[k - 1].is_whitespace() {
4983 k -= 1;
4984 }
4985 if k > 0 && is_terminator(chars[k - 1]) {
4986 break;
4987 }
4988 }
4989 start -= 1;
4990 }
4991 while start < chars.len() && chars[start].is_whitespace() {
4994 start += 1;
4995 }
4996 if start >= chars.len() {
4997 return None;
4998 }
4999
5000 let mut end = start;
5003 while end < chars.len() {
5004 if is_terminator(chars[end]) {
5005 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5007 end += 1;
5008 }
5009 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5012 break;
5013 }
5014 }
5015 end += 1;
5016 }
5017 let end_idx = (end + 1).min(chars.len());
5019
5020 let final_end = if inner {
5021 end_idx
5022 } else {
5023 let mut e = end_idx;
5027 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5028 e += 1;
5029 }
5030 e
5031 };
5032
5033 Some((idx_to_pos(start), idx_to_pos(final_end)))
5034}
5035
5036fn tag_text_object<H: crate::types::Host>(
5040 ed: &Editor<hjkl_buffer::Buffer, H>,
5041 inner: bool,
5042) -> Option<((usize, usize), (usize, usize))> {
5043 let lines = buf_lines_to_vec(&ed.buffer);
5044 if lines.is_empty() {
5045 return None;
5046 }
5047 let pos_to_idx = |pos: (usize, usize)| -> usize {
5051 let mut idx = 0;
5052 for line in lines.iter().take(pos.0) {
5053 idx += line.chars().count() + 1;
5054 }
5055 idx + pos.1
5056 };
5057 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5058 for (r, line) in lines.iter().enumerate() {
5059 let len = line.chars().count();
5060 if idx <= len {
5061 return (r, idx);
5062 }
5063 idx -= len + 1;
5064 }
5065 let last = lines.len().saturating_sub(1);
5066 (last, lines[last].chars().count())
5067 };
5068 let mut chars: Vec<char> = Vec::new();
5069 for (r, line) in lines.iter().enumerate() {
5070 chars.extend(line.chars());
5071 if r + 1 < lines.len() {
5072 chars.push('\n');
5073 }
5074 }
5075 let cursor_idx = pos_to_idx(ed.cursor());
5076
5077 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5085 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5086 let mut i = 0;
5087 while i < chars.len() {
5088 if chars[i] != '<' {
5089 i += 1;
5090 continue;
5091 }
5092 let mut j = i + 1;
5093 while j < chars.len() && chars[j] != '>' {
5094 j += 1;
5095 }
5096 if j >= chars.len() {
5097 break;
5098 }
5099 let inside: String = chars[i + 1..j].iter().collect();
5100 let close_end = j + 1;
5101 let trimmed = inside.trim();
5102 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5103 i = close_end;
5104 continue;
5105 }
5106 if let Some(rest) = trimmed.strip_prefix('/') {
5107 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5108 if !name.is_empty()
5109 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5110 {
5111 let (open_start, content_start, _) = stack[stack_idx].clone();
5112 stack.truncate(stack_idx);
5113 let content_end = i;
5114 let candidate = (open_start, content_start, content_end, close_end);
5115 if cursor_idx >= content_start && cursor_idx <= content_end {
5116 innermost = match innermost {
5117 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5118 Some(candidate)
5119 }
5120 None => Some(candidate),
5121 existing => existing,
5122 };
5123 } else if open_start >= cursor_idx && next_after.is_none() {
5124 next_after = Some(candidate);
5125 }
5126 }
5127 } else if !trimmed.ends_with('/') {
5128 let name: String = trimmed
5129 .split(|c: char| c.is_whitespace() || c == '/')
5130 .next()
5131 .unwrap_or("")
5132 .to_string();
5133 if !name.is_empty() {
5134 stack.push((i, close_end, name));
5135 }
5136 }
5137 i = close_end;
5138 }
5139
5140 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5141 if inner {
5142 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5143 } else {
5144 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5145 }
5146}
5147
5148fn is_wordchar(c: char) -> bool {
5149 c.is_alphanumeric() || c == '_'
5150}
5151
5152pub(crate) use hjkl_buffer::is_keyword_char;
5156
5157fn word_text_object<H: crate::types::Host>(
5158 ed: &Editor<hjkl_buffer::Buffer, H>,
5159 inner: bool,
5160 big: bool,
5161) -> Option<((usize, usize), (usize, usize))> {
5162 let (row, col) = ed.cursor();
5163 let line = buf_line(&ed.buffer, row)?;
5164 let chars: Vec<char> = line.chars().collect();
5165 if chars.is_empty() {
5166 return None;
5167 }
5168 let at = col.min(chars.len().saturating_sub(1));
5169 let classify = |c: char| -> u8 {
5170 if c.is_whitespace() {
5171 0
5172 } else if big || is_wordchar(c) {
5173 1
5174 } else {
5175 2
5176 }
5177 };
5178 let cls = classify(chars[at]);
5179 let mut start = at;
5180 while start > 0 && classify(chars[start - 1]) == cls {
5181 start -= 1;
5182 }
5183 let mut end = at;
5184 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5185 end += 1;
5186 }
5187 let char_byte = |i: usize| {
5189 if i >= chars.len() {
5190 line.len()
5191 } else {
5192 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5193 }
5194 };
5195 let mut start_col = char_byte(start);
5196 let mut end_col = char_byte(end + 1);
5198 if !inner {
5199 let mut t = end + 1;
5201 let mut included_trailing = false;
5202 while t < chars.len() && chars[t].is_whitespace() {
5203 included_trailing = true;
5204 t += 1;
5205 }
5206 if included_trailing {
5207 end_col = char_byte(t);
5208 } else {
5209 let mut s = start;
5210 while s > 0 && chars[s - 1].is_whitespace() {
5211 s -= 1;
5212 }
5213 start_col = char_byte(s);
5214 }
5215 }
5216 Some(((row, start_col), (row, end_col)))
5217}
5218
5219fn quote_text_object<H: crate::types::Host>(
5220 ed: &Editor<hjkl_buffer::Buffer, H>,
5221 q: char,
5222 inner: bool,
5223) -> Option<((usize, usize), (usize, usize))> {
5224 let (row, col) = ed.cursor();
5225 let line = buf_line(&ed.buffer, row)?;
5226 let bytes = line.as_bytes();
5227 let q_byte = q as u8;
5228 let mut positions: Vec<usize> = Vec::new();
5230 for (i, &b) in bytes.iter().enumerate() {
5231 if b == q_byte {
5232 positions.push(i);
5233 }
5234 }
5235 if positions.len() < 2 {
5236 return None;
5237 }
5238 let mut open_idx: Option<usize> = None;
5239 let mut close_idx: Option<usize> = None;
5240 for pair in positions.chunks(2) {
5241 if pair.len() < 2 {
5242 break;
5243 }
5244 if col >= pair[0] && col <= pair[1] {
5245 open_idx = Some(pair[0]);
5246 close_idx = Some(pair[1]);
5247 break;
5248 }
5249 if col < pair[0] {
5250 open_idx = Some(pair[0]);
5251 close_idx = Some(pair[1]);
5252 break;
5253 }
5254 }
5255 let open = open_idx?;
5256 let close = close_idx?;
5257 if inner {
5259 if close <= open + 1 {
5260 return None;
5261 }
5262 Some(((row, open + 1), (row, close)))
5263 } else {
5264 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5271 let mut end = after_close;
5273 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5274 end += 1;
5275 }
5276 Some(((row, open), (row, end)))
5277 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5278 let mut start = open;
5280 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5281 start -= 1;
5282 }
5283 Some(((row, start), (row, close + 1)))
5284 } else {
5285 Some(((row, open), (row, close + 1)))
5286 }
5287 }
5288}
5289
5290fn bracket_text_object<H: crate::types::Host>(
5291 ed: &Editor<hjkl_buffer::Buffer, H>,
5292 open: char,
5293 inner: bool,
5294) -> Option<(Pos, Pos, MotionKind)> {
5295 let close = match open {
5296 '(' => ')',
5297 '[' => ']',
5298 '{' => '}',
5299 '<' => '>',
5300 _ => return None,
5301 };
5302 let (row, col) = ed.cursor();
5303 let lines = buf_lines_to_vec(&ed.buffer);
5304 let lines = lines.as_slice();
5305 let open_pos = find_open_bracket(lines, row, col, open, close)
5310 .or_else(|| find_next_open(lines, row, col, open))?;
5311 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5312 if inner {
5314 if close_pos.0 > open_pos.0 + 1 {
5320 let inner_row_start = open_pos.0 + 1;
5322 let inner_row_end = close_pos.0 - 1;
5323 let end_col = lines
5324 .get(inner_row_end)
5325 .map(|l| l.chars().count())
5326 .unwrap_or(0);
5327 return Some((
5328 (inner_row_start, 0),
5329 (inner_row_end, end_col),
5330 MotionKind::Linewise,
5331 ));
5332 }
5333 let inner_start = advance_pos(lines, open_pos);
5334 if inner_start.0 > close_pos.0
5335 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5336 {
5337 return None;
5338 }
5339 Some((inner_start, close_pos, MotionKind::Exclusive))
5340 } else {
5341 Some((
5342 open_pos,
5343 advance_pos(lines, close_pos),
5344 MotionKind::Exclusive,
5345 ))
5346 }
5347}
5348
5349fn find_open_bracket(
5350 lines: &[String],
5351 row: usize,
5352 col: usize,
5353 open: char,
5354 close: char,
5355) -> Option<(usize, usize)> {
5356 let mut depth: i32 = 0;
5357 let mut r = row;
5358 let mut c = col as isize;
5359 loop {
5360 let cur = &lines[r];
5361 let chars: Vec<char> = cur.chars().collect();
5362 if (c as usize) >= chars.len() {
5366 c = chars.len() as isize - 1;
5367 }
5368 while c >= 0 {
5369 let ch = chars[c as usize];
5370 if ch == close {
5371 depth += 1;
5372 } else if ch == open {
5373 if depth == 0 {
5374 return Some((r, c as usize));
5375 }
5376 depth -= 1;
5377 }
5378 c -= 1;
5379 }
5380 if r == 0 {
5381 return None;
5382 }
5383 r -= 1;
5384 c = lines[r].chars().count() as isize - 1;
5385 }
5386}
5387
5388fn find_close_bracket(
5389 lines: &[String],
5390 row: usize,
5391 start_col: usize,
5392 open: char,
5393 close: char,
5394) -> Option<(usize, usize)> {
5395 let mut depth: i32 = 0;
5396 let mut r = row;
5397 let mut c = start_col;
5398 loop {
5399 let cur = &lines[r];
5400 let chars: Vec<char> = cur.chars().collect();
5401 while c < chars.len() {
5402 let ch = chars[c];
5403 if ch == open {
5404 depth += 1;
5405 } else if ch == close {
5406 if depth == 0 {
5407 return Some((r, c));
5408 }
5409 depth -= 1;
5410 }
5411 c += 1;
5412 }
5413 if r + 1 >= lines.len() {
5414 return None;
5415 }
5416 r += 1;
5417 c = 0;
5418 }
5419}
5420
5421fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5425 let mut r = row;
5426 let mut c = col;
5427 while r < lines.len() {
5428 let chars: Vec<char> = lines[r].chars().collect();
5429 while c < chars.len() {
5430 if chars[c] == open {
5431 return Some((r, c));
5432 }
5433 c += 1;
5434 }
5435 r += 1;
5436 c = 0;
5437 }
5438 None
5439}
5440
5441fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5442 let (r, c) = pos;
5443 let line_len = lines[r].chars().count();
5444 if c < line_len {
5445 (r, c + 1)
5446 } else if r + 1 < lines.len() {
5447 (r + 1, 0)
5448 } else {
5449 pos
5450 }
5451}
5452
5453fn paragraph_text_object<H: crate::types::Host>(
5454 ed: &Editor<hjkl_buffer::Buffer, H>,
5455 inner: bool,
5456) -> Option<((usize, usize), (usize, usize))> {
5457 let (row, _) = ed.cursor();
5458 let lines = buf_lines_to_vec(&ed.buffer);
5459 if lines.is_empty() {
5460 return None;
5461 }
5462 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5464 if is_blank(row) {
5465 return None;
5466 }
5467 let mut top = row;
5468 while top > 0 && !is_blank(top - 1) {
5469 top -= 1;
5470 }
5471 let mut bot = row;
5472 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5473 bot += 1;
5474 }
5475 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5477 bot += 1;
5478 }
5479 let end_col = lines[bot].chars().count();
5480 Some(((top, 0), (bot, end_col)))
5481}
5482
5483fn read_vim_range<H: crate::types::Host>(
5489 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5490 start: (usize, usize),
5491 end: (usize, usize),
5492 kind: MotionKind,
5493) -> String {
5494 let (top, bot) = order(start, end);
5495 ed.sync_buffer_content_from_textarea();
5496 let lines = buf_lines_to_vec(&ed.buffer);
5497 match kind {
5498 MotionKind::Linewise => {
5499 let lo = top.0;
5500 let hi = bot.0.min(lines.len().saturating_sub(1));
5501 let mut text = lines[lo..=hi].join("\n");
5502 text.push('\n');
5503 text
5504 }
5505 MotionKind::Inclusive | MotionKind::Exclusive => {
5506 let inclusive = matches!(kind, MotionKind::Inclusive);
5507 let mut out = String::new();
5509 for row in top.0..=bot.0 {
5510 let line = lines.get(row).map(String::as_str).unwrap_or("");
5511 let lo = if row == top.0 { top.1 } else { 0 };
5512 let hi_unclamped = if row == bot.0 {
5513 if inclusive { bot.1 + 1 } else { bot.1 }
5514 } else {
5515 line.chars().count() + 1
5516 };
5517 let row_chars: Vec<char> = line.chars().collect();
5518 let hi = hi_unclamped.min(row_chars.len());
5519 if lo < hi {
5520 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5521 }
5522 if row < bot.0 {
5523 out.push('\n');
5524 }
5525 }
5526 out
5527 }
5528 }
5529}
5530
5531fn cut_vim_range<H: crate::types::Host>(
5540 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5541 start: (usize, usize),
5542 end: (usize, usize),
5543 kind: MotionKind,
5544) -> String {
5545 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5546 let (top, bot) = order(start, end);
5547 ed.sync_buffer_content_from_textarea();
5548 let (buf_start, buf_end, buf_kind) = match kind {
5549 MotionKind::Linewise => (
5550 Position::new(top.0, 0),
5551 Position::new(bot.0, 0),
5552 BufKind::Line,
5553 ),
5554 MotionKind::Inclusive => {
5555 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5556 let next = if bot.1 < line_chars {
5560 Position::new(bot.0, bot.1 + 1)
5561 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5562 Position::new(bot.0 + 1, 0)
5563 } else {
5564 Position::new(bot.0, line_chars)
5565 };
5566 (Position::new(top.0, top.1), next, BufKind::Char)
5567 }
5568 MotionKind::Exclusive => (
5569 Position::new(top.0, top.1),
5570 Position::new(bot.0, bot.1),
5571 BufKind::Char,
5572 ),
5573 };
5574 let inverse = ed.mutate_edit(Edit::DeleteRange {
5575 start: buf_start,
5576 end: buf_end,
5577 kind: buf_kind,
5578 });
5579 let text = match inverse {
5580 Edit::InsertStr { text, .. } => text,
5581 _ => String::new(),
5582 };
5583 if !text.is_empty() {
5584 ed.record_yank_to_host(text.clone());
5585 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5586 }
5587 ed.push_buffer_cursor_to_textarea();
5588 text
5589}
5590
5591fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5597 use hjkl_buffer::{Edit, MotionKind, Position};
5598 ed.sync_buffer_content_from_textarea();
5599 let cursor = buf_cursor_pos(&ed.buffer);
5600 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5601 if cursor.col >= line_chars {
5602 return;
5603 }
5604 let inverse = ed.mutate_edit(Edit::DeleteRange {
5605 start: cursor,
5606 end: Position::new(cursor.row, line_chars),
5607 kind: MotionKind::Char,
5608 });
5609 if let Edit::InsertStr { text, .. } = inverse
5610 && !text.is_empty()
5611 {
5612 ed.record_yank_to_host(text.clone());
5613 ed.vim.yank_linewise = false;
5614 ed.set_yank(text);
5615 }
5616 buf_set_cursor_pos(&mut ed.buffer, cursor);
5617 ed.push_buffer_cursor_to_textarea();
5618}
5619
5620fn do_char_delete<H: crate::types::Host>(
5621 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5622 forward: bool,
5623 count: usize,
5624) {
5625 use hjkl_buffer::{Edit, MotionKind, Position};
5626 ed.push_undo();
5627 ed.sync_buffer_content_from_textarea();
5628 let mut deleted = String::new();
5631 for _ in 0..count {
5632 let cursor = buf_cursor_pos(&ed.buffer);
5633 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5634 if forward {
5635 if cursor.col >= line_chars {
5638 continue;
5639 }
5640 let inverse = ed.mutate_edit(Edit::DeleteRange {
5641 start: cursor,
5642 end: Position::new(cursor.row, cursor.col + 1),
5643 kind: MotionKind::Char,
5644 });
5645 if let Edit::InsertStr { text, .. } = inverse {
5646 deleted.push_str(&text);
5647 }
5648 } else {
5649 if cursor.col == 0 {
5651 continue;
5652 }
5653 let inverse = ed.mutate_edit(Edit::DeleteRange {
5654 start: Position::new(cursor.row, cursor.col - 1),
5655 end: cursor,
5656 kind: MotionKind::Char,
5657 });
5658 if let Edit::InsertStr { text, .. } = inverse {
5659 deleted = text + &deleted;
5662 }
5663 }
5664 }
5665 if !deleted.is_empty() {
5666 ed.record_yank_to_host(deleted.clone());
5667 ed.record_delete(deleted, false);
5668 }
5669 ed.push_buffer_cursor_to_textarea();
5670}
5671
5672fn adjust_number<H: crate::types::Host>(
5676 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5677 delta: i64,
5678) -> bool {
5679 use hjkl_buffer::{Edit, MotionKind, Position};
5680 ed.sync_buffer_content_from_textarea();
5681 let cursor = buf_cursor_pos(&ed.buffer);
5682 let row = cursor.row;
5683 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5684 Some(l) => l.chars().collect(),
5685 None => return false,
5686 };
5687 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5688 return false;
5689 };
5690 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5691 digit_start - 1
5692 } else {
5693 digit_start
5694 };
5695 let mut span_end = digit_start;
5696 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5697 span_end += 1;
5698 }
5699 let s: String = chars[span_start..span_end].iter().collect();
5700 let Ok(n) = s.parse::<i64>() else {
5701 return false;
5702 };
5703 let new_s = n.saturating_add(delta).to_string();
5704
5705 ed.push_undo();
5706 let span_start_pos = Position::new(row, span_start);
5707 let span_end_pos = Position::new(row, span_end);
5708 ed.mutate_edit(Edit::DeleteRange {
5709 start: span_start_pos,
5710 end: span_end_pos,
5711 kind: MotionKind::Char,
5712 });
5713 ed.mutate_edit(Edit::InsertStr {
5714 at: span_start_pos,
5715 text: new_s.clone(),
5716 });
5717 let new_len = new_s.chars().count();
5718 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5719 ed.push_buffer_cursor_to_textarea();
5720 true
5721}
5722
5723pub(crate) fn replace_char<H: crate::types::Host>(
5724 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5725 ch: char,
5726 count: usize,
5727) {
5728 use hjkl_buffer::{Edit, MotionKind, Position};
5729 ed.push_undo();
5730 ed.sync_buffer_content_from_textarea();
5731 for _ in 0..count {
5732 let cursor = buf_cursor_pos(&ed.buffer);
5733 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5734 if cursor.col >= line_chars {
5735 break;
5736 }
5737 ed.mutate_edit(Edit::DeleteRange {
5738 start: cursor,
5739 end: Position::new(cursor.row, cursor.col + 1),
5740 kind: MotionKind::Char,
5741 });
5742 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5743 }
5744 crate::motions::move_left(&mut ed.buffer, 1);
5746 ed.push_buffer_cursor_to_textarea();
5747}
5748
5749fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5750 use hjkl_buffer::{Edit, MotionKind, Position};
5751 ed.sync_buffer_content_from_textarea();
5752 let cursor = buf_cursor_pos(&ed.buffer);
5753 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5754 return;
5755 };
5756 let toggled = if c.is_uppercase() {
5757 c.to_lowercase().next().unwrap_or(c)
5758 } else {
5759 c.to_uppercase().next().unwrap_or(c)
5760 };
5761 ed.mutate_edit(Edit::DeleteRange {
5762 start: cursor,
5763 end: Position::new(cursor.row, cursor.col + 1),
5764 kind: MotionKind::Char,
5765 });
5766 ed.mutate_edit(Edit::InsertChar {
5767 at: cursor,
5768 ch: toggled,
5769 });
5770}
5771
5772fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5773 use hjkl_buffer::{Edit, Position};
5774 ed.sync_buffer_content_from_textarea();
5775 let row = buf_cursor_pos(&ed.buffer).row;
5776 if row + 1 >= buf_row_count(&ed.buffer) {
5777 return;
5778 }
5779 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5780 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5781 let next_trimmed = next_raw.trim_start();
5782 let cur_chars = cur_line.chars().count();
5783 let next_chars = next_raw.chars().count();
5784 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5787 " "
5788 } else {
5789 ""
5790 };
5791 let joined = format!("{cur_line}{separator}{next_trimmed}");
5792 ed.mutate_edit(Edit::Replace {
5793 start: Position::new(row, 0),
5794 end: Position::new(row + 1, next_chars),
5795 with: joined,
5796 });
5797 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5801 ed.push_buffer_cursor_to_textarea();
5802}
5803
5804fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5807 use hjkl_buffer::Edit;
5808 ed.sync_buffer_content_from_textarea();
5809 let row = buf_cursor_pos(&ed.buffer).row;
5810 if row + 1 >= buf_row_count(&ed.buffer) {
5811 return;
5812 }
5813 let join_col = buf_line_chars(&ed.buffer, row);
5814 ed.mutate_edit(Edit::JoinLines {
5815 row,
5816 count: 1,
5817 with_space: false,
5818 });
5819 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5821 ed.push_buffer_cursor_to_textarea();
5822}
5823
5824fn do_paste<H: crate::types::Host>(
5825 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5826 before: bool,
5827 count: usize,
5828) {
5829 use hjkl_buffer::{Edit, Position};
5830 ed.push_undo();
5831 let selector = ed.vim.pending_register.take();
5836 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5837 Some(slot) => (slot.text.clone(), slot.linewise),
5838 None => {
5844 let s = &ed.registers().unnamed;
5845 (s.text.clone(), s.linewise)
5846 }
5847 };
5848 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5852 for _ in 0..count {
5853 ed.sync_buffer_content_from_textarea();
5854 let yank = yank.clone();
5855 if yank.is_empty() {
5856 continue;
5857 }
5858 if linewise {
5859 let text = yank.trim_matches('\n').to_string();
5863 let row = buf_cursor_pos(&ed.buffer).row;
5864 let target_row = if before {
5865 ed.mutate_edit(Edit::InsertStr {
5866 at: Position::new(row, 0),
5867 text: format!("{text}\n"),
5868 });
5869 row
5870 } else {
5871 let line_chars = buf_line_chars(&ed.buffer, row);
5872 ed.mutate_edit(Edit::InsertStr {
5873 at: Position::new(row, line_chars),
5874 text: format!("\n{text}"),
5875 });
5876 row + 1
5877 };
5878 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5879 crate::motions::move_first_non_blank(&mut ed.buffer);
5880 ed.push_buffer_cursor_to_textarea();
5881 let payload_lines = text.lines().count().max(1);
5883 let bot_row = target_row + payload_lines - 1;
5884 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5885 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5886 } else {
5887 let cursor = buf_cursor_pos(&ed.buffer);
5891 let at = if before {
5892 cursor
5893 } else {
5894 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5895 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5896 };
5897 ed.mutate_edit(Edit::InsertStr {
5898 at,
5899 text: yank.clone(),
5900 });
5901 crate::motions::move_left(&mut ed.buffer, 1);
5904 ed.push_buffer_cursor_to_textarea();
5905 let lo = (at.row, at.col);
5907 let hi = ed.cursor();
5908 paste_mark = Some((lo, hi));
5909 }
5910 }
5911 if let Some((lo, hi)) = paste_mark {
5912 ed.set_mark('[', lo);
5913 ed.set_mark(']', hi);
5914 }
5915 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5917}
5918
5919pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5920 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5921 let current = ed.snapshot();
5922 ed.redo_stack.push(current);
5923 ed.restore(lines, cursor);
5924 }
5925 ed.vim.mode = Mode::Normal;
5926 clamp_cursor_to_normal_mode(ed);
5930}
5931
5932pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5933 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5934 let current = ed.snapshot();
5935 ed.undo_stack.push(current);
5936 ed.cap_undo();
5937 ed.restore(lines, cursor);
5938 }
5939 ed.vim.mode = Mode::Normal;
5940}
5941
5942fn replay_insert_and_finish<H: crate::types::Host>(
5949 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5950 text: &str,
5951) {
5952 use hjkl_buffer::{Edit, Position};
5953 let cursor = ed.cursor();
5954 ed.mutate_edit(Edit::InsertStr {
5955 at: Position::new(cursor.0, cursor.1),
5956 text: text.to_string(),
5957 });
5958 if ed.vim.insert_session.take().is_some() {
5959 if ed.cursor().1 > 0 {
5960 crate::motions::move_left(&mut ed.buffer, 1);
5961 ed.push_buffer_cursor_to_textarea();
5962 }
5963 ed.vim.mode = Mode::Normal;
5964 }
5965}
5966
5967fn replay_last_change<H: crate::types::Host>(
5968 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5969 outer_count: usize,
5970) {
5971 let Some(change) = ed.vim.last_change.clone() else {
5972 return;
5973 };
5974 ed.vim.replaying = true;
5975 let scale = if outer_count > 0 { outer_count } else { 1 };
5976 match change {
5977 LastChange::OpMotion {
5978 op,
5979 motion,
5980 count,
5981 inserted,
5982 } => {
5983 let total = count.max(1) * scale;
5984 apply_op_with_motion(ed, op, &motion, total);
5985 if let Some(text) = inserted {
5986 replay_insert_and_finish(ed, &text);
5987 }
5988 }
5989 LastChange::OpTextObj {
5990 op,
5991 obj,
5992 inner,
5993 inserted,
5994 } => {
5995 apply_op_with_text_object(ed, op, obj, inner);
5996 if let Some(text) = inserted {
5997 replay_insert_and_finish(ed, &text);
5998 }
5999 }
6000 LastChange::LineOp {
6001 op,
6002 count,
6003 inserted,
6004 } => {
6005 let total = count.max(1) * scale;
6006 execute_line_op(ed, op, total);
6007 if let Some(text) = inserted {
6008 replay_insert_and_finish(ed, &text);
6009 }
6010 }
6011 LastChange::CharDel { forward, count } => {
6012 do_char_delete(ed, forward, count * scale);
6013 }
6014 LastChange::ReplaceChar { ch, count } => {
6015 replace_char(ed, ch, count * scale);
6016 }
6017 LastChange::ToggleCase { count } => {
6018 for _ in 0..count * scale {
6019 ed.push_undo();
6020 toggle_case_at_cursor(ed);
6021 }
6022 }
6023 LastChange::JoinLine { count } => {
6024 for _ in 0..count * scale {
6025 ed.push_undo();
6026 join_line(ed);
6027 }
6028 }
6029 LastChange::Paste { before, count } => {
6030 do_paste(ed, before, count * scale);
6031 }
6032 LastChange::DeleteToEol { inserted } => {
6033 use hjkl_buffer::{Edit, Position};
6034 ed.push_undo();
6035 delete_to_eol(ed);
6036 if let Some(text) = inserted {
6037 let cursor = ed.cursor();
6038 ed.mutate_edit(Edit::InsertStr {
6039 at: Position::new(cursor.0, cursor.1),
6040 text,
6041 });
6042 }
6043 }
6044 LastChange::OpenLine { above, inserted } => {
6045 use hjkl_buffer::{Edit, Position};
6046 ed.push_undo();
6047 ed.sync_buffer_content_from_textarea();
6048 let row = buf_cursor_pos(&ed.buffer).row;
6049 if above {
6050 ed.mutate_edit(Edit::InsertStr {
6051 at: Position::new(row, 0),
6052 text: "\n".to_string(),
6053 });
6054 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6055 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6056 } else {
6057 let line_chars = buf_line_chars(&ed.buffer, row);
6058 ed.mutate_edit(Edit::InsertStr {
6059 at: Position::new(row, line_chars),
6060 text: "\n".to_string(),
6061 });
6062 }
6063 ed.push_buffer_cursor_to_textarea();
6064 let cursor = ed.cursor();
6065 ed.mutate_edit(Edit::InsertStr {
6066 at: Position::new(cursor.0, cursor.1),
6067 text: inserted,
6068 });
6069 }
6070 LastChange::InsertAt {
6071 entry,
6072 inserted,
6073 count,
6074 } => {
6075 use hjkl_buffer::{Edit, Position};
6076 ed.push_undo();
6077 match entry {
6078 InsertEntry::I => {}
6079 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6080 InsertEntry::A => {
6081 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6082 ed.push_buffer_cursor_to_textarea();
6083 }
6084 InsertEntry::ShiftA => {
6085 crate::motions::move_line_end(&mut ed.buffer);
6086 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6087 ed.push_buffer_cursor_to_textarea();
6088 }
6089 }
6090 for _ in 0..count.max(1) {
6091 let cursor = ed.cursor();
6092 ed.mutate_edit(Edit::InsertStr {
6093 at: Position::new(cursor.0, cursor.1),
6094 text: inserted.clone(),
6095 });
6096 }
6097 }
6098 }
6099 ed.vim.replaying = false;
6100}
6101
6102fn extract_inserted(before: &str, after: &str) -> String {
6105 let before_chars: Vec<char> = before.chars().collect();
6106 let after_chars: Vec<char> = after.chars().collect();
6107 if after_chars.len() <= before_chars.len() {
6108 return String::new();
6109 }
6110 let prefix = before_chars
6111 .iter()
6112 .zip(after_chars.iter())
6113 .take_while(|(a, b)| a == b)
6114 .count();
6115 let max_suffix = before_chars.len() - prefix;
6116 let suffix = before_chars
6117 .iter()
6118 .rev()
6119 .zip(after_chars.iter().rev())
6120 .take(max_suffix)
6121 .take_while(|(a, b)| a == b)
6122 .count();
6123 after_chars[prefix..after_chars.len() - suffix]
6124 .iter()
6125 .collect()
6126}
6127
6128#[cfg(all(test, feature = "crossterm"))]
6131mod tests {
6132 use crate::VimMode;
6133 use crate::editor::Editor;
6134 use crate::types::Host;
6135 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6136
6137 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6138 let mut iter = keys.chars().peekable();
6142 while let Some(c) = iter.next() {
6143 if c == '<' {
6144 let mut tag = String::new();
6145 for ch in iter.by_ref() {
6146 if ch == '>' {
6147 break;
6148 }
6149 tag.push(ch);
6150 }
6151 let ev = match tag.as_str() {
6152 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6153 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6154 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6155 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6156 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6157 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6158 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6159 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6160 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6164 s if s.starts_with("C-") => {
6165 let ch = s.chars().nth(2).unwrap();
6166 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6167 }
6168 _ => continue,
6169 };
6170 e.handle_key(ev);
6171 } else {
6172 let mods = if c.is_uppercase() {
6173 KeyModifiers::SHIFT
6174 } else {
6175 KeyModifiers::NONE
6176 };
6177 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6178 }
6179 }
6180 }
6181
6182 fn editor_with(content: &str) -> Editor {
6183 let opts = crate::types::Options {
6188 shiftwidth: 2,
6189 ..crate::types::Options::default()
6190 };
6191 let mut e = Editor::new(
6192 hjkl_buffer::Buffer::new(),
6193 crate::types::DefaultHost::new(),
6194 opts,
6195 );
6196 e.set_content(content);
6197 e
6198 }
6199
6200 #[test]
6201 fn f_char_jumps_on_line() {
6202 let mut e = editor_with("hello world");
6203 run_keys(&mut e, "fw");
6204 assert_eq!(e.cursor(), (0, 6));
6205 }
6206
6207 #[test]
6208 fn cap_f_jumps_backward() {
6209 let mut e = editor_with("hello world");
6210 e.jump_cursor(0, 10);
6211 run_keys(&mut e, "Fo");
6212 assert_eq!(e.cursor().1, 7);
6213 }
6214
6215 #[test]
6216 fn t_stops_before_char() {
6217 let mut e = editor_with("hello");
6218 run_keys(&mut e, "tl");
6219 assert_eq!(e.cursor(), (0, 1));
6220 }
6221
6222 #[test]
6223 fn semicolon_repeats_find() {
6224 let mut e = editor_with("aa.bb.cc");
6225 run_keys(&mut e, "f.");
6226 assert_eq!(e.cursor().1, 2);
6227 run_keys(&mut e, ";");
6228 assert_eq!(e.cursor().1, 5);
6229 }
6230
6231 #[test]
6232 fn comma_repeats_find_reverse() {
6233 let mut e = editor_with("aa.bb.cc");
6234 run_keys(&mut e, "f.");
6235 run_keys(&mut e, ";");
6236 run_keys(&mut e, ",");
6237 assert_eq!(e.cursor().1, 2);
6238 }
6239
6240 #[test]
6241 fn di_quote_deletes_content() {
6242 let mut e = editor_with("foo \"bar\" baz");
6243 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6245 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6246 }
6247
6248 #[test]
6249 fn da_quote_deletes_with_quotes() {
6250 let mut e = editor_with("foo \"bar\" baz");
6253 e.jump_cursor(0, 6);
6254 run_keys(&mut e, "da\"");
6255 assert_eq!(e.buffer().lines()[0], "foo baz");
6256 }
6257
6258 #[test]
6259 fn ci_paren_deletes_and_inserts() {
6260 let mut e = editor_with("fn(a, b, c)");
6261 e.jump_cursor(0, 5);
6262 run_keys(&mut e, "ci(");
6263 assert_eq!(e.vim_mode(), VimMode::Insert);
6264 assert_eq!(e.buffer().lines()[0], "fn()");
6265 }
6266
6267 #[test]
6268 fn diw_deletes_inner_word() {
6269 let mut e = editor_with("hello world");
6270 e.jump_cursor(0, 2);
6271 run_keys(&mut e, "diw");
6272 assert_eq!(e.buffer().lines()[0], " world");
6273 }
6274
6275 #[test]
6276 fn daw_deletes_word_with_trailing_space() {
6277 let mut e = editor_with("hello world");
6278 run_keys(&mut e, "daw");
6279 assert_eq!(e.buffer().lines()[0], "world");
6280 }
6281
6282 #[test]
6283 fn percent_jumps_to_matching_bracket() {
6284 let mut e = editor_with("foo(bar)");
6285 e.jump_cursor(0, 3);
6286 run_keys(&mut e, "%");
6287 assert_eq!(e.cursor().1, 7);
6288 run_keys(&mut e, "%");
6289 assert_eq!(e.cursor().1, 3);
6290 }
6291
6292 #[test]
6293 fn dot_repeats_last_change() {
6294 let mut e = editor_with("aaa bbb ccc");
6295 run_keys(&mut e, "dw");
6296 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6297 run_keys(&mut e, ".");
6298 assert_eq!(e.buffer().lines()[0], "ccc");
6299 }
6300
6301 #[test]
6302 fn dot_repeats_change_operator_with_text() {
6303 let mut e = editor_with("foo foo foo");
6304 run_keys(&mut e, "cwbar<Esc>");
6305 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6306 run_keys(&mut e, "w");
6308 run_keys(&mut e, ".");
6309 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6310 }
6311
6312 #[test]
6313 fn dot_repeats_x() {
6314 let mut e = editor_with("abcdef");
6315 run_keys(&mut e, "x");
6316 run_keys(&mut e, "..");
6317 assert_eq!(e.buffer().lines()[0], "def");
6318 }
6319
6320 #[test]
6321 fn count_operator_motion_compose() {
6322 let mut e = editor_with("one two three four five");
6323 run_keys(&mut e, "d3w");
6324 assert_eq!(e.buffer().lines()[0], "four five");
6325 }
6326
6327 #[test]
6328 fn two_dd_deletes_two_lines() {
6329 let mut e = editor_with("a\nb\nc");
6330 run_keys(&mut e, "2dd");
6331 assert_eq!(e.buffer().lines().len(), 1);
6332 assert_eq!(e.buffer().lines()[0], "c");
6333 }
6334
6335 #[test]
6340 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6341 let mut e = editor_with("one\ntwo\n three\nfour");
6342 e.jump_cursor(1, 2);
6343 run_keys(&mut e, "dd");
6344 assert_eq!(e.buffer().lines()[1], " three");
6346 assert_eq!(e.cursor(), (1, 4));
6347 }
6348
6349 #[test]
6350 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6351 let mut e = editor_with("one\n two\nthree");
6352 e.jump_cursor(2, 0);
6353 run_keys(&mut e, "dd");
6354 assert_eq!(e.buffer().lines().len(), 2);
6356 assert_eq!(e.cursor(), (1, 2));
6357 }
6358
6359 #[test]
6360 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6361 let mut e = editor_with("lonely");
6362 run_keys(&mut e, "dd");
6363 assert_eq!(e.buffer().lines().len(), 1);
6364 assert_eq!(e.buffer().lines()[0], "");
6365 assert_eq!(e.cursor(), (0, 0));
6366 }
6367
6368 #[test]
6369 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6370 let mut e = editor_with("a\nb\nc\n d\ne");
6371 e.jump_cursor(1, 0);
6373 run_keys(&mut e, "3dd");
6374 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6375 assert_eq!(e.cursor(), (1, 0));
6376 }
6377
6378 #[test]
6379 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6380 let mut e = editor_with(" line one\n line two\n xyz!");
6399 e.jump_cursor(0, 8);
6401 assert_eq!(e.cursor(), (0, 8));
6402 run_keys(&mut e, "dd");
6405 assert_eq!(
6406 e.cursor(),
6407 (0, 4),
6408 "dd must place cursor on first-non-blank"
6409 );
6410 run_keys(&mut e, "j");
6414 let (row, col) = e.cursor();
6415 assert_eq!(row, 1);
6416 assert_eq!(
6417 col, 4,
6418 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6419 );
6420 }
6421
6422 #[test]
6423 fn gu_lowercases_motion_range() {
6424 let mut e = editor_with("HELLO WORLD");
6425 run_keys(&mut e, "guw");
6426 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6427 assert_eq!(e.cursor(), (0, 0));
6428 }
6429
6430 #[test]
6431 fn g_u_uppercases_text_object() {
6432 let mut e = editor_with("hello world");
6433 run_keys(&mut e, "gUiw");
6435 assert_eq!(e.buffer().lines()[0], "HELLO world");
6436 assert_eq!(e.cursor(), (0, 0));
6437 }
6438
6439 #[test]
6440 fn g_tilde_toggles_case_of_range() {
6441 let mut e = editor_with("Hello World");
6442 run_keys(&mut e, "g~iw");
6443 assert_eq!(e.buffer().lines()[0], "hELLO World");
6444 }
6445
6446 #[test]
6447 fn g_uu_uppercases_current_line() {
6448 let mut e = editor_with("select 1\nselect 2");
6449 run_keys(&mut e, "gUU");
6450 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6451 assert_eq!(e.buffer().lines()[1], "select 2");
6452 }
6453
6454 #[test]
6455 fn gugu_lowercases_current_line() {
6456 let mut e = editor_with("FOO BAR\nBAZ");
6457 run_keys(&mut e, "gugu");
6458 assert_eq!(e.buffer().lines()[0], "foo bar");
6459 }
6460
6461 #[test]
6462 fn visual_u_uppercases_selection() {
6463 let mut e = editor_with("hello world");
6464 run_keys(&mut e, "veU");
6466 assert_eq!(e.buffer().lines()[0], "HELLO world");
6467 }
6468
6469 #[test]
6470 fn visual_line_u_lowercases_line() {
6471 let mut e = editor_with("HELLO WORLD\nOTHER");
6472 run_keys(&mut e, "Vu");
6473 assert_eq!(e.buffer().lines()[0], "hello world");
6474 assert_eq!(e.buffer().lines()[1], "OTHER");
6475 }
6476
6477 #[test]
6478 fn g_uu_with_count_uppercases_multiple_lines() {
6479 let mut e = editor_with("one\ntwo\nthree\nfour");
6480 run_keys(&mut e, "3gUU");
6482 assert_eq!(e.buffer().lines()[0], "ONE");
6483 assert_eq!(e.buffer().lines()[1], "TWO");
6484 assert_eq!(e.buffer().lines()[2], "THREE");
6485 assert_eq!(e.buffer().lines()[3], "four");
6486 }
6487
6488 #[test]
6489 fn double_gt_indents_current_line() {
6490 let mut e = editor_with("hello");
6491 run_keys(&mut e, ">>");
6492 assert_eq!(e.buffer().lines()[0], " hello");
6493 assert_eq!(e.cursor(), (0, 2));
6495 }
6496
6497 #[test]
6498 fn double_lt_outdents_current_line() {
6499 let mut e = editor_with(" hello");
6500 run_keys(&mut e, "<lt><lt>");
6501 assert_eq!(e.buffer().lines()[0], " hello");
6502 assert_eq!(e.cursor(), (0, 2));
6503 }
6504
6505 #[test]
6506 fn count_double_gt_indents_multiple_lines() {
6507 let mut e = editor_with("a\nb\nc\nd");
6508 run_keys(&mut e, "3>>");
6510 assert_eq!(e.buffer().lines()[0], " a");
6511 assert_eq!(e.buffer().lines()[1], " b");
6512 assert_eq!(e.buffer().lines()[2], " c");
6513 assert_eq!(e.buffer().lines()[3], "d");
6514 }
6515
6516 #[test]
6517 fn outdent_clips_ragged_leading_whitespace() {
6518 let mut e = editor_with(" x");
6521 run_keys(&mut e, "<lt><lt>");
6522 assert_eq!(e.buffer().lines()[0], "x");
6523 }
6524
6525 #[test]
6526 fn indent_motion_is_always_linewise() {
6527 let mut e = editor_with("foo bar");
6530 run_keys(&mut e, ">w");
6531 assert_eq!(e.buffer().lines()[0], " foo bar");
6532 }
6533
6534 #[test]
6535 fn indent_text_object_extends_over_paragraph() {
6536 let mut e = editor_with("a\nb\n\nc\nd");
6537 run_keys(&mut e, ">ap");
6539 assert_eq!(e.buffer().lines()[0], " a");
6540 assert_eq!(e.buffer().lines()[1], " b");
6541 assert_eq!(e.buffer().lines()[2], "");
6542 assert_eq!(e.buffer().lines()[3], "c");
6543 }
6544
6545 #[test]
6546 fn visual_line_indent_shifts_selected_rows() {
6547 let mut e = editor_with("x\ny\nz");
6548 run_keys(&mut e, "Vj>");
6550 assert_eq!(e.buffer().lines()[0], " x");
6551 assert_eq!(e.buffer().lines()[1], " y");
6552 assert_eq!(e.buffer().lines()[2], "z");
6553 }
6554
6555 #[test]
6556 fn outdent_empty_line_is_noop() {
6557 let mut e = editor_with("\nfoo");
6558 run_keys(&mut e, "<lt><lt>");
6559 assert_eq!(e.buffer().lines()[0], "");
6560 }
6561
6562 #[test]
6563 fn indent_skips_empty_lines() {
6564 let mut e = editor_with("");
6567 run_keys(&mut e, ">>");
6568 assert_eq!(e.buffer().lines()[0], "");
6569 }
6570
6571 #[test]
6572 fn insert_ctrl_t_indents_current_line() {
6573 let mut e = editor_with("x");
6574 run_keys(&mut e, "i<C-t>");
6576 assert_eq!(e.buffer().lines()[0], " x");
6577 assert_eq!(e.cursor(), (0, 2));
6580 }
6581
6582 #[test]
6583 fn insert_ctrl_d_outdents_current_line() {
6584 let mut e = editor_with(" x");
6585 run_keys(&mut e, "A<C-d>");
6587 assert_eq!(e.buffer().lines()[0], " x");
6588 }
6589
6590 #[test]
6591 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6592 let mut e = editor_with("first\nsecond");
6593 e.jump_cursor(1, 0);
6594 run_keys(&mut e, "h");
6595 assert_eq!(e.cursor(), (1, 0));
6597 }
6598
6599 #[test]
6600 fn l_at_last_char_does_not_wrap_to_next_line() {
6601 let mut e = editor_with("ab\ncd");
6602 e.jump_cursor(0, 1);
6604 run_keys(&mut e, "l");
6605 assert_eq!(e.cursor(), (0, 1));
6607 }
6608
6609 #[test]
6610 fn count_l_clamps_at_line_end() {
6611 let mut e = editor_with("abcde");
6612 run_keys(&mut e, "20l");
6615 assert_eq!(e.cursor(), (0, 4));
6616 }
6617
6618 #[test]
6619 fn count_h_clamps_at_col_zero() {
6620 let mut e = editor_with("abcde");
6621 e.jump_cursor(0, 3);
6622 run_keys(&mut e, "20h");
6623 assert_eq!(e.cursor(), (0, 0));
6624 }
6625
6626 #[test]
6627 fn dl_on_last_char_still_deletes_it() {
6628 let mut e = editor_with("ab");
6632 e.jump_cursor(0, 1);
6633 run_keys(&mut e, "dl");
6634 assert_eq!(e.buffer().lines()[0], "a");
6635 }
6636
6637 #[test]
6638 fn case_op_preserves_yank_register() {
6639 let mut e = editor_with("target");
6640 run_keys(&mut e, "yy");
6641 let yank_before = e.yank().to_string();
6642 run_keys(&mut e, "gUU");
6644 assert_eq!(e.buffer().lines()[0], "TARGET");
6645 assert_eq!(
6646 e.yank(),
6647 yank_before,
6648 "case ops must preserve the yank buffer"
6649 );
6650 }
6651
6652 #[test]
6653 fn dap_deletes_paragraph() {
6654 let mut e = editor_with("a\nb\n\nc\nd");
6655 run_keys(&mut e, "dap");
6656 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6657 }
6658
6659 #[test]
6660 fn dit_deletes_inner_tag_content() {
6661 let mut e = editor_with("<b>hello</b>");
6662 e.jump_cursor(0, 4);
6664 run_keys(&mut e, "dit");
6665 assert_eq!(e.buffer().lines()[0], "<b></b>");
6666 }
6667
6668 #[test]
6669 fn dat_deletes_around_tag() {
6670 let mut e = editor_with("hi <b>foo</b> bye");
6671 e.jump_cursor(0, 6);
6672 run_keys(&mut e, "dat");
6673 assert_eq!(e.buffer().lines()[0], "hi bye");
6674 }
6675
6676 #[test]
6677 fn dit_picks_innermost_tag() {
6678 let mut e = editor_with("<a><b>x</b></a>");
6679 e.jump_cursor(0, 6);
6681 run_keys(&mut e, "dit");
6682 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6684 }
6685
6686 #[test]
6687 fn dat_innermost_tag_pair() {
6688 let mut e = editor_with("<a><b>x</b></a>");
6689 e.jump_cursor(0, 6);
6690 run_keys(&mut e, "dat");
6691 assert_eq!(e.buffer().lines()[0], "<a></a>");
6692 }
6693
6694 #[test]
6695 fn dit_outside_any_tag_no_op() {
6696 let mut e = editor_with("plain text");
6697 e.jump_cursor(0, 3);
6698 run_keys(&mut e, "dit");
6699 assert_eq!(e.buffer().lines()[0], "plain text");
6701 }
6702
6703 #[test]
6704 fn cit_changes_inner_tag_content() {
6705 let mut e = editor_with("<b>hello</b>");
6706 e.jump_cursor(0, 4);
6707 run_keys(&mut e, "citNEW<Esc>");
6708 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6709 }
6710
6711 #[test]
6712 fn cat_changes_around_tag() {
6713 let mut e = editor_with("hi <b>foo</b> bye");
6714 e.jump_cursor(0, 6);
6715 run_keys(&mut e, "catBAR<Esc>");
6716 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6717 }
6718
6719 #[test]
6720 fn yit_yanks_inner_tag_content() {
6721 let mut e = editor_with("<b>hello</b>");
6722 e.jump_cursor(0, 4);
6723 run_keys(&mut e, "yit");
6724 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6725 }
6726
6727 #[test]
6728 fn yat_yanks_full_tag_pair() {
6729 let mut e = editor_with("hi <b>foo</b> bye");
6730 e.jump_cursor(0, 6);
6731 run_keys(&mut e, "yat");
6732 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6733 }
6734
6735 #[test]
6736 fn vit_visually_selects_inner_tag() {
6737 let mut e = editor_with("<b>hello</b>");
6738 e.jump_cursor(0, 4);
6739 run_keys(&mut e, "vit");
6740 assert_eq!(e.vim_mode(), VimMode::Visual);
6741 run_keys(&mut e, "y");
6742 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6743 }
6744
6745 #[test]
6746 fn vat_visually_selects_around_tag() {
6747 let mut e = editor_with("x<b>foo</b>y");
6748 e.jump_cursor(0, 5);
6749 run_keys(&mut e, "vat");
6750 assert_eq!(e.vim_mode(), VimMode::Visual);
6751 run_keys(&mut e, "y");
6752 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6753 }
6754
6755 #[test]
6758 #[allow(non_snake_case)]
6759 fn diW_deletes_inner_big_word() {
6760 let mut e = editor_with("foo.bar baz");
6761 e.jump_cursor(0, 2);
6762 run_keys(&mut e, "diW");
6763 assert_eq!(e.buffer().lines()[0], " baz");
6765 }
6766
6767 #[test]
6768 #[allow(non_snake_case)]
6769 fn daW_deletes_around_big_word() {
6770 let mut e = editor_with("foo.bar baz");
6771 e.jump_cursor(0, 2);
6772 run_keys(&mut e, "daW");
6773 assert_eq!(e.buffer().lines()[0], "baz");
6774 }
6775
6776 #[test]
6777 fn di_double_quote_deletes_inside() {
6778 let mut e = editor_with("a \"hello\" b");
6779 e.jump_cursor(0, 4);
6780 run_keys(&mut e, "di\"");
6781 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6782 }
6783
6784 #[test]
6785 fn da_double_quote_deletes_around() {
6786 let mut e = editor_with("a \"hello\" b");
6788 e.jump_cursor(0, 4);
6789 run_keys(&mut e, "da\"");
6790 assert_eq!(e.buffer().lines()[0], "a b");
6791 }
6792
6793 #[test]
6794 fn di_single_quote_deletes_inside() {
6795 let mut e = editor_with("x 'foo' y");
6796 e.jump_cursor(0, 4);
6797 run_keys(&mut e, "di'");
6798 assert_eq!(e.buffer().lines()[0], "x '' y");
6799 }
6800
6801 #[test]
6802 fn da_single_quote_deletes_around() {
6803 let mut e = editor_with("x 'foo' y");
6805 e.jump_cursor(0, 4);
6806 run_keys(&mut e, "da'");
6807 assert_eq!(e.buffer().lines()[0], "x y");
6808 }
6809
6810 #[test]
6811 fn di_backtick_deletes_inside() {
6812 let mut e = editor_with("p `q` r");
6813 e.jump_cursor(0, 3);
6814 run_keys(&mut e, "di`");
6815 assert_eq!(e.buffer().lines()[0], "p `` r");
6816 }
6817
6818 #[test]
6819 fn da_backtick_deletes_around() {
6820 let mut e = editor_with("p `q` r");
6822 e.jump_cursor(0, 3);
6823 run_keys(&mut e, "da`");
6824 assert_eq!(e.buffer().lines()[0], "p r");
6825 }
6826
6827 #[test]
6828 fn di_paren_deletes_inside() {
6829 let mut e = editor_with("f(arg)");
6830 e.jump_cursor(0, 3);
6831 run_keys(&mut e, "di(");
6832 assert_eq!(e.buffer().lines()[0], "f()");
6833 }
6834
6835 #[test]
6836 fn di_paren_alias_b_works() {
6837 let mut e = editor_with("f(arg)");
6838 e.jump_cursor(0, 3);
6839 run_keys(&mut e, "dib");
6840 assert_eq!(e.buffer().lines()[0], "f()");
6841 }
6842
6843 #[test]
6844 fn di_bracket_deletes_inside() {
6845 let mut e = editor_with("a[b,c]d");
6846 e.jump_cursor(0, 3);
6847 run_keys(&mut e, "di[");
6848 assert_eq!(e.buffer().lines()[0], "a[]d");
6849 }
6850
6851 #[test]
6852 fn da_bracket_deletes_around() {
6853 let mut e = editor_with("a[b,c]d");
6854 e.jump_cursor(0, 3);
6855 run_keys(&mut e, "da[");
6856 assert_eq!(e.buffer().lines()[0], "ad");
6857 }
6858
6859 #[test]
6860 fn di_brace_deletes_inside() {
6861 let mut e = editor_with("x{y}z");
6862 e.jump_cursor(0, 2);
6863 run_keys(&mut e, "di{");
6864 assert_eq!(e.buffer().lines()[0], "x{}z");
6865 }
6866
6867 #[test]
6868 fn da_brace_deletes_around() {
6869 let mut e = editor_with("x{y}z");
6870 e.jump_cursor(0, 2);
6871 run_keys(&mut e, "da{");
6872 assert_eq!(e.buffer().lines()[0], "xz");
6873 }
6874
6875 #[test]
6876 fn di_brace_alias_capital_b_works() {
6877 let mut e = editor_with("x{y}z");
6878 e.jump_cursor(0, 2);
6879 run_keys(&mut e, "diB");
6880 assert_eq!(e.buffer().lines()[0], "x{}z");
6881 }
6882
6883 #[test]
6884 fn di_angle_deletes_inside() {
6885 let mut e = editor_with("p<q>r");
6886 e.jump_cursor(0, 2);
6887 run_keys(&mut e, "di<lt>");
6889 assert_eq!(e.buffer().lines()[0], "p<>r");
6890 }
6891
6892 #[test]
6893 fn da_angle_deletes_around() {
6894 let mut e = editor_with("p<q>r");
6895 e.jump_cursor(0, 2);
6896 run_keys(&mut e, "da<lt>");
6897 assert_eq!(e.buffer().lines()[0], "pr");
6898 }
6899
6900 #[test]
6901 fn dip_deletes_inner_paragraph() {
6902 let mut e = editor_with("a\nb\nc\n\nd");
6903 e.jump_cursor(1, 0);
6904 run_keys(&mut e, "dip");
6905 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6908 }
6909
6910 #[test]
6913 fn sentence_motion_close_paren_jumps_forward() {
6914 let mut e = editor_with("Alpha. Beta. Gamma.");
6915 e.jump_cursor(0, 0);
6916 run_keys(&mut e, ")");
6917 assert_eq!(e.cursor(), (0, 7));
6919 run_keys(&mut e, ")");
6920 assert_eq!(e.cursor(), (0, 13));
6921 }
6922
6923 #[test]
6924 fn sentence_motion_open_paren_jumps_backward() {
6925 let mut e = editor_with("Alpha. Beta. Gamma.");
6926 e.jump_cursor(0, 13);
6927 run_keys(&mut e, "(");
6928 assert_eq!(e.cursor(), (0, 7));
6931 run_keys(&mut e, "(");
6932 assert_eq!(e.cursor(), (0, 0));
6933 }
6934
6935 #[test]
6936 fn sentence_motion_count() {
6937 let mut e = editor_with("A. B. C. D.");
6938 e.jump_cursor(0, 0);
6939 run_keys(&mut e, "3)");
6940 assert_eq!(e.cursor(), (0, 9));
6942 }
6943
6944 #[test]
6945 fn dis_deletes_inner_sentence() {
6946 let mut e = editor_with("First one. Second one. Third one.");
6947 e.jump_cursor(0, 13);
6948 run_keys(&mut e, "dis");
6949 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6951 }
6952
6953 #[test]
6954 fn das_deletes_around_sentence_with_trailing_space() {
6955 let mut e = editor_with("Alpha. Beta. Gamma.");
6956 e.jump_cursor(0, 8);
6957 run_keys(&mut e, "das");
6958 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6961 }
6962
6963 #[test]
6964 fn dis_handles_double_terminator() {
6965 let mut e = editor_with("Wow!? Next.");
6966 e.jump_cursor(0, 1);
6967 run_keys(&mut e, "dis");
6968 assert_eq!(e.buffer().lines()[0], " Next.");
6971 }
6972
6973 #[test]
6974 fn dis_first_sentence_from_cursor_at_zero() {
6975 let mut e = editor_with("Alpha. Beta.");
6976 e.jump_cursor(0, 0);
6977 run_keys(&mut e, "dis");
6978 assert_eq!(e.buffer().lines()[0], " Beta.");
6979 }
6980
6981 #[test]
6982 fn yis_yanks_inner_sentence() {
6983 let mut e = editor_with("Hello world. Bye.");
6984 e.jump_cursor(0, 5);
6985 run_keys(&mut e, "yis");
6986 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6987 }
6988
6989 #[test]
6990 fn vis_visually_selects_inner_sentence() {
6991 let mut e = editor_with("First. Second.");
6992 e.jump_cursor(0, 1);
6993 run_keys(&mut e, "vis");
6994 assert_eq!(e.vim_mode(), VimMode::Visual);
6995 run_keys(&mut e, "y");
6996 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6997 }
6998
6999 #[test]
7000 fn ciw_changes_inner_word() {
7001 let mut e = editor_with("hello world");
7002 e.jump_cursor(0, 1);
7003 run_keys(&mut e, "ciwHEY<Esc>");
7004 assert_eq!(e.buffer().lines()[0], "HEY world");
7005 }
7006
7007 #[test]
7008 fn yiw_yanks_inner_word() {
7009 let mut e = editor_with("hello world");
7010 e.jump_cursor(0, 1);
7011 run_keys(&mut e, "yiw");
7012 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7013 }
7014
7015 #[test]
7016 fn viw_selects_inner_word() {
7017 let mut e = editor_with("hello world");
7018 e.jump_cursor(0, 2);
7019 run_keys(&mut e, "viw");
7020 assert_eq!(e.vim_mode(), VimMode::Visual);
7021 run_keys(&mut e, "y");
7022 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7023 }
7024
7025 #[test]
7026 fn ci_paren_changes_inside() {
7027 let mut e = editor_with("f(old)");
7028 e.jump_cursor(0, 3);
7029 run_keys(&mut e, "ci(NEW<Esc>");
7030 assert_eq!(e.buffer().lines()[0], "f(NEW)");
7031 }
7032
7033 #[test]
7034 fn yi_double_quote_yanks_inside() {
7035 let mut e = editor_with("say \"hi there\" then");
7036 e.jump_cursor(0, 6);
7037 run_keys(&mut e, "yi\"");
7038 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
7039 }
7040
7041 #[test]
7042 fn vap_visual_selects_around_paragraph() {
7043 let mut e = editor_with("a\nb\n\nc");
7044 e.jump_cursor(0, 0);
7045 run_keys(&mut e, "vap");
7046 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7047 run_keys(&mut e, "y");
7048 let text = e.registers().read('"').unwrap().text.clone();
7050 assert!(text.starts_with("a\nb"));
7051 }
7052
7053 #[test]
7054 fn star_finds_next_occurrence() {
7055 let mut e = editor_with("foo bar foo baz");
7056 run_keys(&mut e, "*");
7057 assert_eq!(e.cursor().1, 8);
7058 }
7059
7060 #[test]
7061 fn star_skips_substring_match() {
7062 let mut e = editor_with("foo foobar baz");
7065 run_keys(&mut e, "*");
7066 assert_eq!(e.cursor().1, 0);
7067 }
7068
7069 #[test]
7070 fn g_star_matches_substring() {
7071 let mut e = editor_with("foo foobar baz");
7074 run_keys(&mut e, "g*");
7075 assert_eq!(e.cursor().1, 4);
7076 }
7077
7078 #[test]
7079 fn g_pound_matches_substring_backward() {
7080 let mut e = editor_with("foo foobar baz foo");
7083 run_keys(&mut e, "$b");
7084 assert_eq!(e.cursor().1, 15);
7085 run_keys(&mut e, "g#");
7086 assert_eq!(e.cursor().1, 4);
7087 }
7088
7089 #[test]
7090 fn n_repeats_last_search_forward() {
7091 let mut e = editor_with("foo bar foo baz foo");
7092 run_keys(&mut e, "/foo<CR>");
7095 assert_eq!(e.cursor().1, 8);
7096 run_keys(&mut e, "n");
7097 assert_eq!(e.cursor().1, 16);
7098 }
7099
7100 #[test]
7101 fn shift_n_reverses_search() {
7102 let mut e = editor_with("foo bar foo baz foo");
7103 run_keys(&mut e, "/foo<CR>");
7104 run_keys(&mut e, "n");
7105 assert_eq!(e.cursor().1, 16);
7106 run_keys(&mut e, "N");
7107 assert_eq!(e.cursor().1, 8);
7108 }
7109
7110 #[test]
7111 fn n_noop_without_pattern() {
7112 let mut e = editor_with("foo bar");
7113 run_keys(&mut e, "n");
7114 assert_eq!(e.cursor(), (0, 0));
7115 }
7116
7117 #[test]
7118 fn visual_line_preserves_cursor_column() {
7119 let mut e = editor_with("hello world\nanother one\nbye");
7122 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7124 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7125 assert_eq!(e.cursor(), (0, 5));
7126 run_keys(&mut e, "j");
7127 assert_eq!(e.cursor(), (1, 5));
7128 }
7129
7130 #[test]
7131 fn visual_line_yank_includes_trailing_newline() {
7132 let mut e = editor_with("aaa\nbbb\nccc");
7133 run_keys(&mut e, "Vjy");
7134 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7136 }
7137
7138 #[test]
7139 fn visual_line_yank_last_line_trailing_newline() {
7140 let mut e = editor_with("aaa\nbbb\nccc");
7141 run_keys(&mut e, "jj");
7143 run_keys(&mut e, "Vy");
7144 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7145 }
7146
7147 #[test]
7148 fn yy_on_last_line_has_trailing_newline() {
7149 let mut e = editor_with("aaa\nbbb\nccc");
7150 run_keys(&mut e, "jj");
7151 run_keys(&mut e, "yy");
7152 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7153 }
7154
7155 #[test]
7156 fn yy_in_middle_has_trailing_newline() {
7157 let mut e = editor_with("aaa\nbbb\nccc");
7158 run_keys(&mut e, "j");
7159 run_keys(&mut e, "yy");
7160 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7161 }
7162
7163 #[test]
7164 fn di_single_quote() {
7165 let mut e = editor_with("say 'hello world' now");
7166 e.jump_cursor(0, 7);
7167 run_keys(&mut e, "di'");
7168 assert_eq!(e.buffer().lines()[0], "say '' now");
7169 }
7170
7171 #[test]
7172 fn da_single_quote() {
7173 let mut e = editor_with("say 'hello' now");
7175 e.jump_cursor(0, 7);
7176 run_keys(&mut e, "da'");
7177 assert_eq!(e.buffer().lines()[0], "say now");
7178 }
7179
7180 #[test]
7181 fn di_backtick() {
7182 let mut e = editor_with("say `hi` now");
7183 e.jump_cursor(0, 5);
7184 run_keys(&mut e, "di`");
7185 assert_eq!(e.buffer().lines()[0], "say `` now");
7186 }
7187
7188 #[test]
7189 fn di_brace() {
7190 let mut e = editor_with("fn { a; b; c }");
7191 e.jump_cursor(0, 7);
7192 run_keys(&mut e, "di{");
7193 assert_eq!(e.buffer().lines()[0], "fn {}");
7194 }
7195
7196 #[test]
7197 fn di_bracket() {
7198 let mut e = editor_with("arr[1, 2, 3]");
7199 e.jump_cursor(0, 5);
7200 run_keys(&mut e, "di[");
7201 assert_eq!(e.buffer().lines()[0], "arr[]");
7202 }
7203
7204 #[test]
7205 fn dab_deletes_around_paren() {
7206 let mut e = editor_with("fn(a, b) + 1");
7207 e.jump_cursor(0, 4);
7208 run_keys(&mut e, "dab");
7209 assert_eq!(e.buffer().lines()[0], "fn + 1");
7210 }
7211
7212 #[test]
7213 fn da_big_b_deletes_around_brace() {
7214 let mut e = editor_with("x = {a: 1}");
7215 e.jump_cursor(0, 6);
7216 run_keys(&mut e, "daB");
7217 assert_eq!(e.buffer().lines()[0], "x = ");
7218 }
7219
7220 #[test]
7221 fn di_big_w_deletes_bigword() {
7222 let mut e = editor_with("foo-bar baz");
7223 e.jump_cursor(0, 2);
7224 run_keys(&mut e, "diW");
7225 assert_eq!(e.buffer().lines()[0], " baz");
7226 }
7227
7228 #[test]
7229 fn visual_select_inner_word() {
7230 let mut e = editor_with("hello world");
7231 e.jump_cursor(0, 2);
7232 run_keys(&mut e, "viw");
7233 assert_eq!(e.vim_mode(), VimMode::Visual);
7234 run_keys(&mut e, "y");
7235 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7236 }
7237
7238 #[test]
7239 fn visual_select_inner_quote() {
7240 let mut e = editor_with("foo \"bar\" baz");
7241 e.jump_cursor(0, 6);
7242 run_keys(&mut e, "vi\"");
7243 run_keys(&mut e, "y");
7244 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7245 }
7246
7247 #[test]
7248 fn visual_select_inner_paren() {
7249 let mut e = editor_with("fn(a, b)");
7250 e.jump_cursor(0, 4);
7251 run_keys(&mut e, "vi(");
7252 run_keys(&mut e, "y");
7253 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7254 }
7255
7256 #[test]
7257 fn visual_select_outer_brace() {
7258 let mut e = editor_with("{x}");
7259 e.jump_cursor(0, 1);
7260 run_keys(&mut e, "va{");
7261 run_keys(&mut e, "y");
7262 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7263 }
7264
7265 #[test]
7266 fn ci_paren_forward_scans_when_cursor_before_pair() {
7267 let mut e = editor_with("foo(bar)");
7270 e.jump_cursor(0, 0);
7271 run_keys(&mut e, "ci(NEW<Esc>");
7272 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7273 }
7274
7275 #[test]
7276 fn ci_paren_forward_scans_across_lines() {
7277 let mut e = editor_with("first\nfoo(bar)\nlast");
7278 e.jump_cursor(0, 0);
7279 run_keys(&mut e, "ci(NEW<Esc>");
7280 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7281 }
7282
7283 #[test]
7284 fn ci_brace_forward_scans_when_cursor_before_pair() {
7285 let mut e = editor_with("let x = {y};");
7286 e.jump_cursor(0, 0);
7287 run_keys(&mut e, "ci{NEW<Esc>");
7288 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7289 }
7290
7291 #[test]
7292 fn cit_forward_scans_when_cursor_before_tag() {
7293 let mut e = editor_with("text <b>hello</b> rest");
7296 e.jump_cursor(0, 0);
7297 run_keys(&mut e, "citNEW<Esc>");
7298 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7299 }
7300
7301 #[test]
7302 fn dat_forward_scans_when_cursor_before_tag() {
7303 let mut e = editor_with("text <b>hello</b> rest");
7305 e.jump_cursor(0, 0);
7306 run_keys(&mut e, "dat");
7307 assert_eq!(e.buffer().lines()[0], "text rest");
7308 }
7309
7310 #[test]
7311 fn ci_paren_still_works_when_cursor_inside() {
7312 let mut e = editor_with("fn(a, b)");
7315 e.jump_cursor(0, 4);
7316 run_keys(&mut e, "ci(NEW<Esc>");
7317 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7318 }
7319
7320 #[test]
7321 fn caw_changes_word_with_trailing_space() {
7322 let mut e = editor_with("hello world");
7323 run_keys(&mut e, "cawfoo<Esc>");
7324 assert_eq!(e.buffer().lines()[0], "fooworld");
7325 }
7326
7327 #[test]
7328 fn visual_char_yank_preserves_raw_text() {
7329 let mut e = editor_with("hello world");
7330 run_keys(&mut e, "vllly");
7331 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7332 }
7333
7334 #[test]
7335 fn single_line_visual_line_selects_full_line_on_yank() {
7336 let mut e = editor_with("hello world\nbye");
7337 run_keys(&mut e, "V");
7338 run_keys(&mut e, "y");
7341 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7342 }
7343
7344 #[test]
7345 fn visual_line_extends_both_directions() {
7346 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7347 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7349 assert_eq!(e.cursor(), (3, 0));
7350 run_keys(&mut e, "k");
7351 assert_eq!(e.cursor(), (2, 0));
7353 run_keys(&mut e, "k");
7354 assert_eq!(e.cursor(), (1, 0));
7355 }
7356
7357 #[test]
7358 fn visual_char_preserves_cursor_column() {
7359 let mut e = editor_with("hello world");
7360 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7362 assert_eq!(e.cursor(), (0, 5));
7363 run_keys(&mut e, "ll");
7364 assert_eq!(e.cursor(), (0, 7));
7365 }
7366
7367 #[test]
7368 fn visual_char_highlight_bounds_order() {
7369 let mut e = editor_with("abcdef");
7370 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7372 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7375 }
7376
7377 #[test]
7378 fn visual_line_highlight_bounds() {
7379 let mut e = editor_with("a\nb\nc");
7380 run_keys(&mut e, "V");
7381 assert_eq!(e.line_highlight(), Some((0, 0)));
7382 run_keys(&mut e, "j");
7383 assert_eq!(e.line_highlight(), Some((0, 1)));
7384 run_keys(&mut e, "j");
7385 assert_eq!(e.line_highlight(), Some((0, 2)));
7386 }
7387
7388 #[test]
7391 fn h_moves_left() {
7392 let mut e = editor_with("hello");
7393 e.jump_cursor(0, 3);
7394 run_keys(&mut e, "h");
7395 assert_eq!(e.cursor(), (0, 2));
7396 }
7397
7398 #[test]
7399 fn l_moves_right() {
7400 let mut e = editor_with("hello");
7401 run_keys(&mut e, "l");
7402 assert_eq!(e.cursor(), (0, 1));
7403 }
7404
7405 #[test]
7406 fn k_moves_up() {
7407 let mut e = editor_with("a\nb\nc");
7408 e.jump_cursor(2, 0);
7409 run_keys(&mut e, "k");
7410 assert_eq!(e.cursor(), (1, 0));
7411 }
7412
7413 #[test]
7414 fn zero_moves_to_line_start() {
7415 let mut e = editor_with(" hello");
7416 run_keys(&mut e, "$");
7417 run_keys(&mut e, "0");
7418 assert_eq!(e.cursor().1, 0);
7419 }
7420
7421 #[test]
7422 fn caret_moves_to_first_non_blank() {
7423 let mut e = editor_with(" hello");
7424 run_keys(&mut e, "0");
7425 run_keys(&mut e, "^");
7426 assert_eq!(e.cursor().1, 4);
7427 }
7428
7429 #[test]
7430 fn dollar_moves_to_last_char() {
7431 let mut e = editor_with("hello");
7432 run_keys(&mut e, "$");
7433 assert_eq!(e.cursor().1, 4);
7434 }
7435
7436 #[test]
7437 fn dollar_on_empty_line_stays_at_col_zero() {
7438 let mut e = editor_with("");
7439 run_keys(&mut e, "$");
7440 assert_eq!(e.cursor().1, 0);
7441 }
7442
7443 #[test]
7444 fn w_jumps_to_next_word() {
7445 let mut e = editor_with("foo bar baz");
7446 run_keys(&mut e, "w");
7447 assert_eq!(e.cursor().1, 4);
7448 }
7449
7450 #[test]
7451 fn b_jumps_back_a_word() {
7452 let mut e = editor_with("foo bar");
7453 e.jump_cursor(0, 6);
7454 run_keys(&mut e, "b");
7455 assert_eq!(e.cursor().1, 4);
7456 }
7457
7458 #[test]
7459 fn e_jumps_to_word_end() {
7460 let mut e = editor_with("foo bar");
7461 run_keys(&mut e, "e");
7462 assert_eq!(e.cursor().1, 2);
7463 }
7464
7465 #[test]
7468 fn d_dollar_deletes_to_eol() {
7469 let mut e = editor_with("hello world");
7470 e.jump_cursor(0, 5);
7471 run_keys(&mut e, "d$");
7472 assert_eq!(e.buffer().lines()[0], "hello");
7473 }
7474
7475 #[test]
7476 fn d_zero_deletes_to_line_start() {
7477 let mut e = editor_with("hello world");
7478 e.jump_cursor(0, 6);
7479 run_keys(&mut e, "d0");
7480 assert_eq!(e.buffer().lines()[0], "world");
7481 }
7482
7483 #[test]
7484 fn d_caret_deletes_to_first_non_blank() {
7485 let mut e = editor_with(" hello");
7486 e.jump_cursor(0, 6);
7487 run_keys(&mut e, "d^");
7488 assert_eq!(e.buffer().lines()[0], " llo");
7489 }
7490
7491 #[test]
7492 fn d_capital_g_deletes_to_end_of_file() {
7493 let mut e = editor_with("a\nb\nc\nd");
7494 e.jump_cursor(1, 0);
7495 run_keys(&mut e, "dG");
7496 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7497 }
7498
7499 #[test]
7500 fn d_gg_deletes_to_start_of_file() {
7501 let mut e = editor_with("a\nb\nc\nd");
7502 e.jump_cursor(2, 0);
7503 run_keys(&mut e, "dgg");
7504 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7505 }
7506
7507 #[test]
7508 fn cw_is_ce_quirk() {
7509 let mut e = editor_with("foo bar");
7512 run_keys(&mut e, "cwxyz<Esc>");
7513 assert_eq!(e.buffer().lines()[0], "xyz bar");
7514 }
7515
7516 #[test]
7519 fn big_d_deletes_to_eol() {
7520 let mut e = editor_with("hello world");
7521 e.jump_cursor(0, 5);
7522 run_keys(&mut e, "D");
7523 assert_eq!(e.buffer().lines()[0], "hello");
7524 }
7525
7526 #[test]
7527 fn big_c_deletes_to_eol_and_inserts() {
7528 let mut e = editor_with("hello world");
7529 e.jump_cursor(0, 5);
7530 run_keys(&mut e, "C!<Esc>");
7531 assert_eq!(e.buffer().lines()[0], "hello!");
7532 }
7533
7534 #[test]
7535 fn j_joins_next_line_with_space() {
7536 let mut e = editor_with("hello\nworld");
7537 run_keys(&mut e, "J");
7538 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7539 }
7540
7541 #[test]
7542 fn j_strips_leading_whitespace_on_join() {
7543 let mut e = editor_with("hello\n world");
7544 run_keys(&mut e, "J");
7545 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7546 }
7547
7548 #[test]
7549 fn big_x_deletes_char_before_cursor() {
7550 let mut e = editor_with("hello");
7551 e.jump_cursor(0, 3);
7552 run_keys(&mut e, "X");
7553 assert_eq!(e.buffer().lines()[0], "helo");
7554 }
7555
7556 #[test]
7557 fn s_substitutes_char_and_enters_insert() {
7558 let mut e = editor_with("hello");
7559 run_keys(&mut e, "sX<Esc>");
7560 assert_eq!(e.buffer().lines()[0], "Xello");
7561 }
7562
7563 #[test]
7564 fn count_x_deletes_many() {
7565 let mut e = editor_with("abcdef");
7566 run_keys(&mut e, "3x");
7567 assert_eq!(e.buffer().lines()[0], "def");
7568 }
7569
7570 #[test]
7573 fn p_pastes_charwise_after_cursor() {
7574 let mut e = editor_with("hello");
7575 run_keys(&mut e, "yw");
7576 run_keys(&mut e, "$p");
7577 assert_eq!(e.buffer().lines()[0], "hellohello");
7578 }
7579
7580 #[test]
7581 fn capital_p_pastes_charwise_before_cursor() {
7582 let mut e = editor_with("hello");
7583 run_keys(&mut e, "v");
7585 run_keys(&mut e, "l");
7586 run_keys(&mut e, "y");
7587 run_keys(&mut e, "$P");
7588 assert_eq!(e.buffer().lines()[0], "hellheo");
7591 }
7592
7593 #[test]
7594 fn p_pastes_linewise_below() {
7595 let mut e = editor_with("one\ntwo\nthree");
7596 run_keys(&mut e, "yy");
7597 run_keys(&mut e, "p");
7598 assert_eq!(
7599 e.buffer().lines(),
7600 &[
7601 "one".to_string(),
7602 "one".to_string(),
7603 "two".to_string(),
7604 "three".to_string()
7605 ]
7606 );
7607 }
7608
7609 #[test]
7610 fn capital_p_pastes_linewise_above() {
7611 let mut e = editor_with("one\ntwo");
7612 e.jump_cursor(1, 0);
7613 run_keys(&mut e, "yy");
7614 run_keys(&mut e, "P");
7615 assert_eq!(
7616 e.buffer().lines(),
7617 &["one".to_string(), "two".to_string(), "two".to_string()]
7618 );
7619 }
7620
7621 #[test]
7624 fn hash_finds_previous_occurrence() {
7625 let mut e = editor_with("foo bar foo baz foo");
7626 e.jump_cursor(0, 16);
7628 run_keys(&mut e, "#");
7629 assert_eq!(e.cursor().1, 8);
7630 }
7631
7632 #[test]
7635 fn visual_line_delete_removes_full_lines() {
7636 let mut e = editor_with("a\nb\nc\nd");
7637 run_keys(&mut e, "Vjd");
7638 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7639 }
7640
7641 #[test]
7642 fn visual_line_change_leaves_blank_line() {
7643 let mut e = editor_with("a\nb\nc");
7644 run_keys(&mut e, "Vjc");
7645 assert_eq!(e.vim_mode(), VimMode::Insert);
7646 run_keys(&mut e, "X<Esc>");
7647 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7651 }
7652
7653 #[test]
7654 fn cc_leaves_blank_line() {
7655 let mut e = editor_with("a\nb\nc");
7656 e.jump_cursor(1, 0);
7657 run_keys(&mut e, "ccX<Esc>");
7658 assert_eq!(
7659 e.buffer().lines(),
7660 &["a".to_string(), "X".to_string(), "c".to_string()]
7661 );
7662 }
7663
7664 #[test]
7669 fn big_w_skips_hyphens() {
7670 let mut e = editor_with("foo-bar baz");
7672 run_keys(&mut e, "W");
7673 assert_eq!(e.cursor().1, 8);
7674 }
7675
7676 #[test]
7677 fn big_w_crosses_lines() {
7678 let mut e = editor_with("foo-bar\nbaz-qux");
7679 run_keys(&mut e, "W");
7680 assert_eq!(e.cursor(), (1, 0));
7681 }
7682
7683 #[test]
7684 fn big_b_skips_hyphens() {
7685 let mut e = editor_with("foo-bar baz");
7686 e.jump_cursor(0, 9);
7687 run_keys(&mut e, "B");
7688 assert_eq!(e.cursor().1, 8);
7689 run_keys(&mut e, "B");
7690 assert_eq!(e.cursor().1, 0);
7691 }
7692
7693 #[test]
7694 fn big_e_jumps_to_big_word_end() {
7695 let mut e = editor_with("foo-bar baz");
7696 run_keys(&mut e, "E");
7697 assert_eq!(e.cursor().1, 6);
7698 run_keys(&mut e, "E");
7699 assert_eq!(e.cursor().1, 10);
7700 }
7701
7702 #[test]
7703 fn dw_with_big_word_variant() {
7704 let mut e = editor_with("foo-bar baz");
7706 run_keys(&mut e, "dW");
7707 assert_eq!(e.buffer().lines()[0], "baz");
7708 }
7709
7710 #[test]
7713 fn insert_ctrl_w_deletes_word_back() {
7714 let mut e = editor_with("");
7715 run_keys(&mut e, "i");
7716 for c in "hello world".chars() {
7717 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7718 }
7719 run_keys(&mut e, "<C-w>");
7720 assert_eq!(e.buffer().lines()[0], "hello ");
7721 }
7722
7723 #[test]
7724 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7725 let mut e = editor_with("hello\nworld");
7729 e.jump_cursor(1, 0);
7730 run_keys(&mut e, "i");
7731 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7732 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7735 assert_eq!(e.cursor(), (0, 0));
7736 }
7737
7738 #[test]
7739 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7740 let mut e = editor_with("foo bar\nbaz");
7741 e.jump_cursor(1, 0);
7742 run_keys(&mut e, "i");
7743 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7744 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7746 assert_eq!(e.cursor(), (0, 4));
7747 }
7748
7749 #[test]
7750 fn insert_ctrl_u_deletes_to_line_start() {
7751 let mut e = editor_with("");
7752 run_keys(&mut e, "i");
7753 for c in "hello world".chars() {
7754 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7755 }
7756 run_keys(&mut e, "<C-u>");
7757 assert_eq!(e.buffer().lines()[0], "");
7758 }
7759
7760 #[test]
7761 fn insert_ctrl_o_runs_one_normal_command() {
7762 let mut e = editor_with("hello world");
7763 run_keys(&mut e, "A");
7765 assert_eq!(e.vim_mode(), VimMode::Insert);
7766 e.jump_cursor(0, 0);
7768 run_keys(&mut e, "<C-o>");
7769 assert_eq!(e.vim_mode(), VimMode::Normal);
7770 run_keys(&mut e, "dw");
7771 assert_eq!(e.vim_mode(), VimMode::Insert);
7773 assert_eq!(e.buffer().lines()[0], "world");
7774 }
7775
7776 #[test]
7779 fn j_through_empty_line_preserves_column() {
7780 let mut e = editor_with("hello world\n\nanother line");
7781 run_keys(&mut e, "llllll");
7783 assert_eq!(e.cursor(), (0, 6));
7784 run_keys(&mut e, "j");
7787 assert_eq!(e.cursor(), (1, 0));
7788 run_keys(&mut e, "j");
7790 assert_eq!(e.cursor(), (2, 6));
7791 }
7792
7793 #[test]
7794 fn j_through_shorter_line_preserves_column() {
7795 let mut e = editor_with("hello world\nhi\nanother line");
7796 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7799 run_keys(&mut e, "j");
7800 assert_eq!(e.cursor(), (2, 7));
7801 }
7802
7803 #[test]
7804 fn esc_from_insert_sticky_matches_visible_cursor() {
7805 let mut e = editor_with(" this is a line\n another one of a similar size");
7809 e.jump_cursor(0, 12);
7810 run_keys(&mut e, "I");
7811 assert_eq!(e.cursor(), (0, 4));
7812 run_keys(&mut e, "X<Esc>");
7813 assert_eq!(e.cursor(), (0, 4));
7814 run_keys(&mut e, "j");
7815 assert_eq!(e.cursor(), (1, 4));
7816 }
7817
7818 #[test]
7819 fn esc_from_insert_sticky_tracks_inserted_chars() {
7820 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7821 run_keys(&mut e, "i");
7822 run_keys(&mut e, "abc<Esc>");
7823 assert_eq!(e.cursor(), (0, 2));
7824 run_keys(&mut e, "j");
7825 assert_eq!(e.cursor(), (1, 2));
7826 }
7827
7828 #[test]
7829 fn esc_from_insert_sticky_tracks_arrow_nav() {
7830 let mut e = editor_with("xxxxxx\nyyyyyy");
7831 run_keys(&mut e, "i");
7832 run_keys(&mut e, "abc");
7833 for _ in 0..2 {
7834 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7835 }
7836 run_keys(&mut e, "<Esc>");
7837 assert_eq!(e.cursor(), (0, 0));
7838 run_keys(&mut e, "j");
7839 assert_eq!(e.cursor(), (1, 0));
7840 }
7841
7842 #[test]
7843 fn esc_from_insert_at_col_14_followed_by_j() {
7844 let line = "x".repeat(30);
7847 let buf = format!("{line}\n{line}");
7848 let mut e = editor_with(&buf);
7849 e.jump_cursor(0, 14);
7850 run_keys(&mut e, "i");
7851 for c in "test ".chars() {
7852 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7853 }
7854 run_keys(&mut e, "<Esc>");
7855 assert_eq!(e.cursor(), (0, 18));
7856 run_keys(&mut e, "j");
7857 assert_eq!(e.cursor(), (1, 18));
7858 }
7859
7860 #[test]
7861 fn linewise_paste_resets_sticky_column() {
7862 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7866 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7868 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7872 run_keys(&mut e, "j");
7874 assert_eq!(e.cursor(), (3, 2));
7875 }
7876
7877 #[test]
7878 fn horizontal_motion_resyncs_sticky_column() {
7879 let mut e = editor_with("hello world\n\nanother line");
7883 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7886 assert_eq!(e.cursor(), (2, 3));
7887 }
7888
7889 #[test]
7892 fn ctrl_v_enters_visual_block() {
7893 let mut e = editor_with("aaa\nbbb\nccc");
7894 run_keys(&mut e, "<C-v>");
7895 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7896 }
7897
7898 #[test]
7899 fn visual_block_esc_returns_to_normal() {
7900 let mut e = editor_with("aaa\nbbb\nccc");
7901 run_keys(&mut e, "<C-v>");
7902 run_keys(&mut e, "<Esc>");
7903 assert_eq!(e.vim_mode(), VimMode::Normal);
7904 }
7905
7906 #[test]
7907 fn backtick_lt_jumps_to_visual_start_mark() {
7908 let mut e = editor_with("foo bar baz\n");
7912 run_keys(&mut e, "v");
7913 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7916 run_keys(&mut e, "`<lt>");
7918 assert_eq!(e.cursor(), (0, 0));
7919 }
7920
7921 #[test]
7922 fn backtick_gt_jumps_to_visual_end_mark() {
7923 let mut e = editor_with("foo bar baz\n");
7924 run_keys(&mut e, "v");
7925 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7927 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7929 assert_eq!(e.cursor(), (0, 4));
7930 }
7931
7932 #[test]
7933 fn visual_exit_sets_lt_gt_marks() {
7934 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7937 run_keys(&mut e, "V");
7939 run_keys(&mut e, "j");
7940 run_keys(&mut e, "<Esc>");
7941 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7942 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7943 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7944 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7945 }
7946
7947 #[test]
7948 fn visual_exit_marks_use_lower_higher_order() {
7949 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7953 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7955 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7957 let lt = e.mark('<').unwrap();
7958 let gt = e.mark('>').unwrap();
7959 assert_eq!(lt.0, 2);
7960 assert_eq!(gt.0, 3);
7961 }
7962
7963 #[test]
7964 fn visualline_exit_marks_snap_to_line_edges() {
7965 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7967 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7969 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7971 let lt = e.mark('<').unwrap();
7972 let gt = e.mark('>').unwrap();
7973 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7974 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7976 }
7977
7978 #[test]
7979 fn visualblock_exit_marks_use_block_corners() {
7980 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7984 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7986 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7989 let lt = e.mark('<').unwrap();
7990 let gt = e.mark('>').unwrap();
7991 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7993 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7994 }
7995
7996 #[test]
7997 fn visual_block_delete_removes_column_range() {
7998 let mut e = editor_with("hello\nworld\nhappy");
7999 run_keys(&mut e, "l");
8001 run_keys(&mut e, "<C-v>");
8002 run_keys(&mut e, "jj");
8003 run_keys(&mut e, "ll");
8004 run_keys(&mut e, "d");
8005 assert_eq!(
8007 e.buffer().lines(),
8008 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
8009 );
8010 }
8011
8012 #[test]
8013 fn visual_block_yank_joins_with_newlines() {
8014 let mut e = editor_with("hello\nworld\nhappy");
8015 run_keys(&mut e, "<C-v>");
8016 run_keys(&mut e, "jj");
8017 run_keys(&mut e, "ll");
8018 run_keys(&mut e, "y");
8019 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
8020 }
8021
8022 #[test]
8023 fn visual_block_replace_fills_block() {
8024 let mut e = editor_with("hello\nworld\nhappy");
8025 run_keys(&mut e, "<C-v>");
8026 run_keys(&mut e, "jj");
8027 run_keys(&mut e, "ll");
8028 run_keys(&mut e, "rx");
8029 assert_eq!(
8030 e.buffer().lines(),
8031 &[
8032 "xxxlo".to_string(),
8033 "xxxld".to_string(),
8034 "xxxpy".to_string()
8035 ]
8036 );
8037 }
8038
8039 #[test]
8040 fn visual_block_insert_repeats_across_rows() {
8041 let mut e = editor_with("hello\nworld\nhappy");
8042 run_keys(&mut e, "<C-v>");
8043 run_keys(&mut e, "jj");
8044 run_keys(&mut e, "I");
8045 run_keys(&mut e, "# <Esc>");
8046 assert_eq!(
8047 e.buffer().lines(),
8048 &[
8049 "# hello".to_string(),
8050 "# world".to_string(),
8051 "# happy".to_string()
8052 ]
8053 );
8054 }
8055
8056 #[test]
8057 fn block_highlight_returns_none_outside_block_mode() {
8058 let mut e = editor_with("abc");
8059 assert!(e.block_highlight().is_none());
8060 run_keys(&mut e, "v");
8061 assert!(e.block_highlight().is_none());
8062 run_keys(&mut e, "<Esc>V");
8063 assert!(e.block_highlight().is_none());
8064 }
8065
8066 #[test]
8067 fn block_highlight_bounds_track_anchor_and_cursor() {
8068 let mut e = editor_with("aaaa\nbbbb\ncccc");
8069 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8071 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8074 }
8075
8076 #[test]
8077 fn visual_block_delete_handles_short_lines() {
8078 let mut e = editor_with("hello\nhi\nworld");
8080 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8082 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8084 assert_eq!(
8089 e.buffer().lines(),
8090 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8091 );
8092 }
8093
8094 #[test]
8095 fn visual_block_yank_pads_short_lines_with_empties() {
8096 let mut e = editor_with("hello\nhi\nworld");
8097 run_keys(&mut e, "l");
8098 run_keys(&mut e, "<C-v>");
8099 run_keys(&mut e, "jjll");
8100 run_keys(&mut e, "y");
8101 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8103 }
8104
8105 #[test]
8106 fn visual_block_replace_skips_past_eol() {
8107 let mut e = editor_with("ab\ncd\nef");
8110 run_keys(&mut e, "l");
8112 run_keys(&mut e, "<C-v>");
8113 run_keys(&mut e, "jjllllll");
8114 run_keys(&mut e, "rX");
8115 assert_eq!(
8118 e.buffer().lines(),
8119 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8120 );
8121 }
8122
8123 #[test]
8124 fn visual_block_with_empty_line_in_middle() {
8125 let mut e = editor_with("abcd\n\nefgh");
8126 run_keys(&mut e, "<C-v>");
8127 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8129 assert_eq!(
8132 e.buffer().lines(),
8133 &["d".to_string(), "".to_string(), "h".to_string()]
8134 );
8135 }
8136
8137 #[test]
8138 fn block_insert_pads_empty_lines_to_block_column() {
8139 let mut e = editor_with("this is a line\n\nthis is a line");
8142 e.jump_cursor(0, 3);
8143 run_keys(&mut e, "<C-v>");
8144 run_keys(&mut e, "jj");
8145 run_keys(&mut e, "I");
8146 run_keys(&mut e, "XX<Esc>");
8147 assert_eq!(
8148 e.buffer().lines(),
8149 &[
8150 "thiXXs is a line".to_string(),
8151 " XX".to_string(),
8152 "thiXXs is a line".to_string()
8153 ]
8154 );
8155 }
8156
8157 #[test]
8158 fn block_insert_pads_short_lines_to_block_column() {
8159 let mut e = editor_with("aaaaa\nbb\naaaaa");
8160 e.jump_cursor(0, 3);
8161 run_keys(&mut e, "<C-v>");
8162 run_keys(&mut e, "jj");
8163 run_keys(&mut e, "I");
8164 run_keys(&mut e, "Y<Esc>");
8165 assert_eq!(
8167 e.buffer().lines(),
8168 &[
8169 "aaaYaa".to_string(),
8170 "bb Y".to_string(),
8171 "aaaYaa".to_string()
8172 ]
8173 );
8174 }
8175
8176 #[test]
8177 fn visual_block_append_repeats_across_rows() {
8178 let mut e = editor_with("foo\nbar\nbaz");
8179 run_keys(&mut e, "<C-v>");
8180 run_keys(&mut e, "jj");
8181 run_keys(&mut e, "A");
8184 run_keys(&mut e, "!<Esc>");
8185 assert_eq!(
8186 e.buffer().lines(),
8187 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8188 );
8189 }
8190
8191 #[test]
8194 fn slash_opens_forward_search_prompt() {
8195 let mut e = editor_with("hello world");
8196 run_keys(&mut e, "/");
8197 let p = e.search_prompt().expect("prompt should be active");
8198 assert!(p.text.is_empty());
8199 assert!(p.forward);
8200 }
8201
8202 #[test]
8203 fn question_opens_backward_search_prompt() {
8204 let mut e = editor_with("hello world");
8205 run_keys(&mut e, "?");
8206 let p = e.search_prompt().expect("prompt should be active");
8207 assert!(!p.forward);
8208 }
8209
8210 #[test]
8211 fn search_prompt_typing_updates_pattern_live() {
8212 let mut e = editor_with("foo bar\nbaz");
8213 run_keys(&mut e, "/bar");
8214 assert_eq!(e.search_prompt().unwrap().text, "bar");
8215 assert!(e.search_state().pattern.is_some());
8217 }
8218
8219 #[test]
8220 fn search_prompt_backspace_and_enter() {
8221 let mut e = editor_with("hello world\nagain");
8222 run_keys(&mut e, "/worlx");
8223 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8224 assert_eq!(e.search_prompt().unwrap().text, "worl");
8225 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8226 assert!(e.search_prompt().is_none());
8228 assert_eq!(e.last_search(), Some("worl"));
8229 assert_eq!(e.cursor(), (0, 6));
8230 }
8231
8232 #[test]
8233 fn empty_search_prompt_enter_repeats_last_search() {
8234 let mut e = editor_with("foo bar foo baz foo");
8235 run_keys(&mut e, "/foo");
8236 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8237 assert_eq!(e.cursor().1, 8);
8238 run_keys(&mut e, "/");
8240 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8241 assert_eq!(e.cursor().1, 16);
8242 assert_eq!(e.last_search(), Some("foo"));
8243 }
8244
8245 #[test]
8246 fn search_history_records_committed_patterns() {
8247 let mut e = editor_with("alpha beta gamma");
8248 run_keys(&mut e, "/alpha<CR>");
8249 run_keys(&mut e, "/beta<CR>");
8250 let history = e.vim.search_history.clone();
8252 assert_eq!(history, vec!["alpha", "beta"]);
8253 }
8254
8255 #[test]
8256 fn search_history_dedupes_consecutive_repeats() {
8257 let mut e = editor_with("foo bar foo");
8258 run_keys(&mut e, "/foo<CR>");
8259 run_keys(&mut e, "/foo<CR>");
8260 run_keys(&mut e, "/bar<CR>");
8261 run_keys(&mut e, "/bar<CR>");
8262 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8264 }
8265
8266 #[test]
8267 fn ctrl_p_walks_history_backward() {
8268 let mut e = editor_with("alpha beta gamma");
8269 run_keys(&mut e, "/alpha<CR>");
8270 run_keys(&mut e, "/beta<CR>");
8271 run_keys(&mut e, "/");
8273 assert_eq!(e.search_prompt().unwrap().text, "");
8274 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8275 assert_eq!(e.search_prompt().unwrap().text, "beta");
8276 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8277 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8278 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8280 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8281 }
8282
8283 #[test]
8284 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8285 let mut e = editor_with("a b c");
8286 run_keys(&mut e, "/a<CR>");
8287 run_keys(&mut e, "/b<CR>");
8288 run_keys(&mut e, "/c<CR>");
8289 run_keys(&mut e, "/");
8290 for _ in 0..3 {
8292 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8293 }
8294 assert_eq!(e.search_prompt().unwrap().text, "a");
8295 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8296 assert_eq!(e.search_prompt().unwrap().text, "b");
8297 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8298 assert_eq!(e.search_prompt().unwrap().text, "c");
8299 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8301 assert_eq!(e.search_prompt().unwrap().text, "c");
8302 }
8303
8304 #[test]
8305 fn typing_after_history_walk_resets_cursor() {
8306 let mut e = editor_with("foo");
8307 run_keys(&mut e, "/foo<CR>");
8308 run_keys(&mut e, "/");
8309 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8310 assert_eq!(e.search_prompt().unwrap().text, "foo");
8311 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8314 assert_eq!(e.search_prompt().unwrap().text, "foox");
8315 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8316 assert_eq!(e.search_prompt().unwrap().text, "foo");
8317 }
8318
8319 #[test]
8320 fn empty_backward_search_prompt_enter_repeats_last_search() {
8321 let mut e = editor_with("foo bar foo baz foo");
8322 run_keys(&mut e, "/foo");
8324 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8325 assert_eq!(e.cursor().1, 8);
8326 run_keys(&mut e, "?");
8327 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8328 assert_eq!(e.cursor().1, 0);
8329 assert_eq!(e.last_search(), Some("foo"));
8330 }
8331
8332 #[test]
8333 fn search_prompt_esc_cancels_but_keeps_last_search() {
8334 let mut e = editor_with("foo bar\nbaz");
8335 run_keys(&mut e, "/bar");
8336 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8337 assert!(e.search_prompt().is_none());
8338 assert_eq!(e.last_search(), Some("bar"));
8339 }
8340
8341 #[test]
8342 fn search_then_n_and_shift_n_navigate() {
8343 let mut e = editor_with("foo bar foo baz foo");
8344 run_keys(&mut e, "/foo");
8345 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8346 assert_eq!(e.cursor().1, 8);
8348 run_keys(&mut e, "n");
8349 assert_eq!(e.cursor().1, 16);
8350 run_keys(&mut e, "N");
8351 assert_eq!(e.cursor().1, 8);
8352 }
8353
8354 #[test]
8355 fn question_mark_searches_backward_on_enter() {
8356 let mut e = editor_with("foo bar foo baz");
8357 e.jump_cursor(0, 10);
8358 run_keys(&mut e, "?foo");
8359 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8360 assert_eq!(e.cursor(), (0, 8));
8362 }
8363
8364 #[test]
8367 fn big_y_yanks_to_end_of_line() {
8368 let mut e = editor_with("hello world");
8369 e.jump_cursor(0, 6);
8370 run_keys(&mut e, "Y");
8371 assert_eq!(e.last_yank.as_deref(), Some("world"));
8372 }
8373
8374 #[test]
8375 fn big_y_from_line_start_yanks_full_line() {
8376 let mut e = editor_with("hello world");
8377 run_keys(&mut e, "Y");
8378 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8379 }
8380
8381 #[test]
8382 fn gj_joins_without_inserting_space() {
8383 let mut e = editor_with("hello\n world");
8384 run_keys(&mut e, "gJ");
8385 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8387 }
8388
8389 #[test]
8390 fn gj_noop_on_last_line() {
8391 let mut e = editor_with("only");
8392 run_keys(&mut e, "gJ");
8393 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8394 }
8395
8396 #[test]
8397 fn ge_jumps_to_previous_word_end() {
8398 let mut e = editor_with("foo bar baz");
8399 e.jump_cursor(0, 5);
8400 run_keys(&mut e, "ge");
8401 assert_eq!(e.cursor(), (0, 2));
8402 }
8403
8404 #[test]
8405 fn ge_respects_word_class() {
8406 let mut e = editor_with("foo-bar baz");
8409 e.jump_cursor(0, 5);
8410 run_keys(&mut e, "ge");
8411 assert_eq!(e.cursor(), (0, 3));
8412 }
8413
8414 #[test]
8415 fn big_ge_treats_hyphens_as_part_of_word() {
8416 let mut e = editor_with("foo-bar baz");
8419 e.jump_cursor(0, 10);
8420 run_keys(&mut e, "gE");
8421 assert_eq!(e.cursor(), (0, 6));
8422 }
8423
8424 #[test]
8425 fn ge_crosses_line_boundary() {
8426 let mut e = editor_with("foo\nbar");
8427 e.jump_cursor(1, 0);
8428 run_keys(&mut e, "ge");
8429 assert_eq!(e.cursor(), (0, 2));
8430 }
8431
8432 #[test]
8433 fn dge_deletes_to_end_of_previous_word() {
8434 let mut e = editor_with("foo bar baz");
8435 e.jump_cursor(0, 8);
8436 run_keys(&mut e, "dge");
8439 assert_eq!(e.buffer().lines()[0], "foo baaz");
8440 }
8441
8442 #[test]
8443 fn ctrl_scroll_keys_do_not_panic() {
8444 let mut e = editor_with(
8447 (0..50)
8448 .map(|i| format!("line{i}"))
8449 .collect::<Vec<_>>()
8450 .join("\n")
8451 .as_str(),
8452 );
8453 run_keys(&mut e, "<C-f>");
8454 run_keys(&mut e, "<C-b>");
8455 assert!(!e.buffer().lines().is_empty());
8457 }
8458
8459 #[test]
8466 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8467 let mut e = Editor::new(
8468 hjkl_buffer::Buffer::new(),
8469 crate::types::DefaultHost::new(),
8470 crate::types::Options::default(),
8471 );
8472 e.set_content("row0\nrow1\nrow2");
8473 run_keys(&mut e, "3iX<Down><Esc>");
8475 assert!(e.buffer().lines()[0].contains('X'));
8477 assert!(
8480 !e.buffer().lines()[1].contains("row0"),
8481 "row1 leaked row0 contents: {:?}",
8482 e.buffer().lines()[1]
8483 );
8484 assert_eq!(e.buffer().lines().len(), 3);
8487 }
8488
8489 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8492 let mut e = Editor::new(
8493 hjkl_buffer::Buffer::new(),
8494 crate::types::DefaultHost::new(),
8495 crate::types::Options::default(),
8496 );
8497 let body = (0..n)
8498 .map(|i| format!(" line{}", i))
8499 .collect::<Vec<_>>()
8500 .join("\n");
8501 e.set_content(&body);
8502 e.set_viewport_height(viewport);
8503 e
8504 }
8505
8506 #[test]
8507 fn ctrl_d_moves_cursor_half_page_down() {
8508 let mut e = editor_with_rows(100, 20);
8509 run_keys(&mut e, "<C-d>");
8510 assert_eq!(e.cursor().0, 10);
8511 }
8512
8513 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8514 let mut e = Editor::new(
8515 hjkl_buffer::Buffer::new(),
8516 crate::types::DefaultHost::new(),
8517 crate::types::Options::default(),
8518 );
8519 e.set_content(&lines.join("\n"));
8520 e.set_viewport_height(viewport);
8521 let v = e.host_mut().viewport_mut();
8522 v.height = viewport;
8523 v.width = text_width;
8524 v.text_width = text_width;
8525 v.wrap = hjkl_buffer::Wrap::Char;
8526 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8527 e
8528 }
8529
8530 #[test]
8531 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8532 let lines = ["aaaabbbbcccc"; 10];
8536 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8537 e.jump_cursor(4, 0);
8538 e.ensure_cursor_in_scrolloff();
8539 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8540 assert!(csr <= 6, "csr={csr}");
8541 }
8542
8543 #[test]
8544 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8545 let lines = ["aaaabbbbcccc"; 10];
8546 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8547 e.jump_cursor(7, 0);
8550 e.ensure_cursor_in_scrolloff();
8551 e.jump_cursor(2, 0);
8552 e.ensure_cursor_in_scrolloff();
8553 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8554 assert!(csr >= 5, "csr={csr}");
8556 }
8557
8558 #[test]
8559 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8560 let lines = ["aaaabbbbcccc"; 5];
8561 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8562 e.jump_cursor(4, 11);
8563 e.ensure_cursor_in_scrolloff();
8564 let top = e.host().viewport().top_row;
8569 assert_eq!(top, 1);
8570 }
8571
8572 #[test]
8573 fn ctrl_u_moves_cursor_half_page_up() {
8574 let mut e = editor_with_rows(100, 20);
8575 e.jump_cursor(50, 0);
8576 run_keys(&mut e, "<C-u>");
8577 assert_eq!(e.cursor().0, 40);
8578 }
8579
8580 #[test]
8581 fn ctrl_f_moves_cursor_full_page_down() {
8582 let mut e = editor_with_rows(100, 20);
8583 run_keys(&mut e, "<C-f>");
8584 assert_eq!(e.cursor().0, 18);
8586 }
8587
8588 #[test]
8589 fn ctrl_b_moves_cursor_full_page_up() {
8590 let mut e = editor_with_rows(100, 20);
8591 e.jump_cursor(50, 0);
8592 run_keys(&mut e, "<C-b>");
8593 assert_eq!(e.cursor().0, 32);
8594 }
8595
8596 #[test]
8597 fn ctrl_d_lands_on_first_non_blank() {
8598 let mut e = editor_with_rows(100, 20);
8599 run_keys(&mut e, "<C-d>");
8600 assert_eq!(e.cursor().1, 2);
8602 }
8603
8604 #[test]
8605 fn ctrl_d_clamps_at_end_of_buffer() {
8606 let mut e = editor_with_rows(5, 20);
8607 run_keys(&mut e, "<C-d>");
8608 assert_eq!(e.cursor().0, 4);
8609 }
8610
8611 #[test]
8612 fn capital_h_jumps_to_viewport_top() {
8613 let mut e = editor_with_rows(100, 10);
8614 e.jump_cursor(50, 0);
8615 e.set_viewport_top(45);
8616 let top = e.host().viewport().top_row;
8617 run_keys(&mut e, "H");
8618 assert_eq!(e.cursor().0, top);
8619 assert_eq!(e.cursor().1, 2);
8620 }
8621
8622 #[test]
8623 fn capital_l_jumps_to_viewport_bottom() {
8624 let mut e = editor_with_rows(100, 10);
8625 e.jump_cursor(50, 0);
8626 e.set_viewport_top(45);
8627 let top = e.host().viewport().top_row;
8628 run_keys(&mut e, "L");
8629 assert_eq!(e.cursor().0, top + 9);
8630 }
8631
8632 #[test]
8633 fn capital_m_jumps_to_viewport_middle() {
8634 let mut e = editor_with_rows(100, 10);
8635 e.jump_cursor(50, 0);
8636 e.set_viewport_top(45);
8637 let top = e.host().viewport().top_row;
8638 run_keys(&mut e, "M");
8639 assert_eq!(e.cursor().0, top + 4);
8641 }
8642
8643 #[test]
8644 fn g_capital_m_lands_at_line_midpoint() {
8645 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8647 assert_eq!(e.cursor(), (0, 6));
8649 }
8650
8651 #[test]
8652 fn g_capital_m_on_empty_line_stays_at_zero() {
8653 let mut e = editor_with("");
8654 run_keys(&mut e, "gM");
8655 assert_eq!(e.cursor(), (0, 0));
8656 }
8657
8658 #[test]
8659 fn g_capital_m_uses_current_line_only() {
8660 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8663 run_keys(&mut e, "gM");
8664 assert_eq!(e.cursor(), (1, 6));
8665 }
8666
8667 #[test]
8668 fn capital_h_count_offsets_from_top() {
8669 let mut e = editor_with_rows(100, 10);
8670 e.jump_cursor(50, 0);
8671 e.set_viewport_top(45);
8672 let top = e.host().viewport().top_row;
8673 run_keys(&mut e, "3H");
8674 assert_eq!(e.cursor().0, top + 2);
8675 }
8676
8677 #[test]
8680 fn ctrl_o_returns_to_pre_g_position() {
8681 let mut e = editor_with_rows(50, 20);
8682 e.jump_cursor(5, 2);
8683 run_keys(&mut e, "G");
8684 assert_eq!(e.cursor().0, 49);
8685 run_keys(&mut e, "<C-o>");
8686 assert_eq!(e.cursor(), (5, 2));
8687 }
8688
8689 #[test]
8690 fn ctrl_i_redoes_jump_after_ctrl_o() {
8691 let mut e = editor_with_rows(50, 20);
8692 e.jump_cursor(5, 2);
8693 run_keys(&mut e, "G");
8694 let post = e.cursor();
8695 run_keys(&mut e, "<C-o>");
8696 run_keys(&mut e, "<C-i>");
8697 assert_eq!(e.cursor(), post);
8698 }
8699
8700 #[test]
8701 fn new_jump_clears_forward_stack() {
8702 let mut e = editor_with_rows(50, 20);
8703 e.jump_cursor(5, 2);
8704 run_keys(&mut e, "G");
8705 run_keys(&mut e, "<C-o>");
8706 run_keys(&mut e, "gg");
8707 run_keys(&mut e, "<C-i>");
8708 assert_eq!(e.cursor().0, 0);
8709 }
8710
8711 #[test]
8712 fn ctrl_o_on_empty_stack_is_noop() {
8713 let mut e = editor_with_rows(10, 20);
8714 e.jump_cursor(3, 1);
8715 run_keys(&mut e, "<C-o>");
8716 assert_eq!(e.cursor(), (3, 1));
8717 }
8718
8719 #[test]
8720 fn asterisk_search_pushes_jump() {
8721 let mut e = editor_with("foo bar\nbaz foo end");
8722 e.jump_cursor(0, 0);
8723 run_keys(&mut e, "*");
8724 let after = e.cursor();
8725 assert_ne!(after, (0, 0));
8726 run_keys(&mut e, "<C-o>");
8727 assert_eq!(e.cursor(), (0, 0));
8728 }
8729
8730 #[test]
8731 fn h_viewport_jump_is_recorded() {
8732 let mut e = editor_with_rows(100, 10);
8733 e.jump_cursor(50, 0);
8734 e.set_viewport_top(45);
8735 let pre = e.cursor();
8736 run_keys(&mut e, "H");
8737 assert_ne!(e.cursor(), pre);
8738 run_keys(&mut e, "<C-o>");
8739 assert_eq!(e.cursor(), pre);
8740 }
8741
8742 #[test]
8743 fn j_k_motion_does_not_push_jump() {
8744 let mut e = editor_with_rows(50, 20);
8745 e.jump_cursor(5, 0);
8746 run_keys(&mut e, "jjj");
8747 run_keys(&mut e, "<C-o>");
8748 assert_eq!(e.cursor().0, 8);
8749 }
8750
8751 #[test]
8752 fn jumplist_caps_at_100() {
8753 let mut e = editor_with_rows(200, 20);
8754 for i in 0..101 {
8755 e.jump_cursor(i, 0);
8756 run_keys(&mut e, "G");
8757 }
8758 assert!(e.vim.jump_back.len() <= 100);
8759 }
8760
8761 #[test]
8762 fn tab_acts_as_ctrl_i() {
8763 let mut e = editor_with_rows(50, 20);
8764 e.jump_cursor(5, 2);
8765 run_keys(&mut e, "G");
8766 let post = e.cursor();
8767 run_keys(&mut e, "<C-o>");
8768 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8769 assert_eq!(e.cursor(), post);
8770 }
8771
8772 #[test]
8775 fn ma_then_backtick_a_jumps_exact() {
8776 let mut e = editor_with_rows(50, 20);
8777 e.jump_cursor(5, 3);
8778 run_keys(&mut e, "ma");
8779 e.jump_cursor(20, 0);
8780 run_keys(&mut e, "`a");
8781 assert_eq!(e.cursor(), (5, 3));
8782 }
8783
8784 #[test]
8785 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8786 let mut e = editor_with_rows(50, 20);
8787 e.jump_cursor(5, 6);
8789 run_keys(&mut e, "ma");
8790 e.jump_cursor(30, 4);
8791 run_keys(&mut e, "'a");
8792 assert_eq!(e.cursor(), (5, 2));
8793 }
8794
8795 #[test]
8796 fn goto_mark_pushes_jumplist() {
8797 let mut e = editor_with_rows(50, 20);
8798 e.jump_cursor(10, 2);
8799 run_keys(&mut e, "mz");
8800 e.jump_cursor(3, 0);
8801 run_keys(&mut e, "`z");
8802 assert_eq!(e.cursor(), (10, 2));
8803 run_keys(&mut e, "<C-o>");
8804 assert_eq!(e.cursor(), (3, 0));
8805 }
8806
8807 #[test]
8808 fn goto_missing_mark_is_noop() {
8809 let mut e = editor_with_rows(50, 20);
8810 e.jump_cursor(3, 1);
8811 run_keys(&mut e, "`q");
8812 assert_eq!(e.cursor(), (3, 1));
8813 }
8814
8815 #[test]
8816 fn uppercase_mark_stored_under_uppercase_key() {
8817 let mut e = editor_with_rows(50, 20);
8818 e.jump_cursor(5, 3);
8819 run_keys(&mut e, "mA");
8820 assert_eq!(e.mark('A'), Some((5, 3)));
8823 assert!(e.mark('a').is_none());
8824 }
8825
8826 #[test]
8827 fn mark_survives_document_shrink_via_clamp() {
8828 let mut e = editor_with_rows(50, 20);
8829 e.jump_cursor(40, 4);
8830 run_keys(&mut e, "mx");
8831 e.set_content("a\nb\nc\nd\ne");
8833 run_keys(&mut e, "`x");
8834 let (r, _) = e.cursor();
8836 assert!(r <= 4);
8837 }
8838
8839 #[test]
8840 fn g_semicolon_walks_back_through_edits() {
8841 let mut e = editor_with("alpha\nbeta\ngamma");
8842 e.jump_cursor(0, 0);
8845 run_keys(&mut e, "iX<Esc>");
8846 e.jump_cursor(2, 0);
8847 run_keys(&mut e, "iY<Esc>");
8848 run_keys(&mut e, "g;");
8850 assert_eq!(e.cursor(), (2, 1));
8851 run_keys(&mut e, "g;");
8853 assert_eq!(e.cursor(), (0, 1));
8854 run_keys(&mut e, "g;");
8856 assert_eq!(e.cursor(), (0, 1));
8857 }
8858
8859 #[test]
8860 fn g_comma_walks_forward_after_g_semicolon() {
8861 let mut e = editor_with("a\nb\nc");
8862 e.jump_cursor(0, 0);
8863 run_keys(&mut e, "iX<Esc>");
8864 e.jump_cursor(2, 0);
8865 run_keys(&mut e, "iY<Esc>");
8866 run_keys(&mut e, "g;");
8867 run_keys(&mut e, "g;");
8868 assert_eq!(e.cursor(), (0, 1));
8869 run_keys(&mut e, "g,");
8870 assert_eq!(e.cursor(), (2, 1));
8871 }
8872
8873 #[test]
8874 fn new_edit_during_walk_trims_forward_entries() {
8875 let mut e = editor_with("a\nb\nc\nd");
8876 e.jump_cursor(0, 0);
8877 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8879 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8882 run_keys(&mut e, "g;");
8883 assert_eq!(e.cursor(), (0, 1));
8884 run_keys(&mut e, "iZ<Esc>");
8886 run_keys(&mut e, "g,");
8888 assert_ne!(e.cursor(), (2, 1));
8890 }
8891
8892 #[test]
8898 fn capital_mark_set_and_jump() {
8899 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8900 e.jump_cursor(2, 1);
8901 run_keys(&mut e, "mA");
8902 e.jump_cursor(0, 0);
8904 run_keys(&mut e, "'A");
8906 assert_eq!(e.cursor().0, 2);
8908 }
8909
8910 #[test]
8911 fn capital_mark_survives_set_content() {
8912 let mut e = editor_with("first buffer line\nsecond");
8913 e.jump_cursor(1, 3);
8914 run_keys(&mut e, "mA");
8915 e.set_content("totally different content\non many\nrows of text");
8917 e.jump_cursor(0, 0);
8919 run_keys(&mut e, "'A");
8920 assert_eq!(e.cursor().0, 1);
8921 }
8922
8923 #[test]
8928 fn capital_mark_shifts_with_edit() {
8929 let mut e = editor_with("a\nb\nc\nd");
8930 e.jump_cursor(3, 0);
8931 run_keys(&mut e, "mA");
8932 e.jump_cursor(0, 0);
8934 run_keys(&mut e, "dd");
8935 e.jump_cursor(0, 0);
8936 run_keys(&mut e, "'A");
8937 assert_eq!(e.cursor().0, 2);
8938 }
8939
8940 #[test]
8941 fn mark_below_delete_shifts_up() {
8942 let mut e = editor_with("a\nb\nc\nd\ne");
8943 e.jump_cursor(3, 0);
8945 run_keys(&mut e, "ma");
8946 e.jump_cursor(0, 0);
8948 run_keys(&mut e, "dd");
8949 e.jump_cursor(0, 0);
8951 run_keys(&mut e, "'a");
8952 assert_eq!(e.cursor().0, 2);
8953 assert_eq!(e.buffer().line(2).unwrap(), "d");
8954 }
8955
8956 #[test]
8957 fn mark_on_deleted_row_is_dropped() {
8958 let mut e = editor_with("a\nb\nc\nd");
8959 e.jump_cursor(1, 0);
8961 run_keys(&mut e, "ma");
8962 run_keys(&mut e, "dd");
8964 e.jump_cursor(2, 0);
8966 run_keys(&mut e, "'a");
8967 assert_eq!(e.cursor().0, 2);
8969 }
8970
8971 #[test]
8972 fn mark_above_edit_unchanged() {
8973 let mut e = editor_with("a\nb\nc\nd\ne");
8974 e.jump_cursor(0, 0);
8976 run_keys(&mut e, "ma");
8977 e.jump_cursor(3, 0);
8979 run_keys(&mut e, "dd");
8980 e.jump_cursor(2, 0);
8982 run_keys(&mut e, "'a");
8983 assert_eq!(e.cursor().0, 0);
8984 }
8985
8986 #[test]
8987 fn mark_shifts_down_after_insert() {
8988 let mut e = editor_with("a\nb\nc");
8989 e.jump_cursor(2, 0);
8991 run_keys(&mut e, "ma");
8992 e.jump_cursor(0, 0);
8994 run_keys(&mut e, "Onew<Esc>");
8995 e.jump_cursor(0, 0);
8998 run_keys(&mut e, "'a");
8999 assert_eq!(e.cursor().0, 3);
9000 assert_eq!(e.buffer().line(3).unwrap(), "c");
9001 }
9002
9003 #[test]
9006 fn forward_search_commit_pushes_jump() {
9007 let mut e = editor_with("alpha beta\nfoo target end\nmore");
9008 e.jump_cursor(0, 0);
9009 run_keys(&mut e, "/target<CR>");
9010 assert_ne!(e.cursor(), (0, 0));
9012 run_keys(&mut e, "<C-o>");
9014 assert_eq!(e.cursor(), (0, 0));
9015 }
9016
9017 #[test]
9018 fn search_commit_no_match_does_not_push_jump() {
9019 let mut e = editor_with("alpha beta\nfoo end");
9020 e.jump_cursor(0, 3);
9021 let pre_len = e.vim.jump_back.len();
9022 run_keys(&mut e, "/zzznotfound<CR>");
9023 assert_eq!(e.vim.jump_back.len(), pre_len);
9025 }
9026
9027 #[test]
9030 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
9031 let mut e = editor_with("hello world");
9032 run_keys(&mut e, "lll");
9033 let (row, col) = e.cursor();
9034 assert_eq!(e.buffer.cursor().row, row);
9035 assert_eq!(e.buffer.cursor().col, col);
9036 }
9037
9038 #[test]
9039 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
9040 let mut e = editor_with("aaaa\nbbbb\ncccc");
9041 run_keys(&mut e, "jj");
9042 let (row, col) = e.cursor();
9043 assert_eq!(e.buffer.cursor().row, row);
9044 assert_eq!(e.buffer.cursor().col, col);
9045 }
9046
9047 #[test]
9048 fn buffer_cursor_mirrors_textarea_after_word_motion() {
9049 let mut e = editor_with("foo bar baz");
9050 run_keys(&mut e, "ww");
9051 let (row, col) = e.cursor();
9052 assert_eq!(e.buffer.cursor().row, row);
9053 assert_eq!(e.buffer.cursor().col, col);
9054 }
9055
9056 #[test]
9057 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
9058 let mut e = editor_with("a\nb\nc\nd\ne");
9059 run_keys(&mut e, "G");
9060 let (row, col) = e.cursor();
9061 assert_eq!(e.buffer.cursor().row, row);
9062 assert_eq!(e.buffer.cursor().col, col);
9063 }
9064
9065 #[test]
9066 fn editor_sticky_col_tracks_horizontal_motion() {
9067 let mut e = editor_with("longline\nhi\nlongline");
9068 run_keys(&mut e, "fl");
9073 let landed = e.cursor().1;
9074 assert!(landed > 0, "fl should have moved");
9075 run_keys(&mut e, "j");
9076 assert_eq!(e.sticky_col(), Some(landed));
9079 }
9080
9081 #[test]
9082 fn buffer_content_mirrors_textarea_after_insert() {
9083 let mut e = editor_with("hello");
9084 run_keys(&mut e, "iXYZ<Esc>");
9085 let text = e.buffer().lines().join("\n");
9086 assert_eq!(e.buffer.as_string(), text);
9087 }
9088
9089 #[test]
9090 fn buffer_content_mirrors_textarea_after_delete() {
9091 let mut e = editor_with("alpha bravo charlie");
9092 run_keys(&mut e, "dw");
9093 let text = e.buffer().lines().join("\n");
9094 assert_eq!(e.buffer.as_string(), text);
9095 }
9096
9097 #[test]
9098 fn buffer_content_mirrors_textarea_after_dd() {
9099 let mut e = editor_with("a\nb\nc\nd");
9100 run_keys(&mut e, "jdd");
9101 let text = e.buffer().lines().join("\n");
9102 assert_eq!(e.buffer.as_string(), text);
9103 }
9104
9105 #[test]
9106 fn buffer_content_mirrors_textarea_after_open_line() {
9107 let mut e = editor_with("foo\nbar");
9108 run_keys(&mut e, "oNEW<Esc>");
9109 let text = e.buffer().lines().join("\n");
9110 assert_eq!(e.buffer.as_string(), text);
9111 }
9112
9113 #[test]
9114 fn buffer_content_mirrors_textarea_after_paste() {
9115 let mut e = editor_with("hello");
9116 run_keys(&mut e, "yy");
9117 run_keys(&mut e, "p");
9118 let text = e.buffer().lines().join("\n");
9119 assert_eq!(e.buffer.as_string(), text);
9120 }
9121
9122 #[test]
9123 fn buffer_selection_none_in_normal_mode() {
9124 let e = editor_with("foo bar");
9125 assert!(e.buffer_selection().is_none());
9126 }
9127
9128 #[test]
9129 fn buffer_selection_char_in_visual_mode() {
9130 use hjkl_buffer::{Position, Selection};
9131 let mut e = editor_with("hello world");
9132 run_keys(&mut e, "vlll");
9133 assert_eq!(
9134 e.buffer_selection(),
9135 Some(Selection::Char {
9136 anchor: Position::new(0, 0),
9137 head: Position::new(0, 3),
9138 })
9139 );
9140 }
9141
9142 #[test]
9143 fn buffer_selection_line_in_visual_line_mode() {
9144 use hjkl_buffer::Selection;
9145 let mut e = editor_with("a\nb\nc\nd");
9146 run_keys(&mut e, "Vj");
9147 assert_eq!(
9148 e.buffer_selection(),
9149 Some(Selection::Line {
9150 anchor_row: 0,
9151 head_row: 1,
9152 })
9153 );
9154 }
9155
9156 #[test]
9157 fn wrapscan_off_blocks_wrap_around() {
9158 let mut e = editor_with("first\nsecond\nthird\n");
9159 e.settings_mut().wrapscan = false;
9160 e.jump_cursor(2, 0);
9162 run_keys(&mut e, "/first<CR>");
9163 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9165 e.settings_mut().wrapscan = true;
9167 run_keys(&mut e, "/first<CR>");
9168 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9169 }
9170
9171 #[test]
9172 fn smartcase_uppercase_pattern_stays_sensitive() {
9173 let mut e = editor_with("foo\nFoo\nBAR\n");
9174 e.settings_mut().ignore_case = true;
9175 e.settings_mut().smartcase = true;
9176 run_keys(&mut e, "/foo<CR>");
9179 let r1 = e
9180 .search_state()
9181 .pattern
9182 .as_ref()
9183 .unwrap()
9184 .as_str()
9185 .to_string();
9186 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9187 run_keys(&mut e, "/Foo<CR>");
9189 let r2 = e
9190 .search_state()
9191 .pattern
9192 .as_ref()
9193 .unwrap()
9194 .as_str()
9195 .to_string();
9196 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9197 }
9198
9199 #[test]
9200 fn enter_with_autoindent_copies_leading_whitespace() {
9201 let mut e = editor_with(" foo");
9202 e.jump_cursor(0, 7);
9203 run_keys(&mut e, "i<CR>");
9204 assert_eq!(e.buffer.line(1).unwrap(), " ");
9205 }
9206
9207 #[test]
9208 fn enter_without_autoindent_inserts_bare_newline() {
9209 let mut e = editor_with(" foo");
9210 e.settings_mut().autoindent = false;
9211 e.jump_cursor(0, 7);
9212 run_keys(&mut e, "i<CR>");
9213 assert_eq!(e.buffer.line(1).unwrap(), "");
9214 }
9215
9216 #[test]
9217 fn iskeyword_default_treats_alnum_underscore_as_word() {
9218 let mut e = editor_with("foo_bar baz");
9219 e.jump_cursor(0, 0);
9223 run_keys(&mut e, "*");
9224 let p = e
9225 .search_state()
9226 .pattern
9227 .as_ref()
9228 .unwrap()
9229 .as_str()
9230 .to_string();
9231 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9232 }
9233
9234 #[test]
9235 fn w_motion_respects_custom_iskeyword() {
9236 let mut e = editor_with("foo-bar baz");
9240 run_keys(&mut e, "w");
9241 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9242 let mut e2 = editor_with("foo-bar baz");
9245 e2.set_iskeyword("@,_,45");
9246 run_keys(&mut e2, "w");
9247 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9248 }
9249
9250 #[test]
9251 fn iskeyword_with_dash_treats_dash_as_word_char() {
9252 let mut e = editor_with("foo-bar baz");
9253 e.settings_mut().iskeyword = "@,_,45".to_string();
9254 e.jump_cursor(0, 0);
9255 run_keys(&mut e, "*");
9256 let p = e
9257 .search_state()
9258 .pattern
9259 .as_ref()
9260 .unwrap()
9261 .as_str()
9262 .to_string();
9263 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9264 }
9265
9266 #[test]
9267 fn timeoutlen_drops_pending_g_prefix() {
9268 use std::time::{Duration, Instant};
9269 let mut e = editor_with("a\nb\nc");
9270 e.jump_cursor(2, 0);
9271 run_keys(&mut e, "g");
9273 assert!(matches!(e.vim.pending, super::Pending::G));
9274 e.settings.timeout_len = Duration::from_nanos(0);
9282 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9283 e.vim.last_input_host_at = Some(Duration::ZERO);
9284 run_keys(&mut e, "g");
9288 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9290 }
9291
9292 #[test]
9293 fn undobreak_on_breaks_group_at_arrow_motion() {
9294 let mut e = editor_with("");
9295 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9297 let line = e.buffer.line(0).unwrap_or("").to_string();
9300 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9301 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9302 }
9303
9304 #[test]
9305 fn undobreak_off_keeps_full_run_in_one_group() {
9306 let mut e = editor_with("");
9307 e.settings_mut().undo_break_on_motion = false;
9308 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9309 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9312 }
9313
9314 #[test]
9315 fn undobreak_round_trips_through_options() {
9316 let e = editor_with("");
9317 let opts = e.current_options();
9318 assert!(opts.undo_break_on_motion);
9319 let mut e2 = editor_with("");
9320 let mut new_opts = opts.clone();
9321 new_opts.undo_break_on_motion = false;
9322 e2.apply_options(&new_opts);
9323 assert!(!e2.current_options().undo_break_on_motion);
9324 }
9325
9326 #[test]
9327 fn undo_levels_cap_drops_oldest() {
9328 let mut e = editor_with("abcde");
9329 e.settings_mut().undo_levels = 3;
9330 run_keys(&mut e, "ra");
9331 run_keys(&mut e, "lrb");
9332 run_keys(&mut e, "lrc");
9333 run_keys(&mut e, "lrd");
9334 run_keys(&mut e, "lre");
9335 assert_eq!(e.undo_stack_len(), 3);
9336 }
9337
9338 #[test]
9339 fn tab_inserts_literal_tab_when_noexpandtab() {
9340 let mut e = editor_with("");
9341 e.settings_mut().expandtab = false;
9344 e.settings_mut().softtabstop = 0;
9345 run_keys(&mut e, "i");
9346 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9347 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9348 }
9349
9350 #[test]
9351 fn tab_inserts_spaces_when_expandtab() {
9352 let mut e = editor_with("");
9353 e.settings_mut().expandtab = true;
9354 e.settings_mut().tabstop = 4;
9355 run_keys(&mut e, "i");
9356 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9357 assert_eq!(e.buffer.line(0).unwrap(), " ");
9358 }
9359
9360 #[test]
9361 fn tab_with_softtabstop_fills_to_next_boundary() {
9362 let mut e = editor_with("ab");
9364 e.settings_mut().expandtab = true;
9365 e.settings_mut().tabstop = 8;
9366 e.settings_mut().softtabstop = 4;
9367 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9369 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9370 }
9371
9372 #[test]
9373 fn backspace_deletes_softtab_run() {
9374 let mut e = editor_with(" x");
9377 e.settings_mut().softtabstop = 4;
9378 run_keys(&mut e, "fxi");
9380 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9381 assert_eq!(e.buffer.line(0).unwrap(), "x");
9382 }
9383
9384 #[test]
9385 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9386 let mut e = editor_with(" x");
9389 e.settings_mut().softtabstop = 4;
9390 run_keys(&mut e, "fxi");
9391 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9392 assert_eq!(e.buffer.line(0).unwrap(), " x");
9393 }
9394
9395 #[test]
9396 fn readonly_blocks_insert_mutation() {
9397 let mut e = editor_with("hello");
9398 e.settings_mut().readonly = true;
9399 run_keys(&mut e, "iX<Esc>");
9400 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9401 }
9402
9403 #[cfg(feature = "ratatui")]
9404 #[test]
9405 fn intern_ratatui_style_dedups_repeated_styles() {
9406 use ratatui::style::{Color, Style};
9407 let mut e = editor_with("");
9408 let red = Style::default().fg(Color::Red);
9409 let blue = Style::default().fg(Color::Blue);
9410 let id_r1 = e.intern_ratatui_style(red);
9411 let id_r2 = e.intern_ratatui_style(red);
9412 let id_b = e.intern_ratatui_style(blue);
9413 assert_eq!(id_r1, id_r2);
9414 assert_ne!(id_r1, id_b);
9415 assert_eq!(e.style_table().len(), 2);
9416 }
9417
9418 #[cfg(feature = "ratatui")]
9419 #[test]
9420 fn install_ratatui_syntax_spans_translates_styled_spans() {
9421 use ratatui::style::{Color, Style};
9422 let mut e = editor_with("SELECT foo");
9423 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9424 let by_row = e.buffer_spans();
9425 assert_eq!(by_row.len(), 1);
9426 assert_eq!(by_row[0].len(), 1);
9427 assert_eq!(by_row[0][0].start_byte, 0);
9428 assert_eq!(by_row[0][0].end_byte, 6);
9429 let id = by_row[0][0].style;
9430 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9431 }
9432
9433 #[cfg(feature = "ratatui")]
9434 #[test]
9435 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9436 use ratatui::style::{Color, Style};
9437 let mut e = editor_with("hello");
9438 e.install_ratatui_syntax_spans(vec![vec![(
9439 0,
9440 usize::MAX,
9441 Style::default().fg(Color::Blue),
9442 )]]);
9443 let by_row = e.buffer_spans();
9444 assert_eq!(by_row[0][0].end_byte, 5);
9445 }
9446
9447 #[cfg(feature = "ratatui")]
9448 #[test]
9449 fn install_ratatui_syntax_spans_drops_zero_width() {
9450 use ratatui::style::{Color, Style};
9451 let mut e = editor_with("abc");
9452 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9453 assert!(e.buffer_spans()[0].is_empty());
9454 }
9455
9456 #[test]
9457 fn named_register_yank_into_a_then_paste_from_a() {
9458 let mut e = editor_with("hello world\nsecond");
9459 run_keys(&mut e, "\"ayw");
9460 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9462 run_keys(&mut e, "j0\"aP");
9464 assert_eq!(e.buffer().lines()[1], "hello second");
9465 }
9466
9467 #[test]
9468 fn capital_r_overstrikes_chars() {
9469 let mut e = editor_with("hello");
9470 e.jump_cursor(0, 0);
9471 run_keys(&mut e, "RXY<Esc>");
9472 assert_eq!(e.buffer().lines()[0], "XYllo");
9474 }
9475
9476 #[test]
9477 fn capital_r_at_eol_appends() {
9478 let mut e = editor_with("hi");
9479 e.jump_cursor(0, 1);
9480 run_keys(&mut e, "RXYZ<Esc>");
9482 assert_eq!(e.buffer().lines()[0], "hXYZ");
9483 }
9484
9485 #[test]
9486 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9487 let mut e = editor_with("abc");
9491 e.jump_cursor(0, 0);
9492 run_keys(&mut e, "RX<Esc>");
9493 assert_eq!(e.buffer().lines()[0], "Xbc");
9494 }
9495
9496 #[test]
9497 fn ctrl_r_in_insert_pastes_named_register() {
9498 let mut e = editor_with("hello world");
9499 run_keys(&mut e, "\"ayw");
9501 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9502 run_keys(&mut e, "o");
9504 assert_eq!(e.vim_mode(), VimMode::Insert);
9505 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9506 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9507 assert_eq!(e.buffer().lines()[1], "hello ");
9508 assert_eq!(e.cursor(), (1, 6));
9510 assert_eq!(e.vim_mode(), VimMode::Insert);
9512 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9513 assert_eq!(e.buffer().lines()[1], "hello X");
9514 }
9515
9516 #[test]
9517 fn ctrl_r_with_unnamed_register() {
9518 let mut e = editor_with("foo");
9519 run_keys(&mut e, "yiw");
9520 run_keys(&mut e, "A ");
9521 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9523 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9524 assert_eq!(e.buffer().lines()[0], "foo foo");
9525 }
9526
9527 #[test]
9528 fn ctrl_r_unknown_selector_is_no_op() {
9529 let mut e = editor_with("abc");
9530 run_keys(&mut e, "A");
9531 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9532 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9535 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9536 assert_eq!(e.buffer().lines()[0], "abcZ");
9537 }
9538
9539 #[test]
9540 fn ctrl_r_multiline_register_pastes_with_newlines() {
9541 let mut e = editor_with("alpha\nbeta\ngamma");
9542 run_keys(&mut e, "\"byy");
9544 run_keys(&mut e, "j\"byy");
9545 run_keys(&mut e, "ggVj\"by");
9549 let payload = e.registers().read('b').unwrap().text.clone();
9550 assert!(payload.contains('\n'));
9551 run_keys(&mut e, "Go");
9552 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9553 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9554 let total_lines = e.buffer().lines().len();
9557 assert!(total_lines >= 5);
9558 }
9559
9560 #[test]
9561 fn yank_zero_holds_last_yank_after_delete() {
9562 let mut e = editor_with("hello world");
9563 run_keys(&mut e, "yw");
9564 let yanked = e.registers().read('0').unwrap().text.clone();
9565 assert!(!yanked.is_empty());
9566 run_keys(&mut e, "dw");
9568 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9569 assert!(!e.registers().read('1').unwrap().text.is_empty());
9571 }
9572
9573 #[test]
9574 fn delete_ring_rotates_through_one_through_nine() {
9575 let mut e = editor_with("a b c d e f g h i j");
9576 for _ in 0..3 {
9578 run_keys(&mut e, "dw");
9579 }
9580 let r1 = e.registers().read('1').unwrap().text.clone();
9582 let r2 = e.registers().read('2').unwrap().text.clone();
9583 let r3 = e.registers().read('3').unwrap().text.clone();
9584 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9585 assert_ne!(r1, r2);
9586 assert_ne!(r2, r3);
9587 }
9588
9589 #[test]
9590 fn capital_register_appends_to_lowercase() {
9591 let mut e = editor_with("foo bar");
9592 run_keys(&mut e, "\"ayw");
9593 let first = e.registers().read('a').unwrap().text.clone();
9594 assert!(first.contains("foo"));
9595 run_keys(&mut e, "w\"Ayw");
9597 let combined = e.registers().read('a').unwrap().text.clone();
9598 assert!(combined.starts_with(&first));
9599 assert!(combined.contains("bar"));
9600 }
9601
9602 #[test]
9603 fn zf_in_visual_line_creates_closed_fold() {
9604 let mut e = editor_with("a\nb\nc\nd\ne");
9605 e.jump_cursor(1, 0);
9607 run_keys(&mut e, "Vjjzf");
9608 assert_eq!(e.buffer().folds().len(), 1);
9609 let f = e.buffer().folds()[0];
9610 assert_eq!(f.start_row, 1);
9611 assert_eq!(f.end_row, 3);
9612 assert!(f.closed);
9613 }
9614
9615 #[test]
9616 fn zfj_in_normal_creates_two_row_fold() {
9617 let mut e = editor_with("a\nb\nc\nd\ne");
9618 e.jump_cursor(1, 0);
9619 run_keys(&mut e, "zfj");
9620 assert_eq!(e.buffer().folds().len(), 1);
9621 let f = e.buffer().folds()[0];
9622 assert_eq!(f.start_row, 1);
9623 assert_eq!(f.end_row, 2);
9624 assert!(f.closed);
9625 assert_eq!(e.cursor().0, 1);
9627 }
9628
9629 #[test]
9630 fn zf_with_count_folds_count_rows() {
9631 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9632 e.jump_cursor(0, 0);
9633 run_keys(&mut e, "zf3j");
9635 assert_eq!(e.buffer().folds().len(), 1);
9636 let f = e.buffer().folds()[0];
9637 assert_eq!(f.start_row, 0);
9638 assert_eq!(f.end_row, 3);
9639 }
9640
9641 #[test]
9642 fn zfk_folds_upward_range() {
9643 let mut e = editor_with("a\nb\nc\nd\ne");
9644 e.jump_cursor(3, 0);
9645 run_keys(&mut e, "zfk");
9646 let f = e.buffer().folds()[0];
9647 assert_eq!(f.start_row, 2);
9649 assert_eq!(f.end_row, 3);
9650 }
9651
9652 #[test]
9653 fn zf_capital_g_folds_to_bottom() {
9654 let mut e = editor_with("a\nb\nc\nd\ne");
9655 e.jump_cursor(1, 0);
9656 run_keys(&mut e, "zfG");
9658 let f = e.buffer().folds()[0];
9659 assert_eq!(f.start_row, 1);
9660 assert_eq!(f.end_row, 4);
9661 }
9662
9663 #[test]
9664 fn zfgg_folds_to_top_via_operator_pipeline() {
9665 let mut e = editor_with("a\nb\nc\nd\ne");
9666 e.jump_cursor(3, 0);
9667 run_keys(&mut e, "zfgg");
9671 let f = e.buffer().folds()[0];
9672 assert_eq!(f.start_row, 0);
9673 assert_eq!(f.end_row, 3);
9674 }
9675
9676 #[test]
9677 fn zfip_folds_paragraph_via_text_object() {
9678 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9679 e.jump_cursor(1, 0);
9680 run_keys(&mut e, "zfip");
9682 assert_eq!(e.buffer().folds().len(), 1);
9683 let f = e.buffer().folds()[0];
9684 assert_eq!(f.start_row, 0);
9685 assert_eq!(f.end_row, 2);
9686 }
9687
9688 #[test]
9689 fn zfap_folds_paragraph_with_trailing_blank() {
9690 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9691 e.jump_cursor(0, 0);
9692 run_keys(&mut e, "zfap");
9694 let f = e.buffer().folds()[0];
9695 assert_eq!(f.start_row, 0);
9696 assert_eq!(f.end_row, 3);
9697 }
9698
9699 #[test]
9700 fn zf_paragraph_motion_folds_to_blank() {
9701 let mut e = editor_with("alpha\nbeta\n\ngamma");
9702 e.jump_cursor(0, 0);
9703 run_keys(&mut e, "zf}");
9705 let f = e.buffer().folds()[0];
9706 assert_eq!(f.start_row, 0);
9707 assert_eq!(f.end_row, 2);
9708 }
9709
9710 #[test]
9711 fn za_toggles_fold_under_cursor() {
9712 let mut e = editor_with("a\nb\nc\nd");
9713 e.buffer_mut().add_fold(1, 2, true);
9714 e.jump_cursor(1, 0);
9715 run_keys(&mut e, "za");
9716 assert!(!e.buffer().folds()[0].closed);
9717 run_keys(&mut e, "za");
9718 assert!(e.buffer().folds()[0].closed);
9719 }
9720
9721 #[test]
9722 fn zr_opens_all_folds_zm_closes_all() {
9723 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9724 e.buffer_mut().add_fold(0, 1, true);
9725 e.buffer_mut().add_fold(2, 3, true);
9726 e.buffer_mut().add_fold(4, 5, true);
9727 run_keys(&mut e, "zR");
9728 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9729 run_keys(&mut e, "zM");
9730 assert!(e.buffer().folds().iter().all(|f| f.closed));
9731 }
9732
9733 #[test]
9734 fn ze_clears_all_folds() {
9735 let mut e = editor_with("a\nb\nc\nd");
9736 e.buffer_mut().add_fold(0, 1, true);
9737 e.buffer_mut().add_fold(2, 3, false);
9738 run_keys(&mut e, "zE");
9739 assert!(e.buffer().folds().is_empty());
9740 }
9741
9742 #[test]
9743 fn g_underscore_jumps_to_last_non_blank() {
9744 let mut e = editor_with("hello world ");
9745 run_keys(&mut e, "g_");
9746 assert_eq!(e.cursor().1, 10);
9748 }
9749
9750 #[test]
9751 fn gj_and_gk_alias_j_and_k() {
9752 let mut e = editor_with("a\nb\nc");
9753 run_keys(&mut e, "gj");
9754 assert_eq!(e.cursor().0, 1);
9755 run_keys(&mut e, "gk");
9756 assert_eq!(e.cursor().0, 0);
9757 }
9758
9759 #[test]
9760 fn paragraph_motions_walk_blank_lines() {
9761 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9762 run_keys(&mut e, "}");
9763 assert_eq!(e.cursor().0, 2);
9764 run_keys(&mut e, "}");
9765 assert_eq!(e.cursor().0, 5);
9766 run_keys(&mut e, "{");
9767 assert_eq!(e.cursor().0, 2);
9768 }
9769
9770 #[test]
9771 fn gv_reenters_last_visual_selection() {
9772 let mut e = editor_with("alpha\nbeta\ngamma");
9773 run_keys(&mut e, "Vj");
9774 run_keys(&mut e, "<Esc>");
9776 assert_eq!(e.vim_mode(), VimMode::Normal);
9777 run_keys(&mut e, "gv");
9779 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9780 }
9781
9782 #[test]
9783 fn o_in_visual_swaps_anchor_and_cursor() {
9784 let mut e = editor_with("hello world");
9785 run_keys(&mut e, "vllll");
9787 assert_eq!(e.cursor().1, 4);
9788 run_keys(&mut e, "o");
9790 assert_eq!(e.cursor().1, 0);
9791 assert_eq!(e.vim.visual_anchor, (0, 4));
9793 }
9794
9795 #[test]
9796 fn editing_inside_fold_invalidates_it() {
9797 let mut e = editor_with("a\nb\nc\nd");
9798 e.buffer_mut().add_fold(1, 2, true);
9799 e.jump_cursor(1, 0);
9800 run_keys(&mut e, "iX<Esc>");
9802 assert!(e.buffer().folds().is_empty());
9804 }
9805
9806 #[test]
9807 fn zd_removes_fold_under_cursor() {
9808 let mut e = editor_with("a\nb\nc\nd");
9809 e.buffer_mut().add_fold(1, 2, true);
9810 e.jump_cursor(2, 0);
9811 run_keys(&mut e, "zd");
9812 assert!(e.buffer().folds().is_empty());
9813 }
9814
9815 #[test]
9816 fn take_fold_ops_observes_z_keystroke_dispatch() {
9817 use crate::types::FoldOp;
9822 let mut e = editor_with("a\nb\nc\nd");
9823 e.buffer_mut().add_fold(1, 2, true);
9824 e.jump_cursor(1, 0);
9825 let _ = e.take_fold_ops();
9828 run_keys(&mut e, "zo");
9829 run_keys(&mut e, "zM");
9830 let ops = e.take_fold_ops();
9831 assert_eq!(ops.len(), 2);
9832 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9833 assert!(matches!(ops[1], FoldOp::CloseAll));
9834 assert!(e.take_fold_ops().is_empty());
9836 }
9837
9838 #[test]
9839 fn edit_pipeline_emits_invalidate_fold_op() {
9840 use crate::types::FoldOp;
9843 let mut e = editor_with("a\nb\nc\nd");
9844 e.buffer_mut().add_fold(1, 2, true);
9845 e.jump_cursor(1, 0);
9846 let _ = e.take_fold_ops();
9847 run_keys(&mut e, "iX<Esc>");
9848 let ops = e.take_fold_ops();
9849 assert!(
9850 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9851 "expected at least one Invalidate op, got {ops:?}"
9852 );
9853 }
9854
9855 #[test]
9856 fn dot_mark_jumps_to_last_edit_position() {
9857 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9858 e.jump_cursor(2, 0);
9859 run_keys(&mut e, "iX<Esc>");
9861 let after_edit = e.cursor();
9862 run_keys(&mut e, "gg");
9864 assert_eq!(e.cursor().0, 0);
9865 run_keys(&mut e, "'.");
9867 assert_eq!(e.cursor().0, after_edit.0);
9868 }
9869
9870 #[test]
9871 fn quote_quote_returns_to_pre_jump_position() {
9872 let mut e = editor_with_rows(50, 20);
9873 e.jump_cursor(10, 2);
9874 let before = e.cursor();
9875 run_keys(&mut e, "G");
9877 assert_ne!(e.cursor(), before);
9878 run_keys(&mut e, "''");
9880 assert_eq!(e.cursor().0, before.0);
9881 }
9882
9883 #[test]
9884 fn backtick_backtick_restores_exact_pre_jump_pos() {
9885 let mut e = editor_with_rows(50, 20);
9886 e.jump_cursor(7, 3);
9887 let before = e.cursor();
9888 run_keys(&mut e, "G");
9889 run_keys(&mut e, "``");
9890 assert_eq!(e.cursor(), before);
9891 }
9892
9893 #[test]
9894 fn macro_record_and_replay_basic() {
9895 let mut e = editor_with("foo\nbar\nbaz");
9896 run_keys(&mut e, "qaIX<Esc>jq");
9898 assert_eq!(e.buffer().lines()[0], "Xfoo");
9899 run_keys(&mut e, "@a");
9901 assert_eq!(e.buffer().lines()[1], "Xbar");
9902 run_keys(&mut e, "j@@");
9904 assert_eq!(e.buffer().lines()[2], "Xbaz");
9905 }
9906
9907 #[test]
9908 fn macro_count_replays_n_times() {
9909 let mut e = editor_with("a\nb\nc\nd\ne");
9910 run_keys(&mut e, "qajq");
9912 assert_eq!(e.cursor().0, 1);
9913 run_keys(&mut e, "3@a");
9915 assert_eq!(e.cursor().0, 4);
9916 }
9917
9918 #[test]
9919 fn macro_capital_q_appends_to_lowercase_register() {
9920 let mut e = editor_with("hello");
9921 run_keys(&mut e, "qall<Esc>q");
9922 run_keys(&mut e, "qAhh<Esc>q");
9923 let text = e.registers().read('a').unwrap().text.clone();
9926 assert!(text.contains("ll<Esc>"));
9927 assert!(text.contains("hh<Esc>"));
9928 }
9929
9930 #[test]
9931 fn buffer_selection_block_in_visual_block_mode() {
9932 use hjkl_buffer::{Position, Selection};
9933 let mut e = editor_with("aaaa\nbbbb\ncccc");
9934 run_keys(&mut e, "<C-v>jl");
9935 assert_eq!(
9936 e.buffer_selection(),
9937 Some(Selection::Block {
9938 anchor: Position::new(0, 0),
9939 head: Position::new(1, 1),
9940 })
9941 );
9942 }
9943
9944 #[test]
9947 fn n_after_question_mark_keeps_walking_backward() {
9948 let mut e = editor_with("foo bar foo baz foo end");
9951 e.jump_cursor(0, 22);
9952 run_keys(&mut e, "?foo<CR>");
9953 assert_eq!(e.cursor().1, 16);
9954 run_keys(&mut e, "n");
9955 assert_eq!(e.cursor().1, 8);
9956 run_keys(&mut e, "N");
9957 assert_eq!(e.cursor().1, 16);
9958 }
9959
9960 #[test]
9961 fn nested_macro_chord_records_literal_keys() {
9962 let mut e = editor_with("alpha\nbeta\ngamma");
9965 run_keys(&mut e, "qblq");
9967 run_keys(&mut e, "qaIX<Esc>q");
9970 e.jump_cursor(1, 0);
9972 run_keys(&mut e, "@a");
9973 assert_eq!(e.buffer().lines()[1], "Xbeta");
9974 }
9975
9976 #[test]
9977 fn shift_gt_motion_indents_one_line() {
9978 let mut e = editor_with("hello world");
9982 run_keys(&mut e, ">w");
9983 assert_eq!(e.buffer().lines()[0], " hello world");
9984 }
9985
9986 #[test]
9987 fn shift_lt_motion_outdents_one_line() {
9988 let mut e = editor_with(" hello world");
9989 run_keys(&mut e, "<lt>w");
9990 assert_eq!(e.buffer().lines()[0], " hello world");
9992 }
9993
9994 #[test]
9995 fn shift_gt_text_object_indents_paragraph() {
9996 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9997 e.jump_cursor(0, 0);
9998 run_keys(&mut e, ">ip");
9999 assert_eq!(e.buffer().lines()[0], " alpha");
10000 assert_eq!(e.buffer().lines()[1], " beta");
10001 assert_eq!(e.buffer().lines()[2], " gamma");
10002 assert_eq!(e.buffer().lines()[4], "rest");
10004 }
10005
10006 #[test]
10007 fn ctrl_o_runs_exactly_one_normal_command() {
10008 let mut e = editor_with("alpha beta gamma");
10011 e.jump_cursor(0, 0);
10012 run_keys(&mut e, "i");
10013 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
10014 run_keys(&mut e, "dw");
10015 assert_eq!(e.vim_mode(), VimMode::Insert);
10017 run_keys(&mut e, "X");
10019 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
10020 }
10021
10022 #[test]
10023 fn macro_replay_respects_mode_switching() {
10024 let mut e = editor_with("hi");
10028 run_keys(&mut e, "qaiX<Esc>0q");
10029 assert_eq!(e.vim_mode(), VimMode::Normal);
10030 e.set_content("yo");
10032 run_keys(&mut e, "@a");
10033 assert_eq!(e.vim_mode(), VimMode::Normal);
10034 assert_eq!(e.cursor().1, 0);
10035 assert_eq!(e.buffer().lines()[0], "Xyo");
10036 }
10037
10038 #[test]
10039 fn macro_recorded_text_round_trips_through_register() {
10040 let mut e = editor_with("");
10044 run_keys(&mut e, "qaiX<Esc>q");
10045 let text = e.registers().read('a').unwrap().text.clone();
10046 assert!(text.starts_with("iX"));
10047 run_keys(&mut e, "@a");
10049 assert_eq!(e.buffer().lines()[0], "XX");
10050 }
10051
10052 #[test]
10053 fn dot_after_macro_replays_macros_last_change() {
10054 let mut e = editor_with("ab\ncd\nef");
10057 run_keys(&mut e, "qaIX<Esc>jq");
10060 assert_eq!(e.buffer().lines()[0], "Xab");
10061 run_keys(&mut e, "@a");
10062 assert_eq!(e.buffer().lines()[1], "Xcd");
10063 let row_before_dot = e.cursor().0;
10066 run_keys(&mut e, ".");
10067 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10068 }
10069
10070 fn si_editor(content: &str) -> Editor {
10076 let opts = crate::types::Options {
10077 shiftwidth: 4,
10078 softtabstop: 4,
10079 expandtab: true,
10080 smartindent: true,
10081 autoindent: true,
10082 ..crate::types::Options::default()
10083 };
10084 let mut e = Editor::new(
10085 hjkl_buffer::Buffer::new(),
10086 crate::types::DefaultHost::new(),
10087 opts,
10088 );
10089 e.set_content(content);
10090 e
10091 }
10092
10093 #[test]
10094 fn smartindent_bumps_indent_after_open_brace() {
10095 let mut e = si_editor("fn foo() {");
10097 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10099 assert_eq!(
10100 e.buffer().lines()[1],
10101 " ",
10102 "smartindent should bump one shiftwidth after {{"
10103 );
10104 }
10105
10106 #[test]
10107 fn smartindent_no_bump_when_off() {
10108 let mut e = si_editor("fn foo() {");
10111 e.settings_mut().smartindent = false;
10112 e.jump_cursor(0, 10);
10113 run_keys(&mut e, "i<CR>");
10114 assert_eq!(
10115 e.buffer().lines()[1],
10116 "",
10117 "without smartindent, no bump: new line copies empty leading ws"
10118 );
10119 }
10120
10121 #[test]
10122 fn smartindent_uses_tab_when_noexpandtab() {
10123 let opts = crate::types::Options {
10125 shiftwidth: 4,
10126 softtabstop: 0,
10127 expandtab: false,
10128 smartindent: true,
10129 autoindent: true,
10130 ..crate::types::Options::default()
10131 };
10132 let mut e = Editor::new(
10133 hjkl_buffer::Buffer::new(),
10134 crate::types::DefaultHost::new(),
10135 opts,
10136 );
10137 e.set_content("fn foo() {");
10138 e.jump_cursor(0, 10);
10139 run_keys(&mut e, "i<CR>");
10140 assert_eq!(
10141 e.buffer().lines()[1],
10142 "\t",
10143 "noexpandtab: smartindent bump inserts a literal tab"
10144 );
10145 }
10146
10147 #[test]
10148 fn smartindent_dedent_on_close_brace() {
10149 let mut e = si_editor("fn foo() {");
10152 e.set_content("fn foo() {\n ");
10154 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10156 assert_eq!(
10157 e.buffer().lines()[1],
10158 "}",
10159 "close brace on whitespace-only line should dedent"
10160 );
10161 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10162 }
10163
10164 #[test]
10165 fn smartindent_no_dedent_when_off() {
10166 let mut e = si_editor("fn foo() {\n ");
10168 e.settings_mut().smartindent = false;
10169 e.jump_cursor(1, 4);
10170 run_keys(&mut e, "i}");
10171 assert_eq!(
10172 e.buffer().lines()[1],
10173 " }",
10174 "without smartindent, `}}` just appends at cursor"
10175 );
10176 }
10177
10178 #[test]
10179 fn smartindent_no_dedent_mid_line() {
10180 let mut e = si_editor(" let x = 1");
10183 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10185 assert_eq!(
10186 e.buffer().lines()[0],
10187 " let x = 1}",
10188 "mid-line `}}` should not dedent"
10189 );
10190 }
10191
10192 #[test]
10196 fn count_5x_fills_unnamed_register() {
10197 let mut e = editor_with("hello world\n");
10198 e.jump_cursor(0, 0);
10199 run_keys(&mut e, "5x");
10200 assert_eq!(e.buffer().lines()[0], " world");
10201 assert_eq!(e.cursor(), (0, 0));
10202 assert_eq!(e.yank(), "hello");
10203 }
10204
10205 #[test]
10206 fn x_fills_unnamed_register_single_char() {
10207 let mut e = editor_with("abc\n");
10208 e.jump_cursor(0, 0);
10209 run_keys(&mut e, "x");
10210 assert_eq!(e.buffer().lines()[0], "bc");
10211 assert_eq!(e.yank(), "a");
10212 }
10213
10214 #[test]
10215 fn big_x_fills_unnamed_register() {
10216 let mut e = editor_with("hello\n");
10217 e.jump_cursor(0, 3);
10218 run_keys(&mut e, "X");
10219 assert_eq!(e.buffer().lines()[0], "helo");
10220 assert_eq!(e.yank(), "l");
10221 }
10222
10223 #[test]
10225 fn g_motion_trailing_newline_lands_on_last_content_row() {
10226 let mut e = editor_with("foo\nbar\nbaz\n");
10227 e.jump_cursor(0, 0);
10228 run_keys(&mut e, "G");
10229 assert_eq!(
10231 e.cursor().0,
10232 2,
10233 "G should land on row 2 (baz), not row 3 (phantom empty)"
10234 );
10235 }
10236
10237 #[test]
10239 fn dd_last_line_clamps_cursor_to_new_last_row() {
10240 let mut e = editor_with("foo\nbar\n");
10241 e.jump_cursor(1, 0);
10242 run_keys(&mut e, "dd");
10243 assert_eq!(e.buffer().lines()[0], "foo");
10244 assert_eq!(
10245 e.cursor(),
10246 (0, 0),
10247 "cursor should clamp to row 0 after dd on last content line"
10248 );
10249 }
10250
10251 #[test]
10253 fn d_dollar_cursor_on_last_char() {
10254 let mut e = editor_with("hello world\n");
10255 e.jump_cursor(0, 5);
10256 run_keys(&mut e, "d$");
10257 assert_eq!(e.buffer().lines()[0], "hello");
10258 assert_eq!(
10259 e.cursor(),
10260 (0, 4),
10261 "d$ should leave cursor on col 4, not col 5"
10262 );
10263 }
10264
10265 #[test]
10267 fn undo_insert_clamps_cursor_to_last_valid_col() {
10268 let mut e = editor_with("hello\n");
10269 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10271 assert_eq!(e.buffer().lines()[0], "hello");
10272 assert_eq!(
10273 e.cursor(),
10274 (0, 4),
10275 "undo should clamp cursor to col 4 on 'hello'"
10276 );
10277 }
10278
10279 #[test]
10281 fn da_doublequote_eats_trailing_whitespace() {
10282 let mut e = editor_with("say \"hello\" there\n");
10283 e.jump_cursor(0, 6);
10284 run_keys(&mut e, "da\"");
10285 assert_eq!(e.buffer().lines()[0], "say there");
10286 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10287 }
10288
10289 #[test]
10291 fn dab_cursor_col_clamped_after_delete() {
10292 let mut e = editor_with("fn x() {\n body\n}\n");
10293 e.jump_cursor(1, 4);
10294 run_keys(&mut e, "daB");
10295 assert_eq!(e.buffer().lines()[0], "fn x() ");
10296 assert_eq!(
10297 e.cursor(),
10298 (0, 6),
10299 "daB should leave cursor at col 6, not 7"
10300 );
10301 }
10302
10303 #[test]
10305 fn dib_preserves_surrounding_newlines() {
10306 let mut e = editor_with("{\n body\n}\n");
10307 e.jump_cursor(1, 4);
10308 run_keys(&mut e, "diB");
10309 assert_eq!(e.buffer().lines()[0], "{");
10310 assert_eq!(e.buffer().lines()[1], "}");
10311 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10312 }
10313
10314 #[test]
10315 fn is_chord_pending_tracks_replace_state() {
10316 let mut e = editor_with("abc\n");
10317 assert!(!e.is_chord_pending());
10318 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10320 assert!(e.is_chord_pending(), "engine should be pending after r");
10321 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10323 assert!(
10324 !e.is_chord_pending(),
10325 "engine pending should clear after replace"
10326 );
10327 }
10328
10329 #[test]
10332 fn yiw_sets_lbr_rbr_marks_around_word() {
10333 let mut e = editor_with("hello world");
10336 run_keys(&mut e, "yiw");
10337 let lo = e.mark('[').expect("'[' must be set after yiw");
10338 let hi = e.mark(']').expect("']' must be set after yiw");
10339 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10340 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10341 }
10342
10343 #[test]
10344 fn yj_linewise_sets_marks_at_line_edges() {
10345 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10348 run_keys(&mut e, "yj");
10349 let lo = e.mark('[').expect("'[' must be set after yj");
10350 let hi = e.mark(']').expect("']' must be set after yj");
10351 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10352 assert_eq!(
10353 hi,
10354 (1, 4),
10355 "'] snaps to (bot_row, last_col) for linewise yank"
10356 );
10357 }
10358
10359 #[test]
10360 fn dd_sets_lbr_rbr_marks_to_cursor() {
10361 let mut e = editor_with("aaa\nbbb");
10364 run_keys(&mut e, "dd");
10365 let lo = e.mark('[').expect("'[' must be set after dd");
10366 let hi = e.mark(']').expect("']' must be set after dd");
10367 assert_eq!(lo, hi, "after delete both marks are at the same position");
10368 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10369 }
10370
10371 #[test]
10372 fn dw_sets_lbr_rbr_marks_to_cursor() {
10373 let mut e = editor_with("hello world");
10376 run_keys(&mut e, "dw");
10377 let lo = e.mark('[').expect("'[' must be set after dw");
10378 let hi = e.mark(']').expect("']' must be set after dw");
10379 assert_eq!(lo, hi, "after delete both marks are at the same position");
10380 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10381 }
10382
10383 #[test]
10384 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10385 let mut e = editor_with("hello world");
10390 run_keys(&mut e, "cwfoo<Esc>");
10391 let lo = e.mark('[').expect("'[' must be set after cw");
10392 let hi = e.mark(']').expect("']' must be set after cw");
10393 assert_eq!(lo, (0, 0), "'[ should be start of change");
10394 assert_eq!(hi.0, 0, "'] should be on row 0");
10397 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10398 }
10399
10400 #[test]
10401 fn cw_with_no_insertion_sets_marks_at_change_start() {
10402 let mut e = editor_with("hello world");
10405 run_keys(&mut e, "cw<Esc>");
10406 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10407 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10408 assert_eq!(lo.0, 0, "'[ should be on row 0");
10409 assert_eq!(hi.0, 0, "'] should be on row 0");
10410 assert_eq!(lo, hi, "marks coincide when insert is empty");
10412 }
10413
10414 #[test]
10415 fn p_charwise_sets_marks_around_pasted_text() {
10416 let mut e = editor_with("abc xyz");
10419 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10422 let hi = e.mark(']').expect("']' set after charwise paste");
10423 assert!(lo <= hi, "'[ must not exceed ']'");
10424 assert_eq!(
10426 hi.1.wrapping_sub(lo.1),
10427 2,
10428 "'] - '[ should span 2 cols for a 3-char paste"
10429 );
10430 }
10431
10432 #[test]
10433 fn p_linewise_sets_marks_at_line_edges() {
10434 let mut e = editor_with("aaa\nbbb\nccc");
10437 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10441 let hi = e.mark(']').expect("']' set after linewise paste");
10442 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10443 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10444 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10445 }
10446
10447 #[test]
10448 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10449 let mut e = editor_with("hello world");
10453 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10457 assert_eq!(
10459 e.cursor(),
10460 (0, 4),
10461 "visual `[v`] should land on last yanked char"
10462 );
10463 assert_eq!(
10465 e.vim_mode(),
10466 crate::VimMode::Visual,
10467 "should be in Visual mode"
10468 );
10469 }
10470
10471 #[test]
10477 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10478 let mut e = editor_with("hello\nworld\n");
10481 e.jump_cursor(0, 0);
10482 run_keys(&mut e, "iX<Esc>j`.");
10483 assert_eq!(
10484 e.cursor(),
10485 (0, 0),
10486 "dot mark should jump to the change-start (col 0), not post-insert col"
10487 );
10488 }
10489
10490 #[test]
10493 fn count_100g_clamps_to_last_content_row() {
10494 let mut e = editor_with("foo\nbar\nbaz\n");
10497 e.jump_cursor(0, 0);
10498 run_keys(&mut e, "100G");
10499 assert_eq!(
10500 e.cursor(),
10501 (2, 0),
10502 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10503 );
10504 }
10505
10506 #[test]
10509 fn gi_resumes_last_insert_position() {
10510 let mut e = editor_with("world\nhello\n");
10516 e.jump_cursor(0, 0);
10517 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10518 assert_eq!(
10519 e.vim_mode(),
10520 crate::VimMode::Normal,
10521 "should be in Normal mode after gi<Esc>"
10522 );
10523 assert_eq!(
10524 e.cursor(),
10525 (0, 1),
10526 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10527 );
10528 }
10529
10530 #[test]
10534 fn visual_block_change_cursor_on_last_inserted_char() {
10535 let mut e = editor_with("foo\nbar\nbaz\n");
10539 e.jump_cursor(0, 0);
10540 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10541 let lines = e.buffer().lines().to_vec();
10542 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10543 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10544 assert_eq!(
10545 e.cursor(),
10546 (0, 1),
10547 "cursor should be on last char of inserted 'ZZ' (col 1)"
10548 );
10549 }
10550
10551 #[test]
10556 fn register_blackhole_delete_preserves_unnamed_register() {
10557 let mut e = editor_with("foo bar baz\n");
10564 e.jump_cursor(0, 0);
10565 run_keys(&mut e, "yiww\"_dwbp");
10566 let lines = e.buffer().lines().to_vec();
10567 assert_eq!(
10568 lines[0], "ffoooo baz",
10569 "black-hole delete must not corrupt unnamed register"
10570 );
10571 assert_eq!(
10572 e.cursor(),
10573 (0, 3),
10574 "cursor should be on last pasted char (col 3)"
10575 );
10576 }
10577
10578 #[test]
10581 fn after_z_zz_sets_viewport_pinned() {
10582 let mut e = editor_with("a\nb\nc\nd\ne");
10583 e.jump_cursor(2, 0);
10584 e.after_z('z', 1);
10585 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10586 }
10587
10588 #[test]
10589 fn after_z_zo_opens_fold_at_cursor() {
10590 let mut e = editor_with("a\nb\nc\nd");
10591 e.buffer_mut().add_fold(1, 2, true);
10592 e.jump_cursor(1, 0);
10593 e.after_z('o', 1);
10594 assert!(
10595 !e.buffer().folds()[0].closed,
10596 "zo must open the fold at the cursor row"
10597 );
10598 }
10599
10600 #[test]
10601 fn after_z_zm_closes_all_folds() {
10602 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10603 e.buffer_mut().add_fold(0, 1, false);
10604 e.buffer_mut().add_fold(4, 5, false);
10605 e.after_z('M', 1);
10606 assert!(
10607 e.buffer().folds().iter().all(|f| f.closed),
10608 "zM must close all folds"
10609 );
10610 }
10611
10612 #[test]
10613 fn after_z_zd_removes_fold_at_cursor() {
10614 let mut e = editor_with("a\nb\nc\nd");
10615 e.buffer_mut().add_fold(1, 2, true);
10616 e.jump_cursor(1, 0);
10617 e.after_z('d', 1);
10618 assert!(
10619 e.buffer().folds().is_empty(),
10620 "zd must remove the fold at the cursor row"
10621 );
10622 }
10623
10624 #[test]
10625 fn after_z_zf_in_visual_creates_fold() {
10626 let mut e = editor_with("a\nb\nc\nd\ne");
10627 e.jump_cursor(1, 0);
10629 run_keys(&mut e, "V2j");
10630 e.after_z('f', 1);
10632 let folds = e.buffer().folds();
10633 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10634 assert_eq!(folds[0].start_row, 1);
10635 assert_eq!(folds[0].end_row, 3);
10636 assert!(folds[0].closed);
10637 }
10638
10639 #[test]
10642 fn apply_op_motion_dw_deletes_word() {
10643 let mut e = editor_with("hello world");
10645 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
10646 assert_eq!(
10647 e.buffer().lines().first().cloned().unwrap_or_default(),
10648 "world"
10649 );
10650 }
10651
10652 #[test]
10653 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
10654 let mut e = editor_with("hello world");
10656 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
10657 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10660 assert!(
10661 line.starts_with(' ') || line == " world",
10662 "cw quirk: got {line:?}"
10663 );
10664 assert_eq!(e.vim_mode(), VimMode::Insert);
10665 }
10666
10667 #[test]
10668 fn apply_op_double_dd_deletes_line() {
10669 let mut e = editor_with("line1\nline2\nline3");
10670 e.apply_op_double(crate::vim::Operator::Delete, 1);
10672 let lines: Vec<_> = e.buffer().lines().to_vec();
10673 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
10674 }
10675
10676 #[test]
10677 fn apply_op_double_yy_does_not_modify_buffer() {
10678 let mut e = editor_with("hello");
10679 e.apply_op_double(crate::vim::Operator::Yank, 1);
10680 assert_eq!(
10681 e.buffer().lines().first().cloned().unwrap_or_default(),
10682 "hello"
10683 );
10684 }
10685
10686 #[test]
10687 fn apply_op_double_dd_count2_deletes_two_lines() {
10688 let mut e = editor_with("line1\nline2\nline3");
10689 e.apply_op_double(crate::vim::Operator::Delete, 2);
10690 let lines: Vec<_> = e.buffer().lines().to_vec();
10691 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
10692 }
10693
10694 #[test]
10695 fn apply_op_motion_unknown_key_is_noop() {
10696 let mut e = editor_with("hello");
10698 let before = e.cursor();
10699 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
10701 assert_eq!(
10702 e.buffer().lines().first().cloned().unwrap_or_default(),
10703 "hello"
10704 );
10705 }
10706
10707 #[test]
10710 fn apply_op_find_dfx_deletes_to_x() {
10711 let mut e = editor_with("hello x world");
10713 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10714 assert_eq!(
10715 e.buffer().lines().first().cloned().unwrap_or_default(),
10716 " world",
10717 "dfx must delete 'hello x'"
10718 );
10719 }
10720
10721 #[test]
10722 fn apply_op_find_dtx_deletes_up_to_x() {
10723 let mut e = editor_with("hello x world");
10725 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
10726 assert_eq!(
10727 e.buffer().lines().first().cloned().unwrap_or_default(),
10728 "x world",
10729 "dtx must delete 'hello ' leaving 'x world'"
10730 );
10731 }
10732
10733 #[test]
10734 fn apply_op_find_records_last_find() {
10735 let mut e = editor_with("hello x world");
10737 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10738 let _ = e.cursor(); }
10745
10746 #[test]
10749 fn apply_op_text_obj_diw_deletes_word() {
10750 let mut e = editor_with("hello world");
10752 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', true, 1);
10753 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10754 assert!(
10759 !line.contains("hello"),
10760 "diw must delete 'hello', remaining: {line:?}"
10761 );
10762 }
10763
10764 #[test]
10765 fn apply_op_text_obj_daw_deletes_around_word() {
10766 let mut e = editor_with("hello world");
10768 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', false, 1);
10769 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10770 assert!(
10771 !line.contains("hello"),
10772 "daw must delete 'hello' and surrounding space, remaining: {line:?}"
10773 );
10774 }
10775
10776 #[test]
10777 fn apply_op_text_obj_invalid_char_no_op() {
10778 let mut e = editor_with("hello world");
10780 let before = e.buffer().as_string();
10781 e.apply_op_text_obj(crate::vim::Operator::Delete, 'X', true, 1);
10782 assert_eq!(
10783 e.buffer().as_string(),
10784 before,
10785 "unknown text-object char must be a no-op"
10786 );
10787 }
10788
10789 #[test]
10792 fn apply_op_g_dgg_deletes_to_top() {
10793 let mut e = editor_with("line1\nline2\nline3");
10795 e.apply_op_motion(crate::vim::Operator::Delete, 'j', 1);
10797 e.apply_op_g(crate::vim::Operator::Delete, 'g', 1);
10800 let lines: Vec<_> = e.buffer().lines().to_vec();
10802 assert_eq!(lines, vec!["line3"], "dgg must delete to file top");
10803 }
10804
10805 #[test]
10806 fn apply_op_g_dge_deletes_word_end_back() {
10807 let mut e = editor_with("hello world");
10820 let before = e.buffer().as_string();
10821 e.apply_op_g(crate::vim::Operator::Delete, 'X', 1);
10823 assert_eq!(
10824 e.buffer().as_string(),
10825 before,
10826 "apply_op_g with unknown char must be a no-op"
10827 );
10828 e.apply_op_g(crate::vim::Operator::Delete, 'e', 1);
10830 }
10832
10833 #[test]
10834 fn apply_op_g_dgj_deletes_screen_down() {
10835 let mut e = editor_with("line1\nline2\nline3");
10838 e.apply_op_g(crate::vim::Operator::Delete, 'j', 1);
10839 let lines: Vec<_> = e.buffer().lines().to_vec();
10840 assert_eq!(lines, vec!["line3"], "dgj must delete current+next line");
10842 }
10843
10844 fn blank_editor() -> Editor {
10847 Editor::new(
10848 hjkl_buffer::Buffer::new(),
10849 crate::types::DefaultHost::new(),
10850 crate::types::Options::default(),
10851 )
10852 }
10853
10854 #[test]
10855 fn set_pending_register_valid_letter_sets_field() {
10856 let mut e = blank_editor();
10857 assert!(e.vim.pending_register.is_none());
10858 e.set_pending_register('a');
10859 assert_eq!(e.vim.pending_register, Some('a'));
10860 }
10861
10862 #[test]
10863 fn set_pending_register_invalid_char_no_op() {
10864 let mut e = blank_editor();
10865 e.set_pending_register('!');
10866 assert!(
10867 e.vim.pending_register.is_none(),
10868 "invalid register char must not set pending_register"
10869 );
10870 }
10871
10872 #[test]
10873 fn set_pending_register_special_plus_sets_field() {
10874 let mut e = blank_editor();
10876 e.set_pending_register('+');
10877 assert_eq!(e.vim.pending_register, Some('+'));
10878 }
10879
10880 #[test]
10881 fn set_pending_register_star_sets_field() {
10882 let mut e = blank_editor();
10884 e.set_pending_register('*');
10885 assert_eq!(e.vim.pending_register, Some('*'));
10886 }
10887
10888 #[test]
10889 fn set_pending_register_underscore_sets_field() {
10890 let mut e = blank_editor();
10892 e.set_pending_register('_');
10893 assert_eq!(e.vim.pending_register, Some('_'));
10894 }
10895}