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 hjkl_vim::MotionKind::WordForward => {
2560 execute_motion(ed, Motion::WordFwd, count);
2561 }
2562 hjkl_vim::MotionKind::BigWordForward => {
2563 execute_motion(ed, Motion::BigWordFwd, count);
2564 }
2565 hjkl_vim::MotionKind::WordBackward => {
2566 execute_motion(ed, Motion::WordBack, count);
2567 }
2568 hjkl_vim::MotionKind::BigWordBackward => {
2569 execute_motion(ed, Motion::BigWordBack, count);
2570 }
2571 hjkl_vim::MotionKind::WordEnd => {
2572 execute_motion(ed, Motion::WordEnd, count);
2573 }
2574 hjkl_vim::MotionKind::BigWordEnd => {
2575 execute_motion(ed, Motion::BigWordEnd, count);
2576 }
2577 hjkl_vim::MotionKind::LineStart => {
2578 execute_motion(ed, Motion::LineStart, 1);
2581 }
2582 hjkl_vim::MotionKind::FirstNonBlank => {
2583 execute_motion(ed, Motion::FirstNonBlank, 1);
2586 }
2587 hjkl_vim::MotionKind::GotoLine => {
2588 execute_motion(ed, Motion::FileBottom, count);
2595 }
2596 hjkl_vim::MotionKind::LineEnd => {
2597 execute_motion(ed, Motion::LineEnd, 1);
2601 }
2602 _ => {
2603 }
2607 }
2608}
2609
2610fn apply_sticky_col<H: crate::types::Host>(
2615 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2616 motion: &Motion,
2617 pre_col: usize,
2618) {
2619 if is_vertical_motion(motion) {
2620 let want = ed.sticky_col.unwrap_or(pre_col);
2621 ed.sticky_col = Some(want);
2624 let (row, _) = ed.cursor();
2625 let line_len = buf_line_chars(&ed.buffer, row);
2626 let max_col = line_len.saturating_sub(1);
2630 let target = want.min(max_col);
2631 ed.jump_cursor(row, target);
2632 } else {
2633 ed.sticky_col = Some(ed.cursor().1);
2636 }
2637}
2638
2639fn is_vertical_motion(motion: &Motion) -> bool {
2640 matches!(
2644 motion,
2645 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2646 )
2647}
2648
2649fn apply_motion_cursor<H: crate::types::Host>(
2650 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2651 motion: &Motion,
2652 count: usize,
2653) {
2654 apply_motion_cursor_ctx(ed, motion, count, false)
2655}
2656
2657fn apply_motion_cursor_ctx<H: crate::types::Host>(
2658 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2659 motion: &Motion,
2660 count: usize,
2661 as_operator: bool,
2662) {
2663 match motion {
2664 Motion::Left => {
2665 crate::motions::move_left(&mut ed.buffer, count);
2667 ed.push_buffer_cursor_to_textarea();
2668 }
2669 Motion::Right => {
2670 if as_operator {
2674 crate::motions::move_right_to_end(&mut ed.buffer, count);
2675 } else {
2676 crate::motions::move_right_in_line(&mut ed.buffer, count);
2677 }
2678 ed.push_buffer_cursor_to_textarea();
2679 }
2680 Motion::Up => {
2681 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2685 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2686 ed.push_buffer_cursor_to_textarea();
2687 }
2688 Motion::Down => {
2689 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2690 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2691 ed.push_buffer_cursor_to_textarea();
2692 }
2693 Motion::ScreenUp => {
2694 let v = *ed.host.viewport();
2695 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2696 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2697 ed.push_buffer_cursor_to_textarea();
2698 }
2699 Motion::ScreenDown => {
2700 let v = *ed.host.viewport();
2701 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2702 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2703 ed.push_buffer_cursor_to_textarea();
2704 }
2705 Motion::WordFwd => {
2706 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2707 ed.push_buffer_cursor_to_textarea();
2708 }
2709 Motion::WordBack => {
2710 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2711 ed.push_buffer_cursor_to_textarea();
2712 }
2713 Motion::WordEnd => {
2714 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2715 ed.push_buffer_cursor_to_textarea();
2716 }
2717 Motion::BigWordFwd => {
2718 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2719 ed.push_buffer_cursor_to_textarea();
2720 }
2721 Motion::BigWordBack => {
2722 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2723 ed.push_buffer_cursor_to_textarea();
2724 }
2725 Motion::BigWordEnd => {
2726 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2727 ed.push_buffer_cursor_to_textarea();
2728 }
2729 Motion::WordEndBack => {
2730 crate::motions::move_word_end_back(
2731 &mut ed.buffer,
2732 false,
2733 count,
2734 &ed.settings.iskeyword,
2735 );
2736 ed.push_buffer_cursor_to_textarea();
2737 }
2738 Motion::BigWordEndBack => {
2739 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2740 ed.push_buffer_cursor_to_textarea();
2741 }
2742 Motion::LineStart => {
2743 crate::motions::move_line_start(&mut ed.buffer);
2744 ed.push_buffer_cursor_to_textarea();
2745 }
2746 Motion::FirstNonBlank => {
2747 crate::motions::move_first_non_blank(&mut ed.buffer);
2748 ed.push_buffer_cursor_to_textarea();
2749 }
2750 Motion::LineEnd => {
2751 crate::motions::move_line_end(&mut ed.buffer);
2753 ed.push_buffer_cursor_to_textarea();
2754 }
2755 Motion::FileTop => {
2756 if count > 1 {
2759 crate::motions::move_bottom(&mut ed.buffer, count);
2760 } else {
2761 crate::motions::move_top(&mut ed.buffer);
2762 }
2763 ed.push_buffer_cursor_to_textarea();
2764 }
2765 Motion::FileBottom => {
2766 if count > 1 {
2769 crate::motions::move_bottom(&mut ed.buffer, count);
2770 } else {
2771 crate::motions::move_bottom(&mut ed.buffer, 0);
2772 }
2773 ed.push_buffer_cursor_to_textarea();
2774 }
2775 Motion::Find { ch, forward, till } => {
2776 for _ in 0..count {
2777 if !find_char_on_line(ed, *ch, *forward, *till) {
2778 break;
2779 }
2780 }
2781 }
2782 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2784 let _ = matching_bracket(ed);
2785 }
2786 Motion::WordAtCursor {
2787 forward,
2788 whole_word,
2789 } => {
2790 word_at_cursor_search(ed, *forward, *whole_word, count);
2791 }
2792 Motion::SearchNext { reverse } => {
2793 if let Some(pattern) = ed.vim.last_search.clone() {
2797 push_search_pattern(ed, &pattern);
2798 }
2799 if ed.search_state().pattern.is_none() {
2800 return;
2801 }
2802 let forward = ed.vim.last_search_forward != *reverse;
2806 for _ in 0..count.max(1) {
2807 if forward {
2808 ed.search_advance_forward(true);
2809 } else {
2810 ed.search_advance_backward(true);
2811 }
2812 }
2813 ed.push_buffer_cursor_to_textarea();
2814 }
2815 Motion::ViewportTop => {
2816 let v = *ed.host().viewport();
2817 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2818 ed.push_buffer_cursor_to_textarea();
2819 }
2820 Motion::ViewportMiddle => {
2821 let v = *ed.host().viewport();
2822 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2823 ed.push_buffer_cursor_to_textarea();
2824 }
2825 Motion::ViewportBottom => {
2826 let v = *ed.host().viewport();
2827 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2828 ed.push_buffer_cursor_to_textarea();
2829 }
2830 Motion::LastNonBlank => {
2831 crate::motions::move_last_non_blank(&mut ed.buffer);
2832 ed.push_buffer_cursor_to_textarea();
2833 }
2834 Motion::LineMiddle => {
2835 let row = ed.cursor().0;
2836 let line_chars = buf_line_chars(&ed.buffer, row);
2837 let target = line_chars / 2;
2840 ed.jump_cursor(row, target);
2841 }
2842 Motion::ParagraphPrev => {
2843 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2844 ed.push_buffer_cursor_to_textarea();
2845 }
2846 Motion::ParagraphNext => {
2847 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2848 ed.push_buffer_cursor_to_textarea();
2849 }
2850 Motion::SentencePrev => {
2851 for _ in 0..count.max(1) {
2852 if let Some((row, col)) = sentence_boundary(ed, false) {
2853 ed.jump_cursor(row, col);
2854 }
2855 }
2856 }
2857 Motion::SentenceNext => {
2858 for _ in 0..count.max(1) {
2859 if let Some((row, col)) = sentence_boundary(ed, true) {
2860 ed.jump_cursor(row, col);
2861 }
2862 }
2863 }
2864 }
2865}
2866
2867fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2868 ed.sync_buffer_content_from_textarea();
2874 crate::motions::move_first_non_blank(&mut ed.buffer);
2875 ed.push_buffer_cursor_to_textarea();
2876}
2877
2878fn find_char_on_line<H: crate::types::Host>(
2879 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2880 ch: char,
2881 forward: bool,
2882 till: bool,
2883) -> bool {
2884 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2885 if moved {
2886 ed.push_buffer_cursor_to_textarea();
2887 }
2888 moved
2889}
2890
2891fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2892 let moved = crate::motions::match_bracket(&mut ed.buffer);
2893 if moved {
2894 ed.push_buffer_cursor_to_textarea();
2895 }
2896 moved
2897}
2898
2899fn word_at_cursor_search<H: crate::types::Host>(
2900 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2901 forward: bool,
2902 whole_word: bool,
2903 count: usize,
2904) {
2905 let (row, col) = ed.cursor();
2906 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2907 let chars: Vec<char> = line.chars().collect();
2908 if chars.is_empty() {
2909 return;
2910 }
2911 let spec = ed.settings().iskeyword.clone();
2913 let is_word = |c: char| is_keyword_char(c, &spec);
2914 let mut start = col.min(chars.len().saturating_sub(1));
2915 while start > 0 && is_word(chars[start - 1]) {
2916 start -= 1;
2917 }
2918 let mut end = start;
2919 while end < chars.len() && is_word(chars[end]) {
2920 end += 1;
2921 }
2922 if end <= start {
2923 return;
2924 }
2925 let word: String = chars[start..end].iter().collect();
2926 let escaped = regex_escape(&word);
2927 let pattern = if whole_word {
2928 format!(r"\b{escaped}\b")
2929 } else {
2930 escaped
2931 };
2932 push_search_pattern(ed, &pattern);
2933 if ed.search_state().pattern.is_none() {
2934 return;
2935 }
2936 ed.vim.last_search = Some(pattern);
2938 ed.vim.last_search_forward = forward;
2939 for _ in 0..count.max(1) {
2940 if forward {
2941 ed.search_advance_forward(true);
2942 } else {
2943 ed.search_advance_backward(true);
2944 }
2945 }
2946 ed.push_buffer_cursor_to_textarea();
2947}
2948
2949fn regex_escape(s: &str) -> String {
2950 let mut out = String::with_capacity(s.len());
2951 for c in s.chars() {
2952 if matches!(
2953 c,
2954 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2955 ) {
2956 out.push('\\');
2957 }
2958 out.push(c);
2959 }
2960 out
2961}
2962
2963pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2977 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2978 op: Operator,
2979 motion_key: char,
2980 total_count: usize,
2981) {
2982 let input = Input {
2983 key: Key::Char(motion_key),
2984 ctrl: false,
2985 alt: false,
2986 shift: false,
2987 };
2988 let Some(motion) = parse_motion(&input) else {
2989 return;
2990 };
2991 let motion = match motion {
2992 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2993 Some((ch, forward, till)) => Motion::Find {
2994 ch,
2995 forward: if reverse { !forward } else { forward },
2996 till,
2997 },
2998 None => return,
2999 },
3000 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3002 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3003 m => m,
3004 };
3005 apply_op_with_motion(ed, op, &motion, total_count);
3006 if let Motion::Find { ch, forward, till } = &motion {
3007 ed.vim.last_find = Some((*ch, *forward, *till));
3008 }
3009 if !ed.vim.replaying && op_is_change(op) {
3010 ed.vim.last_change = Some(LastChange::OpMotion {
3011 op,
3012 motion,
3013 count: total_count,
3014 inserted: None,
3015 });
3016 }
3017}
3018
3019pub(crate) fn apply_op_double<H: crate::types::Host>(
3022 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3023 op: Operator,
3024 total_count: usize,
3025) {
3026 execute_line_op(ed, op, total_count);
3027 if !ed.vim.replaying {
3028 ed.vim.last_change = Some(LastChange::LineOp {
3029 op,
3030 count: total_count,
3031 inserted: None,
3032 });
3033 }
3034}
3035
3036fn handle_after_op<H: crate::types::Host>(
3037 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3038 input: Input,
3039 op: Operator,
3040 count1: usize,
3041) -> bool {
3042 if let Key::Char(d @ '0'..='9') = input.key
3044 && !input.ctrl
3045 && (d != '0' || ed.vim.count > 0)
3046 {
3047 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
3048 ed.vim.pending = Pending::Op { op, count1 };
3049 return true;
3050 }
3051
3052 if input.key == Key::Esc {
3054 ed.vim.count = 0;
3055 return true;
3056 }
3057
3058 let double_ch = match op {
3062 Operator::Delete => Some('d'),
3063 Operator::Change => Some('c'),
3064 Operator::Yank => Some('y'),
3065 Operator::Indent => Some('>'),
3066 Operator::Outdent => Some('<'),
3067 Operator::Uppercase => Some('U'),
3068 Operator::Lowercase => Some('u'),
3069 Operator::ToggleCase => Some('~'),
3070 Operator::Fold => None,
3071 Operator::Reflow => Some('q'),
3074 };
3075 if let Key::Char(c) = input.key
3076 && !input.ctrl
3077 && Some(c) == double_ch
3078 {
3079 let count2 = take_count(&mut ed.vim);
3080 let total = count1.max(1) * count2.max(1);
3081 execute_line_op(ed, op, total);
3082 if !ed.vim.replaying {
3083 ed.vim.last_change = Some(LastChange::LineOp {
3084 op,
3085 count: total,
3086 inserted: None,
3087 });
3088 }
3089 return true;
3090 }
3091
3092 if let Key::Char('i') | Key::Char('a') = input.key
3094 && !input.ctrl
3095 {
3096 let inner = matches!(input.key, Key::Char('i'));
3097 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
3098 return true;
3099 }
3100
3101 if input.key == Key::Char('g') && !input.ctrl {
3103 ed.vim.pending = Pending::OpG { op, count1 };
3104 return true;
3105 }
3106
3107 if let Some((forward, till)) = find_entry(&input) {
3109 ed.vim.pending = Pending::OpFind {
3110 op,
3111 count1,
3112 forward,
3113 till,
3114 };
3115 return true;
3116 }
3117
3118 let count2 = take_count(&mut ed.vim);
3120 let total = count1.max(1) * count2.max(1);
3121 if let Some(motion) = parse_motion(&input) {
3122 let motion = match motion {
3123 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3124 Some((ch, forward, till)) => Motion::Find {
3125 ch,
3126 forward: if reverse { !forward } else { forward },
3127 till,
3128 },
3129 None => return true,
3130 },
3131 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3135 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3136 m => m,
3137 };
3138 apply_op_with_motion(ed, op, &motion, total);
3139 if let Motion::Find { ch, forward, till } = &motion {
3140 ed.vim.last_find = Some((*ch, *forward, *till));
3141 }
3142 if !ed.vim.replaying && op_is_change(op) {
3143 ed.vim.last_change = Some(LastChange::OpMotion {
3144 op,
3145 motion,
3146 count: total,
3147 inserted: None,
3148 });
3149 }
3150 return true;
3151 }
3152
3153 true
3155}
3156
3157pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3167 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3168 op: Operator,
3169 ch: char,
3170 total_count: usize,
3171) {
3172 if matches!(
3175 op,
3176 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3177 ) {
3178 let op_char = match op {
3179 Operator::Uppercase => 'U',
3180 Operator::Lowercase => 'u',
3181 Operator::ToggleCase => '~',
3182 _ => unreachable!(),
3183 };
3184 if ch == op_char {
3185 execute_line_op(ed, op, total_count);
3186 if !ed.vim.replaying {
3187 ed.vim.last_change = Some(LastChange::LineOp {
3188 op,
3189 count: total_count,
3190 inserted: None,
3191 });
3192 }
3193 return;
3194 }
3195 }
3196 let motion = match ch {
3197 'g' => Motion::FileTop,
3198 'e' => Motion::WordEndBack,
3199 'E' => Motion::BigWordEndBack,
3200 'j' => Motion::ScreenDown,
3201 'k' => Motion::ScreenUp,
3202 _ => return, };
3204 apply_op_with_motion(ed, op, &motion, total_count);
3205 if !ed.vim.replaying && op_is_change(op) {
3206 ed.vim.last_change = Some(LastChange::OpMotion {
3207 op,
3208 motion,
3209 count: total_count,
3210 inserted: None,
3211 });
3212 }
3213}
3214
3215fn handle_op_after_g<H: crate::types::Host>(
3216 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3217 input: Input,
3218 op: Operator,
3219 count1: usize,
3220) -> bool {
3221 if input.ctrl {
3222 return true;
3223 }
3224 let count2 = take_count(&mut ed.vim);
3225 let total = count1.max(1) * count2.max(1);
3226 if let Key::Char(ch) = input.key {
3227 apply_op_g_inner(ed, op, ch, total);
3228 }
3229 true
3230}
3231
3232fn handle_after_g<H: crate::types::Host>(
3233 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3234 input: Input,
3235) -> bool {
3236 let count = take_count(&mut ed.vim);
3237 if let Key::Char(ch) = input.key {
3240 apply_after_g(ed, ch, count);
3241 }
3242 true
3243}
3244
3245pub(crate) fn apply_after_g<H: crate::types::Host>(
3250 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3251 ch: char,
3252 count: usize,
3253) {
3254 match ch {
3255 'g' => {
3256 let pre = ed.cursor();
3258 if count > 1 {
3259 ed.jump_cursor(count - 1, 0);
3260 } else {
3261 ed.jump_cursor(0, 0);
3262 }
3263 move_first_non_whitespace(ed);
3264 if ed.cursor() != pre {
3265 push_jump(ed, pre);
3266 }
3267 }
3268 'e' => execute_motion(ed, Motion::WordEndBack, count),
3269 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3270 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3272 'M' => execute_motion(ed, Motion::LineMiddle, count),
3274 'v' => {
3276 if let Some(snap) = ed.vim.last_visual {
3277 match snap.mode {
3278 Mode::Visual => {
3279 ed.vim.visual_anchor = snap.anchor;
3280 ed.vim.mode = Mode::Visual;
3281 }
3282 Mode::VisualLine => {
3283 ed.vim.visual_line_anchor = snap.anchor.0;
3284 ed.vim.mode = Mode::VisualLine;
3285 }
3286 Mode::VisualBlock => {
3287 ed.vim.block_anchor = snap.anchor;
3288 ed.vim.block_vcol = snap.block_vcol;
3289 ed.vim.mode = Mode::VisualBlock;
3290 }
3291 _ => {}
3292 }
3293 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3294 }
3295 }
3296 'j' => execute_motion(ed, Motion::ScreenDown, count),
3300 'k' => execute_motion(ed, Motion::ScreenUp, count),
3301 'U' => {
3305 ed.vim.pending = Pending::Op {
3306 op: Operator::Uppercase,
3307 count1: count,
3308 };
3309 }
3310 'u' => {
3311 ed.vim.pending = Pending::Op {
3312 op: Operator::Lowercase,
3313 count1: count,
3314 };
3315 }
3316 '~' => {
3317 ed.vim.pending = Pending::Op {
3318 op: Operator::ToggleCase,
3319 count1: count,
3320 };
3321 }
3322 'q' => {
3323 ed.vim.pending = Pending::Op {
3326 op: Operator::Reflow,
3327 count1: count,
3328 };
3329 }
3330 'J' => {
3331 for _ in 0..count.max(1) {
3333 ed.push_undo();
3334 join_line_raw(ed);
3335 }
3336 if !ed.vim.replaying {
3337 ed.vim.last_change = Some(LastChange::JoinLine {
3338 count: count.max(1),
3339 });
3340 }
3341 }
3342 'd' => {
3343 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3348 }
3349 'i' => {
3354 if let Some((row, col)) = ed.vim.last_insert_pos {
3355 ed.jump_cursor(row, col);
3356 }
3357 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3358 }
3359 ';' => walk_change_list(ed, -1, count.max(1)),
3362 ',' => walk_change_list(ed, 1, count.max(1)),
3363 '*' => execute_motion(
3367 ed,
3368 Motion::WordAtCursor {
3369 forward: true,
3370 whole_word: false,
3371 },
3372 count,
3373 ),
3374 '#' => execute_motion(
3375 ed,
3376 Motion::WordAtCursor {
3377 forward: false,
3378 whole_word: false,
3379 },
3380 count,
3381 ),
3382 _ => {}
3383 }
3384}
3385
3386fn handle_after_z<H: crate::types::Host>(
3387 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3388 input: Input,
3389) -> bool {
3390 let count = take_count(&mut ed.vim);
3391 if let Key::Char(ch) = input.key {
3394 apply_after_z(ed, ch, count);
3395 }
3396 true
3397}
3398
3399pub(crate) fn apply_after_z<H: crate::types::Host>(
3404 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3405 ch: char,
3406 count: usize,
3407) {
3408 use crate::editor::CursorScrollTarget;
3409 let row = ed.cursor().0;
3410 match ch {
3411 'z' => {
3412 ed.scroll_cursor_to(CursorScrollTarget::Center);
3413 ed.vim.viewport_pinned = true;
3414 }
3415 't' => {
3416 ed.scroll_cursor_to(CursorScrollTarget::Top);
3417 ed.vim.viewport_pinned = true;
3418 }
3419 'b' => {
3420 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3421 ed.vim.viewport_pinned = true;
3422 }
3423 'o' => {
3428 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3429 }
3430 'c' => {
3431 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3432 }
3433 'a' => {
3434 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3435 }
3436 'R' => {
3437 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3438 }
3439 'M' => {
3440 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3441 }
3442 'E' => {
3443 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3444 }
3445 'd' => {
3446 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3447 }
3448 'f' => {
3449 if matches!(
3450 ed.vim.mode,
3451 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3452 ) {
3453 let anchor_row = match ed.vim.mode {
3456 Mode::VisualLine => ed.vim.visual_line_anchor,
3457 Mode::VisualBlock => ed.vim.block_anchor.0,
3458 _ => ed.vim.visual_anchor.0,
3459 };
3460 let cur = ed.cursor().0;
3461 let top = anchor_row.min(cur);
3462 let bot = anchor_row.max(cur);
3463 ed.apply_fold_op(crate::types::FoldOp::Add {
3464 start_row: top,
3465 end_row: bot,
3466 closed: true,
3467 });
3468 ed.vim.mode = Mode::Normal;
3469 } else {
3470 ed.vim.pending = Pending::Op {
3475 op: Operator::Fold,
3476 count1: count,
3477 };
3478 }
3479 }
3480 _ => {}
3481 }
3482}
3483
3484fn handle_replace<H: crate::types::Host>(
3485 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3486 input: Input,
3487) -> bool {
3488 if let Key::Char(ch) = input.key {
3489 if ed.vim.mode == Mode::VisualBlock {
3490 block_replace(ed, ch);
3491 return true;
3492 }
3493 let count = take_count(&mut ed.vim);
3494 replace_char(ed, ch, count.max(1));
3495 if !ed.vim.replaying {
3496 ed.vim.last_change = Some(LastChange::ReplaceChar {
3497 ch,
3498 count: count.max(1),
3499 });
3500 }
3501 }
3502 true
3503}
3504
3505fn handle_find_target<H: crate::types::Host>(
3506 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3507 input: Input,
3508 forward: bool,
3509 till: bool,
3510) -> bool {
3511 let Key::Char(ch) = input.key else {
3512 return true;
3513 };
3514 let count = take_count(&mut ed.vim);
3515 apply_find_char(ed, ch, forward, till, count.max(1));
3516 true
3517}
3518
3519pub(crate) fn apply_find_char<H: crate::types::Host>(
3525 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3526 ch: char,
3527 forward: bool,
3528 till: bool,
3529 count: usize,
3530) {
3531 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3532 ed.vim.last_find = Some((ch, forward, till));
3533}
3534
3535pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3541 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3542 op: Operator,
3543 ch: char,
3544 forward: bool,
3545 till: bool,
3546 total_count: usize,
3547) {
3548 let motion = Motion::Find { ch, forward, till };
3549 apply_op_with_motion(ed, op, &motion, total_count);
3550 ed.vim.last_find = Some((ch, forward, till));
3551 if !ed.vim.replaying && op_is_change(op) {
3552 ed.vim.last_change = Some(LastChange::OpMotion {
3553 op,
3554 motion,
3555 count: total_count,
3556 inserted: None,
3557 });
3558 }
3559}
3560
3561fn handle_op_find_target<H: crate::types::Host>(
3562 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3563 input: Input,
3564 op: Operator,
3565 count1: usize,
3566 forward: bool,
3567 till: bool,
3568) -> bool {
3569 let Key::Char(ch) = input.key else {
3570 return true;
3571 };
3572 let count2 = take_count(&mut ed.vim);
3573 let total = count1.max(1) * count2.max(1);
3574 apply_op_find_motion(ed, op, ch, forward, till, total);
3575 true
3576}
3577
3578pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3588 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3589 op: Operator,
3590 ch: char,
3591 inner: bool,
3592 _total_count: usize,
3593) -> bool {
3594 let obj = match ch {
3597 'w' => TextObject::Word { big: false },
3598 'W' => TextObject::Word { big: true },
3599 '"' | '\'' | '`' => TextObject::Quote(ch),
3600 '(' | ')' | 'b' => TextObject::Bracket('('),
3601 '[' | ']' => TextObject::Bracket('['),
3602 '{' | '}' | 'B' => TextObject::Bracket('{'),
3603 '<' | '>' => TextObject::Bracket('<'),
3604 'p' => TextObject::Paragraph,
3605 't' => TextObject::XmlTag,
3606 's' => TextObject::Sentence,
3607 _ => return false,
3608 };
3609 apply_op_with_text_object(ed, op, obj, inner);
3610 if !ed.vim.replaying && op_is_change(op) {
3611 ed.vim.last_change = Some(LastChange::OpTextObj {
3612 op,
3613 obj,
3614 inner,
3615 inserted: None,
3616 });
3617 }
3618 true
3619}
3620
3621fn handle_text_object<H: crate::types::Host>(
3622 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3623 input: Input,
3624 op: Operator,
3625 _count1: usize,
3626 inner: bool,
3627) -> bool {
3628 let Key::Char(ch) = input.key else {
3629 return true;
3630 };
3631 apply_op_text_obj_inner(ed, op, ch, inner, 1);
3634 true
3635}
3636
3637fn handle_visual_text_obj<H: crate::types::Host>(
3638 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3639 input: Input,
3640 inner: bool,
3641) -> bool {
3642 let Key::Char(ch) = input.key else {
3643 return true;
3644 };
3645 let obj = match ch {
3646 'w' => TextObject::Word { big: false },
3647 'W' => TextObject::Word { big: true },
3648 '"' | '\'' | '`' => TextObject::Quote(ch),
3649 '(' | ')' | 'b' => TextObject::Bracket('('),
3650 '[' | ']' => TextObject::Bracket('['),
3651 '{' | '}' | 'B' => TextObject::Bracket('{'),
3652 '<' | '>' => TextObject::Bracket('<'),
3653 'p' => TextObject::Paragraph,
3654 't' => TextObject::XmlTag,
3655 's' => TextObject::Sentence,
3656 _ => return true,
3657 };
3658 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3659 return true;
3660 };
3661 match kind {
3665 MotionKind::Linewise => {
3666 ed.vim.visual_line_anchor = start.0;
3667 ed.vim.mode = Mode::VisualLine;
3668 ed.jump_cursor(end.0, 0);
3669 }
3670 _ => {
3671 ed.vim.mode = Mode::Visual;
3672 ed.vim.visual_anchor = (start.0, start.1);
3673 let (er, ec) = retreat_one(ed, end);
3674 ed.jump_cursor(er, ec);
3675 }
3676 }
3677 true
3678}
3679
3680fn retreat_one<H: crate::types::Host>(
3682 ed: &Editor<hjkl_buffer::Buffer, H>,
3683 pos: (usize, usize),
3684) -> (usize, usize) {
3685 let (r, c) = pos;
3686 if c > 0 {
3687 (r, c - 1)
3688 } else if r > 0 {
3689 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3690 (r - 1, prev_len)
3691 } else {
3692 (0, 0)
3693 }
3694}
3695
3696fn op_is_change(op: Operator) -> bool {
3697 matches!(op, Operator::Delete | Operator::Change)
3698}
3699
3700fn handle_normal_only<H: crate::types::Host>(
3703 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3704 input: &Input,
3705 count: usize,
3706) -> bool {
3707 if input.ctrl {
3708 return false;
3709 }
3710 match input.key {
3711 Key::Char('i') => {
3712 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3713 true
3714 }
3715 Key::Char('I') => {
3716 move_first_non_whitespace(ed);
3717 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3718 true
3719 }
3720 Key::Char('a') => {
3721 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3722 ed.push_buffer_cursor_to_textarea();
3723 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3724 true
3725 }
3726 Key::Char('A') => {
3727 crate::motions::move_line_end(&mut ed.buffer);
3728 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3729 ed.push_buffer_cursor_to_textarea();
3730 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3731 true
3732 }
3733 Key::Char('R') => {
3734 begin_insert(ed, count.max(1), InsertReason::Replace);
3737 true
3738 }
3739 Key::Char('o') => {
3740 use hjkl_buffer::{Edit, Position};
3741 ed.push_undo();
3742 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3745 ed.sync_buffer_content_from_textarea();
3746 let row = buf_cursor_pos(&ed.buffer).row;
3747 let line_chars = buf_line_chars(&ed.buffer, row);
3748 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3751 let indent = compute_enter_indent(&ed.settings, prev_line);
3752 ed.mutate_edit(Edit::InsertStr {
3753 at: Position::new(row, line_chars),
3754 text: format!("\n{indent}"),
3755 });
3756 ed.push_buffer_cursor_to_textarea();
3757 true
3758 }
3759 Key::Char('O') => {
3760 use hjkl_buffer::{Edit, Position};
3761 ed.push_undo();
3762 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3763 ed.sync_buffer_content_from_textarea();
3764 let row = buf_cursor_pos(&ed.buffer).row;
3765 let indent = if row > 0 {
3769 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3770 compute_enter_indent(&ed.settings, above)
3771 } else {
3772 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3773 cur.chars()
3774 .take_while(|c| *c == ' ' || *c == '\t')
3775 .collect::<String>()
3776 };
3777 ed.mutate_edit(Edit::InsertStr {
3778 at: Position::new(row, 0),
3779 text: format!("{indent}\n"),
3780 });
3781 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3786 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3787 let new_row = buf_cursor_pos(&ed.buffer).row;
3788 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3789 ed.push_buffer_cursor_to_textarea();
3790 true
3791 }
3792 Key::Char('x') => {
3793 do_char_delete(ed, true, count.max(1));
3794 if !ed.vim.replaying {
3795 ed.vim.last_change = Some(LastChange::CharDel {
3796 forward: true,
3797 count: count.max(1),
3798 });
3799 }
3800 true
3801 }
3802 Key::Char('X') => {
3803 do_char_delete(ed, false, count.max(1));
3804 if !ed.vim.replaying {
3805 ed.vim.last_change = Some(LastChange::CharDel {
3806 forward: false,
3807 count: count.max(1),
3808 });
3809 }
3810 true
3811 }
3812 Key::Char('~') => {
3813 for _ in 0..count.max(1) {
3814 ed.push_undo();
3815 toggle_case_at_cursor(ed);
3816 }
3817 if !ed.vim.replaying {
3818 ed.vim.last_change = Some(LastChange::ToggleCase {
3819 count: count.max(1),
3820 });
3821 }
3822 true
3823 }
3824 Key::Char('J') => {
3825 for _ in 0..count.max(1) {
3826 ed.push_undo();
3827 join_line(ed);
3828 }
3829 if !ed.vim.replaying {
3830 ed.vim.last_change = Some(LastChange::JoinLine {
3831 count: count.max(1),
3832 });
3833 }
3834 true
3835 }
3836 Key::Char('D') => {
3837 ed.push_undo();
3838 delete_to_eol(ed);
3839 crate::motions::move_left(&mut ed.buffer, 1);
3841 ed.push_buffer_cursor_to_textarea();
3842 if !ed.vim.replaying {
3843 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3844 }
3845 true
3846 }
3847 Key::Char('Y') => {
3848 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3850 true
3851 }
3852 Key::Char('C') => {
3853 ed.push_undo();
3854 delete_to_eol(ed);
3855 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3856 true
3857 }
3858 Key::Char('s') => {
3859 use hjkl_buffer::{Edit, MotionKind, Position};
3860 ed.push_undo();
3861 ed.sync_buffer_content_from_textarea();
3862 for _ in 0..count.max(1) {
3863 let cursor = buf_cursor_pos(&ed.buffer);
3864 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3865 if cursor.col >= line_chars {
3866 break;
3867 }
3868 ed.mutate_edit(Edit::DeleteRange {
3869 start: cursor,
3870 end: Position::new(cursor.row, cursor.col + 1),
3871 kind: MotionKind::Char,
3872 });
3873 }
3874 ed.push_buffer_cursor_to_textarea();
3875 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3876 if !ed.vim.replaying {
3878 ed.vim.last_change = Some(LastChange::OpMotion {
3879 op: Operator::Change,
3880 motion: Motion::Right,
3881 count: count.max(1),
3882 inserted: None,
3883 });
3884 }
3885 true
3886 }
3887 Key::Char('p') => {
3888 do_paste(ed, false, count.max(1));
3889 if !ed.vim.replaying {
3890 ed.vim.last_change = Some(LastChange::Paste {
3891 before: false,
3892 count: count.max(1),
3893 });
3894 }
3895 true
3896 }
3897 Key::Char('P') => {
3898 do_paste(ed, true, count.max(1));
3899 if !ed.vim.replaying {
3900 ed.vim.last_change = Some(LastChange::Paste {
3901 before: true,
3902 count: count.max(1),
3903 });
3904 }
3905 true
3906 }
3907 Key::Char('u') => {
3908 do_undo(ed);
3909 true
3910 }
3911 Key::Char('r') => {
3912 ed.vim.count = count;
3913 ed.vim.pending = Pending::Replace;
3914 true
3915 }
3916 Key::Char('/') => {
3917 enter_search(ed, true);
3918 true
3919 }
3920 Key::Char('?') => {
3921 enter_search(ed, false);
3922 true
3923 }
3924 Key::Char('.') => {
3925 replay_last_change(ed, count);
3926 true
3927 }
3928 _ => false,
3929 }
3930}
3931
3932fn begin_insert_noundo<H: crate::types::Host>(
3934 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3935 count: usize,
3936 reason: InsertReason,
3937) {
3938 let reason = if ed.vim.replaying {
3939 InsertReason::ReplayOnly
3940 } else {
3941 reason
3942 };
3943 let (row, _) = ed.cursor();
3944 ed.vim.insert_session = Some(InsertSession {
3945 count,
3946 row_min: row,
3947 row_max: row,
3948 before_lines: buf_lines_to_vec(&ed.buffer),
3949 reason,
3950 });
3951 ed.vim.mode = Mode::Insert;
3952}
3953
3954fn apply_op_with_motion<H: crate::types::Host>(
3957 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3958 op: Operator,
3959 motion: &Motion,
3960 count: usize,
3961) {
3962 let start = ed.cursor();
3963 apply_motion_cursor_ctx(ed, motion, count, true);
3968 let end = ed.cursor();
3969 let kind = motion_kind(motion);
3970 ed.jump_cursor(start.0, start.1);
3972 run_operator_over_range(ed, op, start, end, kind);
3973}
3974
3975fn apply_op_with_text_object<H: crate::types::Host>(
3976 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3977 op: Operator,
3978 obj: TextObject,
3979 inner: bool,
3980) {
3981 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3982 return;
3983 };
3984 ed.jump_cursor(start.0, start.1);
3985 run_operator_over_range(ed, op, start, end, kind);
3986}
3987
3988fn motion_kind(motion: &Motion) -> MotionKind {
3989 match motion {
3990 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3991 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3992 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3993 MotionKind::Linewise
3994 }
3995 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3996 MotionKind::Inclusive
3997 }
3998 Motion::Find { .. } => MotionKind::Inclusive,
3999 Motion::MatchBracket => MotionKind::Inclusive,
4000 Motion::LineEnd => MotionKind::Inclusive,
4002 _ => MotionKind::Exclusive,
4003 }
4004}
4005
4006fn run_operator_over_range<H: crate::types::Host>(
4007 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4008 op: Operator,
4009 start: (usize, usize),
4010 end: (usize, usize),
4011 kind: MotionKind,
4012) {
4013 let (top, bot) = order(start, end);
4014 if top == bot {
4015 return;
4016 }
4017
4018 match op {
4019 Operator::Yank => {
4020 let text = read_vim_range(ed, top, bot, kind);
4021 if !text.is_empty() {
4022 ed.record_yank_to_host(text.clone());
4023 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
4024 }
4025 let rbr = match kind {
4029 MotionKind::Linewise => {
4030 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
4031 (bot.0, last_col)
4032 }
4033 MotionKind::Inclusive => (bot.0, bot.1),
4034 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
4035 };
4036 ed.set_mark('[', top);
4037 ed.set_mark(']', rbr);
4038 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4039 ed.push_buffer_cursor_to_textarea();
4040 }
4041 Operator::Delete => {
4042 ed.push_undo();
4043 cut_vim_range(ed, top, bot, kind);
4044 if !matches!(kind, MotionKind::Linewise) {
4049 clamp_cursor_to_normal_mode(ed);
4050 }
4051 ed.vim.mode = Mode::Normal;
4052 let pos = ed.cursor();
4056 ed.set_mark('[', pos);
4057 ed.set_mark(']', pos);
4058 }
4059 Operator::Change => {
4060 ed.vim.change_mark_start = Some(top);
4065 ed.push_undo();
4066 cut_vim_range(ed, top, bot, kind);
4067 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4068 }
4069 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4070 apply_case_op_to_selection(ed, op, top, bot, kind);
4071 }
4072 Operator::Indent | Operator::Outdent => {
4073 ed.push_undo();
4076 if op == Operator::Indent {
4077 indent_rows(ed, top.0, bot.0, 1);
4078 } else {
4079 outdent_rows(ed, top.0, bot.0, 1);
4080 }
4081 ed.vim.mode = Mode::Normal;
4082 }
4083 Operator::Fold => {
4084 if bot.0 >= top.0 {
4088 ed.apply_fold_op(crate::types::FoldOp::Add {
4089 start_row: top.0,
4090 end_row: bot.0,
4091 closed: true,
4092 });
4093 }
4094 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4095 ed.push_buffer_cursor_to_textarea();
4096 ed.vim.mode = Mode::Normal;
4097 }
4098 Operator::Reflow => {
4099 ed.push_undo();
4100 reflow_rows(ed, top.0, bot.0);
4101 ed.vim.mode = Mode::Normal;
4102 }
4103 }
4104}
4105
4106fn reflow_rows<H: crate::types::Host>(
4111 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4112 top: usize,
4113 bot: usize,
4114) {
4115 let width = ed.settings().textwidth.max(1);
4116 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4117 let bot = bot.min(lines.len().saturating_sub(1));
4118 if top > bot {
4119 return;
4120 }
4121 let original = lines[top..=bot].to_vec();
4122 let mut wrapped: Vec<String> = Vec::new();
4123 let mut paragraph: Vec<String> = Vec::new();
4124 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4125 if para.is_empty() {
4126 return;
4127 }
4128 let words = para.join(" ");
4129 let mut current = String::new();
4130 for word in words.split_whitespace() {
4131 let extra = if current.is_empty() {
4132 word.chars().count()
4133 } else {
4134 current.chars().count() + 1 + word.chars().count()
4135 };
4136 if extra > width && !current.is_empty() {
4137 out.push(std::mem::take(&mut current));
4138 current.push_str(word);
4139 } else if current.is_empty() {
4140 current.push_str(word);
4141 } else {
4142 current.push(' ');
4143 current.push_str(word);
4144 }
4145 }
4146 if !current.is_empty() {
4147 out.push(current);
4148 }
4149 para.clear();
4150 };
4151 for line in &original {
4152 if line.trim().is_empty() {
4153 flush(&mut paragraph, &mut wrapped, width);
4154 wrapped.push(String::new());
4155 } else {
4156 paragraph.push(line.clone());
4157 }
4158 }
4159 flush(&mut paragraph, &mut wrapped, width);
4160
4161 let after: Vec<String> = lines.split_off(bot + 1);
4163 lines.truncate(top);
4164 lines.extend(wrapped);
4165 lines.extend(after);
4166 ed.restore(lines, (top, 0));
4167 ed.mark_content_dirty();
4168}
4169
4170fn apply_case_op_to_selection<H: crate::types::Host>(
4176 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4177 op: Operator,
4178 top: (usize, usize),
4179 bot: (usize, usize),
4180 kind: MotionKind,
4181) {
4182 use hjkl_buffer::Edit;
4183 ed.push_undo();
4184 let saved_yank = ed.yank().to_string();
4185 let saved_yank_linewise = ed.vim.yank_linewise;
4186 let selection = cut_vim_range(ed, top, bot, kind);
4187 let transformed = match op {
4188 Operator::Uppercase => selection.to_uppercase(),
4189 Operator::Lowercase => selection.to_lowercase(),
4190 Operator::ToggleCase => toggle_case_str(&selection),
4191 _ => unreachable!(),
4192 };
4193 if !transformed.is_empty() {
4194 let cursor = buf_cursor_pos(&ed.buffer);
4195 ed.mutate_edit(Edit::InsertStr {
4196 at: cursor,
4197 text: transformed,
4198 });
4199 }
4200 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4201 ed.push_buffer_cursor_to_textarea();
4202 ed.set_yank(saved_yank);
4203 ed.vim.yank_linewise = saved_yank_linewise;
4204 ed.vim.mode = Mode::Normal;
4205}
4206
4207fn indent_rows<H: crate::types::Host>(
4212 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4213 top: usize,
4214 bot: usize,
4215 count: usize,
4216) {
4217 ed.sync_buffer_content_from_textarea();
4218 let width = ed.settings().shiftwidth * count.max(1);
4219 let pad: String = " ".repeat(width);
4220 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4221 let bot = bot.min(lines.len().saturating_sub(1));
4222 for line in lines.iter_mut().take(bot + 1).skip(top) {
4223 if !line.is_empty() {
4224 line.insert_str(0, &pad);
4225 }
4226 }
4227 ed.restore(lines, (top, 0));
4230 move_first_non_whitespace(ed);
4231}
4232
4233fn outdent_rows<H: crate::types::Host>(
4237 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4238 top: usize,
4239 bot: usize,
4240 count: usize,
4241) {
4242 ed.sync_buffer_content_from_textarea();
4243 let width = ed.settings().shiftwidth * count.max(1);
4244 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4245 let bot = bot.min(lines.len().saturating_sub(1));
4246 for line in lines.iter_mut().take(bot + 1).skip(top) {
4247 let strip: usize = line
4248 .chars()
4249 .take(width)
4250 .take_while(|c| *c == ' ' || *c == '\t')
4251 .count();
4252 if strip > 0 {
4253 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4254 line.drain(..byte_len);
4255 }
4256 }
4257 ed.restore(lines, (top, 0));
4258 move_first_non_whitespace(ed);
4259}
4260
4261fn toggle_case_str(s: &str) -> String {
4262 s.chars()
4263 .map(|c| {
4264 if c.is_lowercase() {
4265 c.to_uppercase().next().unwrap_or(c)
4266 } else if c.is_uppercase() {
4267 c.to_lowercase().next().unwrap_or(c)
4268 } else {
4269 c
4270 }
4271 })
4272 .collect()
4273}
4274
4275fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4276 if a <= b { (a, b) } else { (b, a) }
4277}
4278
4279fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4284 let (row, col) = ed.cursor();
4285 let line_chars = buf_line_chars(&ed.buffer, row);
4286 let max_col = line_chars.saturating_sub(1);
4287 if col > max_col {
4288 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4289 ed.push_buffer_cursor_to_textarea();
4290 }
4291}
4292
4293fn execute_line_op<H: crate::types::Host>(
4296 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4297 op: Operator,
4298 count: usize,
4299) {
4300 let (row, col) = ed.cursor();
4301 let total = buf_row_count(&ed.buffer);
4302 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4303
4304 match op {
4305 Operator::Yank => {
4306 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4308 if !text.is_empty() {
4309 ed.record_yank_to_host(text.clone());
4310 ed.record_yank(text, true);
4311 }
4312 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4315 ed.set_mark('[', (row, 0));
4316 ed.set_mark(']', (end_row, last_col));
4317 buf_set_cursor_rc(&mut ed.buffer, row, col);
4318 ed.push_buffer_cursor_to_textarea();
4319 ed.vim.mode = Mode::Normal;
4320 }
4321 Operator::Delete => {
4322 ed.push_undo();
4323 let deleted_through_last = end_row + 1 >= total;
4324 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4325 let total_after = buf_row_count(&ed.buffer);
4329 let raw_target = if deleted_through_last {
4330 row.saturating_sub(1).min(total_after.saturating_sub(1))
4331 } else {
4332 row.min(total_after.saturating_sub(1))
4333 };
4334 let target_row = if raw_target > 0
4340 && raw_target + 1 == total_after
4341 && buf_line(&ed.buffer, raw_target)
4342 .map(str::is_empty)
4343 .unwrap_or(false)
4344 {
4345 raw_target - 1
4346 } else {
4347 raw_target
4348 };
4349 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4350 ed.push_buffer_cursor_to_textarea();
4351 move_first_non_whitespace(ed);
4352 ed.sticky_col = Some(ed.cursor().1);
4353 ed.vim.mode = Mode::Normal;
4354 let pos = ed.cursor();
4357 ed.set_mark('[', pos);
4358 ed.set_mark(']', pos);
4359 }
4360 Operator::Change => {
4361 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4365 ed.vim.change_mark_start = Some((row, 0));
4367 ed.push_undo();
4368 ed.sync_buffer_content_from_textarea();
4369 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4371 if end_row > row {
4372 ed.mutate_edit(Edit::DeleteRange {
4373 start: Position::new(row + 1, 0),
4374 end: Position::new(end_row, 0),
4375 kind: BufKind::Line,
4376 });
4377 }
4378 let line_chars = buf_line_chars(&ed.buffer, row);
4379 if line_chars > 0 {
4380 ed.mutate_edit(Edit::DeleteRange {
4381 start: Position::new(row, 0),
4382 end: Position::new(row, line_chars),
4383 kind: BufKind::Char,
4384 });
4385 }
4386 if !payload.is_empty() {
4387 ed.record_yank_to_host(payload.clone());
4388 ed.record_delete(payload, true);
4389 }
4390 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4391 ed.push_buffer_cursor_to_textarea();
4392 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4393 }
4394 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4395 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4399 move_first_non_whitespace(ed);
4402 }
4403 Operator::Indent | Operator::Outdent => {
4404 ed.push_undo();
4406 if op == Operator::Indent {
4407 indent_rows(ed, row, end_row, 1);
4408 } else {
4409 outdent_rows(ed, row, end_row, 1);
4410 }
4411 ed.sticky_col = Some(ed.cursor().1);
4412 ed.vim.mode = Mode::Normal;
4413 }
4414 Operator::Fold => unreachable!("Fold has no line-op double"),
4416 Operator::Reflow => {
4417 ed.push_undo();
4419 reflow_rows(ed, row, end_row);
4420 move_first_non_whitespace(ed);
4421 ed.sticky_col = Some(ed.cursor().1);
4422 ed.vim.mode = Mode::Normal;
4423 }
4424 }
4425}
4426
4427fn apply_visual_operator<H: crate::types::Host>(
4430 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4431 op: Operator,
4432) {
4433 match ed.vim.mode {
4434 Mode::VisualLine => {
4435 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4436 let top = cursor_row.min(ed.vim.visual_line_anchor);
4437 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4438 ed.vim.yank_linewise = true;
4439 match op {
4440 Operator::Yank => {
4441 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4442 if !text.is_empty() {
4443 ed.record_yank_to_host(text.clone());
4444 ed.record_yank(text, true);
4445 }
4446 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4447 ed.push_buffer_cursor_to_textarea();
4448 ed.vim.mode = Mode::Normal;
4449 }
4450 Operator::Delete => {
4451 ed.push_undo();
4452 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4453 ed.vim.mode = Mode::Normal;
4454 }
4455 Operator::Change => {
4456 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4459 ed.push_undo();
4460 ed.sync_buffer_content_from_textarea();
4461 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4462 if bot > top {
4463 ed.mutate_edit(Edit::DeleteRange {
4464 start: Position::new(top + 1, 0),
4465 end: Position::new(bot, 0),
4466 kind: BufKind::Line,
4467 });
4468 }
4469 let line_chars = buf_line_chars(&ed.buffer, top);
4470 if line_chars > 0 {
4471 ed.mutate_edit(Edit::DeleteRange {
4472 start: Position::new(top, 0),
4473 end: Position::new(top, line_chars),
4474 kind: BufKind::Char,
4475 });
4476 }
4477 if !payload.is_empty() {
4478 ed.record_yank_to_host(payload.clone());
4479 ed.record_delete(payload, true);
4480 }
4481 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4482 ed.push_buffer_cursor_to_textarea();
4483 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4484 }
4485 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4486 let bot = buf_cursor_pos(&ed.buffer)
4487 .row
4488 .max(ed.vim.visual_line_anchor);
4489 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4490 move_first_non_whitespace(ed);
4491 }
4492 Operator::Indent | Operator::Outdent => {
4493 ed.push_undo();
4494 let (cursor_row, _) = ed.cursor();
4495 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4496 if op == Operator::Indent {
4497 indent_rows(ed, top, bot, 1);
4498 } else {
4499 outdent_rows(ed, top, bot, 1);
4500 }
4501 ed.vim.mode = Mode::Normal;
4502 }
4503 Operator::Reflow => {
4504 ed.push_undo();
4505 let (cursor_row, _) = ed.cursor();
4506 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4507 reflow_rows(ed, top, bot);
4508 ed.vim.mode = Mode::Normal;
4509 }
4510 Operator::Fold => unreachable!("Visual zf takes its own path"),
4513 }
4514 }
4515 Mode::Visual => {
4516 ed.vim.yank_linewise = false;
4517 let anchor = ed.vim.visual_anchor;
4518 let cursor = ed.cursor();
4519 let (top, bot) = order(anchor, cursor);
4520 match op {
4521 Operator::Yank => {
4522 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4523 if !text.is_empty() {
4524 ed.record_yank_to_host(text.clone());
4525 ed.record_yank(text, false);
4526 }
4527 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4528 ed.push_buffer_cursor_to_textarea();
4529 ed.vim.mode = Mode::Normal;
4530 }
4531 Operator::Delete => {
4532 ed.push_undo();
4533 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4534 ed.vim.mode = Mode::Normal;
4535 }
4536 Operator::Change => {
4537 ed.push_undo();
4538 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4539 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4540 }
4541 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4542 let anchor = ed.vim.visual_anchor;
4544 let cursor = ed.cursor();
4545 let (top, bot) = order(anchor, cursor);
4546 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4547 }
4548 Operator::Indent | Operator::Outdent => {
4549 ed.push_undo();
4550 let anchor = ed.vim.visual_anchor;
4551 let cursor = ed.cursor();
4552 let (top, bot) = order(anchor, cursor);
4553 if op == Operator::Indent {
4554 indent_rows(ed, top.0, bot.0, 1);
4555 } else {
4556 outdent_rows(ed, top.0, bot.0, 1);
4557 }
4558 ed.vim.mode = Mode::Normal;
4559 }
4560 Operator::Reflow => {
4561 ed.push_undo();
4562 let anchor = ed.vim.visual_anchor;
4563 let cursor = ed.cursor();
4564 let (top, bot) = order(anchor, cursor);
4565 reflow_rows(ed, top.0, bot.0);
4566 ed.vim.mode = Mode::Normal;
4567 }
4568 Operator::Fold => unreachable!("Visual zf takes its own path"),
4569 }
4570 }
4571 Mode::VisualBlock => apply_block_operator(ed, op),
4572 _ => {}
4573 }
4574}
4575
4576fn block_bounds<H: crate::types::Host>(
4581 ed: &Editor<hjkl_buffer::Buffer, H>,
4582) -> (usize, usize, usize, usize) {
4583 let (ar, ac) = ed.vim.block_anchor;
4584 let (cr, _) = ed.cursor();
4585 let cc = ed.vim.block_vcol;
4586 let top = ar.min(cr);
4587 let bot = ar.max(cr);
4588 let left = ac.min(cc);
4589 let right = ac.max(cc);
4590 (top, bot, left, right)
4591}
4592
4593fn update_block_vcol<H: crate::types::Host>(
4598 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4599 motion: &Motion,
4600) {
4601 match motion {
4602 Motion::Left
4603 | Motion::Right
4604 | Motion::WordFwd
4605 | Motion::BigWordFwd
4606 | Motion::WordBack
4607 | Motion::BigWordBack
4608 | Motion::WordEnd
4609 | Motion::BigWordEnd
4610 | Motion::WordEndBack
4611 | Motion::BigWordEndBack
4612 | Motion::LineStart
4613 | Motion::FirstNonBlank
4614 | Motion::LineEnd
4615 | Motion::Find { .. }
4616 | Motion::FindRepeat { .. }
4617 | Motion::MatchBracket => {
4618 ed.vim.block_vcol = ed.cursor().1;
4619 }
4620 _ => {}
4622 }
4623}
4624
4625fn apply_block_operator<H: crate::types::Host>(
4630 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4631 op: Operator,
4632) {
4633 let (top, bot, left, right) = block_bounds(ed);
4634 let yank = block_yank(ed, top, bot, left, right);
4636
4637 match op {
4638 Operator::Yank => {
4639 if !yank.is_empty() {
4640 ed.record_yank_to_host(yank.clone());
4641 ed.record_yank(yank, false);
4642 }
4643 ed.vim.mode = Mode::Normal;
4644 ed.jump_cursor(top, left);
4645 }
4646 Operator::Delete => {
4647 ed.push_undo();
4648 delete_block_contents(ed, top, bot, left, right);
4649 if !yank.is_empty() {
4650 ed.record_yank_to_host(yank.clone());
4651 ed.record_delete(yank, false);
4652 }
4653 ed.vim.mode = Mode::Normal;
4654 ed.jump_cursor(top, left);
4655 }
4656 Operator::Change => {
4657 ed.push_undo();
4658 delete_block_contents(ed, top, bot, left, right);
4659 if !yank.is_empty() {
4660 ed.record_yank_to_host(yank.clone());
4661 ed.record_delete(yank, false);
4662 }
4663 ed.jump_cursor(top, left);
4664 begin_insert_noundo(
4665 ed,
4666 1,
4667 InsertReason::BlockChange {
4668 top,
4669 bot,
4670 col: left,
4671 },
4672 );
4673 }
4674 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4675 ed.push_undo();
4676 transform_block_case(ed, op, top, bot, left, right);
4677 ed.vim.mode = Mode::Normal;
4678 ed.jump_cursor(top, left);
4679 }
4680 Operator::Indent | Operator::Outdent => {
4681 ed.push_undo();
4685 if op == Operator::Indent {
4686 indent_rows(ed, top, bot, 1);
4687 } else {
4688 outdent_rows(ed, top, bot, 1);
4689 }
4690 ed.vim.mode = Mode::Normal;
4691 }
4692 Operator::Fold => unreachable!("Visual zf takes its own path"),
4693 Operator::Reflow => {
4694 ed.push_undo();
4698 reflow_rows(ed, top, bot);
4699 ed.vim.mode = Mode::Normal;
4700 }
4701 }
4702}
4703
4704fn transform_block_case<H: crate::types::Host>(
4708 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4709 op: Operator,
4710 top: usize,
4711 bot: usize,
4712 left: usize,
4713 right: usize,
4714) {
4715 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4716 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4717 let chars: Vec<char> = lines[r].chars().collect();
4718 if left >= chars.len() {
4719 continue;
4720 }
4721 let end = (right + 1).min(chars.len());
4722 let head: String = chars[..left].iter().collect();
4723 let mid: String = chars[left..end].iter().collect();
4724 let tail: String = chars[end..].iter().collect();
4725 let transformed = match op {
4726 Operator::Uppercase => mid.to_uppercase(),
4727 Operator::Lowercase => mid.to_lowercase(),
4728 Operator::ToggleCase => toggle_case_str(&mid),
4729 _ => mid,
4730 };
4731 lines[r] = format!("{head}{transformed}{tail}");
4732 }
4733 let saved_yank = ed.yank().to_string();
4734 let saved_linewise = ed.vim.yank_linewise;
4735 ed.restore(lines, (top, left));
4736 ed.set_yank(saved_yank);
4737 ed.vim.yank_linewise = saved_linewise;
4738}
4739
4740fn block_yank<H: crate::types::Host>(
4741 ed: &Editor<hjkl_buffer::Buffer, H>,
4742 top: usize,
4743 bot: usize,
4744 left: usize,
4745 right: usize,
4746) -> String {
4747 let lines = buf_lines_to_vec(&ed.buffer);
4748 let mut rows: Vec<String> = Vec::new();
4749 for r in top..=bot {
4750 let line = match lines.get(r) {
4751 Some(l) => l,
4752 None => break,
4753 };
4754 let chars: Vec<char> = line.chars().collect();
4755 let end = (right + 1).min(chars.len());
4756 if left >= chars.len() {
4757 rows.push(String::new());
4758 } else {
4759 rows.push(chars[left..end].iter().collect());
4760 }
4761 }
4762 rows.join("\n")
4763}
4764
4765fn delete_block_contents<H: crate::types::Host>(
4766 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4767 top: usize,
4768 bot: usize,
4769 left: usize,
4770 right: usize,
4771) {
4772 use hjkl_buffer::{Edit, MotionKind, Position};
4773 ed.sync_buffer_content_from_textarea();
4774 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4775 if last_row < top {
4776 return;
4777 }
4778 ed.mutate_edit(Edit::DeleteRange {
4779 start: Position::new(top, left),
4780 end: Position::new(last_row, right),
4781 kind: MotionKind::Block,
4782 });
4783 ed.push_buffer_cursor_to_textarea();
4784}
4785
4786fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4788 let (top, bot, left, right) = block_bounds(ed);
4789 ed.push_undo();
4790 ed.sync_buffer_content_from_textarea();
4791 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4792 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4793 let chars: Vec<char> = lines[r].chars().collect();
4794 if left >= chars.len() {
4795 continue;
4796 }
4797 let end = (right + 1).min(chars.len());
4798 let before: String = chars[..left].iter().collect();
4799 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4800 let after: String = chars[end..].iter().collect();
4801 lines[r] = format!("{before}{middle}{after}");
4802 }
4803 reset_textarea_lines(ed, lines);
4804 ed.vim.mode = Mode::Normal;
4805 ed.jump_cursor(top, left);
4806}
4807
4808fn reset_textarea_lines<H: crate::types::Host>(
4812 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4813 lines: Vec<String>,
4814) {
4815 let cursor = ed.cursor();
4816 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4817 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4818 ed.mark_content_dirty();
4819}
4820
4821type Pos = (usize, usize);
4827
4828fn text_object_range<H: crate::types::Host>(
4832 ed: &Editor<hjkl_buffer::Buffer, H>,
4833 obj: TextObject,
4834 inner: bool,
4835) -> Option<(Pos, Pos, MotionKind)> {
4836 match obj {
4837 TextObject::Word { big } => {
4838 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4839 }
4840 TextObject::Quote(q) => {
4841 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4842 }
4843 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4844 TextObject::Paragraph => {
4845 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4846 }
4847 TextObject::XmlTag => {
4848 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4849 }
4850 TextObject::Sentence => {
4851 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4852 }
4853 }
4854}
4855
4856fn sentence_boundary<H: crate::types::Host>(
4860 ed: &Editor<hjkl_buffer::Buffer, H>,
4861 forward: bool,
4862) -> Option<(usize, usize)> {
4863 let lines = buf_lines_to_vec(&ed.buffer);
4864 if lines.is_empty() {
4865 return None;
4866 }
4867 let pos_to_idx = |pos: (usize, usize)| -> usize {
4868 let mut idx = 0;
4869 for line in lines.iter().take(pos.0) {
4870 idx += line.chars().count() + 1;
4871 }
4872 idx + pos.1
4873 };
4874 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4875 for (r, line) in lines.iter().enumerate() {
4876 let len = line.chars().count();
4877 if idx <= len {
4878 return (r, idx);
4879 }
4880 idx -= len + 1;
4881 }
4882 let last = lines.len().saturating_sub(1);
4883 (last, lines[last].chars().count())
4884 };
4885 let mut chars: Vec<char> = Vec::new();
4886 for (r, line) in lines.iter().enumerate() {
4887 chars.extend(line.chars());
4888 if r + 1 < lines.len() {
4889 chars.push('\n');
4890 }
4891 }
4892 if chars.is_empty() {
4893 return None;
4894 }
4895 let total = chars.len();
4896 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4897 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4898
4899 if forward {
4900 let mut i = cursor_idx + 1;
4903 while i < total {
4904 if is_terminator(chars[i]) {
4905 while i + 1 < total && is_terminator(chars[i + 1]) {
4906 i += 1;
4907 }
4908 if i + 1 >= total {
4909 return None;
4910 }
4911 if chars[i + 1].is_whitespace() {
4912 let mut j = i + 1;
4913 while j < total && chars[j].is_whitespace() {
4914 j += 1;
4915 }
4916 if j >= total {
4917 return None;
4918 }
4919 return Some(idx_to_pos(j));
4920 }
4921 }
4922 i += 1;
4923 }
4924 None
4925 } else {
4926 let find_start = |from: usize| -> Option<usize> {
4930 let mut start = from;
4931 while start > 0 {
4932 let prev = chars[start - 1];
4933 if prev.is_whitespace() {
4934 let mut k = start - 1;
4935 while k > 0 && chars[k - 1].is_whitespace() {
4936 k -= 1;
4937 }
4938 if k > 0 && is_terminator(chars[k - 1]) {
4939 break;
4940 }
4941 }
4942 start -= 1;
4943 }
4944 while start < total && chars[start].is_whitespace() {
4945 start += 1;
4946 }
4947 (start < total).then_some(start)
4948 };
4949 let current_start = find_start(cursor_idx)?;
4950 if current_start < cursor_idx {
4951 return Some(idx_to_pos(current_start));
4952 }
4953 let mut k = current_start;
4956 while k > 0 && chars[k - 1].is_whitespace() {
4957 k -= 1;
4958 }
4959 if k == 0 {
4960 return None;
4961 }
4962 let prev_start = find_start(k - 1)?;
4963 Some(idx_to_pos(prev_start))
4964 }
4965}
4966
4967fn sentence_text_object<H: crate::types::Host>(
4973 ed: &Editor<hjkl_buffer::Buffer, H>,
4974 inner: bool,
4975) -> Option<((usize, usize), (usize, usize))> {
4976 let lines = buf_lines_to_vec(&ed.buffer);
4977 if lines.is_empty() {
4978 return None;
4979 }
4980 let pos_to_idx = |pos: (usize, usize)| -> usize {
4983 let mut idx = 0;
4984 for line in lines.iter().take(pos.0) {
4985 idx += line.chars().count() + 1;
4986 }
4987 idx + pos.1
4988 };
4989 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4990 for (r, line) in lines.iter().enumerate() {
4991 let len = line.chars().count();
4992 if idx <= len {
4993 return (r, idx);
4994 }
4995 idx -= len + 1;
4996 }
4997 let last = lines.len().saturating_sub(1);
4998 (last, lines[last].chars().count())
4999 };
5000 let mut chars: Vec<char> = Vec::new();
5001 for (r, line) in lines.iter().enumerate() {
5002 chars.extend(line.chars());
5003 if r + 1 < lines.len() {
5004 chars.push('\n');
5005 }
5006 }
5007 if chars.is_empty() {
5008 return None;
5009 }
5010
5011 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
5012 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
5013
5014 let mut start = cursor_idx;
5018 while start > 0 {
5019 let prev = chars[start - 1];
5020 if prev.is_whitespace() {
5021 let mut k = start - 1;
5025 while k > 0 && chars[k - 1].is_whitespace() {
5026 k -= 1;
5027 }
5028 if k > 0 && is_terminator(chars[k - 1]) {
5029 break;
5030 }
5031 }
5032 start -= 1;
5033 }
5034 while start < chars.len() && chars[start].is_whitespace() {
5037 start += 1;
5038 }
5039 if start >= chars.len() {
5040 return None;
5041 }
5042
5043 let mut end = start;
5046 while end < chars.len() {
5047 if is_terminator(chars[end]) {
5048 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
5050 end += 1;
5051 }
5052 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
5055 break;
5056 }
5057 }
5058 end += 1;
5059 }
5060 let end_idx = (end + 1).min(chars.len());
5062
5063 let final_end = if inner {
5064 end_idx
5065 } else {
5066 let mut e = end_idx;
5070 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
5071 e += 1;
5072 }
5073 e
5074 };
5075
5076 Some((idx_to_pos(start), idx_to_pos(final_end)))
5077}
5078
5079fn tag_text_object<H: crate::types::Host>(
5083 ed: &Editor<hjkl_buffer::Buffer, H>,
5084 inner: bool,
5085) -> Option<((usize, usize), (usize, usize))> {
5086 let lines = buf_lines_to_vec(&ed.buffer);
5087 if lines.is_empty() {
5088 return None;
5089 }
5090 let pos_to_idx = |pos: (usize, usize)| -> usize {
5094 let mut idx = 0;
5095 for line in lines.iter().take(pos.0) {
5096 idx += line.chars().count() + 1;
5097 }
5098 idx + pos.1
5099 };
5100 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5101 for (r, line) in lines.iter().enumerate() {
5102 let len = line.chars().count();
5103 if idx <= len {
5104 return (r, idx);
5105 }
5106 idx -= len + 1;
5107 }
5108 let last = lines.len().saturating_sub(1);
5109 (last, lines[last].chars().count())
5110 };
5111 let mut chars: Vec<char> = Vec::new();
5112 for (r, line) in lines.iter().enumerate() {
5113 chars.extend(line.chars());
5114 if r + 1 < lines.len() {
5115 chars.push('\n');
5116 }
5117 }
5118 let cursor_idx = pos_to_idx(ed.cursor());
5119
5120 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5128 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5129 let mut i = 0;
5130 while i < chars.len() {
5131 if chars[i] != '<' {
5132 i += 1;
5133 continue;
5134 }
5135 let mut j = i + 1;
5136 while j < chars.len() && chars[j] != '>' {
5137 j += 1;
5138 }
5139 if j >= chars.len() {
5140 break;
5141 }
5142 let inside: String = chars[i + 1..j].iter().collect();
5143 let close_end = j + 1;
5144 let trimmed = inside.trim();
5145 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5146 i = close_end;
5147 continue;
5148 }
5149 if let Some(rest) = trimmed.strip_prefix('/') {
5150 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5151 if !name.is_empty()
5152 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5153 {
5154 let (open_start, content_start, _) = stack[stack_idx].clone();
5155 stack.truncate(stack_idx);
5156 let content_end = i;
5157 let candidate = (open_start, content_start, content_end, close_end);
5158 if cursor_idx >= content_start && cursor_idx <= content_end {
5159 innermost = match innermost {
5160 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5161 Some(candidate)
5162 }
5163 None => Some(candidate),
5164 existing => existing,
5165 };
5166 } else if open_start >= cursor_idx && next_after.is_none() {
5167 next_after = Some(candidate);
5168 }
5169 }
5170 } else if !trimmed.ends_with('/') {
5171 let name: String = trimmed
5172 .split(|c: char| c.is_whitespace() || c == '/')
5173 .next()
5174 .unwrap_or("")
5175 .to_string();
5176 if !name.is_empty() {
5177 stack.push((i, close_end, name));
5178 }
5179 }
5180 i = close_end;
5181 }
5182
5183 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5184 if inner {
5185 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5186 } else {
5187 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5188 }
5189}
5190
5191fn is_wordchar(c: char) -> bool {
5192 c.is_alphanumeric() || c == '_'
5193}
5194
5195pub(crate) use hjkl_buffer::is_keyword_char;
5199
5200fn word_text_object<H: crate::types::Host>(
5201 ed: &Editor<hjkl_buffer::Buffer, H>,
5202 inner: bool,
5203 big: bool,
5204) -> Option<((usize, usize), (usize, usize))> {
5205 let (row, col) = ed.cursor();
5206 let line = buf_line(&ed.buffer, row)?;
5207 let chars: Vec<char> = line.chars().collect();
5208 if chars.is_empty() {
5209 return None;
5210 }
5211 let at = col.min(chars.len().saturating_sub(1));
5212 let classify = |c: char| -> u8 {
5213 if c.is_whitespace() {
5214 0
5215 } else if big || is_wordchar(c) {
5216 1
5217 } else {
5218 2
5219 }
5220 };
5221 let cls = classify(chars[at]);
5222 let mut start = at;
5223 while start > 0 && classify(chars[start - 1]) == cls {
5224 start -= 1;
5225 }
5226 let mut end = at;
5227 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5228 end += 1;
5229 }
5230 let char_byte = |i: usize| {
5232 if i >= chars.len() {
5233 line.len()
5234 } else {
5235 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5236 }
5237 };
5238 let mut start_col = char_byte(start);
5239 let mut end_col = char_byte(end + 1);
5241 if !inner {
5242 let mut t = end + 1;
5244 let mut included_trailing = false;
5245 while t < chars.len() && chars[t].is_whitespace() {
5246 included_trailing = true;
5247 t += 1;
5248 }
5249 if included_trailing {
5250 end_col = char_byte(t);
5251 } else {
5252 let mut s = start;
5253 while s > 0 && chars[s - 1].is_whitespace() {
5254 s -= 1;
5255 }
5256 start_col = char_byte(s);
5257 }
5258 }
5259 Some(((row, start_col), (row, end_col)))
5260}
5261
5262fn quote_text_object<H: crate::types::Host>(
5263 ed: &Editor<hjkl_buffer::Buffer, H>,
5264 q: char,
5265 inner: bool,
5266) -> Option<((usize, usize), (usize, usize))> {
5267 let (row, col) = ed.cursor();
5268 let line = buf_line(&ed.buffer, row)?;
5269 let bytes = line.as_bytes();
5270 let q_byte = q as u8;
5271 let mut positions: Vec<usize> = Vec::new();
5273 for (i, &b) in bytes.iter().enumerate() {
5274 if b == q_byte {
5275 positions.push(i);
5276 }
5277 }
5278 if positions.len() < 2 {
5279 return None;
5280 }
5281 let mut open_idx: Option<usize> = None;
5282 let mut close_idx: Option<usize> = None;
5283 for pair in positions.chunks(2) {
5284 if pair.len() < 2 {
5285 break;
5286 }
5287 if col >= pair[0] && col <= pair[1] {
5288 open_idx = Some(pair[0]);
5289 close_idx = Some(pair[1]);
5290 break;
5291 }
5292 if col < pair[0] {
5293 open_idx = Some(pair[0]);
5294 close_idx = Some(pair[1]);
5295 break;
5296 }
5297 }
5298 let open = open_idx?;
5299 let close = close_idx?;
5300 if inner {
5302 if close <= open + 1 {
5303 return None;
5304 }
5305 Some(((row, open + 1), (row, close)))
5306 } else {
5307 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5314 let mut end = after_close;
5316 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5317 end += 1;
5318 }
5319 Some(((row, open), (row, end)))
5320 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5321 let mut start = open;
5323 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5324 start -= 1;
5325 }
5326 Some(((row, start), (row, close + 1)))
5327 } else {
5328 Some(((row, open), (row, close + 1)))
5329 }
5330 }
5331}
5332
5333fn bracket_text_object<H: crate::types::Host>(
5334 ed: &Editor<hjkl_buffer::Buffer, H>,
5335 open: char,
5336 inner: bool,
5337) -> Option<(Pos, Pos, MotionKind)> {
5338 let close = match open {
5339 '(' => ')',
5340 '[' => ']',
5341 '{' => '}',
5342 '<' => '>',
5343 _ => return None,
5344 };
5345 let (row, col) = ed.cursor();
5346 let lines = buf_lines_to_vec(&ed.buffer);
5347 let lines = lines.as_slice();
5348 let open_pos = find_open_bracket(lines, row, col, open, close)
5353 .or_else(|| find_next_open(lines, row, col, open))?;
5354 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5355 if inner {
5357 if close_pos.0 > open_pos.0 + 1 {
5363 let inner_row_start = open_pos.0 + 1;
5365 let inner_row_end = close_pos.0 - 1;
5366 let end_col = lines
5367 .get(inner_row_end)
5368 .map(|l| l.chars().count())
5369 .unwrap_or(0);
5370 return Some((
5371 (inner_row_start, 0),
5372 (inner_row_end, end_col),
5373 MotionKind::Linewise,
5374 ));
5375 }
5376 let inner_start = advance_pos(lines, open_pos);
5377 if inner_start.0 > close_pos.0
5378 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5379 {
5380 return None;
5381 }
5382 Some((inner_start, close_pos, MotionKind::Exclusive))
5383 } else {
5384 Some((
5385 open_pos,
5386 advance_pos(lines, close_pos),
5387 MotionKind::Exclusive,
5388 ))
5389 }
5390}
5391
5392fn find_open_bracket(
5393 lines: &[String],
5394 row: usize,
5395 col: usize,
5396 open: char,
5397 close: char,
5398) -> Option<(usize, usize)> {
5399 let mut depth: i32 = 0;
5400 let mut r = row;
5401 let mut c = col as isize;
5402 loop {
5403 let cur = &lines[r];
5404 let chars: Vec<char> = cur.chars().collect();
5405 if (c as usize) >= chars.len() {
5409 c = chars.len() as isize - 1;
5410 }
5411 while c >= 0 {
5412 let ch = chars[c as usize];
5413 if ch == close {
5414 depth += 1;
5415 } else if ch == open {
5416 if depth == 0 {
5417 return Some((r, c as usize));
5418 }
5419 depth -= 1;
5420 }
5421 c -= 1;
5422 }
5423 if r == 0 {
5424 return None;
5425 }
5426 r -= 1;
5427 c = lines[r].chars().count() as isize - 1;
5428 }
5429}
5430
5431fn find_close_bracket(
5432 lines: &[String],
5433 row: usize,
5434 start_col: usize,
5435 open: char,
5436 close: char,
5437) -> Option<(usize, usize)> {
5438 let mut depth: i32 = 0;
5439 let mut r = row;
5440 let mut c = start_col;
5441 loop {
5442 let cur = &lines[r];
5443 let chars: Vec<char> = cur.chars().collect();
5444 while c < chars.len() {
5445 let ch = chars[c];
5446 if ch == open {
5447 depth += 1;
5448 } else if ch == close {
5449 if depth == 0 {
5450 return Some((r, c));
5451 }
5452 depth -= 1;
5453 }
5454 c += 1;
5455 }
5456 if r + 1 >= lines.len() {
5457 return None;
5458 }
5459 r += 1;
5460 c = 0;
5461 }
5462}
5463
5464fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5468 let mut r = row;
5469 let mut c = col;
5470 while r < lines.len() {
5471 let chars: Vec<char> = lines[r].chars().collect();
5472 while c < chars.len() {
5473 if chars[c] == open {
5474 return Some((r, c));
5475 }
5476 c += 1;
5477 }
5478 r += 1;
5479 c = 0;
5480 }
5481 None
5482}
5483
5484fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5485 let (r, c) = pos;
5486 let line_len = lines[r].chars().count();
5487 if c < line_len {
5488 (r, c + 1)
5489 } else if r + 1 < lines.len() {
5490 (r + 1, 0)
5491 } else {
5492 pos
5493 }
5494}
5495
5496fn paragraph_text_object<H: crate::types::Host>(
5497 ed: &Editor<hjkl_buffer::Buffer, H>,
5498 inner: bool,
5499) -> Option<((usize, usize), (usize, usize))> {
5500 let (row, _) = ed.cursor();
5501 let lines = buf_lines_to_vec(&ed.buffer);
5502 if lines.is_empty() {
5503 return None;
5504 }
5505 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5507 if is_blank(row) {
5508 return None;
5509 }
5510 let mut top = row;
5511 while top > 0 && !is_blank(top - 1) {
5512 top -= 1;
5513 }
5514 let mut bot = row;
5515 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5516 bot += 1;
5517 }
5518 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5520 bot += 1;
5521 }
5522 let end_col = lines[bot].chars().count();
5523 Some(((top, 0), (bot, end_col)))
5524}
5525
5526fn read_vim_range<H: crate::types::Host>(
5532 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5533 start: (usize, usize),
5534 end: (usize, usize),
5535 kind: MotionKind,
5536) -> String {
5537 let (top, bot) = order(start, end);
5538 ed.sync_buffer_content_from_textarea();
5539 let lines = buf_lines_to_vec(&ed.buffer);
5540 match kind {
5541 MotionKind::Linewise => {
5542 let lo = top.0;
5543 let hi = bot.0.min(lines.len().saturating_sub(1));
5544 let mut text = lines[lo..=hi].join("\n");
5545 text.push('\n');
5546 text
5547 }
5548 MotionKind::Inclusive | MotionKind::Exclusive => {
5549 let inclusive = matches!(kind, MotionKind::Inclusive);
5550 let mut out = String::new();
5552 for row in top.0..=bot.0 {
5553 let line = lines.get(row).map(String::as_str).unwrap_or("");
5554 let lo = if row == top.0 { top.1 } else { 0 };
5555 let hi_unclamped = if row == bot.0 {
5556 if inclusive { bot.1 + 1 } else { bot.1 }
5557 } else {
5558 line.chars().count() + 1
5559 };
5560 let row_chars: Vec<char> = line.chars().collect();
5561 let hi = hi_unclamped.min(row_chars.len());
5562 if lo < hi {
5563 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5564 }
5565 if row < bot.0 {
5566 out.push('\n');
5567 }
5568 }
5569 out
5570 }
5571 }
5572}
5573
5574fn cut_vim_range<H: crate::types::Host>(
5583 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5584 start: (usize, usize),
5585 end: (usize, usize),
5586 kind: MotionKind,
5587) -> String {
5588 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5589 let (top, bot) = order(start, end);
5590 ed.sync_buffer_content_from_textarea();
5591 let (buf_start, buf_end, buf_kind) = match kind {
5592 MotionKind::Linewise => (
5593 Position::new(top.0, 0),
5594 Position::new(bot.0, 0),
5595 BufKind::Line,
5596 ),
5597 MotionKind::Inclusive => {
5598 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5599 let next = if bot.1 < line_chars {
5603 Position::new(bot.0, bot.1 + 1)
5604 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5605 Position::new(bot.0 + 1, 0)
5606 } else {
5607 Position::new(bot.0, line_chars)
5608 };
5609 (Position::new(top.0, top.1), next, BufKind::Char)
5610 }
5611 MotionKind::Exclusive => (
5612 Position::new(top.0, top.1),
5613 Position::new(bot.0, bot.1),
5614 BufKind::Char,
5615 ),
5616 };
5617 let inverse = ed.mutate_edit(Edit::DeleteRange {
5618 start: buf_start,
5619 end: buf_end,
5620 kind: buf_kind,
5621 });
5622 let text = match inverse {
5623 Edit::InsertStr { text, .. } => text,
5624 _ => String::new(),
5625 };
5626 if !text.is_empty() {
5627 ed.record_yank_to_host(text.clone());
5628 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5629 }
5630 ed.push_buffer_cursor_to_textarea();
5631 text
5632}
5633
5634fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5640 use hjkl_buffer::{Edit, MotionKind, Position};
5641 ed.sync_buffer_content_from_textarea();
5642 let cursor = buf_cursor_pos(&ed.buffer);
5643 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5644 if cursor.col >= line_chars {
5645 return;
5646 }
5647 let inverse = ed.mutate_edit(Edit::DeleteRange {
5648 start: cursor,
5649 end: Position::new(cursor.row, line_chars),
5650 kind: MotionKind::Char,
5651 });
5652 if let Edit::InsertStr { text, .. } = inverse
5653 && !text.is_empty()
5654 {
5655 ed.record_yank_to_host(text.clone());
5656 ed.vim.yank_linewise = false;
5657 ed.set_yank(text);
5658 }
5659 buf_set_cursor_pos(&mut ed.buffer, cursor);
5660 ed.push_buffer_cursor_to_textarea();
5661}
5662
5663fn do_char_delete<H: crate::types::Host>(
5664 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5665 forward: bool,
5666 count: usize,
5667) {
5668 use hjkl_buffer::{Edit, MotionKind, Position};
5669 ed.push_undo();
5670 ed.sync_buffer_content_from_textarea();
5671 let mut deleted = String::new();
5674 for _ in 0..count {
5675 let cursor = buf_cursor_pos(&ed.buffer);
5676 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5677 if forward {
5678 if cursor.col >= line_chars {
5681 continue;
5682 }
5683 let inverse = ed.mutate_edit(Edit::DeleteRange {
5684 start: cursor,
5685 end: Position::new(cursor.row, cursor.col + 1),
5686 kind: MotionKind::Char,
5687 });
5688 if let Edit::InsertStr { text, .. } = inverse {
5689 deleted.push_str(&text);
5690 }
5691 } else {
5692 if cursor.col == 0 {
5694 continue;
5695 }
5696 let inverse = ed.mutate_edit(Edit::DeleteRange {
5697 start: Position::new(cursor.row, cursor.col - 1),
5698 end: cursor,
5699 kind: MotionKind::Char,
5700 });
5701 if let Edit::InsertStr { text, .. } = inverse {
5702 deleted = text + &deleted;
5705 }
5706 }
5707 }
5708 if !deleted.is_empty() {
5709 ed.record_yank_to_host(deleted.clone());
5710 ed.record_delete(deleted, false);
5711 }
5712 ed.push_buffer_cursor_to_textarea();
5713}
5714
5715fn adjust_number<H: crate::types::Host>(
5719 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5720 delta: i64,
5721) -> bool {
5722 use hjkl_buffer::{Edit, MotionKind, Position};
5723 ed.sync_buffer_content_from_textarea();
5724 let cursor = buf_cursor_pos(&ed.buffer);
5725 let row = cursor.row;
5726 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5727 Some(l) => l.chars().collect(),
5728 None => return false,
5729 };
5730 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5731 return false;
5732 };
5733 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5734 digit_start - 1
5735 } else {
5736 digit_start
5737 };
5738 let mut span_end = digit_start;
5739 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5740 span_end += 1;
5741 }
5742 let s: String = chars[span_start..span_end].iter().collect();
5743 let Ok(n) = s.parse::<i64>() else {
5744 return false;
5745 };
5746 let new_s = n.saturating_add(delta).to_string();
5747
5748 ed.push_undo();
5749 let span_start_pos = Position::new(row, span_start);
5750 let span_end_pos = Position::new(row, span_end);
5751 ed.mutate_edit(Edit::DeleteRange {
5752 start: span_start_pos,
5753 end: span_end_pos,
5754 kind: MotionKind::Char,
5755 });
5756 ed.mutate_edit(Edit::InsertStr {
5757 at: span_start_pos,
5758 text: new_s.clone(),
5759 });
5760 let new_len = new_s.chars().count();
5761 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5762 ed.push_buffer_cursor_to_textarea();
5763 true
5764}
5765
5766pub(crate) fn replace_char<H: crate::types::Host>(
5767 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5768 ch: char,
5769 count: usize,
5770) {
5771 use hjkl_buffer::{Edit, MotionKind, Position};
5772 ed.push_undo();
5773 ed.sync_buffer_content_from_textarea();
5774 for _ in 0..count {
5775 let cursor = buf_cursor_pos(&ed.buffer);
5776 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5777 if cursor.col >= line_chars {
5778 break;
5779 }
5780 ed.mutate_edit(Edit::DeleteRange {
5781 start: cursor,
5782 end: Position::new(cursor.row, cursor.col + 1),
5783 kind: MotionKind::Char,
5784 });
5785 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5786 }
5787 crate::motions::move_left(&mut ed.buffer, 1);
5789 ed.push_buffer_cursor_to_textarea();
5790}
5791
5792fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5793 use hjkl_buffer::{Edit, MotionKind, Position};
5794 ed.sync_buffer_content_from_textarea();
5795 let cursor = buf_cursor_pos(&ed.buffer);
5796 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5797 return;
5798 };
5799 let toggled = if c.is_uppercase() {
5800 c.to_lowercase().next().unwrap_or(c)
5801 } else {
5802 c.to_uppercase().next().unwrap_or(c)
5803 };
5804 ed.mutate_edit(Edit::DeleteRange {
5805 start: cursor,
5806 end: Position::new(cursor.row, cursor.col + 1),
5807 kind: MotionKind::Char,
5808 });
5809 ed.mutate_edit(Edit::InsertChar {
5810 at: cursor,
5811 ch: toggled,
5812 });
5813}
5814
5815fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5816 use hjkl_buffer::{Edit, Position};
5817 ed.sync_buffer_content_from_textarea();
5818 let row = buf_cursor_pos(&ed.buffer).row;
5819 if row + 1 >= buf_row_count(&ed.buffer) {
5820 return;
5821 }
5822 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5823 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5824 let next_trimmed = next_raw.trim_start();
5825 let cur_chars = cur_line.chars().count();
5826 let next_chars = next_raw.chars().count();
5827 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5830 " "
5831 } else {
5832 ""
5833 };
5834 let joined = format!("{cur_line}{separator}{next_trimmed}");
5835 ed.mutate_edit(Edit::Replace {
5836 start: Position::new(row, 0),
5837 end: Position::new(row + 1, next_chars),
5838 with: joined,
5839 });
5840 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5844 ed.push_buffer_cursor_to_textarea();
5845}
5846
5847fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5850 use hjkl_buffer::Edit;
5851 ed.sync_buffer_content_from_textarea();
5852 let row = buf_cursor_pos(&ed.buffer).row;
5853 if row + 1 >= buf_row_count(&ed.buffer) {
5854 return;
5855 }
5856 let join_col = buf_line_chars(&ed.buffer, row);
5857 ed.mutate_edit(Edit::JoinLines {
5858 row,
5859 count: 1,
5860 with_space: false,
5861 });
5862 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5864 ed.push_buffer_cursor_to_textarea();
5865}
5866
5867fn do_paste<H: crate::types::Host>(
5868 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5869 before: bool,
5870 count: usize,
5871) {
5872 use hjkl_buffer::{Edit, Position};
5873 ed.push_undo();
5874 let selector = ed.vim.pending_register.take();
5879 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5880 Some(slot) => (slot.text.clone(), slot.linewise),
5881 None => {
5887 let s = &ed.registers().unnamed;
5888 (s.text.clone(), s.linewise)
5889 }
5890 };
5891 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5895 for _ in 0..count {
5896 ed.sync_buffer_content_from_textarea();
5897 let yank = yank.clone();
5898 if yank.is_empty() {
5899 continue;
5900 }
5901 if linewise {
5902 let text = yank.trim_matches('\n').to_string();
5906 let row = buf_cursor_pos(&ed.buffer).row;
5907 let target_row = if before {
5908 ed.mutate_edit(Edit::InsertStr {
5909 at: Position::new(row, 0),
5910 text: format!("{text}\n"),
5911 });
5912 row
5913 } else {
5914 let line_chars = buf_line_chars(&ed.buffer, row);
5915 ed.mutate_edit(Edit::InsertStr {
5916 at: Position::new(row, line_chars),
5917 text: format!("\n{text}"),
5918 });
5919 row + 1
5920 };
5921 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5922 crate::motions::move_first_non_blank(&mut ed.buffer);
5923 ed.push_buffer_cursor_to_textarea();
5924 let payload_lines = text.lines().count().max(1);
5926 let bot_row = target_row + payload_lines - 1;
5927 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5928 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5929 } else {
5930 let cursor = buf_cursor_pos(&ed.buffer);
5934 let at = if before {
5935 cursor
5936 } else {
5937 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5938 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5939 };
5940 ed.mutate_edit(Edit::InsertStr {
5941 at,
5942 text: yank.clone(),
5943 });
5944 crate::motions::move_left(&mut ed.buffer, 1);
5947 ed.push_buffer_cursor_to_textarea();
5948 let lo = (at.row, at.col);
5950 let hi = ed.cursor();
5951 paste_mark = Some((lo, hi));
5952 }
5953 }
5954 if let Some((lo, hi)) = paste_mark {
5955 ed.set_mark('[', lo);
5956 ed.set_mark(']', hi);
5957 }
5958 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5960}
5961
5962pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5963 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5964 let current = ed.snapshot();
5965 ed.redo_stack.push(current);
5966 ed.restore(lines, cursor);
5967 }
5968 ed.vim.mode = Mode::Normal;
5969 clamp_cursor_to_normal_mode(ed);
5973}
5974
5975pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5976 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5977 let current = ed.snapshot();
5978 ed.undo_stack.push(current);
5979 ed.cap_undo();
5980 ed.restore(lines, cursor);
5981 }
5982 ed.vim.mode = Mode::Normal;
5983}
5984
5985fn replay_insert_and_finish<H: crate::types::Host>(
5992 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5993 text: &str,
5994) {
5995 use hjkl_buffer::{Edit, Position};
5996 let cursor = ed.cursor();
5997 ed.mutate_edit(Edit::InsertStr {
5998 at: Position::new(cursor.0, cursor.1),
5999 text: text.to_string(),
6000 });
6001 if ed.vim.insert_session.take().is_some() {
6002 if ed.cursor().1 > 0 {
6003 crate::motions::move_left(&mut ed.buffer, 1);
6004 ed.push_buffer_cursor_to_textarea();
6005 }
6006 ed.vim.mode = Mode::Normal;
6007 }
6008}
6009
6010fn replay_last_change<H: crate::types::Host>(
6011 ed: &mut Editor<hjkl_buffer::Buffer, H>,
6012 outer_count: usize,
6013) {
6014 let Some(change) = ed.vim.last_change.clone() else {
6015 return;
6016 };
6017 ed.vim.replaying = true;
6018 let scale = if outer_count > 0 { outer_count } else { 1 };
6019 match change {
6020 LastChange::OpMotion {
6021 op,
6022 motion,
6023 count,
6024 inserted,
6025 } => {
6026 let total = count.max(1) * scale;
6027 apply_op_with_motion(ed, op, &motion, total);
6028 if let Some(text) = inserted {
6029 replay_insert_and_finish(ed, &text);
6030 }
6031 }
6032 LastChange::OpTextObj {
6033 op,
6034 obj,
6035 inner,
6036 inserted,
6037 } => {
6038 apply_op_with_text_object(ed, op, obj, inner);
6039 if let Some(text) = inserted {
6040 replay_insert_and_finish(ed, &text);
6041 }
6042 }
6043 LastChange::LineOp {
6044 op,
6045 count,
6046 inserted,
6047 } => {
6048 let total = count.max(1) * scale;
6049 execute_line_op(ed, op, total);
6050 if let Some(text) = inserted {
6051 replay_insert_and_finish(ed, &text);
6052 }
6053 }
6054 LastChange::CharDel { forward, count } => {
6055 do_char_delete(ed, forward, count * scale);
6056 }
6057 LastChange::ReplaceChar { ch, count } => {
6058 replace_char(ed, ch, count * scale);
6059 }
6060 LastChange::ToggleCase { count } => {
6061 for _ in 0..count * scale {
6062 ed.push_undo();
6063 toggle_case_at_cursor(ed);
6064 }
6065 }
6066 LastChange::JoinLine { count } => {
6067 for _ in 0..count * scale {
6068 ed.push_undo();
6069 join_line(ed);
6070 }
6071 }
6072 LastChange::Paste { before, count } => {
6073 do_paste(ed, before, count * scale);
6074 }
6075 LastChange::DeleteToEol { inserted } => {
6076 use hjkl_buffer::{Edit, Position};
6077 ed.push_undo();
6078 delete_to_eol(ed);
6079 if let Some(text) = inserted {
6080 let cursor = ed.cursor();
6081 ed.mutate_edit(Edit::InsertStr {
6082 at: Position::new(cursor.0, cursor.1),
6083 text,
6084 });
6085 }
6086 }
6087 LastChange::OpenLine { above, inserted } => {
6088 use hjkl_buffer::{Edit, Position};
6089 ed.push_undo();
6090 ed.sync_buffer_content_from_textarea();
6091 let row = buf_cursor_pos(&ed.buffer).row;
6092 if above {
6093 ed.mutate_edit(Edit::InsertStr {
6094 at: Position::new(row, 0),
6095 text: "\n".to_string(),
6096 });
6097 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
6098 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
6099 } else {
6100 let line_chars = buf_line_chars(&ed.buffer, row);
6101 ed.mutate_edit(Edit::InsertStr {
6102 at: Position::new(row, line_chars),
6103 text: "\n".to_string(),
6104 });
6105 }
6106 ed.push_buffer_cursor_to_textarea();
6107 let cursor = ed.cursor();
6108 ed.mutate_edit(Edit::InsertStr {
6109 at: Position::new(cursor.0, cursor.1),
6110 text: inserted,
6111 });
6112 }
6113 LastChange::InsertAt {
6114 entry,
6115 inserted,
6116 count,
6117 } => {
6118 use hjkl_buffer::{Edit, Position};
6119 ed.push_undo();
6120 match entry {
6121 InsertEntry::I => {}
6122 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6123 InsertEntry::A => {
6124 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6125 ed.push_buffer_cursor_to_textarea();
6126 }
6127 InsertEntry::ShiftA => {
6128 crate::motions::move_line_end(&mut ed.buffer);
6129 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6130 ed.push_buffer_cursor_to_textarea();
6131 }
6132 }
6133 for _ in 0..count.max(1) {
6134 let cursor = ed.cursor();
6135 ed.mutate_edit(Edit::InsertStr {
6136 at: Position::new(cursor.0, cursor.1),
6137 text: inserted.clone(),
6138 });
6139 }
6140 }
6141 }
6142 ed.vim.replaying = false;
6143}
6144
6145fn extract_inserted(before: &str, after: &str) -> String {
6148 let before_chars: Vec<char> = before.chars().collect();
6149 let after_chars: Vec<char> = after.chars().collect();
6150 if after_chars.len() <= before_chars.len() {
6151 return String::new();
6152 }
6153 let prefix = before_chars
6154 .iter()
6155 .zip(after_chars.iter())
6156 .take_while(|(a, b)| a == b)
6157 .count();
6158 let max_suffix = before_chars.len() - prefix;
6159 let suffix = before_chars
6160 .iter()
6161 .rev()
6162 .zip(after_chars.iter().rev())
6163 .take(max_suffix)
6164 .take_while(|(a, b)| a == b)
6165 .count();
6166 after_chars[prefix..after_chars.len() - suffix]
6167 .iter()
6168 .collect()
6169}
6170
6171#[cfg(all(test, feature = "crossterm"))]
6174mod tests {
6175 use crate::VimMode;
6176 use crate::editor::Editor;
6177 use crate::types::Host;
6178 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6179
6180 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6181 let mut iter = keys.chars().peekable();
6185 while let Some(c) = iter.next() {
6186 if c == '<' {
6187 let mut tag = String::new();
6188 for ch in iter.by_ref() {
6189 if ch == '>' {
6190 break;
6191 }
6192 tag.push(ch);
6193 }
6194 let ev = match tag.as_str() {
6195 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6196 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6197 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6198 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6199 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6200 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6201 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6202 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6203 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6207 s if s.starts_with("C-") => {
6208 let ch = s.chars().nth(2).unwrap();
6209 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6210 }
6211 _ => continue,
6212 };
6213 e.handle_key(ev);
6214 } else {
6215 let mods = if c.is_uppercase() {
6216 KeyModifiers::SHIFT
6217 } else {
6218 KeyModifiers::NONE
6219 };
6220 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6221 }
6222 }
6223 }
6224
6225 fn editor_with(content: &str) -> Editor {
6226 let opts = crate::types::Options {
6231 shiftwidth: 2,
6232 ..crate::types::Options::default()
6233 };
6234 let mut e = Editor::new(
6235 hjkl_buffer::Buffer::new(),
6236 crate::types::DefaultHost::new(),
6237 opts,
6238 );
6239 e.set_content(content);
6240 e
6241 }
6242
6243 #[test]
6244 fn f_char_jumps_on_line() {
6245 let mut e = editor_with("hello world");
6246 run_keys(&mut e, "fw");
6247 assert_eq!(e.cursor(), (0, 6));
6248 }
6249
6250 #[test]
6251 fn cap_f_jumps_backward() {
6252 let mut e = editor_with("hello world");
6253 e.jump_cursor(0, 10);
6254 run_keys(&mut e, "Fo");
6255 assert_eq!(e.cursor().1, 7);
6256 }
6257
6258 #[test]
6259 fn t_stops_before_char() {
6260 let mut e = editor_with("hello");
6261 run_keys(&mut e, "tl");
6262 assert_eq!(e.cursor(), (0, 1));
6263 }
6264
6265 #[test]
6266 fn semicolon_repeats_find() {
6267 let mut e = editor_with("aa.bb.cc");
6268 run_keys(&mut e, "f.");
6269 assert_eq!(e.cursor().1, 2);
6270 run_keys(&mut e, ";");
6271 assert_eq!(e.cursor().1, 5);
6272 }
6273
6274 #[test]
6275 fn comma_repeats_find_reverse() {
6276 let mut e = editor_with("aa.bb.cc");
6277 run_keys(&mut e, "f.");
6278 run_keys(&mut e, ";");
6279 run_keys(&mut e, ",");
6280 assert_eq!(e.cursor().1, 2);
6281 }
6282
6283 #[test]
6284 fn di_quote_deletes_content() {
6285 let mut e = editor_with("foo \"bar\" baz");
6286 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6288 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6289 }
6290
6291 #[test]
6292 fn da_quote_deletes_with_quotes() {
6293 let mut e = editor_with("foo \"bar\" baz");
6296 e.jump_cursor(0, 6);
6297 run_keys(&mut e, "da\"");
6298 assert_eq!(e.buffer().lines()[0], "foo baz");
6299 }
6300
6301 #[test]
6302 fn ci_paren_deletes_and_inserts() {
6303 let mut e = editor_with("fn(a, b, c)");
6304 e.jump_cursor(0, 5);
6305 run_keys(&mut e, "ci(");
6306 assert_eq!(e.vim_mode(), VimMode::Insert);
6307 assert_eq!(e.buffer().lines()[0], "fn()");
6308 }
6309
6310 #[test]
6311 fn diw_deletes_inner_word() {
6312 let mut e = editor_with("hello world");
6313 e.jump_cursor(0, 2);
6314 run_keys(&mut e, "diw");
6315 assert_eq!(e.buffer().lines()[0], " world");
6316 }
6317
6318 #[test]
6319 fn daw_deletes_word_with_trailing_space() {
6320 let mut e = editor_with("hello world");
6321 run_keys(&mut e, "daw");
6322 assert_eq!(e.buffer().lines()[0], "world");
6323 }
6324
6325 #[test]
6326 fn percent_jumps_to_matching_bracket() {
6327 let mut e = editor_with("foo(bar)");
6328 e.jump_cursor(0, 3);
6329 run_keys(&mut e, "%");
6330 assert_eq!(e.cursor().1, 7);
6331 run_keys(&mut e, "%");
6332 assert_eq!(e.cursor().1, 3);
6333 }
6334
6335 #[test]
6336 fn dot_repeats_last_change() {
6337 let mut e = editor_with("aaa bbb ccc");
6338 run_keys(&mut e, "dw");
6339 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6340 run_keys(&mut e, ".");
6341 assert_eq!(e.buffer().lines()[0], "ccc");
6342 }
6343
6344 #[test]
6345 fn dot_repeats_change_operator_with_text() {
6346 let mut e = editor_with("foo foo foo");
6347 run_keys(&mut e, "cwbar<Esc>");
6348 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6349 run_keys(&mut e, "w");
6351 run_keys(&mut e, ".");
6352 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6353 }
6354
6355 #[test]
6356 fn dot_repeats_x() {
6357 let mut e = editor_with("abcdef");
6358 run_keys(&mut e, "x");
6359 run_keys(&mut e, "..");
6360 assert_eq!(e.buffer().lines()[0], "def");
6361 }
6362
6363 #[test]
6364 fn count_operator_motion_compose() {
6365 let mut e = editor_with("one two three four five");
6366 run_keys(&mut e, "d3w");
6367 assert_eq!(e.buffer().lines()[0], "four five");
6368 }
6369
6370 #[test]
6371 fn two_dd_deletes_two_lines() {
6372 let mut e = editor_with("a\nb\nc");
6373 run_keys(&mut e, "2dd");
6374 assert_eq!(e.buffer().lines().len(), 1);
6375 assert_eq!(e.buffer().lines()[0], "c");
6376 }
6377
6378 #[test]
6383 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6384 let mut e = editor_with("one\ntwo\n three\nfour");
6385 e.jump_cursor(1, 2);
6386 run_keys(&mut e, "dd");
6387 assert_eq!(e.buffer().lines()[1], " three");
6389 assert_eq!(e.cursor(), (1, 4));
6390 }
6391
6392 #[test]
6393 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6394 let mut e = editor_with("one\n two\nthree");
6395 e.jump_cursor(2, 0);
6396 run_keys(&mut e, "dd");
6397 assert_eq!(e.buffer().lines().len(), 2);
6399 assert_eq!(e.cursor(), (1, 2));
6400 }
6401
6402 #[test]
6403 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6404 let mut e = editor_with("lonely");
6405 run_keys(&mut e, "dd");
6406 assert_eq!(e.buffer().lines().len(), 1);
6407 assert_eq!(e.buffer().lines()[0], "");
6408 assert_eq!(e.cursor(), (0, 0));
6409 }
6410
6411 #[test]
6412 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6413 let mut e = editor_with("a\nb\nc\n d\ne");
6414 e.jump_cursor(1, 0);
6416 run_keys(&mut e, "3dd");
6417 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6418 assert_eq!(e.cursor(), (1, 0));
6419 }
6420
6421 #[test]
6422 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6423 let mut e = editor_with(" line one\n line two\n xyz!");
6442 e.jump_cursor(0, 8);
6444 assert_eq!(e.cursor(), (0, 8));
6445 run_keys(&mut e, "dd");
6448 assert_eq!(
6449 e.cursor(),
6450 (0, 4),
6451 "dd must place cursor on first-non-blank"
6452 );
6453 run_keys(&mut e, "j");
6457 let (row, col) = e.cursor();
6458 assert_eq!(row, 1);
6459 assert_eq!(
6460 col, 4,
6461 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6462 );
6463 }
6464
6465 #[test]
6466 fn gu_lowercases_motion_range() {
6467 let mut e = editor_with("HELLO WORLD");
6468 run_keys(&mut e, "guw");
6469 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6470 assert_eq!(e.cursor(), (0, 0));
6471 }
6472
6473 #[test]
6474 fn g_u_uppercases_text_object() {
6475 let mut e = editor_with("hello world");
6476 run_keys(&mut e, "gUiw");
6478 assert_eq!(e.buffer().lines()[0], "HELLO world");
6479 assert_eq!(e.cursor(), (0, 0));
6480 }
6481
6482 #[test]
6483 fn g_tilde_toggles_case_of_range() {
6484 let mut e = editor_with("Hello World");
6485 run_keys(&mut e, "g~iw");
6486 assert_eq!(e.buffer().lines()[0], "hELLO World");
6487 }
6488
6489 #[test]
6490 fn g_uu_uppercases_current_line() {
6491 let mut e = editor_with("select 1\nselect 2");
6492 run_keys(&mut e, "gUU");
6493 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6494 assert_eq!(e.buffer().lines()[1], "select 2");
6495 }
6496
6497 #[test]
6498 fn gugu_lowercases_current_line() {
6499 let mut e = editor_with("FOO BAR\nBAZ");
6500 run_keys(&mut e, "gugu");
6501 assert_eq!(e.buffer().lines()[0], "foo bar");
6502 }
6503
6504 #[test]
6505 fn visual_u_uppercases_selection() {
6506 let mut e = editor_with("hello world");
6507 run_keys(&mut e, "veU");
6509 assert_eq!(e.buffer().lines()[0], "HELLO world");
6510 }
6511
6512 #[test]
6513 fn visual_line_u_lowercases_line() {
6514 let mut e = editor_with("HELLO WORLD\nOTHER");
6515 run_keys(&mut e, "Vu");
6516 assert_eq!(e.buffer().lines()[0], "hello world");
6517 assert_eq!(e.buffer().lines()[1], "OTHER");
6518 }
6519
6520 #[test]
6521 fn g_uu_with_count_uppercases_multiple_lines() {
6522 let mut e = editor_with("one\ntwo\nthree\nfour");
6523 run_keys(&mut e, "3gUU");
6525 assert_eq!(e.buffer().lines()[0], "ONE");
6526 assert_eq!(e.buffer().lines()[1], "TWO");
6527 assert_eq!(e.buffer().lines()[2], "THREE");
6528 assert_eq!(e.buffer().lines()[3], "four");
6529 }
6530
6531 #[test]
6532 fn double_gt_indents_current_line() {
6533 let mut e = editor_with("hello");
6534 run_keys(&mut e, ">>");
6535 assert_eq!(e.buffer().lines()[0], " hello");
6536 assert_eq!(e.cursor(), (0, 2));
6538 }
6539
6540 #[test]
6541 fn double_lt_outdents_current_line() {
6542 let mut e = editor_with(" hello");
6543 run_keys(&mut e, "<lt><lt>");
6544 assert_eq!(e.buffer().lines()[0], " hello");
6545 assert_eq!(e.cursor(), (0, 2));
6546 }
6547
6548 #[test]
6549 fn count_double_gt_indents_multiple_lines() {
6550 let mut e = editor_with("a\nb\nc\nd");
6551 run_keys(&mut e, "3>>");
6553 assert_eq!(e.buffer().lines()[0], " a");
6554 assert_eq!(e.buffer().lines()[1], " b");
6555 assert_eq!(e.buffer().lines()[2], " c");
6556 assert_eq!(e.buffer().lines()[3], "d");
6557 }
6558
6559 #[test]
6560 fn outdent_clips_ragged_leading_whitespace() {
6561 let mut e = editor_with(" x");
6564 run_keys(&mut e, "<lt><lt>");
6565 assert_eq!(e.buffer().lines()[0], "x");
6566 }
6567
6568 #[test]
6569 fn indent_motion_is_always_linewise() {
6570 let mut e = editor_with("foo bar");
6573 run_keys(&mut e, ">w");
6574 assert_eq!(e.buffer().lines()[0], " foo bar");
6575 }
6576
6577 #[test]
6578 fn indent_text_object_extends_over_paragraph() {
6579 let mut e = editor_with("a\nb\n\nc\nd");
6580 run_keys(&mut e, ">ap");
6582 assert_eq!(e.buffer().lines()[0], " a");
6583 assert_eq!(e.buffer().lines()[1], " b");
6584 assert_eq!(e.buffer().lines()[2], "");
6585 assert_eq!(e.buffer().lines()[3], "c");
6586 }
6587
6588 #[test]
6589 fn visual_line_indent_shifts_selected_rows() {
6590 let mut e = editor_with("x\ny\nz");
6591 run_keys(&mut e, "Vj>");
6593 assert_eq!(e.buffer().lines()[0], " x");
6594 assert_eq!(e.buffer().lines()[1], " y");
6595 assert_eq!(e.buffer().lines()[2], "z");
6596 }
6597
6598 #[test]
6599 fn outdent_empty_line_is_noop() {
6600 let mut e = editor_with("\nfoo");
6601 run_keys(&mut e, "<lt><lt>");
6602 assert_eq!(e.buffer().lines()[0], "");
6603 }
6604
6605 #[test]
6606 fn indent_skips_empty_lines() {
6607 let mut e = editor_with("");
6610 run_keys(&mut e, ">>");
6611 assert_eq!(e.buffer().lines()[0], "");
6612 }
6613
6614 #[test]
6615 fn insert_ctrl_t_indents_current_line() {
6616 let mut e = editor_with("x");
6617 run_keys(&mut e, "i<C-t>");
6619 assert_eq!(e.buffer().lines()[0], " x");
6620 assert_eq!(e.cursor(), (0, 2));
6623 }
6624
6625 #[test]
6626 fn insert_ctrl_d_outdents_current_line() {
6627 let mut e = editor_with(" x");
6628 run_keys(&mut e, "A<C-d>");
6630 assert_eq!(e.buffer().lines()[0], " x");
6631 }
6632
6633 #[test]
6634 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6635 let mut e = editor_with("first\nsecond");
6636 e.jump_cursor(1, 0);
6637 run_keys(&mut e, "h");
6638 assert_eq!(e.cursor(), (1, 0));
6640 }
6641
6642 #[test]
6643 fn l_at_last_char_does_not_wrap_to_next_line() {
6644 let mut e = editor_with("ab\ncd");
6645 e.jump_cursor(0, 1);
6647 run_keys(&mut e, "l");
6648 assert_eq!(e.cursor(), (0, 1));
6650 }
6651
6652 #[test]
6653 fn count_l_clamps_at_line_end() {
6654 let mut e = editor_with("abcde");
6655 run_keys(&mut e, "20l");
6658 assert_eq!(e.cursor(), (0, 4));
6659 }
6660
6661 #[test]
6662 fn count_h_clamps_at_col_zero() {
6663 let mut e = editor_with("abcde");
6664 e.jump_cursor(0, 3);
6665 run_keys(&mut e, "20h");
6666 assert_eq!(e.cursor(), (0, 0));
6667 }
6668
6669 #[test]
6670 fn dl_on_last_char_still_deletes_it() {
6671 let mut e = editor_with("ab");
6675 e.jump_cursor(0, 1);
6676 run_keys(&mut e, "dl");
6677 assert_eq!(e.buffer().lines()[0], "a");
6678 }
6679
6680 #[test]
6681 fn case_op_preserves_yank_register() {
6682 let mut e = editor_with("target");
6683 run_keys(&mut e, "yy");
6684 let yank_before = e.yank().to_string();
6685 run_keys(&mut e, "gUU");
6687 assert_eq!(e.buffer().lines()[0], "TARGET");
6688 assert_eq!(
6689 e.yank(),
6690 yank_before,
6691 "case ops must preserve the yank buffer"
6692 );
6693 }
6694
6695 #[test]
6696 fn dap_deletes_paragraph() {
6697 let mut e = editor_with("a\nb\n\nc\nd");
6698 run_keys(&mut e, "dap");
6699 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6700 }
6701
6702 #[test]
6703 fn dit_deletes_inner_tag_content() {
6704 let mut e = editor_with("<b>hello</b>");
6705 e.jump_cursor(0, 4);
6707 run_keys(&mut e, "dit");
6708 assert_eq!(e.buffer().lines()[0], "<b></b>");
6709 }
6710
6711 #[test]
6712 fn dat_deletes_around_tag() {
6713 let mut e = editor_with("hi <b>foo</b> bye");
6714 e.jump_cursor(0, 6);
6715 run_keys(&mut e, "dat");
6716 assert_eq!(e.buffer().lines()[0], "hi bye");
6717 }
6718
6719 #[test]
6720 fn dit_picks_innermost_tag() {
6721 let mut e = editor_with("<a><b>x</b></a>");
6722 e.jump_cursor(0, 6);
6724 run_keys(&mut e, "dit");
6725 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6727 }
6728
6729 #[test]
6730 fn dat_innermost_tag_pair() {
6731 let mut e = editor_with("<a><b>x</b></a>");
6732 e.jump_cursor(0, 6);
6733 run_keys(&mut e, "dat");
6734 assert_eq!(e.buffer().lines()[0], "<a></a>");
6735 }
6736
6737 #[test]
6738 fn dit_outside_any_tag_no_op() {
6739 let mut e = editor_with("plain text");
6740 e.jump_cursor(0, 3);
6741 run_keys(&mut e, "dit");
6742 assert_eq!(e.buffer().lines()[0], "plain text");
6744 }
6745
6746 #[test]
6747 fn cit_changes_inner_tag_content() {
6748 let mut e = editor_with("<b>hello</b>");
6749 e.jump_cursor(0, 4);
6750 run_keys(&mut e, "citNEW<Esc>");
6751 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6752 }
6753
6754 #[test]
6755 fn cat_changes_around_tag() {
6756 let mut e = editor_with("hi <b>foo</b> bye");
6757 e.jump_cursor(0, 6);
6758 run_keys(&mut e, "catBAR<Esc>");
6759 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6760 }
6761
6762 #[test]
6763 fn yit_yanks_inner_tag_content() {
6764 let mut e = editor_with("<b>hello</b>");
6765 e.jump_cursor(0, 4);
6766 run_keys(&mut e, "yit");
6767 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6768 }
6769
6770 #[test]
6771 fn yat_yanks_full_tag_pair() {
6772 let mut e = editor_with("hi <b>foo</b> bye");
6773 e.jump_cursor(0, 6);
6774 run_keys(&mut e, "yat");
6775 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6776 }
6777
6778 #[test]
6779 fn vit_visually_selects_inner_tag() {
6780 let mut e = editor_with("<b>hello</b>");
6781 e.jump_cursor(0, 4);
6782 run_keys(&mut e, "vit");
6783 assert_eq!(e.vim_mode(), VimMode::Visual);
6784 run_keys(&mut e, "y");
6785 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6786 }
6787
6788 #[test]
6789 fn vat_visually_selects_around_tag() {
6790 let mut e = editor_with("x<b>foo</b>y");
6791 e.jump_cursor(0, 5);
6792 run_keys(&mut e, "vat");
6793 assert_eq!(e.vim_mode(), VimMode::Visual);
6794 run_keys(&mut e, "y");
6795 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6796 }
6797
6798 #[test]
6801 #[allow(non_snake_case)]
6802 fn diW_deletes_inner_big_word() {
6803 let mut e = editor_with("foo.bar baz");
6804 e.jump_cursor(0, 2);
6805 run_keys(&mut e, "diW");
6806 assert_eq!(e.buffer().lines()[0], " baz");
6808 }
6809
6810 #[test]
6811 #[allow(non_snake_case)]
6812 fn daW_deletes_around_big_word() {
6813 let mut e = editor_with("foo.bar baz");
6814 e.jump_cursor(0, 2);
6815 run_keys(&mut e, "daW");
6816 assert_eq!(e.buffer().lines()[0], "baz");
6817 }
6818
6819 #[test]
6820 fn di_double_quote_deletes_inside() {
6821 let mut e = editor_with("a \"hello\" b");
6822 e.jump_cursor(0, 4);
6823 run_keys(&mut e, "di\"");
6824 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6825 }
6826
6827 #[test]
6828 fn da_double_quote_deletes_around() {
6829 let mut e = editor_with("a \"hello\" b");
6831 e.jump_cursor(0, 4);
6832 run_keys(&mut e, "da\"");
6833 assert_eq!(e.buffer().lines()[0], "a b");
6834 }
6835
6836 #[test]
6837 fn di_single_quote_deletes_inside() {
6838 let mut e = editor_with("x 'foo' y");
6839 e.jump_cursor(0, 4);
6840 run_keys(&mut e, "di'");
6841 assert_eq!(e.buffer().lines()[0], "x '' y");
6842 }
6843
6844 #[test]
6845 fn da_single_quote_deletes_around() {
6846 let mut e = editor_with("x 'foo' y");
6848 e.jump_cursor(0, 4);
6849 run_keys(&mut e, "da'");
6850 assert_eq!(e.buffer().lines()[0], "x y");
6851 }
6852
6853 #[test]
6854 fn di_backtick_deletes_inside() {
6855 let mut e = editor_with("p `q` r");
6856 e.jump_cursor(0, 3);
6857 run_keys(&mut e, "di`");
6858 assert_eq!(e.buffer().lines()[0], "p `` r");
6859 }
6860
6861 #[test]
6862 fn da_backtick_deletes_around() {
6863 let mut e = editor_with("p `q` r");
6865 e.jump_cursor(0, 3);
6866 run_keys(&mut e, "da`");
6867 assert_eq!(e.buffer().lines()[0], "p r");
6868 }
6869
6870 #[test]
6871 fn di_paren_deletes_inside() {
6872 let mut e = editor_with("f(arg)");
6873 e.jump_cursor(0, 3);
6874 run_keys(&mut e, "di(");
6875 assert_eq!(e.buffer().lines()[0], "f()");
6876 }
6877
6878 #[test]
6879 fn di_paren_alias_b_works() {
6880 let mut e = editor_with("f(arg)");
6881 e.jump_cursor(0, 3);
6882 run_keys(&mut e, "dib");
6883 assert_eq!(e.buffer().lines()[0], "f()");
6884 }
6885
6886 #[test]
6887 fn di_bracket_deletes_inside() {
6888 let mut e = editor_with("a[b,c]d");
6889 e.jump_cursor(0, 3);
6890 run_keys(&mut e, "di[");
6891 assert_eq!(e.buffer().lines()[0], "a[]d");
6892 }
6893
6894 #[test]
6895 fn da_bracket_deletes_around() {
6896 let mut e = editor_with("a[b,c]d");
6897 e.jump_cursor(0, 3);
6898 run_keys(&mut e, "da[");
6899 assert_eq!(e.buffer().lines()[0], "ad");
6900 }
6901
6902 #[test]
6903 fn di_brace_deletes_inside() {
6904 let mut e = editor_with("x{y}z");
6905 e.jump_cursor(0, 2);
6906 run_keys(&mut e, "di{");
6907 assert_eq!(e.buffer().lines()[0], "x{}z");
6908 }
6909
6910 #[test]
6911 fn da_brace_deletes_around() {
6912 let mut e = editor_with("x{y}z");
6913 e.jump_cursor(0, 2);
6914 run_keys(&mut e, "da{");
6915 assert_eq!(e.buffer().lines()[0], "xz");
6916 }
6917
6918 #[test]
6919 fn di_brace_alias_capital_b_works() {
6920 let mut e = editor_with("x{y}z");
6921 e.jump_cursor(0, 2);
6922 run_keys(&mut e, "diB");
6923 assert_eq!(e.buffer().lines()[0], "x{}z");
6924 }
6925
6926 #[test]
6927 fn di_angle_deletes_inside() {
6928 let mut e = editor_with("p<q>r");
6929 e.jump_cursor(0, 2);
6930 run_keys(&mut e, "di<lt>");
6932 assert_eq!(e.buffer().lines()[0], "p<>r");
6933 }
6934
6935 #[test]
6936 fn da_angle_deletes_around() {
6937 let mut e = editor_with("p<q>r");
6938 e.jump_cursor(0, 2);
6939 run_keys(&mut e, "da<lt>");
6940 assert_eq!(e.buffer().lines()[0], "pr");
6941 }
6942
6943 #[test]
6944 fn dip_deletes_inner_paragraph() {
6945 let mut e = editor_with("a\nb\nc\n\nd");
6946 e.jump_cursor(1, 0);
6947 run_keys(&mut e, "dip");
6948 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6951 }
6952
6953 #[test]
6956 fn sentence_motion_close_paren_jumps_forward() {
6957 let mut e = editor_with("Alpha. Beta. Gamma.");
6958 e.jump_cursor(0, 0);
6959 run_keys(&mut e, ")");
6960 assert_eq!(e.cursor(), (0, 7));
6962 run_keys(&mut e, ")");
6963 assert_eq!(e.cursor(), (0, 13));
6964 }
6965
6966 #[test]
6967 fn sentence_motion_open_paren_jumps_backward() {
6968 let mut e = editor_with("Alpha. Beta. Gamma.");
6969 e.jump_cursor(0, 13);
6970 run_keys(&mut e, "(");
6971 assert_eq!(e.cursor(), (0, 7));
6974 run_keys(&mut e, "(");
6975 assert_eq!(e.cursor(), (0, 0));
6976 }
6977
6978 #[test]
6979 fn sentence_motion_count() {
6980 let mut e = editor_with("A. B. C. D.");
6981 e.jump_cursor(0, 0);
6982 run_keys(&mut e, "3)");
6983 assert_eq!(e.cursor(), (0, 9));
6985 }
6986
6987 #[test]
6988 fn dis_deletes_inner_sentence() {
6989 let mut e = editor_with("First one. Second one. Third one.");
6990 e.jump_cursor(0, 13);
6991 run_keys(&mut e, "dis");
6992 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6994 }
6995
6996 #[test]
6997 fn das_deletes_around_sentence_with_trailing_space() {
6998 let mut e = editor_with("Alpha. Beta. Gamma.");
6999 e.jump_cursor(0, 8);
7000 run_keys(&mut e, "das");
7001 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
7004 }
7005
7006 #[test]
7007 fn dis_handles_double_terminator() {
7008 let mut e = editor_with("Wow!? Next.");
7009 e.jump_cursor(0, 1);
7010 run_keys(&mut e, "dis");
7011 assert_eq!(e.buffer().lines()[0], " Next.");
7014 }
7015
7016 #[test]
7017 fn dis_first_sentence_from_cursor_at_zero() {
7018 let mut e = editor_with("Alpha. Beta.");
7019 e.jump_cursor(0, 0);
7020 run_keys(&mut e, "dis");
7021 assert_eq!(e.buffer().lines()[0], " Beta.");
7022 }
7023
7024 #[test]
7025 fn yis_yanks_inner_sentence() {
7026 let mut e = editor_with("Hello world. Bye.");
7027 e.jump_cursor(0, 5);
7028 run_keys(&mut e, "yis");
7029 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
7030 }
7031
7032 #[test]
7033 fn vis_visually_selects_inner_sentence() {
7034 let mut e = editor_with("First. Second.");
7035 e.jump_cursor(0, 1);
7036 run_keys(&mut e, "vis");
7037 assert_eq!(e.vim_mode(), VimMode::Visual);
7038 run_keys(&mut e, "y");
7039 assert_eq!(e.registers().read('"').unwrap().text, "First.");
7040 }
7041
7042 #[test]
7043 fn ciw_changes_inner_word() {
7044 let mut e = editor_with("hello world");
7045 e.jump_cursor(0, 1);
7046 run_keys(&mut e, "ciwHEY<Esc>");
7047 assert_eq!(e.buffer().lines()[0], "HEY world");
7048 }
7049
7050 #[test]
7051 fn yiw_yanks_inner_word() {
7052 let mut e = editor_with("hello world");
7053 e.jump_cursor(0, 1);
7054 run_keys(&mut e, "yiw");
7055 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7056 }
7057
7058 #[test]
7059 fn viw_selects_inner_word() {
7060 let mut e = editor_with("hello world");
7061 e.jump_cursor(0, 2);
7062 run_keys(&mut e, "viw");
7063 assert_eq!(e.vim_mode(), VimMode::Visual);
7064 run_keys(&mut e, "y");
7065 assert_eq!(e.registers().read('"').unwrap().text, "hello");
7066 }
7067
7068 #[test]
7069 fn ci_paren_changes_inside() {
7070 let mut e = editor_with("f(old)");
7071 e.jump_cursor(0, 3);
7072 run_keys(&mut e, "ci(NEW<Esc>");
7073 assert_eq!(e.buffer().lines()[0], "f(NEW)");
7074 }
7075
7076 #[test]
7077 fn yi_double_quote_yanks_inside() {
7078 let mut e = editor_with("say \"hi there\" then");
7079 e.jump_cursor(0, 6);
7080 run_keys(&mut e, "yi\"");
7081 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
7082 }
7083
7084 #[test]
7085 fn vap_visual_selects_around_paragraph() {
7086 let mut e = editor_with("a\nb\n\nc");
7087 e.jump_cursor(0, 0);
7088 run_keys(&mut e, "vap");
7089 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7090 run_keys(&mut e, "y");
7091 let text = e.registers().read('"').unwrap().text.clone();
7093 assert!(text.starts_with("a\nb"));
7094 }
7095
7096 #[test]
7097 fn star_finds_next_occurrence() {
7098 let mut e = editor_with("foo bar foo baz");
7099 run_keys(&mut e, "*");
7100 assert_eq!(e.cursor().1, 8);
7101 }
7102
7103 #[test]
7104 fn star_skips_substring_match() {
7105 let mut e = editor_with("foo foobar baz");
7108 run_keys(&mut e, "*");
7109 assert_eq!(e.cursor().1, 0);
7110 }
7111
7112 #[test]
7113 fn g_star_matches_substring() {
7114 let mut e = editor_with("foo foobar baz");
7117 run_keys(&mut e, "g*");
7118 assert_eq!(e.cursor().1, 4);
7119 }
7120
7121 #[test]
7122 fn g_pound_matches_substring_backward() {
7123 let mut e = editor_with("foo foobar baz foo");
7126 run_keys(&mut e, "$b");
7127 assert_eq!(e.cursor().1, 15);
7128 run_keys(&mut e, "g#");
7129 assert_eq!(e.cursor().1, 4);
7130 }
7131
7132 #[test]
7133 fn n_repeats_last_search_forward() {
7134 let mut e = editor_with("foo bar foo baz foo");
7135 run_keys(&mut e, "/foo<CR>");
7138 assert_eq!(e.cursor().1, 8);
7139 run_keys(&mut e, "n");
7140 assert_eq!(e.cursor().1, 16);
7141 }
7142
7143 #[test]
7144 fn shift_n_reverses_search() {
7145 let mut e = editor_with("foo bar foo baz foo");
7146 run_keys(&mut e, "/foo<CR>");
7147 run_keys(&mut e, "n");
7148 assert_eq!(e.cursor().1, 16);
7149 run_keys(&mut e, "N");
7150 assert_eq!(e.cursor().1, 8);
7151 }
7152
7153 #[test]
7154 fn n_noop_without_pattern() {
7155 let mut e = editor_with("foo bar");
7156 run_keys(&mut e, "n");
7157 assert_eq!(e.cursor(), (0, 0));
7158 }
7159
7160 #[test]
7161 fn visual_line_preserves_cursor_column() {
7162 let mut e = editor_with("hello world\nanother one\nbye");
7165 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7167 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7168 assert_eq!(e.cursor(), (0, 5));
7169 run_keys(&mut e, "j");
7170 assert_eq!(e.cursor(), (1, 5));
7171 }
7172
7173 #[test]
7174 fn visual_line_yank_includes_trailing_newline() {
7175 let mut e = editor_with("aaa\nbbb\nccc");
7176 run_keys(&mut e, "Vjy");
7177 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7179 }
7180
7181 #[test]
7182 fn visual_line_yank_last_line_trailing_newline() {
7183 let mut e = editor_with("aaa\nbbb\nccc");
7184 run_keys(&mut e, "jj");
7186 run_keys(&mut e, "Vy");
7187 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7188 }
7189
7190 #[test]
7191 fn yy_on_last_line_has_trailing_newline() {
7192 let mut e = editor_with("aaa\nbbb\nccc");
7193 run_keys(&mut e, "jj");
7194 run_keys(&mut e, "yy");
7195 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7196 }
7197
7198 #[test]
7199 fn yy_in_middle_has_trailing_newline() {
7200 let mut e = editor_with("aaa\nbbb\nccc");
7201 run_keys(&mut e, "j");
7202 run_keys(&mut e, "yy");
7203 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7204 }
7205
7206 #[test]
7207 fn di_single_quote() {
7208 let mut e = editor_with("say 'hello world' now");
7209 e.jump_cursor(0, 7);
7210 run_keys(&mut e, "di'");
7211 assert_eq!(e.buffer().lines()[0], "say '' now");
7212 }
7213
7214 #[test]
7215 fn da_single_quote() {
7216 let mut e = editor_with("say 'hello' now");
7218 e.jump_cursor(0, 7);
7219 run_keys(&mut e, "da'");
7220 assert_eq!(e.buffer().lines()[0], "say now");
7221 }
7222
7223 #[test]
7224 fn di_backtick() {
7225 let mut e = editor_with("say `hi` now");
7226 e.jump_cursor(0, 5);
7227 run_keys(&mut e, "di`");
7228 assert_eq!(e.buffer().lines()[0], "say `` now");
7229 }
7230
7231 #[test]
7232 fn di_brace() {
7233 let mut e = editor_with("fn { a; b; c }");
7234 e.jump_cursor(0, 7);
7235 run_keys(&mut e, "di{");
7236 assert_eq!(e.buffer().lines()[0], "fn {}");
7237 }
7238
7239 #[test]
7240 fn di_bracket() {
7241 let mut e = editor_with("arr[1, 2, 3]");
7242 e.jump_cursor(0, 5);
7243 run_keys(&mut e, "di[");
7244 assert_eq!(e.buffer().lines()[0], "arr[]");
7245 }
7246
7247 #[test]
7248 fn dab_deletes_around_paren() {
7249 let mut e = editor_with("fn(a, b) + 1");
7250 e.jump_cursor(0, 4);
7251 run_keys(&mut e, "dab");
7252 assert_eq!(e.buffer().lines()[0], "fn + 1");
7253 }
7254
7255 #[test]
7256 fn da_big_b_deletes_around_brace() {
7257 let mut e = editor_with("x = {a: 1}");
7258 e.jump_cursor(0, 6);
7259 run_keys(&mut e, "daB");
7260 assert_eq!(e.buffer().lines()[0], "x = ");
7261 }
7262
7263 #[test]
7264 fn di_big_w_deletes_bigword() {
7265 let mut e = editor_with("foo-bar baz");
7266 e.jump_cursor(0, 2);
7267 run_keys(&mut e, "diW");
7268 assert_eq!(e.buffer().lines()[0], " baz");
7269 }
7270
7271 #[test]
7272 fn visual_select_inner_word() {
7273 let mut e = editor_with("hello world");
7274 e.jump_cursor(0, 2);
7275 run_keys(&mut e, "viw");
7276 assert_eq!(e.vim_mode(), VimMode::Visual);
7277 run_keys(&mut e, "y");
7278 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7279 }
7280
7281 #[test]
7282 fn visual_select_inner_quote() {
7283 let mut e = editor_with("foo \"bar\" baz");
7284 e.jump_cursor(0, 6);
7285 run_keys(&mut e, "vi\"");
7286 run_keys(&mut e, "y");
7287 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7288 }
7289
7290 #[test]
7291 fn visual_select_inner_paren() {
7292 let mut e = editor_with("fn(a, b)");
7293 e.jump_cursor(0, 4);
7294 run_keys(&mut e, "vi(");
7295 run_keys(&mut e, "y");
7296 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7297 }
7298
7299 #[test]
7300 fn visual_select_outer_brace() {
7301 let mut e = editor_with("{x}");
7302 e.jump_cursor(0, 1);
7303 run_keys(&mut e, "va{");
7304 run_keys(&mut e, "y");
7305 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7306 }
7307
7308 #[test]
7309 fn ci_paren_forward_scans_when_cursor_before_pair() {
7310 let mut e = editor_with("foo(bar)");
7313 e.jump_cursor(0, 0);
7314 run_keys(&mut e, "ci(NEW<Esc>");
7315 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7316 }
7317
7318 #[test]
7319 fn ci_paren_forward_scans_across_lines() {
7320 let mut e = editor_with("first\nfoo(bar)\nlast");
7321 e.jump_cursor(0, 0);
7322 run_keys(&mut e, "ci(NEW<Esc>");
7323 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7324 }
7325
7326 #[test]
7327 fn ci_brace_forward_scans_when_cursor_before_pair() {
7328 let mut e = editor_with("let x = {y};");
7329 e.jump_cursor(0, 0);
7330 run_keys(&mut e, "ci{NEW<Esc>");
7331 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7332 }
7333
7334 #[test]
7335 fn cit_forward_scans_when_cursor_before_tag() {
7336 let mut e = editor_with("text <b>hello</b> rest");
7339 e.jump_cursor(0, 0);
7340 run_keys(&mut e, "citNEW<Esc>");
7341 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7342 }
7343
7344 #[test]
7345 fn dat_forward_scans_when_cursor_before_tag() {
7346 let mut e = editor_with("text <b>hello</b> rest");
7348 e.jump_cursor(0, 0);
7349 run_keys(&mut e, "dat");
7350 assert_eq!(e.buffer().lines()[0], "text rest");
7351 }
7352
7353 #[test]
7354 fn ci_paren_still_works_when_cursor_inside() {
7355 let mut e = editor_with("fn(a, b)");
7358 e.jump_cursor(0, 4);
7359 run_keys(&mut e, "ci(NEW<Esc>");
7360 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7361 }
7362
7363 #[test]
7364 fn caw_changes_word_with_trailing_space() {
7365 let mut e = editor_with("hello world");
7366 run_keys(&mut e, "cawfoo<Esc>");
7367 assert_eq!(e.buffer().lines()[0], "fooworld");
7368 }
7369
7370 #[test]
7371 fn visual_char_yank_preserves_raw_text() {
7372 let mut e = editor_with("hello world");
7373 run_keys(&mut e, "vllly");
7374 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7375 }
7376
7377 #[test]
7378 fn single_line_visual_line_selects_full_line_on_yank() {
7379 let mut e = editor_with("hello world\nbye");
7380 run_keys(&mut e, "V");
7381 run_keys(&mut e, "y");
7384 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7385 }
7386
7387 #[test]
7388 fn visual_line_extends_both_directions() {
7389 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7390 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7392 assert_eq!(e.cursor(), (3, 0));
7393 run_keys(&mut e, "k");
7394 assert_eq!(e.cursor(), (2, 0));
7396 run_keys(&mut e, "k");
7397 assert_eq!(e.cursor(), (1, 0));
7398 }
7399
7400 #[test]
7401 fn visual_char_preserves_cursor_column() {
7402 let mut e = editor_with("hello world");
7403 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7405 assert_eq!(e.cursor(), (0, 5));
7406 run_keys(&mut e, "ll");
7407 assert_eq!(e.cursor(), (0, 7));
7408 }
7409
7410 #[test]
7411 fn visual_char_highlight_bounds_order() {
7412 let mut e = editor_with("abcdef");
7413 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7415 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7418 }
7419
7420 #[test]
7421 fn visual_line_highlight_bounds() {
7422 let mut e = editor_with("a\nb\nc");
7423 run_keys(&mut e, "V");
7424 assert_eq!(e.line_highlight(), Some((0, 0)));
7425 run_keys(&mut e, "j");
7426 assert_eq!(e.line_highlight(), Some((0, 1)));
7427 run_keys(&mut e, "j");
7428 assert_eq!(e.line_highlight(), Some((0, 2)));
7429 }
7430
7431 #[test]
7434 fn h_moves_left() {
7435 let mut e = editor_with("hello");
7436 e.jump_cursor(0, 3);
7437 run_keys(&mut e, "h");
7438 assert_eq!(e.cursor(), (0, 2));
7439 }
7440
7441 #[test]
7442 fn l_moves_right() {
7443 let mut e = editor_with("hello");
7444 run_keys(&mut e, "l");
7445 assert_eq!(e.cursor(), (0, 1));
7446 }
7447
7448 #[test]
7449 fn k_moves_up() {
7450 let mut e = editor_with("a\nb\nc");
7451 e.jump_cursor(2, 0);
7452 run_keys(&mut e, "k");
7453 assert_eq!(e.cursor(), (1, 0));
7454 }
7455
7456 #[test]
7457 fn zero_moves_to_line_start() {
7458 let mut e = editor_with(" hello");
7459 run_keys(&mut e, "$");
7460 run_keys(&mut e, "0");
7461 assert_eq!(e.cursor().1, 0);
7462 }
7463
7464 #[test]
7465 fn caret_moves_to_first_non_blank() {
7466 let mut e = editor_with(" hello");
7467 run_keys(&mut e, "0");
7468 run_keys(&mut e, "^");
7469 assert_eq!(e.cursor().1, 4);
7470 }
7471
7472 #[test]
7473 fn dollar_moves_to_last_char() {
7474 let mut e = editor_with("hello");
7475 run_keys(&mut e, "$");
7476 assert_eq!(e.cursor().1, 4);
7477 }
7478
7479 #[test]
7480 fn dollar_on_empty_line_stays_at_col_zero() {
7481 let mut e = editor_with("");
7482 run_keys(&mut e, "$");
7483 assert_eq!(e.cursor().1, 0);
7484 }
7485
7486 #[test]
7487 fn w_jumps_to_next_word() {
7488 let mut e = editor_with("foo bar baz");
7489 run_keys(&mut e, "w");
7490 assert_eq!(e.cursor().1, 4);
7491 }
7492
7493 #[test]
7494 fn b_jumps_back_a_word() {
7495 let mut e = editor_with("foo bar");
7496 e.jump_cursor(0, 6);
7497 run_keys(&mut e, "b");
7498 assert_eq!(e.cursor().1, 4);
7499 }
7500
7501 #[test]
7502 fn e_jumps_to_word_end() {
7503 let mut e = editor_with("foo bar");
7504 run_keys(&mut e, "e");
7505 assert_eq!(e.cursor().1, 2);
7506 }
7507
7508 #[test]
7511 fn d_dollar_deletes_to_eol() {
7512 let mut e = editor_with("hello world");
7513 e.jump_cursor(0, 5);
7514 run_keys(&mut e, "d$");
7515 assert_eq!(e.buffer().lines()[0], "hello");
7516 }
7517
7518 #[test]
7519 fn d_zero_deletes_to_line_start() {
7520 let mut e = editor_with("hello world");
7521 e.jump_cursor(0, 6);
7522 run_keys(&mut e, "d0");
7523 assert_eq!(e.buffer().lines()[0], "world");
7524 }
7525
7526 #[test]
7527 fn d_caret_deletes_to_first_non_blank() {
7528 let mut e = editor_with(" hello");
7529 e.jump_cursor(0, 6);
7530 run_keys(&mut e, "d^");
7531 assert_eq!(e.buffer().lines()[0], " llo");
7532 }
7533
7534 #[test]
7535 fn d_capital_g_deletes_to_end_of_file() {
7536 let mut e = editor_with("a\nb\nc\nd");
7537 e.jump_cursor(1, 0);
7538 run_keys(&mut e, "dG");
7539 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7540 }
7541
7542 #[test]
7543 fn d_gg_deletes_to_start_of_file() {
7544 let mut e = editor_with("a\nb\nc\nd");
7545 e.jump_cursor(2, 0);
7546 run_keys(&mut e, "dgg");
7547 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7548 }
7549
7550 #[test]
7551 fn cw_is_ce_quirk() {
7552 let mut e = editor_with("foo bar");
7555 run_keys(&mut e, "cwxyz<Esc>");
7556 assert_eq!(e.buffer().lines()[0], "xyz bar");
7557 }
7558
7559 #[test]
7562 fn big_d_deletes_to_eol() {
7563 let mut e = editor_with("hello world");
7564 e.jump_cursor(0, 5);
7565 run_keys(&mut e, "D");
7566 assert_eq!(e.buffer().lines()[0], "hello");
7567 }
7568
7569 #[test]
7570 fn big_c_deletes_to_eol_and_inserts() {
7571 let mut e = editor_with("hello world");
7572 e.jump_cursor(0, 5);
7573 run_keys(&mut e, "C!<Esc>");
7574 assert_eq!(e.buffer().lines()[0], "hello!");
7575 }
7576
7577 #[test]
7578 fn j_joins_next_line_with_space() {
7579 let mut e = editor_with("hello\nworld");
7580 run_keys(&mut e, "J");
7581 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7582 }
7583
7584 #[test]
7585 fn j_strips_leading_whitespace_on_join() {
7586 let mut e = editor_with("hello\n world");
7587 run_keys(&mut e, "J");
7588 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7589 }
7590
7591 #[test]
7592 fn big_x_deletes_char_before_cursor() {
7593 let mut e = editor_with("hello");
7594 e.jump_cursor(0, 3);
7595 run_keys(&mut e, "X");
7596 assert_eq!(e.buffer().lines()[0], "helo");
7597 }
7598
7599 #[test]
7600 fn s_substitutes_char_and_enters_insert() {
7601 let mut e = editor_with("hello");
7602 run_keys(&mut e, "sX<Esc>");
7603 assert_eq!(e.buffer().lines()[0], "Xello");
7604 }
7605
7606 #[test]
7607 fn count_x_deletes_many() {
7608 let mut e = editor_with("abcdef");
7609 run_keys(&mut e, "3x");
7610 assert_eq!(e.buffer().lines()[0], "def");
7611 }
7612
7613 #[test]
7616 fn p_pastes_charwise_after_cursor() {
7617 let mut e = editor_with("hello");
7618 run_keys(&mut e, "yw");
7619 run_keys(&mut e, "$p");
7620 assert_eq!(e.buffer().lines()[0], "hellohello");
7621 }
7622
7623 #[test]
7624 fn capital_p_pastes_charwise_before_cursor() {
7625 let mut e = editor_with("hello");
7626 run_keys(&mut e, "v");
7628 run_keys(&mut e, "l");
7629 run_keys(&mut e, "y");
7630 run_keys(&mut e, "$P");
7631 assert_eq!(e.buffer().lines()[0], "hellheo");
7634 }
7635
7636 #[test]
7637 fn p_pastes_linewise_below() {
7638 let mut e = editor_with("one\ntwo\nthree");
7639 run_keys(&mut e, "yy");
7640 run_keys(&mut e, "p");
7641 assert_eq!(
7642 e.buffer().lines(),
7643 &[
7644 "one".to_string(),
7645 "one".to_string(),
7646 "two".to_string(),
7647 "three".to_string()
7648 ]
7649 );
7650 }
7651
7652 #[test]
7653 fn capital_p_pastes_linewise_above() {
7654 let mut e = editor_with("one\ntwo");
7655 e.jump_cursor(1, 0);
7656 run_keys(&mut e, "yy");
7657 run_keys(&mut e, "P");
7658 assert_eq!(
7659 e.buffer().lines(),
7660 &["one".to_string(), "two".to_string(), "two".to_string()]
7661 );
7662 }
7663
7664 #[test]
7667 fn hash_finds_previous_occurrence() {
7668 let mut e = editor_with("foo bar foo baz foo");
7669 e.jump_cursor(0, 16);
7671 run_keys(&mut e, "#");
7672 assert_eq!(e.cursor().1, 8);
7673 }
7674
7675 #[test]
7678 fn visual_line_delete_removes_full_lines() {
7679 let mut e = editor_with("a\nb\nc\nd");
7680 run_keys(&mut e, "Vjd");
7681 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7682 }
7683
7684 #[test]
7685 fn visual_line_change_leaves_blank_line() {
7686 let mut e = editor_with("a\nb\nc");
7687 run_keys(&mut e, "Vjc");
7688 assert_eq!(e.vim_mode(), VimMode::Insert);
7689 run_keys(&mut e, "X<Esc>");
7690 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7694 }
7695
7696 #[test]
7697 fn cc_leaves_blank_line() {
7698 let mut e = editor_with("a\nb\nc");
7699 e.jump_cursor(1, 0);
7700 run_keys(&mut e, "ccX<Esc>");
7701 assert_eq!(
7702 e.buffer().lines(),
7703 &["a".to_string(), "X".to_string(), "c".to_string()]
7704 );
7705 }
7706
7707 #[test]
7712 fn big_w_skips_hyphens() {
7713 let mut e = editor_with("foo-bar baz");
7715 run_keys(&mut e, "W");
7716 assert_eq!(e.cursor().1, 8);
7717 }
7718
7719 #[test]
7720 fn big_w_crosses_lines() {
7721 let mut e = editor_with("foo-bar\nbaz-qux");
7722 run_keys(&mut e, "W");
7723 assert_eq!(e.cursor(), (1, 0));
7724 }
7725
7726 #[test]
7727 fn big_b_skips_hyphens() {
7728 let mut e = editor_with("foo-bar baz");
7729 e.jump_cursor(0, 9);
7730 run_keys(&mut e, "B");
7731 assert_eq!(e.cursor().1, 8);
7732 run_keys(&mut e, "B");
7733 assert_eq!(e.cursor().1, 0);
7734 }
7735
7736 #[test]
7737 fn big_e_jumps_to_big_word_end() {
7738 let mut e = editor_with("foo-bar baz");
7739 run_keys(&mut e, "E");
7740 assert_eq!(e.cursor().1, 6);
7741 run_keys(&mut e, "E");
7742 assert_eq!(e.cursor().1, 10);
7743 }
7744
7745 #[test]
7746 fn dw_with_big_word_variant() {
7747 let mut e = editor_with("foo-bar baz");
7749 run_keys(&mut e, "dW");
7750 assert_eq!(e.buffer().lines()[0], "baz");
7751 }
7752
7753 #[test]
7756 fn insert_ctrl_w_deletes_word_back() {
7757 let mut e = editor_with("");
7758 run_keys(&mut e, "i");
7759 for c in "hello world".chars() {
7760 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7761 }
7762 run_keys(&mut e, "<C-w>");
7763 assert_eq!(e.buffer().lines()[0], "hello ");
7764 }
7765
7766 #[test]
7767 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7768 let mut e = editor_with("hello\nworld");
7772 e.jump_cursor(1, 0);
7773 run_keys(&mut e, "i");
7774 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7775 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7778 assert_eq!(e.cursor(), (0, 0));
7779 }
7780
7781 #[test]
7782 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7783 let mut e = editor_with("foo bar\nbaz");
7784 e.jump_cursor(1, 0);
7785 run_keys(&mut e, "i");
7786 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7787 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7789 assert_eq!(e.cursor(), (0, 4));
7790 }
7791
7792 #[test]
7793 fn insert_ctrl_u_deletes_to_line_start() {
7794 let mut e = editor_with("");
7795 run_keys(&mut e, "i");
7796 for c in "hello world".chars() {
7797 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7798 }
7799 run_keys(&mut e, "<C-u>");
7800 assert_eq!(e.buffer().lines()[0], "");
7801 }
7802
7803 #[test]
7804 fn insert_ctrl_o_runs_one_normal_command() {
7805 let mut e = editor_with("hello world");
7806 run_keys(&mut e, "A");
7808 assert_eq!(e.vim_mode(), VimMode::Insert);
7809 e.jump_cursor(0, 0);
7811 run_keys(&mut e, "<C-o>");
7812 assert_eq!(e.vim_mode(), VimMode::Normal);
7813 run_keys(&mut e, "dw");
7814 assert_eq!(e.vim_mode(), VimMode::Insert);
7816 assert_eq!(e.buffer().lines()[0], "world");
7817 }
7818
7819 #[test]
7822 fn j_through_empty_line_preserves_column() {
7823 let mut e = editor_with("hello world\n\nanother line");
7824 run_keys(&mut e, "llllll");
7826 assert_eq!(e.cursor(), (0, 6));
7827 run_keys(&mut e, "j");
7830 assert_eq!(e.cursor(), (1, 0));
7831 run_keys(&mut e, "j");
7833 assert_eq!(e.cursor(), (2, 6));
7834 }
7835
7836 #[test]
7837 fn j_through_shorter_line_preserves_column() {
7838 let mut e = editor_with("hello world\nhi\nanother line");
7839 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7842 run_keys(&mut e, "j");
7843 assert_eq!(e.cursor(), (2, 7));
7844 }
7845
7846 #[test]
7847 fn esc_from_insert_sticky_matches_visible_cursor() {
7848 let mut e = editor_with(" this is a line\n another one of a similar size");
7852 e.jump_cursor(0, 12);
7853 run_keys(&mut e, "I");
7854 assert_eq!(e.cursor(), (0, 4));
7855 run_keys(&mut e, "X<Esc>");
7856 assert_eq!(e.cursor(), (0, 4));
7857 run_keys(&mut e, "j");
7858 assert_eq!(e.cursor(), (1, 4));
7859 }
7860
7861 #[test]
7862 fn esc_from_insert_sticky_tracks_inserted_chars() {
7863 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7864 run_keys(&mut e, "i");
7865 run_keys(&mut e, "abc<Esc>");
7866 assert_eq!(e.cursor(), (0, 2));
7867 run_keys(&mut e, "j");
7868 assert_eq!(e.cursor(), (1, 2));
7869 }
7870
7871 #[test]
7872 fn esc_from_insert_sticky_tracks_arrow_nav() {
7873 let mut e = editor_with("xxxxxx\nyyyyyy");
7874 run_keys(&mut e, "i");
7875 run_keys(&mut e, "abc");
7876 for _ in 0..2 {
7877 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7878 }
7879 run_keys(&mut e, "<Esc>");
7880 assert_eq!(e.cursor(), (0, 0));
7881 run_keys(&mut e, "j");
7882 assert_eq!(e.cursor(), (1, 0));
7883 }
7884
7885 #[test]
7886 fn esc_from_insert_at_col_14_followed_by_j() {
7887 let line = "x".repeat(30);
7890 let buf = format!("{line}\n{line}");
7891 let mut e = editor_with(&buf);
7892 e.jump_cursor(0, 14);
7893 run_keys(&mut e, "i");
7894 for c in "test ".chars() {
7895 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7896 }
7897 run_keys(&mut e, "<Esc>");
7898 assert_eq!(e.cursor(), (0, 18));
7899 run_keys(&mut e, "j");
7900 assert_eq!(e.cursor(), (1, 18));
7901 }
7902
7903 #[test]
7904 fn linewise_paste_resets_sticky_column() {
7905 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7909 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7911 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7915 run_keys(&mut e, "j");
7917 assert_eq!(e.cursor(), (3, 2));
7918 }
7919
7920 #[test]
7921 fn horizontal_motion_resyncs_sticky_column() {
7922 let mut e = editor_with("hello world\n\nanother line");
7926 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7929 assert_eq!(e.cursor(), (2, 3));
7930 }
7931
7932 #[test]
7935 fn ctrl_v_enters_visual_block() {
7936 let mut e = editor_with("aaa\nbbb\nccc");
7937 run_keys(&mut e, "<C-v>");
7938 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7939 }
7940
7941 #[test]
7942 fn visual_block_esc_returns_to_normal() {
7943 let mut e = editor_with("aaa\nbbb\nccc");
7944 run_keys(&mut e, "<C-v>");
7945 run_keys(&mut e, "<Esc>");
7946 assert_eq!(e.vim_mode(), VimMode::Normal);
7947 }
7948
7949 #[test]
7950 fn backtick_lt_jumps_to_visual_start_mark() {
7951 let mut e = editor_with("foo bar baz\n");
7955 run_keys(&mut e, "v");
7956 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7959 run_keys(&mut e, "`<lt>");
7961 assert_eq!(e.cursor(), (0, 0));
7962 }
7963
7964 #[test]
7965 fn backtick_gt_jumps_to_visual_end_mark() {
7966 let mut e = editor_with("foo bar baz\n");
7967 run_keys(&mut e, "v");
7968 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7970 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7972 assert_eq!(e.cursor(), (0, 4));
7973 }
7974
7975 #[test]
7976 fn visual_exit_sets_lt_gt_marks() {
7977 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7980 run_keys(&mut e, "V");
7982 run_keys(&mut e, "j");
7983 run_keys(&mut e, "<Esc>");
7984 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7985 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7986 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7987 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7988 }
7989
7990 #[test]
7991 fn visual_exit_marks_use_lower_higher_order() {
7992 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7996 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7998 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
8000 let lt = e.mark('<').unwrap();
8001 let gt = e.mark('>').unwrap();
8002 assert_eq!(lt.0, 2);
8003 assert_eq!(gt.0, 3);
8004 }
8005
8006 #[test]
8007 fn visualline_exit_marks_snap_to_line_edges() {
8008 let mut e = editor_with("aaaaa\nbbbbb\ncc");
8010 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
8012 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
8014 let lt = e.mark('<').unwrap();
8015 let gt = e.mark('>').unwrap();
8016 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
8017 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
8019 }
8020
8021 #[test]
8022 fn visualblock_exit_marks_use_block_corners() {
8023 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
8027 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
8029 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
8032 let lt = e.mark('<').unwrap();
8033 let gt = e.mark('>').unwrap();
8034 assert_eq!(lt, (0, 2), "'< should be top-left corner");
8036 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
8037 }
8038
8039 #[test]
8040 fn visual_block_delete_removes_column_range() {
8041 let mut e = editor_with("hello\nworld\nhappy");
8042 run_keys(&mut e, "l");
8044 run_keys(&mut e, "<C-v>");
8045 run_keys(&mut e, "jj");
8046 run_keys(&mut e, "ll");
8047 run_keys(&mut e, "d");
8048 assert_eq!(
8050 e.buffer().lines(),
8051 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
8052 );
8053 }
8054
8055 #[test]
8056 fn visual_block_yank_joins_with_newlines() {
8057 let mut e = editor_with("hello\nworld\nhappy");
8058 run_keys(&mut e, "<C-v>");
8059 run_keys(&mut e, "jj");
8060 run_keys(&mut e, "ll");
8061 run_keys(&mut e, "y");
8062 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
8063 }
8064
8065 #[test]
8066 fn visual_block_replace_fills_block() {
8067 let mut e = editor_with("hello\nworld\nhappy");
8068 run_keys(&mut e, "<C-v>");
8069 run_keys(&mut e, "jj");
8070 run_keys(&mut e, "ll");
8071 run_keys(&mut e, "rx");
8072 assert_eq!(
8073 e.buffer().lines(),
8074 &[
8075 "xxxlo".to_string(),
8076 "xxxld".to_string(),
8077 "xxxpy".to_string()
8078 ]
8079 );
8080 }
8081
8082 #[test]
8083 fn visual_block_insert_repeats_across_rows() {
8084 let mut e = editor_with("hello\nworld\nhappy");
8085 run_keys(&mut e, "<C-v>");
8086 run_keys(&mut e, "jj");
8087 run_keys(&mut e, "I");
8088 run_keys(&mut e, "# <Esc>");
8089 assert_eq!(
8090 e.buffer().lines(),
8091 &[
8092 "# hello".to_string(),
8093 "# world".to_string(),
8094 "# happy".to_string()
8095 ]
8096 );
8097 }
8098
8099 #[test]
8100 fn block_highlight_returns_none_outside_block_mode() {
8101 let mut e = editor_with("abc");
8102 assert!(e.block_highlight().is_none());
8103 run_keys(&mut e, "v");
8104 assert!(e.block_highlight().is_none());
8105 run_keys(&mut e, "<Esc>V");
8106 assert!(e.block_highlight().is_none());
8107 }
8108
8109 #[test]
8110 fn block_highlight_bounds_track_anchor_and_cursor() {
8111 let mut e = editor_with("aaaa\nbbbb\ncccc");
8112 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8114 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8117 }
8118
8119 #[test]
8120 fn visual_block_delete_handles_short_lines() {
8121 let mut e = editor_with("hello\nhi\nworld");
8123 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8125 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8127 assert_eq!(
8132 e.buffer().lines(),
8133 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8134 );
8135 }
8136
8137 #[test]
8138 fn visual_block_yank_pads_short_lines_with_empties() {
8139 let mut e = editor_with("hello\nhi\nworld");
8140 run_keys(&mut e, "l");
8141 run_keys(&mut e, "<C-v>");
8142 run_keys(&mut e, "jjll");
8143 run_keys(&mut e, "y");
8144 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8146 }
8147
8148 #[test]
8149 fn visual_block_replace_skips_past_eol() {
8150 let mut e = editor_with("ab\ncd\nef");
8153 run_keys(&mut e, "l");
8155 run_keys(&mut e, "<C-v>");
8156 run_keys(&mut e, "jjllllll");
8157 run_keys(&mut e, "rX");
8158 assert_eq!(
8161 e.buffer().lines(),
8162 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8163 );
8164 }
8165
8166 #[test]
8167 fn visual_block_with_empty_line_in_middle() {
8168 let mut e = editor_with("abcd\n\nefgh");
8169 run_keys(&mut e, "<C-v>");
8170 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8172 assert_eq!(
8175 e.buffer().lines(),
8176 &["d".to_string(), "".to_string(), "h".to_string()]
8177 );
8178 }
8179
8180 #[test]
8181 fn block_insert_pads_empty_lines_to_block_column() {
8182 let mut e = editor_with("this is a line\n\nthis is a line");
8185 e.jump_cursor(0, 3);
8186 run_keys(&mut e, "<C-v>");
8187 run_keys(&mut e, "jj");
8188 run_keys(&mut e, "I");
8189 run_keys(&mut e, "XX<Esc>");
8190 assert_eq!(
8191 e.buffer().lines(),
8192 &[
8193 "thiXXs is a line".to_string(),
8194 " XX".to_string(),
8195 "thiXXs is a line".to_string()
8196 ]
8197 );
8198 }
8199
8200 #[test]
8201 fn block_insert_pads_short_lines_to_block_column() {
8202 let mut e = editor_with("aaaaa\nbb\naaaaa");
8203 e.jump_cursor(0, 3);
8204 run_keys(&mut e, "<C-v>");
8205 run_keys(&mut e, "jj");
8206 run_keys(&mut e, "I");
8207 run_keys(&mut e, "Y<Esc>");
8208 assert_eq!(
8210 e.buffer().lines(),
8211 &[
8212 "aaaYaa".to_string(),
8213 "bb Y".to_string(),
8214 "aaaYaa".to_string()
8215 ]
8216 );
8217 }
8218
8219 #[test]
8220 fn visual_block_append_repeats_across_rows() {
8221 let mut e = editor_with("foo\nbar\nbaz");
8222 run_keys(&mut e, "<C-v>");
8223 run_keys(&mut e, "jj");
8224 run_keys(&mut e, "A");
8227 run_keys(&mut e, "!<Esc>");
8228 assert_eq!(
8229 e.buffer().lines(),
8230 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8231 );
8232 }
8233
8234 #[test]
8237 fn slash_opens_forward_search_prompt() {
8238 let mut e = editor_with("hello world");
8239 run_keys(&mut e, "/");
8240 let p = e.search_prompt().expect("prompt should be active");
8241 assert!(p.text.is_empty());
8242 assert!(p.forward);
8243 }
8244
8245 #[test]
8246 fn question_opens_backward_search_prompt() {
8247 let mut e = editor_with("hello world");
8248 run_keys(&mut e, "?");
8249 let p = e.search_prompt().expect("prompt should be active");
8250 assert!(!p.forward);
8251 }
8252
8253 #[test]
8254 fn search_prompt_typing_updates_pattern_live() {
8255 let mut e = editor_with("foo bar\nbaz");
8256 run_keys(&mut e, "/bar");
8257 assert_eq!(e.search_prompt().unwrap().text, "bar");
8258 assert!(e.search_state().pattern.is_some());
8260 }
8261
8262 #[test]
8263 fn search_prompt_backspace_and_enter() {
8264 let mut e = editor_with("hello world\nagain");
8265 run_keys(&mut e, "/worlx");
8266 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8267 assert_eq!(e.search_prompt().unwrap().text, "worl");
8268 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8269 assert!(e.search_prompt().is_none());
8271 assert_eq!(e.last_search(), Some("worl"));
8272 assert_eq!(e.cursor(), (0, 6));
8273 }
8274
8275 #[test]
8276 fn empty_search_prompt_enter_repeats_last_search() {
8277 let mut e = editor_with("foo bar foo baz foo");
8278 run_keys(&mut e, "/foo");
8279 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8280 assert_eq!(e.cursor().1, 8);
8281 run_keys(&mut e, "/");
8283 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8284 assert_eq!(e.cursor().1, 16);
8285 assert_eq!(e.last_search(), Some("foo"));
8286 }
8287
8288 #[test]
8289 fn search_history_records_committed_patterns() {
8290 let mut e = editor_with("alpha beta gamma");
8291 run_keys(&mut e, "/alpha<CR>");
8292 run_keys(&mut e, "/beta<CR>");
8293 let history = e.vim.search_history.clone();
8295 assert_eq!(history, vec!["alpha", "beta"]);
8296 }
8297
8298 #[test]
8299 fn search_history_dedupes_consecutive_repeats() {
8300 let mut e = editor_with("foo bar foo");
8301 run_keys(&mut e, "/foo<CR>");
8302 run_keys(&mut e, "/foo<CR>");
8303 run_keys(&mut e, "/bar<CR>");
8304 run_keys(&mut e, "/bar<CR>");
8305 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8307 }
8308
8309 #[test]
8310 fn ctrl_p_walks_history_backward() {
8311 let mut e = editor_with("alpha beta gamma");
8312 run_keys(&mut e, "/alpha<CR>");
8313 run_keys(&mut e, "/beta<CR>");
8314 run_keys(&mut e, "/");
8316 assert_eq!(e.search_prompt().unwrap().text, "");
8317 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8318 assert_eq!(e.search_prompt().unwrap().text, "beta");
8319 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8320 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8321 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8323 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8324 }
8325
8326 #[test]
8327 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8328 let mut e = editor_with("a b c");
8329 run_keys(&mut e, "/a<CR>");
8330 run_keys(&mut e, "/b<CR>");
8331 run_keys(&mut e, "/c<CR>");
8332 run_keys(&mut e, "/");
8333 for _ in 0..3 {
8335 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8336 }
8337 assert_eq!(e.search_prompt().unwrap().text, "a");
8338 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8339 assert_eq!(e.search_prompt().unwrap().text, "b");
8340 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8341 assert_eq!(e.search_prompt().unwrap().text, "c");
8342 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8344 assert_eq!(e.search_prompt().unwrap().text, "c");
8345 }
8346
8347 #[test]
8348 fn typing_after_history_walk_resets_cursor() {
8349 let mut e = editor_with("foo");
8350 run_keys(&mut e, "/foo<CR>");
8351 run_keys(&mut e, "/");
8352 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8353 assert_eq!(e.search_prompt().unwrap().text, "foo");
8354 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8357 assert_eq!(e.search_prompt().unwrap().text, "foox");
8358 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8359 assert_eq!(e.search_prompt().unwrap().text, "foo");
8360 }
8361
8362 #[test]
8363 fn empty_backward_search_prompt_enter_repeats_last_search() {
8364 let mut e = editor_with("foo bar foo baz foo");
8365 run_keys(&mut e, "/foo");
8367 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8368 assert_eq!(e.cursor().1, 8);
8369 run_keys(&mut e, "?");
8370 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8371 assert_eq!(e.cursor().1, 0);
8372 assert_eq!(e.last_search(), Some("foo"));
8373 }
8374
8375 #[test]
8376 fn search_prompt_esc_cancels_but_keeps_last_search() {
8377 let mut e = editor_with("foo bar\nbaz");
8378 run_keys(&mut e, "/bar");
8379 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8380 assert!(e.search_prompt().is_none());
8381 assert_eq!(e.last_search(), Some("bar"));
8382 }
8383
8384 #[test]
8385 fn search_then_n_and_shift_n_navigate() {
8386 let mut e = editor_with("foo bar foo baz foo");
8387 run_keys(&mut e, "/foo");
8388 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8389 assert_eq!(e.cursor().1, 8);
8391 run_keys(&mut e, "n");
8392 assert_eq!(e.cursor().1, 16);
8393 run_keys(&mut e, "N");
8394 assert_eq!(e.cursor().1, 8);
8395 }
8396
8397 #[test]
8398 fn question_mark_searches_backward_on_enter() {
8399 let mut e = editor_with("foo bar foo baz");
8400 e.jump_cursor(0, 10);
8401 run_keys(&mut e, "?foo");
8402 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8403 assert_eq!(e.cursor(), (0, 8));
8405 }
8406
8407 #[test]
8410 fn big_y_yanks_to_end_of_line() {
8411 let mut e = editor_with("hello world");
8412 e.jump_cursor(0, 6);
8413 run_keys(&mut e, "Y");
8414 assert_eq!(e.last_yank.as_deref(), Some("world"));
8415 }
8416
8417 #[test]
8418 fn big_y_from_line_start_yanks_full_line() {
8419 let mut e = editor_with("hello world");
8420 run_keys(&mut e, "Y");
8421 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8422 }
8423
8424 #[test]
8425 fn gj_joins_without_inserting_space() {
8426 let mut e = editor_with("hello\n world");
8427 run_keys(&mut e, "gJ");
8428 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8430 }
8431
8432 #[test]
8433 fn gj_noop_on_last_line() {
8434 let mut e = editor_with("only");
8435 run_keys(&mut e, "gJ");
8436 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8437 }
8438
8439 #[test]
8440 fn ge_jumps_to_previous_word_end() {
8441 let mut e = editor_with("foo bar baz");
8442 e.jump_cursor(0, 5);
8443 run_keys(&mut e, "ge");
8444 assert_eq!(e.cursor(), (0, 2));
8445 }
8446
8447 #[test]
8448 fn ge_respects_word_class() {
8449 let mut e = editor_with("foo-bar baz");
8452 e.jump_cursor(0, 5);
8453 run_keys(&mut e, "ge");
8454 assert_eq!(e.cursor(), (0, 3));
8455 }
8456
8457 #[test]
8458 fn big_ge_treats_hyphens_as_part_of_word() {
8459 let mut e = editor_with("foo-bar baz");
8462 e.jump_cursor(0, 10);
8463 run_keys(&mut e, "gE");
8464 assert_eq!(e.cursor(), (0, 6));
8465 }
8466
8467 #[test]
8468 fn ge_crosses_line_boundary() {
8469 let mut e = editor_with("foo\nbar");
8470 e.jump_cursor(1, 0);
8471 run_keys(&mut e, "ge");
8472 assert_eq!(e.cursor(), (0, 2));
8473 }
8474
8475 #[test]
8476 fn dge_deletes_to_end_of_previous_word() {
8477 let mut e = editor_with("foo bar baz");
8478 e.jump_cursor(0, 8);
8479 run_keys(&mut e, "dge");
8482 assert_eq!(e.buffer().lines()[0], "foo baaz");
8483 }
8484
8485 #[test]
8486 fn ctrl_scroll_keys_do_not_panic() {
8487 let mut e = editor_with(
8490 (0..50)
8491 .map(|i| format!("line{i}"))
8492 .collect::<Vec<_>>()
8493 .join("\n")
8494 .as_str(),
8495 );
8496 run_keys(&mut e, "<C-f>");
8497 run_keys(&mut e, "<C-b>");
8498 assert!(!e.buffer().lines().is_empty());
8500 }
8501
8502 #[test]
8509 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8510 let mut e = Editor::new(
8511 hjkl_buffer::Buffer::new(),
8512 crate::types::DefaultHost::new(),
8513 crate::types::Options::default(),
8514 );
8515 e.set_content("row0\nrow1\nrow2");
8516 run_keys(&mut e, "3iX<Down><Esc>");
8518 assert!(e.buffer().lines()[0].contains('X'));
8520 assert!(
8523 !e.buffer().lines()[1].contains("row0"),
8524 "row1 leaked row0 contents: {:?}",
8525 e.buffer().lines()[1]
8526 );
8527 assert_eq!(e.buffer().lines().len(), 3);
8530 }
8531
8532 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8535 let mut e = Editor::new(
8536 hjkl_buffer::Buffer::new(),
8537 crate::types::DefaultHost::new(),
8538 crate::types::Options::default(),
8539 );
8540 let body = (0..n)
8541 .map(|i| format!(" line{}", i))
8542 .collect::<Vec<_>>()
8543 .join("\n");
8544 e.set_content(&body);
8545 e.set_viewport_height(viewport);
8546 e
8547 }
8548
8549 #[test]
8550 fn ctrl_d_moves_cursor_half_page_down() {
8551 let mut e = editor_with_rows(100, 20);
8552 run_keys(&mut e, "<C-d>");
8553 assert_eq!(e.cursor().0, 10);
8554 }
8555
8556 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8557 let mut e = Editor::new(
8558 hjkl_buffer::Buffer::new(),
8559 crate::types::DefaultHost::new(),
8560 crate::types::Options::default(),
8561 );
8562 e.set_content(&lines.join("\n"));
8563 e.set_viewport_height(viewport);
8564 let v = e.host_mut().viewport_mut();
8565 v.height = viewport;
8566 v.width = text_width;
8567 v.text_width = text_width;
8568 v.wrap = hjkl_buffer::Wrap::Char;
8569 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8570 e
8571 }
8572
8573 #[test]
8574 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8575 let lines = ["aaaabbbbcccc"; 10];
8579 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8580 e.jump_cursor(4, 0);
8581 e.ensure_cursor_in_scrolloff();
8582 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8583 assert!(csr <= 6, "csr={csr}");
8584 }
8585
8586 #[test]
8587 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8588 let lines = ["aaaabbbbcccc"; 10];
8589 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8590 e.jump_cursor(7, 0);
8593 e.ensure_cursor_in_scrolloff();
8594 e.jump_cursor(2, 0);
8595 e.ensure_cursor_in_scrolloff();
8596 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8597 assert!(csr >= 5, "csr={csr}");
8599 }
8600
8601 #[test]
8602 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8603 let lines = ["aaaabbbbcccc"; 5];
8604 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8605 e.jump_cursor(4, 11);
8606 e.ensure_cursor_in_scrolloff();
8607 let top = e.host().viewport().top_row;
8612 assert_eq!(top, 1);
8613 }
8614
8615 #[test]
8616 fn ctrl_u_moves_cursor_half_page_up() {
8617 let mut e = editor_with_rows(100, 20);
8618 e.jump_cursor(50, 0);
8619 run_keys(&mut e, "<C-u>");
8620 assert_eq!(e.cursor().0, 40);
8621 }
8622
8623 #[test]
8624 fn ctrl_f_moves_cursor_full_page_down() {
8625 let mut e = editor_with_rows(100, 20);
8626 run_keys(&mut e, "<C-f>");
8627 assert_eq!(e.cursor().0, 18);
8629 }
8630
8631 #[test]
8632 fn ctrl_b_moves_cursor_full_page_up() {
8633 let mut e = editor_with_rows(100, 20);
8634 e.jump_cursor(50, 0);
8635 run_keys(&mut e, "<C-b>");
8636 assert_eq!(e.cursor().0, 32);
8637 }
8638
8639 #[test]
8640 fn ctrl_d_lands_on_first_non_blank() {
8641 let mut e = editor_with_rows(100, 20);
8642 run_keys(&mut e, "<C-d>");
8643 assert_eq!(e.cursor().1, 2);
8645 }
8646
8647 #[test]
8648 fn ctrl_d_clamps_at_end_of_buffer() {
8649 let mut e = editor_with_rows(5, 20);
8650 run_keys(&mut e, "<C-d>");
8651 assert_eq!(e.cursor().0, 4);
8652 }
8653
8654 #[test]
8655 fn capital_h_jumps_to_viewport_top() {
8656 let mut e = editor_with_rows(100, 10);
8657 e.jump_cursor(50, 0);
8658 e.set_viewport_top(45);
8659 let top = e.host().viewport().top_row;
8660 run_keys(&mut e, "H");
8661 assert_eq!(e.cursor().0, top);
8662 assert_eq!(e.cursor().1, 2);
8663 }
8664
8665 #[test]
8666 fn capital_l_jumps_to_viewport_bottom() {
8667 let mut e = editor_with_rows(100, 10);
8668 e.jump_cursor(50, 0);
8669 e.set_viewport_top(45);
8670 let top = e.host().viewport().top_row;
8671 run_keys(&mut e, "L");
8672 assert_eq!(e.cursor().0, top + 9);
8673 }
8674
8675 #[test]
8676 fn capital_m_jumps_to_viewport_middle() {
8677 let mut e = editor_with_rows(100, 10);
8678 e.jump_cursor(50, 0);
8679 e.set_viewport_top(45);
8680 let top = e.host().viewport().top_row;
8681 run_keys(&mut e, "M");
8682 assert_eq!(e.cursor().0, top + 4);
8684 }
8685
8686 #[test]
8687 fn g_capital_m_lands_at_line_midpoint() {
8688 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8690 assert_eq!(e.cursor(), (0, 6));
8692 }
8693
8694 #[test]
8695 fn g_capital_m_on_empty_line_stays_at_zero() {
8696 let mut e = editor_with("");
8697 run_keys(&mut e, "gM");
8698 assert_eq!(e.cursor(), (0, 0));
8699 }
8700
8701 #[test]
8702 fn g_capital_m_uses_current_line_only() {
8703 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8706 run_keys(&mut e, "gM");
8707 assert_eq!(e.cursor(), (1, 6));
8708 }
8709
8710 #[test]
8711 fn capital_h_count_offsets_from_top() {
8712 let mut e = editor_with_rows(100, 10);
8713 e.jump_cursor(50, 0);
8714 e.set_viewport_top(45);
8715 let top = e.host().viewport().top_row;
8716 run_keys(&mut e, "3H");
8717 assert_eq!(e.cursor().0, top + 2);
8718 }
8719
8720 #[test]
8723 fn ctrl_o_returns_to_pre_g_position() {
8724 let mut e = editor_with_rows(50, 20);
8725 e.jump_cursor(5, 2);
8726 run_keys(&mut e, "G");
8727 assert_eq!(e.cursor().0, 49);
8728 run_keys(&mut e, "<C-o>");
8729 assert_eq!(e.cursor(), (5, 2));
8730 }
8731
8732 #[test]
8733 fn ctrl_i_redoes_jump_after_ctrl_o() {
8734 let mut e = editor_with_rows(50, 20);
8735 e.jump_cursor(5, 2);
8736 run_keys(&mut e, "G");
8737 let post = e.cursor();
8738 run_keys(&mut e, "<C-o>");
8739 run_keys(&mut e, "<C-i>");
8740 assert_eq!(e.cursor(), post);
8741 }
8742
8743 #[test]
8744 fn new_jump_clears_forward_stack() {
8745 let mut e = editor_with_rows(50, 20);
8746 e.jump_cursor(5, 2);
8747 run_keys(&mut e, "G");
8748 run_keys(&mut e, "<C-o>");
8749 run_keys(&mut e, "gg");
8750 run_keys(&mut e, "<C-i>");
8751 assert_eq!(e.cursor().0, 0);
8752 }
8753
8754 #[test]
8755 fn ctrl_o_on_empty_stack_is_noop() {
8756 let mut e = editor_with_rows(10, 20);
8757 e.jump_cursor(3, 1);
8758 run_keys(&mut e, "<C-o>");
8759 assert_eq!(e.cursor(), (3, 1));
8760 }
8761
8762 #[test]
8763 fn asterisk_search_pushes_jump() {
8764 let mut e = editor_with("foo bar\nbaz foo end");
8765 e.jump_cursor(0, 0);
8766 run_keys(&mut e, "*");
8767 let after = e.cursor();
8768 assert_ne!(after, (0, 0));
8769 run_keys(&mut e, "<C-o>");
8770 assert_eq!(e.cursor(), (0, 0));
8771 }
8772
8773 #[test]
8774 fn h_viewport_jump_is_recorded() {
8775 let mut e = editor_with_rows(100, 10);
8776 e.jump_cursor(50, 0);
8777 e.set_viewport_top(45);
8778 let pre = e.cursor();
8779 run_keys(&mut e, "H");
8780 assert_ne!(e.cursor(), pre);
8781 run_keys(&mut e, "<C-o>");
8782 assert_eq!(e.cursor(), pre);
8783 }
8784
8785 #[test]
8786 fn j_k_motion_does_not_push_jump() {
8787 let mut e = editor_with_rows(50, 20);
8788 e.jump_cursor(5, 0);
8789 run_keys(&mut e, "jjj");
8790 run_keys(&mut e, "<C-o>");
8791 assert_eq!(e.cursor().0, 8);
8792 }
8793
8794 #[test]
8795 fn jumplist_caps_at_100() {
8796 let mut e = editor_with_rows(200, 20);
8797 for i in 0..101 {
8798 e.jump_cursor(i, 0);
8799 run_keys(&mut e, "G");
8800 }
8801 assert!(e.vim.jump_back.len() <= 100);
8802 }
8803
8804 #[test]
8805 fn tab_acts_as_ctrl_i() {
8806 let mut e = editor_with_rows(50, 20);
8807 e.jump_cursor(5, 2);
8808 run_keys(&mut e, "G");
8809 let post = e.cursor();
8810 run_keys(&mut e, "<C-o>");
8811 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8812 assert_eq!(e.cursor(), post);
8813 }
8814
8815 #[test]
8818 fn ma_then_backtick_a_jumps_exact() {
8819 let mut e = editor_with_rows(50, 20);
8820 e.jump_cursor(5, 3);
8821 run_keys(&mut e, "ma");
8822 e.jump_cursor(20, 0);
8823 run_keys(&mut e, "`a");
8824 assert_eq!(e.cursor(), (5, 3));
8825 }
8826
8827 #[test]
8828 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8829 let mut e = editor_with_rows(50, 20);
8830 e.jump_cursor(5, 6);
8832 run_keys(&mut e, "ma");
8833 e.jump_cursor(30, 4);
8834 run_keys(&mut e, "'a");
8835 assert_eq!(e.cursor(), (5, 2));
8836 }
8837
8838 #[test]
8839 fn goto_mark_pushes_jumplist() {
8840 let mut e = editor_with_rows(50, 20);
8841 e.jump_cursor(10, 2);
8842 run_keys(&mut e, "mz");
8843 e.jump_cursor(3, 0);
8844 run_keys(&mut e, "`z");
8845 assert_eq!(e.cursor(), (10, 2));
8846 run_keys(&mut e, "<C-o>");
8847 assert_eq!(e.cursor(), (3, 0));
8848 }
8849
8850 #[test]
8851 fn goto_missing_mark_is_noop() {
8852 let mut e = editor_with_rows(50, 20);
8853 e.jump_cursor(3, 1);
8854 run_keys(&mut e, "`q");
8855 assert_eq!(e.cursor(), (3, 1));
8856 }
8857
8858 #[test]
8859 fn uppercase_mark_stored_under_uppercase_key() {
8860 let mut e = editor_with_rows(50, 20);
8861 e.jump_cursor(5, 3);
8862 run_keys(&mut e, "mA");
8863 assert_eq!(e.mark('A'), Some((5, 3)));
8866 assert!(e.mark('a').is_none());
8867 }
8868
8869 #[test]
8870 fn mark_survives_document_shrink_via_clamp() {
8871 let mut e = editor_with_rows(50, 20);
8872 e.jump_cursor(40, 4);
8873 run_keys(&mut e, "mx");
8874 e.set_content("a\nb\nc\nd\ne");
8876 run_keys(&mut e, "`x");
8877 let (r, _) = e.cursor();
8879 assert!(r <= 4);
8880 }
8881
8882 #[test]
8883 fn g_semicolon_walks_back_through_edits() {
8884 let mut e = editor_with("alpha\nbeta\ngamma");
8885 e.jump_cursor(0, 0);
8888 run_keys(&mut e, "iX<Esc>");
8889 e.jump_cursor(2, 0);
8890 run_keys(&mut e, "iY<Esc>");
8891 run_keys(&mut e, "g;");
8893 assert_eq!(e.cursor(), (2, 1));
8894 run_keys(&mut e, "g;");
8896 assert_eq!(e.cursor(), (0, 1));
8897 run_keys(&mut e, "g;");
8899 assert_eq!(e.cursor(), (0, 1));
8900 }
8901
8902 #[test]
8903 fn g_comma_walks_forward_after_g_semicolon() {
8904 let mut e = editor_with("a\nb\nc");
8905 e.jump_cursor(0, 0);
8906 run_keys(&mut e, "iX<Esc>");
8907 e.jump_cursor(2, 0);
8908 run_keys(&mut e, "iY<Esc>");
8909 run_keys(&mut e, "g;");
8910 run_keys(&mut e, "g;");
8911 assert_eq!(e.cursor(), (0, 1));
8912 run_keys(&mut e, "g,");
8913 assert_eq!(e.cursor(), (2, 1));
8914 }
8915
8916 #[test]
8917 fn new_edit_during_walk_trims_forward_entries() {
8918 let mut e = editor_with("a\nb\nc\nd");
8919 e.jump_cursor(0, 0);
8920 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8922 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8925 run_keys(&mut e, "g;");
8926 assert_eq!(e.cursor(), (0, 1));
8927 run_keys(&mut e, "iZ<Esc>");
8929 run_keys(&mut e, "g,");
8931 assert_ne!(e.cursor(), (2, 1));
8933 }
8934
8935 #[test]
8941 fn capital_mark_set_and_jump() {
8942 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8943 e.jump_cursor(2, 1);
8944 run_keys(&mut e, "mA");
8945 e.jump_cursor(0, 0);
8947 run_keys(&mut e, "'A");
8949 assert_eq!(e.cursor().0, 2);
8951 }
8952
8953 #[test]
8954 fn capital_mark_survives_set_content() {
8955 let mut e = editor_with("first buffer line\nsecond");
8956 e.jump_cursor(1, 3);
8957 run_keys(&mut e, "mA");
8958 e.set_content("totally different content\non many\nrows of text");
8960 e.jump_cursor(0, 0);
8962 run_keys(&mut e, "'A");
8963 assert_eq!(e.cursor().0, 1);
8964 }
8965
8966 #[test]
8971 fn capital_mark_shifts_with_edit() {
8972 let mut e = editor_with("a\nb\nc\nd");
8973 e.jump_cursor(3, 0);
8974 run_keys(&mut e, "mA");
8975 e.jump_cursor(0, 0);
8977 run_keys(&mut e, "dd");
8978 e.jump_cursor(0, 0);
8979 run_keys(&mut e, "'A");
8980 assert_eq!(e.cursor().0, 2);
8981 }
8982
8983 #[test]
8984 fn mark_below_delete_shifts_up() {
8985 let mut e = editor_with("a\nb\nc\nd\ne");
8986 e.jump_cursor(3, 0);
8988 run_keys(&mut e, "ma");
8989 e.jump_cursor(0, 0);
8991 run_keys(&mut e, "dd");
8992 e.jump_cursor(0, 0);
8994 run_keys(&mut e, "'a");
8995 assert_eq!(e.cursor().0, 2);
8996 assert_eq!(e.buffer().line(2).unwrap(), "d");
8997 }
8998
8999 #[test]
9000 fn mark_on_deleted_row_is_dropped() {
9001 let mut e = editor_with("a\nb\nc\nd");
9002 e.jump_cursor(1, 0);
9004 run_keys(&mut e, "ma");
9005 run_keys(&mut e, "dd");
9007 e.jump_cursor(2, 0);
9009 run_keys(&mut e, "'a");
9010 assert_eq!(e.cursor().0, 2);
9012 }
9013
9014 #[test]
9015 fn mark_above_edit_unchanged() {
9016 let mut e = editor_with("a\nb\nc\nd\ne");
9017 e.jump_cursor(0, 0);
9019 run_keys(&mut e, "ma");
9020 e.jump_cursor(3, 0);
9022 run_keys(&mut e, "dd");
9023 e.jump_cursor(2, 0);
9025 run_keys(&mut e, "'a");
9026 assert_eq!(e.cursor().0, 0);
9027 }
9028
9029 #[test]
9030 fn mark_shifts_down_after_insert() {
9031 let mut e = editor_with("a\nb\nc");
9032 e.jump_cursor(2, 0);
9034 run_keys(&mut e, "ma");
9035 e.jump_cursor(0, 0);
9037 run_keys(&mut e, "Onew<Esc>");
9038 e.jump_cursor(0, 0);
9041 run_keys(&mut e, "'a");
9042 assert_eq!(e.cursor().0, 3);
9043 assert_eq!(e.buffer().line(3).unwrap(), "c");
9044 }
9045
9046 #[test]
9049 fn forward_search_commit_pushes_jump() {
9050 let mut e = editor_with("alpha beta\nfoo target end\nmore");
9051 e.jump_cursor(0, 0);
9052 run_keys(&mut e, "/target<CR>");
9053 assert_ne!(e.cursor(), (0, 0));
9055 run_keys(&mut e, "<C-o>");
9057 assert_eq!(e.cursor(), (0, 0));
9058 }
9059
9060 #[test]
9061 fn search_commit_no_match_does_not_push_jump() {
9062 let mut e = editor_with("alpha beta\nfoo end");
9063 e.jump_cursor(0, 3);
9064 let pre_len = e.vim.jump_back.len();
9065 run_keys(&mut e, "/zzznotfound<CR>");
9066 assert_eq!(e.vim.jump_back.len(), pre_len);
9068 }
9069
9070 #[test]
9073 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
9074 let mut e = editor_with("hello world");
9075 run_keys(&mut e, "lll");
9076 let (row, col) = e.cursor();
9077 assert_eq!(e.buffer.cursor().row, row);
9078 assert_eq!(e.buffer.cursor().col, col);
9079 }
9080
9081 #[test]
9082 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
9083 let mut e = editor_with("aaaa\nbbbb\ncccc");
9084 run_keys(&mut e, "jj");
9085 let (row, col) = e.cursor();
9086 assert_eq!(e.buffer.cursor().row, row);
9087 assert_eq!(e.buffer.cursor().col, col);
9088 }
9089
9090 #[test]
9091 fn buffer_cursor_mirrors_textarea_after_word_motion() {
9092 let mut e = editor_with("foo bar baz");
9093 run_keys(&mut e, "ww");
9094 let (row, col) = e.cursor();
9095 assert_eq!(e.buffer.cursor().row, row);
9096 assert_eq!(e.buffer.cursor().col, col);
9097 }
9098
9099 #[test]
9100 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
9101 let mut e = editor_with("a\nb\nc\nd\ne");
9102 run_keys(&mut e, "G");
9103 let (row, col) = e.cursor();
9104 assert_eq!(e.buffer.cursor().row, row);
9105 assert_eq!(e.buffer.cursor().col, col);
9106 }
9107
9108 #[test]
9109 fn editor_sticky_col_tracks_horizontal_motion() {
9110 let mut e = editor_with("longline\nhi\nlongline");
9111 run_keys(&mut e, "fl");
9116 let landed = e.cursor().1;
9117 assert!(landed > 0, "fl should have moved");
9118 run_keys(&mut e, "j");
9119 assert_eq!(e.sticky_col(), Some(landed));
9122 }
9123
9124 #[test]
9125 fn buffer_content_mirrors_textarea_after_insert() {
9126 let mut e = editor_with("hello");
9127 run_keys(&mut e, "iXYZ<Esc>");
9128 let text = e.buffer().lines().join("\n");
9129 assert_eq!(e.buffer.as_string(), text);
9130 }
9131
9132 #[test]
9133 fn buffer_content_mirrors_textarea_after_delete() {
9134 let mut e = editor_with("alpha bravo charlie");
9135 run_keys(&mut e, "dw");
9136 let text = e.buffer().lines().join("\n");
9137 assert_eq!(e.buffer.as_string(), text);
9138 }
9139
9140 #[test]
9141 fn buffer_content_mirrors_textarea_after_dd() {
9142 let mut e = editor_with("a\nb\nc\nd");
9143 run_keys(&mut e, "jdd");
9144 let text = e.buffer().lines().join("\n");
9145 assert_eq!(e.buffer.as_string(), text);
9146 }
9147
9148 #[test]
9149 fn buffer_content_mirrors_textarea_after_open_line() {
9150 let mut e = editor_with("foo\nbar");
9151 run_keys(&mut e, "oNEW<Esc>");
9152 let text = e.buffer().lines().join("\n");
9153 assert_eq!(e.buffer.as_string(), text);
9154 }
9155
9156 #[test]
9157 fn buffer_content_mirrors_textarea_after_paste() {
9158 let mut e = editor_with("hello");
9159 run_keys(&mut e, "yy");
9160 run_keys(&mut e, "p");
9161 let text = e.buffer().lines().join("\n");
9162 assert_eq!(e.buffer.as_string(), text);
9163 }
9164
9165 #[test]
9166 fn buffer_selection_none_in_normal_mode() {
9167 let e = editor_with("foo bar");
9168 assert!(e.buffer_selection().is_none());
9169 }
9170
9171 #[test]
9172 fn buffer_selection_char_in_visual_mode() {
9173 use hjkl_buffer::{Position, Selection};
9174 let mut e = editor_with("hello world");
9175 run_keys(&mut e, "vlll");
9176 assert_eq!(
9177 e.buffer_selection(),
9178 Some(Selection::Char {
9179 anchor: Position::new(0, 0),
9180 head: Position::new(0, 3),
9181 })
9182 );
9183 }
9184
9185 #[test]
9186 fn buffer_selection_line_in_visual_line_mode() {
9187 use hjkl_buffer::Selection;
9188 let mut e = editor_with("a\nb\nc\nd");
9189 run_keys(&mut e, "Vj");
9190 assert_eq!(
9191 e.buffer_selection(),
9192 Some(Selection::Line {
9193 anchor_row: 0,
9194 head_row: 1,
9195 })
9196 );
9197 }
9198
9199 #[test]
9200 fn wrapscan_off_blocks_wrap_around() {
9201 let mut e = editor_with("first\nsecond\nthird\n");
9202 e.settings_mut().wrapscan = false;
9203 e.jump_cursor(2, 0);
9205 run_keys(&mut e, "/first<CR>");
9206 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9208 e.settings_mut().wrapscan = true;
9210 run_keys(&mut e, "/first<CR>");
9211 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9212 }
9213
9214 #[test]
9215 fn smartcase_uppercase_pattern_stays_sensitive() {
9216 let mut e = editor_with("foo\nFoo\nBAR\n");
9217 e.settings_mut().ignore_case = true;
9218 e.settings_mut().smartcase = true;
9219 run_keys(&mut e, "/foo<CR>");
9222 let r1 = e
9223 .search_state()
9224 .pattern
9225 .as_ref()
9226 .unwrap()
9227 .as_str()
9228 .to_string();
9229 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9230 run_keys(&mut e, "/Foo<CR>");
9232 let r2 = e
9233 .search_state()
9234 .pattern
9235 .as_ref()
9236 .unwrap()
9237 .as_str()
9238 .to_string();
9239 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9240 }
9241
9242 #[test]
9243 fn enter_with_autoindent_copies_leading_whitespace() {
9244 let mut e = editor_with(" foo");
9245 e.jump_cursor(0, 7);
9246 run_keys(&mut e, "i<CR>");
9247 assert_eq!(e.buffer.line(1).unwrap(), " ");
9248 }
9249
9250 #[test]
9251 fn enter_without_autoindent_inserts_bare_newline() {
9252 let mut e = editor_with(" foo");
9253 e.settings_mut().autoindent = false;
9254 e.jump_cursor(0, 7);
9255 run_keys(&mut e, "i<CR>");
9256 assert_eq!(e.buffer.line(1).unwrap(), "");
9257 }
9258
9259 #[test]
9260 fn iskeyword_default_treats_alnum_underscore_as_word() {
9261 let mut e = editor_with("foo_bar baz");
9262 e.jump_cursor(0, 0);
9266 run_keys(&mut e, "*");
9267 let p = e
9268 .search_state()
9269 .pattern
9270 .as_ref()
9271 .unwrap()
9272 .as_str()
9273 .to_string();
9274 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9275 }
9276
9277 #[test]
9278 fn w_motion_respects_custom_iskeyword() {
9279 let mut e = editor_with("foo-bar baz");
9283 run_keys(&mut e, "w");
9284 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9285 let mut e2 = editor_with("foo-bar baz");
9288 e2.set_iskeyword("@,_,45");
9289 run_keys(&mut e2, "w");
9290 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9291 }
9292
9293 #[test]
9294 fn iskeyword_with_dash_treats_dash_as_word_char() {
9295 let mut e = editor_with("foo-bar baz");
9296 e.settings_mut().iskeyword = "@,_,45".to_string();
9297 e.jump_cursor(0, 0);
9298 run_keys(&mut e, "*");
9299 let p = e
9300 .search_state()
9301 .pattern
9302 .as_ref()
9303 .unwrap()
9304 .as_str()
9305 .to_string();
9306 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9307 }
9308
9309 #[test]
9310 fn timeoutlen_drops_pending_g_prefix() {
9311 use std::time::{Duration, Instant};
9312 let mut e = editor_with("a\nb\nc");
9313 e.jump_cursor(2, 0);
9314 run_keys(&mut e, "g");
9316 assert!(matches!(e.vim.pending, super::Pending::G));
9317 e.settings.timeout_len = Duration::from_nanos(0);
9325 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9326 e.vim.last_input_host_at = Some(Duration::ZERO);
9327 run_keys(&mut e, "g");
9331 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9333 }
9334
9335 #[test]
9336 fn undobreak_on_breaks_group_at_arrow_motion() {
9337 let mut e = editor_with("");
9338 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9340 let line = e.buffer.line(0).unwrap_or("").to_string();
9343 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9344 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9345 }
9346
9347 #[test]
9348 fn undobreak_off_keeps_full_run_in_one_group() {
9349 let mut e = editor_with("");
9350 e.settings_mut().undo_break_on_motion = false;
9351 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9352 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9355 }
9356
9357 #[test]
9358 fn undobreak_round_trips_through_options() {
9359 let e = editor_with("");
9360 let opts = e.current_options();
9361 assert!(opts.undo_break_on_motion);
9362 let mut e2 = editor_with("");
9363 let mut new_opts = opts.clone();
9364 new_opts.undo_break_on_motion = false;
9365 e2.apply_options(&new_opts);
9366 assert!(!e2.current_options().undo_break_on_motion);
9367 }
9368
9369 #[test]
9370 fn undo_levels_cap_drops_oldest() {
9371 let mut e = editor_with("abcde");
9372 e.settings_mut().undo_levels = 3;
9373 run_keys(&mut e, "ra");
9374 run_keys(&mut e, "lrb");
9375 run_keys(&mut e, "lrc");
9376 run_keys(&mut e, "lrd");
9377 run_keys(&mut e, "lre");
9378 assert_eq!(e.undo_stack_len(), 3);
9379 }
9380
9381 #[test]
9382 fn tab_inserts_literal_tab_when_noexpandtab() {
9383 let mut e = editor_with("");
9384 e.settings_mut().expandtab = false;
9387 e.settings_mut().softtabstop = 0;
9388 run_keys(&mut e, "i");
9389 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9390 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9391 }
9392
9393 #[test]
9394 fn tab_inserts_spaces_when_expandtab() {
9395 let mut e = editor_with("");
9396 e.settings_mut().expandtab = true;
9397 e.settings_mut().tabstop = 4;
9398 run_keys(&mut e, "i");
9399 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9400 assert_eq!(e.buffer.line(0).unwrap(), " ");
9401 }
9402
9403 #[test]
9404 fn tab_with_softtabstop_fills_to_next_boundary() {
9405 let mut e = editor_with("ab");
9407 e.settings_mut().expandtab = true;
9408 e.settings_mut().tabstop = 8;
9409 e.settings_mut().softtabstop = 4;
9410 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9412 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9413 }
9414
9415 #[test]
9416 fn backspace_deletes_softtab_run() {
9417 let mut e = editor_with(" x");
9420 e.settings_mut().softtabstop = 4;
9421 run_keys(&mut e, "fxi");
9423 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9424 assert_eq!(e.buffer.line(0).unwrap(), "x");
9425 }
9426
9427 #[test]
9428 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9429 let mut e = editor_with(" x");
9432 e.settings_mut().softtabstop = 4;
9433 run_keys(&mut e, "fxi");
9434 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9435 assert_eq!(e.buffer.line(0).unwrap(), " x");
9436 }
9437
9438 #[test]
9439 fn readonly_blocks_insert_mutation() {
9440 let mut e = editor_with("hello");
9441 e.settings_mut().readonly = true;
9442 run_keys(&mut e, "iX<Esc>");
9443 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9444 }
9445
9446 #[cfg(feature = "ratatui")]
9447 #[test]
9448 fn intern_ratatui_style_dedups_repeated_styles() {
9449 use ratatui::style::{Color, Style};
9450 let mut e = editor_with("");
9451 let red = Style::default().fg(Color::Red);
9452 let blue = Style::default().fg(Color::Blue);
9453 let id_r1 = e.intern_ratatui_style(red);
9454 let id_r2 = e.intern_ratatui_style(red);
9455 let id_b = e.intern_ratatui_style(blue);
9456 assert_eq!(id_r1, id_r2);
9457 assert_ne!(id_r1, id_b);
9458 assert_eq!(e.style_table().len(), 2);
9459 }
9460
9461 #[cfg(feature = "ratatui")]
9462 #[test]
9463 fn install_ratatui_syntax_spans_translates_styled_spans() {
9464 use ratatui::style::{Color, Style};
9465 let mut e = editor_with("SELECT foo");
9466 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9467 let by_row = e.buffer_spans();
9468 assert_eq!(by_row.len(), 1);
9469 assert_eq!(by_row[0].len(), 1);
9470 assert_eq!(by_row[0][0].start_byte, 0);
9471 assert_eq!(by_row[0][0].end_byte, 6);
9472 let id = by_row[0][0].style;
9473 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9474 }
9475
9476 #[cfg(feature = "ratatui")]
9477 #[test]
9478 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9479 use ratatui::style::{Color, Style};
9480 let mut e = editor_with("hello");
9481 e.install_ratatui_syntax_spans(vec![vec![(
9482 0,
9483 usize::MAX,
9484 Style::default().fg(Color::Blue),
9485 )]]);
9486 let by_row = e.buffer_spans();
9487 assert_eq!(by_row[0][0].end_byte, 5);
9488 }
9489
9490 #[cfg(feature = "ratatui")]
9491 #[test]
9492 fn install_ratatui_syntax_spans_drops_zero_width() {
9493 use ratatui::style::{Color, Style};
9494 let mut e = editor_with("abc");
9495 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9496 assert!(e.buffer_spans()[0].is_empty());
9497 }
9498
9499 #[test]
9500 fn named_register_yank_into_a_then_paste_from_a() {
9501 let mut e = editor_with("hello world\nsecond");
9502 run_keys(&mut e, "\"ayw");
9503 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9505 run_keys(&mut e, "j0\"aP");
9507 assert_eq!(e.buffer().lines()[1], "hello second");
9508 }
9509
9510 #[test]
9511 fn capital_r_overstrikes_chars() {
9512 let mut e = editor_with("hello");
9513 e.jump_cursor(0, 0);
9514 run_keys(&mut e, "RXY<Esc>");
9515 assert_eq!(e.buffer().lines()[0], "XYllo");
9517 }
9518
9519 #[test]
9520 fn capital_r_at_eol_appends() {
9521 let mut e = editor_with("hi");
9522 e.jump_cursor(0, 1);
9523 run_keys(&mut e, "RXYZ<Esc>");
9525 assert_eq!(e.buffer().lines()[0], "hXYZ");
9526 }
9527
9528 #[test]
9529 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9530 let mut e = editor_with("abc");
9534 e.jump_cursor(0, 0);
9535 run_keys(&mut e, "RX<Esc>");
9536 assert_eq!(e.buffer().lines()[0], "Xbc");
9537 }
9538
9539 #[test]
9540 fn ctrl_r_in_insert_pastes_named_register() {
9541 let mut e = editor_with("hello world");
9542 run_keys(&mut e, "\"ayw");
9544 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9545 run_keys(&mut e, "o");
9547 assert_eq!(e.vim_mode(), VimMode::Insert);
9548 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9549 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9550 assert_eq!(e.buffer().lines()[1], "hello ");
9551 assert_eq!(e.cursor(), (1, 6));
9553 assert_eq!(e.vim_mode(), VimMode::Insert);
9555 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9556 assert_eq!(e.buffer().lines()[1], "hello X");
9557 }
9558
9559 #[test]
9560 fn ctrl_r_with_unnamed_register() {
9561 let mut e = editor_with("foo");
9562 run_keys(&mut e, "yiw");
9563 run_keys(&mut e, "A ");
9564 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9566 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9567 assert_eq!(e.buffer().lines()[0], "foo foo");
9568 }
9569
9570 #[test]
9571 fn ctrl_r_unknown_selector_is_no_op() {
9572 let mut e = editor_with("abc");
9573 run_keys(&mut e, "A");
9574 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9575 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9578 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9579 assert_eq!(e.buffer().lines()[0], "abcZ");
9580 }
9581
9582 #[test]
9583 fn ctrl_r_multiline_register_pastes_with_newlines() {
9584 let mut e = editor_with("alpha\nbeta\ngamma");
9585 run_keys(&mut e, "\"byy");
9587 run_keys(&mut e, "j\"byy");
9588 run_keys(&mut e, "ggVj\"by");
9592 let payload = e.registers().read('b').unwrap().text.clone();
9593 assert!(payload.contains('\n'));
9594 run_keys(&mut e, "Go");
9595 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9596 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9597 let total_lines = e.buffer().lines().len();
9600 assert!(total_lines >= 5);
9601 }
9602
9603 #[test]
9604 fn yank_zero_holds_last_yank_after_delete() {
9605 let mut e = editor_with("hello world");
9606 run_keys(&mut e, "yw");
9607 let yanked = e.registers().read('0').unwrap().text.clone();
9608 assert!(!yanked.is_empty());
9609 run_keys(&mut e, "dw");
9611 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9612 assert!(!e.registers().read('1').unwrap().text.is_empty());
9614 }
9615
9616 #[test]
9617 fn delete_ring_rotates_through_one_through_nine() {
9618 let mut e = editor_with("a b c d e f g h i j");
9619 for _ in 0..3 {
9621 run_keys(&mut e, "dw");
9622 }
9623 let r1 = e.registers().read('1').unwrap().text.clone();
9625 let r2 = e.registers().read('2').unwrap().text.clone();
9626 let r3 = e.registers().read('3').unwrap().text.clone();
9627 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9628 assert_ne!(r1, r2);
9629 assert_ne!(r2, r3);
9630 }
9631
9632 #[test]
9633 fn capital_register_appends_to_lowercase() {
9634 let mut e = editor_with("foo bar");
9635 run_keys(&mut e, "\"ayw");
9636 let first = e.registers().read('a').unwrap().text.clone();
9637 assert!(first.contains("foo"));
9638 run_keys(&mut e, "w\"Ayw");
9640 let combined = e.registers().read('a').unwrap().text.clone();
9641 assert!(combined.starts_with(&first));
9642 assert!(combined.contains("bar"));
9643 }
9644
9645 #[test]
9646 fn zf_in_visual_line_creates_closed_fold() {
9647 let mut e = editor_with("a\nb\nc\nd\ne");
9648 e.jump_cursor(1, 0);
9650 run_keys(&mut e, "Vjjzf");
9651 assert_eq!(e.buffer().folds().len(), 1);
9652 let f = e.buffer().folds()[0];
9653 assert_eq!(f.start_row, 1);
9654 assert_eq!(f.end_row, 3);
9655 assert!(f.closed);
9656 }
9657
9658 #[test]
9659 fn zfj_in_normal_creates_two_row_fold() {
9660 let mut e = editor_with("a\nb\nc\nd\ne");
9661 e.jump_cursor(1, 0);
9662 run_keys(&mut e, "zfj");
9663 assert_eq!(e.buffer().folds().len(), 1);
9664 let f = e.buffer().folds()[0];
9665 assert_eq!(f.start_row, 1);
9666 assert_eq!(f.end_row, 2);
9667 assert!(f.closed);
9668 assert_eq!(e.cursor().0, 1);
9670 }
9671
9672 #[test]
9673 fn zf_with_count_folds_count_rows() {
9674 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9675 e.jump_cursor(0, 0);
9676 run_keys(&mut e, "zf3j");
9678 assert_eq!(e.buffer().folds().len(), 1);
9679 let f = e.buffer().folds()[0];
9680 assert_eq!(f.start_row, 0);
9681 assert_eq!(f.end_row, 3);
9682 }
9683
9684 #[test]
9685 fn zfk_folds_upward_range() {
9686 let mut e = editor_with("a\nb\nc\nd\ne");
9687 e.jump_cursor(3, 0);
9688 run_keys(&mut e, "zfk");
9689 let f = e.buffer().folds()[0];
9690 assert_eq!(f.start_row, 2);
9692 assert_eq!(f.end_row, 3);
9693 }
9694
9695 #[test]
9696 fn zf_capital_g_folds_to_bottom() {
9697 let mut e = editor_with("a\nb\nc\nd\ne");
9698 e.jump_cursor(1, 0);
9699 run_keys(&mut e, "zfG");
9701 let f = e.buffer().folds()[0];
9702 assert_eq!(f.start_row, 1);
9703 assert_eq!(f.end_row, 4);
9704 }
9705
9706 #[test]
9707 fn zfgg_folds_to_top_via_operator_pipeline() {
9708 let mut e = editor_with("a\nb\nc\nd\ne");
9709 e.jump_cursor(3, 0);
9710 run_keys(&mut e, "zfgg");
9714 let f = e.buffer().folds()[0];
9715 assert_eq!(f.start_row, 0);
9716 assert_eq!(f.end_row, 3);
9717 }
9718
9719 #[test]
9720 fn zfip_folds_paragraph_via_text_object() {
9721 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9722 e.jump_cursor(1, 0);
9723 run_keys(&mut e, "zfip");
9725 assert_eq!(e.buffer().folds().len(), 1);
9726 let f = e.buffer().folds()[0];
9727 assert_eq!(f.start_row, 0);
9728 assert_eq!(f.end_row, 2);
9729 }
9730
9731 #[test]
9732 fn zfap_folds_paragraph_with_trailing_blank() {
9733 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9734 e.jump_cursor(0, 0);
9735 run_keys(&mut e, "zfap");
9737 let f = e.buffer().folds()[0];
9738 assert_eq!(f.start_row, 0);
9739 assert_eq!(f.end_row, 3);
9740 }
9741
9742 #[test]
9743 fn zf_paragraph_motion_folds_to_blank() {
9744 let mut e = editor_with("alpha\nbeta\n\ngamma");
9745 e.jump_cursor(0, 0);
9746 run_keys(&mut e, "zf}");
9748 let f = e.buffer().folds()[0];
9749 assert_eq!(f.start_row, 0);
9750 assert_eq!(f.end_row, 2);
9751 }
9752
9753 #[test]
9754 fn za_toggles_fold_under_cursor() {
9755 let mut e = editor_with("a\nb\nc\nd");
9756 e.buffer_mut().add_fold(1, 2, true);
9757 e.jump_cursor(1, 0);
9758 run_keys(&mut e, "za");
9759 assert!(!e.buffer().folds()[0].closed);
9760 run_keys(&mut e, "za");
9761 assert!(e.buffer().folds()[0].closed);
9762 }
9763
9764 #[test]
9765 fn zr_opens_all_folds_zm_closes_all() {
9766 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9767 e.buffer_mut().add_fold(0, 1, true);
9768 e.buffer_mut().add_fold(2, 3, true);
9769 e.buffer_mut().add_fold(4, 5, true);
9770 run_keys(&mut e, "zR");
9771 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9772 run_keys(&mut e, "zM");
9773 assert!(e.buffer().folds().iter().all(|f| f.closed));
9774 }
9775
9776 #[test]
9777 fn ze_clears_all_folds() {
9778 let mut e = editor_with("a\nb\nc\nd");
9779 e.buffer_mut().add_fold(0, 1, true);
9780 e.buffer_mut().add_fold(2, 3, false);
9781 run_keys(&mut e, "zE");
9782 assert!(e.buffer().folds().is_empty());
9783 }
9784
9785 #[test]
9786 fn g_underscore_jumps_to_last_non_blank() {
9787 let mut e = editor_with("hello world ");
9788 run_keys(&mut e, "g_");
9789 assert_eq!(e.cursor().1, 10);
9791 }
9792
9793 #[test]
9794 fn gj_and_gk_alias_j_and_k() {
9795 let mut e = editor_with("a\nb\nc");
9796 run_keys(&mut e, "gj");
9797 assert_eq!(e.cursor().0, 1);
9798 run_keys(&mut e, "gk");
9799 assert_eq!(e.cursor().0, 0);
9800 }
9801
9802 #[test]
9803 fn paragraph_motions_walk_blank_lines() {
9804 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9805 run_keys(&mut e, "}");
9806 assert_eq!(e.cursor().0, 2);
9807 run_keys(&mut e, "}");
9808 assert_eq!(e.cursor().0, 5);
9809 run_keys(&mut e, "{");
9810 assert_eq!(e.cursor().0, 2);
9811 }
9812
9813 #[test]
9814 fn gv_reenters_last_visual_selection() {
9815 let mut e = editor_with("alpha\nbeta\ngamma");
9816 run_keys(&mut e, "Vj");
9817 run_keys(&mut e, "<Esc>");
9819 assert_eq!(e.vim_mode(), VimMode::Normal);
9820 run_keys(&mut e, "gv");
9822 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9823 }
9824
9825 #[test]
9826 fn o_in_visual_swaps_anchor_and_cursor() {
9827 let mut e = editor_with("hello world");
9828 run_keys(&mut e, "vllll");
9830 assert_eq!(e.cursor().1, 4);
9831 run_keys(&mut e, "o");
9833 assert_eq!(e.cursor().1, 0);
9834 assert_eq!(e.vim.visual_anchor, (0, 4));
9836 }
9837
9838 #[test]
9839 fn editing_inside_fold_invalidates_it() {
9840 let mut e = editor_with("a\nb\nc\nd");
9841 e.buffer_mut().add_fold(1, 2, true);
9842 e.jump_cursor(1, 0);
9843 run_keys(&mut e, "iX<Esc>");
9845 assert!(e.buffer().folds().is_empty());
9847 }
9848
9849 #[test]
9850 fn zd_removes_fold_under_cursor() {
9851 let mut e = editor_with("a\nb\nc\nd");
9852 e.buffer_mut().add_fold(1, 2, true);
9853 e.jump_cursor(2, 0);
9854 run_keys(&mut e, "zd");
9855 assert!(e.buffer().folds().is_empty());
9856 }
9857
9858 #[test]
9859 fn take_fold_ops_observes_z_keystroke_dispatch() {
9860 use crate::types::FoldOp;
9865 let mut e = editor_with("a\nb\nc\nd");
9866 e.buffer_mut().add_fold(1, 2, true);
9867 e.jump_cursor(1, 0);
9868 let _ = e.take_fold_ops();
9871 run_keys(&mut e, "zo");
9872 run_keys(&mut e, "zM");
9873 let ops = e.take_fold_ops();
9874 assert_eq!(ops.len(), 2);
9875 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9876 assert!(matches!(ops[1], FoldOp::CloseAll));
9877 assert!(e.take_fold_ops().is_empty());
9879 }
9880
9881 #[test]
9882 fn edit_pipeline_emits_invalidate_fold_op() {
9883 use crate::types::FoldOp;
9886 let mut e = editor_with("a\nb\nc\nd");
9887 e.buffer_mut().add_fold(1, 2, true);
9888 e.jump_cursor(1, 0);
9889 let _ = e.take_fold_ops();
9890 run_keys(&mut e, "iX<Esc>");
9891 let ops = e.take_fold_ops();
9892 assert!(
9893 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9894 "expected at least one Invalidate op, got {ops:?}"
9895 );
9896 }
9897
9898 #[test]
9899 fn dot_mark_jumps_to_last_edit_position() {
9900 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9901 e.jump_cursor(2, 0);
9902 run_keys(&mut e, "iX<Esc>");
9904 let after_edit = e.cursor();
9905 run_keys(&mut e, "gg");
9907 assert_eq!(e.cursor().0, 0);
9908 run_keys(&mut e, "'.");
9910 assert_eq!(e.cursor().0, after_edit.0);
9911 }
9912
9913 #[test]
9914 fn quote_quote_returns_to_pre_jump_position() {
9915 let mut e = editor_with_rows(50, 20);
9916 e.jump_cursor(10, 2);
9917 let before = e.cursor();
9918 run_keys(&mut e, "G");
9920 assert_ne!(e.cursor(), before);
9921 run_keys(&mut e, "''");
9923 assert_eq!(e.cursor().0, before.0);
9924 }
9925
9926 #[test]
9927 fn backtick_backtick_restores_exact_pre_jump_pos() {
9928 let mut e = editor_with_rows(50, 20);
9929 e.jump_cursor(7, 3);
9930 let before = e.cursor();
9931 run_keys(&mut e, "G");
9932 run_keys(&mut e, "``");
9933 assert_eq!(e.cursor(), before);
9934 }
9935
9936 #[test]
9937 fn macro_record_and_replay_basic() {
9938 let mut e = editor_with("foo\nbar\nbaz");
9939 run_keys(&mut e, "qaIX<Esc>jq");
9941 assert_eq!(e.buffer().lines()[0], "Xfoo");
9942 run_keys(&mut e, "@a");
9944 assert_eq!(e.buffer().lines()[1], "Xbar");
9945 run_keys(&mut e, "j@@");
9947 assert_eq!(e.buffer().lines()[2], "Xbaz");
9948 }
9949
9950 #[test]
9951 fn macro_count_replays_n_times() {
9952 let mut e = editor_with("a\nb\nc\nd\ne");
9953 run_keys(&mut e, "qajq");
9955 assert_eq!(e.cursor().0, 1);
9956 run_keys(&mut e, "3@a");
9958 assert_eq!(e.cursor().0, 4);
9959 }
9960
9961 #[test]
9962 fn macro_capital_q_appends_to_lowercase_register() {
9963 let mut e = editor_with("hello");
9964 run_keys(&mut e, "qall<Esc>q");
9965 run_keys(&mut e, "qAhh<Esc>q");
9966 let text = e.registers().read('a').unwrap().text.clone();
9969 assert!(text.contains("ll<Esc>"));
9970 assert!(text.contains("hh<Esc>"));
9971 }
9972
9973 #[test]
9974 fn buffer_selection_block_in_visual_block_mode() {
9975 use hjkl_buffer::{Position, Selection};
9976 let mut e = editor_with("aaaa\nbbbb\ncccc");
9977 run_keys(&mut e, "<C-v>jl");
9978 assert_eq!(
9979 e.buffer_selection(),
9980 Some(Selection::Block {
9981 anchor: Position::new(0, 0),
9982 head: Position::new(1, 1),
9983 })
9984 );
9985 }
9986
9987 #[test]
9990 fn n_after_question_mark_keeps_walking_backward() {
9991 let mut e = editor_with("foo bar foo baz foo end");
9994 e.jump_cursor(0, 22);
9995 run_keys(&mut e, "?foo<CR>");
9996 assert_eq!(e.cursor().1, 16);
9997 run_keys(&mut e, "n");
9998 assert_eq!(e.cursor().1, 8);
9999 run_keys(&mut e, "N");
10000 assert_eq!(e.cursor().1, 16);
10001 }
10002
10003 #[test]
10004 fn nested_macro_chord_records_literal_keys() {
10005 let mut e = editor_with("alpha\nbeta\ngamma");
10008 run_keys(&mut e, "qblq");
10010 run_keys(&mut e, "qaIX<Esc>q");
10013 e.jump_cursor(1, 0);
10015 run_keys(&mut e, "@a");
10016 assert_eq!(e.buffer().lines()[1], "Xbeta");
10017 }
10018
10019 #[test]
10020 fn shift_gt_motion_indents_one_line() {
10021 let mut e = editor_with("hello world");
10025 run_keys(&mut e, ">w");
10026 assert_eq!(e.buffer().lines()[0], " hello world");
10027 }
10028
10029 #[test]
10030 fn shift_lt_motion_outdents_one_line() {
10031 let mut e = editor_with(" hello world");
10032 run_keys(&mut e, "<lt>w");
10033 assert_eq!(e.buffer().lines()[0], " hello world");
10035 }
10036
10037 #[test]
10038 fn shift_gt_text_object_indents_paragraph() {
10039 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
10040 e.jump_cursor(0, 0);
10041 run_keys(&mut e, ">ip");
10042 assert_eq!(e.buffer().lines()[0], " alpha");
10043 assert_eq!(e.buffer().lines()[1], " beta");
10044 assert_eq!(e.buffer().lines()[2], " gamma");
10045 assert_eq!(e.buffer().lines()[4], "rest");
10047 }
10048
10049 #[test]
10050 fn ctrl_o_runs_exactly_one_normal_command() {
10051 let mut e = editor_with("alpha beta gamma");
10054 e.jump_cursor(0, 0);
10055 run_keys(&mut e, "i");
10056 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
10057 run_keys(&mut e, "dw");
10058 assert_eq!(e.vim_mode(), VimMode::Insert);
10060 run_keys(&mut e, "X");
10062 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
10063 }
10064
10065 #[test]
10066 fn macro_replay_respects_mode_switching() {
10067 let mut e = editor_with("hi");
10071 run_keys(&mut e, "qaiX<Esc>0q");
10072 assert_eq!(e.vim_mode(), VimMode::Normal);
10073 e.set_content("yo");
10075 run_keys(&mut e, "@a");
10076 assert_eq!(e.vim_mode(), VimMode::Normal);
10077 assert_eq!(e.cursor().1, 0);
10078 assert_eq!(e.buffer().lines()[0], "Xyo");
10079 }
10080
10081 #[test]
10082 fn macro_recorded_text_round_trips_through_register() {
10083 let mut e = editor_with("");
10087 run_keys(&mut e, "qaiX<Esc>q");
10088 let text = e.registers().read('a').unwrap().text.clone();
10089 assert!(text.starts_with("iX"));
10090 run_keys(&mut e, "@a");
10092 assert_eq!(e.buffer().lines()[0], "XX");
10093 }
10094
10095 #[test]
10096 fn dot_after_macro_replays_macros_last_change() {
10097 let mut e = editor_with("ab\ncd\nef");
10100 run_keys(&mut e, "qaIX<Esc>jq");
10103 assert_eq!(e.buffer().lines()[0], "Xab");
10104 run_keys(&mut e, "@a");
10105 assert_eq!(e.buffer().lines()[1], "Xcd");
10106 let row_before_dot = e.cursor().0;
10109 run_keys(&mut e, ".");
10110 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10111 }
10112
10113 fn si_editor(content: &str) -> Editor {
10119 let opts = crate::types::Options {
10120 shiftwidth: 4,
10121 softtabstop: 4,
10122 expandtab: true,
10123 smartindent: true,
10124 autoindent: true,
10125 ..crate::types::Options::default()
10126 };
10127 let mut e = Editor::new(
10128 hjkl_buffer::Buffer::new(),
10129 crate::types::DefaultHost::new(),
10130 opts,
10131 );
10132 e.set_content(content);
10133 e
10134 }
10135
10136 #[test]
10137 fn smartindent_bumps_indent_after_open_brace() {
10138 let mut e = si_editor("fn foo() {");
10140 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10142 assert_eq!(
10143 e.buffer().lines()[1],
10144 " ",
10145 "smartindent should bump one shiftwidth after {{"
10146 );
10147 }
10148
10149 #[test]
10150 fn smartindent_no_bump_when_off() {
10151 let mut e = si_editor("fn foo() {");
10154 e.settings_mut().smartindent = false;
10155 e.jump_cursor(0, 10);
10156 run_keys(&mut e, "i<CR>");
10157 assert_eq!(
10158 e.buffer().lines()[1],
10159 "",
10160 "without smartindent, no bump: new line copies empty leading ws"
10161 );
10162 }
10163
10164 #[test]
10165 fn smartindent_uses_tab_when_noexpandtab() {
10166 let opts = crate::types::Options {
10168 shiftwidth: 4,
10169 softtabstop: 0,
10170 expandtab: false,
10171 smartindent: true,
10172 autoindent: true,
10173 ..crate::types::Options::default()
10174 };
10175 let mut e = Editor::new(
10176 hjkl_buffer::Buffer::new(),
10177 crate::types::DefaultHost::new(),
10178 opts,
10179 );
10180 e.set_content("fn foo() {");
10181 e.jump_cursor(0, 10);
10182 run_keys(&mut e, "i<CR>");
10183 assert_eq!(
10184 e.buffer().lines()[1],
10185 "\t",
10186 "noexpandtab: smartindent bump inserts a literal tab"
10187 );
10188 }
10189
10190 #[test]
10191 fn smartindent_dedent_on_close_brace() {
10192 let mut e = si_editor("fn foo() {");
10195 e.set_content("fn foo() {\n ");
10197 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10199 assert_eq!(
10200 e.buffer().lines()[1],
10201 "}",
10202 "close brace on whitespace-only line should dedent"
10203 );
10204 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10205 }
10206
10207 #[test]
10208 fn smartindent_no_dedent_when_off() {
10209 let mut e = si_editor("fn foo() {\n ");
10211 e.settings_mut().smartindent = false;
10212 e.jump_cursor(1, 4);
10213 run_keys(&mut e, "i}");
10214 assert_eq!(
10215 e.buffer().lines()[1],
10216 " }",
10217 "without smartindent, `}}` just appends at cursor"
10218 );
10219 }
10220
10221 #[test]
10222 fn smartindent_no_dedent_mid_line() {
10223 let mut e = si_editor(" let x = 1");
10226 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10228 assert_eq!(
10229 e.buffer().lines()[0],
10230 " let x = 1}",
10231 "mid-line `}}` should not dedent"
10232 );
10233 }
10234
10235 #[test]
10239 fn count_5x_fills_unnamed_register() {
10240 let mut e = editor_with("hello world\n");
10241 e.jump_cursor(0, 0);
10242 run_keys(&mut e, "5x");
10243 assert_eq!(e.buffer().lines()[0], " world");
10244 assert_eq!(e.cursor(), (0, 0));
10245 assert_eq!(e.yank(), "hello");
10246 }
10247
10248 #[test]
10249 fn x_fills_unnamed_register_single_char() {
10250 let mut e = editor_with("abc\n");
10251 e.jump_cursor(0, 0);
10252 run_keys(&mut e, "x");
10253 assert_eq!(e.buffer().lines()[0], "bc");
10254 assert_eq!(e.yank(), "a");
10255 }
10256
10257 #[test]
10258 fn big_x_fills_unnamed_register() {
10259 let mut e = editor_with("hello\n");
10260 e.jump_cursor(0, 3);
10261 run_keys(&mut e, "X");
10262 assert_eq!(e.buffer().lines()[0], "helo");
10263 assert_eq!(e.yank(), "l");
10264 }
10265
10266 #[test]
10268 fn g_motion_trailing_newline_lands_on_last_content_row() {
10269 let mut e = editor_with("foo\nbar\nbaz\n");
10270 e.jump_cursor(0, 0);
10271 run_keys(&mut e, "G");
10272 assert_eq!(
10274 e.cursor().0,
10275 2,
10276 "G should land on row 2 (baz), not row 3 (phantom empty)"
10277 );
10278 }
10279
10280 #[test]
10282 fn dd_last_line_clamps_cursor_to_new_last_row() {
10283 let mut e = editor_with("foo\nbar\n");
10284 e.jump_cursor(1, 0);
10285 run_keys(&mut e, "dd");
10286 assert_eq!(e.buffer().lines()[0], "foo");
10287 assert_eq!(
10288 e.cursor(),
10289 (0, 0),
10290 "cursor should clamp to row 0 after dd on last content line"
10291 );
10292 }
10293
10294 #[test]
10296 fn d_dollar_cursor_on_last_char() {
10297 let mut e = editor_with("hello world\n");
10298 e.jump_cursor(0, 5);
10299 run_keys(&mut e, "d$");
10300 assert_eq!(e.buffer().lines()[0], "hello");
10301 assert_eq!(
10302 e.cursor(),
10303 (0, 4),
10304 "d$ should leave cursor on col 4, not col 5"
10305 );
10306 }
10307
10308 #[test]
10310 fn undo_insert_clamps_cursor_to_last_valid_col() {
10311 let mut e = editor_with("hello\n");
10312 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10314 assert_eq!(e.buffer().lines()[0], "hello");
10315 assert_eq!(
10316 e.cursor(),
10317 (0, 4),
10318 "undo should clamp cursor to col 4 on 'hello'"
10319 );
10320 }
10321
10322 #[test]
10324 fn da_doublequote_eats_trailing_whitespace() {
10325 let mut e = editor_with("say \"hello\" there\n");
10326 e.jump_cursor(0, 6);
10327 run_keys(&mut e, "da\"");
10328 assert_eq!(e.buffer().lines()[0], "say there");
10329 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10330 }
10331
10332 #[test]
10334 fn dab_cursor_col_clamped_after_delete() {
10335 let mut e = editor_with("fn x() {\n body\n}\n");
10336 e.jump_cursor(1, 4);
10337 run_keys(&mut e, "daB");
10338 assert_eq!(e.buffer().lines()[0], "fn x() ");
10339 assert_eq!(
10340 e.cursor(),
10341 (0, 6),
10342 "daB should leave cursor at col 6, not 7"
10343 );
10344 }
10345
10346 #[test]
10348 fn dib_preserves_surrounding_newlines() {
10349 let mut e = editor_with("{\n body\n}\n");
10350 e.jump_cursor(1, 4);
10351 run_keys(&mut e, "diB");
10352 assert_eq!(e.buffer().lines()[0], "{");
10353 assert_eq!(e.buffer().lines()[1], "}");
10354 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10355 }
10356
10357 #[test]
10358 fn is_chord_pending_tracks_replace_state() {
10359 let mut e = editor_with("abc\n");
10360 assert!(!e.is_chord_pending());
10361 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10363 assert!(e.is_chord_pending(), "engine should be pending after r");
10364 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10366 assert!(
10367 !e.is_chord_pending(),
10368 "engine pending should clear after replace"
10369 );
10370 }
10371
10372 #[test]
10375 fn yiw_sets_lbr_rbr_marks_around_word() {
10376 let mut e = editor_with("hello world");
10379 run_keys(&mut e, "yiw");
10380 let lo = e.mark('[').expect("'[' must be set after yiw");
10381 let hi = e.mark(']').expect("']' must be set after yiw");
10382 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10383 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10384 }
10385
10386 #[test]
10387 fn yj_linewise_sets_marks_at_line_edges() {
10388 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10391 run_keys(&mut e, "yj");
10392 let lo = e.mark('[').expect("'[' must be set after yj");
10393 let hi = e.mark(']').expect("']' must be set after yj");
10394 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10395 assert_eq!(
10396 hi,
10397 (1, 4),
10398 "'] snaps to (bot_row, last_col) for linewise yank"
10399 );
10400 }
10401
10402 #[test]
10403 fn dd_sets_lbr_rbr_marks_to_cursor() {
10404 let mut e = editor_with("aaa\nbbb");
10407 run_keys(&mut e, "dd");
10408 let lo = e.mark('[').expect("'[' must be set after dd");
10409 let hi = e.mark(']').expect("']' must be set after dd");
10410 assert_eq!(lo, hi, "after delete both marks are at the same position");
10411 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10412 }
10413
10414 #[test]
10415 fn dw_sets_lbr_rbr_marks_to_cursor() {
10416 let mut e = editor_with("hello world");
10419 run_keys(&mut e, "dw");
10420 let lo = e.mark('[').expect("'[' must be set after dw");
10421 let hi = e.mark(']').expect("']' must be set after dw");
10422 assert_eq!(lo, hi, "after delete both marks are at the same position");
10423 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10424 }
10425
10426 #[test]
10427 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10428 let mut e = editor_with("hello world");
10433 run_keys(&mut e, "cwfoo<Esc>");
10434 let lo = e.mark('[').expect("'[' must be set after cw");
10435 let hi = e.mark(']').expect("']' must be set after cw");
10436 assert_eq!(lo, (0, 0), "'[ should be start of change");
10437 assert_eq!(hi.0, 0, "'] should be on row 0");
10440 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10441 }
10442
10443 #[test]
10444 fn cw_with_no_insertion_sets_marks_at_change_start() {
10445 let mut e = editor_with("hello world");
10448 run_keys(&mut e, "cw<Esc>");
10449 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10450 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10451 assert_eq!(lo.0, 0, "'[ should be on row 0");
10452 assert_eq!(hi.0, 0, "'] should be on row 0");
10453 assert_eq!(lo, hi, "marks coincide when insert is empty");
10455 }
10456
10457 #[test]
10458 fn p_charwise_sets_marks_around_pasted_text() {
10459 let mut e = editor_with("abc xyz");
10462 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10465 let hi = e.mark(']').expect("']' set after charwise paste");
10466 assert!(lo <= hi, "'[ must not exceed ']'");
10467 assert_eq!(
10469 hi.1.wrapping_sub(lo.1),
10470 2,
10471 "'] - '[ should span 2 cols for a 3-char paste"
10472 );
10473 }
10474
10475 #[test]
10476 fn p_linewise_sets_marks_at_line_edges() {
10477 let mut e = editor_with("aaa\nbbb\nccc");
10480 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10484 let hi = e.mark(']').expect("']' set after linewise paste");
10485 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10486 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10487 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10488 }
10489
10490 #[test]
10491 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10492 let mut e = editor_with("hello world");
10496 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10500 assert_eq!(
10502 e.cursor(),
10503 (0, 4),
10504 "visual `[v`] should land on last yanked char"
10505 );
10506 assert_eq!(
10508 e.vim_mode(),
10509 crate::VimMode::Visual,
10510 "should be in Visual mode"
10511 );
10512 }
10513
10514 #[test]
10520 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10521 let mut e = editor_with("hello\nworld\n");
10524 e.jump_cursor(0, 0);
10525 run_keys(&mut e, "iX<Esc>j`.");
10526 assert_eq!(
10527 e.cursor(),
10528 (0, 0),
10529 "dot mark should jump to the change-start (col 0), not post-insert col"
10530 );
10531 }
10532
10533 #[test]
10536 fn count_100g_clamps_to_last_content_row() {
10537 let mut e = editor_with("foo\nbar\nbaz\n");
10540 e.jump_cursor(0, 0);
10541 run_keys(&mut e, "100G");
10542 assert_eq!(
10543 e.cursor(),
10544 (2, 0),
10545 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10546 );
10547 }
10548
10549 #[test]
10552 fn gi_resumes_last_insert_position() {
10553 let mut e = editor_with("world\nhello\n");
10559 e.jump_cursor(0, 0);
10560 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10561 assert_eq!(
10562 e.vim_mode(),
10563 crate::VimMode::Normal,
10564 "should be in Normal mode after gi<Esc>"
10565 );
10566 assert_eq!(
10567 e.cursor(),
10568 (0, 1),
10569 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10570 );
10571 }
10572
10573 #[test]
10577 fn visual_block_change_cursor_on_last_inserted_char() {
10578 let mut e = editor_with("foo\nbar\nbaz\n");
10582 e.jump_cursor(0, 0);
10583 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10584 let lines = e.buffer().lines().to_vec();
10585 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10586 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10587 assert_eq!(
10588 e.cursor(),
10589 (0, 1),
10590 "cursor should be on last char of inserted 'ZZ' (col 1)"
10591 );
10592 }
10593
10594 #[test]
10599 fn register_blackhole_delete_preserves_unnamed_register() {
10600 let mut e = editor_with("foo bar baz\n");
10607 e.jump_cursor(0, 0);
10608 run_keys(&mut e, "yiww\"_dwbp");
10609 let lines = e.buffer().lines().to_vec();
10610 assert_eq!(
10611 lines[0], "ffoooo baz",
10612 "black-hole delete must not corrupt unnamed register"
10613 );
10614 assert_eq!(
10615 e.cursor(),
10616 (0, 3),
10617 "cursor should be on last pasted char (col 3)"
10618 );
10619 }
10620
10621 #[test]
10624 fn after_z_zz_sets_viewport_pinned() {
10625 let mut e = editor_with("a\nb\nc\nd\ne");
10626 e.jump_cursor(2, 0);
10627 e.after_z('z', 1);
10628 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10629 }
10630
10631 #[test]
10632 fn after_z_zo_opens_fold_at_cursor() {
10633 let mut e = editor_with("a\nb\nc\nd");
10634 e.buffer_mut().add_fold(1, 2, true);
10635 e.jump_cursor(1, 0);
10636 e.after_z('o', 1);
10637 assert!(
10638 !e.buffer().folds()[0].closed,
10639 "zo must open the fold at the cursor row"
10640 );
10641 }
10642
10643 #[test]
10644 fn after_z_zm_closes_all_folds() {
10645 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10646 e.buffer_mut().add_fold(0, 1, false);
10647 e.buffer_mut().add_fold(4, 5, false);
10648 e.after_z('M', 1);
10649 assert!(
10650 e.buffer().folds().iter().all(|f| f.closed),
10651 "zM must close all folds"
10652 );
10653 }
10654
10655 #[test]
10656 fn after_z_zd_removes_fold_at_cursor() {
10657 let mut e = editor_with("a\nb\nc\nd");
10658 e.buffer_mut().add_fold(1, 2, true);
10659 e.jump_cursor(1, 0);
10660 e.after_z('d', 1);
10661 assert!(
10662 e.buffer().folds().is_empty(),
10663 "zd must remove the fold at the cursor row"
10664 );
10665 }
10666
10667 #[test]
10668 fn after_z_zf_in_visual_creates_fold() {
10669 let mut e = editor_with("a\nb\nc\nd\ne");
10670 e.jump_cursor(1, 0);
10672 run_keys(&mut e, "V2j");
10673 e.after_z('f', 1);
10675 let folds = e.buffer().folds();
10676 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10677 assert_eq!(folds[0].start_row, 1);
10678 assert_eq!(folds[0].end_row, 3);
10679 assert!(folds[0].closed);
10680 }
10681
10682 #[test]
10685 fn apply_op_motion_dw_deletes_word() {
10686 let mut e = editor_with("hello world");
10688 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
10689 assert_eq!(
10690 e.buffer().lines().first().cloned().unwrap_or_default(),
10691 "world"
10692 );
10693 }
10694
10695 #[test]
10696 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
10697 let mut e = editor_with("hello world");
10699 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
10700 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10703 assert!(
10704 line.starts_with(' ') || line == " world",
10705 "cw quirk: got {line:?}"
10706 );
10707 assert_eq!(e.vim_mode(), VimMode::Insert);
10708 }
10709
10710 #[test]
10711 fn apply_op_double_dd_deletes_line() {
10712 let mut e = editor_with("line1\nline2\nline3");
10713 e.apply_op_double(crate::vim::Operator::Delete, 1);
10715 let lines: Vec<_> = e.buffer().lines().to_vec();
10716 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
10717 }
10718
10719 #[test]
10720 fn apply_op_double_yy_does_not_modify_buffer() {
10721 let mut e = editor_with("hello");
10722 e.apply_op_double(crate::vim::Operator::Yank, 1);
10723 assert_eq!(
10724 e.buffer().lines().first().cloned().unwrap_or_default(),
10725 "hello"
10726 );
10727 }
10728
10729 #[test]
10730 fn apply_op_double_dd_count2_deletes_two_lines() {
10731 let mut e = editor_with("line1\nline2\nline3");
10732 e.apply_op_double(crate::vim::Operator::Delete, 2);
10733 let lines: Vec<_> = e.buffer().lines().to_vec();
10734 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
10735 }
10736
10737 #[test]
10738 fn apply_op_motion_unknown_key_is_noop() {
10739 let mut e = editor_with("hello");
10741 let before = e.cursor();
10742 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
10744 assert_eq!(
10745 e.buffer().lines().first().cloned().unwrap_or_default(),
10746 "hello"
10747 );
10748 }
10749
10750 #[test]
10753 fn apply_op_find_dfx_deletes_to_x() {
10754 let mut e = editor_with("hello x world");
10756 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10757 assert_eq!(
10758 e.buffer().lines().first().cloned().unwrap_or_default(),
10759 " world",
10760 "dfx must delete 'hello x'"
10761 );
10762 }
10763
10764 #[test]
10765 fn apply_op_find_dtx_deletes_up_to_x() {
10766 let mut e = editor_with("hello x world");
10768 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
10769 assert_eq!(
10770 e.buffer().lines().first().cloned().unwrap_or_default(),
10771 "x world",
10772 "dtx must delete 'hello ' leaving 'x world'"
10773 );
10774 }
10775
10776 #[test]
10777 fn apply_op_find_records_last_find() {
10778 let mut e = editor_with("hello x world");
10780 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10781 let _ = e.cursor(); }
10788
10789 #[test]
10792 fn apply_op_text_obj_diw_deletes_word() {
10793 let mut e = editor_with("hello world");
10795 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', true, 1);
10796 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10797 assert!(
10802 !line.contains("hello"),
10803 "diw must delete 'hello', remaining: {line:?}"
10804 );
10805 }
10806
10807 #[test]
10808 fn apply_op_text_obj_daw_deletes_around_word() {
10809 let mut e = editor_with("hello world");
10811 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', false, 1);
10812 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10813 assert!(
10814 !line.contains("hello"),
10815 "daw must delete 'hello' and surrounding space, remaining: {line:?}"
10816 );
10817 }
10818
10819 #[test]
10820 fn apply_op_text_obj_invalid_char_no_op() {
10821 let mut e = editor_with("hello world");
10823 let before = e.buffer().as_string();
10824 e.apply_op_text_obj(crate::vim::Operator::Delete, 'X', true, 1);
10825 assert_eq!(
10826 e.buffer().as_string(),
10827 before,
10828 "unknown text-object char must be a no-op"
10829 );
10830 }
10831
10832 #[test]
10835 fn apply_op_g_dgg_deletes_to_top() {
10836 let mut e = editor_with("line1\nline2\nline3");
10838 e.apply_op_motion(crate::vim::Operator::Delete, 'j', 1);
10840 e.apply_op_g(crate::vim::Operator::Delete, 'g', 1);
10843 let lines: Vec<_> = e.buffer().lines().to_vec();
10845 assert_eq!(lines, vec!["line3"], "dgg must delete to file top");
10846 }
10847
10848 #[test]
10849 fn apply_op_g_dge_deletes_word_end_back() {
10850 let mut e = editor_with("hello world");
10863 let before = e.buffer().as_string();
10864 e.apply_op_g(crate::vim::Operator::Delete, 'X', 1);
10866 assert_eq!(
10867 e.buffer().as_string(),
10868 before,
10869 "apply_op_g with unknown char must be a no-op"
10870 );
10871 e.apply_op_g(crate::vim::Operator::Delete, 'e', 1);
10873 }
10875
10876 #[test]
10877 fn apply_op_g_dgj_deletes_screen_down() {
10878 let mut e = editor_with("line1\nline2\nline3");
10881 e.apply_op_g(crate::vim::Operator::Delete, 'j', 1);
10882 let lines: Vec<_> = e.buffer().lines().to_vec();
10883 assert_eq!(lines, vec!["line3"], "dgj must delete current+next line");
10885 }
10886
10887 fn blank_editor() -> Editor {
10890 Editor::new(
10891 hjkl_buffer::Buffer::new(),
10892 crate::types::DefaultHost::new(),
10893 crate::types::Options::default(),
10894 )
10895 }
10896
10897 #[test]
10898 fn set_pending_register_valid_letter_sets_field() {
10899 let mut e = blank_editor();
10900 assert!(e.vim.pending_register.is_none());
10901 e.set_pending_register('a');
10902 assert_eq!(e.vim.pending_register, Some('a'));
10903 }
10904
10905 #[test]
10906 fn set_pending_register_invalid_char_no_op() {
10907 let mut e = blank_editor();
10908 e.set_pending_register('!');
10909 assert!(
10910 e.vim.pending_register.is_none(),
10911 "invalid register char must not set pending_register"
10912 );
10913 }
10914
10915 #[test]
10916 fn set_pending_register_special_plus_sets_field() {
10917 let mut e = blank_editor();
10919 e.set_pending_register('+');
10920 assert_eq!(e.vim.pending_register, Some('+'));
10921 }
10922
10923 #[test]
10924 fn set_pending_register_star_sets_field() {
10925 let mut e = blank_editor();
10927 e.set_pending_register('*');
10928 assert_eq!(e.vim.pending_register, Some('*'));
10929 }
10930
10931 #[test]
10932 fn set_pending_register_underscore_sets_field() {
10933 let mut e = blank_editor();
10935 e.set_pending_register('_');
10936 assert_eq!(e.vim.pending_register, Some('_'));
10937 }
10938}