1use crate::VimMode;
75use crate::input::{Input, Key};
76
77use crate::buf_helpers::{
78 buf_cursor_pos, buf_line, buf_line_bytes, buf_line_chars, buf_lines_to_vec, buf_row_count,
79 buf_set_cursor_pos, buf_set_cursor_rc,
80};
81use crate::editor::Editor;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum Mode {
87 #[default]
88 Normal,
89 Insert,
90 Visual,
91 VisualLine,
92 VisualBlock,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Default)]
100enum Pending {
101 #[default]
102 None,
103 Op { op: Operator, count1: usize },
106 OpTextObj {
108 op: Operator,
109 count1: usize,
110 inner: bool,
111 },
112 OpG { op: Operator, count1: usize },
114 G,
116 Find { forward: bool, till: bool },
118 OpFind {
120 op: Operator,
121 count1: usize,
122 forward: bool,
123 till: bool,
124 },
125 Replace,
127 VisualTextObj { inner: bool },
130 Z,
132 SetMark,
134 GotoMarkLine,
137 GotoMarkChar,
140 SelectRegister,
143 RecordMacroTarget,
147 PlayMacroTarget { count: usize },
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum Operator {
157 Delete,
158 Change,
159 Yank,
160 Uppercase,
163 Lowercase,
165 ToggleCase,
169 Indent,
174 Outdent,
177 Fold,
181 Reflow,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum Motion {
190 Left,
191 Right,
192 Up,
193 Down,
194 WordFwd,
195 BigWordFwd,
196 WordBack,
197 BigWordBack,
198 WordEnd,
199 BigWordEnd,
200 WordEndBack,
202 BigWordEndBack,
204 LineStart,
205 FirstNonBlank,
206 LineEnd,
207 FileTop,
208 FileBottom,
209 Find {
210 ch: char,
211 forward: bool,
212 till: bool,
213 },
214 FindRepeat {
215 reverse: bool,
216 },
217 MatchBracket,
218 WordAtCursor {
219 forward: bool,
220 whole_word: bool,
223 },
224 SearchNext {
226 reverse: bool,
227 },
228 ViewportTop,
230 ViewportMiddle,
232 ViewportBottom,
234 LastNonBlank,
236 LineMiddle,
239 ParagraphPrev,
241 ParagraphNext,
243 SentencePrev,
245 SentenceNext,
247 ScreenDown,
250 ScreenUp,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum TextObject {
256 Word {
257 big: bool,
258 },
259 Quote(char),
260 Bracket(char),
261 Paragraph,
262 XmlTag,
266 Sentence,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum MotionKind {
276 Exclusive,
278 Inclusive,
280 Linewise,
282}
283
284#[derive(Debug, Clone)]
288enum LastChange {
289 OpMotion {
291 op: Operator,
292 motion: Motion,
293 count: usize,
294 inserted: Option<String>,
295 },
296 OpTextObj {
298 op: Operator,
299 obj: TextObject,
300 inner: bool,
301 inserted: Option<String>,
302 },
303 LineOp {
305 op: Operator,
306 count: usize,
307 inserted: Option<String>,
308 },
309 CharDel { forward: bool, count: usize },
311 ReplaceChar { ch: char, count: usize },
313 ToggleCase { count: usize },
315 JoinLine { count: usize },
317 Paste { before: bool, count: usize },
319 DeleteToEol { inserted: Option<String> },
321 OpenLine { above: bool, inserted: String },
323 InsertAt {
325 entry: InsertEntry,
326 inserted: String,
327 count: usize,
328 },
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332enum InsertEntry {
333 I,
334 A,
335 ShiftI,
336 ShiftA,
337}
338
339#[derive(Default)]
342pub struct VimState {
343 mode: Mode,
344 pending: Pending,
345 count: usize,
346 last_find: Option<(char, bool, bool)>,
348 last_change: Option<LastChange>,
349 insert_session: Option<InsertSession>,
351 pub(super) visual_anchor: (usize, usize),
355 pub(super) visual_line_anchor: usize,
357 pub(super) block_anchor: (usize, usize),
360 pub(super) block_vcol: usize,
366 pub(super) yank_linewise: bool,
368 pub(super) pending_register: Option<char>,
371 pub(super) recording_macro: Option<char>,
375 pub(super) recording_keys: Vec<crate::input::Input>,
380 pub(super) replaying_macro: bool,
383 pub(super) last_macro: Option<char>,
385 pub(super) last_edit_pos: Option<(usize, usize)>,
388 pub(super) last_insert_pos: Option<(usize, usize)>,
392 pub(super) change_list: Vec<(usize, usize)>,
396 pub(super) change_list_cursor: Option<usize>,
399 pub(super) last_visual: Option<LastVisual>,
402 pub(super) viewport_pinned: bool,
406 replaying: bool,
408 one_shot_normal: bool,
411 pub(super) search_prompt: Option<SearchPrompt>,
413 pub(super) last_search: Option<String>,
417 pub(super) last_search_forward: bool,
421 pub(super) jump_back: Vec<(usize, usize)>,
426 pub(super) jump_fwd: Vec<(usize, usize)>,
429 pub(super) insert_pending_register: bool,
433 pub(super) change_mark_start: Option<(usize, usize)>,
439 pub(super) search_history: Vec<String>,
443 pub(super) search_history_cursor: Option<usize>,
448 pub(super) last_input_at: Option<std::time::Instant>,
457 pub(super) last_input_host_at: Option<core::time::Duration>,
461}
462
463const SEARCH_HISTORY_MAX: usize = 100;
464pub(crate) const CHANGE_LIST_MAX: usize = 100;
465
466#[derive(Debug, Clone)]
469pub struct SearchPrompt {
470 pub text: String,
471 pub cursor: usize,
472 pub forward: bool,
473}
474
475#[derive(Debug, Clone)]
476struct InsertSession {
477 count: usize,
478 row_min: usize,
480 row_max: usize,
481 before_lines: Vec<String>,
485 reason: InsertReason,
486}
487
488#[derive(Debug, Clone)]
489enum InsertReason {
490 Enter(InsertEntry),
492 Open { above: bool },
494 AfterChange,
497 DeleteToEol,
499 ReplayOnly,
502 BlockEdge { top: usize, bot: usize, col: usize },
506 BlockChange { top: usize, bot: usize, col: usize },
511 Replace,
515}
516
517#[derive(Debug, Clone, Copy)]
527pub(super) struct LastVisual {
528 pub mode: Mode,
529 pub anchor: (usize, usize),
530 pub cursor: (usize, usize),
531 pub block_vcol: usize,
532}
533
534impl VimState {
535 pub fn public_mode(&self) -> VimMode {
536 match self.mode {
537 Mode::Normal => VimMode::Normal,
538 Mode::Insert => VimMode::Insert,
539 Mode::Visual => VimMode::Visual,
540 Mode::VisualLine => VimMode::VisualLine,
541 Mode::VisualBlock => VimMode::VisualBlock,
542 }
543 }
544
545 pub fn force_normal(&mut self) {
546 self.mode = Mode::Normal;
547 self.pending = Pending::None;
548 self.count = 0;
549 self.insert_session = None;
550 }
551
552 pub(crate) fn clear_pending_prefix(&mut self) {
562 self.pending = Pending::None;
563 self.count = 0;
564 self.pending_register = None;
565 self.insert_pending_register = false;
566 }
567
568 pub fn is_visual(&self) -> bool {
569 matches!(
570 self.mode,
571 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
572 )
573 }
574
575 pub fn is_visual_char(&self) -> bool {
576 self.mode == Mode::Visual
577 }
578
579 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
580 self.visual_anchor = anchor;
581 self.mode = Mode::Visual;
582 }
583
584 pub(crate) fn pending_count_val(&self) -> Option<u32> {
587 if self.count == 0 {
588 None
589 } else {
590 Some(self.count as u32)
591 }
592 }
593
594 pub(crate) fn is_chord_pending(&self) -> bool {
597 !matches!(self.pending, Pending::None)
598 }
599
600 pub(crate) fn pending_op_char(&self) -> Option<char> {
604 let op = match &self.pending {
605 Pending::Op { op, .. }
606 | Pending::OpTextObj { op, .. }
607 | Pending::OpG { op, .. }
608 | Pending::OpFind { op, .. } => Some(*op),
609 _ => None,
610 };
611 op.map(|o| match o {
612 Operator::Delete => 'd',
613 Operator::Change => 'c',
614 Operator::Yank => 'y',
615 Operator::Uppercase => 'U',
616 Operator::Lowercase => 'u',
617 Operator::ToggleCase => '~',
618 Operator::Indent => '>',
619 Operator::Outdent => '<',
620 Operator::Fold => 'z',
621 Operator::Reflow => 'q',
622 })
623 }
624}
625
626fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
632 ed.vim.search_prompt = Some(SearchPrompt {
633 text: String::new(),
634 cursor: 0,
635 forward,
636 });
637 ed.vim.search_history_cursor = None;
638 ed.set_search_pattern(None);
642}
643
644fn push_search_pattern<H: crate::types::Host>(
649 ed: &mut Editor<hjkl_buffer::Buffer, H>,
650 pattern: &str,
651) {
652 let compiled = if pattern.is_empty() {
653 None
654 } else {
655 let case_insensitive = ed.settings().ignore_case
662 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
663 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
664 std::borrow::Cow::Owned(format!("(?i){pattern}"))
665 } else {
666 std::borrow::Cow::Borrowed(pattern)
667 };
668 regex::Regex::new(&effective).ok()
669 };
670 let wrap = ed.settings().wrapscan;
671 ed.set_search_pattern(compiled);
675 ed.search_state_mut().wrap_around = wrap;
676}
677
678fn step_search_prompt<H: crate::types::Host>(
679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
680 input: Input,
681) -> bool {
682 let history_dir = match (input.key, input.ctrl) {
686 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
687 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
688 _ => None,
689 };
690 if let Some(dir) = history_dir {
691 walk_search_history(ed, dir);
692 return true;
693 }
694 match input.key {
695 Key::Esc => {
696 let text = ed
699 .vim
700 .search_prompt
701 .take()
702 .map(|p| p.text)
703 .unwrap_or_default();
704 if !text.is_empty() {
705 ed.vim.last_search = Some(text);
706 }
707 ed.vim.search_history_cursor = None;
708 }
709 Key::Enter => {
710 let prompt = ed.vim.search_prompt.take();
711 if let Some(p) = prompt {
712 let pattern = if p.text.is_empty() {
715 ed.vim.last_search.clone()
716 } else {
717 Some(p.text.clone())
718 };
719 if let Some(pattern) = pattern {
720 push_search_pattern(ed, &pattern);
721 let pre = ed.cursor();
722 if p.forward {
723 ed.search_advance_forward(true);
724 } else {
725 ed.search_advance_backward(true);
726 }
727 ed.push_buffer_cursor_to_textarea();
728 if ed.cursor() != pre {
729 push_jump(ed, pre);
730 }
731 record_search_history(ed, &pattern);
732 ed.vim.last_search = Some(pattern);
733 ed.vim.last_search_forward = p.forward;
734 }
735 }
736 ed.vim.search_history_cursor = None;
737 }
738 Key::Backspace => {
739 ed.vim.search_history_cursor = None;
740 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
741 if p.text.pop().is_some() {
742 p.cursor = p.text.chars().count();
743 Some(p.text.clone())
744 } else {
745 None
746 }
747 });
748 if let Some(text) = new_text {
749 push_search_pattern(ed, &text);
750 }
751 }
752 Key::Char(c) => {
753 ed.vim.search_history_cursor = None;
754 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
755 p.text.push(c);
756 p.cursor = p.text.chars().count();
757 p.text.clone()
758 });
759 if let Some(text) = new_text {
760 push_search_pattern(ed, &text);
761 }
762 }
763 _ => {}
764 }
765 true
766}
767
768fn walk_change_list<H: crate::types::Host>(
772 ed: &mut Editor<hjkl_buffer::Buffer, H>,
773 dir: isize,
774 count: usize,
775) {
776 if ed.vim.change_list.is_empty() {
777 return;
778 }
779 let len = ed.vim.change_list.len();
780 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
781 (None, -1) => len as isize - 1,
782 (None, 1) => return, (Some(i), -1) => i as isize - 1,
784 (Some(i), 1) => i as isize + 1,
785 _ => return,
786 };
787 for _ in 1..count {
788 let next = idx + dir;
789 if next < 0 || next >= len as isize {
790 break;
791 }
792 idx = next;
793 }
794 if idx < 0 || idx >= len as isize {
795 return;
796 }
797 let idx = idx as usize;
798 ed.vim.change_list_cursor = Some(idx);
799 let (row, col) = ed.vim.change_list[idx];
800 ed.jump_cursor(row, col);
801}
802
803fn record_search_history<H: crate::types::Host>(
807 ed: &mut Editor<hjkl_buffer::Buffer, H>,
808 pattern: &str,
809) {
810 if pattern.is_empty() {
811 return;
812 }
813 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
814 return;
815 }
816 ed.vim.search_history.push(pattern.to_string());
817 let len = ed.vim.search_history.len();
818 if len > SEARCH_HISTORY_MAX {
819 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
820 }
821}
822
823fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
829 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
830 return;
831 }
832 let len = ed.vim.search_history.len();
833 let next_idx = match (ed.vim.search_history_cursor, dir) {
834 (None, -1) => Some(len - 1),
835 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
837 (Some(i), 1) if i + 1 < len => Some(i + 1),
838 _ => None,
839 };
840 let Some(idx) = next_idx else {
841 return;
842 };
843 ed.vim.search_history_cursor = Some(idx);
844 let text = ed.vim.search_history[idx].clone();
845 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
846 prompt.cursor = text.chars().count();
847 prompt.text = text.clone();
848 }
849 push_search_pattern(ed, &text);
850}
851
852pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
853 ed.sync_buffer_content_from_textarea();
858 let now = std::time::Instant::now();
866 let host_now = ed.host.now();
867 let timed_out = match ed.vim.last_input_host_at {
868 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
869 None => false,
870 };
871 if timed_out {
872 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
873 || ed.vim.count != 0
874 || ed.vim.pending_register.is_some()
875 || ed.vim.insert_pending_register;
876 if chord_in_flight {
877 ed.vim.clear_pending_prefix();
878 }
879 }
880 ed.vim.last_input_at = Some(now);
881 ed.vim.last_input_host_at = Some(host_now);
882 if ed.vim.recording_macro.is_some()
887 && !ed.vim.replaying_macro
888 && matches!(ed.vim.pending, Pending::None)
889 && ed.vim.mode != Mode::Insert
890 && input.key == Key::Char('q')
891 && !input.ctrl
892 && !input.alt
893 {
894 let reg = ed.vim.recording_macro.take().unwrap();
895 let keys = std::mem::take(&mut ed.vim.recording_keys);
896 let text = crate::input::encode_macro(&keys);
897 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
898 return true;
899 }
900 if ed.vim.search_prompt.is_some() {
902 return step_search_prompt(ed, input);
903 }
904 let pending_was_macro_chord = matches!(
908 ed.vim.pending,
909 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
910 );
911 let was_insert = ed.vim.mode == Mode::Insert;
912 let pre_visual_snapshot = match ed.vim.mode {
915 Mode::Visual => Some(LastVisual {
916 mode: Mode::Visual,
917 anchor: ed.vim.visual_anchor,
918 cursor: ed.cursor(),
919 block_vcol: 0,
920 }),
921 Mode::VisualLine => Some(LastVisual {
922 mode: Mode::VisualLine,
923 anchor: (ed.vim.visual_line_anchor, 0),
924 cursor: ed.cursor(),
925 block_vcol: 0,
926 }),
927 Mode::VisualBlock => Some(LastVisual {
928 mode: Mode::VisualBlock,
929 anchor: ed.vim.block_anchor,
930 cursor: ed.cursor(),
931 block_vcol: ed.vim.block_vcol,
932 }),
933 _ => None,
934 };
935 let consumed = match ed.vim.mode {
936 Mode::Insert => step_insert(ed, input),
937 _ => step_normal(ed, input),
938 };
939 if let Some(snap) = pre_visual_snapshot
940 && !matches!(
941 ed.vim.mode,
942 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
943 )
944 {
945 let (lo, hi) = match snap.mode {
961 Mode::Visual => {
962 if snap.anchor <= snap.cursor {
963 (snap.anchor, snap.cursor)
964 } else {
965 (snap.cursor, snap.anchor)
966 }
967 }
968 Mode::VisualLine => {
969 let r_lo = snap.anchor.0.min(snap.cursor.0);
970 let r_hi = snap.anchor.0.max(snap.cursor.0);
971 let last_col = ed
972 .buffer()
973 .lines()
974 .get(r_hi)
975 .map(|l| l.chars().count().saturating_sub(1))
976 .unwrap_or(0);
977 ((r_lo, 0), (r_hi, last_col))
978 }
979 Mode::VisualBlock => {
980 let (r1, c1) = snap.anchor;
981 let (r2, c2) = snap.cursor;
982 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
983 }
984 _ => {
985 if snap.anchor <= snap.cursor {
988 (snap.anchor, snap.cursor)
989 } else {
990 (snap.cursor, snap.anchor)
991 }
992 }
993 };
994 ed.set_mark('<', lo);
995 ed.set_mark('>', hi);
996 ed.vim.last_visual = Some(snap);
997 }
998 if !was_insert
1002 && ed.vim.one_shot_normal
1003 && ed.vim.mode == Mode::Normal
1004 && matches!(ed.vim.pending, Pending::None)
1005 {
1006 ed.vim.one_shot_normal = false;
1007 ed.vim.mode = Mode::Insert;
1008 }
1009 ed.sync_buffer_content_from_textarea();
1015 if !ed.vim.viewport_pinned {
1019 ed.ensure_cursor_in_scrolloff();
1020 }
1021 ed.vim.viewport_pinned = false;
1022 if ed.vim.recording_macro.is_some()
1027 && !ed.vim.replaying_macro
1028 && input.key != Key::Char('q')
1029 && !pending_was_macro_chord
1030 {
1031 ed.vim.recording_keys.push(input);
1032 }
1033 consumed
1034}
1035
1036fn step_insert<H: crate::types::Host>(
1039 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1040 input: Input,
1041) -> bool {
1042 if ed.vim.insert_pending_register {
1046 ed.vim.insert_pending_register = false;
1047 if let Key::Char(c) = input.key
1048 && !input.ctrl
1049 {
1050 insert_register_text(ed, c);
1051 }
1052 return true;
1053 }
1054
1055 if input.key == Key::Esc {
1056 finish_insert_session(ed);
1057 ed.vim.mode = Mode::Normal;
1058 let col = ed.cursor().1;
1063 ed.vim.last_insert_pos = Some(ed.cursor());
1067 if col > 0 {
1068 crate::motions::move_left(&mut ed.buffer, 1);
1069 ed.push_buffer_cursor_to_textarea();
1070 }
1071 ed.sticky_col = Some(ed.cursor().1);
1072 return true;
1073 }
1074
1075 if input.ctrl {
1077 match input.key {
1078 Key::Char('w') => {
1079 use hjkl_buffer::{Edit, MotionKind};
1080 ed.sync_buffer_content_from_textarea();
1081 let cursor = buf_cursor_pos(&ed.buffer);
1082 if cursor.row == 0 && cursor.col == 0 {
1083 return true;
1084 }
1085 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1088 let word_start = buf_cursor_pos(&ed.buffer);
1089 if word_start == cursor {
1090 return true;
1091 }
1092 buf_set_cursor_pos(&mut ed.buffer, cursor);
1093 ed.mutate_edit(Edit::DeleteRange {
1094 start: word_start,
1095 end: cursor,
1096 kind: MotionKind::Char,
1097 });
1098 ed.push_buffer_cursor_to_textarea();
1099 return true;
1100 }
1101 Key::Char('u') => {
1102 use hjkl_buffer::{Edit, MotionKind, Position};
1103 ed.sync_buffer_content_from_textarea();
1104 let cursor = buf_cursor_pos(&ed.buffer);
1105 if cursor.col > 0 {
1106 ed.mutate_edit(Edit::DeleteRange {
1107 start: Position::new(cursor.row, 0),
1108 end: cursor,
1109 kind: MotionKind::Char,
1110 });
1111 ed.push_buffer_cursor_to_textarea();
1112 }
1113 return true;
1114 }
1115 Key::Char('h') => {
1116 use hjkl_buffer::{Edit, MotionKind, Position};
1117 ed.sync_buffer_content_from_textarea();
1118 let cursor = buf_cursor_pos(&ed.buffer);
1119 if cursor.col > 0 {
1120 ed.mutate_edit(Edit::DeleteRange {
1121 start: Position::new(cursor.row, cursor.col - 1),
1122 end: cursor,
1123 kind: MotionKind::Char,
1124 });
1125 } else if cursor.row > 0 {
1126 let prev_row = cursor.row - 1;
1127 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1128 ed.mutate_edit(Edit::JoinLines {
1129 row: prev_row,
1130 count: 1,
1131 with_space: false,
1132 });
1133 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1134 }
1135 ed.push_buffer_cursor_to_textarea();
1136 return true;
1137 }
1138 Key::Char('o') => {
1139 ed.vim.one_shot_normal = true;
1142 ed.vim.mode = Mode::Normal;
1143 return true;
1144 }
1145 Key::Char('r') => {
1146 ed.vim.insert_pending_register = true;
1149 return true;
1150 }
1151 Key::Char('t') => {
1152 let (row, col) = ed.cursor();
1157 let sw = ed.settings().shiftwidth;
1158 indent_rows(ed, row, row, 1);
1159 ed.jump_cursor(row, col + sw);
1160 return true;
1161 }
1162 Key::Char('d') => {
1163 let (row, col) = ed.cursor();
1167 let before_len = buf_line_bytes(&ed.buffer, row);
1168 outdent_rows(ed, row, row, 1);
1169 let after_len = buf_line_bytes(&ed.buffer, row);
1170 let stripped = before_len.saturating_sub(after_len);
1171 let new_col = col.saturating_sub(stripped);
1172 ed.jump_cursor(row, new_col);
1173 return true;
1174 }
1175 _ => {}
1176 }
1177 }
1178
1179 let (row, _) = ed.cursor();
1182 if let Some(ref mut session) = ed.vim.insert_session {
1183 session.row_min = session.row_min.min(row);
1184 session.row_max = session.row_max.max(row);
1185 }
1186 let mutated = handle_insert_key(ed, input);
1187 if mutated {
1188 ed.mark_content_dirty();
1189 let (row, _) = ed.cursor();
1190 if let Some(ref mut session) = ed.vim.insert_session {
1191 session.row_min = session.row_min.min(row);
1192 session.row_max = session.row_max.max(row);
1193 }
1194 }
1195 true
1196}
1197
1198fn insert_register_text<H: crate::types::Host>(
1203 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1204 selector: char,
1205) {
1206 use hjkl_buffer::Edit;
1207 let text = match ed.registers().read(selector) {
1208 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1209 _ => return,
1210 };
1211 ed.sync_buffer_content_from_textarea();
1212 let cursor = buf_cursor_pos(&ed.buffer);
1213 ed.mutate_edit(Edit::InsertStr {
1214 at: cursor,
1215 text: text.clone(),
1216 });
1217 let mut row = cursor.row;
1220 let mut col = cursor.col;
1221 for ch in text.chars() {
1222 if ch == '\n' {
1223 row += 1;
1224 col = 0;
1225 } else {
1226 col += 1;
1227 }
1228 }
1229 buf_set_cursor_rc(&mut ed.buffer, row, col);
1230 ed.push_buffer_cursor_to_textarea();
1231 ed.mark_content_dirty();
1232 if let Some(ref mut session) = ed.vim.insert_session {
1233 session.row_min = session.row_min.min(row);
1234 session.row_max = session.row_max.max(row);
1235 }
1236}
1237
1238pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1257 if !settings.autoindent {
1258 return String::new();
1259 }
1260 let base: String = prev_line
1262 .chars()
1263 .take_while(|c| *c == ' ' || *c == '\t')
1264 .collect();
1265
1266 if settings.smartindent {
1267 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1271 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1272 let unit = if settings.expandtab {
1273 if settings.softtabstop > 0 {
1274 " ".repeat(settings.softtabstop)
1275 } else {
1276 " ".repeat(settings.shiftwidth)
1277 }
1278 } else {
1279 "\t".to_string()
1280 };
1281 return format!("{base}{unit}");
1282 }
1283 }
1284
1285 base
1286}
1287
1288fn try_dedent_close_bracket<H: crate::types::Host>(
1298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1299 cursor: hjkl_buffer::Position,
1300 ch: char,
1301) -> bool {
1302 use hjkl_buffer::{Edit, MotionKind, Position};
1303
1304 if !ed.settings.smartindent {
1305 return false;
1306 }
1307 if !matches!(ch, '}' | ')' | ']') {
1308 return false;
1309 }
1310
1311 let line = match buf_line(&ed.buffer, cursor.row) {
1312 Some(l) => l.to_string(),
1313 None => return false,
1314 };
1315
1316 let before: String = line.chars().take(cursor.col).collect();
1318 if !before.chars().all(|c| c == ' ' || c == '\t') {
1319 return false;
1320 }
1321 if before.is_empty() {
1322 return false;
1324 }
1325
1326 let unit_len: usize = if ed.settings.expandtab {
1328 if ed.settings.softtabstop > 0 {
1329 ed.settings.softtabstop
1330 } else {
1331 ed.settings.shiftwidth
1332 }
1333 } else {
1334 1
1336 };
1337
1338 let strip_len = if ed.settings.expandtab {
1340 let spaces = before.chars().filter(|c| *c == ' ').count();
1342 if spaces < unit_len {
1343 return false;
1344 }
1345 unit_len
1346 } else {
1347 if !before.starts_with('\t') {
1349 return false;
1350 }
1351 1
1352 };
1353
1354 ed.mutate_edit(Edit::DeleteRange {
1356 start: Position::new(cursor.row, 0),
1357 end: Position::new(cursor.row, strip_len),
1358 kind: MotionKind::Char,
1359 });
1360 let new_col = cursor.col.saturating_sub(strip_len);
1365 ed.mutate_edit(Edit::InsertChar {
1366 at: Position::new(cursor.row, new_col),
1367 ch,
1368 });
1369 true
1370}
1371
1372fn handle_insert_key<H: crate::types::Host>(
1379 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1380 input: Input,
1381) -> bool {
1382 use hjkl_buffer::{Edit, MotionKind, Position};
1383 ed.sync_buffer_content_from_textarea();
1384 let cursor = buf_cursor_pos(&ed.buffer);
1385 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1386 let in_replace = matches!(
1390 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1391 Some(InsertReason::Replace)
1392 );
1393 let mutated = match input.key {
1394 Key::Char(c) if in_replace && cursor.col < line_chars => {
1395 ed.mutate_edit(Edit::DeleteRange {
1396 start: cursor,
1397 end: Position::new(cursor.row, cursor.col + 1),
1398 kind: MotionKind::Char,
1399 });
1400 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1401 true
1402 }
1403 Key::Char(c) => {
1404 if !try_dedent_close_bracket(ed, cursor, c) {
1405 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1406 }
1407 true
1408 }
1409 Key::Enter => {
1410 let prev_line = buf_line(&ed.buffer, cursor.row)
1411 .unwrap_or_default()
1412 .to_string();
1413 let indent = compute_enter_indent(&ed.settings, &prev_line);
1414 let text = format!("\n{indent}");
1415 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1416 true
1417 }
1418 Key::Tab => {
1419 if ed.settings.expandtab {
1420 let sts = ed.settings.softtabstop;
1423 let n = if sts > 0 {
1424 sts - (cursor.col % sts)
1425 } else {
1426 ed.settings.tabstop.max(1)
1427 };
1428 ed.mutate_edit(Edit::InsertStr {
1429 at: cursor,
1430 text: " ".repeat(n),
1431 });
1432 } else {
1433 ed.mutate_edit(Edit::InsertChar {
1434 at: cursor,
1435 ch: '\t',
1436 });
1437 }
1438 true
1439 }
1440 Key::Backspace => {
1441 let sts = ed.settings.softtabstop;
1445 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1446 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1447 let chars: Vec<char> = line.chars().collect();
1448 let run_start = cursor.col - sts;
1449 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1450 ed.mutate_edit(Edit::DeleteRange {
1451 start: Position::new(cursor.row, run_start),
1452 end: cursor,
1453 kind: MotionKind::Char,
1454 });
1455 return true;
1456 }
1457 }
1458 if cursor.col > 0 {
1459 ed.mutate_edit(Edit::DeleteRange {
1460 start: Position::new(cursor.row, cursor.col - 1),
1461 end: cursor,
1462 kind: MotionKind::Char,
1463 });
1464 true
1465 } else if cursor.row > 0 {
1466 let prev_row = cursor.row - 1;
1467 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1468 ed.mutate_edit(Edit::JoinLines {
1469 row: prev_row,
1470 count: 1,
1471 with_space: false,
1472 });
1473 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1474 true
1475 } else {
1476 false
1477 }
1478 }
1479 Key::Delete => {
1480 if cursor.col < line_chars {
1481 ed.mutate_edit(Edit::DeleteRange {
1482 start: cursor,
1483 end: Position::new(cursor.row, cursor.col + 1),
1484 kind: MotionKind::Char,
1485 });
1486 true
1487 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1488 ed.mutate_edit(Edit::JoinLines {
1489 row: cursor.row,
1490 count: 1,
1491 with_space: false,
1492 });
1493 buf_set_cursor_pos(&mut ed.buffer, cursor);
1494 true
1495 } else {
1496 false
1497 }
1498 }
1499 Key::Left => {
1500 crate::motions::move_left(&mut ed.buffer, 1);
1501 break_undo_group_in_insert(ed);
1502 false
1503 }
1504 Key::Right => {
1505 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1508 break_undo_group_in_insert(ed);
1509 false
1510 }
1511 Key::Up => {
1512 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1513 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1514 break_undo_group_in_insert(ed);
1515 false
1516 }
1517 Key::Down => {
1518 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1519 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1520 break_undo_group_in_insert(ed);
1521 false
1522 }
1523 Key::Home => {
1524 crate::motions::move_line_start(&mut ed.buffer);
1525 break_undo_group_in_insert(ed);
1526 false
1527 }
1528 Key::End => {
1529 crate::motions::move_line_end(&mut ed.buffer);
1530 break_undo_group_in_insert(ed);
1531 false
1532 }
1533 Key::PageUp => {
1534 let rows = viewport_full_rows(ed, 1) as isize;
1538 scroll_cursor_rows(ed, -rows);
1539 return false;
1540 }
1541 Key::PageDown => {
1542 let rows = viewport_full_rows(ed, 1) as isize;
1543 scroll_cursor_rows(ed, rows);
1544 return false;
1545 }
1546 _ => false,
1549 };
1550 ed.push_buffer_cursor_to_textarea();
1551 mutated
1552}
1553
1554fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1555 let Some(session) = ed.vim.insert_session.take() else {
1556 return;
1557 };
1558 let lines = buf_lines_to_vec(&ed.buffer);
1559 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1563 let before_end = session
1564 .row_max
1565 .min(session.before_lines.len().saturating_sub(1));
1566 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1567 session.before_lines[session.row_min..=before_end].join("\n")
1568 } else {
1569 String::new()
1570 };
1571 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1572 lines[session.row_min..=after_end].join("\n")
1573 } else {
1574 String::new()
1575 };
1576 let inserted = extract_inserted(&before, &after);
1577 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1578 use hjkl_buffer::{Edit, Position};
1579 for _ in 0..session.count - 1 {
1580 let (row, col) = ed.cursor();
1581 ed.mutate_edit(Edit::InsertStr {
1582 at: Position::new(row, col),
1583 text: inserted.clone(),
1584 });
1585 }
1586 }
1587 fn replicate_block_text<H: crate::types::Host>(
1591 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1592 inserted: &str,
1593 top: usize,
1594 bot: usize,
1595 col: usize,
1596 ) {
1597 use hjkl_buffer::{Edit, Position};
1598 for r in (top + 1)..=bot {
1599 let line_len = buf_line_chars(&ed.buffer, r);
1600 if col > line_len {
1601 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1602 ed.mutate_edit(Edit::InsertStr {
1603 at: Position::new(r, line_len),
1604 text: pad,
1605 });
1606 }
1607 ed.mutate_edit(Edit::InsertStr {
1608 at: Position::new(r, col),
1609 text: inserted.to_string(),
1610 });
1611 }
1612 }
1613
1614 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1615 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1618 replicate_block_text(ed, &inserted, top, bot, col);
1619 buf_set_cursor_rc(&mut ed.buffer, top, col);
1620 ed.push_buffer_cursor_to_textarea();
1621 }
1622 return;
1623 }
1624 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1625 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1629 replicate_block_text(ed, &inserted, top, bot, col);
1630 let ins_chars = inserted.chars().count();
1631 let line_len = buf_line_chars(&ed.buffer, top);
1632 let target_col = (col + ins_chars).min(line_len);
1633 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1634 ed.push_buffer_cursor_to_textarea();
1635 }
1636 return;
1637 }
1638 if ed.vim.replaying {
1639 return;
1640 }
1641 match session.reason {
1642 InsertReason::Enter(entry) => {
1643 ed.vim.last_change = Some(LastChange::InsertAt {
1644 entry,
1645 inserted,
1646 count: session.count,
1647 });
1648 }
1649 InsertReason::Open { above } => {
1650 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1651 }
1652 InsertReason::AfterChange => {
1653 if let Some(
1654 LastChange::OpMotion { inserted: ins, .. }
1655 | LastChange::OpTextObj { inserted: ins, .. }
1656 | LastChange::LineOp { inserted: ins, .. },
1657 ) = ed.vim.last_change.as_mut()
1658 {
1659 *ins = Some(inserted);
1660 }
1661 if let Some(start) = ed.vim.change_mark_start.take() {
1667 let end = ed.cursor();
1668 ed.set_mark('[', start);
1669 ed.set_mark(']', end);
1670 }
1671 }
1672 InsertReason::DeleteToEol => {
1673 ed.vim.last_change = Some(LastChange::DeleteToEol {
1674 inserted: Some(inserted),
1675 });
1676 }
1677 InsertReason::ReplayOnly => {}
1678 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1679 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1680 InsertReason::Replace => {
1681 ed.vim.last_change = Some(LastChange::DeleteToEol {
1686 inserted: Some(inserted),
1687 });
1688 }
1689 }
1690}
1691
1692fn begin_insert<H: crate::types::Host>(
1693 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1694 count: usize,
1695 reason: InsertReason,
1696) {
1697 let record = !matches!(reason, InsertReason::ReplayOnly);
1698 if record {
1699 ed.push_undo();
1700 }
1701 let reason = if ed.vim.replaying {
1702 InsertReason::ReplayOnly
1703 } else {
1704 reason
1705 };
1706 let (row, _) = ed.cursor();
1707 ed.vim.insert_session = Some(InsertSession {
1708 count,
1709 row_min: row,
1710 row_max: row,
1711 before_lines: buf_lines_to_vec(&ed.buffer),
1712 reason,
1713 });
1714 ed.vim.mode = Mode::Insert;
1715}
1716
1717pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1732 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1733) {
1734 if !ed.settings.undo_break_on_motion {
1735 return;
1736 }
1737 if ed.vim.replaying {
1738 return;
1739 }
1740 if ed.vim.insert_session.is_none() {
1741 return;
1742 }
1743 ed.push_undo();
1744 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1745 let mut lines: Vec<String> = Vec::with_capacity(n);
1746 for r in 0..n {
1747 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1748 }
1749 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1750 if let Some(ref mut session) = ed.vim.insert_session {
1751 session.before_lines = lines;
1752 session.row_min = row;
1753 session.row_max = row;
1754 }
1755}
1756
1757fn step_normal<H: crate::types::Host>(
1760 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1761 input: Input,
1762) -> bool {
1763 if let Key::Char(d @ '0'..='9') = input.key
1765 && !input.ctrl
1766 && !input.alt
1767 && !matches!(
1768 ed.vim.pending,
1769 Pending::Replace
1770 | Pending::Find { .. }
1771 | Pending::OpFind { .. }
1772 | Pending::VisualTextObj { .. }
1773 )
1774 && (d != '0' || ed.vim.count > 0)
1775 {
1776 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1777 return true;
1778 }
1779
1780 match std::mem::take(&mut ed.vim.pending) {
1782 Pending::Replace => return handle_replace(ed, input),
1783 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1784 Pending::OpFind {
1785 op,
1786 count1,
1787 forward,
1788 till,
1789 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1790 Pending::G => return handle_after_g(ed, input),
1791 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1792 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1793 Pending::OpTextObj { op, count1, inner } => {
1794 return handle_text_object(ed, input, op, count1, inner);
1795 }
1796 Pending::VisualTextObj { inner } => {
1797 return handle_visual_text_obj(ed, input, inner);
1798 }
1799 Pending::Z => return handle_after_z(ed, input),
1800 Pending::SetMark => return handle_set_mark(ed, input),
1801 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1802 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1803 Pending::SelectRegister => return handle_select_register(ed, input),
1804 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1805 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1806 Pending::None => {}
1807 }
1808
1809 let count = take_count(&mut ed.vim);
1810
1811 match input.key {
1813 Key::Esc => {
1814 ed.vim.force_normal();
1815 return true;
1816 }
1817 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1818 ed.vim.visual_anchor = ed.cursor();
1819 ed.vim.mode = Mode::Visual;
1820 return true;
1821 }
1822 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1823 let (row, _) = ed.cursor();
1824 ed.vim.visual_line_anchor = row;
1825 ed.vim.mode = Mode::VisualLine;
1826 return true;
1827 }
1828 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1829 ed.vim.visual_anchor = ed.cursor();
1830 ed.vim.mode = Mode::Visual;
1831 return true;
1832 }
1833 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1834 let (row, _) = ed.cursor();
1835 ed.vim.visual_line_anchor = row;
1836 ed.vim.mode = Mode::VisualLine;
1837 return true;
1838 }
1839 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1840 let cur = ed.cursor();
1841 ed.vim.block_anchor = cur;
1842 ed.vim.block_vcol = cur.1;
1843 ed.vim.mode = Mode::VisualBlock;
1844 return true;
1845 }
1846 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1847 ed.vim.mode = Mode::Normal;
1849 return true;
1850 }
1851 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1854 Mode::Visual => {
1855 let cur = ed.cursor();
1856 let anchor = ed.vim.visual_anchor;
1857 ed.vim.visual_anchor = cur;
1858 ed.jump_cursor(anchor.0, anchor.1);
1859 return true;
1860 }
1861 Mode::VisualLine => {
1862 let cur_row = ed.cursor().0;
1863 let anchor_row = ed.vim.visual_line_anchor;
1864 ed.vim.visual_line_anchor = cur_row;
1865 ed.jump_cursor(anchor_row, 0);
1866 return true;
1867 }
1868 Mode::VisualBlock => {
1869 let cur = ed.cursor();
1870 let anchor = ed.vim.block_anchor;
1871 ed.vim.block_anchor = cur;
1872 ed.vim.block_vcol = anchor.1;
1873 ed.jump_cursor(anchor.0, anchor.1);
1874 return true;
1875 }
1876 _ => {}
1877 },
1878 _ => {}
1879 }
1880
1881 if ed.vim.is_visual()
1883 && let Some(op) = visual_operator(&input)
1884 {
1885 apply_visual_operator(ed, op);
1886 return true;
1887 }
1888
1889 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1893 match input.key {
1894 Key::Char('r') => {
1895 ed.vim.pending = Pending::Replace;
1896 return true;
1897 }
1898 Key::Char('I') => {
1899 let (top, bot, left, _right) = block_bounds(ed);
1900 ed.jump_cursor(top, left);
1901 ed.vim.mode = Mode::Normal;
1902 begin_insert(
1903 ed,
1904 1,
1905 InsertReason::BlockEdge {
1906 top,
1907 bot,
1908 col: left,
1909 },
1910 );
1911 return true;
1912 }
1913 Key::Char('A') => {
1914 let (top, bot, _left, right) = block_bounds(ed);
1915 let line_len = buf_line_chars(&ed.buffer, top);
1916 let col = (right + 1).min(line_len);
1917 ed.jump_cursor(top, col);
1918 ed.vim.mode = Mode::Normal;
1919 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1920 return true;
1921 }
1922 _ => {}
1923 }
1924 }
1925
1926 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1928 && !input.ctrl
1929 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1930 {
1931 let inner = matches!(input.key, Key::Char('i'));
1932 ed.vim.pending = Pending::VisualTextObj { inner };
1933 return true;
1934 }
1935
1936 if input.ctrl
1941 && let Key::Char(c) = input.key
1942 {
1943 match c {
1944 'd' => {
1945 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1946 return true;
1947 }
1948 'u' => {
1949 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1950 return true;
1951 }
1952 'f' => {
1953 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1954 return true;
1955 }
1956 'b' => {
1957 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1958 return true;
1959 }
1960 'r' => {
1961 do_redo(ed);
1962 return true;
1963 }
1964 'a' if ed.vim.mode == Mode::Normal => {
1965 adjust_number(ed, count.max(1) as i64);
1966 return true;
1967 }
1968 'x' if ed.vim.mode == Mode::Normal => {
1969 adjust_number(ed, -(count.max(1) as i64));
1970 return true;
1971 }
1972 'o' if ed.vim.mode == Mode::Normal => {
1973 for _ in 0..count.max(1) {
1974 jump_back(ed);
1975 }
1976 return true;
1977 }
1978 'i' if ed.vim.mode == Mode::Normal => {
1979 for _ in 0..count.max(1) {
1980 jump_forward(ed);
1981 }
1982 return true;
1983 }
1984 _ => {}
1985 }
1986 }
1987
1988 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1990 for _ in 0..count.max(1) {
1991 jump_forward(ed);
1992 }
1993 return true;
1994 }
1995
1996 if let Some(motion) = parse_motion(&input) {
1998 execute_motion(ed, motion.clone(), count);
1999 if ed.vim.mode == Mode::VisualBlock {
2001 update_block_vcol(ed, &motion);
2002 }
2003 if let Motion::Find { ch, forward, till } = motion {
2004 ed.vim.last_find = Some((ch, forward, till));
2005 }
2006 return true;
2007 }
2008
2009 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
2011 return true;
2012 }
2013
2014 if ed.vim.mode == Mode::Normal
2016 && let Key::Char(op_ch) = input.key
2017 && !input.ctrl
2018 && let Some(op) = char_to_operator(op_ch)
2019 {
2020 ed.vim.pending = Pending::Op { op, count1: count };
2021 return true;
2022 }
2023
2024 if ed.vim.mode == Mode::Normal
2026 && let Some((forward, till)) = find_entry(&input)
2027 {
2028 ed.vim.count = count;
2029 ed.vim.pending = Pending::Find { forward, till };
2030 return true;
2031 }
2032
2033 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
2035 ed.vim.count = count;
2036 ed.vim.pending = Pending::G;
2037 return true;
2038 }
2039
2040 if !input.ctrl
2042 && input.key == Key::Char('z')
2043 && matches!(
2044 ed.vim.mode,
2045 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2046 )
2047 {
2048 ed.vim.pending = Pending::Z;
2049 return true;
2050 }
2051
2052 if !input.ctrl
2058 && matches!(
2059 ed.vim.mode,
2060 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2061 )
2062 && input.key == Key::Char('`')
2063 {
2064 ed.vim.pending = Pending::GotoMarkChar;
2065 return true;
2066 }
2067 if !input.ctrl && ed.vim.mode == Mode::Normal {
2068 match input.key {
2069 Key::Char('m') => {
2070 ed.vim.pending = Pending::SetMark;
2071 return true;
2072 }
2073 Key::Char('\'') => {
2074 ed.vim.pending = Pending::GotoMarkLine;
2075 return true;
2076 }
2077 Key::Char('`') => {
2078 ed.vim.pending = Pending::GotoMarkChar;
2080 return true;
2081 }
2082 Key::Char('"') => {
2083 ed.vim.pending = Pending::SelectRegister;
2086 return true;
2087 }
2088 Key::Char('@') => {
2089 ed.vim.pending = Pending::PlayMacroTarget { count };
2093 return true;
2094 }
2095 Key::Char('q') if ed.vim.recording_macro.is_none() => {
2096 ed.vim.pending = Pending::RecordMacroTarget;
2101 return true;
2102 }
2103 _ => {}
2104 }
2105 }
2106
2107 true
2109}
2110
2111fn handle_set_mark<H: crate::types::Host>(
2112 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2113 input: Input,
2114) -> bool {
2115 if let Key::Char(c) = input.key
2116 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
2117 {
2118 let pos = ed.cursor();
2123 ed.set_mark(c, pos);
2124 }
2125 true
2126}
2127
2128fn handle_select_register<H: crate::types::Host>(
2134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2135 input: Input,
2136) -> bool {
2137 if let Key::Char(c) = input.key {
2138 ed.set_pending_register(c);
2139 }
2140 true
2141}
2142
2143fn handle_record_macro_target<H: crate::types::Host>(
2148 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2149 input: Input,
2150) -> bool {
2151 if let Key::Char(c) = input.key
2152 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2153 {
2154 ed.vim.recording_macro = Some(c);
2155 if c.is_ascii_uppercase() {
2158 let lower = c.to_ascii_lowercase();
2159 let text = ed
2163 .registers()
2164 .read(lower)
2165 .map(|s| s.text.clone())
2166 .unwrap_or_default();
2167 ed.vim.recording_keys = crate::input::decode_macro(&text);
2168 } else {
2169 ed.vim.recording_keys.clear();
2170 }
2171 }
2172 true
2173}
2174
2175fn handle_play_macro_target<H: crate::types::Host>(
2181 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2182 input: Input,
2183 count: usize,
2184) -> bool {
2185 let reg = match input.key {
2186 Key::Char('@') => ed.vim.last_macro,
2187 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2188 Some(c.to_ascii_lowercase())
2189 }
2190 _ => None,
2191 };
2192 let Some(reg) = reg else {
2193 return true;
2194 };
2195 let text = match ed.registers().read(reg) {
2198 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2199 _ => return true,
2200 };
2201 let keys = crate::input::decode_macro(&text);
2202 ed.vim.last_macro = Some(reg);
2203 let times = count.max(1);
2204 let was_replaying = ed.vim.replaying_macro;
2205 ed.vim.replaying_macro = true;
2206 for _ in 0..times {
2207 for k in keys.iter().copied() {
2208 step(ed, k);
2209 }
2210 }
2211 ed.vim.replaying_macro = was_replaying;
2212 true
2213}
2214
2215fn handle_goto_mark<H: crate::types::Host>(
2216 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2217 input: Input,
2218 linewise: bool,
2219) -> bool {
2220 let Key::Char(c) = input.key else {
2221 return true;
2222 };
2223 let target = match c {
2230 'a'..='z' | 'A'..='Z' => ed.mark(c),
2231 '\'' | '`' => ed.vim.jump_back.last().copied(),
2232 '.' => ed.vim.last_edit_pos,
2233 '[' | ']' | '<' | '>' => ed.mark(c),
2238 _ => None,
2239 };
2240 let Some((row, col)) = target else {
2241 return true;
2242 };
2243 let pre = ed.cursor();
2244 let (r, c_clamped) = clamp_pos(ed, (row, col));
2245 if linewise {
2246 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2247 ed.push_buffer_cursor_to_textarea();
2248 move_first_non_whitespace(ed);
2249 } else {
2250 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2251 ed.push_buffer_cursor_to_textarea();
2252 }
2253 if ed.cursor() != pre {
2254 push_jump(ed, pre);
2255 }
2256 ed.sticky_col = Some(ed.cursor().1);
2257 true
2258}
2259
2260fn take_count(vim: &mut VimState) -> usize {
2261 if vim.count > 0 {
2262 let n = vim.count;
2263 vim.count = 0;
2264 n
2265 } else {
2266 1
2267 }
2268}
2269
2270fn char_to_operator(c: char) -> Option<Operator> {
2271 match c {
2272 'd' => Some(Operator::Delete),
2273 'c' => Some(Operator::Change),
2274 'y' => Some(Operator::Yank),
2275 '>' => Some(Operator::Indent),
2276 '<' => Some(Operator::Outdent),
2277 _ => None,
2278 }
2279}
2280
2281fn visual_operator(input: &Input) -> Option<Operator> {
2282 if input.ctrl {
2283 return None;
2284 }
2285 match input.key {
2286 Key::Char('y') => Some(Operator::Yank),
2287 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2288 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2289 Key::Char('U') => Some(Operator::Uppercase),
2291 Key::Char('u') => Some(Operator::Lowercase),
2292 Key::Char('~') => Some(Operator::ToggleCase),
2293 Key::Char('>') => Some(Operator::Indent),
2295 Key::Char('<') => Some(Operator::Outdent),
2296 _ => None,
2297 }
2298}
2299
2300fn find_entry(input: &Input) -> Option<(bool, bool)> {
2301 if input.ctrl {
2302 return None;
2303 }
2304 match input.key {
2305 Key::Char('f') => Some((true, false)),
2306 Key::Char('F') => Some((false, false)),
2307 Key::Char('t') => Some((true, true)),
2308 Key::Char('T') => Some((false, true)),
2309 _ => None,
2310 }
2311}
2312
2313const JUMPLIST_MAX: usize = 100;
2317
2318fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2323 ed.vim.jump_back.push(from);
2324 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2325 ed.vim.jump_back.remove(0);
2326 }
2327 ed.vim.jump_fwd.clear();
2328}
2329
2330fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2333 let Some(target) = ed.vim.jump_back.pop() else {
2334 return;
2335 };
2336 let cur = ed.cursor();
2337 ed.vim.jump_fwd.push(cur);
2338 let (r, c) = clamp_pos(ed, target);
2339 ed.jump_cursor(r, c);
2340 ed.sticky_col = Some(c);
2341}
2342
2343fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2346 let Some(target) = ed.vim.jump_fwd.pop() else {
2347 return;
2348 };
2349 let cur = ed.cursor();
2350 ed.vim.jump_back.push(cur);
2351 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2352 ed.vim.jump_back.remove(0);
2353 }
2354 let (r, c) = clamp_pos(ed, target);
2355 ed.jump_cursor(r, c);
2356 ed.sticky_col = Some(c);
2357}
2358
2359fn clamp_pos<H: crate::types::Host>(
2362 ed: &Editor<hjkl_buffer::Buffer, H>,
2363 pos: (usize, usize),
2364) -> (usize, usize) {
2365 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2366 let r = pos.0.min(last_row);
2367 let line_len = buf_line_chars(&ed.buffer, r);
2368 let c = pos.1.min(line_len.saturating_sub(1));
2369 (r, c)
2370}
2371
2372fn is_big_jump(motion: &Motion) -> bool {
2374 matches!(
2375 motion,
2376 Motion::FileTop
2377 | Motion::FileBottom
2378 | Motion::MatchBracket
2379 | Motion::WordAtCursor { .. }
2380 | Motion::SearchNext { .. }
2381 | Motion::ViewportTop
2382 | Motion::ViewportMiddle
2383 | Motion::ViewportBottom
2384 )
2385}
2386
2387fn viewport_half_rows<H: crate::types::Host>(
2392 ed: &Editor<hjkl_buffer::Buffer, H>,
2393 count: usize,
2394) -> usize {
2395 let h = ed.viewport_height_value() as usize;
2396 (h / 2).max(1).saturating_mul(count.max(1))
2397}
2398
2399fn viewport_full_rows<H: crate::types::Host>(
2402 ed: &Editor<hjkl_buffer::Buffer, H>,
2403 count: usize,
2404) -> usize {
2405 let h = ed.viewport_height_value() as usize;
2406 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2407}
2408
2409fn scroll_cursor_rows<H: crate::types::Host>(
2414 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2415 delta: isize,
2416) {
2417 if delta == 0 {
2418 return;
2419 }
2420 ed.sync_buffer_content_from_textarea();
2421 let (row, _) = ed.cursor();
2422 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2423 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2424 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2425 crate::motions::move_first_non_blank(&mut ed.buffer);
2426 ed.push_buffer_cursor_to_textarea();
2427 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2428}
2429
2430fn parse_motion(input: &Input) -> Option<Motion> {
2433 if input.ctrl {
2434 return None;
2435 }
2436 match input.key {
2437 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2438 Key::Char('l') | Key::Right => Some(Motion::Right),
2439 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2440 Key::Char('k') | Key::Up => Some(Motion::Up),
2441 Key::Char('w') => Some(Motion::WordFwd),
2442 Key::Char('W') => Some(Motion::BigWordFwd),
2443 Key::Char('b') => Some(Motion::WordBack),
2444 Key::Char('B') => Some(Motion::BigWordBack),
2445 Key::Char('e') => Some(Motion::WordEnd),
2446 Key::Char('E') => Some(Motion::BigWordEnd),
2447 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2448 Key::Char('^') => Some(Motion::FirstNonBlank),
2449 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2450 Key::Char('G') => Some(Motion::FileBottom),
2451 Key::Char('%') => Some(Motion::MatchBracket),
2452 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2453 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2454 Key::Char('*') => Some(Motion::WordAtCursor {
2455 forward: true,
2456 whole_word: true,
2457 }),
2458 Key::Char('#') => Some(Motion::WordAtCursor {
2459 forward: false,
2460 whole_word: true,
2461 }),
2462 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2463 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2464 Key::Char('H') => Some(Motion::ViewportTop),
2465 Key::Char('M') => Some(Motion::ViewportMiddle),
2466 Key::Char('L') => Some(Motion::ViewportBottom),
2467 Key::Char('{') => Some(Motion::ParagraphPrev),
2468 Key::Char('}') => Some(Motion::ParagraphNext),
2469 Key::Char('(') => Some(Motion::SentencePrev),
2470 Key::Char(')') => Some(Motion::SentenceNext),
2471 _ => None,
2472 }
2473}
2474
2475pub(crate) fn execute_motion<H: crate::types::Host>(
2478 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2479 motion: Motion,
2480 count: usize,
2481) {
2482 let count = count.max(1);
2483 let motion = match motion {
2485 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2486 Some((ch, forward, till)) => Motion::Find {
2487 ch,
2488 forward: if reverse { !forward } else { forward },
2489 till,
2490 },
2491 None => return,
2492 },
2493 other => other,
2494 };
2495 let pre_pos = ed.cursor();
2496 let pre_col = pre_pos.1;
2497 apply_motion_cursor(ed, &motion, count);
2498 let post_pos = ed.cursor();
2499 if is_big_jump(&motion) && pre_pos != post_pos {
2500 push_jump(ed, pre_pos);
2501 }
2502 apply_sticky_col(ed, &motion, pre_col);
2503 ed.sync_buffer_from_textarea();
2508}
2509
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
2936fn handle_after_op<H: crate::types::Host>(
2937 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2938 input: Input,
2939 op: Operator,
2940 count1: usize,
2941) -> bool {
2942 if let Key::Char(d @ '0'..='9') = input.key
2944 && !input.ctrl
2945 && (d != '0' || ed.vim.count > 0)
2946 {
2947 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2948 ed.vim.pending = Pending::Op { op, count1 };
2949 return true;
2950 }
2951
2952 if input.key == Key::Esc {
2954 ed.vim.count = 0;
2955 return true;
2956 }
2957
2958 let double_ch = match op {
2962 Operator::Delete => Some('d'),
2963 Operator::Change => Some('c'),
2964 Operator::Yank => Some('y'),
2965 Operator::Indent => Some('>'),
2966 Operator::Outdent => Some('<'),
2967 Operator::Uppercase => Some('U'),
2968 Operator::Lowercase => Some('u'),
2969 Operator::ToggleCase => Some('~'),
2970 Operator::Fold => None,
2971 Operator::Reflow => Some('q'),
2974 };
2975 if let Key::Char(c) = input.key
2976 && !input.ctrl
2977 && Some(c) == double_ch
2978 {
2979 let count2 = take_count(&mut ed.vim);
2980 let total = count1.max(1) * count2.max(1);
2981 execute_line_op(ed, op, total);
2982 if !ed.vim.replaying {
2983 ed.vim.last_change = Some(LastChange::LineOp {
2984 op,
2985 count: total,
2986 inserted: None,
2987 });
2988 }
2989 return true;
2990 }
2991
2992 if let Key::Char('i') | Key::Char('a') = input.key
2994 && !input.ctrl
2995 {
2996 let inner = matches!(input.key, Key::Char('i'));
2997 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2998 return true;
2999 }
3000
3001 if input.key == Key::Char('g') && !input.ctrl {
3003 ed.vim.pending = Pending::OpG { op, count1 };
3004 return true;
3005 }
3006
3007 if let Some((forward, till)) = find_entry(&input) {
3009 ed.vim.pending = Pending::OpFind {
3010 op,
3011 count1,
3012 forward,
3013 till,
3014 };
3015 return true;
3016 }
3017
3018 let count2 = take_count(&mut ed.vim);
3020 let total = count1.max(1) * count2.max(1);
3021 if let Some(motion) = parse_motion(&input) {
3022 let motion = match motion {
3023 Motion::FindRepeat { reverse } => match ed.vim.last_find {
3024 Some((ch, forward, till)) => Motion::Find {
3025 ch,
3026 forward: if reverse { !forward } else { forward },
3027 till,
3028 },
3029 None => return true,
3030 },
3031 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
3035 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
3036 m => m,
3037 };
3038 apply_op_with_motion(ed, op, &motion, total);
3039 if let Motion::Find { ch, forward, till } = &motion {
3040 ed.vim.last_find = Some((*ch, *forward, *till));
3041 }
3042 if !ed.vim.replaying && op_is_change(op) {
3043 ed.vim.last_change = Some(LastChange::OpMotion {
3044 op,
3045 motion,
3046 count: total,
3047 inserted: None,
3048 });
3049 }
3050 return true;
3051 }
3052
3053 true
3055}
3056
3057pub(crate) fn apply_op_g_inner<H: crate::types::Host>(
3067 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3068 op: Operator,
3069 ch: char,
3070 total_count: usize,
3071) {
3072 if matches!(
3075 op,
3076 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3077 ) {
3078 let op_char = match op {
3079 Operator::Uppercase => 'U',
3080 Operator::Lowercase => 'u',
3081 Operator::ToggleCase => '~',
3082 _ => unreachable!(),
3083 };
3084 if ch == op_char {
3085 execute_line_op(ed, op, total_count);
3086 if !ed.vim.replaying {
3087 ed.vim.last_change = Some(LastChange::LineOp {
3088 op,
3089 count: total_count,
3090 inserted: None,
3091 });
3092 }
3093 return;
3094 }
3095 }
3096 let motion = match ch {
3097 'g' => Motion::FileTop,
3098 'e' => Motion::WordEndBack,
3099 'E' => Motion::BigWordEndBack,
3100 'j' => Motion::ScreenDown,
3101 'k' => Motion::ScreenUp,
3102 _ => return, };
3104 apply_op_with_motion(ed, op, &motion, total_count);
3105 if !ed.vim.replaying && op_is_change(op) {
3106 ed.vim.last_change = Some(LastChange::OpMotion {
3107 op,
3108 motion,
3109 count: total_count,
3110 inserted: None,
3111 });
3112 }
3113}
3114
3115fn handle_op_after_g<H: crate::types::Host>(
3116 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3117 input: Input,
3118 op: Operator,
3119 count1: usize,
3120) -> bool {
3121 if input.ctrl {
3122 return true;
3123 }
3124 let count2 = take_count(&mut ed.vim);
3125 let total = count1.max(1) * count2.max(1);
3126 if let Key::Char(ch) = input.key {
3127 apply_op_g_inner(ed, op, ch, total);
3128 }
3129 true
3130}
3131
3132fn handle_after_g<H: crate::types::Host>(
3133 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3134 input: Input,
3135) -> bool {
3136 let count = take_count(&mut ed.vim);
3137 if let Key::Char(ch) = input.key {
3140 apply_after_g(ed, ch, count);
3141 }
3142 true
3143}
3144
3145pub(crate) fn apply_after_g<H: crate::types::Host>(
3150 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3151 ch: char,
3152 count: usize,
3153) {
3154 match ch {
3155 'g' => {
3156 let pre = ed.cursor();
3158 if count > 1 {
3159 ed.jump_cursor(count - 1, 0);
3160 } else {
3161 ed.jump_cursor(0, 0);
3162 }
3163 move_first_non_whitespace(ed);
3164 if ed.cursor() != pre {
3165 push_jump(ed, pre);
3166 }
3167 }
3168 'e' => execute_motion(ed, Motion::WordEndBack, count),
3169 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3170 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3172 'M' => execute_motion(ed, Motion::LineMiddle, count),
3174 'v' => {
3176 if let Some(snap) = ed.vim.last_visual {
3177 match snap.mode {
3178 Mode::Visual => {
3179 ed.vim.visual_anchor = snap.anchor;
3180 ed.vim.mode = Mode::Visual;
3181 }
3182 Mode::VisualLine => {
3183 ed.vim.visual_line_anchor = snap.anchor.0;
3184 ed.vim.mode = Mode::VisualLine;
3185 }
3186 Mode::VisualBlock => {
3187 ed.vim.block_anchor = snap.anchor;
3188 ed.vim.block_vcol = snap.block_vcol;
3189 ed.vim.mode = Mode::VisualBlock;
3190 }
3191 _ => {}
3192 }
3193 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3194 }
3195 }
3196 'j' => execute_motion(ed, Motion::ScreenDown, count),
3200 'k' => execute_motion(ed, Motion::ScreenUp, count),
3201 'U' => {
3205 ed.vim.pending = Pending::Op {
3206 op: Operator::Uppercase,
3207 count1: count,
3208 };
3209 }
3210 'u' => {
3211 ed.vim.pending = Pending::Op {
3212 op: Operator::Lowercase,
3213 count1: count,
3214 };
3215 }
3216 '~' => {
3217 ed.vim.pending = Pending::Op {
3218 op: Operator::ToggleCase,
3219 count1: count,
3220 };
3221 }
3222 'q' => {
3223 ed.vim.pending = Pending::Op {
3226 op: Operator::Reflow,
3227 count1: count,
3228 };
3229 }
3230 'J' => {
3231 for _ in 0..count.max(1) {
3233 ed.push_undo();
3234 join_line_raw(ed);
3235 }
3236 if !ed.vim.replaying {
3237 ed.vim.last_change = Some(LastChange::JoinLine {
3238 count: count.max(1),
3239 });
3240 }
3241 }
3242 'd' => {
3243 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3248 }
3249 'i' => {
3254 if let Some((row, col)) = ed.vim.last_insert_pos {
3255 ed.jump_cursor(row, col);
3256 }
3257 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3258 }
3259 ';' => walk_change_list(ed, -1, count.max(1)),
3262 ',' => walk_change_list(ed, 1, count.max(1)),
3263 '*' => execute_motion(
3267 ed,
3268 Motion::WordAtCursor {
3269 forward: true,
3270 whole_word: false,
3271 },
3272 count,
3273 ),
3274 '#' => execute_motion(
3275 ed,
3276 Motion::WordAtCursor {
3277 forward: false,
3278 whole_word: false,
3279 },
3280 count,
3281 ),
3282 _ => {}
3283 }
3284}
3285
3286fn handle_after_z<H: crate::types::Host>(
3287 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3288 input: Input,
3289) -> bool {
3290 let count = take_count(&mut ed.vim);
3291 if let Key::Char(ch) = input.key {
3294 apply_after_z(ed, ch, count);
3295 }
3296 true
3297}
3298
3299pub(crate) fn apply_after_z<H: crate::types::Host>(
3304 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3305 ch: char,
3306 count: usize,
3307) {
3308 use crate::editor::CursorScrollTarget;
3309 let row = ed.cursor().0;
3310 match ch {
3311 'z' => {
3312 ed.scroll_cursor_to(CursorScrollTarget::Center);
3313 ed.vim.viewport_pinned = true;
3314 }
3315 't' => {
3316 ed.scroll_cursor_to(CursorScrollTarget::Top);
3317 ed.vim.viewport_pinned = true;
3318 }
3319 'b' => {
3320 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3321 ed.vim.viewport_pinned = true;
3322 }
3323 'o' => {
3328 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3329 }
3330 'c' => {
3331 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3332 }
3333 'a' => {
3334 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3335 }
3336 'R' => {
3337 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3338 }
3339 'M' => {
3340 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3341 }
3342 'E' => {
3343 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3344 }
3345 'd' => {
3346 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3347 }
3348 'f' => {
3349 if matches!(
3350 ed.vim.mode,
3351 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3352 ) {
3353 let anchor_row = match ed.vim.mode {
3356 Mode::VisualLine => ed.vim.visual_line_anchor,
3357 Mode::VisualBlock => ed.vim.block_anchor.0,
3358 _ => ed.vim.visual_anchor.0,
3359 };
3360 let cur = ed.cursor().0;
3361 let top = anchor_row.min(cur);
3362 let bot = anchor_row.max(cur);
3363 ed.apply_fold_op(crate::types::FoldOp::Add {
3364 start_row: top,
3365 end_row: bot,
3366 closed: true,
3367 });
3368 ed.vim.mode = Mode::Normal;
3369 } else {
3370 ed.vim.pending = Pending::Op {
3375 op: Operator::Fold,
3376 count1: count,
3377 };
3378 }
3379 }
3380 _ => {}
3381 }
3382}
3383
3384fn handle_replace<H: crate::types::Host>(
3385 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3386 input: Input,
3387) -> bool {
3388 if let Key::Char(ch) = input.key {
3389 if ed.vim.mode == Mode::VisualBlock {
3390 block_replace(ed, ch);
3391 return true;
3392 }
3393 let count = take_count(&mut ed.vim);
3394 replace_char(ed, ch, count.max(1));
3395 if !ed.vim.replaying {
3396 ed.vim.last_change = Some(LastChange::ReplaceChar {
3397 ch,
3398 count: count.max(1),
3399 });
3400 }
3401 }
3402 true
3403}
3404
3405fn handle_find_target<H: crate::types::Host>(
3406 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3407 input: Input,
3408 forward: bool,
3409 till: bool,
3410) -> bool {
3411 let Key::Char(ch) = input.key else {
3412 return true;
3413 };
3414 let count = take_count(&mut ed.vim);
3415 apply_find_char(ed, ch, forward, till, count.max(1));
3416 true
3417}
3418
3419pub(crate) fn apply_find_char<H: crate::types::Host>(
3425 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3426 ch: char,
3427 forward: bool,
3428 till: bool,
3429 count: usize,
3430) {
3431 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3432 ed.vim.last_find = Some((ch, forward, till));
3433}
3434
3435pub(crate) fn apply_op_find_motion<H: crate::types::Host>(
3441 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3442 op: Operator,
3443 ch: char,
3444 forward: bool,
3445 till: bool,
3446 total_count: usize,
3447) {
3448 let motion = Motion::Find { ch, forward, till };
3449 apply_op_with_motion(ed, op, &motion, total_count);
3450 ed.vim.last_find = Some((ch, forward, till));
3451 if !ed.vim.replaying && op_is_change(op) {
3452 ed.vim.last_change = Some(LastChange::OpMotion {
3453 op,
3454 motion,
3455 count: total_count,
3456 inserted: None,
3457 });
3458 }
3459}
3460
3461fn handle_op_find_target<H: crate::types::Host>(
3462 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3463 input: Input,
3464 op: Operator,
3465 count1: usize,
3466 forward: bool,
3467 till: bool,
3468) -> bool {
3469 let Key::Char(ch) = input.key else {
3470 return true;
3471 };
3472 let count2 = take_count(&mut ed.vim);
3473 let total = count1.max(1) * count2.max(1);
3474 apply_op_find_motion(ed, op, ch, forward, till, total);
3475 true
3476}
3477
3478pub(crate) fn apply_op_text_obj_inner<H: crate::types::Host>(
3488 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3489 op: Operator,
3490 ch: char,
3491 inner: bool,
3492 _total_count: usize,
3493) -> bool {
3494 let obj = match ch {
3497 'w' => TextObject::Word { big: false },
3498 'W' => TextObject::Word { big: true },
3499 '"' | '\'' | '`' => TextObject::Quote(ch),
3500 '(' | ')' | 'b' => TextObject::Bracket('('),
3501 '[' | ']' => TextObject::Bracket('['),
3502 '{' | '}' | 'B' => TextObject::Bracket('{'),
3503 '<' | '>' => TextObject::Bracket('<'),
3504 'p' => TextObject::Paragraph,
3505 't' => TextObject::XmlTag,
3506 's' => TextObject::Sentence,
3507 _ => return false,
3508 };
3509 apply_op_with_text_object(ed, op, obj, inner);
3510 if !ed.vim.replaying && op_is_change(op) {
3511 ed.vim.last_change = Some(LastChange::OpTextObj {
3512 op,
3513 obj,
3514 inner,
3515 inserted: None,
3516 });
3517 }
3518 true
3519}
3520
3521fn handle_text_object<H: crate::types::Host>(
3522 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3523 input: Input,
3524 op: Operator,
3525 _count1: usize,
3526 inner: bool,
3527) -> bool {
3528 let Key::Char(ch) = input.key else {
3529 return true;
3530 };
3531 apply_op_text_obj_inner(ed, op, ch, inner, 1);
3534 true
3535}
3536
3537fn handle_visual_text_obj<H: crate::types::Host>(
3538 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3539 input: Input,
3540 inner: bool,
3541) -> bool {
3542 let Key::Char(ch) = input.key else {
3543 return true;
3544 };
3545 let obj = match ch {
3546 'w' => TextObject::Word { big: false },
3547 'W' => TextObject::Word { big: true },
3548 '"' | '\'' | '`' => TextObject::Quote(ch),
3549 '(' | ')' | 'b' => TextObject::Bracket('('),
3550 '[' | ']' => TextObject::Bracket('['),
3551 '{' | '}' | 'B' => TextObject::Bracket('{'),
3552 '<' | '>' => TextObject::Bracket('<'),
3553 'p' => TextObject::Paragraph,
3554 't' => TextObject::XmlTag,
3555 's' => TextObject::Sentence,
3556 _ => return true,
3557 };
3558 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3559 return true;
3560 };
3561 match kind {
3565 MotionKind::Linewise => {
3566 ed.vim.visual_line_anchor = start.0;
3567 ed.vim.mode = Mode::VisualLine;
3568 ed.jump_cursor(end.0, 0);
3569 }
3570 _ => {
3571 ed.vim.mode = Mode::Visual;
3572 ed.vim.visual_anchor = (start.0, start.1);
3573 let (er, ec) = retreat_one(ed, end);
3574 ed.jump_cursor(er, ec);
3575 }
3576 }
3577 true
3578}
3579
3580fn retreat_one<H: crate::types::Host>(
3582 ed: &Editor<hjkl_buffer::Buffer, H>,
3583 pos: (usize, usize),
3584) -> (usize, usize) {
3585 let (r, c) = pos;
3586 if c > 0 {
3587 (r, c - 1)
3588 } else if r > 0 {
3589 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3590 (r - 1, prev_len)
3591 } else {
3592 (0, 0)
3593 }
3594}
3595
3596fn op_is_change(op: Operator) -> bool {
3597 matches!(op, Operator::Delete | Operator::Change)
3598}
3599
3600fn handle_normal_only<H: crate::types::Host>(
3603 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3604 input: &Input,
3605 count: usize,
3606) -> bool {
3607 if input.ctrl {
3608 return false;
3609 }
3610 match input.key {
3611 Key::Char('i') => {
3612 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3613 true
3614 }
3615 Key::Char('I') => {
3616 move_first_non_whitespace(ed);
3617 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3618 true
3619 }
3620 Key::Char('a') => {
3621 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3622 ed.push_buffer_cursor_to_textarea();
3623 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3624 true
3625 }
3626 Key::Char('A') => {
3627 crate::motions::move_line_end(&mut ed.buffer);
3628 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3629 ed.push_buffer_cursor_to_textarea();
3630 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3631 true
3632 }
3633 Key::Char('R') => {
3634 begin_insert(ed, count.max(1), InsertReason::Replace);
3637 true
3638 }
3639 Key::Char('o') => {
3640 use hjkl_buffer::{Edit, Position};
3641 ed.push_undo();
3642 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3645 ed.sync_buffer_content_from_textarea();
3646 let row = buf_cursor_pos(&ed.buffer).row;
3647 let line_chars = buf_line_chars(&ed.buffer, row);
3648 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3651 let indent = compute_enter_indent(&ed.settings, prev_line);
3652 ed.mutate_edit(Edit::InsertStr {
3653 at: Position::new(row, line_chars),
3654 text: format!("\n{indent}"),
3655 });
3656 ed.push_buffer_cursor_to_textarea();
3657 true
3658 }
3659 Key::Char('O') => {
3660 use hjkl_buffer::{Edit, Position};
3661 ed.push_undo();
3662 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3663 ed.sync_buffer_content_from_textarea();
3664 let row = buf_cursor_pos(&ed.buffer).row;
3665 let indent = if row > 0 {
3669 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3670 compute_enter_indent(&ed.settings, above)
3671 } else {
3672 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3673 cur.chars()
3674 .take_while(|c| *c == ' ' || *c == '\t')
3675 .collect::<String>()
3676 };
3677 ed.mutate_edit(Edit::InsertStr {
3678 at: Position::new(row, 0),
3679 text: format!("{indent}\n"),
3680 });
3681 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3686 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3687 let new_row = buf_cursor_pos(&ed.buffer).row;
3688 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3689 ed.push_buffer_cursor_to_textarea();
3690 true
3691 }
3692 Key::Char('x') => {
3693 do_char_delete(ed, true, count.max(1));
3694 if !ed.vim.replaying {
3695 ed.vim.last_change = Some(LastChange::CharDel {
3696 forward: true,
3697 count: count.max(1),
3698 });
3699 }
3700 true
3701 }
3702 Key::Char('X') => {
3703 do_char_delete(ed, false, count.max(1));
3704 if !ed.vim.replaying {
3705 ed.vim.last_change = Some(LastChange::CharDel {
3706 forward: false,
3707 count: count.max(1),
3708 });
3709 }
3710 true
3711 }
3712 Key::Char('~') => {
3713 for _ in 0..count.max(1) {
3714 ed.push_undo();
3715 toggle_case_at_cursor(ed);
3716 }
3717 if !ed.vim.replaying {
3718 ed.vim.last_change = Some(LastChange::ToggleCase {
3719 count: count.max(1),
3720 });
3721 }
3722 true
3723 }
3724 Key::Char('J') => {
3725 for _ in 0..count.max(1) {
3726 ed.push_undo();
3727 join_line(ed);
3728 }
3729 if !ed.vim.replaying {
3730 ed.vim.last_change = Some(LastChange::JoinLine {
3731 count: count.max(1),
3732 });
3733 }
3734 true
3735 }
3736 Key::Char('D') => {
3737 ed.push_undo();
3738 delete_to_eol(ed);
3739 crate::motions::move_left(&mut ed.buffer, 1);
3741 ed.push_buffer_cursor_to_textarea();
3742 if !ed.vim.replaying {
3743 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3744 }
3745 true
3746 }
3747 Key::Char('Y') => {
3748 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3750 true
3751 }
3752 Key::Char('C') => {
3753 ed.push_undo();
3754 delete_to_eol(ed);
3755 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3756 true
3757 }
3758 Key::Char('s') => {
3759 use hjkl_buffer::{Edit, MotionKind, Position};
3760 ed.push_undo();
3761 ed.sync_buffer_content_from_textarea();
3762 for _ in 0..count.max(1) {
3763 let cursor = buf_cursor_pos(&ed.buffer);
3764 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3765 if cursor.col >= line_chars {
3766 break;
3767 }
3768 ed.mutate_edit(Edit::DeleteRange {
3769 start: cursor,
3770 end: Position::new(cursor.row, cursor.col + 1),
3771 kind: MotionKind::Char,
3772 });
3773 }
3774 ed.push_buffer_cursor_to_textarea();
3775 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3776 if !ed.vim.replaying {
3778 ed.vim.last_change = Some(LastChange::OpMotion {
3779 op: Operator::Change,
3780 motion: Motion::Right,
3781 count: count.max(1),
3782 inserted: None,
3783 });
3784 }
3785 true
3786 }
3787 Key::Char('p') => {
3788 do_paste(ed, false, count.max(1));
3789 if !ed.vim.replaying {
3790 ed.vim.last_change = Some(LastChange::Paste {
3791 before: false,
3792 count: count.max(1),
3793 });
3794 }
3795 true
3796 }
3797 Key::Char('P') => {
3798 do_paste(ed, true, count.max(1));
3799 if !ed.vim.replaying {
3800 ed.vim.last_change = Some(LastChange::Paste {
3801 before: true,
3802 count: count.max(1),
3803 });
3804 }
3805 true
3806 }
3807 Key::Char('u') => {
3808 do_undo(ed);
3809 true
3810 }
3811 Key::Char('r') => {
3812 ed.vim.count = count;
3813 ed.vim.pending = Pending::Replace;
3814 true
3815 }
3816 Key::Char('/') => {
3817 enter_search(ed, true);
3818 true
3819 }
3820 Key::Char('?') => {
3821 enter_search(ed, false);
3822 true
3823 }
3824 Key::Char('.') => {
3825 replay_last_change(ed, count);
3826 true
3827 }
3828 _ => false,
3829 }
3830}
3831
3832fn begin_insert_noundo<H: crate::types::Host>(
3834 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3835 count: usize,
3836 reason: InsertReason,
3837) {
3838 let reason = if ed.vim.replaying {
3839 InsertReason::ReplayOnly
3840 } else {
3841 reason
3842 };
3843 let (row, _) = ed.cursor();
3844 ed.vim.insert_session = Some(InsertSession {
3845 count,
3846 row_min: row,
3847 row_max: row,
3848 before_lines: buf_lines_to_vec(&ed.buffer),
3849 reason,
3850 });
3851 ed.vim.mode = Mode::Insert;
3852}
3853
3854fn apply_op_with_motion<H: crate::types::Host>(
3857 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3858 op: Operator,
3859 motion: &Motion,
3860 count: usize,
3861) {
3862 let start = ed.cursor();
3863 apply_motion_cursor_ctx(ed, motion, count, true);
3868 let end = ed.cursor();
3869 let kind = motion_kind(motion);
3870 ed.jump_cursor(start.0, start.1);
3872 run_operator_over_range(ed, op, start, end, kind);
3873}
3874
3875fn apply_op_with_text_object<H: crate::types::Host>(
3876 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3877 op: Operator,
3878 obj: TextObject,
3879 inner: bool,
3880) {
3881 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3882 return;
3883 };
3884 ed.jump_cursor(start.0, start.1);
3885 run_operator_over_range(ed, op, start, end, kind);
3886}
3887
3888fn motion_kind(motion: &Motion) -> MotionKind {
3889 match motion {
3890 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3891 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3892 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3893 MotionKind::Linewise
3894 }
3895 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3896 MotionKind::Inclusive
3897 }
3898 Motion::Find { .. } => MotionKind::Inclusive,
3899 Motion::MatchBracket => MotionKind::Inclusive,
3900 Motion::LineEnd => MotionKind::Inclusive,
3902 _ => MotionKind::Exclusive,
3903 }
3904}
3905
3906fn run_operator_over_range<H: crate::types::Host>(
3907 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3908 op: Operator,
3909 start: (usize, usize),
3910 end: (usize, usize),
3911 kind: MotionKind,
3912) {
3913 let (top, bot) = order(start, end);
3914 if top == bot {
3915 return;
3916 }
3917
3918 match op {
3919 Operator::Yank => {
3920 let text = read_vim_range(ed, top, bot, kind);
3921 if !text.is_empty() {
3922 ed.record_yank_to_host(text.clone());
3923 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3924 }
3925 let rbr = match kind {
3929 MotionKind::Linewise => {
3930 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3931 (bot.0, last_col)
3932 }
3933 MotionKind::Inclusive => (bot.0, bot.1),
3934 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3935 };
3936 ed.set_mark('[', top);
3937 ed.set_mark(']', rbr);
3938 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3939 ed.push_buffer_cursor_to_textarea();
3940 }
3941 Operator::Delete => {
3942 ed.push_undo();
3943 cut_vim_range(ed, top, bot, kind);
3944 if !matches!(kind, MotionKind::Linewise) {
3949 clamp_cursor_to_normal_mode(ed);
3950 }
3951 ed.vim.mode = Mode::Normal;
3952 let pos = ed.cursor();
3956 ed.set_mark('[', pos);
3957 ed.set_mark(']', pos);
3958 }
3959 Operator::Change => {
3960 ed.vim.change_mark_start = Some(top);
3965 ed.push_undo();
3966 cut_vim_range(ed, top, bot, kind);
3967 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3968 }
3969 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3970 apply_case_op_to_selection(ed, op, top, bot, kind);
3971 }
3972 Operator::Indent | Operator::Outdent => {
3973 ed.push_undo();
3976 if op == Operator::Indent {
3977 indent_rows(ed, top.0, bot.0, 1);
3978 } else {
3979 outdent_rows(ed, top.0, bot.0, 1);
3980 }
3981 ed.vim.mode = Mode::Normal;
3982 }
3983 Operator::Fold => {
3984 if bot.0 >= top.0 {
3988 ed.apply_fold_op(crate::types::FoldOp::Add {
3989 start_row: top.0,
3990 end_row: bot.0,
3991 closed: true,
3992 });
3993 }
3994 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3995 ed.push_buffer_cursor_to_textarea();
3996 ed.vim.mode = Mode::Normal;
3997 }
3998 Operator::Reflow => {
3999 ed.push_undo();
4000 reflow_rows(ed, top.0, bot.0);
4001 ed.vim.mode = Mode::Normal;
4002 }
4003 }
4004}
4005
4006fn reflow_rows<H: crate::types::Host>(
4011 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4012 top: usize,
4013 bot: usize,
4014) {
4015 let width = ed.settings().textwidth.max(1);
4016 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4017 let bot = bot.min(lines.len().saturating_sub(1));
4018 if top > bot {
4019 return;
4020 }
4021 let original = lines[top..=bot].to_vec();
4022 let mut wrapped: Vec<String> = Vec::new();
4023 let mut paragraph: Vec<String> = Vec::new();
4024 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
4025 if para.is_empty() {
4026 return;
4027 }
4028 let words = para.join(" ");
4029 let mut current = String::new();
4030 for word in words.split_whitespace() {
4031 let extra = if current.is_empty() {
4032 word.chars().count()
4033 } else {
4034 current.chars().count() + 1 + word.chars().count()
4035 };
4036 if extra > width && !current.is_empty() {
4037 out.push(std::mem::take(&mut current));
4038 current.push_str(word);
4039 } else if current.is_empty() {
4040 current.push_str(word);
4041 } else {
4042 current.push(' ');
4043 current.push_str(word);
4044 }
4045 }
4046 if !current.is_empty() {
4047 out.push(current);
4048 }
4049 para.clear();
4050 };
4051 for line in &original {
4052 if line.trim().is_empty() {
4053 flush(&mut paragraph, &mut wrapped, width);
4054 wrapped.push(String::new());
4055 } else {
4056 paragraph.push(line.clone());
4057 }
4058 }
4059 flush(&mut paragraph, &mut wrapped, width);
4060
4061 let after: Vec<String> = lines.split_off(bot + 1);
4063 lines.truncate(top);
4064 lines.extend(wrapped);
4065 lines.extend(after);
4066 ed.restore(lines, (top, 0));
4067 ed.mark_content_dirty();
4068}
4069
4070fn apply_case_op_to_selection<H: crate::types::Host>(
4076 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4077 op: Operator,
4078 top: (usize, usize),
4079 bot: (usize, usize),
4080 kind: MotionKind,
4081) {
4082 use hjkl_buffer::Edit;
4083 ed.push_undo();
4084 let saved_yank = ed.yank().to_string();
4085 let saved_yank_linewise = ed.vim.yank_linewise;
4086 let selection = cut_vim_range(ed, top, bot, kind);
4087 let transformed = match op {
4088 Operator::Uppercase => selection.to_uppercase(),
4089 Operator::Lowercase => selection.to_lowercase(),
4090 Operator::ToggleCase => toggle_case_str(&selection),
4091 _ => unreachable!(),
4092 };
4093 if !transformed.is_empty() {
4094 let cursor = buf_cursor_pos(&ed.buffer);
4095 ed.mutate_edit(Edit::InsertStr {
4096 at: cursor,
4097 text: transformed,
4098 });
4099 }
4100 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4101 ed.push_buffer_cursor_to_textarea();
4102 ed.set_yank(saved_yank);
4103 ed.vim.yank_linewise = saved_yank_linewise;
4104 ed.vim.mode = Mode::Normal;
4105}
4106
4107fn indent_rows<H: crate::types::Host>(
4112 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4113 top: usize,
4114 bot: usize,
4115 count: usize,
4116) {
4117 ed.sync_buffer_content_from_textarea();
4118 let width = ed.settings().shiftwidth * count.max(1);
4119 let pad: String = " ".repeat(width);
4120 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4121 let bot = bot.min(lines.len().saturating_sub(1));
4122 for line in lines.iter_mut().take(bot + 1).skip(top) {
4123 if !line.is_empty() {
4124 line.insert_str(0, &pad);
4125 }
4126 }
4127 ed.restore(lines, (top, 0));
4130 move_first_non_whitespace(ed);
4131}
4132
4133fn outdent_rows<H: crate::types::Host>(
4137 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4138 top: usize,
4139 bot: usize,
4140 count: usize,
4141) {
4142 ed.sync_buffer_content_from_textarea();
4143 let width = ed.settings().shiftwidth * count.max(1);
4144 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4145 let bot = bot.min(lines.len().saturating_sub(1));
4146 for line in lines.iter_mut().take(bot + 1).skip(top) {
4147 let strip: usize = line
4148 .chars()
4149 .take(width)
4150 .take_while(|c| *c == ' ' || *c == '\t')
4151 .count();
4152 if strip > 0 {
4153 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4154 line.drain(..byte_len);
4155 }
4156 }
4157 ed.restore(lines, (top, 0));
4158 move_first_non_whitespace(ed);
4159}
4160
4161fn toggle_case_str(s: &str) -> String {
4162 s.chars()
4163 .map(|c| {
4164 if c.is_lowercase() {
4165 c.to_uppercase().next().unwrap_or(c)
4166 } else if c.is_uppercase() {
4167 c.to_lowercase().next().unwrap_or(c)
4168 } else {
4169 c
4170 }
4171 })
4172 .collect()
4173}
4174
4175fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4176 if a <= b { (a, b) } else { (b, a) }
4177}
4178
4179fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4184 let (row, col) = ed.cursor();
4185 let line_chars = buf_line_chars(&ed.buffer, row);
4186 let max_col = line_chars.saturating_sub(1);
4187 if col > max_col {
4188 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4189 ed.push_buffer_cursor_to_textarea();
4190 }
4191}
4192
4193fn execute_line_op<H: crate::types::Host>(
4196 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4197 op: Operator,
4198 count: usize,
4199) {
4200 let (row, col) = ed.cursor();
4201 let total = buf_row_count(&ed.buffer);
4202 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4203
4204 match op {
4205 Operator::Yank => {
4206 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4208 if !text.is_empty() {
4209 ed.record_yank_to_host(text.clone());
4210 ed.record_yank(text, true);
4211 }
4212 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4215 ed.set_mark('[', (row, 0));
4216 ed.set_mark(']', (end_row, last_col));
4217 buf_set_cursor_rc(&mut ed.buffer, row, col);
4218 ed.push_buffer_cursor_to_textarea();
4219 ed.vim.mode = Mode::Normal;
4220 }
4221 Operator::Delete => {
4222 ed.push_undo();
4223 let deleted_through_last = end_row + 1 >= total;
4224 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4225 let total_after = buf_row_count(&ed.buffer);
4229 let raw_target = if deleted_through_last {
4230 row.saturating_sub(1).min(total_after.saturating_sub(1))
4231 } else {
4232 row.min(total_after.saturating_sub(1))
4233 };
4234 let target_row = if raw_target > 0
4240 && raw_target + 1 == total_after
4241 && buf_line(&ed.buffer, raw_target)
4242 .map(str::is_empty)
4243 .unwrap_or(false)
4244 {
4245 raw_target - 1
4246 } else {
4247 raw_target
4248 };
4249 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4250 ed.push_buffer_cursor_to_textarea();
4251 move_first_non_whitespace(ed);
4252 ed.sticky_col = Some(ed.cursor().1);
4253 ed.vim.mode = Mode::Normal;
4254 let pos = ed.cursor();
4257 ed.set_mark('[', pos);
4258 ed.set_mark(']', pos);
4259 }
4260 Operator::Change => {
4261 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4265 ed.vim.change_mark_start = Some((row, 0));
4267 ed.push_undo();
4268 ed.sync_buffer_content_from_textarea();
4269 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4271 if end_row > row {
4272 ed.mutate_edit(Edit::DeleteRange {
4273 start: Position::new(row + 1, 0),
4274 end: Position::new(end_row, 0),
4275 kind: BufKind::Line,
4276 });
4277 }
4278 let line_chars = buf_line_chars(&ed.buffer, row);
4279 if line_chars > 0 {
4280 ed.mutate_edit(Edit::DeleteRange {
4281 start: Position::new(row, 0),
4282 end: Position::new(row, line_chars),
4283 kind: BufKind::Char,
4284 });
4285 }
4286 if !payload.is_empty() {
4287 ed.record_yank_to_host(payload.clone());
4288 ed.record_delete(payload, true);
4289 }
4290 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4291 ed.push_buffer_cursor_to_textarea();
4292 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4293 }
4294 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4295 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4299 move_first_non_whitespace(ed);
4302 }
4303 Operator::Indent | Operator::Outdent => {
4304 ed.push_undo();
4306 if op == Operator::Indent {
4307 indent_rows(ed, row, end_row, 1);
4308 } else {
4309 outdent_rows(ed, row, end_row, 1);
4310 }
4311 ed.sticky_col = Some(ed.cursor().1);
4312 ed.vim.mode = Mode::Normal;
4313 }
4314 Operator::Fold => unreachable!("Fold has no line-op double"),
4316 Operator::Reflow => {
4317 ed.push_undo();
4319 reflow_rows(ed, row, end_row);
4320 move_first_non_whitespace(ed);
4321 ed.sticky_col = Some(ed.cursor().1);
4322 ed.vim.mode = Mode::Normal;
4323 }
4324 }
4325}
4326
4327fn apply_visual_operator<H: crate::types::Host>(
4330 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4331 op: Operator,
4332) {
4333 match ed.vim.mode {
4334 Mode::VisualLine => {
4335 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4336 let top = cursor_row.min(ed.vim.visual_line_anchor);
4337 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4338 ed.vim.yank_linewise = true;
4339 match op {
4340 Operator::Yank => {
4341 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4342 if !text.is_empty() {
4343 ed.record_yank_to_host(text.clone());
4344 ed.record_yank(text, true);
4345 }
4346 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4347 ed.push_buffer_cursor_to_textarea();
4348 ed.vim.mode = Mode::Normal;
4349 }
4350 Operator::Delete => {
4351 ed.push_undo();
4352 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4353 ed.vim.mode = Mode::Normal;
4354 }
4355 Operator::Change => {
4356 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4359 ed.push_undo();
4360 ed.sync_buffer_content_from_textarea();
4361 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4362 if bot > top {
4363 ed.mutate_edit(Edit::DeleteRange {
4364 start: Position::new(top + 1, 0),
4365 end: Position::new(bot, 0),
4366 kind: BufKind::Line,
4367 });
4368 }
4369 let line_chars = buf_line_chars(&ed.buffer, top);
4370 if line_chars > 0 {
4371 ed.mutate_edit(Edit::DeleteRange {
4372 start: Position::new(top, 0),
4373 end: Position::new(top, line_chars),
4374 kind: BufKind::Char,
4375 });
4376 }
4377 if !payload.is_empty() {
4378 ed.record_yank_to_host(payload.clone());
4379 ed.record_delete(payload, true);
4380 }
4381 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4382 ed.push_buffer_cursor_to_textarea();
4383 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4384 }
4385 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4386 let bot = buf_cursor_pos(&ed.buffer)
4387 .row
4388 .max(ed.vim.visual_line_anchor);
4389 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4390 move_first_non_whitespace(ed);
4391 }
4392 Operator::Indent | Operator::Outdent => {
4393 ed.push_undo();
4394 let (cursor_row, _) = ed.cursor();
4395 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4396 if op == Operator::Indent {
4397 indent_rows(ed, top, bot, 1);
4398 } else {
4399 outdent_rows(ed, top, bot, 1);
4400 }
4401 ed.vim.mode = Mode::Normal;
4402 }
4403 Operator::Reflow => {
4404 ed.push_undo();
4405 let (cursor_row, _) = ed.cursor();
4406 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4407 reflow_rows(ed, top, bot);
4408 ed.vim.mode = Mode::Normal;
4409 }
4410 Operator::Fold => unreachable!("Visual zf takes its own path"),
4413 }
4414 }
4415 Mode::Visual => {
4416 ed.vim.yank_linewise = false;
4417 let anchor = ed.vim.visual_anchor;
4418 let cursor = ed.cursor();
4419 let (top, bot) = order(anchor, cursor);
4420 match op {
4421 Operator::Yank => {
4422 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4423 if !text.is_empty() {
4424 ed.record_yank_to_host(text.clone());
4425 ed.record_yank(text, false);
4426 }
4427 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4428 ed.push_buffer_cursor_to_textarea();
4429 ed.vim.mode = Mode::Normal;
4430 }
4431 Operator::Delete => {
4432 ed.push_undo();
4433 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4434 ed.vim.mode = Mode::Normal;
4435 }
4436 Operator::Change => {
4437 ed.push_undo();
4438 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4439 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4440 }
4441 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4442 let anchor = ed.vim.visual_anchor;
4444 let cursor = ed.cursor();
4445 let (top, bot) = order(anchor, cursor);
4446 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4447 }
4448 Operator::Indent | Operator::Outdent => {
4449 ed.push_undo();
4450 let anchor = ed.vim.visual_anchor;
4451 let cursor = ed.cursor();
4452 let (top, bot) = order(anchor, cursor);
4453 if op == Operator::Indent {
4454 indent_rows(ed, top.0, bot.0, 1);
4455 } else {
4456 outdent_rows(ed, top.0, bot.0, 1);
4457 }
4458 ed.vim.mode = Mode::Normal;
4459 }
4460 Operator::Reflow => {
4461 ed.push_undo();
4462 let anchor = ed.vim.visual_anchor;
4463 let cursor = ed.cursor();
4464 let (top, bot) = order(anchor, cursor);
4465 reflow_rows(ed, top.0, bot.0);
4466 ed.vim.mode = Mode::Normal;
4467 }
4468 Operator::Fold => unreachable!("Visual zf takes its own path"),
4469 }
4470 }
4471 Mode::VisualBlock => apply_block_operator(ed, op),
4472 _ => {}
4473 }
4474}
4475
4476fn block_bounds<H: crate::types::Host>(
4481 ed: &Editor<hjkl_buffer::Buffer, H>,
4482) -> (usize, usize, usize, usize) {
4483 let (ar, ac) = ed.vim.block_anchor;
4484 let (cr, _) = ed.cursor();
4485 let cc = ed.vim.block_vcol;
4486 let top = ar.min(cr);
4487 let bot = ar.max(cr);
4488 let left = ac.min(cc);
4489 let right = ac.max(cc);
4490 (top, bot, left, right)
4491}
4492
4493fn update_block_vcol<H: crate::types::Host>(
4498 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4499 motion: &Motion,
4500) {
4501 match motion {
4502 Motion::Left
4503 | Motion::Right
4504 | Motion::WordFwd
4505 | Motion::BigWordFwd
4506 | Motion::WordBack
4507 | Motion::BigWordBack
4508 | Motion::WordEnd
4509 | Motion::BigWordEnd
4510 | Motion::WordEndBack
4511 | Motion::BigWordEndBack
4512 | Motion::LineStart
4513 | Motion::FirstNonBlank
4514 | Motion::LineEnd
4515 | Motion::Find { .. }
4516 | Motion::FindRepeat { .. }
4517 | Motion::MatchBracket => {
4518 ed.vim.block_vcol = ed.cursor().1;
4519 }
4520 _ => {}
4522 }
4523}
4524
4525fn apply_block_operator<H: crate::types::Host>(
4530 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4531 op: Operator,
4532) {
4533 let (top, bot, left, right) = block_bounds(ed);
4534 let yank = block_yank(ed, top, bot, left, right);
4536
4537 match op {
4538 Operator::Yank => {
4539 if !yank.is_empty() {
4540 ed.record_yank_to_host(yank.clone());
4541 ed.record_yank(yank, false);
4542 }
4543 ed.vim.mode = Mode::Normal;
4544 ed.jump_cursor(top, left);
4545 }
4546 Operator::Delete => {
4547 ed.push_undo();
4548 delete_block_contents(ed, top, bot, left, right);
4549 if !yank.is_empty() {
4550 ed.record_yank_to_host(yank.clone());
4551 ed.record_delete(yank, false);
4552 }
4553 ed.vim.mode = Mode::Normal;
4554 ed.jump_cursor(top, left);
4555 }
4556 Operator::Change => {
4557 ed.push_undo();
4558 delete_block_contents(ed, top, bot, left, right);
4559 if !yank.is_empty() {
4560 ed.record_yank_to_host(yank.clone());
4561 ed.record_delete(yank, false);
4562 }
4563 ed.jump_cursor(top, left);
4564 begin_insert_noundo(
4565 ed,
4566 1,
4567 InsertReason::BlockChange {
4568 top,
4569 bot,
4570 col: left,
4571 },
4572 );
4573 }
4574 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4575 ed.push_undo();
4576 transform_block_case(ed, op, top, bot, left, right);
4577 ed.vim.mode = Mode::Normal;
4578 ed.jump_cursor(top, left);
4579 }
4580 Operator::Indent | Operator::Outdent => {
4581 ed.push_undo();
4585 if op == Operator::Indent {
4586 indent_rows(ed, top, bot, 1);
4587 } else {
4588 outdent_rows(ed, top, bot, 1);
4589 }
4590 ed.vim.mode = Mode::Normal;
4591 }
4592 Operator::Fold => unreachable!("Visual zf takes its own path"),
4593 Operator::Reflow => {
4594 ed.push_undo();
4598 reflow_rows(ed, top, bot);
4599 ed.vim.mode = Mode::Normal;
4600 }
4601 }
4602}
4603
4604fn transform_block_case<H: crate::types::Host>(
4608 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4609 op: Operator,
4610 top: usize,
4611 bot: usize,
4612 left: usize,
4613 right: usize,
4614) {
4615 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4616 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4617 let chars: Vec<char> = lines[r].chars().collect();
4618 if left >= chars.len() {
4619 continue;
4620 }
4621 let end = (right + 1).min(chars.len());
4622 let head: String = chars[..left].iter().collect();
4623 let mid: String = chars[left..end].iter().collect();
4624 let tail: String = chars[end..].iter().collect();
4625 let transformed = match op {
4626 Operator::Uppercase => mid.to_uppercase(),
4627 Operator::Lowercase => mid.to_lowercase(),
4628 Operator::ToggleCase => toggle_case_str(&mid),
4629 _ => mid,
4630 };
4631 lines[r] = format!("{head}{transformed}{tail}");
4632 }
4633 let saved_yank = ed.yank().to_string();
4634 let saved_linewise = ed.vim.yank_linewise;
4635 ed.restore(lines, (top, left));
4636 ed.set_yank(saved_yank);
4637 ed.vim.yank_linewise = saved_linewise;
4638}
4639
4640fn block_yank<H: crate::types::Host>(
4641 ed: &Editor<hjkl_buffer::Buffer, H>,
4642 top: usize,
4643 bot: usize,
4644 left: usize,
4645 right: usize,
4646) -> String {
4647 let lines = buf_lines_to_vec(&ed.buffer);
4648 let mut rows: Vec<String> = Vec::new();
4649 for r in top..=bot {
4650 let line = match lines.get(r) {
4651 Some(l) => l,
4652 None => break,
4653 };
4654 let chars: Vec<char> = line.chars().collect();
4655 let end = (right + 1).min(chars.len());
4656 if left >= chars.len() {
4657 rows.push(String::new());
4658 } else {
4659 rows.push(chars[left..end].iter().collect());
4660 }
4661 }
4662 rows.join("\n")
4663}
4664
4665fn delete_block_contents<H: crate::types::Host>(
4666 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4667 top: usize,
4668 bot: usize,
4669 left: usize,
4670 right: usize,
4671) {
4672 use hjkl_buffer::{Edit, MotionKind, Position};
4673 ed.sync_buffer_content_from_textarea();
4674 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4675 if last_row < top {
4676 return;
4677 }
4678 ed.mutate_edit(Edit::DeleteRange {
4679 start: Position::new(top, left),
4680 end: Position::new(last_row, right),
4681 kind: MotionKind::Block,
4682 });
4683 ed.push_buffer_cursor_to_textarea();
4684}
4685
4686fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4688 let (top, bot, left, right) = block_bounds(ed);
4689 ed.push_undo();
4690 ed.sync_buffer_content_from_textarea();
4691 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4692 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4693 let chars: Vec<char> = lines[r].chars().collect();
4694 if left >= chars.len() {
4695 continue;
4696 }
4697 let end = (right + 1).min(chars.len());
4698 let before: String = chars[..left].iter().collect();
4699 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4700 let after: String = chars[end..].iter().collect();
4701 lines[r] = format!("{before}{middle}{after}");
4702 }
4703 reset_textarea_lines(ed, lines);
4704 ed.vim.mode = Mode::Normal;
4705 ed.jump_cursor(top, left);
4706}
4707
4708fn reset_textarea_lines<H: crate::types::Host>(
4712 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4713 lines: Vec<String>,
4714) {
4715 let cursor = ed.cursor();
4716 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4717 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4718 ed.mark_content_dirty();
4719}
4720
4721type Pos = (usize, usize);
4727
4728fn text_object_range<H: crate::types::Host>(
4732 ed: &Editor<hjkl_buffer::Buffer, H>,
4733 obj: TextObject,
4734 inner: bool,
4735) -> Option<(Pos, Pos, MotionKind)> {
4736 match obj {
4737 TextObject::Word { big } => {
4738 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4739 }
4740 TextObject::Quote(q) => {
4741 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4742 }
4743 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4744 TextObject::Paragraph => {
4745 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4746 }
4747 TextObject::XmlTag => {
4748 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4749 }
4750 TextObject::Sentence => {
4751 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4752 }
4753 }
4754}
4755
4756fn sentence_boundary<H: crate::types::Host>(
4760 ed: &Editor<hjkl_buffer::Buffer, H>,
4761 forward: bool,
4762) -> Option<(usize, usize)> {
4763 let lines = buf_lines_to_vec(&ed.buffer);
4764 if lines.is_empty() {
4765 return None;
4766 }
4767 let pos_to_idx = |pos: (usize, usize)| -> usize {
4768 let mut idx = 0;
4769 for line in lines.iter().take(pos.0) {
4770 idx += line.chars().count() + 1;
4771 }
4772 idx + pos.1
4773 };
4774 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4775 for (r, line) in lines.iter().enumerate() {
4776 let len = line.chars().count();
4777 if idx <= len {
4778 return (r, idx);
4779 }
4780 idx -= len + 1;
4781 }
4782 let last = lines.len().saturating_sub(1);
4783 (last, lines[last].chars().count())
4784 };
4785 let mut chars: Vec<char> = Vec::new();
4786 for (r, line) in lines.iter().enumerate() {
4787 chars.extend(line.chars());
4788 if r + 1 < lines.len() {
4789 chars.push('\n');
4790 }
4791 }
4792 if chars.is_empty() {
4793 return None;
4794 }
4795 let total = chars.len();
4796 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4797 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4798
4799 if forward {
4800 let mut i = cursor_idx + 1;
4803 while i < total {
4804 if is_terminator(chars[i]) {
4805 while i + 1 < total && is_terminator(chars[i + 1]) {
4806 i += 1;
4807 }
4808 if i + 1 >= total {
4809 return None;
4810 }
4811 if chars[i + 1].is_whitespace() {
4812 let mut j = i + 1;
4813 while j < total && chars[j].is_whitespace() {
4814 j += 1;
4815 }
4816 if j >= total {
4817 return None;
4818 }
4819 return Some(idx_to_pos(j));
4820 }
4821 }
4822 i += 1;
4823 }
4824 None
4825 } else {
4826 let find_start = |from: usize| -> Option<usize> {
4830 let mut start = from;
4831 while start > 0 {
4832 let prev = chars[start - 1];
4833 if prev.is_whitespace() {
4834 let mut k = start - 1;
4835 while k > 0 && chars[k - 1].is_whitespace() {
4836 k -= 1;
4837 }
4838 if k > 0 && is_terminator(chars[k - 1]) {
4839 break;
4840 }
4841 }
4842 start -= 1;
4843 }
4844 while start < total && chars[start].is_whitespace() {
4845 start += 1;
4846 }
4847 (start < total).then_some(start)
4848 };
4849 let current_start = find_start(cursor_idx)?;
4850 if current_start < cursor_idx {
4851 return Some(idx_to_pos(current_start));
4852 }
4853 let mut k = current_start;
4856 while k > 0 && chars[k - 1].is_whitespace() {
4857 k -= 1;
4858 }
4859 if k == 0 {
4860 return None;
4861 }
4862 let prev_start = find_start(k - 1)?;
4863 Some(idx_to_pos(prev_start))
4864 }
4865}
4866
4867fn sentence_text_object<H: crate::types::Host>(
4873 ed: &Editor<hjkl_buffer::Buffer, H>,
4874 inner: bool,
4875) -> Option<((usize, usize), (usize, usize))> {
4876 let lines = buf_lines_to_vec(&ed.buffer);
4877 if lines.is_empty() {
4878 return None;
4879 }
4880 let pos_to_idx = |pos: (usize, usize)| -> usize {
4883 let mut idx = 0;
4884 for line in lines.iter().take(pos.0) {
4885 idx += line.chars().count() + 1;
4886 }
4887 idx + pos.1
4888 };
4889 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4890 for (r, line) in lines.iter().enumerate() {
4891 let len = line.chars().count();
4892 if idx <= len {
4893 return (r, idx);
4894 }
4895 idx -= len + 1;
4896 }
4897 let last = lines.len().saturating_sub(1);
4898 (last, lines[last].chars().count())
4899 };
4900 let mut chars: Vec<char> = Vec::new();
4901 for (r, line) in lines.iter().enumerate() {
4902 chars.extend(line.chars());
4903 if r + 1 < lines.len() {
4904 chars.push('\n');
4905 }
4906 }
4907 if chars.is_empty() {
4908 return None;
4909 }
4910
4911 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4912 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4913
4914 let mut start = cursor_idx;
4918 while start > 0 {
4919 let prev = chars[start - 1];
4920 if prev.is_whitespace() {
4921 let mut k = start - 1;
4925 while k > 0 && chars[k - 1].is_whitespace() {
4926 k -= 1;
4927 }
4928 if k > 0 && is_terminator(chars[k - 1]) {
4929 break;
4930 }
4931 }
4932 start -= 1;
4933 }
4934 while start < chars.len() && chars[start].is_whitespace() {
4937 start += 1;
4938 }
4939 if start >= chars.len() {
4940 return None;
4941 }
4942
4943 let mut end = start;
4946 while end < chars.len() {
4947 if is_terminator(chars[end]) {
4948 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4950 end += 1;
4951 }
4952 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4955 break;
4956 }
4957 }
4958 end += 1;
4959 }
4960 let end_idx = (end + 1).min(chars.len());
4962
4963 let final_end = if inner {
4964 end_idx
4965 } else {
4966 let mut e = end_idx;
4970 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4971 e += 1;
4972 }
4973 e
4974 };
4975
4976 Some((idx_to_pos(start), idx_to_pos(final_end)))
4977}
4978
4979fn tag_text_object<H: crate::types::Host>(
4983 ed: &Editor<hjkl_buffer::Buffer, H>,
4984 inner: bool,
4985) -> Option<((usize, usize), (usize, usize))> {
4986 let lines = buf_lines_to_vec(&ed.buffer);
4987 if lines.is_empty() {
4988 return None;
4989 }
4990 let pos_to_idx = |pos: (usize, usize)| -> usize {
4994 let mut idx = 0;
4995 for line in lines.iter().take(pos.0) {
4996 idx += line.chars().count() + 1;
4997 }
4998 idx + pos.1
4999 };
5000 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
5001 for (r, line) in lines.iter().enumerate() {
5002 let len = line.chars().count();
5003 if idx <= len {
5004 return (r, idx);
5005 }
5006 idx -= len + 1;
5007 }
5008 let last = lines.len().saturating_sub(1);
5009 (last, lines[last].chars().count())
5010 };
5011 let mut chars: Vec<char> = Vec::new();
5012 for (r, line) in lines.iter().enumerate() {
5013 chars.extend(line.chars());
5014 if r + 1 < lines.len() {
5015 chars.push('\n');
5016 }
5017 }
5018 let cursor_idx = pos_to_idx(ed.cursor());
5019
5020 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
5028 let mut next_after: Option<(usize, usize, usize, usize)> = None;
5029 let mut i = 0;
5030 while i < chars.len() {
5031 if chars[i] != '<' {
5032 i += 1;
5033 continue;
5034 }
5035 let mut j = i + 1;
5036 while j < chars.len() && chars[j] != '>' {
5037 j += 1;
5038 }
5039 if j >= chars.len() {
5040 break;
5041 }
5042 let inside: String = chars[i + 1..j].iter().collect();
5043 let close_end = j + 1;
5044 let trimmed = inside.trim();
5045 if trimmed.starts_with('!') || trimmed.starts_with('?') {
5046 i = close_end;
5047 continue;
5048 }
5049 if let Some(rest) = trimmed.strip_prefix('/') {
5050 let name = rest.split_whitespace().next().unwrap_or("").to_string();
5051 if !name.is_empty()
5052 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
5053 {
5054 let (open_start, content_start, _) = stack[stack_idx].clone();
5055 stack.truncate(stack_idx);
5056 let content_end = i;
5057 let candidate = (open_start, content_start, content_end, close_end);
5058 if cursor_idx >= content_start && cursor_idx <= content_end {
5059 innermost = match innermost {
5060 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
5061 Some(candidate)
5062 }
5063 None => Some(candidate),
5064 existing => existing,
5065 };
5066 } else if open_start >= cursor_idx && next_after.is_none() {
5067 next_after = Some(candidate);
5068 }
5069 }
5070 } else if !trimmed.ends_with('/') {
5071 let name: String = trimmed
5072 .split(|c: char| c.is_whitespace() || c == '/')
5073 .next()
5074 .unwrap_or("")
5075 .to_string();
5076 if !name.is_empty() {
5077 stack.push((i, close_end, name));
5078 }
5079 }
5080 i = close_end;
5081 }
5082
5083 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
5084 if inner {
5085 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
5086 } else {
5087 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
5088 }
5089}
5090
5091fn is_wordchar(c: char) -> bool {
5092 c.is_alphanumeric() || c == '_'
5093}
5094
5095pub(crate) use hjkl_buffer::is_keyword_char;
5099
5100fn word_text_object<H: crate::types::Host>(
5101 ed: &Editor<hjkl_buffer::Buffer, H>,
5102 inner: bool,
5103 big: bool,
5104) -> Option<((usize, usize), (usize, usize))> {
5105 let (row, col) = ed.cursor();
5106 let line = buf_line(&ed.buffer, row)?;
5107 let chars: Vec<char> = line.chars().collect();
5108 if chars.is_empty() {
5109 return None;
5110 }
5111 let at = col.min(chars.len().saturating_sub(1));
5112 let classify = |c: char| -> u8 {
5113 if c.is_whitespace() {
5114 0
5115 } else if big || is_wordchar(c) {
5116 1
5117 } else {
5118 2
5119 }
5120 };
5121 let cls = classify(chars[at]);
5122 let mut start = at;
5123 while start > 0 && classify(chars[start - 1]) == cls {
5124 start -= 1;
5125 }
5126 let mut end = at;
5127 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
5128 end += 1;
5129 }
5130 let char_byte = |i: usize| {
5132 if i >= chars.len() {
5133 line.len()
5134 } else {
5135 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5136 }
5137 };
5138 let mut start_col = char_byte(start);
5139 let mut end_col = char_byte(end + 1);
5141 if !inner {
5142 let mut t = end + 1;
5144 let mut included_trailing = false;
5145 while t < chars.len() && chars[t].is_whitespace() {
5146 included_trailing = true;
5147 t += 1;
5148 }
5149 if included_trailing {
5150 end_col = char_byte(t);
5151 } else {
5152 let mut s = start;
5153 while s > 0 && chars[s - 1].is_whitespace() {
5154 s -= 1;
5155 }
5156 start_col = char_byte(s);
5157 }
5158 }
5159 Some(((row, start_col), (row, end_col)))
5160}
5161
5162fn quote_text_object<H: crate::types::Host>(
5163 ed: &Editor<hjkl_buffer::Buffer, H>,
5164 q: char,
5165 inner: bool,
5166) -> Option<((usize, usize), (usize, usize))> {
5167 let (row, col) = ed.cursor();
5168 let line = buf_line(&ed.buffer, row)?;
5169 let bytes = line.as_bytes();
5170 let q_byte = q as u8;
5171 let mut positions: Vec<usize> = Vec::new();
5173 for (i, &b) in bytes.iter().enumerate() {
5174 if b == q_byte {
5175 positions.push(i);
5176 }
5177 }
5178 if positions.len() < 2 {
5179 return None;
5180 }
5181 let mut open_idx: Option<usize> = None;
5182 let mut close_idx: Option<usize> = None;
5183 for pair in positions.chunks(2) {
5184 if pair.len() < 2 {
5185 break;
5186 }
5187 if col >= pair[0] && col <= pair[1] {
5188 open_idx = Some(pair[0]);
5189 close_idx = Some(pair[1]);
5190 break;
5191 }
5192 if col < pair[0] {
5193 open_idx = Some(pair[0]);
5194 close_idx = Some(pair[1]);
5195 break;
5196 }
5197 }
5198 let open = open_idx?;
5199 let close = close_idx?;
5200 if inner {
5202 if close <= open + 1 {
5203 return None;
5204 }
5205 Some(((row, open + 1), (row, close)))
5206 } else {
5207 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5214 let mut end = after_close;
5216 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5217 end += 1;
5218 }
5219 Some(((row, open), (row, end)))
5220 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5221 let mut start = open;
5223 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5224 start -= 1;
5225 }
5226 Some(((row, start), (row, close + 1)))
5227 } else {
5228 Some(((row, open), (row, close + 1)))
5229 }
5230 }
5231}
5232
5233fn bracket_text_object<H: crate::types::Host>(
5234 ed: &Editor<hjkl_buffer::Buffer, H>,
5235 open: char,
5236 inner: bool,
5237) -> Option<(Pos, Pos, MotionKind)> {
5238 let close = match open {
5239 '(' => ')',
5240 '[' => ']',
5241 '{' => '}',
5242 '<' => '>',
5243 _ => return None,
5244 };
5245 let (row, col) = ed.cursor();
5246 let lines = buf_lines_to_vec(&ed.buffer);
5247 let lines = lines.as_slice();
5248 let open_pos = find_open_bracket(lines, row, col, open, close)
5253 .or_else(|| find_next_open(lines, row, col, open))?;
5254 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5255 if inner {
5257 if close_pos.0 > open_pos.0 + 1 {
5263 let inner_row_start = open_pos.0 + 1;
5265 let inner_row_end = close_pos.0 - 1;
5266 let end_col = lines
5267 .get(inner_row_end)
5268 .map(|l| l.chars().count())
5269 .unwrap_or(0);
5270 return Some((
5271 (inner_row_start, 0),
5272 (inner_row_end, end_col),
5273 MotionKind::Linewise,
5274 ));
5275 }
5276 let inner_start = advance_pos(lines, open_pos);
5277 if inner_start.0 > close_pos.0
5278 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5279 {
5280 return None;
5281 }
5282 Some((inner_start, close_pos, MotionKind::Exclusive))
5283 } else {
5284 Some((
5285 open_pos,
5286 advance_pos(lines, close_pos),
5287 MotionKind::Exclusive,
5288 ))
5289 }
5290}
5291
5292fn find_open_bracket(
5293 lines: &[String],
5294 row: usize,
5295 col: usize,
5296 open: char,
5297 close: char,
5298) -> Option<(usize, usize)> {
5299 let mut depth: i32 = 0;
5300 let mut r = row;
5301 let mut c = col as isize;
5302 loop {
5303 let cur = &lines[r];
5304 let chars: Vec<char> = cur.chars().collect();
5305 if (c as usize) >= chars.len() {
5309 c = chars.len() as isize - 1;
5310 }
5311 while c >= 0 {
5312 let ch = chars[c as usize];
5313 if ch == close {
5314 depth += 1;
5315 } else if ch == open {
5316 if depth == 0 {
5317 return Some((r, c as usize));
5318 }
5319 depth -= 1;
5320 }
5321 c -= 1;
5322 }
5323 if r == 0 {
5324 return None;
5325 }
5326 r -= 1;
5327 c = lines[r].chars().count() as isize - 1;
5328 }
5329}
5330
5331fn find_close_bracket(
5332 lines: &[String],
5333 row: usize,
5334 start_col: usize,
5335 open: char,
5336 close: char,
5337) -> Option<(usize, usize)> {
5338 let mut depth: i32 = 0;
5339 let mut r = row;
5340 let mut c = start_col;
5341 loop {
5342 let cur = &lines[r];
5343 let chars: Vec<char> = cur.chars().collect();
5344 while c < chars.len() {
5345 let ch = chars[c];
5346 if ch == open {
5347 depth += 1;
5348 } else if ch == close {
5349 if depth == 0 {
5350 return Some((r, c));
5351 }
5352 depth -= 1;
5353 }
5354 c += 1;
5355 }
5356 if r + 1 >= lines.len() {
5357 return None;
5358 }
5359 r += 1;
5360 c = 0;
5361 }
5362}
5363
5364fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5368 let mut r = row;
5369 let mut c = col;
5370 while r < lines.len() {
5371 let chars: Vec<char> = lines[r].chars().collect();
5372 while c < chars.len() {
5373 if chars[c] == open {
5374 return Some((r, c));
5375 }
5376 c += 1;
5377 }
5378 r += 1;
5379 c = 0;
5380 }
5381 None
5382}
5383
5384fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5385 let (r, c) = pos;
5386 let line_len = lines[r].chars().count();
5387 if c < line_len {
5388 (r, c + 1)
5389 } else if r + 1 < lines.len() {
5390 (r + 1, 0)
5391 } else {
5392 pos
5393 }
5394}
5395
5396fn paragraph_text_object<H: crate::types::Host>(
5397 ed: &Editor<hjkl_buffer::Buffer, H>,
5398 inner: bool,
5399) -> Option<((usize, usize), (usize, usize))> {
5400 let (row, _) = ed.cursor();
5401 let lines = buf_lines_to_vec(&ed.buffer);
5402 if lines.is_empty() {
5403 return None;
5404 }
5405 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5407 if is_blank(row) {
5408 return None;
5409 }
5410 let mut top = row;
5411 while top > 0 && !is_blank(top - 1) {
5412 top -= 1;
5413 }
5414 let mut bot = row;
5415 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5416 bot += 1;
5417 }
5418 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5420 bot += 1;
5421 }
5422 let end_col = lines[bot].chars().count();
5423 Some(((top, 0), (bot, end_col)))
5424}
5425
5426fn read_vim_range<H: crate::types::Host>(
5432 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5433 start: (usize, usize),
5434 end: (usize, usize),
5435 kind: MotionKind,
5436) -> String {
5437 let (top, bot) = order(start, end);
5438 ed.sync_buffer_content_from_textarea();
5439 let lines = buf_lines_to_vec(&ed.buffer);
5440 match kind {
5441 MotionKind::Linewise => {
5442 let lo = top.0;
5443 let hi = bot.0.min(lines.len().saturating_sub(1));
5444 let mut text = lines[lo..=hi].join("\n");
5445 text.push('\n');
5446 text
5447 }
5448 MotionKind::Inclusive | MotionKind::Exclusive => {
5449 let inclusive = matches!(kind, MotionKind::Inclusive);
5450 let mut out = String::new();
5452 for row in top.0..=bot.0 {
5453 let line = lines.get(row).map(String::as_str).unwrap_or("");
5454 let lo = if row == top.0 { top.1 } else { 0 };
5455 let hi_unclamped = if row == bot.0 {
5456 if inclusive { bot.1 + 1 } else { bot.1 }
5457 } else {
5458 line.chars().count() + 1
5459 };
5460 let row_chars: Vec<char> = line.chars().collect();
5461 let hi = hi_unclamped.min(row_chars.len());
5462 if lo < hi {
5463 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5464 }
5465 if row < bot.0 {
5466 out.push('\n');
5467 }
5468 }
5469 out
5470 }
5471 }
5472}
5473
5474fn cut_vim_range<H: crate::types::Host>(
5483 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5484 start: (usize, usize),
5485 end: (usize, usize),
5486 kind: MotionKind,
5487) -> String {
5488 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5489 let (top, bot) = order(start, end);
5490 ed.sync_buffer_content_from_textarea();
5491 let (buf_start, buf_end, buf_kind) = match kind {
5492 MotionKind::Linewise => (
5493 Position::new(top.0, 0),
5494 Position::new(bot.0, 0),
5495 BufKind::Line,
5496 ),
5497 MotionKind::Inclusive => {
5498 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5499 let next = if bot.1 < line_chars {
5503 Position::new(bot.0, bot.1 + 1)
5504 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5505 Position::new(bot.0 + 1, 0)
5506 } else {
5507 Position::new(bot.0, line_chars)
5508 };
5509 (Position::new(top.0, top.1), next, BufKind::Char)
5510 }
5511 MotionKind::Exclusive => (
5512 Position::new(top.0, top.1),
5513 Position::new(bot.0, bot.1),
5514 BufKind::Char,
5515 ),
5516 };
5517 let inverse = ed.mutate_edit(Edit::DeleteRange {
5518 start: buf_start,
5519 end: buf_end,
5520 kind: buf_kind,
5521 });
5522 let text = match inverse {
5523 Edit::InsertStr { text, .. } => text,
5524 _ => String::new(),
5525 };
5526 if !text.is_empty() {
5527 ed.record_yank_to_host(text.clone());
5528 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5529 }
5530 ed.push_buffer_cursor_to_textarea();
5531 text
5532}
5533
5534fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5540 use hjkl_buffer::{Edit, MotionKind, Position};
5541 ed.sync_buffer_content_from_textarea();
5542 let cursor = buf_cursor_pos(&ed.buffer);
5543 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5544 if cursor.col >= line_chars {
5545 return;
5546 }
5547 let inverse = ed.mutate_edit(Edit::DeleteRange {
5548 start: cursor,
5549 end: Position::new(cursor.row, line_chars),
5550 kind: MotionKind::Char,
5551 });
5552 if let Edit::InsertStr { text, .. } = inverse
5553 && !text.is_empty()
5554 {
5555 ed.record_yank_to_host(text.clone());
5556 ed.vim.yank_linewise = false;
5557 ed.set_yank(text);
5558 }
5559 buf_set_cursor_pos(&mut ed.buffer, cursor);
5560 ed.push_buffer_cursor_to_textarea();
5561}
5562
5563fn do_char_delete<H: crate::types::Host>(
5564 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5565 forward: bool,
5566 count: usize,
5567) {
5568 use hjkl_buffer::{Edit, MotionKind, Position};
5569 ed.push_undo();
5570 ed.sync_buffer_content_from_textarea();
5571 let mut deleted = String::new();
5574 for _ in 0..count {
5575 let cursor = buf_cursor_pos(&ed.buffer);
5576 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5577 if forward {
5578 if cursor.col >= line_chars {
5581 continue;
5582 }
5583 let inverse = ed.mutate_edit(Edit::DeleteRange {
5584 start: cursor,
5585 end: Position::new(cursor.row, cursor.col + 1),
5586 kind: MotionKind::Char,
5587 });
5588 if let Edit::InsertStr { text, .. } = inverse {
5589 deleted.push_str(&text);
5590 }
5591 } else {
5592 if cursor.col == 0 {
5594 continue;
5595 }
5596 let inverse = ed.mutate_edit(Edit::DeleteRange {
5597 start: Position::new(cursor.row, cursor.col - 1),
5598 end: cursor,
5599 kind: MotionKind::Char,
5600 });
5601 if let Edit::InsertStr { text, .. } = inverse {
5602 deleted = text + &deleted;
5605 }
5606 }
5607 }
5608 if !deleted.is_empty() {
5609 ed.record_yank_to_host(deleted.clone());
5610 ed.record_delete(deleted, false);
5611 }
5612 ed.push_buffer_cursor_to_textarea();
5613}
5614
5615fn adjust_number<H: crate::types::Host>(
5619 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5620 delta: i64,
5621) -> bool {
5622 use hjkl_buffer::{Edit, MotionKind, Position};
5623 ed.sync_buffer_content_from_textarea();
5624 let cursor = buf_cursor_pos(&ed.buffer);
5625 let row = cursor.row;
5626 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5627 Some(l) => l.chars().collect(),
5628 None => return false,
5629 };
5630 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5631 return false;
5632 };
5633 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5634 digit_start - 1
5635 } else {
5636 digit_start
5637 };
5638 let mut span_end = digit_start;
5639 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5640 span_end += 1;
5641 }
5642 let s: String = chars[span_start..span_end].iter().collect();
5643 let Ok(n) = s.parse::<i64>() else {
5644 return false;
5645 };
5646 let new_s = n.saturating_add(delta).to_string();
5647
5648 ed.push_undo();
5649 let span_start_pos = Position::new(row, span_start);
5650 let span_end_pos = Position::new(row, span_end);
5651 ed.mutate_edit(Edit::DeleteRange {
5652 start: span_start_pos,
5653 end: span_end_pos,
5654 kind: MotionKind::Char,
5655 });
5656 ed.mutate_edit(Edit::InsertStr {
5657 at: span_start_pos,
5658 text: new_s.clone(),
5659 });
5660 let new_len = new_s.chars().count();
5661 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5662 ed.push_buffer_cursor_to_textarea();
5663 true
5664}
5665
5666pub(crate) fn replace_char<H: crate::types::Host>(
5667 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5668 ch: char,
5669 count: usize,
5670) {
5671 use hjkl_buffer::{Edit, MotionKind, Position};
5672 ed.push_undo();
5673 ed.sync_buffer_content_from_textarea();
5674 for _ in 0..count {
5675 let cursor = buf_cursor_pos(&ed.buffer);
5676 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5677 if cursor.col >= line_chars {
5678 break;
5679 }
5680 ed.mutate_edit(Edit::DeleteRange {
5681 start: cursor,
5682 end: Position::new(cursor.row, cursor.col + 1),
5683 kind: MotionKind::Char,
5684 });
5685 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5686 }
5687 crate::motions::move_left(&mut ed.buffer, 1);
5689 ed.push_buffer_cursor_to_textarea();
5690}
5691
5692fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5693 use hjkl_buffer::{Edit, MotionKind, Position};
5694 ed.sync_buffer_content_from_textarea();
5695 let cursor = buf_cursor_pos(&ed.buffer);
5696 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5697 return;
5698 };
5699 let toggled = if c.is_uppercase() {
5700 c.to_lowercase().next().unwrap_or(c)
5701 } else {
5702 c.to_uppercase().next().unwrap_or(c)
5703 };
5704 ed.mutate_edit(Edit::DeleteRange {
5705 start: cursor,
5706 end: Position::new(cursor.row, cursor.col + 1),
5707 kind: MotionKind::Char,
5708 });
5709 ed.mutate_edit(Edit::InsertChar {
5710 at: cursor,
5711 ch: toggled,
5712 });
5713}
5714
5715fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5716 use hjkl_buffer::{Edit, Position};
5717 ed.sync_buffer_content_from_textarea();
5718 let row = buf_cursor_pos(&ed.buffer).row;
5719 if row + 1 >= buf_row_count(&ed.buffer) {
5720 return;
5721 }
5722 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5723 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5724 let next_trimmed = next_raw.trim_start();
5725 let cur_chars = cur_line.chars().count();
5726 let next_chars = next_raw.chars().count();
5727 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5730 " "
5731 } else {
5732 ""
5733 };
5734 let joined = format!("{cur_line}{separator}{next_trimmed}");
5735 ed.mutate_edit(Edit::Replace {
5736 start: Position::new(row, 0),
5737 end: Position::new(row + 1, next_chars),
5738 with: joined,
5739 });
5740 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5744 ed.push_buffer_cursor_to_textarea();
5745}
5746
5747fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5750 use hjkl_buffer::Edit;
5751 ed.sync_buffer_content_from_textarea();
5752 let row = buf_cursor_pos(&ed.buffer).row;
5753 if row + 1 >= buf_row_count(&ed.buffer) {
5754 return;
5755 }
5756 let join_col = buf_line_chars(&ed.buffer, row);
5757 ed.mutate_edit(Edit::JoinLines {
5758 row,
5759 count: 1,
5760 with_space: false,
5761 });
5762 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5764 ed.push_buffer_cursor_to_textarea();
5765}
5766
5767fn do_paste<H: crate::types::Host>(
5768 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5769 before: bool,
5770 count: usize,
5771) {
5772 use hjkl_buffer::{Edit, Position};
5773 ed.push_undo();
5774 let selector = ed.vim.pending_register.take();
5779 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5780 Some(slot) => (slot.text.clone(), slot.linewise),
5781 None => {
5787 let s = &ed.registers().unnamed;
5788 (s.text.clone(), s.linewise)
5789 }
5790 };
5791 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5795 for _ in 0..count {
5796 ed.sync_buffer_content_from_textarea();
5797 let yank = yank.clone();
5798 if yank.is_empty() {
5799 continue;
5800 }
5801 if linewise {
5802 let text = yank.trim_matches('\n').to_string();
5806 let row = buf_cursor_pos(&ed.buffer).row;
5807 let target_row = if before {
5808 ed.mutate_edit(Edit::InsertStr {
5809 at: Position::new(row, 0),
5810 text: format!("{text}\n"),
5811 });
5812 row
5813 } else {
5814 let line_chars = buf_line_chars(&ed.buffer, row);
5815 ed.mutate_edit(Edit::InsertStr {
5816 at: Position::new(row, line_chars),
5817 text: format!("\n{text}"),
5818 });
5819 row + 1
5820 };
5821 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5822 crate::motions::move_first_non_blank(&mut ed.buffer);
5823 ed.push_buffer_cursor_to_textarea();
5824 let payload_lines = text.lines().count().max(1);
5826 let bot_row = target_row + payload_lines - 1;
5827 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5828 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5829 } else {
5830 let cursor = buf_cursor_pos(&ed.buffer);
5834 let at = if before {
5835 cursor
5836 } else {
5837 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5838 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5839 };
5840 ed.mutate_edit(Edit::InsertStr {
5841 at,
5842 text: yank.clone(),
5843 });
5844 crate::motions::move_left(&mut ed.buffer, 1);
5847 ed.push_buffer_cursor_to_textarea();
5848 let lo = (at.row, at.col);
5850 let hi = ed.cursor();
5851 paste_mark = Some((lo, hi));
5852 }
5853 }
5854 if let Some((lo, hi)) = paste_mark {
5855 ed.set_mark('[', lo);
5856 ed.set_mark(']', hi);
5857 }
5858 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5860}
5861
5862pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5863 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5864 let current = ed.snapshot();
5865 ed.redo_stack.push(current);
5866 ed.restore(lines, cursor);
5867 }
5868 ed.vim.mode = Mode::Normal;
5869 clamp_cursor_to_normal_mode(ed);
5873}
5874
5875pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5876 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5877 let current = ed.snapshot();
5878 ed.undo_stack.push(current);
5879 ed.cap_undo();
5880 ed.restore(lines, cursor);
5881 }
5882 ed.vim.mode = Mode::Normal;
5883}
5884
5885fn replay_insert_and_finish<H: crate::types::Host>(
5892 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5893 text: &str,
5894) {
5895 use hjkl_buffer::{Edit, Position};
5896 let cursor = ed.cursor();
5897 ed.mutate_edit(Edit::InsertStr {
5898 at: Position::new(cursor.0, cursor.1),
5899 text: text.to_string(),
5900 });
5901 if ed.vim.insert_session.take().is_some() {
5902 if ed.cursor().1 > 0 {
5903 crate::motions::move_left(&mut ed.buffer, 1);
5904 ed.push_buffer_cursor_to_textarea();
5905 }
5906 ed.vim.mode = Mode::Normal;
5907 }
5908}
5909
5910fn replay_last_change<H: crate::types::Host>(
5911 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5912 outer_count: usize,
5913) {
5914 let Some(change) = ed.vim.last_change.clone() else {
5915 return;
5916 };
5917 ed.vim.replaying = true;
5918 let scale = if outer_count > 0 { outer_count } else { 1 };
5919 match change {
5920 LastChange::OpMotion {
5921 op,
5922 motion,
5923 count,
5924 inserted,
5925 } => {
5926 let total = count.max(1) * scale;
5927 apply_op_with_motion(ed, op, &motion, total);
5928 if let Some(text) = inserted {
5929 replay_insert_and_finish(ed, &text);
5930 }
5931 }
5932 LastChange::OpTextObj {
5933 op,
5934 obj,
5935 inner,
5936 inserted,
5937 } => {
5938 apply_op_with_text_object(ed, op, obj, inner);
5939 if let Some(text) = inserted {
5940 replay_insert_and_finish(ed, &text);
5941 }
5942 }
5943 LastChange::LineOp {
5944 op,
5945 count,
5946 inserted,
5947 } => {
5948 let total = count.max(1) * scale;
5949 execute_line_op(ed, op, total);
5950 if let Some(text) = inserted {
5951 replay_insert_and_finish(ed, &text);
5952 }
5953 }
5954 LastChange::CharDel { forward, count } => {
5955 do_char_delete(ed, forward, count * scale);
5956 }
5957 LastChange::ReplaceChar { ch, count } => {
5958 replace_char(ed, ch, count * scale);
5959 }
5960 LastChange::ToggleCase { count } => {
5961 for _ in 0..count * scale {
5962 ed.push_undo();
5963 toggle_case_at_cursor(ed);
5964 }
5965 }
5966 LastChange::JoinLine { count } => {
5967 for _ in 0..count * scale {
5968 ed.push_undo();
5969 join_line(ed);
5970 }
5971 }
5972 LastChange::Paste { before, count } => {
5973 do_paste(ed, before, count * scale);
5974 }
5975 LastChange::DeleteToEol { inserted } => {
5976 use hjkl_buffer::{Edit, Position};
5977 ed.push_undo();
5978 delete_to_eol(ed);
5979 if let Some(text) = inserted {
5980 let cursor = ed.cursor();
5981 ed.mutate_edit(Edit::InsertStr {
5982 at: Position::new(cursor.0, cursor.1),
5983 text,
5984 });
5985 }
5986 }
5987 LastChange::OpenLine { above, inserted } => {
5988 use hjkl_buffer::{Edit, Position};
5989 ed.push_undo();
5990 ed.sync_buffer_content_from_textarea();
5991 let row = buf_cursor_pos(&ed.buffer).row;
5992 if above {
5993 ed.mutate_edit(Edit::InsertStr {
5994 at: Position::new(row, 0),
5995 text: "\n".to_string(),
5996 });
5997 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5998 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5999 } else {
6000 let line_chars = buf_line_chars(&ed.buffer, row);
6001 ed.mutate_edit(Edit::InsertStr {
6002 at: Position::new(row, line_chars),
6003 text: "\n".to_string(),
6004 });
6005 }
6006 ed.push_buffer_cursor_to_textarea();
6007 let cursor = ed.cursor();
6008 ed.mutate_edit(Edit::InsertStr {
6009 at: Position::new(cursor.0, cursor.1),
6010 text: inserted,
6011 });
6012 }
6013 LastChange::InsertAt {
6014 entry,
6015 inserted,
6016 count,
6017 } => {
6018 use hjkl_buffer::{Edit, Position};
6019 ed.push_undo();
6020 match entry {
6021 InsertEntry::I => {}
6022 InsertEntry::ShiftI => move_first_non_whitespace(ed),
6023 InsertEntry::A => {
6024 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6025 ed.push_buffer_cursor_to_textarea();
6026 }
6027 InsertEntry::ShiftA => {
6028 crate::motions::move_line_end(&mut ed.buffer);
6029 crate::motions::move_right_to_end(&mut ed.buffer, 1);
6030 ed.push_buffer_cursor_to_textarea();
6031 }
6032 }
6033 for _ in 0..count.max(1) {
6034 let cursor = ed.cursor();
6035 ed.mutate_edit(Edit::InsertStr {
6036 at: Position::new(cursor.0, cursor.1),
6037 text: inserted.clone(),
6038 });
6039 }
6040 }
6041 }
6042 ed.vim.replaying = false;
6043}
6044
6045fn extract_inserted(before: &str, after: &str) -> String {
6048 let before_chars: Vec<char> = before.chars().collect();
6049 let after_chars: Vec<char> = after.chars().collect();
6050 if after_chars.len() <= before_chars.len() {
6051 return String::new();
6052 }
6053 let prefix = before_chars
6054 .iter()
6055 .zip(after_chars.iter())
6056 .take_while(|(a, b)| a == b)
6057 .count();
6058 let max_suffix = before_chars.len() - prefix;
6059 let suffix = before_chars
6060 .iter()
6061 .rev()
6062 .zip(after_chars.iter().rev())
6063 .take(max_suffix)
6064 .take_while(|(a, b)| a == b)
6065 .count();
6066 after_chars[prefix..after_chars.len() - suffix]
6067 .iter()
6068 .collect()
6069}
6070
6071#[cfg(all(test, feature = "crossterm"))]
6074mod tests {
6075 use crate::VimMode;
6076 use crate::editor::Editor;
6077 use crate::types::Host;
6078 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
6079
6080 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
6081 let mut iter = keys.chars().peekable();
6085 while let Some(c) = iter.next() {
6086 if c == '<' {
6087 let mut tag = String::new();
6088 for ch in iter.by_ref() {
6089 if ch == '>' {
6090 break;
6091 }
6092 tag.push(ch);
6093 }
6094 let ev = match tag.as_str() {
6095 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
6096 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
6097 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
6098 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
6099 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
6100 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
6101 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
6102 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
6103 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
6107 s if s.starts_with("C-") => {
6108 let ch = s.chars().nth(2).unwrap();
6109 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
6110 }
6111 _ => continue,
6112 };
6113 e.handle_key(ev);
6114 } else {
6115 let mods = if c.is_uppercase() {
6116 KeyModifiers::SHIFT
6117 } else {
6118 KeyModifiers::NONE
6119 };
6120 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
6121 }
6122 }
6123 }
6124
6125 fn editor_with(content: &str) -> Editor {
6126 let opts = crate::types::Options {
6131 shiftwidth: 2,
6132 ..crate::types::Options::default()
6133 };
6134 let mut e = Editor::new(
6135 hjkl_buffer::Buffer::new(),
6136 crate::types::DefaultHost::new(),
6137 opts,
6138 );
6139 e.set_content(content);
6140 e
6141 }
6142
6143 #[test]
6144 fn f_char_jumps_on_line() {
6145 let mut e = editor_with("hello world");
6146 run_keys(&mut e, "fw");
6147 assert_eq!(e.cursor(), (0, 6));
6148 }
6149
6150 #[test]
6151 fn cap_f_jumps_backward() {
6152 let mut e = editor_with("hello world");
6153 e.jump_cursor(0, 10);
6154 run_keys(&mut e, "Fo");
6155 assert_eq!(e.cursor().1, 7);
6156 }
6157
6158 #[test]
6159 fn t_stops_before_char() {
6160 let mut e = editor_with("hello");
6161 run_keys(&mut e, "tl");
6162 assert_eq!(e.cursor(), (0, 1));
6163 }
6164
6165 #[test]
6166 fn semicolon_repeats_find() {
6167 let mut e = editor_with("aa.bb.cc");
6168 run_keys(&mut e, "f.");
6169 assert_eq!(e.cursor().1, 2);
6170 run_keys(&mut e, ";");
6171 assert_eq!(e.cursor().1, 5);
6172 }
6173
6174 #[test]
6175 fn comma_repeats_find_reverse() {
6176 let mut e = editor_with("aa.bb.cc");
6177 run_keys(&mut e, "f.");
6178 run_keys(&mut e, ";");
6179 run_keys(&mut e, ",");
6180 assert_eq!(e.cursor().1, 2);
6181 }
6182
6183 #[test]
6184 fn di_quote_deletes_content() {
6185 let mut e = editor_with("foo \"bar\" baz");
6186 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6188 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6189 }
6190
6191 #[test]
6192 fn da_quote_deletes_with_quotes() {
6193 let mut e = editor_with("foo \"bar\" baz");
6196 e.jump_cursor(0, 6);
6197 run_keys(&mut e, "da\"");
6198 assert_eq!(e.buffer().lines()[0], "foo baz");
6199 }
6200
6201 #[test]
6202 fn ci_paren_deletes_and_inserts() {
6203 let mut e = editor_with("fn(a, b, c)");
6204 e.jump_cursor(0, 5);
6205 run_keys(&mut e, "ci(");
6206 assert_eq!(e.vim_mode(), VimMode::Insert);
6207 assert_eq!(e.buffer().lines()[0], "fn()");
6208 }
6209
6210 #[test]
6211 fn diw_deletes_inner_word() {
6212 let mut e = editor_with("hello world");
6213 e.jump_cursor(0, 2);
6214 run_keys(&mut e, "diw");
6215 assert_eq!(e.buffer().lines()[0], " world");
6216 }
6217
6218 #[test]
6219 fn daw_deletes_word_with_trailing_space() {
6220 let mut e = editor_with("hello world");
6221 run_keys(&mut e, "daw");
6222 assert_eq!(e.buffer().lines()[0], "world");
6223 }
6224
6225 #[test]
6226 fn percent_jumps_to_matching_bracket() {
6227 let mut e = editor_with("foo(bar)");
6228 e.jump_cursor(0, 3);
6229 run_keys(&mut e, "%");
6230 assert_eq!(e.cursor().1, 7);
6231 run_keys(&mut e, "%");
6232 assert_eq!(e.cursor().1, 3);
6233 }
6234
6235 #[test]
6236 fn dot_repeats_last_change() {
6237 let mut e = editor_with("aaa bbb ccc");
6238 run_keys(&mut e, "dw");
6239 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6240 run_keys(&mut e, ".");
6241 assert_eq!(e.buffer().lines()[0], "ccc");
6242 }
6243
6244 #[test]
6245 fn dot_repeats_change_operator_with_text() {
6246 let mut e = editor_with("foo foo foo");
6247 run_keys(&mut e, "cwbar<Esc>");
6248 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6249 run_keys(&mut e, "w");
6251 run_keys(&mut e, ".");
6252 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6253 }
6254
6255 #[test]
6256 fn dot_repeats_x() {
6257 let mut e = editor_with("abcdef");
6258 run_keys(&mut e, "x");
6259 run_keys(&mut e, "..");
6260 assert_eq!(e.buffer().lines()[0], "def");
6261 }
6262
6263 #[test]
6264 fn count_operator_motion_compose() {
6265 let mut e = editor_with("one two three four five");
6266 run_keys(&mut e, "d3w");
6267 assert_eq!(e.buffer().lines()[0], "four five");
6268 }
6269
6270 #[test]
6271 fn two_dd_deletes_two_lines() {
6272 let mut e = editor_with("a\nb\nc");
6273 run_keys(&mut e, "2dd");
6274 assert_eq!(e.buffer().lines().len(), 1);
6275 assert_eq!(e.buffer().lines()[0], "c");
6276 }
6277
6278 #[test]
6283 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6284 let mut e = editor_with("one\ntwo\n three\nfour");
6285 e.jump_cursor(1, 2);
6286 run_keys(&mut e, "dd");
6287 assert_eq!(e.buffer().lines()[1], " three");
6289 assert_eq!(e.cursor(), (1, 4));
6290 }
6291
6292 #[test]
6293 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6294 let mut e = editor_with("one\n two\nthree");
6295 e.jump_cursor(2, 0);
6296 run_keys(&mut e, "dd");
6297 assert_eq!(e.buffer().lines().len(), 2);
6299 assert_eq!(e.cursor(), (1, 2));
6300 }
6301
6302 #[test]
6303 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6304 let mut e = editor_with("lonely");
6305 run_keys(&mut e, "dd");
6306 assert_eq!(e.buffer().lines().len(), 1);
6307 assert_eq!(e.buffer().lines()[0], "");
6308 assert_eq!(e.cursor(), (0, 0));
6309 }
6310
6311 #[test]
6312 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6313 let mut e = editor_with("a\nb\nc\n d\ne");
6314 e.jump_cursor(1, 0);
6316 run_keys(&mut e, "3dd");
6317 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6318 assert_eq!(e.cursor(), (1, 0));
6319 }
6320
6321 #[test]
6322 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6323 let mut e = editor_with(" line one\n line two\n xyz!");
6342 e.jump_cursor(0, 8);
6344 assert_eq!(e.cursor(), (0, 8));
6345 run_keys(&mut e, "dd");
6348 assert_eq!(
6349 e.cursor(),
6350 (0, 4),
6351 "dd must place cursor on first-non-blank"
6352 );
6353 run_keys(&mut e, "j");
6357 let (row, col) = e.cursor();
6358 assert_eq!(row, 1);
6359 assert_eq!(
6360 col, 4,
6361 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6362 );
6363 }
6364
6365 #[test]
6366 fn gu_lowercases_motion_range() {
6367 let mut e = editor_with("HELLO WORLD");
6368 run_keys(&mut e, "guw");
6369 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6370 assert_eq!(e.cursor(), (0, 0));
6371 }
6372
6373 #[test]
6374 fn g_u_uppercases_text_object() {
6375 let mut e = editor_with("hello world");
6376 run_keys(&mut e, "gUiw");
6378 assert_eq!(e.buffer().lines()[0], "HELLO world");
6379 assert_eq!(e.cursor(), (0, 0));
6380 }
6381
6382 #[test]
6383 fn g_tilde_toggles_case_of_range() {
6384 let mut e = editor_with("Hello World");
6385 run_keys(&mut e, "g~iw");
6386 assert_eq!(e.buffer().lines()[0], "hELLO World");
6387 }
6388
6389 #[test]
6390 fn g_uu_uppercases_current_line() {
6391 let mut e = editor_with("select 1\nselect 2");
6392 run_keys(&mut e, "gUU");
6393 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6394 assert_eq!(e.buffer().lines()[1], "select 2");
6395 }
6396
6397 #[test]
6398 fn gugu_lowercases_current_line() {
6399 let mut e = editor_with("FOO BAR\nBAZ");
6400 run_keys(&mut e, "gugu");
6401 assert_eq!(e.buffer().lines()[0], "foo bar");
6402 }
6403
6404 #[test]
6405 fn visual_u_uppercases_selection() {
6406 let mut e = editor_with("hello world");
6407 run_keys(&mut e, "veU");
6409 assert_eq!(e.buffer().lines()[0], "HELLO world");
6410 }
6411
6412 #[test]
6413 fn visual_line_u_lowercases_line() {
6414 let mut e = editor_with("HELLO WORLD\nOTHER");
6415 run_keys(&mut e, "Vu");
6416 assert_eq!(e.buffer().lines()[0], "hello world");
6417 assert_eq!(e.buffer().lines()[1], "OTHER");
6418 }
6419
6420 #[test]
6421 fn g_uu_with_count_uppercases_multiple_lines() {
6422 let mut e = editor_with("one\ntwo\nthree\nfour");
6423 run_keys(&mut e, "3gUU");
6425 assert_eq!(e.buffer().lines()[0], "ONE");
6426 assert_eq!(e.buffer().lines()[1], "TWO");
6427 assert_eq!(e.buffer().lines()[2], "THREE");
6428 assert_eq!(e.buffer().lines()[3], "four");
6429 }
6430
6431 #[test]
6432 fn double_gt_indents_current_line() {
6433 let mut e = editor_with("hello");
6434 run_keys(&mut e, ">>");
6435 assert_eq!(e.buffer().lines()[0], " hello");
6436 assert_eq!(e.cursor(), (0, 2));
6438 }
6439
6440 #[test]
6441 fn double_lt_outdents_current_line() {
6442 let mut e = editor_with(" hello");
6443 run_keys(&mut e, "<lt><lt>");
6444 assert_eq!(e.buffer().lines()[0], " hello");
6445 assert_eq!(e.cursor(), (0, 2));
6446 }
6447
6448 #[test]
6449 fn count_double_gt_indents_multiple_lines() {
6450 let mut e = editor_with("a\nb\nc\nd");
6451 run_keys(&mut e, "3>>");
6453 assert_eq!(e.buffer().lines()[0], " a");
6454 assert_eq!(e.buffer().lines()[1], " b");
6455 assert_eq!(e.buffer().lines()[2], " c");
6456 assert_eq!(e.buffer().lines()[3], "d");
6457 }
6458
6459 #[test]
6460 fn outdent_clips_ragged_leading_whitespace() {
6461 let mut e = editor_with(" x");
6464 run_keys(&mut e, "<lt><lt>");
6465 assert_eq!(e.buffer().lines()[0], "x");
6466 }
6467
6468 #[test]
6469 fn indent_motion_is_always_linewise() {
6470 let mut e = editor_with("foo bar");
6473 run_keys(&mut e, ">w");
6474 assert_eq!(e.buffer().lines()[0], " foo bar");
6475 }
6476
6477 #[test]
6478 fn indent_text_object_extends_over_paragraph() {
6479 let mut e = editor_with("a\nb\n\nc\nd");
6480 run_keys(&mut e, ">ap");
6482 assert_eq!(e.buffer().lines()[0], " a");
6483 assert_eq!(e.buffer().lines()[1], " b");
6484 assert_eq!(e.buffer().lines()[2], "");
6485 assert_eq!(e.buffer().lines()[3], "c");
6486 }
6487
6488 #[test]
6489 fn visual_line_indent_shifts_selected_rows() {
6490 let mut e = editor_with("x\ny\nz");
6491 run_keys(&mut e, "Vj>");
6493 assert_eq!(e.buffer().lines()[0], " x");
6494 assert_eq!(e.buffer().lines()[1], " y");
6495 assert_eq!(e.buffer().lines()[2], "z");
6496 }
6497
6498 #[test]
6499 fn outdent_empty_line_is_noop() {
6500 let mut e = editor_with("\nfoo");
6501 run_keys(&mut e, "<lt><lt>");
6502 assert_eq!(e.buffer().lines()[0], "");
6503 }
6504
6505 #[test]
6506 fn indent_skips_empty_lines() {
6507 let mut e = editor_with("");
6510 run_keys(&mut e, ">>");
6511 assert_eq!(e.buffer().lines()[0], "");
6512 }
6513
6514 #[test]
6515 fn insert_ctrl_t_indents_current_line() {
6516 let mut e = editor_with("x");
6517 run_keys(&mut e, "i<C-t>");
6519 assert_eq!(e.buffer().lines()[0], " x");
6520 assert_eq!(e.cursor(), (0, 2));
6523 }
6524
6525 #[test]
6526 fn insert_ctrl_d_outdents_current_line() {
6527 let mut e = editor_with(" x");
6528 run_keys(&mut e, "A<C-d>");
6530 assert_eq!(e.buffer().lines()[0], " x");
6531 }
6532
6533 #[test]
6534 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6535 let mut e = editor_with("first\nsecond");
6536 e.jump_cursor(1, 0);
6537 run_keys(&mut e, "h");
6538 assert_eq!(e.cursor(), (1, 0));
6540 }
6541
6542 #[test]
6543 fn l_at_last_char_does_not_wrap_to_next_line() {
6544 let mut e = editor_with("ab\ncd");
6545 e.jump_cursor(0, 1);
6547 run_keys(&mut e, "l");
6548 assert_eq!(e.cursor(), (0, 1));
6550 }
6551
6552 #[test]
6553 fn count_l_clamps_at_line_end() {
6554 let mut e = editor_with("abcde");
6555 run_keys(&mut e, "20l");
6558 assert_eq!(e.cursor(), (0, 4));
6559 }
6560
6561 #[test]
6562 fn count_h_clamps_at_col_zero() {
6563 let mut e = editor_with("abcde");
6564 e.jump_cursor(0, 3);
6565 run_keys(&mut e, "20h");
6566 assert_eq!(e.cursor(), (0, 0));
6567 }
6568
6569 #[test]
6570 fn dl_on_last_char_still_deletes_it() {
6571 let mut e = editor_with("ab");
6575 e.jump_cursor(0, 1);
6576 run_keys(&mut e, "dl");
6577 assert_eq!(e.buffer().lines()[0], "a");
6578 }
6579
6580 #[test]
6581 fn case_op_preserves_yank_register() {
6582 let mut e = editor_with("target");
6583 run_keys(&mut e, "yy");
6584 let yank_before = e.yank().to_string();
6585 run_keys(&mut e, "gUU");
6587 assert_eq!(e.buffer().lines()[0], "TARGET");
6588 assert_eq!(
6589 e.yank(),
6590 yank_before,
6591 "case ops must preserve the yank buffer"
6592 );
6593 }
6594
6595 #[test]
6596 fn dap_deletes_paragraph() {
6597 let mut e = editor_with("a\nb\n\nc\nd");
6598 run_keys(&mut e, "dap");
6599 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6600 }
6601
6602 #[test]
6603 fn dit_deletes_inner_tag_content() {
6604 let mut e = editor_with("<b>hello</b>");
6605 e.jump_cursor(0, 4);
6607 run_keys(&mut e, "dit");
6608 assert_eq!(e.buffer().lines()[0], "<b></b>");
6609 }
6610
6611 #[test]
6612 fn dat_deletes_around_tag() {
6613 let mut e = editor_with("hi <b>foo</b> bye");
6614 e.jump_cursor(0, 6);
6615 run_keys(&mut e, "dat");
6616 assert_eq!(e.buffer().lines()[0], "hi bye");
6617 }
6618
6619 #[test]
6620 fn dit_picks_innermost_tag() {
6621 let mut e = editor_with("<a><b>x</b></a>");
6622 e.jump_cursor(0, 6);
6624 run_keys(&mut e, "dit");
6625 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6627 }
6628
6629 #[test]
6630 fn dat_innermost_tag_pair() {
6631 let mut e = editor_with("<a><b>x</b></a>");
6632 e.jump_cursor(0, 6);
6633 run_keys(&mut e, "dat");
6634 assert_eq!(e.buffer().lines()[0], "<a></a>");
6635 }
6636
6637 #[test]
6638 fn dit_outside_any_tag_no_op() {
6639 let mut e = editor_with("plain text");
6640 e.jump_cursor(0, 3);
6641 run_keys(&mut e, "dit");
6642 assert_eq!(e.buffer().lines()[0], "plain text");
6644 }
6645
6646 #[test]
6647 fn cit_changes_inner_tag_content() {
6648 let mut e = editor_with("<b>hello</b>");
6649 e.jump_cursor(0, 4);
6650 run_keys(&mut e, "citNEW<Esc>");
6651 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6652 }
6653
6654 #[test]
6655 fn cat_changes_around_tag() {
6656 let mut e = editor_with("hi <b>foo</b> bye");
6657 e.jump_cursor(0, 6);
6658 run_keys(&mut e, "catBAR<Esc>");
6659 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6660 }
6661
6662 #[test]
6663 fn yit_yanks_inner_tag_content() {
6664 let mut e = editor_with("<b>hello</b>");
6665 e.jump_cursor(0, 4);
6666 run_keys(&mut e, "yit");
6667 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6668 }
6669
6670 #[test]
6671 fn yat_yanks_full_tag_pair() {
6672 let mut e = editor_with("hi <b>foo</b> bye");
6673 e.jump_cursor(0, 6);
6674 run_keys(&mut e, "yat");
6675 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6676 }
6677
6678 #[test]
6679 fn vit_visually_selects_inner_tag() {
6680 let mut e = editor_with("<b>hello</b>");
6681 e.jump_cursor(0, 4);
6682 run_keys(&mut e, "vit");
6683 assert_eq!(e.vim_mode(), VimMode::Visual);
6684 run_keys(&mut e, "y");
6685 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6686 }
6687
6688 #[test]
6689 fn vat_visually_selects_around_tag() {
6690 let mut e = editor_with("x<b>foo</b>y");
6691 e.jump_cursor(0, 5);
6692 run_keys(&mut e, "vat");
6693 assert_eq!(e.vim_mode(), VimMode::Visual);
6694 run_keys(&mut e, "y");
6695 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6696 }
6697
6698 #[test]
6701 #[allow(non_snake_case)]
6702 fn diW_deletes_inner_big_word() {
6703 let mut e = editor_with("foo.bar baz");
6704 e.jump_cursor(0, 2);
6705 run_keys(&mut e, "diW");
6706 assert_eq!(e.buffer().lines()[0], " baz");
6708 }
6709
6710 #[test]
6711 #[allow(non_snake_case)]
6712 fn daW_deletes_around_big_word() {
6713 let mut e = editor_with("foo.bar baz");
6714 e.jump_cursor(0, 2);
6715 run_keys(&mut e, "daW");
6716 assert_eq!(e.buffer().lines()[0], "baz");
6717 }
6718
6719 #[test]
6720 fn di_double_quote_deletes_inside() {
6721 let mut e = editor_with("a \"hello\" b");
6722 e.jump_cursor(0, 4);
6723 run_keys(&mut e, "di\"");
6724 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6725 }
6726
6727 #[test]
6728 fn da_double_quote_deletes_around() {
6729 let mut e = editor_with("a \"hello\" b");
6731 e.jump_cursor(0, 4);
6732 run_keys(&mut e, "da\"");
6733 assert_eq!(e.buffer().lines()[0], "a b");
6734 }
6735
6736 #[test]
6737 fn di_single_quote_deletes_inside() {
6738 let mut e = editor_with("x 'foo' y");
6739 e.jump_cursor(0, 4);
6740 run_keys(&mut e, "di'");
6741 assert_eq!(e.buffer().lines()[0], "x '' y");
6742 }
6743
6744 #[test]
6745 fn da_single_quote_deletes_around() {
6746 let mut e = editor_with("x 'foo' y");
6748 e.jump_cursor(0, 4);
6749 run_keys(&mut e, "da'");
6750 assert_eq!(e.buffer().lines()[0], "x y");
6751 }
6752
6753 #[test]
6754 fn di_backtick_deletes_inside() {
6755 let mut e = editor_with("p `q` r");
6756 e.jump_cursor(0, 3);
6757 run_keys(&mut e, "di`");
6758 assert_eq!(e.buffer().lines()[0], "p `` r");
6759 }
6760
6761 #[test]
6762 fn da_backtick_deletes_around() {
6763 let mut e = editor_with("p `q` r");
6765 e.jump_cursor(0, 3);
6766 run_keys(&mut e, "da`");
6767 assert_eq!(e.buffer().lines()[0], "p r");
6768 }
6769
6770 #[test]
6771 fn di_paren_deletes_inside() {
6772 let mut e = editor_with("f(arg)");
6773 e.jump_cursor(0, 3);
6774 run_keys(&mut e, "di(");
6775 assert_eq!(e.buffer().lines()[0], "f()");
6776 }
6777
6778 #[test]
6779 fn di_paren_alias_b_works() {
6780 let mut e = editor_with("f(arg)");
6781 e.jump_cursor(0, 3);
6782 run_keys(&mut e, "dib");
6783 assert_eq!(e.buffer().lines()[0], "f()");
6784 }
6785
6786 #[test]
6787 fn di_bracket_deletes_inside() {
6788 let mut e = editor_with("a[b,c]d");
6789 e.jump_cursor(0, 3);
6790 run_keys(&mut e, "di[");
6791 assert_eq!(e.buffer().lines()[0], "a[]d");
6792 }
6793
6794 #[test]
6795 fn da_bracket_deletes_around() {
6796 let mut e = editor_with("a[b,c]d");
6797 e.jump_cursor(0, 3);
6798 run_keys(&mut e, "da[");
6799 assert_eq!(e.buffer().lines()[0], "ad");
6800 }
6801
6802 #[test]
6803 fn di_brace_deletes_inside() {
6804 let mut e = editor_with("x{y}z");
6805 e.jump_cursor(0, 2);
6806 run_keys(&mut e, "di{");
6807 assert_eq!(e.buffer().lines()[0], "x{}z");
6808 }
6809
6810 #[test]
6811 fn da_brace_deletes_around() {
6812 let mut e = editor_with("x{y}z");
6813 e.jump_cursor(0, 2);
6814 run_keys(&mut e, "da{");
6815 assert_eq!(e.buffer().lines()[0], "xz");
6816 }
6817
6818 #[test]
6819 fn di_brace_alias_capital_b_works() {
6820 let mut e = editor_with("x{y}z");
6821 e.jump_cursor(0, 2);
6822 run_keys(&mut e, "diB");
6823 assert_eq!(e.buffer().lines()[0], "x{}z");
6824 }
6825
6826 #[test]
6827 fn di_angle_deletes_inside() {
6828 let mut e = editor_with("p<q>r");
6829 e.jump_cursor(0, 2);
6830 run_keys(&mut e, "di<lt>");
6832 assert_eq!(e.buffer().lines()[0], "p<>r");
6833 }
6834
6835 #[test]
6836 fn da_angle_deletes_around() {
6837 let mut e = editor_with("p<q>r");
6838 e.jump_cursor(0, 2);
6839 run_keys(&mut e, "da<lt>");
6840 assert_eq!(e.buffer().lines()[0], "pr");
6841 }
6842
6843 #[test]
6844 fn dip_deletes_inner_paragraph() {
6845 let mut e = editor_with("a\nb\nc\n\nd");
6846 e.jump_cursor(1, 0);
6847 run_keys(&mut e, "dip");
6848 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6851 }
6852
6853 #[test]
6856 fn sentence_motion_close_paren_jumps_forward() {
6857 let mut e = editor_with("Alpha. Beta. Gamma.");
6858 e.jump_cursor(0, 0);
6859 run_keys(&mut e, ")");
6860 assert_eq!(e.cursor(), (0, 7));
6862 run_keys(&mut e, ")");
6863 assert_eq!(e.cursor(), (0, 13));
6864 }
6865
6866 #[test]
6867 fn sentence_motion_open_paren_jumps_backward() {
6868 let mut e = editor_with("Alpha. Beta. Gamma.");
6869 e.jump_cursor(0, 13);
6870 run_keys(&mut e, "(");
6871 assert_eq!(e.cursor(), (0, 7));
6874 run_keys(&mut e, "(");
6875 assert_eq!(e.cursor(), (0, 0));
6876 }
6877
6878 #[test]
6879 fn sentence_motion_count() {
6880 let mut e = editor_with("A. B. C. D.");
6881 e.jump_cursor(0, 0);
6882 run_keys(&mut e, "3)");
6883 assert_eq!(e.cursor(), (0, 9));
6885 }
6886
6887 #[test]
6888 fn dis_deletes_inner_sentence() {
6889 let mut e = editor_with("First one. Second one. Third one.");
6890 e.jump_cursor(0, 13);
6891 run_keys(&mut e, "dis");
6892 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6894 }
6895
6896 #[test]
6897 fn das_deletes_around_sentence_with_trailing_space() {
6898 let mut e = editor_with("Alpha. Beta. Gamma.");
6899 e.jump_cursor(0, 8);
6900 run_keys(&mut e, "das");
6901 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6904 }
6905
6906 #[test]
6907 fn dis_handles_double_terminator() {
6908 let mut e = editor_with("Wow!? Next.");
6909 e.jump_cursor(0, 1);
6910 run_keys(&mut e, "dis");
6911 assert_eq!(e.buffer().lines()[0], " Next.");
6914 }
6915
6916 #[test]
6917 fn dis_first_sentence_from_cursor_at_zero() {
6918 let mut e = editor_with("Alpha. Beta.");
6919 e.jump_cursor(0, 0);
6920 run_keys(&mut e, "dis");
6921 assert_eq!(e.buffer().lines()[0], " Beta.");
6922 }
6923
6924 #[test]
6925 fn yis_yanks_inner_sentence() {
6926 let mut e = editor_with("Hello world. Bye.");
6927 e.jump_cursor(0, 5);
6928 run_keys(&mut e, "yis");
6929 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6930 }
6931
6932 #[test]
6933 fn vis_visually_selects_inner_sentence() {
6934 let mut e = editor_with("First. Second.");
6935 e.jump_cursor(0, 1);
6936 run_keys(&mut e, "vis");
6937 assert_eq!(e.vim_mode(), VimMode::Visual);
6938 run_keys(&mut e, "y");
6939 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6940 }
6941
6942 #[test]
6943 fn ciw_changes_inner_word() {
6944 let mut e = editor_with("hello world");
6945 e.jump_cursor(0, 1);
6946 run_keys(&mut e, "ciwHEY<Esc>");
6947 assert_eq!(e.buffer().lines()[0], "HEY world");
6948 }
6949
6950 #[test]
6951 fn yiw_yanks_inner_word() {
6952 let mut e = editor_with("hello world");
6953 e.jump_cursor(0, 1);
6954 run_keys(&mut e, "yiw");
6955 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6956 }
6957
6958 #[test]
6959 fn viw_selects_inner_word() {
6960 let mut e = editor_with("hello world");
6961 e.jump_cursor(0, 2);
6962 run_keys(&mut e, "viw");
6963 assert_eq!(e.vim_mode(), VimMode::Visual);
6964 run_keys(&mut e, "y");
6965 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6966 }
6967
6968 #[test]
6969 fn ci_paren_changes_inside() {
6970 let mut e = editor_with("f(old)");
6971 e.jump_cursor(0, 3);
6972 run_keys(&mut e, "ci(NEW<Esc>");
6973 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6974 }
6975
6976 #[test]
6977 fn yi_double_quote_yanks_inside() {
6978 let mut e = editor_with("say \"hi there\" then");
6979 e.jump_cursor(0, 6);
6980 run_keys(&mut e, "yi\"");
6981 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6982 }
6983
6984 #[test]
6985 fn vap_visual_selects_around_paragraph() {
6986 let mut e = editor_with("a\nb\n\nc");
6987 e.jump_cursor(0, 0);
6988 run_keys(&mut e, "vap");
6989 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6990 run_keys(&mut e, "y");
6991 let text = e.registers().read('"').unwrap().text.clone();
6993 assert!(text.starts_with("a\nb"));
6994 }
6995
6996 #[test]
6997 fn star_finds_next_occurrence() {
6998 let mut e = editor_with("foo bar foo baz");
6999 run_keys(&mut e, "*");
7000 assert_eq!(e.cursor().1, 8);
7001 }
7002
7003 #[test]
7004 fn star_skips_substring_match() {
7005 let mut e = editor_with("foo foobar baz");
7008 run_keys(&mut e, "*");
7009 assert_eq!(e.cursor().1, 0);
7010 }
7011
7012 #[test]
7013 fn g_star_matches_substring() {
7014 let mut e = editor_with("foo foobar baz");
7017 run_keys(&mut e, "g*");
7018 assert_eq!(e.cursor().1, 4);
7019 }
7020
7021 #[test]
7022 fn g_pound_matches_substring_backward() {
7023 let mut e = editor_with("foo foobar baz foo");
7026 run_keys(&mut e, "$b");
7027 assert_eq!(e.cursor().1, 15);
7028 run_keys(&mut e, "g#");
7029 assert_eq!(e.cursor().1, 4);
7030 }
7031
7032 #[test]
7033 fn n_repeats_last_search_forward() {
7034 let mut e = editor_with("foo bar foo baz foo");
7035 run_keys(&mut e, "/foo<CR>");
7038 assert_eq!(e.cursor().1, 8);
7039 run_keys(&mut e, "n");
7040 assert_eq!(e.cursor().1, 16);
7041 }
7042
7043 #[test]
7044 fn shift_n_reverses_search() {
7045 let mut e = editor_with("foo bar foo baz foo");
7046 run_keys(&mut e, "/foo<CR>");
7047 run_keys(&mut e, "n");
7048 assert_eq!(e.cursor().1, 16);
7049 run_keys(&mut e, "N");
7050 assert_eq!(e.cursor().1, 8);
7051 }
7052
7053 #[test]
7054 fn n_noop_without_pattern() {
7055 let mut e = editor_with("foo bar");
7056 run_keys(&mut e, "n");
7057 assert_eq!(e.cursor(), (0, 0));
7058 }
7059
7060 #[test]
7061 fn visual_line_preserves_cursor_column() {
7062 let mut e = editor_with("hello world\nanother one\nbye");
7065 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
7067 assert_eq!(e.vim_mode(), VimMode::VisualLine);
7068 assert_eq!(e.cursor(), (0, 5));
7069 run_keys(&mut e, "j");
7070 assert_eq!(e.cursor(), (1, 5));
7071 }
7072
7073 #[test]
7074 fn visual_line_yank_includes_trailing_newline() {
7075 let mut e = editor_with("aaa\nbbb\nccc");
7076 run_keys(&mut e, "Vjy");
7077 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
7079 }
7080
7081 #[test]
7082 fn visual_line_yank_last_line_trailing_newline() {
7083 let mut e = editor_with("aaa\nbbb\nccc");
7084 run_keys(&mut e, "jj");
7086 run_keys(&mut e, "Vy");
7087 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7088 }
7089
7090 #[test]
7091 fn yy_on_last_line_has_trailing_newline() {
7092 let mut e = editor_with("aaa\nbbb\nccc");
7093 run_keys(&mut e, "jj");
7094 run_keys(&mut e, "yy");
7095 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
7096 }
7097
7098 #[test]
7099 fn yy_in_middle_has_trailing_newline() {
7100 let mut e = editor_with("aaa\nbbb\nccc");
7101 run_keys(&mut e, "j");
7102 run_keys(&mut e, "yy");
7103 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
7104 }
7105
7106 #[test]
7107 fn di_single_quote() {
7108 let mut e = editor_with("say 'hello world' now");
7109 e.jump_cursor(0, 7);
7110 run_keys(&mut e, "di'");
7111 assert_eq!(e.buffer().lines()[0], "say '' now");
7112 }
7113
7114 #[test]
7115 fn da_single_quote() {
7116 let mut e = editor_with("say 'hello' now");
7118 e.jump_cursor(0, 7);
7119 run_keys(&mut e, "da'");
7120 assert_eq!(e.buffer().lines()[0], "say now");
7121 }
7122
7123 #[test]
7124 fn di_backtick() {
7125 let mut e = editor_with("say `hi` now");
7126 e.jump_cursor(0, 5);
7127 run_keys(&mut e, "di`");
7128 assert_eq!(e.buffer().lines()[0], "say `` now");
7129 }
7130
7131 #[test]
7132 fn di_brace() {
7133 let mut e = editor_with("fn { a; b; c }");
7134 e.jump_cursor(0, 7);
7135 run_keys(&mut e, "di{");
7136 assert_eq!(e.buffer().lines()[0], "fn {}");
7137 }
7138
7139 #[test]
7140 fn di_bracket() {
7141 let mut e = editor_with("arr[1, 2, 3]");
7142 e.jump_cursor(0, 5);
7143 run_keys(&mut e, "di[");
7144 assert_eq!(e.buffer().lines()[0], "arr[]");
7145 }
7146
7147 #[test]
7148 fn dab_deletes_around_paren() {
7149 let mut e = editor_with("fn(a, b) + 1");
7150 e.jump_cursor(0, 4);
7151 run_keys(&mut e, "dab");
7152 assert_eq!(e.buffer().lines()[0], "fn + 1");
7153 }
7154
7155 #[test]
7156 fn da_big_b_deletes_around_brace() {
7157 let mut e = editor_with("x = {a: 1}");
7158 e.jump_cursor(0, 6);
7159 run_keys(&mut e, "daB");
7160 assert_eq!(e.buffer().lines()[0], "x = ");
7161 }
7162
7163 #[test]
7164 fn di_big_w_deletes_bigword() {
7165 let mut e = editor_with("foo-bar baz");
7166 e.jump_cursor(0, 2);
7167 run_keys(&mut e, "diW");
7168 assert_eq!(e.buffer().lines()[0], " baz");
7169 }
7170
7171 #[test]
7172 fn visual_select_inner_word() {
7173 let mut e = editor_with("hello world");
7174 e.jump_cursor(0, 2);
7175 run_keys(&mut e, "viw");
7176 assert_eq!(e.vim_mode(), VimMode::Visual);
7177 run_keys(&mut e, "y");
7178 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7179 }
7180
7181 #[test]
7182 fn visual_select_inner_quote() {
7183 let mut e = editor_with("foo \"bar\" baz");
7184 e.jump_cursor(0, 6);
7185 run_keys(&mut e, "vi\"");
7186 run_keys(&mut e, "y");
7187 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7188 }
7189
7190 #[test]
7191 fn visual_select_inner_paren() {
7192 let mut e = editor_with("fn(a, b)");
7193 e.jump_cursor(0, 4);
7194 run_keys(&mut e, "vi(");
7195 run_keys(&mut e, "y");
7196 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7197 }
7198
7199 #[test]
7200 fn visual_select_outer_brace() {
7201 let mut e = editor_with("{x}");
7202 e.jump_cursor(0, 1);
7203 run_keys(&mut e, "va{");
7204 run_keys(&mut e, "y");
7205 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7206 }
7207
7208 #[test]
7209 fn ci_paren_forward_scans_when_cursor_before_pair() {
7210 let mut e = editor_with("foo(bar)");
7213 e.jump_cursor(0, 0);
7214 run_keys(&mut e, "ci(NEW<Esc>");
7215 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7216 }
7217
7218 #[test]
7219 fn ci_paren_forward_scans_across_lines() {
7220 let mut e = editor_with("first\nfoo(bar)\nlast");
7221 e.jump_cursor(0, 0);
7222 run_keys(&mut e, "ci(NEW<Esc>");
7223 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7224 }
7225
7226 #[test]
7227 fn ci_brace_forward_scans_when_cursor_before_pair() {
7228 let mut e = editor_with("let x = {y};");
7229 e.jump_cursor(0, 0);
7230 run_keys(&mut e, "ci{NEW<Esc>");
7231 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7232 }
7233
7234 #[test]
7235 fn cit_forward_scans_when_cursor_before_tag() {
7236 let mut e = editor_with("text <b>hello</b> rest");
7239 e.jump_cursor(0, 0);
7240 run_keys(&mut e, "citNEW<Esc>");
7241 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7242 }
7243
7244 #[test]
7245 fn dat_forward_scans_when_cursor_before_tag() {
7246 let mut e = editor_with("text <b>hello</b> rest");
7248 e.jump_cursor(0, 0);
7249 run_keys(&mut e, "dat");
7250 assert_eq!(e.buffer().lines()[0], "text rest");
7251 }
7252
7253 #[test]
7254 fn ci_paren_still_works_when_cursor_inside() {
7255 let mut e = editor_with("fn(a, b)");
7258 e.jump_cursor(0, 4);
7259 run_keys(&mut e, "ci(NEW<Esc>");
7260 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7261 }
7262
7263 #[test]
7264 fn caw_changes_word_with_trailing_space() {
7265 let mut e = editor_with("hello world");
7266 run_keys(&mut e, "cawfoo<Esc>");
7267 assert_eq!(e.buffer().lines()[0], "fooworld");
7268 }
7269
7270 #[test]
7271 fn visual_char_yank_preserves_raw_text() {
7272 let mut e = editor_with("hello world");
7273 run_keys(&mut e, "vllly");
7274 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7275 }
7276
7277 #[test]
7278 fn single_line_visual_line_selects_full_line_on_yank() {
7279 let mut e = editor_with("hello world\nbye");
7280 run_keys(&mut e, "V");
7281 run_keys(&mut e, "y");
7284 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7285 }
7286
7287 #[test]
7288 fn visual_line_extends_both_directions() {
7289 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7290 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7292 assert_eq!(e.cursor(), (3, 0));
7293 run_keys(&mut e, "k");
7294 assert_eq!(e.cursor(), (2, 0));
7296 run_keys(&mut e, "k");
7297 assert_eq!(e.cursor(), (1, 0));
7298 }
7299
7300 #[test]
7301 fn visual_char_preserves_cursor_column() {
7302 let mut e = editor_with("hello world");
7303 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7305 assert_eq!(e.cursor(), (0, 5));
7306 run_keys(&mut e, "ll");
7307 assert_eq!(e.cursor(), (0, 7));
7308 }
7309
7310 #[test]
7311 fn visual_char_highlight_bounds_order() {
7312 let mut e = editor_with("abcdef");
7313 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7315 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7318 }
7319
7320 #[test]
7321 fn visual_line_highlight_bounds() {
7322 let mut e = editor_with("a\nb\nc");
7323 run_keys(&mut e, "V");
7324 assert_eq!(e.line_highlight(), Some((0, 0)));
7325 run_keys(&mut e, "j");
7326 assert_eq!(e.line_highlight(), Some((0, 1)));
7327 run_keys(&mut e, "j");
7328 assert_eq!(e.line_highlight(), Some((0, 2)));
7329 }
7330
7331 #[test]
7334 fn h_moves_left() {
7335 let mut e = editor_with("hello");
7336 e.jump_cursor(0, 3);
7337 run_keys(&mut e, "h");
7338 assert_eq!(e.cursor(), (0, 2));
7339 }
7340
7341 #[test]
7342 fn l_moves_right() {
7343 let mut e = editor_with("hello");
7344 run_keys(&mut e, "l");
7345 assert_eq!(e.cursor(), (0, 1));
7346 }
7347
7348 #[test]
7349 fn k_moves_up() {
7350 let mut e = editor_with("a\nb\nc");
7351 e.jump_cursor(2, 0);
7352 run_keys(&mut e, "k");
7353 assert_eq!(e.cursor(), (1, 0));
7354 }
7355
7356 #[test]
7357 fn zero_moves_to_line_start() {
7358 let mut e = editor_with(" hello");
7359 run_keys(&mut e, "$");
7360 run_keys(&mut e, "0");
7361 assert_eq!(e.cursor().1, 0);
7362 }
7363
7364 #[test]
7365 fn caret_moves_to_first_non_blank() {
7366 let mut e = editor_with(" hello");
7367 run_keys(&mut e, "0");
7368 run_keys(&mut e, "^");
7369 assert_eq!(e.cursor().1, 4);
7370 }
7371
7372 #[test]
7373 fn dollar_moves_to_last_char() {
7374 let mut e = editor_with("hello");
7375 run_keys(&mut e, "$");
7376 assert_eq!(e.cursor().1, 4);
7377 }
7378
7379 #[test]
7380 fn dollar_on_empty_line_stays_at_col_zero() {
7381 let mut e = editor_with("");
7382 run_keys(&mut e, "$");
7383 assert_eq!(e.cursor().1, 0);
7384 }
7385
7386 #[test]
7387 fn w_jumps_to_next_word() {
7388 let mut e = editor_with("foo bar baz");
7389 run_keys(&mut e, "w");
7390 assert_eq!(e.cursor().1, 4);
7391 }
7392
7393 #[test]
7394 fn b_jumps_back_a_word() {
7395 let mut e = editor_with("foo bar");
7396 e.jump_cursor(0, 6);
7397 run_keys(&mut e, "b");
7398 assert_eq!(e.cursor().1, 4);
7399 }
7400
7401 #[test]
7402 fn e_jumps_to_word_end() {
7403 let mut e = editor_with("foo bar");
7404 run_keys(&mut e, "e");
7405 assert_eq!(e.cursor().1, 2);
7406 }
7407
7408 #[test]
7411 fn d_dollar_deletes_to_eol() {
7412 let mut e = editor_with("hello world");
7413 e.jump_cursor(0, 5);
7414 run_keys(&mut e, "d$");
7415 assert_eq!(e.buffer().lines()[0], "hello");
7416 }
7417
7418 #[test]
7419 fn d_zero_deletes_to_line_start() {
7420 let mut e = editor_with("hello world");
7421 e.jump_cursor(0, 6);
7422 run_keys(&mut e, "d0");
7423 assert_eq!(e.buffer().lines()[0], "world");
7424 }
7425
7426 #[test]
7427 fn d_caret_deletes_to_first_non_blank() {
7428 let mut e = editor_with(" hello");
7429 e.jump_cursor(0, 6);
7430 run_keys(&mut e, "d^");
7431 assert_eq!(e.buffer().lines()[0], " llo");
7432 }
7433
7434 #[test]
7435 fn d_capital_g_deletes_to_end_of_file() {
7436 let mut e = editor_with("a\nb\nc\nd");
7437 e.jump_cursor(1, 0);
7438 run_keys(&mut e, "dG");
7439 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7440 }
7441
7442 #[test]
7443 fn d_gg_deletes_to_start_of_file() {
7444 let mut e = editor_with("a\nb\nc\nd");
7445 e.jump_cursor(2, 0);
7446 run_keys(&mut e, "dgg");
7447 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7448 }
7449
7450 #[test]
7451 fn cw_is_ce_quirk() {
7452 let mut e = editor_with("foo bar");
7455 run_keys(&mut e, "cwxyz<Esc>");
7456 assert_eq!(e.buffer().lines()[0], "xyz bar");
7457 }
7458
7459 #[test]
7462 fn big_d_deletes_to_eol() {
7463 let mut e = editor_with("hello world");
7464 e.jump_cursor(0, 5);
7465 run_keys(&mut e, "D");
7466 assert_eq!(e.buffer().lines()[0], "hello");
7467 }
7468
7469 #[test]
7470 fn big_c_deletes_to_eol_and_inserts() {
7471 let mut e = editor_with("hello world");
7472 e.jump_cursor(0, 5);
7473 run_keys(&mut e, "C!<Esc>");
7474 assert_eq!(e.buffer().lines()[0], "hello!");
7475 }
7476
7477 #[test]
7478 fn j_joins_next_line_with_space() {
7479 let mut e = editor_with("hello\nworld");
7480 run_keys(&mut e, "J");
7481 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7482 }
7483
7484 #[test]
7485 fn j_strips_leading_whitespace_on_join() {
7486 let mut e = editor_with("hello\n world");
7487 run_keys(&mut e, "J");
7488 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7489 }
7490
7491 #[test]
7492 fn big_x_deletes_char_before_cursor() {
7493 let mut e = editor_with("hello");
7494 e.jump_cursor(0, 3);
7495 run_keys(&mut e, "X");
7496 assert_eq!(e.buffer().lines()[0], "helo");
7497 }
7498
7499 #[test]
7500 fn s_substitutes_char_and_enters_insert() {
7501 let mut e = editor_with("hello");
7502 run_keys(&mut e, "sX<Esc>");
7503 assert_eq!(e.buffer().lines()[0], "Xello");
7504 }
7505
7506 #[test]
7507 fn count_x_deletes_many() {
7508 let mut e = editor_with("abcdef");
7509 run_keys(&mut e, "3x");
7510 assert_eq!(e.buffer().lines()[0], "def");
7511 }
7512
7513 #[test]
7516 fn p_pastes_charwise_after_cursor() {
7517 let mut e = editor_with("hello");
7518 run_keys(&mut e, "yw");
7519 run_keys(&mut e, "$p");
7520 assert_eq!(e.buffer().lines()[0], "hellohello");
7521 }
7522
7523 #[test]
7524 fn capital_p_pastes_charwise_before_cursor() {
7525 let mut e = editor_with("hello");
7526 run_keys(&mut e, "v");
7528 run_keys(&mut e, "l");
7529 run_keys(&mut e, "y");
7530 run_keys(&mut e, "$P");
7531 assert_eq!(e.buffer().lines()[0], "hellheo");
7534 }
7535
7536 #[test]
7537 fn p_pastes_linewise_below() {
7538 let mut e = editor_with("one\ntwo\nthree");
7539 run_keys(&mut e, "yy");
7540 run_keys(&mut e, "p");
7541 assert_eq!(
7542 e.buffer().lines(),
7543 &[
7544 "one".to_string(),
7545 "one".to_string(),
7546 "two".to_string(),
7547 "three".to_string()
7548 ]
7549 );
7550 }
7551
7552 #[test]
7553 fn capital_p_pastes_linewise_above() {
7554 let mut e = editor_with("one\ntwo");
7555 e.jump_cursor(1, 0);
7556 run_keys(&mut e, "yy");
7557 run_keys(&mut e, "P");
7558 assert_eq!(
7559 e.buffer().lines(),
7560 &["one".to_string(), "two".to_string(), "two".to_string()]
7561 );
7562 }
7563
7564 #[test]
7567 fn hash_finds_previous_occurrence() {
7568 let mut e = editor_with("foo bar foo baz foo");
7569 e.jump_cursor(0, 16);
7571 run_keys(&mut e, "#");
7572 assert_eq!(e.cursor().1, 8);
7573 }
7574
7575 #[test]
7578 fn visual_line_delete_removes_full_lines() {
7579 let mut e = editor_with("a\nb\nc\nd");
7580 run_keys(&mut e, "Vjd");
7581 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7582 }
7583
7584 #[test]
7585 fn visual_line_change_leaves_blank_line() {
7586 let mut e = editor_with("a\nb\nc");
7587 run_keys(&mut e, "Vjc");
7588 assert_eq!(e.vim_mode(), VimMode::Insert);
7589 run_keys(&mut e, "X<Esc>");
7590 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7594 }
7595
7596 #[test]
7597 fn cc_leaves_blank_line() {
7598 let mut e = editor_with("a\nb\nc");
7599 e.jump_cursor(1, 0);
7600 run_keys(&mut e, "ccX<Esc>");
7601 assert_eq!(
7602 e.buffer().lines(),
7603 &["a".to_string(), "X".to_string(), "c".to_string()]
7604 );
7605 }
7606
7607 #[test]
7612 fn big_w_skips_hyphens() {
7613 let mut e = editor_with("foo-bar baz");
7615 run_keys(&mut e, "W");
7616 assert_eq!(e.cursor().1, 8);
7617 }
7618
7619 #[test]
7620 fn big_w_crosses_lines() {
7621 let mut e = editor_with("foo-bar\nbaz-qux");
7622 run_keys(&mut e, "W");
7623 assert_eq!(e.cursor(), (1, 0));
7624 }
7625
7626 #[test]
7627 fn big_b_skips_hyphens() {
7628 let mut e = editor_with("foo-bar baz");
7629 e.jump_cursor(0, 9);
7630 run_keys(&mut e, "B");
7631 assert_eq!(e.cursor().1, 8);
7632 run_keys(&mut e, "B");
7633 assert_eq!(e.cursor().1, 0);
7634 }
7635
7636 #[test]
7637 fn big_e_jumps_to_big_word_end() {
7638 let mut e = editor_with("foo-bar baz");
7639 run_keys(&mut e, "E");
7640 assert_eq!(e.cursor().1, 6);
7641 run_keys(&mut e, "E");
7642 assert_eq!(e.cursor().1, 10);
7643 }
7644
7645 #[test]
7646 fn dw_with_big_word_variant() {
7647 let mut e = editor_with("foo-bar baz");
7649 run_keys(&mut e, "dW");
7650 assert_eq!(e.buffer().lines()[0], "baz");
7651 }
7652
7653 #[test]
7656 fn insert_ctrl_w_deletes_word_back() {
7657 let mut e = editor_with("");
7658 run_keys(&mut e, "i");
7659 for c in "hello world".chars() {
7660 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7661 }
7662 run_keys(&mut e, "<C-w>");
7663 assert_eq!(e.buffer().lines()[0], "hello ");
7664 }
7665
7666 #[test]
7667 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7668 let mut e = editor_with("hello\nworld");
7672 e.jump_cursor(1, 0);
7673 run_keys(&mut e, "i");
7674 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7675 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7678 assert_eq!(e.cursor(), (0, 0));
7679 }
7680
7681 #[test]
7682 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7683 let mut e = editor_with("foo bar\nbaz");
7684 e.jump_cursor(1, 0);
7685 run_keys(&mut e, "i");
7686 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7687 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7689 assert_eq!(e.cursor(), (0, 4));
7690 }
7691
7692 #[test]
7693 fn insert_ctrl_u_deletes_to_line_start() {
7694 let mut e = editor_with("");
7695 run_keys(&mut e, "i");
7696 for c in "hello world".chars() {
7697 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7698 }
7699 run_keys(&mut e, "<C-u>");
7700 assert_eq!(e.buffer().lines()[0], "");
7701 }
7702
7703 #[test]
7704 fn insert_ctrl_o_runs_one_normal_command() {
7705 let mut e = editor_with("hello world");
7706 run_keys(&mut e, "A");
7708 assert_eq!(e.vim_mode(), VimMode::Insert);
7709 e.jump_cursor(0, 0);
7711 run_keys(&mut e, "<C-o>");
7712 assert_eq!(e.vim_mode(), VimMode::Normal);
7713 run_keys(&mut e, "dw");
7714 assert_eq!(e.vim_mode(), VimMode::Insert);
7716 assert_eq!(e.buffer().lines()[0], "world");
7717 }
7718
7719 #[test]
7722 fn j_through_empty_line_preserves_column() {
7723 let mut e = editor_with("hello world\n\nanother line");
7724 run_keys(&mut e, "llllll");
7726 assert_eq!(e.cursor(), (0, 6));
7727 run_keys(&mut e, "j");
7730 assert_eq!(e.cursor(), (1, 0));
7731 run_keys(&mut e, "j");
7733 assert_eq!(e.cursor(), (2, 6));
7734 }
7735
7736 #[test]
7737 fn j_through_shorter_line_preserves_column() {
7738 let mut e = editor_with("hello world\nhi\nanother line");
7739 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7742 run_keys(&mut e, "j");
7743 assert_eq!(e.cursor(), (2, 7));
7744 }
7745
7746 #[test]
7747 fn esc_from_insert_sticky_matches_visible_cursor() {
7748 let mut e = editor_with(" this is a line\n another one of a similar size");
7752 e.jump_cursor(0, 12);
7753 run_keys(&mut e, "I");
7754 assert_eq!(e.cursor(), (0, 4));
7755 run_keys(&mut e, "X<Esc>");
7756 assert_eq!(e.cursor(), (0, 4));
7757 run_keys(&mut e, "j");
7758 assert_eq!(e.cursor(), (1, 4));
7759 }
7760
7761 #[test]
7762 fn esc_from_insert_sticky_tracks_inserted_chars() {
7763 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7764 run_keys(&mut e, "i");
7765 run_keys(&mut e, "abc<Esc>");
7766 assert_eq!(e.cursor(), (0, 2));
7767 run_keys(&mut e, "j");
7768 assert_eq!(e.cursor(), (1, 2));
7769 }
7770
7771 #[test]
7772 fn esc_from_insert_sticky_tracks_arrow_nav() {
7773 let mut e = editor_with("xxxxxx\nyyyyyy");
7774 run_keys(&mut e, "i");
7775 run_keys(&mut e, "abc");
7776 for _ in 0..2 {
7777 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7778 }
7779 run_keys(&mut e, "<Esc>");
7780 assert_eq!(e.cursor(), (0, 0));
7781 run_keys(&mut e, "j");
7782 assert_eq!(e.cursor(), (1, 0));
7783 }
7784
7785 #[test]
7786 fn esc_from_insert_at_col_14_followed_by_j() {
7787 let line = "x".repeat(30);
7790 let buf = format!("{line}\n{line}");
7791 let mut e = editor_with(&buf);
7792 e.jump_cursor(0, 14);
7793 run_keys(&mut e, "i");
7794 for c in "test ".chars() {
7795 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7796 }
7797 run_keys(&mut e, "<Esc>");
7798 assert_eq!(e.cursor(), (0, 18));
7799 run_keys(&mut e, "j");
7800 assert_eq!(e.cursor(), (1, 18));
7801 }
7802
7803 #[test]
7804 fn linewise_paste_resets_sticky_column() {
7805 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7809 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7811 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7815 run_keys(&mut e, "j");
7817 assert_eq!(e.cursor(), (3, 2));
7818 }
7819
7820 #[test]
7821 fn horizontal_motion_resyncs_sticky_column() {
7822 let mut e = editor_with("hello world\n\nanother line");
7826 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7829 assert_eq!(e.cursor(), (2, 3));
7830 }
7831
7832 #[test]
7835 fn ctrl_v_enters_visual_block() {
7836 let mut e = editor_with("aaa\nbbb\nccc");
7837 run_keys(&mut e, "<C-v>");
7838 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7839 }
7840
7841 #[test]
7842 fn visual_block_esc_returns_to_normal() {
7843 let mut e = editor_with("aaa\nbbb\nccc");
7844 run_keys(&mut e, "<C-v>");
7845 run_keys(&mut e, "<Esc>");
7846 assert_eq!(e.vim_mode(), VimMode::Normal);
7847 }
7848
7849 #[test]
7850 fn backtick_lt_jumps_to_visual_start_mark() {
7851 let mut e = editor_with("foo bar baz\n");
7855 run_keys(&mut e, "v");
7856 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7859 run_keys(&mut e, "`<lt>");
7861 assert_eq!(e.cursor(), (0, 0));
7862 }
7863
7864 #[test]
7865 fn backtick_gt_jumps_to_visual_end_mark() {
7866 let mut e = editor_with("foo bar baz\n");
7867 run_keys(&mut e, "v");
7868 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7870 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7872 assert_eq!(e.cursor(), (0, 4));
7873 }
7874
7875 #[test]
7876 fn visual_exit_sets_lt_gt_marks() {
7877 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7880 run_keys(&mut e, "V");
7882 run_keys(&mut e, "j");
7883 run_keys(&mut e, "<Esc>");
7884 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7885 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7886 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7887 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7888 }
7889
7890 #[test]
7891 fn visual_exit_marks_use_lower_higher_order() {
7892 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7896 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7898 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7900 let lt = e.mark('<').unwrap();
7901 let gt = e.mark('>').unwrap();
7902 assert_eq!(lt.0, 2);
7903 assert_eq!(gt.0, 3);
7904 }
7905
7906 #[test]
7907 fn visualline_exit_marks_snap_to_line_edges() {
7908 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7910 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7912 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7914 let lt = e.mark('<').unwrap();
7915 let gt = e.mark('>').unwrap();
7916 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7917 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7919 }
7920
7921 #[test]
7922 fn visualblock_exit_marks_use_block_corners() {
7923 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7927 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7929 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7932 let lt = e.mark('<').unwrap();
7933 let gt = e.mark('>').unwrap();
7934 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7936 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7937 }
7938
7939 #[test]
7940 fn visual_block_delete_removes_column_range() {
7941 let mut e = editor_with("hello\nworld\nhappy");
7942 run_keys(&mut e, "l");
7944 run_keys(&mut e, "<C-v>");
7945 run_keys(&mut e, "jj");
7946 run_keys(&mut e, "ll");
7947 run_keys(&mut e, "d");
7948 assert_eq!(
7950 e.buffer().lines(),
7951 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7952 );
7953 }
7954
7955 #[test]
7956 fn visual_block_yank_joins_with_newlines() {
7957 let mut e = editor_with("hello\nworld\nhappy");
7958 run_keys(&mut e, "<C-v>");
7959 run_keys(&mut e, "jj");
7960 run_keys(&mut e, "ll");
7961 run_keys(&mut e, "y");
7962 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7963 }
7964
7965 #[test]
7966 fn visual_block_replace_fills_block() {
7967 let mut e = editor_with("hello\nworld\nhappy");
7968 run_keys(&mut e, "<C-v>");
7969 run_keys(&mut e, "jj");
7970 run_keys(&mut e, "ll");
7971 run_keys(&mut e, "rx");
7972 assert_eq!(
7973 e.buffer().lines(),
7974 &[
7975 "xxxlo".to_string(),
7976 "xxxld".to_string(),
7977 "xxxpy".to_string()
7978 ]
7979 );
7980 }
7981
7982 #[test]
7983 fn visual_block_insert_repeats_across_rows() {
7984 let mut e = editor_with("hello\nworld\nhappy");
7985 run_keys(&mut e, "<C-v>");
7986 run_keys(&mut e, "jj");
7987 run_keys(&mut e, "I");
7988 run_keys(&mut e, "# <Esc>");
7989 assert_eq!(
7990 e.buffer().lines(),
7991 &[
7992 "# hello".to_string(),
7993 "# world".to_string(),
7994 "# happy".to_string()
7995 ]
7996 );
7997 }
7998
7999 #[test]
8000 fn block_highlight_returns_none_outside_block_mode() {
8001 let mut e = editor_with("abc");
8002 assert!(e.block_highlight().is_none());
8003 run_keys(&mut e, "v");
8004 assert!(e.block_highlight().is_none());
8005 run_keys(&mut e, "<Esc>V");
8006 assert!(e.block_highlight().is_none());
8007 }
8008
8009 #[test]
8010 fn block_highlight_bounds_track_anchor_and_cursor() {
8011 let mut e = editor_with("aaaa\nbbbb\ncccc");
8012 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
8014 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
8017 }
8018
8019 #[test]
8020 fn visual_block_delete_handles_short_lines() {
8021 let mut e = editor_with("hello\nhi\nworld");
8023 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
8025 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8027 assert_eq!(
8032 e.buffer().lines(),
8033 &["ho".to_string(), "h".to_string(), "wd".to_string()]
8034 );
8035 }
8036
8037 #[test]
8038 fn visual_block_yank_pads_short_lines_with_empties() {
8039 let mut e = editor_with("hello\nhi\nworld");
8040 run_keys(&mut e, "l");
8041 run_keys(&mut e, "<C-v>");
8042 run_keys(&mut e, "jjll");
8043 run_keys(&mut e, "y");
8044 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
8046 }
8047
8048 #[test]
8049 fn visual_block_replace_skips_past_eol() {
8050 let mut e = editor_with("ab\ncd\nef");
8053 run_keys(&mut e, "l");
8055 run_keys(&mut e, "<C-v>");
8056 run_keys(&mut e, "jjllllll");
8057 run_keys(&mut e, "rX");
8058 assert_eq!(
8061 e.buffer().lines(),
8062 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
8063 );
8064 }
8065
8066 #[test]
8067 fn visual_block_with_empty_line_in_middle() {
8068 let mut e = editor_with("abcd\n\nefgh");
8069 run_keys(&mut e, "<C-v>");
8070 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
8072 assert_eq!(
8075 e.buffer().lines(),
8076 &["d".to_string(), "".to_string(), "h".to_string()]
8077 );
8078 }
8079
8080 #[test]
8081 fn block_insert_pads_empty_lines_to_block_column() {
8082 let mut e = editor_with("this is a line\n\nthis is a line");
8085 e.jump_cursor(0, 3);
8086 run_keys(&mut e, "<C-v>");
8087 run_keys(&mut e, "jj");
8088 run_keys(&mut e, "I");
8089 run_keys(&mut e, "XX<Esc>");
8090 assert_eq!(
8091 e.buffer().lines(),
8092 &[
8093 "thiXXs is a line".to_string(),
8094 " XX".to_string(),
8095 "thiXXs is a line".to_string()
8096 ]
8097 );
8098 }
8099
8100 #[test]
8101 fn block_insert_pads_short_lines_to_block_column() {
8102 let mut e = editor_with("aaaaa\nbb\naaaaa");
8103 e.jump_cursor(0, 3);
8104 run_keys(&mut e, "<C-v>");
8105 run_keys(&mut e, "jj");
8106 run_keys(&mut e, "I");
8107 run_keys(&mut e, "Y<Esc>");
8108 assert_eq!(
8110 e.buffer().lines(),
8111 &[
8112 "aaaYaa".to_string(),
8113 "bb Y".to_string(),
8114 "aaaYaa".to_string()
8115 ]
8116 );
8117 }
8118
8119 #[test]
8120 fn visual_block_append_repeats_across_rows() {
8121 let mut e = editor_with("foo\nbar\nbaz");
8122 run_keys(&mut e, "<C-v>");
8123 run_keys(&mut e, "jj");
8124 run_keys(&mut e, "A");
8127 run_keys(&mut e, "!<Esc>");
8128 assert_eq!(
8129 e.buffer().lines(),
8130 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8131 );
8132 }
8133
8134 #[test]
8137 fn slash_opens_forward_search_prompt() {
8138 let mut e = editor_with("hello world");
8139 run_keys(&mut e, "/");
8140 let p = e.search_prompt().expect("prompt should be active");
8141 assert!(p.text.is_empty());
8142 assert!(p.forward);
8143 }
8144
8145 #[test]
8146 fn question_opens_backward_search_prompt() {
8147 let mut e = editor_with("hello world");
8148 run_keys(&mut e, "?");
8149 let p = e.search_prompt().expect("prompt should be active");
8150 assert!(!p.forward);
8151 }
8152
8153 #[test]
8154 fn search_prompt_typing_updates_pattern_live() {
8155 let mut e = editor_with("foo bar\nbaz");
8156 run_keys(&mut e, "/bar");
8157 assert_eq!(e.search_prompt().unwrap().text, "bar");
8158 assert!(e.search_state().pattern.is_some());
8160 }
8161
8162 #[test]
8163 fn search_prompt_backspace_and_enter() {
8164 let mut e = editor_with("hello world\nagain");
8165 run_keys(&mut e, "/worlx");
8166 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8167 assert_eq!(e.search_prompt().unwrap().text, "worl");
8168 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8169 assert!(e.search_prompt().is_none());
8171 assert_eq!(e.last_search(), Some("worl"));
8172 assert_eq!(e.cursor(), (0, 6));
8173 }
8174
8175 #[test]
8176 fn empty_search_prompt_enter_repeats_last_search() {
8177 let mut e = editor_with("foo bar foo baz foo");
8178 run_keys(&mut e, "/foo");
8179 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8180 assert_eq!(e.cursor().1, 8);
8181 run_keys(&mut e, "/");
8183 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8184 assert_eq!(e.cursor().1, 16);
8185 assert_eq!(e.last_search(), Some("foo"));
8186 }
8187
8188 #[test]
8189 fn search_history_records_committed_patterns() {
8190 let mut e = editor_with("alpha beta gamma");
8191 run_keys(&mut e, "/alpha<CR>");
8192 run_keys(&mut e, "/beta<CR>");
8193 let history = e.vim.search_history.clone();
8195 assert_eq!(history, vec!["alpha", "beta"]);
8196 }
8197
8198 #[test]
8199 fn search_history_dedupes_consecutive_repeats() {
8200 let mut e = editor_with("foo bar foo");
8201 run_keys(&mut e, "/foo<CR>");
8202 run_keys(&mut e, "/foo<CR>");
8203 run_keys(&mut e, "/bar<CR>");
8204 run_keys(&mut e, "/bar<CR>");
8205 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8207 }
8208
8209 #[test]
8210 fn ctrl_p_walks_history_backward() {
8211 let mut e = editor_with("alpha beta gamma");
8212 run_keys(&mut e, "/alpha<CR>");
8213 run_keys(&mut e, "/beta<CR>");
8214 run_keys(&mut e, "/");
8216 assert_eq!(e.search_prompt().unwrap().text, "");
8217 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8218 assert_eq!(e.search_prompt().unwrap().text, "beta");
8219 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8220 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8221 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8223 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8224 }
8225
8226 #[test]
8227 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8228 let mut e = editor_with("a b c");
8229 run_keys(&mut e, "/a<CR>");
8230 run_keys(&mut e, "/b<CR>");
8231 run_keys(&mut e, "/c<CR>");
8232 run_keys(&mut e, "/");
8233 for _ in 0..3 {
8235 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8236 }
8237 assert_eq!(e.search_prompt().unwrap().text, "a");
8238 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8239 assert_eq!(e.search_prompt().unwrap().text, "b");
8240 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8241 assert_eq!(e.search_prompt().unwrap().text, "c");
8242 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8244 assert_eq!(e.search_prompt().unwrap().text, "c");
8245 }
8246
8247 #[test]
8248 fn typing_after_history_walk_resets_cursor() {
8249 let mut e = editor_with("foo");
8250 run_keys(&mut e, "/foo<CR>");
8251 run_keys(&mut e, "/");
8252 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8253 assert_eq!(e.search_prompt().unwrap().text, "foo");
8254 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8257 assert_eq!(e.search_prompt().unwrap().text, "foox");
8258 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8259 assert_eq!(e.search_prompt().unwrap().text, "foo");
8260 }
8261
8262 #[test]
8263 fn empty_backward_search_prompt_enter_repeats_last_search() {
8264 let mut e = editor_with("foo bar foo baz foo");
8265 run_keys(&mut e, "/foo");
8267 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8268 assert_eq!(e.cursor().1, 8);
8269 run_keys(&mut e, "?");
8270 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8271 assert_eq!(e.cursor().1, 0);
8272 assert_eq!(e.last_search(), Some("foo"));
8273 }
8274
8275 #[test]
8276 fn search_prompt_esc_cancels_but_keeps_last_search() {
8277 let mut e = editor_with("foo bar\nbaz");
8278 run_keys(&mut e, "/bar");
8279 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8280 assert!(e.search_prompt().is_none());
8281 assert_eq!(e.last_search(), Some("bar"));
8282 }
8283
8284 #[test]
8285 fn search_then_n_and_shift_n_navigate() {
8286 let mut e = editor_with("foo bar foo baz foo");
8287 run_keys(&mut e, "/foo");
8288 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8289 assert_eq!(e.cursor().1, 8);
8291 run_keys(&mut e, "n");
8292 assert_eq!(e.cursor().1, 16);
8293 run_keys(&mut e, "N");
8294 assert_eq!(e.cursor().1, 8);
8295 }
8296
8297 #[test]
8298 fn question_mark_searches_backward_on_enter() {
8299 let mut e = editor_with("foo bar foo baz");
8300 e.jump_cursor(0, 10);
8301 run_keys(&mut e, "?foo");
8302 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8303 assert_eq!(e.cursor(), (0, 8));
8305 }
8306
8307 #[test]
8310 fn big_y_yanks_to_end_of_line() {
8311 let mut e = editor_with("hello world");
8312 e.jump_cursor(0, 6);
8313 run_keys(&mut e, "Y");
8314 assert_eq!(e.last_yank.as_deref(), Some("world"));
8315 }
8316
8317 #[test]
8318 fn big_y_from_line_start_yanks_full_line() {
8319 let mut e = editor_with("hello world");
8320 run_keys(&mut e, "Y");
8321 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8322 }
8323
8324 #[test]
8325 fn gj_joins_without_inserting_space() {
8326 let mut e = editor_with("hello\n world");
8327 run_keys(&mut e, "gJ");
8328 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8330 }
8331
8332 #[test]
8333 fn gj_noop_on_last_line() {
8334 let mut e = editor_with("only");
8335 run_keys(&mut e, "gJ");
8336 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8337 }
8338
8339 #[test]
8340 fn ge_jumps_to_previous_word_end() {
8341 let mut e = editor_with("foo bar baz");
8342 e.jump_cursor(0, 5);
8343 run_keys(&mut e, "ge");
8344 assert_eq!(e.cursor(), (0, 2));
8345 }
8346
8347 #[test]
8348 fn ge_respects_word_class() {
8349 let mut e = editor_with("foo-bar baz");
8352 e.jump_cursor(0, 5);
8353 run_keys(&mut e, "ge");
8354 assert_eq!(e.cursor(), (0, 3));
8355 }
8356
8357 #[test]
8358 fn big_ge_treats_hyphens_as_part_of_word() {
8359 let mut e = editor_with("foo-bar baz");
8362 e.jump_cursor(0, 10);
8363 run_keys(&mut e, "gE");
8364 assert_eq!(e.cursor(), (0, 6));
8365 }
8366
8367 #[test]
8368 fn ge_crosses_line_boundary() {
8369 let mut e = editor_with("foo\nbar");
8370 e.jump_cursor(1, 0);
8371 run_keys(&mut e, "ge");
8372 assert_eq!(e.cursor(), (0, 2));
8373 }
8374
8375 #[test]
8376 fn dge_deletes_to_end_of_previous_word() {
8377 let mut e = editor_with("foo bar baz");
8378 e.jump_cursor(0, 8);
8379 run_keys(&mut e, "dge");
8382 assert_eq!(e.buffer().lines()[0], "foo baaz");
8383 }
8384
8385 #[test]
8386 fn ctrl_scroll_keys_do_not_panic() {
8387 let mut e = editor_with(
8390 (0..50)
8391 .map(|i| format!("line{i}"))
8392 .collect::<Vec<_>>()
8393 .join("\n")
8394 .as_str(),
8395 );
8396 run_keys(&mut e, "<C-f>");
8397 run_keys(&mut e, "<C-b>");
8398 assert!(!e.buffer().lines().is_empty());
8400 }
8401
8402 #[test]
8409 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8410 let mut e = Editor::new(
8411 hjkl_buffer::Buffer::new(),
8412 crate::types::DefaultHost::new(),
8413 crate::types::Options::default(),
8414 );
8415 e.set_content("row0\nrow1\nrow2");
8416 run_keys(&mut e, "3iX<Down><Esc>");
8418 assert!(e.buffer().lines()[0].contains('X'));
8420 assert!(
8423 !e.buffer().lines()[1].contains("row0"),
8424 "row1 leaked row0 contents: {:?}",
8425 e.buffer().lines()[1]
8426 );
8427 assert_eq!(e.buffer().lines().len(), 3);
8430 }
8431
8432 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8435 let mut e = Editor::new(
8436 hjkl_buffer::Buffer::new(),
8437 crate::types::DefaultHost::new(),
8438 crate::types::Options::default(),
8439 );
8440 let body = (0..n)
8441 .map(|i| format!(" line{}", i))
8442 .collect::<Vec<_>>()
8443 .join("\n");
8444 e.set_content(&body);
8445 e.set_viewport_height(viewport);
8446 e
8447 }
8448
8449 #[test]
8450 fn ctrl_d_moves_cursor_half_page_down() {
8451 let mut e = editor_with_rows(100, 20);
8452 run_keys(&mut e, "<C-d>");
8453 assert_eq!(e.cursor().0, 10);
8454 }
8455
8456 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8457 let mut e = Editor::new(
8458 hjkl_buffer::Buffer::new(),
8459 crate::types::DefaultHost::new(),
8460 crate::types::Options::default(),
8461 );
8462 e.set_content(&lines.join("\n"));
8463 e.set_viewport_height(viewport);
8464 let v = e.host_mut().viewport_mut();
8465 v.height = viewport;
8466 v.width = text_width;
8467 v.text_width = text_width;
8468 v.wrap = hjkl_buffer::Wrap::Char;
8469 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8470 e
8471 }
8472
8473 #[test]
8474 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8475 let lines = ["aaaabbbbcccc"; 10];
8479 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8480 e.jump_cursor(4, 0);
8481 e.ensure_cursor_in_scrolloff();
8482 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8483 assert!(csr <= 6, "csr={csr}");
8484 }
8485
8486 #[test]
8487 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8488 let lines = ["aaaabbbbcccc"; 10];
8489 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8490 e.jump_cursor(7, 0);
8493 e.ensure_cursor_in_scrolloff();
8494 e.jump_cursor(2, 0);
8495 e.ensure_cursor_in_scrolloff();
8496 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8497 assert!(csr >= 5, "csr={csr}");
8499 }
8500
8501 #[test]
8502 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8503 let lines = ["aaaabbbbcccc"; 5];
8504 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8505 e.jump_cursor(4, 11);
8506 e.ensure_cursor_in_scrolloff();
8507 let top = e.host().viewport().top_row;
8512 assert_eq!(top, 1);
8513 }
8514
8515 #[test]
8516 fn ctrl_u_moves_cursor_half_page_up() {
8517 let mut e = editor_with_rows(100, 20);
8518 e.jump_cursor(50, 0);
8519 run_keys(&mut e, "<C-u>");
8520 assert_eq!(e.cursor().0, 40);
8521 }
8522
8523 #[test]
8524 fn ctrl_f_moves_cursor_full_page_down() {
8525 let mut e = editor_with_rows(100, 20);
8526 run_keys(&mut e, "<C-f>");
8527 assert_eq!(e.cursor().0, 18);
8529 }
8530
8531 #[test]
8532 fn ctrl_b_moves_cursor_full_page_up() {
8533 let mut e = editor_with_rows(100, 20);
8534 e.jump_cursor(50, 0);
8535 run_keys(&mut e, "<C-b>");
8536 assert_eq!(e.cursor().0, 32);
8537 }
8538
8539 #[test]
8540 fn ctrl_d_lands_on_first_non_blank() {
8541 let mut e = editor_with_rows(100, 20);
8542 run_keys(&mut e, "<C-d>");
8543 assert_eq!(e.cursor().1, 2);
8545 }
8546
8547 #[test]
8548 fn ctrl_d_clamps_at_end_of_buffer() {
8549 let mut e = editor_with_rows(5, 20);
8550 run_keys(&mut e, "<C-d>");
8551 assert_eq!(e.cursor().0, 4);
8552 }
8553
8554 #[test]
8555 fn capital_h_jumps_to_viewport_top() {
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, "H");
8561 assert_eq!(e.cursor().0, top);
8562 assert_eq!(e.cursor().1, 2);
8563 }
8564
8565 #[test]
8566 fn capital_l_jumps_to_viewport_bottom() {
8567 let mut e = editor_with_rows(100, 10);
8568 e.jump_cursor(50, 0);
8569 e.set_viewport_top(45);
8570 let top = e.host().viewport().top_row;
8571 run_keys(&mut e, "L");
8572 assert_eq!(e.cursor().0, top + 9);
8573 }
8574
8575 #[test]
8576 fn capital_m_jumps_to_viewport_middle() {
8577 let mut e = editor_with_rows(100, 10);
8578 e.jump_cursor(50, 0);
8579 e.set_viewport_top(45);
8580 let top = e.host().viewport().top_row;
8581 run_keys(&mut e, "M");
8582 assert_eq!(e.cursor().0, top + 4);
8584 }
8585
8586 #[test]
8587 fn g_capital_m_lands_at_line_midpoint() {
8588 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8590 assert_eq!(e.cursor(), (0, 6));
8592 }
8593
8594 #[test]
8595 fn g_capital_m_on_empty_line_stays_at_zero() {
8596 let mut e = editor_with("");
8597 run_keys(&mut e, "gM");
8598 assert_eq!(e.cursor(), (0, 0));
8599 }
8600
8601 #[test]
8602 fn g_capital_m_uses_current_line_only() {
8603 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8606 run_keys(&mut e, "gM");
8607 assert_eq!(e.cursor(), (1, 6));
8608 }
8609
8610 #[test]
8611 fn capital_h_count_offsets_from_top() {
8612 let mut e = editor_with_rows(100, 10);
8613 e.jump_cursor(50, 0);
8614 e.set_viewport_top(45);
8615 let top = e.host().viewport().top_row;
8616 run_keys(&mut e, "3H");
8617 assert_eq!(e.cursor().0, top + 2);
8618 }
8619
8620 #[test]
8623 fn ctrl_o_returns_to_pre_g_position() {
8624 let mut e = editor_with_rows(50, 20);
8625 e.jump_cursor(5, 2);
8626 run_keys(&mut e, "G");
8627 assert_eq!(e.cursor().0, 49);
8628 run_keys(&mut e, "<C-o>");
8629 assert_eq!(e.cursor(), (5, 2));
8630 }
8631
8632 #[test]
8633 fn ctrl_i_redoes_jump_after_ctrl_o() {
8634 let mut e = editor_with_rows(50, 20);
8635 e.jump_cursor(5, 2);
8636 run_keys(&mut e, "G");
8637 let post = e.cursor();
8638 run_keys(&mut e, "<C-o>");
8639 run_keys(&mut e, "<C-i>");
8640 assert_eq!(e.cursor(), post);
8641 }
8642
8643 #[test]
8644 fn new_jump_clears_forward_stack() {
8645 let mut e = editor_with_rows(50, 20);
8646 e.jump_cursor(5, 2);
8647 run_keys(&mut e, "G");
8648 run_keys(&mut e, "<C-o>");
8649 run_keys(&mut e, "gg");
8650 run_keys(&mut e, "<C-i>");
8651 assert_eq!(e.cursor().0, 0);
8652 }
8653
8654 #[test]
8655 fn ctrl_o_on_empty_stack_is_noop() {
8656 let mut e = editor_with_rows(10, 20);
8657 e.jump_cursor(3, 1);
8658 run_keys(&mut e, "<C-o>");
8659 assert_eq!(e.cursor(), (3, 1));
8660 }
8661
8662 #[test]
8663 fn asterisk_search_pushes_jump() {
8664 let mut e = editor_with("foo bar\nbaz foo end");
8665 e.jump_cursor(0, 0);
8666 run_keys(&mut e, "*");
8667 let after = e.cursor();
8668 assert_ne!(after, (0, 0));
8669 run_keys(&mut e, "<C-o>");
8670 assert_eq!(e.cursor(), (0, 0));
8671 }
8672
8673 #[test]
8674 fn h_viewport_jump_is_recorded() {
8675 let mut e = editor_with_rows(100, 10);
8676 e.jump_cursor(50, 0);
8677 e.set_viewport_top(45);
8678 let pre = e.cursor();
8679 run_keys(&mut e, "H");
8680 assert_ne!(e.cursor(), pre);
8681 run_keys(&mut e, "<C-o>");
8682 assert_eq!(e.cursor(), pre);
8683 }
8684
8685 #[test]
8686 fn j_k_motion_does_not_push_jump() {
8687 let mut e = editor_with_rows(50, 20);
8688 e.jump_cursor(5, 0);
8689 run_keys(&mut e, "jjj");
8690 run_keys(&mut e, "<C-o>");
8691 assert_eq!(e.cursor().0, 8);
8692 }
8693
8694 #[test]
8695 fn jumplist_caps_at_100() {
8696 let mut e = editor_with_rows(200, 20);
8697 for i in 0..101 {
8698 e.jump_cursor(i, 0);
8699 run_keys(&mut e, "G");
8700 }
8701 assert!(e.vim.jump_back.len() <= 100);
8702 }
8703
8704 #[test]
8705 fn tab_acts_as_ctrl_i() {
8706 let mut e = editor_with_rows(50, 20);
8707 e.jump_cursor(5, 2);
8708 run_keys(&mut e, "G");
8709 let post = e.cursor();
8710 run_keys(&mut e, "<C-o>");
8711 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8712 assert_eq!(e.cursor(), post);
8713 }
8714
8715 #[test]
8718 fn ma_then_backtick_a_jumps_exact() {
8719 let mut e = editor_with_rows(50, 20);
8720 e.jump_cursor(5, 3);
8721 run_keys(&mut e, "ma");
8722 e.jump_cursor(20, 0);
8723 run_keys(&mut e, "`a");
8724 assert_eq!(e.cursor(), (5, 3));
8725 }
8726
8727 #[test]
8728 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8729 let mut e = editor_with_rows(50, 20);
8730 e.jump_cursor(5, 6);
8732 run_keys(&mut e, "ma");
8733 e.jump_cursor(30, 4);
8734 run_keys(&mut e, "'a");
8735 assert_eq!(e.cursor(), (5, 2));
8736 }
8737
8738 #[test]
8739 fn goto_mark_pushes_jumplist() {
8740 let mut e = editor_with_rows(50, 20);
8741 e.jump_cursor(10, 2);
8742 run_keys(&mut e, "mz");
8743 e.jump_cursor(3, 0);
8744 run_keys(&mut e, "`z");
8745 assert_eq!(e.cursor(), (10, 2));
8746 run_keys(&mut e, "<C-o>");
8747 assert_eq!(e.cursor(), (3, 0));
8748 }
8749
8750 #[test]
8751 fn goto_missing_mark_is_noop() {
8752 let mut e = editor_with_rows(50, 20);
8753 e.jump_cursor(3, 1);
8754 run_keys(&mut e, "`q");
8755 assert_eq!(e.cursor(), (3, 1));
8756 }
8757
8758 #[test]
8759 fn uppercase_mark_stored_under_uppercase_key() {
8760 let mut e = editor_with_rows(50, 20);
8761 e.jump_cursor(5, 3);
8762 run_keys(&mut e, "mA");
8763 assert_eq!(e.mark('A'), Some((5, 3)));
8766 assert!(e.mark('a').is_none());
8767 }
8768
8769 #[test]
8770 fn mark_survives_document_shrink_via_clamp() {
8771 let mut e = editor_with_rows(50, 20);
8772 e.jump_cursor(40, 4);
8773 run_keys(&mut e, "mx");
8774 e.set_content("a\nb\nc\nd\ne");
8776 run_keys(&mut e, "`x");
8777 let (r, _) = e.cursor();
8779 assert!(r <= 4);
8780 }
8781
8782 #[test]
8783 fn g_semicolon_walks_back_through_edits() {
8784 let mut e = editor_with("alpha\nbeta\ngamma");
8785 e.jump_cursor(0, 0);
8788 run_keys(&mut e, "iX<Esc>");
8789 e.jump_cursor(2, 0);
8790 run_keys(&mut e, "iY<Esc>");
8791 run_keys(&mut e, "g;");
8793 assert_eq!(e.cursor(), (2, 1));
8794 run_keys(&mut e, "g;");
8796 assert_eq!(e.cursor(), (0, 1));
8797 run_keys(&mut e, "g;");
8799 assert_eq!(e.cursor(), (0, 1));
8800 }
8801
8802 #[test]
8803 fn g_comma_walks_forward_after_g_semicolon() {
8804 let mut e = editor_with("a\nb\nc");
8805 e.jump_cursor(0, 0);
8806 run_keys(&mut e, "iX<Esc>");
8807 e.jump_cursor(2, 0);
8808 run_keys(&mut e, "iY<Esc>");
8809 run_keys(&mut e, "g;");
8810 run_keys(&mut e, "g;");
8811 assert_eq!(e.cursor(), (0, 1));
8812 run_keys(&mut e, "g,");
8813 assert_eq!(e.cursor(), (2, 1));
8814 }
8815
8816 #[test]
8817 fn new_edit_during_walk_trims_forward_entries() {
8818 let mut e = editor_with("a\nb\nc\nd");
8819 e.jump_cursor(0, 0);
8820 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8822 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8825 run_keys(&mut e, "g;");
8826 assert_eq!(e.cursor(), (0, 1));
8827 run_keys(&mut e, "iZ<Esc>");
8829 run_keys(&mut e, "g,");
8831 assert_ne!(e.cursor(), (2, 1));
8833 }
8834
8835 #[test]
8841 fn capital_mark_set_and_jump() {
8842 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8843 e.jump_cursor(2, 1);
8844 run_keys(&mut e, "mA");
8845 e.jump_cursor(0, 0);
8847 run_keys(&mut e, "'A");
8849 assert_eq!(e.cursor().0, 2);
8851 }
8852
8853 #[test]
8854 fn capital_mark_survives_set_content() {
8855 let mut e = editor_with("first buffer line\nsecond");
8856 e.jump_cursor(1, 3);
8857 run_keys(&mut e, "mA");
8858 e.set_content("totally different content\non many\nrows of text");
8860 e.jump_cursor(0, 0);
8862 run_keys(&mut e, "'A");
8863 assert_eq!(e.cursor().0, 1);
8864 }
8865
8866 #[test]
8871 fn capital_mark_shifts_with_edit() {
8872 let mut e = editor_with("a\nb\nc\nd");
8873 e.jump_cursor(3, 0);
8874 run_keys(&mut e, "mA");
8875 e.jump_cursor(0, 0);
8877 run_keys(&mut e, "dd");
8878 e.jump_cursor(0, 0);
8879 run_keys(&mut e, "'A");
8880 assert_eq!(e.cursor().0, 2);
8881 }
8882
8883 #[test]
8884 fn mark_below_delete_shifts_up() {
8885 let mut e = editor_with("a\nb\nc\nd\ne");
8886 e.jump_cursor(3, 0);
8888 run_keys(&mut e, "ma");
8889 e.jump_cursor(0, 0);
8891 run_keys(&mut e, "dd");
8892 e.jump_cursor(0, 0);
8894 run_keys(&mut e, "'a");
8895 assert_eq!(e.cursor().0, 2);
8896 assert_eq!(e.buffer().line(2).unwrap(), "d");
8897 }
8898
8899 #[test]
8900 fn mark_on_deleted_row_is_dropped() {
8901 let mut e = editor_with("a\nb\nc\nd");
8902 e.jump_cursor(1, 0);
8904 run_keys(&mut e, "ma");
8905 run_keys(&mut e, "dd");
8907 e.jump_cursor(2, 0);
8909 run_keys(&mut e, "'a");
8910 assert_eq!(e.cursor().0, 2);
8912 }
8913
8914 #[test]
8915 fn mark_above_edit_unchanged() {
8916 let mut e = editor_with("a\nb\nc\nd\ne");
8917 e.jump_cursor(0, 0);
8919 run_keys(&mut e, "ma");
8920 e.jump_cursor(3, 0);
8922 run_keys(&mut e, "dd");
8923 e.jump_cursor(2, 0);
8925 run_keys(&mut e, "'a");
8926 assert_eq!(e.cursor().0, 0);
8927 }
8928
8929 #[test]
8930 fn mark_shifts_down_after_insert() {
8931 let mut e = editor_with("a\nb\nc");
8932 e.jump_cursor(2, 0);
8934 run_keys(&mut e, "ma");
8935 e.jump_cursor(0, 0);
8937 run_keys(&mut e, "Onew<Esc>");
8938 e.jump_cursor(0, 0);
8941 run_keys(&mut e, "'a");
8942 assert_eq!(e.cursor().0, 3);
8943 assert_eq!(e.buffer().line(3).unwrap(), "c");
8944 }
8945
8946 #[test]
8949 fn forward_search_commit_pushes_jump() {
8950 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8951 e.jump_cursor(0, 0);
8952 run_keys(&mut e, "/target<CR>");
8953 assert_ne!(e.cursor(), (0, 0));
8955 run_keys(&mut e, "<C-o>");
8957 assert_eq!(e.cursor(), (0, 0));
8958 }
8959
8960 #[test]
8961 fn search_commit_no_match_does_not_push_jump() {
8962 let mut e = editor_with("alpha beta\nfoo end");
8963 e.jump_cursor(0, 3);
8964 let pre_len = e.vim.jump_back.len();
8965 run_keys(&mut e, "/zzznotfound<CR>");
8966 assert_eq!(e.vim.jump_back.len(), pre_len);
8968 }
8969
8970 #[test]
8973 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8974 let mut e = editor_with("hello world");
8975 run_keys(&mut e, "lll");
8976 let (row, col) = e.cursor();
8977 assert_eq!(e.buffer.cursor().row, row);
8978 assert_eq!(e.buffer.cursor().col, col);
8979 }
8980
8981 #[test]
8982 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8983 let mut e = editor_with("aaaa\nbbbb\ncccc");
8984 run_keys(&mut e, "jj");
8985 let (row, col) = e.cursor();
8986 assert_eq!(e.buffer.cursor().row, row);
8987 assert_eq!(e.buffer.cursor().col, col);
8988 }
8989
8990 #[test]
8991 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8992 let mut e = editor_with("foo bar baz");
8993 run_keys(&mut e, "ww");
8994 let (row, col) = e.cursor();
8995 assert_eq!(e.buffer.cursor().row, row);
8996 assert_eq!(e.buffer.cursor().col, col);
8997 }
8998
8999 #[test]
9000 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
9001 let mut e = editor_with("a\nb\nc\nd\ne");
9002 run_keys(&mut e, "G");
9003 let (row, col) = e.cursor();
9004 assert_eq!(e.buffer.cursor().row, row);
9005 assert_eq!(e.buffer.cursor().col, col);
9006 }
9007
9008 #[test]
9009 fn editor_sticky_col_tracks_horizontal_motion() {
9010 let mut e = editor_with("longline\nhi\nlongline");
9011 run_keys(&mut e, "fl");
9016 let landed = e.cursor().1;
9017 assert!(landed > 0, "fl should have moved");
9018 run_keys(&mut e, "j");
9019 assert_eq!(e.sticky_col(), Some(landed));
9022 }
9023
9024 #[test]
9025 fn buffer_content_mirrors_textarea_after_insert() {
9026 let mut e = editor_with("hello");
9027 run_keys(&mut e, "iXYZ<Esc>");
9028 let text = e.buffer().lines().join("\n");
9029 assert_eq!(e.buffer.as_string(), text);
9030 }
9031
9032 #[test]
9033 fn buffer_content_mirrors_textarea_after_delete() {
9034 let mut e = editor_with("alpha bravo charlie");
9035 run_keys(&mut e, "dw");
9036 let text = e.buffer().lines().join("\n");
9037 assert_eq!(e.buffer.as_string(), text);
9038 }
9039
9040 #[test]
9041 fn buffer_content_mirrors_textarea_after_dd() {
9042 let mut e = editor_with("a\nb\nc\nd");
9043 run_keys(&mut e, "jdd");
9044 let text = e.buffer().lines().join("\n");
9045 assert_eq!(e.buffer.as_string(), text);
9046 }
9047
9048 #[test]
9049 fn buffer_content_mirrors_textarea_after_open_line() {
9050 let mut e = editor_with("foo\nbar");
9051 run_keys(&mut e, "oNEW<Esc>");
9052 let text = e.buffer().lines().join("\n");
9053 assert_eq!(e.buffer.as_string(), text);
9054 }
9055
9056 #[test]
9057 fn buffer_content_mirrors_textarea_after_paste() {
9058 let mut e = editor_with("hello");
9059 run_keys(&mut e, "yy");
9060 run_keys(&mut e, "p");
9061 let text = e.buffer().lines().join("\n");
9062 assert_eq!(e.buffer.as_string(), text);
9063 }
9064
9065 #[test]
9066 fn buffer_selection_none_in_normal_mode() {
9067 let e = editor_with("foo bar");
9068 assert!(e.buffer_selection().is_none());
9069 }
9070
9071 #[test]
9072 fn buffer_selection_char_in_visual_mode() {
9073 use hjkl_buffer::{Position, Selection};
9074 let mut e = editor_with("hello world");
9075 run_keys(&mut e, "vlll");
9076 assert_eq!(
9077 e.buffer_selection(),
9078 Some(Selection::Char {
9079 anchor: Position::new(0, 0),
9080 head: Position::new(0, 3),
9081 })
9082 );
9083 }
9084
9085 #[test]
9086 fn buffer_selection_line_in_visual_line_mode() {
9087 use hjkl_buffer::Selection;
9088 let mut e = editor_with("a\nb\nc\nd");
9089 run_keys(&mut e, "Vj");
9090 assert_eq!(
9091 e.buffer_selection(),
9092 Some(Selection::Line {
9093 anchor_row: 0,
9094 head_row: 1,
9095 })
9096 );
9097 }
9098
9099 #[test]
9100 fn wrapscan_off_blocks_wrap_around() {
9101 let mut e = editor_with("first\nsecond\nthird\n");
9102 e.settings_mut().wrapscan = false;
9103 e.jump_cursor(2, 0);
9105 run_keys(&mut e, "/first<CR>");
9106 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
9108 e.settings_mut().wrapscan = true;
9110 run_keys(&mut e, "/first<CR>");
9111 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
9112 }
9113
9114 #[test]
9115 fn smartcase_uppercase_pattern_stays_sensitive() {
9116 let mut e = editor_with("foo\nFoo\nBAR\n");
9117 e.settings_mut().ignore_case = true;
9118 e.settings_mut().smartcase = true;
9119 run_keys(&mut e, "/foo<CR>");
9122 let r1 = e
9123 .search_state()
9124 .pattern
9125 .as_ref()
9126 .unwrap()
9127 .as_str()
9128 .to_string();
9129 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9130 run_keys(&mut e, "/Foo<CR>");
9132 let r2 = e
9133 .search_state()
9134 .pattern
9135 .as_ref()
9136 .unwrap()
9137 .as_str()
9138 .to_string();
9139 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9140 }
9141
9142 #[test]
9143 fn enter_with_autoindent_copies_leading_whitespace() {
9144 let mut e = editor_with(" foo");
9145 e.jump_cursor(0, 7);
9146 run_keys(&mut e, "i<CR>");
9147 assert_eq!(e.buffer.line(1).unwrap(), " ");
9148 }
9149
9150 #[test]
9151 fn enter_without_autoindent_inserts_bare_newline() {
9152 let mut e = editor_with(" foo");
9153 e.settings_mut().autoindent = false;
9154 e.jump_cursor(0, 7);
9155 run_keys(&mut e, "i<CR>");
9156 assert_eq!(e.buffer.line(1).unwrap(), "");
9157 }
9158
9159 #[test]
9160 fn iskeyword_default_treats_alnum_underscore_as_word() {
9161 let mut e = editor_with("foo_bar baz");
9162 e.jump_cursor(0, 0);
9166 run_keys(&mut e, "*");
9167 let p = e
9168 .search_state()
9169 .pattern
9170 .as_ref()
9171 .unwrap()
9172 .as_str()
9173 .to_string();
9174 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9175 }
9176
9177 #[test]
9178 fn w_motion_respects_custom_iskeyword() {
9179 let mut e = editor_with("foo-bar baz");
9183 run_keys(&mut e, "w");
9184 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9185 let mut e2 = editor_with("foo-bar baz");
9188 e2.set_iskeyword("@,_,45");
9189 run_keys(&mut e2, "w");
9190 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9191 }
9192
9193 #[test]
9194 fn iskeyword_with_dash_treats_dash_as_word_char() {
9195 let mut e = editor_with("foo-bar baz");
9196 e.settings_mut().iskeyword = "@,_,45".to_string();
9197 e.jump_cursor(0, 0);
9198 run_keys(&mut e, "*");
9199 let p = e
9200 .search_state()
9201 .pattern
9202 .as_ref()
9203 .unwrap()
9204 .as_str()
9205 .to_string();
9206 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9207 }
9208
9209 #[test]
9210 fn timeoutlen_drops_pending_g_prefix() {
9211 use std::time::{Duration, Instant};
9212 let mut e = editor_with("a\nb\nc");
9213 e.jump_cursor(2, 0);
9214 run_keys(&mut e, "g");
9216 assert!(matches!(e.vim.pending, super::Pending::G));
9217 e.settings.timeout_len = Duration::from_nanos(0);
9225 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9226 e.vim.last_input_host_at = Some(Duration::ZERO);
9227 run_keys(&mut e, "g");
9231 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9233 }
9234
9235 #[test]
9236 fn undobreak_on_breaks_group_at_arrow_motion() {
9237 let mut e = editor_with("");
9238 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9240 let line = e.buffer.line(0).unwrap_or("").to_string();
9243 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9244 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9245 }
9246
9247 #[test]
9248 fn undobreak_off_keeps_full_run_in_one_group() {
9249 let mut e = editor_with("");
9250 e.settings_mut().undo_break_on_motion = false;
9251 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9252 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9255 }
9256
9257 #[test]
9258 fn undobreak_round_trips_through_options() {
9259 let e = editor_with("");
9260 let opts = e.current_options();
9261 assert!(opts.undo_break_on_motion);
9262 let mut e2 = editor_with("");
9263 let mut new_opts = opts.clone();
9264 new_opts.undo_break_on_motion = false;
9265 e2.apply_options(&new_opts);
9266 assert!(!e2.current_options().undo_break_on_motion);
9267 }
9268
9269 #[test]
9270 fn undo_levels_cap_drops_oldest() {
9271 let mut e = editor_with("abcde");
9272 e.settings_mut().undo_levels = 3;
9273 run_keys(&mut e, "ra");
9274 run_keys(&mut e, "lrb");
9275 run_keys(&mut e, "lrc");
9276 run_keys(&mut e, "lrd");
9277 run_keys(&mut e, "lre");
9278 assert_eq!(e.undo_stack_len(), 3);
9279 }
9280
9281 #[test]
9282 fn tab_inserts_literal_tab_when_noexpandtab() {
9283 let mut e = editor_with("");
9284 e.settings_mut().expandtab = false;
9287 e.settings_mut().softtabstop = 0;
9288 run_keys(&mut e, "i");
9289 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9290 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9291 }
9292
9293 #[test]
9294 fn tab_inserts_spaces_when_expandtab() {
9295 let mut e = editor_with("");
9296 e.settings_mut().expandtab = true;
9297 e.settings_mut().tabstop = 4;
9298 run_keys(&mut e, "i");
9299 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9300 assert_eq!(e.buffer.line(0).unwrap(), " ");
9301 }
9302
9303 #[test]
9304 fn tab_with_softtabstop_fills_to_next_boundary() {
9305 let mut e = editor_with("ab");
9307 e.settings_mut().expandtab = true;
9308 e.settings_mut().tabstop = 8;
9309 e.settings_mut().softtabstop = 4;
9310 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9312 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9313 }
9314
9315 #[test]
9316 fn backspace_deletes_softtab_run() {
9317 let mut e = editor_with(" x");
9320 e.settings_mut().softtabstop = 4;
9321 run_keys(&mut e, "fxi");
9323 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9324 assert_eq!(e.buffer.line(0).unwrap(), "x");
9325 }
9326
9327 #[test]
9328 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9329 let mut e = editor_with(" x");
9332 e.settings_mut().softtabstop = 4;
9333 run_keys(&mut e, "fxi");
9334 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9335 assert_eq!(e.buffer.line(0).unwrap(), " x");
9336 }
9337
9338 #[test]
9339 fn readonly_blocks_insert_mutation() {
9340 let mut e = editor_with("hello");
9341 e.settings_mut().readonly = true;
9342 run_keys(&mut e, "iX<Esc>");
9343 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9344 }
9345
9346 #[cfg(feature = "ratatui")]
9347 #[test]
9348 fn intern_ratatui_style_dedups_repeated_styles() {
9349 use ratatui::style::{Color, Style};
9350 let mut e = editor_with("");
9351 let red = Style::default().fg(Color::Red);
9352 let blue = Style::default().fg(Color::Blue);
9353 let id_r1 = e.intern_ratatui_style(red);
9354 let id_r2 = e.intern_ratatui_style(red);
9355 let id_b = e.intern_ratatui_style(blue);
9356 assert_eq!(id_r1, id_r2);
9357 assert_ne!(id_r1, id_b);
9358 assert_eq!(e.style_table().len(), 2);
9359 }
9360
9361 #[cfg(feature = "ratatui")]
9362 #[test]
9363 fn install_ratatui_syntax_spans_translates_styled_spans() {
9364 use ratatui::style::{Color, Style};
9365 let mut e = editor_with("SELECT foo");
9366 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9367 let by_row = e.buffer_spans();
9368 assert_eq!(by_row.len(), 1);
9369 assert_eq!(by_row[0].len(), 1);
9370 assert_eq!(by_row[0][0].start_byte, 0);
9371 assert_eq!(by_row[0][0].end_byte, 6);
9372 let id = by_row[0][0].style;
9373 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9374 }
9375
9376 #[cfg(feature = "ratatui")]
9377 #[test]
9378 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9379 use ratatui::style::{Color, Style};
9380 let mut e = editor_with("hello");
9381 e.install_ratatui_syntax_spans(vec![vec![(
9382 0,
9383 usize::MAX,
9384 Style::default().fg(Color::Blue),
9385 )]]);
9386 let by_row = e.buffer_spans();
9387 assert_eq!(by_row[0][0].end_byte, 5);
9388 }
9389
9390 #[cfg(feature = "ratatui")]
9391 #[test]
9392 fn install_ratatui_syntax_spans_drops_zero_width() {
9393 use ratatui::style::{Color, Style};
9394 let mut e = editor_with("abc");
9395 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9396 assert!(e.buffer_spans()[0].is_empty());
9397 }
9398
9399 #[test]
9400 fn named_register_yank_into_a_then_paste_from_a() {
9401 let mut e = editor_with("hello world\nsecond");
9402 run_keys(&mut e, "\"ayw");
9403 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9405 run_keys(&mut e, "j0\"aP");
9407 assert_eq!(e.buffer().lines()[1], "hello second");
9408 }
9409
9410 #[test]
9411 fn capital_r_overstrikes_chars() {
9412 let mut e = editor_with("hello");
9413 e.jump_cursor(0, 0);
9414 run_keys(&mut e, "RXY<Esc>");
9415 assert_eq!(e.buffer().lines()[0], "XYllo");
9417 }
9418
9419 #[test]
9420 fn capital_r_at_eol_appends() {
9421 let mut e = editor_with("hi");
9422 e.jump_cursor(0, 1);
9423 run_keys(&mut e, "RXYZ<Esc>");
9425 assert_eq!(e.buffer().lines()[0], "hXYZ");
9426 }
9427
9428 #[test]
9429 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9430 let mut e = editor_with("abc");
9434 e.jump_cursor(0, 0);
9435 run_keys(&mut e, "RX<Esc>");
9436 assert_eq!(e.buffer().lines()[0], "Xbc");
9437 }
9438
9439 #[test]
9440 fn ctrl_r_in_insert_pastes_named_register() {
9441 let mut e = editor_with("hello world");
9442 run_keys(&mut e, "\"ayw");
9444 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9445 run_keys(&mut e, "o");
9447 assert_eq!(e.vim_mode(), VimMode::Insert);
9448 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9449 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9450 assert_eq!(e.buffer().lines()[1], "hello ");
9451 assert_eq!(e.cursor(), (1, 6));
9453 assert_eq!(e.vim_mode(), VimMode::Insert);
9455 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9456 assert_eq!(e.buffer().lines()[1], "hello X");
9457 }
9458
9459 #[test]
9460 fn ctrl_r_with_unnamed_register() {
9461 let mut e = editor_with("foo");
9462 run_keys(&mut e, "yiw");
9463 run_keys(&mut e, "A ");
9464 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9466 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9467 assert_eq!(e.buffer().lines()[0], "foo foo");
9468 }
9469
9470 #[test]
9471 fn ctrl_r_unknown_selector_is_no_op() {
9472 let mut e = editor_with("abc");
9473 run_keys(&mut e, "A");
9474 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9475 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9478 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9479 assert_eq!(e.buffer().lines()[0], "abcZ");
9480 }
9481
9482 #[test]
9483 fn ctrl_r_multiline_register_pastes_with_newlines() {
9484 let mut e = editor_with("alpha\nbeta\ngamma");
9485 run_keys(&mut e, "\"byy");
9487 run_keys(&mut e, "j\"byy");
9488 run_keys(&mut e, "ggVj\"by");
9492 let payload = e.registers().read('b').unwrap().text.clone();
9493 assert!(payload.contains('\n'));
9494 run_keys(&mut e, "Go");
9495 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9496 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9497 let total_lines = e.buffer().lines().len();
9500 assert!(total_lines >= 5);
9501 }
9502
9503 #[test]
9504 fn yank_zero_holds_last_yank_after_delete() {
9505 let mut e = editor_with("hello world");
9506 run_keys(&mut e, "yw");
9507 let yanked = e.registers().read('0').unwrap().text.clone();
9508 assert!(!yanked.is_empty());
9509 run_keys(&mut e, "dw");
9511 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9512 assert!(!e.registers().read('1').unwrap().text.is_empty());
9514 }
9515
9516 #[test]
9517 fn delete_ring_rotates_through_one_through_nine() {
9518 let mut e = editor_with("a b c d e f g h i j");
9519 for _ in 0..3 {
9521 run_keys(&mut e, "dw");
9522 }
9523 let r1 = e.registers().read('1').unwrap().text.clone();
9525 let r2 = e.registers().read('2').unwrap().text.clone();
9526 let r3 = e.registers().read('3').unwrap().text.clone();
9527 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9528 assert_ne!(r1, r2);
9529 assert_ne!(r2, r3);
9530 }
9531
9532 #[test]
9533 fn capital_register_appends_to_lowercase() {
9534 let mut e = editor_with("foo bar");
9535 run_keys(&mut e, "\"ayw");
9536 let first = e.registers().read('a').unwrap().text.clone();
9537 assert!(first.contains("foo"));
9538 run_keys(&mut e, "w\"Ayw");
9540 let combined = e.registers().read('a').unwrap().text.clone();
9541 assert!(combined.starts_with(&first));
9542 assert!(combined.contains("bar"));
9543 }
9544
9545 #[test]
9546 fn zf_in_visual_line_creates_closed_fold() {
9547 let mut e = editor_with("a\nb\nc\nd\ne");
9548 e.jump_cursor(1, 0);
9550 run_keys(&mut e, "Vjjzf");
9551 assert_eq!(e.buffer().folds().len(), 1);
9552 let f = e.buffer().folds()[0];
9553 assert_eq!(f.start_row, 1);
9554 assert_eq!(f.end_row, 3);
9555 assert!(f.closed);
9556 }
9557
9558 #[test]
9559 fn zfj_in_normal_creates_two_row_fold() {
9560 let mut e = editor_with("a\nb\nc\nd\ne");
9561 e.jump_cursor(1, 0);
9562 run_keys(&mut e, "zfj");
9563 assert_eq!(e.buffer().folds().len(), 1);
9564 let f = e.buffer().folds()[0];
9565 assert_eq!(f.start_row, 1);
9566 assert_eq!(f.end_row, 2);
9567 assert!(f.closed);
9568 assert_eq!(e.cursor().0, 1);
9570 }
9571
9572 #[test]
9573 fn zf_with_count_folds_count_rows() {
9574 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9575 e.jump_cursor(0, 0);
9576 run_keys(&mut e, "zf3j");
9578 assert_eq!(e.buffer().folds().len(), 1);
9579 let f = e.buffer().folds()[0];
9580 assert_eq!(f.start_row, 0);
9581 assert_eq!(f.end_row, 3);
9582 }
9583
9584 #[test]
9585 fn zfk_folds_upward_range() {
9586 let mut e = editor_with("a\nb\nc\nd\ne");
9587 e.jump_cursor(3, 0);
9588 run_keys(&mut e, "zfk");
9589 let f = e.buffer().folds()[0];
9590 assert_eq!(f.start_row, 2);
9592 assert_eq!(f.end_row, 3);
9593 }
9594
9595 #[test]
9596 fn zf_capital_g_folds_to_bottom() {
9597 let mut e = editor_with("a\nb\nc\nd\ne");
9598 e.jump_cursor(1, 0);
9599 run_keys(&mut e, "zfG");
9601 let f = e.buffer().folds()[0];
9602 assert_eq!(f.start_row, 1);
9603 assert_eq!(f.end_row, 4);
9604 }
9605
9606 #[test]
9607 fn zfgg_folds_to_top_via_operator_pipeline() {
9608 let mut e = editor_with("a\nb\nc\nd\ne");
9609 e.jump_cursor(3, 0);
9610 run_keys(&mut e, "zfgg");
9614 let f = e.buffer().folds()[0];
9615 assert_eq!(f.start_row, 0);
9616 assert_eq!(f.end_row, 3);
9617 }
9618
9619 #[test]
9620 fn zfip_folds_paragraph_via_text_object() {
9621 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9622 e.jump_cursor(1, 0);
9623 run_keys(&mut e, "zfip");
9625 assert_eq!(e.buffer().folds().len(), 1);
9626 let f = e.buffer().folds()[0];
9627 assert_eq!(f.start_row, 0);
9628 assert_eq!(f.end_row, 2);
9629 }
9630
9631 #[test]
9632 fn zfap_folds_paragraph_with_trailing_blank() {
9633 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9634 e.jump_cursor(0, 0);
9635 run_keys(&mut e, "zfap");
9637 let f = e.buffer().folds()[0];
9638 assert_eq!(f.start_row, 0);
9639 assert_eq!(f.end_row, 3);
9640 }
9641
9642 #[test]
9643 fn zf_paragraph_motion_folds_to_blank() {
9644 let mut e = editor_with("alpha\nbeta\n\ngamma");
9645 e.jump_cursor(0, 0);
9646 run_keys(&mut e, "zf}");
9648 let f = e.buffer().folds()[0];
9649 assert_eq!(f.start_row, 0);
9650 assert_eq!(f.end_row, 2);
9651 }
9652
9653 #[test]
9654 fn za_toggles_fold_under_cursor() {
9655 let mut e = editor_with("a\nb\nc\nd");
9656 e.buffer_mut().add_fold(1, 2, true);
9657 e.jump_cursor(1, 0);
9658 run_keys(&mut e, "za");
9659 assert!(!e.buffer().folds()[0].closed);
9660 run_keys(&mut e, "za");
9661 assert!(e.buffer().folds()[0].closed);
9662 }
9663
9664 #[test]
9665 fn zr_opens_all_folds_zm_closes_all() {
9666 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9667 e.buffer_mut().add_fold(0, 1, true);
9668 e.buffer_mut().add_fold(2, 3, true);
9669 e.buffer_mut().add_fold(4, 5, true);
9670 run_keys(&mut e, "zR");
9671 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9672 run_keys(&mut e, "zM");
9673 assert!(e.buffer().folds().iter().all(|f| f.closed));
9674 }
9675
9676 #[test]
9677 fn ze_clears_all_folds() {
9678 let mut e = editor_with("a\nb\nc\nd");
9679 e.buffer_mut().add_fold(0, 1, true);
9680 e.buffer_mut().add_fold(2, 3, false);
9681 run_keys(&mut e, "zE");
9682 assert!(e.buffer().folds().is_empty());
9683 }
9684
9685 #[test]
9686 fn g_underscore_jumps_to_last_non_blank() {
9687 let mut e = editor_with("hello world ");
9688 run_keys(&mut e, "g_");
9689 assert_eq!(e.cursor().1, 10);
9691 }
9692
9693 #[test]
9694 fn gj_and_gk_alias_j_and_k() {
9695 let mut e = editor_with("a\nb\nc");
9696 run_keys(&mut e, "gj");
9697 assert_eq!(e.cursor().0, 1);
9698 run_keys(&mut e, "gk");
9699 assert_eq!(e.cursor().0, 0);
9700 }
9701
9702 #[test]
9703 fn paragraph_motions_walk_blank_lines() {
9704 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9705 run_keys(&mut e, "}");
9706 assert_eq!(e.cursor().0, 2);
9707 run_keys(&mut e, "}");
9708 assert_eq!(e.cursor().0, 5);
9709 run_keys(&mut e, "{");
9710 assert_eq!(e.cursor().0, 2);
9711 }
9712
9713 #[test]
9714 fn gv_reenters_last_visual_selection() {
9715 let mut e = editor_with("alpha\nbeta\ngamma");
9716 run_keys(&mut e, "Vj");
9717 run_keys(&mut e, "<Esc>");
9719 assert_eq!(e.vim_mode(), VimMode::Normal);
9720 run_keys(&mut e, "gv");
9722 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9723 }
9724
9725 #[test]
9726 fn o_in_visual_swaps_anchor_and_cursor() {
9727 let mut e = editor_with("hello world");
9728 run_keys(&mut e, "vllll");
9730 assert_eq!(e.cursor().1, 4);
9731 run_keys(&mut e, "o");
9733 assert_eq!(e.cursor().1, 0);
9734 assert_eq!(e.vim.visual_anchor, (0, 4));
9736 }
9737
9738 #[test]
9739 fn editing_inside_fold_invalidates_it() {
9740 let mut e = editor_with("a\nb\nc\nd");
9741 e.buffer_mut().add_fold(1, 2, true);
9742 e.jump_cursor(1, 0);
9743 run_keys(&mut e, "iX<Esc>");
9745 assert!(e.buffer().folds().is_empty());
9747 }
9748
9749 #[test]
9750 fn zd_removes_fold_under_cursor() {
9751 let mut e = editor_with("a\nb\nc\nd");
9752 e.buffer_mut().add_fold(1, 2, true);
9753 e.jump_cursor(2, 0);
9754 run_keys(&mut e, "zd");
9755 assert!(e.buffer().folds().is_empty());
9756 }
9757
9758 #[test]
9759 fn take_fold_ops_observes_z_keystroke_dispatch() {
9760 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();
9771 run_keys(&mut e, "zo");
9772 run_keys(&mut e, "zM");
9773 let ops = e.take_fold_ops();
9774 assert_eq!(ops.len(), 2);
9775 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9776 assert!(matches!(ops[1], FoldOp::CloseAll));
9777 assert!(e.take_fold_ops().is_empty());
9779 }
9780
9781 #[test]
9782 fn edit_pipeline_emits_invalidate_fold_op() {
9783 use crate::types::FoldOp;
9786 let mut e = editor_with("a\nb\nc\nd");
9787 e.buffer_mut().add_fold(1, 2, true);
9788 e.jump_cursor(1, 0);
9789 let _ = e.take_fold_ops();
9790 run_keys(&mut e, "iX<Esc>");
9791 let ops = e.take_fold_ops();
9792 assert!(
9793 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9794 "expected at least one Invalidate op, got {ops:?}"
9795 );
9796 }
9797
9798 #[test]
9799 fn dot_mark_jumps_to_last_edit_position() {
9800 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9801 e.jump_cursor(2, 0);
9802 run_keys(&mut e, "iX<Esc>");
9804 let after_edit = e.cursor();
9805 run_keys(&mut e, "gg");
9807 assert_eq!(e.cursor().0, 0);
9808 run_keys(&mut e, "'.");
9810 assert_eq!(e.cursor().0, after_edit.0);
9811 }
9812
9813 #[test]
9814 fn quote_quote_returns_to_pre_jump_position() {
9815 let mut e = editor_with_rows(50, 20);
9816 e.jump_cursor(10, 2);
9817 let before = e.cursor();
9818 run_keys(&mut e, "G");
9820 assert_ne!(e.cursor(), before);
9821 run_keys(&mut e, "''");
9823 assert_eq!(e.cursor().0, before.0);
9824 }
9825
9826 #[test]
9827 fn backtick_backtick_restores_exact_pre_jump_pos() {
9828 let mut e = editor_with_rows(50, 20);
9829 e.jump_cursor(7, 3);
9830 let before = e.cursor();
9831 run_keys(&mut e, "G");
9832 run_keys(&mut e, "``");
9833 assert_eq!(e.cursor(), before);
9834 }
9835
9836 #[test]
9837 fn macro_record_and_replay_basic() {
9838 let mut e = editor_with("foo\nbar\nbaz");
9839 run_keys(&mut e, "qaIX<Esc>jq");
9841 assert_eq!(e.buffer().lines()[0], "Xfoo");
9842 run_keys(&mut e, "@a");
9844 assert_eq!(e.buffer().lines()[1], "Xbar");
9845 run_keys(&mut e, "j@@");
9847 assert_eq!(e.buffer().lines()[2], "Xbaz");
9848 }
9849
9850 #[test]
9851 fn macro_count_replays_n_times() {
9852 let mut e = editor_with("a\nb\nc\nd\ne");
9853 run_keys(&mut e, "qajq");
9855 assert_eq!(e.cursor().0, 1);
9856 run_keys(&mut e, "3@a");
9858 assert_eq!(e.cursor().0, 4);
9859 }
9860
9861 #[test]
9862 fn macro_capital_q_appends_to_lowercase_register() {
9863 let mut e = editor_with("hello");
9864 run_keys(&mut e, "qall<Esc>q");
9865 run_keys(&mut e, "qAhh<Esc>q");
9866 let text = e.registers().read('a').unwrap().text.clone();
9869 assert!(text.contains("ll<Esc>"));
9870 assert!(text.contains("hh<Esc>"));
9871 }
9872
9873 #[test]
9874 fn buffer_selection_block_in_visual_block_mode() {
9875 use hjkl_buffer::{Position, Selection};
9876 let mut e = editor_with("aaaa\nbbbb\ncccc");
9877 run_keys(&mut e, "<C-v>jl");
9878 assert_eq!(
9879 e.buffer_selection(),
9880 Some(Selection::Block {
9881 anchor: Position::new(0, 0),
9882 head: Position::new(1, 1),
9883 })
9884 );
9885 }
9886
9887 #[test]
9890 fn n_after_question_mark_keeps_walking_backward() {
9891 let mut e = editor_with("foo bar foo baz foo end");
9894 e.jump_cursor(0, 22);
9895 run_keys(&mut e, "?foo<CR>");
9896 assert_eq!(e.cursor().1, 16);
9897 run_keys(&mut e, "n");
9898 assert_eq!(e.cursor().1, 8);
9899 run_keys(&mut e, "N");
9900 assert_eq!(e.cursor().1, 16);
9901 }
9902
9903 #[test]
9904 fn nested_macro_chord_records_literal_keys() {
9905 let mut e = editor_with("alpha\nbeta\ngamma");
9908 run_keys(&mut e, "qblq");
9910 run_keys(&mut e, "qaIX<Esc>q");
9913 e.jump_cursor(1, 0);
9915 run_keys(&mut e, "@a");
9916 assert_eq!(e.buffer().lines()[1], "Xbeta");
9917 }
9918
9919 #[test]
9920 fn shift_gt_motion_indents_one_line() {
9921 let mut e = editor_with("hello world");
9925 run_keys(&mut e, ">w");
9926 assert_eq!(e.buffer().lines()[0], " hello world");
9927 }
9928
9929 #[test]
9930 fn shift_lt_motion_outdents_one_line() {
9931 let mut e = editor_with(" hello world");
9932 run_keys(&mut e, "<lt>w");
9933 assert_eq!(e.buffer().lines()[0], " hello world");
9935 }
9936
9937 #[test]
9938 fn shift_gt_text_object_indents_paragraph() {
9939 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9940 e.jump_cursor(0, 0);
9941 run_keys(&mut e, ">ip");
9942 assert_eq!(e.buffer().lines()[0], " alpha");
9943 assert_eq!(e.buffer().lines()[1], " beta");
9944 assert_eq!(e.buffer().lines()[2], " gamma");
9945 assert_eq!(e.buffer().lines()[4], "rest");
9947 }
9948
9949 #[test]
9950 fn ctrl_o_runs_exactly_one_normal_command() {
9951 let mut e = editor_with("alpha beta gamma");
9954 e.jump_cursor(0, 0);
9955 run_keys(&mut e, "i");
9956 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9957 run_keys(&mut e, "dw");
9958 assert_eq!(e.vim_mode(), VimMode::Insert);
9960 run_keys(&mut e, "X");
9962 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9963 }
9964
9965 #[test]
9966 fn macro_replay_respects_mode_switching() {
9967 let mut e = editor_with("hi");
9971 run_keys(&mut e, "qaiX<Esc>0q");
9972 assert_eq!(e.vim_mode(), VimMode::Normal);
9973 e.set_content("yo");
9975 run_keys(&mut e, "@a");
9976 assert_eq!(e.vim_mode(), VimMode::Normal);
9977 assert_eq!(e.cursor().1, 0);
9978 assert_eq!(e.buffer().lines()[0], "Xyo");
9979 }
9980
9981 #[test]
9982 fn macro_recorded_text_round_trips_through_register() {
9983 let mut e = editor_with("");
9987 run_keys(&mut e, "qaiX<Esc>q");
9988 let text = e.registers().read('a').unwrap().text.clone();
9989 assert!(text.starts_with("iX"));
9990 run_keys(&mut e, "@a");
9992 assert_eq!(e.buffer().lines()[0], "XX");
9993 }
9994
9995 #[test]
9996 fn dot_after_macro_replays_macros_last_change() {
9997 let mut e = editor_with("ab\ncd\nef");
10000 run_keys(&mut e, "qaIX<Esc>jq");
10003 assert_eq!(e.buffer().lines()[0], "Xab");
10004 run_keys(&mut e, "@a");
10005 assert_eq!(e.buffer().lines()[1], "Xcd");
10006 let row_before_dot = e.cursor().0;
10009 run_keys(&mut e, ".");
10010 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
10011 }
10012
10013 fn si_editor(content: &str) -> Editor {
10019 let opts = crate::types::Options {
10020 shiftwidth: 4,
10021 softtabstop: 4,
10022 expandtab: true,
10023 smartindent: true,
10024 autoindent: true,
10025 ..crate::types::Options::default()
10026 };
10027 let mut e = Editor::new(
10028 hjkl_buffer::Buffer::new(),
10029 crate::types::DefaultHost::new(),
10030 opts,
10031 );
10032 e.set_content(content);
10033 e
10034 }
10035
10036 #[test]
10037 fn smartindent_bumps_indent_after_open_brace() {
10038 let mut e = si_editor("fn foo() {");
10040 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
10042 assert_eq!(
10043 e.buffer().lines()[1],
10044 " ",
10045 "smartindent should bump one shiftwidth after {{"
10046 );
10047 }
10048
10049 #[test]
10050 fn smartindent_no_bump_when_off() {
10051 let mut e = si_editor("fn foo() {");
10054 e.settings_mut().smartindent = false;
10055 e.jump_cursor(0, 10);
10056 run_keys(&mut e, "i<CR>");
10057 assert_eq!(
10058 e.buffer().lines()[1],
10059 "",
10060 "without smartindent, no bump: new line copies empty leading ws"
10061 );
10062 }
10063
10064 #[test]
10065 fn smartindent_uses_tab_when_noexpandtab() {
10066 let opts = crate::types::Options {
10068 shiftwidth: 4,
10069 softtabstop: 0,
10070 expandtab: false,
10071 smartindent: true,
10072 autoindent: true,
10073 ..crate::types::Options::default()
10074 };
10075 let mut e = Editor::new(
10076 hjkl_buffer::Buffer::new(),
10077 crate::types::DefaultHost::new(),
10078 opts,
10079 );
10080 e.set_content("fn foo() {");
10081 e.jump_cursor(0, 10);
10082 run_keys(&mut e, "i<CR>");
10083 assert_eq!(
10084 e.buffer().lines()[1],
10085 "\t",
10086 "noexpandtab: smartindent bump inserts a literal tab"
10087 );
10088 }
10089
10090 #[test]
10091 fn smartindent_dedent_on_close_brace() {
10092 let mut e = si_editor("fn foo() {");
10095 e.set_content("fn foo() {\n ");
10097 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
10099 assert_eq!(
10100 e.buffer().lines()[1],
10101 "}",
10102 "close brace on whitespace-only line should dedent"
10103 );
10104 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
10105 }
10106
10107 #[test]
10108 fn smartindent_no_dedent_when_off() {
10109 let mut e = si_editor("fn foo() {\n ");
10111 e.settings_mut().smartindent = false;
10112 e.jump_cursor(1, 4);
10113 run_keys(&mut e, "i}");
10114 assert_eq!(
10115 e.buffer().lines()[1],
10116 " }",
10117 "without smartindent, `}}` just appends at cursor"
10118 );
10119 }
10120
10121 #[test]
10122 fn smartindent_no_dedent_mid_line() {
10123 let mut e = si_editor(" let x = 1");
10126 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
10128 assert_eq!(
10129 e.buffer().lines()[0],
10130 " let x = 1}",
10131 "mid-line `}}` should not dedent"
10132 );
10133 }
10134
10135 #[test]
10139 fn count_5x_fills_unnamed_register() {
10140 let mut e = editor_with("hello world\n");
10141 e.jump_cursor(0, 0);
10142 run_keys(&mut e, "5x");
10143 assert_eq!(e.buffer().lines()[0], " world");
10144 assert_eq!(e.cursor(), (0, 0));
10145 assert_eq!(e.yank(), "hello");
10146 }
10147
10148 #[test]
10149 fn x_fills_unnamed_register_single_char() {
10150 let mut e = editor_with("abc\n");
10151 e.jump_cursor(0, 0);
10152 run_keys(&mut e, "x");
10153 assert_eq!(e.buffer().lines()[0], "bc");
10154 assert_eq!(e.yank(), "a");
10155 }
10156
10157 #[test]
10158 fn big_x_fills_unnamed_register() {
10159 let mut e = editor_with("hello\n");
10160 e.jump_cursor(0, 3);
10161 run_keys(&mut e, "X");
10162 assert_eq!(e.buffer().lines()[0], "helo");
10163 assert_eq!(e.yank(), "l");
10164 }
10165
10166 #[test]
10168 fn g_motion_trailing_newline_lands_on_last_content_row() {
10169 let mut e = editor_with("foo\nbar\nbaz\n");
10170 e.jump_cursor(0, 0);
10171 run_keys(&mut e, "G");
10172 assert_eq!(
10174 e.cursor().0,
10175 2,
10176 "G should land on row 2 (baz), not row 3 (phantom empty)"
10177 );
10178 }
10179
10180 #[test]
10182 fn dd_last_line_clamps_cursor_to_new_last_row() {
10183 let mut e = editor_with("foo\nbar\n");
10184 e.jump_cursor(1, 0);
10185 run_keys(&mut e, "dd");
10186 assert_eq!(e.buffer().lines()[0], "foo");
10187 assert_eq!(
10188 e.cursor(),
10189 (0, 0),
10190 "cursor should clamp to row 0 after dd on last content line"
10191 );
10192 }
10193
10194 #[test]
10196 fn d_dollar_cursor_on_last_char() {
10197 let mut e = editor_with("hello world\n");
10198 e.jump_cursor(0, 5);
10199 run_keys(&mut e, "d$");
10200 assert_eq!(e.buffer().lines()[0], "hello");
10201 assert_eq!(
10202 e.cursor(),
10203 (0, 4),
10204 "d$ should leave cursor on col 4, not col 5"
10205 );
10206 }
10207
10208 #[test]
10210 fn undo_insert_clamps_cursor_to_last_valid_col() {
10211 let mut e = editor_with("hello\n");
10212 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10214 assert_eq!(e.buffer().lines()[0], "hello");
10215 assert_eq!(
10216 e.cursor(),
10217 (0, 4),
10218 "undo should clamp cursor to col 4 on 'hello'"
10219 );
10220 }
10221
10222 #[test]
10224 fn da_doublequote_eats_trailing_whitespace() {
10225 let mut e = editor_with("say \"hello\" there\n");
10226 e.jump_cursor(0, 6);
10227 run_keys(&mut e, "da\"");
10228 assert_eq!(e.buffer().lines()[0], "say there");
10229 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10230 }
10231
10232 #[test]
10234 fn dab_cursor_col_clamped_after_delete() {
10235 let mut e = editor_with("fn x() {\n body\n}\n");
10236 e.jump_cursor(1, 4);
10237 run_keys(&mut e, "daB");
10238 assert_eq!(e.buffer().lines()[0], "fn x() ");
10239 assert_eq!(
10240 e.cursor(),
10241 (0, 6),
10242 "daB should leave cursor at col 6, not 7"
10243 );
10244 }
10245
10246 #[test]
10248 fn dib_preserves_surrounding_newlines() {
10249 let mut e = editor_with("{\n body\n}\n");
10250 e.jump_cursor(1, 4);
10251 run_keys(&mut e, "diB");
10252 assert_eq!(e.buffer().lines()[0], "{");
10253 assert_eq!(e.buffer().lines()[1], "}");
10254 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10255 }
10256
10257 #[test]
10258 fn is_chord_pending_tracks_replace_state() {
10259 let mut e = editor_with("abc\n");
10260 assert!(!e.is_chord_pending());
10261 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10263 assert!(e.is_chord_pending(), "engine should be pending after r");
10264 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10266 assert!(
10267 !e.is_chord_pending(),
10268 "engine pending should clear after replace"
10269 );
10270 }
10271
10272 #[test]
10275 fn yiw_sets_lbr_rbr_marks_around_word() {
10276 let mut e = editor_with("hello world");
10279 run_keys(&mut e, "yiw");
10280 let lo = e.mark('[').expect("'[' must be set after yiw");
10281 let hi = e.mark(']').expect("']' must be set after yiw");
10282 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10283 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10284 }
10285
10286 #[test]
10287 fn yj_linewise_sets_marks_at_line_edges() {
10288 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10291 run_keys(&mut e, "yj");
10292 let lo = e.mark('[').expect("'[' must be set after yj");
10293 let hi = e.mark(']').expect("']' must be set after yj");
10294 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10295 assert_eq!(
10296 hi,
10297 (1, 4),
10298 "'] snaps to (bot_row, last_col) for linewise yank"
10299 );
10300 }
10301
10302 #[test]
10303 fn dd_sets_lbr_rbr_marks_to_cursor() {
10304 let mut e = editor_with("aaa\nbbb");
10307 run_keys(&mut e, "dd");
10308 let lo = e.mark('[').expect("'[' must be set after dd");
10309 let hi = e.mark(']').expect("']' must be set after dd");
10310 assert_eq!(lo, hi, "after delete both marks are at the same position");
10311 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10312 }
10313
10314 #[test]
10315 fn dw_sets_lbr_rbr_marks_to_cursor() {
10316 let mut e = editor_with("hello world");
10319 run_keys(&mut e, "dw");
10320 let lo = e.mark('[').expect("'[' must be set after dw");
10321 let hi = e.mark(']').expect("']' must be set after dw");
10322 assert_eq!(lo, hi, "after delete both marks are at the same position");
10323 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10324 }
10325
10326 #[test]
10327 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10328 let mut e = editor_with("hello world");
10333 run_keys(&mut e, "cwfoo<Esc>");
10334 let lo = e.mark('[').expect("'[' must be set after cw");
10335 let hi = e.mark(']').expect("']' must be set after cw");
10336 assert_eq!(lo, (0, 0), "'[ should be start of change");
10337 assert_eq!(hi.0, 0, "'] should be on row 0");
10340 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10341 }
10342
10343 #[test]
10344 fn cw_with_no_insertion_sets_marks_at_change_start() {
10345 let mut e = editor_with("hello world");
10348 run_keys(&mut e, "cw<Esc>");
10349 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10350 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10351 assert_eq!(lo.0, 0, "'[ should be on row 0");
10352 assert_eq!(hi.0, 0, "'] should be on row 0");
10353 assert_eq!(lo, hi, "marks coincide when insert is empty");
10355 }
10356
10357 #[test]
10358 fn p_charwise_sets_marks_around_pasted_text() {
10359 let mut e = editor_with("abc xyz");
10362 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10365 let hi = e.mark(']').expect("']' set after charwise paste");
10366 assert!(lo <= hi, "'[ must not exceed ']'");
10367 assert_eq!(
10369 hi.1.wrapping_sub(lo.1),
10370 2,
10371 "'] - '[ should span 2 cols for a 3-char paste"
10372 );
10373 }
10374
10375 #[test]
10376 fn p_linewise_sets_marks_at_line_edges() {
10377 let mut e = editor_with("aaa\nbbb\nccc");
10380 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10384 let hi = e.mark(']').expect("']' set after linewise paste");
10385 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10386 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10387 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10388 }
10389
10390 #[test]
10391 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10392 let mut e = editor_with("hello world");
10396 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10400 assert_eq!(
10402 e.cursor(),
10403 (0, 4),
10404 "visual `[v`] should land on last yanked char"
10405 );
10406 assert_eq!(
10408 e.vim_mode(),
10409 crate::VimMode::Visual,
10410 "should be in Visual mode"
10411 );
10412 }
10413
10414 #[test]
10420 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10421 let mut e = editor_with("hello\nworld\n");
10424 e.jump_cursor(0, 0);
10425 run_keys(&mut e, "iX<Esc>j`.");
10426 assert_eq!(
10427 e.cursor(),
10428 (0, 0),
10429 "dot mark should jump to the change-start (col 0), not post-insert col"
10430 );
10431 }
10432
10433 #[test]
10436 fn count_100g_clamps_to_last_content_row() {
10437 let mut e = editor_with("foo\nbar\nbaz\n");
10440 e.jump_cursor(0, 0);
10441 run_keys(&mut e, "100G");
10442 assert_eq!(
10443 e.cursor(),
10444 (2, 0),
10445 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10446 );
10447 }
10448
10449 #[test]
10452 fn gi_resumes_last_insert_position() {
10453 let mut e = editor_with("world\nhello\n");
10459 e.jump_cursor(0, 0);
10460 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10461 assert_eq!(
10462 e.vim_mode(),
10463 crate::VimMode::Normal,
10464 "should be in Normal mode after gi<Esc>"
10465 );
10466 assert_eq!(
10467 e.cursor(),
10468 (0, 1),
10469 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10470 );
10471 }
10472
10473 #[test]
10477 fn visual_block_change_cursor_on_last_inserted_char() {
10478 let mut e = editor_with("foo\nbar\nbaz\n");
10482 e.jump_cursor(0, 0);
10483 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10484 let lines = e.buffer().lines().to_vec();
10485 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10486 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10487 assert_eq!(
10488 e.cursor(),
10489 (0, 1),
10490 "cursor should be on last char of inserted 'ZZ' (col 1)"
10491 );
10492 }
10493
10494 #[test]
10499 fn register_blackhole_delete_preserves_unnamed_register() {
10500 let mut e = editor_with("foo bar baz\n");
10507 e.jump_cursor(0, 0);
10508 run_keys(&mut e, "yiww\"_dwbp");
10509 let lines = e.buffer().lines().to_vec();
10510 assert_eq!(
10511 lines[0], "ffoooo baz",
10512 "black-hole delete must not corrupt unnamed register"
10513 );
10514 assert_eq!(
10515 e.cursor(),
10516 (0, 3),
10517 "cursor should be on last pasted char (col 3)"
10518 );
10519 }
10520
10521 #[test]
10524 fn after_z_zz_sets_viewport_pinned() {
10525 let mut e = editor_with("a\nb\nc\nd\ne");
10526 e.jump_cursor(2, 0);
10527 e.after_z('z', 1);
10528 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10529 }
10530
10531 #[test]
10532 fn after_z_zo_opens_fold_at_cursor() {
10533 let mut e = editor_with("a\nb\nc\nd");
10534 e.buffer_mut().add_fold(1, 2, true);
10535 e.jump_cursor(1, 0);
10536 e.after_z('o', 1);
10537 assert!(
10538 !e.buffer().folds()[0].closed,
10539 "zo must open the fold at the cursor row"
10540 );
10541 }
10542
10543 #[test]
10544 fn after_z_zm_closes_all_folds() {
10545 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10546 e.buffer_mut().add_fold(0, 1, false);
10547 e.buffer_mut().add_fold(4, 5, false);
10548 e.after_z('M', 1);
10549 assert!(
10550 e.buffer().folds().iter().all(|f| f.closed),
10551 "zM must close all folds"
10552 );
10553 }
10554
10555 #[test]
10556 fn after_z_zd_removes_fold_at_cursor() {
10557 let mut e = editor_with("a\nb\nc\nd");
10558 e.buffer_mut().add_fold(1, 2, true);
10559 e.jump_cursor(1, 0);
10560 e.after_z('d', 1);
10561 assert!(
10562 e.buffer().folds().is_empty(),
10563 "zd must remove the fold at the cursor row"
10564 );
10565 }
10566
10567 #[test]
10568 fn after_z_zf_in_visual_creates_fold() {
10569 let mut e = editor_with("a\nb\nc\nd\ne");
10570 e.jump_cursor(1, 0);
10572 run_keys(&mut e, "V2j");
10573 e.after_z('f', 1);
10575 let folds = e.buffer().folds();
10576 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10577 assert_eq!(folds[0].start_row, 1);
10578 assert_eq!(folds[0].end_row, 3);
10579 assert!(folds[0].closed);
10580 }
10581
10582 #[test]
10585 fn apply_op_motion_dw_deletes_word() {
10586 let mut e = editor_with("hello world");
10588 e.apply_op_motion(crate::vim::Operator::Delete, 'w', 1);
10589 assert_eq!(
10590 e.buffer().lines().first().cloned().unwrap_or_default(),
10591 "world"
10592 );
10593 }
10594
10595 #[test]
10596 fn apply_op_motion_cw_quirk_leaves_trailing_space() {
10597 let mut e = editor_with("hello world");
10599 e.apply_op_motion(crate::vim::Operator::Change, 'w', 1);
10600 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10603 assert!(
10604 line.starts_with(' ') || line == " world",
10605 "cw quirk: got {line:?}"
10606 );
10607 assert_eq!(e.vim_mode(), VimMode::Insert);
10608 }
10609
10610 #[test]
10611 fn apply_op_double_dd_deletes_line() {
10612 let mut e = editor_with("line1\nline2\nline3");
10613 e.apply_op_double(crate::vim::Operator::Delete, 1);
10615 let lines: Vec<_> = e.buffer().lines().to_vec();
10616 assert_eq!(lines, vec!["line2", "line3"], "dd should delete line1");
10617 }
10618
10619 #[test]
10620 fn apply_op_double_yy_does_not_modify_buffer() {
10621 let mut e = editor_with("hello");
10622 e.apply_op_double(crate::vim::Operator::Yank, 1);
10623 assert_eq!(
10624 e.buffer().lines().first().cloned().unwrap_or_default(),
10625 "hello"
10626 );
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
10650 #[test]
10653 fn apply_op_find_dfx_deletes_to_x() {
10654 let mut e = editor_with("hello x world");
10656 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10657 assert_eq!(
10658 e.buffer().lines().first().cloned().unwrap_or_default(),
10659 " world",
10660 "dfx must delete 'hello x'"
10661 );
10662 }
10663
10664 #[test]
10665 fn apply_op_find_dtx_deletes_up_to_x() {
10666 let mut e = editor_with("hello x world");
10668 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, true, 1);
10669 assert_eq!(
10670 e.buffer().lines().first().cloned().unwrap_or_default(),
10671 "x world",
10672 "dtx must delete 'hello ' leaving 'x world'"
10673 );
10674 }
10675
10676 #[test]
10677 fn apply_op_find_records_last_find() {
10678 let mut e = editor_with("hello x world");
10680 e.apply_op_find(crate::vim::Operator::Delete, 'x', true, false, 1);
10681 let _ = e.cursor(); }
10688
10689 #[test]
10692 fn apply_op_text_obj_diw_deletes_word() {
10693 let mut e = editor_with("hello world");
10695 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', true, 1);
10696 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10697 assert!(
10702 !line.contains("hello"),
10703 "diw must delete 'hello', remaining: {line:?}"
10704 );
10705 }
10706
10707 #[test]
10708 fn apply_op_text_obj_daw_deletes_around_word() {
10709 let mut e = editor_with("hello world");
10711 e.apply_op_text_obj(crate::vim::Operator::Delete, 'w', false, 1);
10712 let line = e.buffer().lines().first().cloned().unwrap_or_default();
10713 assert!(
10714 !line.contains("hello"),
10715 "daw must delete 'hello' and surrounding space, remaining: {line:?}"
10716 );
10717 }
10718
10719 #[test]
10720 fn apply_op_text_obj_invalid_char_no_op() {
10721 let mut e = editor_with("hello world");
10723 let before = e.buffer().as_string();
10724 e.apply_op_text_obj(crate::vim::Operator::Delete, 'X', true, 1);
10725 assert_eq!(
10726 e.buffer().as_string(),
10727 before,
10728 "unknown text-object char must be a no-op"
10729 );
10730 }
10731
10732 #[test]
10735 fn apply_op_g_dgg_deletes_to_top() {
10736 let mut e = editor_with("line1\nline2\nline3");
10738 e.apply_op_motion(crate::vim::Operator::Delete, 'j', 1);
10740 e.apply_op_g(crate::vim::Operator::Delete, 'g', 1);
10743 let lines: Vec<_> = e.buffer().lines().to_vec();
10745 assert_eq!(lines, vec!["line3"], "dgg must delete to file top");
10746 }
10747
10748 #[test]
10749 fn apply_op_g_dge_deletes_word_end_back() {
10750 let mut e = editor_with("hello world");
10763 let before = e.buffer().as_string();
10764 e.apply_op_g(crate::vim::Operator::Delete, 'X', 1);
10766 assert_eq!(
10767 e.buffer().as_string(),
10768 before,
10769 "apply_op_g with unknown char must be a no-op"
10770 );
10771 e.apply_op_g(crate::vim::Operator::Delete, 'e', 1);
10773 }
10775
10776 #[test]
10777 fn apply_op_g_dgj_deletes_screen_down() {
10778 let mut e = editor_with("line1\nline2\nline3");
10781 e.apply_op_g(crate::vim::Operator::Delete, 'j', 1);
10782 let lines: Vec<_> = e.buffer().lines().to_vec();
10783 assert_eq!(lines, vec!["line3"], "dgj must delete current+next line");
10785 }
10786
10787 fn blank_editor() -> Editor {
10790 Editor::new(
10791 hjkl_buffer::Buffer::new(),
10792 crate::types::DefaultHost::new(),
10793 crate::types::Options::default(),
10794 )
10795 }
10796
10797 #[test]
10798 fn set_pending_register_valid_letter_sets_field() {
10799 let mut e = blank_editor();
10800 assert!(e.vim.pending_register.is_none());
10801 e.set_pending_register('a');
10802 assert_eq!(e.vim.pending_register, Some('a'));
10803 }
10804
10805 #[test]
10806 fn set_pending_register_invalid_char_no_op() {
10807 let mut e = blank_editor();
10808 e.set_pending_register('!');
10809 assert!(
10810 e.vim.pending_register.is_none(),
10811 "invalid register char must not set pending_register"
10812 );
10813 }
10814
10815 #[test]
10816 fn set_pending_register_special_plus_sets_field() {
10817 let mut e = blank_editor();
10819 e.set_pending_register('+');
10820 assert_eq!(e.vim.pending_register, Some('+'));
10821 }
10822
10823 #[test]
10824 fn set_pending_register_star_sets_field() {
10825 let mut e = blank_editor();
10827 e.set_pending_register('*');
10828 assert_eq!(e.vim.pending_register, Some('*'));
10829 }
10830
10831 #[test]
10832 fn set_pending_register_underscore_sets_field() {
10833 let mut e = blank_editor();
10835 e.set_pending_register('_');
10836 assert_eq!(e.vim.pending_register, Some('_'));
10837 }
10838}