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) change_list: Vec<(usize, usize)>,
392 pub(super) change_list_cursor: Option<usize>,
395 pub(super) last_visual: Option<LastVisual>,
398 pub(super) viewport_pinned: bool,
402 replaying: bool,
404 one_shot_normal: bool,
407 pub(super) search_prompt: Option<SearchPrompt>,
409 pub(super) last_search: Option<String>,
413 pub(super) last_search_forward: bool,
417 pub(super) jump_back: Vec<(usize, usize)>,
422 pub(super) jump_fwd: Vec<(usize, usize)>,
425 pub(super) insert_pending_register: bool,
429 pub(super) change_mark_start: Option<(usize, usize)>,
435 pub(super) search_history: Vec<String>,
439 pub(super) search_history_cursor: Option<usize>,
444 pub(super) last_input_at: Option<std::time::Instant>,
453 pub(super) last_input_host_at: Option<core::time::Duration>,
457}
458
459const SEARCH_HISTORY_MAX: usize = 100;
460pub(crate) const CHANGE_LIST_MAX: usize = 100;
461
462#[derive(Debug, Clone)]
465pub struct SearchPrompt {
466 pub text: String,
467 pub cursor: usize,
468 pub forward: bool,
469}
470
471#[derive(Debug, Clone)]
472struct InsertSession {
473 count: usize,
474 row_min: usize,
476 row_max: usize,
477 before_lines: Vec<String>,
481 reason: InsertReason,
482}
483
484#[derive(Debug, Clone)]
485enum InsertReason {
486 Enter(InsertEntry),
488 Open { above: bool },
490 AfterChange,
493 DeleteToEol,
495 ReplayOnly,
498 BlockEdge { top: usize, bot: usize, col: usize },
502 Replace,
506}
507
508#[derive(Debug, Clone, Copy)]
518pub(super) struct LastVisual {
519 pub mode: Mode,
520 pub anchor: (usize, usize),
521 pub cursor: (usize, usize),
522 pub block_vcol: usize,
523}
524
525impl VimState {
526 pub fn public_mode(&self) -> VimMode {
527 match self.mode {
528 Mode::Normal => VimMode::Normal,
529 Mode::Insert => VimMode::Insert,
530 Mode::Visual => VimMode::Visual,
531 Mode::VisualLine => VimMode::VisualLine,
532 Mode::VisualBlock => VimMode::VisualBlock,
533 }
534 }
535
536 pub fn force_normal(&mut self) {
537 self.mode = Mode::Normal;
538 self.pending = Pending::None;
539 self.count = 0;
540 self.insert_session = None;
541 }
542
543 pub(crate) fn clear_pending_prefix(&mut self) {
553 self.pending = Pending::None;
554 self.count = 0;
555 self.pending_register = None;
556 self.insert_pending_register = false;
557 }
558
559 pub fn is_visual(&self) -> bool {
560 matches!(
561 self.mode,
562 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
563 )
564 }
565
566 pub fn is_visual_char(&self) -> bool {
567 self.mode == Mode::Visual
568 }
569
570 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
571 self.visual_anchor = anchor;
572 self.mode = Mode::Visual;
573 }
574
575 pub(crate) fn pending_count_val(&self) -> Option<u32> {
578 if self.count == 0 {
579 None
580 } else {
581 Some(self.count as u32)
582 }
583 }
584
585 pub(crate) fn is_chord_pending(&self) -> bool {
588 !matches!(self.pending, Pending::None)
589 }
590
591 pub(crate) fn pending_op_char(&self) -> Option<char> {
595 let op = match &self.pending {
596 Pending::Op { op, .. }
597 | Pending::OpTextObj { op, .. }
598 | Pending::OpG { op, .. }
599 | Pending::OpFind { op, .. } => Some(*op),
600 _ => None,
601 };
602 op.map(|o| match o {
603 Operator::Delete => 'd',
604 Operator::Change => 'c',
605 Operator::Yank => 'y',
606 Operator::Uppercase => 'U',
607 Operator::Lowercase => 'u',
608 Operator::ToggleCase => '~',
609 Operator::Indent => '>',
610 Operator::Outdent => '<',
611 Operator::Fold => 'z',
612 Operator::Reflow => 'q',
613 })
614 }
615}
616
617fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
623 ed.vim.search_prompt = Some(SearchPrompt {
624 text: String::new(),
625 cursor: 0,
626 forward,
627 });
628 ed.vim.search_history_cursor = None;
629 ed.set_search_pattern(None);
633}
634
635fn push_search_pattern<H: crate::types::Host>(
640 ed: &mut Editor<hjkl_buffer::Buffer, H>,
641 pattern: &str,
642) {
643 let compiled = if pattern.is_empty() {
644 None
645 } else {
646 let case_insensitive = ed.settings().ignore_case
653 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
654 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
655 std::borrow::Cow::Owned(format!("(?i){pattern}"))
656 } else {
657 std::borrow::Cow::Borrowed(pattern)
658 };
659 regex::Regex::new(&effective).ok()
660 };
661 let wrap = ed.settings().wrapscan;
662 ed.set_search_pattern(compiled);
666 ed.search_state_mut().wrap_around = wrap;
667}
668
669fn step_search_prompt<H: crate::types::Host>(
670 ed: &mut Editor<hjkl_buffer::Buffer, H>,
671 input: Input,
672) -> bool {
673 let history_dir = match (input.key, input.ctrl) {
677 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
678 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
679 _ => None,
680 };
681 if let Some(dir) = history_dir {
682 walk_search_history(ed, dir);
683 return true;
684 }
685 match input.key {
686 Key::Esc => {
687 let text = ed
690 .vim
691 .search_prompt
692 .take()
693 .map(|p| p.text)
694 .unwrap_or_default();
695 if !text.is_empty() {
696 ed.vim.last_search = Some(text);
697 }
698 ed.vim.search_history_cursor = None;
699 }
700 Key::Enter => {
701 let prompt = ed.vim.search_prompt.take();
702 if let Some(p) = prompt {
703 let pattern = if p.text.is_empty() {
706 ed.vim.last_search.clone()
707 } else {
708 Some(p.text.clone())
709 };
710 if let Some(pattern) = pattern {
711 push_search_pattern(ed, &pattern);
712 let pre = ed.cursor();
713 if p.forward {
714 ed.search_advance_forward(true);
715 } else {
716 ed.search_advance_backward(true);
717 }
718 ed.push_buffer_cursor_to_textarea();
719 if ed.cursor() != pre {
720 push_jump(ed, pre);
721 }
722 record_search_history(ed, &pattern);
723 ed.vim.last_search = Some(pattern);
724 ed.vim.last_search_forward = p.forward;
725 }
726 }
727 ed.vim.search_history_cursor = None;
728 }
729 Key::Backspace => {
730 ed.vim.search_history_cursor = None;
731 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
732 if p.text.pop().is_some() {
733 p.cursor = p.text.chars().count();
734 Some(p.text.clone())
735 } else {
736 None
737 }
738 });
739 if let Some(text) = new_text {
740 push_search_pattern(ed, &text);
741 }
742 }
743 Key::Char(c) => {
744 ed.vim.search_history_cursor = None;
745 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
746 p.text.push(c);
747 p.cursor = p.text.chars().count();
748 p.text.clone()
749 });
750 if let Some(text) = new_text {
751 push_search_pattern(ed, &text);
752 }
753 }
754 _ => {}
755 }
756 true
757}
758
759fn walk_change_list<H: crate::types::Host>(
763 ed: &mut Editor<hjkl_buffer::Buffer, H>,
764 dir: isize,
765 count: usize,
766) {
767 if ed.vim.change_list.is_empty() {
768 return;
769 }
770 let len = ed.vim.change_list.len();
771 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
772 (None, -1) => len as isize - 1,
773 (None, 1) => return, (Some(i), -1) => i as isize - 1,
775 (Some(i), 1) => i as isize + 1,
776 _ => return,
777 };
778 for _ in 1..count {
779 let next = idx + dir;
780 if next < 0 || next >= len as isize {
781 break;
782 }
783 idx = next;
784 }
785 if idx < 0 || idx >= len as isize {
786 return;
787 }
788 let idx = idx as usize;
789 ed.vim.change_list_cursor = Some(idx);
790 let (row, col) = ed.vim.change_list[idx];
791 ed.jump_cursor(row, col);
792}
793
794fn record_search_history<H: crate::types::Host>(
798 ed: &mut Editor<hjkl_buffer::Buffer, H>,
799 pattern: &str,
800) {
801 if pattern.is_empty() {
802 return;
803 }
804 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
805 return;
806 }
807 ed.vim.search_history.push(pattern.to_string());
808 let len = ed.vim.search_history.len();
809 if len > SEARCH_HISTORY_MAX {
810 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
811 }
812}
813
814fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
820 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
821 return;
822 }
823 let len = ed.vim.search_history.len();
824 let next_idx = match (ed.vim.search_history_cursor, dir) {
825 (None, -1) => Some(len - 1),
826 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
828 (Some(i), 1) if i + 1 < len => Some(i + 1),
829 _ => None,
830 };
831 let Some(idx) = next_idx else {
832 return;
833 };
834 ed.vim.search_history_cursor = Some(idx);
835 let text = ed.vim.search_history[idx].clone();
836 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
837 prompt.cursor = text.chars().count();
838 prompt.text = text.clone();
839 }
840 push_search_pattern(ed, &text);
841}
842
843pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
844 ed.sync_buffer_content_from_textarea();
849 let now = std::time::Instant::now();
857 let host_now = ed.host.now();
858 let timed_out = match ed.vim.last_input_host_at {
859 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
860 None => false,
861 };
862 if timed_out {
863 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
864 || ed.vim.count != 0
865 || ed.vim.pending_register.is_some()
866 || ed.vim.insert_pending_register;
867 if chord_in_flight {
868 ed.vim.clear_pending_prefix();
869 }
870 }
871 ed.vim.last_input_at = Some(now);
872 ed.vim.last_input_host_at = Some(host_now);
873 if ed.vim.recording_macro.is_some()
878 && !ed.vim.replaying_macro
879 && matches!(ed.vim.pending, Pending::None)
880 && ed.vim.mode != Mode::Insert
881 && input.key == Key::Char('q')
882 && !input.ctrl
883 && !input.alt
884 {
885 let reg = ed.vim.recording_macro.take().unwrap();
886 let keys = std::mem::take(&mut ed.vim.recording_keys);
887 let text = crate::input::encode_macro(&keys);
888 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
889 return true;
890 }
891 if ed.vim.search_prompt.is_some() {
893 return step_search_prompt(ed, input);
894 }
895 let pending_was_macro_chord = matches!(
899 ed.vim.pending,
900 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
901 );
902 let was_insert = ed.vim.mode == Mode::Insert;
903 let pre_visual_snapshot = match ed.vim.mode {
906 Mode::Visual => Some(LastVisual {
907 mode: Mode::Visual,
908 anchor: ed.vim.visual_anchor,
909 cursor: ed.cursor(),
910 block_vcol: 0,
911 }),
912 Mode::VisualLine => Some(LastVisual {
913 mode: Mode::VisualLine,
914 anchor: (ed.vim.visual_line_anchor, 0),
915 cursor: ed.cursor(),
916 block_vcol: 0,
917 }),
918 Mode::VisualBlock => Some(LastVisual {
919 mode: Mode::VisualBlock,
920 anchor: ed.vim.block_anchor,
921 cursor: ed.cursor(),
922 block_vcol: ed.vim.block_vcol,
923 }),
924 _ => None,
925 };
926 let consumed = match ed.vim.mode {
927 Mode::Insert => step_insert(ed, input),
928 _ => step_normal(ed, input),
929 };
930 if let Some(snap) = pre_visual_snapshot
931 && !matches!(
932 ed.vim.mode,
933 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
934 )
935 {
936 let (lo, hi) = match snap.mode {
952 Mode::Visual => {
953 if snap.anchor <= snap.cursor {
954 (snap.anchor, snap.cursor)
955 } else {
956 (snap.cursor, snap.anchor)
957 }
958 }
959 Mode::VisualLine => {
960 let r_lo = snap.anchor.0.min(snap.cursor.0);
961 let r_hi = snap.anchor.0.max(snap.cursor.0);
962 let last_col = ed
963 .buffer()
964 .lines()
965 .get(r_hi)
966 .map(|l| l.chars().count().saturating_sub(1))
967 .unwrap_or(0);
968 ((r_lo, 0), (r_hi, last_col))
969 }
970 Mode::VisualBlock => {
971 let (r1, c1) = snap.anchor;
972 let (r2, c2) = snap.cursor;
973 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
974 }
975 _ => {
976 if snap.anchor <= snap.cursor {
979 (snap.anchor, snap.cursor)
980 } else {
981 (snap.cursor, snap.anchor)
982 }
983 }
984 };
985 ed.set_mark('<', lo);
986 ed.set_mark('>', hi);
987 ed.vim.last_visual = Some(snap);
988 }
989 if !was_insert
993 && ed.vim.one_shot_normal
994 && ed.vim.mode == Mode::Normal
995 && matches!(ed.vim.pending, Pending::None)
996 {
997 ed.vim.one_shot_normal = false;
998 ed.vim.mode = Mode::Insert;
999 }
1000 ed.sync_buffer_content_from_textarea();
1006 if !ed.vim.viewport_pinned {
1010 ed.ensure_cursor_in_scrolloff();
1011 }
1012 ed.vim.viewport_pinned = false;
1013 if ed.vim.recording_macro.is_some()
1018 && !ed.vim.replaying_macro
1019 && input.key != Key::Char('q')
1020 && !pending_was_macro_chord
1021 {
1022 ed.vim.recording_keys.push(input);
1023 }
1024 consumed
1025}
1026
1027fn step_insert<H: crate::types::Host>(
1030 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1031 input: Input,
1032) -> bool {
1033 if ed.vim.insert_pending_register {
1037 ed.vim.insert_pending_register = false;
1038 if let Key::Char(c) = input.key
1039 && !input.ctrl
1040 {
1041 insert_register_text(ed, c);
1042 }
1043 return true;
1044 }
1045
1046 if input.key == Key::Esc {
1047 finish_insert_session(ed);
1048 ed.vim.mode = Mode::Normal;
1049 let col = ed.cursor().1;
1054 if col > 0 {
1055 crate::motions::move_left(&mut ed.buffer, 1);
1056 ed.push_buffer_cursor_to_textarea();
1057 }
1058 ed.sticky_col = Some(ed.cursor().1);
1059 return true;
1060 }
1061
1062 if input.ctrl {
1064 match input.key {
1065 Key::Char('w') => {
1066 use hjkl_buffer::{Edit, MotionKind};
1067 ed.sync_buffer_content_from_textarea();
1068 let cursor = buf_cursor_pos(&ed.buffer);
1069 if cursor.row == 0 && cursor.col == 0 {
1070 return true;
1071 }
1072 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1075 let word_start = buf_cursor_pos(&ed.buffer);
1076 if word_start == cursor {
1077 return true;
1078 }
1079 buf_set_cursor_pos(&mut ed.buffer, cursor);
1080 ed.mutate_edit(Edit::DeleteRange {
1081 start: word_start,
1082 end: cursor,
1083 kind: MotionKind::Char,
1084 });
1085 ed.push_buffer_cursor_to_textarea();
1086 return true;
1087 }
1088 Key::Char('u') => {
1089 use hjkl_buffer::{Edit, MotionKind, Position};
1090 ed.sync_buffer_content_from_textarea();
1091 let cursor = buf_cursor_pos(&ed.buffer);
1092 if cursor.col > 0 {
1093 ed.mutate_edit(Edit::DeleteRange {
1094 start: Position::new(cursor.row, 0),
1095 end: cursor,
1096 kind: MotionKind::Char,
1097 });
1098 ed.push_buffer_cursor_to_textarea();
1099 }
1100 return true;
1101 }
1102 Key::Char('h') => {
1103 use hjkl_buffer::{Edit, MotionKind, Position};
1104 ed.sync_buffer_content_from_textarea();
1105 let cursor = buf_cursor_pos(&ed.buffer);
1106 if cursor.col > 0 {
1107 ed.mutate_edit(Edit::DeleteRange {
1108 start: Position::new(cursor.row, cursor.col - 1),
1109 end: cursor,
1110 kind: MotionKind::Char,
1111 });
1112 } else if cursor.row > 0 {
1113 let prev_row = cursor.row - 1;
1114 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1115 ed.mutate_edit(Edit::JoinLines {
1116 row: prev_row,
1117 count: 1,
1118 with_space: false,
1119 });
1120 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1121 }
1122 ed.push_buffer_cursor_to_textarea();
1123 return true;
1124 }
1125 Key::Char('o') => {
1126 ed.vim.one_shot_normal = true;
1129 ed.vim.mode = Mode::Normal;
1130 return true;
1131 }
1132 Key::Char('r') => {
1133 ed.vim.insert_pending_register = true;
1136 return true;
1137 }
1138 Key::Char('t') => {
1139 let (row, col) = ed.cursor();
1144 let sw = ed.settings().shiftwidth;
1145 indent_rows(ed, row, row, 1);
1146 ed.jump_cursor(row, col + sw);
1147 return true;
1148 }
1149 Key::Char('d') => {
1150 let (row, col) = ed.cursor();
1154 let before_len = buf_line_bytes(&ed.buffer, row);
1155 outdent_rows(ed, row, row, 1);
1156 let after_len = buf_line_bytes(&ed.buffer, row);
1157 let stripped = before_len.saturating_sub(after_len);
1158 let new_col = col.saturating_sub(stripped);
1159 ed.jump_cursor(row, new_col);
1160 return true;
1161 }
1162 _ => {}
1163 }
1164 }
1165
1166 let (row, _) = ed.cursor();
1169 if let Some(ref mut session) = ed.vim.insert_session {
1170 session.row_min = session.row_min.min(row);
1171 session.row_max = session.row_max.max(row);
1172 }
1173 let mutated = handle_insert_key(ed, input);
1174 if mutated {
1175 ed.mark_content_dirty();
1176 let (row, _) = ed.cursor();
1177 if let Some(ref mut session) = ed.vim.insert_session {
1178 session.row_min = session.row_min.min(row);
1179 session.row_max = session.row_max.max(row);
1180 }
1181 }
1182 true
1183}
1184
1185fn insert_register_text<H: crate::types::Host>(
1190 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1191 selector: char,
1192) {
1193 use hjkl_buffer::Edit;
1194 let text = match ed.registers().read(selector) {
1195 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1196 _ => return,
1197 };
1198 ed.sync_buffer_content_from_textarea();
1199 let cursor = buf_cursor_pos(&ed.buffer);
1200 ed.mutate_edit(Edit::InsertStr {
1201 at: cursor,
1202 text: text.clone(),
1203 });
1204 let mut row = cursor.row;
1207 let mut col = cursor.col;
1208 for ch in text.chars() {
1209 if ch == '\n' {
1210 row += 1;
1211 col = 0;
1212 } else {
1213 col += 1;
1214 }
1215 }
1216 buf_set_cursor_rc(&mut ed.buffer, row, col);
1217 ed.push_buffer_cursor_to_textarea();
1218 ed.mark_content_dirty();
1219 if let Some(ref mut session) = ed.vim.insert_session {
1220 session.row_min = session.row_min.min(row);
1221 session.row_max = session.row_max.max(row);
1222 }
1223}
1224
1225pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1244 if !settings.autoindent {
1245 return String::new();
1246 }
1247 let base: String = prev_line
1249 .chars()
1250 .take_while(|c| *c == ' ' || *c == '\t')
1251 .collect();
1252
1253 if settings.smartindent {
1254 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1258 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1259 let unit = if settings.expandtab {
1260 if settings.softtabstop > 0 {
1261 " ".repeat(settings.softtabstop)
1262 } else {
1263 " ".repeat(settings.shiftwidth)
1264 }
1265 } else {
1266 "\t".to_string()
1267 };
1268 return format!("{base}{unit}");
1269 }
1270 }
1271
1272 base
1273}
1274
1275fn try_dedent_close_bracket<H: crate::types::Host>(
1285 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1286 cursor: hjkl_buffer::Position,
1287 ch: char,
1288) -> bool {
1289 use hjkl_buffer::{Edit, MotionKind, Position};
1290
1291 if !ed.settings.smartindent {
1292 return false;
1293 }
1294 if !matches!(ch, '}' | ')' | ']') {
1295 return false;
1296 }
1297
1298 let line = match buf_line(&ed.buffer, cursor.row) {
1299 Some(l) => l.to_string(),
1300 None => return false,
1301 };
1302
1303 let before: String = line.chars().take(cursor.col).collect();
1305 if !before.chars().all(|c| c == ' ' || c == '\t') {
1306 return false;
1307 }
1308 if before.is_empty() {
1309 return false;
1311 }
1312
1313 let unit_len: usize = if ed.settings.expandtab {
1315 if ed.settings.softtabstop > 0 {
1316 ed.settings.softtabstop
1317 } else {
1318 ed.settings.shiftwidth
1319 }
1320 } else {
1321 1
1323 };
1324
1325 let strip_len = if ed.settings.expandtab {
1327 let spaces = before.chars().filter(|c| *c == ' ').count();
1329 if spaces < unit_len {
1330 return false;
1331 }
1332 unit_len
1333 } else {
1334 if !before.starts_with('\t') {
1336 return false;
1337 }
1338 1
1339 };
1340
1341 ed.mutate_edit(Edit::DeleteRange {
1343 start: Position::new(cursor.row, 0),
1344 end: Position::new(cursor.row, strip_len),
1345 kind: MotionKind::Char,
1346 });
1347 let new_col = cursor.col.saturating_sub(strip_len);
1352 ed.mutate_edit(Edit::InsertChar {
1353 at: Position::new(cursor.row, new_col),
1354 ch,
1355 });
1356 true
1357}
1358
1359fn handle_insert_key<H: crate::types::Host>(
1366 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1367 input: Input,
1368) -> bool {
1369 use hjkl_buffer::{Edit, MotionKind, Position};
1370 ed.sync_buffer_content_from_textarea();
1371 let cursor = buf_cursor_pos(&ed.buffer);
1372 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1373 let in_replace = matches!(
1377 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1378 Some(InsertReason::Replace)
1379 );
1380 let mutated = match input.key {
1381 Key::Char(c) if in_replace && cursor.col < line_chars => {
1382 ed.mutate_edit(Edit::DeleteRange {
1383 start: cursor,
1384 end: Position::new(cursor.row, cursor.col + 1),
1385 kind: MotionKind::Char,
1386 });
1387 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1388 true
1389 }
1390 Key::Char(c) => {
1391 if !try_dedent_close_bracket(ed, cursor, c) {
1392 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1393 }
1394 true
1395 }
1396 Key::Enter => {
1397 let prev_line = buf_line(&ed.buffer, cursor.row)
1398 .unwrap_or_default()
1399 .to_string();
1400 let indent = compute_enter_indent(&ed.settings, &prev_line);
1401 let text = format!("\n{indent}");
1402 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1403 true
1404 }
1405 Key::Tab => {
1406 if ed.settings.expandtab {
1407 let sts = ed.settings.softtabstop;
1410 let n = if sts > 0 {
1411 sts - (cursor.col % sts)
1412 } else {
1413 ed.settings.tabstop.max(1)
1414 };
1415 ed.mutate_edit(Edit::InsertStr {
1416 at: cursor,
1417 text: " ".repeat(n),
1418 });
1419 } else {
1420 ed.mutate_edit(Edit::InsertChar {
1421 at: cursor,
1422 ch: '\t',
1423 });
1424 }
1425 true
1426 }
1427 Key::Backspace => {
1428 let sts = ed.settings.softtabstop;
1432 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1433 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1434 let chars: Vec<char> = line.chars().collect();
1435 let run_start = cursor.col - sts;
1436 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1437 ed.mutate_edit(Edit::DeleteRange {
1438 start: Position::new(cursor.row, run_start),
1439 end: cursor,
1440 kind: MotionKind::Char,
1441 });
1442 return true;
1443 }
1444 }
1445 if cursor.col > 0 {
1446 ed.mutate_edit(Edit::DeleteRange {
1447 start: Position::new(cursor.row, cursor.col - 1),
1448 end: cursor,
1449 kind: MotionKind::Char,
1450 });
1451 true
1452 } else if cursor.row > 0 {
1453 let prev_row = cursor.row - 1;
1454 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1455 ed.mutate_edit(Edit::JoinLines {
1456 row: prev_row,
1457 count: 1,
1458 with_space: false,
1459 });
1460 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1461 true
1462 } else {
1463 false
1464 }
1465 }
1466 Key::Delete => {
1467 if cursor.col < line_chars {
1468 ed.mutate_edit(Edit::DeleteRange {
1469 start: cursor,
1470 end: Position::new(cursor.row, cursor.col + 1),
1471 kind: MotionKind::Char,
1472 });
1473 true
1474 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1475 ed.mutate_edit(Edit::JoinLines {
1476 row: cursor.row,
1477 count: 1,
1478 with_space: false,
1479 });
1480 buf_set_cursor_pos(&mut ed.buffer, cursor);
1481 true
1482 } else {
1483 false
1484 }
1485 }
1486 Key::Left => {
1487 crate::motions::move_left(&mut ed.buffer, 1);
1488 break_undo_group_in_insert(ed);
1489 false
1490 }
1491 Key::Right => {
1492 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1495 break_undo_group_in_insert(ed);
1496 false
1497 }
1498 Key::Up => {
1499 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1500 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1501 break_undo_group_in_insert(ed);
1502 false
1503 }
1504 Key::Down => {
1505 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1506 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1507 break_undo_group_in_insert(ed);
1508 false
1509 }
1510 Key::Home => {
1511 crate::motions::move_line_start(&mut ed.buffer);
1512 break_undo_group_in_insert(ed);
1513 false
1514 }
1515 Key::End => {
1516 crate::motions::move_line_end(&mut ed.buffer);
1517 break_undo_group_in_insert(ed);
1518 false
1519 }
1520 Key::PageUp => {
1521 let rows = viewport_full_rows(ed, 1) as isize;
1525 scroll_cursor_rows(ed, -rows);
1526 return false;
1527 }
1528 Key::PageDown => {
1529 let rows = viewport_full_rows(ed, 1) as isize;
1530 scroll_cursor_rows(ed, rows);
1531 return false;
1532 }
1533 _ => false,
1536 };
1537 ed.push_buffer_cursor_to_textarea();
1538 mutated
1539}
1540
1541fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1542 let Some(session) = ed.vim.insert_session.take() else {
1543 return;
1544 };
1545 let lines = buf_lines_to_vec(&ed.buffer);
1546 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1550 let before_end = session
1551 .row_max
1552 .min(session.before_lines.len().saturating_sub(1));
1553 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1554 session.before_lines[session.row_min..=before_end].join("\n")
1555 } else {
1556 String::new()
1557 };
1558 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1559 lines[session.row_min..=after_end].join("\n")
1560 } else {
1561 String::new()
1562 };
1563 let inserted = extract_inserted(&before, &after);
1564 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1565 use hjkl_buffer::{Edit, Position};
1566 for _ in 0..session.count - 1 {
1567 let (row, col) = ed.cursor();
1568 ed.mutate_edit(Edit::InsertStr {
1569 at: Position::new(row, col),
1570 text: inserted.clone(),
1571 });
1572 }
1573 }
1574 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1575 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1576 use hjkl_buffer::{Edit, Position};
1577 for r in (top + 1)..=bot {
1578 let line_len = buf_line_chars(&ed.buffer, r);
1579 if col > line_len {
1580 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1583 ed.mutate_edit(Edit::InsertStr {
1584 at: Position::new(r, line_len),
1585 text: pad,
1586 });
1587 }
1588 ed.mutate_edit(Edit::InsertStr {
1589 at: Position::new(r, col),
1590 text: inserted.clone(),
1591 });
1592 }
1593 buf_set_cursor_rc(&mut ed.buffer, top, col);
1594 ed.push_buffer_cursor_to_textarea();
1595 }
1596 return;
1597 }
1598 if ed.vim.replaying {
1599 return;
1600 }
1601 match session.reason {
1602 InsertReason::Enter(entry) => {
1603 ed.vim.last_change = Some(LastChange::InsertAt {
1604 entry,
1605 inserted,
1606 count: session.count,
1607 });
1608 }
1609 InsertReason::Open { above } => {
1610 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1611 }
1612 InsertReason::AfterChange => {
1613 if let Some(
1614 LastChange::OpMotion { inserted: ins, .. }
1615 | LastChange::OpTextObj { inserted: ins, .. }
1616 | LastChange::LineOp { inserted: ins, .. },
1617 ) = ed.vim.last_change.as_mut()
1618 {
1619 *ins = Some(inserted);
1620 }
1621 if let Some(start) = ed.vim.change_mark_start.take() {
1627 let end = ed.cursor();
1628 ed.set_mark('[', start);
1629 ed.set_mark(']', end);
1630 }
1631 }
1632 InsertReason::DeleteToEol => {
1633 ed.vim.last_change = Some(LastChange::DeleteToEol {
1634 inserted: Some(inserted),
1635 });
1636 }
1637 InsertReason::ReplayOnly => {}
1638 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1639 InsertReason::Replace => {
1640 ed.vim.last_change = Some(LastChange::DeleteToEol {
1645 inserted: Some(inserted),
1646 });
1647 }
1648 }
1649}
1650
1651fn begin_insert<H: crate::types::Host>(
1652 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1653 count: usize,
1654 reason: InsertReason,
1655) {
1656 let record = !matches!(reason, InsertReason::ReplayOnly);
1657 if record {
1658 ed.push_undo();
1659 }
1660 let reason = if ed.vim.replaying {
1661 InsertReason::ReplayOnly
1662 } else {
1663 reason
1664 };
1665 let (row, _) = ed.cursor();
1666 ed.vim.insert_session = Some(InsertSession {
1667 count,
1668 row_min: row,
1669 row_max: row,
1670 before_lines: buf_lines_to_vec(&ed.buffer),
1671 reason,
1672 });
1673 ed.vim.mode = Mode::Insert;
1674}
1675
1676pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1691 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1692) {
1693 if !ed.settings.undo_break_on_motion {
1694 return;
1695 }
1696 if ed.vim.replaying {
1697 return;
1698 }
1699 if ed.vim.insert_session.is_none() {
1700 return;
1701 }
1702 ed.push_undo();
1703 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1704 let mut lines: Vec<String> = Vec::with_capacity(n);
1705 for r in 0..n {
1706 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1707 }
1708 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1709 if let Some(ref mut session) = ed.vim.insert_session {
1710 session.before_lines = lines;
1711 session.row_min = row;
1712 session.row_max = row;
1713 }
1714}
1715
1716fn step_normal<H: crate::types::Host>(
1719 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1720 input: Input,
1721) -> bool {
1722 if let Key::Char(d @ '0'..='9') = input.key
1724 && !input.ctrl
1725 && !input.alt
1726 && !matches!(
1727 ed.vim.pending,
1728 Pending::Replace
1729 | Pending::Find { .. }
1730 | Pending::OpFind { .. }
1731 | Pending::VisualTextObj { .. }
1732 )
1733 && (d != '0' || ed.vim.count > 0)
1734 {
1735 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1736 return true;
1737 }
1738
1739 match std::mem::take(&mut ed.vim.pending) {
1741 Pending::Replace => return handle_replace(ed, input),
1742 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1743 Pending::OpFind {
1744 op,
1745 count1,
1746 forward,
1747 till,
1748 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1749 Pending::G => return handle_after_g(ed, input),
1750 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1751 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1752 Pending::OpTextObj { op, count1, inner } => {
1753 return handle_text_object(ed, input, op, count1, inner);
1754 }
1755 Pending::VisualTextObj { inner } => {
1756 return handle_visual_text_obj(ed, input, inner);
1757 }
1758 Pending::Z => return handle_after_z(ed, input),
1759 Pending::SetMark => return handle_set_mark(ed, input),
1760 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1761 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1762 Pending::SelectRegister => return handle_select_register(ed, input),
1763 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1764 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1765 Pending::None => {}
1766 }
1767
1768 let count = take_count(&mut ed.vim);
1769
1770 match input.key {
1772 Key::Esc => {
1773 ed.vim.force_normal();
1774 return true;
1775 }
1776 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1777 ed.vim.visual_anchor = ed.cursor();
1778 ed.vim.mode = Mode::Visual;
1779 return true;
1780 }
1781 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1782 let (row, _) = ed.cursor();
1783 ed.vim.visual_line_anchor = row;
1784 ed.vim.mode = Mode::VisualLine;
1785 return true;
1786 }
1787 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1788 ed.vim.visual_anchor = ed.cursor();
1789 ed.vim.mode = Mode::Visual;
1790 return true;
1791 }
1792 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1793 let (row, _) = ed.cursor();
1794 ed.vim.visual_line_anchor = row;
1795 ed.vim.mode = Mode::VisualLine;
1796 return true;
1797 }
1798 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1799 let cur = ed.cursor();
1800 ed.vim.block_anchor = cur;
1801 ed.vim.block_vcol = cur.1;
1802 ed.vim.mode = Mode::VisualBlock;
1803 return true;
1804 }
1805 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1806 ed.vim.mode = Mode::Normal;
1808 return true;
1809 }
1810 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1813 Mode::Visual => {
1814 let cur = ed.cursor();
1815 let anchor = ed.vim.visual_anchor;
1816 ed.vim.visual_anchor = cur;
1817 ed.jump_cursor(anchor.0, anchor.1);
1818 return true;
1819 }
1820 Mode::VisualLine => {
1821 let cur_row = ed.cursor().0;
1822 let anchor_row = ed.vim.visual_line_anchor;
1823 ed.vim.visual_line_anchor = cur_row;
1824 ed.jump_cursor(anchor_row, 0);
1825 return true;
1826 }
1827 Mode::VisualBlock => {
1828 let cur = ed.cursor();
1829 let anchor = ed.vim.block_anchor;
1830 ed.vim.block_anchor = cur;
1831 ed.vim.block_vcol = anchor.1;
1832 ed.jump_cursor(anchor.0, anchor.1);
1833 return true;
1834 }
1835 _ => {}
1836 },
1837 _ => {}
1838 }
1839
1840 if ed.vim.is_visual()
1842 && let Some(op) = visual_operator(&input)
1843 {
1844 apply_visual_operator(ed, op);
1845 return true;
1846 }
1847
1848 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1852 match input.key {
1853 Key::Char('r') => {
1854 ed.vim.pending = Pending::Replace;
1855 return true;
1856 }
1857 Key::Char('I') => {
1858 let (top, bot, left, _right) = block_bounds(ed);
1859 ed.jump_cursor(top, left);
1860 ed.vim.mode = Mode::Normal;
1861 begin_insert(
1862 ed,
1863 1,
1864 InsertReason::BlockEdge {
1865 top,
1866 bot,
1867 col: left,
1868 },
1869 );
1870 return true;
1871 }
1872 Key::Char('A') => {
1873 let (top, bot, _left, right) = block_bounds(ed);
1874 let line_len = buf_line_chars(&ed.buffer, top);
1875 let col = (right + 1).min(line_len);
1876 ed.jump_cursor(top, col);
1877 ed.vim.mode = Mode::Normal;
1878 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1879 return true;
1880 }
1881 _ => {}
1882 }
1883 }
1884
1885 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1887 && !input.ctrl
1888 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1889 {
1890 let inner = matches!(input.key, Key::Char('i'));
1891 ed.vim.pending = Pending::VisualTextObj { inner };
1892 return true;
1893 }
1894
1895 if input.ctrl
1900 && let Key::Char(c) = input.key
1901 {
1902 match c {
1903 'd' => {
1904 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1905 return true;
1906 }
1907 'u' => {
1908 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1909 return true;
1910 }
1911 'f' => {
1912 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1913 return true;
1914 }
1915 'b' => {
1916 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1917 return true;
1918 }
1919 'r' => {
1920 do_redo(ed);
1921 return true;
1922 }
1923 'a' if ed.vim.mode == Mode::Normal => {
1924 adjust_number(ed, count.max(1) as i64);
1925 return true;
1926 }
1927 'x' if ed.vim.mode == Mode::Normal => {
1928 adjust_number(ed, -(count.max(1) as i64));
1929 return true;
1930 }
1931 'o' if ed.vim.mode == Mode::Normal => {
1932 for _ in 0..count.max(1) {
1933 jump_back(ed);
1934 }
1935 return true;
1936 }
1937 'i' if ed.vim.mode == Mode::Normal => {
1938 for _ in 0..count.max(1) {
1939 jump_forward(ed);
1940 }
1941 return true;
1942 }
1943 _ => {}
1944 }
1945 }
1946
1947 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1949 for _ in 0..count.max(1) {
1950 jump_forward(ed);
1951 }
1952 return true;
1953 }
1954
1955 if let Some(motion) = parse_motion(&input) {
1957 execute_motion(ed, motion.clone(), count);
1958 if ed.vim.mode == Mode::VisualBlock {
1960 update_block_vcol(ed, &motion);
1961 }
1962 if let Motion::Find { ch, forward, till } = motion {
1963 ed.vim.last_find = Some((ch, forward, till));
1964 }
1965 return true;
1966 }
1967
1968 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
1970 return true;
1971 }
1972
1973 if ed.vim.mode == Mode::Normal
1975 && let Key::Char(op_ch) = input.key
1976 && !input.ctrl
1977 && let Some(op) = char_to_operator(op_ch)
1978 {
1979 ed.vim.pending = Pending::Op { op, count1: count };
1980 return true;
1981 }
1982
1983 if ed.vim.mode == Mode::Normal
1985 && let Some((forward, till)) = find_entry(&input)
1986 {
1987 ed.vim.count = count;
1988 ed.vim.pending = Pending::Find { forward, till };
1989 return true;
1990 }
1991
1992 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
1994 ed.vim.count = count;
1995 ed.vim.pending = Pending::G;
1996 return true;
1997 }
1998
1999 if !input.ctrl
2001 && input.key == Key::Char('z')
2002 && matches!(
2003 ed.vim.mode,
2004 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2005 )
2006 {
2007 ed.vim.pending = Pending::Z;
2008 return true;
2009 }
2010
2011 if !input.ctrl
2017 && matches!(
2018 ed.vim.mode,
2019 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2020 )
2021 && input.key == Key::Char('`')
2022 {
2023 ed.vim.pending = Pending::GotoMarkChar;
2024 return true;
2025 }
2026 if !input.ctrl && ed.vim.mode == Mode::Normal {
2027 match input.key {
2028 Key::Char('m') => {
2029 ed.vim.pending = Pending::SetMark;
2030 return true;
2031 }
2032 Key::Char('\'') => {
2033 ed.vim.pending = Pending::GotoMarkLine;
2034 return true;
2035 }
2036 Key::Char('`') => {
2037 ed.vim.pending = Pending::GotoMarkChar;
2039 return true;
2040 }
2041 Key::Char('"') => {
2042 ed.vim.pending = Pending::SelectRegister;
2045 return true;
2046 }
2047 Key::Char('@') => {
2048 ed.vim.pending = Pending::PlayMacroTarget { count };
2052 return true;
2053 }
2054 Key::Char('q') if ed.vim.recording_macro.is_none() => {
2055 ed.vim.pending = Pending::RecordMacroTarget;
2060 return true;
2061 }
2062 _ => {}
2063 }
2064 }
2065
2066 true
2068}
2069
2070fn handle_set_mark<H: crate::types::Host>(
2071 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2072 input: Input,
2073) -> bool {
2074 if let Key::Char(c) = input.key
2075 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
2076 {
2077 let pos = ed.cursor();
2082 ed.set_mark(c, pos);
2083 }
2084 true
2085}
2086
2087fn handle_select_register<H: crate::types::Host>(
2091 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2092 input: Input,
2093) -> bool {
2094 if let Key::Char(c) = input.key
2095 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*'))
2096 {
2097 ed.vim.pending_register = Some(c);
2098 }
2099 true
2100}
2101
2102fn handle_record_macro_target<H: crate::types::Host>(
2107 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2108 input: Input,
2109) -> bool {
2110 if let Key::Char(c) = input.key
2111 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2112 {
2113 ed.vim.recording_macro = Some(c);
2114 if c.is_ascii_uppercase() {
2117 let lower = c.to_ascii_lowercase();
2118 let text = ed
2122 .registers()
2123 .read(lower)
2124 .map(|s| s.text.clone())
2125 .unwrap_or_default();
2126 ed.vim.recording_keys = crate::input::decode_macro(&text);
2127 } else {
2128 ed.vim.recording_keys.clear();
2129 }
2130 }
2131 true
2132}
2133
2134fn handle_play_macro_target<H: crate::types::Host>(
2140 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2141 input: Input,
2142 count: usize,
2143) -> bool {
2144 let reg = match input.key {
2145 Key::Char('@') => ed.vim.last_macro,
2146 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2147 Some(c.to_ascii_lowercase())
2148 }
2149 _ => None,
2150 };
2151 let Some(reg) = reg else {
2152 return true;
2153 };
2154 let text = match ed.registers().read(reg) {
2157 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2158 _ => return true,
2159 };
2160 let keys = crate::input::decode_macro(&text);
2161 ed.vim.last_macro = Some(reg);
2162 let times = count.max(1);
2163 let was_replaying = ed.vim.replaying_macro;
2164 ed.vim.replaying_macro = true;
2165 for _ in 0..times {
2166 for k in keys.iter().copied() {
2167 step(ed, k);
2168 }
2169 }
2170 ed.vim.replaying_macro = was_replaying;
2171 true
2172}
2173
2174fn handle_goto_mark<H: crate::types::Host>(
2175 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2176 input: Input,
2177 linewise: bool,
2178) -> bool {
2179 let Key::Char(c) = input.key else {
2180 return true;
2181 };
2182 let target = match c {
2189 'a'..='z' | 'A'..='Z' => ed.mark(c),
2190 '\'' | '`' => ed.vim.jump_back.last().copied(),
2191 '.' => ed.vim.last_edit_pos,
2192 '[' | ']' | '<' | '>' => ed.mark(c),
2197 _ => None,
2198 };
2199 let Some((row, col)) = target else {
2200 return true;
2201 };
2202 let pre = ed.cursor();
2203 let (r, c_clamped) = clamp_pos(ed, (row, col));
2204 if linewise {
2205 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2206 ed.push_buffer_cursor_to_textarea();
2207 move_first_non_whitespace(ed);
2208 } else {
2209 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2210 ed.push_buffer_cursor_to_textarea();
2211 }
2212 if ed.cursor() != pre {
2213 push_jump(ed, pre);
2214 }
2215 ed.sticky_col = Some(ed.cursor().1);
2216 true
2217}
2218
2219fn take_count(vim: &mut VimState) -> usize {
2220 if vim.count > 0 {
2221 let n = vim.count;
2222 vim.count = 0;
2223 n
2224 } else {
2225 1
2226 }
2227}
2228
2229fn char_to_operator(c: char) -> Option<Operator> {
2230 match c {
2231 'd' => Some(Operator::Delete),
2232 'c' => Some(Operator::Change),
2233 'y' => Some(Operator::Yank),
2234 '>' => Some(Operator::Indent),
2235 '<' => Some(Operator::Outdent),
2236 _ => None,
2237 }
2238}
2239
2240fn visual_operator(input: &Input) -> Option<Operator> {
2241 if input.ctrl {
2242 return None;
2243 }
2244 match input.key {
2245 Key::Char('y') => Some(Operator::Yank),
2246 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2247 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2248 Key::Char('U') => Some(Operator::Uppercase),
2250 Key::Char('u') => Some(Operator::Lowercase),
2251 Key::Char('~') => Some(Operator::ToggleCase),
2252 Key::Char('>') => Some(Operator::Indent),
2254 Key::Char('<') => Some(Operator::Outdent),
2255 _ => None,
2256 }
2257}
2258
2259fn find_entry(input: &Input) -> Option<(bool, bool)> {
2260 if input.ctrl {
2261 return None;
2262 }
2263 match input.key {
2264 Key::Char('f') => Some((true, false)),
2265 Key::Char('F') => Some((false, false)),
2266 Key::Char('t') => Some((true, true)),
2267 Key::Char('T') => Some((false, true)),
2268 _ => None,
2269 }
2270}
2271
2272const JUMPLIST_MAX: usize = 100;
2276
2277fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2282 ed.vim.jump_back.push(from);
2283 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2284 ed.vim.jump_back.remove(0);
2285 }
2286 ed.vim.jump_fwd.clear();
2287}
2288
2289fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2292 let Some(target) = ed.vim.jump_back.pop() else {
2293 return;
2294 };
2295 let cur = ed.cursor();
2296 ed.vim.jump_fwd.push(cur);
2297 let (r, c) = clamp_pos(ed, target);
2298 ed.jump_cursor(r, c);
2299 ed.sticky_col = Some(c);
2300}
2301
2302fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2305 let Some(target) = ed.vim.jump_fwd.pop() else {
2306 return;
2307 };
2308 let cur = ed.cursor();
2309 ed.vim.jump_back.push(cur);
2310 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2311 ed.vim.jump_back.remove(0);
2312 }
2313 let (r, c) = clamp_pos(ed, target);
2314 ed.jump_cursor(r, c);
2315 ed.sticky_col = Some(c);
2316}
2317
2318fn clamp_pos<H: crate::types::Host>(
2321 ed: &Editor<hjkl_buffer::Buffer, H>,
2322 pos: (usize, usize),
2323) -> (usize, usize) {
2324 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2325 let r = pos.0.min(last_row);
2326 let line_len = buf_line_chars(&ed.buffer, r);
2327 let c = pos.1.min(line_len.saturating_sub(1));
2328 (r, c)
2329}
2330
2331fn is_big_jump(motion: &Motion) -> bool {
2333 matches!(
2334 motion,
2335 Motion::FileTop
2336 | Motion::FileBottom
2337 | Motion::MatchBracket
2338 | Motion::WordAtCursor { .. }
2339 | Motion::SearchNext { .. }
2340 | Motion::ViewportTop
2341 | Motion::ViewportMiddle
2342 | Motion::ViewportBottom
2343 )
2344}
2345
2346fn viewport_half_rows<H: crate::types::Host>(
2351 ed: &Editor<hjkl_buffer::Buffer, H>,
2352 count: usize,
2353) -> usize {
2354 let h = ed.viewport_height_value() as usize;
2355 (h / 2).max(1).saturating_mul(count.max(1))
2356}
2357
2358fn viewport_full_rows<H: crate::types::Host>(
2361 ed: &Editor<hjkl_buffer::Buffer, H>,
2362 count: usize,
2363) -> usize {
2364 let h = ed.viewport_height_value() as usize;
2365 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2366}
2367
2368fn scroll_cursor_rows<H: crate::types::Host>(
2373 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2374 delta: isize,
2375) {
2376 if delta == 0 {
2377 return;
2378 }
2379 ed.sync_buffer_content_from_textarea();
2380 let (row, _) = ed.cursor();
2381 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2382 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2383 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2384 crate::motions::move_first_non_blank(&mut ed.buffer);
2385 ed.push_buffer_cursor_to_textarea();
2386 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2387}
2388
2389fn parse_motion(input: &Input) -> Option<Motion> {
2392 if input.ctrl {
2393 return None;
2394 }
2395 match input.key {
2396 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2397 Key::Char('l') | Key::Right => Some(Motion::Right),
2398 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2399 Key::Char('k') | Key::Up => Some(Motion::Up),
2400 Key::Char('w') => Some(Motion::WordFwd),
2401 Key::Char('W') => Some(Motion::BigWordFwd),
2402 Key::Char('b') => Some(Motion::WordBack),
2403 Key::Char('B') => Some(Motion::BigWordBack),
2404 Key::Char('e') => Some(Motion::WordEnd),
2405 Key::Char('E') => Some(Motion::BigWordEnd),
2406 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2407 Key::Char('^') => Some(Motion::FirstNonBlank),
2408 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2409 Key::Char('G') => Some(Motion::FileBottom),
2410 Key::Char('%') => Some(Motion::MatchBracket),
2411 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2412 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2413 Key::Char('*') => Some(Motion::WordAtCursor {
2414 forward: true,
2415 whole_word: true,
2416 }),
2417 Key::Char('#') => Some(Motion::WordAtCursor {
2418 forward: false,
2419 whole_word: true,
2420 }),
2421 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2422 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2423 Key::Char('H') => Some(Motion::ViewportTop),
2424 Key::Char('M') => Some(Motion::ViewportMiddle),
2425 Key::Char('L') => Some(Motion::ViewportBottom),
2426 Key::Char('{') => Some(Motion::ParagraphPrev),
2427 Key::Char('}') => Some(Motion::ParagraphNext),
2428 Key::Char('(') => Some(Motion::SentencePrev),
2429 Key::Char(')') => Some(Motion::SentenceNext),
2430 _ => None,
2431 }
2432}
2433
2434fn execute_motion<H: crate::types::Host>(
2437 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2438 motion: Motion,
2439 count: usize,
2440) {
2441 let count = count.max(1);
2442 let motion = match motion {
2444 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2445 Some((ch, forward, till)) => Motion::Find {
2446 ch,
2447 forward: if reverse { !forward } else { forward },
2448 till,
2449 },
2450 None => return,
2451 },
2452 other => other,
2453 };
2454 let pre_pos = ed.cursor();
2455 let pre_col = pre_pos.1;
2456 apply_motion_cursor(ed, &motion, count);
2457 let post_pos = ed.cursor();
2458 if is_big_jump(&motion) && pre_pos != post_pos {
2459 push_jump(ed, pre_pos);
2460 }
2461 apply_sticky_col(ed, &motion, pre_col);
2462 ed.sync_buffer_from_textarea();
2467}
2468
2469fn apply_sticky_col<H: crate::types::Host>(
2474 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2475 motion: &Motion,
2476 pre_col: usize,
2477) {
2478 if is_vertical_motion(motion) {
2479 let want = ed.sticky_col.unwrap_or(pre_col);
2480 ed.sticky_col = Some(want);
2483 let (row, _) = ed.cursor();
2484 let line_len = buf_line_chars(&ed.buffer, row);
2485 let max_col = line_len.saturating_sub(1);
2489 let target = want.min(max_col);
2490 ed.jump_cursor(row, target);
2491 } else {
2492 ed.sticky_col = Some(ed.cursor().1);
2495 }
2496}
2497
2498fn is_vertical_motion(motion: &Motion) -> bool {
2499 matches!(
2503 motion,
2504 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2505 )
2506}
2507
2508fn apply_motion_cursor<H: crate::types::Host>(
2509 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2510 motion: &Motion,
2511 count: usize,
2512) {
2513 apply_motion_cursor_ctx(ed, motion, count, false)
2514}
2515
2516fn apply_motion_cursor_ctx<H: crate::types::Host>(
2517 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2518 motion: &Motion,
2519 count: usize,
2520 as_operator: bool,
2521) {
2522 match motion {
2523 Motion::Left => {
2524 crate::motions::move_left(&mut ed.buffer, count);
2526 ed.push_buffer_cursor_to_textarea();
2527 }
2528 Motion::Right => {
2529 if as_operator {
2533 crate::motions::move_right_to_end(&mut ed.buffer, count);
2534 } else {
2535 crate::motions::move_right_in_line(&mut ed.buffer, count);
2536 }
2537 ed.push_buffer_cursor_to_textarea();
2538 }
2539 Motion::Up => {
2540 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2544 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2545 ed.push_buffer_cursor_to_textarea();
2546 }
2547 Motion::Down => {
2548 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2549 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2550 ed.push_buffer_cursor_to_textarea();
2551 }
2552 Motion::ScreenUp => {
2553 let v = *ed.host.viewport();
2554 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2555 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2556 ed.push_buffer_cursor_to_textarea();
2557 }
2558 Motion::ScreenDown => {
2559 let v = *ed.host.viewport();
2560 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2561 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2562 ed.push_buffer_cursor_to_textarea();
2563 }
2564 Motion::WordFwd => {
2565 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2566 ed.push_buffer_cursor_to_textarea();
2567 }
2568 Motion::WordBack => {
2569 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2570 ed.push_buffer_cursor_to_textarea();
2571 }
2572 Motion::WordEnd => {
2573 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2574 ed.push_buffer_cursor_to_textarea();
2575 }
2576 Motion::BigWordFwd => {
2577 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2578 ed.push_buffer_cursor_to_textarea();
2579 }
2580 Motion::BigWordBack => {
2581 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2582 ed.push_buffer_cursor_to_textarea();
2583 }
2584 Motion::BigWordEnd => {
2585 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2586 ed.push_buffer_cursor_to_textarea();
2587 }
2588 Motion::WordEndBack => {
2589 crate::motions::move_word_end_back(
2590 &mut ed.buffer,
2591 false,
2592 count,
2593 &ed.settings.iskeyword,
2594 );
2595 ed.push_buffer_cursor_to_textarea();
2596 }
2597 Motion::BigWordEndBack => {
2598 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2599 ed.push_buffer_cursor_to_textarea();
2600 }
2601 Motion::LineStart => {
2602 crate::motions::move_line_start(&mut ed.buffer);
2603 ed.push_buffer_cursor_to_textarea();
2604 }
2605 Motion::FirstNonBlank => {
2606 crate::motions::move_first_non_blank(&mut ed.buffer);
2607 ed.push_buffer_cursor_to_textarea();
2608 }
2609 Motion::LineEnd => {
2610 crate::motions::move_line_end(&mut ed.buffer);
2612 ed.push_buffer_cursor_to_textarea();
2613 }
2614 Motion::FileTop => {
2615 if count > 1 {
2618 crate::motions::move_bottom(&mut ed.buffer, count);
2619 } else {
2620 crate::motions::move_top(&mut ed.buffer);
2621 }
2622 ed.push_buffer_cursor_to_textarea();
2623 }
2624 Motion::FileBottom => {
2625 if count > 1 {
2628 crate::motions::move_bottom(&mut ed.buffer, count);
2629 } else {
2630 crate::motions::move_bottom(&mut ed.buffer, 0);
2631 }
2632 ed.push_buffer_cursor_to_textarea();
2633 }
2634 Motion::Find { ch, forward, till } => {
2635 for _ in 0..count {
2636 if !find_char_on_line(ed, *ch, *forward, *till) {
2637 break;
2638 }
2639 }
2640 }
2641 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2643 let _ = matching_bracket(ed);
2644 }
2645 Motion::WordAtCursor {
2646 forward,
2647 whole_word,
2648 } => {
2649 word_at_cursor_search(ed, *forward, *whole_word, count);
2650 }
2651 Motion::SearchNext { reverse } => {
2652 if let Some(pattern) = ed.vim.last_search.clone() {
2656 push_search_pattern(ed, &pattern);
2657 }
2658 if ed.search_state().pattern.is_none() {
2659 return;
2660 }
2661 let forward = ed.vim.last_search_forward != *reverse;
2665 for _ in 0..count.max(1) {
2666 if forward {
2667 ed.search_advance_forward(true);
2668 } else {
2669 ed.search_advance_backward(true);
2670 }
2671 }
2672 ed.push_buffer_cursor_to_textarea();
2673 }
2674 Motion::ViewportTop => {
2675 let v = *ed.host().viewport();
2676 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2677 ed.push_buffer_cursor_to_textarea();
2678 }
2679 Motion::ViewportMiddle => {
2680 let v = *ed.host().viewport();
2681 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2682 ed.push_buffer_cursor_to_textarea();
2683 }
2684 Motion::ViewportBottom => {
2685 let v = *ed.host().viewport();
2686 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2687 ed.push_buffer_cursor_to_textarea();
2688 }
2689 Motion::LastNonBlank => {
2690 crate::motions::move_last_non_blank(&mut ed.buffer);
2691 ed.push_buffer_cursor_to_textarea();
2692 }
2693 Motion::LineMiddle => {
2694 let row = ed.cursor().0;
2695 let line_chars = buf_line_chars(&ed.buffer, row);
2696 let target = line_chars / 2;
2699 ed.jump_cursor(row, target);
2700 }
2701 Motion::ParagraphPrev => {
2702 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2703 ed.push_buffer_cursor_to_textarea();
2704 }
2705 Motion::ParagraphNext => {
2706 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2707 ed.push_buffer_cursor_to_textarea();
2708 }
2709 Motion::SentencePrev => {
2710 for _ in 0..count.max(1) {
2711 if let Some((row, col)) = sentence_boundary(ed, false) {
2712 ed.jump_cursor(row, col);
2713 }
2714 }
2715 }
2716 Motion::SentenceNext => {
2717 for _ in 0..count.max(1) {
2718 if let Some((row, col)) = sentence_boundary(ed, true) {
2719 ed.jump_cursor(row, col);
2720 }
2721 }
2722 }
2723 }
2724}
2725
2726fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2727 ed.sync_buffer_content_from_textarea();
2733 crate::motions::move_first_non_blank(&mut ed.buffer);
2734 ed.push_buffer_cursor_to_textarea();
2735}
2736
2737fn find_char_on_line<H: crate::types::Host>(
2738 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2739 ch: char,
2740 forward: bool,
2741 till: bool,
2742) -> bool {
2743 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2744 if moved {
2745 ed.push_buffer_cursor_to_textarea();
2746 }
2747 moved
2748}
2749
2750fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2751 let moved = crate::motions::match_bracket(&mut ed.buffer);
2752 if moved {
2753 ed.push_buffer_cursor_to_textarea();
2754 }
2755 moved
2756}
2757
2758fn word_at_cursor_search<H: crate::types::Host>(
2759 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2760 forward: bool,
2761 whole_word: bool,
2762 count: usize,
2763) {
2764 let (row, col) = ed.cursor();
2765 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2766 let chars: Vec<char> = line.chars().collect();
2767 if chars.is_empty() {
2768 return;
2769 }
2770 let spec = ed.settings().iskeyword.clone();
2772 let is_word = |c: char| is_keyword_char(c, &spec);
2773 let mut start = col.min(chars.len().saturating_sub(1));
2774 while start > 0 && is_word(chars[start - 1]) {
2775 start -= 1;
2776 }
2777 let mut end = start;
2778 while end < chars.len() && is_word(chars[end]) {
2779 end += 1;
2780 }
2781 if end <= start {
2782 return;
2783 }
2784 let word: String = chars[start..end].iter().collect();
2785 let escaped = regex_escape(&word);
2786 let pattern = if whole_word {
2787 format!(r"\b{escaped}\b")
2788 } else {
2789 escaped
2790 };
2791 push_search_pattern(ed, &pattern);
2792 if ed.search_state().pattern.is_none() {
2793 return;
2794 }
2795 ed.vim.last_search = Some(pattern);
2797 ed.vim.last_search_forward = forward;
2798 for _ in 0..count.max(1) {
2799 if forward {
2800 ed.search_advance_forward(true);
2801 } else {
2802 ed.search_advance_backward(true);
2803 }
2804 }
2805 ed.push_buffer_cursor_to_textarea();
2806}
2807
2808fn regex_escape(s: &str) -> String {
2809 let mut out = String::with_capacity(s.len());
2810 for c in s.chars() {
2811 if matches!(
2812 c,
2813 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2814 ) {
2815 out.push('\\');
2816 }
2817 out.push(c);
2818 }
2819 out
2820}
2821
2822fn handle_after_op<H: crate::types::Host>(
2825 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2826 input: Input,
2827 op: Operator,
2828 count1: usize,
2829) -> bool {
2830 if let Key::Char(d @ '0'..='9') = input.key
2832 && !input.ctrl
2833 && (d != '0' || ed.vim.count > 0)
2834 {
2835 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2836 ed.vim.pending = Pending::Op { op, count1 };
2837 return true;
2838 }
2839
2840 if input.key == Key::Esc {
2842 ed.vim.count = 0;
2843 return true;
2844 }
2845
2846 let double_ch = match op {
2850 Operator::Delete => Some('d'),
2851 Operator::Change => Some('c'),
2852 Operator::Yank => Some('y'),
2853 Operator::Indent => Some('>'),
2854 Operator::Outdent => Some('<'),
2855 Operator::Uppercase => Some('U'),
2856 Operator::Lowercase => Some('u'),
2857 Operator::ToggleCase => Some('~'),
2858 Operator::Fold => None,
2859 Operator::Reflow => Some('q'),
2862 };
2863 if let Key::Char(c) = input.key
2864 && !input.ctrl
2865 && Some(c) == double_ch
2866 {
2867 let count2 = take_count(&mut ed.vim);
2868 let total = count1.max(1) * count2.max(1);
2869 execute_line_op(ed, op, total);
2870 if !ed.vim.replaying {
2871 ed.vim.last_change = Some(LastChange::LineOp {
2872 op,
2873 count: total,
2874 inserted: None,
2875 });
2876 }
2877 return true;
2878 }
2879
2880 if let Key::Char('i') | Key::Char('a') = input.key
2882 && !input.ctrl
2883 {
2884 let inner = matches!(input.key, Key::Char('i'));
2885 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2886 return true;
2887 }
2888
2889 if input.key == Key::Char('g') && !input.ctrl {
2891 ed.vim.pending = Pending::OpG { op, count1 };
2892 return true;
2893 }
2894
2895 if let Some((forward, till)) = find_entry(&input) {
2897 ed.vim.pending = Pending::OpFind {
2898 op,
2899 count1,
2900 forward,
2901 till,
2902 };
2903 return true;
2904 }
2905
2906 let count2 = take_count(&mut ed.vim);
2908 let total = count1.max(1) * count2.max(1);
2909 if let Some(motion) = parse_motion(&input) {
2910 let motion = match motion {
2911 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2912 Some((ch, forward, till)) => Motion::Find {
2913 ch,
2914 forward: if reverse { !forward } else { forward },
2915 till,
2916 },
2917 None => return true,
2918 },
2919 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2923 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2924 m => m,
2925 };
2926 apply_op_with_motion(ed, op, &motion, total);
2927 if let Motion::Find { ch, forward, till } = &motion {
2928 ed.vim.last_find = Some((*ch, *forward, *till));
2929 }
2930 if !ed.vim.replaying && op_is_change(op) {
2931 ed.vim.last_change = Some(LastChange::OpMotion {
2932 op,
2933 motion,
2934 count: total,
2935 inserted: None,
2936 });
2937 }
2938 return true;
2939 }
2940
2941 true
2943}
2944
2945fn handle_op_after_g<H: crate::types::Host>(
2946 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2947 input: Input,
2948 op: Operator,
2949 count1: usize,
2950) -> bool {
2951 if input.ctrl {
2952 return true;
2953 }
2954 let count2 = take_count(&mut ed.vim);
2955 let total = count1.max(1) * count2.max(1);
2956 if matches!(
2960 op,
2961 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2962 ) {
2963 let op_char = match op {
2964 Operator::Uppercase => 'U',
2965 Operator::Lowercase => 'u',
2966 Operator::ToggleCase => '~',
2967 _ => unreachable!(),
2968 };
2969 if input.key == Key::Char(op_char) {
2970 execute_line_op(ed, op, total);
2971 if !ed.vim.replaying {
2972 ed.vim.last_change = Some(LastChange::LineOp {
2973 op,
2974 count: total,
2975 inserted: None,
2976 });
2977 }
2978 return true;
2979 }
2980 }
2981 let motion = match input.key {
2982 Key::Char('g') => Motion::FileTop,
2983 Key::Char('e') => Motion::WordEndBack,
2984 Key::Char('E') => Motion::BigWordEndBack,
2985 Key::Char('j') => Motion::ScreenDown,
2986 Key::Char('k') => Motion::ScreenUp,
2987 _ => return true,
2988 };
2989 apply_op_with_motion(ed, op, &motion, total);
2990 if !ed.vim.replaying && op_is_change(op) {
2991 ed.vim.last_change = Some(LastChange::OpMotion {
2992 op,
2993 motion,
2994 count: total,
2995 inserted: None,
2996 });
2997 }
2998 true
2999}
3000
3001fn handle_after_g<H: crate::types::Host>(
3002 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3003 input: Input,
3004) -> bool {
3005 let count = take_count(&mut ed.vim);
3006 match input.key {
3007 Key::Char('g') => {
3008 let pre = ed.cursor();
3010 if count > 1 {
3011 ed.jump_cursor(count - 1, 0);
3012 } else {
3013 ed.jump_cursor(0, 0);
3014 }
3015 move_first_non_whitespace(ed);
3016 if ed.cursor() != pre {
3017 push_jump(ed, pre);
3018 }
3019 }
3020 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
3021 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
3022 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
3024 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
3026 Key::Char('v') => {
3028 if let Some(snap) = ed.vim.last_visual {
3029 match snap.mode {
3030 Mode::Visual => {
3031 ed.vim.visual_anchor = snap.anchor;
3032 ed.vim.mode = Mode::Visual;
3033 }
3034 Mode::VisualLine => {
3035 ed.vim.visual_line_anchor = snap.anchor.0;
3036 ed.vim.mode = Mode::VisualLine;
3037 }
3038 Mode::VisualBlock => {
3039 ed.vim.block_anchor = snap.anchor;
3040 ed.vim.block_vcol = snap.block_vcol;
3041 ed.vim.mode = Mode::VisualBlock;
3042 }
3043 _ => {}
3044 }
3045 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3046 }
3047 }
3048 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
3052 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
3053 Key::Char('U') => {
3057 ed.vim.pending = Pending::Op {
3058 op: Operator::Uppercase,
3059 count1: count,
3060 };
3061 }
3062 Key::Char('u') => {
3063 ed.vim.pending = Pending::Op {
3064 op: Operator::Lowercase,
3065 count1: count,
3066 };
3067 }
3068 Key::Char('~') => {
3069 ed.vim.pending = Pending::Op {
3070 op: Operator::ToggleCase,
3071 count1: count,
3072 };
3073 }
3074 Key::Char('q') => {
3075 ed.vim.pending = Pending::Op {
3078 op: Operator::Reflow,
3079 count1: count,
3080 };
3081 }
3082 Key::Char('J') => {
3083 for _ in 0..count.max(1) {
3085 ed.push_undo();
3086 join_line_raw(ed);
3087 }
3088 if !ed.vim.replaying {
3089 ed.vim.last_change = Some(LastChange::JoinLine {
3090 count: count.max(1),
3091 });
3092 }
3093 }
3094 Key::Char('d') => {
3095 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3100 }
3101 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
3104 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
3105 Key::Char('*') => execute_motion(
3109 ed,
3110 Motion::WordAtCursor {
3111 forward: true,
3112 whole_word: false,
3113 },
3114 count,
3115 ),
3116 Key::Char('#') => execute_motion(
3117 ed,
3118 Motion::WordAtCursor {
3119 forward: false,
3120 whole_word: false,
3121 },
3122 count,
3123 ),
3124 _ => {}
3125 }
3126 true
3127}
3128
3129fn handle_after_z<H: crate::types::Host>(
3130 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3131 input: Input,
3132) -> bool {
3133 use crate::editor::CursorScrollTarget;
3134 let row = ed.cursor().0;
3135 match input.key {
3136 Key::Char('z') => {
3137 ed.scroll_cursor_to(CursorScrollTarget::Center);
3138 ed.vim.viewport_pinned = true;
3139 }
3140 Key::Char('t') => {
3141 ed.scroll_cursor_to(CursorScrollTarget::Top);
3142 ed.vim.viewport_pinned = true;
3143 }
3144 Key::Char('b') => {
3145 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3146 ed.vim.viewport_pinned = true;
3147 }
3148 Key::Char('o') => {
3153 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3154 }
3155 Key::Char('c') => {
3156 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3157 }
3158 Key::Char('a') => {
3159 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3160 }
3161 Key::Char('R') => {
3162 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3163 }
3164 Key::Char('M') => {
3165 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3166 }
3167 Key::Char('E') => {
3168 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3169 }
3170 Key::Char('d') => {
3171 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3172 }
3173 Key::Char('f') => {
3174 if matches!(
3175 ed.vim.mode,
3176 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3177 ) {
3178 let anchor_row = match ed.vim.mode {
3181 Mode::VisualLine => ed.vim.visual_line_anchor,
3182 Mode::VisualBlock => ed.vim.block_anchor.0,
3183 _ => ed.vim.visual_anchor.0,
3184 };
3185 let cur = ed.cursor().0;
3186 let top = anchor_row.min(cur);
3187 let bot = anchor_row.max(cur);
3188 ed.apply_fold_op(crate::types::FoldOp::Add {
3189 start_row: top,
3190 end_row: bot,
3191 closed: true,
3192 });
3193 ed.vim.mode = Mode::Normal;
3194 } else {
3195 let count = take_count(&mut ed.vim);
3200 ed.vim.pending = Pending::Op {
3201 op: Operator::Fold,
3202 count1: count,
3203 };
3204 }
3205 }
3206 _ => {}
3207 }
3208 true
3209}
3210
3211fn handle_replace<H: crate::types::Host>(
3212 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3213 input: Input,
3214) -> bool {
3215 if let Key::Char(ch) = input.key {
3216 if ed.vim.mode == Mode::VisualBlock {
3217 block_replace(ed, ch);
3218 return true;
3219 }
3220 let count = take_count(&mut ed.vim);
3221 replace_char(ed, ch, count.max(1));
3222 if !ed.vim.replaying {
3223 ed.vim.last_change = Some(LastChange::ReplaceChar {
3224 ch,
3225 count: count.max(1),
3226 });
3227 }
3228 }
3229 true
3230}
3231
3232fn handle_find_target<H: crate::types::Host>(
3233 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3234 input: Input,
3235 forward: bool,
3236 till: bool,
3237) -> bool {
3238 let Key::Char(ch) = input.key else {
3239 return true;
3240 };
3241 let count = take_count(&mut ed.vim);
3242 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3243 ed.vim.last_find = Some((ch, forward, till));
3244 true
3245}
3246
3247fn handle_op_find_target<H: crate::types::Host>(
3248 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3249 input: Input,
3250 op: Operator,
3251 count1: usize,
3252 forward: bool,
3253 till: bool,
3254) -> bool {
3255 let Key::Char(ch) = input.key else {
3256 return true;
3257 };
3258 let count2 = take_count(&mut ed.vim);
3259 let total = count1.max(1) * count2.max(1);
3260 let motion = Motion::Find { ch, forward, till };
3261 apply_op_with_motion(ed, op, &motion, total);
3262 ed.vim.last_find = Some((ch, forward, till));
3263 if !ed.vim.replaying && op_is_change(op) {
3264 ed.vim.last_change = Some(LastChange::OpMotion {
3265 op,
3266 motion,
3267 count: total,
3268 inserted: None,
3269 });
3270 }
3271 true
3272}
3273
3274fn handle_text_object<H: crate::types::Host>(
3275 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3276 input: Input,
3277 op: Operator,
3278 _count1: usize,
3279 inner: bool,
3280) -> bool {
3281 let Key::Char(ch) = input.key else {
3282 return true;
3283 };
3284 let obj = match ch {
3285 'w' => TextObject::Word { big: false },
3286 'W' => TextObject::Word { big: true },
3287 '"' | '\'' | '`' => TextObject::Quote(ch),
3288 '(' | ')' | 'b' => TextObject::Bracket('('),
3289 '[' | ']' => TextObject::Bracket('['),
3290 '{' | '}' | 'B' => TextObject::Bracket('{'),
3291 '<' | '>' => TextObject::Bracket('<'),
3292 'p' => TextObject::Paragraph,
3293 't' => TextObject::XmlTag,
3294 's' => TextObject::Sentence,
3295 _ => return true,
3296 };
3297 apply_op_with_text_object(ed, op, obj, inner);
3298 if !ed.vim.replaying && op_is_change(op) {
3299 ed.vim.last_change = Some(LastChange::OpTextObj {
3300 op,
3301 obj,
3302 inner,
3303 inserted: None,
3304 });
3305 }
3306 true
3307}
3308
3309fn handle_visual_text_obj<H: crate::types::Host>(
3310 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3311 input: Input,
3312 inner: bool,
3313) -> bool {
3314 let Key::Char(ch) = input.key else {
3315 return true;
3316 };
3317 let obj = match ch {
3318 'w' => TextObject::Word { big: false },
3319 'W' => TextObject::Word { big: true },
3320 '"' | '\'' | '`' => TextObject::Quote(ch),
3321 '(' | ')' | 'b' => TextObject::Bracket('('),
3322 '[' | ']' => TextObject::Bracket('['),
3323 '{' | '}' | 'B' => TextObject::Bracket('{'),
3324 '<' | '>' => TextObject::Bracket('<'),
3325 'p' => TextObject::Paragraph,
3326 't' => TextObject::XmlTag,
3327 's' => TextObject::Sentence,
3328 _ => return true,
3329 };
3330 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3331 return true;
3332 };
3333 match kind {
3337 MotionKind::Linewise => {
3338 ed.vim.visual_line_anchor = start.0;
3339 ed.vim.mode = Mode::VisualLine;
3340 ed.jump_cursor(end.0, 0);
3341 }
3342 _ => {
3343 ed.vim.mode = Mode::Visual;
3344 ed.vim.visual_anchor = (start.0, start.1);
3345 let (er, ec) = retreat_one(ed, end);
3346 ed.jump_cursor(er, ec);
3347 }
3348 }
3349 true
3350}
3351
3352fn retreat_one<H: crate::types::Host>(
3354 ed: &Editor<hjkl_buffer::Buffer, H>,
3355 pos: (usize, usize),
3356) -> (usize, usize) {
3357 let (r, c) = pos;
3358 if c > 0 {
3359 (r, c - 1)
3360 } else if r > 0 {
3361 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3362 (r - 1, prev_len)
3363 } else {
3364 (0, 0)
3365 }
3366}
3367
3368fn op_is_change(op: Operator) -> bool {
3369 matches!(op, Operator::Delete | Operator::Change)
3370}
3371
3372fn handle_normal_only<H: crate::types::Host>(
3375 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3376 input: &Input,
3377 count: usize,
3378) -> bool {
3379 if input.ctrl {
3380 return false;
3381 }
3382 match input.key {
3383 Key::Char('i') => {
3384 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3385 true
3386 }
3387 Key::Char('I') => {
3388 move_first_non_whitespace(ed);
3389 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3390 true
3391 }
3392 Key::Char('a') => {
3393 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3394 ed.push_buffer_cursor_to_textarea();
3395 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3396 true
3397 }
3398 Key::Char('A') => {
3399 crate::motions::move_line_end(&mut ed.buffer);
3400 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3401 ed.push_buffer_cursor_to_textarea();
3402 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3403 true
3404 }
3405 Key::Char('R') => {
3406 begin_insert(ed, count.max(1), InsertReason::Replace);
3409 true
3410 }
3411 Key::Char('o') => {
3412 use hjkl_buffer::{Edit, Position};
3413 ed.push_undo();
3414 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3417 ed.sync_buffer_content_from_textarea();
3418 let row = buf_cursor_pos(&ed.buffer).row;
3419 let line_chars = buf_line_chars(&ed.buffer, row);
3420 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3423 let indent = compute_enter_indent(&ed.settings, prev_line);
3424 ed.mutate_edit(Edit::InsertStr {
3425 at: Position::new(row, line_chars),
3426 text: format!("\n{indent}"),
3427 });
3428 ed.push_buffer_cursor_to_textarea();
3429 true
3430 }
3431 Key::Char('O') => {
3432 use hjkl_buffer::{Edit, Position};
3433 ed.push_undo();
3434 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3435 ed.sync_buffer_content_from_textarea();
3436 let row = buf_cursor_pos(&ed.buffer).row;
3437 let indent = if row > 0 {
3441 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3442 compute_enter_indent(&ed.settings, above)
3443 } else {
3444 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3445 cur.chars()
3446 .take_while(|c| *c == ' ' || *c == '\t')
3447 .collect::<String>()
3448 };
3449 ed.mutate_edit(Edit::InsertStr {
3450 at: Position::new(row, 0),
3451 text: format!("{indent}\n"),
3452 });
3453 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3458 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3459 let new_row = buf_cursor_pos(&ed.buffer).row;
3460 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3461 ed.push_buffer_cursor_to_textarea();
3462 true
3463 }
3464 Key::Char('x') => {
3465 do_char_delete(ed, true, count.max(1));
3466 if !ed.vim.replaying {
3467 ed.vim.last_change = Some(LastChange::CharDel {
3468 forward: true,
3469 count: count.max(1),
3470 });
3471 }
3472 true
3473 }
3474 Key::Char('X') => {
3475 do_char_delete(ed, false, count.max(1));
3476 if !ed.vim.replaying {
3477 ed.vim.last_change = Some(LastChange::CharDel {
3478 forward: false,
3479 count: count.max(1),
3480 });
3481 }
3482 true
3483 }
3484 Key::Char('~') => {
3485 for _ in 0..count.max(1) {
3486 ed.push_undo();
3487 toggle_case_at_cursor(ed);
3488 }
3489 if !ed.vim.replaying {
3490 ed.vim.last_change = Some(LastChange::ToggleCase {
3491 count: count.max(1),
3492 });
3493 }
3494 true
3495 }
3496 Key::Char('J') => {
3497 for _ in 0..count.max(1) {
3498 ed.push_undo();
3499 join_line(ed);
3500 }
3501 if !ed.vim.replaying {
3502 ed.vim.last_change = Some(LastChange::JoinLine {
3503 count: count.max(1),
3504 });
3505 }
3506 true
3507 }
3508 Key::Char('D') => {
3509 ed.push_undo();
3510 delete_to_eol(ed);
3511 crate::motions::move_left(&mut ed.buffer, 1);
3513 ed.push_buffer_cursor_to_textarea();
3514 if !ed.vim.replaying {
3515 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3516 }
3517 true
3518 }
3519 Key::Char('Y') => {
3520 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3522 true
3523 }
3524 Key::Char('C') => {
3525 ed.push_undo();
3526 delete_to_eol(ed);
3527 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3528 true
3529 }
3530 Key::Char('s') => {
3531 use hjkl_buffer::{Edit, MotionKind, Position};
3532 ed.push_undo();
3533 ed.sync_buffer_content_from_textarea();
3534 for _ in 0..count.max(1) {
3535 let cursor = buf_cursor_pos(&ed.buffer);
3536 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3537 if cursor.col >= line_chars {
3538 break;
3539 }
3540 ed.mutate_edit(Edit::DeleteRange {
3541 start: cursor,
3542 end: Position::new(cursor.row, cursor.col + 1),
3543 kind: MotionKind::Char,
3544 });
3545 }
3546 ed.push_buffer_cursor_to_textarea();
3547 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3548 if !ed.vim.replaying {
3550 ed.vim.last_change = Some(LastChange::OpMotion {
3551 op: Operator::Change,
3552 motion: Motion::Right,
3553 count: count.max(1),
3554 inserted: None,
3555 });
3556 }
3557 true
3558 }
3559 Key::Char('p') => {
3560 do_paste(ed, false, count.max(1));
3561 if !ed.vim.replaying {
3562 ed.vim.last_change = Some(LastChange::Paste {
3563 before: false,
3564 count: count.max(1),
3565 });
3566 }
3567 true
3568 }
3569 Key::Char('P') => {
3570 do_paste(ed, true, count.max(1));
3571 if !ed.vim.replaying {
3572 ed.vim.last_change = Some(LastChange::Paste {
3573 before: true,
3574 count: count.max(1),
3575 });
3576 }
3577 true
3578 }
3579 Key::Char('u') => {
3580 do_undo(ed);
3581 true
3582 }
3583 Key::Char('r') => {
3584 ed.vim.count = count;
3585 ed.vim.pending = Pending::Replace;
3586 true
3587 }
3588 Key::Char('/') => {
3589 enter_search(ed, true);
3590 true
3591 }
3592 Key::Char('?') => {
3593 enter_search(ed, false);
3594 true
3595 }
3596 Key::Char('.') => {
3597 replay_last_change(ed, count);
3598 true
3599 }
3600 _ => false,
3601 }
3602}
3603
3604fn begin_insert_noundo<H: crate::types::Host>(
3606 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3607 count: usize,
3608 reason: InsertReason,
3609) {
3610 let reason = if ed.vim.replaying {
3611 InsertReason::ReplayOnly
3612 } else {
3613 reason
3614 };
3615 let (row, _) = ed.cursor();
3616 ed.vim.insert_session = Some(InsertSession {
3617 count,
3618 row_min: row,
3619 row_max: row,
3620 before_lines: buf_lines_to_vec(&ed.buffer),
3621 reason,
3622 });
3623 ed.vim.mode = Mode::Insert;
3624}
3625
3626fn apply_op_with_motion<H: crate::types::Host>(
3629 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3630 op: Operator,
3631 motion: &Motion,
3632 count: usize,
3633) {
3634 let start = ed.cursor();
3635 apply_motion_cursor_ctx(ed, motion, count, true);
3640 let end = ed.cursor();
3641 let kind = motion_kind(motion);
3642 ed.jump_cursor(start.0, start.1);
3644 run_operator_over_range(ed, op, start, end, kind);
3645}
3646
3647fn apply_op_with_text_object<H: crate::types::Host>(
3648 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3649 op: Operator,
3650 obj: TextObject,
3651 inner: bool,
3652) {
3653 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3654 return;
3655 };
3656 ed.jump_cursor(start.0, start.1);
3657 run_operator_over_range(ed, op, start, end, kind);
3658}
3659
3660fn motion_kind(motion: &Motion) -> MotionKind {
3661 match motion {
3662 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3663 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3664 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3665 MotionKind::Linewise
3666 }
3667 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3668 MotionKind::Inclusive
3669 }
3670 Motion::Find { .. } => MotionKind::Inclusive,
3671 Motion::MatchBracket => MotionKind::Inclusive,
3672 Motion::LineEnd => MotionKind::Inclusive,
3674 _ => MotionKind::Exclusive,
3675 }
3676}
3677
3678fn run_operator_over_range<H: crate::types::Host>(
3679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3680 op: Operator,
3681 start: (usize, usize),
3682 end: (usize, usize),
3683 kind: MotionKind,
3684) {
3685 let (top, bot) = order(start, end);
3686 if top == bot {
3687 return;
3688 }
3689
3690 match op {
3691 Operator::Yank => {
3692 let text = read_vim_range(ed, top, bot, kind);
3693 if !text.is_empty() {
3694 ed.record_yank_to_host(text.clone());
3695 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3696 }
3697 let rbr = match kind {
3701 MotionKind::Linewise => {
3702 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3703 (bot.0, last_col)
3704 }
3705 MotionKind::Inclusive => (bot.0, bot.1),
3706 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3707 };
3708 ed.set_mark('[', top);
3709 ed.set_mark(']', rbr);
3710 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3711 ed.push_buffer_cursor_to_textarea();
3712 }
3713 Operator::Delete => {
3714 ed.push_undo();
3715 cut_vim_range(ed, top, bot, kind);
3716 if !matches!(kind, MotionKind::Linewise) {
3721 clamp_cursor_to_normal_mode(ed);
3722 }
3723 ed.vim.mode = Mode::Normal;
3724 let pos = ed.cursor();
3728 ed.set_mark('[', pos);
3729 ed.set_mark(']', pos);
3730 }
3731 Operator::Change => {
3732 ed.vim.change_mark_start = Some(top);
3737 ed.push_undo();
3738 cut_vim_range(ed, top, bot, kind);
3739 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3740 }
3741 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3742 apply_case_op_to_selection(ed, op, top, bot, kind);
3743 }
3744 Operator::Indent | Operator::Outdent => {
3745 ed.push_undo();
3748 if op == Operator::Indent {
3749 indent_rows(ed, top.0, bot.0, 1);
3750 } else {
3751 outdent_rows(ed, top.0, bot.0, 1);
3752 }
3753 ed.vim.mode = Mode::Normal;
3754 }
3755 Operator::Fold => {
3756 if bot.0 >= top.0 {
3760 ed.apply_fold_op(crate::types::FoldOp::Add {
3761 start_row: top.0,
3762 end_row: bot.0,
3763 closed: true,
3764 });
3765 }
3766 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3767 ed.push_buffer_cursor_to_textarea();
3768 ed.vim.mode = Mode::Normal;
3769 }
3770 Operator::Reflow => {
3771 ed.push_undo();
3772 reflow_rows(ed, top.0, bot.0);
3773 ed.vim.mode = Mode::Normal;
3774 }
3775 }
3776}
3777
3778fn reflow_rows<H: crate::types::Host>(
3783 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3784 top: usize,
3785 bot: usize,
3786) {
3787 let width = ed.settings().textwidth.max(1);
3788 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3789 let bot = bot.min(lines.len().saturating_sub(1));
3790 if top > bot {
3791 return;
3792 }
3793 let original = lines[top..=bot].to_vec();
3794 let mut wrapped: Vec<String> = Vec::new();
3795 let mut paragraph: Vec<String> = Vec::new();
3796 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3797 if para.is_empty() {
3798 return;
3799 }
3800 let words = para.join(" ");
3801 let mut current = String::new();
3802 for word in words.split_whitespace() {
3803 let extra = if current.is_empty() {
3804 word.chars().count()
3805 } else {
3806 current.chars().count() + 1 + word.chars().count()
3807 };
3808 if extra > width && !current.is_empty() {
3809 out.push(std::mem::take(&mut current));
3810 current.push_str(word);
3811 } else if current.is_empty() {
3812 current.push_str(word);
3813 } else {
3814 current.push(' ');
3815 current.push_str(word);
3816 }
3817 }
3818 if !current.is_empty() {
3819 out.push(current);
3820 }
3821 para.clear();
3822 };
3823 for line in &original {
3824 if line.trim().is_empty() {
3825 flush(&mut paragraph, &mut wrapped, width);
3826 wrapped.push(String::new());
3827 } else {
3828 paragraph.push(line.clone());
3829 }
3830 }
3831 flush(&mut paragraph, &mut wrapped, width);
3832
3833 let after: Vec<String> = lines.split_off(bot + 1);
3835 lines.truncate(top);
3836 lines.extend(wrapped);
3837 lines.extend(after);
3838 ed.restore(lines, (top, 0));
3839 ed.mark_content_dirty();
3840}
3841
3842fn apply_case_op_to_selection<H: crate::types::Host>(
3848 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3849 op: Operator,
3850 top: (usize, usize),
3851 bot: (usize, usize),
3852 kind: MotionKind,
3853) {
3854 use hjkl_buffer::Edit;
3855 ed.push_undo();
3856 let saved_yank = ed.yank().to_string();
3857 let saved_yank_linewise = ed.vim.yank_linewise;
3858 let selection = cut_vim_range(ed, top, bot, kind);
3859 let transformed = match op {
3860 Operator::Uppercase => selection.to_uppercase(),
3861 Operator::Lowercase => selection.to_lowercase(),
3862 Operator::ToggleCase => toggle_case_str(&selection),
3863 _ => unreachable!(),
3864 };
3865 if !transformed.is_empty() {
3866 let cursor = buf_cursor_pos(&ed.buffer);
3867 ed.mutate_edit(Edit::InsertStr {
3868 at: cursor,
3869 text: transformed,
3870 });
3871 }
3872 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3873 ed.push_buffer_cursor_to_textarea();
3874 ed.set_yank(saved_yank);
3875 ed.vim.yank_linewise = saved_yank_linewise;
3876 ed.vim.mode = Mode::Normal;
3877}
3878
3879fn indent_rows<H: crate::types::Host>(
3884 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3885 top: usize,
3886 bot: usize,
3887 count: usize,
3888) {
3889 ed.sync_buffer_content_from_textarea();
3890 let width = ed.settings().shiftwidth * count.max(1);
3891 let pad: String = " ".repeat(width);
3892 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3893 let bot = bot.min(lines.len().saturating_sub(1));
3894 for line in lines.iter_mut().take(bot + 1).skip(top) {
3895 if !line.is_empty() {
3896 line.insert_str(0, &pad);
3897 }
3898 }
3899 ed.restore(lines, (top, 0));
3902 move_first_non_whitespace(ed);
3903}
3904
3905fn outdent_rows<H: crate::types::Host>(
3909 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3910 top: usize,
3911 bot: usize,
3912 count: usize,
3913) {
3914 ed.sync_buffer_content_from_textarea();
3915 let width = ed.settings().shiftwidth * count.max(1);
3916 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3917 let bot = bot.min(lines.len().saturating_sub(1));
3918 for line in lines.iter_mut().take(bot + 1).skip(top) {
3919 let strip: usize = line
3920 .chars()
3921 .take(width)
3922 .take_while(|c| *c == ' ' || *c == '\t')
3923 .count();
3924 if strip > 0 {
3925 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3926 line.drain(..byte_len);
3927 }
3928 }
3929 ed.restore(lines, (top, 0));
3930 move_first_non_whitespace(ed);
3931}
3932
3933fn toggle_case_str(s: &str) -> String {
3934 s.chars()
3935 .map(|c| {
3936 if c.is_lowercase() {
3937 c.to_uppercase().next().unwrap_or(c)
3938 } else if c.is_uppercase() {
3939 c.to_lowercase().next().unwrap_or(c)
3940 } else {
3941 c
3942 }
3943 })
3944 .collect()
3945}
3946
3947fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3948 if a <= b { (a, b) } else { (b, a) }
3949}
3950
3951fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3956 let (row, col) = ed.cursor();
3957 let line_chars = buf_line_chars(&ed.buffer, row);
3958 let max_col = line_chars.saturating_sub(1);
3959 if col > max_col {
3960 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
3961 ed.push_buffer_cursor_to_textarea();
3962 }
3963}
3964
3965fn execute_line_op<H: crate::types::Host>(
3968 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3969 op: Operator,
3970 count: usize,
3971) {
3972 let (row, col) = ed.cursor();
3973 let total = buf_row_count(&ed.buffer);
3974 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3975
3976 match op {
3977 Operator::Yank => {
3978 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3980 if !text.is_empty() {
3981 ed.record_yank_to_host(text.clone());
3982 ed.record_yank(text, true);
3983 }
3984 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
3987 ed.set_mark('[', (row, 0));
3988 ed.set_mark(']', (end_row, last_col));
3989 buf_set_cursor_rc(&mut ed.buffer, row, col);
3990 ed.push_buffer_cursor_to_textarea();
3991 ed.vim.mode = Mode::Normal;
3992 }
3993 Operator::Delete => {
3994 ed.push_undo();
3995 let deleted_through_last = end_row + 1 >= total;
3996 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3997 let total_after = buf_row_count(&ed.buffer);
4001 let raw_target = if deleted_through_last {
4002 row.saturating_sub(1).min(total_after.saturating_sub(1))
4003 } else {
4004 row.min(total_after.saturating_sub(1))
4005 };
4006 let target_row = if raw_target > 0
4012 && raw_target + 1 == total_after
4013 && buf_line(&ed.buffer, raw_target)
4014 .map(str::is_empty)
4015 .unwrap_or(false)
4016 {
4017 raw_target - 1
4018 } else {
4019 raw_target
4020 };
4021 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4022 ed.push_buffer_cursor_to_textarea();
4023 move_first_non_whitespace(ed);
4024 ed.sticky_col = Some(ed.cursor().1);
4025 ed.vim.mode = Mode::Normal;
4026 let pos = ed.cursor();
4029 ed.set_mark('[', pos);
4030 ed.set_mark(']', pos);
4031 }
4032 Operator::Change => {
4033 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4037 ed.vim.change_mark_start = Some((row, 0));
4039 ed.push_undo();
4040 ed.sync_buffer_content_from_textarea();
4041 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4043 if end_row > row {
4044 ed.mutate_edit(Edit::DeleteRange {
4045 start: Position::new(row + 1, 0),
4046 end: Position::new(end_row, 0),
4047 kind: BufKind::Line,
4048 });
4049 }
4050 let line_chars = buf_line_chars(&ed.buffer, row);
4051 if line_chars > 0 {
4052 ed.mutate_edit(Edit::DeleteRange {
4053 start: Position::new(row, 0),
4054 end: Position::new(row, line_chars),
4055 kind: BufKind::Char,
4056 });
4057 }
4058 if !payload.is_empty() {
4059 ed.record_yank_to_host(payload.clone());
4060 ed.record_delete(payload, true);
4061 }
4062 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4063 ed.push_buffer_cursor_to_textarea();
4064 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4065 }
4066 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4067 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4071 move_first_non_whitespace(ed);
4074 }
4075 Operator::Indent | Operator::Outdent => {
4076 ed.push_undo();
4078 if op == Operator::Indent {
4079 indent_rows(ed, row, end_row, 1);
4080 } else {
4081 outdent_rows(ed, row, end_row, 1);
4082 }
4083 ed.sticky_col = Some(ed.cursor().1);
4084 ed.vim.mode = Mode::Normal;
4085 }
4086 Operator::Fold => unreachable!("Fold has no line-op double"),
4088 Operator::Reflow => {
4089 ed.push_undo();
4091 reflow_rows(ed, row, end_row);
4092 move_first_non_whitespace(ed);
4093 ed.sticky_col = Some(ed.cursor().1);
4094 ed.vim.mode = Mode::Normal;
4095 }
4096 }
4097}
4098
4099fn apply_visual_operator<H: crate::types::Host>(
4102 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4103 op: Operator,
4104) {
4105 match ed.vim.mode {
4106 Mode::VisualLine => {
4107 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4108 let top = cursor_row.min(ed.vim.visual_line_anchor);
4109 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4110 ed.vim.yank_linewise = true;
4111 match op {
4112 Operator::Yank => {
4113 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4114 if !text.is_empty() {
4115 ed.record_yank_to_host(text.clone());
4116 ed.record_yank(text, true);
4117 }
4118 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4119 ed.push_buffer_cursor_to_textarea();
4120 ed.vim.mode = Mode::Normal;
4121 }
4122 Operator::Delete => {
4123 ed.push_undo();
4124 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4125 ed.vim.mode = Mode::Normal;
4126 }
4127 Operator::Change => {
4128 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4131 ed.push_undo();
4132 ed.sync_buffer_content_from_textarea();
4133 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4134 if bot > top {
4135 ed.mutate_edit(Edit::DeleteRange {
4136 start: Position::new(top + 1, 0),
4137 end: Position::new(bot, 0),
4138 kind: BufKind::Line,
4139 });
4140 }
4141 let line_chars = buf_line_chars(&ed.buffer, top);
4142 if line_chars > 0 {
4143 ed.mutate_edit(Edit::DeleteRange {
4144 start: Position::new(top, 0),
4145 end: Position::new(top, line_chars),
4146 kind: BufKind::Char,
4147 });
4148 }
4149 if !payload.is_empty() {
4150 ed.record_yank_to_host(payload.clone());
4151 ed.record_delete(payload, true);
4152 }
4153 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4154 ed.push_buffer_cursor_to_textarea();
4155 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4156 }
4157 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4158 let bot = buf_cursor_pos(&ed.buffer)
4159 .row
4160 .max(ed.vim.visual_line_anchor);
4161 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4162 move_first_non_whitespace(ed);
4163 }
4164 Operator::Indent | Operator::Outdent => {
4165 ed.push_undo();
4166 let (cursor_row, _) = ed.cursor();
4167 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4168 if op == Operator::Indent {
4169 indent_rows(ed, top, bot, 1);
4170 } else {
4171 outdent_rows(ed, top, bot, 1);
4172 }
4173 ed.vim.mode = Mode::Normal;
4174 }
4175 Operator::Reflow => {
4176 ed.push_undo();
4177 let (cursor_row, _) = ed.cursor();
4178 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4179 reflow_rows(ed, top, bot);
4180 ed.vim.mode = Mode::Normal;
4181 }
4182 Operator::Fold => unreachable!("Visual zf takes its own path"),
4185 }
4186 }
4187 Mode::Visual => {
4188 ed.vim.yank_linewise = false;
4189 let anchor = ed.vim.visual_anchor;
4190 let cursor = ed.cursor();
4191 let (top, bot) = order(anchor, cursor);
4192 match op {
4193 Operator::Yank => {
4194 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4195 if !text.is_empty() {
4196 ed.record_yank_to_host(text.clone());
4197 ed.record_yank(text, false);
4198 }
4199 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4200 ed.push_buffer_cursor_to_textarea();
4201 ed.vim.mode = Mode::Normal;
4202 }
4203 Operator::Delete => {
4204 ed.push_undo();
4205 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4206 ed.vim.mode = Mode::Normal;
4207 }
4208 Operator::Change => {
4209 ed.push_undo();
4210 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4211 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4212 }
4213 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4214 let anchor = ed.vim.visual_anchor;
4216 let cursor = ed.cursor();
4217 let (top, bot) = order(anchor, cursor);
4218 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4219 }
4220 Operator::Indent | Operator::Outdent => {
4221 ed.push_undo();
4222 let anchor = ed.vim.visual_anchor;
4223 let cursor = ed.cursor();
4224 let (top, bot) = order(anchor, cursor);
4225 if op == Operator::Indent {
4226 indent_rows(ed, top.0, bot.0, 1);
4227 } else {
4228 outdent_rows(ed, top.0, bot.0, 1);
4229 }
4230 ed.vim.mode = Mode::Normal;
4231 }
4232 Operator::Reflow => {
4233 ed.push_undo();
4234 let anchor = ed.vim.visual_anchor;
4235 let cursor = ed.cursor();
4236 let (top, bot) = order(anchor, cursor);
4237 reflow_rows(ed, top.0, bot.0);
4238 ed.vim.mode = Mode::Normal;
4239 }
4240 Operator::Fold => unreachable!("Visual zf takes its own path"),
4241 }
4242 }
4243 Mode::VisualBlock => apply_block_operator(ed, op),
4244 _ => {}
4245 }
4246}
4247
4248fn block_bounds<H: crate::types::Host>(
4253 ed: &Editor<hjkl_buffer::Buffer, H>,
4254) -> (usize, usize, usize, usize) {
4255 let (ar, ac) = ed.vim.block_anchor;
4256 let (cr, _) = ed.cursor();
4257 let cc = ed.vim.block_vcol;
4258 let top = ar.min(cr);
4259 let bot = ar.max(cr);
4260 let left = ac.min(cc);
4261 let right = ac.max(cc);
4262 (top, bot, left, right)
4263}
4264
4265fn update_block_vcol<H: crate::types::Host>(
4270 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4271 motion: &Motion,
4272) {
4273 match motion {
4274 Motion::Left
4275 | Motion::Right
4276 | Motion::WordFwd
4277 | Motion::BigWordFwd
4278 | Motion::WordBack
4279 | Motion::BigWordBack
4280 | Motion::WordEnd
4281 | Motion::BigWordEnd
4282 | Motion::WordEndBack
4283 | Motion::BigWordEndBack
4284 | Motion::LineStart
4285 | Motion::FirstNonBlank
4286 | Motion::LineEnd
4287 | Motion::Find { .. }
4288 | Motion::FindRepeat { .. }
4289 | Motion::MatchBracket => {
4290 ed.vim.block_vcol = ed.cursor().1;
4291 }
4292 _ => {}
4294 }
4295}
4296
4297fn apply_block_operator<H: crate::types::Host>(
4302 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4303 op: Operator,
4304) {
4305 let (top, bot, left, right) = block_bounds(ed);
4306 let yank = block_yank(ed, top, bot, left, right);
4308
4309 match op {
4310 Operator::Yank => {
4311 if !yank.is_empty() {
4312 ed.record_yank_to_host(yank.clone());
4313 ed.record_yank(yank, false);
4314 }
4315 ed.vim.mode = Mode::Normal;
4316 ed.jump_cursor(top, left);
4317 }
4318 Operator::Delete => {
4319 ed.push_undo();
4320 delete_block_contents(ed, top, bot, left, right);
4321 if !yank.is_empty() {
4322 ed.record_yank_to_host(yank.clone());
4323 ed.record_delete(yank, false);
4324 }
4325 ed.vim.mode = Mode::Normal;
4326 ed.jump_cursor(top, left);
4327 }
4328 Operator::Change => {
4329 ed.push_undo();
4330 delete_block_contents(ed, top, bot, left, right);
4331 if !yank.is_empty() {
4332 ed.record_yank_to_host(yank.clone());
4333 ed.record_delete(yank, false);
4334 }
4335 ed.jump_cursor(top, left);
4336 begin_insert_noundo(
4337 ed,
4338 1,
4339 InsertReason::BlockEdge {
4340 top,
4341 bot,
4342 col: left,
4343 },
4344 );
4345 }
4346 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4347 ed.push_undo();
4348 transform_block_case(ed, op, top, bot, left, right);
4349 ed.vim.mode = Mode::Normal;
4350 ed.jump_cursor(top, left);
4351 }
4352 Operator::Indent | Operator::Outdent => {
4353 ed.push_undo();
4357 if op == Operator::Indent {
4358 indent_rows(ed, top, bot, 1);
4359 } else {
4360 outdent_rows(ed, top, bot, 1);
4361 }
4362 ed.vim.mode = Mode::Normal;
4363 }
4364 Operator::Fold => unreachable!("Visual zf takes its own path"),
4365 Operator::Reflow => {
4366 ed.push_undo();
4370 reflow_rows(ed, top, bot);
4371 ed.vim.mode = Mode::Normal;
4372 }
4373 }
4374}
4375
4376fn transform_block_case<H: crate::types::Host>(
4380 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4381 op: Operator,
4382 top: usize,
4383 bot: usize,
4384 left: usize,
4385 right: usize,
4386) {
4387 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4388 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4389 let chars: Vec<char> = lines[r].chars().collect();
4390 if left >= chars.len() {
4391 continue;
4392 }
4393 let end = (right + 1).min(chars.len());
4394 let head: String = chars[..left].iter().collect();
4395 let mid: String = chars[left..end].iter().collect();
4396 let tail: String = chars[end..].iter().collect();
4397 let transformed = match op {
4398 Operator::Uppercase => mid.to_uppercase(),
4399 Operator::Lowercase => mid.to_lowercase(),
4400 Operator::ToggleCase => toggle_case_str(&mid),
4401 _ => mid,
4402 };
4403 lines[r] = format!("{head}{transformed}{tail}");
4404 }
4405 let saved_yank = ed.yank().to_string();
4406 let saved_linewise = ed.vim.yank_linewise;
4407 ed.restore(lines, (top, left));
4408 ed.set_yank(saved_yank);
4409 ed.vim.yank_linewise = saved_linewise;
4410}
4411
4412fn block_yank<H: crate::types::Host>(
4413 ed: &Editor<hjkl_buffer::Buffer, H>,
4414 top: usize,
4415 bot: usize,
4416 left: usize,
4417 right: usize,
4418) -> String {
4419 let lines = buf_lines_to_vec(&ed.buffer);
4420 let mut rows: Vec<String> = Vec::new();
4421 for r in top..=bot {
4422 let line = match lines.get(r) {
4423 Some(l) => l,
4424 None => break,
4425 };
4426 let chars: Vec<char> = line.chars().collect();
4427 let end = (right + 1).min(chars.len());
4428 if left >= chars.len() {
4429 rows.push(String::new());
4430 } else {
4431 rows.push(chars[left..end].iter().collect());
4432 }
4433 }
4434 rows.join("\n")
4435}
4436
4437fn delete_block_contents<H: crate::types::Host>(
4438 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4439 top: usize,
4440 bot: usize,
4441 left: usize,
4442 right: usize,
4443) {
4444 use hjkl_buffer::{Edit, MotionKind, Position};
4445 ed.sync_buffer_content_from_textarea();
4446 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4447 if last_row < top {
4448 return;
4449 }
4450 ed.mutate_edit(Edit::DeleteRange {
4451 start: Position::new(top, left),
4452 end: Position::new(last_row, right),
4453 kind: MotionKind::Block,
4454 });
4455 ed.push_buffer_cursor_to_textarea();
4456}
4457
4458fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4460 let (top, bot, left, right) = block_bounds(ed);
4461 ed.push_undo();
4462 ed.sync_buffer_content_from_textarea();
4463 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4464 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4465 let chars: Vec<char> = lines[r].chars().collect();
4466 if left >= chars.len() {
4467 continue;
4468 }
4469 let end = (right + 1).min(chars.len());
4470 let before: String = chars[..left].iter().collect();
4471 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4472 let after: String = chars[end..].iter().collect();
4473 lines[r] = format!("{before}{middle}{after}");
4474 }
4475 reset_textarea_lines(ed, lines);
4476 ed.vim.mode = Mode::Normal;
4477 ed.jump_cursor(top, left);
4478}
4479
4480fn reset_textarea_lines<H: crate::types::Host>(
4484 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4485 lines: Vec<String>,
4486) {
4487 let cursor = ed.cursor();
4488 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4489 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4490 ed.mark_content_dirty();
4491}
4492
4493type Pos = (usize, usize);
4499
4500fn text_object_range<H: crate::types::Host>(
4504 ed: &Editor<hjkl_buffer::Buffer, H>,
4505 obj: TextObject,
4506 inner: bool,
4507) -> Option<(Pos, Pos, MotionKind)> {
4508 match obj {
4509 TextObject::Word { big } => {
4510 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4511 }
4512 TextObject::Quote(q) => {
4513 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4514 }
4515 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4516 TextObject::Paragraph => {
4517 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4518 }
4519 TextObject::XmlTag => {
4520 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4521 }
4522 TextObject::Sentence => {
4523 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4524 }
4525 }
4526}
4527
4528fn sentence_boundary<H: crate::types::Host>(
4532 ed: &Editor<hjkl_buffer::Buffer, H>,
4533 forward: bool,
4534) -> Option<(usize, usize)> {
4535 let lines = buf_lines_to_vec(&ed.buffer);
4536 if lines.is_empty() {
4537 return None;
4538 }
4539 let pos_to_idx = |pos: (usize, usize)| -> usize {
4540 let mut idx = 0;
4541 for line in lines.iter().take(pos.0) {
4542 idx += line.chars().count() + 1;
4543 }
4544 idx + pos.1
4545 };
4546 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4547 for (r, line) in lines.iter().enumerate() {
4548 let len = line.chars().count();
4549 if idx <= len {
4550 return (r, idx);
4551 }
4552 idx -= len + 1;
4553 }
4554 let last = lines.len().saturating_sub(1);
4555 (last, lines[last].chars().count())
4556 };
4557 let mut chars: Vec<char> = Vec::new();
4558 for (r, line) in lines.iter().enumerate() {
4559 chars.extend(line.chars());
4560 if r + 1 < lines.len() {
4561 chars.push('\n');
4562 }
4563 }
4564 if chars.is_empty() {
4565 return None;
4566 }
4567 let total = chars.len();
4568 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4569 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4570
4571 if forward {
4572 let mut i = cursor_idx + 1;
4575 while i < total {
4576 if is_terminator(chars[i]) {
4577 while i + 1 < total && is_terminator(chars[i + 1]) {
4578 i += 1;
4579 }
4580 if i + 1 >= total {
4581 return None;
4582 }
4583 if chars[i + 1].is_whitespace() {
4584 let mut j = i + 1;
4585 while j < total && chars[j].is_whitespace() {
4586 j += 1;
4587 }
4588 if j >= total {
4589 return None;
4590 }
4591 return Some(idx_to_pos(j));
4592 }
4593 }
4594 i += 1;
4595 }
4596 None
4597 } else {
4598 let find_start = |from: usize| -> Option<usize> {
4602 let mut start = from;
4603 while start > 0 {
4604 let prev = chars[start - 1];
4605 if prev.is_whitespace() {
4606 let mut k = start - 1;
4607 while k > 0 && chars[k - 1].is_whitespace() {
4608 k -= 1;
4609 }
4610 if k > 0 && is_terminator(chars[k - 1]) {
4611 break;
4612 }
4613 }
4614 start -= 1;
4615 }
4616 while start < total && chars[start].is_whitespace() {
4617 start += 1;
4618 }
4619 (start < total).then_some(start)
4620 };
4621 let current_start = find_start(cursor_idx)?;
4622 if current_start < cursor_idx {
4623 return Some(idx_to_pos(current_start));
4624 }
4625 let mut k = current_start;
4628 while k > 0 && chars[k - 1].is_whitespace() {
4629 k -= 1;
4630 }
4631 if k == 0 {
4632 return None;
4633 }
4634 let prev_start = find_start(k - 1)?;
4635 Some(idx_to_pos(prev_start))
4636 }
4637}
4638
4639fn sentence_text_object<H: crate::types::Host>(
4645 ed: &Editor<hjkl_buffer::Buffer, H>,
4646 inner: bool,
4647) -> Option<((usize, usize), (usize, usize))> {
4648 let lines = buf_lines_to_vec(&ed.buffer);
4649 if lines.is_empty() {
4650 return None;
4651 }
4652 let pos_to_idx = |pos: (usize, usize)| -> usize {
4655 let mut idx = 0;
4656 for line in lines.iter().take(pos.0) {
4657 idx += line.chars().count() + 1;
4658 }
4659 idx + pos.1
4660 };
4661 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4662 for (r, line) in lines.iter().enumerate() {
4663 let len = line.chars().count();
4664 if idx <= len {
4665 return (r, idx);
4666 }
4667 idx -= len + 1;
4668 }
4669 let last = lines.len().saturating_sub(1);
4670 (last, lines[last].chars().count())
4671 };
4672 let mut chars: Vec<char> = Vec::new();
4673 for (r, line) in lines.iter().enumerate() {
4674 chars.extend(line.chars());
4675 if r + 1 < lines.len() {
4676 chars.push('\n');
4677 }
4678 }
4679 if chars.is_empty() {
4680 return None;
4681 }
4682
4683 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4684 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4685
4686 let mut start = cursor_idx;
4690 while start > 0 {
4691 let prev = chars[start - 1];
4692 if prev.is_whitespace() {
4693 let mut k = start - 1;
4697 while k > 0 && chars[k - 1].is_whitespace() {
4698 k -= 1;
4699 }
4700 if k > 0 && is_terminator(chars[k - 1]) {
4701 break;
4702 }
4703 }
4704 start -= 1;
4705 }
4706 while start < chars.len() && chars[start].is_whitespace() {
4709 start += 1;
4710 }
4711 if start >= chars.len() {
4712 return None;
4713 }
4714
4715 let mut end = start;
4718 while end < chars.len() {
4719 if is_terminator(chars[end]) {
4720 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4722 end += 1;
4723 }
4724 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4727 break;
4728 }
4729 }
4730 end += 1;
4731 }
4732 let end_idx = (end + 1).min(chars.len());
4734
4735 let final_end = if inner {
4736 end_idx
4737 } else {
4738 let mut e = end_idx;
4742 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4743 e += 1;
4744 }
4745 e
4746 };
4747
4748 Some((idx_to_pos(start), idx_to_pos(final_end)))
4749}
4750
4751fn tag_text_object<H: crate::types::Host>(
4755 ed: &Editor<hjkl_buffer::Buffer, H>,
4756 inner: bool,
4757) -> Option<((usize, usize), (usize, usize))> {
4758 let lines = buf_lines_to_vec(&ed.buffer);
4759 if lines.is_empty() {
4760 return None;
4761 }
4762 let pos_to_idx = |pos: (usize, usize)| -> usize {
4766 let mut idx = 0;
4767 for line in lines.iter().take(pos.0) {
4768 idx += line.chars().count() + 1;
4769 }
4770 idx + pos.1
4771 };
4772 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4773 for (r, line) in lines.iter().enumerate() {
4774 let len = line.chars().count();
4775 if idx <= len {
4776 return (r, idx);
4777 }
4778 idx -= len + 1;
4779 }
4780 let last = lines.len().saturating_sub(1);
4781 (last, lines[last].chars().count())
4782 };
4783 let mut chars: Vec<char> = Vec::new();
4784 for (r, line) in lines.iter().enumerate() {
4785 chars.extend(line.chars());
4786 if r + 1 < lines.len() {
4787 chars.push('\n');
4788 }
4789 }
4790 let cursor_idx = pos_to_idx(ed.cursor());
4791
4792 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4800 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4801 let mut i = 0;
4802 while i < chars.len() {
4803 if chars[i] != '<' {
4804 i += 1;
4805 continue;
4806 }
4807 let mut j = i + 1;
4808 while j < chars.len() && chars[j] != '>' {
4809 j += 1;
4810 }
4811 if j >= chars.len() {
4812 break;
4813 }
4814 let inside: String = chars[i + 1..j].iter().collect();
4815 let close_end = j + 1;
4816 let trimmed = inside.trim();
4817 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4818 i = close_end;
4819 continue;
4820 }
4821 if let Some(rest) = trimmed.strip_prefix('/') {
4822 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4823 if !name.is_empty()
4824 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4825 {
4826 let (open_start, content_start, _) = stack[stack_idx].clone();
4827 stack.truncate(stack_idx);
4828 let content_end = i;
4829 let candidate = (open_start, content_start, content_end, close_end);
4830 if cursor_idx >= content_start && cursor_idx <= content_end {
4831 innermost = match innermost {
4832 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4833 Some(candidate)
4834 }
4835 None => Some(candidate),
4836 existing => existing,
4837 };
4838 } else if open_start >= cursor_idx && next_after.is_none() {
4839 next_after = Some(candidate);
4840 }
4841 }
4842 } else if !trimmed.ends_with('/') {
4843 let name: String = trimmed
4844 .split(|c: char| c.is_whitespace() || c == '/')
4845 .next()
4846 .unwrap_or("")
4847 .to_string();
4848 if !name.is_empty() {
4849 stack.push((i, close_end, name));
4850 }
4851 }
4852 i = close_end;
4853 }
4854
4855 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4856 if inner {
4857 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4858 } else {
4859 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4860 }
4861}
4862
4863fn is_wordchar(c: char) -> bool {
4864 c.is_alphanumeric() || c == '_'
4865}
4866
4867pub(crate) use hjkl_buffer::is_keyword_char;
4871
4872fn word_text_object<H: crate::types::Host>(
4873 ed: &Editor<hjkl_buffer::Buffer, H>,
4874 inner: bool,
4875 big: bool,
4876) -> Option<((usize, usize), (usize, usize))> {
4877 let (row, col) = ed.cursor();
4878 let line = buf_line(&ed.buffer, row)?;
4879 let chars: Vec<char> = line.chars().collect();
4880 if chars.is_empty() {
4881 return None;
4882 }
4883 let at = col.min(chars.len().saturating_sub(1));
4884 let classify = |c: char| -> u8 {
4885 if c.is_whitespace() {
4886 0
4887 } else if big || is_wordchar(c) {
4888 1
4889 } else {
4890 2
4891 }
4892 };
4893 let cls = classify(chars[at]);
4894 let mut start = at;
4895 while start > 0 && classify(chars[start - 1]) == cls {
4896 start -= 1;
4897 }
4898 let mut end = at;
4899 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4900 end += 1;
4901 }
4902 let char_byte = |i: usize| {
4904 if i >= chars.len() {
4905 line.len()
4906 } else {
4907 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4908 }
4909 };
4910 let mut start_col = char_byte(start);
4911 let mut end_col = char_byte(end + 1);
4913 if !inner {
4914 let mut t = end + 1;
4916 let mut included_trailing = false;
4917 while t < chars.len() && chars[t].is_whitespace() {
4918 included_trailing = true;
4919 t += 1;
4920 }
4921 if included_trailing {
4922 end_col = char_byte(t);
4923 } else {
4924 let mut s = start;
4925 while s > 0 && chars[s - 1].is_whitespace() {
4926 s -= 1;
4927 }
4928 start_col = char_byte(s);
4929 }
4930 }
4931 Some(((row, start_col), (row, end_col)))
4932}
4933
4934fn quote_text_object<H: crate::types::Host>(
4935 ed: &Editor<hjkl_buffer::Buffer, H>,
4936 q: char,
4937 inner: bool,
4938) -> Option<((usize, usize), (usize, usize))> {
4939 let (row, col) = ed.cursor();
4940 let line = buf_line(&ed.buffer, row)?;
4941 let bytes = line.as_bytes();
4942 let q_byte = q as u8;
4943 let mut positions: Vec<usize> = Vec::new();
4945 for (i, &b) in bytes.iter().enumerate() {
4946 if b == q_byte {
4947 positions.push(i);
4948 }
4949 }
4950 if positions.len() < 2 {
4951 return None;
4952 }
4953 let mut open_idx: Option<usize> = None;
4954 let mut close_idx: Option<usize> = None;
4955 for pair in positions.chunks(2) {
4956 if pair.len() < 2 {
4957 break;
4958 }
4959 if col >= pair[0] && col <= pair[1] {
4960 open_idx = Some(pair[0]);
4961 close_idx = Some(pair[1]);
4962 break;
4963 }
4964 if col < pair[0] {
4965 open_idx = Some(pair[0]);
4966 close_idx = Some(pair[1]);
4967 break;
4968 }
4969 }
4970 let open = open_idx?;
4971 let close = close_idx?;
4972 if inner {
4974 if close <= open + 1 {
4975 return None;
4976 }
4977 Some(((row, open + 1), (row, close)))
4978 } else {
4979 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
4986 let mut end = after_close;
4988 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
4989 end += 1;
4990 }
4991 Some(((row, open), (row, end)))
4992 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
4993 let mut start = open;
4995 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
4996 start -= 1;
4997 }
4998 Some(((row, start), (row, close + 1)))
4999 } else {
5000 Some(((row, open), (row, close + 1)))
5001 }
5002 }
5003}
5004
5005fn bracket_text_object<H: crate::types::Host>(
5006 ed: &Editor<hjkl_buffer::Buffer, H>,
5007 open: char,
5008 inner: bool,
5009) -> Option<(Pos, Pos, MotionKind)> {
5010 let close = match open {
5011 '(' => ')',
5012 '[' => ']',
5013 '{' => '}',
5014 '<' => '>',
5015 _ => return None,
5016 };
5017 let (row, col) = ed.cursor();
5018 let lines = buf_lines_to_vec(&ed.buffer);
5019 let lines = lines.as_slice();
5020 let open_pos = find_open_bracket(lines, row, col, open, close)
5025 .or_else(|| find_next_open(lines, row, col, open))?;
5026 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5027 if inner {
5029 if close_pos.0 > open_pos.0 + 1 {
5035 let inner_row_start = open_pos.0 + 1;
5037 let inner_row_end = close_pos.0 - 1;
5038 let end_col = lines
5039 .get(inner_row_end)
5040 .map(|l| l.chars().count())
5041 .unwrap_or(0);
5042 return Some((
5043 (inner_row_start, 0),
5044 (inner_row_end, end_col),
5045 MotionKind::Linewise,
5046 ));
5047 }
5048 let inner_start = advance_pos(lines, open_pos);
5049 if inner_start.0 > close_pos.0
5050 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5051 {
5052 return None;
5053 }
5054 Some((inner_start, close_pos, MotionKind::Exclusive))
5055 } else {
5056 Some((
5057 open_pos,
5058 advance_pos(lines, close_pos),
5059 MotionKind::Exclusive,
5060 ))
5061 }
5062}
5063
5064fn find_open_bracket(
5065 lines: &[String],
5066 row: usize,
5067 col: usize,
5068 open: char,
5069 close: char,
5070) -> Option<(usize, usize)> {
5071 let mut depth: i32 = 0;
5072 let mut r = row;
5073 let mut c = col as isize;
5074 loop {
5075 let cur = &lines[r];
5076 let chars: Vec<char> = cur.chars().collect();
5077 if (c as usize) >= chars.len() {
5081 c = chars.len() as isize - 1;
5082 }
5083 while c >= 0 {
5084 let ch = chars[c as usize];
5085 if ch == close {
5086 depth += 1;
5087 } else if ch == open {
5088 if depth == 0 {
5089 return Some((r, c as usize));
5090 }
5091 depth -= 1;
5092 }
5093 c -= 1;
5094 }
5095 if r == 0 {
5096 return None;
5097 }
5098 r -= 1;
5099 c = lines[r].chars().count() as isize - 1;
5100 }
5101}
5102
5103fn find_close_bracket(
5104 lines: &[String],
5105 row: usize,
5106 start_col: usize,
5107 open: char,
5108 close: char,
5109) -> Option<(usize, usize)> {
5110 let mut depth: i32 = 0;
5111 let mut r = row;
5112 let mut c = start_col;
5113 loop {
5114 let cur = &lines[r];
5115 let chars: Vec<char> = cur.chars().collect();
5116 while c < chars.len() {
5117 let ch = chars[c];
5118 if ch == open {
5119 depth += 1;
5120 } else if ch == close {
5121 if depth == 0 {
5122 return Some((r, c));
5123 }
5124 depth -= 1;
5125 }
5126 c += 1;
5127 }
5128 if r + 1 >= lines.len() {
5129 return None;
5130 }
5131 r += 1;
5132 c = 0;
5133 }
5134}
5135
5136fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5140 let mut r = row;
5141 let mut c = col;
5142 while r < lines.len() {
5143 let chars: Vec<char> = lines[r].chars().collect();
5144 while c < chars.len() {
5145 if chars[c] == open {
5146 return Some((r, c));
5147 }
5148 c += 1;
5149 }
5150 r += 1;
5151 c = 0;
5152 }
5153 None
5154}
5155
5156fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5157 let (r, c) = pos;
5158 let line_len = lines[r].chars().count();
5159 if c < line_len {
5160 (r, c + 1)
5161 } else if r + 1 < lines.len() {
5162 (r + 1, 0)
5163 } else {
5164 pos
5165 }
5166}
5167
5168fn paragraph_text_object<H: crate::types::Host>(
5169 ed: &Editor<hjkl_buffer::Buffer, H>,
5170 inner: bool,
5171) -> Option<((usize, usize), (usize, usize))> {
5172 let (row, _) = ed.cursor();
5173 let lines = buf_lines_to_vec(&ed.buffer);
5174 if lines.is_empty() {
5175 return None;
5176 }
5177 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5179 if is_blank(row) {
5180 return None;
5181 }
5182 let mut top = row;
5183 while top > 0 && !is_blank(top - 1) {
5184 top -= 1;
5185 }
5186 let mut bot = row;
5187 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5188 bot += 1;
5189 }
5190 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5192 bot += 1;
5193 }
5194 let end_col = lines[bot].chars().count();
5195 Some(((top, 0), (bot, end_col)))
5196}
5197
5198fn read_vim_range<H: crate::types::Host>(
5204 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5205 start: (usize, usize),
5206 end: (usize, usize),
5207 kind: MotionKind,
5208) -> String {
5209 let (top, bot) = order(start, end);
5210 ed.sync_buffer_content_from_textarea();
5211 let lines = buf_lines_to_vec(&ed.buffer);
5212 match kind {
5213 MotionKind::Linewise => {
5214 let lo = top.0;
5215 let hi = bot.0.min(lines.len().saturating_sub(1));
5216 let mut text = lines[lo..=hi].join("\n");
5217 text.push('\n');
5218 text
5219 }
5220 MotionKind::Inclusive | MotionKind::Exclusive => {
5221 let inclusive = matches!(kind, MotionKind::Inclusive);
5222 let mut out = String::new();
5224 for row in top.0..=bot.0 {
5225 let line = lines.get(row).map(String::as_str).unwrap_or("");
5226 let lo = if row == top.0 { top.1 } else { 0 };
5227 let hi_unclamped = if row == bot.0 {
5228 if inclusive { bot.1 + 1 } else { bot.1 }
5229 } else {
5230 line.chars().count() + 1
5231 };
5232 let row_chars: Vec<char> = line.chars().collect();
5233 let hi = hi_unclamped.min(row_chars.len());
5234 if lo < hi {
5235 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5236 }
5237 if row < bot.0 {
5238 out.push('\n');
5239 }
5240 }
5241 out
5242 }
5243 }
5244}
5245
5246fn cut_vim_range<H: crate::types::Host>(
5255 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5256 start: (usize, usize),
5257 end: (usize, usize),
5258 kind: MotionKind,
5259) -> String {
5260 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5261 let (top, bot) = order(start, end);
5262 ed.sync_buffer_content_from_textarea();
5263 let (buf_start, buf_end, buf_kind) = match kind {
5264 MotionKind::Linewise => (
5265 Position::new(top.0, 0),
5266 Position::new(bot.0, 0),
5267 BufKind::Line,
5268 ),
5269 MotionKind::Inclusive => {
5270 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5271 let next = if bot.1 < line_chars {
5275 Position::new(bot.0, bot.1 + 1)
5276 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5277 Position::new(bot.0 + 1, 0)
5278 } else {
5279 Position::new(bot.0, line_chars)
5280 };
5281 (Position::new(top.0, top.1), next, BufKind::Char)
5282 }
5283 MotionKind::Exclusive => (
5284 Position::new(top.0, top.1),
5285 Position::new(bot.0, bot.1),
5286 BufKind::Char,
5287 ),
5288 };
5289 let inverse = ed.mutate_edit(Edit::DeleteRange {
5290 start: buf_start,
5291 end: buf_end,
5292 kind: buf_kind,
5293 });
5294 let text = match inverse {
5295 Edit::InsertStr { text, .. } => text,
5296 _ => String::new(),
5297 };
5298 if !text.is_empty() {
5299 ed.record_yank_to_host(text.clone());
5300 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5301 }
5302 ed.push_buffer_cursor_to_textarea();
5303 text
5304}
5305
5306fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5312 use hjkl_buffer::{Edit, MotionKind, Position};
5313 ed.sync_buffer_content_from_textarea();
5314 let cursor = buf_cursor_pos(&ed.buffer);
5315 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5316 if cursor.col >= line_chars {
5317 return;
5318 }
5319 let inverse = ed.mutate_edit(Edit::DeleteRange {
5320 start: cursor,
5321 end: Position::new(cursor.row, line_chars),
5322 kind: MotionKind::Char,
5323 });
5324 if let Edit::InsertStr { text, .. } = inverse
5325 && !text.is_empty()
5326 {
5327 ed.record_yank_to_host(text.clone());
5328 ed.vim.yank_linewise = false;
5329 ed.set_yank(text);
5330 }
5331 buf_set_cursor_pos(&mut ed.buffer, cursor);
5332 ed.push_buffer_cursor_to_textarea();
5333}
5334
5335fn do_char_delete<H: crate::types::Host>(
5336 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5337 forward: bool,
5338 count: usize,
5339) {
5340 use hjkl_buffer::{Edit, MotionKind, Position};
5341 ed.push_undo();
5342 ed.sync_buffer_content_from_textarea();
5343 let mut deleted = String::new();
5346 for _ in 0..count {
5347 let cursor = buf_cursor_pos(&ed.buffer);
5348 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5349 if forward {
5350 if cursor.col >= line_chars {
5353 continue;
5354 }
5355 let inverse = ed.mutate_edit(Edit::DeleteRange {
5356 start: cursor,
5357 end: Position::new(cursor.row, cursor.col + 1),
5358 kind: MotionKind::Char,
5359 });
5360 if let Edit::InsertStr { text, .. } = inverse {
5361 deleted.push_str(&text);
5362 }
5363 } else {
5364 if cursor.col == 0 {
5366 continue;
5367 }
5368 let inverse = ed.mutate_edit(Edit::DeleteRange {
5369 start: Position::new(cursor.row, cursor.col - 1),
5370 end: cursor,
5371 kind: MotionKind::Char,
5372 });
5373 if let Edit::InsertStr { text, .. } = inverse {
5374 deleted = text + &deleted;
5377 }
5378 }
5379 }
5380 if !deleted.is_empty() {
5381 ed.record_yank_to_host(deleted.clone());
5382 ed.record_delete(deleted, false);
5383 }
5384 ed.push_buffer_cursor_to_textarea();
5385}
5386
5387fn adjust_number<H: crate::types::Host>(
5391 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5392 delta: i64,
5393) -> bool {
5394 use hjkl_buffer::{Edit, MotionKind, Position};
5395 ed.sync_buffer_content_from_textarea();
5396 let cursor = buf_cursor_pos(&ed.buffer);
5397 let row = cursor.row;
5398 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5399 Some(l) => l.chars().collect(),
5400 None => return false,
5401 };
5402 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5403 return false;
5404 };
5405 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5406 digit_start - 1
5407 } else {
5408 digit_start
5409 };
5410 let mut span_end = digit_start;
5411 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5412 span_end += 1;
5413 }
5414 let s: String = chars[span_start..span_end].iter().collect();
5415 let Ok(n) = s.parse::<i64>() else {
5416 return false;
5417 };
5418 let new_s = n.saturating_add(delta).to_string();
5419
5420 ed.push_undo();
5421 let span_start_pos = Position::new(row, span_start);
5422 let span_end_pos = Position::new(row, span_end);
5423 ed.mutate_edit(Edit::DeleteRange {
5424 start: span_start_pos,
5425 end: span_end_pos,
5426 kind: MotionKind::Char,
5427 });
5428 ed.mutate_edit(Edit::InsertStr {
5429 at: span_start_pos,
5430 text: new_s.clone(),
5431 });
5432 let new_len = new_s.chars().count();
5433 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5434 ed.push_buffer_cursor_to_textarea();
5435 true
5436}
5437
5438pub(crate) fn replace_char<H: crate::types::Host>(
5439 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5440 ch: char,
5441 count: usize,
5442) {
5443 use hjkl_buffer::{Edit, MotionKind, Position};
5444 ed.push_undo();
5445 ed.sync_buffer_content_from_textarea();
5446 for _ in 0..count {
5447 let cursor = buf_cursor_pos(&ed.buffer);
5448 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5449 if cursor.col >= line_chars {
5450 break;
5451 }
5452 ed.mutate_edit(Edit::DeleteRange {
5453 start: cursor,
5454 end: Position::new(cursor.row, cursor.col + 1),
5455 kind: MotionKind::Char,
5456 });
5457 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5458 }
5459 crate::motions::move_left(&mut ed.buffer, 1);
5461 ed.push_buffer_cursor_to_textarea();
5462}
5463
5464fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5465 use hjkl_buffer::{Edit, MotionKind, Position};
5466 ed.sync_buffer_content_from_textarea();
5467 let cursor = buf_cursor_pos(&ed.buffer);
5468 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5469 return;
5470 };
5471 let toggled = if c.is_uppercase() {
5472 c.to_lowercase().next().unwrap_or(c)
5473 } else {
5474 c.to_uppercase().next().unwrap_or(c)
5475 };
5476 ed.mutate_edit(Edit::DeleteRange {
5477 start: cursor,
5478 end: Position::new(cursor.row, cursor.col + 1),
5479 kind: MotionKind::Char,
5480 });
5481 ed.mutate_edit(Edit::InsertChar {
5482 at: cursor,
5483 ch: toggled,
5484 });
5485}
5486
5487fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5488 use hjkl_buffer::{Edit, Position};
5489 ed.sync_buffer_content_from_textarea();
5490 let row = buf_cursor_pos(&ed.buffer).row;
5491 if row + 1 >= buf_row_count(&ed.buffer) {
5492 return;
5493 }
5494 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5495 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5496 let next_trimmed = next_raw.trim_start();
5497 let cur_chars = cur_line.chars().count();
5498 let next_chars = next_raw.chars().count();
5499 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5502 " "
5503 } else {
5504 ""
5505 };
5506 let joined = format!("{cur_line}{separator}{next_trimmed}");
5507 ed.mutate_edit(Edit::Replace {
5508 start: Position::new(row, 0),
5509 end: Position::new(row + 1, next_chars),
5510 with: joined,
5511 });
5512 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5516 ed.push_buffer_cursor_to_textarea();
5517}
5518
5519fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5522 use hjkl_buffer::Edit;
5523 ed.sync_buffer_content_from_textarea();
5524 let row = buf_cursor_pos(&ed.buffer).row;
5525 if row + 1 >= buf_row_count(&ed.buffer) {
5526 return;
5527 }
5528 let join_col = buf_line_chars(&ed.buffer, row);
5529 ed.mutate_edit(Edit::JoinLines {
5530 row,
5531 count: 1,
5532 with_space: false,
5533 });
5534 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5536 ed.push_buffer_cursor_to_textarea();
5537}
5538
5539fn do_paste<H: crate::types::Host>(
5540 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5541 before: bool,
5542 count: usize,
5543) {
5544 use hjkl_buffer::{Edit, Position};
5545 ed.push_undo();
5546 let selector = ed.vim.pending_register.take();
5551 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5552 Some(slot) => (slot.text.clone(), slot.linewise),
5553 None => {
5559 let s = &ed.registers().unnamed;
5560 (s.text.clone(), s.linewise)
5561 }
5562 };
5563 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5567 for _ in 0..count {
5568 ed.sync_buffer_content_from_textarea();
5569 let yank = yank.clone();
5570 if yank.is_empty() {
5571 continue;
5572 }
5573 if linewise {
5574 let text = yank.trim_matches('\n').to_string();
5578 let row = buf_cursor_pos(&ed.buffer).row;
5579 let target_row = if before {
5580 ed.mutate_edit(Edit::InsertStr {
5581 at: Position::new(row, 0),
5582 text: format!("{text}\n"),
5583 });
5584 row
5585 } else {
5586 let line_chars = buf_line_chars(&ed.buffer, row);
5587 ed.mutate_edit(Edit::InsertStr {
5588 at: Position::new(row, line_chars),
5589 text: format!("\n{text}"),
5590 });
5591 row + 1
5592 };
5593 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5594 crate::motions::move_first_non_blank(&mut ed.buffer);
5595 ed.push_buffer_cursor_to_textarea();
5596 let payload_lines = text.lines().count().max(1);
5598 let bot_row = target_row + payload_lines - 1;
5599 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5600 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5601 } else {
5602 let cursor = buf_cursor_pos(&ed.buffer);
5606 let at = if before {
5607 cursor
5608 } else {
5609 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5610 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5611 };
5612 ed.mutate_edit(Edit::InsertStr {
5613 at,
5614 text: yank.clone(),
5615 });
5616 crate::motions::move_left(&mut ed.buffer, 1);
5619 ed.push_buffer_cursor_to_textarea();
5620 let lo = (at.row, at.col);
5622 let hi = ed.cursor();
5623 paste_mark = Some((lo, hi));
5624 }
5625 }
5626 if let Some((lo, hi)) = paste_mark {
5627 ed.set_mark('[', lo);
5628 ed.set_mark(']', hi);
5629 }
5630 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5632}
5633
5634pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5635 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5636 let current = ed.snapshot();
5637 ed.redo_stack.push(current);
5638 ed.restore(lines, cursor);
5639 }
5640 ed.vim.mode = Mode::Normal;
5641 clamp_cursor_to_normal_mode(ed);
5645}
5646
5647pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5648 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5649 let current = ed.snapshot();
5650 ed.undo_stack.push(current);
5651 ed.cap_undo();
5652 ed.restore(lines, cursor);
5653 }
5654 ed.vim.mode = Mode::Normal;
5655}
5656
5657fn replay_insert_and_finish<H: crate::types::Host>(
5664 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5665 text: &str,
5666) {
5667 use hjkl_buffer::{Edit, Position};
5668 let cursor = ed.cursor();
5669 ed.mutate_edit(Edit::InsertStr {
5670 at: Position::new(cursor.0, cursor.1),
5671 text: text.to_string(),
5672 });
5673 if ed.vim.insert_session.take().is_some() {
5674 if ed.cursor().1 > 0 {
5675 crate::motions::move_left(&mut ed.buffer, 1);
5676 ed.push_buffer_cursor_to_textarea();
5677 }
5678 ed.vim.mode = Mode::Normal;
5679 }
5680}
5681
5682fn replay_last_change<H: crate::types::Host>(
5683 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5684 outer_count: usize,
5685) {
5686 let Some(change) = ed.vim.last_change.clone() else {
5687 return;
5688 };
5689 ed.vim.replaying = true;
5690 let scale = if outer_count > 0 { outer_count } else { 1 };
5691 match change {
5692 LastChange::OpMotion {
5693 op,
5694 motion,
5695 count,
5696 inserted,
5697 } => {
5698 let total = count.max(1) * scale;
5699 apply_op_with_motion(ed, op, &motion, total);
5700 if let Some(text) = inserted {
5701 replay_insert_and_finish(ed, &text);
5702 }
5703 }
5704 LastChange::OpTextObj {
5705 op,
5706 obj,
5707 inner,
5708 inserted,
5709 } => {
5710 apply_op_with_text_object(ed, op, obj, inner);
5711 if let Some(text) = inserted {
5712 replay_insert_and_finish(ed, &text);
5713 }
5714 }
5715 LastChange::LineOp {
5716 op,
5717 count,
5718 inserted,
5719 } => {
5720 let total = count.max(1) * scale;
5721 execute_line_op(ed, op, total);
5722 if let Some(text) = inserted {
5723 replay_insert_and_finish(ed, &text);
5724 }
5725 }
5726 LastChange::CharDel { forward, count } => {
5727 do_char_delete(ed, forward, count * scale);
5728 }
5729 LastChange::ReplaceChar { ch, count } => {
5730 replace_char(ed, ch, count * scale);
5731 }
5732 LastChange::ToggleCase { count } => {
5733 for _ in 0..count * scale {
5734 ed.push_undo();
5735 toggle_case_at_cursor(ed);
5736 }
5737 }
5738 LastChange::JoinLine { count } => {
5739 for _ in 0..count * scale {
5740 ed.push_undo();
5741 join_line(ed);
5742 }
5743 }
5744 LastChange::Paste { before, count } => {
5745 do_paste(ed, before, count * scale);
5746 }
5747 LastChange::DeleteToEol { inserted } => {
5748 use hjkl_buffer::{Edit, Position};
5749 ed.push_undo();
5750 delete_to_eol(ed);
5751 if let Some(text) = inserted {
5752 let cursor = ed.cursor();
5753 ed.mutate_edit(Edit::InsertStr {
5754 at: Position::new(cursor.0, cursor.1),
5755 text,
5756 });
5757 }
5758 }
5759 LastChange::OpenLine { above, inserted } => {
5760 use hjkl_buffer::{Edit, Position};
5761 ed.push_undo();
5762 ed.sync_buffer_content_from_textarea();
5763 let row = buf_cursor_pos(&ed.buffer).row;
5764 if above {
5765 ed.mutate_edit(Edit::InsertStr {
5766 at: Position::new(row, 0),
5767 text: "\n".to_string(),
5768 });
5769 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5770 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5771 } else {
5772 let line_chars = buf_line_chars(&ed.buffer, row);
5773 ed.mutate_edit(Edit::InsertStr {
5774 at: Position::new(row, line_chars),
5775 text: "\n".to_string(),
5776 });
5777 }
5778 ed.push_buffer_cursor_to_textarea();
5779 let cursor = ed.cursor();
5780 ed.mutate_edit(Edit::InsertStr {
5781 at: Position::new(cursor.0, cursor.1),
5782 text: inserted,
5783 });
5784 }
5785 LastChange::InsertAt {
5786 entry,
5787 inserted,
5788 count,
5789 } => {
5790 use hjkl_buffer::{Edit, Position};
5791 ed.push_undo();
5792 match entry {
5793 InsertEntry::I => {}
5794 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5795 InsertEntry::A => {
5796 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5797 ed.push_buffer_cursor_to_textarea();
5798 }
5799 InsertEntry::ShiftA => {
5800 crate::motions::move_line_end(&mut ed.buffer);
5801 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5802 ed.push_buffer_cursor_to_textarea();
5803 }
5804 }
5805 for _ in 0..count.max(1) {
5806 let cursor = ed.cursor();
5807 ed.mutate_edit(Edit::InsertStr {
5808 at: Position::new(cursor.0, cursor.1),
5809 text: inserted.clone(),
5810 });
5811 }
5812 }
5813 }
5814 ed.vim.replaying = false;
5815}
5816
5817fn extract_inserted(before: &str, after: &str) -> String {
5820 let before_chars: Vec<char> = before.chars().collect();
5821 let after_chars: Vec<char> = after.chars().collect();
5822 if after_chars.len() <= before_chars.len() {
5823 return String::new();
5824 }
5825 let prefix = before_chars
5826 .iter()
5827 .zip(after_chars.iter())
5828 .take_while(|(a, b)| a == b)
5829 .count();
5830 let max_suffix = before_chars.len() - prefix;
5831 let suffix = before_chars
5832 .iter()
5833 .rev()
5834 .zip(after_chars.iter().rev())
5835 .take(max_suffix)
5836 .take_while(|(a, b)| a == b)
5837 .count();
5838 after_chars[prefix..after_chars.len() - suffix]
5839 .iter()
5840 .collect()
5841}
5842
5843#[cfg(all(test, feature = "crossterm"))]
5846mod tests {
5847 use crate::VimMode;
5848 use crate::editor::Editor;
5849 use crate::types::Host;
5850 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5851
5852 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5853 let mut iter = keys.chars().peekable();
5857 while let Some(c) = iter.next() {
5858 if c == '<' {
5859 let mut tag = String::new();
5860 for ch in iter.by_ref() {
5861 if ch == '>' {
5862 break;
5863 }
5864 tag.push(ch);
5865 }
5866 let ev = match tag.as_str() {
5867 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5868 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5869 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5870 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5871 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5872 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5873 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5874 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5875 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5879 s if s.starts_with("C-") => {
5880 let ch = s.chars().nth(2).unwrap();
5881 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5882 }
5883 _ => continue,
5884 };
5885 e.handle_key(ev);
5886 } else {
5887 let mods = if c.is_uppercase() {
5888 KeyModifiers::SHIFT
5889 } else {
5890 KeyModifiers::NONE
5891 };
5892 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5893 }
5894 }
5895 }
5896
5897 fn editor_with(content: &str) -> Editor {
5898 let opts = crate::types::Options {
5903 shiftwidth: 2,
5904 ..crate::types::Options::default()
5905 };
5906 let mut e = Editor::new(
5907 hjkl_buffer::Buffer::new(),
5908 crate::types::DefaultHost::new(),
5909 opts,
5910 );
5911 e.set_content(content);
5912 e
5913 }
5914
5915 #[test]
5916 fn f_char_jumps_on_line() {
5917 let mut e = editor_with("hello world");
5918 run_keys(&mut e, "fw");
5919 assert_eq!(e.cursor(), (0, 6));
5920 }
5921
5922 #[test]
5923 fn cap_f_jumps_backward() {
5924 let mut e = editor_with("hello world");
5925 e.jump_cursor(0, 10);
5926 run_keys(&mut e, "Fo");
5927 assert_eq!(e.cursor().1, 7);
5928 }
5929
5930 #[test]
5931 fn t_stops_before_char() {
5932 let mut e = editor_with("hello");
5933 run_keys(&mut e, "tl");
5934 assert_eq!(e.cursor(), (0, 1));
5935 }
5936
5937 #[test]
5938 fn semicolon_repeats_find() {
5939 let mut e = editor_with("aa.bb.cc");
5940 run_keys(&mut e, "f.");
5941 assert_eq!(e.cursor().1, 2);
5942 run_keys(&mut e, ";");
5943 assert_eq!(e.cursor().1, 5);
5944 }
5945
5946 #[test]
5947 fn comma_repeats_find_reverse() {
5948 let mut e = editor_with("aa.bb.cc");
5949 run_keys(&mut e, "f.");
5950 run_keys(&mut e, ";");
5951 run_keys(&mut e, ",");
5952 assert_eq!(e.cursor().1, 2);
5953 }
5954
5955 #[test]
5956 fn di_quote_deletes_content() {
5957 let mut e = editor_with("foo \"bar\" baz");
5958 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5960 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5961 }
5962
5963 #[test]
5964 fn da_quote_deletes_with_quotes() {
5965 let mut e = editor_with("foo \"bar\" baz");
5968 e.jump_cursor(0, 6);
5969 run_keys(&mut e, "da\"");
5970 assert_eq!(e.buffer().lines()[0], "foo baz");
5971 }
5972
5973 #[test]
5974 fn ci_paren_deletes_and_inserts() {
5975 let mut e = editor_with("fn(a, b, c)");
5976 e.jump_cursor(0, 5);
5977 run_keys(&mut e, "ci(");
5978 assert_eq!(e.vim_mode(), VimMode::Insert);
5979 assert_eq!(e.buffer().lines()[0], "fn()");
5980 }
5981
5982 #[test]
5983 fn diw_deletes_inner_word() {
5984 let mut e = editor_with("hello world");
5985 e.jump_cursor(0, 2);
5986 run_keys(&mut e, "diw");
5987 assert_eq!(e.buffer().lines()[0], " world");
5988 }
5989
5990 #[test]
5991 fn daw_deletes_word_with_trailing_space() {
5992 let mut e = editor_with("hello world");
5993 run_keys(&mut e, "daw");
5994 assert_eq!(e.buffer().lines()[0], "world");
5995 }
5996
5997 #[test]
5998 fn percent_jumps_to_matching_bracket() {
5999 let mut e = editor_with("foo(bar)");
6000 e.jump_cursor(0, 3);
6001 run_keys(&mut e, "%");
6002 assert_eq!(e.cursor().1, 7);
6003 run_keys(&mut e, "%");
6004 assert_eq!(e.cursor().1, 3);
6005 }
6006
6007 #[test]
6008 fn dot_repeats_last_change() {
6009 let mut e = editor_with("aaa bbb ccc");
6010 run_keys(&mut e, "dw");
6011 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6012 run_keys(&mut e, ".");
6013 assert_eq!(e.buffer().lines()[0], "ccc");
6014 }
6015
6016 #[test]
6017 fn dot_repeats_change_operator_with_text() {
6018 let mut e = editor_with("foo foo foo");
6019 run_keys(&mut e, "cwbar<Esc>");
6020 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6021 run_keys(&mut e, "w");
6023 run_keys(&mut e, ".");
6024 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6025 }
6026
6027 #[test]
6028 fn dot_repeats_x() {
6029 let mut e = editor_with("abcdef");
6030 run_keys(&mut e, "x");
6031 run_keys(&mut e, "..");
6032 assert_eq!(e.buffer().lines()[0], "def");
6033 }
6034
6035 #[test]
6036 fn count_operator_motion_compose() {
6037 let mut e = editor_with("one two three four five");
6038 run_keys(&mut e, "d3w");
6039 assert_eq!(e.buffer().lines()[0], "four five");
6040 }
6041
6042 #[test]
6043 fn two_dd_deletes_two_lines() {
6044 let mut e = editor_with("a\nb\nc");
6045 run_keys(&mut e, "2dd");
6046 assert_eq!(e.buffer().lines().len(), 1);
6047 assert_eq!(e.buffer().lines()[0], "c");
6048 }
6049
6050 #[test]
6055 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6056 let mut e = editor_with("one\ntwo\n three\nfour");
6057 e.jump_cursor(1, 2);
6058 run_keys(&mut e, "dd");
6059 assert_eq!(e.buffer().lines()[1], " three");
6061 assert_eq!(e.cursor(), (1, 4));
6062 }
6063
6064 #[test]
6065 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6066 let mut e = editor_with("one\n two\nthree");
6067 e.jump_cursor(2, 0);
6068 run_keys(&mut e, "dd");
6069 assert_eq!(e.buffer().lines().len(), 2);
6071 assert_eq!(e.cursor(), (1, 2));
6072 }
6073
6074 #[test]
6075 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6076 let mut e = editor_with("lonely");
6077 run_keys(&mut e, "dd");
6078 assert_eq!(e.buffer().lines().len(), 1);
6079 assert_eq!(e.buffer().lines()[0], "");
6080 assert_eq!(e.cursor(), (0, 0));
6081 }
6082
6083 #[test]
6084 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6085 let mut e = editor_with("a\nb\nc\n d\ne");
6086 e.jump_cursor(1, 0);
6088 run_keys(&mut e, "3dd");
6089 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6090 assert_eq!(e.cursor(), (1, 0));
6091 }
6092
6093 #[test]
6094 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6095 let mut e = editor_with(" line one\n line two\n xyz!");
6114 e.jump_cursor(0, 8);
6116 assert_eq!(e.cursor(), (0, 8));
6117 run_keys(&mut e, "dd");
6120 assert_eq!(
6121 e.cursor(),
6122 (0, 4),
6123 "dd must place cursor on first-non-blank"
6124 );
6125 run_keys(&mut e, "j");
6129 let (row, col) = e.cursor();
6130 assert_eq!(row, 1);
6131 assert_eq!(
6132 col, 4,
6133 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6134 );
6135 }
6136
6137 #[test]
6138 fn gu_lowercases_motion_range() {
6139 let mut e = editor_with("HELLO WORLD");
6140 run_keys(&mut e, "guw");
6141 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6142 assert_eq!(e.cursor(), (0, 0));
6143 }
6144
6145 #[test]
6146 fn g_u_uppercases_text_object() {
6147 let mut e = editor_with("hello world");
6148 run_keys(&mut e, "gUiw");
6150 assert_eq!(e.buffer().lines()[0], "HELLO world");
6151 assert_eq!(e.cursor(), (0, 0));
6152 }
6153
6154 #[test]
6155 fn g_tilde_toggles_case_of_range() {
6156 let mut e = editor_with("Hello World");
6157 run_keys(&mut e, "g~iw");
6158 assert_eq!(e.buffer().lines()[0], "hELLO World");
6159 }
6160
6161 #[test]
6162 fn g_uu_uppercases_current_line() {
6163 let mut e = editor_with("select 1\nselect 2");
6164 run_keys(&mut e, "gUU");
6165 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6166 assert_eq!(e.buffer().lines()[1], "select 2");
6167 }
6168
6169 #[test]
6170 fn gugu_lowercases_current_line() {
6171 let mut e = editor_with("FOO BAR\nBAZ");
6172 run_keys(&mut e, "gugu");
6173 assert_eq!(e.buffer().lines()[0], "foo bar");
6174 }
6175
6176 #[test]
6177 fn visual_u_uppercases_selection() {
6178 let mut e = editor_with("hello world");
6179 run_keys(&mut e, "veU");
6181 assert_eq!(e.buffer().lines()[0], "HELLO world");
6182 }
6183
6184 #[test]
6185 fn visual_line_u_lowercases_line() {
6186 let mut e = editor_with("HELLO WORLD\nOTHER");
6187 run_keys(&mut e, "Vu");
6188 assert_eq!(e.buffer().lines()[0], "hello world");
6189 assert_eq!(e.buffer().lines()[1], "OTHER");
6190 }
6191
6192 #[test]
6193 fn g_uu_with_count_uppercases_multiple_lines() {
6194 let mut e = editor_with("one\ntwo\nthree\nfour");
6195 run_keys(&mut e, "3gUU");
6197 assert_eq!(e.buffer().lines()[0], "ONE");
6198 assert_eq!(e.buffer().lines()[1], "TWO");
6199 assert_eq!(e.buffer().lines()[2], "THREE");
6200 assert_eq!(e.buffer().lines()[3], "four");
6201 }
6202
6203 #[test]
6204 fn double_gt_indents_current_line() {
6205 let mut e = editor_with("hello");
6206 run_keys(&mut e, ">>");
6207 assert_eq!(e.buffer().lines()[0], " hello");
6208 assert_eq!(e.cursor(), (0, 2));
6210 }
6211
6212 #[test]
6213 fn double_lt_outdents_current_line() {
6214 let mut e = editor_with(" hello");
6215 run_keys(&mut e, "<lt><lt>");
6216 assert_eq!(e.buffer().lines()[0], " hello");
6217 assert_eq!(e.cursor(), (0, 2));
6218 }
6219
6220 #[test]
6221 fn count_double_gt_indents_multiple_lines() {
6222 let mut e = editor_with("a\nb\nc\nd");
6223 run_keys(&mut e, "3>>");
6225 assert_eq!(e.buffer().lines()[0], " a");
6226 assert_eq!(e.buffer().lines()[1], " b");
6227 assert_eq!(e.buffer().lines()[2], " c");
6228 assert_eq!(e.buffer().lines()[3], "d");
6229 }
6230
6231 #[test]
6232 fn outdent_clips_ragged_leading_whitespace() {
6233 let mut e = editor_with(" x");
6236 run_keys(&mut e, "<lt><lt>");
6237 assert_eq!(e.buffer().lines()[0], "x");
6238 }
6239
6240 #[test]
6241 fn indent_motion_is_always_linewise() {
6242 let mut e = editor_with("foo bar");
6245 run_keys(&mut e, ">w");
6246 assert_eq!(e.buffer().lines()[0], " foo bar");
6247 }
6248
6249 #[test]
6250 fn indent_text_object_extends_over_paragraph() {
6251 let mut e = editor_with("a\nb\n\nc\nd");
6252 run_keys(&mut e, ">ap");
6254 assert_eq!(e.buffer().lines()[0], " a");
6255 assert_eq!(e.buffer().lines()[1], " b");
6256 assert_eq!(e.buffer().lines()[2], "");
6257 assert_eq!(e.buffer().lines()[3], "c");
6258 }
6259
6260 #[test]
6261 fn visual_line_indent_shifts_selected_rows() {
6262 let mut e = editor_with("x\ny\nz");
6263 run_keys(&mut e, "Vj>");
6265 assert_eq!(e.buffer().lines()[0], " x");
6266 assert_eq!(e.buffer().lines()[1], " y");
6267 assert_eq!(e.buffer().lines()[2], "z");
6268 }
6269
6270 #[test]
6271 fn outdent_empty_line_is_noop() {
6272 let mut e = editor_with("\nfoo");
6273 run_keys(&mut e, "<lt><lt>");
6274 assert_eq!(e.buffer().lines()[0], "");
6275 }
6276
6277 #[test]
6278 fn indent_skips_empty_lines() {
6279 let mut e = editor_with("");
6282 run_keys(&mut e, ">>");
6283 assert_eq!(e.buffer().lines()[0], "");
6284 }
6285
6286 #[test]
6287 fn insert_ctrl_t_indents_current_line() {
6288 let mut e = editor_with("x");
6289 run_keys(&mut e, "i<C-t>");
6291 assert_eq!(e.buffer().lines()[0], " x");
6292 assert_eq!(e.cursor(), (0, 2));
6295 }
6296
6297 #[test]
6298 fn insert_ctrl_d_outdents_current_line() {
6299 let mut e = editor_with(" x");
6300 run_keys(&mut e, "A<C-d>");
6302 assert_eq!(e.buffer().lines()[0], " x");
6303 }
6304
6305 #[test]
6306 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6307 let mut e = editor_with("first\nsecond");
6308 e.jump_cursor(1, 0);
6309 run_keys(&mut e, "h");
6310 assert_eq!(e.cursor(), (1, 0));
6312 }
6313
6314 #[test]
6315 fn l_at_last_char_does_not_wrap_to_next_line() {
6316 let mut e = editor_with("ab\ncd");
6317 e.jump_cursor(0, 1);
6319 run_keys(&mut e, "l");
6320 assert_eq!(e.cursor(), (0, 1));
6322 }
6323
6324 #[test]
6325 fn count_l_clamps_at_line_end() {
6326 let mut e = editor_with("abcde");
6327 run_keys(&mut e, "20l");
6330 assert_eq!(e.cursor(), (0, 4));
6331 }
6332
6333 #[test]
6334 fn count_h_clamps_at_col_zero() {
6335 let mut e = editor_with("abcde");
6336 e.jump_cursor(0, 3);
6337 run_keys(&mut e, "20h");
6338 assert_eq!(e.cursor(), (0, 0));
6339 }
6340
6341 #[test]
6342 fn dl_on_last_char_still_deletes_it() {
6343 let mut e = editor_with("ab");
6347 e.jump_cursor(0, 1);
6348 run_keys(&mut e, "dl");
6349 assert_eq!(e.buffer().lines()[0], "a");
6350 }
6351
6352 #[test]
6353 fn case_op_preserves_yank_register() {
6354 let mut e = editor_with("target");
6355 run_keys(&mut e, "yy");
6356 let yank_before = e.yank().to_string();
6357 run_keys(&mut e, "gUU");
6359 assert_eq!(e.buffer().lines()[0], "TARGET");
6360 assert_eq!(
6361 e.yank(),
6362 yank_before,
6363 "case ops must preserve the yank buffer"
6364 );
6365 }
6366
6367 #[test]
6368 fn dap_deletes_paragraph() {
6369 let mut e = editor_with("a\nb\n\nc\nd");
6370 run_keys(&mut e, "dap");
6371 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6372 }
6373
6374 #[test]
6375 fn dit_deletes_inner_tag_content() {
6376 let mut e = editor_with("<b>hello</b>");
6377 e.jump_cursor(0, 4);
6379 run_keys(&mut e, "dit");
6380 assert_eq!(e.buffer().lines()[0], "<b></b>");
6381 }
6382
6383 #[test]
6384 fn dat_deletes_around_tag() {
6385 let mut e = editor_with("hi <b>foo</b> bye");
6386 e.jump_cursor(0, 6);
6387 run_keys(&mut e, "dat");
6388 assert_eq!(e.buffer().lines()[0], "hi bye");
6389 }
6390
6391 #[test]
6392 fn dit_picks_innermost_tag() {
6393 let mut e = editor_with("<a><b>x</b></a>");
6394 e.jump_cursor(0, 6);
6396 run_keys(&mut e, "dit");
6397 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6399 }
6400
6401 #[test]
6402 fn dat_innermost_tag_pair() {
6403 let mut e = editor_with("<a><b>x</b></a>");
6404 e.jump_cursor(0, 6);
6405 run_keys(&mut e, "dat");
6406 assert_eq!(e.buffer().lines()[0], "<a></a>");
6407 }
6408
6409 #[test]
6410 fn dit_outside_any_tag_no_op() {
6411 let mut e = editor_with("plain text");
6412 e.jump_cursor(0, 3);
6413 run_keys(&mut e, "dit");
6414 assert_eq!(e.buffer().lines()[0], "plain text");
6416 }
6417
6418 #[test]
6419 fn cit_changes_inner_tag_content() {
6420 let mut e = editor_with("<b>hello</b>");
6421 e.jump_cursor(0, 4);
6422 run_keys(&mut e, "citNEW<Esc>");
6423 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6424 }
6425
6426 #[test]
6427 fn cat_changes_around_tag() {
6428 let mut e = editor_with("hi <b>foo</b> bye");
6429 e.jump_cursor(0, 6);
6430 run_keys(&mut e, "catBAR<Esc>");
6431 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6432 }
6433
6434 #[test]
6435 fn yit_yanks_inner_tag_content() {
6436 let mut e = editor_with("<b>hello</b>");
6437 e.jump_cursor(0, 4);
6438 run_keys(&mut e, "yit");
6439 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6440 }
6441
6442 #[test]
6443 fn yat_yanks_full_tag_pair() {
6444 let mut e = editor_with("hi <b>foo</b> bye");
6445 e.jump_cursor(0, 6);
6446 run_keys(&mut e, "yat");
6447 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6448 }
6449
6450 #[test]
6451 fn vit_visually_selects_inner_tag() {
6452 let mut e = editor_with("<b>hello</b>");
6453 e.jump_cursor(0, 4);
6454 run_keys(&mut e, "vit");
6455 assert_eq!(e.vim_mode(), VimMode::Visual);
6456 run_keys(&mut e, "y");
6457 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6458 }
6459
6460 #[test]
6461 fn vat_visually_selects_around_tag() {
6462 let mut e = editor_with("x<b>foo</b>y");
6463 e.jump_cursor(0, 5);
6464 run_keys(&mut e, "vat");
6465 assert_eq!(e.vim_mode(), VimMode::Visual);
6466 run_keys(&mut e, "y");
6467 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6468 }
6469
6470 #[test]
6473 #[allow(non_snake_case)]
6474 fn diW_deletes_inner_big_word() {
6475 let mut e = editor_with("foo.bar baz");
6476 e.jump_cursor(0, 2);
6477 run_keys(&mut e, "diW");
6478 assert_eq!(e.buffer().lines()[0], " baz");
6480 }
6481
6482 #[test]
6483 #[allow(non_snake_case)]
6484 fn daW_deletes_around_big_word() {
6485 let mut e = editor_with("foo.bar baz");
6486 e.jump_cursor(0, 2);
6487 run_keys(&mut e, "daW");
6488 assert_eq!(e.buffer().lines()[0], "baz");
6489 }
6490
6491 #[test]
6492 fn di_double_quote_deletes_inside() {
6493 let mut e = editor_with("a \"hello\" b");
6494 e.jump_cursor(0, 4);
6495 run_keys(&mut e, "di\"");
6496 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6497 }
6498
6499 #[test]
6500 fn da_double_quote_deletes_around() {
6501 let mut e = editor_with("a \"hello\" b");
6503 e.jump_cursor(0, 4);
6504 run_keys(&mut e, "da\"");
6505 assert_eq!(e.buffer().lines()[0], "a b");
6506 }
6507
6508 #[test]
6509 fn di_single_quote_deletes_inside() {
6510 let mut e = editor_with("x 'foo' y");
6511 e.jump_cursor(0, 4);
6512 run_keys(&mut e, "di'");
6513 assert_eq!(e.buffer().lines()[0], "x '' y");
6514 }
6515
6516 #[test]
6517 fn da_single_quote_deletes_around() {
6518 let mut e = editor_with("x 'foo' y");
6520 e.jump_cursor(0, 4);
6521 run_keys(&mut e, "da'");
6522 assert_eq!(e.buffer().lines()[0], "x y");
6523 }
6524
6525 #[test]
6526 fn di_backtick_deletes_inside() {
6527 let mut e = editor_with("p `q` r");
6528 e.jump_cursor(0, 3);
6529 run_keys(&mut e, "di`");
6530 assert_eq!(e.buffer().lines()[0], "p `` r");
6531 }
6532
6533 #[test]
6534 fn da_backtick_deletes_around() {
6535 let mut e = editor_with("p `q` r");
6537 e.jump_cursor(0, 3);
6538 run_keys(&mut e, "da`");
6539 assert_eq!(e.buffer().lines()[0], "p r");
6540 }
6541
6542 #[test]
6543 fn di_paren_deletes_inside() {
6544 let mut e = editor_with("f(arg)");
6545 e.jump_cursor(0, 3);
6546 run_keys(&mut e, "di(");
6547 assert_eq!(e.buffer().lines()[0], "f()");
6548 }
6549
6550 #[test]
6551 fn di_paren_alias_b_works() {
6552 let mut e = editor_with("f(arg)");
6553 e.jump_cursor(0, 3);
6554 run_keys(&mut e, "dib");
6555 assert_eq!(e.buffer().lines()[0], "f()");
6556 }
6557
6558 #[test]
6559 fn di_bracket_deletes_inside() {
6560 let mut e = editor_with("a[b,c]d");
6561 e.jump_cursor(0, 3);
6562 run_keys(&mut e, "di[");
6563 assert_eq!(e.buffer().lines()[0], "a[]d");
6564 }
6565
6566 #[test]
6567 fn da_bracket_deletes_around() {
6568 let mut e = editor_with("a[b,c]d");
6569 e.jump_cursor(0, 3);
6570 run_keys(&mut e, "da[");
6571 assert_eq!(e.buffer().lines()[0], "ad");
6572 }
6573
6574 #[test]
6575 fn di_brace_deletes_inside() {
6576 let mut e = editor_with("x{y}z");
6577 e.jump_cursor(0, 2);
6578 run_keys(&mut e, "di{");
6579 assert_eq!(e.buffer().lines()[0], "x{}z");
6580 }
6581
6582 #[test]
6583 fn da_brace_deletes_around() {
6584 let mut e = editor_with("x{y}z");
6585 e.jump_cursor(0, 2);
6586 run_keys(&mut e, "da{");
6587 assert_eq!(e.buffer().lines()[0], "xz");
6588 }
6589
6590 #[test]
6591 fn di_brace_alias_capital_b_works() {
6592 let mut e = editor_with("x{y}z");
6593 e.jump_cursor(0, 2);
6594 run_keys(&mut e, "diB");
6595 assert_eq!(e.buffer().lines()[0], "x{}z");
6596 }
6597
6598 #[test]
6599 fn di_angle_deletes_inside() {
6600 let mut e = editor_with("p<q>r");
6601 e.jump_cursor(0, 2);
6602 run_keys(&mut e, "di<lt>");
6604 assert_eq!(e.buffer().lines()[0], "p<>r");
6605 }
6606
6607 #[test]
6608 fn da_angle_deletes_around() {
6609 let mut e = editor_with("p<q>r");
6610 e.jump_cursor(0, 2);
6611 run_keys(&mut e, "da<lt>");
6612 assert_eq!(e.buffer().lines()[0], "pr");
6613 }
6614
6615 #[test]
6616 fn dip_deletes_inner_paragraph() {
6617 let mut e = editor_with("a\nb\nc\n\nd");
6618 e.jump_cursor(1, 0);
6619 run_keys(&mut e, "dip");
6620 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6623 }
6624
6625 #[test]
6628 fn sentence_motion_close_paren_jumps_forward() {
6629 let mut e = editor_with("Alpha. Beta. Gamma.");
6630 e.jump_cursor(0, 0);
6631 run_keys(&mut e, ")");
6632 assert_eq!(e.cursor(), (0, 7));
6634 run_keys(&mut e, ")");
6635 assert_eq!(e.cursor(), (0, 13));
6636 }
6637
6638 #[test]
6639 fn sentence_motion_open_paren_jumps_backward() {
6640 let mut e = editor_with("Alpha. Beta. Gamma.");
6641 e.jump_cursor(0, 13);
6642 run_keys(&mut e, "(");
6643 assert_eq!(e.cursor(), (0, 7));
6646 run_keys(&mut e, "(");
6647 assert_eq!(e.cursor(), (0, 0));
6648 }
6649
6650 #[test]
6651 fn sentence_motion_count() {
6652 let mut e = editor_with("A. B. C. D.");
6653 e.jump_cursor(0, 0);
6654 run_keys(&mut e, "3)");
6655 assert_eq!(e.cursor(), (0, 9));
6657 }
6658
6659 #[test]
6660 fn dis_deletes_inner_sentence() {
6661 let mut e = editor_with("First one. Second one. Third one.");
6662 e.jump_cursor(0, 13);
6663 run_keys(&mut e, "dis");
6664 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6666 }
6667
6668 #[test]
6669 fn das_deletes_around_sentence_with_trailing_space() {
6670 let mut e = editor_with("Alpha. Beta. Gamma.");
6671 e.jump_cursor(0, 8);
6672 run_keys(&mut e, "das");
6673 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6676 }
6677
6678 #[test]
6679 fn dis_handles_double_terminator() {
6680 let mut e = editor_with("Wow!? Next.");
6681 e.jump_cursor(0, 1);
6682 run_keys(&mut e, "dis");
6683 assert_eq!(e.buffer().lines()[0], " Next.");
6686 }
6687
6688 #[test]
6689 fn dis_first_sentence_from_cursor_at_zero() {
6690 let mut e = editor_with("Alpha. Beta.");
6691 e.jump_cursor(0, 0);
6692 run_keys(&mut e, "dis");
6693 assert_eq!(e.buffer().lines()[0], " Beta.");
6694 }
6695
6696 #[test]
6697 fn yis_yanks_inner_sentence() {
6698 let mut e = editor_with("Hello world. Bye.");
6699 e.jump_cursor(0, 5);
6700 run_keys(&mut e, "yis");
6701 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6702 }
6703
6704 #[test]
6705 fn vis_visually_selects_inner_sentence() {
6706 let mut e = editor_with("First. Second.");
6707 e.jump_cursor(0, 1);
6708 run_keys(&mut e, "vis");
6709 assert_eq!(e.vim_mode(), VimMode::Visual);
6710 run_keys(&mut e, "y");
6711 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6712 }
6713
6714 #[test]
6715 fn ciw_changes_inner_word() {
6716 let mut e = editor_with("hello world");
6717 e.jump_cursor(0, 1);
6718 run_keys(&mut e, "ciwHEY<Esc>");
6719 assert_eq!(e.buffer().lines()[0], "HEY world");
6720 }
6721
6722 #[test]
6723 fn yiw_yanks_inner_word() {
6724 let mut e = editor_with("hello world");
6725 e.jump_cursor(0, 1);
6726 run_keys(&mut e, "yiw");
6727 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6728 }
6729
6730 #[test]
6731 fn viw_selects_inner_word() {
6732 let mut e = editor_with("hello world");
6733 e.jump_cursor(0, 2);
6734 run_keys(&mut e, "viw");
6735 assert_eq!(e.vim_mode(), VimMode::Visual);
6736 run_keys(&mut e, "y");
6737 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6738 }
6739
6740 #[test]
6741 fn ci_paren_changes_inside() {
6742 let mut e = editor_with("f(old)");
6743 e.jump_cursor(0, 3);
6744 run_keys(&mut e, "ci(NEW<Esc>");
6745 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6746 }
6747
6748 #[test]
6749 fn yi_double_quote_yanks_inside() {
6750 let mut e = editor_with("say \"hi there\" then");
6751 e.jump_cursor(0, 6);
6752 run_keys(&mut e, "yi\"");
6753 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6754 }
6755
6756 #[test]
6757 fn vap_visual_selects_around_paragraph() {
6758 let mut e = editor_with("a\nb\n\nc");
6759 e.jump_cursor(0, 0);
6760 run_keys(&mut e, "vap");
6761 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6762 run_keys(&mut e, "y");
6763 let text = e.registers().read('"').unwrap().text.clone();
6765 assert!(text.starts_with("a\nb"));
6766 }
6767
6768 #[test]
6769 fn star_finds_next_occurrence() {
6770 let mut e = editor_with("foo bar foo baz");
6771 run_keys(&mut e, "*");
6772 assert_eq!(e.cursor().1, 8);
6773 }
6774
6775 #[test]
6776 fn star_skips_substring_match() {
6777 let mut e = editor_with("foo foobar baz");
6780 run_keys(&mut e, "*");
6781 assert_eq!(e.cursor().1, 0);
6782 }
6783
6784 #[test]
6785 fn g_star_matches_substring() {
6786 let mut e = editor_with("foo foobar baz");
6789 run_keys(&mut e, "g*");
6790 assert_eq!(e.cursor().1, 4);
6791 }
6792
6793 #[test]
6794 fn g_pound_matches_substring_backward() {
6795 let mut e = editor_with("foo foobar baz foo");
6798 run_keys(&mut e, "$b");
6799 assert_eq!(e.cursor().1, 15);
6800 run_keys(&mut e, "g#");
6801 assert_eq!(e.cursor().1, 4);
6802 }
6803
6804 #[test]
6805 fn n_repeats_last_search_forward() {
6806 let mut e = editor_with("foo bar foo baz foo");
6807 run_keys(&mut e, "/foo<CR>");
6810 assert_eq!(e.cursor().1, 8);
6811 run_keys(&mut e, "n");
6812 assert_eq!(e.cursor().1, 16);
6813 }
6814
6815 #[test]
6816 fn shift_n_reverses_search() {
6817 let mut e = editor_with("foo bar foo baz foo");
6818 run_keys(&mut e, "/foo<CR>");
6819 run_keys(&mut e, "n");
6820 assert_eq!(e.cursor().1, 16);
6821 run_keys(&mut e, "N");
6822 assert_eq!(e.cursor().1, 8);
6823 }
6824
6825 #[test]
6826 fn n_noop_without_pattern() {
6827 let mut e = editor_with("foo bar");
6828 run_keys(&mut e, "n");
6829 assert_eq!(e.cursor(), (0, 0));
6830 }
6831
6832 #[test]
6833 fn visual_line_preserves_cursor_column() {
6834 let mut e = editor_with("hello world\nanother one\nbye");
6837 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6839 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6840 assert_eq!(e.cursor(), (0, 5));
6841 run_keys(&mut e, "j");
6842 assert_eq!(e.cursor(), (1, 5));
6843 }
6844
6845 #[test]
6846 fn visual_line_yank_includes_trailing_newline() {
6847 let mut e = editor_with("aaa\nbbb\nccc");
6848 run_keys(&mut e, "Vjy");
6849 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6851 }
6852
6853 #[test]
6854 fn visual_line_yank_last_line_trailing_newline() {
6855 let mut e = editor_with("aaa\nbbb\nccc");
6856 run_keys(&mut e, "jj");
6858 run_keys(&mut e, "Vy");
6859 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6860 }
6861
6862 #[test]
6863 fn yy_on_last_line_has_trailing_newline() {
6864 let mut e = editor_with("aaa\nbbb\nccc");
6865 run_keys(&mut e, "jj");
6866 run_keys(&mut e, "yy");
6867 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6868 }
6869
6870 #[test]
6871 fn yy_in_middle_has_trailing_newline() {
6872 let mut e = editor_with("aaa\nbbb\nccc");
6873 run_keys(&mut e, "j");
6874 run_keys(&mut e, "yy");
6875 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6876 }
6877
6878 #[test]
6879 fn di_single_quote() {
6880 let mut e = editor_with("say 'hello world' now");
6881 e.jump_cursor(0, 7);
6882 run_keys(&mut e, "di'");
6883 assert_eq!(e.buffer().lines()[0], "say '' now");
6884 }
6885
6886 #[test]
6887 fn da_single_quote() {
6888 let mut e = editor_with("say 'hello' now");
6890 e.jump_cursor(0, 7);
6891 run_keys(&mut e, "da'");
6892 assert_eq!(e.buffer().lines()[0], "say now");
6893 }
6894
6895 #[test]
6896 fn di_backtick() {
6897 let mut e = editor_with("say `hi` now");
6898 e.jump_cursor(0, 5);
6899 run_keys(&mut e, "di`");
6900 assert_eq!(e.buffer().lines()[0], "say `` now");
6901 }
6902
6903 #[test]
6904 fn di_brace() {
6905 let mut e = editor_with("fn { a; b; c }");
6906 e.jump_cursor(0, 7);
6907 run_keys(&mut e, "di{");
6908 assert_eq!(e.buffer().lines()[0], "fn {}");
6909 }
6910
6911 #[test]
6912 fn di_bracket() {
6913 let mut e = editor_with("arr[1, 2, 3]");
6914 e.jump_cursor(0, 5);
6915 run_keys(&mut e, "di[");
6916 assert_eq!(e.buffer().lines()[0], "arr[]");
6917 }
6918
6919 #[test]
6920 fn dab_deletes_around_paren() {
6921 let mut e = editor_with("fn(a, b) + 1");
6922 e.jump_cursor(0, 4);
6923 run_keys(&mut e, "dab");
6924 assert_eq!(e.buffer().lines()[0], "fn + 1");
6925 }
6926
6927 #[test]
6928 fn da_big_b_deletes_around_brace() {
6929 let mut e = editor_with("x = {a: 1}");
6930 e.jump_cursor(0, 6);
6931 run_keys(&mut e, "daB");
6932 assert_eq!(e.buffer().lines()[0], "x = ");
6933 }
6934
6935 #[test]
6936 fn di_big_w_deletes_bigword() {
6937 let mut e = editor_with("foo-bar baz");
6938 e.jump_cursor(0, 2);
6939 run_keys(&mut e, "diW");
6940 assert_eq!(e.buffer().lines()[0], " baz");
6941 }
6942
6943 #[test]
6944 fn visual_select_inner_word() {
6945 let mut e = editor_with("hello world");
6946 e.jump_cursor(0, 2);
6947 run_keys(&mut e, "viw");
6948 assert_eq!(e.vim_mode(), VimMode::Visual);
6949 run_keys(&mut e, "y");
6950 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6951 }
6952
6953 #[test]
6954 fn visual_select_inner_quote() {
6955 let mut e = editor_with("foo \"bar\" baz");
6956 e.jump_cursor(0, 6);
6957 run_keys(&mut e, "vi\"");
6958 run_keys(&mut e, "y");
6959 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6960 }
6961
6962 #[test]
6963 fn visual_select_inner_paren() {
6964 let mut e = editor_with("fn(a, b)");
6965 e.jump_cursor(0, 4);
6966 run_keys(&mut e, "vi(");
6967 run_keys(&mut e, "y");
6968 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6969 }
6970
6971 #[test]
6972 fn visual_select_outer_brace() {
6973 let mut e = editor_with("{x}");
6974 e.jump_cursor(0, 1);
6975 run_keys(&mut e, "va{");
6976 run_keys(&mut e, "y");
6977 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6978 }
6979
6980 #[test]
6981 fn ci_paren_forward_scans_when_cursor_before_pair() {
6982 let mut e = editor_with("foo(bar)");
6985 e.jump_cursor(0, 0);
6986 run_keys(&mut e, "ci(NEW<Esc>");
6987 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
6988 }
6989
6990 #[test]
6991 fn ci_paren_forward_scans_across_lines() {
6992 let mut e = editor_with("first\nfoo(bar)\nlast");
6993 e.jump_cursor(0, 0);
6994 run_keys(&mut e, "ci(NEW<Esc>");
6995 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
6996 }
6997
6998 #[test]
6999 fn ci_brace_forward_scans_when_cursor_before_pair() {
7000 let mut e = editor_with("let x = {y};");
7001 e.jump_cursor(0, 0);
7002 run_keys(&mut e, "ci{NEW<Esc>");
7003 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7004 }
7005
7006 #[test]
7007 fn cit_forward_scans_when_cursor_before_tag() {
7008 let mut e = editor_with("text <b>hello</b> rest");
7011 e.jump_cursor(0, 0);
7012 run_keys(&mut e, "citNEW<Esc>");
7013 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7014 }
7015
7016 #[test]
7017 fn dat_forward_scans_when_cursor_before_tag() {
7018 let mut e = editor_with("text <b>hello</b> rest");
7020 e.jump_cursor(0, 0);
7021 run_keys(&mut e, "dat");
7022 assert_eq!(e.buffer().lines()[0], "text rest");
7023 }
7024
7025 #[test]
7026 fn ci_paren_still_works_when_cursor_inside() {
7027 let mut e = editor_with("fn(a, b)");
7030 e.jump_cursor(0, 4);
7031 run_keys(&mut e, "ci(NEW<Esc>");
7032 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7033 }
7034
7035 #[test]
7036 fn caw_changes_word_with_trailing_space() {
7037 let mut e = editor_with("hello world");
7038 run_keys(&mut e, "cawfoo<Esc>");
7039 assert_eq!(e.buffer().lines()[0], "fooworld");
7040 }
7041
7042 #[test]
7043 fn visual_char_yank_preserves_raw_text() {
7044 let mut e = editor_with("hello world");
7045 run_keys(&mut e, "vllly");
7046 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7047 }
7048
7049 #[test]
7050 fn single_line_visual_line_selects_full_line_on_yank() {
7051 let mut e = editor_with("hello world\nbye");
7052 run_keys(&mut e, "V");
7053 run_keys(&mut e, "y");
7056 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7057 }
7058
7059 #[test]
7060 fn visual_line_extends_both_directions() {
7061 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7062 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7064 assert_eq!(e.cursor(), (3, 0));
7065 run_keys(&mut e, "k");
7066 assert_eq!(e.cursor(), (2, 0));
7068 run_keys(&mut e, "k");
7069 assert_eq!(e.cursor(), (1, 0));
7070 }
7071
7072 #[test]
7073 fn visual_char_preserves_cursor_column() {
7074 let mut e = editor_with("hello world");
7075 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7077 assert_eq!(e.cursor(), (0, 5));
7078 run_keys(&mut e, "ll");
7079 assert_eq!(e.cursor(), (0, 7));
7080 }
7081
7082 #[test]
7083 fn visual_char_highlight_bounds_order() {
7084 let mut e = editor_with("abcdef");
7085 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7087 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7090 }
7091
7092 #[test]
7093 fn visual_line_highlight_bounds() {
7094 let mut e = editor_with("a\nb\nc");
7095 run_keys(&mut e, "V");
7096 assert_eq!(e.line_highlight(), Some((0, 0)));
7097 run_keys(&mut e, "j");
7098 assert_eq!(e.line_highlight(), Some((0, 1)));
7099 run_keys(&mut e, "j");
7100 assert_eq!(e.line_highlight(), Some((0, 2)));
7101 }
7102
7103 #[test]
7106 fn h_moves_left() {
7107 let mut e = editor_with("hello");
7108 e.jump_cursor(0, 3);
7109 run_keys(&mut e, "h");
7110 assert_eq!(e.cursor(), (0, 2));
7111 }
7112
7113 #[test]
7114 fn l_moves_right() {
7115 let mut e = editor_with("hello");
7116 run_keys(&mut e, "l");
7117 assert_eq!(e.cursor(), (0, 1));
7118 }
7119
7120 #[test]
7121 fn k_moves_up() {
7122 let mut e = editor_with("a\nb\nc");
7123 e.jump_cursor(2, 0);
7124 run_keys(&mut e, "k");
7125 assert_eq!(e.cursor(), (1, 0));
7126 }
7127
7128 #[test]
7129 fn zero_moves_to_line_start() {
7130 let mut e = editor_with(" hello");
7131 run_keys(&mut e, "$");
7132 run_keys(&mut e, "0");
7133 assert_eq!(e.cursor().1, 0);
7134 }
7135
7136 #[test]
7137 fn caret_moves_to_first_non_blank() {
7138 let mut e = editor_with(" hello");
7139 run_keys(&mut e, "0");
7140 run_keys(&mut e, "^");
7141 assert_eq!(e.cursor().1, 4);
7142 }
7143
7144 #[test]
7145 fn dollar_moves_to_last_char() {
7146 let mut e = editor_with("hello");
7147 run_keys(&mut e, "$");
7148 assert_eq!(e.cursor().1, 4);
7149 }
7150
7151 #[test]
7152 fn dollar_on_empty_line_stays_at_col_zero() {
7153 let mut e = editor_with("");
7154 run_keys(&mut e, "$");
7155 assert_eq!(e.cursor().1, 0);
7156 }
7157
7158 #[test]
7159 fn w_jumps_to_next_word() {
7160 let mut e = editor_with("foo bar baz");
7161 run_keys(&mut e, "w");
7162 assert_eq!(e.cursor().1, 4);
7163 }
7164
7165 #[test]
7166 fn b_jumps_back_a_word() {
7167 let mut e = editor_with("foo bar");
7168 e.jump_cursor(0, 6);
7169 run_keys(&mut e, "b");
7170 assert_eq!(e.cursor().1, 4);
7171 }
7172
7173 #[test]
7174 fn e_jumps_to_word_end() {
7175 let mut e = editor_with("foo bar");
7176 run_keys(&mut e, "e");
7177 assert_eq!(e.cursor().1, 2);
7178 }
7179
7180 #[test]
7183 fn d_dollar_deletes_to_eol() {
7184 let mut e = editor_with("hello world");
7185 e.jump_cursor(0, 5);
7186 run_keys(&mut e, "d$");
7187 assert_eq!(e.buffer().lines()[0], "hello");
7188 }
7189
7190 #[test]
7191 fn d_zero_deletes_to_line_start() {
7192 let mut e = editor_with("hello world");
7193 e.jump_cursor(0, 6);
7194 run_keys(&mut e, "d0");
7195 assert_eq!(e.buffer().lines()[0], "world");
7196 }
7197
7198 #[test]
7199 fn d_caret_deletes_to_first_non_blank() {
7200 let mut e = editor_with(" hello");
7201 e.jump_cursor(0, 6);
7202 run_keys(&mut e, "d^");
7203 assert_eq!(e.buffer().lines()[0], " llo");
7204 }
7205
7206 #[test]
7207 fn d_capital_g_deletes_to_end_of_file() {
7208 let mut e = editor_with("a\nb\nc\nd");
7209 e.jump_cursor(1, 0);
7210 run_keys(&mut e, "dG");
7211 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7212 }
7213
7214 #[test]
7215 fn d_gg_deletes_to_start_of_file() {
7216 let mut e = editor_with("a\nb\nc\nd");
7217 e.jump_cursor(2, 0);
7218 run_keys(&mut e, "dgg");
7219 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7220 }
7221
7222 #[test]
7223 fn cw_is_ce_quirk() {
7224 let mut e = editor_with("foo bar");
7227 run_keys(&mut e, "cwxyz<Esc>");
7228 assert_eq!(e.buffer().lines()[0], "xyz bar");
7229 }
7230
7231 #[test]
7234 fn big_d_deletes_to_eol() {
7235 let mut e = editor_with("hello world");
7236 e.jump_cursor(0, 5);
7237 run_keys(&mut e, "D");
7238 assert_eq!(e.buffer().lines()[0], "hello");
7239 }
7240
7241 #[test]
7242 fn big_c_deletes_to_eol_and_inserts() {
7243 let mut e = editor_with("hello world");
7244 e.jump_cursor(0, 5);
7245 run_keys(&mut e, "C!<Esc>");
7246 assert_eq!(e.buffer().lines()[0], "hello!");
7247 }
7248
7249 #[test]
7250 fn j_joins_next_line_with_space() {
7251 let mut e = editor_with("hello\nworld");
7252 run_keys(&mut e, "J");
7253 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7254 }
7255
7256 #[test]
7257 fn j_strips_leading_whitespace_on_join() {
7258 let mut e = editor_with("hello\n world");
7259 run_keys(&mut e, "J");
7260 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7261 }
7262
7263 #[test]
7264 fn big_x_deletes_char_before_cursor() {
7265 let mut e = editor_with("hello");
7266 e.jump_cursor(0, 3);
7267 run_keys(&mut e, "X");
7268 assert_eq!(e.buffer().lines()[0], "helo");
7269 }
7270
7271 #[test]
7272 fn s_substitutes_char_and_enters_insert() {
7273 let mut e = editor_with("hello");
7274 run_keys(&mut e, "sX<Esc>");
7275 assert_eq!(e.buffer().lines()[0], "Xello");
7276 }
7277
7278 #[test]
7279 fn count_x_deletes_many() {
7280 let mut e = editor_with("abcdef");
7281 run_keys(&mut e, "3x");
7282 assert_eq!(e.buffer().lines()[0], "def");
7283 }
7284
7285 #[test]
7288 fn p_pastes_charwise_after_cursor() {
7289 let mut e = editor_with("hello");
7290 run_keys(&mut e, "yw");
7291 run_keys(&mut e, "$p");
7292 assert_eq!(e.buffer().lines()[0], "hellohello");
7293 }
7294
7295 #[test]
7296 fn capital_p_pastes_charwise_before_cursor() {
7297 let mut e = editor_with("hello");
7298 run_keys(&mut e, "v");
7300 run_keys(&mut e, "l");
7301 run_keys(&mut e, "y");
7302 run_keys(&mut e, "$P");
7303 assert_eq!(e.buffer().lines()[0], "hellheo");
7306 }
7307
7308 #[test]
7309 fn p_pastes_linewise_below() {
7310 let mut e = editor_with("one\ntwo\nthree");
7311 run_keys(&mut e, "yy");
7312 run_keys(&mut e, "p");
7313 assert_eq!(
7314 e.buffer().lines(),
7315 &[
7316 "one".to_string(),
7317 "one".to_string(),
7318 "two".to_string(),
7319 "three".to_string()
7320 ]
7321 );
7322 }
7323
7324 #[test]
7325 fn capital_p_pastes_linewise_above() {
7326 let mut e = editor_with("one\ntwo");
7327 e.jump_cursor(1, 0);
7328 run_keys(&mut e, "yy");
7329 run_keys(&mut e, "P");
7330 assert_eq!(
7331 e.buffer().lines(),
7332 &["one".to_string(), "two".to_string(), "two".to_string()]
7333 );
7334 }
7335
7336 #[test]
7339 fn hash_finds_previous_occurrence() {
7340 let mut e = editor_with("foo bar foo baz foo");
7341 e.jump_cursor(0, 16);
7343 run_keys(&mut e, "#");
7344 assert_eq!(e.cursor().1, 8);
7345 }
7346
7347 #[test]
7350 fn visual_line_delete_removes_full_lines() {
7351 let mut e = editor_with("a\nb\nc\nd");
7352 run_keys(&mut e, "Vjd");
7353 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7354 }
7355
7356 #[test]
7357 fn visual_line_change_leaves_blank_line() {
7358 let mut e = editor_with("a\nb\nc");
7359 run_keys(&mut e, "Vjc");
7360 assert_eq!(e.vim_mode(), VimMode::Insert);
7361 run_keys(&mut e, "X<Esc>");
7362 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7366 }
7367
7368 #[test]
7369 fn cc_leaves_blank_line() {
7370 let mut e = editor_with("a\nb\nc");
7371 e.jump_cursor(1, 0);
7372 run_keys(&mut e, "ccX<Esc>");
7373 assert_eq!(
7374 e.buffer().lines(),
7375 &["a".to_string(), "X".to_string(), "c".to_string()]
7376 );
7377 }
7378
7379 #[test]
7384 fn big_w_skips_hyphens() {
7385 let mut e = editor_with("foo-bar baz");
7387 run_keys(&mut e, "W");
7388 assert_eq!(e.cursor().1, 8);
7389 }
7390
7391 #[test]
7392 fn big_w_crosses_lines() {
7393 let mut e = editor_with("foo-bar\nbaz-qux");
7394 run_keys(&mut e, "W");
7395 assert_eq!(e.cursor(), (1, 0));
7396 }
7397
7398 #[test]
7399 fn big_b_skips_hyphens() {
7400 let mut e = editor_with("foo-bar baz");
7401 e.jump_cursor(0, 9);
7402 run_keys(&mut e, "B");
7403 assert_eq!(e.cursor().1, 8);
7404 run_keys(&mut e, "B");
7405 assert_eq!(e.cursor().1, 0);
7406 }
7407
7408 #[test]
7409 fn big_e_jumps_to_big_word_end() {
7410 let mut e = editor_with("foo-bar baz");
7411 run_keys(&mut e, "E");
7412 assert_eq!(e.cursor().1, 6);
7413 run_keys(&mut e, "E");
7414 assert_eq!(e.cursor().1, 10);
7415 }
7416
7417 #[test]
7418 fn dw_with_big_word_variant() {
7419 let mut e = editor_with("foo-bar baz");
7421 run_keys(&mut e, "dW");
7422 assert_eq!(e.buffer().lines()[0], "baz");
7423 }
7424
7425 #[test]
7428 fn insert_ctrl_w_deletes_word_back() {
7429 let mut e = editor_with("");
7430 run_keys(&mut e, "i");
7431 for c in "hello world".chars() {
7432 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7433 }
7434 run_keys(&mut e, "<C-w>");
7435 assert_eq!(e.buffer().lines()[0], "hello ");
7436 }
7437
7438 #[test]
7439 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7440 let mut e = editor_with("hello\nworld");
7444 e.jump_cursor(1, 0);
7445 run_keys(&mut e, "i");
7446 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7447 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7450 assert_eq!(e.cursor(), (0, 0));
7451 }
7452
7453 #[test]
7454 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7455 let mut e = editor_with("foo bar\nbaz");
7456 e.jump_cursor(1, 0);
7457 run_keys(&mut e, "i");
7458 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7459 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7461 assert_eq!(e.cursor(), (0, 4));
7462 }
7463
7464 #[test]
7465 fn insert_ctrl_u_deletes_to_line_start() {
7466 let mut e = editor_with("");
7467 run_keys(&mut e, "i");
7468 for c in "hello world".chars() {
7469 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7470 }
7471 run_keys(&mut e, "<C-u>");
7472 assert_eq!(e.buffer().lines()[0], "");
7473 }
7474
7475 #[test]
7476 fn insert_ctrl_o_runs_one_normal_command() {
7477 let mut e = editor_with("hello world");
7478 run_keys(&mut e, "A");
7480 assert_eq!(e.vim_mode(), VimMode::Insert);
7481 e.jump_cursor(0, 0);
7483 run_keys(&mut e, "<C-o>");
7484 assert_eq!(e.vim_mode(), VimMode::Normal);
7485 run_keys(&mut e, "dw");
7486 assert_eq!(e.vim_mode(), VimMode::Insert);
7488 assert_eq!(e.buffer().lines()[0], "world");
7489 }
7490
7491 #[test]
7494 fn j_through_empty_line_preserves_column() {
7495 let mut e = editor_with("hello world\n\nanother line");
7496 run_keys(&mut e, "llllll");
7498 assert_eq!(e.cursor(), (0, 6));
7499 run_keys(&mut e, "j");
7502 assert_eq!(e.cursor(), (1, 0));
7503 run_keys(&mut e, "j");
7505 assert_eq!(e.cursor(), (2, 6));
7506 }
7507
7508 #[test]
7509 fn j_through_shorter_line_preserves_column() {
7510 let mut e = editor_with("hello world\nhi\nanother line");
7511 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7514 run_keys(&mut e, "j");
7515 assert_eq!(e.cursor(), (2, 7));
7516 }
7517
7518 #[test]
7519 fn esc_from_insert_sticky_matches_visible_cursor() {
7520 let mut e = editor_with(" this is a line\n another one of a similar size");
7524 e.jump_cursor(0, 12);
7525 run_keys(&mut e, "I");
7526 assert_eq!(e.cursor(), (0, 4));
7527 run_keys(&mut e, "X<Esc>");
7528 assert_eq!(e.cursor(), (0, 4));
7529 run_keys(&mut e, "j");
7530 assert_eq!(e.cursor(), (1, 4));
7531 }
7532
7533 #[test]
7534 fn esc_from_insert_sticky_tracks_inserted_chars() {
7535 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7536 run_keys(&mut e, "i");
7537 run_keys(&mut e, "abc<Esc>");
7538 assert_eq!(e.cursor(), (0, 2));
7539 run_keys(&mut e, "j");
7540 assert_eq!(e.cursor(), (1, 2));
7541 }
7542
7543 #[test]
7544 fn esc_from_insert_sticky_tracks_arrow_nav() {
7545 let mut e = editor_with("xxxxxx\nyyyyyy");
7546 run_keys(&mut e, "i");
7547 run_keys(&mut e, "abc");
7548 for _ in 0..2 {
7549 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7550 }
7551 run_keys(&mut e, "<Esc>");
7552 assert_eq!(e.cursor(), (0, 0));
7553 run_keys(&mut e, "j");
7554 assert_eq!(e.cursor(), (1, 0));
7555 }
7556
7557 #[test]
7558 fn esc_from_insert_at_col_14_followed_by_j() {
7559 let line = "x".repeat(30);
7562 let buf = format!("{line}\n{line}");
7563 let mut e = editor_with(&buf);
7564 e.jump_cursor(0, 14);
7565 run_keys(&mut e, "i");
7566 for c in "test ".chars() {
7567 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7568 }
7569 run_keys(&mut e, "<Esc>");
7570 assert_eq!(e.cursor(), (0, 18));
7571 run_keys(&mut e, "j");
7572 assert_eq!(e.cursor(), (1, 18));
7573 }
7574
7575 #[test]
7576 fn linewise_paste_resets_sticky_column() {
7577 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7581 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7583 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7587 run_keys(&mut e, "j");
7589 assert_eq!(e.cursor(), (3, 2));
7590 }
7591
7592 #[test]
7593 fn horizontal_motion_resyncs_sticky_column() {
7594 let mut e = editor_with("hello world\n\nanother line");
7598 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7601 assert_eq!(e.cursor(), (2, 3));
7602 }
7603
7604 #[test]
7607 fn ctrl_v_enters_visual_block() {
7608 let mut e = editor_with("aaa\nbbb\nccc");
7609 run_keys(&mut e, "<C-v>");
7610 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7611 }
7612
7613 #[test]
7614 fn visual_block_esc_returns_to_normal() {
7615 let mut e = editor_with("aaa\nbbb\nccc");
7616 run_keys(&mut e, "<C-v>");
7617 run_keys(&mut e, "<Esc>");
7618 assert_eq!(e.vim_mode(), VimMode::Normal);
7619 }
7620
7621 #[test]
7622 fn backtick_lt_jumps_to_visual_start_mark() {
7623 let mut e = editor_with("foo bar baz\n");
7627 run_keys(&mut e, "v");
7628 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7631 run_keys(&mut e, "`<lt>");
7633 assert_eq!(e.cursor(), (0, 0));
7634 }
7635
7636 #[test]
7637 fn backtick_gt_jumps_to_visual_end_mark() {
7638 let mut e = editor_with("foo bar baz\n");
7639 run_keys(&mut e, "v");
7640 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7642 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7644 assert_eq!(e.cursor(), (0, 4));
7645 }
7646
7647 #[test]
7648 fn visual_exit_sets_lt_gt_marks() {
7649 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7652 run_keys(&mut e, "V");
7654 run_keys(&mut e, "j");
7655 run_keys(&mut e, "<Esc>");
7656 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7657 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7658 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7659 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7660 }
7661
7662 #[test]
7663 fn visual_exit_marks_use_lower_higher_order() {
7664 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7668 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7670 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7672 let lt = e.mark('<').unwrap();
7673 let gt = e.mark('>').unwrap();
7674 assert_eq!(lt.0, 2);
7675 assert_eq!(gt.0, 3);
7676 }
7677
7678 #[test]
7679 fn visualline_exit_marks_snap_to_line_edges() {
7680 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7682 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7684 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7686 let lt = e.mark('<').unwrap();
7687 let gt = e.mark('>').unwrap();
7688 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7689 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7691 }
7692
7693 #[test]
7694 fn visualblock_exit_marks_use_block_corners() {
7695 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7699 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7701 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7704 let lt = e.mark('<').unwrap();
7705 let gt = e.mark('>').unwrap();
7706 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7708 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7709 }
7710
7711 #[test]
7712 fn visual_block_delete_removes_column_range() {
7713 let mut e = editor_with("hello\nworld\nhappy");
7714 run_keys(&mut e, "l");
7716 run_keys(&mut e, "<C-v>");
7717 run_keys(&mut e, "jj");
7718 run_keys(&mut e, "ll");
7719 run_keys(&mut e, "d");
7720 assert_eq!(
7722 e.buffer().lines(),
7723 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7724 );
7725 }
7726
7727 #[test]
7728 fn visual_block_yank_joins_with_newlines() {
7729 let mut e = editor_with("hello\nworld\nhappy");
7730 run_keys(&mut e, "<C-v>");
7731 run_keys(&mut e, "jj");
7732 run_keys(&mut e, "ll");
7733 run_keys(&mut e, "y");
7734 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7735 }
7736
7737 #[test]
7738 fn visual_block_replace_fills_block() {
7739 let mut e = editor_with("hello\nworld\nhappy");
7740 run_keys(&mut e, "<C-v>");
7741 run_keys(&mut e, "jj");
7742 run_keys(&mut e, "ll");
7743 run_keys(&mut e, "rx");
7744 assert_eq!(
7745 e.buffer().lines(),
7746 &[
7747 "xxxlo".to_string(),
7748 "xxxld".to_string(),
7749 "xxxpy".to_string()
7750 ]
7751 );
7752 }
7753
7754 #[test]
7755 fn visual_block_insert_repeats_across_rows() {
7756 let mut e = editor_with("hello\nworld\nhappy");
7757 run_keys(&mut e, "<C-v>");
7758 run_keys(&mut e, "jj");
7759 run_keys(&mut e, "I");
7760 run_keys(&mut e, "# <Esc>");
7761 assert_eq!(
7762 e.buffer().lines(),
7763 &[
7764 "# hello".to_string(),
7765 "# world".to_string(),
7766 "# happy".to_string()
7767 ]
7768 );
7769 }
7770
7771 #[test]
7772 fn block_highlight_returns_none_outside_block_mode() {
7773 let mut e = editor_with("abc");
7774 assert!(e.block_highlight().is_none());
7775 run_keys(&mut e, "v");
7776 assert!(e.block_highlight().is_none());
7777 run_keys(&mut e, "<Esc>V");
7778 assert!(e.block_highlight().is_none());
7779 }
7780
7781 #[test]
7782 fn block_highlight_bounds_track_anchor_and_cursor() {
7783 let mut e = editor_with("aaaa\nbbbb\ncccc");
7784 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7786 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7789 }
7790
7791 #[test]
7792 fn visual_block_delete_handles_short_lines() {
7793 let mut e = editor_with("hello\nhi\nworld");
7795 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7797 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7799 assert_eq!(
7804 e.buffer().lines(),
7805 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7806 );
7807 }
7808
7809 #[test]
7810 fn visual_block_yank_pads_short_lines_with_empties() {
7811 let mut e = editor_with("hello\nhi\nworld");
7812 run_keys(&mut e, "l");
7813 run_keys(&mut e, "<C-v>");
7814 run_keys(&mut e, "jjll");
7815 run_keys(&mut e, "y");
7816 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7818 }
7819
7820 #[test]
7821 fn visual_block_replace_skips_past_eol() {
7822 let mut e = editor_with("ab\ncd\nef");
7825 run_keys(&mut e, "l");
7827 run_keys(&mut e, "<C-v>");
7828 run_keys(&mut e, "jjllllll");
7829 run_keys(&mut e, "rX");
7830 assert_eq!(
7833 e.buffer().lines(),
7834 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7835 );
7836 }
7837
7838 #[test]
7839 fn visual_block_with_empty_line_in_middle() {
7840 let mut e = editor_with("abcd\n\nefgh");
7841 run_keys(&mut e, "<C-v>");
7842 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7844 assert_eq!(
7847 e.buffer().lines(),
7848 &["d".to_string(), "".to_string(), "h".to_string()]
7849 );
7850 }
7851
7852 #[test]
7853 fn block_insert_pads_empty_lines_to_block_column() {
7854 let mut e = editor_with("this is a line\n\nthis is a line");
7857 e.jump_cursor(0, 3);
7858 run_keys(&mut e, "<C-v>");
7859 run_keys(&mut e, "jj");
7860 run_keys(&mut e, "I");
7861 run_keys(&mut e, "XX<Esc>");
7862 assert_eq!(
7863 e.buffer().lines(),
7864 &[
7865 "thiXXs is a line".to_string(),
7866 " XX".to_string(),
7867 "thiXXs is a line".to_string()
7868 ]
7869 );
7870 }
7871
7872 #[test]
7873 fn block_insert_pads_short_lines_to_block_column() {
7874 let mut e = editor_with("aaaaa\nbb\naaaaa");
7875 e.jump_cursor(0, 3);
7876 run_keys(&mut e, "<C-v>");
7877 run_keys(&mut e, "jj");
7878 run_keys(&mut e, "I");
7879 run_keys(&mut e, "Y<Esc>");
7880 assert_eq!(
7882 e.buffer().lines(),
7883 &[
7884 "aaaYaa".to_string(),
7885 "bb Y".to_string(),
7886 "aaaYaa".to_string()
7887 ]
7888 );
7889 }
7890
7891 #[test]
7892 fn visual_block_append_repeats_across_rows() {
7893 let mut e = editor_with("foo\nbar\nbaz");
7894 run_keys(&mut e, "<C-v>");
7895 run_keys(&mut e, "jj");
7896 run_keys(&mut e, "A");
7899 run_keys(&mut e, "!<Esc>");
7900 assert_eq!(
7901 e.buffer().lines(),
7902 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7903 );
7904 }
7905
7906 #[test]
7909 fn slash_opens_forward_search_prompt() {
7910 let mut e = editor_with("hello world");
7911 run_keys(&mut e, "/");
7912 let p = e.search_prompt().expect("prompt should be active");
7913 assert!(p.text.is_empty());
7914 assert!(p.forward);
7915 }
7916
7917 #[test]
7918 fn question_opens_backward_search_prompt() {
7919 let mut e = editor_with("hello world");
7920 run_keys(&mut e, "?");
7921 let p = e.search_prompt().expect("prompt should be active");
7922 assert!(!p.forward);
7923 }
7924
7925 #[test]
7926 fn search_prompt_typing_updates_pattern_live() {
7927 let mut e = editor_with("foo bar\nbaz");
7928 run_keys(&mut e, "/bar");
7929 assert_eq!(e.search_prompt().unwrap().text, "bar");
7930 assert!(e.search_state().pattern.is_some());
7932 }
7933
7934 #[test]
7935 fn search_prompt_backspace_and_enter() {
7936 let mut e = editor_with("hello world\nagain");
7937 run_keys(&mut e, "/worlx");
7938 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7939 assert_eq!(e.search_prompt().unwrap().text, "worl");
7940 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7941 assert!(e.search_prompt().is_none());
7943 assert_eq!(e.last_search(), Some("worl"));
7944 assert_eq!(e.cursor(), (0, 6));
7945 }
7946
7947 #[test]
7948 fn empty_search_prompt_enter_repeats_last_search() {
7949 let mut e = editor_with("foo bar foo baz foo");
7950 run_keys(&mut e, "/foo");
7951 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7952 assert_eq!(e.cursor().1, 8);
7953 run_keys(&mut e, "/");
7955 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7956 assert_eq!(e.cursor().1, 16);
7957 assert_eq!(e.last_search(), Some("foo"));
7958 }
7959
7960 #[test]
7961 fn search_history_records_committed_patterns() {
7962 let mut e = editor_with("alpha beta gamma");
7963 run_keys(&mut e, "/alpha<CR>");
7964 run_keys(&mut e, "/beta<CR>");
7965 let history = e.vim.search_history.clone();
7967 assert_eq!(history, vec!["alpha", "beta"]);
7968 }
7969
7970 #[test]
7971 fn search_history_dedupes_consecutive_repeats() {
7972 let mut e = editor_with("foo bar foo");
7973 run_keys(&mut e, "/foo<CR>");
7974 run_keys(&mut e, "/foo<CR>");
7975 run_keys(&mut e, "/bar<CR>");
7976 run_keys(&mut e, "/bar<CR>");
7977 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
7979 }
7980
7981 #[test]
7982 fn ctrl_p_walks_history_backward() {
7983 let mut e = editor_with("alpha beta gamma");
7984 run_keys(&mut e, "/alpha<CR>");
7985 run_keys(&mut e, "/beta<CR>");
7986 run_keys(&mut e, "/");
7988 assert_eq!(e.search_prompt().unwrap().text, "");
7989 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7990 assert_eq!(e.search_prompt().unwrap().text, "beta");
7991 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7992 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7993 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7995 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7996 }
7997
7998 #[test]
7999 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8000 let mut e = editor_with("a b c");
8001 run_keys(&mut e, "/a<CR>");
8002 run_keys(&mut e, "/b<CR>");
8003 run_keys(&mut e, "/c<CR>");
8004 run_keys(&mut e, "/");
8005 for _ in 0..3 {
8007 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8008 }
8009 assert_eq!(e.search_prompt().unwrap().text, "a");
8010 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8011 assert_eq!(e.search_prompt().unwrap().text, "b");
8012 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8013 assert_eq!(e.search_prompt().unwrap().text, "c");
8014 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8016 assert_eq!(e.search_prompt().unwrap().text, "c");
8017 }
8018
8019 #[test]
8020 fn typing_after_history_walk_resets_cursor() {
8021 let mut e = editor_with("foo");
8022 run_keys(&mut e, "/foo<CR>");
8023 run_keys(&mut e, "/");
8024 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8025 assert_eq!(e.search_prompt().unwrap().text, "foo");
8026 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8029 assert_eq!(e.search_prompt().unwrap().text, "foox");
8030 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8031 assert_eq!(e.search_prompt().unwrap().text, "foo");
8032 }
8033
8034 #[test]
8035 fn empty_backward_search_prompt_enter_repeats_last_search() {
8036 let mut e = editor_with("foo bar foo baz foo");
8037 run_keys(&mut e, "/foo");
8039 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8040 assert_eq!(e.cursor().1, 8);
8041 run_keys(&mut e, "?");
8042 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8043 assert_eq!(e.cursor().1, 0);
8044 assert_eq!(e.last_search(), Some("foo"));
8045 }
8046
8047 #[test]
8048 fn search_prompt_esc_cancels_but_keeps_last_search() {
8049 let mut e = editor_with("foo bar\nbaz");
8050 run_keys(&mut e, "/bar");
8051 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8052 assert!(e.search_prompt().is_none());
8053 assert_eq!(e.last_search(), Some("bar"));
8054 }
8055
8056 #[test]
8057 fn search_then_n_and_shift_n_navigate() {
8058 let mut e = editor_with("foo bar foo baz foo");
8059 run_keys(&mut e, "/foo");
8060 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8061 assert_eq!(e.cursor().1, 8);
8063 run_keys(&mut e, "n");
8064 assert_eq!(e.cursor().1, 16);
8065 run_keys(&mut e, "N");
8066 assert_eq!(e.cursor().1, 8);
8067 }
8068
8069 #[test]
8070 fn question_mark_searches_backward_on_enter() {
8071 let mut e = editor_with("foo bar foo baz");
8072 e.jump_cursor(0, 10);
8073 run_keys(&mut e, "?foo");
8074 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8075 assert_eq!(e.cursor(), (0, 8));
8077 }
8078
8079 #[test]
8082 fn big_y_yanks_to_end_of_line() {
8083 let mut e = editor_with("hello world");
8084 e.jump_cursor(0, 6);
8085 run_keys(&mut e, "Y");
8086 assert_eq!(e.last_yank.as_deref(), Some("world"));
8087 }
8088
8089 #[test]
8090 fn big_y_from_line_start_yanks_full_line() {
8091 let mut e = editor_with("hello world");
8092 run_keys(&mut e, "Y");
8093 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8094 }
8095
8096 #[test]
8097 fn gj_joins_without_inserting_space() {
8098 let mut e = editor_with("hello\n world");
8099 run_keys(&mut e, "gJ");
8100 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8102 }
8103
8104 #[test]
8105 fn gj_noop_on_last_line() {
8106 let mut e = editor_with("only");
8107 run_keys(&mut e, "gJ");
8108 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8109 }
8110
8111 #[test]
8112 fn ge_jumps_to_previous_word_end() {
8113 let mut e = editor_with("foo bar baz");
8114 e.jump_cursor(0, 5);
8115 run_keys(&mut e, "ge");
8116 assert_eq!(e.cursor(), (0, 2));
8117 }
8118
8119 #[test]
8120 fn ge_respects_word_class() {
8121 let mut e = editor_with("foo-bar baz");
8124 e.jump_cursor(0, 5);
8125 run_keys(&mut e, "ge");
8126 assert_eq!(e.cursor(), (0, 3));
8127 }
8128
8129 #[test]
8130 fn big_ge_treats_hyphens_as_part_of_word() {
8131 let mut e = editor_with("foo-bar baz");
8134 e.jump_cursor(0, 10);
8135 run_keys(&mut e, "gE");
8136 assert_eq!(e.cursor(), (0, 6));
8137 }
8138
8139 #[test]
8140 fn ge_crosses_line_boundary() {
8141 let mut e = editor_with("foo\nbar");
8142 e.jump_cursor(1, 0);
8143 run_keys(&mut e, "ge");
8144 assert_eq!(e.cursor(), (0, 2));
8145 }
8146
8147 #[test]
8148 fn dge_deletes_to_end_of_previous_word() {
8149 let mut e = editor_with("foo bar baz");
8150 e.jump_cursor(0, 8);
8151 run_keys(&mut e, "dge");
8154 assert_eq!(e.buffer().lines()[0], "foo baaz");
8155 }
8156
8157 #[test]
8158 fn ctrl_scroll_keys_do_not_panic() {
8159 let mut e = editor_with(
8162 (0..50)
8163 .map(|i| format!("line{i}"))
8164 .collect::<Vec<_>>()
8165 .join("\n")
8166 .as_str(),
8167 );
8168 run_keys(&mut e, "<C-f>");
8169 run_keys(&mut e, "<C-b>");
8170 assert!(!e.buffer().lines().is_empty());
8172 }
8173
8174 #[test]
8181 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8182 let mut e = Editor::new(
8183 hjkl_buffer::Buffer::new(),
8184 crate::types::DefaultHost::new(),
8185 crate::types::Options::default(),
8186 );
8187 e.set_content("row0\nrow1\nrow2");
8188 run_keys(&mut e, "3iX<Down><Esc>");
8190 assert!(e.buffer().lines()[0].contains('X'));
8192 assert!(
8195 !e.buffer().lines()[1].contains("row0"),
8196 "row1 leaked row0 contents: {:?}",
8197 e.buffer().lines()[1]
8198 );
8199 assert_eq!(e.buffer().lines().len(), 3);
8202 }
8203
8204 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8207 let mut e = Editor::new(
8208 hjkl_buffer::Buffer::new(),
8209 crate::types::DefaultHost::new(),
8210 crate::types::Options::default(),
8211 );
8212 let body = (0..n)
8213 .map(|i| format!(" line{}", i))
8214 .collect::<Vec<_>>()
8215 .join("\n");
8216 e.set_content(&body);
8217 e.set_viewport_height(viewport);
8218 e
8219 }
8220
8221 #[test]
8222 fn ctrl_d_moves_cursor_half_page_down() {
8223 let mut e = editor_with_rows(100, 20);
8224 run_keys(&mut e, "<C-d>");
8225 assert_eq!(e.cursor().0, 10);
8226 }
8227
8228 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8229 let mut e = Editor::new(
8230 hjkl_buffer::Buffer::new(),
8231 crate::types::DefaultHost::new(),
8232 crate::types::Options::default(),
8233 );
8234 e.set_content(&lines.join("\n"));
8235 e.set_viewport_height(viewport);
8236 let v = e.host_mut().viewport_mut();
8237 v.height = viewport;
8238 v.width = text_width;
8239 v.text_width = text_width;
8240 v.wrap = hjkl_buffer::Wrap::Char;
8241 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8242 e
8243 }
8244
8245 #[test]
8246 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8247 let lines = ["aaaabbbbcccc"; 10];
8251 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8252 e.jump_cursor(4, 0);
8253 e.ensure_cursor_in_scrolloff();
8254 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8255 assert!(csr <= 6, "csr={csr}");
8256 }
8257
8258 #[test]
8259 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8260 let lines = ["aaaabbbbcccc"; 10];
8261 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8262 e.jump_cursor(7, 0);
8265 e.ensure_cursor_in_scrolloff();
8266 e.jump_cursor(2, 0);
8267 e.ensure_cursor_in_scrolloff();
8268 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8269 assert!(csr >= 5, "csr={csr}");
8271 }
8272
8273 #[test]
8274 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8275 let lines = ["aaaabbbbcccc"; 5];
8276 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8277 e.jump_cursor(4, 11);
8278 e.ensure_cursor_in_scrolloff();
8279 let top = e.host().viewport().top_row;
8284 assert_eq!(top, 1);
8285 }
8286
8287 #[test]
8288 fn ctrl_u_moves_cursor_half_page_up() {
8289 let mut e = editor_with_rows(100, 20);
8290 e.jump_cursor(50, 0);
8291 run_keys(&mut e, "<C-u>");
8292 assert_eq!(e.cursor().0, 40);
8293 }
8294
8295 #[test]
8296 fn ctrl_f_moves_cursor_full_page_down() {
8297 let mut e = editor_with_rows(100, 20);
8298 run_keys(&mut e, "<C-f>");
8299 assert_eq!(e.cursor().0, 18);
8301 }
8302
8303 #[test]
8304 fn ctrl_b_moves_cursor_full_page_up() {
8305 let mut e = editor_with_rows(100, 20);
8306 e.jump_cursor(50, 0);
8307 run_keys(&mut e, "<C-b>");
8308 assert_eq!(e.cursor().0, 32);
8309 }
8310
8311 #[test]
8312 fn ctrl_d_lands_on_first_non_blank() {
8313 let mut e = editor_with_rows(100, 20);
8314 run_keys(&mut e, "<C-d>");
8315 assert_eq!(e.cursor().1, 2);
8317 }
8318
8319 #[test]
8320 fn ctrl_d_clamps_at_end_of_buffer() {
8321 let mut e = editor_with_rows(5, 20);
8322 run_keys(&mut e, "<C-d>");
8323 assert_eq!(e.cursor().0, 4);
8324 }
8325
8326 #[test]
8327 fn capital_h_jumps_to_viewport_top() {
8328 let mut e = editor_with_rows(100, 10);
8329 e.jump_cursor(50, 0);
8330 e.set_viewport_top(45);
8331 let top = e.host().viewport().top_row;
8332 run_keys(&mut e, "H");
8333 assert_eq!(e.cursor().0, top);
8334 assert_eq!(e.cursor().1, 2);
8335 }
8336
8337 #[test]
8338 fn capital_l_jumps_to_viewport_bottom() {
8339 let mut e = editor_with_rows(100, 10);
8340 e.jump_cursor(50, 0);
8341 e.set_viewport_top(45);
8342 let top = e.host().viewport().top_row;
8343 run_keys(&mut e, "L");
8344 assert_eq!(e.cursor().0, top + 9);
8345 }
8346
8347 #[test]
8348 fn capital_m_jumps_to_viewport_middle() {
8349 let mut e = editor_with_rows(100, 10);
8350 e.jump_cursor(50, 0);
8351 e.set_viewport_top(45);
8352 let top = e.host().viewport().top_row;
8353 run_keys(&mut e, "M");
8354 assert_eq!(e.cursor().0, top + 4);
8356 }
8357
8358 #[test]
8359 fn g_capital_m_lands_at_line_midpoint() {
8360 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8362 assert_eq!(e.cursor(), (0, 6));
8364 }
8365
8366 #[test]
8367 fn g_capital_m_on_empty_line_stays_at_zero() {
8368 let mut e = editor_with("");
8369 run_keys(&mut e, "gM");
8370 assert_eq!(e.cursor(), (0, 0));
8371 }
8372
8373 #[test]
8374 fn g_capital_m_uses_current_line_only() {
8375 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8378 run_keys(&mut e, "gM");
8379 assert_eq!(e.cursor(), (1, 6));
8380 }
8381
8382 #[test]
8383 fn capital_h_count_offsets_from_top() {
8384 let mut e = editor_with_rows(100, 10);
8385 e.jump_cursor(50, 0);
8386 e.set_viewport_top(45);
8387 let top = e.host().viewport().top_row;
8388 run_keys(&mut e, "3H");
8389 assert_eq!(e.cursor().0, top + 2);
8390 }
8391
8392 #[test]
8395 fn ctrl_o_returns_to_pre_g_position() {
8396 let mut e = editor_with_rows(50, 20);
8397 e.jump_cursor(5, 2);
8398 run_keys(&mut e, "G");
8399 assert_eq!(e.cursor().0, 49);
8400 run_keys(&mut e, "<C-o>");
8401 assert_eq!(e.cursor(), (5, 2));
8402 }
8403
8404 #[test]
8405 fn ctrl_i_redoes_jump_after_ctrl_o() {
8406 let mut e = editor_with_rows(50, 20);
8407 e.jump_cursor(5, 2);
8408 run_keys(&mut e, "G");
8409 let post = e.cursor();
8410 run_keys(&mut e, "<C-o>");
8411 run_keys(&mut e, "<C-i>");
8412 assert_eq!(e.cursor(), post);
8413 }
8414
8415 #[test]
8416 fn new_jump_clears_forward_stack() {
8417 let mut e = editor_with_rows(50, 20);
8418 e.jump_cursor(5, 2);
8419 run_keys(&mut e, "G");
8420 run_keys(&mut e, "<C-o>");
8421 run_keys(&mut e, "gg");
8422 run_keys(&mut e, "<C-i>");
8423 assert_eq!(e.cursor().0, 0);
8424 }
8425
8426 #[test]
8427 fn ctrl_o_on_empty_stack_is_noop() {
8428 let mut e = editor_with_rows(10, 20);
8429 e.jump_cursor(3, 1);
8430 run_keys(&mut e, "<C-o>");
8431 assert_eq!(e.cursor(), (3, 1));
8432 }
8433
8434 #[test]
8435 fn asterisk_search_pushes_jump() {
8436 let mut e = editor_with("foo bar\nbaz foo end");
8437 e.jump_cursor(0, 0);
8438 run_keys(&mut e, "*");
8439 let after = e.cursor();
8440 assert_ne!(after, (0, 0));
8441 run_keys(&mut e, "<C-o>");
8442 assert_eq!(e.cursor(), (0, 0));
8443 }
8444
8445 #[test]
8446 fn h_viewport_jump_is_recorded() {
8447 let mut e = editor_with_rows(100, 10);
8448 e.jump_cursor(50, 0);
8449 e.set_viewport_top(45);
8450 let pre = e.cursor();
8451 run_keys(&mut e, "H");
8452 assert_ne!(e.cursor(), pre);
8453 run_keys(&mut e, "<C-o>");
8454 assert_eq!(e.cursor(), pre);
8455 }
8456
8457 #[test]
8458 fn j_k_motion_does_not_push_jump() {
8459 let mut e = editor_with_rows(50, 20);
8460 e.jump_cursor(5, 0);
8461 run_keys(&mut e, "jjj");
8462 run_keys(&mut e, "<C-o>");
8463 assert_eq!(e.cursor().0, 8);
8464 }
8465
8466 #[test]
8467 fn jumplist_caps_at_100() {
8468 let mut e = editor_with_rows(200, 20);
8469 for i in 0..101 {
8470 e.jump_cursor(i, 0);
8471 run_keys(&mut e, "G");
8472 }
8473 assert!(e.vim.jump_back.len() <= 100);
8474 }
8475
8476 #[test]
8477 fn tab_acts_as_ctrl_i() {
8478 let mut e = editor_with_rows(50, 20);
8479 e.jump_cursor(5, 2);
8480 run_keys(&mut e, "G");
8481 let post = e.cursor();
8482 run_keys(&mut e, "<C-o>");
8483 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8484 assert_eq!(e.cursor(), post);
8485 }
8486
8487 #[test]
8490 fn ma_then_backtick_a_jumps_exact() {
8491 let mut e = editor_with_rows(50, 20);
8492 e.jump_cursor(5, 3);
8493 run_keys(&mut e, "ma");
8494 e.jump_cursor(20, 0);
8495 run_keys(&mut e, "`a");
8496 assert_eq!(e.cursor(), (5, 3));
8497 }
8498
8499 #[test]
8500 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8501 let mut e = editor_with_rows(50, 20);
8502 e.jump_cursor(5, 6);
8504 run_keys(&mut e, "ma");
8505 e.jump_cursor(30, 4);
8506 run_keys(&mut e, "'a");
8507 assert_eq!(e.cursor(), (5, 2));
8508 }
8509
8510 #[test]
8511 fn goto_mark_pushes_jumplist() {
8512 let mut e = editor_with_rows(50, 20);
8513 e.jump_cursor(10, 2);
8514 run_keys(&mut e, "mz");
8515 e.jump_cursor(3, 0);
8516 run_keys(&mut e, "`z");
8517 assert_eq!(e.cursor(), (10, 2));
8518 run_keys(&mut e, "<C-o>");
8519 assert_eq!(e.cursor(), (3, 0));
8520 }
8521
8522 #[test]
8523 fn goto_missing_mark_is_noop() {
8524 let mut e = editor_with_rows(50, 20);
8525 e.jump_cursor(3, 1);
8526 run_keys(&mut e, "`q");
8527 assert_eq!(e.cursor(), (3, 1));
8528 }
8529
8530 #[test]
8531 fn uppercase_mark_stored_under_uppercase_key() {
8532 let mut e = editor_with_rows(50, 20);
8533 e.jump_cursor(5, 3);
8534 run_keys(&mut e, "mA");
8535 assert_eq!(e.mark('A'), Some((5, 3)));
8538 assert!(e.mark('a').is_none());
8539 }
8540
8541 #[test]
8542 fn mark_survives_document_shrink_via_clamp() {
8543 let mut e = editor_with_rows(50, 20);
8544 e.jump_cursor(40, 4);
8545 run_keys(&mut e, "mx");
8546 e.set_content("a\nb\nc\nd\ne");
8548 run_keys(&mut e, "`x");
8549 let (r, _) = e.cursor();
8551 assert!(r <= 4);
8552 }
8553
8554 #[test]
8555 fn g_semicolon_walks_back_through_edits() {
8556 let mut e = editor_with("alpha\nbeta\ngamma");
8557 e.jump_cursor(0, 0);
8560 run_keys(&mut e, "iX<Esc>");
8561 e.jump_cursor(2, 0);
8562 run_keys(&mut e, "iY<Esc>");
8563 run_keys(&mut e, "g;");
8565 assert_eq!(e.cursor(), (2, 1));
8566 run_keys(&mut e, "g;");
8568 assert_eq!(e.cursor(), (0, 1));
8569 run_keys(&mut e, "g;");
8571 assert_eq!(e.cursor(), (0, 1));
8572 }
8573
8574 #[test]
8575 fn g_comma_walks_forward_after_g_semicolon() {
8576 let mut e = editor_with("a\nb\nc");
8577 e.jump_cursor(0, 0);
8578 run_keys(&mut e, "iX<Esc>");
8579 e.jump_cursor(2, 0);
8580 run_keys(&mut e, "iY<Esc>");
8581 run_keys(&mut e, "g;");
8582 run_keys(&mut e, "g;");
8583 assert_eq!(e.cursor(), (0, 1));
8584 run_keys(&mut e, "g,");
8585 assert_eq!(e.cursor(), (2, 1));
8586 }
8587
8588 #[test]
8589 fn new_edit_during_walk_trims_forward_entries() {
8590 let mut e = editor_with("a\nb\nc\nd");
8591 e.jump_cursor(0, 0);
8592 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8594 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8597 run_keys(&mut e, "g;");
8598 assert_eq!(e.cursor(), (0, 1));
8599 run_keys(&mut e, "iZ<Esc>");
8601 run_keys(&mut e, "g,");
8603 assert_ne!(e.cursor(), (2, 1));
8605 }
8606
8607 #[test]
8613 fn capital_mark_set_and_jump() {
8614 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8615 e.jump_cursor(2, 1);
8616 run_keys(&mut e, "mA");
8617 e.jump_cursor(0, 0);
8619 run_keys(&mut e, "'A");
8621 assert_eq!(e.cursor().0, 2);
8623 }
8624
8625 #[test]
8626 fn capital_mark_survives_set_content() {
8627 let mut e = editor_with("first buffer line\nsecond");
8628 e.jump_cursor(1, 3);
8629 run_keys(&mut e, "mA");
8630 e.set_content("totally different content\non many\nrows of text");
8632 e.jump_cursor(0, 0);
8634 run_keys(&mut e, "'A");
8635 assert_eq!(e.cursor().0, 1);
8636 }
8637
8638 #[test]
8643 fn capital_mark_shifts_with_edit() {
8644 let mut e = editor_with("a\nb\nc\nd");
8645 e.jump_cursor(3, 0);
8646 run_keys(&mut e, "mA");
8647 e.jump_cursor(0, 0);
8649 run_keys(&mut e, "dd");
8650 e.jump_cursor(0, 0);
8651 run_keys(&mut e, "'A");
8652 assert_eq!(e.cursor().0, 2);
8653 }
8654
8655 #[test]
8656 fn mark_below_delete_shifts_up() {
8657 let mut e = editor_with("a\nb\nc\nd\ne");
8658 e.jump_cursor(3, 0);
8660 run_keys(&mut e, "ma");
8661 e.jump_cursor(0, 0);
8663 run_keys(&mut e, "dd");
8664 e.jump_cursor(0, 0);
8666 run_keys(&mut e, "'a");
8667 assert_eq!(e.cursor().0, 2);
8668 assert_eq!(e.buffer().line(2).unwrap(), "d");
8669 }
8670
8671 #[test]
8672 fn mark_on_deleted_row_is_dropped() {
8673 let mut e = editor_with("a\nb\nc\nd");
8674 e.jump_cursor(1, 0);
8676 run_keys(&mut e, "ma");
8677 run_keys(&mut e, "dd");
8679 e.jump_cursor(2, 0);
8681 run_keys(&mut e, "'a");
8682 assert_eq!(e.cursor().0, 2);
8684 }
8685
8686 #[test]
8687 fn mark_above_edit_unchanged() {
8688 let mut e = editor_with("a\nb\nc\nd\ne");
8689 e.jump_cursor(0, 0);
8691 run_keys(&mut e, "ma");
8692 e.jump_cursor(3, 0);
8694 run_keys(&mut e, "dd");
8695 e.jump_cursor(2, 0);
8697 run_keys(&mut e, "'a");
8698 assert_eq!(e.cursor().0, 0);
8699 }
8700
8701 #[test]
8702 fn mark_shifts_down_after_insert() {
8703 let mut e = editor_with("a\nb\nc");
8704 e.jump_cursor(2, 0);
8706 run_keys(&mut e, "ma");
8707 e.jump_cursor(0, 0);
8709 run_keys(&mut e, "Onew<Esc>");
8710 e.jump_cursor(0, 0);
8713 run_keys(&mut e, "'a");
8714 assert_eq!(e.cursor().0, 3);
8715 assert_eq!(e.buffer().line(3).unwrap(), "c");
8716 }
8717
8718 #[test]
8721 fn forward_search_commit_pushes_jump() {
8722 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8723 e.jump_cursor(0, 0);
8724 run_keys(&mut e, "/target<CR>");
8725 assert_ne!(e.cursor(), (0, 0));
8727 run_keys(&mut e, "<C-o>");
8729 assert_eq!(e.cursor(), (0, 0));
8730 }
8731
8732 #[test]
8733 fn search_commit_no_match_does_not_push_jump() {
8734 let mut e = editor_with("alpha beta\nfoo end");
8735 e.jump_cursor(0, 3);
8736 let pre_len = e.vim.jump_back.len();
8737 run_keys(&mut e, "/zzznotfound<CR>");
8738 assert_eq!(e.vim.jump_back.len(), pre_len);
8740 }
8741
8742 #[test]
8745 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8746 let mut e = editor_with("hello world");
8747 run_keys(&mut e, "lll");
8748 let (row, col) = e.cursor();
8749 assert_eq!(e.buffer.cursor().row, row);
8750 assert_eq!(e.buffer.cursor().col, col);
8751 }
8752
8753 #[test]
8754 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8755 let mut e = editor_with("aaaa\nbbbb\ncccc");
8756 run_keys(&mut e, "jj");
8757 let (row, col) = e.cursor();
8758 assert_eq!(e.buffer.cursor().row, row);
8759 assert_eq!(e.buffer.cursor().col, col);
8760 }
8761
8762 #[test]
8763 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8764 let mut e = editor_with("foo bar baz");
8765 run_keys(&mut e, "ww");
8766 let (row, col) = e.cursor();
8767 assert_eq!(e.buffer.cursor().row, row);
8768 assert_eq!(e.buffer.cursor().col, col);
8769 }
8770
8771 #[test]
8772 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8773 let mut e = editor_with("a\nb\nc\nd\ne");
8774 run_keys(&mut e, "G");
8775 let (row, col) = e.cursor();
8776 assert_eq!(e.buffer.cursor().row, row);
8777 assert_eq!(e.buffer.cursor().col, col);
8778 }
8779
8780 #[test]
8781 fn editor_sticky_col_tracks_horizontal_motion() {
8782 let mut e = editor_with("longline\nhi\nlongline");
8783 run_keys(&mut e, "fl");
8788 let landed = e.cursor().1;
8789 assert!(landed > 0, "fl should have moved");
8790 run_keys(&mut e, "j");
8791 assert_eq!(e.sticky_col(), Some(landed));
8794 }
8795
8796 #[test]
8797 fn buffer_content_mirrors_textarea_after_insert() {
8798 let mut e = editor_with("hello");
8799 run_keys(&mut e, "iXYZ<Esc>");
8800 let text = e.buffer().lines().join("\n");
8801 assert_eq!(e.buffer.as_string(), text);
8802 }
8803
8804 #[test]
8805 fn buffer_content_mirrors_textarea_after_delete() {
8806 let mut e = editor_with("alpha bravo charlie");
8807 run_keys(&mut e, "dw");
8808 let text = e.buffer().lines().join("\n");
8809 assert_eq!(e.buffer.as_string(), text);
8810 }
8811
8812 #[test]
8813 fn buffer_content_mirrors_textarea_after_dd() {
8814 let mut e = editor_with("a\nb\nc\nd");
8815 run_keys(&mut e, "jdd");
8816 let text = e.buffer().lines().join("\n");
8817 assert_eq!(e.buffer.as_string(), text);
8818 }
8819
8820 #[test]
8821 fn buffer_content_mirrors_textarea_after_open_line() {
8822 let mut e = editor_with("foo\nbar");
8823 run_keys(&mut e, "oNEW<Esc>");
8824 let text = e.buffer().lines().join("\n");
8825 assert_eq!(e.buffer.as_string(), text);
8826 }
8827
8828 #[test]
8829 fn buffer_content_mirrors_textarea_after_paste() {
8830 let mut e = editor_with("hello");
8831 run_keys(&mut e, "yy");
8832 run_keys(&mut e, "p");
8833 let text = e.buffer().lines().join("\n");
8834 assert_eq!(e.buffer.as_string(), text);
8835 }
8836
8837 #[test]
8838 fn buffer_selection_none_in_normal_mode() {
8839 let e = editor_with("foo bar");
8840 assert!(e.buffer_selection().is_none());
8841 }
8842
8843 #[test]
8844 fn buffer_selection_char_in_visual_mode() {
8845 use hjkl_buffer::{Position, Selection};
8846 let mut e = editor_with("hello world");
8847 run_keys(&mut e, "vlll");
8848 assert_eq!(
8849 e.buffer_selection(),
8850 Some(Selection::Char {
8851 anchor: Position::new(0, 0),
8852 head: Position::new(0, 3),
8853 })
8854 );
8855 }
8856
8857 #[test]
8858 fn buffer_selection_line_in_visual_line_mode() {
8859 use hjkl_buffer::Selection;
8860 let mut e = editor_with("a\nb\nc\nd");
8861 run_keys(&mut e, "Vj");
8862 assert_eq!(
8863 e.buffer_selection(),
8864 Some(Selection::Line {
8865 anchor_row: 0,
8866 head_row: 1,
8867 })
8868 );
8869 }
8870
8871 #[test]
8872 fn wrapscan_off_blocks_wrap_around() {
8873 let mut e = editor_with("first\nsecond\nthird\n");
8874 e.settings_mut().wrapscan = false;
8875 e.jump_cursor(2, 0);
8877 run_keys(&mut e, "/first<CR>");
8878 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8880 e.settings_mut().wrapscan = true;
8882 run_keys(&mut e, "/first<CR>");
8883 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8884 }
8885
8886 #[test]
8887 fn smartcase_uppercase_pattern_stays_sensitive() {
8888 let mut e = editor_with("foo\nFoo\nBAR\n");
8889 e.settings_mut().ignore_case = true;
8890 e.settings_mut().smartcase = true;
8891 run_keys(&mut e, "/foo<CR>");
8894 let r1 = e
8895 .search_state()
8896 .pattern
8897 .as_ref()
8898 .unwrap()
8899 .as_str()
8900 .to_string();
8901 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8902 run_keys(&mut e, "/Foo<CR>");
8904 let r2 = e
8905 .search_state()
8906 .pattern
8907 .as_ref()
8908 .unwrap()
8909 .as_str()
8910 .to_string();
8911 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8912 }
8913
8914 #[test]
8915 fn enter_with_autoindent_copies_leading_whitespace() {
8916 let mut e = editor_with(" foo");
8917 e.jump_cursor(0, 7);
8918 run_keys(&mut e, "i<CR>");
8919 assert_eq!(e.buffer.line(1).unwrap(), " ");
8920 }
8921
8922 #[test]
8923 fn enter_without_autoindent_inserts_bare_newline() {
8924 let mut e = editor_with(" foo");
8925 e.settings_mut().autoindent = false;
8926 e.jump_cursor(0, 7);
8927 run_keys(&mut e, "i<CR>");
8928 assert_eq!(e.buffer.line(1).unwrap(), "");
8929 }
8930
8931 #[test]
8932 fn iskeyword_default_treats_alnum_underscore_as_word() {
8933 let mut e = editor_with("foo_bar baz");
8934 e.jump_cursor(0, 0);
8938 run_keys(&mut e, "*");
8939 let p = e
8940 .search_state()
8941 .pattern
8942 .as_ref()
8943 .unwrap()
8944 .as_str()
8945 .to_string();
8946 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8947 }
8948
8949 #[test]
8950 fn w_motion_respects_custom_iskeyword() {
8951 let mut e = editor_with("foo-bar baz");
8955 run_keys(&mut e, "w");
8956 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
8957 let mut e2 = editor_with("foo-bar baz");
8960 e2.set_iskeyword("@,_,45");
8961 run_keys(&mut e2, "w");
8962 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
8963 }
8964
8965 #[test]
8966 fn iskeyword_with_dash_treats_dash_as_word_char() {
8967 let mut e = editor_with("foo-bar baz");
8968 e.settings_mut().iskeyword = "@,_,45".to_string();
8969 e.jump_cursor(0, 0);
8970 run_keys(&mut e, "*");
8971 let p = e
8972 .search_state()
8973 .pattern
8974 .as_ref()
8975 .unwrap()
8976 .as_str()
8977 .to_string();
8978 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
8979 }
8980
8981 #[test]
8982 fn timeoutlen_drops_pending_g_prefix() {
8983 use std::time::{Duration, Instant};
8984 let mut e = editor_with("a\nb\nc");
8985 e.jump_cursor(2, 0);
8986 run_keys(&mut e, "g");
8988 assert!(matches!(e.vim.pending, super::Pending::G));
8989 e.settings.timeout_len = Duration::from_nanos(0);
8997 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
8998 e.vim.last_input_host_at = Some(Duration::ZERO);
8999 run_keys(&mut e, "g");
9003 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9005 }
9006
9007 #[test]
9008 fn undobreak_on_breaks_group_at_arrow_motion() {
9009 let mut e = editor_with("");
9010 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9012 let line = e.buffer.line(0).unwrap_or("").to_string();
9015 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9016 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9017 }
9018
9019 #[test]
9020 fn undobreak_off_keeps_full_run_in_one_group() {
9021 let mut e = editor_with("");
9022 e.settings_mut().undo_break_on_motion = false;
9023 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9024 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9027 }
9028
9029 #[test]
9030 fn undobreak_round_trips_through_options() {
9031 let e = editor_with("");
9032 let opts = e.current_options();
9033 assert!(opts.undo_break_on_motion);
9034 let mut e2 = editor_with("");
9035 let mut new_opts = opts.clone();
9036 new_opts.undo_break_on_motion = false;
9037 e2.apply_options(&new_opts);
9038 assert!(!e2.current_options().undo_break_on_motion);
9039 }
9040
9041 #[test]
9042 fn undo_levels_cap_drops_oldest() {
9043 let mut e = editor_with("abcde");
9044 e.settings_mut().undo_levels = 3;
9045 run_keys(&mut e, "ra");
9046 run_keys(&mut e, "lrb");
9047 run_keys(&mut e, "lrc");
9048 run_keys(&mut e, "lrd");
9049 run_keys(&mut e, "lre");
9050 assert_eq!(e.undo_stack_len(), 3);
9051 }
9052
9053 #[test]
9054 fn tab_inserts_literal_tab_when_noexpandtab() {
9055 let mut e = editor_with("");
9056 e.settings_mut().expandtab = false;
9059 e.settings_mut().softtabstop = 0;
9060 run_keys(&mut e, "i");
9061 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9062 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9063 }
9064
9065 #[test]
9066 fn tab_inserts_spaces_when_expandtab() {
9067 let mut e = editor_with("");
9068 e.settings_mut().expandtab = true;
9069 e.settings_mut().tabstop = 4;
9070 run_keys(&mut e, "i");
9071 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9072 assert_eq!(e.buffer.line(0).unwrap(), " ");
9073 }
9074
9075 #[test]
9076 fn tab_with_softtabstop_fills_to_next_boundary() {
9077 let mut e = editor_with("ab");
9079 e.settings_mut().expandtab = true;
9080 e.settings_mut().tabstop = 8;
9081 e.settings_mut().softtabstop = 4;
9082 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9084 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9085 }
9086
9087 #[test]
9088 fn backspace_deletes_softtab_run() {
9089 let mut e = editor_with(" x");
9092 e.settings_mut().softtabstop = 4;
9093 run_keys(&mut e, "fxi");
9095 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9096 assert_eq!(e.buffer.line(0).unwrap(), "x");
9097 }
9098
9099 #[test]
9100 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9101 let mut e = editor_with(" x");
9104 e.settings_mut().softtabstop = 4;
9105 run_keys(&mut e, "fxi");
9106 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9107 assert_eq!(e.buffer.line(0).unwrap(), " x");
9108 }
9109
9110 #[test]
9111 fn readonly_blocks_insert_mutation() {
9112 let mut e = editor_with("hello");
9113 e.settings_mut().readonly = true;
9114 run_keys(&mut e, "iX<Esc>");
9115 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9116 }
9117
9118 #[cfg(feature = "ratatui")]
9119 #[test]
9120 fn intern_ratatui_style_dedups_repeated_styles() {
9121 use ratatui::style::{Color, Style};
9122 let mut e = editor_with("");
9123 let red = Style::default().fg(Color::Red);
9124 let blue = Style::default().fg(Color::Blue);
9125 let id_r1 = e.intern_ratatui_style(red);
9126 let id_r2 = e.intern_ratatui_style(red);
9127 let id_b = e.intern_ratatui_style(blue);
9128 assert_eq!(id_r1, id_r2);
9129 assert_ne!(id_r1, id_b);
9130 assert_eq!(e.style_table().len(), 2);
9131 }
9132
9133 #[cfg(feature = "ratatui")]
9134 #[test]
9135 fn install_ratatui_syntax_spans_translates_styled_spans() {
9136 use ratatui::style::{Color, Style};
9137 let mut e = editor_with("SELECT foo");
9138 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9139 let by_row = e.buffer_spans();
9140 assert_eq!(by_row.len(), 1);
9141 assert_eq!(by_row[0].len(), 1);
9142 assert_eq!(by_row[0][0].start_byte, 0);
9143 assert_eq!(by_row[0][0].end_byte, 6);
9144 let id = by_row[0][0].style;
9145 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9146 }
9147
9148 #[cfg(feature = "ratatui")]
9149 #[test]
9150 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9151 use ratatui::style::{Color, Style};
9152 let mut e = editor_with("hello");
9153 e.install_ratatui_syntax_spans(vec![vec![(
9154 0,
9155 usize::MAX,
9156 Style::default().fg(Color::Blue),
9157 )]]);
9158 let by_row = e.buffer_spans();
9159 assert_eq!(by_row[0][0].end_byte, 5);
9160 }
9161
9162 #[cfg(feature = "ratatui")]
9163 #[test]
9164 fn install_ratatui_syntax_spans_drops_zero_width() {
9165 use ratatui::style::{Color, Style};
9166 let mut e = editor_with("abc");
9167 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9168 assert!(e.buffer_spans()[0].is_empty());
9169 }
9170
9171 #[test]
9172 fn named_register_yank_into_a_then_paste_from_a() {
9173 let mut e = editor_with("hello world\nsecond");
9174 run_keys(&mut e, "\"ayw");
9175 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9177 run_keys(&mut e, "j0\"aP");
9179 assert_eq!(e.buffer().lines()[1], "hello second");
9180 }
9181
9182 #[test]
9183 fn capital_r_overstrikes_chars() {
9184 let mut e = editor_with("hello");
9185 e.jump_cursor(0, 0);
9186 run_keys(&mut e, "RXY<Esc>");
9187 assert_eq!(e.buffer().lines()[0], "XYllo");
9189 }
9190
9191 #[test]
9192 fn capital_r_at_eol_appends() {
9193 let mut e = editor_with("hi");
9194 e.jump_cursor(0, 1);
9195 run_keys(&mut e, "RXYZ<Esc>");
9197 assert_eq!(e.buffer().lines()[0], "hXYZ");
9198 }
9199
9200 #[test]
9201 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9202 let mut e = editor_with("abc");
9206 e.jump_cursor(0, 0);
9207 run_keys(&mut e, "RX<Esc>");
9208 assert_eq!(e.buffer().lines()[0], "Xbc");
9209 }
9210
9211 #[test]
9212 fn ctrl_r_in_insert_pastes_named_register() {
9213 let mut e = editor_with("hello world");
9214 run_keys(&mut e, "\"ayw");
9216 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9217 run_keys(&mut e, "o");
9219 assert_eq!(e.vim_mode(), VimMode::Insert);
9220 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9221 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9222 assert_eq!(e.buffer().lines()[1], "hello ");
9223 assert_eq!(e.cursor(), (1, 6));
9225 assert_eq!(e.vim_mode(), VimMode::Insert);
9227 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9228 assert_eq!(e.buffer().lines()[1], "hello X");
9229 }
9230
9231 #[test]
9232 fn ctrl_r_with_unnamed_register() {
9233 let mut e = editor_with("foo");
9234 run_keys(&mut e, "yiw");
9235 run_keys(&mut e, "A ");
9236 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9238 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9239 assert_eq!(e.buffer().lines()[0], "foo foo");
9240 }
9241
9242 #[test]
9243 fn ctrl_r_unknown_selector_is_no_op() {
9244 let mut e = editor_with("abc");
9245 run_keys(&mut e, "A");
9246 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9247 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9250 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9251 assert_eq!(e.buffer().lines()[0], "abcZ");
9252 }
9253
9254 #[test]
9255 fn ctrl_r_multiline_register_pastes_with_newlines() {
9256 let mut e = editor_with("alpha\nbeta\ngamma");
9257 run_keys(&mut e, "\"byy");
9259 run_keys(&mut e, "j\"byy");
9260 run_keys(&mut e, "ggVj\"by");
9264 let payload = e.registers().read('b').unwrap().text.clone();
9265 assert!(payload.contains('\n'));
9266 run_keys(&mut e, "Go");
9267 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9268 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9269 let total_lines = e.buffer().lines().len();
9272 assert!(total_lines >= 5);
9273 }
9274
9275 #[test]
9276 fn yank_zero_holds_last_yank_after_delete() {
9277 let mut e = editor_with("hello world");
9278 run_keys(&mut e, "yw");
9279 let yanked = e.registers().read('0').unwrap().text.clone();
9280 assert!(!yanked.is_empty());
9281 run_keys(&mut e, "dw");
9283 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9284 assert!(!e.registers().read('1').unwrap().text.is_empty());
9286 }
9287
9288 #[test]
9289 fn delete_ring_rotates_through_one_through_nine() {
9290 let mut e = editor_with("a b c d e f g h i j");
9291 for _ in 0..3 {
9293 run_keys(&mut e, "dw");
9294 }
9295 let r1 = e.registers().read('1').unwrap().text.clone();
9297 let r2 = e.registers().read('2').unwrap().text.clone();
9298 let r3 = e.registers().read('3').unwrap().text.clone();
9299 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9300 assert_ne!(r1, r2);
9301 assert_ne!(r2, r3);
9302 }
9303
9304 #[test]
9305 fn capital_register_appends_to_lowercase() {
9306 let mut e = editor_with("foo bar");
9307 run_keys(&mut e, "\"ayw");
9308 let first = e.registers().read('a').unwrap().text.clone();
9309 assert!(first.contains("foo"));
9310 run_keys(&mut e, "w\"Ayw");
9312 let combined = e.registers().read('a').unwrap().text.clone();
9313 assert!(combined.starts_with(&first));
9314 assert!(combined.contains("bar"));
9315 }
9316
9317 #[test]
9318 fn zf_in_visual_line_creates_closed_fold() {
9319 let mut e = editor_with("a\nb\nc\nd\ne");
9320 e.jump_cursor(1, 0);
9322 run_keys(&mut e, "Vjjzf");
9323 assert_eq!(e.buffer().folds().len(), 1);
9324 let f = e.buffer().folds()[0];
9325 assert_eq!(f.start_row, 1);
9326 assert_eq!(f.end_row, 3);
9327 assert!(f.closed);
9328 }
9329
9330 #[test]
9331 fn zfj_in_normal_creates_two_row_fold() {
9332 let mut e = editor_with("a\nb\nc\nd\ne");
9333 e.jump_cursor(1, 0);
9334 run_keys(&mut e, "zfj");
9335 assert_eq!(e.buffer().folds().len(), 1);
9336 let f = e.buffer().folds()[0];
9337 assert_eq!(f.start_row, 1);
9338 assert_eq!(f.end_row, 2);
9339 assert!(f.closed);
9340 assert_eq!(e.cursor().0, 1);
9342 }
9343
9344 #[test]
9345 fn zf_with_count_folds_count_rows() {
9346 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9347 e.jump_cursor(0, 0);
9348 run_keys(&mut e, "zf3j");
9350 assert_eq!(e.buffer().folds().len(), 1);
9351 let f = e.buffer().folds()[0];
9352 assert_eq!(f.start_row, 0);
9353 assert_eq!(f.end_row, 3);
9354 }
9355
9356 #[test]
9357 fn zfk_folds_upward_range() {
9358 let mut e = editor_with("a\nb\nc\nd\ne");
9359 e.jump_cursor(3, 0);
9360 run_keys(&mut e, "zfk");
9361 let f = e.buffer().folds()[0];
9362 assert_eq!(f.start_row, 2);
9364 assert_eq!(f.end_row, 3);
9365 }
9366
9367 #[test]
9368 fn zf_capital_g_folds_to_bottom() {
9369 let mut e = editor_with("a\nb\nc\nd\ne");
9370 e.jump_cursor(1, 0);
9371 run_keys(&mut e, "zfG");
9373 let f = e.buffer().folds()[0];
9374 assert_eq!(f.start_row, 1);
9375 assert_eq!(f.end_row, 4);
9376 }
9377
9378 #[test]
9379 fn zfgg_folds_to_top_via_operator_pipeline() {
9380 let mut e = editor_with("a\nb\nc\nd\ne");
9381 e.jump_cursor(3, 0);
9382 run_keys(&mut e, "zfgg");
9386 let f = e.buffer().folds()[0];
9387 assert_eq!(f.start_row, 0);
9388 assert_eq!(f.end_row, 3);
9389 }
9390
9391 #[test]
9392 fn zfip_folds_paragraph_via_text_object() {
9393 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9394 e.jump_cursor(1, 0);
9395 run_keys(&mut e, "zfip");
9397 assert_eq!(e.buffer().folds().len(), 1);
9398 let f = e.buffer().folds()[0];
9399 assert_eq!(f.start_row, 0);
9400 assert_eq!(f.end_row, 2);
9401 }
9402
9403 #[test]
9404 fn zfap_folds_paragraph_with_trailing_blank() {
9405 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9406 e.jump_cursor(0, 0);
9407 run_keys(&mut e, "zfap");
9409 let f = e.buffer().folds()[0];
9410 assert_eq!(f.start_row, 0);
9411 assert_eq!(f.end_row, 3);
9412 }
9413
9414 #[test]
9415 fn zf_paragraph_motion_folds_to_blank() {
9416 let mut e = editor_with("alpha\nbeta\n\ngamma");
9417 e.jump_cursor(0, 0);
9418 run_keys(&mut e, "zf}");
9420 let f = e.buffer().folds()[0];
9421 assert_eq!(f.start_row, 0);
9422 assert_eq!(f.end_row, 2);
9423 }
9424
9425 #[test]
9426 fn za_toggles_fold_under_cursor() {
9427 let mut e = editor_with("a\nb\nc\nd");
9428 e.buffer_mut().add_fold(1, 2, true);
9429 e.jump_cursor(1, 0);
9430 run_keys(&mut e, "za");
9431 assert!(!e.buffer().folds()[0].closed);
9432 run_keys(&mut e, "za");
9433 assert!(e.buffer().folds()[0].closed);
9434 }
9435
9436 #[test]
9437 fn zr_opens_all_folds_zm_closes_all() {
9438 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9439 e.buffer_mut().add_fold(0, 1, true);
9440 e.buffer_mut().add_fold(2, 3, true);
9441 e.buffer_mut().add_fold(4, 5, true);
9442 run_keys(&mut e, "zR");
9443 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9444 run_keys(&mut e, "zM");
9445 assert!(e.buffer().folds().iter().all(|f| f.closed));
9446 }
9447
9448 #[test]
9449 fn ze_clears_all_folds() {
9450 let mut e = editor_with("a\nb\nc\nd");
9451 e.buffer_mut().add_fold(0, 1, true);
9452 e.buffer_mut().add_fold(2, 3, false);
9453 run_keys(&mut e, "zE");
9454 assert!(e.buffer().folds().is_empty());
9455 }
9456
9457 #[test]
9458 fn g_underscore_jumps_to_last_non_blank() {
9459 let mut e = editor_with("hello world ");
9460 run_keys(&mut e, "g_");
9461 assert_eq!(e.cursor().1, 10);
9463 }
9464
9465 #[test]
9466 fn gj_and_gk_alias_j_and_k() {
9467 let mut e = editor_with("a\nb\nc");
9468 run_keys(&mut e, "gj");
9469 assert_eq!(e.cursor().0, 1);
9470 run_keys(&mut e, "gk");
9471 assert_eq!(e.cursor().0, 0);
9472 }
9473
9474 #[test]
9475 fn paragraph_motions_walk_blank_lines() {
9476 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9477 run_keys(&mut e, "}");
9478 assert_eq!(e.cursor().0, 2);
9479 run_keys(&mut e, "}");
9480 assert_eq!(e.cursor().0, 5);
9481 run_keys(&mut e, "{");
9482 assert_eq!(e.cursor().0, 2);
9483 }
9484
9485 #[test]
9486 fn gv_reenters_last_visual_selection() {
9487 let mut e = editor_with("alpha\nbeta\ngamma");
9488 run_keys(&mut e, "Vj");
9489 run_keys(&mut e, "<Esc>");
9491 assert_eq!(e.vim_mode(), VimMode::Normal);
9492 run_keys(&mut e, "gv");
9494 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9495 }
9496
9497 #[test]
9498 fn o_in_visual_swaps_anchor_and_cursor() {
9499 let mut e = editor_with("hello world");
9500 run_keys(&mut e, "vllll");
9502 assert_eq!(e.cursor().1, 4);
9503 run_keys(&mut e, "o");
9505 assert_eq!(e.cursor().1, 0);
9506 assert_eq!(e.vim.visual_anchor, (0, 4));
9508 }
9509
9510 #[test]
9511 fn editing_inside_fold_invalidates_it() {
9512 let mut e = editor_with("a\nb\nc\nd");
9513 e.buffer_mut().add_fold(1, 2, true);
9514 e.jump_cursor(1, 0);
9515 run_keys(&mut e, "iX<Esc>");
9517 assert!(e.buffer().folds().is_empty());
9519 }
9520
9521 #[test]
9522 fn zd_removes_fold_under_cursor() {
9523 let mut e = editor_with("a\nb\nc\nd");
9524 e.buffer_mut().add_fold(1, 2, true);
9525 e.jump_cursor(2, 0);
9526 run_keys(&mut e, "zd");
9527 assert!(e.buffer().folds().is_empty());
9528 }
9529
9530 #[test]
9531 fn take_fold_ops_observes_z_keystroke_dispatch() {
9532 use crate::types::FoldOp;
9537 let mut e = editor_with("a\nb\nc\nd");
9538 e.buffer_mut().add_fold(1, 2, true);
9539 e.jump_cursor(1, 0);
9540 let _ = e.take_fold_ops();
9543 run_keys(&mut e, "zo");
9544 run_keys(&mut e, "zM");
9545 let ops = e.take_fold_ops();
9546 assert_eq!(ops.len(), 2);
9547 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9548 assert!(matches!(ops[1], FoldOp::CloseAll));
9549 assert!(e.take_fold_ops().is_empty());
9551 }
9552
9553 #[test]
9554 fn edit_pipeline_emits_invalidate_fold_op() {
9555 use crate::types::FoldOp;
9558 let mut e = editor_with("a\nb\nc\nd");
9559 e.buffer_mut().add_fold(1, 2, true);
9560 e.jump_cursor(1, 0);
9561 let _ = e.take_fold_ops();
9562 run_keys(&mut e, "iX<Esc>");
9563 let ops = e.take_fold_ops();
9564 assert!(
9565 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9566 "expected at least one Invalidate op, got {ops:?}"
9567 );
9568 }
9569
9570 #[test]
9571 fn dot_mark_jumps_to_last_edit_position() {
9572 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9573 e.jump_cursor(2, 0);
9574 run_keys(&mut e, "iX<Esc>");
9576 let after_edit = e.cursor();
9577 run_keys(&mut e, "gg");
9579 assert_eq!(e.cursor().0, 0);
9580 run_keys(&mut e, "'.");
9582 assert_eq!(e.cursor().0, after_edit.0);
9583 }
9584
9585 #[test]
9586 fn quote_quote_returns_to_pre_jump_position() {
9587 let mut e = editor_with_rows(50, 20);
9588 e.jump_cursor(10, 2);
9589 let before = e.cursor();
9590 run_keys(&mut e, "G");
9592 assert_ne!(e.cursor(), before);
9593 run_keys(&mut e, "''");
9595 assert_eq!(e.cursor().0, before.0);
9596 }
9597
9598 #[test]
9599 fn backtick_backtick_restores_exact_pre_jump_pos() {
9600 let mut e = editor_with_rows(50, 20);
9601 e.jump_cursor(7, 3);
9602 let before = e.cursor();
9603 run_keys(&mut e, "G");
9604 run_keys(&mut e, "``");
9605 assert_eq!(e.cursor(), before);
9606 }
9607
9608 #[test]
9609 fn macro_record_and_replay_basic() {
9610 let mut e = editor_with("foo\nbar\nbaz");
9611 run_keys(&mut e, "qaIX<Esc>jq");
9613 assert_eq!(e.buffer().lines()[0], "Xfoo");
9614 run_keys(&mut e, "@a");
9616 assert_eq!(e.buffer().lines()[1], "Xbar");
9617 run_keys(&mut e, "j@@");
9619 assert_eq!(e.buffer().lines()[2], "Xbaz");
9620 }
9621
9622 #[test]
9623 fn macro_count_replays_n_times() {
9624 let mut e = editor_with("a\nb\nc\nd\ne");
9625 run_keys(&mut e, "qajq");
9627 assert_eq!(e.cursor().0, 1);
9628 run_keys(&mut e, "3@a");
9630 assert_eq!(e.cursor().0, 4);
9631 }
9632
9633 #[test]
9634 fn macro_capital_q_appends_to_lowercase_register() {
9635 let mut e = editor_with("hello");
9636 run_keys(&mut e, "qall<Esc>q");
9637 run_keys(&mut e, "qAhh<Esc>q");
9638 let text = e.registers().read('a').unwrap().text.clone();
9641 assert!(text.contains("ll<Esc>"));
9642 assert!(text.contains("hh<Esc>"));
9643 }
9644
9645 #[test]
9646 fn buffer_selection_block_in_visual_block_mode() {
9647 use hjkl_buffer::{Position, Selection};
9648 let mut e = editor_with("aaaa\nbbbb\ncccc");
9649 run_keys(&mut e, "<C-v>jl");
9650 assert_eq!(
9651 e.buffer_selection(),
9652 Some(Selection::Block {
9653 anchor: Position::new(0, 0),
9654 head: Position::new(1, 1),
9655 })
9656 );
9657 }
9658
9659 #[test]
9662 fn n_after_question_mark_keeps_walking_backward() {
9663 let mut e = editor_with("foo bar foo baz foo end");
9666 e.jump_cursor(0, 22);
9667 run_keys(&mut e, "?foo<CR>");
9668 assert_eq!(e.cursor().1, 16);
9669 run_keys(&mut e, "n");
9670 assert_eq!(e.cursor().1, 8);
9671 run_keys(&mut e, "N");
9672 assert_eq!(e.cursor().1, 16);
9673 }
9674
9675 #[test]
9676 fn nested_macro_chord_records_literal_keys() {
9677 let mut e = editor_with("alpha\nbeta\ngamma");
9680 run_keys(&mut e, "qblq");
9682 run_keys(&mut e, "qaIX<Esc>q");
9685 e.jump_cursor(1, 0);
9687 run_keys(&mut e, "@a");
9688 assert_eq!(e.buffer().lines()[1], "Xbeta");
9689 }
9690
9691 #[test]
9692 fn shift_gt_motion_indents_one_line() {
9693 let mut e = editor_with("hello world");
9697 run_keys(&mut e, ">w");
9698 assert_eq!(e.buffer().lines()[0], " hello world");
9699 }
9700
9701 #[test]
9702 fn shift_lt_motion_outdents_one_line() {
9703 let mut e = editor_with(" hello world");
9704 run_keys(&mut e, "<lt>w");
9705 assert_eq!(e.buffer().lines()[0], " hello world");
9707 }
9708
9709 #[test]
9710 fn shift_gt_text_object_indents_paragraph() {
9711 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9712 e.jump_cursor(0, 0);
9713 run_keys(&mut e, ">ip");
9714 assert_eq!(e.buffer().lines()[0], " alpha");
9715 assert_eq!(e.buffer().lines()[1], " beta");
9716 assert_eq!(e.buffer().lines()[2], " gamma");
9717 assert_eq!(e.buffer().lines()[4], "rest");
9719 }
9720
9721 #[test]
9722 fn ctrl_o_runs_exactly_one_normal_command() {
9723 let mut e = editor_with("alpha beta gamma");
9726 e.jump_cursor(0, 0);
9727 run_keys(&mut e, "i");
9728 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9729 run_keys(&mut e, "dw");
9730 assert_eq!(e.vim_mode(), VimMode::Insert);
9732 run_keys(&mut e, "X");
9734 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9735 }
9736
9737 #[test]
9738 fn macro_replay_respects_mode_switching() {
9739 let mut e = editor_with("hi");
9743 run_keys(&mut e, "qaiX<Esc>0q");
9744 assert_eq!(e.vim_mode(), VimMode::Normal);
9745 e.set_content("yo");
9747 run_keys(&mut e, "@a");
9748 assert_eq!(e.vim_mode(), VimMode::Normal);
9749 assert_eq!(e.cursor().1, 0);
9750 assert_eq!(e.buffer().lines()[0], "Xyo");
9751 }
9752
9753 #[test]
9754 fn macro_recorded_text_round_trips_through_register() {
9755 let mut e = editor_with("");
9759 run_keys(&mut e, "qaiX<Esc>q");
9760 let text = e.registers().read('a').unwrap().text.clone();
9761 assert!(text.starts_with("iX"));
9762 run_keys(&mut e, "@a");
9764 assert_eq!(e.buffer().lines()[0], "XX");
9765 }
9766
9767 #[test]
9768 fn dot_after_macro_replays_macros_last_change() {
9769 let mut e = editor_with("ab\ncd\nef");
9772 run_keys(&mut e, "qaIX<Esc>jq");
9775 assert_eq!(e.buffer().lines()[0], "Xab");
9776 run_keys(&mut e, "@a");
9777 assert_eq!(e.buffer().lines()[1], "Xcd");
9778 let row_before_dot = e.cursor().0;
9781 run_keys(&mut e, ".");
9782 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9783 }
9784
9785 fn si_editor(content: &str) -> Editor {
9791 let opts = crate::types::Options {
9792 shiftwidth: 4,
9793 softtabstop: 4,
9794 expandtab: true,
9795 smartindent: true,
9796 autoindent: true,
9797 ..crate::types::Options::default()
9798 };
9799 let mut e = Editor::new(
9800 hjkl_buffer::Buffer::new(),
9801 crate::types::DefaultHost::new(),
9802 opts,
9803 );
9804 e.set_content(content);
9805 e
9806 }
9807
9808 #[test]
9809 fn smartindent_bumps_indent_after_open_brace() {
9810 let mut e = si_editor("fn foo() {");
9812 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9814 assert_eq!(
9815 e.buffer().lines()[1],
9816 " ",
9817 "smartindent should bump one shiftwidth after {{"
9818 );
9819 }
9820
9821 #[test]
9822 fn smartindent_no_bump_when_off() {
9823 let mut e = si_editor("fn foo() {");
9826 e.settings_mut().smartindent = false;
9827 e.jump_cursor(0, 10);
9828 run_keys(&mut e, "i<CR>");
9829 assert_eq!(
9830 e.buffer().lines()[1],
9831 "",
9832 "without smartindent, no bump: new line copies empty leading ws"
9833 );
9834 }
9835
9836 #[test]
9837 fn smartindent_uses_tab_when_noexpandtab() {
9838 let opts = crate::types::Options {
9840 shiftwidth: 4,
9841 softtabstop: 0,
9842 expandtab: false,
9843 smartindent: true,
9844 autoindent: true,
9845 ..crate::types::Options::default()
9846 };
9847 let mut e = Editor::new(
9848 hjkl_buffer::Buffer::new(),
9849 crate::types::DefaultHost::new(),
9850 opts,
9851 );
9852 e.set_content("fn foo() {");
9853 e.jump_cursor(0, 10);
9854 run_keys(&mut e, "i<CR>");
9855 assert_eq!(
9856 e.buffer().lines()[1],
9857 "\t",
9858 "noexpandtab: smartindent bump inserts a literal tab"
9859 );
9860 }
9861
9862 #[test]
9863 fn smartindent_dedent_on_close_brace() {
9864 let mut e = si_editor("fn foo() {");
9867 e.set_content("fn foo() {\n ");
9869 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9871 assert_eq!(
9872 e.buffer().lines()[1],
9873 "}",
9874 "close brace on whitespace-only line should dedent"
9875 );
9876 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9877 }
9878
9879 #[test]
9880 fn smartindent_no_dedent_when_off() {
9881 let mut e = si_editor("fn foo() {\n ");
9883 e.settings_mut().smartindent = false;
9884 e.jump_cursor(1, 4);
9885 run_keys(&mut e, "i}");
9886 assert_eq!(
9887 e.buffer().lines()[1],
9888 " }",
9889 "without smartindent, `}}` just appends at cursor"
9890 );
9891 }
9892
9893 #[test]
9894 fn smartindent_no_dedent_mid_line() {
9895 let mut e = si_editor(" let x = 1");
9898 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9900 assert_eq!(
9901 e.buffer().lines()[0],
9902 " let x = 1}",
9903 "mid-line `}}` should not dedent"
9904 );
9905 }
9906
9907 #[test]
9911 fn count_5x_fills_unnamed_register() {
9912 let mut e = editor_with("hello world\n");
9913 e.jump_cursor(0, 0);
9914 run_keys(&mut e, "5x");
9915 assert_eq!(e.buffer().lines()[0], " world");
9916 assert_eq!(e.cursor(), (0, 0));
9917 assert_eq!(e.yank(), "hello");
9918 }
9919
9920 #[test]
9921 fn x_fills_unnamed_register_single_char() {
9922 let mut e = editor_with("abc\n");
9923 e.jump_cursor(0, 0);
9924 run_keys(&mut e, "x");
9925 assert_eq!(e.buffer().lines()[0], "bc");
9926 assert_eq!(e.yank(), "a");
9927 }
9928
9929 #[test]
9930 fn big_x_fills_unnamed_register() {
9931 let mut e = editor_with("hello\n");
9932 e.jump_cursor(0, 3);
9933 run_keys(&mut e, "X");
9934 assert_eq!(e.buffer().lines()[0], "helo");
9935 assert_eq!(e.yank(), "l");
9936 }
9937
9938 #[test]
9940 fn g_motion_trailing_newline_lands_on_last_content_row() {
9941 let mut e = editor_with("foo\nbar\nbaz\n");
9942 e.jump_cursor(0, 0);
9943 run_keys(&mut e, "G");
9944 assert_eq!(
9946 e.cursor().0,
9947 2,
9948 "G should land on row 2 (baz), not row 3 (phantom empty)"
9949 );
9950 }
9951
9952 #[test]
9954 fn dd_last_line_clamps_cursor_to_new_last_row() {
9955 let mut e = editor_with("foo\nbar\n");
9956 e.jump_cursor(1, 0);
9957 run_keys(&mut e, "dd");
9958 assert_eq!(e.buffer().lines()[0], "foo");
9959 assert_eq!(
9960 e.cursor(),
9961 (0, 0),
9962 "cursor should clamp to row 0 after dd on last content line"
9963 );
9964 }
9965
9966 #[test]
9968 fn d_dollar_cursor_on_last_char() {
9969 let mut e = editor_with("hello world\n");
9970 e.jump_cursor(0, 5);
9971 run_keys(&mut e, "d$");
9972 assert_eq!(e.buffer().lines()[0], "hello");
9973 assert_eq!(
9974 e.cursor(),
9975 (0, 4),
9976 "d$ should leave cursor on col 4, not col 5"
9977 );
9978 }
9979
9980 #[test]
9982 fn undo_insert_clamps_cursor_to_last_valid_col() {
9983 let mut e = editor_with("hello\n");
9984 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
9986 assert_eq!(e.buffer().lines()[0], "hello");
9987 assert_eq!(
9988 e.cursor(),
9989 (0, 4),
9990 "undo should clamp cursor to col 4 on 'hello'"
9991 );
9992 }
9993
9994 #[test]
9996 fn da_doublequote_eats_trailing_whitespace() {
9997 let mut e = editor_with("say \"hello\" there\n");
9998 e.jump_cursor(0, 6);
9999 run_keys(&mut e, "da\"");
10000 assert_eq!(e.buffer().lines()[0], "say there");
10001 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10002 }
10003
10004 #[test]
10006 fn dab_cursor_col_clamped_after_delete() {
10007 let mut e = editor_with("fn x() {\n body\n}\n");
10008 e.jump_cursor(1, 4);
10009 run_keys(&mut e, "daB");
10010 assert_eq!(e.buffer().lines()[0], "fn x() ");
10011 assert_eq!(
10012 e.cursor(),
10013 (0, 6),
10014 "daB should leave cursor at col 6, not 7"
10015 );
10016 }
10017
10018 #[test]
10020 fn dib_preserves_surrounding_newlines() {
10021 let mut e = editor_with("{\n body\n}\n");
10022 e.jump_cursor(1, 4);
10023 run_keys(&mut e, "diB");
10024 assert_eq!(e.buffer().lines()[0], "{");
10025 assert_eq!(e.buffer().lines()[1], "}");
10026 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10027 }
10028
10029 #[test]
10030 fn is_chord_pending_tracks_replace_state() {
10031 let mut e = editor_with("abc\n");
10032 assert!(!e.is_chord_pending());
10033 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10035 assert!(e.is_chord_pending(), "engine should be pending after r");
10036 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10038 assert!(
10039 !e.is_chord_pending(),
10040 "engine pending should clear after replace"
10041 );
10042 }
10043
10044 #[test]
10047 fn yiw_sets_lbr_rbr_marks_around_word() {
10048 let mut e = editor_with("hello world");
10051 run_keys(&mut e, "yiw");
10052 let lo = e.mark('[').expect("'[' must be set after yiw");
10053 let hi = e.mark(']').expect("']' must be set after yiw");
10054 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10055 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10056 }
10057
10058 #[test]
10059 fn yj_linewise_sets_marks_at_line_edges() {
10060 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10063 run_keys(&mut e, "yj");
10064 let lo = e.mark('[').expect("'[' must be set after yj");
10065 let hi = e.mark(']').expect("']' must be set after yj");
10066 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10067 assert_eq!(
10068 hi,
10069 (1, 4),
10070 "'] snaps to (bot_row, last_col) for linewise yank"
10071 );
10072 }
10073
10074 #[test]
10075 fn dd_sets_lbr_rbr_marks_to_cursor() {
10076 let mut e = editor_with("aaa\nbbb");
10079 run_keys(&mut e, "dd");
10080 let lo = e.mark('[').expect("'[' must be set after dd");
10081 let hi = e.mark(']').expect("']' must be set after dd");
10082 assert_eq!(lo, hi, "after delete both marks are at the same position");
10083 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10084 }
10085
10086 #[test]
10087 fn dw_sets_lbr_rbr_marks_to_cursor() {
10088 let mut e = editor_with("hello world");
10091 run_keys(&mut e, "dw");
10092 let lo = e.mark('[').expect("'[' must be set after dw");
10093 let hi = e.mark(']').expect("']' must be set after dw");
10094 assert_eq!(lo, hi, "after delete both marks are at the same position");
10095 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10096 }
10097
10098 #[test]
10099 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10100 let mut e = editor_with("hello world");
10105 run_keys(&mut e, "cwfoo<Esc>");
10106 let lo = e.mark('[').expect("'[' must be set after cw");
10107 let hi = e.mark(']').expect("']' must be set after cw");
10108 assert_eq!(lo, (0, 0), "'[ should be start of change");
10109 assert_eq!(hi.0, 0, "'] should be on row 0");
10112 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10113 }
10114
10115 #[test]
10116 fn cw_with_no_insertion_sets_marks_at_change_start() {
10117 let mut e = editor_with("hello world");
10120 run_keys(&mut e, "cw<Esc>");
10121 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10122 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10123 assert_eq!(lo.0, 0, "'[ should be on row 0");
10124 assert_eq!(hi.0, 0, "'] should be on row 0");
10125 assert_eq!(lo, hi, "marks coincide when insert is empty");
10127 }
10128
10129 #[test]
10130 fn p_charwise_sets_marks_around_pasted_text() {
10131 let mut e = editor_with("abc xyz");
10134 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10137 let hi = e.mark(']').expect("']' set after charwise paste");
10138 assert!(lo <= hi, "'[ must not exceed ']'");
10139 assert_eq!(
10141 hi.1.wrapping_sub(lo.1),
10142 2,
10143 "'] - '[ should span 2 cols for a 3-char paste"
10144 );
10145 }
10146
10147 #[test]
10148 fn p_linewise_sets_marks_at_line_edges() {
10149 let mut e = editor_with("aaa\nbbb\nccc");
10152 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10156 let hi = e.mark(']').expect("']' set after linewise paste");
10157 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10158 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10159 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10160 }
10161
10162 #[test]
10163 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10164 let mut e = editor_with("hello world");
10168 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10172 assert_eq!(
10174 e.cursor(),
10175 (0, 4),
10176 "visual `[v`] should land on last yanked char"
10177 );
10178 assert_eq!(
10180 e.vim_mode(),
10181 crate::VimMode::Visual,
10182 "should be in Visual mode"
10183 );
10184 }
10185}