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>(
2132 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2133 input: Input,
2134) -> bool {
2135 if let Key::Char(c) = input.key
2136 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*' | '_'))
2137 {
2138 ed.vim.pending_register = Some(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
2475fn 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
2510fn apply_sticky_col<H: crate::types::Host>(
2515 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2516 motion: &Motion,
2517 pre_col: usize,
2518) {
2519 if is_vertical_motion(motion) {
2520 let want = ed.sticky_col.unwrap_or(pre_col);
2521 ed.sticky_col = Some(want);
2524 let (row, _) = ed.cursor();
2525 let line_len = buf_line_chars(&ed.buffer, row);
2526 let max_col = line_len.saturating_sub(1);
2530 let target = want.min(max_col);
2531 ed.jump_cursor(row, target);
2532 } else {
2533 ed.sticky_col = Some(ed.cursor().1);
2536 }
2537}
2538
2539fn is_vertical_motion(motion: &Motion) -> bool {
2540 matches!(
2544 motion,
2545 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2546 )
2547}
2548
2549fn apply_motion_cursor<H: crate::types::Host>(
2550 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2551 motion: &Motion,
2552 count: usize,
2553) {
2554 apply_motion_cursor_ctx(ed, motion, count, false)
2555}
2556
2557fn apply_motion_cursor_ctx<H: crate::types::Host>(
2558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2559 motion: &Motion,
2560 count: usize,
2561 as_operator: bool,
2562) {
2563 match motion {
2564 Motion::Left => {
2565 crate::motions::move_left(&mut ed.buffer, count);
2567 ed.push_buffer_cursor_to_textarea();
2568 }
2569 Motion::Right => {
2570 if as_operator {
2574 crate::motions::move_right_to_end(&mut ed.buffer, count);
2575 } else {
2576 crate::motions::move_right_in_line(&mut ed.buffer, count);
2577 }
2578 ed.push_buffer_cursor_to_textarea();
2579 }
2580 Motion::Up => {
2581 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2585 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2586 ed.push_buffer_cursor_to_textarea();
2587 }
2588 Motion::Down => {
2589 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2590 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2591 ed.push_buffer_cursor_to_textarea();
2592 }
2593 Motion::ScreenUp => {
2594 let v = *ed.host.viewport();
2595 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2596 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2597 ed.push_buffer_cursor_to_textarea();
2598 }
2599 Motion::ScreenDown => {
2600 let v = *ed.host.viewport();
2601 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2602 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2603 ed.push_buffer_cursor_to_textarea();
2604 }
2605 Motion::WordFwd => {
2606 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2607 ed.push_buffer_cursor_to_textarea();
2608 }
2609 Motion::WordBack => {
2610 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2611 ed.push_buffer_cursor_to_textarea();
2612 }
2613 Motion::WordEnd => {
2614 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2615 ed.push_buffer_cursor_to_textarea();
2616 }
2617 Motion::BigWordFwd => {
2618 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2619 ed.push_buffer_cursor_to_textarea();
2620 }
2621 Motion::BigWordBack => {
2622 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2623 ed.push_buffer_cursor_to_textarea();
2624 }
2625 Motion::BigWordEnd => {
2626 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2627 ed.push_buffer_cursor_to_textarea();
2628 }
2629 Motion::WordEndBack => {
2630 crate::motions::move_word_end_back(
2631 &mut ed.buffer,
2632 false,
2633 count,
2634 &ed.settings.iskeyword,
2635 );
2636 ed.push_buffer_cursor_to_textarea();
2637 }
2638 Motion::BigWordEndBack => {
2639 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2640 ed.push_buffer_cursor_to_textarea();
2641 }
2642 Motion::LineStart => {
2643 crate::motions::move_line_start(&mut ed.buffer);
2644 ed.push_buffer_cursor_to_textarea();
2645 }
2646 Motion::FirstNonBlank => {
2647 crate::motions::move_first_non_blank(&mut ed.buffer);
2648 ed.push_buffer_cursor_to_textarea();
2649 }
2650 Motion::LineEnd => {
2651 crate::motions::move_line_end(&mut ed.buffer);
2653 ed.push_buffer_cursor_to_textarea();
2654 }
2655 Motion::FileTop => {
2656 if count > 1 {
2659 crate::motions::move_bottom(&mut ed.buffer, count);
2660 } else {
2661 crate::motions::move_top(&mut ed.buffer);
2662 }
2663 ed.push_buffer_cursor_to_textarea();
2664 }
2665 Motion::FileBottom => {
2666 if count > 1 {
2669 crate::motions::move_bottom(&mut ed.buffer, count);
2670 } else {
2671 crate::motions::move_bottom(&mut ed.buffer, 0);
2672 }
2673 ed.push_buffer_cursor_to_textarea();
2674 }
2675 Motion::Find { ch, forward, till } => {
2676 for _ in 0..count {
2677 if !find_char_on_line(ed, *ch, *forward, *till) {
2678 break;
2679 }
2680 }
2681 }
2682 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2684 let _ = matching_bracket(ed);
2685 }
2686 Motion::WordAtCursor {
2687 forward,
2688 whole_word,
2689 } => {
2690 word_at_cursor_search(ed, *forward, *whole_word, count);
2691 }
2692 Motion::SearchNext { reverse } => {
2693 if let Some(pattern) = ed.vim.last_search.clone() {
2697 push_search_pattern(ed, &pattern);
2698 }
2699 if ed.search_state().pattern.is_none() {
2700 return;
2701 }
2702 let forward = ed.vim.last_search_forward != *reverse;
2706 for _ in 0..count.max(1) {
2707 if forward {
2708 ed.search_advance_forward(true);
2709 } else {
2710 ed.search_advance_backward(true);
2711 }
2712 }
2713 ed.push_buffer_cursor_to_textarea();
2714 }
2715 Motion::ViewportTop => {
2716 let v = *ed.host().viewport();
2717 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2718 ed.push_buffer_cursor_to_textarea();
2719 }
2720 Motion::ViewportMiddle => {
2721 let v = *ed.host().viewport();
2722 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2723 ed.push_buffer_cursor_to_textarea();
2724 }
2725 Motion::ViewportBottom => {
2726 let v = *ed.host().viewport();
2727 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2728 ed.push_buffer_cursor_to_textarea();
2729 }
2730 Motion::LastNonBlank => {
2731 crate::motions::move_last_non_blank(&mut ed.buffer);
2732 ed.push_buffer_cursor_to_textarea();
2733 }
2734 Motion::LineMiddle => {
2735 let row = ed.cursor().0;
2736 let line_chars = buf_line_chars(&ed.buffer, row);
2737 let target = line_chars / 2;
2740 ed.jump_cursor(row, target);
2741 }
2742 Motion::ParagraphPrev => {
2743 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2744 ed.push_buffer_cursor_to_textarea();
2745 }
2746 Motion::ParagraphNext => {
2747 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2748 ed.push_buffer_cursor_to_textarea();
2749 }
2750 Motion::SentencePrev => {
2751 for _ in 0..count.max(1) {
2752 if let Some((row, col)) = sentence_boundary(ed, false) {
2753 ed.jump_cursor(row, col);
2754 }
2755 }
2756 }
2757 Motion::SentenceNext => {
2758 for _ in 0..count.max(1) {
2759 if let Some((row, col)) = sentence_boundary(ed, true) {
2760 ed.jump_cursor(row, col);
2761 }
2762 }
2763 }
2764 }
2765}
2766
2767fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2768 ed.sync_buffer_content_from_textarea();
2774 crate::motions::move_first_non_blank(&mut ed.buffer);
2775 ed.push_buffer_cursor_to_textarea();
2776}
2777
2778fn find_char_on_line<H: crate::types::Host>(
2779 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2780 ch: char,
2781 forward: bool,
2782 till: bool,
2783) -> bool {
2784 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2785 if moved {
2786 ed.push_buffer_cursor_to_textarea();
2787 }
2788 moved
2789}
2790
2791fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2792 let moved = crate::motions::match_bracket(&mut ed.buffer);
2793 if moved {
2794 ed.push_buffer_cursor_to_textarea();
2795 }
2796 moved
2797}
2798
2799fn word_at_cursor_search<H: crate::types::Host>(
2800 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2801 forward: bool,
2802 whole_word: bool,
2803 count: usize,
2804) {
2805 let (row, col) = ed.cursor();
2806 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2807 let chars: Vec<char> = line.chars().collect();
2808 if chars.is_empty() {
2809 return;
2810 }
2811 let spec = ed.settings().iskeyword.clone();
2813 let is_word = |c: char| is_keyword_char(c, &spec);
2814 let mut start = col.min(chars.len().saturating_sub(1));
2815 while start > 0 && is_word(chars[start - 1]) {
2816 start -= 1;
2817 }
2818 let mut end = start;
2819 while end < chars.len() && is_word(chars[end]) {
2820 end += 1;
2821 }
2822 if end <= start {
2823 return;
2824 }
2825 let word: String = chars[start..end].iter().collect();
2826 let escaped = regex_escape(&word);
2827 let pattern = if whole_word {
2828 format!(r"\b{escaped}\b")
2829 } else {
2830 escaped
2831 };
2832 push_search_pattern(ed, &pattern);
2833 if ed.search_state().pattern.is_none() {
2834 return;
2835 }
2836 ed.vim.last_search = Some(pattern);
2838 ed.vim.last_search_forward = forward;
2839 for _ in 0..count.max(1) {
2840 if forward {
2841 ed.search_advance_forward(true);
2842 } else {
2843 ed.search_advance_backward(true);
2844 }
2845 }
2846 ed.push_buffer_cursor_to_textarea();
2847}
2848
2849fn regex_escape(s: &str) -> String {
2850 let mut out = String::with_capacity(s.len());
2851 for c in s.chars() {
2852 if matches!(
2853 c,
2854 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2855 ) {
2856 out.push('\\');
2857 }
2858 out.push(c);
2859 }
2860 out
2861}
2862
2863fn handle_after_op<H: crate::types::Host>(
2866 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2867 input: Input,
2868 op: Operator,
2869 count1: usize,
2870) -> bool {
2871 if let Key::Char(d @ '0'..='9') = input.key
2873 && !input.ctrl
2874 && (d != '0' || ed.vim.count > 0)
2875 {
2876 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2877 ed.vim.pending = Pending::Op { op, count1 };
2878 return true;
2879 }
2880
2881 if input.key == Key::Esc {
2883 ed.vim.count = 0;
2884 return true;
2885 }
2886
2887 let double_ch = match op {
2891 Operator::Delete => Some('d'),
2892 Operator::Change => Some('c'),
2893 Operator::Yank => Some('y'),
2894 Operator::Indent => Some('>'),
2895 Operator::Outdent => Some('<'),
2896 Operator::Uppercase => Some('U'),
2897 Operator::Lowercase => Some('u'),
2898 Operator::ToggleCase => Some('~'),
2899 Operator::Fold => None,
2900 Operator::Reflow => Some('q'),
2903 };
2904 if let Key::Char(c) = input.key
2905 && !input.ctrl
2906 && Some(c) == double_ch
2907 {
2908 let count2 = take_count(&mut ed.vim);
2909 let total = count1.max(1) * count2.max(1);
2910 execute_line_op(ed, op, total);
2911 if !ed.vim.replaying {
2912 ed.vim.last_change = Some(LastChange::LineOp {
2913 op,
2914 count: total,
2915 inserted: None,
2916 });
2917 }
2918 return true;
2919 }
2920
2921 if let Key::Char('i') | Key::Char('a') = input.key
2923 && !input.ctrl
2924 {
2925 let inner = matches!(input.key, Key::Char('i'));
2926 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2927 return true;
2928 }
2929
2930 if input.key == Key::Char('g') && !input.ctrl {
2932 ed.vim.pending = Pending::OpG { op, count1 };
2933 return true;
2934 }
2935
2936 if let Some((forward, till)) = find_entry(&input) {
2938 ed.vim.pending = Pending::OpFind {
2939 op,
2940 count1,
2941 forward,
2942 till,
2943 };
2944 return true;
2945 }
2946
2947 let count2 = take_count(&mut ed.vim);
2949 let total = count1.max(1) * count2.max(1);
2950 if let Some(motion) = parse_motion(&input) {
2951 let motion = match motion {
2952 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2953 Some((ch, forward, till)) => Motion::Find {
2954 ch,
2955 forward: if reverse { !forward } else { forward },
2956 till,
2957 },
2958 None => return true,
2959 },
2960 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2964 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2965 m => m,
2966 };
2967 apply_op_with_motion(ed, op, &motion, total);
2968 if let Motion::Find { ch, forward, till } = &motion {
2969 ed.vim.last_find = Some((*ch, *forward, *till));
2970 }
2971 if !ed.vim.replaying && op_is_change(op) {
2972 ed.vim.last_change = Some(LastChange::OpMotion {
2973 op,
2974 motion,
2975 count: total,
2976 inserted: None,
2977 });
2978 }
2979 return true;
2980 }
2981
2982 true
2984}
2985
2986fn handle_op_after_g<H: crate::types::Host>(
2987 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2988 input: Input,
2989 op: Operator,
2990 count1: usize,
2991) -> bool {
2992 if input.ctrl {
2993 return true;
2994 }
2995 let count2 = take_count(&mut ed.vim);
2996 let total = count1.max(1) * count2.max(1);
2997 if matches!(
3001 op,
3002 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3003 ) {
3004 let op_char = match op {
3005 Operator::Uppercase => 'U',
3006 Operator::Lowercase => 'u',
3007 Operator::ToggleCase => '~',
3008 _ => unreachable!(),
3009 };
3010 if input.key == Key::Char(op_char) {
3011 execute_line_op(ed, op, total);
3012 if !ed.vim.replaying {
3013 ed.vim.last_change = Some(LastChange::LineOp {
3014 op,
3015 count: total,
3016 inserted: None,
3017 });
3018 }
3019 return true;
3020 }
3021 }
3022 let motion = match input.key {
3023 Key::Char('g') => Motion::FileTop,
3024 Key::Char('e') => Motion::WordEndBack,
3025 Key::Char('E') => Motion::BigWordEndBack,
3026 Key::Char('j') => Motion::ScreenDown,
3027 Key::Char('k') => Motion::ScreenUp,
3028 _ => return true,
3029 };
3030 apply_op_with_motion(ed, op, &motion, total);
3031 if !ed.vim.replaying && op_is_change(op) {
3032 ed.vim.last_change = Some(LastChange::OpMotion {
3033 op,
3034 motion,
3035 count: total,
3036 inserted: None,
3037 });
3038 }
3039 true
3040}
3041
3042fn handle_after_g<H: crate::types::Host>(
3043 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3044 input: Input,
3045) -> bool {
3046 let count = take_count(&mut ed.vim);
3047 match input.key {
3048 Key::Char('g') => {
3049 let pre = ed.cursor();
3051 if count > 1 {
3052 ed.jump_cursor(count - 1, 0);
3053 } else {
3054 ed.jump_cursor(0, 0);
3055 }
3056 move_first_non_whitespace(ed);
3057 if ed.cursor() != pre {
3058 push_jump(ed, pre);
3059 }
3060 }
3061 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
3062 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
3063 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
3065 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
3067 Key::Char('v') => {
3069 if let Some(snap) = ed.vim.last_visual {
3070 match snap.mode {
3071 Mode::Visual => {
3072 ed.vim.visual_anchor = snap.anchor;
3073 ed.vim.mode = Mode::Visual;
3074 }
3075 Mode::VisualLine => {
3076 ed.vim.visual_line_anchor = snap.anchor.0;
3077 ed.vim.mode = Mode::VisualLine;
3078 }
3079 Mode::VisualBlock => {
3080 ed.vim.block_anchor = snap.anchor;
3081 ed.vim.block_vcol = snap.block_vcol;
3082 ed.vim.mode = Mode::VisualBlock;
3083 }
3084 _ => {}
3085 }
3086 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3087 }
3088 }
3089 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
3093 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
3094 Key::Char('U') => {
3098 ed.vim.pending = Pending::Op {
3099 op: Operator::Uppercase,
3100 count1: count,
3101 };
3102 }
3103 Key::Char('u') => {
3104 ed.vim.pending = Pending::Op {
3105 op: Operator::Lowercase,
3106 count1: count,
3107 };
3108 }
3109 Key::Char('~') => {
3110 ed.vim.pending = Pending::Op {
3111 op: Operator::ToggleCase,
3112 count1: count,
3113 };
3114 }
3115 Key::Char('q') => {
3116 ed.vim.pending = Pending::Op {
3119 op: Operator::Reflow,
3120 count1: count,
3121 };
3122 }
3123 Key::Char('J') => {
3124 for _ in 0..count.max(1) {
3126 ed.push_undo();
3127 join_line_raw(ed);
3128 }
3129 if !ed.vim.replaying {
3130 ed.vim.last_change = Some(LastChange::JoinLine {
3131 count: count.max(1),
3132 });
3133 }
3134 }
3135 Key::Char('d') => {
3136 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3141 }
3142 Key::Char('i') => {
3147 if let Some((row, col)) = ed.vim.last_insert_pos {
3148 ed.jump_cursor(row, col);
3149 }
3150 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3151 }
3152 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
3155 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
3156 Key::Char('*') => execute_motion(
3160 ed,
3161 Motion::WordAtCursor {
3162 forward: true,
3163 whole_word: false,
3164 },
3165 count,
3166 ),
3167 Key::Char('#') => execute_motion(
3168 ed,
3169 Motion::WordAtCursor {
3170 forward: false,
3171 whole_word: false,
3172 },
3173 count,
3174 ),
3175 _ => {}
3176 }
3177 true
3178}
3179
3180fn handle_after_z<H: crate::types::Host>(
3181 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3182 input: Input,
3183) -> bool {
3184 use crate::editor::CursorScrollTarget;
3185 let row = ed.cursor().0;
3186 match input.key {
3187 Key::Char('z') => {
3188 ed.scroll_cursor_to(CursorScrollTarget::Center);
3189 ed.vim.viewport_pinned = true;
3190 }
3191 Key::Char('t') => {
3192 ed.scroll_cursor_to(CursorScrollTarget::Top);
3193 ed.vim.viewport_pinned = true;
3194 }
3195 Key::Char('b') => {
3196 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3197 ed.vim.viewport_pinned = true;
3198 }
3199 Key::Char('o') => {
3204 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3205 }
3206 Key::Char('c') => {
3207 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3208 }
3209 Key::Char('a') => {
3210 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3211 }
3212 Key::Char('R') => {
3213 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3214 }
3215 Key::Char('M') => {
3216 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3217 }
3218 Key::Char('E') => {
3219 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3220 }
3221 Key::Char('d') => {
3222 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3223 }
3224 Key::Char('f') => {
3225 if matches!(
3226 ed.vim.mode,
3227 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3228 ) {
3229 let anchor_row = match ed.vim.mode {
3232 Mode::VisualLine => ed.vim.visual_line_anchor,
3233 Mode::VisualBlock => ed.vim.block_anchor.0,
3234 _ => ed.vim.visual_anchor.0,
3235 };
3236 let cur = ed.cursor().0;
3237 let top = anchor_row.min(cur);
3238 let bot = anchor_row.max(cur);
3239 ed.apply_fold_op(crate::types::FoldOp::Add {
3240 start_row: top,
3241 end_row: bot,
3242 closed: true,
3243 });
3244 ed.vim.mode = Mode::Normal;
3245 } else {
3246 let count = take_count(&mut ed.vim);
3251 ed.vim.pending = Pending::Op {
3252 op: Operator::Fold,
3253 count1: count,
3254 };
3255 }
3256 }
3257 _ => {}
3258 }
3259 true
3260}
3261
3262fn handle_replace<H: crate::types::Host>(
3263 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3264 input: Input,
3265) -> bool {
3266 if let Key::Char(ch) = input.key {
3267 if ed.vim.mode == Mode::VisualBlock {
3268 block_replace(ed, ch);
3269 return true;
3270 }
3271 let count = take_count(&mut ed.vim);
3272 replace_char(ed, ch, count.max(1));
3273 if !ed.vim.replaying {
3274 ed.vim.last_change = Some(LastChange::ReplaceChar {
3275 ch,
3276 count: count.max(1),
3277 });
3278 }
3279 }
3280 true
3281}
3282
3283fn handle_find_target<H: crate::types::Host>(
3284 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3285 input: Input,
3286 forward: bool,
3287 till: bool,
3288) -> bool {
3289 let Key::Char(ch) = input.key else {
3290 return true;
3291 };
3292 let count = take_count(&mut ed.vim);
3293 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3294 ed.vim.last_find = Some((ch, forward, till));
3295 true
3296}
3297
3298fn handle_op_find_target<H: crate::types::Host>(
3299 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3300 input: Input,
3301 op: Operator,
3302 count1: usize,
3303 forward: bool,
3304 till: bool,
3305) -> bool {
3306 let Key::Char(ch) = input.key else {
3307 return true;
3308 };
3309 let count2 = take_count(&mut ed.vim);
3310 let total = count1.max(1) * count2.max(1);
3311 let motion = Motion::Find { ch, forward, till };
3312 apply_op_with_motion(ed, op, &motion, total);
3313 ed.vim.last_find = Some((ch, forward, till));
3314 if !ed.vim.replaying && op_is_change(op) {
3315 ed.vim.last_change = Some(LastChange::OpMotion {
3316 op,
3317 motion,
3318 count: total,
3319 inserted: None,
3320 });
3321 }
3322 true
3323}
3324
3325fn handle_text_object<H: crate::types::Host>(
3326 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3327 input: Input,
3328 op: Operator,
3329 _count1: usize,
3330 inner: bool,
3331) -> bool {
3332 let Key::Char(ch) = input.key else {
3333 return true;
3334 };
3335 let obj = match ch {
3336 'w' => TextObject::Word { big: false },
3337 'W' => TextObject::Word { big: true },
3338 '"' | '\'' | '`' => TextObject::Quote(ch),
3339 '(' | ')' | 'b' => TextObject::Bracket('('),
3340 '[' | ']' => TextObject::Bracket('['),
3341 '{' | '}' | 'B' => TextObject::Bracket('{'),
3342 '<' | '>' => TextObject::Bracket('<'),
3343 'p' => TextObject::Paragraph,
3344 't' => TextObject::XmlTag,
3345 's' => TextObject::Sentence,
3346 _ => return true,
3347 };
3348 apply_op_with_text_object(ed, op, obj, inner);
3349 if !ed.vim.replaying && op_is_change(op) {
3350 ed.vim.last_change = Some(LastChange::OpTextObj {
3351 op,
3352 obj,
3353 inner,
3354 inserted: None,
3355 });
3356 }
3357 true
3358}
3359
3360fn handle_visual_text_obj<H: crate::types::Host>(
3361 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3362 input: Input,
3363 inner: bool,
3364) -> bool {
3365 let Key::Char(ch) = input.key else {
3366 return true;
3367 };
3368 let obj = match ch {
3369 'w' => TextObject::Word { big: false },
3370 'W' => TextObject::Word { big: true },
3371 '"' | '\'' | '`' => TextObject::Quote(ch),
3372 '(' | ')' | 'b' => TextObject::Bracket('('),
3373 '[' | ']' => TextObject::Bracket('['),
3374 '{' | '}' | 'B' => TextObject::Bracket('{'),
3375 '<' | '>' => TextObject::Bracket('<'),
3376 'p' => TextObject::Paragraph,
3377 't' => TextObject::XmlTag,
3378 's' => TextObject::Sentence,
3379 _ => return true,
3380 };
3381 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3382 return true;
3383 };
3384 match kind {
3388 MotionKind::Linewise => {
3389 ed.vim.visual_line_anchor = start.0;
3390 ed.vim.mode = Mode::VisualLine;
3391 ed.jump_cursor(end.0, 0);
3392 }
3393 _ => {
3394 ed.vim.mode = Mode::Visual;
3395 ed.vim.visual_anchor = (start.0, start.1);
3396 let (er, ec) = retreat_one(ed, end);
3397 ed.jump_cursor(er, ec);
3398 }
3399 }
3400 true
3401}
3402
3403fn retreat_one<H: crate::types::Host>(
3405 ed: &Editor<hjkl_buffer::Buffer, H>,
3406 pos: (usize, usize),
3407) -> (usize, usize) {
3408 let (r, c) = pos;
3409 if c > 0 {
3410 (r, c - 1)
3411 } else if r > 0 {
3412 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3413 (r - 1, prev_len)
3414 } else {
3415 (0, 0)
3416 }
3417}
3418
3419fn op_is_change(op: Operator) -> bool {
3420 matches!(op, Operator::Delete | Operator::Change)
3421}
3422
3423fn handle_normal_only<H: crate::types::Host>(
3426 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3427 input: &Input,
3428 count: usize,
3429) -> bool {
3430 if input.ctrl {
3431 return false;
3432 }
3433 match input.key {
3434 Key::Char('i') => {
3435 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3436 true
3437 }
3438 Key::Char('I') => {
3439 move_first_non_whitespace(ed);
3440 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3441 true
3442 }
3443 Key::Char('a') => {
3444 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3445 ed.push_buffer_cursor_to_textarea();
3446 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3447 true
3448 }
3449 Key::Char('A') => {
3450 crate::motions::move_line_end(&mut ed.buffer);
3451 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3452 ed.push_buffer_cursor_to_textarea();
3453 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3454 true
3455 }
3456 Key::Char('R') => {
3457 begin_insert(ed, count.max(1), InsertReason::Replace);
3460 true
3461 }
3462 Key::Char('o') => {
3463 use hjkl_buffer::{Edit, Position};
3464 ed.push_undo();
3465 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3468 ed.sync_buffer_content_from_textarea();
3469 let row = buf_cursor_pos(&ed.buffer).row;
3470 let line_chars = buf_line_chars(&ed.buffer, row);
3471 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3474 let indent = compute_enter_indent(&ed.settings, prev_line);
3475 ed.mutate_edit(Edit::InsertStr {
3476 at: Position::new(row, line_chars),
3477 text: format!("\n{indent}"),
3478 });
3479 ed.push_buffer_cursor_to_textarea();
3480 true
3481 }
3482 Key::Char('O') => {
3483 use hjkl_buffer::{Edit, Position};
3484 ed.push_undo();
3485 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3486 ed.sync_buffer_content_from_textarea();
3487 let row = buf_cursor_pos(&ed.buffer).row;
3488 let indent = if row > 0 {
3492 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3493 compute_enter_indent(&ed.settings, above)
3494 } else {
3495 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3496 cur.chars()
3497 .take_while(|c| *c == ' ' || *c == '\t')
3498 .collect::<String>()
3499 };
3500 ed.mutate_edit(Edit::InsertStr {
3501 at: Position::new(row, 0),
3502 text: format!("{indent}\n"),
3503 });
3504 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3509 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3510 let new_row = buf_cursor_pos(&ed.buffer).row;
3511 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3512 ed.push_buffer_cursor_to_textarea();
3513 true
3514 }
3515 Key::Char('x') => {
3516 do_char_delete(ed, true, count.max(1));
3517 if !ed.vim.replaying {
3518 ed.vim.last_change = Some(LastChange::CharDel {
3519 forward: true,
3520 count: count.max(1),
3521 });
3522 }
3523 true
3524 }
3525 Key::Char('X') => {
3526 do_char_delete(ed, false, count.max(1));
3527 if !ed.vim.replaying {
3528 ed.vim.last_change = Some(LastChange::CharDel {
3529 forward: false,
3530 count: count.max(1),
3531 });
3532 }
3533 true
3534 }
3535 Key::Char('~') => {
3536 for _ in 0..count.max(1) {
3537 ed.push_undo();
3538 toggle_case_at_cursor(ed);
3539 }
3540 if !ed.vim.replaying {
3541 ed.vim.last_change = Some(LastChange::ToggleCase {
3542 count: count.max(1),
3543 });
3544 }
3545 true
3546 }
3547 Key::Char('J') => {
3548 for _ in 0..count.max(1) {
3549 ed.push_undo();
3550 join_line(ed);
3551 }
3552 if !ed.vim.replaying {
3553 ed.vim.last_change = Some(LastChange::JoinLine {
3554 count: count.max(1),
3555 });
3556 }
3557 true
3558 }
3559 Key::Char('D') => {
3560 ed.push_undo();
3561 delete_to_eol(ed);
3562 crate::motions::move_left(&mut ed.buffer, 1);
3564 ed.push_buffer_cursor_to_textarea();
3565 if !ed.vim.replaying {
3566 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3567 }
3568 true
3569 }
3570 Key::Char('Y') => {
3571 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3573 true
3574 }
3575 Key::Char('C') => {
3576 ed.push_undo();
3577 delete_to_eol(ed);
3578 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3579 true
3580 }
3581 Key::Char('s') => {
3582 use hjkl_buffer::{Edit, MotionKind, Position};
3583 ed.push_undo();
3584 ed.sync_buffer_content_from_textarea();
3585 for _ in 0..count.max(1) {
3586 let cursor = buf_cursor_pos(&ed.buffer);
3587 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3588 if cursor.col >= line_chars {
3589 break;
3590 }
3591 ed.mutate_edit(Edit::DeleteRange {
3592 start: cursor,
3593 end: Position::new(cursor.row, cursor.col + 1),
3594 kind: MotionKind::Char,
3595 });
3596 }
3597 ed.push_buffer_cursor_to_textarea();
3598 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3599 if !ed.vim.replaying {
3601 ed.vim.last_change = Some(LastChange::OpMotion {
3602 op: Operator::Change,
3603 motion: Motion::Right,
3604 count: count.max(1),
3605 inserted: None,
3606 });
3607 }
3608 true
3609 }
3610 Key::Char('p') => {
3611 do_paste(ed, false, count.max(1));
3612 if !ed.vim.replaying {
3613 ed.vim.last_change = Some(LastChange::Paste {
3614 before: false,
3615 count: count.max(1),
3616 });
3617 }
3618 true
3619 }
3620 Key::Char('P') => {
3621 do_paste(ed, true, count.max(1));
3622 if !ed.vim.replaying {
3623 ed.vim.last_change = Some(LastChange::Paste {
3624 before: true,
3625 count: count.max(1),
3626 });
3627 }
3628 true
3629 }
3630 Key::Char('u') => {
3631 do_undo(ed);
3632 true
3633 }
3634 Key::Char('r') => {
3635 ed.vim.count = count;
3636 ed.vim.pending = Pending::Replace;
3637 true
3638 }
3639 Key::Char('/') => {
3640 enter_search(ed, true);
3641 true
3642 }
3643 Key::Char('?') => {
3644 enter_search(ed, false);
3645 true
3646 }
3647 Key::Char('.') => {
3648 replay_last_change(ed, count);
3649 true
3650 }
3651 _ => false,
3652 }
3653}
3654
3655fn begin_insert_noundo<H: crate::types::Host>(
3657 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3658 count: usize,
3659 reason: InsertReason,
3660) {
3661 let reason = if ed.vim.replaying {
3662 InsertReason::ReplayOnly
3663 } else {
3664 reason
3665 };
3666 let (row, _) = ed.cursor();
3667 ed.vim.insert_session = Some(InsertSession {
3668 count,
3669 row_min: row,
3670 row_max: row,
3671 before_lines: buf_lines_to_vec(&ed.buffer),
3672 reason,
3673 });
3674 ed.vim.mode = Mode::Insert;
3675}
3676
3677fn apply_op_with_motion<H: crate::types::Host>(
3680 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3681 op: Operator,
3682 motion: &Motion,
3683 count: usize,
3684) {
3685 let start = ed.cursor();
3686 apply_motion_cursor_ctx(ed, motion, count, true);
3691 let end = ed.cursor();
3692 let kind = motion_kind(motion);
3693 ed.jump_cursor(start.0, start.1);
3695 run_operator_over_range(ed, op, start, end, kind);
3696}
3697
3698fn apply_op_with_text_object<H: crate::types::Host>(
3699 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3700 op: Operator,
3701 obj: TextObject,
3702 inner: bool,
3703) {
3704 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3705 return;
3706 };
3707 ed.jump_cursor(start.0, start.1);
3708 run_operator_over_range(ed, op, start, end, kind);
3709}
3710
3711fn motion_kind(motion: &Motion) -> MotionKind {
3712 match motion {
3713 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3714 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3715 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3716 MotionKind::Linewise
3717 }
3718 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3719 MotionKind::Inclusive
3720 }
3721 Motion::Find { .. } => MotionKind::Inclusive,
3722 Motion::MatchBracket => MotionKind::Inclusive,
3723 Motion::LineEnd => MotionKind::Inclusive,
3725 _ => MotionKind::Exclusive,
3726 }
3727}
3728
3729fn run_operator_over_range<H: crate::types::Host>(
3730 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3731 op: Operator,
3732 start: (usize, usize),
3733 end: (usize, usize),
3734 kind: MotionKind,
3735) {
3736 let (top, bot) = order(start, end);
3737 if top == bot {
3738 return;
3739 }
3740
3741 match op {
3742 Operator::Yank => {
3743 let text = read_vim_range(ed, top, bot, kind);
3744 if !text.is_empty() {
3745 ed.record_yank_to_host(text.clone());
3746 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3747 }
3748 let rbr = match kind {
3752 MotionKind::Linewise => {
3753 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3754 (bot.0, last_col)
3755 }
3756 MotionKind::Inclusive => (bot.0, bot.1),
3757 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3758 };
3759 ed.set_mark('[', top);
3760 ed.set_mark(']', rbr);
3761 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3762 ed.push_buffer_cursor_to_textarea();
3763 }
3764 Operator::Delete => {
3765 ed.push_undo();
3766 cut_vim_range(ed, top, bot, kind);
3767 if !matches!(kind, MotionKind::Linewise) {
3772 clamp_cursor_to_normal_mode(ed);
3773 }
3774 ed.vim.mode = Mode::Normal;
3775 let pos = ed.cursor();
3779 ed.set_mark('[', pos);
3780 ed.set_mark(']', pos);
3781 }
3782 Operator::Change => {
3783 ed.vim.change_mark_start = Some(top);
3788 ed.push_undo();
3789 cut_vim_range(ed, top, bot, kind);
3790 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3791 }
3792 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3793 apply_case_op_to_selection(ed, op, top, bot, kind);
3794 }
3795 Operator::Indent | Operator::Outdent => {
3796 ed.push_undo();
3799 if op == Operator::Indent {
3800 indent_rows(ed, top.0, bot.0, 1);
3801 } else {
3802 outdent_rows(ed, top.0, bot.0, 1);
3803 }
3804 ed.vim.mode = Mode::Normal;
3805 }
3806 Operator::Fold => {
3807 if bot.0 >= top.0 {
3811 ed.apply_fold_op(crate::types::FoldOp::Add {
3812 start_row: top.0,
3813 end_row: bot.0,
3814 closed: true,
3815 });
3816 }
3817 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3818 ed.push_buffer_cursor_to_textarea();
3819 ed.vim.mode = Mode::Normal;
3820 }
3821 Operator::Reflow => {
3822 ed.push_undo();
3823 reflow_rows(ed, top.0, bot.0);
3824 ed.vim.mode = Mode::Normal;
3825 }
3826 }
3827}
3828
3829fn reflow_rows<H: crate::types::Host>(
3834 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3835 top: usize,
3836 bot: usize,
3837) {
3838 let width = ed.settings().textwidth.max(1);
3839 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3840 let bot = bot.min(lines.len().saturating_sub(1));
3841 if top > bot {
3842 return;
3843 }
3844 let original = lines[top..=bot].to_vec();
3845 let mut wrapped: Vec<String> = Vec::new();
3846 let mut paragraph: Vec<String> = Vec::new();
3847 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3848 if para.is_empty() {
3849 return;
3850 }
3851 let words = para.join(" ");
3852 let mut current = String::new();
3853 for word in words.split_whitespace() {
3854 let extra = if current.is_empty() {
3855 word.chars().count()
3856 } else {
3857 current.chars().count() + 1 + word.chars().count()
3858 };
3859 if extra > width && !current.is_empty() {
3860 out.push(std::mem::take(&mut current));
3861 current.push_str(word);
3862 } else if current.is_empty() {
3863 current.push_str(word);
3864 } else {
3865 current.push(' ');
3866 current.push_str(word);
3867 }
3868 }
3869 if !current.is_empty() {
3870 out.push(current);
3871 }
3872 para.clear();
3873 };
3874 for line in &original {
3875 if line.trim().is_empty() {
3876 flush(&mut paragraph, &mut wrapped, width);
3877 wrapped.push(String::new());
3878 } else {
3879 paragraph.push(line.clone());
3880 }
3881 }
3882 flush(&mut paragraph, &mut wrapped, width);
3883
3884 let after: Vec<String> = lines.split_off(bot + 1);
3886 lines.truncate(top);
3887 lines.extend(wrapped);
3888 lines.extend(after);
3889 ed.restore(lines, (top, 0));
3890 ed.mark_content_dirty();
3891}
3892
3893fn apply_case_op_to_selection<H: crate::types::Host>(
3899 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3900 op: Operator,
3901 top: (usize, usize),
3902 bot: (usize, usize),
3903 kind: MotionKind,
3904) {
3905 use hjkl_buffer::Edit;
3906 ed.push_undo();
3907 let saved_yank = ed.yank().to_string();
3908 let saved_yank_linewise = ed.vim.yank_linewise;
3909 let selection = cut_vim_range(ed, top, bot, kind);
3910 let transformed = match op {
3911 Operator::Uppercase => selection.to_uppercase(),
3912 Operator::Lowercase => selection.to_lowercase(),
3913 Operator::ToggleCase => toggle_case_str(&selection),
3914 _ => unreachable!(),
3915 };
3916 if !transformed.is_empty() {
3917 let cursor = buf_cursor_pos(&ed.buffer);
3918 ed.mutate_edit(Edit::InsertStr {
3919 at: cursor,
3920 text: transformed,
3921 });
3922 }
3923 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3924 ed.push_buffer_cursor_to_textarea();
3925 ed.set_yank(saved_yank);
3926 ed.vim.yank_linewise = saved_yank_linewise;
3927 ed.vim.mode = Mode::Normal;
3928}
3929
3930fn indent_rows<H: crate::types::Host>(
3935 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3936 top: usize,
3937 bot: usize,
3938 count: usize,
3939) {
3940 ed.sync_buffer_content_from_textarea();
3941 let width = ed.settings().shiftwidth * count.max(1);
3942 let pad: String = " ".repeat(width);
3943 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3944 let bot = bot.min(lines.len().saturating_sub(1));
3945 for line in lines.iter_mut().take(bot + 1).skip(top) {
3946 if !line.is_empty() {
3947 line.insert_str(0, &pad);
3948 }
3949 }
3950 ed.restore(lines, (top, 0));
3953 move_first_non_whitespace(ed);
3954}
3955
3956fn outdent_rows<H: crate::types::Host>(
3960 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3961 top: usize,
3962 bot: usize,
3963 count: usize,
3964) {
3965 ed.sync_buffer_content_from_textarea();
3966 let width = ed.settings().shiftwidth * count.max(1);
3967 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3968 let bot = bot.min(lines.len().saturating_sub(1));
3969 for line in lines.iter_mut().take(bot + 1).skip(top) {
3970 let strip: usize = line
3971 .chars()
3972 .take(width)
3973 .take_while(|c| *c == ' ' || *c == '\t')
3974 .count();
3975 if strip > 0 {
3976 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3977 line.drain(..byte_len);
3978 }
3979 }
3980 ed.restore(lines, (top, 0));
3981 move_first_non_whitespace(ed);
3982}
3983
3984fn toggle_case_str(s: &str) -> String {
3985 s.chars()
3986 .map(|c| {
3987 if c.is_lowercase() {
3988 c.to_uppercase().next().unwrap_or(c)
3989 } else if c.is_uppercase() {
3990 c.to_lowercase().next().unwrap_or(c)
3991 } else {
3992 c
3993 }
3994 })
3995 .collect()
3996}
3997
3998fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3999 if a <= b { (a, b) } else { (b, a) }
4000}
4001
4002fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4007 let (row, col) = ed.cursor();
4008 let line_chars = buf_line_chars(&ed.buffer, row);
4009 let max_col = line_chars.saturating_sub(1);
4010 if col > max_col {
4011 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4012 ed.push_buffer_cursor_to_textarea();
4013 }
4014}
4015
4016fn execute_line_op<H: crate::types::Host>(
4019 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4020 op: Operator,
4021 count: usize,
4022) {
4023 let (row, col) = ed.cursor();
4024 let total = buf_row_count(&ed.buffer);
4025 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4026
4027 match op {
4028 Operator::Yank => {
4029 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4031 if !text.is_empty() {
4032 ed.record_yank_to_host(text.clone());
4033 ed.record_yank(text, true);
4034 }
4035 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4038 ed.set_mark('[', (row, 0));
4039 ed.set_mark(']', (end_row, last_col));
4040 buf_set_cursor_rc(&mut ed.buffer, row, col);
4041 ed.push_buffer_cursor_to_textarea();
4042 ed.vim.mode = Mode::Normal;
4043 }
4044 Operator::Delete => {
4045 ed.push_undo();
4046 let deleted_through_last = end_row + 1 >= total;
4047 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4048 let total_after = buf_row_count(&ed.buffer);
4052 let raw_target = if deleted_through_last {
4053 row.saturating_sub(1).min(total_after.saturating_sub(1))
4054 } else {
4055 row.min(total_after.saturating_sub(1))
4056 };
4057 let target_row = if raw_target > 0
4063 && raw_target + 1 == total_after
4064 && buf_line(&ed.buffer, raw_target)
4065 .map(str::is_empty)
4066 .unwrap_or(false)
4067 {
4068 raw_target - 1
4069 } else {
4070 raw_target
4071 };
4072 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4073 ed.push_buffer_cursor_to_textarea();
4074 move_first_non_whitespace(ed);
4075 ed.sticky_col = Some(ed.cursor().1);
4076 ed.vim.mode = Mode::Normal;
4077 let pos = ed.cursor();
4080 ed.set_mark('[', pos);
4081 ed.set_mark(']', pos);
4082 }
4083 Operator::Change => {
4084 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4088 ed.vim.change_mark_start = Some((row, 0));
4090 ed.push_undo();
4091 ed.sync_buffer_content_from_textarea();
4092 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4094 if end_row > row {
4095 ed.mutate_edit(Edit::DeleteRange {
4096 start: Position::new(row + 1, 0),
4097 end: Position::new(end_row, 0),
4098 kind: BufKind::Line,
4099 });
4100 }
4101 let line_chars = buf_line_chars(&ed.buffer, row);
4102 if line_chars > 0 {
4103 ed.mutate_edit(Edit::DeleteRange {
4104 start: Position::new(row, 0),
4105 end: Position::new(row, line_chars),
4106 kind: BufKind::Char,
4107 });
4108 }
4109 if !payload.is_empty() {
4110 ed.record_yank_to_host(payload.clone());
4111 ed.record_delete(payload, true);
4112 }
4113 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4114 ed.push_buffer_cursor_to_textarea();
4115 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4116 }
4117 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4118 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4122 move_first_non_whitespace(ed);
4125 }
4126 Operator::Indent | Operator::Outdent => {
4127 ed.push_undo();
4129 if op == Operator::Indent {
4130 indent_rows(ed, row, end_row, 1);
4131 } else {
4132 outdent_rows(ed, row, end_row, 1);
4133 }
4134 ed.sticky_col = Some(ed.cursor().1);
4135 ed.vim.mode = Mode::Normal;
4136 }
4137 Operator::Fold => unreachable!("Fold has no line-op double"),
4139 Operator::Reflow => {
4140 ed.push_undo();
4142 reflow_rows(ed, row, end_row);
4143 move_first_non_whitespace(ed);
4144 ed.sticky_col = Some(ed.cursor().1);
4145 ed.vim.mode = Mode::Normal;
4146 }
4147 }
4148}
4149
4150fn apply_visual_operator<H: crate::types::Host>(
4153 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4154 op: Operator,
4155) {
4156 match ed.vim.mode {
4157 Mode::VisualLine => {
4158 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4159 let top = cursor_row.min(ed.vim.visual_line_anchor);
4160 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4161 ed.vim.yank_linewise = true;
4162 match op {
4163 Operator::Yank => {
4164 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4165 if !text.is_empty() {
4166 ed.record_yank_to_host(text.clone());
4167 ed.record_yank(text, true);
4168 }
4169 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4170 ed.push_buffer_cursor_to_textarea();
4171 ed.vim.mode = Mode::Normal;
4172 }
4173 Operator::Delete => {
4174 ed.push_undo();
4175 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4176 ed.vim.mode = Mode::Normal;
4177 }
4178 Operator::Change => {
4179 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4182 ed.push_undo();
4183 ed.sync_buffer_content_from_textarea();
4184 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4185 if bot > top {
4186 ed.mutate_edit(Edit::DeleteRange {
4187 start: Position::new(top + 1, 0),
4188 end: Position::new(bot, 0),
4189 kind: BufKind::Line,
4190 });
4191 }
4192 let line_chars = buf_line_chars(&ed.buffer, top);
4193 if line_chars > 0 {
4194 ed.mutate_edit(Edit::DeleteRange {
4195 start: Position::new(top, 0),
4196 end: Position::new(top, line_chars),
4197 kind: BufKind::Char,
4198 });
4199 }
4200 if !payload.is_empty() {
4201 ed.record_yank_to_host(payload.clone());
4202 ed.record_delete(payload, true);
4203 }
4204 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4205 ed.push_buffer_cursor_to_textarea();
4206 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4207 }
4208 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4209 let bot = buf_cursor_pos(&ed.buffer)
4210 .row
4211 .max(ed.vim.visual_line_anchor);
4212 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4213 move_first_non_whitespace(ed);
4214 }
4215 Operator::Indent | Operator::Outdent => {
4216 ed.push_undo();
4217 let (cursor_row, _) = ed.cursor();
4218 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4219 if op == Operator::Indent {
4220 indent_rows(ed, top, bot, 1);
4221 } else {
4222 outdent_rows(ed, top, bot, 1);
4223 }
4224 ed.vim.mode = Mode::Normal;
4225 }
4226 Operator::Reflow => {
4227 ed.push_undo();
4228 let (cursor_row, _) = ed.cursor();
4229 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4230 reflow_rows(ed, top, bot);
4231 ed.vim.mode = Mode::Normal;
4232 }
4233 Operator::Fold => unreachable!("Visual zf takes its own path"),
4236 }
4237 }
4238 Mode::Visual => {
4239 ed.vim.yank_linewise = false;
4240 let anchor = ed.vim.visual_anchor;
4241 let cursor = ed.cursor();
4242 let (top, bot) = order(anchor, cursor);
4243 match op {
4244 Operator::Yank => {
4245 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4246 if !text.is_empty() {
4247 ed.record_yank_to_host(text.clone());
4248 ed.record_yank(text, false);
4249 }
4250 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4251 ed.push_buffer_cursor_to_textarea();
4252 ed.vim.mode = Mode::Normal;
4253 }
4254 Operator::Delete => {
4255 ed.push_undo();
4256 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4257 ed.vim.mode = Mode::Normal;
4258 }
4259 Operator::Change => {
4260 ed.push_undo();
4261 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4262 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4263 }
4264 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4265 let anchor = ed.vim.visual_anchor;
4267 let cursor = ed.cursor();
4268 let (top, bot) = order(anchor, cursor);
4269 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4270 }
4271 Operator::Indent | Operator::Outdent => {
4272 ed.push_undo();
4273 let anchor = ed.vim.visual_anchor;
4274 let cursor = ed.cursor();
4275 let (top, bot) = order(anchor, cursor);
4276 if op == Operator::Indent {
4277 indent_rows(ed, top.0, bot.0, 1);
4278 } else {
4279 outdent_rows(ed, top.0, bot.0, 1);
4280 }
4281 ed.vim.mode = Mode::Normal;
4282 }
4283 Operator::Reflow => {
4284 ed.push_undo();
4285 let anchor = ed.vim.visual_anchor;
4286 let cursor = ed.cursor();
4287 let (top, bot) = order(anchor, cursor);
4288 reflow_rows(ed, top.0, bot.0);
4289 ed.vim.mode = Mode::Normal;
4290 }
4291 Operator::Fold => unreachable!("Visual zf takes its own path"),
4292 }
4293 }
4294 Mode::VisualBlock => apply_block_operator(ed, op),
4295 _ => {}
4296 }
4297}
4298
4299fn block_bounds<H: crate::types::Host>(
4304 ed: &Editor<hjkl_buffer::Buffer, H>,
4305) -> (usize, usize, usize, usize) {
4306 let (ar, ac) = ed.vim.block_anchor;
4307 let (cr, _) = ed.cursor();
4308 let cc = ed.vim.block_vcol;
4309 let top = ar.min(cr);
4310 let bot = ar.max(cr);
4311 let left = ac.min(cc);
4312 let right = ac.max(cc);
4313 (top, bot, left, right)
4314}
4315
4316fn update_block_vcol<H: crate::types::Host>(
4321 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4322 motion: &Motion,
4323) {
4324 match motion {
4325 Motion::Left
4326 | Motion::Right
4327 | Motion::WordFwd
4328 | Motion::BigWordFwd
4329 | Motion::WordBack
4330 | Motion::BigWordBack
4331 | Motion::WordEnd
4332 | Motion::BigWordEnd
4333 | Motion::WordEndBack
4334 | Motion::BigWordEndBack
4335 | Motion::LineStart
4336 | Motion::FirstNonBlank
4337 | Motion::LineEnd
4338 | Motion::Find { .. }
4339 | Motion::FindRepeat { .. }
4340 | Motion::MatchBracket => {
4341 ed.vim.block_vcol = ed.cursor().1;
4342 }
4343 _ => {}
4345 }
4346}
4347
4348fn apply_block_operator<H: crate::types::Host>(
4353 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4354 op: Operator,
4355) {
4356 let (top, bot, left, right) = block_bounds(ed);
4357 let yank = block_yank(ed, top, bot, left, right);
4359
4360 match op {
4361 Operator::Yank => {
4362 if !yank.is_empty() {
4363 ed.record_yank_to_host(yank.clone());
4364 ed.record_yank(yank, false);
4365 }
4366 ed.vim.mode = Mode::Normal;
4367 ed.jump_cursor(top, left);
4368 }
4369 Operator::Delete => {
4370 ed.push_undo();
4371 delete_block_contents(ed, top, bot, left, right);
4372 if !yank.is_empty() {
4373 ed.record_yank_to_host(yank.clone());
4374 ed.record_delete(yank, false);
4375 }
4376 ed.vim.mode = Mode::Normal;
4377 ed.jump_cursor(top, left);
4378 }
4379 Operator::Change => {
4380 ed.push_undo();
4381 delete_block_contents(ed, top, bot, left, right);
4382 if !yank.is_empty() {
4383 ed.record_yank_to_host(yank.clone());
4384 ed.record_delete(yank, false);
4385 }
4386 ed.jump_cursor(top, left);
4387 begin_insert_noundo(
4388 ed,
4389 1,
4390 InsertReason::BlockChange {
4391 top,
4392 bot,
4393 col: left,
4394 },
4395 );
4396 }
4397 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4398 ed.push_undo();
4399 transform_block_case(ed, op, top, bot, left, right);
4400 ed.vim.mode = Mode::Normal;
4401 ed.jump_cursor(top, left);
4402 }
4403 Operator::Indent | Operator::Outdent => {
4404 ed.push_undo();
4408 if op == Operator::Indent {
4409 indent_rows(ed, top, bot, 1);
4410 } else {
4411 outdent_rows(ed, top, bot, 1);
4412 }
4413 ed.vim.mode = Mode::Normal;
4414 }
4415 Operator::Fold => unreachable!("Visual zf takes its own path"),
4416 Operator::Reflow => {
4417 ed.push_undo();
4421 reflow_rows(ed, top, bot);
4422 ed.vim.mode = Mode::Normal;
4423 }
4424 }
4425}
4426
4427fn transform_block_case<H: crate::types::Host>(
4431 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4432 op: Operator,
4433 top: usize,
4434 bot: usize,
4435 left: usize,
4436 right: usize,
4437) {
4438 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4439 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4440 let chars: Vec<char> = lines[r].chars().collect();
4441 if left >= chars.len() {
4442 continue;
4443 }
4444 let end = (right + 1).min(chars.len());
4445 let head: String = chars[..left].iter().collect();
4446 let mid: String = chars[left..end].iter().collect();
4447 let tail: String = chars[end..].iter().collect();
4448 let transformed = match op {
4449 Operator::Uppercase => mid.to_uppercase(),
4450 Operator::Lowercase => mid.to_lowercase(),
4451 Operator::ToggleCase => toggle_case_str(&mid),
4452 _ => mid,
4453 };
4454 lines[r] = format!("{head}{transformed}{tail}");
4455 }
4456 let saved_yank = ed.yank().to_string();
4457 let saved_linewise = ed.vim.yank_linewise;
4458 ed.restore(lines, (top, left));
4459 ed.set_yank(saved_yank);
4460 ed.vim.yank_linewise = saved_linewise;
4461}
4462
4463fn block_yank<H: crate::types::Host>(
4464 ed: &Editor<hjkl_buffer::Buffer, H>,
4465 top: usize,
4466 bot: usize,
4467 left: usize,
4468 right: usize,
4469) -> String {
4470 let lines = buf_lines_to_vec(&ed.buffer);
4471 let mut rows: Vec<String> = Vec::new();
4472 for r in top..=bot {
4473 let line = match lines.get(r) {
4474 Some(l) => l,
4475 None => break,
4476 };
4477 let chars: Vec<char> = line.chars().collect();
4478 let end = (right + 1).min(chars.len());
4479 if left >= chars.len() {
4480 rows.push(String::new());
4481 } else {
4482 rows.push(chars[left..end].iter().collect());
4483 }
4484 }
4485 rows.join("\n")
4486}
4487
4488fn delete_block_contents<H: crate::types::Host>(
4489 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4490 top: usize,
4491 bot: usize,
4492 left: usize,
4493 right: usize,
4494) {
4495 use hjkl_buffer::{Edit, MotionKind, Position};
4496 ed.sync_buffer_content_from_textarea();
4497 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4498 if last_row < top {
4499 return;
4500 }
4501 ed.mutate_edit(Edit::DeleteRange {
4502 start: Position::new(top, left),
4503 end: Position::new(last_row, right),
4504 kind: MotionKind::Block,
4505 });
4506 ed.push_buffer_cursor_to_textarea();
4507}
4508
4509fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4511 let (top, bot, left, right) = block_bounds(ed);
4512 ed.push_undo();
4513 ed.sync_buffer_content_from_textarea();
4514 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4515 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4516 let chars: Vec<char> = lines[r].chars().collect();
4517 if left >= chars.len() {
4518 continue;
4519 }
4520 let end = (right + 1).min(chars.len());
4521 let before: String = chars[..left].iter().collect();
4522 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4523 let after: String = chars[end..].iter().collect();
4524 lines[r] = format!("{before}{middle}{after}");
4525 }
4526 reset_textarea_lines(ed, lines);
4527 ed.vim.mode = Mode::Normal;
4528 ed.jump_cursor(top, left);
4529}
4530
4531fn reset_textarea_lines<H: crate::types::Host>(
4535 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4536 lines: Vec<String>,
4537) {
4538 let cursor = ed.cursor();
4539 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4540 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4541 ed.mark_content_dirty();
4542}
4543
4544type Pos = (usize, usize);
4550
4551fn text_object_range<H: crate::types::Host>(
4555 ed: &Editor<hjkl_buffer::Buffer, H>,
4556 obj: TextObject,
4557 inner: bool,
4558) -> Option<(Pos, Pos, MotionKind)> {
4559 match obj {
4560 TextObject::Word { big } => {
4561 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4562 }
4563 TextObject::Quote(q) => {
4564 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4565 }
4566 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4567 TextObject::Paragraph => {
4568 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4569 }
4570 TextObject::XmlTag => {
4571 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4572 }
4573 TextObject::Sentence => {
4574 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4575 }
4576 }
4577}
4578
4579fn sentence_boundary<H: crate::types::Host>(
4583 ed: &Editor<hjkl_buffer::Buffer, H>,
4584 forward: bool,
4585) -> Option<(usize, usize)> {
4586 let lines = buf_lines_to_vec(&ed.buffer);
4587 if lines.is_empty() {
4588 return None;
4589 }
4590 let pos_to_idx = |pos: (usize, usize)| -> usize {
4591 let mut idx = 0;
4592 for line in lines.iter().take(pos.0) {
4593 idx += line.chars().count() + 1;
4594 }
4595 idx + pos.1
4596 };
4597 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4598 for (r, line) in lines.iter().enumerate() {
4599 let len = line.chars().count();
4600 if idx <= len {
4601 return (r, idx);
4602 }
4603 idx -= len + 1;
4604 }
4605 let last = lines.len().saturating_sub(1);
4606 (last, lines[last].chars().count())
4607 };
4608 let mut chars: Vec<char> = Vec::new();
4609 for (r, line) in lines.iter().enumerate() {
4610 chars.extend(line.chars());
4611 if r + 1 < lines.len() {
4612 chars.push('\n');
4613 }
4614 }
4615 if chars.is_empty() {
4616 return None;
4617 }
4618 let total = chars.len();
4619 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4620 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4621
4622 if forward {
4623 let mut i = cursor_idx + 1;
4626 while i < total {
4627 if is_terminator(chars[i]) {
4628 while i + 1 < total && is_terminator(chars[i + 1]) {
4629 i += 1;
4630 }
4631 if i + 1 >= total {
4632 return None;
4633 }
4634 if chars[i + 1].is_whitespace() {
4635 let mut j = i + 1;
4636 while j < total && chars[j].is_whitespace() {
4637 j += 1;
4638 }
4639 if j >= total {
4640 return None;
4641 }
4642 return Some(idx_to_pos(j));
4643 }
4644 }
4645 i += 1;
4646 }
4647 None
4648 } else {
4649 let find_start = |from: usize| -> Option<usize> {
4653 let mut start = from;
4654 while start > 0 {
4655 let prev = chars[start - 1];
4656 if prev.is_whitespace() {
4657 let mut k = start - 1;
4658 while k > 0 && chars[k - 1].is_whitespace() {
4659 k -= 1;
4660 }
4661 if k > 0 && is_terminator(chars[k - 1]) {
4662 break;
4663 }
4664 }
4665 start -= 1;
4666 }
4667 while start < total && chars[start].is_whitespace() {
4668 start += 1;
4669 }
4670 (start < total).then_some(start)
4671 };
4672 let current_start = find_start(cursor_idx)?;
4673 if current_start < cursor_idx {
4674 return Some(idx_to_pos(current_start));
4675 }
4676 let mut k = current_start;
4679 while k > 0 && chars[k - 1].is_whitespace() {
4680 k -= 1;
4681 }
4682 if k == 0 {
4683 return None;
4684 }
4685 let prev_start = find_start(k - 1)?;
4686 Some(idx_to_pos(prev_start))
4687 }
4688}
4689
4690fn sentence_text_object<H: crate::types::Host>(
4696 ed: &Editor<hjkl_buffer::Buffer, H>,
4697 inner: bool,
4698) -> Option<((usize, usize), (usize, usize))> {
4699 let lines = buf_lines_to_vec(&ed.buffer);
4700 if lines.is_empty() {
4701 return None;
4702 }
4703 let pos_to_idx = |pos: (usize, usize)| -> usize {
4706 let mut idx = 0;
4707 for line in lines.iter().take(pos.0) {
4708 idx += line.chars().count() + 1;
4709 }
4710 idx + pos.1
4711 };
4712 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4713 for (r, line) in lines.iter().enumerate() {
4714 let len = line.chars().count();
4715 if idx <= len {
4716 return (r, idx);
4717 }
4718 idx -= len + 1;
4719 }
4720 let last = lines.len().saturating_sub(1);
4721 (last, lines[last].chars().count())
4722 };
4723 let mut chars: Vec<char> = Vec::new();
4724 for (r, line) in lines.iter().enumerate() {
4725 chars.extend(line.chars());
4726 if r + 1 < lines.len() {
4727 chars.push('\n');
4728 }
4729 }
4730 if chars.is_empty() {
4731 return None;
4732 }
4733
4734 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4735 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4736
4737 let mut start = cursor_idx;
4741 while start > 0 {
4742 let prev = chars[start - 1];
4743 if prev.is_whitespace() {
4744 let mut k = start - 1;
4748 while k > 0 && chars[k - 1].is_whitespace() {
4749 k -= 1;
4750 }
4751 if k > 0 && is_terminator(chars[k - 1]) {
4752 break;
4753 }
4754 }
4755 start -= 1;
4756 }
4757 while start < chars.len() && chars[start].is_whitespace() {
4760 start += 1;
4761 }
4762 if start >= chars.len() {
4763 return None;
4764 }
4765
4766 let mut end = start;
4769 while end < chars.len() {
4770 if is_terminator(chars[end]) {
4771 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4773 end += 1;
4774 }
4775 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4778 break;
4779 }
4780 }
4781 end += 1;
4782 }
4783 let end_idx = (end + 1).min(chars.len());
4785
4786 let final_end = if inner {
4787 end_idx
4788 } else {
4789 let mut e = end_idx;
4793 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4794 e += 1;
4795 }
4796 e
4797 };
4798
4799 Some((idx_to_pos(start), idx_to_pos(final_end)))
4800}
4801
4802fn tag_text_object<H: crate::types::Host>(
4806 ed: &Editor<hjkl_buffer::Buffer, H>,
4807 inner: bool,
4808) -> Option<((usize, usize), (usize, usize))> {
4809 let lines = buf_lines_to_vec(&ed.buffer);
4810 if lines.is_empty() {
4811 return None;
4812 }
4813 let pos_to_idx = |pos: (usize, usize)| -> usize {
4817 let mut idx = 0;
4818 for line in lines.iter().take(pos.0) {
4819 idx += line.chars().count() + 1;
4820 }
4821 idx + pos.1
4822 };
4823 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4824 for (r, line) in lines.iter().enumerate() {
4825 let len = line.chars().count();
4826 if idx <= len {
4827 return (r, idx);
4828 }
4829 idx -= len + 1;
4830 }
4831 let last = lines.len().saturating_sub(1);
4832 (last, lines[last].chars().count())
4833 };
4834 let mut chars: Vec<char> = Vec::new();
4835 for (r, line) in lines.iter().enumerate() {
4836 chars.extend(line.chars());
4837 if r + 1 < lines.len() {
4838 chars.push('\n');
4839 }
4840 }
4841 let cursor_idx = pos_to_idx(ed.cursor());
4842
4843 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4851 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4852 let mut i = 0;
4853 while i < chars.len() {
4854 if chars[i] != '<' {
4855 i += 1;
4856 continue;
4857 }
4858 let mut j = i + 1;
4859 while j < chars.len() && chars[j] != '>' {
4860 j += 1;
4861 }
4862 if j >= chars.len() {
4863 break;
4864 }
4865 let inside: String = chars[i + 1..j].iter().collect();
4866 let close_end = j + 1;
4867 let trimmed = inside.trim();
4868 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4869 i = close_end;
4870 continue;
4871 }
4872 if let Some(rest) = trimmed.strip_prefix('/') {
4873 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4874 if !name.is_empty()
4875 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4876 {
4877 let (open_start, content_start, _) = stack[stack_idx].clone();
4878 stack.truncate(stack_idx);
4879 let content_end = i;
4880 let candidate = (open_start, content_start, content_end, close_end);
4881 if cursor_idx >= content_start && cursor_idx <= content_end {
4882 innermost = match innermost {
4883 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4884 Some(candidate)
4885 }
4886 None => Some(candidate),
4887 existing => existing,
4888 };
4889 } else if open_start >= cursor_idx && next_after.is_none() {
4890 next_after = Some(candidate);
4891 }
4892 }
4893 } else if !trimmed.ends_with('/') {
4894 let name: String = trimmed
4895 .split(|c: char| c.is_whitespace() || c == '/')
4896 .next()
4897 .unwrap_or("")
4898 .to_string();
4899 if !name.is_empty() {
4900 stack.push((i, close_end, name));
4901 }
4902 }
4903 i = close_end;
4904 }
4905
4906 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4907 if inner {
4908 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4909 } else {
4910 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4911 }
4912}
4913
4914fn is_wordchar(c: char) -> bool {
4915 c.is_alphanumeric() || c == '_'
4916}
4917
4918pub(crate) use hjkl_buffer::is_keyword_char;
4922
4923fn word_text_object<H: crate::types::Host>(
4924 ed: &Editor<hjkl_buffer::Buffer, H>,
4925 inner: bool,
4926 big: bool,
4927) -> Option<((usize, usize), (usize, usize))> {
4928 let (row, col) = ed.cursor();
4929 let line = buf_line(&ed.buffer, row)?;
4930 let chars: Vec<char> = line.chars().collect();
4931 if chars.is_empty() {
4932 return None;
4933 }
4934 let at = col.min(chars.len().saturating_sub(1));
4935 let classify = |c: char| -> u8 {
4936 if c.is_whitespace() {
4937 0
4938 } else if big || is_wordchar(c) {
4939 1
4940 } else {
4941 2
4942 }
4943 };
4944 let cls = classify(chars[at]);
4945 let mut start = at;
4946 while start > 0 && classify(chars[start - 1]) == cls {
4947 start -= 1;
4948 }
4949 let mut end = at;
4950 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4951 end += 1;
4952 }
4953 let char_byte = |i: usize| {
4955 if i >= chars.len() {
4956 line.len()
4957 } else {
4958 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4959 }
4960 };
4961 let mut start_col = char_byte(start);
4962 let mut end_col = char_byte(end + 1);
4964 if !inner {
4965 let mut t = end + 1;
4967 let mut included_trailing = false;
4968 while t < chars.len() && chars[t].is_whitespace() {
4969 included_trailing = true;
4970 t += 1;
4971 }
4972 if included_trailing {
4973 end_col = char_byte(t);
4974 } else {
4975 let mut s = start;
4976 while s > 0 && chars[s - 1].is_whitespace() {
4977 s -= 1;
4978 }
4979 start_col = char_byte(s);
4980 }
4981 }
4982 Some(((row, start_col), (row, end_col)))
4983}
4984
4985fn quote_text_object<H: crate::types::Host>(
4986 ed: &Editor<hjkl_buffer::Buffer, H>,
4987 q: char,
4988 inner: bool,
4989) -> Option<((usize, usize), (usize, usize))> {
4990 let (row, col) = ed.cursor();
4991 let line = buf_line(&ed.buffer, row)?;
4992 let bytes = line.as_bytes();
4993 let q_byte = q as u8;
4994 let mut positions: Vec<usize> = Vec::new();
4996 for (i, &b) in bytes.iter().enumerate() {
4997 if b == q_byte {
4998 positions.push(i);
4999 }
5000 }
5001 if positions.len() < 2 {
5002 return None;
5003 }
5004 let mut open_idx: Option<usize> = None;
5005 let mut close_idx: Option<usize> = None;
5006 for pair in positions.chunks(2) {
5007 if pair.len() < 2 {
5008 break;
5009 }
5010 if col >= pair[0] && col <= pair[1] {
5011 open_idx = Some(pair[0]);
5012 close_idx = Some(pair[1]);
5013 break;
5014 }
5015 if col < pair[0] {
5016 open_idx = Some(pair[0]);
5017 close_idx = Some(pair[1]);
5018 break;
5019 }
5020 }
5021 let open = open_idx?;
5022 let close = close_idx?;
5023 if inner {
5025 if close <= open + 1 {
5026 return None;
5027 }
5028 Some(((row, open + 1), (row, close)))
5029 } else {
5030 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5037 let mut end = after_close;
5039 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5040 end += 1;
5041 }
5042 Some(((row, open), (row, end)))
5043 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5044 let mut start = open;
5046 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5047 start -= 1;
5048 }
5049 Some(((row, start), (row, close + 1)))
5050 } else {
5051 Some(((row, open), (row, close + 1)))
5052 }
5053 }
5054}
5055
5056fn bracket_text_object<H: crate::types::Host>(
5057 ed: &Editor<hjkl_buffer::Buffer, H>,
5058 open: char,
5059 inner: bool,
5060) -> Option<(Pos, Pos, MotionKind)> {
5061 let close = match open {
5062 '(' => ')',
5063 '[' => ']',
5064 '{' => '}',
5065 '<' => '>',
5066 _ => return None,
5067 };
5068 let (row, col) = ed.cursor();
5069 let lines = buf_lines_to_vec(&ed.buffer);
5070 let lines = lines.as_slice();
5071 let open_pos = find_open_bracket(lines, row, col, open, close)
5076 .or_else(|| find_next_open(lines, row, col, open))?;
5077 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5078 if inner {
5080 if close_pos.0 > open_pos.0 + 1 {
5086 let inner_row_start = open_pos.0 + 1;
5088 let inner_row_end = close_pos.0 - 1;
5089 let end_col = lines
5090 .get(inner_row_end)
5091 .map(|l| l.chars().count())
5092 .unwrap_or(0);
5093 return Some((
5094 (inner_row_start, 0),
5095 (inner_row_end, end_col),
5096 MotionKind::Linewise,
5097 ));
5098 }
5099 let inner_start = advance_pos(lines, open_pos);
5100 if inner_start.0 > close_pos.0
5101 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5102 {
5103 return None;
5104 }
5105 Some((inner_start, close_pos, MotionKind::Exclusive))
5106 } else {
5107 Some((
5108 open_pos,
5109 advance_pos(lines, close_pos),
5110 MotionKind::Exclusive,
5111 ))
5112 }
5113}
5114
5115fn find_open_bracket(
5116 lines: &[String],
5117 row: usize,
5118 col: usize,
5119 open: char,
5120 close: char,
5121) -> Option<(usize, usize)> {
5122 let mut depth: i32 = 0;
5123 let mut r = row;
5124 let mut c = col as isize;
5125 loop {
5126 let cur = &lines[r];
5127 let chars: Vec<char> = cur.chars().collect();
5128 if (c as usize) >= chars.len() {
5132 c = chars.len() as isize - 1;
5133 }
5134 while c >= 0 {
5135 let ch = chars[c as usize];
5136 if ch == close {
5137 depth += 1;
5138 } else if ch == open {
5139 if depth == 0 {
5140 return Some((r, c as usize));
5141 }
5142 depth -= 1;
5143 }
5144 c -= 1;
5145 }
5146 if r == 0 {
5147 return None;
5148 }
5149 r -= 1;
5150 c = lines[r].chars().count() as isize - 1;
5151 }
5152}
5153
5154fn find_close_bracket(
5155 lines: &[String],
5156 row: usize,
5157 start_col: usize,
5158 open: char,
5159 close: char,
5160) -> Option<(usize, usize)> {
5161 let mut depth: i32 = 0;
5162 let mut r = row;
5163 let mut c = start_col;
5164 loop {
5165 let cur = &lines[r];
5166 let chars: Vec<char> = cur.chars().collect();
5167 while c < chars.len() {
5168 let ch = chars[c];
5169 if ch == open {
5170 depth += 1;
5171 } else if ch == close {
5172 if depth == 0 {
5173 return Some((r, c));
5174 }
5175 depth -= 1;
5176 }
5177 c += 1;
5178 }
5179 if r + 1 >= lines.len() {
5180 return None;
5181 }
5182 r += 1;
5183 c = 0;
5184 }
5185}
5186
5187fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5191 let mut r = row;
5192 let mut c = col;
5193 while r < lines.len() {
5194 let chars: Vec<char> = lines[r].chars().collect();
5195 while c < chars.len() {
5196 if chars[c] == open {
5197 return Some((r, c));
5198 }
5199 c += 1;
5200 }
5201 r += 1;
5202 c = 0;
5203 }
5204 None
5205}
5206
5207fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5208 let (r, c) = pos;
5209 let line_len = lines[r].chars().count();
5210 if c < line_len {
5211 (r, c + 1)
5212 } else if r + 1 < lines.len() {
5213 (r + 1, 0)
5214 } else {
5215 pos
5216 }
5217}
5218
5219fn paragraph_text_object<H: crate::types::Host>(
5220 ed: &Editor<hjkl_buffer::Buffer, H>,
5221 inner: bool,
5222) -> Option<((usize, usize), (usize, usize))> {
5223 let (row, _) = ed.cursor();
5224 let lines = buf_lines_to_vec(&ed.buffer);
5225 if lines.is_empty() {
5226 return None;
5227 }
5228 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5230 if is_blank(row) {
5231 return None;
5232 }
5233 let mut top = row;
5234 while top > 0 && !is_blank(top - 1) {
5235 top -= 1;
5236 }
5237 let mut bot = row;
5238 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5239 bot += 1;
5240 }
5241 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5243 bot += 1;
5244 }
5245 let end_col = lines[bot].chars().count();
5246 Some(((top, 0), (bot, end_col)))
5247}
5248
5249fn read_vim_range<H: crate::types::Host>(
5255 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5256 start: (usize, usize),
5257 end: (usize, usize),
5258 kind: MotionKind,
5259) -> String {
5260 let (top, bot) = order(start, end);
5261 ed.sync_buffer_content_from_textarea();
5262 let lines = buf_lines_to_vec(&ed.buffer);
5263 match kind {
5264 MotionKind::Linewise => {
5265 let lo = top.0;
5266 let hi = bot.0.min(lines.len().saturating_sub(1));
5267 let mut text = lines[lo..=hi].join("\n");
5268 text.push('\n');
5269 text
5270 }
5271 MotionKind::Inclusive | MotionKind::Exclusive => {
5272 let inclusive = matches!(kind, MotionKind::Inclusive);
5273 let mut out = String::new();
5275 for row in top.0..=bot.0 {
5276 let line = lines.get(row).map(String::as_str).unwrap_or("");
5277 let lo = if row == top.0 { top.1 } else { 0 };
5278 let hi_unclamped = if row == bot.0 {
5279 if inclusive { bot.1 + 1 } else { bot.1 }
5280 } else {
5281 line.chars().count() + 1
5282 };
5283 let row_chars: Vec<char> = line.chars().collect();
5284 let hi = hi_unclamped.min(row_chars.len());
5285 if lo < hi {
5286 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5287 }
5288 if row < bot.0 {
5289 out.push('\n');
5290 }
5291 }
5292 out
5293 }
5294 }
5295}
5296
5297fn cut_vim_range<H: crate::types::Host>(
5306 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5307 start: (usize, usize),
5308 end: (usize, usize),
5309 kind: MotionKind,
5310) -> String {
5311 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5312 let (top, bot) = order(start, end);
5313 ed.sync_buffer_content_from_textarea();
5314 let (buf_start, buf_end, buf_kind) = match kind {
5315 MotionKind::Linewise => (
5316 Position::new(top.0, 0),
5317 Position::new(bot.0, 0),
5318 BufKind::Line,
5319 ),
5320 MotionKind::Inclusive => {
5321 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5322 let next = if bot.1 < line_chars {
5326 Position::new(bot.0, bot.1 + 1)
5327 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5328 Position::new(bot.0 + 1, 0)
5329 } else {
5330 Position::new(bot.0, line_chars)
5331 };
5332 (Position::new(top.0, top.1), next, BufKind::Char)
5333 }
5334 MotionKind::Exclusive => (
5335 Position::new(top.0, top.1),
5336 Position::new(bot.0, bot.1),
5337 BufKind::Char,
5338 ),
5339 };
5340 let inverse = ed.mutate_edit(Edit::DeleteRange {
5341 start: buf_start,
5342 end: buf_end,
5343 kind: buf_kind,
5344 });
5345 let text = match inverse {
5346 Edit::InsertStr { text, .. } => text,
5347 _ => String::new(),
5348 };
5349 if !text.is_empty() {
5350 ed.record_yank_to_host(text.clone());
5351 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5352 }
5353 ed.push_buffer_cursor_to_textarea();
5354 text
5355}
5356
5357fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5363 use hjkl_buffer::{Edit, MotionKind, Position};
5364 ed.sync_buffer_content_from_textarea();
5365 let cursor = buf_cursor_pos(&ed.buffer);
5366 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5367 if cursor.col >= line_chars {
5368 return;
5369 }
5370 let inverse = ed.mutate_edit(Edit::DeleteRange {
5371 start: cursor,
5372 end: Position::new(cursor.row, line_chars),
5373 kind: MotionKind::Char,
5374 });
5375 if let Edit::InsertStr { text, .. } = inverse
5376 && !text.is_empty()
5377 {
5378 ed.record_yank_to_host(text.clone());
5379 ed.vim.yank_linewise = false;
5380 ed.set_yank(text);
5381 }
5382 buf_set_cursor_pos(&mut ed.buffer, cursor);
5383 ed.push_buffer_cursor_to_textarea();
5384}
5385
5386fn do_char_delete<H: crate::types::Host>(
5387 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5388 forward: bool,
5389 count: usize,
5390) {
5391 use hjkl_buffer::{Edit, MotionKind, Position};
5392 ed.push_undo();
5393 ed.sync_buffer_content_from_textarea();
5394 let mut deleted = String::new();
5397 for _ in 0..count {
5398 let cursor = buf_cursor_pos(&ed.buffer);
5399 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5400 if forward {
5401 if cursor.col >= line_chars {
5404 continue;
5405 }
5406 let inverse = ed.mutate_edit(Edit::DeleteRange {
5407 start: cursor,
5408 end: Position::new(cursor.row, cursor.col + 1),
5409 kind: MotionKind::Char,
5410 });
5411 if let Edit::InsertStr { text, .. } = inverse {
5412 deleted.push_str(&text);
5413 }
5414 } else {
5415 if cursor.col == 0 {
5417 continue;
5418 }
5419 let inverse = ed.mutate_edit(Edit::DeleteRange {
5420 start: Position::new(cursor.row, cursor.col - 1),
5421 end: cursor,
5422 kind: MotionKind::Char,
5423 });
5424 if let Edit::InsertStr { text, .. } = inverse {
5425 deleted = text + &deleted;
5428 }
5429 }
5430 }
5431 if !deleted.is_empty() {
5432 ed.record_yank_to_host(deleted.clone());
5433 ed.record_delete(deleted, false);
5434 }
5435 ed.push_buffer_cursor_to_textarea();
5436}
5437
5438fn adjust_number<H: crate::types::Host>(
5442 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5443 delta: i64,
5444) -> bool {
5445 use hjkl_buffer::{Edit, MotionKind, Position};
5446 ed.sync_buffer_content_from_textarea();
5447 let cursor = buf_cursor_pos(&ed.buffer);
5448 let row = cursor.row;
5449 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5450 Some(l) => l.chars().collect(),
5451 None => return false,
5452 };
5453 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5454 return false;
5455 };
5456 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5457 digit_start - 1
5458 } else {
5459 digit_start
5460 };
5461 let mut span_end = digit_start;
5462 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5463 span_end += 1;
5464 }
5465 let s: String = chars[span_start..span_end].iter().collect();
5466 let Ok(n) = s.parse::<i64>() else {
5467 return false;
5468 };
5469 let new_s = n.saturating_add(delta).to_string();
5470
5471 ed.push_undo();
5472 let span_start_pos = Position::new(row, span_start);
5473 let span_end_pos = Position::new(row, span_end);
5474 ed.mutate_edit(Edit::DeleteRange {
5475 start: span_start_pos,
5476 end: span_end_pos,
5477 kind: MotionKind::Char,
5478 });
5479 ed.mutate_edit(Edit::InsertStr {
5480 at: span_start_pos,
5481 text: new_s.clone(),
5482 });
5483 let new_len = new_s.chars().count();
5484 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5485 ed.push_buffer_cursor_to_textarea();
5486 true
5487}
5488
5489pub(crate) fn replace_char<H: crate::types::Host>(
5490 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5491 ch: char,
5492 count: usize,
5493) {
5494 use hjkl_buffer::{Edit, MotionKind, Position};
5495 ed.push_undo();
5496 ed.sync_buffer_content_from_textarea();
5497 for _ in 0..count {
5498 let cursor = buf_cursor_pos(&ed.buffer);
5499 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5500 if cursor.col >= line_chars {
5501 break;
5502 }
5503 ed.mutate_edit(Edit::DeleteRange {
5504 start: cursor,
5505 end: Position::new(cursor.row, cursor.col + 1),
5506 kind: MotionKind::Char,
5507 });
5508 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5509 }
5510 crate::motions::move_left(&mut ed.buffer, 1);
5512 ed.push_buffer_cursor_to_textarea();
5513}
5514
5515fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5516 use hjkl_buffer::{Edit, MotionKind, Position};
5517 ed.sync_buffer_content_from_textarea();
5518 let cursor = buf_cursor_pos(&ed.buffer);
5519 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5520 return;
5521 };
5522 let toggled = if c.is_uppercase() {
5523 c.to_lowercase().next().unwrap_or(c)
5524 } else {
5525 c.to_uppercase().next().unwrap_or(c)
5526 };
5527 ed.mutate_edit(Edit::DeleteRange {
5528 start: cursor,
5529 end: Position::new(cursor.row, cursor.col + 1),
5530 kind: MotionKind::Char,
5531 });
5532 ed.mutate_edit(Edit::InsertChar {
5533 at: cursor,
5534 ch: toggled,
5535 });
5536}
5537
5538fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5539 use hjkl_buffer::{Edit, Position};
5540 ed.sync_buffer_content_from_textarea();
5541 let row = buf_cursor_pos(&ed.buffer).row;
5542 if row + 1 >= buf_row_count(&ed.buffer) {
5543 return;
5544 }
5545 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5546 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5547 let next_trimmed = next_raw.trim_start();
5548 let cur_chars = cur_line.chars().count();
5549 let next_chars = next_raw.chars().count();
5550 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5553 " "
5554 } else {
5555 ""
5556 };
5557 let joined = format!("{cur_line}{separator}{next_trimmed}");
5558 ed.mutate_edit(Edit::Replace {
5559 start: Position::new(row, 0),
5560 end: Position::new(row + 1, next_chars),
5561 with: joined,
5562 });
5563 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5567 ed.push_buffer_cursor_to_textarea();
5568}
5569
5570fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5573 use hjkl_buffer::Edit;
5574 ed.sync_buffer_content_from_textarea();
5575 let row = buf_cursor_pos(&ed.buffer).row;
5576 if row + 1 >= buf_row_count(&ed.buffer) {
5577 return;
5578 }
5579 let join_col = buf_line_chars(&ed.buffer, row);
5580 ed.mutate_edit(Edit::JoinLines {
5581 row,
5582 count: 1,
5583 with_space: false,
5584 });
5585 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5587 ed.push_buffer_cursor_to_textarea();
5588}
5589
5590fn do_paste<H: crate::types::Host>(
5591 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5592 before: bool,
5593 count: usize,
5594) {
5595 use hjkl_buffer::{Edit, Position};
5596 ed.push_undo();
5597 let selector = ed.vim.pending_register.take();
5602 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5603 Some(slot) => (slot.text.clone(), slot.linewise),
5604 None => {
5610 let s = &ed.registers().unnamed;
5611 (s.text.clone(), s.linewise)
5612 }
5613 };
5614 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5618 for _ in 0..count {
5619 ed.sync_buffer_content_from_textarea();
5620 let yank = yank.clone();
5621 if yank.is_empty() {
5622 continue;
5623 }
5624 if linewise {
5625 let text = yank.trim_matches('\n').to_string();
5629 let row = buf_cursor_pos(&ed.buffer).row;
5630 let target_row = if before {
5631 ed.mutate_edit(Edit::InsertStr {
5632 at: Position::new(row, 0),
5633 text: format!("{text}\n"),
5634 });
5635 row
5636 } else {
5637 let line_chars = buf_line_chars(&ed.buffer, row);
5638 ed.mutate_edit(Edit::InsertStr {
5639 at: Position::new(row, line_chars),
5640 text: format!("\n{text}"),
5641 });
5642 row + 1
5643 };
5644 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5645 crate::motions::move_first_non_blank(&mut ed.buffer);
5646 ed.push_buffer_cursor_to_textarea();
5647 let payload_lines = text.lines().count().max(1);
5649 let bot_row = target_row + payload_lines - 1;
5650 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5651 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5652 } else {
5653 let cursor = buf_cursor_pos(&ed.buffer);
5657 let at = if before {
5658 cursor
5659 } else {
5660 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5661 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5662 };
5663 ed.mutate_edit(Edit::InsertStr {
5664 at,
5665 text: yank.clone(),
5666 });
5667 crate::motions::move_left(&mut ed.buffer, 1);
5670 ed.push_buffer_cursor_to_textarea();
5671 let lo = (at.row, at.col);
5673 let hi = ed.cursor();
5674 paste_mark = Some((lo, hi));
5675 }
5676 }
5677 if let Some((lo, hi)) = paste_mark {
5678 ed.set_mark('[', lo);
5679 ed.set_mark(']', hi);
5680 }
5681 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5683}
5684
5685pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5686 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5687 let current = ed.snapshot();
5688 ed.redo_stack.push(current);
5689 ed.restore(lines, cursor);
5690 }
5691 ed.vim.mode = Mode::Normal;
5692 clamp_cursor_to_normal_mode(ed);
5696}
5697
5698pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5699 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5700 let current = ed.snapshot();
5701 ed.undo_stack.push(current);
5702 ed.cap_undo();
5703 ed.restore(lines, cursor);
5704 }
5705 ed.vim.mode = Mode::Normal;
5706}
5707
5708fn replay_insert_and_finish<H: crate::types::Host>(
5715 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5716 text: &str,
5717) {
5718 use hjkl_buffer::{Edit, Position};
5719 let cursor = ed.cursor();
5720 ed.mutate_edit(Edit::InsertStr {
5721 at: Position::new(cursor.0, cursor.1),
5722 text: text.to_string(),
5723 });
5724 if ed.vim.insert_session.take().is_some() {
5725 if ed.cursor().1 > 0 {
5726 crate::motions::move_left(&mut ed.buffer, 1);
5727 ed.push_buffer_cursor_to_textarea();
5728 }
5729 ed.vim.mode = Mode::Normal;
5730 }
5731}
5732
5733fn replay_last_change<H: crate::types::Host>(
5734 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5735 outer_count: usize,
5736) {
5737 let Some(change) = ed.vim.last_change.clone() else {
5738 return;
5739 };
5740 ed.vim.replaying = true;
5741 let scale = if outer_count > 0 { outer_count } else { 1 };
5742 match change {
5743 LastChange::OpMotion {
5744 op,
5745 motion,
5746 count,
5747 inserted,
5748 } => {
5749 let total = count.max(1) * scale;
5750 apply_op_with_motion(ed, op, &motion, total);
5751 if let Some(text) = inserted {
5752 replay_insert_and_finish(ed, &text);
5753 }
5754 }
5755 LastChange::OpTextObj {
5756 op,
5757 obj,
5758 inner,
5759 inserted,
5760 } => {
5761 apply_op_with_text_object(ed, op, obj, inner);
5762 if let Some(text) = inserted {
5763 replay_insert_and_finish(ed, &text);
5764 }
5765 }
5766 LastChange::LineOp {
5767 op,
5768 count,
5769 inserted,
5770 } => {
5771 let total = count.max(1) * scale;
5772 execute_line_op(ed, op, total);
5773 if let Some(text) = inserted {
5774 replay_insert_and_finish(ed, &text);
5775 }
5776 }
5777 LastChange::CharDel { forward, count } => {
5778 do_char_delete(ed, forward, count * scale);
5779 }
5780 LastChange::ReplaceChar { ch, count } => {
5781 replace_char(ed, ch, count * scale);
5782 }
5783 LastChange::ToggleCase { count } => {
5784 for _ in 0..count * scale {
5785 ed.push_undo();
5786 toggle_case_at_cursor(ed);
5787 }
5788 }
5789 LastChange::JoinLine { count } => {
5790 for _ in 0..count * scale {
5791 ed.push_undo();
5792 join_line(ed);
5793 }
5794 }
5795 LastChange::Paste { before, count } => {
5796 do_paste(ed, before, count * scale);
5797 }
5798 LastChange::DeleteToEol { inserted } => {
5799 use hjkl_buffer::{Edit, Position};
5800 ed.push_undo();
5801 delete_to_eol(ed);
5802 if let Some(text) = inserted {
5803 let cursor = ed.cursor();
5804 ed.mutate_edit(Edit::InsertStr {
5805 at: Position::new(cursor.0, cursor.1),
5806 text,
5807 });
5808 }
5809 }
5810 LastChange::OpenLine { above, inserted } => {
5811 use hjkl_buffer::{Edit, Position};
5812 ed.push_undo();
5813 ed.sync_buffer_content_from_textarea();
5814 let row = buf_cursor_pos(&ed.buffer).row;
5815 if above {
5816 ed.mutate_edit(Edit::InsertStr {
5817 at: Position::new(row, 0),
5818 text: "\n".to_string(),
5819 });
5820 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5821 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5822 } else {
5823 let line_chars = buf_line_chars(&ed.buffer, row);
5824 ed.mutate_edit(Edit::InsertStr {
5825 at: Position::new(row, line_chars),
5826 text: "\n".to_string(),
5827 });
5828 }
5829 ed.push_buffer_cursor_to_textarea();
5830 let cursor = ed.cursor();
5831 ed.mutate_edit(Edit::InsertStr {
5832 at: Position::new(cursor.0, cursor.1),
5833 text: inserted,
5834 });
5835 }
5836 LastChange::InsertAt {
5837 entry,
5838 inserted,
5839 count,
5840 } => {
5841 use hjkl_buffer::{Edit, Position};
5842 ed.push_undo();
5843 match entry {
5844 InsertEntry::I => {}
5845 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5846 InsertEntry::A => {
5847 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5848 ed.push_buffer_cursor_to_textarea();
5849 }
5850 InsertEntry::ShiftA => {
5851 crate::motions::move_line_end(&mut ed.buffer);
5852 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5853 ed.push_buffer_cursor_to_textarea();
5854 }
5855 }
5856 for _ in 0..count.max(1) {
5857 let cursor = ed.cursor();
5858 ed.mutate_edit(Edit::InsertStr {
5859 at: Position::new(cursor.0, cursor.1),
5860 text: inserted.clone(),
5861 });
5862 }
5863 }
5864 }
5865 ed.vim.replaying = false;
5866}
5867
5868fn extract_inserted(before: &str, after: &str) -> String {
5871 let before_chars: Vec<char> = before.chars().collect();
5872 let after_chars: Vec<char> = after.chars().collect();
5873 if after_chars.len() <= before_chars.len() {
5874 return String::new();
5875 }
5876 let prefix = before_chars
5877 .iter()
5878 .zip(after_chars.iter())
5879 .take_while(|(a, b)| a == b)
5880 .count();
5881 let max_suffix = before_chars.len() - prefix;
5882 let suffix = before_chars
5883 .iter()
5884 .rev()
5885 .zip(after_chars.iter().rev())
5886 .take(max_suffix)
5887 .take_while(|(a, b)| a == b)
5888 .count();
5889 after_chars[prefix..after_chars.len() - suffix]
5890 .iter()
5891 .collect()
5892}
5893
5894#[cfg(all(test, feature = "crossterm"))]
5897mod tests {
5898 use crate::VimMode;
5899 use crate::editor::Editor;
5900 use crate::types::Host;
5901 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5902
5903 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5904 let mut iter = keys.chars().peekable();
5908 while let Some(c) = iter.next() {
5909 if c == '<' {
5910 let mut tag = String::new();
5911 for ch in iter.by_ref() {
5912 if ch == '>' {
5913 break;
5914 }
5915 tag.push(ch);
5916 }
5917 let ev = match tag.as_str() {
5918 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5919 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5920 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5921 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5922 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5923 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5924 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5925 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5926 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5930 s if s.starts_with("C-") => {
5931 let ch = s.chars().nth(2).unwrap();
5932 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5933 }
5934 _ => continue,
5935 };
5936 e.handle_key(ev);
5937 } else {
5938 let mods = if c.is_uppercase() {
5939 KeyModifiers::SHIFT
5940 } else {
5941 KeyModifiers::NONE
5942 };
5943 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5944 }
5945 }
5946 }
5947
5948 fn editor_with(content: &str) -> Editor {
5949 let opts = crate::types::Options {
5954 shiftwidth: 2,
5955 ..crate::types::Options::default()
5956 };
5957 let mut e = Editor::new(
5958 hjkl_buffer::Buffer::new(),
5959 crate::types::DefaultHost::new(),
5960 opts,
5961 );
5962 e.set_content(content);
5963 e
5964 }
5965
5966 #[test]
5967 fn f_char_jumps_on_line() {
5968 let mut e = editor_with("hello world");
5969 run_keys(&mut e, "fw");
5970 assert_eq!(e.cursor(), (0, 6));
5971 }
5972
5973 #[test]
5974 fn cap_f_jumps_backward() {
5975 let mut e = editor_with("hello world");
5976 e.jump_cursor(0, 10);
5977 run_keys(&mut e, "Fo");
5978 assert_eq!(e.cursor().1, 7);
5979 }
5980
5981 #[test]
5982 fn t_stops_before_char() {
5983 let mut e = editor_with("hello");
5984 run_keys(&mut e, "tl");
5985 assert_eq!(e.cursor(), (0, 1));
5986 }
5987
5988 #[test]
5989 fn semicolon_repeats_find() {
5990 let mut e = editor_with("aa.bb.cc");
5991 run_keys(&mut e, "f.");
5992 assert_eq!(e.cursor().1, 2);
5993 run_keys(&mut e, ";");
5994 assert_eq!(e.cursor().1, 5);
5995 }
5996
5997 #[test]
5998 fn comma_repeats_find_reverse() {
5999 let mut e = editor_with("aa.bb.cc");
6000 run_keys(&mut e, "f.");
6001 run_keys(&mut e, ";");
6002 run_keys(&mut e, ",");
6003 assert_eq!(e.cursor().1, 2);
6004 }
6005
6006 #[test]
6007 fn di_quote_deletes_content() {
6008 let mut e = editor_with("foo \"bar\" baz");
6009 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6011 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6012 }
6013
6014 #[test]
6015 fn da_quote_deletes_with_quotes() {
6016 let mut e = editor_with("foo \"bar\" baz");
6019 e.jump_cursor(0, 6);
6020 run_keys(&mut e, "da\"");
6021 assert_eq!(e.buffer().lines()[0], "foo baz");
6022 }
6023
6024 #[test]
6025 fn ci_paren_deletes_and_inserts() {
6026 let mut e = editor_with("fn(a, b, c)");
6027 e.jump_cursor(0, 5);
6028 run_keys(&mut e, "ci(");
6029 assert_eq!(e.vim_mode(), VimMode::Insert);
6030 assert_eq!(e.buffer().lines()[0], "fn()");
6031 }
6032
6033 #[test]
6034 fn diw_deletes_inner_word() {
6035 let mut e = editor_with("hello world");
6036 e.jump_cursor(0, 2);
6037 run_keys(&mut e, "diw");
6038 assert_eq!(e.buffer().lines()[0], " world");
6039 }
6040
6041 #[test]
6042 fn daw_deletes_word_with_trailing_space() {
6043 let mut e = editor_with("hello world");
6044 run_keys(&mut e, "daw");
6045 assert_eq!(e.buffer().lines()[0], "world");
6046 }
6047
6048 #[test]
6049 fn percent_jumps_to_matching_bracket() {
6050 let mut e = editor_with("foo(bar)");
6051 e.jump_cursor(0, 3);
6052 run_keys(&mut e, "%");
6053 assert_eq!(e.cursor().1, 7);
6054 run_keys(&mut e, "%");
6055 assert_eq!(e.cursor().1, 3);
6056 }
6057
6058 #[test]
6059 fn dot_repeats_last_change() {
6060 let mut e = editor_with("aaa bbb ccc");
6061 run_keys(&mut e, "dw");
6062 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6063 run_keys(&mut e, ".");
6064 assert_eq!(e.buffer().lines()[0], "ccc");
6065 }
6066
6067 #[test]
6068 fn dot_repeats_change_operator_with_text() {
6069 let mut e = editor_with("foo foo foo");
6070 run_keys(&mut e, "cwbar<Esc>");
6071 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6072 run_keys(&mut e, "w");
6074 run_keys(&mut e, ".");
6075 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6076 }
6077
6078 #[test]
6079 fn dot_repeats_x() {
6080 let mut e = editor_with("abcdef");
6081 run_keys(&mut e, "x");
6082 run_keys(&mut e, "..");
6083 assert_eq!(e.buffer().lines()[0], "def");
6084 }
6085
6086 #[test]
6087 fn count_operator_motion_compose() {
6088 let mut e = editor_with("one two three four five");
6089 run_keys(&mut e, "d3w");
6090 assert_eq!(e.buffer().lines()[0], "four five");
6091 }
6092
6093 #[test]
6094 fn two_dd_deletes_two_lines() {
6095 let mut e = editor_with("a\nb\nc");
6096 run_keys(&mut e, "2dd");
6097 assert_eq!(e.buffer().lines().len(), 1);
6098 assert_eq!(e.buffer().lines()[0], "c");
6099 }
6100
6101 #[test]
6106 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6107 let mut e = editor_with("one\ntwo\n three\nfour");
6108 e.jump_cursor(1, 2);
6109 run_keys(&mut e, "dd");
6110 assert_eq!(e.buffer().lines()[1], " three");
6112 assert_eq!(e.cursor(), (1, 4));
6113 }
6114
6115 #[test]
6116 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6117 let mut e = editor_with("one\n two\nthree");
6118 e.jump_cursor(2, 0);
6119 run_keys(&mut e, "dd");
6120 assert_eq!(e.buffer().lines().len(), 2);
6122 assert_eq!(e.cursor(), (1, 2));
6123 }
6124
6125 #[test]
6126 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6127 let mut e = editor_with("lonely");
6128 run_keys(&mut e, "dd");
6129 assert_eq!(e.buffer().lines().len(), 1);
6130 assert_eq!(e.buffer().lines()[0], "");
6131 assert_eq!(e.cursor(), (0, 0));
6132 }
6133
6134 #[test]
6135 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6136 let mut e = editor_with("a\nb\nc\n d\ne");
6137 e.jump_cursor(1, 0);
6139 run_keys(&mut e, "3dd");
6140 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6141 assert_eq!(e.cursor(), (1, 0));
6142 }
6143
6144 #[test]
6145 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6146 let mut e = editor_with(" line one\n line two\n xyz!");
6165 e.jump_cursor(0, 8);
6167 assert_eq!(e.cursor(), (0, 8));
6168 run_keys(&mut e, "dd");
6171 assert_eq!(
6172 e.cursor(),
6173 (0, 4),
6174 "dd must place cursor on first-non-blank"
6175 );
6176 run_keys(&mut e, "j");
6180 let (row, col) = e.cursor();
6181 assert_eq!(row, 1);
6182 assert_eq!(
6183 col, 4,
6184 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6185 );
6186 }
6187
6188 #[test]
6189 fn gu_lowercases_motion_range() {
6190 let mut e = editor_with("HELLO WORLD");
6191 run_keys(&mut e, "guw");
6192 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6193 assert_eq!(e.cursor(), (0, 0));
6194 }
6195
6196 #[test]
6197 fn g_u_uppercases_text_object() {
6198 let mut e = editor_with("hello world");
6199 run_keys(&mut e, "gUiw");
6201 assert_eq!(e.buffer().lines()[0], "HELLO world");
6202 assert_eq!(e.cursor(), (0, 0));
6203 }
6204
6205 #[test]
6206 fn g_tilde_toggles_case_of_range() {
6207 let mut e = editor_with("Hello World");
6208 run_keys(&mut e, "g~iw");
6209 assert_eq!(e.buffer().lines()[0], "hELLO World");
6210 }
6211
6212 #[test]
6213 fn g_uu_uppercases_current_line() {
6214 let mut e = editor_with("select 1\nselect 2");
6215 run_keys(&mut e, "gUU");
6216 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6217 assert_eq!(e.buffer().lines()[1], "select 2");
6218 }
6219
6220 #[test]
6221 fn gugu_lowercases_current_line() {
6222 let mut e = editor_with("FOO BAR\nBAZ");
6223 run_keys(&mut e, "gugu");
6224 assert_eq!(e.buffer().lines()[0], "foo bar");
6225 }
6226
6227 #[test]
6228 fn visual_u_uppercases_selection() {
6229 let mut e = editor_with("hello world");
6230 run_keys(&mut e, "veU");
6232 assert_eq!(e.buffer().lines()[0], "HELLO world");
6233 }
6234
6235 #[test]
6236 fn visual_line_u_lowercases_line() {
6237 let mut e = editor_with("HELLO WORLD\nOTHER");
6238 run_keys(&mut e, "Vu");
6239 assert_eq!(e.buffer().lines()[0], "hello world");
6240 assert_eq!(e.buffer().lines()[1], "OTHER");
6241 }
6242
6243 #[test]
6244 fn g_uu_with_count_uppercases_multiple_lines() {
6245 let mut e = editor_with("one\ntwo\nthree\nfour");
6246 run_keys(&mut e, "3gUU");
6248 assert_eq!(e.buffer().lines()[0], "ONE");
6249 assert_eq!(e.buffer().lines()[1], "TWO");
6250 assert_eq!(e.buffer().lines()[2], "THREE");
6251 assert_eq!(e.buffer().lines()[3], "four");
6252 }
6253
6254 #[test]
6255 fn double_gt_indents_current_line() {
6256 let mut e = editor_with("hello");
6257 run_keys(&mut e, ">>");
6258 assert_eq!(e.buffer().lines()[0], " hello");
6259 assert_eq!(e.cursor(), (0, 2));
6261 }
6262
6263 #[test]
6264 fn double_lt_outdents_current_line() {
6265 let mut e = editor_with(" hello");
6266 run_keys(&mut e, "<lt><lt>");
6267 assert_eq!(e.buffer().lines()[0], " hello");
6268 assert_eq!(e.cursor(), (0, 2));
6269 }
6270
6271 #[test]
6272 fn count_double_gt_indents_multiple_lines() {
6273 let mut e = editor_with("a\nb\nc\nd");
6274 run_keys(&mut e, "3>>");
6276 assert_eq!(e.buffer().lines()[0], " a");
6277 assert_eq!(e.buffer().lines()[1], " b");
6278 assert_eq!(e.buffer().lines()[2], " c");
6279 assert_eq!(e.buffer().lines()[3], "d");
6280 }
6281
6282 #[test]
6283 fn outdent_clips_ragged_leading_whitespace() {
6284 let mut e = editor_with(" x");
6287 run_keys(&mut e, "<lt><lt>");
6288 assert_eq!(e.buffer().lines()[0], "x");
6289 }
6290
6291 #[test]
6292 fn indent_motion_is_always_linewise() {
6293 let mut e = editor_with("foo bar");
6296 run_keys(&mut e, ">w");
6297 assert_eq!(e.buffer().lines()[0], " foo bar");
6298 }
6299
6300 #[test]
6301 fn indent_text_object_extends_over_paragraph() {
6302 let mut e = editor_with("a\nb\n\nc\nd");
6303 run_keys(&mut e, ">ap");
6305 assert_eq!(e.buffer().lines()[0], " a");
6306 assert_eq!(e.buffer().lines()[1], " b");
6307 assert_eq!(e.buffer().lines()[2], "");
6308 assert_eq!(e.buffer().lines()[3], "c");
6309 }
6310
6311 #[test]
6312 fn visual_line_indent_shifts_selected_rows() {
6313 let mut e = editor_with("x\ny\nz");
6314 run_keys(&mut e, "Vj>");
6316 assert_eq!(e.buffer().lines()[0], " x");
6317 assert_eq!(e.buffer().lines()[1], " y");
6318 assert_eq!(e.buffer().lines()[2], "z");
6319 }
6320
6321 #[test]
6322 fn outdent_empty_line_is_noop() {
6323 let mut e = editor_with("\nfoo");
6324 run_keys(&mut e, "<lt><lt>");
6325 assert_eq!(e.buffer().lines()[0], "");
6326 }
6327
6328 #[test]
6329 fn indent_skips_empty_lines() {
6330 let mut e = editor_with("");
6333 run_keys(&mut e, ">>");
6334 assert_eq!(e.buffer().lines()[0], "");
6335 }
6336
6337 #[test]
6338 fn insert_ctrl_t_indents_current_line() {
6339 let mut e = editor_with("x");
6340 run_keys(&mut e, "i<C-t>");
6342 assert_eq!(e.buffer().lines()[0], " x");
6343 assert_eq!(e.cursor(), (0, 2));
6346 }
6347
6348 #[test]
6349 fn insert_ctrl_d_outdents_current_line() {
6350 let mut e = editor_with(" x");
6351 run_keys(&mut e, "A<C-d>");
6353 assert_eq!(e.buffer().lines()[0], " x");
6354 }
6355
6356 #[test]
6357 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6358 let mut e = editor_with("first\nsecond");
6359 e.jump_cursor(1, 0);
6360 run_keys(&mut e, "h");
6361 assert_eq!(e.cursor(), (1, 0));
6363 }
6364
6365 #[test]
6366 fn l_at_last_char_does_not_wrap_to_next_line() {
6367 let mut e = editor_with("ab\ncd");
6368 e.jump_cursor(0, 1);
6370 run_keys(&mut e, "l");
6371 assert_eq!(e.cursor(), (0, 1));
6373 }
6374
6375 #[test]
6376 fn count_l_clamps_at_line_end() {
6377 let mut e = editor_with("abcde");
6378 run_keys(&mut e, "20l");
6381 assert_eq!(e.cursor(), (0, 4));
6382 }
6383
6384 #[test]
6385 fn count_h_clamps_at_col_zero() {
6386 let mut e = editor_with("abcde");
6387 e.jump_cursor(0, 3);
6388 run_keys(&mut e, "20h");
6389 assert_eq!(e.cursor(), (0, 0));
6390 }
6391
6392 #[test]
6393 fn dl_on_last_char_still_deletes_it() {
6394 let mut e = editor_with("ab");
6398 e.jump_cursor(0, 1);
6399 run_keys(&mut e, "dl");
6400 assert_eq!(e.buffer().lines()[0], "a");
6401 }
6402
6403 #[test]
6404 fn case_op_preserves_yank_register() {
6405 let mut e = editor_with("target");
6406 run_keys(&mut e, "yy");
6407 let yank_before = e.yank().to_string();
6408 run_keys(&mut e, "gUU");
6410 assert_eq!(e.buffer().lines()[0], "TARGET");
6411 assert_eq!(
6412 e.yank(),
6413 yank_before,
6414 "case ops must preserve the yank buffer"
6415 );
6416 }
6417
6418 #[test]
6419 fn dap_deletes_paragraph() {
6420 let mut e = editor_with("a\nb\n\nc\nd");
6421 run_keys(&mut e, "dap");
6422 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6423 }
6424
6425 #[test]
6426 fn dit_deletes_inner_tag_content() {
6427 let mut e = editor_with("<b>hello</b>");
6428 e.jump_cursor(0, 4);
6430 run_keys(&mut e, "dit");
6431 assert_eq!(e.buffer().lines()[0], "<b></b>");
6432 }
6433
6434 #[test]
6435 fn dat_deletes_around_tag() {
6436 let mut e = editor_with("hi <b>foo</b> bye");
6437 e.jump_cursor(0, 6);
6438 run_keys(&mut e, "dat");
6439 assert_eq!(e.buffer().lines()[0], "hi bye");
6440 }
6441
6442 #[test]
6443 fn dit_picks_innermost_tag() {
6444 let mut e = editor_with("<a><b>x</b></a>");
6445 e.jump_cursor(0, 6);
6447 run_keys(&mut e, "dit");
6448 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6450 }
6451
6452 #[test]
6453 fn dat_innermost_tag_pair() {
6454 let mut e = editor_with("<a><b>x</b></a>");
6455 e.jump_cursor(0, 6);
6456 run_keys(&mut e, "dat");
6457 assert_eq!(e.buffer().lines()[0], "<a></a>");
6458 }
6459
6460 #[test]
6461 fn dit_outside_any_tag_no_op() {
6462 let mut e = editor_with("plain text");
6463 e.jump_cursor(0, 3);
6464 run_keys(&mut e, "dit");
6465 assert_eq!(e.buffer().lines()[0], "plain text");
6467 }
6468
6469 #[test]
6470 fn cit_changes_inner_tag_content() {
6471 let mut e = editor_with("<b>hello</b>");
6472 e.jump_cursor(0, 4);
6473 run_keys(&mut e, "citNEW<Esc>");
6474 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6475 }
6476
6477 #[test]
6478 fn cat_changes_around_tag() {
6479 let mut e = editor_with("hi <b>foo</b> bye");
6480 e.jump_cursor(0, 6);
6481 run_keys(&mut e, "catBAR<Esc>");
6482 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6483 }
6484
6485 #[test]
6486 fn yit_yanks_inner_tag_content() {
6487 let mut e = editor_with("<b>hello</b>");
6488 e.jump_cursor(0, 4);
6489 run_keys(&mut e, "yit");
6490 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6491 }
6492
6493 #[test]
6494 fn yat_yanks_full_tag_pair() {
6495 let mut e = editor_with("hi <b>foo</b> bye");
6496 e.jump_cursor(0, 6);
6497 run_keys(&mut e, "yat");
6498 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6499 }
6500
6501 #[test]
6502 fn vit_visually_selects_inner_tag() {
6503 let mut e = editor_with("<b>hello</b>");
6504 e.jump_cursor(0, 4);
6505 run_keys(&mut e, "vit");
6506 assert_eq!(e.vim_mode(), VimMode::Visual);
6507 run_keys(&mut e, "y");
6508 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6509 }
6510
6511 #[test]
6512 fn vat_visually_selects_around_tag() {
6513 let mut e = editor_with("x<b>foo</b>y");
6514 e.jump_cursor(0, 5);
6515 run_keys(&mut e, "vat");
6516 assert_eq!(e.vim_mode(), VimMode::Visual);
6517 run_keys(&mut e, "y");
6518 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6519 }
6520
6521 #[test]
6524 #[allow(non_snake_case)]
6525 fn diW_deletes_inner_big_word() {
6526 let mut e = editor_with("foo.bar baz");
6527 e.jump_cursor(0, 2);
6528 run_keys(&mut e, "diW");
6529 assert_eq!(e.buffer().lines()[0], " baz");
6531 }
6532
6533 #[test]
6534 #[allow(non_snake_case)]
6535 fn daW_deletes_around_big_word() {
6536 let mut e = editor_with("foo.bar baz");
6537 e.jump_cursor(0, 2);
6538 run_keys(&mut e, "daW");
6539 assert_eq!(e.buffer().lines()[0], "baz");
6540 }
6541
6542 #[test]
6543 fn di_double_quote_deletes_inside() {
6544 let mut e = editor_with("a \"hello\" b");
6545 e.jump_cursor(0, 4);
6546 run_keys(&mut e, "di\"");
6547 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6548 }
6549
6550 #[test]
6551 fn da_double_quote_deletes_around() {
6552 let mut e = editor_with("a \"hello\" b");
6554 e.jump_cursor(0, 4);
6555 run_keys(&mut e, "da\"");
6556 assert_eq!(e.buffer().lines()[0], "a b");
6557 }
6558
6559 #[test]
6560 fn di_single_quote_deletes_inside() {
6561 let mut e = editor_with("x 'foo' y");
6562 e.jump_cursor(0, 4);
6563 run_keys(&mut e, "di'");
6564 assert_eq!(e.buffer().lines()[0], "x '' y");
6565 }
6566
6567 #[test]
6568 fn da_single_quote_deletes_around() {
6569 let mut e = editor_with("x 'foo' y");
6571 e.jump_cursor(0, 4);
6572 run_keys(&mut e, "da'");
6573 assert_eq!(e.buffer().lines()[0], "x y");
6574 }
6575
6576 #[test]
6577 fn di_backtick_deletes_inside() {
6578 let mut e = editor_with("p `q` r");
6579 e.jump_cursor(0, 3);
6580 run_keys(&mut e, "di`");
6581 assert_eq!(e.buffer().lines()[0], "p `` r");
6582 }
6583
6584 #[test]
6585 fn da_backtick_deletes_around() {
6586 let mut e = editor_with("p `q` r");
6588 e.jump_cursor(0, 3);
6589 run_keys(&mut e, "da`");
6590 assert_eq!(e.buffer().lines()[0], "p r");
6591 }
6592
6593 #[test]
6594 fn di_paren_deletes_inside() {
6595 let mut e = editor_with("f(arg)");
6596 e.jump_cursor(0, 3);
6597 run_keys(&mut e, "di(");
6598 assert_eq!(e.buffer().lines()[0], "f()");
6599 }
6600
6601 #[test]
6602 fn di_paren_alias_b_works() {
6603 let mut e = editor_with("f(arg)");
6604 e.jump_cursor(0, 3);
6605 run_keys(&mut e, "dib");
6606 assert_eq!(e.buffer().lines()[0], "f()");
6607 }
6608
6609 #[test]
6610 fn di_bracket_deletes_inside() {
6611 let mut e = editor_with("a[b,c]d");
6612 e.jump_cursor(0, 3);
6613 run_keys(&mut e, "di[");
6614 assert_eq!(e.buffer().lines()[0], "a[]d");
6615 }
6616
6617 #[test]
6618 fn da_bracket_deletes_around() {
6619 let mut e = editor_with("a[b,c]d");
6620 e.jump_cursor(0, 3);
6621 run_keys(&mut e, "da[");
6622 assert_eq!(e.buffer().lines()[0], "ad");
6623 }
6624
6625 #[test]
6626 fn di_brace_deletes_inside() {
6627 let mut e = editor_with("x{y}z");
6628 e.jump_cursor(0, 2);
6629 run_keys(&mut e, "di{");
6630 assert_eq!(e.buffer().lines()[0], "x{}z");
6631 }
6632
6633 #[test]
6634 fn da_brace_deletes_around() {
6635 let mut e = editor_with("x{y}z");
6636 e.jump_cursor(0, 2);
6637 run_keys(&mut e, "da{");
6638 assert_eq!(e.buffer().lines()[0], "xz");
6639 }
6640
6641 #[test]
6642 fn di_brace_alias_capital_b_works() {
6643 let mut e = editor_with("x{y}z");
6644 e.jump_cursor(0, 2);
6645 run_keys(&mut e, "diB");
6646 assert_eq!(e.buffer().lines()[0], "x{}z");
6647 }
6648
6649 #[test]
6650 fn di_angle_deletes_inside() {
6651 let mut e = editor_with("p<q>r");
6652 e.jump_cursor(0, 2);
6653 run_keys(&mut e, "di<lt>");
6655 assert_eq!(e.buffer().lines()[0], "p<>r");
6656 }
6657
6658 #[test]
6659 fn da_angle_deletes_around() {
6660 let mut e = editor_with("p<q>r");
6661 e.jump_cursor(0, 2);
6662 run_keys(&mut e, "da<lt>");
6663 assert_eq!(e.buffer().lines()[0], "pr");
6664 }
6665
6666 #[test]
6667 fn dip_deletes_inner_paragraph() {
6668 let mut e = editor_with("a\nb\nc\n\nd");
6669 e.jump_cursor(1, 0);
6670 run_keys(&mut e, "dip");
6671 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6674 }
6675
6676 #[test]
6679 fn sentence_motion_close_paren_jumps_forward() {
6680 let mut e = editor_with("Alpha. Beta. Gamma.");
6681 e.jump_cursor(0, 0);
6682 run_keys(&mut e, ")");
6683 assert_eq!(e.cursor(), (0, 7));
6685 run_keys(&mut e, ")");
6686 assert_eq!(e.cursor(), (0, 13));
6687 }
6688
6689 #[test]
6690 fn sentence_motion_open_paren_jumps_backward() {
6691 let mut e = editor_with("Alpha. Beta. Gamma.");
6692 e.jump_cursor(0, 13);
6693 run_keys(&mut e, "(");
6694 assert_eq!(e.cursor(), (0, 7));
6697 run_keys(&mut e, "(");
6698 assert_eq!(e.cursor(), (0, 0));
6699 }
6700
6701 #[test]
6702 fn sentence_motion_count() {
6703 let mut e = editor_with("A. B. C. D.");
6704 e.jump_cursor(0, 0);
6705 run_keys(&mut e, "3)");
6706 assert_eq!(e.cursor(), (0, 9));
6708 }
6709
6710 #[test]
6711 fn dis_deletes_inner_sentence() {
6712 let mut e = editor_with("First one. Second one. Third one.");
6713 e.jump_cursor(0, 13);
6714 run_keys(&mut e, "dis");
6715 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6717 }
6718
6719 #[test]
6720 fn das_deletes_around_sentence_with_trailing_space() {
6721 let mut e = editor_with("Alpha. Beta. Gamma.");
6722 e.jump_cursor(0, 8);
6723 run_keys(&mut e, "das");
6724 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6727 }
6728
6729 #[test]
6730 fn dis_handles_double_terminator() {
6731 let mut e = editor_with("Wow!? Next.");
6732 e.jump_cursor(0, 1);
6733 run_keys(&mut e, "dis");
6734 assert_eq!(e.buffer().lines()[0], " Next.");
6737 }
6738
6739 #[test]
6740 fn dis_first_sentence_from_cursor_at_zero() {
6741 let mut e = editor_with("Alpha. Beta.");
6742 e.jump_cursor(0, 0);
6743 run_keys(&mut e, "dis");
6744 assert_eq!(e.buffer().lines()[0], " Beta.");
6745 }
6746
6747 #[test]
6748 fn yis_yanks_inner_sentence() {
6749 let mut e = editor_with("Hello world. Bye.");
6750 e.jump_cursor(0, 5);
6751 run_keys(&mut e, "yis");
6752 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6753 }
6754
6755 #[test]
6756 fn vis_visually_selects_inner_sentence() {
6757 let mut e = editor_with("First. Second.");
6758 e.jump_cursor(0, 1);
6759 run_keys(&mut e, "vis");
6760 assert_eq!(e.vim_mode(), VimMode::Visual);
6761 run_keys(&mut e, "y");
6762 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6763 }
6764
6765 #[test]
6766 fn ciw_changes_inner_word() {
6767 let mut e = editor_with("hello world");
6768 e.jump_cursor(0, 1);
6769 run_keys(&mut e, "ciwHEY<Esc>");
6770 assert_eq!(e.buffer().lines()[0], "HEY world");
6771 }
6772
6773 #[test]
6774 fn yiw_yanks_inner_word() {
6775 let mut e = editor_with("hello world");
6776 e.jump_cursor(0, 1);
6777 run_keys(&mut e, "yiw");
6778 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6779 }
6780
6781 #[test]
6782 fn viw_selects_inner_word() {
6783 let mut e = editor_with("hello world");
6784 e.jump_cursor(0, 2);
6785 run_keys(&mut e, "viw");
6786 assert_eq!(e.vim_mode(), VimMode::Visual);
6787 run_keys(&mut e, "y");
6788 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6789 }
6790
6791 #[test]
6792 fn ci_paren_changes_inside() {
6793 let mut e = editor_with("f(old)");
6794 e.jump_cursor(0, 3);
6795 run_keys(&mut e, "ci(NEW<Esc>");
6796 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6797 }
6798
6799 #[test]
6800 fn yi_double_quote_yanks_inside() {
6801 let mut e = editor_with("say \"hi there\" then");
6802 e.jump_cursor(0, 6);
6803 run_keys(&mut e, "yi\"");
6804 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6805 }
6806
6807 #[test]
6808 fn vap_visual_selects_around_paragraph() {
6809 let mut e = editor_with("a\nb\n\nc");
6810 e.jump_cursor(0, 0);
6811 run_keys(&mut e, "vap");
6812 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6813 run_keys(&mut e, "y");
6814 let text = e.registers().read('"').unwrap().text.clone();
6816 assert!(text.starts_with("a\nb"));
6817 }
6818
6819 #[test]
6820 fn star_finds_next_occurrence() {
6821 let mut e = editor_with("foo bar foo baz");
6822 run_keys(&mut e, "*");
6823 assert_eq!(e.cursor().1, 8);
6824 }
6825
6826 #[test]
6827 fn star_skips_substring_match() {
6828 let mut e = editor_with("foo foobar baz");
6831 run_keys(&mut e, "*");
6832 assert_eq!(e.cursor().1, 0);
6833 }
6834
6835 #[test]
6836 fn g_star_matches_substring() {
6837 let mut e = editor_with("foo foobar baz");
6840 run_keys(&mut e, "g*");
6841 assert_eq!(e.cursor().1, 4);
6842 }
6843
6844 #[test]
6845 fn g_pound_matches_substring_backward() {
6846 let mut e = editor_with("foo foobar baz foo");
6849 run_keys(&mut e, "$b");
6850 assert_eq!(e.cursor().1, 15);
6851 run_keys(&mut e, "g#");
6852 assert_eq!(e.cursor().1, 4);
6853 }
6854
6855 #[test]
6856 fn n_repeats_last_search_forward() {
6857 let mut e = editor_with("foo bar foo baz foo");
6858 run_keys(&mut e, "/foo<CR>");
6861 assert_eq!(e.cursor().1, 8);
6862 run_keys(&mut e, "n");
6863 assert_eq!(e.cursor().1, 16);
6864 }
6865
6866 #[test]
6867 fn shift_n_reverses_search() {
6868 let mut e = editor_with("foo bar foo baz foo");
6869 run_keys(&mut e, "/foo<CR>");
6870 run_keys(&mut e, "n");
6871 assert_eq!(e.cursor().1, 16);
6872 run_keys(&mut e, "N");
6873 assert_eq!(e.cursor().1, 8);
6874 }
6875
6876 #[test]
6877 fn n_noop_without_pattern() {
6878 let mut e = editor_with("foo bar");
6879 run_keys(&mut e, "n");
6880 assert_eq!(e.cursor(), (0, 0));
6881 }
6882
6883 #[test]
6884 fn visual_line_preserves_cursor_column() {
6885 let mut e = editor_with("hello world\nanother one\nbye");
6888 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6890 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6891 assert_eq!(e.cursor(), (0, 5));
6892 run_keys(&mut e, "j");
6893 assert_eq!(e.cursor(), (1, 5));
6894 }
6895
6896 #[test]
6897 fn visual_line_yank_includes_trailing_newline() {
6898 let mut e = editor_with("aaa\nbbb\nccc");
6899 run_keys(&mut e, "Vjy");
6900 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6902 }
6903
6904 #[test]
6905 fn visual_line_yank_last_line_trailing_newline() {
6906 let mut e = editor_with("aaa\nbbb\nccc");
6907 run_keys(&mut e, "jj");
6909 run_keys(&mut e, "Vy");
6910 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6911 }
6912
6913 #[test]
6914 fn yy_on_last_line_has_trailing_newline() {
6915 let mut e = editor_with("aaa\nbbb\nccc");
6916 run_keys(&mut e, "jj");
6917 run_keys(&mut e, "yy");
6918 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6919 }
6920
6921 #[test]
6922 fn yy_in_middle_has_trailing_newline() {
6923 let mut e = editor_with("aaa\nbbb\nccc");
6924 run_keys(&mut e, "j");
6925 run_keys(&mut e, "yy");
6926 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6927 }
6928
6929 #[test]
6930 fn di_single_quote() {
6931 let mut e = editor_with("say 'hello world' now");
6932 e.jump_cursor(0, 7);
6933 run_keys(&mut e, "di'");
6934 assert_eq!(e.buffer().lines()[0], "say '' now");
6935 }
6936
6937 #[test]
6938 fn da_single_quote() {
6939 let mut e = editor_with("say 'hello' now");
6941 e.jump_cursor(0, 7);
6942 run_keys(&mut e, "da'");
6943 assert_eq!(e.buffer().lines()[0], "say now");
6944 }
6945
6946 #[test]
6947 fn di_backtick() {
6948 let mut e = editor_with("say `hi` now");
6949 e.jump_cursor(0, 5);
6950 run_keys(&mut e, "di`");
6951 assert_eq!(e.buffer().lines()[0], "say `` now");
6952 }
6953
6954 #[test]
6955 fn di_brace() {
6956 let mut e = editor_with("fn { a; b; c }");
6957 e.jump_cursor(0, 7);
6958 run_keys(&mut e, "di{");
6959 assert_eq!(e.buffer().lines()[0], "fn {}");
6960 }
6961
6962 #[test]
6963 fn di_bracket() {
6964 let mut e = editor_with("arr[1, 2, 3]");
6965 e.jump_cursor(0, 5);
6966 run_keys(&mut e, "di[");
6967 assert_eq!(e.buffer().lines()[0], "arr[]");
6968 }
6969
6970 #[test]
6971 fn dab_deletes_around_paren() {
6972 let mut e = editor_with("fn(a, b) + 1");
6973 e.jump_cursor(0, 4);
6974 run_keys(&mut e, "dab");
6975 assert_eq!(e.buffer().lines()[0], "fn + 1");
6976 }
6977
6978 #[test]
6979 fn da_big_b_deletes_around_brace() {
6980 let mut e = editor_with("x = {a: 1}");
6981 e.jump_cursor(0, 6);
6982 run_keys(&mut e, "daB");
6983 assert_eq!(e.buffer().lines()[0], "x = ");
6984 }
6985
6986 #[test]
6987 fn di_big_w_deletes_bigword() {
6988 let mut e = editor_with("foo-bar baz");
6989 e.jump_cursor(0, 2);
6990 run_keys(&mut e, "diW");
6991 assert_eq!(e.buffer().lines()[0], " baz");
6992 }
6993
6994 #[test]
6995 fn visual_select_inner_word() {
6996 let mut e = editor_with("hello world");
6997 e.jump_cursor(0, 2);
6998 run_keys(&mut e, "viw");
6999 assert_eq!(e.vim_mode(), VimMode::Visual);
7000 run_keys(&mut e, "y");
7001 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7002 }
7003
7004 #[test]
7005 fn visual_select_inner_quote() {
7006 let mut e = editor_with("foo \"bar\" baz");
7007 e.jump_cursor(0, 6);
7008 run_keys(&mut e, "vi\"");
7009 run_keys(&mut e, "y");
7010 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7011 }
7012
7013 #[test]
7014 fn visual_select_inner_paren() {
7015 let mut e = editor_with("fn(a, b)");
7016 e.jump_cursor(0, 4);
7017 run_keys(&mut e, "vi(");
7018 run_keys(&mut e, "y");
7019 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7020 }
7021
7022 #[test]
7023 fn visual_select_outer_brace() {
7024 let mut e = editor_with("{x}");
7025 e.jump_cursor(0, 1);
7026 run_keys(&mut e, "va{");
7027 run_keys(&mut e, "y");
7028 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7029 }
7030
7031 #[test]
7032 fn ci_paren_forward_scans_when_cursor_before_pair() {
7033 let mut e = editor_with("foo(bar)");
7036 e.jump_cursor(0, 0);
7037 run_keys(&mut e, "ci(NEW<Esc>");
7038 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7039 }
7040
7041 #[test]
7042 fn ci_paren_forward_scans_across_lines() {
7043 let mut e = editor_with("first\nfoo(bar)\nlast");
7044 e.jump_cursor(0, 0);
7045 run_keys(&mut e, "ci(NEW<Esc>");
7046 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7047 }
7048
7049 #[test]
7050 fn ci_brace_forward_scans_when_cursor_before_pair() {
7051 let mut e = editor_with("let x = {y};");
7052 e.jump_cursor(0, 0);
7053 run_keys(&mut e, "ci{NEW<Esc>");
7054 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7055 }
7056
7057 #[test]
7058 fn cit_forward_scans_when_cursor_before_tag() {
7059 let mut e = editor_with("text <b>hello</b> rest");
7062 e.jump_cursor(0, 0);
7063 run_keys(&mut e, "citNEW<Esc>");
7064 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7065 }
7066
7067 #[test]
7068 fn dat_forward_scans_when_cursor_before_tag() {
7069 let mut e = editor_with("text <b>hello</b> rest");
7071 e.jump_cursor(0, 0);
7072 run_keys(&mut e, "dat");
7073 assert_eq!(e.buffer().lines()[0], "text rest");
7074 }
7075
7076 #[test]
7077 fn ci_paren_still_works_when_cursor_inside() {
7078 let mut e = editor_with("fn(a, b)");
7081 e.jump_cursor(0, 4);
7082 run_keys(&mut e, "ci(NEW<Esc>");
7083 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7084 }
7085
7086 #[test]
7087 fn caw_changes_word_with_trailing_space() {
7088 let mut e = editor_with("hello world");
7089 run_keys(&mut e, "cawfoo<Esc>");
7090 assert_eq!(e.buffer().lines()[0], "fooworld");
7091 }
7092
7093 #[test]
7094 fn visual_char_yank_preserves_raw_text() {
7095 let mut e = editor_with("hello world");
7096 run_keys(&mut e, "vllly");
7097 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7098 }
7099
7100 #[test]
7101 fn single_line_visual_line_selects_full_line_on_yank() {
7102 let mut e = editor_with("hello world\nbye");
7103 run_keys(&mut e, "V");
7104 run_keys(&mut e, "y");
7107 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7108 }
7109
7110 #[test]
7111 fn visual_line_extends_both_directions() {
7112 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7113 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7115 assert_eq!(e.cursor(), (3, 0));
7116 run_keys(&mut e, "k");
7117 assert_eq!(e.cursor(), (2, 0));
7119 run_keys(&mut e, "k");
7120 assert_eq!(e.cursor(), (1, 0));
7121 }
7122
7123 #[test]
7124 fn visual_char_preserves_cursor_column() {
7125 let mut e = editor_with("hello world");
7126 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7128 assert_eq!(e.cursor(), (0, 5));
7129 run_keys(&mut e, "ll");
7130 assert_eq!(e.cursor(), (0, 7));
7131 }
7132
7133 #[test]
7134 fn visual_char_highlight_bounds_order() {
7135 let mut e = editor_with("abcdef");
7136 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7138 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7141 }
7142
7143 #[test]
7144 fn visual_line_highlight_bounds() {
7145 let mut e = editor_with("a\nb\nc");
7146 run_keys(&mut e, "V");
7147 assert_eq!(e.line_highlight(), Some((0, 0)));
7148 run_keys(&mut e, "j");
7149 assert_eq!(e.line_highlight(), Some((0, 1)));
7150 run_keys(&mut e, "j");
7151 assert_eq!(e.line_highlight(), Some((0, 2)));
7152 }
7153
7154 #[test]
7157 fn h_moves_left() {
7158 let mut e = editor_with("hello");
7159 e.jump_cursor(0, 3);
7160 run_keys(&mut e, "h");
7161 assert_eq!(e.cursor(), (0, 2));
7162 }
7163
7164 #[test]
7165 fn l_moves_right() {
7166 let mut e = editor_with("hello");
7167 run_keys(&mut e, "l");
7168 assert_eq!(e.cursor(), (0, 1));
7169 }
7170
7171 #[test]
7172 fn k_moves_up() {
7173 let mut e = editor_with("a\nb\nc");
7174 e.jump_cursor(2, 0);
7175 run_keys(&mut e, "k");
7176 assert_eq!(e.cursor(), (1, 0));
7177 }
7178
7179 #[test]
7180 fn zero_moves_to_line_start() {
7181 let mut e = editor_with(" hello");
7182 run_keys(&mut e, "$");
7183 run_keys(&mut e, "0");
7184 assert_eq!(e.cursor().1, 0);
7185 }
7186
7187 #[test]
7188 fn caret_moves_to_first_non_blank() {
7189 let mut e = editor_with(" hello");
7190 run_keys(&mut e, "0");
7191 run_keys(&mut e, "^");
7192 assert_eq!(e.cursor().1, 4);
7193 }
7194
7195 #[test]
7196 fn dollar_moves_to_last_char() {
7197 let mut e = editor_with("hello");
7198 run_keys(&mut e, "$");
7199 assert_eq!(e.cursor().1, 4);
7200 }
7201
7202 #[test]
7203 fn dollar_on_empty_line_stays_at_col_zero() {
7204 let mut e = editor_with("");
7205 run_keys(&mut e, "$");
7206 assert_eq!(e.cursor().1, 0);
7207 }
7208
7209 #[test]
7210 fn w_jumps_to_next_word() {
7211 let mut e = editor_with("foo bar baz");
7212 run_keys(&mut e, "w");
7213 assert_eq!(e.cursor().1, 4);
7214 }
7215
7216 #[test]
7217 fn b_jumps_back_a_word() {
7218 let mut e = editor_with("foo bar");
7219 e.jump_cursor(0, 6);
7220 run_keys(&mut e, "b");
7221 assert_eq!(e.cursor().1, 4);
7222 }
7223
7224 #[test]
7225 fn e_jumps_to_word_end() {
7226 let mut e = editor_with("foo bar");
7227 run_keys(&mut e, "e");
7228 assert_eq!(e.cursor().1, 2);
7229 }
7230
7231 #[test]
7234 fn d_dollar_deletes_to_eol() {
7235 let mut e = editor_with("hello world");
7236 e.jump_cursor(0, 5);
7237 run_keys(&mut e, "d$");
7238 assert_eq!(e.buffer().lines()[0], "hello");
7239 }
7240
7241 #[test]
7242 fn d_zero_deletes_to_line_start() {
7243 let mut e = editor_with("hello world");
7244 e.jump_cursor(0, 6);
7245 run_keys(&mut e, "d0");
7246 assert_eq!(e.buffer().lines()[0], "world");
7247 }
7248
7249 #[test]
7250 fn d_caret_deletes_to_first_non_blank() {
7251 let mut e = editor_with(" hello");
7252 e.jump_cursor(0, 6);
7253 run_keys(&mut e, "d^");
7254 assert_eq!(e.buffer().lines()[0], " llo");
7255 }
7256
7257 #[test]
7258 fn d_capital_g_deletes_to_end_of_file() {
7259 let mut e = editor_with("a\nb\nc\nd");
7260 e.jump_cursor(1, 0);
7261 run_keys(&mut e, "dG");
7262 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7263 }
7264
7265 #[test]
7266 fn d_gg_deletes_to_start_of_file() {
7267 let mut e = editor_with("a\nb\nc\nd");
7268 e.jump_cursor(2, 0);
7269 run_keys(&mut e, "dgg");
7270 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7271 }
7272
7273 #[test]
7274 fn cw_is_ce_quirk() {
7275 let mut e = editor_with("foo bar");
7278 run_keys(&mut e, "cwxyz<Esc>");
7279 assert_eq!(e.buffer().lines()[0], "xyz bar");
7280 }
7281
7282 #[test]
7285 fn big_d_deletes_to_eol() {
7286 let mut e = editor_with("hello world");
7287 e.jump_cursor(0, 5);
7288 run_keys(&mut e, "D");
7289 assert_eq!(e.buffer().lines()[0], "hello");
7290 }
7291
7292 #[test]
7293 fn big_c_deletes_to_eol_and_inserts() {
7294 let mut e = editor_with("hello world");
7295 e.jump_cursor(0, 5);
7296 run_keys(&mut e, "C!<Esc>");
7297 assert_eq!(e.buffer().lines()[0], "hello!");
7298 }
7299
7300 #[test]
7301 fn j_joins_next_line_with_space() {
7302 let mut e = editor_with("hello\nworld");
7303 run_keys(&mut e, "J");
7304 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7305 }
7306
7307 #[test]
7308 fn j_strips_leading_whitespace_on_join() {
7309 let mut e = editor_with("hello\n world");
7310 run_keys(&mut e, "J");
7311 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7312 }
7313
7314 #[test]
7315 fn big_x_deletes_char_before_cursor() {
7316 let mut e = editor_with("hello");
7317 e.jump_cursor(0, 3);
7318 run_keys(&mut e, "X");
7319 assert_eq!(e.buffer().lines()[0], "helo");
7320 }
7321
7322 #[test]
7323 fn s_substitutes_char_and_enters_insert() {
7324 let mut e = editor_with("hello");
7325 run_keys(&mut e, "sX<Esc>");
7326 assert_eq!(e.buffer().lines()[0], "Xello");
7327 }
7328
7329 #[test]
7330 fn count_x_deletes_many() {
7331 let mut e = editor_with("abcdef");
7332 run_keys(&mut e, "3x");
7333 assert_eq!(e.buffer().lines()[0], "def");
7334 }
7335
7336 #[test]
7339 fn p_pastes_charwise_after_cursor() {
7340 let mut e = editor_with("hello");
7341 run_keys(&mut e, "yw");
7342 run_keys(&mut e, "$p");
7343 assert_eq!(e.buffer().lines()[0], "hellohello");
7344 }
7345
7346 #[test]
7347 fn capital_p_pastes_charwise_before_cursor() {
7348 let mut e = editor_with("hello");
7349 run_keys(&mut e, "v");
7351 run_keys(&mut e, "l");
7352 run_keys(&mut e, "y");
7353 run_keys(&mut e, "$P");
7354 assert_eq!(e.buffer().lines()[0], "hellheo");
7357 }
7358
7359 #[test]
7360 fn p_pastes_linewise_below() {
7361 let mut e = editor_with("one\ntwo\nthree");
7362 run_keys(&mut e, "yy");
7363 run_keys(&mut e, "p");
7364 assert_eq!(
7365 e.buffer().lines(),
7366 &[
7367 "one".to_string(),
7368 "one".to_string(),
7369 "two".to_string(),
7370 "three".to_string()
7371 ]
7372 );
7373 }
7374
7375 #[test]
7376 fn capital_p_pastes_linewise_above() {
7377 let mut e = editor_with("one\ntwo");
7378 e.jump_cursor(1, 0);
7379 run_keys(&mut e, "yy");
7380 run_keys(&mut e, "P");
7381 assert_eq!(
7382 e.buffer().lines(),
7383 &["one".to_string(), "two".to_string(), "two".to_string()]
7384 );
7385 }
7386
7387 #[test]
7390 fn hash_finds_previous_occurrence() {
7391 let mut e = editor_with("foo bar foo baz foo");
7392 e.jump_cursor(0, 16);
7394 run_keys(&mut e, "#");
7395 assert_eq!(e.cursor().1, 8);
7396 }
7397
7398 #[test]
7401 fn visual_line_delete_removes_full_lines() {
7402 let mut e = editor_with("a\nb\nc\nd");
7403 run_keys(&mut e, "Vjd");
7404 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7405 }
7406
7407 #[test]
7408 fn visual_line_change_leaves_blank_line() {
7409 let mut e = editor_with("a\nb\nc");
7410 run_keys(&mut e, "Vjc");
7411 assert_eq!(e.vim_mode(), VimMode::Insert);
7412 run_keys(&mut e, "X<Esc>");
7413 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7417 }
7418
7419 #[test]
7420 fn cc_leaves_blank_line() {
7421 let mut e = editor_with("a\nb\nc");
7422 e.jump_cursor(1, 0);
7423 run_keys(&mut e, "ccX<Esc>");
7424 assert_eq!(
7425 e.buffer().lines(),
7426 &["a".to_string(), "X".to_string(), "c".to_string()]
7427 );
7428 }
7429
7430 #[test]
7435 fn big_w_skips_hyphens() {
7436 let mut e = editor_with("foo-bar baz");
7438 run_keys(&mut e, "W");
7439 assert_eq!(e.cursor().1, 8);
7440 }
7441
7442 #[test]
7443 fn big_w_crosses_lines() {
7444 let mut e = editor_with("foo-bar\nbaz-qux");
7445 run_keys(&mut e, "W");
7446 assert_eq!(e.cursor(), (1, 0));
7447 }
7448
7449 #[test]
7450 fn big_b_skips_hyphens() {
7451 let mut e = editor_with("foo-bar baz");
7452 e.jump_cursor(0, 9);
7453 run_keys(&mut e, "B");
7454 assert_eq!(e.cursor().1, 8);
7455 run_keys(&mut e, "B");
7456 assert_eq!(e.cursor().1, 0);
7457 }
7458
7459 #[test]
7460 fn big_e_jumps_to_big_word_end() {
7461 let mut e = editor_with("foo-bar baz");
7462 run_keys(&mut e, "E");
7463 assert_eq!(e.cursor().1, 6);
7464 run_keys(&mut e, "E");
7465 assert_eq!(e.cursor().1, 10);
7466 }
7467
7468 #[test]
7469 fn dw_with_big_word_variant() {
7470 let mut e = editor_with("foo-bar baz");
7472 run_keys(&mut e, "dW");
7473 assert_eq!(e.buffer().lines()[0], "baz");
7474 }
7475
7476 #[test]
7479 fn insert_ctrl_w_deletes_word_back() {
7480 let mut e = editor_with("");
7481 run_keys(&mut e, "i");
7482 for c in "hello world".chars() {
7483 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7484 }
7485 run_keys(&mut e, "<C-w>");
7486 assert_eq!(e.buffer().lines()[0], "hello ");
7487 }
7488
7489 #[test]
7490 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7491 let mut e = editor_with("hello\nworld");
7495 e.jump_cursor(1, 0);
7496 run_keys(&mut e, "i");
7497 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7498 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7501 assert_eq!(e.cursor(), (0, 0));
7502 }
7503
7504 #[test]
7505 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7506 let mut e = editor_with("foo bar\nbaz");
7507 e.jump_cursor(1, 0);
7508 run_keys(&mut e, "i");
7509 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7510 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7512 assert_eq!(e.cursor(), (0, 4));
7513 }
7514
7515 #[test]
7516 fn insert_ctrl_u_deletes_to_line_start() {
7517 let mut e = editor_with("");
7518 run_keys(&mut e, "i");
7519 for c in "hello world".chars() {
7520 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7521 }
7522 run_keys(&mut e, "<C-u>");
7523 assert_eq!(e.buffer().lines()[0], "");
7524 }
7525
7526 #[test]
7527 fn insert_ctrl_o_runs_one_normal_command() {
7528 let mut e = editor_with("hello world");
7529 run_keys(&mut e, "A");
7531 assert_eq!(e.vim_mode(), VimMode::Insert);
7532 e.jump_cursor(0, 0);
7534 run_keys(&mut e, "<C-o>");
7535 assert_eq!(e.vim_mode(), VimMode::Normal);
7536 run_keys(&mut e, "dw");
7537 assert_eq!(e.vim_mode(), VimMode::Insert);
7539 assert_eq!(e.buffer().lines()[0], "world");
7540 }
7541
7542 #[test]
7545 fn j_through_empty_line_preserves_column() {
7546 let mut e = editor_with("hello world\n\nanother line");
7547 run_keys(&mut e, "llllll");
7549 assert_eq!(e.cursor(), (0, 6));
7550 run_keys(&mut e, "j");
7553 assert_eq!(e.cursor(), (1, 0));
7554 run_keys(&mut e, "j");
7556 assert_eq!(e.cursor(), (2, 6));
7557 }
7558
7559 #[test]
7560 fn j_through_shorter_line_preserves_column() {
7561 let mut e = editor_with("hello world\nhi\nanother line");
7562 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7565 run_keys(&mut e, "j");
7566 assert_eq!(e.cursor(), (2, 7));
7567 }
7568
7569 #[test]
7570 fn esc_from_insert_sticky_matches_visible_cursor() {
7571 let mut e = editor_with(" this is a line\n another one of a similar size");
7575 e.jump_cursor(0, 12);
7576 run_keys(&mut e, "I");
7577 assert_eq!(e.cursor(), (0, 4));
7578 run_keys(&mut e, "X<Esc>");
7579 assert_eq!(e.cursor(), (0, 4));
7580 run_keys(&mut e, "j");
7581 assert_eq!(e.cursor(), (1, 4));
7582 }
7583
7584 #[test]
7585 fn esc_from_insert_sticky_tracks_inserted_chars() {
7586 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7587 run_keys(&mut e, "i");
7588 run_keys(&mut e, "abc<Esc>");
7589 assert_eq!(e.cursor(), (0, 2));
7590 run_keys(&mut e, "j");
7591 assert_eq!(e.cursor(), (1, 2));
7592 }
7593
7594 #[test]
7595 fn esc_from_insert_sticky_tracks_arrow_nav() {
7596 let mut e = editor_with("xxxxxx\nyyyyyy");
7597 run_keys(&mut e, "i");
7598 run_keys(&mut e, "abc");
7599 for _ in 0..2 {
7600 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7601 }
7602 run_keys(&mut e, "<Esc>");
7603 assert_eq!(e.cursor(), (0, 0));
7604 run_keys(&mut e, "j");
7605 assert_eq!(e.cursor(), (1, 0));
7606 }
7607
7608 #[test]
7609 fn esc_from_insert_at_col_14_followed_by_j() {
7610 let line = "x".repeat(30);
7613 let buf = format!("{line}\n{line}");
7614 let mut e = editor_with(&buf);
7615 e.jump_cursor(0, 14);
7616 run_keys(&mut e, "i");
7617 for c in "test ".chars() {
7618 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7619 }
7620 run_keys(&mut e, "<Esc>");
7621 assert_eq!(e.cursor(), (0, 18));
7622 run_keys(&mut e, "j");
7623 assert_eq!(e.cursor(), (1, 18));
7624 }
7625
7626 #[test]
7627 fn linewise_paste_resets_sticky_column() {
7628 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7632 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7634 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7638 run_keys(&mut e, "j");
7640 assert_eq!(e.cursor(), (3, 2));
7641 }
7642
7643 #[test]
7644 fn horizontal_motion_resyncs_sticky_column() {
7645 let mut e = editor_with("hello world\n\nanother line");
7649 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7652 assert_eq!(e.cursor(), (2, 3));
7653 }
7654
7655 #[test]
7658 fn ctrl_v_enters_visual_block() {
7659 let mut e = editor_with("aaa\nbbb\nccc");
7660 run_keys(&mut e, "<C-v>");
7661 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7662 }
7663
7664 #[test]
7665 fn visual_block_esc_returns_to_normal() {
7666 let mut e = editor_with("aaa\nbbb\nccc");
7667 run_keys(&mut e, "<C-v>");
7668 run_keys(&mut e, "<Esc>");
7669 assert_eq!(e.vim_mode(), VimMode::Normal);
7670 }
7671
7672 #[test]
7673 fn backtick_lt_jumps_to_visual_start_mark() {
7674 let mut e = editor_with("foo bar baz\n");
7678 run_keys(&mut e, "v");
7679 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7682 run_keys(&mut e, "`<lt>");
7684 assert_eq!(e.cursor(), (0, 0));
7685 }
7686
7687 #[test]
7688 fn backtick_gt_jumps_to_visual_end_mark() {
7689 let mut e = editor_with("foo bar baz\n");
7690 run_keys(&mut e, "v");
7691 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7693 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7695 assert_eq!(e.cursor(), (0, 4));
7696 }
7697
7698 #[test]
7699 fn visual_exit_sets_lt_gt_marks() {
7700 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7703 run_keys(&mut e, "V");
7705 run_keys(&mut e, "j");
7706 run_keys(&mut e, "<Esc>");
7707 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7708 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7709 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7710 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7711 }
7712
7713 #[test]
7714 fn visual_exit_marks_use_lower_higher_order() {
7715 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7719 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7721 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7723 let lt = e.mark('<').unwrap();
7724 let gt = e.mark('>').unwrap();
7725 assert_eq!(lt.0, 2);
7726 assert_eq!(gt.0, 3);
7727 }
7728
7729 #[test]
7730 fn visualline_exit_marks_snap_to_line_edges() {
7731 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7733 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7735 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7737 let lt = e.mark('<').unwrap();
7738 let gt = e.mark('>').unwrap();
7739 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7740 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7742 }
7743
7744 #[test]
7745 fn visualblock_exit_marks_use_block_corners() {
7746 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7750 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7752 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7755 let lt = e.mark('<').unwrap();
7756 let gt = e.mark('>').unwrap();
7757 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7759 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7760 }
7761
7762 #[test]
7763 fn visual_block_delete_removes_column_range() {
7764 let mut e = editor_with("hello\nworld\nhappy");
7765 run_keys(&mut e, "l");
7767 run_keys(&mut e, "<C-v>");
7768 run_keys(&mut e, "jj");
7769 run_keys(&mut e, "ll");
7770 run_keys(&mut e, "d");
7771 assert_eq!(
7773 e.buffer().lines(),
7774 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7775 );
7776 }
7777
7778 #[test]
7779 fn visual_block_yank_joins_with_newlines() {
7780 let mut e = editor_with("hello\nworld\nhappy");
7781 run_keys(&mut e, "<C-v>");
7782 run_keys(&mut e, "jj");
7783 run_keys(&mut e, "ll");
7784 run_keys(&mut e, "y");
7785 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7786 }
7787
7788 #[test]
7789 fn visual_block_replace_fills_block() {
7790 let mut e = editor_with("hello\nworld\nhappy");
7791 run_keys(&mut e, "<C-v>");
7792 run_keys(&mut e, "jj");
7793 run_keys(&mut e, "ll");
7794 run_keys(&mut e, "rx");
7795 assert_eq!(
7796 e.buffer().lines(),
7797 &[
7798 "xxxlo".to_string(),
7799 "xxxld".to_string(),
7800 "xxxpy".to_string()
7801 ]
7802 );
7803 }
7804
7805 #[test]
7806 fn visual_block_insert_repeats_across_rows() {
7807 let mut e = editor_with("hello\nworld\nhappy");
7808 run_keys(&mut e, "<C-v>");
7809 run_keys(&mut e, "jj");
7810 run_keys(&mut e, "I");
7811 run_keys(&mut e, "# <Esc>");
7812 assert_eq!(
7813 e.buffer().lines(),
7814 &[
7815 "# hello".to_string(),
7816 "# world".to_string(),
7817 "# happy".to_string()
7818 ]
7819 );
7820 }
7821
7822 #[test]
7823 fn block_highlight_returns_none_outside_block_mode() {
7824 let mut e = editor_with("abc");
7825 assert!(e.block_highlight().is_none());
7826 run_keys(&mut e, "v");
7827 assert!(e.block_highlight().is_none());
7828 run_keys(&mut e, "<Esc>V");
7829 assert!(e.block_highlight().is_none());
7830 }
7831
7832 #[test]
7833 fn block_highlight_bounds_track_anchor_and_cursor() {
7834 let mut e = editor_with("aaaa\nbbbb\ncccc");
7835 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7837 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7840 }
7841
7842 #[test]
7843 fn visual_block_delete_handles_short_lines() {
7844 let mut e = editor_with("hello\nhi\nworld");
7846 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7848 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7850 assert_eq!(
7855 e.buffer().lines(),
7856 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7857 );
7858 }
7859
7860 #[test]
7861 fn visual_block_yank_pads_short_lines_with_empties() {
7862 let mut e = editor_with("hello\nhi\nworld");
7863 run_keys(&mut e, "l");
7864 run_keys(&mut e, "<C-v>");
7865 run_keys(&mut e, "jjll");
7866 run_keys(&mut e, "y");
7867 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7869 }
7870
7871 #[test]
7872 fn visual_block_replace_skips_past_eol() {
7873 let mut e = editor_with("ab\ncd\nef");
7876 run_keys(&mut e, "l");
7878 run_keys(&mut e, "<C-v>");
7879 run_keys(&mut e, "jjllllll");
7880 run_keys(&mut e, "rX");
7881 assert_eq!(
7884 e.buffer().lines(),
7885 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7886 );
7887 }
7888
7889 #[test]
7890 fn visual_block_with_empty_line_in_middle() {
7891 let mut e = editor_with("abcd\n\nefgh");
7892 run_keys(&mut e, "<C-v>");
7893 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7895 assert_eq!(
7898 e.buffer().lines(),
7899 &["d".to_string(), "".to_string(), "h".to_string()]
7900 );
7901 }
7902
7903 #[test]
7904 fn block_insert_pads_empty_lines_to_block_column() {
7905 let mut e = editor_with("this is a line\n\nthis is a line");
7908 e.jump_cursor(0, 3);
7909 run_keys(&mut e, "<C-v>");
7910 run_keys(&mut e, "jj");
7911 run_keys(&mut e, "I");
7912 run_keys(&mut e, "XX<Esc>");
7913 assert_eq!(
7914 e.buffer().lines(),
7915 &[
7916 "thiXXs is a line".to_string(),
7917 " XX".to_string(),
7918 "thiXXs is a line".to_string()
7919 ]
7920 );
7921 }
7922
7923 #[test]
7924 fn block_insert_pads_short_lines_to_block_column() {
7925 let mut e = editor_with("aaaaa\nbb\naaaaa");
7926 e.jump_cursor(0, 3);
7927 run_keys(&mut e, "<C-v>");
7928 run_keys(&mut e, "jj");
7929 run_keys(&mut e, "I");
7930 run_keys(&mut e, "Y<Esc>");
7931 assert_eq!(
7933 e.buffer().lines(),
7934 &[
7935 "aaaYaa".to_string(),
7936 "bb Y".to_string(),
7937 "aaaYaa".to_string()
7938 ]
7939 );
7940 }
7941
7942 #[test]
7943 fn visual_block_append_repeats_across_rows() {
7944 let mut e = editor_with("foo\nbar\nbaz");
7945 run_keys(&mut e, "<C-v>");
7946 run_keys(&mut e, "jj");
7947 run_keys(&mut e, "A");
7950 run_keys(&mut e, "!<Esc>");
7951 assert_eq!(
7952 e.buffer().lines(),
7953 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7954 );
7955 }
7956
7957 #[test]
7960 fn slash_opens_forward_search_prompt() {
7961 let mut e = editor_with("hello world");
7962 run_keys(&mut e, "/");
7963 let p = e.search_prompt().expect("prompt should be active");
7964 assert!(p.text.is_empty());
7965 assert!(p.forward);
7966 }
7967
7968 #[test]
7969 fn question_opens_backward_search_prompt() {
7970 let mut e = editor_with("hello world");
7971 run_keys(&mut e, "?");
7972 let p = e.search_prompt().expect("prompt should be active");
7973 assert!(!p.forward);
7974 }
7975
7976 #[test]
7977 fn search_prompt_typing_updates_pattern_live() {
7978 let mut e = editor_with("foo bar\nbaz");
7979 run_keys(&mut e, "/bar");
7980 assert_eq!(e.search_prompt().unwrap().text, "bar");
7981 assert!(e.search_state().pattern.is_some());
7983 }
7984
7985 #[test]
7986 fn search_prompt_backspace_and_enter() {
7987 let mut e = editor_with("hello world\nagain");
7988 run_keys(&mut e, "/worlx");
7989 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7990 assert_eq!(e.search_prompt().unwrap().text, "worl");
7991 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7992 assert!(e.search_prompt().is_none());
7994 assert_eq!(e.last_search(), Some("worl"));
7995 assert_eq!(e.cursor(), (0, 6));
7996 }
7997
7998 #[test]
7999 fn empty_search_prompt_enter_repeats_last_search() {
8000 let mut e = editor_with("foo bar foo baz foo");
8001 run_keys(&mut e, "/foo");
8002 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8003 assert_eq!(e.cursor().1, 8);
8004 run_keys(&mut e, "/");
8006 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8007 assert_eq!(e.cursor().1, 16);
8008 assert_eq!(e.last_search(), Some("foo"));
8009 }
8010
8011 #[test]
8012 fn search_history_records_committed_patterns() {
8013 let mut e = editor_with("alpha beta gamma");
8014 run_keys(&mut e, "/alpha<CR>");
8015 run_keys(&mut e, "/beta<CR>");
8016 let history = e.vim.search_history.clone();
8018 assert_eq!(history, vec!["alpha", "beta"]);
8019 }
8020
8021 #[test]
8022 fn search_history_dedupes_consecutive_repeats() {
8023 let mut e = editor_with("foo bar foo");
8024 run_keys(&mut e, "/foo<CR>");
8025 run_keys(&mut e, "/foo<CR>");
8026 run_keys(&mut e, "/bar<CR>");
8027 run_keys(&mut e, "/bar<CR>");
8028 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8030 }
8031
8032 #[test]
8033 fn ctrl_p_walks_history_backward() {
8034 let mut e = editor_with("alpha beta gamma");
8035 run_keys(&mut e, "/alpha<CR>");
8036 run_keys(&mut e, "/beta<CR>");
8037 run_keys(&mut e, "/");
8039 assert_eq!(e.search_prompt().unwrap().text, "");
8040 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8041 assert_eq!(e.search_prompt().unwrap().text, "beta");
8042 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8043 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8044 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8046 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8047 }
8048
8049 #[test]
8050 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8051 let mut e = editor_with("a b c");
8052 run_keys(&mut e, "/a<CR>");
8053 run_keys(&mut e, "/b<CR>");
8054 run_keys(&mut e, "/c<CR>");
8055 run_keys(&mut e, "/");
8056 for _ in 0..3 {
8058 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8059 }
8060 assert_eq!(e.search_prompt().unwrap().text, "a");
8061 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8062 assert_eq!(e.search_prompt().unwrap().text, "b");
8063 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8064 assert_eq!(e.search_prompt().unwrap().text, "c");
8065 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8067 assert_eq!(e.search_prompt().unwrap().text, "c");
8068 }
8069
8070 #[test]
8071 fn typing_after_history_walk_resets_cursor() {
8072 let mut e = editor_with("foo");
8073 run_keys(&mut e, "/foo<CR>");
8074 run_keys(&mut e, "/");
8075 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8076 assert_eq!(e.search_prompt().unwrap().text, "foo");
8077 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8080 assert_eq!(e.search_prompt().unwrap().text, "foox");
8081 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8082 assert_eq!(e.search_prompt().unwrap().text, "foo");
8083 }
8084
8085 #[test]
8086 fn empty_backward_search_prompt_enter_repeats_last_search() {
8087 let mut e = editor_with("foo bar foo baz foo");
8088 run_keys(&mut e, "/foo");
8090 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8091 assert_eq!(e.cursor().1, 8);
8092 run_keys(&mut e, "?");
8093 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8094 assert_eq!(e.cursor().1, 0);
8095 assert_eq!(e.last_search(), Some("foo"));
8096 }
8097
8098 #[test]
8099 fn search_prompt_esc_cancels_but_keeps_last_search() {
8100 let mut e = editor_with("foo bar\nbaz");
8101 run_keys(&mut e, "/bar");
8102 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8103 assert!(e.search_prompt().is_none());
8104 assert_eq!(e.last_search(), Some("bar"));
8105 }
8106
8107 #[test]
8108 fn search_then_n_and_shift_n_navigate() {
8109 let mut e = editor_with("foo bar foo baz foo");
8110 run_keys(&mut e, "/foo");
8111 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8112 assert_eq!(e.cursor().1, 8);
8114 run_keys(&mut e, "n");
8115 assert_eq!(e.cursor().1, 16);
8116 run_keys(&mut e, "N");
8117 assert_eq!(e.cursor().1, 8);
8118 }
8119
8120 #[test]
8121 fn question_mark_searches_backward_on_enter() {
8122 let mut e = editor_with("foo bar foo baz");
8123 e.jump_cursor(0, 10);
8124 run_keys(&mut e, "?foo");
8125 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8126 assert_eq!(e.cursor(), (0, 8));
8128 }
8129
8130 #[test]
8133 fn big_y_yanks_to_end_of_line() {
8134 let mut e = editor_with("hello world");
8135 e.jump_cursor(0, 6);
8136 run_keys(&mut e, "Y");
8137 assert_eq!(e.last_yank.as_deref(), Some("world"));
8138 }
8139
8140 #[test]
8141 fn big_y_from_line_start_yanks_full_line() {
8142 let mut e = editor_with("hello world");
8143 run_keys(&mut e, "Y");
8144 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8145 }
8146
8147 #[test]
8148 fn gj_joins_without_inserting_space() {
8149 let mut e = editor_with("hello\n world");
8150 run_keys(&mut e, "gJ");
8151 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8153 }
8154
8155 #[test]
8156 fn gj_noop_on_last_line() {
8157 let mut e = editor_with("only");
8158 run_keys(&mut e, "gJ");
8159 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8160 }
8161
8162 #[test]
8163 fn ge_jumps_to_previous_word_end() {
8164 let mut e = editor_with("foo bar baz");
8165 e.jump_cursor(0, 5);
8166 run_keys(&mut e, "ge");
8167 assert_eq!(e.cursor(), (0, 2));
8168 }
8169
8170 #[test]
8171 fn ge_respects_word_class() {
8172 let mut e = editor_with("foo-bar baz");
8175 e.jump_cursor(0, 5);
8176 run_keys(&mut e, "ge");
8177 assert_eq!(e.cursor(), (0, 3));
8178 }
8179
8180 #[test]
8181 fn big_ge_treats_hyphens_as_part_of_word() {
8182 let mut e = editor_with("foo-bar baz");
8185 e.jump_cursor(0, 10);
8186 run_keys(&mut e, "gE");
8187 assert_eq!(e.cursor(), (0, 6));
8188 }
8189
8190 #[test]
8191 fn ge_crosses_line_boundary() {
8192 let mut e = editor_with("foo\nbar");
8193 e.jump_cursor(1, 0);
8194 run_keys(&mut e, "ge");
8195 assert_eq!(e.cursor(), (0, 2));
8196 }
8197
8198 #[test]
8199 fn dge_deletes_to_end_of_previous_word() {
8200 let mut e = editor_with("foo bar baz");
8201 e.jump_cursor(0, 8);
8202 run_keys(&mut e, "dge");
8205 assert_eq!(e.buffer().lines()[0], "foo baaz");
8206 }
8207
8208 #[test]
8209 fn ctrl_scroll_keys_do_not_panic() {
8210 let mut e = editor_with(
8213 (0..50)
8214 .map(|i| format!("line{i}"))
8215 .collect::<Vec<_>>()
8216 .join("\n")
8217 .as_str(),
8218 );
8219 run_keys(&mut e, "<C-f>");
8220 run_keys(&mut e, "<C-b>");
8221 assert!(!e.buffer().lines().is_empty());
8223 }
8224
8225 #[test]
8232 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8233 let mut e = Editor::new(
8234 hjkl_buffer::Buffer::new(),
8235 crate::types::DefaultHost::new(),
8236 crate::types::Options::default(),
8237 );
8238 e.set_content("row0\nrow1\nrow2");
8239 run_keys(&mut e, "3iX<Down><Esc>");
8241 assert!(e.buffer().lines()[0].contains('X'));
8243 assert!(
8246 !e.buffer().lines()[1].contains("row0"),
8247 "row1 leaked row0 contents: {:?}",
8248 e.buffer().lines()[1]
8249 );
8250 assert_eq!(e.buffer().lines().len(), 3);
8253 }
8254
8255 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8258 let mut e = Editor::new(
8259 hjkl_buffer::Buffer::new(),
8260 crate::types::DefaultHost::new(),
8261 crate::types::Options::default(),
8262 );
8263 let body = (0..n)
8264 .map(|i| format!(" line{}", i))
8265 .collect::<Vec<_>>()
8266 .join("\n");
8267 e.set_content(&body);
8268 e.set_viewport_height(viewport);
8269 e
8270 }
8271
8272 #[test]
8273 fn ctrl_d_moves_cursor_half_page_down() {
8274 let mut e = editor_with_rows(100, 20);
8275 run_keys(&mut e, "<C-d>");
8276 assert_eq!(e.cursor().0, 10);
8277 }
8278
8279 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8280 let mut e = Editor::new(
8281 hjkl_buffer::Buffer::new(),
8282 crate::types::DefaultHost::new(),
8283 crate::types::Options::default(),
8284 );
8285 e.set_content(&lines.join("\n"));
8286 e.set_viewport_height(viewport);
8287 let v = e.host_mut().viewport_mut();
8288 v.height = viewport;
8289 v.width = text_width;
8290 v.text_width = text_width;
8291 v.wrap = hjkl_buffer::Wrap::Char;
8292 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8293 e
8294 }
8295
8296 #[test]
8297 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8298 let lines = ["aaaabbbbcccc"; 10];
8302 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8303 e.jump_cursor(4, 0);
8304 e.ensure_cursor_in_scrolloff();
8305 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8306 assert!(csr <= 6, "csr={csr}");
8307 }
8308
8309 #[test]
8310 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8311 let lines = ["aaaabbbbcccc"; 10];
8312 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8313 e.jump_cursor(7, 0);
8316 e.ensure_cursor_in_scrolloff();
8317 e.jump_cursor(2, 0);
8318 e.ensure_cursor_in_scrolloff();
8319 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8320 assert!(csr >= 5, "csr={csr}");
8322 }
8323
8324 #[test]
8325 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8326 let lines = ["aaaabbbbcccc"; 5];
8327 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8328 e.jump_cursor(4, 11);
8329 e.ensure_cursor_in_scrolloff();
8330 let top = e.host().viewport().top_row;
8335 assert_eq!(top, 1);
8336 }
8337
8338 #[test]
8339 fn ctrl_u_moves_cursor_half_page_up() {
8340 let mut e = editor_with_rows(100, 20);
8341 e.jump_cursor(50, 0);
8342 run_keys(&mut e, "<C-u>");
8343 assert_eq!(e.cursor().0, 40);
8344 }
8345
8346 #[test]
8347 fn ctrl_f_moves_cursor_full_page_down() {
8348 let mut e = editor_with_rows(100, 20);
8349 run_keys(&mut e, "<C-f>");
8350 assert_eq!(e.cursor().0, 18);
8352 }
8353
8354 #[test]
8355 fn ctrl_b_moves_cursor_full_page_up() {
8356 let mut e = editor_with_rows(100, 20);
8357 e.jump_cursor(50, 0);
8358 run_keys(&mut e, "<C-b>");
8359 assert_eq!(e.cursor().0, 32);
8360 }
8361
8362 #[test]
8363 fn ctrl_d_lands_on_first_non_blank() {
8364 let mut e = editor_with_rows(100, 20);
8365 run_keys(&mut e, "<C-d>");
8366 assert_eq!(e.cursor().1, 2);
8368 }
8369
8370 #[test]
8371 fn ctrl_d_clamps_at_end_of_buffer() {
8372 let mut e = editor_with_rows(5, 20);
8373 run_keys(&mut e, "<C-d>");
8374 assert_eq!(e.cursor().0, 4);
8375 }
8376
8377 #[test]
8378 fn capital_h_jumps_to_viewport_top() {
8379 let mut e = editor_with_rows(100, 10);
8380 e.jump_cursor(50, 0);
8381 e.set_viewport_top(45);
8382 let top = e.host().viewport().top_row;
8383 run_keys(&mut e, "H");
8384 assert_eq!(e.cursor().0, top);
8385 assert_eq!(e.cursor().1, 2);
8386 }
8387
8388 #[test]
8389 fn capital_l_jumps_to_viewport_bottom() {
8390 let mut e = editor_with_rows(100, 10);
8391 e.jump_cursor(50, 0);
8392 e.set_viewport_top(45);
8393 let top = e.host().viewport().top_row;
8394 run_keys(&mut e, "L");
8395 assert_eq!(e.cursor().0, top + 9);
8396 }
8397
8398 #[test]
8399 fn capital_m_jumps_to_viewport_middle() {
8400 let mut e = editor_with_rows(100, 10);
8401 e.jump_cursor(50, 0);
8402 e.set_viewport_top(45);
8403 let top = e.host().viewport().top_row;
8404 run_keys(&mut e, "M");
8405 assert_eq!(e.cursor().0, top + 4);
8407 }
8408
8409 #[test]
8410 fn g_capital_m_lands_at_line_midpoint() {
8411 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8413 assert_eq!(e.cursor(), (0, 6));
8415 }
8416
8417 #[test]
8418 fn g_capital_m_on_empty_line_stays_at_zero() {
8419 let mut e = editor_with("");
8420 run_keys(&mut e, "gM");
8421 assert_eq!(e.cursor(), (0, 0));
8422 }
8423
8424 #[test]
8425 fn g_capital_m_uses_current_line_only() {
8426 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8429 run_keys(&mut e, "gM");
8430 assert_eq!(e.cursor(), (1, 6));
8431 }
8432
8433 #[test]
8434 fn capital_h_count_offsets_from_top() {
8435 let mut e = editor_with_rows(100, 10);
8436 e.jump_cursor(50, 0);
8437 e.set_viewport_top(45);
8438 let top = e.host().viewport().top_row;
8439 run_keys(&mut e, "3H");
8440 assert_eq!(e.cursor().0, top + 2);
8441 }
8442
8443 #[test]
8446 fn ctrl_o_returns_to_pre_g_position() {
8447 let mut e = editor_with_rows(50, 20);
8448 e.jump_cursor(5, 2);
8449 run_keys(&mut e, "G");
8450 assert_eq!(e.cursor().0, 49);
8451 run_keys(&mut e, "<C-o>");
8452 assert_eq!(e.cursor(), (5, 2));
8453 }
8454
8455 #[test]
8456 fn ctrl_i_redoes_jump_after_ctrl_o() {
8457 let mut e = editor_with_rows(50, 20);
8458 e.jump_cursor(5, 2);
8459 run_keys(&mut e, "G");
8460 let post = e.cursor();
8461 run_keys(&mut e, "<C-o>");
8462 run_keys(&mut e, "<C-i>");
8463 assert_eq!(e.cursor(), post);
8464 }
8465
8466 #[test]
8467 fn new_jump_clears_forward_stack() {
8468 let mut e = editor_with_rows(50, 20);
8469 e.jump_cursor(5, 2);
8470 run_keys(&mut e, "G");
8471 run_keys(&mut e, "<C-o>");
8472 run_keys(&mut e, "gg");
8473 run_keys(&mut e, "<C-i>");
8474 assert_eq!(e.cursor().0, 0);
8475 }
8476
8477 #[test]
8478 fn ctrl_o_on_empty_stack_is_noop() {
8479 let mut e = editor_with_rows(10, 20);
8480 e.jump_cursor(3, 1);
8481 run_keys(&mut e, "<C-o>");
8482 assert_eq!(e.cursor(), (3, 1));
8483 }
8484
8485 #[test]
8486 fn asterisk_search_pushes_jump() {
8487 let mut e = editor_with("foo bar\nbaz foo end");
8488 e.jump_cursor(0, 0);
8489 run_keys(&mut e, "*");
8490 let after = e.cursor();
8491 assert_ne!(after, (0, 0));
8492 run_keys(&mut e, "<C-o>");
8493 assert_eq!(e.cursor(), (0, 0));
8494 }
8495
8496 #[test]
8497 fn h_viewport_jump_is_recorded() {
8498 let mut e = editor_with_rows(100, 10);
8499 e.jump_cursor(50, 0);
8500 e.set_viewport_top(45);
8501 let pre = e.cursor();
8502 run_keys(&mut e, "H");
8503 assert_ne!(e.cursor(), pre);
8504 run_keys(&mut e, "<C-o>");
8505 assert_eq!(e.cursor(), pre);
8506 }
8507
8508 #[test]
8509 fn j_k_motion_does_not_push_jump() {
8510 let mut e = editor_with_rows(50, 20);
8511 e.jump_cursor(5, 0);
8512 run_keys(&mut e, "jjj");
8513 run_keys(&mut e, "<C-o>");
8514 assert_eq!(e.cursor().0, 8);
8515 }
8516
8517 #[test]
8518 fn jumplist_caps_at_100() {
8519 let mut e = editor_with_rows(200, 20);
8520 for i in 0..101 {
8521 e.jump_cursor(i, 0);
8522 run_keys(&mut e, "G");
8523 }
8524 assert!(e.vim.jump_back.len() <= 100);
8525 }
8526
8527 #[test]
8528 fn tab_acts_as_ctrl_i() {
8529 let mut e = editor_with_rows(50, 20);
8530 e.jump_cursor(5, 2);
8531 run_keys(&mut e, "G");
8532 let post = e.cursor();
8533 run_keys(&mut e, "<C-o>");
8534 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8535 assert_eq!(e.cursor(), post);
8536 }
8537
8538 #[test]
8541 fn ma_then_backtick_a_jumps_exact() {
8542 let mut e = editor_with_rows(50, 20);
8543 e.jump_cursor(5, 3);
8544 run_keys(&mut e, "ma");
8545 e.jump_cursor(20, 0);
8546 run_keys(&mut e, "`a");
8547 assert_eq!(e.cursor(), (5, 3));
8548 }
8549
8550 #[test]
8551 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8552 let mut e = editor_with_rows(50, 20);
8553 e.jump_cursor(5, 6);
8555 run_keys(&mut e, "ma");
8556 e.jump_cursor(30, 4);
8557 run_keys(&mut e, "'a");
8558 assert_eq!(e.cursor(), (5, 2));
8559 }
8560
8561 #[test]
8562 fn goto_mark_pushes_jumplist() {
8563 let mut e = editor_with_rows(50, 20);
8564 e.jump_cursor(10, 2);
8565 run_keys(&mut e, "mz");
8566 e.jump_cursor(3, 0);
8567 run_keys(&mut e, "`z");
8568 assert_eq!(e.cursor(), (10, 2));
8569 run_keys(&mut e, "<C-o>");
8570 assert_eq!(e.cursor(), (3, 0));
8571 }
8572
8573 #[test]
8574 fn goto_missing_mark_is_noop() {
8575 let mut e = editor_with_rows(50, 20);
8576 e.jump_cursor(3, 1);
8577 run_keys(&mut e, "`q");
8578 assert_eq!(e.cursor(), (3, 1));
8579 }
8580
8581 #[test]
8582 fn uppercase_mark_stored_under_uppercase_key() {
8583 let mut e = editor_with_rows(50, 20);
8584 e.jump_cursor(5, 3);
8585 run_keys(&mut e, "mA");
8586 assert_eq!(e.mark('A'), Some((5, 3)));
8589 assert!(e.mark('a').is_none());
8590 }
8591
8592 #[test]
8593 fn mark_survives_document_shrink_via_clamp() {
8594 let mut e = editor_with_rows(50, 20);
8595 e.jump_cursor(40, 4);
8596 run_keys(&mut e, "mx");
8597 e.set_content("a\nb\nc\nd\ne");
8599 run_keys(&mut e, "`x");
8600 let (r, _) = e.cursor();
8602 assert!(r <= 4);
8603 }
8604
8605 #[test]
8606 fn g_semicolon_walks_back_through_edits() {
8607 let mut e = editor_with("alpha\nbeta\ngamma");
8608 e.jump_cursor(0, 0);
8611 run_keys(&mut e, "iX<Esc>");
8612 e.jump_cursor(2, 0);
8613 run_keys(&mut e, "iY<Esc>");
8614 run_keys(&mut e, "g;");
8616 assert_eq!(e.cursor(), (2, 1));
8617 run_keys(&mut e, "g;");
8619 assert_eq!(e.cursor(), (0, 1));
8620 run_keys(&mut e, "g;");
8622 assert_eq!(e.cursor(), (0, 1));
8623 }
8624
8625 #[test]
8626 fn g_comma_walks_forward_after_g_semicolon() {
8627 let mut e = editor_with("a\nb\nc");
8628 e.jump_cursor(0, 0);
8629 run_keys(&mut e, "iX<Esc>");
8630 e.jump_cursor(2, 0);
8631 run_keys(&mut e, "iY<Esc>");
8632 run_keys(&mut e, "g;");
8633 run_keys(&mut e, "g;");
8634 assert_eq!(e.cursor(), (0, 1));
8635 run_keys(&mut e, "g,");
8636 assert_eq!(e.cursor(), (2, 1));
8637 }
8638
8639 #[test]
8640 fn new_edit_during_walk_trims_forward_entries() {
8641 let mut e = editor_with("a\nb\nc\nd");
8642 e.jump_cursor(0, 0);
8643 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8645 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8648 run_keys(&mut e, "g;");
8649 assert_eq!(e.cursor(), (0, 1));
8650 run_keys(&mut e, "iZ<Esc>");
8652 run_keys(&mut e, "g,");
8654 assert_ne!(e.cursor(), (2, 1));
8656 }
8657
8658 #[test]
8664 fn capital_mark_set_and_jump() {
8665 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8666 e.jump_cursor(2, 1);
8667 run_keys(&mut e, "mA");
8668 e.jump_cursor(0, 0);
8670 run_keys(&mut e, "'A");
8672 assert_eq!(e.cursor().0, 2);
8674 }
8675
8676 #[test]
8677 fn capital_mark_survives_set_content() {
8678 let mut e = editor_with("first buffer line\nsecond");
8679 e.jump_cursor(1, 3);
8680 run_keys(&mut e, "mA");
8681 e.set_content("totally different content\non many\nrows of text");
8683 e.jump_cursor(0, 0);
8685 run_keys(&mut e, "'A");
8686 assert_eq!(e.cursor().0, 1);
8687 }
8688
8689 #[test]
8694 fn capital_mark_shifts_with_edit() {
8695 let mut e = editor_with("a\nb\nc\nd");
8696 e.jump_cursor(3, 0);
8697 run_keys(&mut e, "mA");
8698 e.jump_cursor(0, 0);
8700 run_keys(&mut e, "dd");
8701 e.jump_cursor(0, 0);
8702 run_keys(&mut e, "'A");
8703 assert_eq!(e.cursor().0, 2);
8704 }
8705
8706 #[test]
8707 fn mark_below_delete_shifts_up() {
8708 let mut e = editor_with("a\nb\nc\nd\ne");
8709 e.jump_cursor(3, 0);
8711 run_keys(&mut e, "ma");
8712 e.jump_cursor(0, 0);
8714 run_keys(&mut e, "dd");
8715 e.jump_cursor(0, 0);
8717 run_keys(&mut e, "'a");
8718 assert_eq!(e.cursor().0, 2);
8719 assert_eq!(e.buffer().line(2).unwrap(), "d");
8720 }
8721
8722 #[test]
8723 fn mark_on_deleted_row_is_dropped() {
8724 let mut e = editor_with("a\nb\nc\nd");
8725 e.jump_cursor(1, 0);
8727 run_keys(&mut e, "ma");
8728 run_keys(&mut e, "dd");
8730 e.jump_cursor(2, 0);
8732 run_keys(&mut e, "'a");
8733 assert_eq!(e.cursor().0, 2);
8735 }
8736
8737 #[test]
8738 fn mark_above_edit_unchanged() {
8739 let mut e = editor_with("a\nb\nc\nd\ne");
8740 e.jump_cursor(0, 0);
8742 run_keys(&mut e, "ma");
8743 e.jump_cursor(3, 0);
8745 run_keys(&mut e, "dd");
8746 e.jump_cursor(2, 0);
8748 run_keys(&mut e, "'a");
8749 assert_eq!(e.cursor().0, 0);
8750 }
8751
8752 #[test]
8753 fn mark_shifts_down_after_insert() {
8754 let mut e = editor_with("a\nb\nc");
8755 e.jump_cursor(2, 0);
8757 run_keys(&mut e, "ma");
8758 e.jump_cursor(0, 0);
8760 run_keys(&mut e, "Onew<Esc>");
8761 e.jump_cursor(0, 0);
8764 run_keys(&mut e, "'a");
8765 assert_eq!(e.cursor().0, 3);
8766 assert_eq!(e.buffer().line(3).unwrap(), "c");
8767 }
8768
8769 #[test]
8772 fn forward_search_commit_pushes_jump() {
8773 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8774 e.jump_cursor(0, 0);
8775 run_keys(&mut e, "/target<CR>");
8776 assert_ne!(e.cursor(), (0, 0));
8778 run_keys(&mut e, "<C-o>");
8780 assert_eq!(e.cursor(), (0, 0));
8781 }
8782
8783 #[test]
8784 fn search_commit_no_match_does_not_push_jump() {
8785 let mut e = editor_with("alpha beta\nfoo end");
8786 e.jump_cursor(0, 3);
8787 let pre_len = e.vim.jump_back.len();
8788 run_keys(&mut e, "/zzznotfound<CR>");
8789 assert_eq!(e.vim.jump_back.len(), pre_len);
8791 }
8792
8793 #[test]
8796 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8797 let mut e = editor_with("hello world");
8798 run_keys(&mut e, "lll");
8799 let (row, col) = e.cursor();
8800 assert_eq!(e.buffer.cursor().row, row);
8801 assert_eq!(e.buffer.cursor().col, col);
8802 }
8803
8804 #[test]
8805 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8806 let mut e = editor_with("aaaa\nbbbb\ncccc");
8807 run_keys(&mut e, "jj");
8808 let (row, col) = e.cursor();
8809 assert_eq!(e.buffer.cursor().row, row);
8810 assert_eq!(e.buffer.cursor().col, col);
8811 }
8812
8813 #[test]
8814 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8815 let mut e = editor_with("foo bar baz");
8816 run_keys(&mut e, "ww");
8817 let (row, col) = e.cursor();
8818 assert_eq!(e.buffer.cursor().row, row);
8819 assert_eq!(e.buffer.cursor().col, col);
8820 }
8821
8822 #[test]
8823 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8824 let mut e = editor_with("a\nb\nc\nd\ne");
8825 run_keys(&mut e, "G");
8826 let (row, col) = e.cursor();
8827 assert_eq!(e.buffer.cursor().row, row);
8828 assert_eq!(e.buffer.cursor().col, col);
8829 }
8830
8831 #[test]
8832 fn editor_sticky_col_tracks_horizontal_motion() {
8833 let mut e = editor_with("longline\nhi\nlongline");
8834 run_keys(&mut e, "fl");
8839 let landed = e.cursor().1;
8840 assert!(landed > 0, "fl should have moved");
8841 run_keys(&mut e, "j");
8842 assert_eq!(e.sticky_col(), Some(landed));
8845 }
8846
8847 #[test]
8848 fn buffer_content_mirrors_textarea_after_insert() {
8849 let mut e = editor_with("hello");
8850 run_keys(&mut e, "iXYZ<Esc>");
8851 let text = e.buffer().lines().join("\n");
8852 assert_eq!(e.buffer.as_string(), text);
8853 }
8854
8855 #[test]
8856 fn buffer_content_mirrors_textarea_after_delete() {
8857 let mut e = editor_with("alpha bravo charlie");
8858 run_keys(&mut e, "dw");
8859 let text = e.buffer().lines().join("\n");
8860 assert_eq!(e.buffer.as_string(), text);
8861 }
8862
8863 #[test]
8864 fn buffer_content_mirrors_textarea_after_dd() {
8865 let mut e = editor_with("a\nb\nc\nd");
8866 run_keys(&mut e, "jdd");
8867 let text = e.buffer().lines().join("\n");
8868 assert_eq!(e.buffer.as_string(), text);
8869 }
8870
8871 #[test]
8872 fn buffer_content_mirrors_textarea_after_open_line() {
8873 let mut e = editor_with("foo\nbar");
8874 run_keys(&mut e, "oNEW<Esc>");
8875 let text = e.buffer().lines().join("\n");
8876 assert_eq!(e.buffer.as_string(), text);
8877 }
8878
8879 #[test]
8880 fn buffer_content_mirrors_textarea_after_paste() {
8881 let mut e = editor_with("hello");
8882 run_keys(&mut e, "yy");
8883 run_keys(&mut e, "p");
8884 let text = e.buffer().lines().join("\n");
8885 assert_eq!(e.buffer.as_string(), text);
8886 }
8887
8888 #[test]
8889 fn buffer_selection_none_in_normal_mode() {
8890 let e = editor_with("foo bar");
8891 assert!(e.buffer_selection().is_none());
8892 }
8893
8894 #[test]
8895 fn buffer_selection_char_in_visual_mode() {
8896 use hjkl_buffer::{Position, Selection};
8897 let mut e = editor_with("hello world");
8898 run_keys(&mut e, "vlll");
8899 assert_eq!(
8900 e.buffer_selection(),
8901 Some(Selection::Char {
8902 anchor: Position::new(0, 0),
8903 head: Position::new(0, 3),
8904 })
8905 );
8906 }
8907
8908 #[test]
8909 fn buffer_selection_line_in_visual_line_mode() {
8910 use hjkl_buffer::Selection;
8911 let mut e = editor_with("a\nb\nc\nd");
8912 run_keys(&mut e, "Vj");
8913 assert_eq!(
8914 e.buffer_selection(),
8915 Some(Selection::Line {
8916 anchor_row: 0,
8917 head_row: 1,
8918 })
8919 );
8920 }
8921
8922 #[test]
8923 fn wrapscan_off_blocks_wrap_around() {
8924 let mut e = editor_with("first\nsecond\nthird\n");
8925 e.settings_mut().wrapscan = false;
8926 e.jump_cursor(2, 0);
8928 run_keys(&mut e, "/first<CR>");
8929 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8931 e.settings_mut().wrapscan = true;
8933 run_keys(&mut e, "/first<CR>");
8934 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8935 }
8936
8937 #[test]
8938 fn smartcase_uppercase_pattern_stays_sensitive() {
8939 let mut e = editor_with("foo\nFoo\nBAR\n");
8940 e.settings_mut().ignore_case = true;
8941 e.settings_mut().smartcase = true;
8942 run_keys(&mut e, "/foo<CR>");
8945 let r1 = e
8946 .search_state()
8947 .pattern
8948 .as_ref()
8949 .unwrap()
8950 .as_str()
8951 .to_string();
8952 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8953 run_keys(&mut e, "/Foo<CR>");
8955 let r2 = e
8956 .search_state()
8957 .pattern
8958 .as_ref()
8959 .unwrap()
8960 .as_str()
8961 .to_string();
8962 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8963 }
8964
8965 #[test]
8966 fn enter_with_autoindent_copies_leading_whitespace() {
8967 let mut e = editor_with(" foo");
8968 e.jump_cursor(0, 7);
8969 run_keys(&mut e, "i<CR>");
8970 assert_eq!(e.buffer.line(1).unwrap(), " ");
8971 }
8972
8973 #[test]
8974 fn enter_without_autoindent_inserts_bare_newline() {
8975 let mut e = editor_with(" foo");
8976 e.settings_mut().autoindent = false;
8977 e.jump_cursor(0, 7);
8978 run_keys(&mut e, "i<CR>");
8979 assert_eq!(e.buffer.line(1).unwrap(), "");
8980 }
8981
8982 #[test]
8983 fn iskeyword_default_treats_alnum_underscore_as_word() {
8984 let mut e = editor_with("foo_bar baz");
8985 e.jump_cursor(0, 0);
8989 run_keys(&mut e, "*");
8990 let p = e
8991 .search_state()
8992 .pattern
8993 .as_ref()
8994 .unwrap()
8995 .as_str()
8996 .to_string();
8997 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8998 }
8999
9000 #[test]
9001 fn w_motion_respects_custom_iskeyword() {
9002 let mut e = editor_with("foo-bar baz");
9006 run_keys(&mut e, "w");
9007 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9008 let mut e2 = editor_with("foo-bar baz");
9011 e2.set_iskeyword("@,_,45");
9012 run_keys(&mut e2, "w");
9013 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9014 }
9015
9016 #[test]
9017 fn iskeyword_with_dash_treats_dash_as_word_char() {
9018 let mut e = editor_with("foo-bar baz");
9019 e.settings_mut().iskeyword = "@,_,45".to_string();
9020 e.jump_cursor(0, 0);
9021 run_keys(&mut e, "*");
9022 let p = e
9023 .search_state()
9024 .pattern
9025 .as_ref()
9026 .unwrap()
9027 .as_str()
9028 .to_string();
9029 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9030 }
9031
9032 #[test]
9033 fn timeoutlen_drops_pending_g_prefix() {
9034 use std::time::{Duration, Instant};
9035 let mut e = editor_with("a\nb\nc");
9036 e.jump_cursor(2, 0);
9037 run_keys(&mut e, "g");
9039 assert!(matches!(e.vim.pending, super::Pending::G));
9040 e.settings.timeout_len = Duration::from_nanos(0);
9048 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9049 e.vim.last_input_host_at = Some(Duration::ZERO);
9050 run_keys(&mut e, "g");
9054 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9056 }
9057
9058 #[test]
9059 fn undobreak_on_breaks_group_at_arrow_motion() {
9060 let mut e = editor_with("");
9061 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9063 let line = e.buffer.line(0).unwrap_or("").to_string();
9066 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9067 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9068 }
9069
9070 #[test]
9071 fn undobreak_off_keeps_full_run_in_one_group() {
9072 let mut e = editor_with("");
9073 e.settings_mut().undo_break_on_motion = false;
9074 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9075 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9078 }
9079
9080 #[test]
9081 fn undobreak_round_trips_through_options() {
9082 let e = editor_with("");
9083 let opts = e.current_options();
9084 assert!(opts.undo_break_on_motion);
9085 let mut e2 = editor_with("");
9086 let mut new_opts = opts.clone();
9087 new_opts.undo_break_on_motion = false;
9088 e2.apply_options(&new_opts);
9089 assert!(!e2.current_options().undo_break_on_motion);
9090 }
9091
9092 #[test]
9093 fn undo_levels_cap_drops_oldest() {
9094 let mut e = editor_with("abcde");
9095 e.settings_mut().undo_levels = 3;
9096 run_keys(&mut e, "ra");
9097 run_keys(&mut e, "lrb");
9098 run_keys(&mut e, "lrc");
9099 run_keys(&mut e, "lrd");
9100 run_keys(&mut e, "lre");
9101 assert_eq!(e.undo_stack_len(), 3);
9102 }
9103
9104 #[test]
9105 fn tab_inserts_literal_tab_when_noexpandtab() {
9106 let mut e = editor_with("");
9107 e.settings_mut().expandtab = false;
9110 e.settings_mut().softtabstop = 0;
9111 run_keys(&mut e, "i");
9112 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9113 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9114 }
9115
9116 #[test]
9117 fn tab_inserts_spaces_when_expandtab() {
9118 let mut e = editor_with("");
9119 e.settings_mut().expandtab = true;
9120 e.settings_mut().tabstop = 4;
9121 run_keys(&mut e, "i");
9122 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9123 assert_eq!(e.buffer.line(0).unwrap(), " ");
9124 }
9125
9126 #[test]
9127 fn tab_with_softtabstop_fills_to_next_boundary() {
9128 let mut e = editor_with("ab");
9130 e.settings_mut().expandtab = true;
9131 e.settings_mut().tabstop = 8;
9132 e.settings_mut().softtabstop = 4;
9133 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9135 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9136 }
9137
9138 #[test]
9139 fn backspace_deletes_softtab_run() {
9140 let mut e = editor_with(" x");
9143 e.settings_mut().softtabstop = 4;
9144 run_keys(&mut e, "fxi");
9146 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9147 assert_eq!(e.buffer.line(0).unwrap(), "x");
9148 }
9149
9150 #[test]
9151 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9152 let mut e = editor_with(" x");
9155 e.settings_mut().softtabstop = 4;
9156 run_keys(&mut e, "fxi");
9157 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9158 assert_eq!(e.buffer.line(0).unwrap(), " x");
9159 }
9160
9161 #[test]
9162 fn readonly_blocks_insert_mutation() {
9163 let mut e = editor_with("hello");
9164 e.settings_mut().readonly = true;
9165 run_keys(&mut e, "iX<Esc>");
9166 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9167 }
9168
9169 #[cfg(feature = "ratatui")]
9170 #[test]
9171 fn intern_ratatui_style_dedups_repeated_styles() {
9172 use ratatui::style::{Color, Style};
9173 let mut e = editor_with("");
9174 let red = Style::default().fg(Color::Red);
9175 let blue = Style::default().fg(Color::Blue);
9176 let id_r1 = e.intern_ratatui_style(red);
9177 let id_r2 = e.intern_ratatui_style(red);
9178 let id_b = e.intern_ratatui_style(blue);
9179 assert_eq!(id_r1, id_r2);
9180 assert_ne!(id_r1, id_b);
9181 assert_eq!(e.style_table().len(), 2);
9182 }
9183
9184 #[cfg(feature = "ratatui")]
9185 #[test]
9186 fn install_ratatui_syntax_spans_translates_styled_spans() {
9187 use ratatui::style::{Color, Style};
9188 let mut e = editor_with("SELECT foo");
9189 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9190 let by_row = e.buffer_spans();
9191 assert_eq!(by_row.len(), 1);
9192 assert_eq!(by_row[0].len(), 1);
9193 assert_eq!(by_row[0][0].start_byte, 0);
9194 assert_eq!(by_row[0][0].end_byte, 6);
9195 let id = by_row[0][0].style;
9196 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9197 }
9198
9199 #[cfg(feature = "ratatui")]
9200 #[test]
9201 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9202 use ratatui::style::{Color, Style};
9203 let mut e = editor_with("hello");
9204 e.install_ratatui_syntax_spans(vec![vec![(
9205 0,
9206 usize::MAX,
9207 Style::default().fg(Color::Blue),
9208 )]]);
9209 let by_row = e.buffer_spans();
9210 assert_eq!(by_row[0][0].end_byte, 5);
9211 }
9212
9213 #[cfg(feature = "ratatui")]
9214 #[test]
9215 fn install_ratatui_syntax_spans_drops_zero_width() {
9216 use ratatui::style::{Color, Style};
9217 let mut e = editor_with("abc");
9218 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9219 assert!(e.buffer_spans()[0].is_empty());
9220 }
9221
9222 #[test]
9223 fn named_register_yank_into_a_then_paste_from_a() {
9224 let mut e = editor_with("hello world\nsecond");
9225 run_keys(&mut e, "\"ayw");
9226 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9228 run_keys(&mut e, "j0\"aP");
9230 assert_eq!(e.buffer().lines()[1], "hello second");
9231 }
9232
9233 #[test]
9234 fn capital_r_overstrikes_chars() {
9235 let mut e = editor_with("hello");
9236 e.jump_cursor(0, 0);
9237 run_keys(&mut e, "RXY<Esc>");
9238 assert_eq!(e.buffer().lines()[0], "XYllo");
9240 }
9241
9242 #[test]
9243 fn capital_r_at_eol_appends() {
9244 let mut e = editor_with("hi");
9245 e.jump_cursor(0, 1);
9246 run_keys(&mut e, "RXYZ<Esc>");
9248 assert_eq!(e.buffer().lines()[0], "hXYZ");
9249 }
9250
9251 #[test]
9252 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9253 let mut e = editor_with("abc");
9257 e.jump_cursor(0, 0);
9258 run_keys(&mut e, "RX<Esc>");
9259 assert_eq!(e.buffer().lines()[0], "Xbc");
9260 }
9261
9262 #[test]
9263 fn ctrl_r_in_insert_pastes_named_register() {
9264 let mut e = editor_with("hello world");
9265 run_keys(&mut e, "\"ayw");
9267 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9268 run_keys(&mut e, "o");
9270 assert_eq!(e.vim_mode(), VimMode::Insert);
9271 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9272 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9273 assert_eq!(e.buffer().lines()[1], "hello ");
9274 assert_eq!(e.cursor(), (1, 6));
9276 assert_eq!(e.vim_mode(), VimMode::Insert);
9278 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9279 assert_eq!(e.buffer().lines()[1], "hello X");
9280 }
9281
9282 #[test]
9283 fn ctrl_r_with_unnamed_register() {
9284 let mut e = editor_with("foo");
9285 run_keys(&mut e, "yiw");
9286 run_keys(&mut e, "A ");
9287 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9289 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9290 assert_eq!(e.buffer().lines()[0], "foo foo");
9291 }
9292
9293 #[test]
9294 fn ctrl_r_unknown_selector_is_no_op() {
9295 let mut e = editor_with("abc");
9296 run_keys(&mut e, "A");
9297 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9298 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9301 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9302 assert_eq!(e.buffer().lines()[0], "abcZ");
9303 }
9304
9305 #[test]
9306 fn ctrl_r_multiline_register_pastes_with_newlines() {
9307 let mut e = editor_with("alpha\nbeta\ngamma");
9308 run_keys(&mut e, "\"byy");
9310 run_keys(&mut e, "j\"byy");
9311 run_keys(&mut e, "ggVj\"by");
9315 let payload = e.registers().read('b').unwrap().text.clone();
9316 assert!(payload.contains('\n'));
9317 run_keys(&mut e, "Go");
9318 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9319 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9320 let total_lines = e.buffer().lines().len();
9323 assert!(total_lines >= 5);
9324 }
9325
9326 #[test]
9327 fn yank_zero_holds_last_yank_after_delete() {
9328 let mut e = editor_with("hello world");
9329 run_keys(&mut e, "yw");
9330 let yanked = e.registers().read('0').unwrap().text.clone();
9331 assert!(!yanked.is_empty());
9332 run_keys(&mut e, "dw");
9334 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9335 assert!(!e.registers().read('1').unwrap().text.is_empty());
9337 }
9338
9339 #[test]
9340 fn delete_ring_rotates_through_one_through_nine() {
9341 let mut e = editor_with("a b c d e f g h i j");
9342 for _ in 0..3 {
9344 run_keys(&mut e, "dw");
9345 }
9346 let r1 = e.registers().read('1').unwrap().text.clone();
9348 let r2 = e.registers().read('2').unwrap().text.clone();
9349 let r3 = e.registers().read('3').unwrap().text.clone();
9350 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9351 assert_ne!(r1, r2);
9352 assert_ne!(r2, r3);
9353 }
9354
9355 #[test]
9356 fn capital_register_appends_to_lowercase() {
9357 let mut e = editor_with("foo bar");
9358 run_keys(&mut e, "\"ayw");
9359 let first = e.registers().read('a').unwrap().text.clone();
9360 assert!(first.contains("foo"));
9361 run_keys(&mut e, "w\"Ayw");
9363 let combined = e.registers().read('a').unwrap().text.clone();
9364 assert!(combined.starts_with(&first));
9365 assert!(combined.contains("bar"));
9366 }
9367
9368 #[test]
9369 fn zf_in_visual_line_creates_closed_fold() {
9370 let mut e = editor_with("a\nb\nc\nd\ne");
9371 e.jump_cursor(1, 0);
9373 run_keys(&mut e, "Vjjzf");
9374 assert_eq!(e.buffer().folds().len(), 1);
9375 let f = e.buffer().folds()[0];
9376 assert_eq!(f.start_row, 1);
9377 assert_eq!(f.end_row, 3);
9378 assert!(f.closed);
9379 }
9380
9381 #[test]
9382 fn zfj_in_normal_creates_two_row_fold() {
9383 let mut e = editor_with("a\nb\nc\nd\ne");
9384 e.jump_cursor(1, 0);
9385 run_keys(&mut e, "zfj");
9386 assert_eq!(e.buffer().folds().len(), 1);
9387 let f = e.buffer().folds()[0];
9388 assert_eq!(f.start_row, 1);
9389 assert_eq!(f.end_row, 2);
9390 assert!(f.closed);
9391 assert_eq!(e.cursor().0, 1);
9393 }
9394
9395 #[test]
9396 fn zf_with_count_folds_count_rows() {
9397 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9398 e.jump_cursor(0, 0);
9399 run_keys(&mut e, "zf3j");
9401 assert_eq!(e.buffer().folds().len(), 1);
9402 let f = e.buffer().folds()[0];
9403 assert_eq!(f.start_row, 0);
9404 assert_eq!(f.end_row, 3);
9405 }
9406
9407 #[test]
9408 fn zfk_folds_upward_range() {
9409 let mut e = editor_with("a\nb\nc\nd\ne");
9410 e.jump_cursor(3, 0);
9411 run_keys(&mut e, "zfk");
9412 let f = e.buffer().folds()[0];
9413 assert_eq!(f.start_row, 2);
9415 assert_eq!(f.end_row, 3);
9416 }
9417
9418 #[test]
9419 fn zf_capital_g_folds_to_bottom() {
9420 let mut e = editor_with("a\nb\nc\nd\ne");
9421 e.jump_cursor(1, 0);
9422 run_keys(&mut e, "zfG");
9424 let f = e.buffer().folds()[0];
9425 assert_eq!(f.start_row, 1);
9426 assert_eq!(f.end_row, 4);
9427 }
9428
9429 #[test]
9430 fn zfgg_folds_to_top_via_operator_pipeline() {
9431 let mut e = editor_with("a\nb\nc\nd\ne");
9432 e.jump_cursor(3, 0);
9433 run_keys(&mut e, "zfgg");
9437 let f = e.buffer().folds()[0];
9438 assert_eq!(f.start_row, 0);
9439 assert_eq!(f.end_row, 3);
9440 }
9441
9442 #[test]
9443 fn zfip_folds_paragraph_via_text_object() {
9444 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9445 e.jump_cursor(1, 0);
9446 run_keys(&mut e, "zfip");
9448 assert_eq!(e.buffer().folds().len(), 1);
9449 let f = e.buffer().folds()[0];
9450 assert_eq!(f.start_row, 0);
9451 assert_eq!(f.end_row, 2);
9452 }
9453
9454 #[test]
9455 fn zfap_folds_paragraph_with_trailing_blank() {
9456 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9457 e.jump_cursor(0, 0);
9458 run_keys(&mut e, "zfap");
9460 let f = e.buffer().folds()[0];
9461 assert_eq!(f.start_row, 0);
9462 assert_eq!(f.end_row, 3);
9463 }
9464
9465 #[test]
9466 fn zf_paragraph_motion_folds_to_blank() {
9467 let mut e = editor_with("alpha\nbeta\n\ngamma");
9468 e.jump_cursor(0, 0);
9469 run_keys(&mut e, "zf}");
9471 let f = e.buffer().folds()[0];
9472 assert_eq!(f.start_row, 0);
9473 assert_eq!(f.end_row, 2);
9474 }
9475
9476 #[test]
9477 fn za_toggles_fold_under_cursor() {
9478 let mut e = editor_with("a\nb\nc\nd");
9479 e.buffer_mut().add_fold(1, 2, true);
9480 e.jump_cursor(1, 0);
9481 run_keys(&mut e, "za");
9482 assert!(!e.buffer().folds()[0].closed);
9483 run_keys(&mut e, "za");
9484 assert!(e.buffer().folds()[0].closed);
9485 }
9486
9487 #[test]
9488 fn zr_opens_all_folds_zm_closes_all() {
9489 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9490 e.buffer_mut().add_fold(0, 1, true);
9491 e.buffer_mut().add_fold(2, 3, true);
9492 e.buffer_mut().add_fold(4, 5, true);
9493 run_keys(&mut e, "zR");
9494 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9495 run_keys(&mut e, "zM");
9496 assert!(e.buffer().folds().iter().all(|f| f.closed));
9497 }
9498
9499 #[test]
9500 fn ze_clears_all_folds() {
9501 let mut e = editor_with("a\nb\nc\nd");
9502 e.buffer_mut().add_fold(0, 1, true);
9503 e.buffer_mut().add_fold(2, 3, false);
9504 run_keys(&mut e, "zE");
9505 assert!(e.buffer().folds().is_empty());
9506 }
9507
9508 #[test]
9509 fn g_underscore_jumps_to_last_non_blank() {
9510 let mut e = editor_with("hello world ");
9511 run_keys(&mut e, "g_");
9512 assert_eq!(e.cursor().1, 10);
9514 }
9515
9516 #[test]
9517 fn gj_and_gk_alias_j_and_k() {
9518 let mut e = editor_with("a\nb\nc");
9519 run_keys(&mut e, "gj");
9520 assert_eq!(e.cursor().0, 1);
9521 run_keys(&mut e, "gk");
9522 assert_eq!(e.cursor().0, 0);
9523 }
9524
9525 #[test]
9526 fn paragraph_motions_walk_blank_lines() {
9527 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9528 run_keys(&mut e, "}");
9529 assert_eq!(e.cursor().0, 2);
9530 run_keys(&mut e, "}");
9531 assert_eq!(e.cursor().0, 5);
9532 run_keys(&mut e, "{");
9533 assert_eq!(e.cursor().0, 2);
9534 }
9535
9536 #[test]
9537 fn gv_reenters_last_visual_selection() {
9538 let mut e = editor_with("alpha\nbeta\ngamma");
9539 run_keys(&mut e, "Vj");
9540 run_keys(&mut e, "<Esc>");
9542 assert_eq!(e.vim_mode(), VimMode::Normal);
9543 run_keys(&mut e, "gv");
9545 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9546 }
9547
9548 #[test]
9549 fn o_in_visual_swaps_anchor_and_cursor() {
9550 let mut e = editor_with("hello world");
9551 run_keys(&mut e, "vllll");
9553 assert_eq!(e.cursor().1, 4);
9554 run_keys(&mut e, "o");
9556 assert_eq!(e.cursor().1, 0);
9557 assert_eq!(e.vim.visual_anchor, (0, 4));
9559 }
9560
9561 #[test]
9562 fn editing_inside_fold_invalidates_it() {
9563 let mut e = editor_with("a\nb\nc\nd");
9564 e.buffer_mut().add_fold(1, 2, true);
9565 e.jump_cursor(1, 0);
9566 run_keys(&mut e, "iX<Esc>");
9568 assert!(e.buffer().folds().is_empty());
9570 }
9571
9572 #[test]
9573 fn zd_removes_fold_under_cursor() {
9574 let mut e = editor_with("a\nb\nc\nd");
9575 e.buffer_mut().add_fold(1, 2, true);
9576 e.jump_cursor(2, 0);
9577 run_keys(&mut e, "zd");
9578 assert!(e.buffer().folds().is_empty());
9579 }
9580
9581 #[test]
9582 fn take_fold_ops_observes_z_keystroke_dispatch() {
9583 use crate::types::FoldOp;
9588 let mut e = editor_with("a\nb\nc\nd");
9589 e.buffer_mut().add_fold(1, 2, true);
9590 e.jump_cursor(1, 0);
9591 let _ = e.take_fold_ops();
9594 run_keys(&mut e, "zo");
9595 run_keys(&mut e, "zM");
9596 let ops = e.take_fold_ops();
9597 assert_eq!(ops.len(), 2);
9598 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9599 assert!(matches!(ops[1], FoldOp::CloseAll));
9600 assert!(e.take_fold_ops().is_empty());
9602 }
9603
9604 #[test]
9605 fn edit_pipeline_emits_invalidate_fold_op() {
9606 use crate::types::FoldOp;
9609 let mut e = editor_with("a\nb\nc\nd");
9610 e.buffer_mut().add_fold(1, 2, true);
9611 e.jump_cursor(1, 0);
9612 let _ = e.take_fold_ops();
9613 run_keys(&mut e, "iX<Esc>");
9614 let ops = e.take_fold_ops();
9615 assert!(
9616 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9617 "expected at least one Invalidate op, got {ops:?}"
9618 );
9619 }
9620
9621 #[test]
9622 fn dot_mark_jumps_to_last_edit_position() {
9623 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9624 e.jump_cursor(2, 0);
9625 run_keys(&mut e, "iX<Esc>");
9627 let after_edit = e.cursor();
9628 run_keys(&mut e, "gg");
9630 assert_eq!(e.cursor().0, 0);
9631 run_keys(&mut e, "'.");
9633 assert_eq!(e.cursor().0, after_edit.0);
9634 }
9635
9636 #[test]
9637 fn quote_quote_returns_to_pre_jump_position() {
9638 let mut e = editor_with_rows(50, 20);
9639 e.jump_cursor(10, 2);
9640 let before = e.cursor();
9641 run_keys(&mut e, "G");
9643 assert_ne!(e.cursor(), before);
9644 run_keys(&mut e, "''");
9646 assert_eq!(e.cursor().0, before.0);
9647 }
9648
9649 #[test]
9650 fn backtick_backtick_restores_exact_pre_jump_pos() {
9651 let mut e = editor_with_rows(50, 20);
9652 e.jump_cursor(7, 3);
9653 let before = e.cursor();
9654 run_keys(&mut e, "G");
9655 run_keys(&mut e, "``");
9656 assert_eq!(e.cursor(), before);
9657 }
9658
9659 #[test]
9660 fn macro_record_and_replay_basic() {
9661 let mut e = editor_with("foo\nbar\nbaz");
9662 run_keys(&mut e, "qaIX<Esc>jq");
9664 assert_eq!(e.buffer().lines()[0], "Xfoo");
9665 run_keys(&mut e, "@a");
9667 assert_eq!(e.buffer().lines()[1], "Xbar");
9668 run_keys(&mut e, "j@@");
9670 assert_eq!(e.buffer().lines()[2], "Xbaz");
9671 }
9672
9673 #[test]
9674 fn macro_count_replays_n_times() {
9675 let mut e = editor_with("a\nb\nc\nd\ne");
9676 run_keys(&mut e, "qajq");
9678 assert_eq!(e.cursor().0, 1);
9679 run_keys(&mut e, "3@a");
9681 assert_eq!(e.cursor().0, 4);
9682 }
9683
9684 #[test]
9685 fn macro_capital_q_appends_to_lowercase_register() {
9686 let mut e = editor_with("hello");
9687 run_keys(&mut e, "qall<Esc>q");
9688 run_keys(&mut e, "qAhh<Esc>q");
9689 let text = e.registers().read('a').unwrap().text.clone();
9692 assert!(text.contains("ll<Esc>"));
9693 assert!(text.contains("hh<Esc>"));
9694 }
9695
9696 #[test]
9697 fn buffer_selection_block_in_visual_block_mode() {
9698 use hjkl_buffer::{Position, Selection};
9699 let mut e = editor_with("aaaa\nbbbb\ncccc");
9700 run_keys(&mut e, "<C-v>jl");
9701 assert_eq!(
9702 e.buffer_selection(),
9703 Some(Selection::Block {
9704 anchor: Position::new(0, 0),
9705 head: Position::new(1, 1),
9706 })
9707 );
9708 }
9709
9710 #[test]
9713 fn n_after_question_mark_keeps_walking_backward() {
9714 let mut e = editor_with("foo bar foo baz foo end");
9717 e.jump_cursor(0, 22);
9718 run_keys(&mut e, "?foo<CR>");
9719 assert_eq!(e.cursor().1, 16);
9720 run_keys(&mut e, "n");
9721 assert_eq!(e.cursor().1, 8);
9722 run_keys(&mut e, "N");
9723 assert_eq!(e.cursor().1, 16);
9724 }
9725
9726 #[test]
9727 fn nested_macro_chord_records_literal_keys() {
9728 let mut e = editor_with("alpha\nbeta\ngamma");
9731 run_keys(&mut e, "qblq");
9733 run_keys(&mut e, "qaIX<Esc>q");
9736 e.jump_cursor(1, 0);
9738 run_keys(&mut e, "@a");
9739 assert_eq!(e.buffer().lines()[1], "Xbeta");
9740 }
9741
9742 #[test]
9743 fn shift_gt_motion_indents_one_line() {
9744 let mut e = editor_with("hello world");
9748 run_keys(&mut e, ">w");
9749 assert_eq!(e.buffer().lines()[0], " hello world");
9750 }
9751
9752 #[test]
9753 fn shift_lt_motion_outdents_one_line() {
9754 let mut e = editor_with(" hello world");
9755 run_keys(&mut e, "<lt>w");
9756 assert_eq!(e.buffer().lines()[0], " hello world");
9758 }
9759
9760 #[test]
9761 fn shift_gt_text_object_indents_paragraph() {
9762 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9763 e.jump_cursor(0, 0);
9764 run_keys(&mut e, ">ip");
9765 assert_eq!(e.buffer().lines()[0], " alpha");
9766 assert_eq!(e.buffer().lines()[1], " beta");
9767 assert_eq!(e.buffer().lines()[2], " gamma");
9768 assert_eq!(e.buffer().lines()[4], "rest");
9770 }
9771
9772 #[test]
9773 fn ctrl_o_runs_exactly_one_normal_command() {
9774 let mut e = editor_with("alpha beta gamma");
9777 e.jump_cursor(0, 0);
9778 run_keys(&mut e, "i");
9779 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9780 run_keys(&mut e, "dw");
9781 assert_eq!(e.vim_mode(), VimMode::Insert);
9783 run_keys(&mut e, "X");
9785 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9786 }
9787
9788 #[test]
9789 fn macro_replay_respects_mode_switching() {
9790 let mut e = editor_with("hi");
9794 run_keys(&mut e, "qaiX<Esc>0q");
9795 assert_eq!(e.vim_mode(), VimMode::Normal);
9796 e.set_content("yo");
9798 run_keys(&mut e, "@a");
9799 assert_eq!(e.vim_mode(), VimMode::Normal);
9800 assert_eq!(e.cursor().1, 0);
9801 assert_eq!(e.buffer().lines()[0], "Xyo");
9802 }
9803
9804 #[test]
9805 fn macro_recorded_text_round_trips_through_register() {
9806 let mut e = editor_with("");
9810 run_keys(&mut e, "qaiX<Esc>q");
9811 let text = e.registers().read('a').unwrap().text.clone();
9812 assert!(text.starts_with("iX"));
9813 run_keys(&mut e, "@a");
9815 assert_eq!(e.buffer().lines()[0], "XX");
9816 }
9817
9818 #[test]
9819 fn dot_after_macro_replays_macros_last_change() {
9820 let mut e = editor_with("ab\ncd\nef");
9823 run_keys(&mut e, "qaIX<Esc>jq");
9826 assert_eq!(e.buffer().lines()[0], "Xab");
9827 run_keys(&mut e, "@a");
9828 assert_eq!(e.buffer().lines()[1], "Xcd");
9829 let row_before_dot = e.cursor().0;
9832 run_keys(&mut e, ".");
9833 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9834 }
9835
9836 fn si_editor(content: &str) -> Editor {
9842 let opts = crate::types::Options {
9843 shiftwidth: 4,
9844 softtabstop: 4,
9845 expandtab: true,
9846 smartindent: true,
9847 autoindent: true,
9848 ..crate::types::Options::default()
9849 };
9850 let mut e = Editor::new(
9851 hjkl_buffer::Buffer::new(),
9852 crate::types::DefaultHost::new(),
9853 opts,
9854 );
9855 e.set_content(content);
9856 e
9857 }
9858
9859 #[test]
9860 fn smartindent_bumps_indent_after_open_brace() {
9861 let mut e = si_editor("fn foo() {");
9863 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9865 assert_eq!(
9866 e.buffer().lines()[1],
9867 " ",
9868 "smartindent should bump one shiftwidth after {{"
9869 );
9870 }
9871
9872 #[test]
9873 fn smartindent_no_bump_when_off() {
9874 let mut e = si_editor("fn foo() {");
9877 e.settings_mut().smartindent = false;
9878 e.jump_cursor(0, 10);
9879 run_keys(&mut e, "i<CR>");
9880 assert_eq!(
9881 e.buffer().lines()[1],
9882 "",
9883 "without smartindent, no bump: new line copies empty leading ws"
9884 );
9885 }
9886
9887 #[test]
9888 fn smartindent_uses_tab_when_noexpandtab() {
9889 let opts = crate::types::Options {
9891 shiftwidth: 4,
9892 softtabstop: 0,
9893 expandtab: false,
9894 smartindent: true,
9895 autoindent: true,
9896 ..crate::types::Options::default()
9897 };
9898 let mut e = Editor::new(
9899 hjkl_buffer::Buffer::new(),
9900 crate::types::DefaultHost::new(),
9901 opts,
9902 );
9903 e.set_content("fn foo() {");
9904 e.jump_cursor(0, 10);
9905 run_keys(&mut e, "i<CR>");
9906 assert_eq!(
9907 e.buffer().lines()[1],
9908 "\t",
9909 "noexpandtab: smartindent bump inserts a literal tab"
9910 );
9911 }
9912
9913 #[test]
9914 fn smartindent_dedent_on_close_brace() {
9915 let mut e = si_editor("fn foo() {");
9918 e.set_content("fn foo() {\n ");
9920 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9922 assert_eq!(
9923 e.buffer().lines()[1],
9924 "}",
9925 "close brace on whitespace-only line should dedent"
9926 );
9927 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9928 }
9929
9930 #[test]
9931 fn smartindent_no_dedent_when_off() {
9932 let mut e = si_editor("fn foo() {\n ");
9934 e.settings_mut().smartindent = false;
9935 e.jump_cursor(1, 4);
9936 run_keys(&mut e, "i}");
9937 assert_eq!(
9938 e.buffer().lines()[1],
9939 " }",
9940 "without smartindent, `}}` just appends at cursor"
9941 );
9942 }
9943
9944 #[test]
9945 fn smartindent_no_dedent_mid_line() {
9946 let mut e = si_editor(" let x = 1");
9949 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9951 assert_eq!(
9952 e.buffer().lines()[0],
9953 " let x = 1}",
9954 "mid-line `}}` should not dedent"
9955 );
9956 }
9957
9958 #[test]
9962 fn count_5x_fills_unnamed_register() {
9963 let mut e = editor_with("hello world\n");
9964 e.jump_cursor(0, 0);
9965 run_keys(&mut e, "5x");
9966 assert_eq!(e.buffer().lines()[0], " world");
9967 assert_eq!(e.cursor(), (0, 0));
9968 assert_eq!(e.yank(), "hello");
9969 }
9970
9971 #[test]
9972 fn x_fills_unnamed_register_single_char() {
9973 let mut e = editor_with("abc\n");
9974 e.jump_cursor(0, 0);
9975 run_keys(&mut e, "x");
9976 assert_eq!(e.buffer().lines()[0], "bc");
9977 assert_eq!(e.yank(), "a");
9978 }
9979
9980 #[test]
9981 fn big_x_fills_unnamed_register() {
9982 let mut e = editor_with("hello\n");
9983 e.jump_cursor(0, 3);
9984 run_keys(&mut e, "X");
9985 assert_eq!(e.buffer().lines()[0], "helo");
9986 assert_eq!(e.yank(), "l");
9987 }
9988
9989 #[test]
9991 fn g_motion_trailing_newline_lands_on_last_content_row() {
9992 let mut e = editor_with("foo\nbar\nbaz\n");
9993 e.jump_cursor(0, 0);
9994 run_keys(&mut e, "G");
9995 assert_eq!(
9997 e.cursor().0,
9998 2,
9999 "G should land on row 2 (baz), not row 3 (phantom empty)"
10000 );
10001 }
10002
10003 #[test]
10005 fn dd_last_line_clamps_cursor_to_new_last_row() {
10006 let mut e = editor_with("foo\nbar\n");
10007 e.jump_cursor(1, 0);
10008 run_keys(&mut e, "dd");
10009 assert_eq!(e.buffer().lines()[0], "foo");
10010 assert_eq!(
10011 e.cursor(),
10012 (0, 0),
10013 "cursor should clamp to row 0 after dd on last content line"
10014 );
10015 }
10016
10017 #[test]
10019 fn d_dollar_cursor_on_last_char() {
10020 let mut e = editor_with("hello world\n");
10021 e.jump_cursor(0, 5);
10022 run_keys(&mut e, "d$");
10023 assert_eq!(e.buffer().lines()[0], "hello");
10024 assert_eq!(
10025 e.cursor(),
10026 (0, 4),
10027 "d$ should leave cursor on col 4, not col 5"
10028 );
10029 }
10030
10031 #[test]
10033 fn undo_insert_clamps_cursor_to_last_valid_col() {
10034 let mut e = editor_with("hello\n");
10035 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10037 assert_eq!(e.buffer().lines()[0], "hello");
10038 assert_eq!(
10039 e.cursor(),
10040 (0, 4),
10041 "undo should clamp cursor to col 4 on 'hello'"
10042 );
10043 }
10044
10045 #[test]
10047 fn da_doublequote_eats_trailing_whitespace() {
10048 let mut e = editor_with("say \"hello\" there\n");
10049 e.jump_cursor(0, 6);
10050 run_keys(&mut e, "da\"");
10051 assert_eq!(e.buffer().lines()[0], "say there");
10052 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10053 }
10054
10055 #[test]
10057 fn dab_cursor_col_clamped_after_delete() {
10058 let mut e = editor_with("fn x() {\n body\n}\n");
10059 e.jump_cursor(1, 4);
10060 run_keys(&mut e, "daB");
10061 assert_eq!(e.buffer().lines()[0], "fn x() ");
10062 assert_eq!(
10063 e.cursor(),
10064 (0, 6),
10065 "daB should leave cursor at col 6, not 7"
10066 );
10067 }
10068
10069 #[test]
10071 fn dib_preserves_surrounding_newlines() {
10072 let mut e = editor_with("{\n body\n}\n");
10073 e.jump_cursor(1, 4);
10074 run_keys(&mut e, "diB");
10075 assert_eq!(e.buffer().lines()[0], "{");
10076 assert_eq!(e.buffer().lines()[1], "}");
10077 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10078 }
10079
10080 #[test]
10081 fn is_chord_pending_tracks_replace_state() {
10082 let mut e = editor_with("abc\n");
10083 assert!(!e.is_chord_pending());
10084 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10086 assert!(e.is_chord_pending(), "engine should be pending after r");
10087 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10089 assert!(
10090 !e.is_chord_pending(),
10091 "engine pending should clear after replace"
10092 );
10093 }
10094
10095 #[test]
10098 fn yiw_sets_lbr_rbr_marks_around_word() {
10099 let mut e = editor_with("hello world");
10102 run_keys(&mut e, "yiw");
10103 let lo = e.mark('[').expect("'[' must be set after yiw");
10104 let hi = e.mark(']').expect("']' must be set after yiw");
10105 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10106 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10107 }
10108
10109 #[test]
10110 fn yj_linewise_sets_marks_at_line_edges() {
10111 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10114 run_keys(&mut e, "yj");
10115 let lo = e.mark('[').expect("'[' must be set after yj");
10116 let hi = e.mark(']').expect("']' must be set after yj");
10117 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10118 assert_eq!(
10119 hi,
10120 (1, 4),
10121 "'] snaps to (bot_row, last_col) for linewise yank"
10122 );
10123 }
10124
10125 #[test]
10126 fn dd_sets_lbr_rbr_marks_to_cursor() {
10127 let mut e = editor_with("aaa\nbbb");
10130 run_keys(&mut e, "dd");
10131 let lo = e.mark('[').expect("'[' must be set after dd");
10132 let hi = e.mark(']').expect("']' must be set after dd");
10133 assert_eq!(lo, hi, "after delete both marks are at the same position");
10134 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10135 }
10136
10137 #[test]
10138 fn dw_sets_lbr_rbr_marks_to_cursor() {
10139 let mut e = editor_with("hello world");
10142 run_keys(&mut e, "dw");
10143 let lo = e.mark('[').expect("'[' must be set after dw");
10144 let hi = e.mark(']').expect("']' must be set after dw");
10145 assert_eq!(lo, hi, "after delete both marks are at the same position");
10146 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10147 }
10148
10149 #[test]
10150 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10151 let mut e = editor_with("hello world");
10156 run_keys(&mut e, "cwfoo<Esc>");
10157 let lo = e.mark('[').expect("'[' must be set after cw");
10158 let hi = e.mark(']').expect("']' must be set after cw");
10159 assert_eq!(lo, (0, 0), "'[ should be start of change");
10160 assert_eq!(hi.0, 0, "'] should be on row 0");
10163 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10164 }
10165
10166 #[test]
10167 fn cw_with_no_insertion_sets_marks_at_change_start() {
10168 let mut e = editor_with("hello world");
10171 run_keys(&mut e, "cw<Esc>");
10172 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10173 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10174 assert_eq!(lo.0, 0, "'[ should be on row 0");
10175 assert_eq!(hi.0, 0, "'] should be on row 0");
10176 assert_eq!(lo, hi, "marks coincide when insert is empty");
10178 }
10179
10180 #[test]
10181 fn p_charwise_sets_marks_around_pasted_text() {
10182 let mut e = editor_with("abc xyz");
10185 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10188 let hi = e.mark(']').expect("']' set after charwise paste");
10189 assert!(lo <= hi, "'[ must not exceed ']'");
10190 assert_eq!(
10192 hi.1.wrapping_sub(lo.1),
10193 2,
10194 "'] - '[ should span 2 cols for a 3-char paste"
10195 );
10196 }
10197
10198 #[test]
10199 fn p_linewise_sets_marks_at_line_edges() {
10200 let mut e = editor_with("aaa\nbbb\nccc");
10203 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10207 let hi = e.mark(']').expect("']' set after linewise paste");
10208 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10209 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10210 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10211 }
10212
10213 #[test]
10214 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10215 let mut e = editor_with("hello world");
10219 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10223 assert_eq!(
10225 e.cursor(),
10226 (0, 4),
10227 "visual `[v`] should land on last yanked char"
10228 );
10229 assert_eq!(
10231 e.vim_mode(),
10232 crate::VimMode::Visual,
10233 "should be in Visual mode"
10234 );
10235 }
10236
10237 #[test]
10243 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10244 let mut e = editor_with("hello\nworld\n");
10247 e.jump_cursor(0, 0);
10248 run_keys(&mut e, "iX<Esc>j`.");
10249 assert_eq!(
10250 e.cursor(),
10251 (0, 0),
10252 "dot mark should jump to the change-start (col 0), not post-insert col"
10253 );
10254 }
10255
10256 #[test]
10259 fn count_100g_clamps_to_last_content_row() {
10260 let mut e = editor_with("foo\nbar\nbaz\n");
10263 e.jump_cursor(0, 0);
10264 run_keys(&mut e, "100G");
10265 assert_eq!(
10266 e.cursor(),
10267 (2, 0),
10268 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10269 );
10270 }
10271
10272 #[test]
10275 fn gi_resumes_last_insert_position() {
10276 let mut e = editor_with("world\nhello\n");
10282 e.jump_cursor(0, 0);
10283 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10284 assert_eq!(
10285 e.vim_mode(),
10286 crate::VimMode::Normal,
10287 "should be in Normal mode after gi<Esc>"
10288 );
10289 assert_eq!(
10290 e.cursor(),
10291 (0, 1),
10292 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10293 );
10294 }
10295
10296 #[test]
10300 fn visual_block_change_cursor_on_last_inserted_char() {
10301 let mut e = editor_with("foo\nbar\nbaz\n");
10305 e.jump_cursor(0, 0);
10306 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10307 let lines = e.buffer().lines().to_vec();
10308 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10309 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10310 assert_eq!(
10311 e.cursor(),
10312 (0, 1),
10313 "cursor should be on last char of inserted 'ZZ' (col 1)"
10314 );
10315 }
10316
10317 #[test]
10322 fn register_blackhole_delete_preserves_unnamed_register() {
10323 let mut e = editor_with("foo bar baz\n");
10330 e.jump_cursor(0, 0);
10331 run_keys(&mut e, "yiww\"_dwbp");
10332 let lines = e.buffer().lines().to_vec();
10333 assert_eq!(
10334 lines[0], "ffoooo baz",
10335 "black-hole delete must not corrupt unnamed register"
10336 );
10337 assert_eq!(
10338 e.cursor(),
10339 (0, 3),
10340 "cursor should be on last pasted char (col 3)"
10341 );
10342 }
10343}