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
2863pub(crate) fn apply_op_motion_key<H: crate::types::Host>(
2877 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2878 op: Operator,
2879 motion_key: char,
2880 total_count: usize,
2881) {
2882 let input = Input {
2883 key: Key::Char(motion_key),
2884 ctrl: false,
2885 alt: false,
2886 shift: false,
2887 };
2888 let Some(motion) = parse_motion(&input) else {
2889 return;
2890 };
2891 let motion = match motion {
2892 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2893 Some((ch, forward, till)) => Motion::Find {
2894 ch,
2895 forward: if reverse { !forward } else { forward },
2896 till,
2897 },
2898 None => return,
2899 },
2900 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2902 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2903 m => m,
2904 };
2905 apply_op_with_motion(ed, op, &motion, total_count);
2906 if let Motion::Find { ch, forward, till } = &motion {
2907 ed.vim.last_find = Some((*ch, *forward, *till));
2908 }
2909 if !ed.vim.replaying && op_is_change(op) {
2910 ed.vim.last_change = Some(LastChange::OpMotion {
2911 op,
2912 motion,
2913 count: total_count,
2914 inserted: None,
2915 });
2916 }
2917}
2918
2919pub(crate) fn apply_op_double<H: crate::types::Host>(
2922 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2923 op: Operator,
2924 total_count: usize,
2925) {
2926 execute_line_op(ed, op, total_count);
2927 if !ed.vim.replaying {
2928 ed.vim.last_change = Some(LastChange::LineOp {
2929 op,
2930 count: total_count,
2931 inserted: None,
2932 });
2933 }
2934}
2935
2936pub(crate) fn enter_op_text_obj<H: crate::types::Host>(
2939 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2940 op: Operator,
2941 count1: usize,
2942 inner: bool,
2943) {
2944 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2945}
2946
2947pub(crate) fn enter_op_g<H: crate::types::Host>(
2950 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2951 op: Operator,
2952 count1: usize,
2953) {
2954 ed.vim.pending = Pending::OpG { op, count1 };
2955}
2956
2957pub(crate) fn enter_op_find<H: crate::types::Host>(
2960 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2961 op: Operator,
2962 count1: usize,
2963 forward: bool,
2964 till: bool,
2965) {
2966 ed.vim.pending = Pending::OpFind {
2967 op,
2968 count1,
2969 forward,
2970 till,
2971 };
2972}
2973
2974fn handle_after_op<H: crate::types::Host>(
2975 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2976 input: Input,
2977 op: Operator,
2978 count1: usize,
2979) -> bool {
2980 if let Key::Char(d @ '0'..='9') = input.key
2982 && !input.ctrl
2983 && (d != '0' || ed.vim.count > 0)
2984 {
2985 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2986 ed.vim.pending = Pending::Op { op, count1 };
2987 return true;
2988 }
2989
2990 if input.key == Key::Esc {
2992 ed.vim.count = 0;
2993 return true;
2994 }
2995
2996 let double_ch = match op {
3000 Operator::Delete => Some('d'),
3001 Operator::Change => Some('c'),
3002 Operator::Yank => Some('y'),
3003 Operator::Indent => Some('>'),
3004 Operator::Outdent => Some('<'),
3005 Operator::Uppercase => Some('U'),
3006 Operator::Lowercase => Some('u'),
3007 Operator::ToggleCase => Some('~'),
3008 Operator::Fold => None,
3009 Operator::Reflow => Some('q'),
3012 };
3013 if let Key::Char(c) = input.key
3014 && !input.ctrl
3015 && Some(c) == double_ch
3016 {
3017 let count2 = take_count(&mut ed.vim);
3018 let total = count1.max(1) * count2.max(1);
3019 execute_line_op(ed, op, total);
3020 if !ed.vim.replaying {
3021 ed.vim.last_change = Some(LastChange::LineOp {
3022 op,
3023 count: total,
3024 inserted: None,
3025 });
3026 }
3027 return true;
3028 }
3029
3030 if let Key::Char('i') | Key::Char('a') = input.key
3032 && !input.ctrl
3033 {
3034 let inner = matches!(input.key, Key::Char('i'));
3035 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
3036 return true;
3037 }
3038
3039 if input.key == Key::Char('g') && !input.ctrl {
3041 ed.vim.pending = Pending::OpG { op, count1 };
3042 return true;
3043 }
3044
3045 if let Some((forward, till)) = find_entry(&input) {
3047 ed.vim.pending = Pending::OpFind {
3048 op,
3049 count1,
3050 forward,
3051 till,
3052 };
3053 return true;
3054 }
3055
3056 let count2 = take_count(&mut ed.vim);
3058 let total = count1.max(1) * count2.max(1);
3059 if let Some(motion) = parse_motion(&input) {
3060 let motion = match motion {
3061 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3062 Some((ch, forward, till)) => Motion::Find {
3063 ch,
3064 forward: if reverse { !forward } else { forward },
3065 till,
3066 },
3067 None => return true,
3068 },
3069 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3073 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3074 m => m,
3075 };
3076 apply_op_with_motion(ed, op, &motion, total);
3077 if let Motion::Find { ch, forward, till } = &motion {
3078 ed.vim.last_find = Some((*ch, *forward, *till));
3079 }
3080 if !ed.vim.replaying && op_is_change(op) {
3081 ed.vim.last_change = Some(LastChange::OpMotion {
3082 op,
3083 motion,
3084 count: total,
3085 inserted: None,
3086 });
3087 }
3088 return true;
3089 }
3090
3091 true
3093}
3094
3095fn handle_op_after_g<H: crate::types::Host>(
3096 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3097 input: Input,
3098 op: Operator,
3099 count1: usize,
3100) -> bool {
3101 if input.ctrl {
3102 return true;
3103 }
3104 let count2 = take_count(&mut ed.vim);
3105 let total = count1.max(1) * count2.max(1);
3106 if matches!(
3110 op,
3111 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3112 ) {
3113 let op_char = match op {
3114 Operator::Uppercase => 'U',
3115 Operator::Lowercase => 'u',
3116 Operator::ToggleCase => '~',
3117 _ => unreachable!(),
3118 };
3119 if input.key == Key::Char(op_char) {
3120 execute_line_op(ed, op, total);
3121 if !ed.vim.replaying {
3122 ed.vim.last_change = Some(LastChange::LineOp {
3123 op,
3124 count: total,
3125 inserted: None,
3126 });
3127 }
3128 return true;
3129 }
3130 }
3131 let motion = match input.key {
3132 Key::Char('g') => Motion::FileTop,
3133 Key::Char('e') => Motion::WordEndBack,
3134 Key::Char('E') => Motion::BigWordEndBack,
3135 Key::Char('j') => Motion::ScreenDown,
3136 Key::Char('k') => Motion::ScreenUp,
3137 _ => return true,
3138 };
3139 apply_op_with_motion(ed, op, &motion, total);
3140 if !ed.vim.replaying && op_is_change(op) {
3141 ed.vim.last_change = Some(LastChange::OpMotion {
3142 op,
3143 motion,
3144 count: total,
3145 inserted: None,
3146 });
3147 }
3148 true
3149}
3150
3151fn handle_after_g<H: crate::types::Host>(
3152 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3153 input: Input,
3154) -> bool {
3155 let count = take_count(&mut ed.vim);
3156 if let Key::Char(ch) = input.key {
3159 apply_after_g(ed, ch, count);
3160 }
3161 true
3162}
3163
3164pub(crate) fn apply_after_g<H: crate::types::Host>(
3169 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3170 ch: char,
3171 count: usize,
3172) {
3173 match ch {
3174 'g' => {
3175 let pre = ed.cursor();
3177 if count > 1 {
3178 ed.jump_cursor(count - 1, 0);
3179 } else {
3180 ed.jump_cursor(0, 0);
3181 }
3182 move_first_non_whitespace(ed);
3183 if ed.cursor() != pre {
3184 push_jump(ed, pre);
3185 }
3186 }
3187 'e' => execute_motion(ed, Motion::WordEndBack, count),
3188 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3189 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3191 'M' => execute_motion(ed, Motion::LineMiddle, count),
3193 'v' => {
3195 if let Some(snap) = ed.vim.last_visual {
3196 match snap.mode {
3197 Mode::Visual => {
3198 ed.vim.visual_anchor = snap.anchor;
3199 ed.vim.mode = Mode::Visual;
3200 }
3201 Mode::VisualLine => {
3202 ed.vim.visual_line_anchor = snap.anchor.0;
3203 ed.vim.mode = Mode::VisualLine;
3204 }
3205 Mode::VisualBlock => {
3206 ed.vim.block_anchor = snap.anchor;
3207 ed.vim.block_vcol = snap.block_vcol;
3208 ed.vim.mode = Mode::VisualBlock;
3209 }
3210 _ => {}
3211 }
3212 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3213 }
3214 }
3215 'j' => execute_motion(ed, Motion::ScreenDown, count),
3219 'k' => execute_motion(ed, Motion::ScreenUp, count),
3220 'U' => {
3224 ed.vim.pending = Pending::Op {
3225 op: Operator::Uppercase,
3226 count1: count,
3227 };
3228 }
3229 'u' => {
3230 ed.vim.pending = Pending::Op {
3231 op: Operator::Lowercase,
3232 count1: count,
3233 };
3234 }
3235 '~' => {
3236 ed.vim.pending = Pending::Op {
3237 op: Operator::ToggleCase,
3238 count1: count,
3239 };
3240 }
3241 'q' => {
3242 ed.vim.pending = Pending::Op {
3245 op: Operator::Reflow,
3246 count1: count,
3247 };
3248 }
3249 'J' => {
3250 for _ in 0..count.max(1) {
3252 ed.push_undo();
3253 join_line_raw(ed);
3254 }
3255 if !ed.vim.replaying {
3256 ed.vim.last_change = Some(LastChange::JoinLine {
3257 count: count.max(1),
3258 });
3259 }
3260 }
3261 'd' => {
3262 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3267 }
3268 'i' => {
3273 if let Some((row, col)) = ed.vim.last_insert_pos {
3274 ed.jump_cursor(row, col);
3275 }
3276 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3277 }
3278 ';' => walk_change_list(ed, -1, count.max(1)),
3281 ',' => walk_change_list(ed, 1, count.max(1)),
3282 '*' => execute_motion(
3286 ed,
3287 Motion::WordAtCursor {
3288 forward: true,
3289 whole_word: false,
3290 },
3291 count,
3292 ),
3293 '#' => execute_motion(
3294 ed,
3295 Motion::WordAtCursor {
3296 forward: false,
3297 whole_word: false,
3298 },
3299 count,
3300 ),
3301 _ => {}
3302 }
3303}
3304
3305fn handle_after_z<H: crate::types::Host>(
3306 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3307 input: Input,
3308) -> bool {
3309 let count = take_count(&mut ed.vim);
3310 if let Key::Char(ch) = input.key {
3313 apply_after_z(ed, ch, count);
3314 }
3315 true
3316}
3317
3318pub(crate) fn apply_after_z<H: crate::types::Host>(
3323 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3324 ch: char,
3325 count: usize,
3326) {
3327 use crate::editor::CursorScrollTarget;
3328 let row = ed.cursor().0;
3329 match ch {
3330 'z' => {
3331 ed.scroll_cursor_to(CursorScrollTarget::Center);
3332 ed.vim.viewport_pinned = true;
3333 }
3334 't' => {
3335 ed.scroll_cursor_to(CursorScrollTarget::Top);
3336 ed.vim.viewport_pinned = true;
3337 }
3338 'b' => {
3339 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3340 ed.vim.viewport_pinned = true;
3341 }
3342 'o' => {
3347 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3348 }
3349 'c' => {
3350 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3351 }
3352 'a' => {
3353 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3354 }
3355 'R' => {
3356 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3357 }
3358 'M' => {
3359 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3360 }
3361 'E' => {
3362 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3363 }
3364 'd' => {
3365 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3366 }
3367 'f' => {
3368 if matches!(
3369 ed.vim.mode,
3370 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3371 ) {
3372 let anchor_row = match ed.vim.mode {
3375 Mode::VisualLine => ed.vim.visual_line_anchor,
3376 Mode::VisualBlock => ed.vim.block_anchor.0,
3377 _ => ed.vim.visual_anchor.0,
3378 };
3379 let cur = ed.cursor().0;
3380 let top = anchor_row.min(cur);
3381 let bot = anchor_row.max(cur);
3382 ed.apply_fold_op(crate::types::FoldOp::Add {
3383 start_row: top,
3384 end_row: bot,
3385 closed: true,
3386 });
3387 ed.vim.mode = Mode::Normal;
3388 } else {
3389 ed.vim.pending = Pending::Op {
3394 op: Operator::Fold,
3395 count1: count,
3396 };
3397 }
3398 }
3399 _ => {}
3400 }
3401}
3402
3403fn handle_replace<H: crate::types::Host>(
3404 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3405 input: Input,
3406) -> bool {
3407 if let Key::Char(ch) = input.key {
3408 if ed.vim.mode == Mode::VisualBlock {
3409 block_replace(ed, ch);
3410 return true;
3411 }
3412 let count = take_count(&mut ed.vim);
3413 replace_char(ed, ch, count.max(1));
3414 if !ed.vim.replaying {
3415 ed.vim.last_change = Some(LastChange::ReplaceChar {
3416 ch,
3417 count: count.max(1),
3418 });
3419 }
3420 }
3421 true
3422}
3423
3424fn handle_find_target<H: crate::types::Host>(
3425 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3426 input: Input,
3427 forward: bool,
3428 till: bool,
3429) -> bool {
3430 let Key::Char(ch) = input.key else {
3431 return true;
3432 };
3433 let count = take_count(&mut ed.vim);
3434 apply_find_char(ed, ch, forward, till, count.max(1));
3435 true
3436}
3437
3438pub(crate) fn apply_find_char<H: crate::types::Host>(
3444 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3445 ch: char,
3446 forward: bool,
3447 till: bool,
3448 count: usize,
3449) {
3450 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3451 ed.vim.last_find = Some((ch, forward, till));
3452}
3453
3454fn handle_op_find_target<H: crate::types::Host>(
3455 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3456 input: Input,
3457 op: Operator,
3458 count1: usize,
3459 forward: bool,
3460 till: bool,
3461) -> bool {
3462 let Key::Char(ch) = input.key else {
3463 return true;
3464 };
3465 let count2 = take_count(&mut ed.vim);
3466 let total = count1.max(1) * count2.max(1);
3467 let motion = Motion::Find { ch, forward, till };
3468 apply_op_with_motion(ed, op, &motion, total);
3469 ed.vim.last_find = Some((ch, forward, till));
3470 if !ed.vim.replaying && op_is_change(op) {
3471 ed.vim.last_change = Some(LastChange::OpMotion {
3472 op,
3473 motion,
3474 count: total,
3475 inserted: None,
3476 });
3477 }
3478 true
3479}
3480
3481fn handle_text_object<H: crate::types::Host>(
3482 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3483 input: Input,
3484 op: Operator,
3485 _count1: usize,
3486 inner: bool,
3487) -> bool {
3488 let Key::Char(ch) = input.key else {
3489 return true;
3490 };
3491 let obj = match ch {
3492 'w' => TextObject::Word { big: false },
3493 'W' => TextObject::Word { big: true },
3494 '"' | '\'' | '`' => TextObject::Quote(ch),
3495 '(' | ')' | 'b' => TextObject::Bracket('('),
3496 '[' | ']' => TextObject::Bracket('['),
3497 '{' | '}' | 'B' => TextObject::Bracket('{'),
3498 '<' | '>' => TextObject::Bracket('<'),
3499 'p' => TextObject::Paragraph,
3500 't' => TextObject::XmlTag,
3501 's' => TextObject::Sentence,
3502 _ => return true,
3503 };
3504 apply_op_with_text_object(ed, op, obj, inner);
3505 if !ed.vim.replaying && op_is_change(op) {
3506 ed.vim.last_change = Some(LastChange::OpTextObj {
3507 op,
3508 obj,
3509 inner,
3510 inserted: None,
3511 });
3512 }
3513 true
3514}
3515
3516fn handle_visual_text_obj<H: crate::types::Host>(
3517 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3518 input: Input,
3519 inner: bool,
3520) -> bool {
3521 let Key::Char(ch) = input.key else {
3522 return true;
3523 };
3524 let obj = match ch {
3525 'w' => TextObject::Word { big: false },
3526 'W' => TextObject::Word { big: true },
3527 '"' | '\'' | '`' => TextObject::Quote(ch),
3528 '(' | ')' | 'b' => TextObject::Bracket('('),
3529 '[' | ']' => TextObject::Bracket('['),
3530 '{' | '}' | 'B' => TextObject::Bracket('{'),
3531 '<' | '>' => TextObject::Bracket('<'),
3532 'p' => TextObject::Paragraph,
3533 't' => TextObject::XmlTag,
3534 's' => TextObject::Sentence,
3535 _ => return true,
3536 };
3537 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3538 return true;
3539 };
3540 match kind {
3544 MotionKind::Linewise => {
3545 ed.vim.visual_line_anchor = start.0;
3546 ed.vim.mode = Mode::VisualLine;
3547 ed.jump_cursor(end.0, 0);
3548 }
3549 _ => {
3550 ed.vim.mode = Mode::Visual;
3551 ed.vim.visual_anchor = (start.0, start.1);
3552 let (er, ec) = retreat_one(ed, end);
3553 ed.jump_cursor(er, ec);
3554 }
3555 }
3556 true
3557}
3558
3559fn retreat_one<H: crate::types::Host>(
3561 ed: &Editor<hjkl_buffer::Buffer, H>,
3562 pos: (usize, usize),
3563) -> (usize, usize) {
3564 let (r, c) = pos;
3565 if c > 0 {
3566 (r, c - 1)
3567 } else if r > 0 {
3568 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3569 (r - 1, prev_len)
3570 } else {
3571 (0, 0)
3572 }
3573}
3574
3575fn op_is_change(op: Operator) -> bool {
3576 matches!(op, Operator::Delete | Operator::Change)
3577}
3578
3579fn handle_normal_only<H: crate::types::Host>(
3582 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3583 input: &Input,
3584 count: usize,
3585) -> bool {
3586 if input.ctrl {
3587 return false;
3588 }
3589 match input.key {
3590 Key::Char('i') => {
3591 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3592 true
3593 }
3594 Key::Char('I') => {
3595 move_first_non_whitespace(ed);
3596 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3597 true
3598 }
3599 Key::Char('a') => {
3600 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3601 ed.push_buffer_cursor_to_textarea();
3602 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3603 true
3604 }
3605 Key::Char('A') => {
3606 crate::motions::move_line_end(&mut ed.buffer);
3607 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3608 ed.push_buffer_cursor_to_textarea();
3609 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3610 true
3611 }
3612 Key::Char('R') => {
3613 begin_insert(ed, count.max(1), InsertReason::Replace);
3616 true
3617 }
3618 Key::Char('o') => {
3619 use hjkl_buffer::{Edit, Position};
3620 ed.push_undo();
3621 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3624 ed.sync_buffer_content_from_textarea();
3625 let row = buf_cursor_pos(&ed.buffer).row;
3626 let line_chars = buf_line_chars(&ed.buffer, row);
3627 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3630 let indent = compute_enter_indent(&ed.settings, prev_line);
3631 ed.mutate_edit(Edit::InsertStr {
3632 at: Position::new(row, line_chars),
3633 text: format!("\n{indent}"),
3634 });
3635 ed.push_buffer_cursor_to_textarea();
3636 true
3637 }
3638 Key::Char('O') => {
3639 use hjkl_buffer::{Edit, Position};
3640 ed.push_undo();
3641 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3642 ed.sync_buffer_content_from_textarea();
3643 let row = buf_cursor_pos(&ed.buffer).row;
3644 let indent = if row > 0 {
3648 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3649 compute_enter_indent(&ed.settings, above)
3650 } else {
3651 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3652 cur.chars()
3653 .take_while(|c| *c == ' ' || *c == '\t')
3654 .collect::<String>()
3655 };
3656 ed.mutate_edit(Edit::InsertStr {
3657 at: Position::new(row, 0),
3658 text: format!("{indent}\n"),
3659 });
3660 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3665 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3666 let new_row = buf_cursor_pos(&ed.buffer).row;
3667 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3668 ed.push_buffer_cursor_to_textarea();
3669 true
3670 }
3671 Key::Char('x') => {
3672 do_char_delete(ed, true, count.max(1));
3673 if !ed.vim.replaying {
3674 ed.vim.last_change = Some(LastChange::CharDel {
3675 forward: true,
3676 count: count.max(1),
3677 });
3678 }
3679 true
3680 }
3681 Key::Char('X') => {
3682 do_char_delete(ed, false, count.max(1));
3683 if !ed.vim.replaying {
3684 ed.vim.last_change = Some(LastChange::CharDel {
3685 forward: false,
3686 count: count.max(1),
3687 });
3688 }
3689 true
3690 }
3691 Key::Char('~') => {
3692 for _ in 0..count.max(1) {
3693 ed.push_undo();
3694 toggle_case_at_cursor(ed);
3695 }
3696 if !ed.vim.replaying {
3697 ed.vim.last_change = Some(LastChange::ToggleCase {
3698 count: count.max(1),
3699 });
3700 }
3701 true
3702 }
3703 Key::Char('J') => {
3704 for _ in 0..count.max(1) {
3705 ed.push_undo();
3706 join_line(ed);
3707 }
3708 if !ed.vim.replaying {
3709 ed.vim.last_change = Some(LastChange::JoinLine {
3710 count: count.max(1),
3711 });
3712 }
3713 true
3714 }
3715 Key::Char('D') => {
3716 ed.push_undo();
3717 delete_to_eol(ed);
3718 crate::motions::move_left(&mut ed.buffer, 1);
3720 ed.push_buffer_cursor_to_textarea();
3721 if !ed.vim.replaying {
3722 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3723 }
3724 true
3725 }
3726 Key::Char('Y') => {
3727 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3729 true
3730 }
3731 Key::Char('C') => {
3732 ed.push_undo();
3733 delete_to_eol(ed);
3734 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3735 true
3736 }
3737 Key::Char('s') => {
3738 use hjkl_buffer::{Edit, MotionKind, Position};
3739 ed.push_undo();
3740 ed.sync_buffer_content_from_textarea();
3741 for _ in 0..count.max(1) {
3742 let cursor = buf_cursor_pos(&ed.buffer);
3743 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3744 if cursor.col >= line_chars {
3745 break;
3746 }
3747 ed.mutate_edit(Edit::DeleteRange {
3748 start: cursor,
3749 end: Position::new(cursor.row, cursor.col + 1),
3750 kind: MotionKind::Char,
3751 });
3752 }
3753 ed.push_buffer_cursor_to_textarea();
3754 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3755 if !ed.vim.replaying {
3757 ed.vim.last_change = Some(LastChange::OpMotion {
3758 op: Operator::Change,
3759 motion: Motion::Right,
3760 count: count.max(1),
3761 inserted: None,
3762 });
3763 }
3764 true
3765 }
3766 Key::Char('p') => {
3767 do_paste(ed, false, count.max(1));
3768 if !ed.vim.replaying {
3769 ed.vim.last_change = Some(LastChange::Paste {
3770 before: false,
3771 count: count.max(1),
3772 });
3773 }
3774 true
3775 }
3776 Key::Char('P') => {
3777 do_paste(ed, true, count.max(1));
3778 if !ed.vim.replaying {
3779 ed.vim.last_change = Some(LastChange::Paste {
3780 before: true,
3781 count: count.max(1),
3782 });
3783 }
3784 true
3785 }
3786 Key::Char('u') => {
3787 do_undo(ed);
3788 true
3789 }
3790 Key::Char('r') => {
3791 ed.vim.count = count;
3792 ed.vim.pending = Pending::Replace;
3793 true
3794 }
3795 Key::Char('/') => {
3796 enter_search(ed, true);
3797 true
3798 }
3799 Key::Char('?') => {
3800 enter_search(ed, false);
3801 true
3802 }
3803 Key::Char('.') => {
3804 replay_last_change(ed, count);
3805 true
3806 }
3807 _ => false,
3808 }
3809}
3810
3811fn begin_insert_noundo<H: crate::types::Host>(
3813 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3814 count: usize,
3815 reason: InsertReason,
3816) {
3817 let reason = if ed.vim.replaying {
3818 InsertReason::ReplayOnly
3819 } else {
3820 reason
3821 };
3822 let (row, _) = ed.cursor();
3823 ed.vim.insert_session = Some(InsertSession {
3824 count,
3825 row_min: row,
3826 row_max: row,
3827 before_lines: buf_lines_to_vec(&ed.buffer),
3828 reason,
3829 });
3830 ed.vim.mode = Mode::Insert;
3831}
3832
3833fn apply_op_with_motion<H: crate::types::Host>(
3836 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3837 op: Operator,
3838 motion: &Motion,
3839 count: usize,
3840) {
3841 let start = ed.cursor();
3842 apply_motion_cursor_ctx(ed, motion, count, true);
3847 let end = ed.cursor();
3848 let kind = motion_kind(motion);
3849 ed.jump_cursor(start.0, start.1);
3851 run_operator_over_range(ed, op, start, end, kind);
3852}
3853
3854fn apply_op_with_text_object<H: crate::types::Host>(
3855 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3856 op: Operator,
3857 obj: TextObject,
3858 inner: bool,
3859) {
3860 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3861 return;
3862 };
3863 ed.jump_cursor(start.0, start.1);
3864 run_operator_over_range(ed, op, start, end, kind);
3865}
3866
3867fn motion_kind(motion: &Motion) -> MotionKind {
3868 match motion {
3869 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3870 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3871 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3872 MotionKind::Linewise
3873 }
3874 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3875 MotionKind::Inclusive
3876 }
3877 Motion::Find { .. } => MotionKind::Inclusive,
3878 Motion::MatchBracket => MotionKind::Inclusive,
3879 Motion::LineEnd => MotionKind::Inclusive,
3881 _ => MotionKind::Exclusive,
3882 }
3883}
3884
3885fn run_operator_over_range<H: crate::types::Host>(
3886 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3887 op: Operator,
3888 start: (usize, usize),
3889 end: (usize, usize),
3890 kind: MotionKind,
3891) {
3892 let (top, bot) = order(start, end);
3893 if top == bot {
3894 return;
3895 }
3896
3897 match op {
3898 Operator::Yank => {
3899 let text = read_vim_range(ed, top, bot, kind);
3900 if !text.is_empty() {
3901 ed.record_yank_to_host(text.clone());
3902 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3903 }
3904 let rbr = match kind {
3908 MotionKind::Linewise => {
3909 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3910 (bot.0, last_col)
3911 }
3912 MotionKind::Inclusive => (bot.0, bot.1),
3913 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3914 };
3915 ed.set_mark('[', top);
3916 ed.set_mark(']', rbr);
3917 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3918 ed.push_buffer_cursor_to_textarea();
3919 }
3920 Operator::Delete => {
3921 ed.push_undo();
3922 cut_vim_range(ed, top, bot, kind);
3923 if !matches!(kind, MotionKind::Linewise) {
3928 clamp_cursor_to_normal_mode(ed);
3929 }
3930 ed.vim.mode = Mode::Normal;
3931 let pos = ed.cursor();
3935 ed.set_mark('[', pos);
3936 ed.set_mark(']', pos);
3937 }
3938 Operator::Change => {
3939 ed.vim.change_mark_start = Some(top);
3944 ed.push_undo();
3945 cut_vim_range(ed, top, bot, kind);
3946 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3947 }
3948 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3949 apply_case_op_to_selection(ed, op, top, bot, kind);
3950 }
3951 Operator::Indent | Operator::Outdent => {
3952 ed.push_undo();
3955 if op == Operator::Indent {
3956 indent_rows(ed, top.0, bot.0, 1);
3957 } else {
3958 outdent_rows(ed, top.0, bot.0, 1);
3959 }
3960 ed.vim.mode = Mode::Normal;
3961 }
3962 Operator::Fold => {
3963 if bot.0 >= top.0 {
3967 ed.apply_fold_op(crate::types::FoldOp::Add {
3968 start_row: top.0,
3969 end_row: bot.0,
3970 closed: true,
3971 });
3972 }
3973 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3974 ed.push_buffer_cursor_to_textarea();
3975 ed.vim.mode = Mode::Normal;
3976 }
3977 Operator::Reflow => {
3978 ed.push_undo();
3979 reflow_rows(ed, top.0, bot.0);
3980 ed.vim.mode = Mode::Normal;
3981 }
3982 }
3983}
3984
3985fn reflow_rows<H: crate::types::Host>(
3990 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3991 top: usize,
3992 bot: usize,
3993) {
3994 let width = ed.settings().textwidth.max(1);
3995 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3996 let bot = bot.min(lines.len().saturating_sub(1));
3997 if top > bot {
3998 return;
3999 }
4000 let original = lines[top..=bot].to_vec();
4001 let mut wrapped: Vec<String> = Vec::new();
4002 let mut paragraph: Vec<String> = Vec::new();
4003 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4004 if para.is_empty() {
4005 return;
4006 }
4007 let words = para.join(" ");
4008 let mut current = String::new();
4009 for word in words.split_whitespace() {
4010 let extra = if current.is_empty() {
4011 word.chars().count()
4012 } else {
4013 current.chars().count() + 1 + word.chars().count()
4014 };
4015 if extra > width && !current.is_empty() {
4016 out.push(std::mem::take(&mut current));
4017 current.push_str(word);
4018 } else if current.is_empty() {
4019 current.push_str(word);
4020 } else {
4021 current.push(' ');
4022 current.push_str(word);
4023 }
4024 }
4025 if !current.is_empty() {
4026 out.push(current);
4027 }
4028 para.clear();
4029 };
4030 for line in &original {
4031 if line.trim().is_empty() {
4032 flush(&mut paragraph, &mut wrapped, width);
4033 wrapped.push(String::new());
4034 } else {
4035 paragraph.push(line.clone());
4036 }
4037 }
4038 flush(&mut paragraph, &mut wrapped, width);
4039
4040 let after: Vec<String> = lines.split_off(bot + 1);
4042 lines.truncate(top);
4043 lines.extend(wrapped);
4044 lines.extend(after);
4045 ed.restore(lines, (top, 0));
4046 ed.mark_content_dirty();
4047}
4048
4049fn apply_case_op_to_selection<H: crate::types::Host>(
4055 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4056 op: Operator,
4057 top: (usize, usize),
4058 bot: (usize, usize),
4059 kind: MotionKind,
4060) {
4061 use hjkl_buffer::Edit;
4062 ed.push_undo();
4063 let saved_yank = ed.yank().to_string();
4064 let saved_yank_linewise = ed.vim.yank_linewise;
4065 let selection = cut_vim_range(ed, top, bot, kind);
4066 let transformed = match op {
4067 Operator::Uppercase => selection.to_uppercase(),
4068 Operator::Lowercase => selection.to_lowercase(),
4069 Operator::ToggleCase => toggle_case_str(&selection),
4070 _ => unreachable!(),
4071 };
4072 if !transformed.is_empty() {
4073 let cursor = buf_cursor_pos(&ed.buffer);
4074 ed.mutate_edit(Edit::InsertStr {
4075 at: cursor,
4076 text: transformed,
4077 });
4078 }
4079 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4080 ed.push_buffer_cursor_to_textarea();
4081 ed.set_yank(saved_yank);
4082 ed.vim.yank_linewise = saved_yank_linewise;
4083 ed.vim.mode = Mode::Normal;
4084}
4085
4086fn indent_rows<H: crate::types::Host>(
4091 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4092 top: usize,
4093 bot: usize,
4094 count: usize,
4095) {
4096 ed.sync_buffer_content_from_textarea();
4097 let width = ed.settings().shiftwidth * count.max(1);
4098 let pad: String = " ".repeat(width);
4099 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4100 let bot = bot.min(lines.len().saturating_sub(1));
4101 for line in lines.iter_mut().take(bot + 1).skip(top) {
4102 if !line.is_empty() {
4103 line.insert_str(0, &pad);
4104 }
4105 }
4106 ed.restore(lines, (top, 0));
4109 move_first_non_whitespace(ed);
4110}
4111
4112fn outdent_rows<H: crate::types::Host>(
4116 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4117 top: usize,
4118 bot: usize,
4119 count: usize,
4120) {
4121 ed.sync_buffer_content_from_textarea();
4122 let width = ed.settings().shiftwidth * count.max(1);
4123 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4124 let bot = bot.min(lines.len().saturating_sub(1));
4125 for line in lines.iter_mut().take(bot + 1).skip(top) {
4126 let strip: usize = line
4127 .chars()
4128 .take(width)
4129 .take_while(|c| *c == ' ' || *c == '\t')
4130 .count();
4131 if strip > 0 {
4132 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4133 line.drain(..byte_len);
4134 }
4135 }
4136 ed.restore(lines, (top, 0));
4137 move_first_non_whitespace(ed);
4138}
4139
4140fn toggle_case_str(s: &str) -> String {
4141 s.chars()
4142 .map(|c| {
4143 if c.is_lowercase() {
4144 c.to_uppercase().next().unwrap_or(c)
4145 } else if c.is_uppercase() {
4146 c.to_lowercase().next().unwrap_or(c)
4147 } else {
4148 c
4149 }
4150 })
4151 .collect()
4152}
4153
4154fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4155 if a <= b { (a, b) } else { (b, a) }
4156}
4157
4158fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4163 let (row, col) = ed.cursor();
4164 let line_chars = buf_line_chars(&ed.buffer, row);
4165 let max_col = line_chars.saturating_sub(1);
4166 if col > max_col {
4167 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4168 ed.push_buffer_cursor_to_textarea();
4169 }
4170}
4171
4172fn execute_line_op<H: crate::types::Host>(
4175 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4176 op: Operator,
4177 count: usize,
4178) {
4179 let (row, col) = ed.cursor();
4180 let total = buf_row_count(&ed.buffer);
4181 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4182
4183 match op {
4184 Operator::Yank => {
4185 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4187 if !text.is_empty() {
4188 ed.record_yank_to_host(text.clone());
4189 ed.record_yank(text, true);
4190 }
4191 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4194 ed.set_mark('[', (row, 0));
4195 ed.set_mark(']', (end_row, last_col));
4196 buf_set_cursor_rc(&mut ed.buffer, row, col);
4197 ed.push_buffer_cursor_to_textarea();
4198 ed.vim.mode = Mode::Normal;
4199 }
4200 Operator::Delete => {
4201 ed.push_undo();
4202 let deleted_through_last = end_row + 1 >= total;
4203 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4204 let total_after = buf_row_count(&ed.buffer);
4208 let raw_target = if deleted_through_last {
4209 row.saturating_sub(1).min(total_after.saturating_sub(1))
4210 } else {
4211 row.min(total_after.saturating_sub(1))
4212 };
4213 let target_row = if raw_target > 0
4219 && raw_target + 1 == total_after
4220 && buf_line(&ed.buffer, raw_target)
4221 .map(str::is_empty)
4222 .unwrap_or(false)
4223 {
4224 raw_target - 1
4225 } else {
4226 raw_target
4227 };
4228 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4229 ed.push_buffer_cursor_to_textarea();
4230 move_first_non_whitespace(ed);
4231 ed.sticky_col = Some(ed.cursor().1);
4232 ed.vim.mode = Mode::Normal;
4233 let pos = ed.cursor();
4236 ed.set_mark('[', pos);
4237 ed.set_mark(']', pos);
4238 }
4239 Operator::Change => {
4240 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4244 ed.vim.change_mark_start = Some((row, 0));
4246 ed.push_undo();
4247 ed.sync_buffer_content_from_textarea();
4248 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4250 if end_row > row {
4251 ed.mutate_edit(Edit::DeleteRange {
4252 start: Position::new(row + 1, 0),
4253 end: Position::new(end_row, 0),
4254 kind: BufKind::Line,
4255 });
4256 }
4257 let line_chars = buf_line_chars(&ed.buffer, row);
4258 if line_chars > 0 {
4259 ed.mutate_edit(Edit::DeleteRange {
4260 start: Position::new(row, 0),
4261 end: Position::new(row, line_chars),
4262 kind: BufKind::Char,
4263 });
4264 }
4265 if !payload.is_empty() {
4266 ed.record_yank_to_host(payload.clone());
4267 ed.record_delete(payload, true);
4268 }
4269 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4270 ed.push_buffer_cursor_to_textarea();
4271 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4272 }
4273 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4274 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4278 move_first_non_whitespace(ed);
4281 }
4282 Operator::Indent | Operator::Outdent => {
4283 ed.push_undo();
4285 if op == Operator::Indent {
4286 indent_rows(ed, row, end_row, 1);
4287 } else {
4288 outdent_rows(ed, row, end_row, 1);
4289 }
4290 ed.sticky_col = Some(ed.cursor().1);
4291 ed.vim.mode = Mode::Normal;
4292 }
4293 Operator::Fold => unreachable!("Fold has no line-op double"),
4295 Operator::Reflow => {
4296 ed.push_undo();
4298 reflow_rows(ed, row, end_row);
4299 move_first_non_whitespace(ed);
4300 ed.sticky_col = Some(ed.cursor().1);
4301 ed.vim.mode = Mode::Normal;
4302 }
4303 }
4304}
4305
4306fn apply_visual_operator<H: crate::types::Host>(
4309 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4310 op: Operator,
4311) {
4312 match ed.vim.mode {
4313 Mode::VisualLine => {
4314 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4315 let top = cursor_row.min(ed.vim.visual_line_anchor);
4316 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4317 ed.vim.yank_linewise = true;
4318 match op {
4319 Operator::Yank => {
4320 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4321 if !text.is_empty() {
4322 ed.record_yank_to_host(text.clone());
4323 ed.record_yank(text, true);
4324 }
4325 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4326 ed.push_buffer_cursor_to_textarea();
4327 ed.vim.mode = Mode::Normal;
4328 }
4329 Operator::Delete => {
4330 ed.push_undo();
4331 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4332 ed.vim.mode = Mode::Normal;
4333 }
4334 Operator::Change => {
4335 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4338 ed.push_undo();
4339 ed.sync_buffer_content_from_textarea();
4340 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4341 if bot > top {
4342 ed.mutate_edit(Edit::DeleteRange {
4343 start: Position::new(top + 1, 0),
4344 end: Position::new(bot, 0),
4345 kind: BufKind::Line,
4346 });
4347 }
4348 let line_chars = buf_line_chars(&ed.buffer, top);
4349 if line_chars > 0 {
4350 ed.mutate_edit(Edit::DeleteRange {
4351 start: Position::new(top, 0),
4352 end: Position::new(top, line_chars),
4353 kind: BufKind::Char,
4354 });
4355 }
4356 if !payload.is_empty() {
4357 ed.record_yank_to_host(payload.clone());
4358 ed.record_delete(payload, true);
4359 }
4360 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4361 ed.push_buffer_cursor_to_textarea();
4362 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4363 }
4364 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4365 let bot = buf_cursor_pos(&ed.buffer)
4366 .row
4367 .max(ed.vim.visual_line_anchor);
4368 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4369 move_first_non_whitespace(ed);
4370 }
4371 Operator::Indent | Operator::Outdent => {
4372 ed.push_undo();
4373 let (cursor_row, _) = ed.cursor();
4374 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4375 if op == Operator::Indent {
4376 indent_rows(ed, top, bot, 1);
4377 } else {
4378 outdent_rows(ed, top, bot, 1);
4379 }
4380 ed.vim.mode = Mode::Normal;
4381 }
4382 Operator::Reflow => {
4383 ed.push_undo();
4384 let (cursor_row, _) = ed.cursor();
4385 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4386 reflow_rows(ed, top, bot);
4387 ed.vim.mode = Mode::Normal;
4388 }
4389 Operator::Fold => unreachable!("Visual zf takes its own path"),
4392 }
4393 }
4394 Mode::Visual => {
4395 ed.vim.yank_linewise = false;
4396 let anchor = ed.vim.visual_anchor;
4397 let cursor = ed.cursor();
4398 let (top, bot) = order(anchor, cursor);
4399 match op {
4400 Operator::Yank => {
4401 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4402 if !text.is_empty() {
4403 ed.record_yank_to_host(text.clone());
4404 ed.record_yank(text, false);
4405 }
4406 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4407 ed.push_buffer_cursor_to_textarea();
4408 ed.vim.mode = Mode::Normal;
4409 }
4410 Operator::Delete => {
4411 ed.push_undo();
4412 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4413 ed.vim.mode = Mode::Normal;
4414 }
4415 Operator::Change => {
4416 ed.push_undo();
4417 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4418 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4419 }
4420 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4421 let anchor = ed.vim.visual_anchor;
4423 let cursor = ed.cursor();
4424 let (top, bot) = order(anchor, cursor);
4425 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4426 }
4427 Operator::Indent | Operator::Outdent => {
4428 ed.push_undo();
4429 let anchor = ed.vim.visual_anchor;
4430 let cursor = ed.cursor();
4431 let (top, bot) = order(anchor, cursor);
4432 if op == Operator::Indent {
4433 indent_rows(ed, top.0, bot.0, 1);
4434 } else {
4435 outdent_rows(ed, top.0, bot.0, 1);
4436 }
4437 ed.vim.mode = Mode::Normal;
4438 }
4439 Operator::Reflow => {
4440 ed.push_undo();
4441 let anchor = ed.vim.visual_anchor;
4442 let cursor = ed.cursor();
4443 let (top, bot) = order(anchor, cursor);
4444 reflow_rows(ed, top.0, bot.0);
4445 ed.vim.mode = Mode::Normal;
4446 }
4447 Operator::Fold => unreachable!("Visual zf takes its own path"),
4448 }
4449 }
4450 Mode::VisualBlock => apply_block_operator(ed, op),
4451 _ => {}
4452 }
4453}
4454
4455fn block_bounds<H: crate::types::Host>(
4460 ed: &Editor<hjkl_buffer::Buffer, H>,
4461) -> (usize, usize, usize, usize) {
4462 let (ar, ac) = ed.vim.block_anchor;
4463 let (cr, _) = ed.cursor();
4464 let cc = ed.vim.block_vcol;
4465 let top = ar.min(cr);
4466 let bot = ar.max(cr);
4467 let left = ac.min(cc);
4468 let right = ac.max(cc);
4469 (top, bot, left, right)
4470}
4471
4472fn update_block_vcol<H: crate::types::Host>(
4477 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4478 motion: &Motion,
4479) {
4480 match motion {
4481 Motion::Left
4482 | Motion::Right
4483 | Motion::WordFwd
4484 | Motion::BigWordFwd
4485 | Motion::WordBack
4486 | Motion::BigWordBack
4487 | Motion::WordEnd
4488 | Motion::BigWordEnd
4489 | Motion::WordEndBack
4490 | Motion::BigWordEndBack
4491 | Motion::LineStart
4492 | Motion::FirstNonBlank
4493 | Motion::LineEnd
4494 | Motion::Find { .. }
4495 | Motion::FindRepeat { .. }
4496 | Motion::MatchBracket => {
4497 ed.vim.block_vcol = ed.cursor().1;
4498 }
4499 _ => {}
4501 }
4502}
4503
4504fn apply_block_operator<H: crate::types::Host>(
4509 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4510 op: Operator,
4511) {
4512 let (top, bot, left, right) = block_bounds(ed);
4513 let yank = block_yank(ed, top, bot, left, right);
4515
4516 match op {
4517 Operator::Yank => {
4518 if !yank.is_empty() {
4519 ed.record_yank_to_host(yank.clone());
4520 ed.record_yank(yank, false);
4521 }
4522 ed.vim.mode = Mode::Normal;
4523 ed.jump_cursor(top, left);
4524 }
4525 Operator::Delete => {
4526 ed.push_undo();
4527 delete_block_contents(ed, top, bot, left, right);
4528 if !yank.is_empty() {
4529 ed.record_yank_to_host(yank.clone());
4530 ed.record_delete(yank, false);
4531 }
4532 ed.vim.mode = Mode::Normal;
4533 ed.jump_cursor(top, left);
4534 }
4535 Operator::Change => {
4536 ed.push_undo();
4537 delete_block_contents(ed, top, bot, left, right);
4538 if !yank.is_empty() {
4539 ed.record_yank_to_host(yank.clone());
4540 ed.record_delete(yank, false);
4541 }
4542 ed.jump_cursor(top, left);
4543 begin_insert_noundo(
4544 ed,
4545 1,
4546 InsertReason::BlockChange {
4547 top,
4548 bot,
4549 col: left,
4550 },
4551 );
4552 }
4553 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4554 ed.push_undo();
4555 transform_block_case(ed, op, top, bot, left, right);
4556 ed.vim.mode = Mode::Normal;
4557 ed.jump_cursor(top, left);
4558 }
4559 Operator::Indent | Operator::Outdent => {
4560 ed.push_undo();
4564 if op == Operator::Indent {
4565 indent_rows(ed, top, bot, 1);
4566 } else {
4567 outdent_rows(ed, top, bot, 1);
4568 }
4569 ed.vim.mode = Mode::Normal;
4570 }
4571 Operator::Fold => unreachable!("Visual zf takes its own path"),
4572 Operator::Reflow => {
4573 ed.push_undo();
4577 reflow_rows(ed, top, bot);
4578 ed.vim.mode = Mode::Normal;
4579 }
4580 }
4581}
4582
4583fn transform_block_case<H: crate::types::Host>(
4587 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4588 op: Operator,
4589 top: usize,
4590 bot: usize,
4591 left: usize,
4592 right: usize,
4593) {
4594 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4595 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4596 let chars: Vec<char> = lines[r].chars().collect();
4597 if left >= chars.len() {
4598 continue;
4599 }
4600 let end = (right + 1).min(chars.len());
4601 let head: String = chars[..left].iter().collect();
4602 let mid: String = chars[left..end].iter().collect();
4603 let tail: String = chars[end..].iter().collect();
4604 let transformed = match op {
4605 Operator::Uppercase => mid.to_uppercase(),
4606 Operator::Lowercase => mid.to_lowercase(),
4607 Operator::ToggleCase => toggle_case_str(&mid),
4608 _ => mid,
4609 };
4610 lines[r] = format!("{head}{transformed}{tail}");
4611 }
4612 let saved_yank = ed.yank().to_string();
4613 let saved_linewise = ed.vim.yank_linewise;
4614 ed.restore(lines, (top, left));
4615 ed.set_yank(saved_yank);
4616 ed.vim.yank_linewise = saved_linewise;
4617}
4618
4619fn block_yank<H: crate::types::Host>(
4620 ed: &Editor<hjkl_buffer::Buffer, H>,
4621 top: usize,
4622 bot: usize,
4623 left: usize,
4624 right: usize,
4625) -> String {
4626 let lines = buf_lines_to_vec(&ed.buffer);
4627 let mut rows: Vec<String> = Vec::new();
4628 for r in top..=bot {
4629 let line = match lines.get(r) {
4630 Some(l) => l,
4631 None => break,
4632 };
4633 let chars: Vec<char> = line.chars().collect();
4634 let end = (right + 1).min(chars.len());
4635 if left >= chars.len() {
4636 rows.push(String::new());
4637 } else {
4638 rows.push(chars[left..end].iter().collect());
4639 }
4640 }
4641 rows.join("\n")
4642}
4643
4644fn delete_block_contents<H: crate::types::Host>(
4645 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4646 top: usize,
4647 bot: usize,
4648 left: usize,
4649 right: usize,
4650) {
4651 use hjkl_buffer::{Edit, MotionKind, Position};
4652 ed.sync_buffer_content_from_textarea();
4653 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4654 if last_row < top {
4655 return;
4656 }
4657 ed.mutate_edit(Edit::DeleteRange {
4658 start: Position::new(top, left),
4659 end: Position::new(last_row, right),
4660 kind: MotionKind::Block,
4661 });
4662 ed.push_buffer_cursor_to_textarea();
4663}
4664
4665fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4667 let (top, bot, left, right) = block_bounds(ed);
4668 ed.push_undo();
4669 ed.sync_buffer_content_from_textarea();
4670 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4671 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4672 let chars: Vec<char> = lines[r].chars().collect();
4673 if left >= chars.len() {
4674 continue;
4675 }
4676 let end = (right + 1).min(chars.len());
4677 let before: String = chars[..left].iter().collect();
4678 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4679 let after: String = chars[end..].iter().collect();
4680 lines[r] = format!("{before}{middle}{after}");
4681 }
4682 reset_textarea_lines(ed, lines);
4683 ed.vim.mode = Mode::Normal;
4684 ed.jump_cursor(top, left);
4685}
4686
4687fn reset_textarea_lines<H: crate::types::Host>(
4691 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4692 lines: Vec<String>,
4693) {
4694 let cursor = ed.cursor();
4695 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4696 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4697 ed.mark_content_dirty();
4698}
4699
4700type Pos = (usize, usize);
4706
4707fn text_object_range<H: crate::types::Host>(
4711 ed: &Editor<hjkl_buffer::Buffer, H>,
4712 obj: TextObject,
4713 inner: bool,
4714) -> Option<(Pos, Pos, MotionKind)> {
4715 match obj {
4716 TextObject::Word { big } => {
4717 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4718 }
4719 TextObject::Quote(q) => {
4720 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4721 }
4722 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4723 TextObject::Paragraph => {
4724 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4725 }
4726 TextObject::XmlTag => {
4727 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4728 }
4729 TextObject::Sentence => {
4730 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4731 }
4732 }
4733}
4734
4735fn sentence_boundary<H: crate::types::Host>(
4739 ed: &Editor<hjkl_buffer::Buffer, H>,
4740 forward: bool,
4741) -> Option<(usize, usize)> {
4742 let lines = buf_lines_to_vec(&ed.buffer);
4743 if lines.is_empty() {
4744 return None;
4745 }
4746 let pos_to_idx = |pos: (usize, usize)| -> usize {
4747 let mut idx = 0;
4748 for line in lines.iter().take(pos.0) {
4749 idx += line.chars().count() + 1;
4750 }
4751 idx + pos.1
4752 };
4753 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4754 for (r, line) in lines.iter().enumerate() {
4755 let len = line.chars().count();
4756 if idx <= len {
4757 return (r, idx);
4758 }
4759 idx -= len + 1;
4760 }
4761 let last = lines.len().saturating_sub(1);
4762 (last, lines[last].chars().count())
4763 };
4764 let mut chars: Vec<char> = Vec::new();
4765 for (r, line) in lines.iter().enumerate() {
4766 chars.extend(line.chars());
4767 if r + 1 < lines.len() {
4768 chars.push('\n');
4769 }
4770 }
4771 if chars.is_empty() {
4772 return None;
4773 }
4774 let total = chars.len();
4775 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4776 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4777
4778 if forward {
4779 let mut i = cursor_idx + 1;
4782 while i < total {
4783 if is_terminator(chars[i]) {
4784 while i + 1 < total && is_terminator(chars[i + 1]) {
4785 i += 1;
4786 }
4787 if i + 1 >= total {
4788 return None;
4789 }
4790 if chars[i + 1].is_whitespace() {
4791 let mut j = i + 1;
4792 while j < total && chars[j].is_whitespace() {
4793 j += 1;
4794 }
4795 if j >= total {
4796 return None;
4797 }
4798 return Some(idx_to_pos(j));
4799 }
4800 }
4801 i += 1;
4802 }
4803 None
4804 } else {
4805 let find_start = |from: usize| -> Option<usize> {
4809 let mut start = from;
4810 while start > 0 {
4811 let prev = chars[start - 1];
4812 if prev.is_whitespace() {
4813 let mut k = start - 1;
4814 while k > 0 && chars[k - 1].is_whitespace() {
4815 k -= 1;
4816 }
4817 if k > 0 && is_terminator(chars[k - 1]) {
4818 break;
4819 }
4820 }
4821 start -= 1;
4822 }
4823 while start < total && chars[start].is_whitespace() {
4824 start += 1;
4825 }
4826 (start < total).then_some(start)
4827 };
4828 let current_start = find_start(cursor_idx)?;
4829 if current_start < cursor_idx {
4830 return Some(idx_to_pos(current_start));
4831 }
4832 let mut k = current_start;
4835 while k > 0 && chars[k - 1].is_whitespace() {
4836 k -= 1;
4837 }
4838 if k == 0 {
4839 return None;
4840 }
4841 let prev_start = find_start(k - 1)?;
4842 Some(idx_to_pos(prev_start))
4843 }
4844}
4845
4846fn sentence_text_object<H: crate::types::Host>(
4852 ed: &Editor<hjkl_buffer::Buffer, H>,
4853 inner: bool,
4854) -> Option<((usize, usize), (usize, usize))> {
4855 let lines = buf_lines_to_vec(&ed.buffer);
4856 if lines.is_empty() {
4857 return None;
4858 }
4859 let pos_to_idx = |pos: (usize, usize)| -> usize {
4862 let mut idx = 0;
4863 for line in lines.iter().take(pos.0) {
4864 idx += line.chars().count() + 1;
4865 }
4866 idx + pos.1
4867 };
4868 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4869 for (r, line) in lines.iter().enumerate() {
4870 let len = line.chars().count();
4871 if idx <= len {
4872 return (r, idx);
4873 }
4874 idx -= len + 1;
4875 }
4876 let last = lines.len().saturating_sub(1);
4877 (last, lines[last].chars().count())
4878 };
4879 let mut chars: Vec<char> = Vec::new();
4880 for (r, line) in lines.iter().enumerate() {
4881 chars.extend(line.chars());
4882 if r + 1 < lines.len() {
4883 chars.push('\n');
4884 }
4885 }
4886 if chars.is_empty() {
4887 return None;
4888 }
4889
4890 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4891 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4892
4893 let mut start = cursor_idx;
4897 while start > 0 {
4898 let prev = chars[start - 1];
4899 if prev.is_whitespace() {
4900 let mut k = start - 1;
4904 while k > 0 && chars[k - 1].is_whitespace() {
4905 k -= 1;
4906 }
4907 if k > 0 && is_terminator(chars[k - 1]) {
4908 break;
4909 }
4910 }
4911 start -= 1;
4912 }
4913 while start < chars.len() && chars[start].is_whitespace() {
4916 start += 1;
4917 }
4918 if start >= chars.len() {
4919 return None;
4920 }
4921
4922 let mut end = start;
4925 while end < chars.len() {
4926 if is_terminator(chars[end]) {
4927 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4929 end += 1;
4930 }
4931 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4934 break;
4935 }
4936 }
4937 end += 1;
4938 }
4939 let end_idx = (end + 1).min(chars.len());
4941
4942 let final_end = if inner {
4943 end_idx
4944 } else {
4945 let mut e = end_idx;
4949 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4950 e += 1;
4951 }
4952 e
4953 };
4954
4955 Some((idx_to_pos(start), idx_to_pos(final_end)))
4956}
4957
4958fn tag_text_object<H: crate::types::Host>(
4962 ed: &Editor<hjkl_buffer::Buffer, H>,
4963 inner: bool,
4964) -> Option<((usize, usize), (usize, usize))> {
4965 let lines = buf_lines_to_vec(&ed.buffer);
4966 if lines.is_empty() {
4967 return None;
4968 }
4969 let pos_to_idx = |pos: (usize, usize)| -> usize {
4973 let mut idx = 0;
4974 for line in lines.iter().take(pos.0) {
4975 idx += line.chars().count() + 1;
4976 }
4977 idx + pos.1
4978 };
4979 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4980 for (r, line) in lines.iter().enumerate() {
4981 let len = line.chars().count();
4982 if idx <= len {
4983 return (r, idx);
4984 }
4985 idx -= len + 1;
4986 }
4987 let last = lines.len().saturating_sub(1);
4988 (last, lines[last].chars().count())
4989 };
4990 let mut chars: Vec<char> = Vec::new();
4991 for (r, line) in lines.iter().enumerate() {
4992 chars.extend(line.chars());
4993 if r + 1 < lines.len() {
4994 chars.push('\n');
4995 }
4996 }
4997 let cursor_idx = pos_to_idx(ed.cursor());
4998
4999 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5007 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5008 let mut i = 0;
5009 while i < chars.len() {
5010 if chars[i] != '<' {
5011 i += 1;
5012 continue;
5013 }
5014 let mut j = i + 1;
5015 while j < chars.len() && chars[j] != '>' {
5016 j += 1;
5017 }
5018 if j >= chars.len() {
5019 break;
5020 }
5021 let inside: String = chars[i + 1..j].iter().collect();
5022 let close_end = j + 1;
5023 let trimmed = inside.trim();
5024 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5025 i = close_end;
5026 continue;
5027 }
5028 if let Some(rest) = trimmed.strip_prefix('/') {
5029 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5030 if !name.is_empty()
5031 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5032 {
5033 let (open_start, content_start, _) = stack[stack_idx].clone();
5034 stack.truncate(stack_idx);
5035 let content_end = i;
5036 let candidate = (open_start, content_start, content_end, close_end);
5037 if cursor_idx >= content_start && cursor_idx <= content_end {
5038 innermost = match innermost {
5039 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5040 Some(candidate)
5041 }
5042 None => Some(candidate),
5043 existing => existing,
5044 };
5045 } else if open_start >= cursor_idx && next_after.is_none() {
5046 next_after = Some(candidate);
5047 }
5048 }
5049 } else if !trimmed.ends_with('/') {
5050 let name: String = trimmed
5051 .split(|c: char| c.is_whitespace() || c == '/')
5052 .next()
5053 .unwrap_or("")
5054 .to_string();
5055 if !name.is_empty() {
5056 stack.push((i, close_end, name));
5057 }
5058 }
5059 i = close_end;
5060 }
5061
5062 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5063 if inner {
5064 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5065 } else {
5066 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5067 }
5068}
5069
5070fn is_wordchar(c: char) -> bool {
5071 c.is_alphanumeric() || c == '_'
5072}
5073
5074pub(crate) use hjkl_buffer::is_keyword_char;
5078
5079fn word_text_object<H: crate::types::Host>(
5080 ed: &Editor<hjkl_buffer::Buffer, H>,
5081 inner: bool,
5082 big: bool,
5083) -> Option<((usize, usize), (usize, usize))> {
5084 let (row, col) = ed.cursor();
5085 let line = buf_line(&ed.buffer, row)?;
5086 let chars: Vec<char> = line.chars().collect();
5087 if chars.is_empty() {
5088 return None;
5089 }
5090 let at = col.min(chars.len().saturating_sub(1));
5091 let classify = |c: char| -> u8 {
5092 if c.is_whitespace() {
5093 0
5094 } else if big || is_wordchar(c) {
5095 1
5096 } else {
5097 2
5098 }
5099 };
5100 let cls = classify(chars[at]);
5101 let mut start = at;
5102 while start > 0 && classify(chars[start - 1]) == cls {
5103 start -= 1;
5104 }
5105 let mut end = at;
5106 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5107 end += 1;
5108 }
5109 let char_byte = |i: usize| {
5111 if i >= chars.len() {
5112 line.len()
5113 } else {
5114 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5115 }
5116 };
5117 let mut start_col = char_byte(start);
5118 let mut end_col = char_byte(end + 1);
5120 if !inner {
5121 let mut t = end + 1;
5123 let mut included_trailing = false;
5124 while t < chars.len() && chars[t].is_whitespace() {
5125 included_trailing = true;
5126 t += 1;
5127 }
5128 if included_trailing {
5129 end_col = char_byte(t);
5130 } else {
5131 let mut s = start;
5132 while s > 0 && chars[s - 1].is_whitespace() {
5133 s -= 1;
5134 }
5135 start_col = char_byte(s);
5136 }
5137 }
5138 Some(((row, start_col), (row, end_col)))
5139}
5140
5141fn quote_text_object<H: crate::types::Host>(
5142 ed: &Editor<hjkl_buffer::Buffer, H>,
5143 q: char,
5144 inner: bool,
5145) -> Option<((usize, usize), (usize, usize))> {
5146 let (row, col) = ed.cursor();
5147 let line = buf_line(&ed.buffer, row)?;
5148 let bytes = line.as_bytes();
5149 let q_byte = q as u8;
5150 let mut positions: Vec<usize> = Vec::new();
5152 for (i, &b) in bytes.iter().enumerate() {
5153 if b == q_byte {
5154 positions.push(i);
5155 }
5156 }
5157 if positions.len() < 2 {
5158 return None;
5159 }
5160 let mut open_idx: Option<usize> = None;
5161 let mut close_idx: Option<usize> = None;
5162 for pair in positions.chunks(2) {
5163 if pair.len() < 2 {
5164 break;
5165 }
5166 if col >= pair[0] && col <= pair[1] {
5167 open_idx = Some(pair[0]);
5168 close_idx = Some(pair[1]);
5169 break;
5170 }
5171 if col < pair[0] {
5172 open_idx = Some(pair[0]);
5173 close_idx = Some(pair[1]);
5174 break;
5175 }
5176 }
5177 let open = open_idx?;
5178 let close = close_idx?;
5179 if inner {
5181 if close <= open + 1 {
5182 return None;
5183 }
5184 Some(((row, open + 1), (row, close)))
5185 } else {
5186 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5193 let mut end = after_close;
5195 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5196 end += 1;
5197 }
5198 Some(((row, open), (row, end)))
5199 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5200 let mut start = open;
5202 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5203 start -= 1;
5204 }
5205 Some(((row, start), (row, close + 1)))
5206 } else {
5207 Some(((row, open), (row, close + 1)))
5208 }
5209 }
5210}
5211
5212fn bracket_text_object<H: crate::types::Host>(
5213 ed: &Editor<hjkl_buffer::Buffer, H>,
5214 open: char,
5215 inner: bool,
5216) -> Option<(Pos, Pos, MotionKind)> {
5217 let close = match open {
5218 '(' => ')',
5219 '[' => ']',
5220 '{' => '}',
5221 '<' => '>',
5222 _ => return None,
5223 };
5224 let (row, col) = ed.cursor();
5225 let lines = buf_lines_to_vec(&ed.buffer);
5226 let lines = lines.as_slice();
5227 let open_pos = find_open_bracket(lines, row, col, open, close)
5232 .or_else(|| find_next_open(lines, row, col, open))?;
5233 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5234 if inner {
5236 if close_pos.0 > open_pos.0 + 1 {
5242 let inner_row_start = open_pos.0 + 1;
5244 let inner_row_end = close_pos.0 - 1;
5245 let end_col = lines
5246 .get(inner_row_end)
5247 .map(|l| l.chars().count())
5248 .unwrap_or(0);
5249 return Some((
5250 (inner_row_start, 0),
5251 (inner_row_end, end_col),
5252 MotionKind::Linewise,
5253 ));
5254 }
5255 let inner_start = advance_pos(lines, open_pos);
5256 if inner_start.0 > close_pos.0
5257 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5258 {
5259 return None;
5260 }
5261 Some((inner_start, close_pos, MotionKind::Exclusive))
5262 } else {
5263 Some((
5264 open_pos,
5265 advance_pos(lines, close_pos),
5266 MotionKind::Exclusive,
5267 ))
5268 }
5269}
5270
5271fn find_open_bracket(
5272 lines: &[String],
5273 row: usize,
5274 col: usize,
5275 open: char,
5276 close: char,
5277) -> Option<(usize, usize)> {
5278 let mut depth: i32 = 0;
5279 let mut r = row;
5280 let mut c = col as isize;
5281 loop {
5282 let cur = &lines[r];
5283 let chars: Vec<char> = cur.chars().collect();
5284 if (c as usize) >= chars.len() {
5288 c = chars.len() as isize - 1;
5289 }
5290 while c >= 0 {
5291 let ch = chars[c as usize];
5292 if ch == close {
5293 depth += 1;
5294 } else if ch == open {
5295 if depth == 0 {
5296 return Some((r, c as usize));
5297 }
5298 depth -= 1;
5299 }
5300 c -= 1;
5301 }
5302 if r == 0 {
5303 return None;
5304 }
5305 r -= 1;
5306 c = lines[r].chars().count() as isize - 1;
5307 }
5308}
5309
5310fn find_close_bracket(
5311 lines: &[String],
5312 row: usize,
5313 start_col: usize,
5314 open: char,
5315 close: char,
5316) -> Option<(usize, usize)> {
5317 let mut depth: i32 = 0;
5318 let mut r = row;
5319 let mut c = start_col;
5320 loop {
5321 let cur = &lines[r];
5322 let chars: Vec<char> = cur.chars().collect();
5323 while c < chars.len() {
5324 let ch = chars[c];
5325 if ch == open {
5326 depth += 1;
5327 } else if ch == close {
5328 if depth == 0 {
5329 return Some((r, c));
5330 }
5331 depth -= 1;
5332 }
5333 c += 1;
5334 }
5335 if r + 1 >= lines.len() {
5336 return None;
5337 }
5338 r += 1;
5339 c = 0;
5340 }
5341}
5342
5343fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5347 let mut r = row;
5348 let mut c = col;
5349 while r < lines.len() {
5350 let chars: Vec<char> = lines[r].chars().collect();
5351 while c < chars.len() {
5352 if chars[c] == open {
5353 return Some((r, c));
5354 }
5355 c += 1;
5356 }
5357 r += 1;
5358 c = 0;
5359 }
5360 None
5361}
5362
5363fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5364 let (r, c) = pos;
5365 let line_len = lines[r].chars().count();
5366 if c < line_len {
5367 (r, c + 1)
5368 } else if r + 1 < lines.len() {
5369 (r + 1, 0)
5370 } else {
5371 pos
5372 }
5373}
5374
5375fn paragraph_text_object<H: crate::types::Host>(
5376 ed: &Editor<hjkl_buffer::Buffer, H>,
5377 inner: bool,
5378) -> Option<((usize, usize), (usize, usize))> {
5379 let (row, _) = ed.cursor();
5380 let lines = buf_lines_to_vec(&ed.buffer);
5381 if lines.is_empty() {
5382 return None;
5383 }
5384 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5386 if is_blank(row) {
5387 return None;
5388 }
5389 let mut top = row;
5390 while top > 0 && !is_blank(top - 1) {
5391 top -= 1;
5392 }
5393 let mut bot = row;
5394 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5395 bot += 1;
5396 }
5397 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5399 bot += 1;
5400 }
5401 let end_col = lines[bot].chars().count();
5402 Some(((top, 0), (bot, end_col)))
5403}
5404
5405fn read_vim_range<H: crate::types::Host>(
5411 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5412 start: (usize, usize),
5413 end: (usize, usize),
5414 kind: MotionKind,
5415) -> String {
5416 let (top, bot) = order(start, end);
5417 ed.sync_buffer_content_from_textarea();
5418 let lines = buf_lines_to_vec(&ed.buffer);
5419 match kind {
5420 MotionKind::Linewise => {
5421 let lo = top.0;
5422 let hi = bot.0.min(lines.len().saturating_sub(1));
5423 let mut text = lines[lo..=hi].join("\n");
5424 text.push('\n');
5425 text
5426 }
5427 MotionKind::Inclusive | MotionKind::Exclusive => {
5428 let inclusive = matches!(kind, MotionKind::Inclusive);
5429 let mut out = String::new();
5431 for row in top.0..=bot.0 {
5432 let line = lines.get(row).map(String::as_str).unwrap_or("");
5433 let lo = if row == top.0 { top.1 } else { 0 };
5434 let hi_unclamped = if row == bot.0 {
5435 if inclusive { bot.1 + 1 } else { bot.1 }
5436 } else {
5437 line.chars().count() + 1
5438 };
5439 let row_chars: Vec<char> = line.chars().collect();
5440 let hi = hi_unclamped.min(row_chars.len());
5441 if lo < hi {
5442 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5443 }
5444 if row < bot.0 {
5445 out.push('\n');
5446 }
5447 }
5448 out
5449 }
5450 }
5451}
5452
5453fn cut_vim_range<H: crate::types::Host>(
5462 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5463 start: (usize, usize),
5464 end: (usize, usize),
5465 kind: MotionKind,
5466) -> String {
5467 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5468 let (top, bot) = order(start, end);
5469 ed.sync_buffer_content_from_textarea();
5470 let (buf_start, buf_end, buf_kind) = match kind {
5471 MotionKind::Linewise => (
5472 Position::new(top.0, 0),
5473 Position::new(bot.0, 0),
5474 BufKind::Line,
5475 ),
5476 MotionKind::Inclusive => {
5477 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5478 let next = if bot.1 < line_chars {
5482 Position::new(bot.0, bot.1 + 1)
5483 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5484 Position::new(bot.0 + 1, 0)
5485 } else {
5486 Position::new(bot.0, line_chars)
5487 };
5488 (Position::new(top.0, top.1), next, BufKind::Char)
5489 }
5490 MotionKind::Exclusive => (
5491 Position::new(top.0, top.1),
5492 Position::new(bot.0, bot.1),
5493 BufKind::Char,
5494 ),
5495 };
5496 let inverse = ed.mutate_edit(Edit::DeleteRange {
5497 start: buf_start,
5498 end: buf_end,
5499 kind: buf_kind,
5500 });
5501 let text = match inverse {
5502 Edit::InsertStr { text, .. } => text,
5503 _ => String::new(),
5504 };
5505 if !text.is_empty() {
5506 ed.record_yank_to_host(text.clone());
5507 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5508 }
5509 ed.push_buffer_cursor_to_textarea();
5510 text
5511}
5512
5513fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5519 use hjkl_buffer::{Edit, MotionKind, Position};
5520 ed.sync_buffer_content_from_textarea();
5521 let cursor = buf_cursor_pos(&ed.buffer);
5522 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5523 if cursor.col >= line_chars {
5524 return;
5525 }
5526 let inverse = ed.mutate_edit(Edit::DeleteRange {
5527 start: cursor,
5528 end: Position::new(cursor.row, line_chars),
5529 kind: MotionKind::Char,
5530 });
5531 if let Edit::InsertStr { text, .. } = inverse
5532 && !text.is_empty()
5533 {
5534 ed.record_yank_to_host(text.clone());
5535 ed.vim.yank_linewise = false;
5536 ed.set_yank(text);
5537 }
5538 buf_set_cursor_pos(&mut ed.buffer, cursor);
5539 ed.push_buffer_cursor_to_textarea();
5540}
5541
5542fn do_char_delete<H: crate::types::Host>(
5543 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5544 forward: bool,
5545 count: usize,
5546) {
5547 use hjkl_buffer::{Edit, MotionKind, Position};
5548 ed.push_undo();
5549 ed.sync_buffer_content_from_textarea();
5550 let mut deleted = String::new();
5553 for _ in 0..count {
5554 let cursor = buf_cursor_pos(&ed.buffer);
5555 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5556 if forward {
5557 if cursor.col >= line_chars {
5560 continue;
5561 }
5562 let inverse = ed.mutate_edit(Edit::DeleteRange {
5563 start: cursor,
5564 end: Position::new(cursor.row, cursor.col + 1),
5565 kind: MotionKind::Char,
5566 });
5567 if let Edit::InsertStr { text, .. } = inverse {
5568 deleted.push_str(&text);
5569 }
5570 } else {
5571 if cursor.col == 0 {
5573 continue;
5574 }
5575 let inverse = ed.mutate_edit(Edit::DeleteRange {
5576 start: Position::new(cursor.row, cursor.col - 1),
5577 end: cursor,
5578 kind: MotionKind::Char,
5579 });
5580 if let Edit::InsertStr { text, .. } = inverse {
5581 deleted = text + &deleted;
5584 }
5585 }
5586 }
5587 if !deleted.is_empty() {
5588 ed.record_yank_to_host(deleted.clone());
5589 ed.record_delete(deleted, false);
5590 }
5591 ed.push_buffer_cursor_to_textarea();
5592}
5593
5594fn adjust_number<H: crate::types::Host>(
5598 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5599 delta: i64,
5600) -> bool {
5601 use hjkl_buffer::{Edit, MotionKind, Position};
5602 ed.sync_buffer_content_from_textarea();
5603 let cursor = buf_cursor_pos(&ed.buffer);
5604 let row = cursor.row;
5605 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5606 Some(l) => l.chars().collect(),
5607 None => return false,
5608 };
5609 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5610 return false;
5611 };
5612 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5613 digit_start - 1
5614 } else {
5615 digit_start
5616 };
5617 let mut span_end = digit_start;
5618 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5619 span_end += 1;
5620 }
5621 let s: String = chars[span_start..span_end].iter().collect();
5622 let Ok(n) = s.parse::<i64>() else {
5623 return false;
5624 };
5625 let new_s = n.saturating_add(delta).to_string();
5626
5627 ed.push_undo();
5628 let span_start_pos = Position::new(row, span_start);
5629 let span_end_pos = Position::new(row, span_end);
5630 ed.mutate_edit(Edit::DeleteRange {
5631 start: span_start_pos,
5632 end: span_end_pos,
5633 kind: MotionKind::Char,
5634 });
5635 ed.mutate_edit(Edit::InsertStr {
5636 at: span_start_pos,
5637 text: new_s.clone(),
5638 });
5639 let new_len = new_s.chars().count();
5640 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5641 ed.push_buffer_cursor_to_textarea();
5642 true
5643}
5644
5645pub(crate) fn replace_char<H: crate::types::Host>(
5646 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5647 ch: char,
5648 count: usize,
5649) {
5650 use hjkl_buffer::{Edit, MotionKind, Position};
5651 ed.push_undo();
5652 ed.sync_buffer_content_from_textarea();
5653 for _ in 0..count {
5654 let cursor = buf_cursor_pos(&ed.buffer);
5655 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5656 if cursor.col >= line_chars {
5657 break;
5658 }
5659 ed.mutate_edit(Edit::DeleteRange {
5660 start: cursor,
5661 end: Position::new(cursor.row, cursor.col + 1),
5662 kind: MotionKind::Char,
5663 });
5664 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5665 }
5666 crate::motions::move_left(&mut ed.buffer, 1);
5668 ed.push_buffer_cursor_to_textarea();
5669}
5670
5671fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5672 use hjkl_buffer::{Edit, MotionKind, Position};
5673 ed.sync_buffer_content_from_textarea();
5674 let cursor = buf_cursor_pos(&ed.buffer);
5675 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5676 return;
5677 };
5678 let toggled = if c.is_uppercase() {
5679 c.to_lowercase().next().unwrap_or(c)
5680 } else {
5681 c.to_uppercase().next().unwrap_or(c)
5682 };
5683 ed.mutate_edit(Edit::DeleteRange {
5684 start: cursor,
5685 end: Position::new(cursor.row, cursor.col + 1),
5686 kind: MotionKind::Char,
5687 });
5688 ed.mutate_edit(Edit::InsertChar {
5689 at: cursor,
5690 ch: toggled,
5691 });
5692}
5693
5694fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5695 use hjkl_buffer::{Edit, Position};
5696 ed.sync_buffer_content_from_textarea();
5697 let row = buf_cursor_pos(&ed.buffer).row;
5698 if row + 1 >= buf_row_count(&ed.buffer) {
5699 return;
5700 }
5701 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5702 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5703 let next_trimmed = next_raw.trim_start();
5704 let cur_chars = cur_line.chars().count();
5705 let next_chars = next_raw.chars().count();
5706 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5709 " "
5710 } else {
5711 ""
5712 };
5713 let joined = format!("{cur_line}{separator}{next_trimmed}");
5714 ed.mutate_edit(Edit::Replace {
5715 start: Position::new(row, 0),
5716 end: Position::new(row + 1, next_chars),
5717 with: joined,
5718 });
5719 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5723 ed.push_buffer_cursor_to_textarea();
5724}
5725
5726fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5729 use hjkl_buffer::Edit;
5730 ed.sync_buffer_content_from_textarea();
5731 let row = buf_cursor_pos(&ed.buffer).row;
5732 if row + 1 >= buf_row_count(&ed.buffer) {
5733 return;
5734 }
5735 let join_col = buf_line_chars(&ed.buffer, row);
5736 ed.mutate_edit(Edit::JoinLines {
5737 row,
5738 count: 1,
5739 with_space: false,
5740 });
5741 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5743 ed.push_buffer_cursor_to_textarea();
5744}
5745
5746fn do_paste<H: crate::types::Host>(
5747 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5748 before: bool,
5749 count: usize,
5750) {
5751 use hjkl_buffer::{Edit, Position};
5752 ed.push_undo();
5753 let selector = ed.vim.pending_register.take();
5758 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5759 Some(slot) => (slot.text.clone(), slot.linewise),
5760 None => {
5766 let s = &ed.registers().unnamed;
5767 (s.text.clone(), s.linewise)
5768 }
5769 };
5770 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5774 for _ in 0..count {
5775 ed.sync_buffer_content_from_textarea();
5776 let yank = yank.clone();
5777 if yank.is_empty() {
5778 continue;
5779 }
5780 if linewise {
5781 let text = yank.trim_matches('\n').to_string();
5785 let row = buf_cursor_pos(&ed.buffer).row;
5786 let target_row = if before {
5787 ed.mutate_edit(Edit::InsertStr {
5788 at: Position::new(row, 0),
5789 text: format!("{text}\n"),
5790 });
5791 row
5792 } else {
5793 let line_chars = buf_line_chars(&ed.buffer, row);
5794 ed.mutate_edit(Edit::InsertStr {
5795 at: Position::new(row, line_chars),
5796 text: format!("\n{text}"),
5797 });
5798 row + 1
5799 };
5800 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5801 crate::motions::move_first_non_blank(&mut ed.buffer);
5802 ed.push_buffer_cursor_to_textarea();
5803 let payload_lines = text.lines().count().max(1);
5805 let bot_row = target_row + payload_lines - 1;
5806 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5807 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5808 } else {
5809 let cursor = buf_cursor_pos(&ed.buffer);
5813 let at = if before {
5814 cursor
5815 } else {
5816 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5817 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5818 };
5819 ed.mutate_edit(Edit::InsertStr {
5820 at,
5821 text: yank.clone(),
5822 });
5823 crate::motions::move_left(&mut ed.buffer, 1);
5826 ed.push_buffer_cursor_to_textarea();
5827 let lo = (at.row, at.col);
5829 let hi = ed.cursor();
5830 paste_mark = Some((lo, hi));
5831 }
5832 }
5833 if let Some((lo, hi)) = paste_mark {
5834 ed.set_mark('[', lo);
5835 ed.set_mark(']', hi);
5836 }
5837 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5839}
5840
5841pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5842 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5843 let current = ed.snapshot();
5844 ed.redo_stack.push(current);
5845 ed.restore(lines, cursor);
5846 }
5847 ed.vim.mode = Mode::Normal;
5848 clamp_cursor_to_normal_mode(ed);
5852}
5853
5854pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5855 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5856 let current = ed.snapshot();
5857 ed.undo_stack.push(current);
5858 ed.cap_undo();
5859 ed.restore(lines, cursor);
5860 }
5861 ed.vim.mode = Mode::Normal;
5862}
5863
5864fn replay_insert_and_finish<H: crate::types::Host>(
5871 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5872 text: &str,
5873) {
5874 use hjkl_buffer::{Edit, Position};
5875 let cursor = ed.cursor();
5876 ed.mutate_edit(Edit::InsertStr {
5877 at: Position::new(cursor.0, cursor.1),
5878 text: text.to_string(),
5879 });
5880 if ed.vim.insert_session.take().is_some() {
5881 if ed.cursor().1 > 0 {
5882 crate::motions::move_left(&mut ed.buffer, 1);
5883 ed.push_buffer_cursor_to_textarea();
5884 }
5885 ed.vim.mode = Mode::Normal;
5886 }
5887}
5888
5889fn replay_last_change<H: crate::types::Host>(
5890 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5891 outer_count: usize,
5892) {
5893 let Some(change) = ed.vim.last_change.clone() else {
5894 return;
5895 };
5896 ed.vim.replaying = true;
5897 let scale = if outer_count > 0 { outer_count } else { 1 };
5898 match change {
5899 LastChange::OpMotion {
5900 op,
5901 motion,
5902 count,
5903 inserted,
5904 } => {
5905 let total = count.max(1) * scale;
5906 apply_op_with_motion(ed, op, &motion, total);
5907 if let Some(text) = inserted {
5908 replay_insert_and_finish(ed, &text);
5909 }
5910 }
5911 LastChange::OpTextObj {
5912 op,
5913 obj,
5914 inner,
5915 inserted,
5916 } => {
5917 apply_op_with_text_object(ed, op, obj, inner);
5918 if let Some(text) = inserted {
5919 replay_insert_and_finish(ed, &text);
5920 }
5921 }
5922 LastChange::LineOp {
5923 op,
5924 count,
5925 inserted,
5926 } => {
5927 let total = count.max(1) * scale;
5928 execute_line_op(ed, op, total);
5929 if let Some(text) = inserted {
5930 replay_insert_and_finish(ed, &text);
5931 }
5932 }
5933 LastChange::CharDel { forward, count } => {
5934 do_char_delete(ed, forward, count * scale);
5935 }
5936 LastChange::ReplaceChar { ch, count } => {
5937 replace_char(ed, ch, count * scale);
5938 }
5939 LastChange::ToggleCase { count } => {
5940 for _ in 0..count * scale {
5941 ed.push_undo();
5942 toggle_case_at_cursor(ed);
5943 }
5944 }
5945 LastChange::JoinLine { count } => {
5946 for _ in 0..count * scale {
5947 ed.push_undo();
5948 join_line(ed);
5949 }
5950 }
5951 LastChange::Paste { before, count } => {
5952 do_paste(ed, before, count * scale);
5953 }
5954 LastChange::DeleteToEol { inserted } => {
5955 use hjkl_buffer::{Edit, Position};
5956 ed.push_undo();
5957 delete_to_eol(ed);
5958 if let Some(text) = inserted {
5959 let cursor = ed.cursor();
5960 ed.mutate_edit(Edit::InsertStr {
5961 at: Position::new(cursor.0, cursor.1),
5962 text,
5963 });
5964 }
5965 }
5966 LastChange::OpenLine { above, inserted } => {
5967 use hjkl_buffer::{Edit, Position};
5968 ed.push_undo();
5969 ed.sync_buffer_content_from_textarea();
5970 let row = buf_cursor_pos(&ed.buffer).row;
5971 if above {
5972 ed.mutate_edit(Edit::InsertStr {
5973 at: Position::new(row, 0),
5974 text: "\n".to_string(),
5975 });
5976 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5977 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5978 } else {
5979 let line_chars = buf_line_chars(&ed.buffer, row);
5980 ed.mutate_edit(Edit::InsertStr {
5981 at: Position::new(row, line_chars),
5982 text: "\n".to_string(),
5983 });
5984 }
5985 ed.push_buffer_cursor_to_textarea();
5986 let cursor = ed.cursor();
5987 ed.mutate_edit(Edit::InsertStr {
5988 at: Position::new(cursor.0, cursor.1),
5989 text: inserted,
5990 });
5991 }
5992 LastChange::InsertAt {
5993 entry,
5994 inserted,
5995 count,
5996 } => {
5997 use hjkl_buffer::{Edit, Position};
5998 ed.push_undo();
5999 match entry {
6000 InsertEntry::I => {}
6001 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6002 InsertEntry::A => {
6003 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6004 ed.push_buffer_cursor_to_textarea();
6005 }
6006 InsertEntry::ShiftA => {
6007 crate::motions::move_line_end(&mut ed.buffer);
6008 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6009 ed.push_buffer_cursor_to_textarea();
6010 }
6011 }
6012 for _ in 0..count.max(1) {
6013 let cursor = ed.cursor();
6014 ed.mutate_edit(Edit::InsertStr {
6015 at: Position::new(cursor.0, cursor.1),
6016 text: inserted.clone(),
6017 });
6018 }
6019 }
6020 }
6021 ed.vim.replaying = false;
6022}
6023
6024fn extract_inserted(before: &str, after: &str) -> String {
6027 let before_chars: Vec<char> = before.chars().collect();
6028 let after_chars: Vec<char> = after.chars().collect();
6029 if after_chars.len() <= before_chars.len() {
6030 return String::new();
6031 }
6032 let prefix = before_chars
6033 .iter()
6034 .zip(after_chars.iter())
6035 .take_while(|(a, b)| a == b)
6036 .count();
6037 let max_suffix = before_chars.len() - prefix;
6038 let suffix = before_chars
6039 .iter()
6040 .rev()
6041 .zip(after_chars.iter().rev())
6042 .take(max_suffix)
6043 .take_while(|(a, b)| a == b)
6044 .count();
6045 after_chars[prefix..after_chars.len() - suffix]
6046 .iter()
6047 .collect()
6048}
6049
6050#[cfg(all(test, feature = "crossterm"))]
6053mod tests {
6054 use crate::VimMode;
6055 use crate::editor::Editor;
6056 use crate::types::Host;
6057 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6058
6059 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6060 let mut iter = keys.chars().peekable();
6064 while let Some(c) = iter.next() {
6065 if c == '<' {
6066 let mut tag = String::new();
6067 for ch in iter.by_ref() {
6068 if ch == '>' {
6069 break;
6070 }
6071 tag.push(ch);
6072 }
6073 let ev = match tag.as_str() {
6074 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6075 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6076 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6077 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6078 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6079 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6080 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6081 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6082 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6086 s if s.starts_with("C-") => {
6087 let ch = s.chars().nth(2).unwrap();
6088 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6089 }
6090 _ => continue,
6091 };
6092 e.handle_key(ev);
6093 } else {
6094 let mods = if c.is_uppercase() {
6095 KeyModifiers::SHIFT
6096 } else {
6097 KeyModifiers::NONE
6098 };
6099 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6100 }
6101 }
6102 }
6103
6104 fn editor_with(content: &str) -> Editor {
6105 let opts = crate::types::Options {
6110 shiftwidth: 2,
6111 ..crate::types::Options::default()
6112 };
6113 let mut e = Editor::new(
6114 hjkl_buffer::Buffer::new(),
6115 crate::types::DefaultHost::new(),
6116 opts,
6117 );
6118 e.set_content(content);
6119 e
6120 }
6121
6122 #[test]
6123 fn f_char_jumps_on_line() {
6124 let mut e = editor_with("hello world");
6125 run_keys(&mut e, "fw");
6126 assert_eq!(e.cursor(), (0, 6));
6127 }
6128
6129 #[test]
6130 fn cap_f_jumps_backward() {
6131 let mut e = editor_with("hello world");
6132 e.jump_cursor(0, 10);
6133 run_keys(&mut e, "Fo");
6134 assert_eq!(e.cursor().1, 7);
6135 }
6136
6137 #[test]
6138 fn t_stops_before_char() {
6139 let mut e = editor_with("hello");
6140 run_keys(&mut e, "tl");
6141 assert_eq!(e.cursor(), (0, 1));
6142 }
6143
6144 #[test]
6145 fn semicolon_repeats_find() {
6146 let mut e = editor_with("aa.bb.cc");
6147 run_keys(&mut e, "f.");
6148 assert_eq!(e.cursor().1, 2);
6149 run_keys(&mut e, ";");
6150 assert_eq!(e.cursor().1, 5);
6151 }
6152
6153 #[test]
6154 fn comma_repeats_find_reverse() {
6155 let mut e = editor_with("aa.bb.cc");
6156 run_keys(&mut e, "f.");
6157 run_keys(&mut e, ";");
6158 run_keys(&mut e, ",");
6159 assert_eq!(e.cursor().1, 2);
6160 }
6161
6162 #[test]
6163 fn di_quote_deletes_content() {
6164 let mut e = editor_with("foo \"bar\" baz");
6165 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6167 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6168 }
6169
6170 #[test]
6171 fn da_quote_deletes_with_quotes() {
6172 let mut e = editor_with("foo \"bar\" baz");
6175 e.jump_cursor(0, 6);
6176 run_keys(&mut e, "da\"");
6177 assert_eq!(e.buffer().lines()[0], "foo baz");
6178 }
6179
6180 #[test]
6181 fn ci_paren_deletes_and_inserts() {
6182 let mut e = editor_with("fn(a, b, c)");
6183 e.jump_cursor(0, 5);
6184 run_keys(&mut e, "ci(");
6185 assert_eq!(e.vim_mode(), VimMode::Insert);
6186 assert_eq!(e.buffer().lines()[0], "fn()");
6187 }
6188
6189 #[test]
6190 fn diw_deletes_inner_word() {
6191 let mut e = editor_with("hello world");
6192 e.jump_cursor(0, 2);
6193 run_keys(&mut e, "diw");
6194 assert_eq!(e.buffer().lines()[0], " world");
6195 }
6196
6197 #[test]
6198 fn daw_deletes_word_with_trailing_space() {
6199 let mut e = editor_with("hello world");
6200 run_keys(&mut e, "daw");
6201 assert_eq!(e.buffer().lines()[0], "world");
6202 }
6203
6204 #[test]
6205 fn percent_jumps_to_matching_bracket() {
6206 let mut e = editor_with("foo(bar)");
6207 e.jump_cursor(0, 3);
6208 run_keys(&mut e, "%");
6209 assert_eq!(e.cursor().1, 7);
6210 run_keys(&mut e, "%");
6211 assert_eq!(e.cursor().1, 3);
6212 }
6213
6214 #[test]
6215 fn dot_repeats_last_change() {
6216 let mut e = editor_with("aaa bbb ccc");
6217 run_keys(&mut e, "dw");
6218 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6219 run_keys(&mut e, ".");
6220 assert_eq!(e.buffer().lines()[0], "ccc");
6221 }
6222
6223 #[test]
6224 fn dot_repeats_change_operator_with_text() {
6225 let mut e = editor_with("foo foo foo");
6226 run_keys(&mut e, "cwbar<Esc>");
6227 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6228 run_keys(&mut e, "w");
6230 run_keys(&mut e, ".");
6231 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6232 }
6233
6234 #[test]
6235 fn dot_repeats_x() {
6236 let mut e = editor_with("abcdef");
6237 run_keys(&mut e, "x");
6238 run_keys(&mut e, "..");
6239 assert_eq!(e.buffer().lines()[0], "def");
6240 }
6241
6242 #[test]
6243 fn count_operator_motion_compose() {
6244 let mut e = editor_with("one two three four five");
6245 run_keys(&mut e, "d3w");
6246 assert_eq!(e.buffer().lines()[0], "four five");
6247 }
6248
6249 #[test]
6250 fn two_dd_deletes_two_lines() {
6251 let mut e = editor_with("a\nb\nc");
6252 run_keys(&mut e, "2dd");
6253 assert_eq!(e.buffer().lines().len(), 1);
6254 assert_eq!(e.buffer().lines()[0], "c");
6255 }
6256
6257 #[test]
6262 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6263 let mut e = editor_with("one\ntwo\n three\nfour");
6264 e.jump_cursor(1, 2);
6265 run_keys(&mut e, "dd");
6266 assert_eq!(e.buffer().lines()[1], " three");
6268 assert_eq!(e.cursor(), (1, 4));
6269 }
6270
6271 #[test]
6272 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6273 let mut e = editor_with("one\n two\nthree");
6274 e.jump_cursor(2, 0);
6275 run_keys(&mut e, "dd");
6276 assert_eq!(e.buffer().lines().len(), 2);
6278 assert_eq!(e.cursor(), (1, 2));
6279 }
6280
6281 #[test]
6282 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6283 let mut e = editor_with("lonely");
6284 run_keys(&mut e, "dd");
6285 assert_eq!(e.buffer().lines().len(), 1);
6286 assert_eq!(e.buffer().lines()[0], "");
6287 assert_eq!(e.cursor(), (0, 0));
6288 }
6289
6290 #[test]
6291 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6292 let mut e = editor_with("a\nb\nc\n d\ne");
6293 e.jump_cursor(1, 0);
6295 run_keys(&mut e, "3dd");
6296 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6297 assert_eq!(e.cursor(), (1, 0));
6298 }
6299
6300 #[test]
6301 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6302 let mut e = editor_with(" line one\n line two\n xyz!");
6321 e.jump_cursor(0, 8);
6323 assert_eq!(e.cursor(), (0, 8));
6324 run_keys(&mut e, "dd");
6327 assert_eq!(
6328 e.cursor(),
6329 (0, 4),
6330 "dd must place cursor on first-non-blank"
6331 );
6332 run_keys(&mut e, "j");
6336 let (row, col) = e.cursor();
6337 assert_eq!(row, 1);
6338 assert_eq!(
6339 col, 4,
6340 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6341 );
6342 }
6343
6344 #[test]
6345 fn gu_lowercases_motion_range() {
6346 let mut e = editor_with("HELLO WORLD");
6347 run_keys(&mut e, "guw");
6348 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6349 assert_eq!(e.cursor(), (0, 0));
6350 }
6351
6352 #[test]
6353 fn g_u_uppercases_text_object() {
6354 let mut e = editor_with("hello world");
6355 run_keys(&mut e, "gUiw");
6357 assert_eq!(e.buffer().lines()[0], "HELLO world");
6358 assert_eq!(e.cursor(), (0, 0));
6359 }
6360
6361 #[test]
6362 fn g_tilde_toggles_case_of_range() {
6363 let mut e = editor_with("Hello World");
6364 run_keys(&mut e, "g~iw");
6365 assert_eq!(e.buffer().lines()[0], "hELLO World");
6366 }
6367
6368 #[test]
6369 fn g_uu_uppercases_current_line() {
6370 let mut e = editor_with("select 1\nselect 2");
6371 run_keys(&mut e, "gUU");
6372 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6373 assert_eq!(e.buffer().lines()[1], "select 2");
6374 }
6375
6376 #[test]
6377 fn gugu_lowercases_current_line() {
6378 let mut e = editor_with("FOO BAR\nBAZ");
6379 run_keys(&mut e, "gugu");
6380 assert_eq!(e.buffer().lines()[0], "foo bar");
6381 }
6382
6383 #[test]
6384 fn visual_u_uppercases_selection() {
6385 let mut e = editor_with("hello world");
6386 run_keys(&mut e, "veU");
6388 assert_eq!(e.buffer().lines()[0], "HELLO world");
6389 }
6390
6391 #[test]
6392 fn visual_line_u_lowercases_line() {
6393 let mut e = editor_with("HELLO WORLD\nOTHER");
6394 run_keys(&mut e, "Vu");
6395 assert_eq!(e.buffer().lines()[0], "hello world");
6396 assert_eq!(e.buffer().lines()[1], "OTHER");
6397 }
6398
6399 #[test]
6400 fn g_uu_with_count_uppercases_multiple_lines() {
6401 let mut e = editor_with("one\ntwo\nthree\nfour");
6402 run_keys(&mut e, "3gUU");
6404 assert_eq!(e.buffer().lines()[0], "ONE");
6405 assert_eq!(e.buffer().lines()[1], "TWO");
6406 assert_eq!(e.buffer().lines()[2], "THREE");
6407 assert_eq!(e.buffer().lines()[3], "four");
6408 }
6409
6410 #[test]
6411 fn double_gt_indents_current_line() {
6412 let mut e = editor_with("hello");
6413 run_keys(&mut e, ">>");
6414 assert_eq!(e.buffer().lines()[0], " hello");
6415 assert_eq!(e.cursor(), (0, 2));
6417 }
6418
6419 #[test]
6420 fn double_lt_outdents_current_line() {
6421 let mut e = editor_with(" hello");
6422 run_keys(&mut e, "<lt><lt>");
6423 assert_eq!(e.buffer().lines()[0], " hello");
6424 assert_eq!(e.cursor(), (0, 2));
6425 }
6426
6427 #[test]
6428 fn count_double_gt_indents_multiple_lines() {
6429 let mut e = editor_with("a\nb\nc\nd");
6430 run_keys(&mut e, "3>>");
6432 assert_eq!(e.buffer().lines()[0], " a");
6433 assert_eq!(e.buffer().lines()[1], " b");
6434 assert_eq!(e.buffer().lines()[2], " c");
6435 assert_eq!(e.buffer().lines()[3], "d");
6436 }
6437
6438 #[test]
6439 fn outdent_clips_ragged_leading_whitespace() {
6440 let mut e = editor_with(" x");
6443 run_keys(&mut e, "<lt><lt>");
6444 assert_eq!(e.buffer().lines()[0], "x");
6445 }
6446
6447 #[test]
6448 fn indent_motion_is_always_linewise() {
6449 let mut e = editor_with("foo bar");
6452 run_keys(&mut e, ">w");
6453 assert_eq!(e.buffer().lines()[0], " foo bar");
6454 }
6455
6456 #[test]
6457 fn indent_text_object_extends_over_paragraph() {
6458 let mut e = editor_with("a\nb\n\nc\nd");
6459 run_keys(&mut e, ">ap");
6461 assert_eq!(e.buffer().lines()[0], " a");
6462 assert_eq!(e.buffer().lines()[1], " b");
6463 assert_eq!(e.buffer().lines()[2], "");
6464 assert_eq!(e.buffer().lines()[3], "c");
6465 }
6466
6467 #[test]
6468 fn visual_line_indent_shifts_selected_rows() {
6469 let mut e = editor_with("x\ny\nz");
6470 run_keys(&mut e, "Vj>");
6472 assert_eq!(e.buffer().lines()[0], " x");
6473 assert_eq!(e.buffer().lines()[1], " y");
6474 assert_eq!(e.buffer().lines()[2], "z");
6475 }
6476
6477 #[test]
6478 fn outdent_empty_line_is_noop() {
6479 let mut e = editor_with("\nfoo");
6480 run_keys(&mut e, "<lt><lt>");
6481 assert_eq!(e.buffer().lines()[0], "");
6482 }
6483
6484 #[test]
6485 fn indent_skips_empty_lines() {
6486 let mut e = editor_with("");
6489 run_keys(&mut e, ">>");
6490 assert_eq!(e.buffer().lines()[0], "");
6491 }
6492
6493 #[test]
6494 fn insert_ctrl_t_indents_current_line() {
6495 let mut e = editor_with("x");
6496 run_keys(&mut e, "i<C-t>");
6498 assert_eq!(e.buffer().lines()[0], " x");
6499 assert_eq!(e.cursor(), (0, 2));
6502 }
6503
6504 #[test]
6505 fn insert_ctrl_d_outdents_current_line() {
6506 let mut e = editor_with(" x");
6507 run_keys(&mut e, "A<C-d>");
6509 assert_eq!(e.buffer().lines()[0], " x");
6510 }
6511
6512 #[test]
6513 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6514 let mut e = editor_with("first\nsecond");
6515 e.jump_cursor(1, 0);
6516 run_keys(&mut e, "h");
6517 assert_eq!(e.cursor(), (1, 0));
6519 }
6520
6521 #[test]
6522 fn l_at_last_char_does_not_wrap_to_next_line() {
6523 let mut e = editor_with("ab\ncd");
6524 e.jump_cursor(0, 1);
6526 run_keys(&mut e, "l");
6527 assert_eq!(e.cursor(), (0, 1));
6529 }
6530
6531 #[test]
6532 fn count_l_clamps_at_line_end() {
6533 let mut e = editor_with("abcde");
6534 run_keys(&mut e, "20l");
6537 assert_eq!(e.cursor(), (0, 4));
6538 }
6539
6540 #[test]
6541 fn count_h_clamps_at_col_zero() {
6542 let mut e = editor_with("abcde");
6543 e.jump_cursor(0, 3);
6544 run_keys(&mut e, "20h");
6545 assert_eq!(e.cursor(), (0, 0));
6546 }
6547
6548 #[test]
6549 fn dl_on_last_char_still_deletes_it() {
6550 let mut e = editor_with("ab");
6554 e.jump_cursor(0, 1);
6555 run_keys(&mut e, "dl");
6556 assert_eq!(e.buffer().lines()[0], "a");
6557 }
6558
6559 #[test]
6560 fn case_op_preserves_yank_register() {
6561 let mut e = editor_with("target");
6562 run_keys(&mut e, "yy");
6563 let yank_before = e.yank().to_string();
6564 run_keys(&mut e, "gUU");
6566 assert_eq!(e.buffer().lines()[0], "TARGET");
6567 assert_eq!(
6568 e.yank(),
6569 yank_before,
6570 "case ops must preserve the yank buffer"
6571 );
6572 }
6573
6574 #[test]
6575 fn dap_deletes_paragraph() {
6576 let mut e = editor_with("a\nb\n\nc\nd");
6577 run_keys(&mut e, "dap");
6578 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6579 }
6580
6581 #[test]
6582 fn dit_deletes_inner_tag_content() {
6583 let mut e = editor_with("<b>hello</b>");
6584 e.jump_cursor(0, 4);
6586 run_keys(&mut e, "dit");
6587 assert_eq!(e.buffer().lines()[0], "<b></b>");
6588 }
6589
6590 #[test]
6591 fn dat_deletes_around_tag() {
6592 let mut e = editor_with("hi <b>foo</b> bye");
6593 e.jump_cursor(0, 6);
6594 run_keys(&mut e, "dat");
6595 assert_eq!(e.buffer().lines()[0], "hi bye");
6596 }
6597
6598 #[test]
6599 fn dit_picks_innermost_tag() {
6600 let mut e = editor_with("<a><b>x</b></a>");
6601 e.jump_cursor(0, 6);
6603 run_keys(&mut e, "dit");
6604 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6606 }
6607
6608 #[test]
6609 fn dat_innermost_tag_pair() {
6610 let mut e = editor_with("<a><b>x</b></a>");
6611 e.jump_cursor(0, 6);
6612 run_keys(&mut e, "dat");
6613 assert_eq!(e.buffer().lines()[0], "<a></a>");
6614 }
6615
6616 #[test]
6617 fn dit_outside_any_tag_no_op() {
6618 let mut e = editor_with("plain text");
6619 e.jump_cursor(0, 3);
6620 run_keys(&mut e, "dit");
6621 assert_eq!(e.buffer().lines()[0], "plain text");
6623 }
6624
6625 #[test]
6626 fn cit_changes_inner_tag_content() {
6627 let mut e = editor_with("<b>hello</b>");
6628 e.jump_cursor(0, 4);
6629 run_keys(&mut e, "citNEW<Esc>");
6630 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6631 }
6632
6633 #[test]
6634 fn cat_changes_around_tag() {
6635 let mut e = editor_with("hi <b>foo</b> bye");
6636 e.jump_cursor(0, 6);
6637 run_keys(&mut e, "catBAR<Esc>");
6638 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6639 }
6640
6641 #[test]
6642 fn yit_yanks_inner_tag_content() {
6643 let mut e = editor_with("<b>hello</b>");
6644 e.jump_cursor(0, 4);
6645 run_keys(&mut e, "yit");
6646 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6647 }
6648
6649 #[test]
6650 fn yat_yanks_full_tag_pair() {
6651 let mut e = editor_with("hi <b>foo</b> bye");
6652 e.jump_cursor(0, 6);
6653 run_keys(&mut e, "yat");
6654 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6655 }
6656
6657 #[test]
6658 fn vit_visually_selects_inner_tag() {
6659 let mut e = editor_with("<b>hello</b>");
6660 e.jump_cursor(0, 4);
6661 run_keys(&mut e, "vit");
6662 assert_eq!(e.vim_mode(), VimMode::Visual);
6663 run_keys(&mut e, "y");
6664 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6665 }
6666
6667 #[test]
6668 fn vat_visually_selects_around_tag() {
6669 let mut e = editor_with("x<b>foo</b>y");
6670 e.jump_cursor(0, 5);
6671 run_keys(&mut e, "vat");
6672 assert_eq!(e.vim_mode(), VimMode::Visual);
6673 run_keys(&mut e, "y");
6674 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6675 }
6676
6677 #[test]
6680 #[allow(non_snake_case)]
6681 fn diW_deletes_inner_big_word() {
6682 let mut e = editor_with("foo.bar baz");
6683 e.jump_cursor(0, 2);
6684 run_keys(&mut e, "diW");
6685 assert_eq!(e.buffer().lines()[0], " baz");
6687 }
6688
6689 #[test]
6690 #[allow(non_snake_case)]
6691 fn daW_deletes_around_big_word() {
6692 let mut e = editor_with("foo.bar baz");
6693 e.jump_cursor(0, 2);
6694 run_keys(&mut e, "daW");
6695 assert_eq!(e.buffer().lines()[0], "baz");
6696 }
6697
6698 #[test]
6699 fn di_double_quote_deletes_inside() {
6700 let mut e = editor_with("a \"hello\" b");
6701 e.jump_cursor(0, 4);
6702 run_keys(&mut e, "di\"");
6703 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6704 }
6705
6706 #[test]
6707 fn da_double_quote_deletes_around() {
6708 let mut e = editor_with("a \"hello\" b");
6710 e.jump_cursor(0, 4);
6711 run_keys(&mut e, "da\"");
6712 assert_eq!(e.buffer().lines()[0], "a b");
6713 }
6714
6715 #[test]
6716 fn di_single_quote_deletes_inside() {
6717 let mut e = editor_with("x 'foo' y");
6718 e.jump_cursor(0, 4);
6719 run_keys(&mut e, "di'");
6720 assert_eq!(e.buffer().lines()[0], "x '' y");
6721 }
6722
6723 #[test]
6724 fn da_single_quote_deletes_around() {
6725 let mut e = editor_with("x 'foo' y");
6727 e.jump_cursor(0, 4);
6728 run_keys(&mut e, "da'");
6729 assert_eq!(e.buffer().lines()[0], "x y");
6730 }
6731
6732 #[test]
6733 fn di_backtick_deletes_inside() {
6734 let mut e = editor_with("p `q` r");
6735 e.jump_cursor(0, 3);
6736 run_keys(&mut e, "di`");
6737 assert_eq!(e.buffer().lines()[0], "p `` r");
6738 }
6739
6740 #[test]
6741 fn da_backtick_deletes_around() {
6742 let mut e = editor_with("p `q` r");
6744 e.jump_cursor(0, 3);
6745 run_keys(&mut e, "da`");
6746 assert_eq!(e.buffer().lines()[0], "p r");
6747 }
6748
6749 #[test]
6750 fn di_paren_deletes_inside() {
6751 let mut e = editor_with("f(arg)");
6752 e.jump_cursor(0, 3);
6753 run_keys(&mut e, "di(");
6754 assert_eq!(e.buffer().lines()[0], "f()");
6755 }
6756
6757 #[test]
6758 fn di_paren_alias_b_works() {
6759 let mut e = editor_with("f(arg)");
6760 e.jump_cursor(0, 3);
6761 run_keys(&mut e, "dib");
6762 assert_eq!(e.buffer().lines()[0], "f()");
6763 }
6764
6765 #[test]
6766 fn di_bracket_deletes_inside() {
6767 let mut e = editor_with("a[b,c]d");
6768 e.jump_cursor(0, 3);
6769 run_keys(&mut e, "di[");
6770 assert_eq!(e.buffer().lines()[0], "a[]d");
6771 }
6772
6773 #[test]
6774 fn da_bracket_deletes_around() {
6775 let mut e = editor_with("a[b,c]d");
6776 e.jump_cursor(0, 3);
6777 run_keys(&mut e, "da[");
6778 assert_eq!(e.buffer().lines()[0], "ad");
6779 }
6780
6781 #[test]
6782 fn di_brace_deletes_inside() {
6783 let mut e = editor_with("x{y}z");
6784 e.jump_cursor(0, 2);
6785 run_keys(&mut e, "di{");
6786 assert_eq!(e.buffer().lines()[0], "x{}z");
6787 }
6788
6789 #[test]
6790 fn da_brace_deletes_around() {
6791 let mut e = editor_with("x{y}z");
6792 e.jump_cursor(0, 2);
6793 run_keys(&mut e, "da{");
6794 assert_eq!(e.buffer().lines()[0], "xz");
6795 }
6796
6797 #[test]
6798 fn di_brace_alias_capital_b_works() {
6799 let mut e = editor_with("x{y}z");
6800 e.jump_cursor(0, 2);
6801 run_keys(&mut e, "diB");
6802 assert_eq!(e.buffer().lines()[0], "x{}z");
6803 }
6804
6805 #[test]
6806 fn di_angle_deletes_inside() {
6807 let mut e = editor_with("p<q>r");
6808 e.jump_cursor(0, 2);
6809 run_keys(&mut e, "di<lt>");
6811 assert_eq!(e.buffer().lines()[0], "p<>r");
6812 }
6813
6814 #[test]
6815 fn da_angle_deletes_around() {
6816 let mut e = editor_with("p<q>r");
6817 e.jump_cursor(0, 2);
6818 run_keys(&mut e, "da<lt>");
6819 assert_eq!(e.buffer().lines()[0], "pr");
6820 }
6821
6822 #[test]
6823 fn dip_deletes_inner_paragraph() {
6824 let mut e = editor_with("a\nb\nc\n\nd");
6825 e.jump_cursor(1, 0);
6826 run_keys(&mut e, "dip");
6827 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6830 }
6831
6832 #[test]
6835 fn sentence_motion_close_paren_jumps_forward() {
6836 let mut e = editor_with("Alpha. Beta. Gamma.");
6837 e.jump_cursor(0, 0);
6838 run_keys(&mut e, ")");
6839 assert_eq!(e.cursor(), (0, 7));
6841 run_keys(&mut e, ")");
6842 assert_eq!(e.cursor(), (0, 13));
6843 }
6844
6845 #[test]
6846 fn sentence_motion_open_paren_jumps_backward() {
6847 let mut e = editor_with("Alpha. Beta. Gamma.");
6848 e.jump_cursor(0, 13);
6849 run_keys(&mut e, "(");
6850 assert_eq!(e.cursor(), (0, 7));
6853 run_keys(&mut e, "(");
6854 assert_eq!(e.cursor(), (0, 0));
6855 }
6856
6857 #[test]
6858 fn sentence_motion_count() {
6859 let mut e = editor_with("A. B. C. D.");
6860 e.jump_cursor(0, 0);
6861 run_keys(&mut e, "3)");
6862 assert_eq!(e.cursor(), (0, 9));
6864 }
6865
6866 #[test]
6867 fn dis_deletes_inner_sentence() {
6868 let mut e = editor_with("First one. Second one. Third one.");
6869 e.jump_cursor(0, 13);
6870 run_keys(&mut e, "dis");
6871 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6873 }
6874
6875 #[test]
6876 fn das_deletes_around_sentence_with_trailing_space() {
6877 let mut e = editor_with("Alpha. Beta. Gamma.");
6878 e.jump_cursor(0, 8);
6879 run_keys(&mut e, "das");
6880 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6883 }
6884
6885 #[test]
6886 fn dis_handles_double_terminator() {
6887 let mut e = editor_with("Wow!? Next.");
6888 e.jump_cursor(0, 1);
6889 run_keys(&mut e, "dis");
6890 assert_eq!(e.buffer().lines()[0], " Next.");
6893 }
6894
6895 #[test]
6896 fn dis_first_sentence_from_cursor_at_zero() {
6897 let mut e = editor_with("Alpha. Beta.");
6898 e.jump_cursor(0, 0);
6899 run_keys(&mut e, "dis");
6900 assert_eq!(e.buffer().lines()[0], " Beta.");
6901 }
6902
6903 #[test]
6904 fn yis_yanks_inner_sentence() {
6905 let mut e = editor_with("Hello world. Bye.");
6906 e.jump_cursor(0, 5);
6907 run_keys(&mut e, "yis");
6908 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6909 }
6910
6911 #[test]
6912 fn vis_visually_selects_inner_sentence() {
6913 let mut e = editor_with("First. Second.");
6914 e.jump_cursor(0, 1);
6915 run_keys(&mut e, "vis");
6916 assert_eq!(e.vim_mode(), VimMode::Visual);
6917 run_keys(&mut e, "y");
6918 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6919 }
6920
6921 #[test]
6922 fn ciw_changes_inner_word() {
6923 let mut e = editor_with("hello world");
6924 e.jump_cursor(0, 1);
6925 run_keys(&mut e, "ciwHEY<Esc>");
6926 assert_eq!(e.buffer().lines()[0], "HEY world");
6927 }
6928
6929 #[test]
6930 fn yiw_yanks_inner_word() {
6931 let mut e = editor_with("hello world");
6932 e.jump_cursor(0, 1);
6933 run_keys(&mut e, "yiw");
6934 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6935 }
6936
6937 #[test]
6938 fn viw_selects_inner_word() {
6939 let mut e = editor_with("hello world");
6940 e.jump_cursor(0, 2);
6941 run_keys(&mut e, "viw");
6942 assert_eq!(e.vim_mode(), VimMode::Visual);
6943 run_keys(&mut e, "y");
6944 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6945 }
6946
6947 #[test]
6948 fn ci_paren_changes_inside() {
6949 let mut e = editor_with("f(old)");
6950 e.jump_cursor(0, 3);
6951 run_keys(&mut e, "ci(NEW<Esc>");
6952 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6953 }
6954
6955 #[test]
6956 fn yi_double_quote_yanks_inside() {
6957 let mut e = editor_with("say \"hi there\" then");
6958 e.jump_cursor(0, 6);
6959 run_keys(&mut e, "yi\"");
6960 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6961 }
6962
6963 #[test]
6964 fn vap_visual_selects_around_paragraph() {
6965 let mut e = editor_with("a\nb\n\nc");
6966 e.jump_cursor(0, 0);
6967 run_keys(&mut e, "vap");
6968 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6969 run_keys(&mut e, "y");
6970 let text = e.registers().read('"').unwrap().text.clone();
6972 assert!(text.starts_with("a\nb"));
6973 }
6974
6975 #[test]
6976 fn star_finds_next_occurrence() {
6977 let mut e = editor_with("foo bar foo baz");
6978 run_keys(&mut e, "*");
6979 assert_eq!(e.cursor().1, 8);
6980 }
6981
6982 #[test]
6983 fn star_skips_substring_match() {
6984 let mut e = editor_with("foo foobar baz");
6987 run_keys(&mut e, "*");
6988 assert_eq!(e.cursor().1, 0);
6989 }
6990
6991 #[test]
6992 fn g_star_matches_substring() {
6993 let mut e = editor_with("foo foobar baz");
6996 run_keys(&mut e, "g*");
6997 assert_eq!(e.cursor().1, 4);
6998 }
6999
7000 #[test]
7001 fn g_pound_matches_substring_backward() {
7002 let mut e = editor_with("foo foobar baz foo");
7005 run_keys(&mut e, "$b");
7006 assert_eq!(e.cursor().1, 15);
7007 run_keys(&mut e, "g#");
7008 assert_eq!(e.cursor().1, 4);
7009 }
7010
7011 #[test]
7012 fn n_repeats_last_search_forward() {
7013 let mut e = editor_with("foo bar foo baz foo");
7014 run_keys(&mut e, "/foo<CR>");
7017 assert_eq!(e.cursor().1, 8);
7018 run_keys(&mut e, "n");
7019 assert_eq!(e.cursor().1, 16);
7020 }
7021
7022 #[test]
7023 fn shift_n_reverses_search() {
7024 let mut e = editor_with("foo bar foo baz foo");
7025 run_keys(&mut e, "/foo<CR>");
7026 run_keys(&mut e, "n");
7027 assert_eq!(e.cursor().1, 16);
7028 run_keys(&mut e, "N");
7029 assert_eq!(e.cursor().1, 8);
7030 }
7031
7032 #[test]
7033 fn n_noop_without_pattern() {
7034 let mut e = editor_with("foo bar");
7035 run_keys(&mut e, "n");
7036 assert_eq!(e.cursor(), (0, 0));
7037 }
7038
7039 #[test]
7040 fn visual_line_preserves_cursor_column() {
7041 let mut e = editor_with("hello world\nanother one\nbye");
7044 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7046 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7047 assert_eq!(e.cursor(), (0, 5));
7048 run_keys(&mut e, "j");
7049 assert_eq!(e.cursor(), (1, 5));
7050 }
7051
7052 #[test]
7053 fn visual_line_yank_includes_trailing_newline() {
7054 let mut e = editor_with("aaa\nbbb\nccc");
7055 run_keys(&mut e, "Vjy");
7056 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7058 }
7059
7060 #[test]
7061 fn visual_line_yank_last_line_trailing_newline() {
7062 let mut e = editor_with("aaa\nbbb\nccc");
7063 run_keys(&mut e, "jj");
7065 run_keys(&mut e, "Vy");
7066 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7067 }
7068
7069 #[test]
7070 fn yy_on_last_line_has_trailing_newline() {
7071 let mut e = editor_with("aaa\nbbb\nccc");
7072 run_keys(&mut e, "jj");
7073 run_keys(&mut e, "yy");
7074 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7075 }
7076
7077 #[test]
7078 fn yy_in_middle_has_trailing_newline() {
7079 let mut e = editor_with("aaa\nbbb\nccc");
7080 run_keys(&mut e, "j");
7081 run_keys(&mut e, "yy");
7082 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7083 }
7084
7085 #[test]
7086 fn di_single_quote() {
7087 let mut e = editor_with("say 'hello world' now");
7088 e.jump_cursor(0, 7);
7089 run_keys(&mut e, "di'");
7090 assert_eq!(e.buffer().lines()[0], "say '' now");
7091 }
7092
7093 #[test]
7094 fn da_single_quote() {
7095 let mut e = editor_with("say 'hello' now");
7097 e.jump_cursor(0, 7);
7098 run_keys(&mut e, "da'");
7099 assert_eq!(e.buffer().lines()[0], "say now");
7100 }
7101
7102 #[test]
7103 fn di_backtick() {
7104 let mut e = editor_with("say `hi` now");
7105 e.jump_cursor(0, 5);
7106 run_keys(&mut e, "di`");
7107 assert_eq!(e.buffer().lines()[0], "say `` now");
7108 }
7109
7110 #[test]
7111 fn di_brace() {
7112 let mut e = editor_with("fn { a; b; c }");
7113 e.jump_cursor(0, 7);
7114 run_keys(&mut e, "di{");
7115 assert_eq!(e.buffer().lines()[0], "fn {}");
7116 }
7117
7118 #[test]
7119 fn di_bracket() {
7120 let mut e = editor_with("arr[1, 2, 3]");
7121 e.jump_cursor(0, 5);
7122 run_keys(&mut e, "di[");
7123 assert_eq!(e.buffer().lines()[0], "arr[]");
7124 }
7125
7126 #[test]
7127 fn dab_deletes_around_paren() {
7128 let mut e = editor_with("fn(a, b) + 1");
7129 e.jump_cursor(0, 4);
7130 run_keys(&mut e, "dab");
7131 assert_eq!(e.buffer().lines()[0], "fn + 1");
7132 }
7133
7134 #[test]
7135 fn da_big_b_deletes_around_brace() {
7136 let mut e = editor_with("x = {a: 1}");
7137 e.jump_cursor(0, 6);
7138 run_keys(&mut e, "daB");
7139 assert_eq!(e.buffer().lines()[0], "x = ");
7140 }
7141
7142 #[test]
7143 fn di_big_w_deletes_bigword() {
7144 let mut e = editor_with("foo-bar baz");
7145 e.jump_cursor(0, 2);
7146 run_keys(&mut e, "diW");
7147 assert_eq!(e.buffer().lines()[0], " baz");
7148 }
7149
7150 #[test]
7151 fn visual_select_inner_word() {
7152 let mut e = editor_with("hello world");
7153 e.jump_cursor(0, 2);
7154 run_keys(&mut e, "viw");
7155 assert_eq!(e.vim_mode(), VimMode::Visual);
7156 run_keys(&mut e, "y");
7157 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7158 }
7159
7160 #[test]
7161 fn visual_select_inner_quote() {
7162 let mut e = editor_with("foo \"bar\" baz");
7163 e.jump_cursor(0, 6);
7164 run_keys(&mut e, "vi\"");
7165 run_keys(&mut e, "y");
7166 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7167 }
7168
7169 #[test]
7170 fn visual_select_inner_paren() {
7171 let mut e = editor_with("fn(a, b)");
7172 e.jump_cursor(0, 4);
7173 run_keys(&mut e, "vi(");
7174 run_keys(&mut e, "y");
7175 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7176 }
7177
7178 #[test]
7179 fn visual_select_outer_brace() {
7180 let mut e = editor_with("{x}");
7181 e.jump_cursor(0, 1);
7182 run_keys(&mut e, "va{");
7183 run_keys(&mut e, "y");
7184 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7185 }
7186
7187 #[test]
7188 fn ci_paren_forward_scans_when_cursor_before_pair() {
7189 let mut e = editor_with("foo(bar)");
7192 e.jump_cursor(0, 0);
7193 run_keys(&mut e, "ci(NEW<Esc>");
7194 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7195 }
7196
7197 #[test]
7198 fn ci_paren_forward_scans_across_lines() {
7199 let mut e = editor_with("first\nfoo(bar)\nlast");
7200 e.jump_cursor(0, 0);
7201 run_keys(&mut e, "ci(NEW<Esc>");
7202 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7203 }
7204
7205 #[test]
7206 fn ci_brace_forward_scans_when_cursor_before_pair() {
7207 let mut e = editor_with("let x = {y};");
7208 e.jump_cursor(0, 0);
7209 run_keys(&mut e, "ci{NEW<Esc>");
7210 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7211 }
7212
7213 #[test]
7214 fn cit_forward_scans_when_cursor_before_tag() {
7215 let mut e = editor_with("text <b>hello</b> rest");
7218 e.jump_cursor(0, 0);
7219 run_keys(&mut e, "citNEW<Esc>");
7220 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7221 }
7222
7223 #[test]
7224 fn dat_forward_scans_when_cursor_before_tag() {
7225 let mut e = editor_with("text <b>hello</b> rest");
7227 e.jump_cursor(0, 0);
7228 run_keys(&mut e, "dat");
7229 assert_eq!(e.buffer().lines()[0], "text rest");
7230 }
7231
7232 #[test]
7233 fn ci_paren_still_works_when_cursor_inside() {
7234 let mut e = editor_with("fn(a, b)");
7237 e.jump_cursor(0, 4);
7238 run_keys(&mut e, "ci(NEW<Esc>");
7239 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7240 }
7241
7242 #[test]
7243 fn caw_changes_word_with_trailing_space() {
7244 let mut e = editor_with("hello world");
7245 run_keys(&mut e, "cawfoo<Esc>");
7246 assert_eq!(e.buffer().lines()[0], "fooworld");
7247 }
7248
7249 #[test]
7250 fn visual_char_yank_preserves_raw_text() {
7251 let mut e = editor_with("hello world");
7252 run_keys(&mut e, "vllly");
7253 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7254 }
7255
7256 #[test]
7257 fn single_line_visual_line_selects_full_line_on_yank() {
7258 let mut e = editor_with("hello world\nbye");
7259 run_keys(&mut e, "V");
7260 run_keys(&mut e, "y");
7263 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7264 }
7265
7266 #[test]
7267 fn visual_line_extends_both_directions() {
7268 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7269 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7271 assert_eq!(e.cursor(), (3, 0));
7272 run_keys(&mut e, "k");
7273 assert_eq!(e.cursor(), (2, 0));
7275 run_keys(&mut e, "k");
7276 assert_eq!(e.cursor(), (1, 0));
7277 }
7278
7279 #[test]
7280 fn visual_char_preserves_cursor_column() {
7281 let mut e = editor_with("hello world");
7282 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7284 assert_eq!(e.cursor(), (0, 5));
7285 run_keys(&mut e, "ll");
7286 assert_eq!(e.cursor(), (0, 7));
7287 }
7288
7289 #[test]
7290 fn visual_char_highlight_bounds_order() {
7291 let mut e = editor_with("abcdef");
7292 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7294 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7297 }
7298
7299 #[test]
7300 fn visual_line_highlight_bounds() {
7301 let mut e = editor_with("a\nb\nc");
7302 run_keys(&mut e, "V");
7303 assert_eq!(e.line_highlight(), Some((0, 0)));
7304 run_keys(&mut e, "j");
7305 assert_eq!(e.line_highlight(), Some((0, 1)));
7306 run_keys(&mut e, "j");
7307 assert_eq!(e.line_highlight(), Some((0, 2)));
7308 }
7309
7310 #[test]
7313 fn h_moves_left() {
7314 let mut e = editor_with("hello");
7315 e.jump_cursor(0, 3);
7316 run_keys(&mut e, "h");
7317 assert_eq!(e.cursor(), (0, 2));
7318 }
7319
7320 #[test]
7321 fn l_moves_right() {
7322 let mut e = editor_with("hello");
7323 run_keys(&mut e, "l");
7324 assert_eq!(e.cursor(), (0, 1));
7325 }
7326
7327 #[test]
7328 fn k_moves_up() {
7329 let mut e = editor_with("a\nb\nc");
7330 e.jump_cursor(2, 0);
7331 run_keys(&mut e, "k");
7332 assert_eq!(e.cursor(), (1, 0));
7333 }
7334
7335 #[test]
7336 fn zero_moves_to_line_start() {
7337 let mut e = editor_with(" hello");
7338 run_keys(&mut e, "$");
7339 run_keys(&mut e, "0");
7340 assert_eq!(e.cursor().1, 0);
7341 }
7342
7343 #[test]
7344 fn caret_moves_to_first_non_blank() {
7345 let mut e = editor_with(" hello");
7346 run_keys(&mut e, "0");
7347 run_keys(&mut e, "^");
7348 assert_eq!(e.cursor().1, 4);
7349 }
7350
7351 #[test]
7352 fn dollar_moves_to_last_char() {
7353 let mut e = editor_with("hello");
7354 run_keys(&mut e, "$");
7355 assert_eq!(e.cursor().1, 4);
7356 }
7357
7358 #[test]
7359 fn dollar_on_empty_line_stays_at_col_zero() {
7360 let mut e = editor_with("");
7361 run_keys(&mut e, "$");
7362 assert_eq!(e.cursor().1, 0);
7363 }
7364
7365 #[test]
7366 fn w_jumps_to_next_word() {
7367 let mut e = editor_with("foo bar baz");
7368 run_keys(&mut e, "w");
7369 assert_eq!(e.cursor().1, 4);
7370 }
7371
7372 #[test]
7373 fn b_jumps_back_a_word() {
7374 let mut e = editor_with("foo bar");
7375 e.jump_cursor(0, 6);
7376 run_keys(&mut e, "b");
7377 assert_eq!(e.cursor().1, 4);
7378 }
7379
7380 #[test]
7381 fn e_jumps_to_word_end() {
7382 let mut e = editor_with("foo bar");
7383 run_keys(&mut e, "e");
7384 assert_eq!(e.cursor().1, 2);
7385 }
7386
7387 #[test]
7390 fn d_dollar_deletes_to_eol() {
7391 let mut e = editor_with("hello world");
7392 e.jump_cursor(0, 5);
7393 run_keys(&mut e, "d$");
7394 assert_eq!(e.buffer().lines()[0], "hello");
7395 }
7396
7397 #[test]
7398 fn d_zero_deletes_to_line_start() {
7399 let mut e = editor_with("hello world");
7400 e.jump_cursor(0, 6);
7401 run_keys(&mut e, "d0");
7402 assert_eq!(e.buffer().lines()[0], "world");
7403 }
7404
7405 #[test]
7406 fn d_caret_deletes_to_first_non_blank() {
7407 let mut e = editor_with(" hello");
7408 e.jump_cursor(0, 6);
7409 run_keys(&mut e, "d^");
7410 assert_eq!(e.buffer().lines()[0], " llo");
7411 }
7412
7413 #[test]
7414 fn d_capital_g_deletes_to_end_of_file() {
7415 let mut e = editor_with("a\nb\nc\nd");
7416 e.jump_cursor(1, 0);
7417 run_keys(&mut e, "dG");
7418 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7419 }
7420
7421 #[test]
7422 fn d_gg_deletes_to_start_of_file() {
7423 let mut e = editor_with("a\nb\nc\nd");
7424 e.jump_cursor(2, 0);
7425 run_keys(&mut e, "dgg");
7426 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7427 }
7428
7429 #[test]
7430 fn cw_is_ce_quirk() {
7431 let mut e = editor_with("foo bar");
7434 run_keys(&mut e, "cwxyz<Esc>");
7435 assert_eq!(e.buffer().lines()[0], "xyz bar");
7436 }
7437
7438 #[test]
7441 fn big_d_deletes_to_eol() {
7442 let mut e = editor_with("hello world");
7443 e.jump_cursor(0, 5);
7444 run_keys(&mut e, "D");
7445 assert_eq!(e.buffer().lines()[0], "hello");
7446 }
7447
7448 #[test]
7449 fn big_c_deletes_to_eol_and_inserts() {
7450 let mut e = editor_with("hello world");
7451 e.jump_cursor(0, 5);
7452 run_keys(&mut e, "C!<Esc>");
7453 assert_eq!(e.buffer().lines()[0], "hello!");
7454 }
7455
7456 #[test]
7457 fn j_joins_next_line_with_space() {
7458 let mut e = editor_with("hello\nworld");
7459 run_keys(&mut e, "J");
7460 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7461 }
7462
7463 #[test]
7464 fn j_strips_leading_whitespace_on_join() {
7465 let mut e = editor_with("hello\n world");
7466 run_keys(&mut e, "J");
7467 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7468 }
7469
7470 #[test]
7471 fn big_x_deletes_char_before_cursor() {
7472 let mut e = editor_with("hello");
7473 e.jump_cursor(0, 3);
7474 run_keys(&mut e, "X");
7475 assert_eq!(e.buffer().lines()[0], "helo");
7476 }
7477
7478 #[test]
7479 fn s_substitutes_char_and_enters_insert() {
7480 let mut e = editor_with("hello");
7481 run_keys(&mut e, "sX<Esc>");
7482 assert_eq!(e.buffer().lines()[0], "Xello");
7483 }
7484
7485 #[test]
7486 fn count_x_deletes_many() {
7487 let mut e = editor_with("abcdef");
7488 run_keys(&mut e, "3x");
7489 assert_eq!(e.buffer().lines()[0], "def");
7490 }
7491
7492 #[test]
7495 fn p_pastes_charwise_after_cursor() {
7496 let mut e = editor_with("hello");
7497 run_keys(&mut e, "yw");
7498 run_keys(&mut e, "$p");
7499 assert_eq!(e.buffer().lines()[0], "hellohello");
7500 }
7501
7502 #[test]
7503 fn capital_p_pastes_charwise_before_cursor() {
7504 let mut e = editor_with("hello");
7505 run_keys(&mut e, "v");
7507 run_keys(&mut e, "l");
7508 run_keys(&mut e, "y");
7509 run_keys(&mut e, "$P");
7510 assert_eq!(e.buffer().lines()[0], "hellheo");
7513 }
7514
7515 #[test]
7516 fn p_pastes_linewise_below() {
7517 let mut e = editor_with("one\ntwo\nthree");
7518 run_keys(&mut e, "yy");
7519 run_keys(&mut e, "p");
7520 assert_eq!(
7521 e.buffer().lines(),
7522 &[
7523 "one".to_string(),
7524 "one".to_string(),
7525 "two".to_string(),
7526 "three".to_string()
7527 ]
7528 );
7529 }
7530
7531 #[test]
7532 fn capital_p_pastes_linewise_above() {
7533 let mut e = editor_with("one\ntwo");
7534 e.jump_cursor(1, 0);
7535 run_keys(&mut e, "yy");
7536 run_keys(&mut e, "P");
7537 assert_eq!(
7538 e.buffer().lines(),
7539 &["one".to_string(), "two".to_string(), "two".to_string()]
7540 );
7541 }
7542
7543 #[test]
7546 fn hash_finds_previous_occurrence() {
7547 let mut e = editor_with("foo bar foo baz foo");
7548 e.jump_cursor(0, 16);
7550 run_keys(&mut e, "#");
7551 assert_eq!(e.cursor().1, 8);
7552 }
7553
7554 #[test]
7557 fn visual_line_delete_removes_full_lines() {
7558 let mut e = editor_with("a\nb\nc\nd");
7559 run_keys(&mut e, "Vjd");
7560 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7561 }
7562
7563 #[test]
7564 fn visual_line_change_leaves_blank_line() {
7565 let mut e = editor_with("a\nb\nc");
7566 run_keys(&mut e, "Vjc");
7567 assert_eq!(e.vim_mode(), VimMode::Insert);
7568 run_keys(&mut e, "X<Esc>");
7569 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7573 }
7574
7575 #[test]
7576 fn cc_leaves_blank_line() {
7577 let mut e = editor_with("a\nb\nc");
7578 e.jump_cursor(1, 0);
7579 run_keys(&mut e, "ccX<Esc>");
7580 assert_eq!(
7581 e.buffer().lines(),
7582 &["a".to_string(), "X".to_string(), "c".to_string()]
7583 );
7584 }
7585
7586 #[test]
7591 fn big_w_skips_hyphens() {
7592 let mut e = editor_with("foo-bar baz");
7594 run_keys(&mut e, "W");
7595 assert_eq!(e.cursor().1, 8);
7596 }
7597
7598 #[test]
7599 fn big_w_crosses_lines() {
7600 let mut e = editor_with("foo-bar\nbaz-qux");
7601 run_keys(&mut e, "W");
7602 assert_eq!(e.cursor(), (1, 0));
7603 }
7604
7605 #[test]
7606 fn big_b_skips_hyphens() {
7607 let mut e = editor_with("foo-bar baz");
7608 e.jump_cursor(0, 9);
7609 run_keys(&mut e, "B");
7610 assert_eq!(e.cursor().1, 8);
7611 run_keys(&mut e, "B");
7612 assert_eq!(e.cursor().1, 0);
7613 }
7614
7615 #[test]
7616 fn big_e_jumps_to_big_word_end() {
7617 let mut e = editor_with("foo-bar baz");
7618 run_keys(&mut e, "E");
7619 assert_eq!(e.cursor().1, 6);
7620 run_keys(&mut e, "E");
7621 assert_eq!(e.cursor().1, 10);
7622 }
7623
7624 #[test]
7625 fn dw_with_big_word_variant() {
7626 let mut e = editor_with("foo-bar baz");
7628 run_keys(&mut e, "dW");
7629 assert_eq!(e.buffer().lines()[0], "baz");
7630 }
7631
7632 #[test]
7635 fn insert_ctrl_w_deletes_word_back() {
7636 let mut e = editor_with("");
7637 run_keys(&mut e, "i");
7638 for c in "hello world".chars() {
7639 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7640 }
7641 run_keys(&mut e, "<C-w>");
7642 assert_eq!(e.buffer().lines()[0], "hello ");
7643 }
7644
7645 #[test]
7646 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7647 let mut e = editor_with("hello\nworld");
7651 e.jump_cursor(1, 0);
7652 run_keys(&mut e, "i");
7653 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7654 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7657 assert_eq!(e.cursor(), (0, 0));
7658 }
7659
7660 #[test]
7661 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7662 let mut e = editor_with("foo bar\nbaz");
7663 e.jump_cursor(1, 0);
7664 run_keys(&mut e, "i");
7665 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7666 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7668 assert_eq!(e.cursor(), (0, 4));
7669 }
7670
7671 #[test]
7672 fn insert_ctrl_u_deletes_to_line_start() {
7673 let mut e = editor_with("");
7674 run_keys(&mut e, "i");
7675 for c in "hello world".chars() {
7676 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7677 }
7678 run_keys(&mut e, "<C-u>");
7679 assert_eq!(e.buffer().lines()[0], "");
7680 }
7681
7682 #[test]
7683 fn insert_ctrl_o_runs_one_normal_command() {
7684 let mut e = editor_with("hello world");
7685 run_keys(&mut e, "A");
7687 assert_eq!(e.vim_mode(), VimMode::Insert);
7688 e.jump_cursor(0, 0);
7690 run_keys(&mut e, "<C-o>");
7691 assert_eq!(e.vim_mode(), VimMode::Normal);
7692 run_keys(&mut e, "dw");
7693 assert_eq!(e.vim_mode(), VimMode::Insert);
7695 assert_eq!(e.buffer().lines()[0], "world");
7696 }
7697
7698 #[test]
7701 fn j_through_empty_line_preserves_column() {
7702 let mut e = editor_with("hello world\n\nanother line");
7703 run_keys(&mut e, "llllll");
7705 assert_eq!(e.cursor(), (0, 6));
7706 run_keys(&mut e, "j");
7709 assert_eq!(e.cursor(), (1, 0));
7710 run_keys(&mut e, "j");
7712 assert_eq!(e.cursor(), (2, 6));
7713 }
7714
7715 #[test]
7716 fn j_through_shorter_line_preserves_column() {
7717 let mut e = editor_with("hello world\nhi\nanother line");
7718 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7721 run_keys(&mut e, "j");
7722 assert_eq!(e.cursor(), (2, 7));
7723 }
7724
7725 #[test]
7726 fn esc_from_insert_sticky_matches_visible_cursor() {
7727 let mut e = editor_with(" this is a line\n another one of a similar size");
7731 e.jump_cursor(0, 12);
7732 run_keys(&mut e, "I");
7733 assert_eq!(e.cursor(), (0, 4));
7734 run_keys(&mut e, "X<Esc>");
7735 assert_eq!(e.cursor(), (0, 4));
7736 run_keys(&mut e, "j");
7737 assert_eq!(e.cursor(), (1, 4));
7738 }
7739
7740 #[test]
7741 fn esc_from_insert_sticky_tracks_inserted_chars() {
7742 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7743 run_keys(&mut e, "i");
7744 run_keys(&mut e, "abc<Esc>");
7745 assert_eq!(e.cursor(), (0, 2));
7746 run_keys(&mut e, "j");
7747 assert_eq!(e.cursor(), (1, 2));
7748 }
7749
7750 #[test]
7751 fn esc_from_insert_sticky_tracks_arrow_nav() {
7752 let mut e = editor_with("xxxxxx\nyyyyyy");
7753 run_keys(&mut e, "i");
7754 run_keys(&mut e, "abc");
7755 for _ in 0..2 {
7756 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7757 }
7758 run_keys(&mut e, "<Esc>");
7759 assert_eq!(e.cursor(), (0, 0));
7760 run_keys(&mut e, "j");
7761 assert_eq!(e.cursor(), (1, 0));
7762 }
7763
7764 #[test]
7765 fn esc_from_insert_at_col_14_followed_by_j() {
7766 let line = "x".repeat(30);
7769 let buf = format!("{line}\n{line}");
7770 let mut e = editor_with(&buf);
7771 e.jump_cursor(0, 14);
7772 run_keys(&mut e, "i");
7773 for c in "test ".chars() {
7774 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7775 }
7776 run_keys(&mut e, "<Esc>");
7777 assert_eq!(e.cursor(), (0, 18));
7778 run_keys(&mut e, "j");
7779 assert_eq!(e.cursor(), (1, 18));
7780 }
7781
7782 #[test]
7783 fn linewise_paste_resets_sticky_column() {
7784 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7788 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7790 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7794 run_keys(&mut e, "j");
7796 assert_eq!(e.cursor(), (3, 2));
7797 }
7798
7799 #[test]
7800 fn horizontal_motion_resyncs_sticky_column() {
7801 let mut e = editor_with("hello world\n\nanother line");
7805 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7808 assert_eq!(e.cursor(), (2, 3));
7809 }
7810
7811 #[test]
7814 fn ctrl_v_enters_visual_block() {
7815 let mut e = editor_with("aaa\nbbb\nccc");
7816 run_keys(&mut e, "<C-v>");
7817 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7818 }
7819
7820 #[test]
7821 fn visual_block_esc_returns_to_normal() {
7822 let mut e = editor_with("aaa\nbbb\nccc");
7823 run_keys(&mut e, "<C-v>");
7824 run_keys(&mut e, "<Esc>");
7825 assert_eq!(e.vim_mode(), VimMode::Normal);
7826 }
7827
7828 #[test]
7829 fn backtick_lt_jumps_to_visual_start_mark() {
7830 let mut e = editor_with("foo bar baz\n");
7834 run_keys(&mut e, "v");
7835 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7838 run_keys(&mut e, "`<lt>");
7840 assert_eq!(e.cursor(), (0, 0));
7841 }
7842
7843 #[test]
7844 fn backtick_gt_jumps_to_visual_end_mark() {
7845 let mut e = editor_with("foo bar baz\n");
7846 run_keys(&mut e, "v");
7847 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7849 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7851 assert_eq!(e.cursor(), (0, 4));
7852 }
7853
7854 #[test]
7855 fn visual_exit_sets_lt_gt_marks() {
7856 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7859 run_keys(&mut e, "V");
7861 run_keys(&mut e, "j");
7862 run_keys(&mut e, "<Esc>");
7863 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7864 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7865 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7866 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7867 }
7868
7869 #[test]
7870 fn visual_exit_marks_use_lower_higher_order() {
7871 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7875 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7877 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7879 let lt = e.mark('<').unwrap();
7880 let gt = e.mark('>').unwrap();
7881 assert_eq!(lt.0, 2);
7882 assert_eq!(gt.0, 3);
7883 }
7884
7885 #[test]
7886 fn visualline_exit_marks_snap_to_line_edges() {
7887 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7889 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7891 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7893 let lt = e.mark('<').unwrap();
7894 let gt = e.mark('>').unwrap();
7895 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7896 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7898 }
7899
7900 #[test]
7901 fn visualblock_exit_marks_use_block_corners() {
7902 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7906 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7908 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7911 let lt = e.mark('<').unwrap();
7912 let gt = e.mark('>').unwrap();
7913 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7915 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7916 }
7917
7918 #[test]
7919 fn visual_block_delete_removes_column_range() {
7920 let mut e = editor_with("hello\nworld\nhappy");
7921 run_keys(&mut e, "l");
7923 run_keys(&mut e, "<C-v>");
7924 run_keys(&mut e, "jj");
7925 run_keys(&mut e, "ll");
7926 run_keys(&mut e, "d");
7927 assert_eq!(
7929 e.buffer().lines(),
7930 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7931 );
7932 }
7933
7934 #[test]
7935 fn visual_block_yank_joins_with_newlines() {
7936 let mut e = editor_with("hello\nworld\nhappy");
7937 run_keys(&mut e, "<C-v>");
7938 run_keys(&mut e, "jj");
7939 run_keys(&mut e, "ll");
7940 run_keys(&mut e, "y");
7941 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7942 }
7943
7944 #[test]
7945 fn visual_block_replace_fills_block() {
7946 let mut e = editor_with("hello\nworld\nhappy");
7947 run_keys(&mut e, "<C-v>");
7948 run_keys(&mut e, "jj");
7949 run_keys(&mut e, "ll");
7950 run_keys(&mut e, "rx");
7951 assert_eq!(
7952 e.buffer().lines(),
7953 &[
7954 "xxxlo".to_string(),
7955 "xxxld".to_string(),
7956 "xxxpy".to_string()
7957 ]
7958 );
7959 }
7960
7961 #[test]
7962 fn visual_block_insert_repeats_across_rows() {
7963 let mut e = editor_with("hello\nworld\nhappy");
7964 run_keys(&mut e, "<C-v>");
7965 run_keys(&mut e, "jj");
7966 run_keys(&mut e, "I");
7967 run_keys(&mut e, "# <Esc>");
7968 assert_eq!(
7969 e.buffer().lines(),
7970 &[
7971 "# hello".to_string(),
7972 "# world".to_string(),
7973 "# happy".to_string()
7974 ]
7975 );
7976 }
7977
7978 #[test]
7979 fn block_highlight_returns_none_outside_block_mode() {
7980 let mut e = editor_with("abc");
7981 assert!(e.block_highlight().is_none());
7982 run_keys(&mut e, "v");
7983 assert!(e.block_highlight().is_none());
7984 run_keys(&mut e, "<Esc>V");
7985 assert!(e.block_highlight().is_none());
7986 }
7987
7988 #[test]
7989 fn block_highlight_bounds_track_anchor_and_cursor() {
7990 let mut e = editor_with("aaaa\nbbbb\ncccc");
7991 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7993 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7996 }
7997
7998 #[test]
7999 fn visual_block_delete_handles_short_lines() {
8000 let mut e = editor_with("hello\nhi\nworld");
8002 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8004 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8006 assert_eq!(
8011 e.buffer().lines(),
8012 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8013 );
8014 }
8015
8016 #[test]
8017 fn visual_block_yank_pads_short_lines_with_empties() {
8018 let mut e = editor_with("hello\nhi\nworld");
8019 run_keys(&mut e, "l");
8020 run_keys(&mut e, "<C-v>");
8021 run_keys(&mut e, "jjll");
8022 run_keys(&mut e, "y");
8023 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8025 }
8026
8027 #[test]
8028 fn visual_block_replace_skips_past_eol() {
8029 let mut e = editor_with("ab\ncd\nef");
8032 run_keys(&mut e, "l");
8034 run_keys(&mut e, "<C-v>");
8035 run_keys(&mut e, "jjllllll");
8036 run_keys(&mut e, "rX");
8037 assert_eq!(
8040 e.buffer().lines(),
8041 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8042 );
8043 }
8044
8045 #[test]
8046 fn visual_block_with_empty_line_in_middle() {
8047 let mut e = editor_with("abcd\n\nefgh");
8048 run_keys(&mut e, "<C-v>");
8049 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8051 assert_eq!(
8054 e.buffer().lines(),
8055 &["d".to_string(), "".to_string(), "h".to_string()]
8056 );
8057 }
8058
8059 #[test]
8060 fn block_insert_pads_empty_lines_to_block_column() {
8061 let mut e = editor_with("this is a line\n\nthis is a line");
8064 e.jump_cursor(0, 3);
8065 run_keys(&mut e, "<C-v>");
8066 run_keys(&mut e, "jj");
8067 run_keys(&mut e, "I");
8068 run_keys(&mut e, "XX<Esc>");
8069 assert_eq!(
8070 e.buffer().lines(),
8071 &[
8072 "thiXXs is a line".to_string(),
8073 " XX".to_string(),
8074 "thiXXs is a line".to_string()
8075 ]
8076 );
8077 }
8078
8079 #[test]
8080 fn block_insert_pads_short_lines_to_block_column() {
8081 let mut e = editor_with("aaaaa\nbb\naaaaa");
8082 e.jump_cursor(0, 3);
8083 run_keys(&mut e, "<C-v>");
8084 run_keys(&mut e, "jj");
8085 run_keys(&mut e, "I");
8086 run_keys(&mut e, "Y<Esc>");
8087 assert_eq!(
8089 e.buffer().lines(),
8090 &[
8091 "aaaYaa".to_string(),
8092 "bb Y".to_string(),
8093 "aaaYaa".to_string()
8094 ]
8095 );
8096 }
8097
8098 #[test]
8099 fn visual_block_append_repeats_across_rows() {
8100 let mut e = editor_with("foo\nbar\nbaz");
8101 run_keys(&mut e, "<C-v>");
8102 run_keys(&mut e, "jj");
8103 run_keys(&mut e, "A");
8106 run_keys(&mut e, "!<Esc>");
8107 assert_eq!(
8108 e.buffer().lines(),
8109 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8110 );
8111 }
8112
8113 #[test]
8116 fn slash_opens_forward_search_prompt() {
8117 let mut e = editor_with("hello world");
8118 run_keys(&mut e, "/");
8119 let p = e.search_prompt().expect("prompt should be active");
8120 assert!(p.text.is_empty());
8121 assert!(p.forward);
8122 }
8123
8124 #[test]
8125 fn question_opens_backward_search_prompt() {
8126 let mut e = editor_with("hello world");
8127 run_keys(&mut e, "?");
8128 let p = e.search_prompt().expect("prompt should be active");
8129 assert!(!p.forward);
8130 }
8131
8132 #[test]
8133 fn search_prompt_typing_updates_pattern_live() {
8134 let mut e = editor_with("foo bar\nbaz");
8135 run_keys(&mut e, "/bar");
8136 assert_eq!(e.search_prompt().unwrap().text, "bar");
8137 assert!(e.search_state().pattern.is_some());
8139 }
8140
8141 #[test]
8142 fn search_prompt_backspace_and_enter() {
8143 let mut e = editor_with("hello world\nagain");
8144 run_keys(&mut e, "/worlx");
8145 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8146 assert_eq!(e.search_prompt().unwrap().text, "worl");
8147 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8148 assert!(e.search_prompt().is_none());
8150 assert_eq!(e.last_search(), Some("worl"));
8151 assert_eq!(e.cursor(), (0, 6));
8152 }
8153
8154 #[test]
8155 fn empty_search_prompt_enter_repeats_last_search() {
8156 let mut e = editor_with("foo bar foo baz foo");
8157 run_keys(&mut e, "/foo");
8158 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8159 assert_eq!(e.cursor().1, 8);
8160 run_keys(&mut e, "/");
8162 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8163 assert_eq!(e.cursor().1, 16);
8164 assert_eq!(e.last_search(), Some("foo"));
8165 }
8166
8167 #[test]
8168 fn search_history_records_committed_patterns() {
8169 let mut e = editor_with("alpha beta gamma");
8170 run_keys(&mut e, "/alpha<CR>");
8171 run_keys(&mut e, "/beta<CR>");
8172 let history = e.vim.search_history.clone();
8174 assert_eq!(history, vec!["alpha", "beta"]);
8175 }
8176
8177 #[test]
8178 fn search_history_dedupes_consecutive_repeats() {
8179 let mut e = editor_with("foo bar foo");
8180 run_keys(&mut e, "/foo<CR>");
8181 run_keys(&mut e, "/foo<CR>");
8182 run_keys(&mut e, "/bar<CR>");
8183 run_keys(&mut e, "/bar<CR>");
8184 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8186 }
8187
8188 #[test]
8189 fn ctrl_p_walks_history_backward() {
8190 let mut e = editor_with("alpha beta gamma");
8191 run_keys(&mut e, "/alpha<CR>");
8192 run_keys(&mut e, "/beta<CR>");
8193 run_keys(&mut e, "/");
8195 assert_eq!(e.search_prompt().unwrap().text, "");
8196 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8197 assert_eq!(e.search_prompt().unwrap().text, "beta");
8198 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8199 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8200 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8202 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8203 }
8204
8205 #[test]
8206 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8207 let mut e = editor_with("a b c");
8208 run_keys(&mut e, "/a<CR>");
8209 run_keys(&mut e, "/b<CR>");
8210 run_keys(&mut e, "/c<CR>");
8211 run_keys(&mut e, "/");
8212 for _ in 0..3 {
8214 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8215 }
8216 assert_eq!(e.search_prompt().unwrap().text, "a");
8217 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8218 assert_eq!(e.search_prompt().unwrap().text, "b");
8219 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8220 assert_eq!(e.search_prompt().unwrap().text, "c");
8221 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8223 assert_eq!(e.search_prompt().unwrap().text, "c");
8224 }
8225
8226 #[test]
8227 fn typing_after_history_walk_resets_cursor() {
8228 let mut e = editor_with("foo");
8229 run_keys(&mut e, "/foo<CR>");
8230 run_keys(&mut e, "/");
8231 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8232 assert_eq!(e.search_prompt().unwrap().text, "foo");
8233 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8236 assert_eq!(e.search_prompt().unwrap().text, "foox");
8237 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8238 assert_eq!(e.search_prompt().unwrap().text, "foo");
8239 }
8240
8241 #[test]
8242 fn empty_backward_search_prompt_enter_repeats_last_search() {
8243 let mut e = editor_with("foo bar foo baz foo");
8244 run_keys(&mut e, "/foo");
8246 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8247 assert_eq!(e.cursor().1, 8);
8248 run_keys(&mut e, "?");
8249 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8250 assert_eq!(e.cursor().1, 0);
8251 assert_eq!(e.last_search(), Some("foo"));
8252 }
8253
8254 #[test]
8255 fn search_prompt_esc_cancels_but_keeps_last_search() {
8256 let mut e = editor_with("foo bar\nbaz");
8257 run_keys(&mut e, "/bar");
8258 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8259 assert!(e.search_prompt().is_none());
8260 assert_eq!(e.last_search(), Some("bar"));
8261 }
8262
8263 #[test]
8264 fn search_then_n_and_shift_n_navigate() {
8265 let mut e = editor_with("foo bar foo baz foo");
8266 run_keys(&mut e, "/foo");
8267 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8268 assert_eq!(e.cursor().1, 8);
8270 run_keys(&mut e, "n");
8271 assert_eq!(e.cursor().1, 16);
8272 run_keys(&mut e, "N");
8273 assert_eq!(e.cursor().1, 8);
8274 }
8275
8276 #[test]
8277 fn question_mark_searches_backward_on_enter() {
8278 let mut e = editor_with("foo bar foo baz");
8279 e.jump_cursor(0, 10);
8280 run_keys(&mut e, "?foo");
8281 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8282 assert_eq!(e.cursor(), (0, 8));
8284 }
8285
8286 #[test]
8289 fn big_y_yanks_to_end_of_line() {
8290 let mut e = editor_with("hello world");
8291 e.jump_cursor(0, 6);
8292 run_keys(&mut e, "Y");
8293 assert_eq!(e.last_yank.as_deref(), Some("world"));
8294 }
8295
8296 #[test]
8297 fn big_y_from_line_start_yanks_full_line() {
8298 let mut e = editor_with("hello world");
8299 run_keys(&mut e, "Y");
8300 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8301 }
8302
8303 #[test]
8304 fn gj_joins_without_inserting_space() {
8305 let mut e = editor_with("hello\n world");
8306 run_keys(&mut e, "gJ");
8307 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8309 }
8310
8311 #[test]
8312 fn gj_noop_on_last_line() {
8313 let mut e = editor_with("only");
8314 run_keys(&mut e, "gJ");
8315 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8316 }
8317
8318 #[test]
8319 fn ge_jumps_to_previous_word_end() {
8320 let mut e = editor_with("foo bar baz");
8321 e.jump_cursor(0, 5);
8322 run_keys(&mut e, "ge");
8323 assert_eq!(e.cursor(), (0, 2));
8324 }
8325
8326 #[test]
8327 fn ge_respects_word_class() {
8328 let mut e = editor_with("foo-bar baz");
8331 e.jump_cursor(0, 5);
8332 run_keys(&mut e, "ge");
8333 assert_eq!(e.cursor(), (0, 3));
8334 }
8335
8336 #[test]
8337 fn big_ge_treats_hyphens_as_part_of_word() {
8338 let mut e = editor_with("foo-bar baz");
8341 e.jump_cursor(0, 10);
8342 run_keys(&mut e, "gE");
8343 assert_eq!(e.cursor(), (0, 6));
8344 }
8345
8346 #[test]
8347 fn ge_crosses_line_boundary() {
8348 let mut e = editor_with("foo\nbar");
8349 e.jump_cursor(1, 0);
8350 run_keys(&mut e, "ge");
8351 assert_eq!(e.cursor(), (0, 2));
8352 }
8353
8354 #[test]
8355 fn dge_deletes_to_end_of_previous_word() {
8356 let mut e = editor_with("foo bar baz");
8357 e.jump_cursor(0, 8);
8358 run_keys(&mut e, "dge");
8361 assert_eq!(e.buffer().lines()[0], "foo baaz");
8362 }
8363
8364 #[test]
8365 fn ctrl_scroll_keys_do_not_panic() {
8366 let mut e = editor_with(
8369 (0..50)
8370 .map(|i| format!("line{i}"))
8371 .collect::<Vec<_>>()
8372 .join("\n")
8373 .as_str(),
8374 );
8375 run_keys(&mut e, "<C-f>");
8376 run_keys(&mut e, "<C-b>");
8377 assert!(!e.buffer().lines().is_empty());
8379 }
8380
8381 #[test]
8388 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8389 let mut e = Editor::new(
8390 hjkl_buffer::Buffer::new(),
8391 crate::types::DefaultHost::new(),
8392 crate::types::Options::default(),
8393 );
8394 e.set_content("row0\nrow1\nrow2");
8395 run_keys(&mut e, "3iX<Down><Esc>");
8397 assert!(e.buffer().lines()[0].contains('X'));
8399 assert!(
8402 !e.buffer().lines()[1].contains("row0"),
8403 "row1 leaked row0 contents: {:?}",
8404 e.buffer().lines()[1]
8405 );
8406 assert_eq!(e.buffer().lines().len(), 3);
8409 }
8410
8411 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8414 let mut e = Editor::new(
8415 hjkl_buffer::Buffer::new(),
8416 crate::types::DefaultHost::new(),
8417 crate::types::Options::default(),
8418 );
8419 let body = (0..n)
8420 .map(|i| format!(" line{}", i))
8421 .collect::<Vec<_>>()
8422 .join("\n");
8423 e.set_content(&body);
8424 e.set_viewport_height(viewport);
8425 e
8426 }
8427
8428 #[test]
8429 fn ctrl_d_moves_cursor_half_page_down() {
8430 let mut e = editor_with_rows(100, 20);
8431 run_keys(&mut e, "<C-d>");
8432 assert_eq!(e.cursor().0, 10);
8433 }
8434
8435 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8436 let mut e = Editor::new(
8437 hjkl_buffer::Buffer::new(),
8438 crate::types::DefaultHost::new(),
8439 crate::types::Options::default(),
8440 );
8441 e.set_content(&lines.join("\n"));
8442 e.set_viewport_height(viewport);
8443 let v = e.host_mut().viewport_mut();
8444 v.height = viewport;
8445 v.width = text_width;
8446 v.text_width = text_width;
8447 v.wrap = hjkl_buffer::Wrap::Char;
8448 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8449 e
8450 }
8451
8452 #[test]
8453 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8454 let lines = ["aaaabbbbcccc"; 10];
8458 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8459 e.jump_cursor(4, 0);
8460 e.ensure_cursor_in_scrolloff();
8461 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8462 assert!(csr <= 6, "csr={csr}");
8463 }
8464
8465 #[test]
8466 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8467 let lines = ["aaaabbbbcccc"; 10];
8468 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8469 e.jump_cursor(7, 0);
8472 e.ensure_cursor_in_scrolloff();
8473 e.jump_cursor(2, 0);
8474 e.ensure_cursor_in_scrolloff();
8475 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8476 assert!(csr >= 5, "csr={csr}");
8478 }
8479
8480 #[test]
8481 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8482 let lines = ["aaaabbbbcccc"; 5];
8483 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8484 e.jump_cursor(4, 11);
8485 e.ensure_cursor_in_scrolloff();
8486 let top = e.host().viewport().top_row;
8491 assert_eq!(top, 1);
8492 }
8493
8494 #[test]
8495 fn ctrl_u_moves_cursor_half_page_up() {
8496 let mut e = editor_with_rows(100, 20);
8497 e.jump_cursor(50, 0);
8498 run_keys(&mut e, "<C-u>");
8499 assert_eq!(e.cursor().0, 40);
8500 }
8501
8502 #[test]
8503 fn ctrl_f_moves_cursor_full_page_down() {
8504 let mut e = editor_with_rows(100, 20);
8505 run_keys(&mut e, "<C-f>");
8506 assert_eq!(e.cursor().0, 18);
8508 }
8509
8510 #[test]
8511 fn ctrl_b_moves_cursor_full_page_up() {
8512 let mut e = editor_with_rows(100, 20);
8513 e.jump_cursor(50, 0);
8514 run_keys(&mut e, "<C-b>");
8515 assert_eq!(e.cursor().0, 32);
8516 }
8517
8518 #[test]
8519 fn ctrl_d_lands_on_first_non_blank() {
8520 let mut e = editor_with_rows(100, 20);
8521 run_keys(&mut e, "<C-d>");
8522 assert_eq!(e.cursor().1, 2);
8524 }
8525
8526 #[test]
8527 fn ctrl_d_clamps_at_end_of_buffer() {
8528 let mut e = editor_with_rows(5, 20);
8529 run_keys(&mut e, "<C-d>");
8530 assert_eq!(e.cursor().0, 4);
8531 }
8532
8533 #[test]
8534 fn capital_h_jumps_to_viewport_top() {
8535 let mut e = editor_with_rows(100, 10);
8536 e.jump_cursor(50, 0);
8537 e.set_viewport_top(45);
8538 let top = e.host().viewport().top_row;
8539 run_keys(&mut e, "H");
8540 assert_eq!(e.cursor().0, top);
8541 assert_eq!(e.cursor().1, 2);
8542 }
8543
8544 #[test]
8545 fn capital_l_jumps_to_viewport_bottom() {
8546 let mut e = editor_with_rows(100, 10);
8547 e.jump_cursor(50, 0);
8548 e.set_viewport_top(45);
8549 let top = e.host().viewport().top_row;
8550 run_keys(&mut e, "L");
8551 assert_eq!(e.cursor().0, top + 9);
8552 }
8553
8554 #[test]
8555 fn capital_m_jumps_to_viewport_middle() {
8556 let mut e = editor_with_rows(100, 10);
8557 e.jump_cursor(50, 0);
8558 e.set_viewport_top(45);
8559 let top = e.host().viewport().top_row;
8560 run_keys(&mut e, "M");
8561 assert_eq!(e.cursor().0, top + 4);
8563 }
8564
8565 #[test]
8566 fn g_capital_m_lands_at_line_midpoint() {
8567 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8569 assert_eq!(e.cursor(), (0, 6));
8571 }
8572
8573 #[test]
8574 fn g_capital_m_on_empty_line_stays_at_zero() {
8575 let mut e = editor_with("");
8576 run_keys(&mut e, "gM");
8577 assert_eq!(e.cursor(), (0, 0));
8578 }
8579
8580 #[test]
8581 fn g_capital_m_uses_current_line_only() {
8582 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8585 run_keys(&mut e, "gM");
8586 assert_eq!(e.cursor(), (1, 6));
8587 }
8588
8589 #[test]
8590 fn capital_h_count_offsets_from_top() {
8591 let mut e = editor_with_rows(100, 10);
8592 e.jump_cursor(50, 0);
8593 e.set_viewport_top(45);
8594 let top = e.host().viewport().top_row;
8595 run_keys(&mut e, "3H");
8596 assert_eq!(e.cursor().0, top + 2);
8597 }
8598
8599 #[test]
8602 fn ctrl_o_returns_to_pre_g_position() {
8603 let mut e = editor_with_rows(50, 20);
8604 e.jump_cursor(5, 2);
8605 run_keys(&mut e, "G");
8606 assert_eq!(e.cursor().0, 49);
8607 run_keys(&mut e, "<C-o>");
8608 assert_eq!(e.cursor(), (5, 2));
8609 }
8610
8611 #[test]
8612 fn ctrl_i_redoes_jump_after_ctrl_o() {
8613 let mut e = editor_with_rows(50, 20);
8614 e.jump_cursor(5, 2);
8615 run_keys(&mut e, "G");
8616 let post = e.cursor();
8617 run_keys(&mut e, "<C-o>");
8618 run_keys(&mut e, "<C-i>");
8619 assert_eq!(e.cursor(), post);
8620 }
8621
8622 #[test]
8623 fn new_jump_clears_forward_stack() {
8624 let mut e = editor_with_rows(50, 20);
8625 e.jump_cursor(5, 2);
8626 run_keys(&mut e, "G");
8627 run_keys(&mut e, "<C-o>");
8628 run_keys(&mut e, "gg");
8629 run_keys(&mut e, "<C-i>");
8630 assert_eq!(e.cursor().0, 0);
8631 }
8632
8633 #[test]
8634 fn ctrl_o_on_empty_stack_is_noop() {
8635 let mut e = editor_with_rows(10, 20);
8636 e.jump_cursor(3, 1);
8637 run_keys(&mut e, "<C-o>");
8638 assert_eq!(e.cursor(), (3, 1));
8639 }
8640
8641 #[test]
8642 fn asterisk_search_pushes_jump() {
8643 let mut e = editor_with("foo bar\nbaz foo end");
8644 e.jump_cursor(0, 0);
8645 run_keys(&mut e, "*");
8646 let after = e.cursor();
8647 assert_ne!(after, (0, 0));
8648 run_keys(&mut e, "<C-o>");
8649 assert_eq!(e.cursor(), (0, 0));
8650 }
8651
8652 #[test]
8653 fn h_viewport_jump_is_recorded() {
8654 let mut e = editor_with_rows(100, 10);
8655 e.jump_cursor(50, 0);
8656 e.set_viewport_top(45);
8657 let pre = e.cursor();
8658 run_keys(&mut e, "H");
8659 assert_ne!(e.cursor(), pre);
8660 run_keys(&mut e, "<C-o>");
8661 assert_eq!(e.cursor(), pre);
8662 }
8663
8664 #[test]
8665 fn j_k_motion_does_not_push_jump() {
8666 let mut e = editor_with_rows(50, 20);
8667 e.jump_cursor(5, 0);
8668 run_keys(&mut e, "jjj");
8669 run_keys(&mut e, "<C-o>");
8670 assert_eq!(e.cursor().0, 8);
8671 }
8672
8673 #[test]
8674 fn jumplist_caps_at_100() {
8675 let mut e = editor_with_rows(200, 20);
8676 for i in 0..101 {
8677 e.jump_cursor(i, 0);
8678 run_keys(&mut e, "G");
8679 }
8680 assert!(e.vim.jump_back.len() <= 100);
8681 }
8682
8683 #[test]
8684 fn tab_acts_as_ctrl_i() {
8685 let mut e = editor_with_rows(50, 20);
8686 e.jump_cursor(5, 2);
8687 run_keys(&mut e, "G");
8688 let post = e.cursor();
8689 run_keys(&mut e, "<C-o>");
8690 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8691 assert_eq!(e.cursor(), post);
8692 }
8693
8694 #[test]
8697 fn ma_then_backtick_a_jumps_exact() {
8698 let mut e = editor_with_rows(50, 20);
8699 e.jump_cursor(5, 3);
8700 run_keys(&mut e, "ma");
8701 e.jump_cursor(20, 0);
8702 run_keys(&mut e, "`a");
8703 assert_eq!(e.cursor(), (5, 3));
8704 }
8705
8706 #[test]
8707 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8708 let mut e = editor_with_rows(50, 20);
8709 e.jump_cursor(5, 6);
8711 run_keys(&mut e, "ma");
8712 e.jump_cursor(30, 4);
8713 run_keys(&mut e, "'a");
8714 assert_eq!(e.cursor(), (5, 2));
8715 }
8716
8717 #[test]
8718 fn goto_mark_pushes_jumplist() {
8719 let mut e = editor_with_rows(50, 20);
8720 e.jump_cursor(10, 2);
8721 run_keys(&mut e, "mz");
8722 e.jump_cursor(3, 0);
8723 run_keys(&mut e, "`z");
8724 assert_eq!(e.cursor(), (10, 2));
8725 run_keys(&mut e, "<C-o>");
8726 assert_eq!(e.cursor(), (3, 0));
8727 }
8728
8729 #[test]
8730 fn goto_missing_mark_is_noop() {
8731 let mut e = editor_with_rows(50, 20);
8732 e.jump_cursor(3, 1);
8733 run_keys(&mut e, "`q");
8734 assert_eq!(e.cursor(), (3, 1));
8735 }
8736
8737 #[test]
8738 fn uppercase_mark_stored_under_uppercase_key() {
8739 let mut e = editor_with_rows(50, 20);
8740 e.jump_cursor(5, 3);
8741 run_keys(&mut e, "mA");
8742 assert_eq!(e.mark('A'), Some((5, 3)));
8745 assert!(e.mark('a').is_none());
8746 }
8747
8748 #[test]
8749 fn mark_survives_document_shrink_via_clamp() {
8750 let mut e = editor_with_rows(50, 20);
8751 e.jump_cursor(40, 4);
8752 run_keys(&mut e, "mx");
8753 e.set_content("a\nb\nc\nd\ne");
8755 run_keys(&mut e, "`x");
8756 let (r, _) = e.cursor();
8758 assert!(r <= 4);
8759 }
8760
8761 #[test]
8762 fn g_semicolon_walks_back_through_edits() {
8763 let mut e = editor_with("alpha\nbeta\ngamma");
8764 e.jump_cursor(0, 0);
8767 run_keys(&mut e, "iX<Esc>");
8768 e.jump_cursor(2, 0);
8769 run_keys(&mut e, "iY<Esc>");
8770 run_keys(&mut e, "g;");
8772 assert_eq!(e.cursor(), (2, 1));
8773 run_keys(&mut e, "g;");
8775 assert_eq!(e.cursor(), (0, 1));
8776 run_keys(&mut e, "g;");
8778 assert_eq!(e.cursor(), (0, 1));
8779 }
8780
8781 #[test]
8782 fn g_comma_walks_forward_after_g_semicolon() {
8783 let mut e = editor_with("a\nb\nc");
8784 e.jump_cursor(0, 0);
8785 run_keys(&mut e, "iX<Esc>");
8786 e.jump_cursor(2, 0);
8787 run_keys(&mut e, "iY<Esc>");
8788 run_keys(&mut e, "g;");
8789 run_keys(&mut e, "g;");
8790 assert_eq!(e.cursor(), (0, 1));
8791 run_keys(&mut e, "g,");
8792 assert_eq!(e.cursor(), (2, 1));
8793 }
8794
8795 #[test]
8796 fn new_edit_during_walk_trims_forward_entries() {
8797 let mut e = editor_with("a\nb\nc\nd");
8798 e.jump_cursor(0, 0);
8799 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8801 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8804 run_keys(&mut e, "g;");
8805 assert_eq!(e.cursor(), (0, 1));
8806 run_keys(&mut e, "iZ<Esc>");
8808 run_keys(&mut e, "g,");
8810 assert_ne!(e.cursor(), (2, 1));
8812 }
8813
8814 #[test]
8820 fn capital_mark_set_and_jump() {
8821 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8822 e.jump_cursor(2, 1);
8823 run_keys(&mut e, "mA");
8824 e.jump_cursor(0, 0);
8826 run_keys(&mut e, "'A");
8828 assert_eq!(e.cursor().0, 2);
8830 }
8831
8832 #[test]
8833 fn capital_mark_survives_set_content() {
8834 let mut e = editor_with("first buffer line\nsecond");
8835 e.jump_cursor(1, 3);
8836 run_keys(&mut e, "mA");
8837 e.set_content("totally different content\non many\nrows of text");
8839 e.jump_cursor(0, 0);
8841 run_keys(&mut e, "'A");
8842 assert_eq!(e.cursor().0, 1);
8843 }
8844
8845 #[test]
8850 fn capital_mark_shifts_with_edit() {
8851 let mut e = editor_with("a\nb\nc\nd");
8852 e.jump_cursor(3, 0);
8853 run_keys(&mut e, "mA");
8854 e.jump_cursor(0, 0);
8856 run_keys(&mut e, "dd");
8857 e.jump_cursor(0, 0);
8858 run_keys(&mut e, "'A");
8859 assert_eq!(e.cursor().0, 2);
8860 }
8861
8862 #[test]
8863 fn mark_below_delete_shifts_up() {
8864 let mut e = editor_with("a\nb\nc\nd\ne");
8865 e.jump_cursor(3, 0);
8867 run_keys(&mut e, "ma");
8868 e.jump_cursor(0, 0);
8870 run_keys(&mut e, "dd");
8871 e.jump_cursor(0, 0);
8873 run_keys(&mut e, "'a");
8874 assert_eq!(e.cursor().0, 2);
8875 assert_eq!(e.buffer().line(2).unwrap(), "d");
8876 }
8877
8878 #[test]
8879 fn mark_on_deleted_row_is_dropped() {
8880 let mut e = editor_with("a\nb\nc\nd");
8881 e.jump_cursor(1, 0);
8883 run_keys(&mut e, "ma");
8884 run_keys(&mut e, "dd");
8886 e.jump_cursor(2, 0);
8888 run_keys(&mut e, "'a");
8889 assert_eq!(e.cursor().0, 2);
8891 }
8892
8893 #[test]
8894 fn mark_above_edit_unchanged() {
8895 let mut e = editor_with("a\nb\nc\nd\ne");
8896 e.jump_cursor(0, 0);
8898 run_keys(&mut e, "ma");
8899 e.jump_cursor(3, 0);
8901 run_keys(&mut e, "dd");
8902 e.jump_cursor(2, 0);
8904 run_keys(&mut e, "'a");
8905 assert_eq!(e.cursor().0, 0);
8906 }
8907
8908 #[test]
8909 fn mark_shifts_down_after_insert() {
8910 let mut e = editor_with("a\nb\nc");
8911 e.jump_cursor(2, 0);
8913 run_keys(&mut e, "ma");
8914 e.jump_cursor(0, 0);
8916 run_keys(&mut e, "Onew<Esc>");
8917 e.jump_cursor(0, 0);
8920 run_keys(&mut e, "'a");
8921 assert_eq!(e.cursor().0, 3);
8922 assert_eq!(e.buffer().line(3).unwrap(), "c");
8923 }
8924
8925 #[test]
8928 fn forward_search_commit_pushes_jump() {
8929 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8930 e.jump_cursor(0, 0);
8931 run_keys(&mut e, "/target<CR>");
8932 assert_ne!(e.cursor(), (0, 0));
8934 run_keys(&mut e, "<C-o>");
8936 assert_eq!(e.cursor(), (0, 0));
8937 }
8938
8939 #[test]
8940 fn search_commit_no_match_does_not_push_jump() {
8941 let mut e = editor_with("alpha beta\nfoo end");
8942 e.jump_cursor(0, 3);
8943 let pre_len = e.vim.jump_back.len();
8944 run_keys(&mut e, "/zzznotfound<CR>");
8945 assert_eq!(e.vim.jump_back.len(), pre_len);
8947 }
8948
8949 #[test]
8952 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8953 let mut e = editor_with("hello world");
8954 run_keys(&mut e, "lll");
8955 let (row, col) = e.cursor();
8956 assert_eq!(e.buffer.cursor().row, row);
8957 assert_eq!(e.buffer.cursor().col, col);
8958 }
8959
8960 #[test]
8961 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8962 let mut e = editor_with("aaaa\nbbbb\ncccc");
8963 run_keys(&mut e, "jj");
8964 let (row, col) = e.cursor();
8965 assert_eq!(e.buffer.cursor().row, row);
8966 assert_eq!(e.buffer.cursor().col, col);
8967 }
8968
8969 #[test]
8970 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8971 let mut e = editor_with("foo bar baz");
8972 run_keys(&mut e, "ww");
8973 let (row, col) = e.cursor();
8974 assert_eq!(e.buffer.cursor().row, row);
8975 assert_eq!(e.buffer.cursor().col, col);
8976 }
8977
8978 #[test]
8979 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8980 let mut e = editor_with("a\nb\nc\nd\ne");
8981 run_keys(&mut e, "G");
8982 let (row, col) = e.cursor();
8983 assert_eq!(e.buffer.cursor().row, row);
8984 assert_eq!(e.buffer.cursor().col, col);
8985 }
8986
8987 #[test]
8988 fn editor_sticky_col_tracks_horizontal_motion() {
8989 let mut e = editor_with("longline\nhi\nlongline");
8990 run_keys(&mut e, "fl");
8995 let landed = e.cursor().1;
8996 assert!(landed > 0, "fl should have moved");
8997 run_keys(&mut e, "j");
8998 assert_eq!(e.sticky_col(), Some(landed));
9001 }
9002
9003 #[test]
9004 fn buffer_content_mirrors_textarea_after_insert() {
9005 let mut e = editor_with("hello");
9006 run_keys(&mut e, "iXYZ<Esc>");
9007 let text = e.buffer().lines().join("\n");
9008 assert_eq!(e.buffer.as_string(), text);
9009 }
9010
9011 #[test]
9012 fn buffer_content_mirrors_textarea_after_delete() {
9013 let mut e = editor_with("alpha bravo charlie");
9014 run_keys(&mut e, "dw");
9015 let text = e.buffer().lines().join("\n");
9016 assert_eq!(e.buffer.as_string(), text);
9017 }
9018
9019 #[test]
9020 fn buffer_content_mirrors_textarea_after_dd() {
9021 let mut e = editor_with("a\nb\nc\nd");
9022 run_keys(&mut e, "jdd");
9023 let text = e.buffer().lines().join("\n");
9024 assert_eq!(e.buffer.as_string(), text);
9025 }
9026
9027 #[test]
9028 fn buffer_content_mirrors_textarea_after_open_line() {
9029 let mut e = editor_with("foo\nbar");
9030 run_keys(&mut e, "oNEW<Esc>");
9031 let text = e.buffer().lines().join("\n");
9032 assert_eq!(e.buffer.as_string(), text);
9033 }
9034
9035 #[test]
9036 fn buffer_content_mirrors_textarea_after_paste() {
9037 let mut e = editor_with("hello");
9038 run_keys(&mut e, "yy");
9039 run_keys(&mut e, "p");
9040 let text = e.buffer().lines().join("\n");
9041 assert_eq!(e.buffer.as_string(), text);
9042 }
9043
9044 #[test]
9045 fn buffer_selection_none_in_normal_mode() {
9046 let e = editor_with("foo bar");
9047 assert!(e.buffer_selection().is_none());
9048 }
9049
9050 #[test]
9051 fn buffer_selection_char_in_visual_mode() {
9052 use hjkl_buffer::{Position, Selection};
9053 let mut e = editor_with("hello world");
9054 run_keys(&mut e, "vlll");
9055 assert_eq!(
9056 e.buffer_selection(),
9057 Some(Selection::Char {
9058 anchor: Position::new(0, 0),
9059 head: Position::new(0, 3),
9060 })
9061 );
9062 }
9063
9064 #[test]
9065 fn buffer_selection_line_in_visual_line_mode() {
9066 use hjkl_buffer::Selection;
9067 let mut e = editor_with("a\nb\nc\nd");
9068 run_keys(&mut e, "Vj");
9069 assert_eq!(
9070 e.buffer_selection(),
9071 Some(Selection::Line {
9072 anchor_row: 0,
9073 head_row: 1,
9074 })
9075 );
9076 }
9077
9078 #[test]
9079 fn wrapscan_off_blocks_wrap_around() {
9080 let mut e = editor_with("first\nsecond\nthird\n");
9081 e.settings_mut().wrapscan = false;
9082 e.jump_cursor(2, 0);
9084 run_keys(&mut e, "/first<CR>");
9085 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9087 e.settings_mut().wrapscan = true;
9089 run_keys(&mut e, "/first<CR>");
9090 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9091 }
9092
9093 #[test]
9094 fn smartcase_uppercase_pattern_stays_sensitive() {
9095 let mut e = editor_with("foo\nFoo\nBAR\n");
9096 e.settings_mut().ignore_case = true;
9097 e.settings_mut().smartcase = true;
9098 run_keys(&mut e, "/foo<CR>");
9101 let r1 = e
9102 .search_state()
9103 .pattern
9104 .as_ref()
9105 .unwrap()
9106 .as_str()
9107 .to_string();
9108 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9109 run_keys(&mut e, "/Foo<CR>");
9111 let r2 = e
9112 .search_state()
9113 .pattern
9114 .as_ref()
9115 .unwrap()
9116 .as_str()
9117 .to_string();
9118 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9119 }
9120
9121 #[test]
9122 fn enter_with_autoindent_copies_leading_whitespace() {
9123 let mut e = editor_with(" foo");
9124 e.jump_cursor(0, 7);
9125 run_keys(&mut e, "i<CR>");
9126 assert_eq!(e.buffer.line(1).unwrap(), " ");
9127 }
9128
9129 #[test]
9130 fn enter_without_autoindent_inserts_bare_newline() {
9131 let mut e = editor_with(" foo");
9132 e.settings_mut().autoindent = false;
9133 e.jump_cursor(0, 7);
9134 run_keys(&mut e, "i<CR>");
9135 assert_eq!(e.buffer.line(1).unwrap(), "");
9136 }
9137
9138 #[test]
9139 fn iskeyword_default_treats_alnum_underscore_as_word() {
9140 let mut e = editor_with("foo_bar baz");
9141 e.jump_cursor(0, 0);
9145 run_keys(&mut e, "*");
9146 let p = e
9147 .search_state()
9148 .pattern
9149 .as_ref()
9150 .unwrap()
9151 .as_str()
9152 .to_string();
9153 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9154 }
9155
9156 #[test]
9157 fn w_motion_respects_custom_iskeyword() {
9158 let mut e = editor_with("foo-bar baz");
9162 run_keys(&mut e, "w");
9163 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9164 let mut e2 = editor_with("foo-bar baz");
9167 e2.set_iskeyword("@,_,45");
9168 run_keys(&mut e2, "w");
9169 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9170 }
9171
9172 #[test]
9173 fn iskeyword_with_dash_treats_dash_as_word_char() {
9174 let mut e = editor_with("foo-bar baz");
9175 e.settings_mut().iskeyword = "@,_,45".to_string();
9176 e.jump_cursor(0, 0);
9177 run_keys(&mut e, "*");
9178 let p = e
9179 .search_state()
9180 .pattern
9181 .as_ref()
9182 .unwrap()
9183 .as_str()
9184 .to_string();
9185 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9186 }
9187
9188 #[test]
9189 fn timeoutlen_drops_pending_g_prefix() {
9190 use std::time::{Duration, Instant};
9191 let mut e = editor_with("a\nb\nc");
9192 e.jump_cursor(2, 0);
9193 run_keys(&mut e, "g");
9195 assert!(matches!(e.vim.pending, super::Pending::G));
9196 e.settings.timeout_len = Duration::from_nanos(0);
9204 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9205 e.vim.last_input_host_at = Some(Duration::ZERO);
9206 run_keys(&mut e, "g");
9210 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9212 }
9213
9214 #[test]
9215 fn undobreak_on_breaks_group_at_arrow_motion() {
9216 let mut e = editor_with("");
9217 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9219 let line = e.buffer.line(0).unwrap_or("").to_string();
9222 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9223 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9224 }
9225
9226 #[test]
9227 fn undobreak_off_keeps_full_run_in_one_group() {
9228 let mut e = editor_with("");
9229 e.settings_mut().undo_break_on_motion = false;
9230 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9231 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9234 }
9235
9236 #[test]
9237 fn undobreak_round_trips_through_options() {
9238 let e = editor_with("");
9239 let opts = e.current_options();
9240 assert!(opts.undo_break_on_motion);
9241 let mut e2 = editor_with("");
9242 let mut new_opts = opts.clone();
9243 new_opts.undo_break_on_motion = false;
9244 e2.apply_options(&new_opts);
9245 assert!(!e2.current_options().undo_break_on_motion);
9246 }
9247
9248 #[test]
9249 fn undo_levels_cap_drops_oldest() {
9250 let mut e = editor_with("abcde");
9251 e.settings_mut().undo_levels = 3;
9252 run_keys(&mut e, "ra");
9253 run_keys(&mut e, "lrb");
9254 run_keys(&mut e, "lrc");
9255 run_keys(&mut e, "lrd");
9256 run_keys(&mut e, "lre");
9257 assert_eq!(e.undo_stack_len(), 3);
9258 }
9259
9260 #[test]
9261 fn tab_inserts_literal_tab_when_noexpandtab() {
9262 let mut e = editor_with("");
9263 e.settings_mut().expandtab = false;
9266 e.settings_mut().softtabstop = 0;
9267 run_keys(&mut e, "i");
9268 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9269 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9270 }
9271
9272 #[test]
9273 fn tab_inserts_spaces_when_expandtab() {
9274 let mut e = editor_with("");
9275 e.settings_mut().expandtab = true;
9276 e.settings_mut().tabstop = 4;
9277 run_keys(&mut e, "i");
9278 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9279 assert_eq!(e.buffer.line(0).unwrap(), " ");
9280 }
9281
9282 #[test]
9283 fn tab_with_softtabstop_fills_to_next_boundary() {
9284 let mut e = editor_with("ab");
9286 e.settings_mut().expandtab = true;
9287 e.settings_mut().tabstop = 8;
9288 e.settings_mut().softtabstop = 4;
9289 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9291 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9292 }
9293
9294 #[test]
9295 fn backspace_deletes_softtab_run() {
9296 let mut e = editor_with(" x");
9299 e.settings_mut().softtabstop = 4;
9300 run_keys(&mut e, "fxi");
9302 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9303 assert_eq!(e.buffer.line(0).unwrap(), "x");
9304 }
9305
9306 #[test]
9307 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9308 let mut e = editor_with(" x");
9311 e.settings_mut().softtabstop = 4;
9312 run_keys(&mut e, "fxi");
9313 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9314 assert_eq!(e.buffer.line(0).unwrap(), " x");
9315 }
9316
9317 #[test]
9318 fn readonly_blocks_insert_mutation() {
9319 let mut e = editor_with("hello");
9320 e.settings_mut().readonly = true;
9321 run_keys(&mut e, "iX<Esc>");
9322 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9323 }
9324
9325 #[cfg(feature = "ratatui")]
9326 #[test]
9327 fn intern_ratatui_style_dedups_repeated_styles() {
9328 use ratatui::style::{Color, Style};
9329 let mut e = editor_with("");
9330 let red = Style::default().fg(Color::Red);
9331 let blue = Style::default().fg(Color::Blue);
9332 let id_r1 = e.intern_ratatui_style(red);
9333 let id_r2 = e.intern_ratatui_style(red);
9334 let id_b = e.intern_ratatui_style(blue);
9335 assert_eq!(id_r1, id_r2);
9336 assert_ne!(id_r1, id_b);
9337 assert_eq!(e.style_table().len(), 2);
9338 }
9339
9340 #[cfg(feature = "ratatui")]
9341 #[test]
9342 fn install_ratatui_syntax_spans_translates_styled_spans() {
9343 use ratatui::style::{Color, Style};
9344 let mut e = editor_with("SELECT foo");
9345 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9346 let by_row = e.buffer_spans();
9347 assert_eq!(by_row.len(), 1);
9348 assert_eq!(by_row[0].len(), 1);
9349 assert_eq!(by_row[0][0].start_byte, 0);
9350 assert_eq!(by_row[0][0].end_byte, 6);
9351 let id = by_row[0][0].style;
9352 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9353 }
9354
9355 #[cfg(feature = "ratatui")]
9356 #[test]
9357 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9358 use ratatui::style::{Color, Style};
9359 let mut e = editor_with("hello");
9360 e.install_ratatui_syntax_spans(vec![vec![(
9361 0,
9362 usize::MAX,
9363 Style::default().fg(Color::Blue),
9364 )]]);
9365 let by_row = e.buffer_spans();
9366 assert_eq!(by_row[0][0].end_byte, 5);
9367 }
9368
9369 #[cfg(feature = "ratatui")]
9370 #[test]
9371 fn install_ratatui_syntax_spans_drops_zero_width() {
9372 use ratatui::style::{Color, Style};
9373 let mut e = editor_with("abc");
9374 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9375 assert!(e.buffer_spans()[0].is_empty());
9376 }
9377
9378 #[test]
9379 fn named_register_yank_into_a_then_paste_from_a() {
9380 let mut e = editor_with("hello world\nsecond");
9381 run_keys(&mut e, "\"ayw");
9382 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9384 run_keys(&mut e, "j0\"aP");
9386 assert_eq!(e.buffer().lines()[1], "hello second");
9387 }
9388
9389 #[test]
9390 fn capital_r_overstrikes_chars() {
9391 let mut e = editor_with("hello");
9392 e.jump_cursor(0, 0);
9393 run_keys(&mut e, "RXY<Esc>");
9394 assert_eq!(e.buffer().lines()[0], "XYllo");
9396 }
9397
9398 #[test]
9399 fn capital_r_at_eol_appends() {
9400 let mut e = editor_with("hi");
9401 e.jump_cursor(0, 1);
9402 run_keys(&mut e, "RXYZ<Esc>");
9404 assert_eq!(e.buffer().lines()[0], "hXYZ");
9405 }
9406
9407 #[test]
9408 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9409 let mut e = editor_with("abc");
9413 e.jump_cursor(0, 0);
9414 run_keys(&mut e, "RX<Esc>");
9415 assert_eq!(e.buffer().lines()[0], "Xbc");
9416 }
9417
9418 #[test]
9419 fn ctrl_r_in_insert_pastes_named_register() {
9420 let mut e = editor_with("hello world");
9421 run_keys(&mut e, "\"ayw");
9423 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9424 run_keys(&mut e, "o");
9426 assert_eq!(e.vim_mode(), VimMode::Insert);
9427 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9428 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9429 assert_eq!(e.buffer().lines()[1], "hello ");
9430 assert_eq!(e.cursor(), (1, 6));
9432 assert_eq!(e.vim_mode(), VimMode::Insert);
9434 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9435 assert_eq!(e.buffer().lines()[1], "hello X");
9436 }
9437
9438 #[test]
9439 fn ctrl_r_with_unnamed_register() {
9440 let mut e = editor_with("foo");
9441 run_keys(&mut e, "yiw");
9442 run_keys(&mut e, "A ");
9443 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9445 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9446 assert_eq!(e.buffer().lines()[0], "foo foo");
9447 }
9448
9449 #[test]
9450 fn ctrl_r_unknown_selector_is_no_op() {
9451 let mut e = editor_with("abc");
9452 run_keys(&mut e, "A");
9453 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9454 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9457 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9458 assert_eq!(e.buffer().lines()[0], "abcZ");
9459 }
9460
9461 #[test]
9462 fn ctrl_r_multiline_register_pastes_with_newlines() {
9463 let mut e = editor_with("alpha\nbeta\ngamma");
9464 run_keys(&mut e, "\"byy");
9466 run_keys(&mut e, "j\"byy");
9467 run_keys(&mut e, "ggVj\"by");
9471 let payload = e.registers().read('b').unwrap().text.clone();
9472 assert!(payload.contains('\n'));
9473 run_keys(&mut e, "Go");
9474 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9475 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9476 let total_lines = e.buffer().lines().len();
9479 assert!(total_lines >= 5);
9480 }
9481
9482 #[test]
9483 fn yank_zero_holds_last_yank_after_delete() {
9484 let mut e = editor_with("hello world");
9485 run_keys(&mut e, "yw");
9486 let yanked = e.registers().read('0').unwrap().text.clone();
9487 assert!(!yanked.is_empty());
9488 run_keys(&mut e, "dw");
9490 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9491 assert!(!e.registers().read('1').unwrap().text.is_empty());
9493 }
9494
9495 #[test]
9496 fn delete_ring_rotates_through_one_through_nine() {
9497 let mut e = editor_with("a b c d e f g h i j");
9498 for _ in 0..3 {
9500 run_keys(&mut e, "dw");
9501 }
9502 let r1 = e.registers().read('1').unwrap().text.clone();
9504 let r2 = e.registers().read('2').unwrap().text.clone();
9505 let r3 = e.registers().read('3').unwrap().text.clone();
9506 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9507 assert_ne!(r1, r2);
9508 assert_ne!(r2, r3);
9509 }
9510
9511 #[test]
9512 fn capital_register_appends_to_lowercase() {
9513 let mut e = editor_with("foo bar");
9514 run_keys(&mut e, "\"ayw");
9515 let first = e.registers().read('a').unwrap().text.clone();
9516 assert!(first.contains("foo"));
9517 run_keys(&mut e, "w\"Ayw");
9519 let combined = e.registers().read('a').unwrap().text.clone();
9520 assert!(combined.starts_with(&first));
9521 assert!(combined.contains("bar"));
9522 }
9523
9524 #[test]
9525 fn zf_in_visual_line_creates_closed_fold() {
9526 let mut e = editor_with("a\nb\nc\nd\ne");
9527 e.jump_cursor(1, 0);
9529 run_keys(&mut e, "Vjjzf");
9530 assert_eq!(e.buffer().folds().len(), 1);
9531 let f = e.buffer().folds()[0];
9532 assert_eq!(f.start_row, 1);
9533 assert_eq!(f.end_row, 3);
9534 assert!(f.closed);
9535 }
9536
9537 #[test]
9538 fn zfj_in_normal_creates_two_row_fold() {
9539 let mut e = editor_with("a\nb\nc\nd\ne");
9540 e.jump_cursor(1, 0);
9541 run_keys(&mut e, "zfj");
9542 assert_eq!(e.buffer().folds().len(), 1);
9543 let f = e.buffer().folds()[0];
9544 assert_eq!(f.start_row, 1);
9545 assert_eq!(f.end_row, 2);
9546 assert!(f.closed);
9547 assert_eq!(e.cursor().0, 1);
9549 }
9550
9551 #[test]
9552 fn zf_with_count_folds_count_rows() {
9553 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9554 e.jump_cursor(0, 0);
9555 run_keys(&mut e, "zf3j");
9557 assert_eq!(e.buffer().folds().len(), 1);
9558 let f = e.buffer().folds()[0];
9559 assert_eq!(f.start_row, 0);
9560 assert_eq!(f.end_row, 3);
9561 }
9562
9563 #[test]
9564 fn zfk_folds_upward_range() {
9565 let mut e = editor_with("a\nb\nc\nd\ne");
9566 e.jump_cursor(3, 0);
9567 run_keys(&mut e, "zfk");
9568 let f = e.buffer().folds()[0];
9569 assert_eq!(f.start_row, 2);
9571 assert_eq!(f.end_row, 3);
9572 }
9573
9574 #[test]
9575 fn zf_capital_g_folds_to_bottom() {
9576 let mut e = editor_with("a\nb\nc\nd\ne");
9577 e.jump_cursor(1, 0);
9578 run_keys(&mut e, "zfG");
9580 let f = e.buffer().folds()[0];
9581 assert_eq!(f.start_row, 1);
9582 assert_eq!(f.end_row, 4);
9583 }
9584
9585 #[test]
9586 fn zfgg_folds_to_top_via_operator_pipeline() {
9587 let mut e = editor_with("a\nb\nc\nd\ne");
9588 e.jump_cursor(3, 0);
9589 run_keys(&mut e, "zfgg");
9593 let f = e.buffer().folds()[0];
9594 assert_eq!(f.start_row, 0);
9595 assert_eq!(f.end_row, 3);
9596 }
9597
9598 #[test]
9599 fn zfip_folds_paragraph_via_text_object() {
9600 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9601 e.jump_cursor(1, 0);
9602 run_keys(&mut e, "zfip");
9604 assert_eq!(e.buffer().folds().len(), 1);
9605 let f = e.buffer().folds()[0];
9606 assert_eq!(f.start_row, 0);
9607 assert_eq!(f.end_row, 2);
9608 }
9609
9610 #[test]
9611 fn zfap_folds_paragraph_with_trailing_blank() {
9612 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9613 e.jump_cursor(0, 0);
9614 run_keys(&mut e, "zfap");
9616 let f = e.buffer().folds()[0];
9617 assert_eq!(f.start_row, 0);
9618 assert_eq!(f.end_row, 3);
9619 }
9620
9621 #[test]
9622 fn zf_paragraph_motion_folds_to_blank() {
9623 let mut e = editor_with("alpha\nbeta\n\ngamma");
9624 e.jump_cursor(0, 0);
9625 run_keys(&mut e, "zf}");
9627 let f = e.buffer().folds()[0];
9628 assert_eq!(f.start_row, 0);
9629 assert_eq!(f.end_row, 2);
9630 }
9631
9632 #[test]
9633 fn za_toggles_fold_under_cursor() {
9634 let mut e = editor_with("a\nb\nc\nd");
9635 e.buffer_mut().add_fold(1, 2, true);
9636 e.jump_cursor(1, 0);
9637 run_keys(&mut e, "za");
9638 assert!(!e.buffer().folds()[0].closed);
9639 run_keys(&mut e, "za");
9640 assert!(e.buffer().folds()[0].closed);
9641 }
9642
9643 #[test]
9644 fn zr_opens_all_folds_zm_closes_all() {
9645 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9646 e.buffer_mut().add_fold(0, 1, true);
9647 e.buffer_mut().add_fold(2, 3, true);
9648 e.buffer_mut().add_fold(4, 5, true);
9649 run_keys(&mut e, "zR");
9650 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9651 run_keys(&mut e, "zM");
9652 assert!(e.buffer().folds().iter().all(|f| f.closed));
9653 }
9654
9655 #[test]
9656 fn ze_clears_all_folds() {
9657 let mut e = editor_with("a\nb\nc\nd");
9658 e.buffer_mut().add_fold(0, 1, true);
9659 e.buffer_mut().add_fold(2, 3, false);
9660 run_keys(&mut e, "zE");
9661 assert!(e.buffer().folds().is_empty());
9662 }
9663
9664 #[test]
9665 fn g_underscore_jumps_to_last_non_blank() {
9666 let mut e = editor_with("hello world ");
9667 run_keys(&mut e, "g_");
9668 assert_eq!(e.cursor().1, 10);
9670 }
9671
9672 #[test]
9673 fn gj_and_gk_alias_j_and_k() {
9674 let mut e = editor_with("a\nb\nc");
9675 run_keys(&mut e, "gj");
9676 assert_eq!(e.cursor().0, 1);
9677 run_keys(&mut e, "gk");
9678 assert_eq!(e.cursor().0, 0);
9679 }
9680
9681 #[test]
9682 fn paragraph_motions_walk_blank_lines() {
9683 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9684 run_keys(&mut e, "}");
9685 assert_eq!(e.cursor().0, 2);
9686 run_keys(&mut e, "}");
9687 assert_eq!(e.cursor().0, 5);
9688 run_keys(&mut e, "{");
9689 assert_eq!(e.cursor().0, 2);
9690 }
9691
9692 #[test]
9693 fn gv_reenters_last_visual_selection() {
9694 let mut e = editor_with("alpha\nbeta\ngamma");
9695 run_keys(&mut e, "Vj");
9696 run_keys(&mut e, "<Esc>");
9698 assert_eq!(e.vim_mode(), VimMode::Normal);
9699 run_keys(&mut e, "gv");
9701 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9702 }
9703
9704 #[test]
9705 fn o_in_visual_swaps_anchor_and_cursor() {
9706 let mut e = editor_with("hello world");
9707 run_keys(&mut e, "vllll");
9709 assert_eq!(e.cursor().1, 4);
9710 run_keys(&mut e, "o");
9712 assert_eq!(e.cursor().1, 0);
9713 assert_eq!(e.vim.visual_anchor, (0, 4));
9715 }
9716
9717 #[test]
9718 fn editing_inside_fold_invalidates_it() {
9719 let mut e = editor_with("a\nb\nc\nd");
9720 e.buffer_mut().add_fold(1, 2, true);
9721 e.jump_cursor(1, 0);
9722 run_keys(&mut e, "iX<Esc>");
9724 assert!(e.buffer().folds().is_empty());
9726 }
9727
9728 #[test]
9729 fn zd_removes_fold_under_cursor() {
9730 let mut e = editor_with("a\nb\nc\nd");
9731 e.buffer_mut().add_fold(1, 2, true);
9732 e.jump_cursor(2, 0);
9733 run_keys(&mut e, "zd");
9734 assert!(e.buffer().folds().is_empty());
9735 }
9736
9737 #[test]
9738 fn take_fold_ops_observes_z_keystroke_dispatch() {
9739 use crate::types::FoldOp;
9744 let mut e = editor_with("a\nb\nc\nd");
9745 e.buffer_mut().add_fold(1, 2, true);
9746 e.jump_cursor(1, 0);
9747 let _ = e.take_fold_ops();
9750 run_keys(&mut e, "zo");
9751 run_keys(&mut e, "zM");
9752 let ops = e.take_fold_ops();
9753 assert_eq!(ops.len(), 2);
9754 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9755 assert!(matches!(ops[1], FoldOp::CloseAll));
9756 assert!(e.take_fold_ops().is_empty());
9758 }
9759
9760 #[test]
9761 fn edit_pipeline_emits_invalidate_fold_op() {
9762 use crate::types::FoldOp;
9765 let mut e = editor_with("a\nb\nc\nd");
9766 e.buffer_mut().add_fold(1, 2, true);
9767 e.jump_cursor(1, 0);
9768 let _ = e.take_fold_ops();
9769 run_keys(&mut e, "iX<Esc>");
9770 let ops = e.take_fold_ops();
9771 assert!(
9772 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9773 "expected at least one Invalidate op, got {ops:?}"
9774 );
9775 }
9776
9777 #[test]
9778 fn dot_mark_jumps_to_last_edit_position() {
9779 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9780 e.jump_cursor(2, 0);
9781 run_keys(&mut e, "iX<Esc>");
9783 let after_edit = e.cursor();
9784 run_keys(&mut e, "gg");
9786 assert_eq!(e.cursor().0, 0);
9787 run_keys(&mut e, "'.");
9789 assert_eq!(e.cursor().0, after_edit.0);
9790 }
9791
9792 #[test]
9793 fn quote_quote_returns_to_pre_jump_position() {
9794 let mut e = editor_with_rows(50, 20);
9795 e.jump_cursor(10, 2);
9796 let before = e.cursor();
9797 run_keys(&mut e, "G");
9799 assert_ne!(e.cursor(), before);
9800 run_keys(&mut e, "''");
9802 assert_eq!(e.cursor().0, before.0);
9803 }
9804
9805 #[test]
9806 fn backtick_backtick_restores_exact_pre_jump_pos() {
9807 let mut e = editor_with_rows(50, 20);
9808 e.jump_cursor(7, 3);
9809 let before = e.cursor();
9810 run_keys(&mut e, "G");
9811 run_keys(&mut e, "``");
9812 assert_eq!(e.cursor(), before);
9813 }
9814
9815 #[test]
9816 fn macro_record_and_replay_basic() {
9817 let mut e = editor_with("foo\nbar\nbaz");
9818 run_keys(&mut e, "qaIX<Esc>jq");
9820 assert_eq!(e.buffer().lines()[0], "Xfoo");
9821 run_keys(&mut e, "@a");
9823 assert_eq!(e.buffer().lines()[1], "Xbar");
9824 run_keys(&mut e, "j@@");
9826 assert_eq!(e.buffer().lines()[2], "Xbaz");
9827 }
9828
9829 #[test]
9830 fn macro_count_replays_n_times() {
9831 let mut e = editor_with("a\nb\nc\nd\ne");
9832 run_keys(&mut e, "qajq");
9834 assert_eq!(e.cursor().0, 1);
9835 run_keys(&mut e, "3@a");
9837 assert_eq!(e.cursor().0, 4);
9838 }
9839
9840 #[test]
9841 fn macro_capital_q_appends_to_lowercase_register() {
9842 let mut e = editor_with("hello");
9843 run_keys(&mut e, "qall<Esc>q");
9844 run_keys(&mut e, "qAhh<Esc>q");
9845 let text = e.registers().read('a').unwrap().text.clone();
9848 assert!(text.contains("ll<Esc>"));
9849 assert!(text.contains("hh<Esc>"));
9850 }
9851
9852 #[test]
9853 fn buffer_selection_block_in_visual_block_mode() {
9854 use hjkl_buffer::{Position, Selection};
9855 let mut e = editor_with("aaaa\nbbbb\ncccc");
9856 run_keys(&mut e, "<C-v>jl");
9857 assert_eq!(
9858 e.buffer_selection(),
9859 Some(Selection::Block {
9860 anchor: Position::new(0, 0),
9861 head: Position::new(1, 1),
9862 })
9863 );
9864 }
9865
9866 #[test]
9869 fn n_after_question_mark_keeps_walking_backward() {
9870 let mut e = editor_with("foo bar foo baz foo end");
9873 e.jump_cursor(0, 22);
9874 run_keys(&mut e, "?foo<CR>");
9875 assert_eq!(e.cursor().1, 16);
9876 run_keys(&mut e, "n");
9877 assert_eq!(e.cursor().1, 8);
9878 run_keys(&mut e, "N");
9879 assert_eq!(e.cursor().1, 16);
9880 }
9881
9882 #[test]
9883 fn nested_macro_chord_records_literal_keys() {
9884 let mut e = editor_with("alpha\nbeta\ngamma");
9887 run_keys(&mut e, "qblq");
9889 run_keys(&mut e, "qaIX<Esc>q");
9892 e.jump_cursor(1, 0);
9894 run_keys(&mut e, "@a");
9895 assert_eq!(e.buffer().lines()[1], "Xbeta");
9896 }
9897
9898 #[test]
9899 fn shift_gt_motion_indents_one_line() {
9900 let mut e = editor_with("hello world");
9904 run_keys(&mut e, ">w");
9905 assert_eq!(e.buffer().lines()[0], " hello world");
9906 }
9907
9908 #[test]
9909 fn shift_lt_motion_outdents_one_line() {
9910 let mut e = editor_with(" hello world");
9911 run_keys(&mut e, "<lt>w");
9912 assert_eq!(e.buffer().lines()[0], " hello world");
9914 }
9915
9916 #[test]
9917 fn shift_gt_text_object_indents_paragraph() {
9918 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9919 e.jump_cursor(0, 0);
9920 run_keys(&mut e, ">ip");
9921 assert_eq!(e.buffer().lines()[0], " alpha");
9922 assert_eq!(e.buffer().lines()[1], " beta");
9923 assert_eq!(e.buffer().lines()[2], " gamma");
9924 assert_eq!(e.buffer().lines()[4], "rest");
9926 }
9927
9928 #[test]
9929 fn ctrl_o_runs_exactly_one_normal_command() {
9930 let mut e = editor_with("alpha beta gamma");
9933 e.jump_cursor(0, 0);
9934 run_keys(&mut e, "i");
9935 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9936 run_keys(&mut e, "dw");
9937 assert_eq!(e.vim_mode(), VimMode::Insert);
9939 run_keys(&mut e, "X");
9941 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9942 }
9943
9944 #[test]
9945 fn macro_replay_respects_mode_switching() {
9946 let mut e = editor_with("hi");
9950 run_keys(&mut e, "qaiX<Esc>0q");
9951 assert_eq!(e.vim_mode(), VimMode::Normal);
9952 e.set_content("yo");
9954 run_keys(&mut e, "@a");
9955 assert_eq!(e.vim_mode(), VimMode::Normal);
9956 assert_eq!(e.cursor().1, 0);
9957 assert_eq!(e.buffer().lines()[0], "Xyo");
9958 }
9959
9960 #[test]
9961 fn macro_recorded_text_round_trips_through_register() {
9962 let mut e = editor_with("");
9966 run_keys(&mut e, "qaiX<Esc>q");
9967 let text = e.registers().read('a').unwrap().text.clone();
9968 assert!(text.starts_with("iX"));
9969 run_keys(&mut e, "@a");
9971 assert_eq!(e.buffer().lines()[0], "XX");
9972 }
9973
9974 #[test]
9975 fn dot_after_macro_replays_macros_last_change() {
9976 let mut e = editor_with("ab\ncd\nef");
9979 run_keys(&mut e, "qaIX<Esc>jq");
9982 assert_eq!(e.buffer().lines()[0], "Xab");
9983 run_keys(&mut e, "@a");
9984 assert_eq!(e.buffer().lines()[1], "Xcd");
9985 let row_before_dot = e.cursor().0;
9988 run_keys(&mut e, ".");
9989 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9990 }
9991
9992 fn si_editor(content: &str) -> Editor {
9998 let opts = crate::types::Options {
9999 shiftwidth: 4,
10000 softtabstop: 4,
10001 expandtab: true,
10002 smartindent: true,
10003 autoindent: true,
10004 ..crate::types::Options::default()
10005 };
10006 let mut e = Editor::new(
10007 hjkl_buffer::Buffer::new(),
10008 crate::types::DefaultHost::new(),
10009 opts,
10010 );
10011 e.set_content(content);
10012 e
10013 }
10014
10015 #[test]
10016 fn smartindent_bumps_indent_after_open_brace() {
10017 let mut e = si_editor("fn foo() {");
10019 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10021 assert_eq!(
10022 e.buffer().lines()[1],
10023 " ",
10024 "smartindent should bump one shiftwidth after {{"
10025 );
10026 }
10027
10028 #[test]
10029 fn smartindent_no_bump_when_off() {
10030 let mut e = si_editor("fn foo() {");
10033 e.settings_mut().smartindent = false;
10034 e.jump_cursor(0, 10);
10035 run_keys(&mut e, "i<CR>");
10036 assert_eq!(
10037 e.buffer().lines()[1],
10038 "",
10039 "without smartindent, no bump: new line copies empty leading ws"
10040 );
10041 }
10042
10043 #[test]
10044 fn smartindent_uses_tab_when_noexpandtab() {
10045 let opts = crate::types::Options {
10047 shiftwidth: 4,
10048 softtabstop: 0,
10049 expandtab: false,
10050 smartindent: true,
10051 autoindent: true,
10052 ..crate::types::Options::default()
10053 };
10054 let mut e = Editor::new(
10055 hjkl_buffer::Buffer::new(),
10056 crate::types::DefaultHost::new(),
10057 opts,
10058 );
10059 e.set_content("fn foo() {");
10060 e.jump_cursor(0, 10);
10061 run_keys(&mut e, "i<CR>");
10062 assert_eq!(
10063 e.buffer().lines()[1],
10064 "\t",
10065 "noexpandtab: smartindent bump inserts a literal tab"
10066 );
10067 }
10068
10069 #[test]
10070 fn smartindent_dedent_on_close_brace() {
10071 let mut e = si_editor("fn foo() {");
10074 e.set_content("fn foo() {\n ");
10076 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10078 assert_eq!(
10079 e.buffer().lines()[1],
10080 "}",
10081 "close brace on whitespace-only line should dedent"
10082 );
10083 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10084 }
10085
10086 #[test]
10087 fn smartindent_no_dedent_when_off() {
10088 let mut e = si_editor("fn foo() {\n ");
10090 e.settings_mut().smartindent = false;
10091 e.jump_cursor(1, 4);
10092 run_keys(&mut e, "i}");
10093 assert_eq!(
10094 e.buffer().lines()[1],
10095 " }",
10096 "without smartindent, `}}` just appends at cursor"
10097 );
10098 }
10099
10100 #[test]
10101 fn smartindent_no_dedent_mid_line() {
10102 let mut e = si_editor(" let x = 1");
10105 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10107 assert_eq!(
10108 e.buffer().lines()[0],
10109 " let x = 1}",
10110 "mid-line `}}` should not dedent"
10111 );
10112 }
10113
10114 #[test]
10118 fn count_5x_fills_unnamed_register() {
10119 let mut e = editor_with("hello world\n");
10120 e.jump_cursor(0, 0);
10121 run_keys(&mut e, "5x");
10122 assert_eq!(e.buffer().lines()[0], " world");
10123 assert_eq!(e.cursor(), (0, 0));
10124 assert_eq!(e.yank(), "hello");
10125 }
10126
10127 #[test]
10128 fn x_fills_unnamed_register_single_char() {
10129 let mut e = editor_with("abc\n");
10130 e.jump_cursor(0, 0);
10131 run_keys(&mut e, "x");
10132 assert_eq!(e.buffer().lines()[0], "bc");
10133 assert_eq!(e.yank(), "a");
10134 }
10135
10136 #[test]
10137 fn big_x_fills_unnamed_register() {
10138 let mut e = editor_with("hello\n");
10139 e.jump_cursor(0, 3);
10140 run_keys(&mut e, "X");
10141 assert_eq!(e.buffer().lines()[0], "helo");
10142 assert_eq!(e.yank(), "l");
10143 }
10144
10145 #[test]
10147 fn g_motion_trailing_newline_lands_on_last_content_row() {
10148 let mut e = editor_with("foo\nbar\nbaz\n");
10149 e.jump_cursor(0, 0);
10150 run_keys(&mut e, "G");
10151 assert_eq!(
10153 e.cursor().0,
10154 2,
10155 "G should land on row 2 (baz), not row 3 (phantom empty)"
10156 );
10157 }
10158
10159 #[test]
10161 fn dd_last_line_clamps_cursor_to_new_last_row() {
10162 let mut e = editor_with("foo\nbar\n");
10163 e.jump_cursor(1, 0);
10164 run_keys(&mut e, "dd");
10165 assert_eq!(e.buffer().lines()[0], "foo");
10166 assert_eq!(
10167 e.cursor(),
10168 (0, 0),
10169 "cursor should clamp to row 0 after dd on last content line"
10170 );
10171 }
10172
10173 #[test]
10175 fn d_dollar_cursor_on_last_char() {
10176 let mut e = editor_with("hello world\n");
10177 e.jump_cursor(0, 5);
10178 run_keys(&mut e, "d$");
10179 assert_eq!(e.buffer().lines()[0], "hello");
10180 assert_eq!(
10181 e.cursor(),
10182 (0, 4),
10183 "d$ should leave cursor on col 4, not col 5"
10184 );
10185 }
10186
10187 #[test]
10189 fn undo_insert_clamps_cursor_to_last_valid_col() {
10190 let mut e = editor_with("hello\n");
10191 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10193 assert_eq!(e.buffer().lines()[0], "hello");
10194 assert_eq!(
10195 e.cursor(),
10196 (0, 4),
10197 "undo should clamp cursor to col 4 on 'hello'"
10198 );
10199 }
10200
10201 #[test]
10203 fn da_doublequote_eats_trailing_whitespace() {
10204 let mut e = editor_with("say \"hello\" there\n");
10205 e.jump_cursor(0, 6);
10206 run_keys(&mut e, "da\"");
10207 assert_eq!(e.buffer().lines()[0], "say there");
10208 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10209 }
10210
10211 #[test]
10213 fn dab_cursor_col_clamped_after_delete() {
10214 let mut e = editor_with("fn x() {\n body\n}\n");
10215 e.jump_cursor(1, 4);
10216 run_keys(&mut e, "daB");
10217 assert_eq!(e.buffer().lines()[0], "fn x() ");
10218 assert_eq!(
10219 e.cursor(),
10220 (0, 6),
10221 "daB should leave cursor at col 6, not 7"
10222 );
10223 }
10224
10225 #[test]
10227 fn dib_preserves_surrounding_newlines() {
10228 let mut e = editor_with("{\n body\n}\n");
10229 e.jump_cursor(1, 4);
10230 run_keys(&mut e, "diB");
10231 assert_eq!(e.buffer().lines()[0], "{");
10232 assert_eq!(e.buffer().lines()[1], "}");
10233 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10234 }
10235
10236 #[test]
10237 fn is_chord_pending_tracks_replace_state() {
10238 let mut e = editor_with("abc\n");
10239 assert!(!e.is_chord_pending());
10240 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10242 assert!(e.is_chord_pending(), "engine should be pending after r");
10243 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10245 assert!(
10246 !e.is_chord_pending(),
10247 "engine pending should clear after replace"
10248 );
10249 }
10250
10251 #[test]
10254 fn yiw_sets_lbr_rbr_marks_around_word() {
10255 let mut e = editor_with("hello world");
10258 run_keys(&mut e, "yiw");
10259 let lo = e.mark('[').expect("'[' must be set after yiw");
10260 let hi = e.mark(']').expect("']' must be set after yiw");
10261 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10262 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10263 }
10264
10265 #[test]
10266 fn yj_linewise_sets_marks_at_line_edges() {
10267 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10270 run_keys(&mut e, "yj");
10271 let lo = e.mark('[').expect("'[' must be set after yj");
10272 let hi = e.mark(']').expect("']' must be set after yj");
10273 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10274 assert_eq!(
10275 hi,
10276 (1, 4),
10277 "'] snaps to (bot_row, last_col) for linewise yank"
10278 );
10279 }
10280
10281 #[test]
10282 fn dd_sets_lbr_rbr_marks_to_cursor() {
10283 let mut e = editor_with("aaa\nbbb");
10286 run_keys(&mut e, "dd");
10287 let lo = e.mark('[').expect("'[' must be set after dd");
10288 let hi = e.mark(']').expect("']' must be set after dd");
10289 assert_eq!(lo, hi, "after delete both marks are at the same position");
10290 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10291 }
10292
10293 #[test]
10294 fn dw_sets_lbr_rbr_marks_to_cursor() {
10295 let mut e = editor_with("hello world");
10298 run_keys(&mut e, "dw");
10299 let lo = e.mark('[').expect("'[' must be set after dw");
10300 let hi = e.mark(']').expect("']' must be set after dw");
10301 assert_eq!(lo, hi, "after delete both marks are at the same position");
10302 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10303 }
10304
10305 #[test]
10306 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10307 let mut e = editor_with("hello world");
10312 run_keys(&mut e, "cwfoo<Esc>");
10313 let lo = e.mark('[').expect("'[' must be set after cw");
10314 let hi = e.mark(']').expect("']' must be set after cw");
10315 assert_eq!(lo, (0, 0), "'[ should be start of change");
10316 assert_eq!(hi.0, 0, "'] should be on row 0");
10319 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10320 }
10321
10322 #[test]
10323 fn cw_with_no_insertion_sets_marks_at_change_start() {
10324 let mut e = editor_with("hello world");
10327 run_keys(&mut e, "cw<Esc>");
10328 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10329 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10330 assert_eq!(lo.0, 0, "'[ should be on row 0");
10331 assert_eq!(hi.0, 0, "'] should be on row 0");
10332 assert_eq!(lo, hi, "marks coincide when insert is empty");
10334 }
10335
10336 #[test]
10337 fn p_charwise_sets_marks_around_pasted_text() {
10338 let mut e = editor_with("abc xyz");
10341 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10344 let hi = e.mark(']').expect("']' set after charwise paste");
10345 assert!(lo <= hi, "'[ must not exceed ']'");
10346 assert_eq!(
10348 hi.1.wrapping_sub(lo.1),
10349 2,
10350 "'] - '[ should span 2 cols for a 3-char paste"
10351 );
10352 }
10353
10354 #[test]
10355 fn p_linewise_sets_marks_at_line_edges() {
10356 let mut e = editor_with("aaa\nbbb\nccc");
10359 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10363 let hi = e.mark(']').expect("']' set after linewise paste");
10364 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10365 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10366 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10367 }
10368
10369 #[test]
10370 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10371 let mut e = editor_with("hello world");
10375 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10379 assert_eq!(
10381 e.cursor(),
10382 (0, 4),
10383 "visual `[v`] should land on last yanked char"
10384 );
10385 assert_eq!(
10387 e.vim_mode(),
10388 crate::VimMode::Visual,
10389 "should be in Visual mode"
10390 );
10391 }
10392
10393 #[test]
10399 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10400 let mut e = editor_with("hello\nworld\n");
10403 e.jump_cursor(0, 0);
10404 run_keys(&mut e, "iX<Esc>j`.");
10405 assert_eq!(
10406 e.cursor(),
10407 (0, 0),
10408 "dot mark should jump to the change-start (col 0), not post-insert col"
10409 );
10410 }
10411
10412 #[test]
10415 fn count_100g_clamps_to_last_content_row() {
10416 let mut e = editor_with("foo\nbar\nbaz\n");
10419 e.jump_cursor(0, 0);
10420 run_keys(&mut e, "100G");
10421 assert_eq!(
10422 e.cursor(),
10423 (2, 0),
10424 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10425 );
10426 }
10427
10428 #[test]
10431 fn gi_resumes_last_insert_position() {
10432 let mut e = editor_with("world\nhello\n");
10438 e.jump_cursor(0, 0);
10439 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10440 assert_eq!(
10441 e.vim_mode(),
10442 crate::VimMode::Normal,
10443 "should be in Normal mode after gi<Esc>"
10444 );
10445 assert_eq!(
10446 e.cursor(),
10447 (0, 1),
10448 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10449 );
10450 }
10451
10452 #[test]
10456 fn visual_block_change_cursor_on_last_inserted_char() {
10457 let mut e = editor_with("foo\nbar\nbaz\n");
10461 e.jump_cursor(0, 0);
10462 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10463 let lines = e.buffer().lines().to_vec();
10464 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10465 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10466 assert_eq!(
10467 e.cursor(),
10468 (0, 1),
10469 "cursor should be on last char of inserted 'ZZ' (col 1)"
10470 );
10471 }
10472
10473 #[test]
10478 fn register_blackhole_delete_preserves_unnamed_register() {
10479 let mut e = editor_with("foo bar baz\n");
10486 e.jump_cursor(0, 0);
10487 run_keys(&mut e, "yiww\"_dwbp");
10488 let lines = e.buffer().lines().to_vec();
10489 assert_eq!(
10490 lines[0], "ffoooo baz",
10491 "black-hole delete must not corrupt unnamed register"
10492 );
10493 assert_eq!(
10494 e.cursor(),
10495 (0, 3),
10496 "cursor should be on last pasted char (col 3)"
10497 );
10498 }
10499
10500 #[test]
10503 fn after_z_zz_sets_viewport_pinned() {
10504 let mut e = editor_with("a\nb\nc\nd\ne");
10505 e.jump_cursor(2, 0);
10506 e.after_z('z', 1);
10507 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10508 }
10509
10510 #[test]
10511 fn after_z_zo_opens_fold_at_cursor() {
10512 let mut e = editor_with("a\nb\nc\nd");
10513 e.buffer_mut().add_fold(1, 2, true);
10514 e.jump_cursor(1, 0);
10515 e.after_z('o', 1);
10516 assert!(
10517 !e.buffer().folds()[0].closed,
10518 "zo must open the fold at the cursor row"
10519 );
10520 }
10521
10522 #[test]
10523 fn after_z_zm_closes_all_folds() {
10524 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10525 e.buffer_mut().add_fold(0, 1, false);
10526 e.buffer_mut().add_fold(4, 5, false);
10527 e.after_z('M', 1);
10528 assert!(
10529 e.buffer().folds().iter().all(|f| f.closed),
10530 "zM must close all folds"
10531 );
10532 }
10533
10534 #[test]
10535 fn after_z_zd_removes_fold_at_cursor() {
10536 let mut e = editor_with("a\nb\nc\nd");
10537 e.buffer_mut().add_fold(1, 2, true);
10538 e.jump_cursor(1, 0);
10539 e.after_z('d', 1);
10540 assert!(
10541 e.buffer().folds().is_empty(),
10542 "zd must remove the fold at the cursor row"
10543 );
10544 }
10545
10546 #[test]
10547 fn after_z_zf_in_visual_creates_fold() {
10548 let mut e = editor_with("a\nb\nc\nd\ne");
10549 e.jump_cursor(1, 0);
10551 run_keys(&mut e, "V2j");
10552 e.after_z('f', 1);
10554 let folds = e.buffer().folds();
10555 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10556 assert_eq!(folds[0].start_row, 1);
10557 assert_eq!(folds[0].end_row, 3);
10558 assert!(folds[0].closed);
10559 }
10560
10561 #[test]
10564 fn apply_op_motion_dw_deletes_word() {
10565 let mut e = editor_with("hello world");
10567 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
10568 assert_eq!(
10569 e.buffer().lines().first().cloned().unwrap_or_default(),
10570 "world"
10571 );
10572 }
10573
10574 #[test]
10575 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
10576 let mut e = editor_with("hello world");
10578 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
10579 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10582 assert!(
10583 line.starts_with(' ') || line == " world",
10584 "cw quirk: got {line:?}"
10585 );
10586 assert_eq!(e.vim_mode(), VimMode::Insert);
10587 }
10588
10589 #[test]
10590 fn apply_op_double_dd_deletes_line() {
10591 let mut e = editor_with("line1\nline2\nline3");
10592 e.apply_op_double(crate::vim::Operator::Delete, 1);
10594 let lines: Vec<_> = e.buffer().lines().to_vec();
10595 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
10596 }
10597
10598 #[test]
10599 fn apply_op_double_yy_does_not_modify_buffer() {
10600 let mut e = editor_with("hello");
10601 e.apply_op_double(crate::vim::Operator::Yank, 1);
10602 assert_eq!(
10603 e.buffer().lines().first().cloned().unwrap_or_default(),
10604 "hello"
10605 );
10606 }
10607
10608 #[test]
10609 fn enter_op_text_obj_sets_pending() {
10610 let mut e = editor_with("hello world");
10611 e.enter_op_text_obj(crate::vim::Operator::Delete, 1, true);
10612 assert!(e.is_chord_pending(), "OpTextObj should set chord pending");
10613 }
10614
10615 #[test]
10616 fn enter_op_g_sets_pending() {
10617 let mut e = editor_with("hello world");
10618 e.enter_op_g(crate::vim::Operator::Delete, 1);
10619 assert!(e.is_chord_pending(), "OpG should set chord pending");
10620 }
10621
10622 #[test]
10623 fn enter_op_find_sets_pending() {
10624 let mut e = editor_with("hello world");
10625 e.enter_op_find(crate::vim::Operator::Delete, 1, true, false);
10626 assert!(e.is_chord_pending(), "OpFind should set chord pending");
10627 }
10628
10629 #[test]
10630 fn apply_op_double_dd_count2_deletes_two_lines() {
10631 let mut e = editor_with("line1\nline2\nline3");
10632 e.apply_op_double(crate::vim::Operator::Delete, 2);
10633 let lines: Vec<_> = e.buffer().lines().to_vec();
10634 assert_eq!(lines, vec!["line3"], "2dd should delete two lines");
10635 }
10636
10637 #[test]
10638 fn apply_op_motion_unknown_key_is_noop() {
10639 let mut e = editor_with("hello");
10641 let before = e.cursor();
10642 e.apply_op_motion(crate::vim::Operator::Delete, 'X', 1); assert_eq!(e.cursor(), before);
10644 assert_eq!(
10645 e.buffer().lines().first().cloned().unwrap_or_default(),
10646 "hello"
10647 );
10648 }
10649}