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