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
2475pub(crate) fn execute_motion<H: crate::types::Host>(
2478 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2479 motion: Motion,
2480 count: usize,
2481) {
2482 let count = count.max(1);
2483 let motion = match motion {
2485 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2486 Some((ch, forward, till)) => Motion::Find {
2487 ch,
2488 forward: if reverse { !forward } else { forward },
2489 till,
2490 },
2491 None => return,
2492 },
2493 other => other,
2494 };
2495 let pre_pos = ed.cursor();
2496 let pre_col = pre_pos.1;
2497 apply_motion_cursor(ed, &motion, count);
2498 let post_pos = ed.cursor();
2499 if is_big_jump(&motion) && pre_pos != post_pos {
2500 push_jump(ed, pre_pos);
2501 }
2502 apply_sticky_col(ed, &motion, pre_col);
2503 ed.sync_buffer_from_textarea();
2508}
2509
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 if let Key::Char(ch) = input.key {
3050 apply_after_g(ed, ch, count);
3051 }
3052 true
3053}
3054
3055pub(crate) fn apply_after_g<H: crate::types::Host>(
3060 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3061 ch: char,
3062 count: usize,
3063) {
3064 match ch {
3065 'g' => {
3066 let pre = ed.cursor();
3068 if count > 1 {
3069 ed.jump_cursor(count - 1, 0);
3070 } else {
3071 ed.jump_cursor(0, 0);
3072 }
3073 move_first_non_whitespace(ed);
3074 if ed.cursor() != pre {
3075 push_jump(ed, pre);
3076 }
3077 }
3078 'e' => execute_motion(ed, Motion::WordEndBack, count),
3079 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3080 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3082 'M' => execute_motion(ed, Motion::LineMiddle, count),
3084 'v' => {
3086 if let Some(snap) = ed.vim.last_visual {
3087 match snap.mode {
3088 Mode::Visual => {
3089 ed.vim.visual_anchor = snap.anchor;
3090 ed.vim.mode = Mode::Visual;
3091 }
3092 Mode::VisualLine => {
3093 ed.vim.visual_line_anchor = snap.anchor.0;
3094 ed.vim.mode = Mode::VisualLine;
3095 }
3096 Mode::VisualBlock => {
3097 ed.vim.block_anchor = snap.anchor;
3098 ed.vim.block_vcol = snap.block_vcol;
3099 ed.vim.mode = Mode::VisualBlock;
3100 }
3101 _ => {}
3102 }
3103 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3104 }
3105 }
3106 'j' => execute_motion(ed, Motion::ScreenDown, count),
3110 'k' => execute_motion(ed, Motion::ScreenUp, count),
3111 'U' => {
3115 ed.vim.pending = Pending::Op {
3116 op: Operator::Uppercase,
3117 count1: count,
3118 };
3119 }
3120 'u' => {
3121 ed.vim.pending = Pending::Op {
3122 op: Operator::Lowercase,
3123 count1: count,
3124 };
3125 }
3126 '~' => {
3127 ed.vim.pending = Pending::Op {
3128 op: Operator::ToggleCase,
3129 count1: count,
3130 };
3131 }
3132 'q' => {
3133 ed.vim.pending = Pending::Op {
3136 op: Operator::Reflow,
3137 count1: count,
3138 };
3139 }
3140 'J' => {
3141 for _ in 0..count.max(1) {
3143 ed.push_undo();
3144 join_line_raw(ed);
3145 }
3146 if !ed.vim.replaying {
3147 ed.vim.last_change = Some(LastChange::JoinLine {
3148 count: count.max(1),
3149 });
3150 }
3151 }
3152 'd' => {
3153 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3158 }
3159 'i' => {
3164 if let Some((row, col)) = ed.vim.last_insert_pos {
3165 ed.jump_cursor(row, col);
3166 }
3167 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3168 }
3169 ';' => walk_change_list(ed, -1, count.max(1)),
3172 ',' => walk_change_list(ed, 1, count.max(1)),
3173 '*' => execute_motion(
3177 ed,
3178 Motion::WordAtCursor {
3179 forward: true,
3180 whole_word: false,
3181 },
3182 count,
3183 ),
3184 '#' => execute_motion(
3185 ed,
3186 Motion::WordAtCursor {
3187 forward: false,
3188 whole_word: false,
3189 },
3190 count,
3191 ),
3192 _ => {}
3193 }
3194}
3195
3196fn handle_after_z<H: crate::types::Host>(
3197 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3198 input: Input,
3199) -> bool {
3200 use crate::editor::CursorScrollTarget;
3201 let row = ed.cursor().0;
3202 match input.key {
3203 Key::Char('z') => {
3204 ed.scroll_cursor_to(CursorScrollTarget::Center);
3205 ed.vim.viewport_pinned = true;
3206 }
3207 Key::Char('t') => {
3208 ed.scroll_cursor_to(CursorScrollTarget::Top);
3209 ed.vim.viewport_pinned = true;
3210 }
3211 Key::Char('b') => {
3212 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3213 ed.vim.viewport_pinned = true;
3214 }
3215 Key::Char('o') => {
3220 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3221 }
3222 Key::Char('c') => {
3223 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3224 }
3225 Key::Char('a') => {
3226 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3227 }
3228 Key::Char('R') => {
3229 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3230 }
3231 Key::Char('M') => {
3232 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3233 }
3234 Key::Char('E') => {
3235 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3236 }
3237 Key::Char('d') => {
3238 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3239 }
3240 Key::Char('f') => {
3241 if matches!(
3242 ed.vim.mode,
3243 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3244 ) {
3245 let anchor_row = match ed.vim.mode {
3248 Mode::VisualLine => ed.vim.visual_line_anchor,
3249 Mode::VisualBlock => ed.vim.block_anchor.0,
3250 _ => ed.vim.visual_anchor.0,
3251 };
3252 let cur = ed.cursor().0;
3253 let top = anchor_row.min(cur);
3254 let bot = anchor_row.max(cur);
3255 ed.apply_fold_op(crate::types::FoldOp::Add {
3256 start_row: top,
3257 end_row: bot,
3258 closed: true,
3259 });
3260 ed.vim.mode = Mode::Normal;
3261 } else {
3262 let count = take_count(&mut ed.vim);
3267 ed.vim.pending = Pending::Op {
3268 op: Operator::Fold,
3269 count1: count,
3270 };
3271 }
3272 }
3273 _ => {}
3274 }
3275 true
3276}
3277
3278fn handle_replace<H: crate::types::Host>(
3279 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3280 input: Input,
3281) -> bool {
3282 if let Key::Char(ch) = input.key {
3283 if ed.vim.mode == Mode::VisualBlock {
3284 block_replace(ed, ch);
3285 return true;
3286 }
3287 let count = take_count(&mut ed.vim);
3288 replace_char(ed, ch, count.max(1));
3289 if !ed.vim.replaying {
3290 ed.vim.last_change = Some(LastChange::ReplaceChar {
3291 ch,
3292 count: count.max(1),
3293 });
3294 }
3295 }
3296 true
3297}
3298
3299fn handle_find_target<H: crate::types::Host>(
3300 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3301 input: Input,
3302 forward: bool,
3303 till: bool,
3304) -> bool {
3305 let Key::Char(ch) = input.key else {
3306 return true;
3307 };
3308 let count = take_count(&mut ed.vim);
3309 apply_find_char(ed, ch, forward, till, count.max(1));
3310 true
3311}
3312
3313pub(crate) fn apply_find_char<H: crate::types::Host>(
3319 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3320 ch: char,
3321 forward: bool,
3322 till: bool,
3323 count: usize,
3324) {
3325 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3326 ed.vim.last_find = Some((ch, forward, till));
3327}
3328
3329fn handle_op_find_target<H: crate::types::Host>(
3330 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3331 input: Input,
3332 op: Operator,
3333 count1: usize,
3334 forward: bool,
3335 till: bool,
3336) -> bool {
3337 let Key::Char(ch) = input.key else {
3338 return true;
3339 };
3340 let count2 = take_count(&mut ed.vim);
3341 let total = count1.max(1) * count2.max(1);
3342 let motion = Motion::Find { ch, forward, till };
3343 apply_op_with_motion(ed, op, &motion, total);
3344 ed.vim.last_find = Some((ch, forward, till));
3345 if !ed.vim.replaying && op_is_change(op) {
3346 ed.vim.last_change = Some(LastChange::OpMotion {
3347 op,
3348 motion,
3349 count: total,
3350 inserted: None,
3351 });
3352 }
3353 true
3354}
3355
3356fn handle_text_object<H: crate::types::Host>(
3357 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3358 input: Input,
3359 op: Operator,
3360 _count1: usize,
3361 inner: bool,
3362) -> bool {
3363 let Key::Char(ch) = input.key else {
3364 return true;
3365 };
3366 let obj = match ch {
3367 'w' => TextObject::Word { big: false },
3368 'W' => TextObject::Word { big: true },
3369 '"' | '\'' | '`' => TextObject::Quote(ch),
3370 '(' | ')' | 'b' => TextObject::Bracket('('),
3371 '[' | ']' => TextObject::Bracket('['),
3372 '{' | '}' | 'B' => TextObject::Bracket('{'),
3373 '<' | '>' => TextObject::Bracket('<'),
3374 'p' => TextObject::Paragraph,
3375 't' => TextObject::XmlTag,
3376 's' => TextObject::Sentence,
3377 _ => return true,
3378 };
3379 apply_op_with_text_object(ed, op, obj, inner);
3380 if !ed.vim.replaying && op_is_change(op) {
3381 ed.vim.last_change = Some(LastChange::OpTextObj {
3382 op,
3383 obj,
3384 inner,
3385 inserted: None,
3386 });
3387 }
3388 true
3389}
3390
3391fn handle_visual_text_obj<H: crate::types::Host>(
3392 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3393 input: Input,
3394 inner: bool,
3395) -> bool {
3396 let Key::Char(ch) = input.key else {
3397 return true;
3398 };
3399 let obj = match ch {
3400 'w' => TextObject::Word { big: false },
3401 'W' => TextObject::Word { big: true },
3402 '"' | '\'' | '`' => TextObject::Quote(ch),
3403 '(' | ')' | 'b' => TextObject::Bracket('('),
3404 '[' | ']' => TextObject::Bracket('['),
3405 '{' | '}' | 'B' => TextObject::Bracket('{'),
3406 '<' | '>' => TextObject::Bracket('<'),
3407 'p' => TextObject::Paragraph,
3408 't' => TextObject::XmlTag,
3409 's' => TextObject::Sentence,
3410 _ => return true,
3411 };
3412 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3413 return true;
3414 };
3415 match kind {
3419 MotionKind::Linewise => {
3420 ed.vim.visual_line_anchor = start.0;
3421 ed.vim.mode = Mode::VisualLine;
3422 ed.jump_cursor(end.0, 0);
3423 }
3424 _ => {
3425 ed.vim.mode = Mode::Visual;
3426 ed.vim.visual_anchor = (start.0, start.1);
3427 let (er, ec) = retreat_one(ed, end);
3428 ed.jump_cursor(er, ec);
3429 }
3430 }
3431 true
3432}
3433
3434fn retreat_one<H: crate::types::Host>(
3436 ed: &Editor<hjkl_buffer::Buffer, H>,
3437 pos: (usize, usize),
3438) -> (usize, usize) {
3439 let (r, c) = pos;
3440 if c > 0 {
3441 (r, c - 1)
3442 } else if r > 0 {
3443 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3444 (r - 1, prev_len)
3445 } else {
3446 (0, 0)
3447 }
3448}
3449
3450fn op_is_change(op: Operator) -> bool {
3451 matches!(op, Operator::Delete | Operator::Change)
3452}
3453
3454fn handle_normal_only<H: crate::types::Host>(
3457 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3458 input: &Input,
3459 count: usize,
3460) -> bool {
3461 if input.ctrl {
3462 return false;
3463 }
3464 match input.key {
3465 Key::Char('i') => {
3466 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3467 true
3468 }
3469 Key::Char('I') => {
3470 move_first_non_whitespace(ed);
3471 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3472 true
3473 }
3474 Key::Char('a') => {
3475 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3476 ed.push_buffer_cursor_to_textarea();
3477 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3478 true
3479 }
3480 Key::Char('A') => {
3481 crate::motions::move_line_end(&mut ed.buffer);
3482 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3483 ed.push_buffer_cursor_to_textarea();
3484 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3485 true
3486 }
3487 Key::Char('R') => {
3488 begin_insert(ed, count.max(1), InsertReason::Replace);
3491 true
3492 }
3493 Key::Char('o') => {
3494 use hjkl_buffer::{Edit, Position};
3495 ed.push_undo();
3496 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3499 ed.sync_buffer_content_from_textarea();
3500 let row = buf_cursor_pos(&ed.buffer).row;
3501 let line_chars = buf_line_chars(&ed.buffer, row);
3502 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3505 let indent = compute_enter_indent(&ed.settings, prev_line);
3506 ed.mutate_edit(Edit::InsertStr {
3507 at: Position::new(row, line_chars),
3508 text: format!("\n{indent}"),
3509 });
3510 ed.push_buffer_cursor_to_textarea();
3511 true
3512 }
3513 Key::Char('O') => {
3514 use hjkl_buffer::{Edit, Position};
3515 ed.push_undo();
3516 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3517 ed.sync_buffer_content_from_textarea();
3518 let row = buf_cursor_pos(&ed.buffer).row;
3519 let indent = if row > 0 {
3523 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3524 compute_enter_indent(&ed.settings, above)
3525 } else {
3526 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3527 cur.chars()
3528 .take_while(|c| *c == ' ' || *c == '\t')
3529 .collect::<String>()
3530 };
3531 ed.mutate_edit(Edit::InsertStr {
3532 at: Position::new(row, 0),
3533 text: format!("{indent}\n"),
3534 });
3535 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3540 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3541 let new_row = buf_cursor_pos(&ed.buffer).row;
3542 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3543 ed.push_buffer_cursor_to_textarea();
3544 true
3545 }
3546 Key::Char('x') => {
3547 do_char_delete(ed, true, count.max(1));
3548 if !ed.vim.replaying {
3549 ed.vim.last_change = Some(LastChange::CharDel {
3550 forward: true,
3551 count: count.max(1),
3552 });
3553 }
3554 true
3555 }
3556 Key::Char('X') => {
3557 do_char_delete(ed, false, count.max(1));
3558 if !ed.vim.replaying {
3559 ed.vim.last_change = Some(LastChange::CharDel {
3560 forward: false,
3561 count: count.max(1),
3562 });
3563 }
3564 true
3565 }
3566 Key::Char('~') => {
3567 for _ in 0..count.max(1) {
3568 ed.push_undo();
3569 toggle_case_at_cursor(ed);
3570 }
3571 if !ed.vim.replaying {
3572 ed.vim.last_change = Some(LastChange::ToggleCase {
3573 count: count.max(1),
3574 });
3575 }
3576 true
3577 }
3578 Key::Char('J') => {
3579 for _ in 0..count.max(1) {
3580 ed.push_undo();
3581 join_line(ed);
3582 }
3583 if !ed.vim.replaying {
3584 ed.vim.last_change = Some(LastChange::JoinLine {
3585 count: count.max(1),
3586 });
3587 }
3588 true
3589 }
3590 Key::Char('D') => {
3591 ed.push_undo();
3592 delete_to_eol(ed);
3593 crate::motions::move_left(&mut ed.buffer, 1);
3595 ed.push_buffer_cursor_to_textarea();
3596 if !ed.vim.replaying {
3597 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3598 }
3599 true
3600 }
3601 Key::Char('Y') => {
3602 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3604 true
3605 }
3606 Key::Char('C') => {
3607 ed.push_undo();
3608 delete_to_eol(ed);
3609 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3610 true
3611 }
3612 Key::Char('s') => {
3613 use hjkl_buffer::{Edit, MotionKind, Position};
3614 ed.push_undo();
3615 ed.sync_buffer_content_from_textarea();
3616 for _ in 0..count.max(1) {
3617 let cursor = buf_cursor_pos(&ed.buffer);
3618 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3619 if cursor.col >= line_chars {
3620 break;
3621 }
3622 ed.mutate_edit(Edit::DeleteRange {
3623 start: cursor,
3624 end: Position::new(cursor.row, cursor.col + 1),
3625 kind: MotionKind::Char,
3626 });
3627 }
3628 ed.push_buffer_cursor_to_textarea();
3629 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3630 if !ed.vim.replaying {
3632 ed.vim.last_change = Some(LastChange::OpMotion {
3633 op: Operator::Change,
3634 motion: Motion::Right,
3635 count: count.max(1),
3636 inserted: None,
3637 });
3638 }
3639 true
3640 }
3641 Key::Char('p') => {
3642 do_paste(ed, false, count.max(1));
3643 if !ed.vim.replaying {
3644 ed.vim.last_change = Some(LastChange::Paste {
3645 before: false,
3646 count: count.max(1),
3647 });
3648 }
3649 true
3650 }
3651 Key::Char('P') => {
3652 do_paste(ed, true, count.max(1));
3653 if !ed.vim.replaying {
3654 ed.vim.last_change = Some(LastChange::Paste {
3655 before: true,
3656 count: count.max(1),
3657 });
3658 }
3659 true
3660 }
3661 Key::Char('u') => {
3662 do_undo(ed);
3663 true
3664 }
3665 Key::Char('r') => {
3666 ed.vim.count = count;
3667 ed.vim.pending = Pending::Replace;
3668 true
3669 }
3670 Key::Char('/') => {
3671 enter_search(ed, true);
3672 true
3673 }
3674 Key::Char('?') => {
3675 enter_search(ed, false);
3676 true
3677 }
3678 Key::Char('.') => {
3679 replay_last_change(ed, count);
3680 true
3681 }
3682 _ => false,
3683 }
3684}
3685
3686fn begin_insert_noundo<H: crate::types::Host>(
3688 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3689 count: usize,
3690 reason: InsertReason,
3691) {
3692 let reason = if ed.vim.replaying {
3693 InsertReason::ReplayOnly
3694 } else {
3695 reason
3696 };
3697 let (row, _) = ed.cursor();
3698 ed.vim.insert_session = Some(InsertSession {
3699 count,
3700 row_min: row,
3701 row_max: row,
3702 before_lines: buf_lines_to_vec(&ed.buffer),
3703 reason,
3704 });
3705 ed.vim.mode = Mode::Insert;
3706}
3707
3708fn apply_op_with_motion<H: crate::types::Host>(
3711 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3712 op: Operator,
3713 motion: &Motion,
3714 count: usize,
3715) {
3716 let start = ed.cursor();
3717 apply_motion_cursor_ctx(ed, motion, count, true);
3722 let end = ed.cursor();
3723 let kind = motion_kind(motion);
3724 ed.jump_cursor(start.0, start.1);
3726 run_operator_over_range(ed, op, start, end, kind);
3727}
3728
3729fn apply_op_with_text_object<H: crate::types::Host>(
3730 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3731 op: Operator,
3732 obj: TextObject,
3733 inner: bool,
3734) {
3735 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3736 return;
3737 };
3738 ed.jump_cursor(start.0, start.1);
3739 run_operator_over_range(ed, op, start, end, kind);
3740}
3741
3742fn motion_kind(motion: &Motion) -> MotionKind {
3743 match motion {
3744 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3745 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3746 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3747 MotionKind::Linewise
3748 }
3749 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3750 MotionKind::Inclusive
3751 }
3752 Motion::Find { .. } => MotionKind::Inclusive,
3753 Motion::MatchBracket => MotionKind::Inclusive,
3754 Motion::LineEnd => MotionKind::Inclusive,
3756 _ => MotionKind::Exclusive,
3757 }
3758}
3759
3760fn run_operator_over_range<H: crate::types::Host>(
3761 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3762 op: Operator,
3763 start: (usize, usize),
3764 end: (usize, usize),
3765 kind: MotionKind,
3766) {
3767 let (top, bot) = order(start, end);
3768 if top == bot {
3769 return;
3770 }
3771
3772 match op {
3773 Operator::Yank => {
3774 let text = read_vim_range(ed, top, bot, kind);
3775 if !text.is_empty() {
3776 ed.record_yank_to_host(text.clone());
3777 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3778 }
3779 let rbr = match kind {
3783 MotionKind::Linewise => {
3784 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3785 (bot.0, last_col)
3786 }
3787 MotionKind::Inclusive => (bot.0, bot.1),
3788 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3789 };
3790 ed.set_mark('[', top);
3791 ed.set_mark(']', rbr);
3792 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3793 ed.push_buffer_cursor_to_textarea();
3794 }
3795 Operator::Delete => {
3796 ed.push_undo();
3797 cut_vim_range(ed, top, bot, kind);
3798 if !matches!(kind, MotionKind::Linewise) {
3803 clamp_cursor_to_normal_mode(ed);
3804 }
3805 ed.vim.mode = Mode::Normal;
3806 let pos = ed.cursor();
3810 ed.set_mark('[', pos);
3811 ed.set_mark(']', pos);
3812 }
3813 Operator::Change => {
3814 ed.vim.change_mark_start = Some(top);
3819 ed.push_undo();
3820 cut_vim_range(ed, top, bot, kind);
3821 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3822 }
3823 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3824 apply_case_op_to_selection(ed, op, top, bot, kind);
3825 }
3826 Operator::Indent | Operator::Outdent => {
3827 ed.push_undo();
3830 if op == Operator::Indent {
3831 indent_rows(ed, top.0, bot.0, 1);
3832 } else {
3833 outdent_rows(ed, top.0, bot.0, 1);
3834 }
3835 ed.vim.mode = Mode::Normal;
3836 }
3837 Operator::Fold => {
3838 if bot.0 >= top.0 {
3842 ed.apply_fold_op(crate::types::FoldOp::Add {
3843 start_row: top.0,
3844 end_row: bot.0,
3845 closed: true,
3846 });
3847 }
3848 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3849 ed.push_buffer_cursor_to_textarea();
3850 ed.vim.mode = Mode::Normal;
3851 }
3852 Operator::Reflow => {
3853 ed.push_undo();
3854 reflow_rows(ed, top.0, bot.0);
3855 ed.vim.mode = Mode::Normal;
3856 }
3857 }
3858}
3859
3860fn reflow_rows<H: crate::types::Host>(
3865 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3866 top: usize,
3867 bot: usize,
3868) {
3869 let width = ed.settings().textwidth.max(1);
3870 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3871 let bot = bot.min(lines.len().saturating_sub(1));
3872 if top > bot {
3873 return;
3874 }
3875 let original = lines[top..=bot].to_vec();
3876 let mut wrapped: Vec<String> = Vec::new();
3877 let mut paragraph: Vec<String> = Vec::new();
3878 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3879 if para.is_empty() {
3880 return;
3881 }
3882 let words = para.join(" ");
3883 let mut current = String::new();
3884 for word in words.split_whitespace() {
3885 let extra = if current.is_empty() {
3886 word.chars().count()
3887 } else {
3888 current.chars().count() + 1 + word.chars().count()
3889 };
3890 if extra > width && !current.is_empty() {
3891 out.push(std::mem::take(&mut current));
3892 current.push_str(word);
3893 } else if current.is_empty() {
3894 current.push_str(word);
3895 } else {
3896 current.push(' ');
3897 current.push_str(word);
3898 }
3899 }
3900 if !current.is_empty() {
3901 out.push(current);
3902 }
3903 para.clear();
3904 };
3905 for line in &original {
3906 if line.trim().is_empty() {
3907 flush(&mut paragraph, &mut wrapped, width);
3908 wrapped.push(String::new());
3909 } else {
3910 paragraph.push(line.clone());
3911 }
3912 }
3913 flush(&mut paragraph, &mut wrapped, width);
3914
3915 let after: Vec<String> = lines.split_off(bot + 1);
3917 lines.truncate(top);
3918 lines.extend(wrapped);
3919 lines.extend(after);
3920 ed.restore(lines, (top, 0));
3921 ed.mark_content_dirty();
3922}
3923
3924fn apply_case_op_to_selection<H: crate::types::Host>(
3930 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3931 op: Operator,
3932 top: (usize, usize),
3933 bot: (usize, usize),
3934 kind: MotionKind,
3935) {
3936 use hjkl_buffer::Edit;
3937 ed.push_undo();
3938 let saved_yank = ed.yank().to_string();
3939 let saved_yank_linewise = ed.vim.yank_linewise;
3940 let selection = cut_vim_range(ed, top, bot, kind);
3941 let transformed = match op {
3942 Operator::Uppercase => selection.to_uppercase(),
3943 Operator::Lowercase => selection.to_lowercase(),
3944 Operator::ToggleCase => toggle_case_str(&selection),
3945 _ => unreachable!(),
3946 };
3947 if !transformed.is_empty() {
3948 let cursor = buf_cursor_pos(&ed.buffer);
3949 ed.mutate_edit(Edit::InsertStr {
3950 at: cursor,
3951 text: transformed,
3952 });
3953 }
3954 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3955 ed.push_buffer_cursor_to_textarea();
3956 ed.set_yank(saved_yank);
3957 ed.vim.yank_linewise = saved_yank_linewise;
3958 ed.vim.mode = Mode::Normal;
3959}
3960
3961fn indent_rows<H: crate::types::Host>(
3966 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3967 top: usize,
3968 bot: usize,
3969 count: usize,
3970) {
3971 ed.sync_buffer_content_from_textarea();
3972 let width = ed.settings().shiftwidth * count.max(1);
3973 let pad: String = " ".repeat(width);
3974 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3975 let bot = bot.min(lines.len().saturating_sub(1));
3976 for line in lines.iter_mut().take(bot + 1).skip(top) {
3977 if !line.is_empty() {
3978 line.insert_str(0, &pad);
3979 }
3980 }
3981 ed.restore(lines, (top, 0));
3984 move_first_non_whitespace(ed);
3985}
3986
3987fn outdent_rows<H: crate::types::Host>(
3991 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3992 top: usize,
3993 bot: usize,
3994 count: usize,
3995) {
3996 ed.sync_buffer_content_from_textarea();
3997 let width = ed.settings().shiftwidth * count.max(1);
3998 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3999 let bot = bot.min(lines.len().saturating_sub(1));
4000 for line in lines.iter_mut().take(bot + 1).skip(top) {
4001 let strip: usize = line
4002 .chars()
4003 .take(width)
4004 .take_while(|c| *c == ' ' || *c == '\t')
4005 .count();
4006 if strip > 0 {
4007 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4008 line.drain(..byte_len);
4009 }
4010 }
4011 ed.restore(lines, (top, 0));
4012 move_first_non_whitespace(ed);
4013}
4014
4015fn toggle_case_str(s: &str) -> String {
4016 s.chars()
4017 .map(|c| {
4018 if c.is_lowercase() {
4019 c.to_uppercase().next().unwrap_or(c)
4020 } else if c.is_uppercase() {
4021 c.to_lowercase().next().unwrap_or(c)
4022 } else {
4023 c
4024 }
4025 })
4026 .collect()
4027}
4028
4029fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4030 if a <= b { (a, b) } else { (b, a) }
4031}
4032
4033fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4038 let (row, col) = ed.cursor();
4039 let line_chars = buf_line_chars(&ed.buffer, row);
4040 let max_col = line_chars.saturating_sub(1);
4041 if col > max_col {
4042 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4043 ed.push_buffer_cursor_to_textarea();
4044 }
4045}
4046
4047fn execute_line_op<H: crate::types::Host>(
4050 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4051 op: Operator,
4052 count: usize,
4053) {
4054 let (row, col) = ed.cursor();
4055 let total = buf_row_count(&ed.buffer);
4056 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4057
4058 match op {
4059 Operator::Yank => {
4060 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4062 if !text.is_empty() {
4063 ed.record_yank_to_host(text.clone());
4064 ed.record_yank(text, true);
4065 }
4066 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4069 ed.set_mark('[', (row, 0));
4070 ed.set_mark(']', (end_row, last_col));
4071 buf_set_cursor_rc(&mut ed.buffer, row, col);
4072 ed.push_buffer_cursor_to_textarea();
4073 ed.vim.mode = Mode::Normal;
4074 }
4075 Operator::Delete => {
4076 ed.push_undo();
4077 let deleted_through_last = end_row + 1 >= total;
4078 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4079 let total_after = buf_row_count(&ed.buffer);
4083 let raw_target = if deleted_through_last {
4084 row.saturating_sub(1).min(total_after.saturating_sub(1))
4085 } else {
4086 row.min(total_after.saturating_sub(1))
4087 };
4088 let target_row = if raw_target > 0
4094 && raw_target + 1 == total_after
4095 && buf_line(&ed.buffer, raw_target)
4096 .map(str::is_empty)
4097 .unwrap_or(false)
4098 {
4099 raw_target - 1
4100 } else {
4101 raw_target
4102 };
4103 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4104 ed.push_buffer_cursor_to_textarea();
4105 move_first_non_whitespace(ed);
4106 ed.sticky_col = Some(ed.cursor().1);
4107 ed.vim.mode = Mode::Normal;
4108 let pos = ed.cursor();
4111 ed.set_mark('[', pos);
4112 ed.set_mark(']', pos);
4113 }
4114 Operator::Change => {
4115 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4119 ed.vim.change_mark_start = Some((row, 0));
4121 ed.push_undo();
4122 ed.sync_buffer_content_from_textarea();
4123 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4125 if end_row > row {
4126 ed.mutate_edit(Edit::DeleteRange {
4127 start: Position::new(row + 1, 0),
4128 end: Position::new(end_row, 0),
4129 kind: BufKind::Line,
4130 });
4131 }
4132 let line_chars = buf_line_chars(&ed.buffer, row);
4133 if line_chars > 0 {
4134 ed.mutate_edit(Edit::DeleteRange {
4135 start: Position::new(row, 0),
4136 end: Position::new(row, line_chars),
4137 kind: BufKind::Char,
4138 });
4139 }
4140 if !payload.is_empty() {
4141 ed.record_yank_to_host(payload.clone());
4142 ed.record_delete(payload, true);
4143 }
4144 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4145 ed.push_buffer_cursor_to_textarea();
4146 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4147 }
4148 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4149 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4153 move_first_non_whitespace(ed);
4156 }
4157 Operator::Indent | Operator::Outdent => {
4158 ed.push_undo();
4160 if op == Operator::Indent {
4161 indent_rows(ed, row, end_row, 1);
4162 } else {
4163 outdent_rows(ed, row, end_row, 1);
4164 }
4165 ed.sticky_col = Some(ed.cursor().1);
4166 ed.vim.mode = Mode::Normal;
4167 }
4168 Operator::Fold => unreachable!("Fold has no line-op double"),
4170 Operator::Reflow => {
4171 ed.push_undo();
4173 reflow_rows(ed, row, end_row);
4174 move_first_non_whitespace(ed);
4175 ed.sticky_col = Some(ed.cursor().1);
4176 ed.vim.mode = Mode::Normal;
4177 }
4178 }
4179}
4180
4181fn apply_visual_operator<H: crate::types::Host>(
4184 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4185 op: Operator,
4186) {
4187 match ed.vim.mode {
4188 Mode::VisualLine => {
4189 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4190 let top = cursor_row.min(ed.vim.visual_line_anchor);
4191 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4192 ed.vim.yank_linewise = true;
4193 match op {
4194 Operator::Yank => {
4195 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4196 if !text.is_empty() {
4197 ed.record_yank_to_host(text.clone());
4198 ed.record_yank(text, true);
4199 }
4200 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4201 ed.push_buffer_cursor_to_textarea();
4202 ed.vim.mode = Mode::Normal;
4203 }
4204 Operator::Delete => {
4205 ed.push_undo();
4206 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4207 ed.vim.mode = Mode::Normal;
4208 }
4209 Operator::Change => {
4210 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4213 ed.push_undo();
4214 ed.sync_buffer_content_from_textarea();
4215 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4216 if bot > top {
4217 ed.mutate_edit(Edit::DeleteRange {
4218 start: Position::new(top + 1, 0),
4219 end: Position::new(bot, 0),
4220 kind: BufKind::Line,
4221 });
4222 }
4223 let line_chars = buf_line_chars(&ed.buffer, top);
4224 if line_chars > 0 {
4225 ed.mutate_edit(Edit::DeleteRange {
4226 start: Position::new(top, 0),
4227 end: Position::new(top, line_chars),
4228 kind: BufKind::Char,
4229 });
4230 }
4231 if !payload.is_empty() {
4232 ed.record_yank_to_host(payload.clone());
4233 ed.record_delete(payload, true);
4234 }
4235 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4236 ed.push_buffer_cursor_to_textarea();
4237 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4238 }
4239 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4240 let bot = buf_cursor_pos(&ed.buffer)
4241 .row
4242 .max(ed.vim.visual_line_anchor);
4243 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4244 move_first_non_whitespace(ed);
4245 }
4246 Operator::Indent | Operator::Outdent => {
4247 ed.push_undo();
4248 let (cursor_row, _) = ed.cursor();
4249 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4250 if op == Operator::Indent {
4251 indent_rows(ed, top, bot, 1);
4252 } else {
4253 outdent_rows(ed, top, bot, 1);
4254 }
4255 ed.vim.mode = Mode::Normal;
4256 }
4257 Operator::Reflow => {
4258 ed.push_undo();
4259 let (cursor_row, _) = ed.cursor();
4260 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4261 reflow_rows(ed, top, bot);
4262 ed.vim.mode = Mode::Normal;
4263 }
4264 Operator::Fold => unreachable!("Visual zf takes its own path"),
4267 }
4268 }
4269 Mode::Visual => {
4270 ed.vim.yank_linewise = false;
4271 let anchor = ed.vim.visual_anchor;
4272 let cursor = ed.cursor();
4273 let (top, bot) = order(anchor, cursor);
4274 match op {
4275 Operator::Yank => {
4276 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4277 if !text.is_empty() {
4278 ed.record_yank_to_host(text.clone());
4279 ed.record_yank(text, false);
4280 }
4281 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4282 ed.push_buffer_cursor_to_textarea();
4283 ed.vim.mode = Mode::Normal;
4284 }
4285 Operator::Delete => {
4286 ed.push_undo();
4287 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4288 ed.vim.mode = Mode::Normal;
4289 }
4290 Operator::Change => {
4291 ed.push_undo();
4292 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4293 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4294 }
4295 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4296 let anchor = ed.vim.visual_anchor;
4298 let cursor = ed.cursor();
4299 let (top, bot) = order(anchor, cursor);
4300 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4301 }
4302 Operator::Indent | Operator::Outdent => {
4303 ed.push_undo();
4304 let anchor = ed.vim.visual_anchor;
4305 let cursor = ed.cursor();
4306 let (top, bot) = order(anchor, cursor);
4307 if op == Operator::Indent {
4308 indent_rows(ed, top.0, bot.0, 1);
4309 } else {
4310 outdent_rows(ed, top.0, bot.0, 1);
4311 }
4312 ed.vim.mode = Mode::Normal;
4313 }
4314 Operator::Reflow => {
4315 ed.push_undo();
4316 let anchor = ed.vim.visual_anchor;
4317 let cursor = ed.cursor();
4318 let (top, bot) = order(anchor, cursor);
4319 reflow_rows(ed, top.0, bot.0);
4320 ed.vim.mode = Mode::Normal;
4321 }
4322 Operator::Fold => unreachable!("Visual zf takes its own path"),
4323 }
4324 }
4325 Mode::VisualBlock => apply_block_operator(ed, op),
4326 _ => {}
4327 }
4328}
4329
4330fn block_bounds<H: crate::types::Host>(
4335 ed: &Editor<hjkl_buffer::Buffer, H>,
4336) -> (usize, usize, usize, usize) {
4337 let (ar, ac) = ed.vim.block_anchor;
4338 let (cr, _) = ed.cursor();
4339 let cc = ed.vim.block_vcol;
4340 let top = ar.min(cr);
4341 let bot = ar.max(cr);
4342 let left = ac.min(cc);
4343 let right = ac.max(cc);
4344 (top, bot, left, right)
4345}
4346
4347fn update_block_vcol<H: crate::types::Host>(
4352 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4353 motion: &Motion,
4354) {
4355 match motion {
4356 Motion::Left
4357 | Motion::Right
4358 | Motion::WordFwd
4359 | Motion::BigWordFwd
4360 | Motion::WordBack
4361 | Motion::BigWordBack
4362 | Motion::WordEnd
4363 | Motion::BigWordEnd
4364 | Motion::WordEndBack
4365 | Motion::BigWordEndBack
4366 | Motion::LineStart
4367 | Motion::FirstNonBlank
4368 | Motion::LineEnd
4369 | Motion::Find { .. }
4370 | Motion::FindRepeat { .. }
4371 | Motion::MatchBracket => {
4372 ed.vim.block_vcol = ed.cursor().1;
4373 }
4374 _ => {}
4376 }
4377}
4378
4379fn apply_block_operator<H: crate::types::Host>(
4384 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4385 op: Operator,
4386) {
4387 let (top, bot, left, right) = block_bounds(ed);
4388 let yank = block_yank(ed, top, bot, left, right);
4390
4391 match op {
4392 Operator::Yank => {
4393 if !yank.is_empty() {
4394 ed.record_yank_to_host(yank.clone());
4395 ed.record_yank(yank, false);
4396 }
4397 ed.vim.mode = Mode::Normal;
4398 ed.jump_cursor(top, left);
4399 }
4400 Operator::Delete => {
4401 ed.push_undo();
4402 delete_block_contents(ed, top, bot, left, right);
4403 if !yank.is_empty() {
4404 ed.record_yank_to_host(yank.clone());
4405 ed.record_delete(yank, false);
4406 }
4407 ed.vim.mode = Mode::Normal;
4408 ed.jump_cursor(top, left);
4409 }
4410 Operator::Change => {
4411 ed.push_undo();
4412 delete_block_contents(ed, top, bot, left, right);
4413 if !yank.is_empty() {
4414 ed.record_yank_to_host(yank.clone());
4415 ed.record_delete(yank, false);
4416 }
4417 ed.jump_cursor(top, left);
4418 begin_insert_noundo(
4419 ed,
4420 1,
4421 InsertReason::BlockChange {
4422 top,
4423 bot,
4424 col: left,
4425 },
4426 );
4427 }
4428 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4429 ed.push_undo();
4430 transform_block_case(ed, op, top, bot, left, right);
4431 ed.vim.mode = Mode::Normal;
4432 ed.jump_cursor(top, left);
4433 }
4434 Operator::Indent | Operator::Outdent => {
4435 ed.push_undo();
4439 if op == Operator::Indent {
4440 indent_rows(ed, top, bot, 1);
4441 } else {
4442 outdent_rows(ed, top, bot, 1);
4443 }
4444 ed.vim.mode = Mode::Normal;
4445 }
4446 Operator::Fold => unreachable!("Visual zf takes its own path"),
4447 Operator::Reflow => {
4448 ed.push_undo();
4452 reflow_rows(ed, top, bot);
4453 ed.vim.mode = Mode::Normal;
4454 }
4455 }
4456}
4457
4458fn transform_block_case<H: crate::types::Host>(
4462 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4463 op: Operator,
4464 top: usize,
4465 bot: usize,
4466 left: usize,
4467 right: usize,
4468) {
4469 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4470 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4471 let chars: Vec<char> = lines[r].chars().collect();
4472 if left >= chars.len() {
4473 continue;
4474 }
4475 let end = (right + 1).min(chars.len());
4476 let head: String = chars[..left].iter().collect();
4477 let mid: String = chars[left..end].iter().collect();
4478 let tail: String = chars[end..].iter().collect();
4479 let transformed = match op {
4480 Operator::Uppercase => mid.to_uppercase(),
4481 Operator::Lowercase => mid.to_lowercase(),
4482 Operator::ToggleCase => toggle_case_str(&mid),
4483 _ => mid,
4484 };
4485 lines[r] = format!("{head}{transformed}{tail}");
4486 }
4487 let saved_yank = ed.yank().to_string();
4488 let saved_linewise = ed.vim.yank_linewise;
4489 ed.restore(lines, (top, left));
4490 ed.set_yank(saved_yank);
4491 ed.vim.yank_linewise = saved_linewise;
4492}
4493
4494fn block_yank<H: crate::types::Host>(
4495 ed: &Editor<hjkl_buffer::Buffer, H>,
4496 top: usize,
4497 bot: usize,
4498 left: usize,
4499 right: usize,
4500) -> String {
4501 let lines = buf_lines_to_vec(&ed.buffer);
4502 let mut rows: Vec<String> = Vec::new();
4503 for r in top..=bot {
4504 let line = match lines.get(r) {
4505 Some(l) => l,
4506 None => break,
4507 };
4508 let chars: Vec<char> = line.chars().collect();
4509 let end = (right + 1).min(chars.len());
4510 if left >= chars.len() {
4511 rows.push(String::new());
4512 } else {
4513 rows.push(chars[left..end].iter().collect());
4514 }
4515 }
4516 rows.join("\n")
4517}
4518
4519fn delete_block_contents<H: crate::types::Host>(
4520 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4521 top: usize,
4522 bot: usize,
4523 left: usize,
4524 right: usize,
4525) {
4526 use hjkl_buffer::{Edit, MotionKind, Position};
4527 ed.sync_buffer_content_from_textarea();
4528 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4529 if last_row < top {
4530 return;
4531 }
4532 ed.mutate_edit(Edit::DeleteRange {
4533 start: Position::new(top, left),
4534 end: Position::new(last_row, right),
4535 kind: MotionKind::Block,
4536 });
4537 ed.push_buffer_cursor_to_textarea();
4538}
4539
4540fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4542 let (top, bot, left, right) = block_bounds(ed);
4543 ed.push_undo();
4544 ed.sync_buffer_content_from_textarea();
4545 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4546 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4547 let chars: Vec<char> = lines[r].chars().collect();
4548 if left >= chars.len() {
4549 continue;
4550 }
4551 let end = (right + 1).min(chars.len());
4552 let before: String = chars[..left].iter().collect();
4553 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4554 let after: String = chars[end..].iter().collect();
4555 lines[r] = format!("{before}{middle}{after}");
4556 }
4557 reset_textarea_lines(ed, lines);
4558 ed.vim.mode = Mode::Normal;
4559 ed.jump_cursor(top, left);
4560}
4561
4562fn reset_textarea_lines<H: crate::types::Host>(
4566 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4567 lines: Vec<String>,
4568) {
4569 let cursor = ed.cursor();
4570 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4571 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4572 ed.mark_content_dirty();
4573}
4574
4575type Pos = (usize, usize);
4581
4582fn text_object_range<H: crate::types::Host>(
4586 ed: &Editor<hjkl_buffer::Buffer, H>,
4587 obj: TextObject,
4588 inner: bool,
4589) -> Option<(Pos, Pos, MotionKind)> {
4590 match obj {
4591 TextObject::Word { big } => {
4592 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4593 }
4594 TextObject::Quote(q) => {
4595 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4596 }
4597 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4598 TextObject::Paragraph => {
4599 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4600 }
4601 TextObject::XmlTag => {
4602 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4603 }
4604 TextObject::Sentence => {
4605 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4606 }
4607 }
4608}
4609
4610fn sentence_boundary<H: crate::types::Host>(
4614 ed: &Editor<hjkl_buffer::Buffer, H>,
4615 forward: bool,
4616) -> Option<(usize, usize)> {
4617 let lines = buf_lines_to_vec(&ed.buffer);
4618 if lines.is_empty() {
4619 return None;
4620 }
4621 let pos_to_idx = |pos: (usize, usize)| -> usize {
4622 let mut idx = 0;
4623 for line in lines.iter().take(pos.0) {
4624 idx += line.chars().count() + 1;
4625 }
4626 idx + pos.1
4627 };
4628 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4629 for (r, line) in lines.iter().enumerate() {
4630 let len = line.chars().count();
4631 if idx <= len {
4632 return (r, idx);
4633 }
4634 idx -= len + 1;
4635 }
4636 let last = lines.len().saturating_sub(1);
4637 (last, lines[last].chars().count())
4638 };
4639 let mut chars: Vec<char> = Vec::new();
4640 for (r, line) in lines.iter().enumerate() {
4641 chars.extend(line.chars());
4642 if r + 1 < lines.len() {
4643 chars.push('\n');
4644 }
4645 }
4646 if chars.is_empty() {
4647 return None;
4648 }
4649 let total = chars.len();
4650 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4651 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4652
4653 if forward {
4654 let mut i = cursor_idx + 1;
4657 while i < total {
4658 if is_terminator(chars[i]) {
4659 while i + 1 < total && is_terminator(chars[i + 1]) {
4660 i += 1;
4661 }
4662 if i + 1 >= total {
4663 return None;
4664 }
4665 if chars[i + 1].is_whitespace() {
4666 let mut j = i + 1;
4667 while j < total && chars[j].is_whitespace() {
4668 j += 1;
4669 }
4670 if j >= total {
4671 return None;
4672 }
4673 return Some(idx_to_pos(j));
4674 }
4675 }
4676 i += 1;
4677 }
4678 None
4679 } else {
4680 let find_start = |from: usize| -> Option<usize> {
4684 let mut start = from;
4685 while start > 0 {
4686 let prev = chars[start - 1];
4687 if prev.is_whitespace() {
4688 let mut k = start - 1;
4689 while k > 0 && chars[k - 1].is_whitespace() {
4690 k -= 1;
4691 }
4692 if k > 0 && is_terminator(chars[k - 1]) {
4693 break;
4694 }
4695 }
4696 start -= 1;
4697 }
4698 while start < total && chars[start].is_whitespace() {
4699 start += 1;
4700 }
4701 (start < total).then_some(start)
4702 };
4703 let current_start = find_start(cursor_idx)?;
4704 if current_start < cursor_idx {
4705 return Some(idx_to_pos(current_start));
4706 }
4707 let mut k = current_start;
4710 while k > 0 && chars[k - 1].is_whitespace() {
4711 k -= 1;
4712 }
4713 if k == 0 {
4714 return None;
4715 }
4716 let prev_start = find_start(k - 1)?;
4717 Some(idx_to_pos(prev_start))
4718 }
4719}
4720
4721fn sentence_text_object<H: crate::types::Host>(
4727 ed: &Editor<hjkl_buffer::Buffer, H>,
4728 inner: bool,
4729) -> Option<((usize, usize), (usize, usize))> {
4730 let lines = buf_lines_to_vec(&ed.buffer);
4731 if lines.is_empty() {
4732 return None;
4733 }
4734 let pos_to_idx = |pos: (usize, usize)| -> usize {
4737 let mut idx = 0;
4738 for line in lines.iter().take(pos.0) {
4739 idx += line.chars().count() + 1;
4740 }
4741 idx + pos.1
4742 };
4743 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4744 for (r, line) in lines.iter().enumerate() {
4745 let len = line.chars().count();
4746 if idx <= len {
4747 return (r, idx);
4748 }
4749 idx -= len + 1;
4750 }
4751 let last = lines.len().saturating_sub(1);
4752 (last, lines[last].chars().count())
4753 };
4754 let mut chars: Vec<char> = Vec::new();
4755 for (r, line) in lines.iter().enumerate() {
4756 chars.extend(line.chars());
4757 if r + 1 < lines.len() {
4758 chars.push('\n');
4759 }
4760 }
4761 if chars.is_empty() {
4762 return None;
4763 }
4764
4765 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4766 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4767
4768 let mut start = cursor_idx;
4772 while start > 0 {
4773 let prev = chars[start - 1];
4774 if prev.is_whitespace() {
4775 let mut k = start - 1;
4779 while k > 0 && chars[k - 1].is_whitespace() {
4780 k -= 1;
4781 }
4782 if k > 0 && is_terminator(chars[k - 1]) {
4783 break;
4784 }
4785 }
4786 start -= 1;
4787 }
4788 while start < chars.len() && chars[start].is_whitespace() {
4791 start += 1;
4792 }
4793 if start >= chars.len() {
4794 return None;
4795 }
4796
4797 let mut end = start;
4800 while end < chars.len() {
4801 if is_terminator(chars[end]) {
4802 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4804 end += 1;
4805 }
4806 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4809 break;
4810 }
4811 }
4812 end += 1;
4813 }
4814 let end_idx = (end + 1).min(chars.len());
4816
4817 let final_end = if inner {
4818 end_idx
4819 } else {
4820 let mut e = end_idx;
4824 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4825 e += 1;
4826 }
4827 e
4828 };
4829
4830 Some((idx_to_pos(start), idx_to_pos(final_end)))
4831}
4832
4833fn tag_text_object<H: crate::types::Host>(
4837 ed: &Editor<hjkl_buffer::Buffer, H>,
4838 inner: bool,
4839) -> Option<((usize, usize), (usize, usize))> {
4840 let lines = buf_lines_to_vec(&ed.buffer);
4841 if lines.is_empty() {
4842 return None;
4843 }
4844 let pos_to_idx = |pos: (usize, usize)| -> usize {
4848 let mut idx = 0;
4849 for line in lines.iter().take(pos.0) {
4850 idx += line.chars().count() + 1;
4851 }
4852 idx + pos.1
4853 };
4854 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4855 for (r, line) in lines.iter().enumerate() {
4856 let len = line.chars().count();
4857 if idx <= len {
4858 return (r, idx);
4859 }
4860 idx -= len + 1;
4861 }
4862 let last = lines.len().saturating_sub(1);
4863 (last, lines[last].chars().count())
4864 };
4865 let mut chars: Vec<char> = Vec::new();
4866 for (r, line) in lines.iter().enumerate() {
4867 chars.extend(line.chars());
4868 if r + 1 < lines.len() {
4869 chars.push('\n');
4870 }
4871 }
4872 let cursor_idx = pos_to_idx(ed.cursor());
4873
4874 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4882 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4883 let mut i = 0;
4884 while i < chars.len() {
4885 if chars[i] != '<' {
4886 i += 1;
4887 continue;
4888 }
4889 let mut j = i + 1;
4890 while j < chars.len() && chars[j] != '>' {
4891 j += 1;
4892 }
4893 if j >= chars.len() {
4894 break;
4895 }
4896 let inside: String = chars[i + 1..j].iter().collect();
4897 let close_end = j + 1;
4898 let trimmed = inside.trim();
4899 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4900 i = close_end;
4901 continue;
4902 }
4903 if let Some(rest) = trimmed.strip_prefix('/') {
4904 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4905 if !name.is_empty()
4906 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4907 {
4908 let (open_start, content_start, _) = stack[stack_idx].clone();
4909 stack.truncate(stack_idx);
4910 let content_end = i;
4911 let candidate = (open_start, content_start, content_end, close_end);
4912 if cursor_idx >= content_start && cursor_idx <= content_end {
4913 innermost = match innermost {
4914 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4915 Some(candidate)
4916 }
4917 None => Some(candidate),
4918 existing => existing,
4919 };
4920 } else if open_start >= cursor_idx && next_after.is_none() {
4921 next_after = Some(candidate);
4922 }
4923 }
4924 } else if !trimmed.ends_with('/') {
4925 let name: String = trimmed
4926 .split(|c: char| c.is_whitespace() || c == '/')
4927 .next()
4928 .unwrap_or("")
4929 .to_string();
4930 if !name.is_empty() {
4931 stack.push((i, close_end, name));
4932 }
4933 }
4934 i = close_end;
4935 }
4936
4937 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4938 if inner {
4939 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4940 } else {
4941 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4942 }
4943}
4944
4945fn is_wordchar(c: char) -> bool {
4946 c.is_alphanumeric() || c == '_'
4947}
4948
4949pub(crate) use hjkl_buffer::is_keyword_char;
4953
4954fn word_text_object<H: crate::types::Host>(
4955 ed: &Editor<hjkl_buffer::Buffer, H>,
4956 inner: bool,
4957 big: bool,
4958) -> Option<((usize, usize), (usize, usize))> {
4959 let (row, col) = ed.cursor();
4960 let line = buf_line(&ed.buffer, row)?;
4961 let chars: Vec<char> = line.chars().collect();
4962 if chars.is_empty() {
4963 return None;
4964 }
4965 let at = col.min(chars.len().saturating_sub(1));
4966 let classify = |c: char| -> u8 {
4967 if c.is_whitespace() {
4968 0
4969 } else if big || is_wordchar(c) {
4970 1
4971 } else {
4972 2
4973 }
4974 };
4975 let cls = classify(chars[at]);
4976 let mut start = at;
4977 while start > 0 && classify(chars[start - 1]) == cls {
4978 start -= 1;
4979 }
4980 let mut end = at;
4981 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4982 end += 1;
4983 }
4984 let char_byte = |i: usize| {
4986 if i >= chars.len() {
4987 line.len()
4988 } else {
4989 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4990 }
4991 };
4992 let mut start_col = char_byte(start);
4993 let mut end_col = char_byte(end + 1);
4995 if !inner {
4996 let mut t = end + 1;
4998 let mut included_trailing = false;
4999 while t < chars.len() && chars[t].is_whitespace() {
5000 included_trailing = true;
5001 t += 1;
5002 }
5003 if included_trailing {
5004 end_col = char_byte(t);
5005 } else {
5006 let mut s = start;
5007 while s > 0 && chars[s - 1].is_whitespace() {
5008 s -= 1;
5009 }
5010 start_col = char_byte(s);
5011 }
5012 }
5013 Some(((row, start_col), (row, end_col)))
5014}
5015
5016fn quote_text_object<H: crate::types::Host>(
5017 ed: &Editor<hjkl_buffer::Buffer, H>,
5018 q: char,
5019 inner: bool,
5020) -> Option<((usize, usize), (usize, usize))> {
5021 let (row, col) = ed.cursor();
5022 let line = buf_line(&ed.buffer, row)?;
5023 let bytes = line.as_bytes();
5024 let q_byte = q as u8;
5025 let mut positions: Vec<usize> = Vec::new();
5027 for (i, &b) in bytes.iter().enumerate() {
5028 if b == q_byte {
5029 positions.push(i);
5030 }
5031 }
5032 if positions.len() < 2 {
5033 return None;
5034 }
5035 let mut open_idx: Option<usize> = None;
5036 let mut close_idx: Option<usize> = None;
5037 for pair in positions.chunks(2) {
5038 if pair.len() < 2 {
5039 break;
5040 }
5041 if col >= pair[0] && col <= pair[1] {
5042 open_idx = Some(pair[0]);
5043 close_idx = Some(pair[1]);
5044 break;
5045 }
5046 if col < pair[0] {
5047 open_idx = Some(pair[0]);
5048 close_idx = Some(pair[1]);
5049 break;
5050 }
5051 }
5052 let open = open_idx?;
5053 let close = close_idx?;
5054 if inner {
5056 if close <= open + 1 {
5057 return None;
5058 }
5059 Some(((row, open + 1), (row, close)))
5060 } else {
5061 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5068 let mut end = after_close;
5070 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5071 end += 1;
5072 }
5073 Some(((row, open), (row, end)))
5074 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5075 let mut start = open;
5077 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5078 start -= 1;
5079 }
5080 Some(((row, start), (row, close + 1)))
5081 } else {
5082 Some(((row, open), (row, close + 1)))
5083 }
5084 }
5085}
5086
5087fn bracket_text_object<H: crate::types::Host>(
5088 ed: &Editor<hjkl_buffer::Buffer, H>,
5089 open: char,
5090 inner: bool,
5091) -> Option<(Pos, Pos, MotionKind)> {
5092 let close = match open {
5093 '(' => ')',
5094 '[' => ']',
5095 '{' => '}',
5096 '<' => '>',
5097 _ => return None,
5098 };
5099 let (row, col) = ed.cursor();
5100 let lines = buf_lines_to_vec(&ed.buffer);
5101 let lines = lines.as_slice();
5102 let open_pos = find_open_bracket(lines, row, col, open, close)
5107 .or_else(|| find_next_open(lines, row, col, open))?;
5108 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5109 if inner {
5111 if close_pos.0 > open_pos.0 + 1 {
5117 let inner_row_start = open_pos.0 + 1;
5119 let inner_row_end = close_pos.0 - 1;
5120 let end_col = lines
5121 .get(inner_row_end)
5122 .map(|l| l.chars().count())
5123 .unwrap_or(0);
5124 return Some((
5125 (inner_row_start, 0),
5126 (inner_row_end, end_col),
5127 MotionKind::Linewise,
5128 ));
5129 }
5130 let inner_start = advance_pos(lines, open_pos);
5131 if inner_start.0 > close_pos.0
5132 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5133 {
5134 return None;
5135 }
5136 Some((inner_start, close_pos, MotionKind::Exclusive))
5137 } else {
5138 Some((
5139 open_pos,
5140 advance_pos(lines, close_pos),
5141 MotionKind::Exclusive,
5142 ))
5143 }
5144}
5145
5146fn find_open_bracket(
5147 lines: &[String],
5148 row: usize,
5149 col: usize,
5150 open: char,
5151 close: char,
5152) -> Option<(usize, usize)> {
5153 let mut depth: i32 = 0;
5154 let mut r = row;
5155 let mut c = col as isize;
5156 loop {
5157 let cur = &lines[r];
5158 let chars: Vec<char> = cur.chars().collect();
5159 if (c as usize) >= chars.len() {
5163 c = chars.len() as isize - 1;
5164 }
5165 while c >= 0 {
5166 let ch = chars[c as usize];
5167 if ch == close {
5168 depth += 1;
5169 } else if ch == open {
5170 if depth == 0 {
5171 return Some((r, c as usize));
5172 }
5173 depth -= 1;
5174 }
5175 c -= 1;
5176 }
5177 if r == 0 {
5178 return None;
5179 }
5180 r -= 1;
5181 c = lines[r].chars().count() as isize - 1;
5182 }
5183}
5184
5185fn find_close_bracket(
5186 lines: &[String],
5187 row: usize,
5188 start_col: usize,
5189 open: char,
5190 close: char,
5191) -> Option<(usize, usize)> {
5192 let mut depth: i32 = 0;
5193 let mut r = row;
5194 let mut c = start_col;
5195 loop {
5196 let cur = &lines[r];
5197 let chars: Vec<char> = cur.chars().collect();
5198 while c < chars.len() {
5199 let ch = chars[c];
5200 if ch == open {
5201 depth += 1;
5202 } else if ch == close {
5203 if depth == 0 {
5204 return Some((r, c));
5205 }
5206 depth -= 1;
5207 }
5208 c += 1;
5209 }
5210 if r + 1 >= lines.len() {
5211 return None;
5212 }
5213 r += 1;
5214 c = 0;
5215 }
5216}
5217
5218fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5222 let mut r = row;
5223 let mut c = col;
5224 while r < lines.len() {
5225 let chars: Vec<char> = lines[r].chars().collect();
5226 while c < chars.len() {
5227 if chars[c] == open {
5228 return Some((r, c));
5229 }
5230 c += 1;
5231 }
5232 r += 1;
5233 c = 0;
5234 }
5235 None
5236}
5237
5238fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5239 let (r, c) = pos;
5240 let line_len = lines[r].chars().count();
5241 if c < line_len {
5242 (r, c + 1)
5243 } else if r + 1 < lines.len() {
5244 (r + 1, 0)
5245 } else {
5246 pos
5247 }
5248}
5249
5250fn paragraph_text_object<H: crate::types::Host>(
5251 ed: &Editor<hjkl_buffer::Buffer, H>,
5252 inner: bool,
5253) -> Option<((usize, usize), (usize, usize))> {
5254 let (row, _) = ed.cursor();
5255 let lines = buf_lines_to_vec(&ed.buffer);
5256 if lines.is_empty() {
5257 return None;
5258 }
5259 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5261 if is_blank(row) {
5262 return None;
5263 }
5264 let mut top = row;
5265 while top > 0 && !is_blank(top - 1) {
5266 top -= 1;
5267 }
5268 let mut bot = row;
5269 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5270 bot += 1;
5271 }
5272 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5274 bot += 1;
5275 }
5276 let end_col = lines[bot].chars().count();
5277 Some(((top, 0), (bot, end_col)))
5278}
5279
5280fn read_vim_range<H: crate::types::Host>(
5286 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5287 start: (usize, usize),
5288 end: (usize, usize),
5289 kind: MotionKind,
5290) -> String {
5291 let (top, bot) = order(start, end);
5292 ed.sync_buffer_content_from_textarea();
5293 let lines = buf_lines_to_vec(&ed.buffer);
5294 match kind {
5295 MotionKind::Linewise => {
5296 let lo = top.0;
5297 let hi = bot.0.min(lines.len().saturating_sub(1));
5298 let mut text = lines[lo..=hi].join("\n");
5299 text.push('\n');
5300 text
5301 }
5302 MotionKind::Inclusive | MotionKind::Exclusive => {
5303 let inclusive = matches!(kind, MotionKind::Inclusive);
5304 let mut out = String::new();
5306 for row in top.0..=bot.0 {
5307 let line = lines.get(row).map(String::as_str).unwrap_or("");
5308 let lo = if row == top.0 { top.1 } else { 0 };
5309 let hi_unclamped = if row == bot.0 {
5310 if inclusive { bot.1 + 1 } else { bot.1 }
5311 } else {
5312 line.chars().count() + 1
5313 };
5314 let row_chars: Vec<char> = line.chars().collect();
5315 let hi = hi_unclamped.min(row_chars.len());
5316 if lo < hi {
5317 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5318 }
5319 if row < bot.0 {
5320 out.push('\n');
5321 }
5322 }
5323 out
5324 }
5325 }
5326}
5327
5328fn cut_vim_range<H: crate::types::Host>(
5337 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5338 start: (usize, usize),
5339 end: (usize, usize),
5340 kind: MotionKind,
5341) -> String {
5342 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5343 let (top, bot) = order(start, end);
5344 ed.sync_buffer_content_from_textarea();
5345 let (buf_start, buf_end, buf_kind) = match kind {
5346 MotionKind::Linewise => (
5347 Position::new(top.0, 0),
5348 Position::new(bot.0, 0),
5349 BufKind::Line,
5350 ),
5351 MotionKind::Inclusive => {
5352 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5353 let next = if bot.1 < line_chars {
5357 Position::new(bot.0, bot.1 + 1)
5358 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5359 Position::new(bot.0 + 1, 0)
5360 } else {
5361 Position::new(bot.0, line_chars)
5362 };
5363 (Position::new(top.0, top.1), next, BufKind::Char)
5364 }
5365 MotionKind::Exclusive => (
5366 Position::new(top.0, top.1),
5367 Position::new(bot.0, bot.1),
5368 BufKind::Char,
5369 ),
5370 };
5371 let inverse = ed.mutate_edit(Edit::DeleteRange {
5372 start: buf_start,
5373 end: buf_end,
5374 kind: buf_kind,
5375 });
5376 let text = match inverse {
5377 Edit::InsertStr { text, .. } => text,
5378 _ => String::new(),
5379 };
5380 if !text.is_empty() {
5381 ed.record_yank_to_host(text.clone());
5382 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5383 }
5384 ed.push_buffer_cursor_to_textarea();
5385 text
5386}
5387
5388fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5394 use hjkl_buffer::{Edit, MotionKind, Position};
5395 ed.sync_buffer_content_from_textarea();
5396 let cursor = buf_cursor_pos(&ed.buffer);
5397 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5398 if cursor.col >= line_chars {
5399 return;
5400 }
5401 let inverse = ed.mutate_edit(Edit::DeleteRange {
5402 start: cursor,
5403 end: Position::new(cursor.row, line_chars),
5404 kind: MotionKind::Char,
5405 });
5406 if let Edit::InsertStr { text, .. } = inverse
5407 && !text.is_empty()
5408 {
5409 ed.record_yank_to_host(text.clone());
5410 ed.vim.yank_linewise = false;
5411 ed.set_yank(text);
5412 }
5413 buf_set_cursor_pos(&mut ed.buffer, cursor);
5414 ed.push_buffer_cursor_to_textarea();
5415}
5416
5417fn do_char_delete<H: crate::types::Host>(
5418 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5419 forward: bool,
5420 count: usize,
5421) {
5422 use hjkl_buffer::{Edit, MotionKind, Position};
5423 ed.push_undo();
5424 ed.sync_buffer_content_from_textarea();
5425 let mut deleted = String::new();
5428 for _ in 0..count {
5429 let cursor = buf_cursor_pos(&ed.buffer);
5430 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5431 if forward {
5432 if cursor.col >= line_chars {
5435 continue;
5436 }
5437 let inverse = ed.mutate_edit(Edit::DeleteRange {
5438 start: cursor,
5439 end: Position::new(cursor.row, cursor.col + 1),
5440 kind: MotionKind::Char,
5441 });
5442 if let Edit::InsertStr { text, .. } = inverse {
5443 deleted.push_str(&text);
5444 }
5445 } else {
5446 if cursor.col == 0 {
5448 continue;
5449 }
5450 let inverse = ed.mutate_edit(Edit::DeleteRange {
5451 start: Position::new(cursor.row, cursor.col - 1),
5452 end: cursor,
5453 kind: MotionKind::Char,
5454 });
5455 if let Edit::InsertStr { text, .. } = inverse {
5456 deleted = text + &deleted;
5459 }
5460 }
5461 }
5462 if !deleted.is_empty() {
5463 ed.record_yank_to_host(deleted.clone());
5464 ed.record_delete(deleted, false);
5465 }
5466 ed.push_buffer_cursor_to_textarea();
5467}
5468
5469fn adjust_number<H: crate::types::Host>(
5473 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5474 delta: i64,
5475) -> bool {
5476 use hjkl_buffer::{Edit, MotionKind, Position};
5477 ed.sync_buffer_content_from_textarea();
5478 let cursor = buf_cursor_pos(&ed.buffer);
5479 let row = cursor.row;
5480 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5481 Some(l) => l.chars().collect(),
5482 None => return false,
5483 };
5484 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5485 return false;
5486 };
5487 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5488 digit_start - 1
5489 } else {
5490 digit_start
5491 };
5492 let mut span_end = digit_start;
5493 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5494 span_end += 1;
5495 }
5496 let s: String = chars[span_start..span_end].iter().collect();
5497 let Ok(n) = s.parse::<i64>() else {
5498 return false;
5499 };
5500 let new_s = n.saturating_add(delta).to_string();
5501
5502 ed.push_undo();
5503 let span_start_pos = Position::new(row, span_start);
5504 let span_end_pos = Position::new(row, span_end);
5505 ed.mutate_edit(Edit::DeleteRange {
5506 start: span_start_pos,
5507 end: span_end_pos,
5508 kind: MotionKind::Char,
5509 });
5510 ed.mutate_edit(Edit::InsertStr {
5511 at: span_start_pos,
5512 text: new_s.clone(),
5513 });
5514 let new_len = new_s.chars().count();
5515 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5516 ed.push_buffer_cursor_to_textarea();
5517 true
5518}
5519
5520pub(crate) fn replace_char<H: crate::types::Host>(
5521 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5522 ch: char,
5523 count: usize,
5524) {
5525 use hjkl_buffer::{Edit, MotionKind, Position};
5526 ed.push_undo();
5527 ed.sync_buffer_content_from_textarea();
5528 for _ in 0..count {
5529 let cursor = buf_cursor_pos(&ed.buffer);
5530 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5531 if cursor.col >= line_chars {
5532 break;
5533 }
5534 ed.mutate_edit(Edit::DeleteRange {
5535 start: cursor,
5536 end: Position::new(cursor.row, cursor.col + 1),
5537 kind: MotionKind::Char,
5538 });
5539 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5540 }
5541 crate::motions::move_left(&mut ed.buffer, 1);
5543 ed.push_buffer_cursor_to_textarea();
5544}
5545
5546fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5547 use hjkl_buffer::{Edit, MotionKind, Position};
5548 ed.sync_buffer_content_from_textarea();
5549 let cursor = buf_cursor_pos(&ed.buffer);
5550 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5551 return;
5552 };
5553 let toggled = if c.is_uppercase() {
5554 c.to_lowercase().next().unwrap_or(c)
5555 } else {
5556 c.to_uppercase().next().unwrap_or(c)
5557 };
5558 ed.mutate_edit(Edit::DeleteRange {
5559 start: cursor,
5560 end: Position::new(cursor.row, cursor.col + 1),
5561 kind: MotionKind::Char,
5562 });
5563 ed.mutate_edit(Edit::InsertChar {
5564 at: cursor,
5565 ch: toggled,
5566 });
5567}
5568
5569fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5570 use hjkl_buffer::{Edit, Position};
5571 ed.sync_buffer_content_from_textarea();
5572 let row = buf_cursor_pos(&ed.buffer).row;
5573 if row + 1 >= buf_row_count(&ed.buffer) {
5574 return;
5575 }
5576 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5577 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5578 let next_trimmed = next_raw.trim_start();
5579 let cur_chars = cur_line.chars().count();
5580 let next_chars = next_raw.chars().count();
5581 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5584 " "
5585 } else {
5586 ""
5587 };
5588 let joined = format!("{cur_line}{separator}{next_trimmed}");
5589 ed.mutate_edit(Edit::Replace {
5590 start: Position::new(row, 0),
5591 end: Position::new(row + 1, next_chars),
5592 with: joined,
5593 });
5594 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5598 ed.push_buffer_cursor_to_textarea();
5599}
5600
5601fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5604 use hjkl_buffer::Edit;
5605 ed.sync_buffer_content_from_textarea();
5606 let row = buf_cursor_pos(&ed.buffer).row;
5607 if row + 1 >= buf_row_count(&ed.buffer) {
5608 return;
5609 }
5610 let join_col = buf_line_chars(&ed.buffer, row);
5611 ed.mutate_edit(Edit::JoinLines {
5612 row,
5613 count: 1,
5614 with_space: false,
5615 });
5616 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5618 ed.push_buffer_cursor_to_textarea();
5619}
5620
5621fn do_paste<H: crate::types::Host>(
5622 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5623 before: bool,
5624 count: usize,
5625) {
5626 use hjkl_buffer::{Edit, Position};
5627 ed.push_undo();
5628 let selector = ed.vim.pending_register.take();
5633 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5634 Some(slot) => (slot.text.clone(), slot.linewise),
5635 None => {
5641 let s = &ed.registers().unnamed;
5642 (s.text.clone(), s.linewise)
5643 }
5644 };
5645 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5649 for _ in 0..count {
5650 ed.sync_buffer_content_from_textarea();
5651 let yank = yank.clone();
5652 if yank.is_empty() {
5653 continue;
5654 }
5655 if linewise {
5656 let text = yank.trim_matches('\n').to_string();
5660 let row = buf_cursor_pos(&ed.buffer).row;
5661 let target_row = if before {
5662 ed.mutate_edit(Edit::InsertStr {
5663 at: Position::new(row, 0),
5664 text: format!("{text}\n"),
5665 });
5666 row
5667 } else {
5668 let line_chars = buf_line_chars(&ed.buffer, row);
5669 ed.mutate_edit(Edit::InsertStr {
5670 at: Position::new(row, line_chars),
5671 text: format!("\n{text}"),
5672 });
5673 row + 1
5674 };
5675 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5676 crate::motions::move_first_non_blank(&mut ed.buffer);
5677 ed.push_buffer_cursor_to_textarea();
5678 let payload_lines = text.lines().count().max(1);
5680 let bot_row = target_row + payload_lines - 1;
5681 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5682 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5683 } else {
5684 let cursor = buf_cursor_pos(&ed.buffer);
5688 let at = if before {
5689 cursor
5690 } else {
5691 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5692 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5693 };
5694 ed.mutate_edit(Edit::InsertStr {
5695 at,
5696 text: yank.clone(),
5697 });
5698 crate::motions::move_left(&mut ed.buffer, 1);
5701 ed.push_buffer_cursor_to_textarea();
5702 let lo = (at.row, at.col);
5704 let hi = ed.cursor();
5705 paste_mark = Some((lo, hi));
5706 }
5707 }
5708 if let Some((lo, hi)) = paste_mark {
5709 ed.set_mark('[', lo);
5710 ed.set_mark(']', hi);
5711 }
5712 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5714}
5715
5716pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5717 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5718 let current = ed.snapshot();
5719 ed.redo_stack.push(current);
5720 ed.restore(lines, cursor);
5721 }
5722 ed.vim.mode = Mode::Normal;
5723 clamp_cursor_to_normal_mode(ed);
5727}
5728
5729pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5730 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5731 let current = ed.snapshot();
5732 ed.undo_stack.push(current);
5733 ed.cap_undo();
5734 ed.restore(lines, cursor);
5735 }
5736 ed.vim.mode = Mode::Normal;
5737}
5738
5739fn replay_insert_and_finish<H: crate::types::Host>(
5746 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5747 text: &str,
5748) {
5749 use hjkl_buffer::{Edit, Position};
5750 let cursor = ed.cursor();
5751 ed.mutate_edit(Edit::InsertStr {
5752 at: Position::new(cursor.0, cursor.1),
5753 text: text.to_string(),
5754 });
5755 if ed.vim.insert_session.take().is_some() {
5756 if ed.cursor().1 > 0 {
5757 crate::motions::move_left(&mut ed.buffer, 1);
5758 ed.push_buffer_cursor_to_textarea();
5759 }
5760 ed.vim.mode = Mode::Normal;
5761 }
5762}
5763
5764fn replay_last_change<H: crate::types::Host>(
5765 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5766 outer_count: usize,
5767) {
5768 let Some(change) = ed.vim.last_change.clone() else {
5769 return;
5770 };
5771 ed.vim.replaying = true;
5772 let scale = if outer_count > 0 { outer_count } else { 1 };
5773 match change {
5774 LastChange::OpMotion {
5775 op,
5776 motion,
5777 count,
5778 inserted,
5779 } => {
5780 let total = count.max(1) * scale;
5781 apply_op_with_motion(ed, op, &motion, total);
5782 if let Some(text) = inserted {
5783 replay_insert_and_finish(ed, &text);
5784 }
5785 }
5786 LastChange::OpTextObj {
5787 op,
5788 obj,
5789 inner,
5790 inserted,
5791 } => {
5792 apply_op_with_text_object(ed, op, obj, inner);
5793 if let Some(text) = inserted {
5794 replay_insert_and_finish(ed, &text);
5795 }
5796 }
5797 LastChange::LineOp {
5798 op,
5799 count,
5800 inserted,
5801 } => {
5802 let total = count.max(1) * scale;
5803 execute_line_op(ed, op, total);
5804 if let Some(text) = inserted {
5805 replay_insert_and_finish(ed, &text);
5806 }
5807 }
5808 LastChange::CharDel { forward, count } => {
5809 do_char_delete(ed, forward, count * scale);
5810 }
5811 LastChange::ReplaceChar { ch, count } => {
5812 replace_char(ed, ch, count * scale);
5813 }
5814 LastChange::ToggleCase { count } => {
5815 for _ in 0..count * scale {
5816 ed.push_undo();
5817 toggle_case_at_cursor(ed);
5818 }
5819 }
5820 LastChange::JoinLine { count } => {
5821 for _ in 0..count * scale {
5822 ed.push_undo();
5823 join_line(ed);
5824 }
5825 }
5826 LastChange::Paste { before, count } => {
5827 do_paste(ed, before, count * scale);
5828 }
5829 LastChange::DeleteToEol { inserted } => {
5830 use hjkl_buffer::{Edit, Position};
5831 ed.push_undo();
5832 delete_to_eol(ed);
5833 if let Some(text) = inserted {
5834 let cursor = ed.cursor();
5835 ed.mutate_edit(Edit::InsertStr {
5836 at: Position::new(cursor.0, cursor.1),
5837 text,
5838 });
5839 }
5840 }
5841 LastChange::OpenLine { above, inserted } => {
5842 use hjkl_buffer::{Edit, Position};
5843 ed.push_undo();
5844 ed.sync_buffer_content_from_textarea();
5845 let row = buf_cursor_pos(&ed.buffer).row;
5846 if above {
5847 ed.mutate_edit(Edit::InsertStr {
5848 at: Position::new(row, 0),
5849 text: "\n".to_string(),
5850 });
5851 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5852 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5853 } else {
5854 let line_chars = buf_line_chars(&ed.buffer, row);
5855 ed.mutate_edit(Edit::InsertStr {
5856 at: Position::new(row, line_chars),
5857 text: "\n".to_string(),
5858 });
5859 }
5860 ed.push_buffer_cursor_to_textarea();
5861 let cursor = ed.cursor();
5862 ed.mutate_edit(Edit::InsertStr {
5863 at: Position::new(cursor.0, cursor.1),
5864 text: inserted,
5865 });
5866 }
5867 LastChange::InsertAt {
5868 entry,
5869 inserted,
5870 count,
5871 } => {
5872 use hjkl_buffer::{Edit, Position};
5873 ed.push_undo();
5874 match entry {
5875 InsertEntry::I => {}
5876 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5877 InsertEntry::A => {
5878 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5879 ed.push_buffer_cursor_to_textarea();
5880 }
5881 InsertEntry::ShiftA => {
5882 crate::motions::move_line_end(&mut ed.buffer);
5883 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5884 ed.push_buffer_cursor_to_textarea();
5885 }
5886 }
5887 for _ in 0..count.max(1) {
5888 let cursor = ed.cursor();
5889 ed.mutate_edit(Edit::InsertStr {
5890 at: Position::new(cursor.0, cursor.1),
5891 text: inserted.clone(),
5892 });
5893 }
5894 }
5895 }
5896 ed.vim.replaying = false;
5897}
5898
5899fn extract_inserted(before: &str, after: &str) -> String {
5902 let before_chars: Vec<char> = before.chars().collect();
5903 let after_chars: Vec<char> = after.chars().collect();
5904 if after_chars.len() <= before_chars.len() {
5905 return String::new();
5906 }
5907 let prefix = before_chars
5908 .iter()
5909 .zip(after_chars.iter())
5910 .take_while(|(a, b)| a == b)
5911 .count();
5912 let max_suffix = before_chars.len() - prefix;
5913 let suffix = before_chars
5914 .iter()
5915 .rev()
5916 .zip(after_chars.iter().rev())
5917 .take(max_suffix)
5918 .take_while(|(a, b)| a == b)
5919 .count();
5920 after_chars[prefix..after_chars.len() - suffix]
5921 .iter()
5922 .collect()
5923}
5924
5925#[cfg(all(test, feature = "crossterm"))]
5928mod tests {
5929 use crate::VimMode;
5930 use crate::editor::Editor;
5931 use crate::types::Host;
5932 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5933
5934 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5935 let mut iter = keys.chars().peekable();
5939 while let Some(c) = iter.next() {
5940 if c == '<' {
5941 let mut tag = String::new();
5942 for ch in iter.by_ref() {
5943 if ch == '>' {
5944 break;
5945 }
5946 tag.push(ch);
5947 }
5948 let ev = match tag.as_str() {
5949 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5950 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5951 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5952 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5953 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5954 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5955 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5956 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5957 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5961 s if s.starts_with("C-") => {
5962 let ch = s.chars().nth(2).unwrap();
5963 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5964 }
5965 _ => continue,
5966 };
5967 e.handle_key(ev);
5968 } else {
5969 let mods = if c.is_uppercase() {
5970 KeyModifiers::SHIFT
5971 } else {
5972 KeyModifiers::NONE
5973 };
5974 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5975 }
5976 }
5977 }
5978
5979 fn editor_with(content: &str) -> Editor {
5980 let opts = crate::types::Options {
5985 shiftwidth: 2,
5986 ..crate::types::Options::default()
5987 };
5988 let mut e = Editor::new(
5989 hjkl_buffer::Buffer::new(),
5990 crate::types::DefaultHost::new(),
5991 opts,
5992 );
5993 e.set_content(content);
5994 e
5995 }
5996
5997 #[test]
5998 fn f_char_jumps_on_line() {
5999 let mut e = editor_with("hello world");
6000 run_keys(&mut e, "fw");
6001 assert_eq!(e.cursor(), (0, 6));
6002 }
6003
6004 #[test]
6005 fn cap_f_jumps_backward() {
6006 let mut e = editor_with("hello world");
6007 e.jump_cursor(0, 10);
6008 run_keys(&mut e, "Fo");
6009 assert_eq!(e.cursor().1, 7);
6010 }
6011
6012 #[test]
6013 fn t_stops_before_char() {
6014 let mut e = editor_with("hello");
6015 run_keys(&mut e, "tl");
6016 assert_eq!(e.cursor(), (0, 1));
6017 }
6018
6019 #[test]
6020 fn semicolon_repeats_find() {
6021 let mut e = editor_with("aa.bb.cc");
6022 run_keys(&mut e, "f.");
6023 assert_eq!(e.cursor().1, 2);
6024 run_keys(&mut e, ";");
6025 assert_eq!(e.cursor().1, 5);
6026 }
6027
6028 #[test]
6029 fn comma_repeats_find_reverse() {
6030 let mut e = editor_with("aa.bb.cc");
6031 run_keys(&mut e, "f.");
6032 run_keys(&mut e, ";");
6033 run_keys(&mut e, ",");
6034 assert_eq!(e.cursor().1, 2);
6035 }
6036
6037 #[test]
6038 fn di_quote_deletes_content() {
6039 let mut e = editor_with("foo \"bar\" baz");
6040 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6042 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6043 }
6044
6045 #[test]
6046 fn da_quote_deletes_with_quotes() {
6047 let mut e = editor_with("foo \"bar\" baz");
6050 e.jump_cursor(0, 6);
6051 run_keys(&mut e, "da\"");
6052 assert_eq!(e.buffer().lines()[0], "foo baz");
6053 }
6054
6055 #[test]
6056 fn ci_paren_deletes_and_inserts() {
6057 let mut e = editor_with("fn(a, b, c)");
6058 e.jump_cursor(0, 5);
6059 run_keys(&mut e, "ci(");
6060 assert_eq!(e.vim_mode(), VimMode::Insert);
6061 assert_eq!(e.buffer().lines()[0], "fn()");
6062 }
6063
6064 #[test]
6065 fn diw_deletes_inner_word() {
6066 let mut e = editor_with("hello world");
6067 e.jump_cursor(0, 2);
6068 run_keys(&mut e, "diw");
6069 assert_eq!(e.buffer().lines()[0], " world");
6070 }
6071
6072 #[test]
6073 fn daw_deletes_word_with_trailing_space() {
6074 let mut e = editor_with("hello world");
6075 run_keys(&mut e, "daw");
6076 assert_eq!(e.buffer().lines()[0], "world");
6077 }
6078
6079 #[test]
6080 fn percent_jumps_to_matching_bracket() {
6081 let mut e = editor_with("foo(bar)");
6082 e.jump_cursor(0, 3);
6083 run_keys(&mut e, "%");
6084 assert_eq!(e.cursor().1, 7);
6085 run_keys(&mut e, "%");
6086 assert_eq!(e.cursor().1, 3);
6087 }
6088
6089 #[test]
6090 fn dot_repeats_last_change() {
6091 let mut e = editor_with("aaa bbb ccc");
6092 run_keys(&mut e, "dw");
6093 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6094 run_keys(&mut e, ".");
6095 assert_eq!(e.buffer().lines()[0], "ccc");
6096 }
6097
6098 #[test]
6099 fn dot_repeats_change_operator_with_text() {
6100 let mut e = editor_with("foo foo foo");
6101 run_keys(&mut e, "cwbar<Esc>");
6102 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6103 run_keys(&mut e, "w");
6105 run_keys(&mut e, ".");
6106 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6107 }
6108
6109 #[test]
6110 fn dot_repeats_x() {
6111 let mut e = editor_with("abcdef");
6112 run_keys(&mut e, "x");
6113 run_keys(&mut e, "..");
6114 assert_eq!(e.buffer().lines()[0], "def");
6115 }
6116
6117 #[test]
6118 fn count_operator_motion_compose() {
6119 let mut e = editor_with("one two three four five");
6120 run_keys(&mut e, "d3w");
6121 assert_eq!(e.buffer().lines()[0], "four five");
6122 }
6123
6124 #[test]
6125 fn two_dd_deletes_two_lines() {
6126 let mut e = editor_with("a\nb\nc");
6127 run_keys(&mut e, "2dd");
6128 assert_eq!(e.buffer().lines().len(), 1);
6129 assert_eq!(e.buffer().lines()[0], "c");
6130 }
6131
6132 #[test]
6137 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6138 let mut e = editor_with("one\ntwo\n three\nfour");
6139 e.jump_cursor(1, 2);
6140 run_keys(&mut e, "dd");
6141 assert_eq!(e.buffer().lines()[1], " three");
6143 assert_eq!(e.cursor(), (1, 4));
6144 }
6145
6146 #[test]
6147 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6148 let mut e = editor_with("one\n two\nthree");
6149 e.jump_cursor(2, 0);
6150 run_keys(&mut e, "dd");
6151 assert_eq!(e.buffer().lines().len(), 2);
6153 assert_eq!(e.cursor(), (1, 2));
6154 }
6155
6156 #[test]
6157 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6158 let mut e = editor_with("lonely");
6159 run_keys(&mut e, "dd");
6160 assert_eq!(e.buffer().lines().len(), 1);
6161 assert_eq!(e.buffer().lines()[0], "");
6162 assert_eq!(e.cursor(), (0, 0));
6163 }
6164
6165 #[test]
6166 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6167 let mut e = editor_with("a\nb\nc\n d\ne");
6168 e.jump_cursor(1, 0);
6170 run_keys(&mut e, "3dd");
6171 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6172 assert_eq!(e.cursor(), (1, 0));
6173 }
6174
6175 #[test]
6176 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6177 let mut e = editor_with(" line one\n line two\n xyz!");
6196 e.jump_cursor(0, 8);
6198 assert_eq!(e.cursor(), (0, 8));
6199 run_keys(&mut e, "dd");
6202 assert_eq!(
6203 e.cursor(),
6204 (0, 4),
6205 "dd must place cursor on first-non-blank"
6206 );
6207 run_keys(&mut e, "j");
6211 let (row, col) = e.cursor();
6212 assert_eq!(row, 1);
6213 assert_eq!(
6214 col, 4,
6215 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6216 );
6217 }
6218
6219 #[test]
6220 fn gu_lowercases_motion_range() {
6221 let mut e = editor_with("HELLO WORLD");
6222 run_keys(&mut e, "guw");
6223 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6224 assert_eq!(e.cursor(), (0, 0));
6225 }
6226
6227 #[test]
6228 fn g_u_uppercases_text_object() {
6229 let mut e = editor_with("hello world");
6230 run_keys(&mut e, "gUiw");
6232 assert_eq!(e.buffer().lines()[0], "HELLO world");
6233 assert_eq!(e.cursor(), (0, 0));
6234 }
6235
6236 #[test]
6237 fn g_tilde_toggles_case_of_range() {
6238 let mut e = editor_with("Hello World");
6239 run_keys(&mut e, "g~iw");
6240 assert_eq!(e.buffer().lines()[0], "hELLO World");
6241 }
6242
6243 #[test]
6244 fn g_uu_uppercases_current_line() {
6245 let mut e = editor_with("select 1\nselect 2");
6246 run_keys(&mut e, "gUU");
6247 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6248 assert_eq!(e.buffer().lines()[1], "select 2");
6249 }
6250
6251 #[test]
6252 fn gugu_lowercases_current_line() {
6253 let mut e = editor_with("FOO BAR\nBAZ");
6254 run_keys(&mut e, "gugu");
6255 assert_eq!(e.buffer().lines()[0], "foo bar");
6256 }
6257
6258 #[test]
6259 fn visual_u_uppercases_selection() {
6260 let mut e = editor_with("hello world");
6261 run_keys(&mut e, "veU");
6263 assert_eq!(e.buffer().lines()[0], "HELLO world");
6264 }
6265
6266 #[test]
6267 fn visual_line_u_lowercases_line() {
6268 let mut e = editor_with("HELLO WORLD\nOTHER");
6269 run_keys(&mut e, "Vu");
6270 assert_eq!(e.buffer().lines()[0], "hello world");
6271 assert_eq!(e.buffer().lines()[1], "OTHER");
6272 }
6273
6274 #[test]
6275 fn g_uu_with_count_uppercases_multiple_lines() {
6276 let mut e = editor_with("one\ntwo\nthree\nfour");
6277 run_keys(&mut e, "3gUU");
6279 assert_eq!(e.buffer().lines()[0], "ONE");
6280 assert_eq!(e.buffer().lines()[1], "TWO");
6281 assert_eq!(e.buffer().lines()[2], "THREE");
6282 assert_eq!(e.buffer().lines()[3], "four");
6283 }
6284
6285 #[test]
6286 fn double_gt_indents_current_line() {
6287 let mut e = editor_with("hello");
6288 run_keys(&mut e, ">>");
6289 assert_eq!(e.buffer().lines()[0], " hello");
6290 assert_eq!(e.cursor(), (0, 2));
6292 }
6293
6294 #[test]
6295 fn double_lt_outdents_current_line() {
6296 let mut e = editor_with(" hello");
6297 run_keys(&mut e, "<lt><lt>");
6298 assert_eq!(e.buffer().lines()[0], " hello");
6299 assert_eq!(e.cursor(), (0, 2));
6300 }
6301
6302 #[test]
6303 fn count_double_gt_indents_multiple_lines() {
6304 let mut e = editor_with("a\nb\nc\nd");
6305 run_keys(&mut e, "3>>");
6307 assert_eq!(e.buffer().lines()[0], " a");
6308 assert_eq!(e.buffer().lines()[1], " b");
6309 assert_eq!(e.buffer().lines()[2], " c");
6310 assert_eq!(e.buffer().lines()[3], "d");
6311 }
6312
6313 #[test]
6314 fn outdent_clips_ragged_leading_whitespace() {
6315 let mut e = editor_with(" x");
6318 run_keys(&mut e, "<lt><lt>");
6319 assert_eq!(e.buffer().lines()[0], "x");
6320 }
6321
6322 #[test]
6323 fn indent_motion_is_always_linewise() {
6324 let mut e = editor_with("foo bar");
6327 run_keys(&mut e, ">w");
6328 assert_eq!(e.buffer().lines()[0], " foo bar");
6329 }
6330
6331 #[test]
6332 fn indent_text_object_extends_over_paragraph() {
6333 let mut e = editor_with("a\nb\n\nc\nd");
6334 run_keys(&mut e, ">ap");
6336 assert_eq!(e.buffer().lines()[0], " a");
6337 assert_eq!(e.buffer().lines()[1], " b");
6338 assert_eq!(e.buffer().lines()[2], "");
6339 assert_eq!(e.buffer().lines()[3], "c");
6340 }
6341
6342 #[test]
6343 fn visual_line_indent_shifts_selected_rows() {
6344 let mut e = editor_with("x\ny\nz");
6345 run_keys(&mut e, "Vj>");
6347 assert_eq!(e.buffer().lines()[0], " x");
6348 assert_eq!(e.buffer().lines()[1], " y");
6349 assert_eq!(e.buffer().lines()[2], "z");
6350 }
6351
6352 #[test]
6353 fn outdent_empty_line_is_noop() {
6354 let mut e = editor_with("\nfoo");
6355 run_keys(&mut e, "<lt><lt>");
6356 assert_eq!(e.buffer().lines()[0], "");
6357 }
6358
6359 #[test]
6360 fn indent_skips_empty_lines() {
6361 let mut e = editor_with("");
6364 run_keys(&mut e, ">>");
6365 assert_eq!(e.buffer().lines()[0], "");
6366 }
6367
6368 #[test]
6369 fn insert_ctrl_t_indents_current_line() {
6370 let mut e = editor_with("x");
6371 run_keys(&mut e, "i<C-t>");
6373 assert_eq!(e.buffer().lines()[0], " x");
6374 assert_eq!(e.cursor(), (0, 2));
6377 }
6378
6379 #[test]
6380 fn insert_ctrl_d_outdents_current_line() {
6381 let mut e = editor_with(" x");
6382 run_keys(&mut e, "A<C-d>");
6384 assert_eq!(e.buffer().lines()[0], " x");
6385 }
6386
6387 #[test]
6388 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6389 let mut e = editor_with("first\nsecond");
6390 e.jump_cursor(1, 0);
6391 run_keys(&mut e, "h");
6392 assert_eq!(e.cursor(), (1, 0));
6394 }
6395
6396 #[test]
6397 fn l_at_last_char_does_not_wrap_to_next_line() {
6398 let mut e = editor_with("ab\ncd");
6399 e.jump_cursor(0, 1);
6401 run_keys(&mut e, "l");
6402 assert_eq!(e.cursor(), (0, 1));
6404 }
6405
6406 #[test]
6407 fn count_l_clamps_at_line_end() {
6408 let mut e = editor_with("abcde");
6409 run_keys(&mut e, "20l");
6412 assert_eq!(e.cursor(), (0, 4));
6413 }
6414
6415 #[test]
6416 fn count_h_clamps_at_col_zero() {
6417 let mut e = editor_with("abcde");
6418 e.jump_cursor(0, 3);
6419 run_keys(&mut e, "20h");
6420 assert_eq!(e.cursor(), (0, 0));
6421 }
6422
6423 #[test]
6424 fn dl_on_last_char_still_deletes_it() {
6425 let mut e = editor_with("ab");
6429 e.jump_cursor(0, 1);
6430 run_keys(&mut e, "dl");
6431 assert_eq!(e.buffer().lines()[0], "a");
6432 }
6433
6434 #[test]
6435 fn case_op_preserves_yank_register() {
6436 let mut e = editor_with("target");
6437 run_keys(&mut e, "yy");
6438 let yank_before = e.yank().to_string();
6439 run_keys(&mut e, "gUU");
6441 assert_eq!(e.buffer().lines()[0], "TARGET");
6442 assert_eq!(
6443 e.yank(),
6444 yank_before,
6445 "case ops must preserve the yank buffer"
6446 );
6447 }
6448
6449 #[test]
6450 fn dap_deletes_paragraph() {
6451 let mut e = editor_with("a\nb\n\nc\nd");
6452 run_keys(&mut e, "dap");
6453 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6454 }
6455
6456 #[test]
6457 fn dit_deletes_inner_tag_content() {
6458 let mut e = editor_with("<b>hello</b>");
6459 e.jump_cursor(0, 4);
6461 run_keys(&mut e, "dit");
6462 assert_eq!(e.buffer().lines()[0], "<b></b>");
6463 }
6464
6465 #[test]
6466 fn dat_deletes_around_tag() {
6467 let mut e = editor_with("hi <b>foo</b> bye");
6468 e.jump_cursor(0, 6);
6469 run_keys(&mut e, "dat");
6470 assert_eq!(e.buffer().lines()[0], "hi bye");
6471 }
6472
6473 #[test]
6474 fn dit_picks_innermost_tag() {
6475 let mut e = editor_with("<a><b>x</b></a>");
6476 e.jump_cursor(0, 6);
6478 run_keys(&mut e, "dit");
6479 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6481 }
6482
6483 #[test]
6484 fn dat_innermost_tag_pair() {
6485 let mut e = editor_with("<a><b>x</b></a>");
6486 e.jump_cursor(0, 6);
6487 run_keys(&mut e, "dat");
6488 assert_eq!(e.buffer().lines()[0], "<a></a>");
6489 }
6490
6491 #[test]
6492 fn dit_outside_any_tag_no_op() {
6493 let mut e = editor_with("plain text");
6494 e.jump_cursor(0, 3);
6495 run_keys(&mut e, "dit");
6496 assert_eq!(e.buffer().lines()[0], "plain text");
6498 }
6499
6500 #[test]
6501 fn cit_changes_inner_tag_content() {
6502 let mut e = editor_with("<b>hello</b>");
6503 e.jump_cursor(0, 4);
6504 run_keys(&mut e, "citNEW<Esc>");
6505 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6506 }
6507
6508 #[test]
6509 fn cat_changes_around_tag() {
6510 let mut e = editor_with("hi <b>foo</b> bye");
6511 e.jump_cursor(0, 6);
6512 run_keys(&mut e, "catBAR<Esc>");
6513 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6514 }
6515
6516 #[test]
6517 fn yit_yanks_inner_tag_content() {
6518 let mut e = editor_with("<b>hello</b>");
6519 e.jump_cursor(0, 4);
6520 run_keys(&mut e, "yit");
6521 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6522 }
6523
6524 #[test]
6525 fn yat_yanks_full_tag_pair() {
6526 let mut e = editor_with("hi <b>foo</b> bye");
6527 e.jump_cursor(0, 6);
6528 run_keys(&mut e, "yat");
6529 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6530 }
6531
6532 #[test]
6533 fn vit_visually_selects_inner_tag() {
6534 let mut e = editor_with("<b>hello</b>");
6535 e.jump_cursor(0, 4);
6536 run_keys(&mut e, "vit");
6537 assert_eq!(e.vim_mode(), VimMode::Visual);
6538 run_keys(&mut e, "y");
6539 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6540 }
6541
6542 #[test]
6543 fn vat_visually_selects_around_tag() {
6544 let mut e = editor_with("x<b>foo</b>y");
6545 e.jump_cursor(0, 5);
6546 run_keys(&mut e, "vat");
6547 assert_eq!(e.vim_mode(), VimMode::Visual);
6548 run_keys(&mut e, "y");
6549 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6550 }
6551
6552 #[test]
6555 #[allow(non_snake_case)]
6556 fn diW_deletes_inner_big_word() {
6557 let mut e = editor_with("foo.bar baz");
6558 e.jump_cursor(0, 2);
6559 run_keys(&mut e, "diW");
6560 assert_eq!(e.buffer().lines()[0], " baz");
6562 }
6563
6564 #[test]
6565 #[allow(non_snake_case)]
6566 fn daW_deletes_around_big_word() {
6567 let mut e = editor_with("foo.bar baz");
6568 e.jump_cursor(0, 2);
6569 run_keys(&mut e, "daW");
6570 assert_eq!(e.buffer().lines()[0], "baz");
6571 }
6572
6573 #[test]
6574 fn di_double_quote_deletes_inside() {
6575 let mut e = editor_with("a \"hello\" b");
6576 e.jump_cursor(0, 4);
6577 run_keys(&mut e, "di\"");
6578 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6579 }
6580
6581 #[test]
6582 fn da_double_quote_deletes_around() {
6583 let mut e = editor_with("a \"hello\" b");
6585 e.jump_cursor(0, 4);
6586 run_keys(&mut e, "da\"");
6587 assert_eq!(e.buffer().lines()[0], "a b");
6588 }
6589
6590 #[test]
6591 fn di_single_quote_deletes_inside() {
6592 let mut e = editor_with("x 'foo' y");
6593 e.jump_cursor(0, 4);
6594 run_keys(&mut e, "di'");
6595 assert_eq!(e.buffer().lines()[0], "x '' y");
6596 }
6597
6598 #[test]
6599 fn da_single_quote_deletes_around() {
6600 let mut e = editor_with("x 'foo' y");
6602 e.jump_cursor(0, 4);
6603 run_keys(&mut e, "da'");
6604 assert_eq!(e.buffer().lines()[0], "x y");
6605 }
6606
6607 #[test]
6608 fn di_backtick_deletes_inside() {
6609 let mut e = editor_with("p `q` r");
6610 e.jump_cursor(0, 3);
6611 run_keys(&mut e, "di`");
6612 assert_eq!(e.buffer().lines()[0], "p `` r");
6613 }
6614
6615 #[test]
6616 fn da_backtick_deletes_around() {
6617 let mut e = editor_with("p `q` r");
6619 e.jump_cursor(0, 3);
6620 run_keys(&mut e, "da`");
6621 assert_eq!(e.buffer().lines()[0], "p r");
6622 }
6623
6624 #[test]
6625 fn di_paren_deletes_inside() {
6626 let mut e = editor_with("f(arg)");
6627 e.jump_cursor(0, 3);
6628 run_keys(&mut e, "di(");
6629 assert_eq!(e.buffer().lines()[0], "f()");
6630 }
6631
6632 #[test]
6633 fn di_paren_alias_b_works() {
6634 let mut e = editor_with("f(arg)");
6635 e.jump_cursor(0, 3);
6636 run_keys(&mut e, "dib");
6637 assert_eq!(e.buffer().lines()[0], "f()");
6638 }
6639
6640 #[test]
6641 fn di_bracket_deletes_inside() {
6642 let mut e = editor_with("a[b,c]d");
6643 e.jump_cursor(0, 3);
6644 run_keys(&mut e, "di[");
6645 assert_eq!(e.buffer().lines()[0], "a[]d");
6646 }
6647
6648 #[test]
6649 fn da_bracket_deletes_around() {
6650 let mut e = editor_with("a[b,c]d");
6651 e.jump_cursor(0, 3);
6652 run_keys(&mut e, "da[");
6653 assert_eq!(e.buffer().lines()[0], "ad");
6654 }
6655
6656 #[test]
6657 fn di_brace_deletes_inside() {
6658 let mut e = editor_with("x{y}z");
6659 e.jump_cursor(0, 2);
6660 run_keys(&mut e, "di{");
6661 assert_eq!(e.buffer().lines()[0], "x{}z");
6662 }
6663
6664 #[test]
6665 fn da_brace_deletes_around() {
6666 let mut e = editor_with("x{y}z");
6667 e.jump_cursor(0, 2);
6668 run_keys(&mut e, "da{");
6669 assert_eq!(e.buffer().lines()[0], "xz");
6670 }
6671
6672 #[test]
6673 fn di_brace_alias_capital_b_works() {
6674 let mut e = editor_with("x{y}z");
6675 e.jump_cursor(0, 2);
6676 run_keys(&mut e, "diB");
6677 assert_eq!(e.buffer().lines()[0], "x{}z");
6678 }
6679
6680 #[test]
6681 fn di_angle_deletes_inside() {
6682 let mut e = editor_with("p<q>r");
6683 e.jump_cursor(0, 2);
6684 run_keys(&mut e, "di<lt>");
6686 assert_eq!(e.buffer().lines()[0], "p<>r");
6687 }
6688
6689 #[test]
6690 fn da_angle_deletes_around() {
6691 let mut e = editor_with("p<q>r");
6692 e.jump_cursor(0, 2);
6693 run_keys(&mut e, "da<lt>");
6694 assert_eq!(e.buffer().lines()[0], "pr");
6695 }
6696
6697 #[test]
6698 fn dip_deletes_inner_paragraph() {
6699 let mut e = editor_with("a\nb\nc\n\nd");
6700 e.jump_cursor(1, 0);
6701 run_keys(&mut e, "dip");
6702 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6705 }
6706
6707 #[test]
6710 fn sentence_motion_close_paren_jumps_forward() {
6711 let mut e = editor_with("Alpha. Beta. Gamma.");
6712 e.jump_cursor(0, 0);
6713 run_keys(&mut e, ")");
6714 assert_eq!(e.cursor(), (0, 7));
6716 run_keys(&mut e, ")");
6717 assert_eq!(e.cursor(), (0, 13));
6718 }
6719
6720 #[test]
6721 fn sentence_motion_open_paren_jumps_backward() {
6722 let mut e = editor_with("Alpha. Beta. Gamma.");
6723 e.jump_cursor(0, 13);
6724 run_keys(&mut e, "(");
6725 assert_eq!(e.cursor(), (0, 7));
6728 run_keys(&mut e, "(");
6729 assert_eq!(e.cursor(), (0, 0));
6730 }
6731
6732 #[test]
6733 fn sentence_motion_count() {
6734 let mut e = editor_with("A. B. C. D.");
6735 e.jump_cursor(0, 0);
6736 run_keys(&mut e, "3)");
6737 assert_eq!(e.cursor(), (0, 9));
6739 }
6740
6741 #[test]
6742 fn dis_deletes_inner_sentence() {
6743 let mut e = editor_with("First one. Second one. Third one.");
6744 e.jump_cursor(0, 13);
6745 run_keys(&mut e, "dis");
6746 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6748 }
6749
6750 #[test]
6751 fn das_deletes_around_sentence_with_trailing_space() {
6752 let mut e = editor_with("Alpha. Beta. Gamma.");
6753 e.jump_cursor(0, 8);
6754 run_keys(&mut e, "das");
6755 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6758 }
6759
6760 #[test]
6761 fn dis_handles_double_terminator() {
6762 let mut e = editor_with("Wow!? Next.");
6763 e.jump_cursor(0, 1);
6764 run_keys(&mut e, "dis");
6765 assert_eq!(e.buffer().lines()[0], " Next.");
6768 }
6769
6770 #[test]
6771 fn dis_first_sentence_from_cursor_at_zero() {
6772 let mut e = editor_with("Alpha. Beta.");
6773 e.jump_cursor(0, 0);
6774 run_keys(&mut e, "dis");
6775 assert_eq!(e.buffer().lines()[0], " Beta.");
6776 }
6777
6778 #[test]
6779 fn yis_yanks_inner_sentence() {
6780 let mut e = editor_with("Hello world. Bye.");
6781 e.jump_cursor(0, 5);
6782 run_keys(&mut e, "yis");
6783 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6784 }
6785
6786 #[test]
6787 fn vis_visually_selects_inner_sentence() {
6788 let mut e = editor_with("First. Second.");
6789 e.jump_cursor(0, 1);
6790 run_keys(&mut e, "vis");
6791 assert_eq!(e.vim_mode(), VimMode::Visual);
6792 run_keys(&mut e, "y");
6793 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6794 }
6795
6796 #[test]
6797 fn ciw_changes_inner_word() {
6798 let mut e = editor_with("hello world");
6799 e.jump_cursor(0, 1);
6800 run_keys(&mut e, "ciwHEY<Esc>");
6801 assert_eq!(e.buffer().lines()[0], "HEY world");
6802 }
6803
6804 #[test]
6805 fn yiw_yanks_inner_word() {
6806 let mut e = editor_with("hello world");
6807 e.jump_cursor(0, 1);
6808 run_keys(&mut e, "yiw");
6809 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6810 }
6811
6812 #[test]
6813 fn viw_selects_inner_word() {
6814 let mut e = editor_with("hello world");
6815 e.jump_cursor(0, 2);
6816 run_keys(&mut e, "viw");
6817 assert_eq!(e.vim_mode(), VimMode::Visual);
6818 run_keys(&mut e, "y");
6819 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6820 }
6821
6822 #[test]
6823 fn ci_paren_changes_inside() {
6824 let mut e = editor_with("f(old)");
6825 e.jump_cursor(0, 3);
6826 run_keys(&mut e, "ci(NEW<Esc>");
6827 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6828 }
6829
6830 #[test]
6831 fn yi_double_quote_yanks_inside() {
6832 let mut e = editor_with("say \"hi there\" then");
6833 e.jump_cursor(0, 6);
6834 run_keys(&mut e, "yi\"");
6835 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6836 }
6837
6838 #[test]
6839 fn vap_visual_selects_around_paragraph() {
6840 let mut e = editor_with("a\nb\n\nc");
6841 e.jump_cursor(0, 0);
6842 run_keys(&mut e, "vap");
6843 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6844 run_keys(&mut e, "y");
6845 let text = e.registers().read('"').unwrap().text.clone();
6847 assert!(text.starts_with("a\nb"));
6848 }
6849
6850 #[test]
6851 fn star_finds_next_occurrence() {
6852 let mut e = editor_with("foo bar foo baz");
6853 run_keys(&mut e, "*");
6854 assert_eq!(e.cursor().1, 8);
6855 }
6856
6857 #[test]
6858 fn star_skips_substring_match() {
6859 let mut e = editor_with("foo foobar baz");
6862 run_keys(&mut e, "*");
6863 assert_eq!(e.cursor().1, 0);
6864 }
6865
6866 #[test]
6867 fn g_star_matches_substring() {
6868 let mut e = editor_with("foo foobar baz");
6871 run_keys(&mut e, "g*");
6872 assert_eq!(e.cursor().1, 4);
6873 }
6874
6875 #[test]
6876 fn g_pound_matches_substring_backward() {
6877 let mut e = editor_with("foo foobar baz foo");
6880 run_keys(&mut e, "$b");
6881 assert_eq!(e.cursor().1, 15);
6882 run_keys(&mut e, "g#");
6883 assert_eq!(e.cursor().1, 4);
6884 }
6885
6886 #[test]
6887 fn n_repeats_last_search_forward() {
6888 let mut e = editor_with("foo bar foo baz foo");
6889 run_keys(&mut e, "/foo<CR>");
6892 assert_eq!(e.cursor().1, 8);
6893 run_keys(&mut e, "n");
6894 assert_eq!(e.cursor().1, 16);
6895 }
6896
6897 #[test]
6898 fn shift_n_reverses_search() {
6899 let mut e = editor_with("foo bar foo baz foo");
6900 run_keys(&mut e, "/foo<CR>");
6901 run_keys(&mut e, "n");
6902 assert_eq!(e.cursor().1, 16);
6903 run_keys(&mut e, "N");
6904 assert_eq!(e.cursor().1, 8);
6905 }
6906
6907 #[test]
6908 fn n_noop_without_pattern() {
6909 let mut e = editor_with("foo bar");
6910 run_keys(&mut e, "n");
6911 assert_eq!(e.cursor(), (0, 0));
6912 }
6913
6914 #[test]
6915 fn visual_line_preserves_cursor_column() {
6916 let mut e = editor_with("hello world\nanother one\nbye");
6919 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6921 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6922 assert_eq!(e.cursor(), (0, 5));
6923 run_keys(&mut e, "j");
6924 assert_eq!(e.cursor(), (1, 5));
6925 }
6926
6927 #[test]
6928 fn visual_line_yank_includes_trailing_newline() {
6929 let mut e = editor_with("aaa\nbbb\nccc");
6930 run_keys(&mut e, "Vjy");
6931 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6933 }
6934
6935 #[test]
6936 fn visual_line_yank_last_line_trailing_newline() {
6937 let mut e = editor_with("aaa\nbbb\nccc");
6938 run_keys(&mut e, "jj");
6940 run_keys(&mut e, "Vy");
6941 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6942 }
6943
6944 #[test]
6945 fn yy_on_last_line_has_trailing_newline() {
6946 let mut e = editor_with("aaa\nbbb\nccc");
6947 run_keys(&mut e, "jj");
6948 run_keys(&mut e, "yy");
6949 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6950 }
6951
6952 #[test]
6953 fn yy_in_middle_has_trailing_newline() {
6954 let mut e = editor_with("aaa\nbbb\nccc");
6955 run_keys(&mut e, "j");
6956 run_keys(&mut e, "yy");
6957 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6958 }
6959
6960 #[test]
6961 fn di_single_quote() {
6962 let mut e = editor_with("say 'hello world' now");
6963 e.jump_cursor(0, 7);
6964 run_keys(&mut e, "di'");
6965 assert_eq!(e.buffer().lines()[0], "say '' now");
6966 }
6967
6968 #[test]
6969 fn da_single_quote() {
6970 let mut e = editor_with("say 'hello' now");
6972 e.jump_cursor(0, 7);
6973 run_keys(&mut e, "da'");
6974 assert_eq!(e.buffer().lines()[0], "say now");
6975 }
6976
6977 #[test]
6978 fn di_backtick() {
6979 let mut e = editor_with("say `hi` now");
6980 e.jump_cursor(0, 5);
6981 run_keys(&mut e, "di`");
6982 assert_eq!(e.buffer().lines()[0], "say `` now");
6983 }
6984
6985 #[test]
6986 fn di_brace() {
6987 let mut e = editor_with("fn { a; b; c }");
6988 e.jump_cursor(0, 7);
6989 run_keys(&mut e, "di{");
6990 assert_eq!(e.buffer().lines()[0], "fn {}");
6991 }
6992
6993 #[test]
6994 fn di_bracket() {
6995 let mut e = editor_with("arr[1, 2, 3]");
6996 e.jump_cursor(0, 5);
6997 run_keys(&mut e, "di[");
6998 assert_eq!(e.buffer().lines()[0], "arr[]");
6999 }
7000
7001 #[test]
7002 fn dab_deletes_around_paren() {
7003 let mut e = editor_with("fn(a, b) + 1");
7004 e.jump_cursor(0, 4);
7005 run_keys(&mut e, "dab");
7006 assert_eq!(e.buffer().lines()[0], "fn + 1");
7007 }
7008
7009 #[test]
7010 fn da_big_b_deletes_around_brace() {
7011 let mut e = editor_with("x = {a: 1}");
7012 e.jump_cursor(0, 6);
7013 run_keys(&mut e, "daB");
7014 assert_eq!(e.buffer().lines()[0], "x = ");
7015 }
7016
7017 #[test]
7018 fn di_big_w_deletes_bigword() {
7019 let mut e = editor_with("foo-bar baz");
7020 e.jump_cursor(0, 2);
7021 run_keys(&mut e, "diW");
7022 assert_eq!(e.buffer().lines()[0], " baz");
7023 }
7024
7025 #[test]
7026 fn visual_select_inner_word() {
7027 let mut e = editor_with("hello world");
7028 e.jump_cursor(0, 2);
7029 run_keys(&mut e, "viw");
7030 assert_eq!(e.vim_mode(), VimMode::Visual);
7031 run_keys(&mut e, "y");
7032 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7033 }
7034
7035 #[test]
7036 fn visual_select_inner_quote() {
7037 let mut e = editor_with("foo \"bar\" baz");
7038 e.jump_cursor(0, 6);
7039 run_keys(&mut e, "vi\"");
7040 run_keys(&mut e, "y");
7041 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7042 }
7043
7044 #[test]
7045 fn visual_select_inner_paren() {
7046 let mut e = editor_with("fn(a, b)");
7047 e.jump_cursor(0, 4);
7048 run_keys(&mut e, "vi(");
7049 run_keys(&mut e, "y");
7050 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7051 }
7052
7053 #[test]
7054 fn visual_select_outer_brace() {
7055 let mut e = editor_with("{x}");
7056 e.jump_cursor(0, 1);
7057 run_keys(&mut e, "va{");
7058 run_keys(&mut e, "y");
7059 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7060 }
7061
7062 #[test]
7063 fn ci_paren_forward_scans_when_cursor_before_pair() {
7064 let mut e = editor_with("foo(bar)");
7067 e.jump_cursor(0, 0);
7068 run_keys(&mut e, "ci(NEW<Esc>");
7069 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7070 }
7071
7072 #[test]
7073 fn ci_paren_forward_scans_across_lines() {
7074 let mut e = editor_with("first\nfoo(bar)\nlast");
7075 e.jump_cursor(0, 0);
7076 run_keys(&mut e, "ci(NEW<Esc>");
7077 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7078 }
7079
7080 #[test]
7081 fn ci_brace_forward_scans_when_cursor_before_pair() {
7082 let mut e = editor_with("let x = {y};");
7083 e.jump_cursor(0, 0);
7084 run_keys(&mut e, "ci{NEW<Esc>");
7085 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7086 }
7087
7088 #[test]
7089 fn cit_forward_scans_when_cursor_before_tag() {
7090 let mut e = editor_with("text <b>hello</b> rest");
7093 e.jump_cursor(0, 0);
7094 run_keys(&mut e, "citNEW<Esc>");
7095 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7096 }
7097
7098 #[test]
7099 fn dat_forward_scans_when_cursor_before_tag() {
7100 let mut e = editor_with("text <b>hello</b> rest");
7102 e.jump_cursor(0, 0);
7103 run_keys(&mut e, "dat");
7104 assert_eq!(e.buffer().lines()[0], "text rest");
7105 }
7106
7107 #[test]
7108 fn ci_paren_still_works_when_cursor_inside() {
7109 let mut e = editor_with("fn(a, b)");
7112 e.jump_cursor(0, 4);
7113 run_keys(&mut e, "ci(NEW<Esc>");
7114 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7115 }
7116
7117 #[test]
7118 fn caw_changes_word_with_trailing_space() {
7119 let mut e = editor_with("hello world");
7120 run_keys(&mut e, "cawfoo<Esc>");
7121 assert_eq!(e.buffer().lines()[0], "fooworld");
7122 }
7123
7124 #[test]
7125 fn visual_char_yank_preserves_raw_text() {
7126 let mut e = editor_with("hello world");
7127 run_keys(&mut e, "vllly");
7128 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7129 }
7130
7131 #[test]
7132 fn single_line_visual_line_selects_full_line_on_yank() {
7133 let mut e = editor_with("hello world\nbye");
7134 run_keys(&mut e, "V");
7135 run_keys(&mut e, "y");
7138 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7139 }
7140
7141 #[test]
7142 fn visual_line_extends_both_directions() {
7143 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7144 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7146 assert_eq!(e.cursor(), (3, 0));
7147 run_keys(&mut e, "k");
7148 assert_eq!(e.cursor(), (2, 0));
7150 run_keys(&mut e, "k");
7151 assert_eq!(e.cursor(), (1, 0));
7152 }
7153
7154 #[test]
7155 fn visual_char_preserves_cursor_column() {
7156 let mut e = editor_with("hello world");
7157 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7159 assert_eq!(e.cursor(), (0, 5));
7160 run_keys(&mut e, "ll");
7161 assert_eq!(e.cursor(), (0, 7));
7162 }
7163
7164 #[test]
7165 fn visual_char_highlight_bounds_order() {
7166 let mut e = editor_with("abcdef");
7167 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7169 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7172 }
7173
7174 #[test]
7175 fn visual_line_highlight_bounds() {
7176 let mut e = editor_with("a\nb\nc");
7177 run_keys(&mut e, "V");
7178 assert_eq!(e.line_highlight(), Some((0, 0)));
7179 run_keys(&mut e, "j");
7180 assert_eq!(e.line_highlight(), Some((0, 1)));
7181 run_keys(&mut e, "j");
7182 assert_eq!(e.line_highlight(), Some((0, 2)));
7183 }
7184
7185 #[test]
7188 fn h_moves_left() {
7189 let mut e = editor_with("hello");
7190 e.jump_cursor(0, 3);
7191 run_keys(&mut e, "h");
7192 assert_eq!(e.cursor(), (0, 2));
7193 }
7194
7195 #[test]
7196 fn l_moves_right() {
7197 let mut e = editor_with("hello");
7198 run_keys(&mut e, "l");
7199 assert_eq!(e.cursor(), (0, 1));
7200 }
7201
7202 #[test]
7203 fn k_moves_up() {
7204 let mut e = editor_with("a\nb\nc");
7205 e.jump_cursor(2, 0);
7206 run_keys(&mut e, "k");
7207 assert_eq!(e.cursor(), (1, 0));
7208 }
7209
7210 #[test]
7211 fn zero_moves_to_line_start() {
7212 let mut e = editor_with(" hello");
7213 run_keys(&mut e, "$");
7214 run_keys(&mut e, "0");
7215 assert_eq!(e.cursor().1, 0);
7216 }
7217
7218 #[test]
7219 fn caret_moves_to_first_non_blank() {
7220 let mut e = editor_with(" hello");
7221 run_keys(&mut e, "0");
7222 run_keys(&mut e, "^");
7223 assert_eq!(e.cursor().1, 4);
7224 }
7225
7226 #[test]
7227 fn dollar_moves_to_last_char() {
7228 let mut e = editor_with("hello");
7229 run_keys(&mut e, "$");
7230 assert_eq!(e.cursor().1, 4);
7231 }
7232
7233 #[test]
7234 fn dollar_on_empty_line_stays_at_col_zero() {
7235 let mut e = editor_with("");
7236 run_keys(&mut e, "$");
7237 assert_eq!(e.cursor().1, 0);
7238 }
7239
7240 #[test]
7241 fn w_jumps_to_next_word() {
7242 let mut e = editor_with("foo bar baz");
7243 run_keys(&mut e, "w");
7244 assert_eq!(e.cursor().1, 4);
7245 }
7246
7247 #[test]
7248 fn b_jumps_back_a_word() {
7249 let mut e = editor_with("foo bar");
7250 e.jump_cursor(0, 6);
7251 run_keys(&mut e, "b");
7252 assert_eq!(e.cursor().1, 4);
7253 }
7254
7255 #[test]
7256 fn e_jumps_to_word_end() {
7257 let mut e = editor_with("foo bar");
7258 run_keys(&mut e, "e");
7259 assert_eq!(e.cursor().1, 2);
7260 }
7261
7262 #[test]
7265 fn d_dollar_deletes_to_eol() {
7266 let mut e = editor_with("hello world");
7267 e.jump_cursor(0, 5);
7268 run_keys(&mut e, "d$");
7269 assert_eq!(e.buffer().lines()[0], "hello");
7270 }
7271
7272 #[test]
7273 fn d_zero_deletes_to_line_start() {
7274 let mut e = editor_with("hello world");
7275 e.jump_cursor(0, 6);
7276 run_keys(&mut e, "d0");
7277 assert_eq!(e.buffer().lines()[0], "world");
7278 }
7279
7280 #[test]
7281 fn d_caret_deletes_to_first_non_blank() {
7282 let mut e = editor_with(" hello");
7283 e.jump_cursor(0, 6);
7284 run_keys(&mut e, "d^");
7285 assert_eq!(e.buffer().lines()[0], " llo");
7286 }
7287
7288 #[test]
7289 fn d_capital_g_deletes_to_end_of_file() {
7290 let mut e = editor_with("a\nb\nc\nd");
7291 e.jump_cursor(1, 0);
7292 run_keys(&mut e, "dG");
7293 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7294 }
7295
7296 #[test]
7297 fn d_gg_deletes_to_start_of_file() {
7298 let mut e = editor_with("a\nb\nc\nd");
7299 e.jump_cursor(2, 0);
7300 run_keys(&mut e, "dgg");
7301 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7302 }
7303
7304 #[test]
7305 fn cw_is_ce_quirk() {
7306 let mut e = editor_with("foo bar");
7309 run_keys(&mut e, "cwxyz<Esc>");
7310 assert_eq!(e.buffer().lines()[0], "xyz bar");
7311 }
7312
7313 #[test]
7316 fn big_d_deletes_to_eol() {
7317 let mut e = editor_with("hello world");
7318 e.jump_cursor(0, 5);
7319 run_keys(&mut e, "D");
7320 assert_eq!(e.buffer().lines()[0], "hello");
7321 }
7322
7323 #[test]
7324 fn big_c_deletes_to_eol_and_inserts() {
7325 let mut e = editor_with("hello world");
7326 e.jump_cursor(0, 5);
7327 run_keys(&mut e, "C!<Esc>");
7328 assert_eq!(e.buffer().lines()[0], "hello!");
7329 }
7330
7331 #[test]
7332 fn j_joins_next_line_with_space() {
7333 let mut e = editor_with("hello\nworld");
7334 run_keys(&mut e, "J");
7335 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7336 }
7337
7338 #[test]
7339 fn j_strips_leading_whitespace_on_join() {
7340 let mut e = editor_with("hello\n world");
7341 run_keys(&mut e, "J");
7342 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7343 }
7344
7345 #[test]
7346 fn big_x_deletes_char_before_cursor() {
7347 let mut e = editor_with("hello");
7348 e.jump_cursor(0, 3);
7349 run_keys(&mut e, "X");
7350 assert_eq!(e.buffer().lines()[0], "helo");
7351 }
7352
7353 #[test]
7354 fn s_substitutes_char_and_enters_insert() {
7355 let mut e = editor_with("hello");
7356 run_keys(&mut e, "sX<Esc>");
7357 assert_eq!(e.buffer().lines()[0], "Xello");
7358 }
7359
7360 #[test]
7361 fn count_x_deletes_many() {
7362 let mut e = editor_with("abcdef");
7363 run_keys(&mut e, "3x");
7364 assert_eq!(e.buffer().lines()[0], "def");
7365 }
7366
7367 #[test]
7370 fn p_pastes_charwise_after_cursor() {
7371 let mut e = editor_with("hello");
7372 run_keys(&mut e, "yw");
7373 run_keys(&mut e, "$p");
7374 assert_eq!(e.buffer().lines()[0], "hellohello");
7375 }
7376
7377 #[test]
7378 fn capital_p_pastes_charwise_before_cursor() {
7379 let mut e = editor_with("hello");
7380 run_keys(&mut e, "v");
7382 run_keys(&mut e, "l");
7383 run_keys(&mut e, "y");
7384 run_keys(&mut e, "$P");
7385 assert_eq!(e.buffer().lines()[0], "hellheo");
7388 }
7389
7390 #[test]
7391 fn p_pastes_linewise_below() {
7392 let mut e = editor_with("one\ntwo\nthree");
7393 run_keys(&mut e, "yy");
7394 run_keys(&mut e, "p");
7395 assert_eq!(
7396 e.buffer().lines(),
7397 &[
7398 "one".to_string(),
7399 "one".to_string(),
7400 "two".to_string(),
7401 "three".to_string()
7402 ]
7403 );
7404 }
7405
7406 #[test]
7407 fn capital_p_pastes_linewise_above() {
7408 let mut e = editor_with("one\ntwo");
7409 e.jump_cursor(1, 0);
7410 run_keys(&mut e, "yy");
7411 run_keys(&mut e, "P");
7412 assert_eq!(
7413 e.buffer().lines(),
7414 &["one".to_string(), "two".to_string(), "two".to_string()]
7415 );
7416 }
7417
7418 #[test]
7421 fn hash_finds_previous_occurrence() {
7422 let mut e = editor_with("foo bar foo baz foo");
7423 e.jump_cursor(0, 16);
7425 run_keys(&mut e, "#");
7426 assert_eq!(e.cursor().1, 8);
7427 }
7428
7429 #[test]
7432 fn visual_line_delete_removes_full_lines() {
7433 let mut e = editor_with("a\nb\nc\nd");
7434 run_keys(&mut e, "Vjd");
7435 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7436 }
7437
7438 #[test]
7439 fn visual_line_change_leaves_blank_line() {
7440 let mut e = editor_with("a\nb\nc");
7441 run_keys(&mut e, "Vjc");
7442 assert_eq!(e.vim_mode(), VimMode::Insert);
7443 run_keys(&mut e, "X<Esc>");
7444 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7448 }
7449
7450 #[test]
7451 fn cc_leaves_blank_line() {
7452 let mut e = editor_with("a\nb\nc");
7453 e.jump_cursor(1, 0);
7454 run_keys(&mut e, "ccX<Esc>");
7455 assert_eq!(
7456 e.buffer().lines(),
7457 &["a".to_string(), "X".to_string(), "c".to_string()]
7458 );
7459 }
7460
7461 #[test]
7466 fn big_w_skips_hyphens() {
7467 let mut e = editor_with("foo-bar baz");
7469 run_keys(&mut e, "W");
7470 assert_eq!(e.cursor().1, 8);
7471 }
7472
7473 #[test]
7474 fn big_w_crosses_lines() {
7475 let mut e = editor_with("foo-bar\nbaz-qux");
7476 run_keys(&mut e, "W");
7477 assert_eq!(e.cursor(), (1, 0));
7478 }
7479
7480 #[test]
7481 fn big_b_skips_hyphens() {
7482 let mut e = editor_with("foo-bar baz");
7483 e.jump_cursor(0, 9);
7484 run_keys(&mut e, "B");
7485 assert_eq!(e.cursor().1, 8);
7486 run_keys(&mut e, "B");
7487 assert_eq!(e.cursor().1, 0);
7488 }
7489
7490 #[test]
7491 fn big_e_jumps_to_big_word_end() {
7492 let mut e = editor_with("foo-bar baz");
7493 run_keys(&mut e, "E");
7494 assert_eq!(e.cursor().1, 6);
7495 run_keys(&mut e, "E");
7496 assert_eq!(e.cursor().1, 10);
7497 }
7498
7499 #[test]
7500 fn dw_with_big_word_variant() {
7501 let mut e = editor_with("foo-bar baz");
7503 run_keys(&mut e, "dW");
7504 assert_eq!(e.buffer().lines()[0], "baz");
7505 }
7506
7507 #[test]
7510 fn insert_ctrl_w_deletes_word_back() {
7511 let mut e = editor_with("");
7512 run_keys(&mut e, "i");
7513 for c in "hello world".chars() {
7514 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7515 }
7516 run_keys(&mut e, "<C-w>");
7517 assert_eq!(e.buffer().lines()[0], "hello ");
7518 }
7519
7520 #[test]
7521 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7522 let mut e = editor_with("hello\nworld");
7526 e.jump_cursor(1, 0);
7527 run_keys(&mut e, "i");
7528 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7529 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7532 assert_eq!(e.cursor(), (0, 0));
7533 }
7534
7535 #[test]
7536 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7537 let mut e = editor_with("foo bar\nbaz");
7538 e.jump_cursor(1, 0);
7539 run_keys(&mut e, "i");
7540 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7541 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7543 assert_eq!(e.cursor(), (0, 4));
7544 }
7545
7546 #[test]
7547 fn insert_ctrl_u_deletes_to_line_start() {
7548 let mut e = editor_with("");
7549 run_keys(&mut e, "i");
7550 for c in "hello world".chars() {
7551 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7552 }
7553 run_keys(&mut e, "<C-u>");
7554 assert_eq!(e.buffer().lines()[0], "");
7555 }
7556
7557 #[test]
7558 fn insert_ctrl_o_runs_one_normal_command() {
7559 let mut e = editor_with("hello world");
7560 run_keys(&mut e, "A");
7562 assert_eq!(e.vim_mode(), VimMode::Insert);
7563 e.jump_cursor(0, 0);
7565 run_keys(&mut e, "<C-o>");
7566 assert_eq!(e.vim_mode(), VimMode::Normal);
7567 run_keys(&mut e, "dw");
7568 assert_eq!(e.vim_mode(), VimMode::Insert);
7570 assert_eq!(e.buffer().lines()[0], "world");
7571 }
7572
7573 #[test]
7576 fn j_through_empty_line_preserves_column() {
7577 let mut e = editor_with("hello world\n\nanother line");
7578 run_keys(&mut e, "llllll");
7580 assert_eq!(e.cursor(), (0, 6));
7581 run_keys(&mut e, "j");
7584 assert_eq!(e.cursor(), (1, 0));
7585 run_keys(&mut e, "j");
7587 assert_eq!(e.cursor(), (2, 6));
7588 }
7589
7590 #[test]
7591 fn j_through_shorter_line_preserves_column() {
7592 let mut e = editor_with("hello world\nhi\nanother line");
7593 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7596 run_keys(&mut e, "j");
7597 assert_eq!(e.cursor(), (2, 7));
7598 }
7599
7600 #[test]
7601 fn esc_from_insert_sticky_matches_visible_cursor() {
7602 let mut e = editor_with(" this is a line\n another one of a similar size");
7606 e.jump_cursor(0, 12);
7607 run_keys(&mut e, "I");
7608 assert_eq!(e.cursor(), (0, 4));
7609 run_keys(&mut e, "X<Esc>");
7610 assert_eq!(e.cursor(), (0, 4));
7611 run_keys(&mut e, "j");
7612 assert_eq!(e.cursor(), (1, 4));
7613 }
7614
7615 #[test]
7616 fn esc_from_insert_sticky_tracks_inserted_chars() {
7617 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7618 run_keys(&mut e, "i");
7619 run_keys(&mut e, "abc<Esc>");
7620 assert_eq!(e.cursor(), (0, 2));
7621 run_keys(&mut e, "j");
7622 assert_eq!(e.cursor(), (1, 2));
7623 }
7624
7625 #[test]
7626 fn esc_from_insert_sticky_tracks_arrow_nav() {
7627 let mut e = editor_with("xxxxxx\nyyyyyy");
7628 run_keys(&mut e, "i");
7629 run_keys(&mut e, "abc");
7630 for _ in 0..2 {
7631 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7632 }
7633 run_keys(&mut e, "<Esc>");
7634 assert_eq!(e.cursor(), (0, 0));
7635 run_keys(&mut e, "j");
7636 assert_eq!(e.cursor(), (1, 0));
7637 }
7638
7639 #[test]
7640 fn esc_from_insert_at_col_14_followed_by_j() {
7641 let line = "x".repeat(30);
7644 let buf = format!("{line}\n{line}");
7645 let mut e = editor_with(&buf);
7646 e.jump_cursor(0, 14);
7647 run_keys(&mut e, "i");
7648 for c in "test ".chars() {
7649 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7650 }
7651 run_keys(&mut e, "<Esc>");
7652 assert_eq!(e.cursor(), (0, 18));
7653 run_keys(&mut e, "j");
7654 assert_eq!(e.cursor(), (1, 18));
7655 }
7656
7657 #[test]
7658 fn linewise_paste_resets_sticky_column() {
7659 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7663 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7665 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7669 run_keys(&mut e, "j");
7671 assert_eq!(e.cursor(), (3, 2));
7672 }
7673
7674 #[test]
7675 fn horizontal_motion_resyncs_sticky_column() {
7676 let mut e = editor_with("hello world\n\nanother line");
7680 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7683 assert_eq!(e.cursor(), (2, 3));
7684 }
7685
7686 #[test]
7689 fn ctrl_v_enters_visual_block() {
7690 let mut e = editor_with("aaa\nbbb\nccc");
7691 run_keys(&mut e, "<C-v>");
7692 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7693 }
7694
7695 #[test]
7696 fn visual_block_esc_returns_to_normal() {
7697 let mut e = editor_with("aaa\nbbb\nccc");
7698 run_keys(&mut e, "<C-v>");
7699 run_keys(&mut e, "<Esc>");
7700 assert_eq!(e.vim_mode(), VimMode::Normal);
7701 }
7702
7703 #[test]
7704 fn backtick_lt_jumps_to_visual_start_mark() {
7705 let mut e = editor_with("foo bar baz\n");
7709 run_keys(&mut e, "v");
7710 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7713 run_keys(&mut e, "`<lt>");
7715 assert_eq!(e.cursor(), (0, 0));
7716 }
7717
7718 #[test]
7719 fn backtick_gt_jumps_to_visual_end_mark() {
7720 let mut e = editor_with("foo bar baz\n");
7721 run_keys(&mut e, "v");
7722 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7724 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7726 assert_eq!(e.cursor(), (0, 4));
7727 }
7728
7729 #[test]
7730 fn visual_exit_sets_lt_gt_marks() {
7731 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7734 run_keys(&mut e, "V");
7736 run_keys(&mut e, "j");
7737 run_keys(&mut e, "<Esc>");
7738 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7739 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7740 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7741 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7742 }
7743
7744 #[test]
7745 fn visual_exit_marks_use_lower_higher_order() {
7746 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7750 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7752 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7754 let lt = e.mark('<').unwrap();
7755 let gt = e.mark('>').unwrap();
7756 assert_eq!(lt.0, 2);
7757 assert_eq!(gt.0, 3);
7758 }
7759
7760 #[test]
7761 fn visualline_exit_marks_snap_to_line_edges() {
7762 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7764 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7766 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7768 let lt = e.mark('<').unwrap();
7769 let gt = e.mark('>').unwrap();
7770 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7771 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7773 }
7774
7775 #[test]
7776 fn visualblock_exit_marks_use_block_corners() {
7777 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7781 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7783 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7786 let lt = e.mark('<').unwrap();
7787 let gt = e.mark('>').unwrap();
7788 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7790 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7791 }
7792
7793 #[test]
7794 fn visual_block_delete_removes_column_range() {
7795 let mut e = editor_with("hello\nworld\nhappy");
7796 run_keys(&mut e, "l");
7798 run_keys(&mut e, "<C-v>");
7799 run_keys(&mut e, "jj");
7800 run_keys(&mut e, "ll");
7801 run_keys(&mut e, "d");
7802 assert_eq!(
7804 e.buffer().lines(),
7805 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7806 );
7807 }
7808
7809 #[test]
7810 fn visual_block_yank_joins_with_newlines() {
7811 let mut e = editor_with("hello\nworld\nhappy");
7812 run_keys(&mut e, "<C-v>");
7813 run_keys(&mut e, "jj");
7814 run_keys(&mut e, "ll");
7815 run_keys(&mut e, "y");
7816 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7817 }
7818
7819 #[test]
7820 fn visual_block_replace_fills_block() {
7821 let mut e = editor_with("hello\nworld\nhappy");
7822 run_keys(&mut e, "<C-v>");
7823 run_keys(&mut e, "jj");
7824 run_keys(&mut e, "ll");
7825 run_keys(&mut e, "rx");
7826 assert_eq!(
7827 e.buffer().lines(),
7828 &[
7829 "xxxlo".to_string(),
7830 "xxxld".to_string(),
7831 "xxxpy".to_string()
7832 ]
7833 );
7834 }
7835
7836 #[test]
7837 fn visual_block_insert_repeats_across_rows() {
7838 let mut e = editor_with("hello\nworld\nhappy");
7839 run_keys(&mut e, "<C-v>");
7840 run_keys(&mut e, "jj");
7841 run_keys(&mut e, "I");
7842 run_keys(&mut e, "# <Esc>");
7843 assert_eq!(
7844 e.buffer().lines(),
7845 &[
7846 "# hello".to_string(),
7847 "# world".to_string(),
7848 "# happy".to_string()
7849 ]
7850 );
7851 }
7852
7853 #[test]
7854 fn block_highlight_returns_none_outside_block_mode() {
7855 let mut e = editor_with("abc");
7856 assert!(e.block_highlight().is_none());
7857 run_keys(&mut e, "v");
7858 assert!(e.block_highlight().is_none());
7859 run_keys(&mut e, "<Esc>V");
7860 assert!(e.block_highlight().is_none());
7861 }
7862
7863 #[test]
7864 fn block_highlight_bounds_track_anchor_and_cursor() {
7865 let mut e = editor_with("aaaa\nbbbb\ncccc");
7866 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7868 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7871 }
7872
7873 #[test]
7874 fn visual_block_delete_handles_short_lines() {
7875 let mut e = editor_with("hello\nhi\nworld");
7877 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7879 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7881 assert_eq!(
7886 e.buffer().lines(),
7887 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7888 );
7889 }
7890
7891 #[test]
7892 fn visual_block_yank_pads_short_lines_with_empties() {
7893 let mut e = editor_with("hello\nhi\nworld");
7894 run_keys(&mut e, "l");
7895 run_keys(&mut e, "<C-v>");
7896 run_keys(&mut e, "jjll");
7897 run_keys(&mut e, "y");
7898 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7900 }
7901
7902 #[test]
7903 fn visual_block_replace_skips_past_eol() {
7904 let mut e = editor_with("ab\ncd\nef");
7907 run_keys(&mut e, "l");
7909 run_keys(&mut e, "<C-v>");
7910 run_keys(&mut e, "jjllllll");
7911 run_keys(&mut e, "rX");
7912 assert_eq!(
7915 e.buffer().lines(),
7916 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7917 );
7918 }
7919
7920 #[test]
7921 fn visual_block_with_empty_line_in_middle() {
7922 let mut e = editor_with("abcd\n\nefgh");
7923 run_keys(&mut e, "<C-v>");
7924 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7926 assert_eq!(
7929 e.buffer().lines(),
7930 &["d".to_string(), "".to_string(), "h".to_string()]
7931 );
7932 }
7933
7934 #[test]
7935 fn block_insert_pads_empty_lines_to_block_column() {
7936 let mut e = editor_with("this is a line\n\nthis is a line");
7939 e.jump_cursor(0, 3);
7940 run_keys(&mut e, "<C-v>");
7941 run_keys(&mut e, "jj");
7942 run_keys(&mut e, "I");
7943 run_keys(&mut e, "XX<Esc>");
7944 assert_eq!(
7945 e.buffer().lines(),
7946 &[
7947 "thiXXs is a line".to_string(),
7948 " XX".to_string(),
7949 "thiXXs is a line".to_string()
7950 ]
7951 );
7952 }
7953
7954 #[test]
7955 fn block_insert_pads_short_lines_to_block_column() {
7956 let mut e = editor_with("aaaaa\nbb\naaaaa");
7957 e.jump_cursor(0, 3);
7958 run_keys(&mut e, "<C-v>");
7959 run_keys(&mut e, "jj");
7960 run_keys(&mut e, "I");
7961 run_keys(&mut e, "Y<Esc>");
7962 assert_eq!(
7964 e.buffer().lines(),
7965 &[
7966 "aaaYaa".to_string(),
7967 "bb Y".to_string(),
7968 "aaaYaa".to_string()
7969 ]
7970 );
7971 }
7972
7973 #[test]
7974 fn visual_block_append_repeats_across_rows() {
7975 let mut e = editor_with("foo\nbar\nbaz");
7976 run_keys(&mut e, "<C-v>");
7977 run_keys(&mut e, "jj");
7978 run_keys(&mut e, "A");
7981 run_keys(&mut e, "!<Esc>");
7982 assert_eq!(
7983 e.buffer().lines(),
7984 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7985 );
7986 }
7987
7988 #[test]
7991 fn slash_opens_forward_search_prompt() {
7992 let mut e = editor_with("hello world");
7993 run_keys(&mut e, "/");
7994 let p = e.search_prompt().expect("prompt should be active");
7995 assert!(p.text.is_empty());
7996 assert!(p.forward);
7997 }
7998
7999 #[test]
8000 fn question_opens_backward_search_prompt() {
8001 let mut e = editor_with("hello world");
8002 run_keys(&mut e, "?");
8003 let p = e.search_prompt().expect("prompt should be active");
8004 assert!(!p.forward);
8005 }
8006
8007 #[test]
8008 fn search_prompt_typing_updates_pattern_live() {
8009 let mut e = editor_with("foo bar\nbaz");
8010 run_keys(&mut e, "/bar");
8011 assert_eq!(e.search_prompt().unwrap().text, "bar");
8012 assert!(e.search_state().pattern.is_some());
8014 }
8015
8016 #[test]
8017 fn search_prompt_backspace_and_enter() {
8018 let mut e = editor_with("hello world\nagain");
8019 run_keys(&mut e, "/worlx");
8020 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8021 assert_eq!(e.search_prompt().unwrap().text, "worl");
8022 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8023 assert!(e.search_prompt().is_none());
8025 assert_eq!(e.last_search(), Some("worl"));
8026 assert_eq!(e.cursor(), (0, 6));
8027 }
8028
8029 #[test]
8030 fn empty_search_prompt_enter_repeats_last_search() {
8031 let mut e = editor_with("foo bar foo baz foo");
8032 run_keys(&mut e, "/foo");
8033 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8034 assert_eq!(e.cursor().1, 8);
8035 run_keys(&mut e, "/");
8037 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8038 assert_eq!(e.cursor().1, 16);
8039 assert_eq!(e.last_search(), Some("foo"));
8040 }
8041
8042 #[test]
8043 fn search_history_records_committed_patterns() {
8044 let mut e = editor_with("alpha beta gamma");
8045 run_keys(&mut e, "/alpha<CR>");
8046 run_keys(&mut e, "/beta<CR>");
8047 let history = e.vim.search_history.clone();
8049 assert_eq!(history, vec!["alpha", "beta"]);
8050 }
8051
8052 #[test]
8053 fn search_history_dedupes_consecutive_repeats() {
8054 let mut e = editor_with("foo bar foo");
8055 run_keys(&mut e, "/foo<CR>");
8056 run_keys(&mut e, "/foo<CR>");
8057 run_keys(&mut e, "/bar<CR>");
8058 run_keys(&mut e, "/bar<CR>");
8059 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8061 }
8062
8063 #[test]
8064 fn ctrl_p_walks_history_backward() {
8065 let mut e = editor_with("alpha beta gamma");
8066 run_keys(&mut e, "/alpha<CR>");
8067 run_keys(&mut e, "/beta<CR>");
8068 run_keys(&mut e, "/");
8070 assert_eq!(e.search_prompt().unwrap().text, "");
8071 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8072 assert_eq!(e.search_prompt().unwrap().text, "beta");
8073 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8074 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8075 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8077 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8078 }
8079
8080 #[test]
8081 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8082 let mut e = editor_with("a b c");
8083 run_keys(&mut e, "/a<CR>");
8084 run_keys(&mut e, "/b<CR>");
8085 run_keys(&mut e, "/c<CR>");
8086 run_keys(&mut e, "/");
8087 for _ in 0..3 {
8089 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8090 }
8091 assert_eq!(e.search_prompt().unwrap().text, "a");
8092 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8093 assert_eq!(e.search_prompt().unwrap().text, "b");
8094 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8095 assert_eq!(e.search_prompt().unwrap().text, "c");
8096 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8098 assert_eq!(e.search_prompt().unwrap().text, "c");
8099 }
8100
8101 #[test]
8102 fn typing_after_history_walk_resets_cursor() {
8103 let mut e = editor_with("foo");
8104 run_keys(&mut e, "/foo<CR>");
8105 run_keys(&mut e, "/");
8106 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8107 assert_eq!(e.search_prompt().unwrap().text, "foo");
8108 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8111 assert_eq!(e.search_prompt().unwrap().text, "foox");
8112 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8113 assert_eq!(e.search_prompt().unwrap().text, "foo");
8114 }
8115
8116 #[test]
8117 fn empty_backward_search_prompt_enter_repeats_last_search() {
8118 let mut e = editor_with("foo bar foo baz foo");
8119 run_keys(&mut e, "/foo");
8121 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8122 assert_eq!(e.cursor().1, 8);
8123 run_keys(&mut e, "?");
8124 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8125 assert_eq!(e.cursor().1, 0);
8126 assert_eq!(e.last_search(), Some("foo"));
8127 }
8128
8129 #[test]
8130 fn search_prompt_esc_cancels_but_keeps_last_search() {
8131 let mut e = editor_with("foo bar\nbaz");
8132 run_keys(&mut e, "/bar");
8133 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8134 assert!(e.search_prompt().is_none());
8135 assert_eq!(e.last_search(), Some("bar"));
8136 }
8137
8138 #[test]
8139 fn search_then_n_and_shift_n_navigate() {
8140 let mut e = editor_with("foo bar foo baz foo");
8141 run_keys(&mut e, "/foo");
8142 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8143 assert_eq!(e.cursor().1, 8);
8145 run_keys(&mut e, "n");
8146 assert_eq!(e.cursor().1, 16);
8147 run_keys(&mut e, "N");
8148 assert_eq!(e.cursor().1, 8);
8149 }
8150
8151 #[test]
8152 fn question_mark_searches_backward_on_enter() {
8153 let mut e = editor_with("foo bar foo baz");
8154 e.jump_cursor(0, 10);
8155 run_keys(&mut e, "?foo");
8156 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8157 assert_eq!(e.cursor(), (0, 8));
8159 }
8160
8161 #[test]
8164 fn big_y_yanks_to_end_of_line() {
8165 let mut e = editor_with("hello world");
8166 e.jump_cursor(0, 6);
8167 run_keys(&mut e, "Y");
8168 assert_eq!(e.last_yank.as_deref(), Some("world"));
8169 }
8170
8171 #[test]
8172 fn big_y_from_line_start_yanks_full_line() {
8173 let mut e = editor_with("hello world");
8174 run_keys(&mut e, "Y");
8175 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8176 }
8177
8178 #[test]
8179 fn gj_joins_without_inserting_space() {
8180 let mut e = editor_with("hello\n world");
8181 run_keys(&mut e, "gJ");
8182 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8184 }
8185
8186 #[test]
8187 fn gj_noop_on_last_line() {
8188 let mut e = editor_with("only");
8189 run_keys(&mut e, "gJ");
8190 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8191 }
8192
8193 #[test]
8194 fn ge_jumps_to_previous_word_end() {
8195 let mut e = editor_with("foo bar baz");
8196 e.jump_cursor(0, 5);
8197 run_keys(&mut e, "ge");
8198 assert_eq!(e.cursor(), (0, 2));
8199 }
8200
8201 #[test]
8202 fn ge_respects_word_class() {
8203 let mut e = editor_with("foo-bar baz");
8206 e.jump_cursor(0, 5);
8207 run_keys(&mut e, "ge");
8208 assert_eq!(e.cursor(), (0, 3));
8209 }
8210
8211 #[test]
8212 fn big_ge_treats_hyphens_as_part_of_word() {
8213 let mut e = editor_with("foo-bar baz");
8216 e.jump_cursor(0, 10);
8217 run_keys(&mut e, "gE");
8218 assert_eq!(e.cursor(), (0, 6));
8219 }
8220
8221 #[test]
8222 fn ge_crosses_line_boundary() {
8223 let mut e = editor_with("foo\nbar");
8224 e.jump_cursor(1, 0);
8225 run_keys(&mut e, "ge");
8226 assert_eq!(e.cursor(), (0, 2));
8227 }
8228
8229 #[test]
8230 fn dge_deletes_to_end_of_previous_word() {
8231 let mut e = editor_with("foo bar baz");
8232 e.jump_cursor(0, 8);
8233 run_keys(&mut e, "dge");
8236 assert_eq!(e.buffer().lines()[0], "foo baaz");
8237 }
8238
8239 #[test]
8240 fn ctrl_scroll_keys_do_not_panic() {
8241 let mut e = editor_with(
8244 (0..50)
8245 .map(|i| format!("line{i}"))
8246 .collect::<Vec<_>>()
8247 .join("\n")
8248 .as_str(),
8249 );
8250 run_keys(&mut e, "<C-f>");
8251 run_keys(&mut e, "<C-b>");
8252 assert!(!e.buffer().lines().is_empty());
8254 }
8255
8256 #[test]
8263 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8264 let mut e = Editor::new(
8265 hjkl_buffer::Buffer::new(),
8266 crate::types::DefaultHost::new(),
8267 crate::types::Options::default(),
8268 );
8269 e.set_content("row0\nrow1\nrow2");
8270 run_keys(&mut e, "3iX<Down><Esc>");
8272 assert!(e.buffer().lines()[0].contains('X'));
8274 assert!(
8277 !e.buffer().lines()[1].contains("row0"),
8278 "row1 leaked row0 contents: {:?}",
8279 e.buffer().lines()[1]
8280 );
8281 assert_eq!(e.buffer().lines().len(), 3);
8284 }
8285
8286 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8289 let mut e = Editor::new(
8290 hjkl_buffer::Buffer::new(),
8291 crate::types::DefaultHost::new(),
8292 crate::types::Options::default(),
8293 );
8294 let body = (0..n)
8295 .map(|i| format!(" line{}", i))
8296 .collect::<Vec<_>>()
8297 .join("\n");
8298 e.set_content(&body);
8299 e.set_viewport_height(viewport);
8300 e
8301 }
8302
8303 #[test]
8304 fn ctrl_d_moves_cursor_half_page_down() {
8305 let mut e = editor_with_rows(100, 20);
8306 run_keys(&mut e, "<C-d>");
8307 assert_eq!(e.cursor().0, 10);
8308 }
8309
8310 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8311 let mut e = Editor::new(
8312 hjkl_buffer::Buffer::new(),
8313 crate::types::DefaultHost::new(),
8314 crate::types::Options::default(),
8315 );
8316 e.set_content(&lines.join("\n"));
8317 e.set_viewport_height(viewport);
8318 let v = e.host_mut().viewport_mut();
8319 v.height = viewport;
8320 v.width = text_width;
8321 v.text_width = text_width;
8322 v.wrap = hjkl_buffer::Wrap::Char;
8323 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8324 e
8325 }
8326
8327 #[test]
8328 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8329 let lines = ["aaaabbbbcccc"; 10];
8333 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8334 e.jump_cursor(4, 0);
8335 e.ensure_cursor_in_scrolloff();
8336 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8337 assert!(csr <= 6, "csr={csr}");
8338 }
8339
8340 #[test]
8341 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8342 let lines = ["aaaabbbbcccc"; 10];
8343 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8344 e.jump_cursor(7, 0);
8347 e.ensure_cursor_in_scrolloff();
8348 e.jump_cursor(2, 0);
8349 e.ensure_cursor_in_scrolloff();
8350 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8351 assert!(csr >= 5, "csr={csr}");
8353 }
8354
8355 #[test]
8356 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8357 let lines = ["aaaabbbbcccc"; 5];
8358 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8359 e.jump_cursor(4, 11);
8360 e.ensure_cursor_in_scrolloff();
8361 let top = e.host().viewport().top_row;
8366 assert_eq!(top, 1);
8367 }
8368
8369 #[test]
8370 fn ctrl_u_moves_cursor_half_page_up() {
8371 let mut e = editor_with_rows(100, 20);
8372 e.jump_cursor(50, 0);
8373 run_keys(&mut e, "<C-u>");
8374 assert_eq!(e.cursor().0, 40);
8375 }
8376
8377 #[test]
8378 fn ctrl_f_moves_cursor_full_page_down() {
8379 let mut e = editor_with_rows(100, 20);
8380 run_keys(&mut e, "<C-f>");
8381 assert_eq!(e.cursor().0, 18);
8383 }
8384
8385 #[test]
8386 fn ctrl_b_moves_cursor_full_page_up() {
8387 let mut e = editor_with_rows(100, 20);
8388 e.jump_cursor(50, 0);
8389 run_keys(&mut e, "<C-b>");
8390 assert_eq!(e.cursor().0, 32);
8391 }
8392
8393 #[test]
8394 fn ctrl_d_lands_on_first_non_blank() {
8395 let mut e = editor_with_rows(100, 20);
8396 run_keys(&mut e, "<C-d>");
8397 assert_eq!(e.cursor().1, 2);
8399 }
8400
8401 #[test]
8402 fn ctrl_d_clamps_at_end_of_buffer() {
8403 let mut e = editor_with_rows(5, 20);
8404 run_keys(&mut e, "<C-d>");
8405 assert_eq!(e.cursor().0, 4);
8406 }
8407
8408 #[test]
8409 fn capital_h_jumps_to_viewport_top() {
8410 let mut e = editor_with_rows(100, 10);
8411 e.jump_cursor(50, 0);
8412 e.set_viewport_top(45);
8413 let top = e.host().viewport().top_row;
8414 run_keys(&mut e, "H");
8415 assert_eq!(e.cursor().0, top);
8416 assert_eq!(e.cursor().1, 2);
8417 }
8418
8419 #[test]
8420 fn capital_l_jumps_to_viewport_bottom() {
8421 let mut e = editor_with_rows(100, 10);
8422 e.jump_cursor(50, 0);
8423 e.set_viewport_top(45);
8424 let top = e.host().viewport().top_row;
8425 run_keys(&mut e, "L");
8426 assert_eq!(e.cursor().0, top + 9);
8427 }
8428
8429 #[test]
8430 fn capital_m_jumps_to_viewport_middle() {
8431 let mut e = editor_with_rows(100, 10);
8432 e.jump_cursor(50, 0);
8433 e.set_viewport_top(45);
8434 let top = e.host().viewport().top_row;
8435 run_keys(&mut e, "M");
8436 assert_eq!(e.cursor().0, top + 4);
8438 }
8439
8440 #[test]
8441 fn g_capital_m_lands_at_line_midpoint() {
8442 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8444 assert_eq!(e.cursor(), (0, 6));
8446 }
8447
8448 #[test]
8449 fn g_capital_m_on_empty_line_stays_at_zero() {
8450 let mut e = editor_with("");
8451 run_keys(&mut e, "gM");
8452 assert_eq!(e.cursor(), (0, 0));
8453 }
8454
8455 #[test]
8456 fn g_capital_m_uses_current_line_only() {
8457 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8460 run_keys(&mut e, "gM");
8461 assert_eq!(e.cursor(), (1, 6));
8462 }
8463
8464 #[test]
8465 fn capital_h_count_offsets_from_top() {
8466 let mut e = editor_with_rows(100, 10);
8467 e.jump_cursor(50, 0);
8468 e.set_viewport_top(45);
8469 let top = e.host().viewport().top_row;
8470 run_keys(&mut e, "3H");
8471 assert_eq!(e.cursor().0, top + 2);
8472 }
8473
8474 #[test]
8477 fn ctrl_o_returns_to_pre_g_position() {
8478 let mut e = editor_with_rows(50, 20);
8479 e.jump_cursor(5, 2);
8480 run_keys(&mut e, "G");
8481 assert_eq!(e.cursor().0, 49);
8482 run_keys(&mut e, "<C-o>");
8483 assert_eq!(e.cursor(), (5, 2));
8484 }
8485
8486 #[test]
8487 fn ctrl_i_redoes_jump_after_ctrl_o() {
8488 let mut e = editor_with_rows(50, 20);
8489 e.jump_cursor(5, 2);
8490 run_keys(&mut e, "G");
8491 let post = e.cursor();
8492 run_keys(&mut e, "<C-o>");
8493 run_keys(&mut e, "<C-i>");
8494 assert_eq!(e.cursor(), post);
8495 }
8496
8497 #[test]
8498 fn new_jump_clears_forward_stack() {
8499 let mut e = editor_with_rows(50, 20);
8500 e.jump_cursor(5, 2);
8501 run_keys(&mut e, "G");
8502 run_keys(&mut e, "<C-o>");
8503 run_keys(&mut e, "gg");
8504 run_keys(&mut e, "<C-i>");
8505 assert_eq!(e.cursor().0, 0);
8506 }
8507
8508 #[test]
8509 fn ctrl_o_on_empty_stack_is_noop() {
8510 let mut e = editor_with_rows(10, 20);
8511 e.jump_cursor(3, 1);
8512 run_keys(&mut e, "<C-o>");
8513 assert_eq!(e.cursor(), (3, 1));
8514 }
8515
8516 #[test]
8517 fn asterisk_search_pushes_jump() {
8518 let mut e = editor_with("foo bar\nbaz foo end");
8519 e.jump_cursor(0, 0);
8520 run_keys(&mut e, "*");
8521 let after = e.cursor();
8522 assert_ne!(after, (0, 0));
8523 run_keys(&mut e, "<C-o>");
8524 assert_eq!(e.cursor(), (0, 0));
8525 }
8526
8527 #[test]
8528 fn h_viewport_jump_is_recorded() {
8529 let mut e = editor_with_rows(100, 10);
8530 e.jump_cursor(50, 0);
8531 e.set_viewport_top(45);
8532 let pre = e.cursor();
8533 run_keys(&mut e, "H");
8534 assert_ne!(e.cursor(), pre);
8535 run_keys(&mut e, "<C-o>");
8536 assert_eq!(e.cursor(), pre);
8537 }
8538
8539 #[test]
8540 fn j_k_motion_does_not_push_jump() {
8541 let mut e = editor_with_rows(50, 20);
8542 e.jump_cursor(5, 0);
8543 run_keys(&mut e, "jjj");
8544 run_keys(&mut e, "<C-o>");
8545 assert_eq!(e.cursor().0, 8);
8546 }
8547
8548 #[test]
8549 fn jumplist_caps_at_100() {
8550 let mut e = editor_with_rows(200, 20);
8551 for i in 0..101 {
8552 e.jump_cursor(i, 0);
8553 run_keys(&mut e, "G");
8554 }
8555 assert!(e.vim.jump_back.len() <= 100);
8556 }
8557
8558 #[test]
8559 fn tab_acts_as_ctrl_i() {
8560 let mut e = editor_with_rows(50, 20);
8561 e.jump_cursor(5, 2);
8562 run_keys(&mut e, "G");
8563 let post = e.cursor();
8564 run_keys(&mut e, "<C-o>");
8565 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8566 assert_eq!(e.cursor(), post);
8567 }
8568
8569 #[test]
8572 fn ma_then_backtick_a_jumps_exact() {
8573 let mut e = editor_with_rows(50, 20);
8574 e.jump_cursor(5, 3);
8575 run_keys(&mut e, "ma");
8576 e.jump_cursor(20, 0);
8577 run_keys(&mut e, "`a");
8578 assert_eq!(e.cursor(), (5, 3));
8579 }
8580
8581 #[test]
8582 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8583 let mut e = editor_with_rows(50, 20);
8584 e.jump_cursor(5, 6);
8586 run_keys(&mut e, "ma");
8587 e.jump_cursor(30, 4);
8588 run_keys(&mut e, "'a");
8589 assert_eq!(e.cursor(), (5, 2));
8590 }
8591
8592 #[test]
8593 fn goto_mark_pushes_jumplist() {
8594 let mut e = editor_with_rows(50, 20);
8595 e.jump_cursor(10, 2);
8596 run_keys(&mut e, "mz");
8597 e.jump_cursor(3, 0);
8598 run_keys(&mut e, "`z");
8599 assert_eq!(e.cursor(), (10, 2));
8600 run_keys(&mut e, "<C-o>");
8601 assert_eq!(e.cursor(), (3, 0));
8602 }
8603
8604 #[test]
8605 fn goto_missing_mark_is_noop() {
8606 let mut e = editor_with_rows(50, 20);
8607 e.jump_cursor(3, 1);
8608 run_keys(&mut e, "`q");
8609 assert_eq!(e.cursor(), (3, 1));
8610 }
8611
8612 #[test]
8613 fn uppercase_mark_stored_under_uppercase_key() {
8614 let mut e = editor_with_rows(50, 20);
8615 e.jump_cursor(5, 3);
8616 run_keys(&mut e, "mA");
8617 assert_eq!(e.mark('A'), Some((5, 3)));
8620 assert!(e.mark('a').is_none());
8621 }
8622
8623 #[test]
8624 fn mark_survives_document_shrink_via_clamp() {
8625 let mut e = editor_with_rows(50, 20);
8626 e.jump_cursor(40, 4);
8627 run_keys(&mut e, "mx");
8628 e.set_content("a\nb\nc\nd\ne");
8630 run_keys(&mut e, "`x");
8631 let (r, _) = e.cursor();
8633 assert!(r <= 4);
8634 }
8635
8636 #[test]
8637 fn g_semicolon_walks_back_through_edits() {
8638 let mut e = editor_with("alpha\nbeta\ngamma");
8639 e.jump_cursor(0, 0);
8642 run_keys(&mut e, "iX<Esc>");
8643 e.jump_cursor(2, 0);
8644 run_keys(&mut e, "iY<Esc>");
8645 run_keys(&mut e, "g;");
8647 assert_eq!(e.cursor(), (2, 1));
8648 run_keys(&mut e, "g;");
8650 assert_eq!(e.cursor(), (0, 1));
8651 run_keys(&mut e, "g;");
8653 assert_eq!(e.cursor(), (0, 1));
8654 }
8655
8656 #[test]
8657 fn g_comma_walks_forward_after_g_semicolon() {
8658 let mut e = editor_with("a\nb\nc");
8659 e.jump_cursor(0, 0);
8660 run_keys(&mut e, "iX<Esc>");
8661 e.jump_cursor(2, 0);
8662 run_keys(&mut e, "iY<Esc>");
8663 run_keys(&mut e, "g;");
8664 run_keys(&mut e, "g;");
8665 assert_eq!(e.cursor(), (0, 1));
8666 run_keys(&mut e, "g,");
8667 assert_eq!(e.cursor(), (2, 1));
8668 }
8669
8670 #[test]
8671 fn new_edit_during_walk_trims_forward_entries() {
8672 let mut e = editor_with("a\nb\nc\nd");
8673 e.jump_cursor(0, 0);
8674 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8676 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8679 run_keys(&mut e, "g;");
8680 assert_eq!(e.cursor(), (0, 1));
8681 run_keys(&mut e, "iZ<Esc>");
8683 run_keys(&mut e, "g,");
8685 assert_ne!(e.cursor(), (2, 1));
8687 }
8688
8689 #[test]
8695 fn capital_mark_set_and_jump() {
8696 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8697 e.jump_cursor(2, 1);
8698 run_keys(&mut e, "mA");
8699 e.jump_cursor(0, 0);
8701 run_keys(&mut e, "'A");
8703 assert_eq!(e.cursor().0, 2);
8705 }
8706
8707 #[test]
8708 fn capital_mark_survives_set_content() {
8709 let mut e = editor_with("first buffer line\nsecond");
8710 e.jump_cursor(1, 3);
8711 run_keys(&mut e, "mA");
8712 e.set_content("totally different content\non many\nrows of text");
8714 e.jump_cursor(0, 0);
8716 run_keys(&mut e, "'A");
8717 assert_eq!(e.cursor().0, 1);
8718 }
8719
8720 #[test]
8725 fn capital_mark_shifts_with_edit() {
8726 let mut e = editor_with("a\nb\nc\nd");
8727 e.jump_cursor(3, 0);
8728 run_keys(&mut e, "mA");
8729 e.jump_cursor(0, 0);
8731 run_keys(&mut e, "dd");
8732 e.jump_cursor(0, 0);
8733 run_keys(&mut e, "'A");
8734 assert_eq!(e.cursor().0, 2);
8735 }
8736
8737 #[test]
8738 fn mark_below_delete_shifts_up() {
8739 let mut e = editor_with("a\nb\nc\nd\ne");
8740 e.jump_cursor(3, 0);
8742 run_keys(&mut e, "ma");
8743 e.jump_cursor(0, 0);
8745 run_keys(&mut e, "dd");
8746 e.jump_cursor(0, 0);
8748 run_keys(&mut e, "'a");
8749 assert_eq!(e.cursor().0, 2);
8750 assert_eq!(e.buffer().line(2).unwrap(), "d");
8751 }
8752
8753 #[test]
8754 fn mark_on_deleted_row_is_dropped() {
8755 let mut e = editor_with("a\nb\nc\nd");
8756 e.jump_cursor(1, 0);
8758 run_keys(&mut e, "ma");
8759 run_keys(&mut e, "dd");
8761 e.jump_cursor(2, 0);
8763 run_keys(&mut e, "'a");
8764 assert_eq!(e.cursor().0, 2);
8766 }
8767
8768 #[test]
8769 fn mark_above_edit_unchanged() {
8770 let mut e = editor_with("a\nb\nc\nd\ne");
8771 e.jump_cursor(0, 0);
8773 run_keys(&mut e, "ma");
8774 e.jump_cursor(3, 0);
8776 run_keys(&mut e, "dd");
8777 e.jump_cursor(2, 0);
8779 run_keys(&mut e, "'a");
8780 assert_eq!(e.cursor().0, 0);
8781 }
8782
8783 #[test]
8784 fn mark_shifts_down_after_insert() {
8785 let mut e = editor_with("a\nb\nc");
8786 e.jump_cursor(2, 0);
8788 run_keys(&mut e, "ma");
8789 e.jump_cursor(0, 0);
8791 run_keys(&mut e, "Onew<Esc>");
8792 e.jump_cursor(0, 0);
8795 run_keys(&mut e, "'a");
8796 assert_eq!(e.cursor().0, 3);
8797 assert_eq!(e.buffer().line(3).unwrap(), "c");
8798 }
8799
8800 #[test]
8803 fn forward_search_commit_pushes_jump() {
8804 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8805 e.jump_cursor(0, 0);
8806 run_keys(&mut e, "/target<CR>");
8807 assert_ne!(e.cursor(), (0, 0));
8809 run_keys(&mut e, "<C-o>");
8811 assert_eq!(e.cursor(), (0, 0));
8812 }
8813
8814 #[test]
8815 fn search_commit_no_match_does_not_push_jump() {
8816 let mut e = editor_with("alpha beta\nfoo end");
8817 e.jump_cursor(0, 3);
8818 let pre_len = e.vim.jump_back.len();
8819 run_keys(&mut e, "/zzznotfound<CR>");
8820 assert_eq!(e.vim.jump_back.len(), pre_len);
8822 }
8823
8824 #[test]
8827 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8828 let mut e = editor_with("hello world");
8829 run_keys(&mut e, "lll");
8830 let (row, col) = e.cursor();
8831 assert_eq!(e.buffer.cursor().row, row);
8832 assert_eq!(e.buffer.cursor().col, col);
8833 }
8834
8835 #[test]
8836 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8837 let mut e = editor_with("aaaa\nbbbb\ncccc");
8838 run_keys(&mut e, "jj");
8839 let (row, col) = e.cursor();
8840 assert_eq!(e.buffer.cursor().row, row);
8841 assert_eq!(e.buffer.cursor().col, col);
8842 }
8843
8844 #[test]
8845 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8846 let mut e = editor_with("foo bar baz");
8847 run_keys(&mut e, "ww");
8848 let (row, col) = e.cursor();
8849 assert_eq!(e.buffer.cursor().row, row);
8850 assert_eq!(e.buffer.cursor().col, col);
8851 }
8852
8853 #[test]
8854 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8855 let mut e = editor_with("a\nb\nc\nd\ne");
8856 run_keys(&mut e, "G");
8857 let (row, col) = e.cursor();
8858 assert_eq!(e.buffer.cursor().row, row);
8859 assert_eq!(e.buffer.cursor().col, col);
8860 }
8861
8862 #[test]
8863 fn editor_sticky_col_tracks_horizontal_motion() {
8864 let mut e = editor_with("longline\nhi\nlongline");
8865 run_keys(&mut e, "fl");
8870 let landed = e.cursor().1;
8871 assert!(landed > 0, "fl should have moved");
8872 run_keys(&mut e, "j");
8873 assert_eq!(e.sticky_col(), Some(landed));
8876 }
8877
8878 #[test]
8879 fn buffer_content_mirrors_textarea_after_insert() {
8880 let mut e = editor_with("hello");
8881 run_keys(&mut e, "iXYZ<Esc>");
8882 let text = e.buffer().lines().join("\n");
8883 assert_eq!(e.buffer.as_string(), text);
8884 }
8885
8886 #[test]
8887 fn buffer_content_mirrors_textarea_after_delete() {
8888 let mut e = editor_with("alpha bravo charlie");
8889 run_keys(&mut e, "dw");
8890 let text = e.buffer().lines().join("\n");
8891 assert_eq!(e.buffer.as_string(), text);
8892 }
8893
8894 #[test]
8895 fn buffer_content_mirrors_textarea_after_dd() {
8896 let mut e = editor_with("a\nb\nc\nd");
8897 run_keys(&mut e, "jdd");
8898 let text = e.buffer().lines().join("\n");
8899 assert_eq!(e.buffer.as_string(), text);
8900 }
8901
8902 #[test]
8903 fn buffer_content_mirrors_textarea_after_open_line() {
8904 let mut e = editor_with("foo\nbar");
8905 run_keys(&mut e, "oNEW<Esc>");
8906 let text = e.buffer().lines().join("\n");
8907 assert_eq!(e.buffer.as_string(), text);
8908 }
8909
8910 #[test]
8911 fn buffer_content_mirrors_textarea_after_paste() {
8912 let mut e = editor_with("hello");
8913 run_keys(&mut e, "yy");
8914 run_keys(&mut e, "p");
8915 let text = e.buffer().lines().join("\n");
8916 assert_eq!(e.buffer.as_string(), text);
8917 }
8918
8919 #[test]
8920 fn buffer_selection_none_in_normal_mode() {
8921 let e = editor_with("foo bar");
8922 assert!(e.buffer_selection().is_none());
8923 }
8924
8925 #[test]
8926 fn buffer_selection_char_in_visual_mode() {
8927 use hjkl_buffer::{Position, Selection};
8928 let mut e = editor_with("hello world");
8929 run_keys(&mut e, "vlll");
8930 assert_eq!(
8931 e.buffer_selection(),
8932 Some(Selection::Char {
8933 anchor: Position::new(0, 0),
8934 head: Position::new(0, 3),
8935 })
8936 );
8937 }
8938
8939 #[test]
8940 fn buffer_selection_line_in_visual_line_mode() {
8941 use hjkl_buffer::Selection;
8942 let mut e = editor_with("a\nb\nc\nd");
8943 run_keys(&mut e, "Vj");
8944 assert_eq!(
8945 e.buffer_selection(),
8946 Some(Selection::Line {
8947 anchor_row: 0,
8948 head_row: 1,
8949 })
8950 );
8951 }
8952
8953 #[test]
8954 fn wrapscan_off_blocks_wrap_around() {
8955 let mut e = editor_with("first\nsecond\nthird\n");
8956 e.settings_mut().wrapscan = false;
8957 e.jump_cursor(2, 0);
8959 run_keys(&mut e, "/first<CR>");
8960 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8962 e.settings_mut().wrapscan = true;
8964 run_keys(&mut e, "/first<CR>");
8965 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8966 }
8967
8968 #[test]
8969 fn smartcase_uppercase_pattern_stays_sensitive() {
8970 let mut e = editor_with("foo\nFoo\nBAR\n");
8971 e.settings_mut().ignore_case = true;
8972 e.settings_mut().smartcase = true;
8973 run_keys(&mut e, "/foo<CR>");
8976 let r1 = e
8977 .search_state()
8978 .pattern
8979 .as_ref()
8980 .unwrap()
8981 .as_str()
8982 .to_string();
8983 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8984 run_keys(&mut e, "/Foo<CR>");
8986 let r2 = e
8987 .search_state()
8988 .pattern
8989 .as_ref()
8990 .unwrap()
8991 .as_str()
8992 .to_string();
8993 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8994 }
8995
8996 #[test]
8997 fn enter_with_autoindent_copies_leading_whitespace() {
8998 let mut e = editor_with(" foo");
8999 e.jump_cursor(0, 7);
9000 run_keys(&mut e, "i<CR>");
9001 assert_eq!(e.buffer.line(1).unwrap(), " ");
9002 }
9003
9004 #[test]
9005 fn enter_without_autoindent_inserts_bare_newline() {
9006 let mut e = editor_with(" foo");
9007 e.settings_mut().autoindent = false;
9008 e.jump_cursor(0, 7);
9009 run_keys(&mut e, "i<CR>");
9010 assert_eq!(e.buffer.line(1).unwrap(), "");
9011 }
9012
9013 #[test]
9014 fn iskeyword_default_treats_alnum_underscore_as_word() {
9015 let mut e = editor_with("foo_bar baz");
9016 e.jump_cursor(0, 0);
9020 run_keys(&mut e, "*");
9021 let p = e
9022 .search_state()
9023 .pattern
9024 .as_ref()
9025 .unwrap()
9026 .as_str()
9027 .to_string();
9028 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9029 }
9030
9031 #[test]
9032 fn w_motion_respects_custom_iskeyword() {
9033 let mut e = editor_with("foo-bar baz");
9037 run_keys(&mut e, "w");
9038 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9039 let mut e2 = editor_with("foo-bar baz");
9042 e2.set_iskeyword("@,_,45");
9043 run_keys(&mut e2, "w");
9044 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9045 }
9046
9047 #[test]
9048 fn iskeyword_with_dash_treats_dash_as_word_char() {
9049 let mut e = editor_with("foo-bar baz");
9050 e.settings_mut().iskeyword = "@,_,45".to_string();
9051 e.jump_cursor(0, 0);
9052 run_keys(&mut e, "*");
9053 let p = e
9054 .search_state()
9055 .pattern
9056 .as_ref()
9057 .unwrap()
9058 .as_str()
9059 .to_string();
9060 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9061 }
9062
9063 #[test]
9064 fn timeoutlen_drops_pending_g_prefix() {
9065 use std::time::{Duration, Instant};
9066 let mut e = editor_with("a\nb\nc");
9067 e.jump_cursor(2, 0);
9068 run_keys(&mut e, "g");
9070 assert!(matches!(e.vim.pending, super::Pending::G));
9071 e.settings.timeout_len = Duration::from_nanos(0);
9079 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9080 e.vim.last_input_host_at = Some(Duration::ZERO);
9081 run_keys(&mut e, "g");
9085 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9087 }
9088
9089 #[test]
9090 fn undobreak_on_breaks_group_at_arrow_motion() {
9091 let mut e = editor_with("");
9092 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9094 let line = e.buffer.line(0).unwrap_or("").to_string();
9097 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9098 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9099 }
9100
9101 #[test]
9102 fn undobreak_off_keeps_full_run_in_one_group() {
9103 let mut e = editor_with("");
9104 e.settings_mut().undo_break_on_motion = false;
9105 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9106 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9109 }
9110
9111 #[test]
9112 fn undobreak_round_trips_through_options() {
9113 let e = editor_with("");
9114 let opts = e.current_options();
9115 assert!(opts.undo_break_on_motion);
9116 let mut e2 = editor_with("");
9117 let mut new_opts = opts.clone();
9118 new_opts.undo_break_on_motion = false;
9119 e2.apply_options(&new_opts);
9120 assert!(!e2.current_options().undo_break_on_motion);
9121 }
9122
9123 #[test]
9124 fn undo_levels_cap_drops_oldest() {
9125 let mut e = editor_with("abcde");
9126 e.settings_mut().undo_levels = 3;
9127 run_keys(&mut e, "ra");
9128 run_keys(&mut e, "lrb");
9129 run_keys(&mut e, "lrc");
9130 run_keys(&mut e, "lrd");
9131 run_keys(&mut e, "lre");
9132 assert_eq!(e.undo_stack_len(), 3);
9133 }
9134
9135 #[test]
9136 fn tab_inserts_literal_tab_when_noexpandtab() {
9137 let mut e = editor_with("");
9138 e.settings_mut().expandtab = false;
9141 e.settings_mut().softtabstop = 0;
9142 run_keys(&mut e, "i");
9143 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9144 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9145 }
9146
9147 #[test]
9148 fn tab_inserts_spaces_when_expandtab() {
9149 let mut e = editor_with("");
9150 e.settings_mut().expandtab = true;
9151 e.settings_mut().tabstop = 4;
9152 run_keys(&mut e, "i");
9153 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9154 assert_eq!(e.buffer.line(0).unwrap(), " ");
9155 }
9156
9157 #[test]
9158 fn tab_with_softtabstop_fills_to_next_boundary() {
9159 let mut e = editor_with("ab");
9161 e.settings_mut().expandtab = true;
9162 e.settings_mut().tabstop = 8;
9163 e.settings_mut().softtabstop = 4;
9164 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9166 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9167 }
9168
9169 #[test]
9170 fn backspace_deletes_softtab_run() {
9171 let mut e = editor_with(" x");
9174 e.settings_mut().softtabstop = 4;
9175 run_keys(&mut e, "fxi");
9177 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9178 assert_eq!(e.buffer.line(0).unwrap(), "x");
9179 }
9180
9181 #[test]
9182 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9183 let mut e = editor_with(" x");
9186 e.settings_mut().softtabstop = 4;
9187 run_keys(&mut e, "fxi");
9188 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9189 assert_eq!(e.buffer.line(0).unwrap(), " x");
9190 }
9191
9192 #[test]
9193 fn readonly_blocks_insert_mutation() {
9194 let mut e = editor_with("hello");
9195 e.settings_mut().readonly = true;
9196 run_keys(&mut e, "iX<Esc>");
9197 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9198 }
9199
9200 #[cfg(feature = "ratatui")]
9201 #[test]
9202 fn intern_ratatui_style_dedups_repeated_styles() {
9203 use ratatui::style::{Color, Style};
9204 let mut e = editor_with("");
9205 let red = Style::default().fg(Color::Red);
9206 let blue = Style::default().fg(Color::Blue);
9207 let id_r1 = e.intern_ratatui_style(red);
9208 let id_r2 = e.intern_ratatui_style(red);
9209 let id_b = e.intern_ratatui_style(blue);
9210 assert_eq!(id_r1, id_r2);
9211 assert_ne!(id_r1, id_b);
9212 assert_eq!(e.style_table().len(), 2);
9213 }
9214
9215 #[cfg(feature = "ratatui")]
9216 #[test]
9217 fn install_ratatui_syntax_spans_translates_styled_spans() {
9218 use ratatui::style::{Color, Style};
9219 let mut e = editor_with("SELECT foo");
9220 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9221 let by_row = e.buffer_spans();
9222 assert_eq!(by_row.len(), 1);
9223 assert_eq!(by_row[0].len(), 1);
9224 assert_eq!(by_row[0][0].start_byte, 0);
9225 assert_eq!(by_row[0][0].end_byte, 6);
9226 let id = by_row[0][0].style;
9227 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9228 }
9229
9230 #[cfg(feature = "ratatui")]
9231 #[test]
9232 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9233 use ratatui::style::{Color, Style};
9234 let mut e = editor_with("hello");
9235 e.install_ratatui_syntax_spans(vec![vec![(
9236 0,
9237 usize::MAX,
9238 Style::default().fg(Color::Blue),
9239 )]]);
9240 let by_row = e.buffer_spans();
9241 assert_eq!(by_row[0][0].end_byte, 5);
9242 }
9243
9244 #[cfg(feature = "ratatui")]
9245 #[test]
9246 fn install_ratatui_syntax_spans_drops_zero_width() {
9247 use ratatui::style::{Color, Style};
9248 let mut e = editor_with("abc");
9249 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9250 assert!(e.buffer_spans()[0].is_empty());
9251 }
9252
9253 #[test]
9254 fn named_register_yank_into_a_then_paste_from_a() {
9255 let mut e = editor_with("hello world\nsecond");
9256 run_keys(&mut e, "\"ayw");
9257 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9259 run_keys(&mut e, "j0\"aP");
9261 assert_eq!(e.buffer().lines()[1], "hello second");
9262 }
9263
9264 #[test]
9265 fn capital_r_overstrikes_chars() {
9266 let mut e = editor_with("hello");
9267 e.jump_cursor(0, 0);
9268 run_keys(&mut e, "RXY<Esc>");
9269 assert_eq!(e.buffer().lines()[0], "XYllo");
9271 }
9272
9273 #[test]
9274 fn capital_r_at_eol_appends() {
9275 let mut e = editor_with("hi");
9276 e.jump_cursor(0, 1);
9277 run_keys(&mut e, "RXYZ<Esc>");
9279 assert_eq!(e.buffer().lines()[0], "hXYZ");
9280 }
9281
9282 #[test]
9283 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9284 let mut e = editor_with("abc");
9288 e.jump_cursor(0, 0);
9289 run_keys(&mut e, "RX<Esc>");
9290 assert_eq!(e.buffer().lines()[0], "Xbc");
9291 }
9292
9293 #[test]
9294 fn ctrl_r_in_insert_pastes_named_register() {
9295 let mut e = editor_with("hello world");
9296 run_keys(&mut e, "\"ayw");
9298 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9299 run_keys(&mut e, "o");
9301 assert_eq!(e.vim_mode(), VimMode::Insert);
9302 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9303 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9304 assert_eq!(e.buffer().lines()[1], "hello ");
9305 assert_eq!(e.cursor(), (1, 6));
9307 assert_eq!(e.vim_mode(), VimMode::Insert);
9309 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9310 assert_eq!(e.buffer().lines()[1], "hello X");
9311 }
9312
9313 #[test]
9314 fn ctrl_r_with_unnamed_register() {
9315 let mut e = editor_with("foo");
9316 run_keys(&mut e, "yiw");
9317 run_keys(&mut e, "A ");
9318 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9320 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9321 assert_eq!(e.buffer().lines()[0], "foo foo");
9322 }
9323
9324 #[test]
9325 fn ctrl_r_unknown_selector_is_no_op() {
9326 let mut e = editor_with("abc");
9327 run_keys(&mut e, "A");
9328 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9329 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9332 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9333 assert_eq!(e.buffer().lines()[0], "abcZ");
9334 }
9335
9336 #[test]
9337 fn ctrl_r_multiline_register_pastes_with_newlines() {
9338 let mut e = editor_with("alpha\nbeta\ngamma");
9339 run_keys(&mut e, "\"byy");
9341 run_keys(&mut e, "j\"byy");
9342 run_keys(&mut e, "ggVj\"by");
9346 let payload = e.registers().read('b').unwrap().text.clone();
9347 assert!(payload.contains('\n'));
9348 run_keys(&mut e, "Go");
9349 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9350 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9351 let total_lines = e.buffer().lines().len();
9354 assert!(total_lines >= 5);
9355 }
9356
9357 #[test]
9358 fn yank_zero_holds_last_yank_after_delete() {
9359 let mut e = editor_with("hello world");
9360 run_keys(&mut e, "yw");
9361 let yanked = e.registers().read('0').unwrap().text.clone();
9362 assert!(!yanked.is_empty());
9363 run_keys(&mut e, "dw");
9365 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9366 assert!(!e.registers().read('1').unwrap().text.is_empty());
9368 }
9369
9370 #[test]
9371 fn delete_ring_rotates_through_one_through_nine() {
9372 let mut e = editor_with("a b c d e f g h i j");
9373 for _ in 0..3 {
9375 run_keys(&mut e, "dw");
9376 }
9377 let r1 = e.registers().read('1').unwrap().text.clone();
9379 let r2 = e.registers().read('2').unwrap().text.clone();
9380 let r3 = e.registers().read('3').unwrap().text.clone();
9381 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9382 assert_ne!(r1, r2);
9383 assert_ne!(r2, r3);
9384 }
9385
9386 #[test]
9387 fn capital_register_appends_to_lowercase() {
9388 let mut e = editor_with("foo bar");
9389 run_keys(&mut e, "\"ayw");
9390 let first = e.registers().read('a').unwrap().text.clone();
9391 assert!(first.contains("foo"));
9392 run_keys(&mut e, "w\"Ayw");
9394 let combined = e.registers().read('a').unwrap().text.clone();
9395 assert!(combined.starts_with(&first));
9396 assert!(combined.contains("bar"));
9397 }
9398
9399 #[test]
9400 fn zf_in_visual_line_creates_closed_fold() {
9401 let mut e = editor_with("a\nb\nc\nd\ne");
9402 e.jump_cursor(1, 0);
9404 run_keys(&mut e, "Vjjzf");
9405 assert_eq!(e.buffer().folds().len(), 1);
9406 let f = e.buffer().folds()[0];
9407 assert_eq!(f.start_row, 1);
9408 assert_eq!(f.end_row, 3);
9409 assert!(f.closed);
9410 }
9411
9412 #[test]
9413 fn zfj_in_normal_creates_two_row_fold() {
9414 let mut e = editor_with("a\nb\nc\nd\ne");
9415 e.jump_cursor(1, 0);
9416 run_keys(&mut e, "zfj");
9417 assert_eq!(e.buffer().folds().len(), 1);
9418 let f = e.buffer().folds()[0];
9419 assert_eq!(f.start_row, 1);
9420 assert_eq!(f.end_row, 2);
9421 assert!(f.closed);
9422 assert_eq!(e.cursor().0, 1);
9424 }
9425
9426 #[test]
9427 fn zf_with_count_folds_count_rows() {
9428 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9429 e.jump_cursor(0, 0);
9430 run_keys(&mut e, "zf3j");
9432 assert_eq!(e.buffer().folds().len(), 1);
9433 let f = e.buffer().folds()[0];
9434 assert_eq!(f.start_row, 0);
9435 assert_eq!(f.end_row, 3);
9436 }
9437
9438 #[test]
9439 fn zfk_folds_upward_range() {
9440 let mut e = editor_with("a\nb\nc\nd\ne");
9441 e.jump_cursor(3, 0);
9442 run_keys(&mut e, "zfk");
9443 let f = e.buffer().folds()[0];
9444 assert_eq!(f.start_row, 2);
9446 assert_eq!(f.end_row, 3);
9447 }
9448
9449 #[test]
9450 fn zf_capital_g_folds_to_bottom() {
9451 let mut e = editor_with("a\nb\nc\nd\ne");
9452 e.jump_cursor(1, 0);
9453 run_keys(&mut e, "zfG");
9455 let f = e.buffer().folds()[0];
9456 assert_eq!(f.start_row, 1);
9457 assert_eq!(f.end_row, 4);
9458 }
9459
9460 #[test]
9461 fn zfgg_folds_to_top_via_operator_pipeline() {
9462 let mut e = editor_with("a\nb\nc\nd\ne");
9463 e.jump_cursor(3, 0);
9464 run_keys(&mut e, "zfgg");
9468 let f = e.buffer().folds()[0];
9469 assert_eq!(f.start_row, 0);
9470 assert_eq!(f.end_row, 3);
9471 }
9472
9473 #[test]
9474 fn zfip_folds_paragraph_via_text_object() {
9475 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9476 e.jump_cursor(1, 0);
9477 run_keys(&mut e, "zfip");
9479 assert_eq!(e.buffer().folds().len(), 1);
9480 let f = e.buffer().folds()[0];
9481 assert_eq!(f.start_row, 0);
9482 assert_eq!(f.end_row, 2);
9483 }
9484
9485 #[test]
9486 fn zfap_folds_paragraph_with_trailing_blank() {
9487 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9488 e.jump_cursor(0, 0);
9489 run_keys(&mut e, "zfap");
9491 let f = e.buffer().folds()[0];
9492 assert_eq!(f.start_row, 0);
9493 assert_eq!(f.end_row, 3);
9494 }
9495
9496 #[test]
9497 fn zf_paragraph_motion_folds_to_blank() {
9498 let mut e = editor_with("alpha\nbeta\n\ngamma");
9499 e.jump_cursor(0, 0);
9500 run_keys(&mut e, "zf}");
9502 let f = e.buffer().folds()[0];
9503 assert_eq!(f.start_row, 0);
9504 assert_eq!(f.end_row, 2);
9505 }
9506
9507 #[test]
9508 fn za_toggles_fold_under_cursor() {
9509 let mut e = editor_with("a\nb\nc\nd");
9510 e.buffer_mut().add_fold(1, 2, true);
9511 e.jump_cursor(1, 0);
9512 run_keys(&mut e, "za");
9513 assert!(!e.buffer().folds()[0].closed);
9514 run_keys(&mut e, "za");
9515 assert!(e.buffer().folds()[0].closed);
9516 }
9517
9518 #[test]
9519 fn zr_opens_all_folds_zm_closes_all() {
9520 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9521 e.buffer_mut().add_fold(0, 1, true);
9522 e.buffer_mut().add_fold(2, 3, true);
9523 e.buffer_mut().add_fold(4, 5, true);
9524 run_keys(&mut e, "zR");
9525 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9526 run_keys(&mut e, "zM");
9527 assert!(e.buffer().folds().iter().all(|f| f.closed));
9528 }
9529
9530 #[test]
9531 fn ze_clears_all_folds() {
9532 let mut e = editor_with("a\nb\nc\nd");
9533 e.buffer_mut().add_fold(0, 1, true);
9534 e.buffer_mut().add_fold(2, 3, false);
9535 run_keys(&mut e, "zE");
9536 assert!(e.buffer().folds().is_empty());
9537 }
9538
9539 #[test]
9540 fn g_underscore_jumps_to_last_non_blank() {
9541 let mut e = editor_with("hello world ");
9542 run_keys(&mut e, "g_");
9543 assert_eq!(e.cursor().1, 10);
9545 }
9546
9547 #[test]
9548 fn gj_and_gk_alias_j_and_k() {
9549 let mut e = editor_with("a\nb\nc");
9550 run_keys(&mut e, "gj");
9551 assert_eq!(e.cursor().0, 1);
9552 run_keys(&mut e, "gk");
9553 assert_eq!(e.cursor().0, 0);
9554 }
9555
9556 #[test]
9557 fn paragraph_motions_walk_blank_lines() {
9558 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9559 run_keys(&mut e, "}");
9560 assert_eq!(e.cursor().0, 2);
9561 run_keys(&mut e, "}");
9562 assert_eq!(e.cursor().0, 5);
9563 run_keys(&mut e, "{");
9564 assert_eq!(e.cursor().0, 2);
9565 }
9566
9567 #[test]
9568 fn gv_reenters_last_visual_selection() {
9569 let mut e = editor_with("alpha\nbeta\ngamma");
9570 run_keys(&mut e, "Vj");
9571 run_keys(&mut e, "<Esc>");
9573 assert_eq!(e.vim_mode(), VimMode::Normal);
9574 run_keys(&mut e, "gv");
9576 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9577 }
9578
9579 #[test]
9580 fn o_in_visual_swaps_anchor_and_cursor() {
9581 let mut e = editor_with("hello world");
9582 run_keys(&mut e, "vllll");
9584 assert_eq!(e.cursor().1, 4);
9585 run_keys(&mut e, "o");
9587 assert_eq!(e.cursor().1, 0);
9588 assert_eq!(e.vim.visual_anchor, (0, 4));
9590 }
9591
9592 #[test]
9593 fn editing_inside_fold_invalidates_it() {
9594 let mut e = editor_with("a\nb\nc\nd");
9595 e.buffer_mut().add_fold(1, 2, true);
9596 e.jump_cursor(1, 0);
9597 run_keys(&mut e, "iX<Esc>");
9599 assert!(e.buffer().folds().is_empty());
9601 }
9602
9603 #[test]
9604 fn zd_removes_fold_under_cursor() {
9605 let mut e = editor_with("a\nb\nc\nd");
9606 e.buffer_mut().add_fold(1, 2, true);
9607 e.jump_cursor(2, 0);
9608 run_keys(&mut e, "zd");
9609 assert!(e.buffer().folds().is_empty());
9610 }
9611
9612 #[test]
9613 fn take_fold_ops_observes_z_keystroke_dispatch() {
9614 use crate::types::FoldOp;
9619 let mut e = editor_with("a\nb\nc\nd");
9620 e.buffer_mut().add_fold(1, 2, true);
9621 e.jump_cursor(1, 0);
9622 let _ = e.take_fold_ops();
9625 run_keys(&mut e, "zo");
9626 run_keys(&mut e, "zM");
9627 let ops = e.take_fold_ops();
9628 assert_eq!(ops.len(), 2);
9629 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9630 assert!(matches!(ops[1], FoldOp::CloseAll));
9631 assert!(e.take_fold_ops().is_empty());
9633 }
9634
9635 #[test]
9636 fn edit_pipeline_emits_invalidate_fold_op() {
9637 use crate::types::FoldOp;
9640 let mut e = editor_with("a\nb\nc\nd");
9641 e.buffer_mut().add_fold(1, 2, true);
9642 e.jump_cursor(1, 0);
9643 let _ = e.take_fold_ops();
9644 run_keys(&mut e, "iX<Esc>");
9645 let ops = e.take_fold_ops();
9646 assert!(
9647 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9648 "expected at least one Invalidate op, got {ops:?}"
9649 );
9650 }
9651
9652 #[test]
9653 fn dot_mark_jumps_to_last_edit_position() {
9654 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9655 e.jump_cursor(2, 0);
9656 run_keys(&mut e, "iX<Esc>");
9658 let after_edit = e.cursor();
9659 run_keys(&mut e, "gg");
9661 assert_eq!(e.cursor().0, 0);
9662 run_keys(&mut e, "'.");
9664 assert_eq!(e.cursor().0, after_edit.0);
9665 }
9666
9667 #[test]
9668 fn quote_quote_returns_to_pre_jump_position() {
9669 let mut e = editor_with_rows(50, 20);
9670 e.jump_cursor(10, 2);
9671 let before = e.cursor();
9672 run_keys(&mut e, "G");
9674 assert_ne!(e.cursor(), before);
9675 run_keys(&mut e, "''");
9677 assert_eq!(e.cursor().0, before.0);
9678 }
9679
9680 #[test]
9681 fn backtick_backtick_restores_exact_pre_jump_pos() {
9682 let mut e = editor_with_rows(50, 20);
9683 e.jump_cursor(7, 3);
9684 let before = e.cursor();
9685 run_keys(&mut e, "G");
9686 run_keys(&mut e, "``");
9687 assert_eq!(e.cursor(), before);
9688 }
9689
9690 #[test]
9691 fn macro_record_and_replay_basic() {
9692 let mut e = editor_with("foo\nbar\nbaz");
9693 run_keys(&mut e, "qaIX<Esc>jq");
9695 assert_eq!(e.buffer().lines()[0], "Xfoo");
9696 run_keys(&mut e, "@a");
9698 assert_eq!(e.buffer().lines()[1], "Xbar");
9699 run_keys(&mut e, "j@@");
9701 assert_eq!(e.buffer().lines()[2], "Xbaz");
9702 }
9703
9704 #[test]
9705 fn macro_count_replays_n_times() {
9706 let mut e = editor_with("a\nb\nc\nd\ne");
9707 run_keys(&mut e, "qajq");
9709 assert_eq!(e.cursor().0, 1);
9710 run_keys(&mut e, "3@a");
9712 assert_eq!(e.cursor().0, 4);
9713 }
9714
9715 #[test]
9716 fn macro_capital_q_appends_to_lowercase_register() {
9717 let mut e = editor_with("hello");
9718 run_keys(&mut e, "qall<Esc>q");
9719 run_keys(&mut e, "qAhh<Esc>q");
9720 let text = e.registers().read('a').unwrap().text.clone();
9723 assert!(text.contains("ll<Esc>"));
9724 assert!(text.contains("hh<Esc>"));
9725 }
9726
9727 #[test]
9728 fn buffer_selection_block_in_visual_block_mode() {
9729 use hjkl_buffer::{Position, Selection};
9730 let mut e = editor_with("aaaa\nbbbb\ncccc");
9731 run_keys(&mut e, "<C-v>jl");
9732 assert_eq!(
9733 e.buffer_selection(),
9734 Some(Selection::Block {
9735 anchor: Position::new(0, 0),
9736 head: Position::new(1, 1),
9737 })
9738 );
9739 }
9740
9741 #[test]
9744 fn n_after_question_mark_keeps_walking_backward() {
9745 let mut e = editor_with("foo bar foo baz foo end");
9748 e.jump_cursor(0, 22);
9749 run_keys(&mut e, "?foo<CR>");
9750 assert_eq!(e.cursor().1, 16);
9751 run_keys(&mut e, "n");
9752 assert_eq!(e.cursor().1, 8);
9753 run_keys(&mut e, "N");
9754 assert_eq!(e.cursor().1, 16);
9755 }
9756
9757 #[test]
9758 fn nested_macro_chord_records_literal_keys() {
9759 let mut e = editor_with("alpha\nbeta\ngamma");
9762 run_keys(&mut e, "qblq");
9764 run_keys(&mut e, "qaIX<Esc>q");
9767 e.jump_cursor(1, 0);
9769 run_keys(&mut e, "@a");
9770 assert_eq!(e.buffer().lines()[1], "Xbeta");
9771 }
9772
9773 #[test]
9774 fn shift_gt_motion_indents_one_line() {
9775 let mut e = editor_with("hello world");
9779 run_keys(&mut e, ">w");
9780 assert_eq!(e.buffer().lines()[0], " hello world");
9781 }
9782
9783 #[test]
9784 fn shift_lt_motion_outdents_one_line() {
9785 let mut e = editor_with(" hello world");
9786 run_keys(&mut e, "<lt>w");
9787 assert_eq!(e.buffer().lines()[0], " hello world");
9789 }
9790
9791 #[test]
9792 fn shift_gt_text_object_indents_paragraph() {
9793 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9794 e.jump_cursor(0, 0);
9795 run_keys(&mut e, ">ip");
9796 assert_eq!(e.buffer().lines()[0], " alpha");
9797 assert_eq!(e.buffer().lines()[1], " beta");
9798 assert_eq!(e.buffer().lines()[2], " gamma");
9799 assert_eq!(e.buffer().lines()[4], "rest");
9801 }
9802
9803 #[test]
9804 fn ctrl_o_runs_exactly_one_normal_command() {
9805 let mut e = editor_with("alpha beta gamma");
9808 e.jump_cursor(0, 0);
9809 run_keys(&mut e, "i");
9810 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9811 run_keys(&mut e, "dw");
9812 assert_eq!(e.vim_mode(), VimMode::Insert);
9814 run_keys(&mut e, "X");
9816 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9817 }
9818
9819 #[test]
9820 fn macro_replay_respects_mode_switching() {
9821 let mut e = editor_with("hi");
9825 run_keys(&mut e, "qaiX<Esc>0q");
9826 assert_eq!(e.vim_mode(), VimMode::Normal);
9827 e.set_content("yo");
9829 run_keys(&mut e, "@a");
9830 assert_eq!(e.vim_mode(), VimMode::Normal);
9831 assert_eq!(e.cursor().1, 0);
9832 assert_eq!(e.buffer().lines()[0], "Xyo");
9833 }
9834
9835 #[test]
9836 fn macro_recorded_text_round_trips_through_register() {
9837 let mut e = editor_with("");
9841 run_keys(&mut e, "qaiX<Esc>q");
9842 let text = e.registers().read('a').unwrap().text.clone();
9843 assert!(text.starts_with("iX"));
9844 run_keys(&mut e, "@a");
9846 assert_eq!(e.buffer().lines()[0], "XX");
9847 }
9848
9849 #[test]
9850 fn dot_after_macro_replays_macros_last_change() {
9851 let mut e = editor_with("ab\ncd\nef");
9854 run_keys(&mut e, "qaIX<Esc>jq");
9857 assert_eq!(e.buffer().lines()[0], "Xab");
9858 run_keys(&mut e, "@a");
9859 assert_eq!(e.buffer().lines()[1], "Xcd");
9860 let row_before_dot = e.cursor().0;
9863 run_keys(&mut e, ".");
9864 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9865 }
9866
9867 fn si_editor(content: &str) -> Editor {
9873 let opts = crate::types::Options {
9874 shiftwidth: 4,
9875 softtabstop: 4,
9876 expandtab: true,
9877 smartindent: true,
9878 autoindent: true,
9879 ..crate::types::Options::default()
9880 };
9881 let mut e = Editor::new(
9882 hjkl_buffer::Buffer::new(),
9883 crate::types::DefaultHost::new(),
9884 opts,
9885 );
9886 e.set_content(content);
9887 e
9888 }
9889
9890 #[test]
9891 fn smartindent_bumps_indent_after_open_brace() {
9892 let mut e = si_editor("fn foo() {");
9894 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9896 assert_eq!(
9897 e.buffer().lines()[1],
9898 " ",
9899 "smartindent should bump one shiftwidth after {{"
9900 );
9901 }
9902
9903 #[test]
9904 fn smartindent_no_bump_when_off() {
9905 let mut e = si_editor("fn foo() {");
9908 e.settings_mut().smartindent = false;
9909 e.jump_cursor(0, 10);
9910 run_keys(&mut e, "i<CR>");
9911 assert_eq!(
9912 e.buffer().lines()[1],
9913 "",
9914 "without smartindent, no bump: new line copies empty leading ws"
9915 );
9916 }
9917
9918 #[test]
9919 fn smartindent_uses_tab_when_noexpandtab() {
9920 let opts = crate::types::Options {
9922 shiftwidth: 4,
9923 softtabstop: 0,
9924 expandtab: false,
9925 smartindent: true,
9926 autoindent: true,
9927 ..crate::types::Options::default()
9928 };
9929 let mut e = Editor::new(
9930 hjkl_buffer::Buffer::new(),
9931 crate::types::DefaultHost::new(),
9932 opts,
9933 );
9934 e.set_content("fn foo() {");
9935 e.jump_cursor(0, 10);
9936 run_keys(&mut e, "i<CR>");
9937 assert_eq!(
9938 e.buffer().lines()[1],
9939 "\t",
9940 "noexpandtab: smartindent bump inserts a literal tab"
9941 );
9942 }
9943
9944 #[test]
9945 fn smartindent_dedent_on_close_brace() {
9946 let mut e = si_editor("fn foo() {");
9949 e.set_content("fn foo() {\n ");
9951 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9953 assert_eq!(
9954 e.buffer().lines()[1],
9955 "}",
9956 "close brace on whitespace-only line should dedent"
9957 );
9958 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9959 }
9960
9961 #[test]
9962 fn smartindent_no_dedent_when_off() {
9963 let mut e = si_editor("fn foo() {\n ");
9965 e.settings_mut().smartindent = false;
9966 e.jump_cursor(1, 4);
9967 run_keys(&mut e, "i}");
9968 assert_eq!(
9969 e.buffer().lines()[1],
9970 " }",
9971 "without smartindent, `}}` just appends at cursor"
9972 );
9973 }
9974
9975 #[test]
9976 fn smartindent_no_dedent_mid_line() {
9977 let mut e = si_editor(" let x = 1");
9980 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9982 assert_eq!(
9983 e.buffer().lines()[0],
9984 " let x = 1}",
9985 "mid-line `}}` should not dedent"
9986 );
9987 }
9988
9989 #[test]
9993 fn count_5x_fills_unnamed_register() {
9994 let mut e = editor_with("hello world\n");
9995 e.jump_cursor(0, 0);
9996 run_keys(&mut e, "5x");
9997 assert_eq!(e.buffer().lines()[0], " world");
9998 assert_eq!(e.cursor(), (0, 0));
9999 assert_eq!(e.yank(), "hello");
10000 }
10001
10002 #[test]
10003 fn x_fills_unnamed_register_single_char() {
10004 let mut e = editor_with("abc\n");
10005 e.jump_cursor(0, 0);
10006 run_keys(&mut e, "x");
10007 assert_eq!(e.buffer().lines()[0], "bc");
10008 assert_eq!(e.yank(), "a");
10009 }
10010
10011 #[test]
10012 fn big_x_fills_unnamed_register() {
10013 let mut e = editor_with("hello\n");
10014 e.jump_cursor(0, 3);
10015 run_keys(&mut e, "X");
10016 assert_eq!(e.buffer().lines()[0], "helo");
10017 assert_eq!(e.yank(), "l");
10018 }
10019
10020 #[test]
10022 fn g_motion_trailing_newline_lands_on_last_content_row() {
10023 let mut e = editor_with("foo\nbar\nbaz\n");
10024 e.jump_cursor(0, 0);
10025 run_keys(&mut e, "G");
10026 assert_eq!(
10028 e.cursor().0,
10029 2,
10030 "G should land on row 2 (baz), not row 3 (phantom empty)"
10031 );
10032 }
10033
10034 #[test]
10036 fn dd_last_line_clamps_cursor_to_new_last_row() {
10037 let mut e = editor_with("foo\nbar\n");
10038 e.jump_cursor(1, 0);
10039 run_keys(&mut e, "dd");
10040 assert_eq!(e.buffer().lines()[0], "foo");
10041 assert_eq!(
10042 e.cursor(),
10043 (0, 0),
10044 "cursor should clamp to row 0 after dd on last content line"
10045 );
10046 }
10047
10048 #[test]
10050 fn d_dollar_cursor_on_last_char() {
10051 let mut e = editor_with("hello world\n");
10052 e.jump_cursor(0, 5);
10053 run_keys(&mut e, "d$");
10054 assert_eq!(e.buffer().lines()[0], "hello");
10055 assert_eq!(
10056 e.cursor(),
10057 (0, 4),
10058 "d$ should leave cursor on col 4, not col 5"
10059 );
10060 }
10061
10062 #[test]
10064 fn undo_insert_clamps_cursor_to_last_valid_col() {
10065 let mut e = editor_with("hello\n");
10066 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10068 assert_eq!(e.buffer().lines()[0], "hello");
10069 assert_eq!(
10070 e.cursor(),
10071 (0, 4),
10072 "undo should clamp cursor to col 4 on 'hello'"
10073 );
10074 }
10075
10076 #[test]
10078 fn da_doublequote_eats_trailing_whitespace() {
10079 let mut e = editor_with("say \"hello\" there\n");
10080 e.jump_cursor(0, 6);
10081 run_keys(&mut e, "da\"");
10082 assert_eq!(e.buffer().lines()[0], "say there");
10083 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10084 }
10085
10086 #[test]
10088 fn dab_cursor_col_clamped_after_delete() {
10089 let mut e = editor_with("fn x() {\n body\n}\n");
10090 e.jump_cursor(1, 4);
10091 run_keys(&mut e, "daB");
10092 assert_eq!(e.buffer().lines()[0], "fn x() ");
10093 assert_eq!(
10094 e.cursor(),
10095 (0, 6),
10096 "daB should leave cursor at col 6, not 7"
10097 );
10098 }
10099
10100 #[test]
10102 fn dib_preserves_surrounding_newlines() {
10103 let mut e = editor_with("{\n body\n}\n");
10104 e.jump_cursor(1, 4);
10105 run_keys(&mut e, "diB");
10106 assert_eq!(e.buffer().lines()[0], "{");
10107 assert_eq!(e.buffer().lines()[1], "}");
10108 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10109 }
10110
10111 #[test]
10112 fn is_chord_pending_tracks_replace_state() {
10113 let mut e = editor_with("abc\n");
10114 assert!(!e.is_chord_pending());
10115 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10117 assert!(e.is_chord_pending(), "engine should be pending after r");
10118 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10120 assert!(
10121 !e.is_chord_pending(),
10122 "engine pending should clear after replace"
10123 );
10124 }
10125
10126 #[test]
10129 fn yiw_sets_lbr_rbr_marks_around_word() {
10130 let mut e = editor_with("hello world");
10133 run_keys(&mut e, "yiw");
10134 let lo = e.mark('[').expect("'[' must be set after yiw");
10135 let hi = e.mark(']').expect("']' must be set after yiw");
10136 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10137 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10138 }
10139
10140 #[test]
10141 fn yj_linewise_sets_marks_at_line_edges() {
10142 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10145 run_keys(&mut e, "yj");
10146 let lo = e.mark('[').expect("'[' must be set after yj");
10147 let hi = e.mark(']').expect("']' must be set after yj");
10148 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10149 assert_eq!(
10150 hi,
10151 (1, 4),
10152 "'] snaps to (bot_row, last_col) for linewise yank"
10153 );
10154 }
10155
10156 #[test]
10157 fn dd_sets_lbr_rbr_marks_to_cursor() {
10158 let mut e = editor_with("aaa\nbbb");
10161 run_keys(&mut e, "dd");
10162 let lo = e.mark('[').expect("'[' must be set after dd");
10163 let hi = e.mark(']').expect("']' must be set after dd");
10164 assert_eq!(lo, hi, "after delete both marks are at the same position");
10165 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10166 }
10167
10168 #[test]
10169 fn dw_sets_lbr_rbr_marks_to_cursor() {
10170 let mut e = editor_with("hello world");
10173 run_keys(&mut e, "dw");
10174 let lo = e.mark('[').expect("'[' must be set after dw");
10175 let hi = e.mark(']').expect("']' must be set after dw");
10176 assert_eq!(lo, hi, "after delete both marks are at the same position");
10177 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10178 }
10179
10180 #[test]
10181 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10182 let mut e = editor_with("hello world");
10187 run_keys(&mut e, "cwfoo<Esc>");
10188 let lo = e.mark('[').expect("'[' must be set after cw");
10189 let hi = e.mark(']').expect("']' must be set after cw");
10190 assert_eq!(lo, (0, 0), "'[ should be start of change");
10191 assert_eq!(hi.0, 0, "'] should be on row 0");
10194 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10195 }
10196
10197 #[test]
10198 fn cw_with_no_insertion_sets_marks_at_change_start() {
10199 let mut e = editor_with("hello world");
10202 run_keys(&mut e, "cw<Esc>");
10203 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10204 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10205 assert_eq!(lo.0, 0, "'[ should be on row 0");
10206 assert_eq!(hi.0, 0, "'] should be on row 0");
10207 assert_eq!(lo, hi, "marks coincide when insert is empty");
10209 }
10210
10211 #[test]
10212 fn p_charwise_sets_marks_around_pasted_text() {
10213 let mut e = editor_with("abc xyz");
10216 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10219 let hi = e.mark(']').expect("']' set after charwise paste");
10220 assert!(lo <= hi, "'[ must not exceed ']'");
10221 assert_eq!(
10223 hi.1.wrapping_sub(lo.1),
10224 2,
10225 "'] - '[ should span 2 cols for a 3-char paste"
10226 );
10227 }
10228
10229 #[test]
10230 fn p_linewise_sets_marks_at_line_edges() {
10231 let mut e = editor_with("aaa\nbbb\nccc");
10234 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10238 let hi = e.mark(']').expect("']' set after linewise paste");
10239 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10240 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10241 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10242 }
10243
10244 #[test]
10245 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10246 let mut e = editor_with("hello world");
10250 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10254 assert_eq!(
10256 e.cursor(),
10257 (0, 4),
10258 "visual `[v`] should land on last yanked char"
10259 );
10260 assert_eq!(
10262 e.vim_mode(),
10263 crate::VimMode::Visual,
10264 "should be in Visual mode"
10265 );
10266 }
10267
10268 #[test]
10274 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10275 let mut e = editor_with("hello\nworld\n");
10278 e.jump_cursor(0, 0);
10279 run_keys(&mut e, "iX<Esc>j`.");
10280 assert_eq!(
10281 e.cursor(),
10282 (0, 0),
10283 "dot mark should jump to the change-start (col 0), not post-insert col"
10284 );
10285 }
10286
10287 #[test]
10290 fn count_100g_clamps_to_last_content_row() {
10291 let mut e = editor_with("foo\nbar\nbaz\n");
10294 e.jump_cursor(0, 0);
10295 run_keys(&mut e, "100G");
10296 assert_eq!(
10297 e.cursor(),
10298 (2, 0),
10299 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10300 );
10301 }
10302
10303 #[test]
10306 fn gi_resumes_last_insert_position() {
10307 let mut e = editor_with("world\nhello\n");
10313 e.jump_cursor(0, 0);
10314 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10315 assert_eq!(
10316 e.vim_mode(),
10317 crate::VimMode::Normal,
10318 "should be in Normal mode after gi<Esc>"
10319 );
10320 assert_eq!(
10321 e.cursor(),
10322 (0, 1),
10323 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10324 );
10325 }
10326
10327 #[test]
10331 fn visual_block_change_cursor_on_last_inserted_char() {
10332 let mut e = editor_with("foo\nbar\nbaz\n");
10336 e.jump_cursor(0, 0);
10337 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10338 let lines = e.buffer().lines().to_vec();
10339 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10340 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10341 assert_eq!(
10342 e.cursor(),
10343 (0, 1),
10344 "cursor should be on last char of inserted 'ZZ' (col 1)"
10345 );
10346 }
10347
10348 #[test]
10353 fn register_blackhole_delete_preserves_unnamed_register() {
10354 let mut e = editor_with("foo bar baz\n");
10361 e.jump_cursor(0, 0);
10362 run_keys(&mut e, "yiww\"_dwbp");
10363 let lines = e.buffer().lines().to_vec();
10364 assert_eq!(
10365 lines[0], "ffoooo baz",
10366 "black-hole delete must not corrupt unnamed register"
10367 );
10368 assert_eq!(
10369 e.cursor(),
10370 (0, 3),
10371 "cursor should be on last pasted char (col 3)"
10372 );
10373 }
10374}