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