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),
2195 _ => None,
2196 };
2197 let Some((row, col)) = target else {
2198 return true;
2199 };
2200 let pre = ed.cursor();
2201 let (r, c_clamped) = clamp_pos(ed, (row, col));
2202 if linewise {
2203 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2204 ed.push_buffer_cursor_to_textarea();
2205 move_first_non_whitespace(ed);
2206 } else {
2207 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2208 ed.push_buffer_cursor_to_textarea();
2209 }
2210 if ed.cursor() != pre {
2211 push_jump(ed, pre);
2212 }
2213 ed.sticky_col = Some(ed.cursor().1);
2214 true
2215}
2216
2217fn take_count(vim: &mut VimState) -> usize {
2218 if vim.count > 0 {
2219 let n = vim.count;
2220 vim.count = 0;
2221 n
2222 } else {
2223 1
2224 }
2225}
2226
2227fn char_to_operator(c: char) -> Option<Operator> {
2228 match c {
2229 'd' => Some(Operator::Delete),
2230 'c' => Some(Operator::Change),
2231 'y' => Some(Operator::Yank),
2232 '>' => Some(Operator::Indent),
2233 '<' => Some(Operator::Outdent),
2234 _ => None,
2235 }
2236}
2237
2238fn visual_operator(input: &Input) -> Option<Operator> {
2239 if input.ctrl {
2240 return None;
2241 }
2242 match input.key {
2243 Key::Char('y') => Some(Operator::Yank),
2244 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2245 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2246 Key::Char('U') => Some(Operator::Uppercase),
2248 Key::Char('u') => Some(Operator::Lowercase),
2249 Key::Char('~') => Some(Operator::ToggleCase),
2250 Key::Char('>') => Some(Operator::Indent),
2252 Key::Char('<') => Some(Operator::Outdent),
2253 _ => None,
2254 }
2255}
2256
2257fn find_entry(input: &Input) -> Option<(bool, bool)> {
2258 if input.ctrl {
2259 return None;
2260 }
2261 match input.key {
2262 Key::Char('f') => Some((true, false)),
2263 Key::Char('F') => Some((false, false)),
2264 Key::Char('t') => Some((true, true)),
2265 Key::Char('T') => Some((false, true)),
2266 _ => None,
2267 }
2268}
2269
2270const JUMPLIST_MAX: usize = 100;
2274
2275fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2280 ed.vim.jump_back.push(from);
2281 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2282 ed.vim.jump_back.remove(0);
2283 }
2284 ed.vim.jump_fwd.clear();
2285}
2286
2287fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2290 let Some(target) = ed.vim.jump_back.pop() else {
2291 return;
2292 };
2293 let cur = ed.cursor();
2294 ed.vim.jump_fwd.push(cur);
2295 let (r, c) = clamp_pos(ed, target);
2296 ed.jump_cursor(r, c);
2297 ed.sticky_col = Some(c);
2298}
2299
2300fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2303 let Some(target) = ed.vim.jump_fwd.pop() else {
2304 return;
2305 };
2306 let cur = ed.cursor();
2307 ed.vim.jump_back.push(cur);
2308 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2309 ed.vim.jump_back.remove(0);
2310 }
2311 let (r, c) = clamp_pos(ed, target);
2312 ed.jump_cursor(r, c);
2313 ed.sticky_col = Some(c);
2314}
2315
2316fn clamp_pos<H: crate::types::Host>(
2319 ed: &Editor<hjkl_buffer::Buffer, H>,
2320 pos: (usize, usize),
2321) -> (usize, usize) {
2322 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2323 let r = pos.0.min(last_row);
2324 let line_len = buf_line_chars(&ed.buffer, r);
2325 let c = pos.1.min(line_len.saturating_sub(1));
2326 (r, c)
2327}
2328
2329fn is_big_jump(motion: &Motion) -> bool {
2331 matches!(
2332 motion,
2333 Motion::FileTop
2334 | Motion::FileBottom
2335 | Motion::MatchBracket
2336 | Motion::WordAtCursor { .. }
2337 | Motion::SearchNext { .. }
2338 | Motion::ViewportTop
2339 | Motion::ViewportMiddle
2340 | Motion::ViewportBottom
2341 )
2342}
2343
2344fn viewport_half_rows<H: crate::types::Host>(
2349 ed: &Editor<hjkl_buffer::Buffer, H>,
2350 count: usize,
2351) -> usize {
2352 let h = ed.viewport_height_value() as usize;
2353 (h / 2).max(1).saturating_mul(count.max(1))
2354}
2355
2356fn viewport_full_rows<H: crate::types::Host>(
2359 ed: &Editor<hjkl_buffer::Buffer, H>,
2360 count: usize,
2361) -> usize {
2362 let h = ed.viewport_height_value() as usize;
2363 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2364}
2365
2366fn scroll_cursor_rows<H: crate::types::Host>(
2371 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2372 delta: isize,
2373) {
2374 if delta == 0 {
2375 return;
2376 }
2377 ed.sync_buffer_content_from_textarea();
2378 let (row, _) = ed.cursor();
2379 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2380 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2381 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2382 crate::motions::move_first_non_blank(&mut ed.buffer);
2383 ed.push_buffer_cursor_to_textarea();
2384 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2385}
2386
2387fn parse_motion(input: &Input) -> Option<Motion> {
2390 if input.ctrl {
2391 return None;
2392 }
2393 match input.key {
2394 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2395 Key::Char('l') | Key::Right => Some(Motion::Right),
2396 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2397 Key::Char('k') | Key::Up => Some(Motion::Up),
2398 Key::Char('w') => Some(Motion::WordFwd),
2399 Key::Char('W') => Some(Motion::BigWordFwd),
2400 Key::Char('b') => Some(Motion::WordBack),
2401 Key::Char('B') => Some(Motion::BigWordBack),
2402 Key::Char('e') => Some(Motion::WordEnd),
2403 Key::Char('E') => Some(Motion::BigWordEnd),
2404 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2405 Key::Char('^') => Some(Motion::FirstNonBlank),
2406 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2407 Key::Char('G') => Some(Motion::FileBottom),
2408 Key::Char('%') => Some(Motion::MatchBracket),
2409 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2410 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2411 Key::Char('*') => Some(Motion::WordAtCursor {
2412 forward: true,
2413 whole_word: true,
2414 }),
2415 Key::Char('#') => Some(Motion::WordAtCursor {
2416 forward: false,
2417 whole_word: true,
2418 }),
2419 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2420 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2421 Key::Char('H') => Some(Motion::ViewportTop),
2422 Key::Char('M') => Some(Motion::ViewportMiddle),
2423 Key::Char('L') => Some(Motion::ViewportBottom),
2424 Key::Char('{') => Some(Motion::ParagraphPrev),
2425 Key::Char('}') => Some(Motion::ParagraphNext),
2426 Key::Char('(') => Some(Motion::SentencePrev),
2427 Key::Char(')') => Some(Motion::SentenceNext),
2428 _ => None,
2429 }
2430}
2431
2432fn execute_motion<H: crate::types::Host>(
2435 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2436 motion: Motion,
2437 count: usize,
2438) {
2439 let count = count.max(1);
2440 let motion = match motion {
2442 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2443 Some((ch, forward, till)) => Motion::Find {
2444 ch,
2445 forward: if reverse { !forward } else { forward },
2446 till,
2447 },
2448 None => return,
2449 },
2450 other => other,
2451 };
2452 let pre_pos = ed.cursor();
2453 let pre_col = pre_pos.1;
2454 apply_motion_cursor(ed, &motion, count);
2455 let post_pos = ed.cursor();
2456 if is_big_jump(&motion) && pre_pos != post_pos {
2457 push_jump(ed, pre_pos);
2458 }
2459 apply_sticky_col(ed, &motion, pre_col);
2460 ed.sync_buffer_from_textarea();
2465}
2466
2467fn apply_sticky_col<H: crate::types::Host>(
2472 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2473 motion: &Motion,
2474 pre_col: usize,
2475) {
2476 if is_vertical_motion(motion) {
2477 let want = ed.sticky_col.unwrap_or(pre_col);
2478 ed.sticky_col = Some(want);
2481 let (row, _) = ed.cursor();
2482 let line_len = buf_line_chars(&ed.buffer, row);
2483 let max_col = line_len.saturating_sub(1);
2487 let target = want.min(max_col);
2488 ed.jump_cursor(row, target);
2489 } else {
2490 ed.sticky_col = Some(ed.cursor().1);
2493 }
2494}
2495
2496fn is_vertical_motion(motion: &Motion) -> bool {
2497 matches!(
2501 motion,
2502 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2503 )
2504}
2505
2506fn apply_motion_cursor<H: crate::types::Host>(
2507 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2508 motion: &Motion,
2509 count: usize,
2510) {
2511 apply_motion_cursor_ctx(ed, motion, count, false)
2512}
2513
2514fn apply_motion_cursor_ctx<H: crate::types::Host>(
2515 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2516 motion: &Motion,
2517 count: usize,
2518 as_operator: bool,
2519) {
2520 match motion {
2521 Motion::Left => {
2522 crate::motions::move_left(&mut ed.buffer, count);
2524 ed.push_buffer_cursor_to_textarea();
2525 }
2526 Motion::Right => {
2527 if as_operator {
2531 crate::motions::move_right_to_end(&mut ed.buffer, count);
2532 } else {
2533 crate::motions::move_right_in_line(&mut ed.buffer, count);
2534 }
2535 ed.push_buffer_cursor_to_textarea();
2536 }
2537 Motion::Up => {
2538 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2542 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2543 ed.push_buffer_cursor_to_textarea();
2544 }
2545 Motion::Down => {
2546 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2547 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2548 ed.push_buffer_cursor_to_textarea();
2549 }
2550 Motion::ScreenUp => {
2551 let v = *ed.host.viewport();
2552 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2553 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2554 ed.push_buffer_cursor_to_textarea();
2555 }
2556 Motion::ScreenDown => {
2557 let v = *ed.host.viewport();
2558 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2559 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2560 ed.push_buffer_cursor_to_textarea();
2561 }
2562 Motion::WordFwd => {
2563 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2564 ed.push_buffer_cursor_to_textarea();
2565 }
2566 Motion::WordBack => {
2567 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2568 ed.push_buffer_cursor_to_textarea();
2569 }
2570 Motion::WordEnd => {
2571 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2572 ed.push_buffer_cursor_to_textarea();
2573 }
2574 Motion::BigWordFwd => {
2575 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2576 ed.push_buffer_cursor_to_textarea();
2577 }
2578 Motion::BigWordBack => {
2579 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2580 ed.push_buffer_cursor_to_textarea();
2581 }
2582 Motion::BigWordEnd => {
2583 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2584 ed.push_buffer_cursor_to_textarea();
2585 }
2586 Motion::WordEndBack => {
2587 crate::motions::move_word_end_back(
2588 &mut ed.buffer,
2589 false,
2590 count,
2591 &ed.settings.iskeyword,
2592 );
2593 ed.push_buffer_cursor_to_textarea();
2594 }
2595 Motion::BigWordEndBack => {
2596 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2597 ed.push_buffer_cursor_to_textarea();
2598 }
2599 Motion::LineStart => {
2600 crate::motions::move_line_start(&mut ed.buffer);
2601 ed.push_buffer_cursor_to_textarea();
2602 }
2603 Motion::FirstNonBlank => {
2604 crate::motions::move_first_non_blank(&mut ed.buffer);
2605 ed.push_buffer_cursor_to_textarea();
2606 }
2607 Motion::LineEnd => {
2608 crate::motions::move_line_end(&mut ed.buffer);
2610 ed.push_buffer_cursor_to_textarea();
2611 }
2612 Motion::FileTop => {
2613 if count > 1 {
2616 crate::motions::move_bottom(&mut ed.buffer, count);
2617 } else {
2618 crate::motions::move_top(&mut ed.buffer);
2619 }
2620 ed.push_buffer_cursor_to_textarea();
2621 }
2622 Motion::FileBottom => {
2623 if count > 1 {
2626 crate::motions::move_bottom(&mut ed.buffer, count);
2627 } else {
2628 crate::motions::move_bottom(&mut ed.buffer, 0);
2629 }
2630 ed.push_buffer_cursor_to_textarea();
2631 }
2632 Motion::Find { ch, forward, till } => {
2633 for _ in 0..count {
2634 if !find_char_on_line(ed, *ch, *forward, *till) {
2635 break;
2636 }
2637 }
2638 }
2639 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2641 let _ = matching_bracket(ed);
2642 }
2643 Motion::WordAtCursor {
2644 forward,
2645 whole_word,
2646 } => {
2647 word_at_cursor_search(ed, *forward, *whole_word, count);
2648 }
2649 Motion::SearchNext { reverse } => {
2650 if let Some(pattern) = ed.vim.last_search.clone() {
2654 push_search_pattern(ed, &pattern);
2655 }
2656 if ed.search_state().pattern.is_none() {
2657 return;
2658 }
2659 let forward = ed.vim.last_search_forward != *reverse;
2663 for _ in 0..count.max(1) {
2664 if forward {
2665 ed.search_advance_forward(true);
2666 } else {
2667 ed.search_advance_backward(true);
2668 }
2669 }
2670 ed.push_buffer_cursor_to_textarea();
2671 }
2672 Motion::ViewportTop => {
2673 let v = *ed.host().viewport();
2674 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2675 ed.push_buffer_cursor_to_textarea();
2676 }
2677 Motion::ViewportMiddle => {
2678 let v = *ed.host().viewport();
2679 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2680 ed.push_buffer_cursor_to_textarea();
2681 }
2682 Motion::ViewportBottom => {
2683 let v = *ed.host().viewport();
2684 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2685 ed.push_buffer_cursor_to_textarea();
2686 }
2687 Motion::LastNonBlank => {
2688 crate::motions::move_last_non_blank(&mut ed.buffer);
2689 ed.push_buffer_cursor_to_textarea();
2690 }
2691 Motion::LineMiddle => {
2692 let row = ed.cursor().0;
2693 let line_chars = buf_line_chars(&ed.buffer, row);
2694 let target = line_chars / 2;
2697 ed.jump_cursor(row, target);
2698 }
2699 Motion::ParagraphPrev => {
2700 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2701 ed.push_buffer_cursor_to_textarea();
2702 }
2703 Motion::ParagraphNext => {
2704 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2705 ed.push_buffer_cursor_to_textarea();
2706 }
2707 Motion::SentencePrev => {
2708 for _ in 0..count.max(1) {
2709 if let Some((row, col)) = sentence_boundary(ed, false) {
2710 ed.jump_cursor(row, col);
2711 }
2712 }
2713 }
2714 Motion::SentenceNext => {
2715 for _ in 0..count.max(1) {
2716 if let Some((row, col)) = sentence_boundary(ed, true) {
2717 ed.jump_cursor(row, col);
2718 }
2719 }
2720 }
2721 }
2722}
2723
2724fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2725 ed.sync_buffer_content_from_textarea();
2731 crate::motions::move_first_non_blank(&mut ed.buffer);
2732 ed.push_buffer_cursor_to_textarea();
2733}
2734
2735fn find_char_on_line<H: crate::types::Host>(
2736 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2737 ch: char,
2738 forward: bool,
2739 till: bool,
2740) -> bool {
2741 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2742 if moved {
2743 ed.push_buffer_cursor_to_textarea();
2744 }
2745 moved
2746}
2747
2748fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2749 let moved = crate::motions::match_bracket(&mut ed.buffer);
2750 if moved {
2751 ed.push_buffer_cursor_to_textarea();
2752 }
2753 moved
2754}
2755
2756fn word_at_cursor_search<H: crate::types::Host>(
2757 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2758 forward: bool,
2759 whole_word: bool,
2760 count: usize,
2761) {
2762 let (row, col) = ed.cursor();
2763 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2764 let chars: Vec<char> = line.chars().collect();
2765 if chars.is_empty() {
2766 return;
2767 }
2768 let spec = ed.settings().iskeyword.clone();
2770 let is_word = |c: char| is_keyword_char(c, &spec);
2771 let mut start = col.min(chars.len().saturating_sub(1));
2772 while start > 0 && is_word(chars[start - 1]) {
2773 start -= 1;
2774 }
2775 let mut end = start;
2776 while end < chars.len() && is_word(chars[end]) {
2777 end += 1;
2778 }
2779 if end <= start {
2780 return;
2781 }
2782 let word: String = chars[start..end].iter().collect();
2783 let escaped = regex_escape(&word);
2784 let pattern = if whole_word {
2785 format!(r"\b{escaped}\b")
2786 } else {
2787 escaped
2788 };
2789 push_search_pattern(ed, &pattern);
2790 if ed.search_state().pattern.is_none() {
2791 return;
2792 }
2793 ed.vim.last_search = Some(pattern);
2795 ed.vim.last_search_forward = forward;
2796 for _ in 0..count.max(1) {
2797 if forward {
2798 ed.search_advance_forward(true);
2799 } else {
2800 ed.search_advance_backward(true);
2801 }
2802 }
2803 ed.push_buffer_cursor_to_textarea();
2804}
2805
2806fn regex_escape(s: &str) -> String {
2807 let mut out = String::with_capacity(s.len());
2808 for c in s.chars() {
2809 if matches!(
2810 c,
2811 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2812 ) {
2813 out.push('\\');
2814 }
2815 out.push(c);
2816 }
2817 out
2818}
2819
2820fn handle_after_op<H: crate::types::Host>(
2823 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2824 input: Input,
2825 op: Operator,
2826 count1: usize,
2827) -> bool {
2828 if let Key::Char(d @ '0'..='9') = input.key
2830 && !input.ctrl
2831 && (d != '0' || ed.vim.count > 0)
2832 {
2833 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2834 ed.vim.pending = Pending::Op { op, count1 };
2835 return true;
2836 }
2837
2838 if input.key == Key::Esc {
2840 ed.vim.count = 0;
2841 return true;
2842 }
2843
2844 let double_ch = match op {
2848 Operator::Delete => Some('d'),
2849 Operator::Change => Some('c'),
2850 Operator::Yank => Some('y'),
2851 Operator::Indent => Some('>'),
2852 Operator::Outdent => Some('<'),
2853 Operator::Uppercase => Some('U'),
2854 Operator::Lowercase => Some('u'),
2855 Operator::ToggleCase => Some('~'),
2856 Operator::Fold => None,
2857 Operator::Reflow => Some('q'),
2860 };
2861 if let Key::Char(c) = input.key
2862 && !input.ctrl
2863 && Some(c) == double_ch
2864 {
2865 let count2 = take_count(&mut ed.vim);
2866 let total = count1.max(1) * count2.max(1);
2867 execute_line_op(ed, op, total);
2868 if !ed.vim.replaying {
2869 ed.vim.last_change = Some(LastChange::LineOp {
2870 op,
2871 count: total,
2872 inserted: None,
2873 });
2874 }
2875 return true;
2876 }
2877
2878 if let Key::Char('i') | Key::Char('a') = input.key
2880 && !input.ctrl
2881 {
2882 let inner = matches!(input.key, Key::Char('i'));
2883 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2884 return true;
2885 }
2886
2887 if input.key == Key::Char('g') && !input.ctrl {
2889 ed.vim.pending = Pending::OpG { op, count1 };
2890 return true;
2891 }
2892
2893 if let Some((forward, till)) = find_entry(&input) {
2895 ed.vim.pending = Pending::OpFind {
2896 op,
2897 count1,
2898 forward,
2899 till,
2900 };
2901 return true;
2902 }
2903
2904 let count2 = take_count(&mut ed.vim);
2906 let total = count1.max(1) * count2.max(1);
2907 if let Some(motion) = parse_motion(&input) {
2908 let motion = match motion {
2909 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2910 Some((ch, forward, till)) => Motion::Find {
2911 ch,
2912 forward: if reverse { !forward } else { forward },
2913 till,
2914 },
2915 None => return true,
2916 },
2917 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2921 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2922 m => m,
2923 };
2924 apply_op_with_motion(ed, op, &motion, total);
2925 if let Motion::Find { ch, forward, till } = &motion {
2926 ed.vim.last_find = Some((*ch, *forward, *till));
2927 }
2928 if !ed.vim.replaying && op_is_change(op) {
2929 ed.vim.last_change = Some(LastChange::OpMotion {
2930 op,
2931 motion,
2932 count: total,
2933 inserted: None,
2934 });
2935 }
2936 return true;
2937 }
2938
2939 true
2941}
2942
2943fn handle_op_after_g<H: crate::types::Host>(
2944 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2945 input: Input,
2946 op: Operator,
2947 count1: usize,
2948) -> bool {
2949 if input.ctrl {
2950 return true;
2951 }
2952 let count2 = take_count(&mut ed.vim);
2953 let total = count1.max(1) * count2.max(1);
2954 if matches!(
2958 op,
2959 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2960 ) {
2961 let op_char = match op {
2962 Operator::Uppercase => 'U',
2963 Operator::Lowercase => 'u',
2964 Operator::ToggleCase => '~',
2965 _ => unreachable!(),
2966 };
2967 if input.key == Key::Char(op_char) {
2968 execute_line_op(ed, op, total);
2969 if !ed.vim.replaying {
2970 ed.vim.last_change = Some(LastChange::LineOp {
2971 op,
2972 count: total,
2973 inserted: None,
2974 });
2975 }
2976 return true;
2977 }
2978 }
2979 let motion = match input.key {
2980 Key::Char('g') => Motion::FileTop,
2981 Key::Char('e') => Motion::WordEndBack,
2982 Key::Char('E') => Motion::BigWordEndBack,
2983 Key::Char('j') => Motion::ScreenDown,
2984 Key::Char('k') => Motion::ScreenUp,
2985 _ => return true,
2986 };
2987 apply_op_with_motion(ed, op, &motion, total);
2988 if !ed.vim.replaying && op_is_change(op) {
2989 ed.vim.last_change = Some(LastChange::OpMotion {
2990 op,
2991 motion,
2992 count: total,
2993 inserted: None,
2994 });
2995 }
2996 true
2997}
2998
2999fn handle_after_g<H: crate::types::Host>(
3000 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3001 input: Input,
3002) -> bool {
3003 let count = take_count(&mut ed.vim);
3004 match input.key {
3005 Key::Char('g') => {
3006 let pre = ed.cursor();
3008 if count > 1 {
3009 ed.jump_cursor(count - 1, 0);
3010 } else {
3011 ed.jump_cursor(0, 0);
3012 }
3013 move_first_non_whitespace(ed);
3014 if ed.cursor() != pre {
3015 push_jump(ed, pre);
3016 }
3017 }
3018 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
3019 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
3020 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
3022 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
3024 Key::Char('v') => {
3026 if let Some(snap) = ed.vim.last_visual {
3027 match snap.mode {
3028 Mode::Visual => {
3029 ed.vim.visual_anchor = snap.anchor;
3030 ed.vim.mode = Mode::Visual;
3031 }
3032 Mode::VisualLine => {
3033 ed.vim.visual_line_anchor = snap.anchor.0;
3034 ed.vim.mode = Mode::VisualLine;
3035 }
3036 Mode::VisualBlock => {
3037 ed.vim.block_anchor = snap.anchor;
3038 ed.vim.block_vcol = snap.block_vcol;
3039 ed.vim.mode = Mode::VisualBlock;
3040 }
3041 _ => {}
3042 }
3043 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3044 }
3045 }
3046 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
3050 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
3051 Key::Char('U') => {
3055 ed.vim.pending = Pending::Op {
3056 op: Operator::Uppercase,
3057 count1: count,
3058 };
3059 }
3060 Key::Char('u') => {
3061 ed.vim.pending = Pending::Op {
3062 op: Operator::Lowercase,
3063 count1: count,
3064 };
3065 }
3066 Key::Char('~') => {
3067 ed.vim.pending = Pending::Op {
3068 op: Operator::ToggleCase,
3069 count1: count,
3070 };
3071 }
3072 Key::Char('q') => {
3073 ed.vim.pending = Pending::Op {
3076 op: Operator::Reflow,
3077 count1: count,
3078 };
3079 }
3080 Key::Char('J') => {
3081 for _ in 0..count.max(1) {
3083 ed.push_undo();
3084 join_line_raw(ed);
3085 }
3086 if !ed.vim.replaying {
3087 ed.vim.last_change = Some(LastChange::JoinLine {
3088 count: count.max(1),
3089 });
3090 }
3091 }
3092 Key::Char('d') => {
3093 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3098 }
3099 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
3102 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
3103 Key::Char('*') => execute_motion(
3107 ed,
3108 Motion::WordAtCursor {
3109 forward: true,
3110 whole_word: false,
3111 },
3112 count,
3113 ),
3114 Key::Char('#') => execute_motion(
3115 ed,
3116 Motion::WordAtCursor {
3117 forward: false,
3118 whole_word: false,
3119 },
3120 count,
3121 ),
3122 _ => {}
3123 }
3124 true
3125}
3126
3127fn handle_after_z<H: crate::types::Host>(
3128 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3129 input: Input,
3130) -> bool {
3131 use crate::editor::CursorScrollTarget;
3132 let row = ed.cursor().0;
3133 match input.key {
3134 Key::Char('z') => {
3135 ed.scroll_cursor_to(CursorScrollTarget::Center);
3136 ed.vim.viewport_pinned = true;
3137 }
3138 Key::Char('t') => {
3139 ed.scroll_cursor_to(CursorScrollTarget::Top);
3140 ed.vim.viewport_pinned = true;
3141 }
3142 Key::Char('b') => {
3143 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3144 ed.vim.viewport_pinned = true;
3145 }
3146 Key::Char('o') => {
3151 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3152 }
3153 Key::Char('c') => {
3154 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3155 }
3156 Key::Char('a') => {
3157 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3158 }
3159 Key::Char('R') => {
3160 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3161 }
3162 Key::Char('M') => {
3163 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3164 }
3165 Key::Char('E') => {
3166 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3167 }
3168 Key::Char('d') => {
3169 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3170 }
3171 Key::Char('f') => {
3172 if matches!(
3173 ed.vim.mode,
3174 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3175 ) {
3176 let anchor_row = match ed.vim.mode {
3179 Mode::VisualLine => ed.vim.visual_line_anchor,
3180 Mode::VisualBlock => ed.vim.block_anchor.0,
3181 _ => ed.vim.visual_anchor.0,
3182 };
3183 let cur = ed.cursor().0;
3184 let top = anchor_row.min(cur);
3185 let bot = anchor_row.max(cur);
3186 ed.apply_fold_op(crate::types::FoldOp::Add {
3187 start_row: top,
3188 end_row: bot,
3189 closed: true,
3190 });
3191 ed.vim.mode = Mode::Normal;
3192 } else {
3193 let count = take_count(&mut ed.vim);
3198 ed.vim.pending = Pending::Op {
3199 op: Operator::Fold,
3200 count1: count,
3201 };
3202 }
3203 }
3204 _ => {}
3205 }
3206 true
3207}
3208
3209fn handle_replace<H: crate::types::Host>(
3210 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3211 input: Input,
3212) -> bool {
3213 if let Key::Char(ch) = input.key {
3214 if ed.vim.mode == Mode::VisualBlock {
3215 block_replace(ed, ch);
3216 return true;
3217 }
3218 let count = take_count(&mut ed.vim);
3219 replace_char(ed, ch, count.max(1));
3220 if !ed.vim.replaying {
3221 ed.vim.last_change = Some(LastChange::ReplaceChar {
3222 ch,
3223 count: count.max(1),
3224 });
3225 }
3226 }
3227 true
3228}
3229
3230fn handle_find_target<H: crate::types::Host>(
3231 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3232 input: Input,
3233 forward: bool,
3234 till: bool,
3235) -> bool {
3236 let Key::Char(ch) = input.key else {
3237 return true;
3238 };
3239 let count = take_count(&mut ed.vim);
3240 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3241 ed.vim.last_find = Some((ch, forward, till));
3242 true
3243}
3244
3245fn handle_op_find_target<H: crate::types::Host>(
3246 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3247 input: Input,
3248 op: Operator,
3249 count1: usize,
3250 forward: bool,
3251 till: bool,
3252) -> bool {
3253 let Key::Char(ch) = input.key else {
3254 return true;
3255 };
3256 let count2 = take_count(&mut ed.vim);
3257 let total = count1.max(1) * count2.max(1);
3258 let motion = Motion::Find { ch, forward, till };
3259 apply_op_with_motion(ed, op, &motion, total);
3260 ed.vim.last_find = Some((ch, forward, till));
3261 if !ed.vim.replaying && op_is_change(op) {
3262 ed.vim.last_change = Some(LastChange::OpMotion {
3263 op,
3264 motion,
3265 count: total,
3266 inserted: None,
3267 });
3268 }
3269 true
3270}
3271
3272fn handle_text_object<H: crate::types::Host>(
3273 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3274 input: Input,
3275 op: Operator,
3276 _count1: usize,
3277 inner: bool,
3278) -> bool {
3279 let Key::Char(ch) = input.key else {
3280 return true;
3281 };
3282 let obj = match ch {
3283 'w' => TextObject::Word { big: false },
3284 'W' => TextObject::Word { big: true },
3285 '"' | '\'' | '`' => TextObject::Quote(ch),
3286 '(' | ')' | 'b' => TextObject::Bracket('('),
3287 '[' | ']' => TextObject::Bracket('['),
3288 '{' | '}' | 'B' => TextObject::Bracket('{'),
3289 '<' | '>' => TextObject::Bracket('<'),
3290 'p' => TextObject::Paragraph,
3291 't' => TextObject::XmlTag,
3292 's' => TextObject::Sentence,
3293 _ => return true,
3294 };
3295 apply_op_with_text_object(ed, op, obj, inner);
3296 if !ed.vim.replaying && op_is_change(op) {
3297 ed.vim.last_change = Some(LastChange::OpTextObj {
3298 op,
3299 obj,
3300 inner,
3301 inserted: None,
3302 });
3303 }
3304 true
3305}
3306
3307fn handle_visual_text_obj<H: crate::types::Host>(
3308 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3309 input: Input,
3310 inner: bool,
3311) -> bool {
3312 let Key::Char(ch) = input.key else {
3313 return true;
3314 };
3315 let obj = match ch {
3316 'w' => TextObject::Word { big: false },
3317 'W' => TextObject::Word { big: true },
3318 '"' | '\'' | '`' => TextObject::Quote(ch),
3319 '(' | ')' | 'b' => TextObject::Bracket('('),
3320 '[' | ']' => TextObject::Bracket('['),
3321 '{' | '}' | 'B' => TextObject::Bracket('{'),
3322 '<' | '>' => TextObject::Bracket('<'),
3323 'p' => TextObject::Paragraph,
3324 't' => TextObject::XmlTag,
3325 's' => TextObject::Sentence,
3326 _ => return true,
3327 };
3328 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3329 return true;
3330 };
3331 match kind {
3335 MotionKind::Linewise => {
3336 ed.vim.visual_line_anchor = start.0;
3337 ed.vim.mode = Mode::VisualLine;
3338 ed.jump_cursor(end.0, 0);
3339 }
3340 _ => {
3341 ed.vim.mode = Mode::Visual;
3342 ed.vim.visual_anchor = (start.0, start.1);
3343 let (er, ec) = retreat_one(ed, end);
3344 ed.jump_cursor(er, ec);
3345 }
3346 }
3347 true
3348}
3349
3350fn retreat_one<H: crate::types::Host>(
3352 ed: &Editor<hjkl_buffer::Buffer, H>,
3353 pos: (usize, usize),
3354) -> (usize, usize) {
3355 let (r, c) = pos;
3356 if c > 0 {
3357 (r, c - 1)
3358 } else if r > 0 {
3359 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3360 (r - 1, prev_len)
3361 } else {
3362 (0, 0)
3363 }
3364}
3365
3366fn op_is_change(op: Operator) -> bool {
3367 matches!(op, Operator::Delete | Operator::Change)
3368}
3369
3370fn handle_normal_only<H: crate::types::Host>(
3373 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3374 input: &Input,
3375 count: usize,
3376) -> bool {
3377 if input.ctrl {
3378 return false;
3379 }
3380 match input.key {
3381 Key::Char('i') => {
3382 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3383 true
3384 }
3385 Key::Char('I') => {
3386 move_first_non_whitespace(ed);
3387 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3388 true
3389 }
3390 Key::Char('a') => {
3391 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3392 ed.push_buffer_cursor_to_textarea();
3393 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3394 true
3395 }
3396 Key::Char('A') => {
3397 crate::motions::move_line_end(&mut ed.buffer);
3398 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3399 ed.push_buffer_cursor_to_textarea();
3400 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3401 true
3402 }
3403 Key::Char('R') => {
3404 begin_insert(ed, count.max(1), InsertReason::Replace);
3407 true
3408 }
3409 Key::Char('o') => {
3410 use hjkl_buffer::{Edit, Position};
3411 ed.push_undo();
3412 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3415 ed.sync_buffer_content_from_textarea();
3416 let row = buf_cursor_pos(&ed.buffer).row;
3417 let line_chars = buf_line_chars(&ed.buffer, row);
3418 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3421 let indent = compute_enter_indent(&ed.settings, prev_line);
3422 ed.mutate_edit(Edit::InsertStr {
3423 at: Position::new(row, line_chars),
3424 text: format!("\n{indent}"),
3425 });
3426 ed.push_buffer_cursor_to_textarea();
3427 true
3428 }
3429 Key::Char('O') => {
3430 use hjkl_buffer::{Edit, Position};
3431 ed.push_undo();
3432 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3433 ed.sync_buffer_content_from_textarea();
3434 let row = buf_cursor_pos(&ed.buffer).row;
3435 let indent = if row > 0 {
3439 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3440 compute_enter_indent(&ed.settings, above)
3441 } else {
3442 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3443 cur.chars()
3444 .take_while(|c| *c == ' ' || *c == '\t')
3445 .collect::<String>()
3446 };
3447 ed.mutate_edit(Edit::InsertStr {
3448 at: Position::new(row, 0),
3449 text: format!("{indent}\n"),
3450 });
3451 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3456 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3457 let new_row = buf_cursor_pos(&ed.buffer).row;
3458 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3459 ed.push_buffer_cursor_to_textarea();
3460 true
3461 }
3462 Key::Char('x') => {
3463 do_char_delete(ed, true, count.max(1));
3464 if !ed.vim.replaying {
3465 ed.vim.last_change = Some(LastChange::CharDel {
3466 forward: true,
3467 count: count.max(1),
3468 });
3469 }
3470 true
3471 }
3472 Key::Char('X') => {
3473 do_char_delete(ed, false, count.max(1));
3474 if !ed.vim.replaying {
3475 ed.vim.last_change = Some(LastChange::CharDel {
3476 forward: false,
3477 count: count.max(1),
3478 });
3479 }
3480 true
3481 }
3482 Key::Char('~') => {
3483 for _ in 0..count.max(1) {
3484 ed.push_undo();
3485 toggle_case_at_cursor(ed);
3486 }
3487 if !ed.vim.replaying {
3488 ed.vim.last_change = Some(LastChange::ToggleCase {
3489 count: count.max(1),
3490 });
3491 }
3492 true
3493 }
3494 Key::Char('J') => {
3495 for _ in 0..count.max(1) {
3496 ed.push_undo();
3497 join_line(ed);
3498 }
3499 if !ed.vim.replaying {
3500 ed.vim.last_change = Some(LastChange::JoinLine {
3501 count: count.max(1),
3502 });
3503 }
3504 true
3505 }
3506 Key::Char('D') => {
3507 ed.push_undo();
3508 delete_to_eol(ed);
3509 crate::motions::move_left(&mut ed.buffer, 1);
3511 ed.push_buffer_cursor_to_textarea();
3512 if !ed.vim.replaying {
3513 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3514 }
3515 true
3516 }
3517 Key::Char('Y') => {
3518 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3520 true
3521 }
3522 Key::Char('C') => {
3523 ed.push_undo();
3524 delete_to_eol(ed);
3525 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3526 true
3527 }
3528 Key::Char('s') => {
3529 use hjkl_buffer::{Edit, MotionKind, Position};
3530 ed.push_undo();
3531 ed.sync_buffer_content_from_textarea();
3532 for _ in 0..count.max(1) {
3533 let cursor = buf_cursor_pos(&ed.buffer);
3534 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3535 if cursor.col >= line_chars {
3536 break;
3537 }
3538 ed.mutate_edit(Edit::DeleteRange {
3539 start: cursor,
3540 end: Position::new(cursor.row, cursor.col + 1),
3541 kind: MotionKind::Char,
3542 });
3543 }
3544 ed.push_buffer_cursor_to_textarea();
3545 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3546 if !ed.vim.replaying {
3548 ed.vim.last_change = Some(LastChange::OpMotion {
3549 op: Operator::Change,
3550 motion: Motion::Right,
3551 count: count.max(1),
3552 inserted: None,
3553 });
3554 }
3555 true
3556 }
3557 Key::Char('p') => {
3558 do_paste(ed, false, count.max(1));
3559 if !ed.vim.replaying {
3560 ed.vim.last_change = Some(LastChange::Paste {
3561 before: false,
3562 count: count.max(1),
3563 });
3564 }
3565 true
3566 }
3567 Key::Char('P') => {
3568 do_paste(ed, true, count.max(1));
3569 if !ed.vim.replaying {
3570 ed.vim.last_change = Some(LastChange::Paste {
3571 before: true,
3572 count: count.max(1),
3573 });
3574 }
3575 true
3576 }
3577 Key::Char('u') => {
3578 do_undo(ed);
3579 true
3580 }
3581 Key::Char('r') => {
3582 ed.vim.count = count;
3583 ed.vim.pending = Pending::Replace;
3584 true
3585 }
3586 Key::Char('/') => {
3587 enter_search(ed, true);
3588 true
3589 }
3590 Key::Char('?') => {
3591 enter_search(ed, false);
3592 true
3593 }
3594 Key::Char('.') => {
3595 replay_last_change(ed, count);
3596 true
3597 }
3598 _ => false,
3599 }
3600}
3601
3602fn begin_insert_noundo<H: crate::types::Host>(
3604 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3605 count: usize,
3606 reason: InsertReason,
3607) {
3608 let reason = if ed.vim.replaying {
3609 InsertReason::ReplayOnly
3610 } else {
3611 reason
3612 };
3613 let (row, _) = ed.cursor();
3614 ed.vim.insert_session = Some(InsertSession {
3615 count,
3616 row_min: row,
3617 row_max: row,
3618 before_lines: buf_lines_to_vec(&ed.buffer),
3619 reason,
3620 });
3621 ed.vim.mode = Mode::Insert;
3622}
3623
3624fn apply_op_with_motion<H: crate::types::Host>(
3627 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3628 op: Operator,
3629 motion: &Motion,
3630 count: usize,
3631) {
3632 let start = ed.cursor();
3633 apply_motion_cursor_ctx(ed, motion, count, true);
3638 let end = ed.cursor();
3639 let kind = motion_kind(motion);
3640 ed.jump_cursor(start.0, start.1);
3642 run_operator_over_range(ed, op, start, end, kind);
3643}
3644
3645fn apply_op_with_text_object<H: crate::types::Host>(
3646 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3647 op: Operator,
3648 obj: TextObject,
3649 inner: bool,
3650) {
3651 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3652 return;
3653 };
3654 ed.jump_cursor(start.0, start.1);
3655 run_operator_over_range(ed, op, start, end, kind);
3656}
3657
3658fn motion_kind(motion: &Motion) -> MotionKind {
3659 match motion {
3660 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3661 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3662 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3663 MotionKind::Linewise
3664 }
3665 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3666 MotionKind::Inclusive
3667 }
3668 Motion::Find { .. } => MotionKind::Inclusive,
3669 Motion::MatchBracket => MotionKind::Inclusive,
3670 Motion::LineEnd => MotionKind::Inclusive,
3672 _ => MotionKind::Exclusive,
3673 }
3674}
3675
3676fn run_operator_over_range<H: crate::types::Host>(
3677 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3678 op: Operator,
3679 start: (usize, usize),
3680 end: (usize, usize),
3681 kind: MotionKind,
3682) {
3683 let (top, bot) = order(start, end);
3684 if top == bot {
3685 return;
3686 }
3687
3688 match op {
3689 Operator::Yank => {
3690 let text = read_vim_range(ed, top, bot, kind);
3691 if !text.is_empty() {
3692 ed.record_yank_to_host(text.clone());
3693 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3694 }
3695 let rbr = match kind {
3699 MotionKind::Linewise => {
3700 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3701 (bot.0, last_col)
3702 }
3703 MotionKind::Inclusive => (bot.0, bot.1),
3704 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3705 };
3706 ed.set_mark('[', top);
3707 ed.set_mark(']', rbr);
3708 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3709 ed.push_buffer_cursor_to_textarea();
3710 }
3711 Operator::Delete => {
3712 ed.push_undo();
3713 cut_vim_range(ed, top, bot, kind);
3714 if !matches!(kind, MotionKind::Linewise) {
3719 clamp_cursor_to_normal_mode(ed);
3720 }
3721 ed.vim.mode = Mode::Normal;
3722 let pos = ed.cursor();
3726 ed.set_mark('[', pos);
3727 ed.set_mark(']', pos);
3728 }
3729 Operator::Change => {
3730 ed.vim.change_mark_start = Some(top);
3735 ed.push_undo();
3736 cut_vim_range(ed, top, bot, kind);
3737 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3738 }
3739 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3740 apply_case_op_to_selection(ed, op, top, bot, kind);
3741 }
3742 Operator::Indent | Operator::Outdent => {
3743 ed.push_undo();
3746 if op == Operator::Indent {
3747 indent_rows(ed, top.0, bot.0, 1);
3748 } else {
3749 outdent_rows(ed, top.0, bot.0, 1);
3750 }
3751 ed.vim.mode = Mode::Normal;
3752 }
3753 Operator::Fold => {
3754 if bot.0 >= top.0 {
3758 ed.apply_fold_op(crate::types::FoldOp::Add {
3759 start_row: top.0,
3760 end_row: bot.0,
3761 closed: true,
3762 });
3763 }
3764 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3765 ed.push_buffer_cursor_to_textarea();
3766 ed.vim.mode = Mode::Normal;
3767 }
3768 Operator::Reflow => {
3769 ed.push_undo();
3770 reflow_rows(ed, top.0, bot.0);
3771 ed.vim.mode = Mode::Normal;
3772 }
3773 }
3774}
3775
3776fn reflow_rows<H: crate::types::Host>(
3781 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3782 top: usize,
3783 bot: usize,
3784) {
3785 let width = ed.settings().textwidth.max(1);
3786 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3787 let bot = bot.min(lines.len().saturating_sub(1));
3788 if top > bot {
3789 return;
3790 }
3791 let original = lines[top..=bot].to_vec();
3792 let mut wrapped: Vec<String> = Vec::new();
3793 let mut paragraph: Vec<String> = Vec::new();
3794 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3795 if para.is_empty() {
3796 return;
3797 }
3798 let words = para.join(" ");
3799 let mut current = String::new();
3800 for word in words.split_whitespace() {
3801 let extra = if current.is_empty() {
3802 word.chars().count()
3803 } else {
3804 current.chars().count() + 1 + word.chars().count()
3805 };
3806 if extra > width && !current.is_empty() {
3807 out.push(std::mem::take(&mut current));
3808 current.push_str(word);
3809 } else if current.is_empty() {
3810 current.push_str(word);
3811 } else {
3812 current.push(' ');
3813 current.push_str(word);
3814 }
3815 }
3816 if !current.is_empty() {
3817 out.push(current);
3818 }
3819 para.clear();
3820 };
3821 for line in &original {
3822 if line.trim().is_empty() {
3823 flush(&mut paragraph, &mut wrapped, width);
3824 wrapped.push(String::new());
3825 } else {
3826 paragraph.push(line.clone());
3827 }
3828 }
3829 flush(&mut paragraph, &mut wrapped, width);
3830
3831 let after: Vec<String> = lines.split_off(bot + 1);
3833 lines.truncate(top);
3834 lines.extend(wrapped);
3835 lines.extend(after);
3836 ed.restore(lines, (top, 0));
3837 ed.mark_content_dirty();
3838}
3839
3840fn apply_case_op_to_selection<H: crate::types::Host>(
3846 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3847 op: Operator,
3848 top: (usize, usize),
3849 bot: (usize, usize),
3850 kind: MotionKind,
3851) {
3852 use hjkl_buffer::Edit;
3853 ed.push_undo();
3854 let saved_yank = ed.yank().to_string();
3855 let saved_yank_linewise = ed.vim.yank_linewise;
3856 let selection = cut_vim_range(ed, top, bot, kind);
3857 let transformed = match op {
3858 Operator::Uppercase => selection.to_uppercase(),
3859 Operator::Lowercase => selection.to_lowercase(),
3860 Operator::ToggleCase => toggle_case_str(&selection),
3861 _ => unreachable!(),
3862 };
3863 if !transformed.is_empty() {
3864 let cursor = buf_cursor_pos(&ed.buffer);
3865 ed.mutate_edit(Edit::InsertStr {
3866 at: cursor,
3867 text: transformed,
3868 });
3869 }
3870 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3871 ed.push_buffer_cursor_to_textarea();
3872 ed.set_yank(saved_yank);
3873 ed.vim.yank_linewise = saved_yank_linewise;
3874 ed.vim.mode = Mode::Normal;
3875}
3876
3877fn indent_rows<H: crate::types::Host>(
3882 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3883 top: usize,
3884 bot: usize,
3885 count: usize,
3886) {
3887 ed.sync_buffer_content_from_textarea();
3888 let width = ed.settings().shiftwidth * count.max(1);
3889 let pad: String = " ".repeat(width);
3890 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3891 let bot = bot.min(lines.len().saturating_sub(1));
3892 for line in lines.iter_mut().take(bot + 1).skip(top) {
3893 if !line.is_empty() {
3894 line.insert_str(0, &pad);
3895 }
3896 }
3897 ed.restore(lines, (top, 0));
3900 move_first_non_whitespace(ed);
3901}
3902
3903fn outdent_rows<H: crate::types::Host>(
3907 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3908 top: usize,
3909 bot: usize,
3910 count: usize,
3911) {
3912 ed.sync_buffer_content_from_textarea();
3913 let width = ed.settings().shiftwidth * count.max(1);
3914 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3915 let bot = bot.min(lines.len().saturating_sub(1));
3916 for line in lines.iter_mut().take(bot + 1).skip(top) {
3917 let strip: usize = line
3918 .chars()
3919 .take(width)
3920 .take_while(|c| *c == ' ' || *c == '\t')
3921 .count();
3922 if strip > 0 {
3923 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3924 line.drain(..byte_len);
3925 }
3926 }
3927 ed.restore(lines, (top, 0));
3928 move_first_non_whitespace(ed);
3929}
3930
3931fn toggle_case_str(s: &str) -> String {
3932 s.chars()
3933 .map(|c| {
3934 if c.is_lowercase() {
3935 c.to_uppercase().next().unwrap_or(c)
3936 } else if c.is_uppercase() {
3937 c.to_lowercase().next().unwrap_or(c)
3938 } else {
3939 c
3940 }
3941 })
3942 .collect()
3943}
3944
3945fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3946 if a <= b { (a, b) } else { (b, a) }
3947}
3948
3949fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3954 let (row, col) = ed.cursor();
3955 let line_chars = buf_line_chars(&ed.buffer, row);
3956 let max_col = line_chars.saturating_sub(1);
3957 if col > max_col {
3958 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
3959 ed.push_buffer_cursor_to_textarea();
3960 }
3961}
3962
3963fn execute_line_op<H: crate::types::Host>(
3966 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3967 op: Operator,
3968 count: usize,
3969) {
3970 let (row, col) = ed.cursor();
3971 let total = buf_row_count(&ed.buffer);
3972 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3973
3974 match op {
3975 Operator::Yank => {
3976 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3978 if !text.is_empty() {
3979 ed.record_yank_to_host(text.clone());
3980 ed.record_yank(text, true);
3981 }
3982 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
3985 ed.set_mark('[', (row, 0));
3986 ed.set_mark(']', (end_row, last_col));
3987 buf_set_cursor_rc(&mut ed.buffer, row, col);
3988 ed.push_buffer_cursor_to_textarea();
3989 ed.vim.mode = Mode::Normal;
3990 }
3991 Operator::Delete => {
3992 ed.push_undo();
3993 let deleted_through_last = end_row + 1 >= total;
3994 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3995 let total_after = buf_row_count(&ed.buffer);
3999 let raw_target = if deleted_through_last {
4000 row.saturating_sub(1).min(total_after.saturating_sub(1))
4001 } else {
4002 row.min(total_after.saturating_sub(1))
4003 };
4004 let target_row = if raw_target > 0
4010 && raw_target + 1 == total_after
4011 && buf_line(&ed.buffer, raw_target)
4012 .map(str::is_empty)
4013 .unwrap_or(false)
4014 {
4015 raw_target - 1
4016 } else {
4017 raw_target
4018 };
4019 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4020 ed.push_buffer_cursor_to_textarea();
4021 move_first_non_whitespace(ed);
4022 ed.sticky_col = Some(ed.cursor().1);
4023 ed.vim.mode = Mode::Normal;
4024 let pos = ed.cursor();
4027 ed.set_mark('[', pos);
4028 ed.set_mark(']', pos);
4029 }
4030 Operator::Change => {
4031 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4035 ed.vim.change_mark_start = Some((row, 0));
4037 ed.push_undo();
4038 ed.sync_buffer_content_from_textarea();
4039 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4041 if end_row > row {
4042 ed.mutate_edit(Edit::DeleteRange {
4043 start: Position::new(row + 1, 0),
4044 end: Position::new(end_row, 0),
4045 kind: BufKind::Line,
4046 });
4047 }
4048 let line_chars = buf_line_chars(&ed.buffer, row);
4049 if line_chars > 0 {
4050 ed.mutate_edit(Edit::DeleteRange {
4051 start: Position::new(row, 0),
4052 end: Position::new(row, line_chars),
4053 kind: BufKind::Char,
4054 });
4055 }
4056 if !payload.is_empty() {
4057 ed.record_yank_to_host(payload.clone());
4058 ed.record_delete(payload, true);
4059 }
4060 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4061 ed.push_buffer_cursor_to_textarea();
4062 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4063 }
4064 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4065 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4069 move_first_non_whitespace(ed);
4072 }
4073 Operator::Indent | Operator::Outdent => {
4074 ed.push_undo();
4076 if op == Operator::Indent {
4077 indent_rows(ed, row, end_row, 1);
4078 } else {
4079 outdent_rows(ed, row, end_row, 1);
4080 }
4081 ed.sticky_col = Some(ed.cursor().1);
4082 ed.vim.mode = Mode::Normal;
4083 }
4084 Operator::Fold => unreachable!("Fold has no line-op double"),
4086 Operator::Reflow => {
4087 ed.push_undo();
4089 reflow_rows(ed, row, end_row);
4090 move_first_non_whitespace(ed);
4091 ed.sticky_col = Some(ed.cursor().1);
4092 ed.vim.mode = Mode::Normal;
4093 }
4094 }
4095}
4096
4097fn apply_visual_operator<H: crate::types::Host>(
4100 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4101 op: Operator,
4102) {
4103 match ed.vim.mode {
4104 Mode::VisualLine => {
4105 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4106 let top = cursor_row.min(ed.vim.visual_line_anchor);
4107 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4108 ed.vim.yank_linewise = true;
4109 match op {
4110 Operator::Yank => {
4111 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4112 if !text.is_empty() {
4113 ed.record_yank_to_host(text.clone());
4114 ed.record_yank(text, true);
4115 }
4116 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4117 ed.push_buffer_cursor_to_textarea();
4118 ed.vim.mode = Mode::Normal;
4119 }
4120 Operator::Delete => {
4121 ed.push_undo();
4122 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4123 ed.vim.mode = Mode::Normal;
4124 }
4125 Operator::Change => {
4126 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4129 ed.push_undo();
4130 ed.sync_buffer_content_from_textarea();
4131 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4132 if bot > top {
4133 ed.mutate_edit(Edit::DeleteRange {
4134 start: Position::new(top + 1, 0),
4135 end: Position::new(bot, 0),
4136 kind: BufKind::Line,
4137 });
4138 }
4139 let line_chars = buf_line_chars(&ed.buffer, top);
4140 if line_chars > 0 {
4141 ed.mutate_edit(Edit::DeleteRange {
4142 start: Position::new(top, 0),
4143 end: Position::new(top, line_chars),
4144 kind: BufKind::Char,
4145 });
4146 }
4147 if !payload.is_empty() {
4148 ed.record_yank_to_host(payload.clone());
4149 ed.record_delete(payload, true);
4150 }
4151 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4152 ed.push_buffer_cursor_to_textarea();
4153 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4154 }
4155 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4156 let bot = buf_cursor_pos(&ed.buffer)
4157 .row
4158 .max(ed.vim.visual_line_anchor);
4159 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4160 move_first_non_whitespace(ed);
4161 }
4162 Operator::Indent | Operator::Outdent => {
4163 ed.push_undo();
4164 let (cursor_row, _) = ed.cursor();
4165 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4166 if op == Operator::Indent {
4167 indent_rows(ed, top, bot, 1);
4168 } else {
4169 outdent_rows(ed, top, bot, 1);
4170 }
4171 ed.vim.mode = Mode::Normal;
4172 }
4173 Operator::Reflow => {
4174 ed.push_undo();
4175 let (cursor_row, _) = ed.cursor();
4176 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4177 reflow_rows(ed, top, bot);
4178 ed.vim.mode = Mode::Normal;
4179 }
4180 Operator::Fold => unreachable!("Visual zf takes its own path"),
4183 }
4184 }
4185 Mode::Visual => {
4186 ed.vim.yank_linewise = false;
4187 let anchor = ed.vim.visual_anchor;
4188 let cursor = ed.cursor();
4189 let (top, bot) = order(anchor, cursor);
4190 match op {
4191 Operator::Yank => {
4192 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4193 if !text.is_empty() {
4194 ed.record_yank_to_host(text.clone());
4195 ed.record_yank(text, false);
4196 }
4197 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4198 ed.push_buffer_cursor_to_textarea();
4199 ed.vim.mode = Mode::Normal;
4200 }
4201 Operator::Delete => {
4202 ed.push_undo();
4203 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4204 ed.vim.mode = Mode::Normal;
4205 }
4206 Operator::Change => {
4207 ed.push_undo();
4208 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4209 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4210 }
4211 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4212 let anchor = ed.vim.visual_anchor;
4214 let cursor = ed.cursor();
4215 let (top, bot) = order(anchor, cursor);
4216 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4217 }
4218 Operator::Indent | Operator::Outdent => {
4219 ed.push_undo();
4220 let anchor = ed.vim.visual_anchor;
4221 let cursor = ed.cursor();
4222 let (top, bot) = order(anchor, cursor);
4223 if op == Operator::Indent {
4224 indent_rows(ed, top.0, bot.0, 1);
4225 } else {
4226 outdent_rows(ed, top.0, bot.0, 1);
4227 }
4228 ed.vim.mode = Mode::Normal;
4229 }
4230 Operator::Reflow => {
4231 ed.push_undo();
4232 let anchor = ed.vim.visual_anchor;
4233 let cursor = ed.cursor();
4234 let (top, bot) = order(anchor, cursor);
4235 reflow_rows(ed, top.0, bot.0);
4236 ed.vim.mode = Mode::Normal;
4237 }
4238 Operator::Fold => unreachable!("Visual zf takes its own path"),
4239 }
4240 }
4241 Mode::VisualBlock => apply_block_operator(ed, op),
4242 _ => {}
4243 }
4244}
4245
4246fn block_bounds<H: crate::types::Host>(
4251 ed: &Editor<hjkl_buffer::Buffer, H>,
4252) -> (usize, usize, usize, usize) {
4253 let (ar, ac) = ed.vim.block_anchor;
4254 let (cr, _) = ed.cursor();
4255 let cc = ed.vim.block_vcol;
4256 let top = ar.min(cr);
4257 let bot = ar.max(cr);
4258 let left = ac.min(cc);
4259 let right = ac.max(cc);
4260 (top, bot, left, right)
4261}
4262
4263fn update_block_vcol<H: crate::types::Host>(
4268 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4269 motion: &Motion,
4270) {
4271 match motion {
4272 Motion::Left
4273 | Motion::Right
4274 | Motion::WordFwd
4275 | Motion::BigWordFwd
4276 | Motion::WordBack
4277 | Motion::BigWordBack
4278 | Motion::WordEnd
4279 | Motion::BigWordEnd
4280 | Motion::WordEndBack
4281 | Motion::BigWordEndBack
4282 | Motion::LineStart
4283 | Motion::FirstNonBlank
4284 | Motion::LineEnd
4285 | Motion::Find { .. }
4286 | Motion::FindRepeat { .. }
4287 | Motion::MatchBracket => {
4288 ed.vim.block_vcol = ed.cursor().1;
4289 }
4290 _ => {}
4292 }
4293}
4294
4295fn apply_block_operator<H: crate::types::Host>(
4300 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4301 op: Operator,
4302) {
4303 let (top, bot, left, right) = block_bounds(ed);
4304 let yank = block_yank(ed, top, bot, left, right);
4306
4307 match op {
4308 Operator::Yank => {
4309 if !yank.is_empty() {
4310 ed.record_yank_to_host(yank.clone());
4311 ed.record_yank(yank, false);
4312 }
4313 ed.vim.mode = Mode::Normal;
4314 ed.jump_cursor(top, left);
4315 }
4316 Operator::Delete => {
4317 ed.push_undo();
4318 delete_block_contents(ed, top, bot, left, right);
4319 if !yank.is_empty() {
4320 ed.record_yank_to_host(yank.clone());
4321 ed.record_delete(yank, false);
4322 }
4323 ed.vim.mode = Mode::Normal;
4324 ed.jump_cursor(top, left);
4325 }
4326 Operator::Change => {
4327 ed.push_undo();
4328 delete_block_contents(ed, top, bot, left, right);
4329 if !yank.is_empty() {
4330 ed.record_yank_to_host(yank.clone());
4331 ed.record_delete(yank, false);
4332 }
4333 ed.jump_cursor(top, left);
4334 begin_insert_noundo(
4335 ed,
4336 1,
4337 InsertReason::BlockEdge {
4338 top,
4339 bot,
4340 col: left,
4341 },
4342 );
4343 }
4344 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4345 ed.push_undo();
4346 transform_block_case(ed, op, top, bot, left, right);
4347 ed.vim.mode = Mode::Normal;
4348 ed.jump_cursor(top, left);
4349 }
4350 Operator::Indent | Operator::Outdent => {
4351 ed.push_undo();
4355 if op == Operator::Indent {
4356 indent_rows(ed, top, bot, 1);
4357 } else {
4358 outdent_rows(ed, top, bot, 1);
4359 }
4360 ed.vim.mode = Mode::Normal;
4361 }
4362 Operator::Fold => unreachable!("Visual zf takes its own path"),
4363 Operator::Reflow => {
4364 ed.push_undo();
4368 reflow_rows(ed, top, bot);
4369 ed.vim.mode = Mode::Normal;
4370 }
4371 }
4372}
4373
4374fn transform_block_case<H: crate::types::Host>(
4378 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4379 op: Operator,
4380 top: usize,
4381 bot: usize,
4382 left: usize,
4383 right: usize,
4384) {
4385 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4386 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4387 let chars: Vec<char> = lines[r].chars().collect();
4388 if left >= chars.len() {
4389 continue;
4390 }
4391 let end = (right + 1).min(chars.len());
4392 let head: String = chars[..left].iter().collect();
4393 let mid: String = chars[left..end].iter().collect();
4394 let tail: String = chars[end..].iter().collect();
4395 let transformed = match op {
4396 Operator::Uppercase => mid.to_uppercase(),
4397 Operator::Lowercase => mid.to_lowercase(),
4398 Operator::ToggleCase => toggle_case_str(&mid),
4399 _ => mid,
4400 };
4401 lines[r] = format!("{head}{transformed}{tail}");
4402 }
4403 let saved_yank = ed.yank().to_string();
4404 let saved_linewise = ed.vim.yank_linewise;
4405 ed.restore(lines, (top, left));
4406 ed.set_yank(saved_yank);
4407 ed.vim.yank_linewise = saved_linewise;
4408}
4409
4410fn block_yank<H: crate::types::Host>(
4411 ed: &Editor<hjkl_buffer::Buffer, H>,
4412 top: usize,
4413 bot: usize,
4414 left: usize,
4415 right: usize,
4416) -> String {
4417 let lines = buf_lines_to_vec(&ed.buffer);
4418 let mut rows: Vec<String> = Vec::new();
4419 for r in top..=bot {
4420 let line = match lines.get(r) {
4421 Some(l) => l,
4422 None => break,
4423 };
4424 let chars: Vec<char> = line.chars().collect();
4425 let end = (right + 1).min(chars.len());
4426 if left >= chars.len() {
4427 rows.push(String::new());
4428 } else {
4429 rows.push(chars[left..end].iter().collect());
4430 }
4431 }
4432 rows.join("\n")
4433}
4434
4435fn delete_block_contents<H: crate::types::Host>(
4436 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4437 top: usize,
4438 bot: usize,
4439 left: usize,
4440 right: usize,
4441) {
4442 use hjkl_buffer::{Edit, MotionKind, Position};
4443 ed.sync_buffer_content_from_textarea();
4444 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4445 if last_row < top {
4446 return;
4447 }
4448 ed.mutate_edit(Edit::DeleteRange {
4449 start: Position::new(top, left),
4450 end: Position::new(last_row, right),
4451 kind: MotionKind::Block,
4452 });
4453 ed.push_buffer_cursor_to_textarea();
4454}
4455
4456fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4458 let (top, bot, left, right) = block_bounds(ed);
4459 ed.push_undo();
4460 ed.sync_buffer_content_from_textarea();
4461 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4462 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4463 let chars: Vec<char> = lines[r].chars().collect();
4464 if left >= chars.len() {
4465 continue;
4466 }
4467 let end = (right + 1).min(chars.len());
4468 let before: String = chars[..left].iter().collect();
4469 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4470 let after: String = chars[end..].iter().collect();
4471 lines[r] = format!("{before}{middle}{after}");
4472 }
4473 reset_textarea_lines(ed, lines);
4474 ed.vim.mode = Mode::Normal;
4475 ed.jump_cursor(top, left);
4476}
4477
4478fn reset_textarea_lines<H: crate::types::Host>(
4482 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4483 lines: Vec<String>,
4484) {
4485 let cursor = ed.cursor();
4486 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4487 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4488 ed.mark_content_dirty();
4489}
4490
4491type Pos = (usize, usize);
4497
4498fn text_object_range<H: crate::types::Host>(
4502 ed: &Editor<hjkl_buffer::Buffer, H>,
4503 obj: TextObject,
4504 inner: bool,
4505) -> Option<(Pos, Pos, MotionKind)> {
4506 match obj {
4507 TextObject::Word { big } => {
4508 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4509 }
4510 TextObject::Quote(q) => {
4511 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4512 }
4513 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4514 TextObject::Paragraph => {
4515 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4516 }
4517 TextObject::XmlTag => {
4518 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4519 }
4520 TextObject::Sentence => {
4521 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4522 }
4523 }
4524}
4525
4526fn sentence_boundary<H: crate::types::Host>(
4530 ed: &Editor<hjkl_buffer::Buffer, H>,
4531 forward: bool,
4532) -> Option<(usize, usize)> {
4533 let lines = buf_lines_to_vec(&ed.buffer);
4534 if lines.is_empty() {
4535 return None;
4536 }
4537 let pos_to_idx = |pos: (usize, usize)| -> usize {
4538 let mut idx = 0;
4539 for line in lines.iter().take(pos.0) {
4540 idx += line.chars().count() + 1;
4541 }
4542 idx + pos.1
4543 };
4544 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4545 for (r, line) in lines.iter().enumerate() {
4546 let len = line.chars().count();
4547 if idx <= len {
4548 return (r, idx);
4549 }
4550 idx -= len + 1;
4551 }
4552 let last = lines.len().saturating_sub(1);
4553 (last, lines[last].chars().count())
4554 };
4555 let mut chars: Vec<char> = Vec::new();
4556 for (r, line) in lines.iter().enumerate() {
4557 chars.extend(line.chars());
4558 if r + 1 < lines.len() {
4559 chars.push('\n');
4560 }
4561 }
4562 if chars.is_empty() {
4563 return None;
4564 }
4565 let total = chars.len();
4566 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4567 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4568
4569 if forward {
4570 let mut i = cursor_idx + 1;
4573 while i < total {
4574 if is_terminator(chars[i]) {
4575 while i + 1 < total && is_terminator(chars[i + 1]) {
4576 i += 1;
4577 }
4578 if i + 1 >= total {
4579 return None;
4580 }
4581 if chars[i + 1].is_whitespace() {
4582 let mut j = i + 1;
4583 while j < total && chars[j].is_whitespace() {
4584 j += 1;
4585 }
4586 if j >= total {
4587 return None;
4588 }
4589 return Some(idx_to_pos(j));
4590 }
4591 }
4592 i += 1;
4593 }
4594 None
4595 } else {
4596 let find_start = |from: usize| -> Option<usize> {
4600 let mut start = from;
4601 while start > 0 {
4602 let prev = chars[start - 1];
4603 if prev.is_whitespace() {
4604 let mut k = start - 1;
4605 while k > 0 && chars[k - 1].is_whitespace() {
4606 k -= 1;
4607 }
4608 if k > 0 && is_terminator(chars[k - 1]) {
4609 break;
4610 }
4611 }
4612 start -= 1;
4613 }
4614 while start < total && chars[start].is_whitespace() {
4615 start += 1;
4616 }
4617 (start < total).then_some(start)
4618 };
4619 let current_start = find_start(cursor_idx)?;
4620 if current_start < cursor_idx {
4621 return Some(idx_to_pos(current_start));
4622 }
4623 let mut k = current_start;
4626 while k > 0 && chars[k - 1].is_whitespace() {
4627 k -= 1;
4628 }
4629 if k == 0 {
4630 return None;
4631 }
4632 let prev_start = find_start(k - 1)?;
4633 Some(idx_to_pos(prev_start))
4634 }
4635}
4636
4637fn sentence_text_object<H: crate::types::Host>(
4643 ed: &Editor<hjkl_buffer::Buffer, H>,
4644 inner: bool,
4645) -> Option<((usize, usize), (usize, usize))> {
4646 let lines = buf_lines_to_vec(&ed.buffer);
4647 if lines.is_empty() {
4648 return None;
4649 }
4650 let pos_to_idx = |pos: (usize, usize)| -> usize {
4653 let mut idx = 0;
4654 for line in lines.iter().take(pos.0) {
4655 idx += line.chars().count() + 1;
4656 }
4657 idx + pos.1
4658 };
4659 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4660 for (r, line) in lines.iter().enumerate() {
4661 let len = line.chars().count();
4662 if idx <= len {
4663 return (r, idx);
4664 }
4665 idx -= len + 1;
4666 }
4667 let last = lines.len().saturating_sub(1);
4668 (last, lines[last].chars().count())
4669 };
4670 let mut chars: Vec<char> = Vec::new();
4671 for (r, line) in lines.iter().enumerate() {
4672 chars.extend(line.chars());
4673 if r + 1 < lines.len() {
4674 chars.push('\n');
4675 }
4676 }
4677 if chars.is_empty() {
4678 return None;
4679 }
4680
4681 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4682 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4683
4684 let mut start = cursor_idx;
4688 while start > 0 {
4689 let prev = chars[start - 1];
4690 if prev.is_whitespace() {
4691 let mut k = start - 1;
4695 while k > 0 && chars[k - 1].is_whitespace() {
4696 k -= 1;
4697 }
4698 if k > 0 && is_terminator(chars[k - 1]) {
4699 break;
4700 }
4701 }
4702 start -= 1;
4703 }
4704 while start < chars.len() && chars[start].is_whitespace() {
4707 start += 1;
4708 }
4709 if start >= chars.len() {
4710 return None;
4711 }
4712
4713 let mut end = start;
4716 while end < chars.len() {
4717 if is_terminator(chars[end]) {
4718 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4720 end += 1;
4721 }
4722 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4725 break;
4726 }
4727 }
4728 end += 1;
4729 }
4730 let end_idx = (end + 1).min(chars.len());
4732
4733 let final_end = if inner {
4734 end_idx
4735 } else {
4736 let mut e = end_idx;
4740 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4741 e += 1;
4742 }
4743 e
4744 };
4745
4746 Some((idx_to_pos(start), idx_to_pos(final_end)))
4747}
4748
4749fn tag_text_object<H: crate::types::Host>(
4753 ed: &Editor<hjkl_buffer::Buffer, H>,
4754 inner: bool,
4755) -> Option<((usize, usize), (usize, usize))> {
4756 let lines = buf_lines_to_vec(&ed.buffer);
4757 if lines.is_empty() {
4758 return None;
4759 }
4760 let pos_to_idx = |pos: (usize, usize)| -> usize {
4764 let mut idx = 0;
4765 for line in lines.iter().take(pos.0) {
4766 idx += line.chars().count() + 1;
4767 }
4768 idx + pos.1
4769 };
4770 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4771 for (r, line) in lines.iter().enumerate() {
4772 let len = line.chars().count();
4773 if idx <= len {
4774 return (r, idx);
4775 }
4776 idx -= len + 1;
4777 }
4778 let last = lines.len().saturating_sub(1);
4779 (last, lines[last].chars().count())
4780 };
4781 let mut chars: Vec<char> = Vec::new();
4782 for (r, line) in lines.iter().enumerate() {
4783 chars.extend(line.chars());
4784 if r + 1 < lines.len() {
4785 chars.push('\n');
4786 }
4787 }
4788 let cursor_idx = pos_to_idx(ed.cursor());
4789
4790 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4798 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4799 let mut i = 0;
4800 while i < chars.len() {
4801 if chars[i] != '<' {
4802 i += 1;
4803 continue;
4804 }
4805 let mut j = i + 1;
4806 while j < chars.len() && chars[j] != '>' {
4807 j += 1;
4808 }
4809 if j >= chars.len() {
4810 break;
4811 }
4812 let inside: String = chars[i + 1..j].iter().collect();
4813 let close_end = j + 1;
4814 let trimmed = inside.trim();
4815 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4816 i = close_end;
4817 continue;
4818 }
4819 if let Some(rest) = trimmed.strip_prefix('/') {
4820 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4821 if !name.is_empty()
4822 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4823 {
4824 let (open_start, content_start, _) = stack[stack_idx].clone();
4825 stack.truncate(stack_idx);
4826 let content_end = i;
4827 let candidate = (open_start, content_start, content_end, close_end);
4828 if cursor_idx >= content_start && cursor_idx <= content_end {
4829 innermost = match innermost {
4830 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4831 Some(candidate)
4832 }
4833 None => Some(candidate),
4834 existing => existing,
4835 };
4836 } else if open_start >= cursor_idx && next_after.is_none() {
4837 next_after = Some(candidate);
4838 }
4839 }
4840 } else if !trimmed.ends_with('/') {
4841 let name: String = trimmed
4842 .split(|c: char| c.is_whitespace() || c == '/')
4843 .next()
4844 .unwrap_or("")
4845 .to_string();
4846 if !name.is_empty() {
4847 stack.push((i, close_end, name));
4848 }
4849 }
4850 i = close_end;
4851 }
4852
4853 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4854 if inner {
4855 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4856 } else {
4857 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4858 }
4859}
4860
4861fn is_wordchar(c: char) -> bool {
4862 c.is_alphanumeric() || c == '_'
4863}
4864
4865pub(crate) use hjkl_buffer::is_keyword_char;
4869
4870fn word_text_object<H: crate::types::Host>(
4871 ed: &Editor<hjkl_buffer::Buffer, H>,
4872 inner: bool,
4873 big: bool,
4874) -> Option<((usize, usize), (usize, usize))> {
4875 let (row, col) = ed.cursor();
4876 let line = buf_line(&ed.buffer, row)?;
4877 let chars: Vec<char> = line.chars().collect();
4878 if chars.is_empty() {
4879 return None;
4880 }
4881 let at = col.min(chars.len().saturating_sub(1));
4882 let classify = |c: char| -> u8 {
4883 if c.is_whitespace() {
4884 0
4885 } else if big || is_wordchar(c) {
4886 1
4887 } else {
4888 2
4889 }
4890 };
4891 let cls = classify(chars[at]);
4892 let mut start = at;
4893 while start > 0 && classify(chars[start - 1]) == cls {
4894 start -= 1;
4895 }
4896 let mut end = at;
4897 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4898 end += 1;
4899 }
4900 let char_byte = |i: usize| {
4902 if i >= chars.len() {
4903 line.len()
4904 } else {
4905 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4906 }
4907 };
4908 let mut start_col = char_byte(start);
4909 let mut end_col = char_byte(end + 1);
4911 if !inner {
4912 let mut t = end + 1;
4914 let mut included_trailing = false;
4915 while t < chars.len() && chars[t].is_whitespace() {
4916 included_trailing = true;
4917 t += 1;
4918 }
4919 if included_trailing {
4920 end_col = char_byte(t);
4921 } else {
4922 let mut s = start;
4923 while s > 0 && chars[s - 1].is_whitespace() {
4924 s -= 1;
4925 }
4926 start_col = char_byte(s);
4927 }
4928 }
4929 Some(((row, start_col), (row, end_col)))
4930}
4931
4932fn quote_text_object<H: crate::types::Host>(
4933 ed: &Editor<hjkl_buffer::Buffer, H>,
4934 q: char,
4935 inner: bool,
4936) -> Option<((usize, usize), (usize, usize))> {
4937 let (row, col) = ed.cursor();
4938 let line = buf_line(&ed.buffer, row)?;
4939 let bytes = line.as_bytes();
4940 let q_byte = q as u8;
4941 let mut positions: Vec<usize> = Vec::new();
4943 for (i, &b) in bytes.iter().enumerate() {
4944 if b == q_byte {
4945 positions.push(i);
4946 }
4947 }
4948 if positions.len() < 2 {
4949 return None;
4950 }
4951 let mut open_idx: Option<usize> = None;
4952 let mut close_idx: Option<usize> = None;
4953 for pair in positions.chunks(2) {
4954 if pair.len() < 2 {
4955 break;
4956 }
4957 if col >= pair[0] && col <= pair[1] {
4958 open_idx = Some(pair[0]);
4959 close_idx = Some(pair[1]);
4960 break;
4961 }
4962 if col < pair[0] {
4963 open_idx = Some(pair[0]);
4964 close_idx = Some(pair[1]);
4965 break;
4966 }
4967 }
4968 let open = open_idx?;
4969 let close = close_idx?;
4970 if inner {
4972 if close <= open + 1 {
4973 return None;
4974 }
4975 Some(((row, open + 1), (row, close)))
4976 } else {
4977 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
4984 let mut end = after_close;
4986 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
4987 end += 1;
4988 }
4989 Some(((row, open), (row, end)))
4990 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
4991 let mut start = open;
4993 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
4994 start -= 1;
4995 }
4996 Some(((row, start), (row, close + 1)))
4997 } else {
4998 Some(((row, open), (row, close + 1)))
4999 }
5000 }
5001}
5002
5003fn bracket_text_object<H: crate::types::Host>(
5004 ed: &Editor<hjkl_buffer::Buffer, H>,
5005 open: char,
5006 inner: bool,
5007) -> Option<(Pos, Pos, MotionKind)> {
5008 let close = match open {
5009 '(' => ')',
5010 '[' => ']',
5011 '{' => '}',
5012 '<' => '>',
5013 _ => return None,
5014 };
5015 let (row, col) = ed.cursor();
5016 let lines = buf_lines_to_vec(&ed.buffer);
5017 let lines = lines.as_slice();
5018 let open_pos = find_open_bracket(lines, row, col, open, close)
5023 .or_else(|| find_next_open(lines, row, col, open))?;
5024 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5025 if inner {
5027 if close_pos.0 > open_pos.0 + 1 {
5033 let inner_row_start = open_pos.0 + 1;
5035 let inner_row_end = close_pos.0 - 1;
5036 let end_col = lines
5037 .get(inner_row_end)
5038 .map(|l| l.chars().count())
5039 .unwrap_or(0);
5040 return Some((
5041 (inner_row_start, 0),
5042 (inner_row_end, end_col),
5043 MotionKind::Linewise,
5044 ));
5045 }
5046 let inner_start = advance_pos(lines, open_pos);
5047 if inner_start.0 > close_pos.0
5048 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5049 {
5050 return None;
5051 }
5052 Some((inner_start, close_pos, MotionKind::Exclusive))
5053 } else {
5054 Some((
5055 open_pos,
5056 advance_pos(lines, close_pos),
5057 MotionKind::Exclusive,
5058 ))
5059 }
5060}
5061
5062fn find_open_bracket(
5063 lines: &[String],
5064 row: usize,
5065 col: usize,
5066 open: char,
5067 close: char,
5068) -> Option<(usize, usize)> {
5069 let mut depth: i32 = 0;
5070 let mut r = row;
5071 let mut c = col as isize;
5072 loop {
5073 let cur = &lines[r];
5074 let chars: Vec<char> = cur.chars().collect();
5075 if (c as usize) >= chars.len() {
5079 c = chars.len() as isize - 1;
5080 }
5081 while c >= 0 {
5082 let ch = chars[c as usize];
5083 if ch == close {
5084 depth += 1;
5085 } else if ch == open {
5086 if depth == 0 {
5087 return Some((r, c as usize));
5088 }
5089 depth -= 1;
5090 }
5091 c -= 1;
5092 }
5093 if r == 0 {
5094 return None;
5095 }
5096 r -= 1;
5097 c = lines[r].chars().count() as isize - 1;
5098 }
5099}
5100
5101fn find_close_bracket(
5102 lines: &[String],
5103 row: usize,
5104 start_col: usize,
5105 open: char,
5106 close: char,
5107) -> Option<(usize, usize)> {
5108 let mut depth: i32 = 0;
5109 let mut r = row;
5110 let mut c = start_col;
5111 loop {
5112 let cur = &lines[r];
5113 let chars: Vec<char> = cur.chars().collect();
5114 while c < chars.len() {
5115 let ch = chars[c];
5116 if ch == open {
5117 depth += 1;
5118 } else if ch == close {
5119 if depth == 0 {
5120 return Some((r, c));
5121 }
5122 depth -= 1;
5123 }
5124 c += 1;
5125 }
5126 if r + 1 >= lines.len() {
5127 return None;
5128 }
5129 r += 1;
5130 c = 0;
5131 }
5132}
5133
5134fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5138 let mut r = row;
5139 let mut c = col;
5140 while r < lines.len() {
5141 let chars: Vec<char> = lines[r].chars().collect();
5142 while c < chars.len() {
5143 if chars[c] == open {
5144 return Some((r, c));
5145 }
5146 c += 1;
5147 }
5148 r += 1;
5149 c = 0;
5150 }
5151 None
5152}
5153
5154fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5155 let (r, c) = pos;
5156 let line_len = lines[r].chars().count();
5157 if c < line_len {
5158 (r, c + 1)
5159 } else if r + 1 < lines.len() {
5160 (r + 1, 0)
5161 } else {
5162 pos
5163 }
5164}
5165
5166fn paragraph_text_object<H: crate::types::Host>(
5167 ed: &Editor<hjkl_buffer::Buffer, H>,
5168 inner: bool,
5169) -> Option<((usize, usize), (usize, usize))> {
5170 let (row, _) = ed.cursor();
5171 let lines = buf_lines_to_vec(&ed.buffer);
5172 if lines.is_empty() {
5173 return None;
5174 }
5175 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5177 if is_blank(row) {
5178 return None;
5179 }
5180 let mut top = row;
5181 while top > 0 && !is_blank(top - 1) {
5182 top -= 1;
5183 }
5184 let mut bot = row;
5185 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5186 bot += 1;
5187 }
5188 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5190 bot += 1;
5191 }
5192 let end_col = lines[bot].chars().count();
5193 Some(((top, 0), (bot, end_col)))
5194}
5195
5196fn read_vim_range<H: crate::types::Host>(
5202 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5203 start: (usize, usize),
5204 end: (usize, usize),
5205 kind: MotionKind,
5206) -> String {
5207 let (top, bot) = order(start, end);
5208 ed.sync_buffer_content_from_textarea();
5209 let lines = buf_lines_to_vec(&ed.buffer);
5210 match kind {
5211 MotionKind::Linewise => {
5212 let lo = top.0;
5213 let hi = bot.0.min(lines.len().saturating_sub(1));
5214 let mut text = lines[lo..=hi].join("\n");
5215 text.push('\n');
5216 text
5217 }
5218 MotionKind::Inclusive | MotionKind::Exclusive => {
5219 let inclusive = matches!(kind, MotionKind::Inclusive);
5220 let mut out = String::new();
5222 for row in top.0..=bot.0 {
5223 let line = lines.get(row).map(String::as_str).unwrap_or("");
5224 let lo = if row == top.0 { top.1 } else { 0 };
5225 let hi_unclamped = if row == bot.0 {
5226 if inclusive { bot.1 + 1 } else { bot.1 }
5227 } else {
5228 line.chars().count() + 1
5229 };
5230 let row_chars: Vec<char> = line.chars().collect();
5231 let hi = hi_unclamped.min(row_chars.len());
5232 if lo < hi {
5233 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5234 }
5235 if row < bot.0 {
5236 out.push('\n');
5237 }
5238 }
5239 out
5240 }
5241 }
5242}
5243
5244fn cut_vim_range<H: crate::types::Host>(
5253 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5254 start: (usize, usize),
5255 end: (usize, usize),
5256 kind: MotionKind,
5257) -> String {
5258 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5259 let (top, bot) = order(start, end);
5260 ed.sync_buffer_content_from_textarea();
5261 let (buf_start, buf_end, buf_kind) = match kind {
5262 MotionKind::Linewise => (
5263 Position::new(top.0, 0),
5264 Position::new(bot.0, 0),
5265 BufKind::Line,
5266 ),
5267 MotionKind::Inclusive => {
5268 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5269 let next = if bot.1 < line_chars {
5273 Position::new(bot.0, bot.1 + 1)
5274 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5275 Position::new(bot.0 + 1, 0)
5276 } else {
5277 Position::new(bot.0, line_chars)
5278 };
5279 (Position::new(top.0, top.1), next, BufKind::Char)
5280 }
5281 MotionKind::Exclusive => (
5282 Position::new(top.0, top.1),
5283 Position::new(bot.0, bot.1),
5284 BufKind::Char,
5285 ),
5286 };
5287 let inverse = ed.mutate_edit(Edit::DeleteRange {
5288 start: buf_start,
5289 end: buf_end,
5290 kind: buf_kind,
5291 });
5292 let text = match inverse {
5293 Edit::InsertStr { text, .. } => text,
5294 _ => String::new(),
5295 };
5296 if !text.is_empty() {
5297 ed.record_yank_to_host(text.clone());
5298 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5299 }
5300 ed.push_buffer_cursor_to_textarea();
5301 text
5302}
5303
5304fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5310 use hjkl_buffer::{Edit, MotionKind, Position};
5311 ed.sync_buffer_content_from_textarea();
5312 let cursor = buf_cursor_pos(&ed.buffer);
5313 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5314 if cursor.col >= line_chars {
5315 return;
5316 }
5317 let inverse = ed.mutate_edit(Edit::DeleteRange {
5318 start: cursor,
5319 end: Position::new(cursor.row, line_chars),
5320 kind: MotionKind::Char,
5321 });
5322 if let Edit::InsertStr { text, .. } = inverse
5323 && !text.is_empty()
5324 {
5325 ed.record_yank_to_host(text.clone());
5326 ed.vim.yank_linewise = false;
5327 ed.set_yank(text);
5328 }
5329 buf_set_cursor_pos(&mut ed.buffer, cursor);
5330 ed.push_buffer_cursor_to_textarea();
5331}
5332
5333fn do_char_delete<H: crate::types::Host>(
5334 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5335 forward: bool,
5336 count: usize,
5337) {
5338 use hjkl_buffer::{Edit, MotionKind, Position};
5339 ed.push_undo();
5340 ed.sync_buffer_content_from_textarea();
5341 let mut deleted = String::new();
5344 for _ in 0..count {
5345 let cursor = buf_cursor_pos(&ed.buffer);
5346 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5347 if forward {
5348 if cursor.col >= line_chars {
5351 continue;
5352 }
5353 let inverse = ed.mutate_edit(Edit::DeleteRange {
5354 start: cursor,
5355 end: Position::new(cursor.row, cursor.col + 1),
5356 kind: MotionKind::Char,
5357 });
5358 if let Edit::InsertStr { text, .. } = inverse {
5359 deleted.push_str(&text);
5360 }
5361 } else {
5362 if cursor.col == 0 {
5364 continue;
5365 }
5366 let inverse = ed.mutate_edit(Edit::DeleteRange {
5367 start: Position::new(cursor.row, cursor.col - 1),
5368 end: cursor,
5369 kind: MotionKind::Char,
5370 });
5371 if let Edit::InsertStr { text, .. } = inverse {
5372 deleted = text + &deleted;
5375 }
5376 }
5377 }
5378 if !deleted.is_empty() {
5379 ed.record_yank_to_host(deleted.clone());
5380 ed.record_delete(deleted, false);
5381 }
5382 ed.push_buffer_cursor_to_textarea();
5383}
5384
5385fn adjust_number<H: crate::types::Host>(
5389 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5390 delta: i64,
5391) -> bool {
5392 use hjkl_buffer::{Edit, MotionKind, Position};
5393 ed.sync_buffer_content_from_textarea();
5394 let cursor = buf_cursor_pos(&ed.buffer);
5395 let row = cursor.row;
5396 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5397 Some(l) => l.chars().collect(),
5398 None => return false,
5399 };
5400 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5401 return false;
5402 };
5403 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5404 digit_start - 1
5405 } else {
5406 digit_start
5407 };
5408 let mut span_end = digit_start;
5409 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5410 span_end += 1;
5411 }
5412 let s: String = chars[span_start..span_end].iter().collect();
5413 let Ok(n) = s.parse::<i64>() else {
5414 return false;
5415 };
5416 let new_s = n.saturating_add(delta).to_string();
5417
5418 ed.push_undo();
5419 let span_start_pos = Position::new(row, span_start);
5420 let span_end_pos = Position::new(row, span_end);
5421 ed.mutate_edit(Edit::DeleteRange {
5422 start: span_start_pos,
5423 end: span_end_pos,
5424 kind: MotionKind::Char,
5425 });
5426 ed.mutate_edit(Edit::InsertStr {
5427 at: span_start_pos,
5428 text: new_s.clone(),
5429 });
5430 let new_len = new_s.chars().count();
5431 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5432 ed.push_buffer_cursor_to_textarea();
5433 true
5434}
5435
5436pub(crate) fn replace_char<H: crate::types::Host>(
5437 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5438 ch: char,
5439 count: usize,
5440) {
5441 use hjkl_buffer::{Edit, MotionKind, Position};
5442 ed.push_undo();
5443 ed.sync_buffer_content_from_textarea();
5444 for _ in 0..count {
5445 let cursor = buf_cursor_pos(&ed.buffer);
5446 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5447 if cursor.col >= line_chars {
5448 break;
5449 }
5450 ed.mutate_edit(Edit::DeleteRange {
5451 start: cursor,
5452 end: Position::new(cursor.row, cursor.col + 1),
5453 kind: MotionKind::Char,
5454 });
5455 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5456 }
5457 crate::motions::move_left(&mut ed.buffer, 1);
5459 ed.push_buffer_cursor_to_textarea();
5460}
5461
5462fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5463 use hjkl_buffer::{Edit, MotionKind, Position};
5464 ed.sync_buffer_content_from_textarea();
5465 let cursor = buf_cursor_pos(&ed.buffer);
5466 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5467 return;
5468 };
5469 let toggled = if c.is_uppercase() {
5470 c.to_lowercase().next().unwrap_or(c)
5471 } else {
5472 c.to_uppercase().next().unwrap_or(c)
5473 };
5474 ed.mutate_edit(Edit::DeleteRange {
5475 start: cursor,
5476 end: Position::new(cursor.row, cursor.col + 1),
5477 kind: MotionKind::Char,
5478 });
5479 ed.mutate_edit(Edit::InsertChar {
5480 at: cursor,
5481 ch: toggled,
5482 });
5483}
5484
5485fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5486 use hjkl_buffer::{Edit, Position};
5487 ed.sync_buffer_content_from_textarea();
5488 let row = buf_cursor_pos(&ed.buffer).row;
5489 if row + 1 >= buf_row_count(&ed.buffer) {
5490 return;
5491 }
5492 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5493 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5494 let next_trimmed = next_raw.trim_start();
5495 let cur_chars = cur_line.chars().count();
5496 let next_chars = next_raw.chars().count();
5497 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5500 " "
5501 } else {
5502 ""
5503 };
5504 let joined = format!("{cur_line}{separator}{next_trimmed}");
5505 ed.mutate_edit(Edit::Replace {
5506 start: Position::new(row, 0),
5507 end: Position::new(row + 1, next_chars),
5508 with: joined,
5509 });
5510 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5514 ed.push_buffer_cursor_to_textarea();
5515}
5516
5517fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5520 use hjkl_buffer::Edit;
5521 ed.sync_buffer_content_from_textarea();
5522 let row = buf_cursor_pos(&ed.buffer).row;
5523 if row + 1 >= buf_row_count(&ed.buffer) {
5524 return;
5525 }
5526 let join_col = buf_line_chars(&ed.buffer, row);
5527 ed.mutate_edit(Edit::JoinLines {
5528 row,
5529 count: 1,
5530 with_space: false,
5531 });
5532 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5534 ed.push_buffer_cursor_to_textarea();
5535}
5536
5537fn do_paste<H: crate::types::Host>(
5538 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5539 before: bool,
5540 count: usize,
5541) {
5542 use hjkl_buffer::{Edit, Position};
5543 ed.push_undo();
5544 let selector = ed.vim.pending_register.take();
5549 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5550 Some(slot) => (slot.text.clone(), slot.linewise),
5551 None => {
5557 let s = &ed.registers().unnamed;
5558 (s.text.clone(), s.linewise)
5559 }
5560 };
5561 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5565 for _ in 0..count {
5566 ed.sync_buffer_content_from_textarea();
5567 let yank = yank.clone();
5568 if yank.is_empty() {
5569 continue;
5570 }
5571 if linewise {
5572 let text = yank.trim_matches('\n').to_string();
5576 let row = buf_cursor_pos(&ed.buffer).row;
5577 let target_row = if before {
5578 ed.mutate_edit(Edit::InsertStr {
5579 at: Position::new(row, 0),
5580 text: format!("{text}\n"),
5581 });
5582 row
5583 } else {
5584 let line_chars = buf_line_chars(&ed.buffer, row);
5585 ed.mutate_edit(Edit::InsertStr {
5586 at: Position::new(row, line_chars),
5587 text: format!("\n{text}"),
5588 });
5589 row + 1
5590 };
5591 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5592 crate::motions::move_first_non_blank(&mut ed.buffer);
5593 ed.push_buffer_cursor_to_textarea();
5594 let payload_lines = text.lines().count().max(1);
5596 let bot_row = target_row + payload_lines - 1;
5597 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5598 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5599 } else {
5600 let cursor = buf_cursor_pos(&ed.buffer);
5604 let at = if before {
5605 cursor
5606 } else {
5607 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5608 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5609 };
5610 ed.mutate_edit(Edit::InsertStr {
5611 at,
5612 text: yank.clone(),
5613 });
5614 crate::motions::move_left(&mut ed.buffer, 1);
5617 ed.push_buffer_cursor_to_textarea();
5618 let lo = (at.row, at.col);
5620 let hi = ed.cursor();
5621 paste_mark = Some((lo, hi));
5622 }
5623 }
5624 if let Some((lo, hi)) = paste_mark {
5625 ed.set_mark('[', lo);
5626 ed.set_mark(']', hi);
5627 }
5628 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5630}
5631
5632pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5633 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5634 let current = ed.snapshot();
5635 ed.redo_stack.push(current);
5636 ed.restore(lines, cursor);
5637 }
5638 ed.vim.mode = Mode::Normal;
5639 clamp_cursor_to_normal_mode(ed);
5643}
5644
5645pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5646 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5647 let current = ed.snapshot();
5648 ed.undo_stack.push(current);
5649 ed.cap_undo();
5650 ed.restore(lines, cursor);
5651 }
5652 ed.vim.mode = Mode::Normal;
5653}
5654
5655fn replay_insert_and_finish<H: crate::types::Host>(
5662 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5663 text: &str,
5664) {
5665 use hjkl_buffer::{Edit, Position};
5666 let cursor = ed.cursor();
5667 ed.mutate_edit(Edit::InsertStr {
5668 at: Position::new(cursor.0, cursor.1),
5669 text: text.to_string(),
5670 });
5671 if ed.vim.insert_session.take().is_some() {
5672 if ed.cursor().1 > 0 {
5673 crate::motions::move_left(&mut ed.buffer, 1);
5674 ed.push_buffer_cursor_to_textarea();
5675 }
5676 ed.vim.mode = Mode::Normal;
5677 }
5678}
5679
5680fn replay_last_change<H: crate::types::Host>(
5681 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5682 outer_count: usize,
5683) {
5684 let Some(change) = ed.vim.last_change.clone() else {
5685 return;
5686 };
5687 ed.vim.replaying = true;
5688 let scale = if outer_count > 0 { outer_count } else { 1 };
5689 match change {
5690 LastChange::OpMotion {
5691 op,
5692 motion,
5693 count,
5694 inserted,
5695 } => {
5696 let total = count.max(1) * scale;
5697 apply_op_with_motion(ed, op, &motion, total);
5698 if let Some(text) = inserted {
5699 replay_insert_and_finish(ed, &text);
5700 }
5701 }
5702 LastChange::OpTextObj {
5703 op,
5704 obj,
5705 inner,
5706 inserted,
5707 } => {
5708 apply_op_with_text_object(ed, op, obj, inner);
5709 if let Some(text) = inserted {
5710 replay_insert_and_finish(ed, &text);
5711 }
5712 }
5713 LastChange::LineOp {
5714 op,
5715 count,
5716 inserted,
5717 } => {
5718 let total = count.max(1) * scale;
5719 execute_line_op(ed, op, total);
5720 if let Some(text) = inserted {
5721 replay_insert_and_finish(ed, &text);
5722 }
5723 }
5724 LastChange::CharDel { forward, count } => {
5725 do_char_delete(ed, forward, count * scale);
5726 }
5727 LastChange::ReplaceChar { ch, count } => {
5728 replace_char(ed, ch, count * scale);
5729 }
5730 LastChange::ToggleCase { count } => {
5731 for _ in 0..count * scale {
5732 ed.push_undo();
5733 toggle_case_at_cursor(ed);
5734 }
5735 }
5736 LastChange::JoinLine { count } => {
5737 for _ in 0..count * scale {
5738 ed.push_undo();
5739 join_line(ed);
5740 }
5741 }
5742 LastChange::Paste { before, count } => {
5743 do_paste(ed, before, count * scale);
5744 }
5745 LastChange::DeleteToEol { inserted } => {
5746 use hjkl_buffer::{Edit, Position};
5747 ed.push_undo();
5748 delete_to_eol(ed);
5749 if let Some(text) = inserted {
5750 let cursor = ed.cursor();
5751 ed.mutate_edit(Edit::InsertStr {
5752 at: Position::new(cursor.0, cursor.1),
5753 text,
5754 });
5755 }
5756 }
5757 LastChange::OpenLine { above, inserted } => {
5758 use hjkl_buffer::{Edit, Position};
5759 ed.push_undo();
5760 ed.sync_buffer_content_from_textarea();
5761 let row = buf_cursor_pos(&ed.buffer).row;
5762 if above {
5763 ed.mutate_edit(Edit::InsertStr {
5764 at: Position::new(row, 0),
5765 text: "\n".to_string(),
5766 });
5767 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5768 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5769 } else {
5770 let line_chars = buf_line_chars(&ed.buffer, row);
5771 ed.mutate_edit(Edit::InsertStr {
5772 at: Position::new(row, line_chars),
5773 text: "\n".to_string(),
5774 });
5775 }
5776 ed.push_buffer_cursor_to_textarea();
5777 let cursor = ed.cursor();
5778 ed.mutate_edit(Edit::InsertStr {
5779 at: Position::new(cursor.0, cursor.1),
5780 text: inserted,
5781 });
5782 }
5783 LastChange::InsertAt {
5784 entry,
5785 inserted,
5786 count,
5787 } => {
5788 use hjkl_buffer::{Edit, Position};
5789 ed.push_undo();
5790 match entry {
5791 InsertEntry::I => {}
5792 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5793 InsertEntry::A => {
5794 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5795 ed.push_buffer_cursor_to_textarea();
5796 }
5797 InsertEntry::ShiftA => {
5798 crate::motions::move_line_end(&mut ed.buffer);
5799 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5800 ed.push_buffer_cursor_to_textarea();
5801 }
5802 }
5803 for _ in 0..count.max(1) {
5804 let cursor = ed.cursor();
5805 ed.mutate_edit(Edit::InsertStr {
5806 at: Position::new(cursor.0, cursor.1),
5807 text: inserted.clone(),
5808 });
5809 }
5810 }
5811 }
5812 ed.vim.replaying = false;
5813}
5814
5815fn extract_inserted(before: &str, after: &str) -> String {
5818 let before_chars: Vec<char> = before.chars().collect();
5819 let after_chars: Vec<char> = after.chars().collect();
5820 if after_chars.len() <= before_chars.len() {
5821 return String::new();
5822 }
5823 let prefix = before_chars
5824 .iter()
5825 .zip(after_chars.iter())
5826 .take_while(|(a, b)| a == b)
5827 .count();
5828 let max_suffix = before_chars.len() - prefix;
5829 let suffix = before_chars
5830 .iter()
5831 .rev()
5832 .zip(after_chars.iter().rev())
5833 .take(max_suffix)
5834 .take_while(|(a, b)| a == b)
5835 .count();
5836 after_chars[prefix..after_chars.len() - suffix]
5837 .iter()
5838 .collect()
5839}
5840
5841#[cfg(all(test, feature = "crossterm"))]
5844mod tests {
5845 use crate::VimMode;
5846 use crate::editor::Editor;
5847 use crate::types::Host;
5848 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5849
5850 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5851 let mut iter = keys.chars().peekable();
5855 while let Some(c) = iter.next() {
5856 if c == '<' {
5857 let mut tag = String::new();
5858 for ch in iter.by_ref() {
5859 if ch == '>' {
5860 break;
5861 }
5862 tag.push(ch);
5863 }
5864 let ev = match tag.as_str() {
5865 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5866 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5867 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5868 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5869 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5870 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5871 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5872 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5873 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5877 s if s.starts_with("C-") => {
5878 let ch = s.chars().nth(2).unwrap();
5879 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5880 }
5881 _ => continue,
5882 };
5883 e.handle_key(ev);
5884 } else {
5885 let mods = if c.is_uppercase() {
5886 KeyModifiers::SHIFT
5887 } else {
5888 KeyModifiers::NONE
5889 };
5890 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5891 }
5892 }
5893 }
5894
5895 fn editor_with(content: &str) -> Editor {
5896 let opts = crate::types::Options {
5901 shiftwidth: 2,
5902 ..crate::types::Options::default()
5903 };
5904 let mut e = Editor::new(
5905 hjkl_buffer::Buffer::new(),
5906 crate::types::DefaultHost::new(),
5907 opts,
5908 );
5909 e.set_content(content);
5910 e
5911 }
5912
5913 #[test]
5914 fn f_char_jumps_on_line() {
5915 let mut e = editor_with("hello world");
5916 run_keys(&mut e, "fw");
5917 assert_eq!(e.cursor(), (0, 6));
5918 }
5919
5920 #[test]
5921 fn cap_f_jumps_backward() {
5922 let mut e = editor_with("hello world");
5923 e.jump_cursor(0, 10);
5924 run_keys(&mut e, "Fo");
5925 assert_eq!(e.cursor().1, 7);
5926 }
5927
5928 #[test]
5929 fn t_stops_before_char() {
5930 let mut e = editor_with("hello");
5931 run_keys(&mut e, "tl");
5932 assert_eq!(e.cursor(), (0, 1));
5933 }
5934
5935 #[test]
5936 fn semicolon_repeats_find() {
5937 let mut e = editor_with("aa.bb.cc");
5938 run_keys(&mut e, "f.");
5939 assert_eq!(e.cursor().1, 2);
5940 run_keys(&mut e, ";");
5941 assert_eq!(e.cursor().1, 5);
5942 }
5943
5944 #[test]
5945 fn comma_repeats_find_reverse() {
5946 let mut e = editor_with("aa.bb.cc");
5947 run_keys(&mut e, "f.");
5948 run_keys(&mut e, ";");
5949 run_keys(&mut e, ",");
5950 assert_eq!(e.cursor().1, 2);
5951 }
5952
5953 #[test]
5954 fn di_quote_deletes_content() {
5955 let mut e = editor_with("foo \"bar\" baz");
5956 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5958 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5959 }
5960
5961 #[test]
5962 fn da_quote_deletes_with_quotes() {
5963 let mut e = editor_with("foo \"bar\" baz");
5966 e.jump_cursor(0, 6);
5967 run_keys(&mut e, "da\"");
5968 assert_eq!(e.buffer().lines()[0], "foo baz");
5969 }
5970
5971 #[test]
5972 fn ci_paren_deletes_and_inserts() {
5973 let mut e = editor_with("fn(a, b, c)");
5974 e.jump_cursor(0, 5);
5975 run_keys(&mut e, "ci(");
5976 assert_eq!(e.vim_mode(), VimMode::Insert);
5977 assert_eq!(e.buffer().lines()[0], "fn()");
5978 }
5979
5980 #[test]
5981 fn diw_deletes_inner_word() {
5982 let mut e = editor_with("hello world");
5983 e.jump_cursor(0, 2);
5984 run_keys(&mut e, "diw");
5985 assert_eq!(e.buffer().lines()[0], " world");
5986 }
5987
5988 #[test]
5989 fn daw_deletes_word_with_trailing_space() {
5990 let mut e = editor_with("hello world");
5991 run_keys(&mut e, "daw");
5992 assert_eq!(e.buffer().lines()[0], "world");
5993 }
5994
5995 #[test]
5996 fn percent_jumps_to_matching_bracket() {
5997 let mut e = editor_with("foo(bar)");
5998 e.jump_cursor(0, 3);
5999 run_keys(&mut e, "%");
6000 assert_eq!(e.cursor().1, 7);
6001 run_keys(&mut e, "%");
6002 assert_eq!(e.cursor().1, 3);
6003 }
6004
6005 #[test]
6006 fn dot_repeats_last_change() {
6007 let mut e = editor_with("aaa bbb ccc");
6008 run_keys(&mut e, "dw");
6009 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6010 run_keys(&mut e, ".");
6011 assert_eq!(e.buffer().lines()[0], "ccc");
6012 }
6013
6014 #[test]
6015 fn dot_repeats_change_operator_with_text() {
6016 let mut e = editor_with("foo foo foo");
6017 run_keys(&mut e, "cwbar<Esc>");
6018 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6019 run_keys(&mut e, "w");
6021 run_keys(&mut e, ".");
6022 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6023 }
6024
6025 #[test]
6026 fn dot_repeats_x() {
6027 let mut e = editor_with("abcdef");
6028 run_keys(&mut e, "x");
6029 run_keys(&mut e, "..");
6030 assert_eq!(e.buffer().lines()[0], "def");
6031 }
6032
6033 #[test]
6034 fn count_operator_motion_compose() {
6035 let mut e = editor_with("one two three four five");
6036 run_keys(&mut e, "d3w");
6037 assert_eq!(e.buffer().lines()[0], "four five");
6038 }
6039
6040 #[test]
6041 fn two_dd_deletes_two_lines() {
6042 let mut e = editor_with("a\nb\nc");
6043 run_keys(&mut e, "2dd");
6044 assert_eq!(e.buffer().lines().len(), 1);
6045 assert_eq!(e.buffer().lines()[0], "c");
6046 }
6047
6048 #[test]
6053 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6054 let mut e = editor_with("one\ntwo\n three\nfour");
6055 e.jump_cursor(1, 2);
6056 run_keys(&mut e, "dd");
6057 assert_eq!(e.buffer().lines()[1], " three");
6059 assert_eq!(e.cursor(), (1, 4));
6060 }
6061
6062 #[test]
6063 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6064 let mut e = editor_with("one\n two\nthree");
6065 e.jump_cursor(2, 0);
6066 run_keys(&mut e, "dd");
6067 assert_eq!(e.buffer().lines().len(), 2);
6069 assert_eq!(e.cursor(), (1, 2));
6070 }
6071
6072 #[test]
6073 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6074 let mut e = editor_with("lonely");
6075 run_keys(&mut e, "dd");
6076 assert_eq!(e.buffer().lines().len(), 1);
6077 assert_eq!(e.buffer().lines()[0], "");
6078 assert_eq!(e.cursor(), (0, 0));
6079 }
6080
6081 #[test]
6082 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6083 let mut e = editor_with("a\nb\nc\n d\ne");
6084 e.jump_cursor(1, 0);
6086 run_keys(&mut e, "3dd");
6087 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6088 assert_eq!(e.cursor(), (1, 0));
6089 }
6090
6091 #[test]
6092 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6093 let mut e = editor_with(" line one\n line two\n xyz!");
6112 e.jump_cursor(0, 8);
6114 assert_eq!(e.cursor(), (0, 8));
6115 run_keys(&mut e, "dd");
6118 assert_eq!(
6119 e.cursor(),
6120 (0, 4),
6121 "dd must place cursor on first-non-blank"
6122 );
6123 run_keys(&mut e, "j");
6127 let (row, col) = e.cursor();
6128 assert_eq!(row, 1);
6129 assert_eq!(
6130 col, 4,
6131 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6132 );
6133 }
6134
6135 #[test]
6136 fn gu_lowercases_motion_range() {
6137 let mut e = editor_with("HELLO WORLD");
6138 run_keys(&mut e, "guw");
6139 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6140 assert_eq!(e.cursor(), (0, 0));
6141 }
6142
6143 #[test]
6144 fn g_u_uppercases_text_object() {
6145 let mut e = editor_with("hello world");
6146 run_keys(&mut e, "gUiw");
6148 assert_eq!(e.buffer().lines()[0], "HELLO world");
6149 assert_eq!(e.cursor(), (0, 0));
6150 }
6151
6152 #[test]
6153 fn g_tilde_toggles_case_of_range() {
6154 let mut e = editor_with("Hello World");
6155 run_keys(&mut e, "g~iw");
6156 assert_eq!(e.buffer().lines()[0], "hELLO World");
6157 }
6158
6159 #[test]
6160 fn g_uu_uppercases_current_line() {
6161 let mut e = editor_with("select 1\nselect 2");
6162 run_keys(&mut e, "gUU");
6163 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6164 assert_eq!(e.buffer().lines()[1], "select 2");
6165 }
6166
6167 #[test]
6168 fn gugu_lowercases_current_line() {
6169 let mut e = editor_with("FOO BAR\nBAZ");
6170 run_keys(&mut e, "gugu");
6171 assert_eq!(e.buffer().lines()[0], "foo bar");
6172 }
6173
6174 #[test]
6175 fn visual_u_uppercases_selection() {
6176 let mut e = editor_with("hello world");
6177 run_keys(&mut e, "veU");
6179 assert_eq!(e.buffer().lines()[0], "HELLO world");
6180 }
6181
6182 #[test]
6183 fn visual_line_u_lowercases_line() {
6184 let mut e = editor_with("HELLO WORLD\nOTHER");
6185 run_keys(&mut e, "Vu");
6186 assert_eq!(e.buffer().lines()[0], "hello world");
6187 assert_eq!(e.buffer().lines()[1], "OTHER");
6188 }
6189
6190 #[test]
6191 fn g_uu_with_count_uppercases_multiple_lines() {
6192 let mut e = editor_with("one\ntwo\nthree\nfour");
6193 run_keys(&mut e, "3gUU");
6195 assert_eq!(e.buffer().lines()[0], "ONE");
6196 assert_eq!(e.buffer().lines()[1], "TWO");
6197 assert_eq!(e.buffer().lines()[2], "THREE");
6198 assert_eq!(e.buffer().lines()[3], "four");
6199 }
6200
6201 #[test]
6202 fn double_gt_indents_current_line() {
6203 let mut e = editor_with("hello");
6204 run_keys(&mut e, ">>");
6205 assert_eq!(e.buffer().lines()[0], " hello");
6206 assert_eq!(e.cursor(), (0, 2));
6208 }
6209
6210 #[test]
6211 fn double_lt_outdents_current_line() {
6212 let mut e = editor_with(" hello");
6213 run_keys(&mut e, "<lt><lt>");
6214 assert_eq!(e.buffer().lines()[0], " hello");
6215 assert_eq!(e.cursor(), (0, 2));
6216 }
6217
6218 #[test]
6219 fn count_double_gt_indents_multiple_lines() {
6220 let mut e = editor_with("a\nb\nc\nd");
6221 run_keys(&mut e, "3>>");
6223 assert_eq!(e.buffer().lines()[0], " a");
6224 assert_eq!(e.buffer().lines()[1], " b");
6225 assert_eq!(e.buffer().lines()[2], " c");
6226 assert_eq!(e.buffer().lines()[3], "d");
6227 }
6228
6229 #[test]
6230 fn outdent_clips_ragged_leading_whitespace() {
6231 let mut e = editor_with(" x");
6234 run_keys(&mut e, "<lt><lt>");
6235 assert_eq!(e.buffer().lines()[0], "x");
6236 }
6237
6238 #[test]
6239 fn indent_motion_is_always_linewise() {
6240 let mut e = editor_with("foo bar");
6243 run_keys(&mut e, ">w");
6244 assert_eq!(e.buffer().lines()[0], " foo bar");
6245 }
6246
6247 #[test]
6248 fn indent_text_object_extends_over_paragraph() {
6249 let mut e = editor_with("a\nb\n\nc\nd");
6250 run_keys(&mut e, ">ap");
6252 assert_eq!(e.buffer().lines()[0], " a");
6253 assert_eq!(e.buffer().lines()[1], " b");
6254 assert_eq!(e.buffer().lines()[2], "");
6255 assert_eq!(e.buffer().lines()[3], "c");
6256 }
6257
6258 #[test]
6259 fn visual_line_indent_shifts_selected_rows() {
6260 let mut e = editor_with("x\ny\nz");
6261 run_keys(&mut e, "Vj>");
6263 assert_eq!(e.buffer().lines()[0], " x");
6264 assert_eq!(e.buffer().lines()[1], " y");
6265 assert_eq!(e.buffer().lines()[2], "z");
6266 }
6267
6268 #[test]
6269 fn outdent_empty_line_is_noop() {
6270 let mut e = editor_with("\nfoo");
6271 run_keys(&mut e, "<lt><lt>");
6272 assert_eq!(e.buffer().lines()[0], "");
6273 }
6274
6275 #[test]
6276 fn indent_skips_empty_lines() {
6277 let mut e = editor_with("");
6280 run_keys(&mut e, ">>");
6281 assert_eq!(e.buffer().lines()[0], "");
6282 }
6283
6284 #[test]
6285 fn insert_ctrl_t_indents_current_line() {
6286 let mut e = editor_with("x");
6287 run_keys(&mut e, "i<C-t>");
6289 assert_eq!(e.buffer().lines()[0], " x");
6290 assert_eq!(e.cursor(), (0, 2));
6293 }
6294
6295 #[test]
6296 fn insert_ctrl_d_outdents_current_line() {
6297 let mut e = editor_with(" x");
6298 run_keys(&mut e, "A<C-d>");
6300 assert_eq!(e.buffer().lines()[0], " x");
6301 }
6302
6303 #[test]
6304 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6305 let mut e = editor_with("first\nsecond");
6306 e.jump_cursor(1, 0);
6307 run_keys(&mut e, "h");
6308 assert_eq!(e.cursor(), (1, 0));
6310 }
6311
6312 #[test]
6313 fn l_at_last_char_does_not_wrap_to_next_line() {
6314 let mut e = editor_with("ab\ncd");
6315 e.jump_cursor(0, 1);
6317 run_keys(&mut e, "l");
6318 assert_eq!(e.cursor(), (0, 1));
6320 }
6321
6322 #[test]
6323 fn count_l_clamps_at_line_end() {
6324 let mut e = editor_with("abcde");
6325 run_keys(&mut e, "20l");
6328 assert_eq!(e.cursor(), (0, 4));
6329 }
6330
6331 #[test]
6332 fn count_h_clamps_at_col_zero() {
6333 let mut e = editor_with("abcde");
6334 e.jump_cursor(0, 3);
6335 run_keys(&mut e, "20h");
6336 assert_eq!(e.cursor(), (0, 0));
6337 }
6338
6339 #[test]
6340 fn dl_on_last_char_still_deletes_it() {
6341 let mut e = editor_with("ab");
6345 e.jump_cursor(0, 1);
6346 run_keys(&mut e, "dl");
6347 assert_eq!(e.buffer().lines()[0], "a");
6348 }
6349
6350 #[test]
6351 fn case_op_preserves_yank_register() {
6352 let mut e = editor_with("target");
6353 run_keys(&mut e, "yy");
6354 let yank_before = e.yank().to_string();
6355 run_keys(&mut e, "gUU");
6357 assert_eq!(e.buffer().lines()[0], "TARGET");
6358 assert_eq!(
6359 e.yank(),
6360 yank_before,
6361 "case ops must preserve the yank buffer"
6362 );
6363 }
6364
6365 #[test]
6366 fn dap_deletes_paragraph() {
6367 let mut e = editor_with("a\nb\n\nc\nd");
6368 run_keys(&mut e, "dap");
6369 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6370 }
6371
6372 #[test]
6373 fn dit_deletes_inner_tag_content() {
6374 let mut e = editor_with("<b>hello</b>");
6375 e.jump_cursor(0, 4);
6377 run_keys(&mut e, "dit");
6378 assert_eq!(e.buffer().lines()[0], "<b></b>");
6379 }
6380
6381 #[test]
6382 fn dat_deletes_around_tag() {
6383 let mut e = editor_with("hi <b>foo</b> bye");
6384 e.jump_cursor(0, 6);
6385 run_keys(&mut e, "dat");
6386 assert_eq!(e.buffer().lines()[0], "hi bye");
6387 }
6388
6389 #[test]
6390 fn dit_picks_innermost_tag() {
6391 let mut e = editor_with("<a><b>x</b></a>");
6392 e.jump_cursor(0, 6);
6394 run_keys(&mut e, "dit");
6395 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6397 }
6398
6399 #[test]
6400 fn dat_innermost_tag_pair() {
6401 let mut e = editor_with("<a><b>x</b></a>");
6402 e.jump_cursor(0, 6);
6403 run_keys(&mut e, "dat");
6404 assert_eq!(e.buffer().lines()[0], "<a></a>");
6405 }
6406
6407 #[test]
6408 fn dit_outside_any_tag_no_op() {
6409 let mut e = editor_with("plain text");
6410 e.jump_cursor(0, 3);
6411 run_keys(&mut e, "dit");
6412 assert_eq!(e.buffer().lines()[0], "plain text");
6414 }
6415
6416 #[test]
6417 fn cit_changes_inner_tag_content() {
6418 let mut e = editor_with("<b>hello</b>");
6419 e.jump_cursor(0, 4);
6420 run_keys(&mut e, "citNEW<Esc>");
6421 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6422 }
6423
6424 #[test]
6425 fn cat_changes_around_tag() {
6426 let mut e = editor_with("hi <b>foo</b> bye");
6427 e.jump_cursor(0, 6);
6428 run_keys(&mut e, "catBAR<Esc>");
6429 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6430 }
6431
6432 #[test]
6433 fn yit_yanks_inner_tag_content() {
6434 let mut e = editor_with("<b>hello</b>");
6435 e.jump_cursor(0, 4);
6436 run_keys(&mut e, "yit");
6437 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6438 }
6439
6440 #[test]
6441 fn yat_yanks_full_tag_pair() {
6442 let mut e = editor_with("hi <b>foo</b> bye");
6443 e.jump_cursor(0, 6);
6444 run_keys(&mut e, "yat");
6445 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6446 }
6447
6448 #[test]
6449 fn vit_visually_selects_inner_tag() {
6450 let mut e = editor_with("<b>hello</b>");
6451 e.jump_cursor(0, 4);
6452 run_keys(&mut e, "vit");
6453 assert_eq!(e.vim_mode(), VimMode::Visual);
6454 run_keys(&mut e, "y");
6455 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6456 }
6457
6458 #[test]
6459 fn vat_visually_selects_around_tag() {
6460 let mut e = editor_with("x<b>foo</b>y");
6461 e.jump_cursor(0, 5);
6462 run_keys(&mut e, "vat");
6463 assert_eq!(e.vim_mode(), VimMode::Visual);
6464 run_keys(&mut e, "y");
6465 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6466 }
6467
6468 #[test]
6471 #[allow(non_snake_case)]
6472 fn diW_deletes_inner_big_word() {
6473 let mut e = editor_with("foo.bar baz");
6474 e.jump_cursor(0, 2);
6475 run_keys(&mut e, "diW");
6476 assert_eq!(e.buffer().lines()[0], " baz");
6478 }
6479
6480 #[test]
6481 #[allow(non_snake_case)]
6482 fn daW_deletes_around_big_word() {
6483 let mut e = editor_with("foo.bar baz");
6484 e.jump_cursor(0, 2);
6485 run_keys(&mut e, "daW");
6486 assert_eq!(e.buffer().lines()[0], "baz");
6487 }
6488
6489 #[test]
6490 fn di_double_quote_deletes_inside() {
6491 let mut e = editor_with("a \"hello\" b");
6492 e.jump_cursor(0, 4);
6493 run_keys(&mut e, "di\"");
6494 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6495 }
6496
6497 #[test]
6498 fn da_double_quote_deletes_around() {
6499 let mut e = editor_with("a \"hello\" b");
6501 e.jump_cursor(0, 4);
6502 run_keys(&mut e, "da\"");
6503 assert_eq!(e.buffer().lines()[0], "a b");
6504 }
6505
6506 #[test]
6507 fn di_single_quote_deletes_inside() {
6508 let mut e = editor_with("x 'foo' y");
6509 e.jump_cursor(0, 4);
6510 run_keys(&mut e, "di'");
6511 assert_eq!(e.buffer().lines()[0], "x '' y");
6512 }
6513
6514 #[test]
6515 fn da_single_quote_deletes_around() {
6516 let mut e = editor_with("x 'foo' y");
6518 e.jump_cursor(0, 4);
6519 run_keys(&mut e, "da'");
6520 assert_eq!(e.buffer().lines()[0], "x y");
6521 }
6522
6523 #[test]
6524 fn di_backtick_deletes_inside() {
6525 let mut e = editor_with("p `q` r");
6526 e.jump_cursor(0, 3);
6527 run_keys(&mut e, "di`");
6528 assert_eq!(e.buffer().lines()[0], "p `` r");
6529 }
6530
6531 #[test]
6532 fn da_backtick_deletes_around() {
6533 let mut e = editor_with("p `q` r");
6535 e.jump_cursor(0, 3);
6536 run_keys(&mut e, "da`");
6537 assert_eq!(e.buffer().lines()[0], "p r");
6538 }
6539
6540 #[test]
6541 fn di_paren_deletes_inside() {
6542 let mut e = editor_with("f(arg)");
6543 e.jump_cursor(0, 3);
6544 run_keys(&mut e, "di(");
6545 assert_eq!(e.buffer().lines()[0], "f()");
6546 }
6547
6548 #[test]
6549 fn di_paren_alias_b_works() {
6550 let mut e = editor_with("f(arg)");
6551 e.jump_cursor(0, 3);
6552 run_keys(&mut e, "dib");
6553 assert_eq!(e.buffer().lines()[0], "f()");
6554 }
6555
6556 #[test]
6557 fn di_bracket_deletes_inside() {
6558 let mut e = editor_with("a[b,c]d");
6559 e.jump_cursor(0, 3);
6560 run_keys(&mut e, "di[");
6561 assert_eq!(e.buffer().lines()[0], "a[]d");
6562 }
6563
6564 #[test]
6565 fn da_bracket_deletes_around() {
6566 let mut e = editor_with("a[b,c]d");
6567 e.jump_cursor(0, 3);
6568 run_keys(&mut e, "da[");
6569 assert_eq!(e.buffer().lines()[0], "ad");
6570 }
6571
6572 #[test]
6573 fn di_brace_deletes_inside() {
6574 let mut e = editor_with("x{y}z");
6575 e.jump_cursor(0, 2);
6576 run_keys(&mut e, "di{");
6577 assert_eq!(e.buffer().lines()[0], "x{}z");
6578 }
6579
6580 #[test]
6581 fn da_brace_deletes_around() {
6582 let mut e = editor_with("x{y}z");
6583 e.jump_cursor(0, 2);
6584 run_keys(&mut e, "da{");
6585 assert_eq!(e.buffer().lines()[0], "xz");
6586 }
6587
6588 #[test]
6589 fn di_brace_alias_capital_b_works() {
6590 let mut e = editor_with("x{y}z");
6591 e.jump_cursor(0, 2);
6592 run_keys(&mut e, "diB");
6593 assert_eq!(e.buffer().lines()[0], "x{}z");
6594 }
6595
6596 #[test]
6597 fn di_angle_deletes_inside() {
6598 let mut e = editor_with("p<q>r");
6599 e.jump_cursor(0, 2);
6600 run_keys(&mut e, "di<lt>");
6602 assert_eq!(e.buffer().lines()[0], "p<>r");
6603 }
6604
6605 #[test]
6606 fn da_angle_deletes_around() {
6607 let mut e = editor_with("p<q>r");
6608 e.jump_cursor(0, 2);
6609 run_keys(&mut e, "da<lt>");
6610 assert_eq!(e.buffer().lines()[0], "pr");
6611 }
6612
6613 #[test]
6614 fn dip_deletes_inner_paragraph() {
6615 let mut e = editor_with("a\nb\nc\n\nd");
6616 e.jump_cursor(1, 0);
6617 run_keys(&mut e, "dip");
6618 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6621 }
6622
6623 #[test]
6626 fn sentence_motion_close_paren_jumps_forward() {
6627 let mut e = editor_with("Alpha. Beta. Gamma.");
6628 e.jump_cursor(0, 0);
6629 run_keys(&mut e, ")");
6630 assert_eq!(e.cursor(), (0, 7));
6632 run_keys(&mut e, ")");
6633 assert_eq!(e.cursor(), (0, 13));
6634 }
6635
6636 #[test]
6637 fn sentence_motion_open_paren_jumps_backward() {
6638 let mut e = editor_with("Alpha. Beta. Gamma.");
6639 e.jump_cursor(0, 13);
6640 run_keys(&mut e, "(");
6641 assert_eq!(e.cursor(), (0, 7));
6644 run_keys(&mut e, "(");
6645 assert_eq!(e.cursor(), (0, 0));
6646 }
6647
6648 #[test]
6649 fn sentence_motion_count() {
6650 let mut e = editor_with("A. B. C. D.");
6651 e.jump_cursor(0, 0);
6652 run_keys(&mut e, "3)");
6653 assert_eq!(e.cursor(), (0, 9));
6655 }
6656
6657 #[test]
6658 fn dis_deletes_inner_sentence() {
6659 let mut e = editor_with("First one. Second one. Third one.");
6660 e.jump_cursor(0, 13);
6661 run_keys(&mut e, "dis");
6662 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6664 }
6665
6666 #[test]
6667 fn das_deletes_around_sentence_with_trailing_space() {
6668 let mut e = editor_with("Alpha. Beta. Gamma.");
6669 e.jump_cursor(0, 8);
6670 run_keys(&mut e, "das");
6671 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6674 }
6675
6676 #[test]
6677 fn dis_handles_double_terminator() {
6678 let mut e = editor_with("Wow!? Next.");
6679 e.jump_cursor(0, 1);
6680 run_keys(&mut e, "dis");
6681 assert_eq!(e.buffer().lines()[0], " Next.");
6684 }
6685
6686 #[test]
6687 fn dis_first_sentence_from_cursor_at_zero() {
6688 let mut e = editor_with("Alpha. Beta.");
6689 e.jump_cursor(0, 0);
6690 run_keys(&mut e, "dis");
6691 assert_eq!(e.buffer().lines()[0], " Beta.");
6692 }
6693
6694 #[test]
6695 fn yis_yanks_inner_sentence() {
6696 let mut e = editor_with("Hello world. Bye.");
6697 e.jump_cursor(0, 5);
6698 run_keys(&mut e, "yis");
6699 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6700 }
6701
6702 #[test]
6703 fn vis_visually_selects_inner_sentence() {
6704 let mut e = editor_with("First. Second.");
6705 e.jump_cursor(0, 1);
6706 run_keys(&mut e, "vis");
6707 assert_eq!(e.vim_mode(), VimMode::Visual);
6708 run_keys(&mut e, "y");
6709 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6710 }
6711
6712 #[test]
6713 fn ciw_changes_inner_word() {
6714 let mut e = editor_with("hello world");
6715 e.jump_cursor(0, 1);
6716 run_keys(&mut e, "ciwHEY<Esc>");
6717 assert_eq!(e.buffer().lines()[0], "HEY world");
6718 }
6719
6720 #[test]
6721 fn yiw_yanks_inner_word() {
6722 let mut e = editor_with("hello world");
6723 e.jump_cursor(0, 1);
6724 run_keys(&mut e, "yiw");
6725 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6726 }
6727
6728 #[test]
6729 fn viw_selects_inner_word() {
6730 let mut e = editor_with("hello world");
6731 e.jump_cursor(0, 2);
6732 run_keys(&mut e, "viw");
6733 assert_eq!(e.vim_mode(), VimMode::Visual);
6734 run_keys(&mut e, "y");
6735 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6736 }
6737
6738 #[test]
6739 fn ci_paren_changes_inside() {
6740 let mut e = editor_with("f(old)");
6741 e.jump_cursor(0, 3);
6742 run_keys(&mut e, "ci(NEW<Esc>");
6743 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6744 }
6745
6746 #[test]
6747 fn yi_double_quote_yanks_inside() {
6748 let mut e = editor_with("say \"hi there\" then");
6749 e.jump_cursor(0, 6);
6750 run_keys(&mut e, "yi\"");
6751 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6752 }
6753
6754 #[test]
6755 fn vap_visual_selects_around_paragraph() {
6756 let mut e = editor_with("a\nb\n\nc");
6757 e.jump_cursor(0, 0);
6758 run_keys(&mut e, "vap");
6759 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6760 run_keys(&mut e, "y");
6761 let text = e.registers().read('"').unwrap().text.clone();
6763 assert!(text.starts_with("a\nb"));
6764 }
6765
6766 #[test]
6767 fn star_finds_next_occurrence() {
6768 let mut e = editor_with("foo bar foo baz");
6769 run_keys(&mut e, "*");
6770 assert_eq!(e.cursor().1, 8);
6771 }
6772
6773 #[test]
6774 fn star_skips_substring_match() {
6775 let mut e = editor_with("foo foobar baz");
6778 run_keys(&mut e, "*");
6779 assert_eq!(e.cursor().1, 0);
6780 }
6781
6782 #[test]
6783 fn g_star_matches_substring() {
6784 let mut e = editor_with("foo foobar baz");
6787 run_keys(&mut e, "g*");
6788 assert_eq!(e.cursor().1, 4);
6789 }
6790
6791 #[test]
6792 fn g_pound_matches_substring_backward() {
6793 let mut e = editor_with("foo foobar baz foo");
6796 run_keys(&mut e, "$b");
6797 assert_eq!(e.cursor().1, 15);
6798 run_keys(&mut e, "g#");
6799 assert_eq!(e.cursor().1, 4);
6800 }
6801
6802 #[test]
6803 fn n_repeats_last_search_forward() {
6804 let mut e = editor_with("foo bar foo baz foo");
6805 run_keys(&mut e, "/foo<CR>");
6808 assert_eq!(e.cursor().1, 8);
6809 run_keys(&mut e, "n");
6810 assert_eq!(e.cursor().1, 16);
6811 }
6812
6813 #[test]
6814 fn shift_n_reverses_search() {
6815 let mut e = editor_with("foo bar foo baz foo");
6816 run_keys(&mut e, "/foo<CR>");
6817 run_keys(&mut e, "n");
6818 assert_eq!(e.cursor().1, 16);
6819 run_keys(&mut e, "N");
6820 assert_eq!(e.cursor().1, 8);
6821 }
6822
6823 #[test]
6824 fn n_noop_without_pattern() {
6825 let mut e = editor_with("foo bar");
6826 run_keys(&mut e, "n");
6827 assert_eq!(e.cursor(), (0, 0));
6828 }
6829
6830 #[test]
6831 fn visual_line_preserves_cursor_column() {
6832 let mut e = editor_with("hello world\nanother one\nbye");
6835 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6837 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6838 assert_eq!(e.cursor(), (0, 5));
6839 run_keys(&mut e, "j");
6840 assert_eq!(e.cursor(), (1, 5));
6841 }
6842
6843 #[test]
6844 fn visual_line_yank_includes_trailing_newline() {
6845 let mut e = editor_with("aaa\nbbb\nccc");
6846 run_keys(&mut e, "Vjy");
6847 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6849 }
6850
6851 #[test]
6852 fn visual_line_yank_last_line_trailing_newline() {
6853 let mut e = editor_with("aaa\nbbb\nccc");
6854 run_keys(&mut e, "jj");
6856 run_keys(&mut e, "Vy");
6857 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6858 }
6859
6860 #[test]
6861 fn yy_on_last_line_has_trailing_newline() {
6862 let mut e = editor_with("aaa\nbbb\nccc");
6863 run_keys(&mut e, "jj");
6864 run_keys(&mut e, "yy");
6865 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6866 }
6867
6868 #[test]
6869 fn yy_in_middle_has_trailing_newline() {
6870 let mut e = editor_with("aaa\nbbb\nccc");
6871 run_keys(&mut e, "j");
6872 run_keys(&mut e, "yy");
6873 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6874 }
6875
6876 #[test]
6877 fn di_single_quote() {
6878 let mut e = editor_with("say 'hello world' now");
6879 e.jump_cursor(0, 7);
6880 run_keys(&mut e, "di'");
6881 assert_eq!(e.buffer().lines()[0], "say '' now");
6882 }
6883
6884 #[test]
6885 fn da_single_quote() {
6886 let mut e = editor_with("say 'hello' now");
6888 e.jump_cursor(0, 7);
6889 run_keys(&mut e, "da'");
6890 assert_eq!(e.buffer().lines()[0], "say now");
6891 }
6892
6893 #[test]
6894 fn di_backtick() {
6895 let mut e = editor_with("say `hi` now");
6896 e.jump_cursor(0, 5);
6897 run_keys(&mut e, "di`");
6898 assert_eq!(e.buffer().lines()[0], "say `` now");
6899 }
6900
6901 #[test]
6902 fn di_brace() {
6903 let mut e = editor_with("fn { a; b; c }");
6904 e.jump_cursor(0, 7);
6905 run_keys(&mut e, "di{");
6906 assert_eq!(e.buffer().lines()[0], "fn {}");
6907 }
6908
6909 #[test]
6910 fn di_bracket() {
6911 let mut e = editor_with("arr[1, 2, 3]");
6912 e.jump_cursor(0, 5);
6913 run_keys(&mut e, "di[");
6914 assert_eq!(e.buffer().lines()[0], "arr[]");
6915 }
6916
6917 #[test]
6918 fn dab_deletes_around_paren() {
6919 let mut e = editor_with("fn(a, b) + 1");
6920 e.jump_cursor(0, 4);
6921 run_keys(&mut e, "dab");
6922 assert_eq!(e.buffer().lines()[0], "fn + 1");
6923 }
6924
6925 #[test]
6926 fn da_big_b_deletes_around_brace() {
6927 let mut e = editor_with("x = {a: 1}");
6928 e.jump_cursor(0, 6);
6929 run_keys(&mut e, "daB");
6930 assert_eq!(e.buffer().lines()[0], "x = ");
6931 }
6932
6933 #[test]
6934 fn di_big_w_deletes_bigword() {
6935 let mut e = editor_with("foo-bar baz");
6936 e.jump_cursor(0, 2);
6937 run_keys(&mut e, "diW");
6938 assert_eq!(e.buffer().lines()[0], " baz");
6939 }
6940
6941 #[test]
6942 fn visual_select_inner_word() {
6943 let mut e = editor_with("hello world");
6944 e.jump_cursor(0, 2);
6945 run_keys(&mut e, "viw");
6946 assert_eq!(e.vim_mode(), VimMode::Visual);
6947 run_keys(&mut e, "y");
6948 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6949 }
6950
6951 #[test]
6952 fn visual_select_inner_quote() {
6953 let mut e = editor_with("foo \"bar\" baz");
6954 e.jump_cursor(0, 6);
6955 run_keys(&mut e, "vi\"");
6956 run_keys(&mut e, "y");
6957 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6958 }
6959
6960 #[test]
6961 fn visual_select_inner_paren() {
6962 let mut e = editor_with("fn(a, b)");
6963 e.jump_cursor(0, 4);
6964 run_keys(&mut e, "vi(");
6965 run_keys(&mut e, "y");
6966 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6967 }
6968
6969 #[test]
6970 fn visual_select_outer_brace() {
6971 let mut e = editor_with("{x}");
6972 e.jump_cursor(0, 1);
6973 run_keys(&mut e, "va{");
6974 run_keys(&mut e, "y");
6975 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6976 }
6977
6978 #[test]
6979 fn ci_paren_forward_scans_when_cursor_before_pair() {
6980 let mut e = editor_with("foo(bar)");
6983 e.jump_cursor(0, 0);
6984 run_keys(&mut e, "ci(NEW<Esc>");
6985 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
6986 }
6987
6988 #[test]
6989 fn ci_paren_forward_scans_across_lines() {
6990 let mut e = editor_with("first\nfoo(bar)\nlast");
6991 e.jump_cursor(0, 0);
6992 run_keys(&mut e, "ci(NEW<Esc>");
6993 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
6994 }
6995
6996 #[test]
6997 fn ci_brace_forward_scans_when_cursor_before_pair() {
6998 let mut e = editor_with("let x = {y};");
6999 e.jump_cursor(0, 0);
7000 run_keys(&mut e, "ci{NEW<Esc>");
7001 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7002 }
7003
7004 #[test]
7005 fn cit_forward_scans_when_cursor_before_tag() {
7006 let mut e = editor_with("text <b>hello</b> rest");
7009 e.jump_cursor(0, 0);
7010 run_keys(&mut e, "citNEW<Esc>");
7011 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7012 }
7013
7014 #[test]
7015 fn dat_forward_scans_when_cursor_before_tag() {
7016 let mut e = editor_with("text <b>hello</b> rest");
7018 e.jump_cursor(0, 0);
7019 run_keys(&mut e, "dat");
7020 assert_eq!(e.buffer().lines()[0], "text rest");
7021 }
7022
7023 #[test]
7024 fn ci_paren_still_works_when_cursor_inside() {
7025 let mut e = editor_with("fn(a, b)");
7028 e.jump_cursor(0, 4);
7029 run_keys(&mut e, "ci(NEW<Esc>");
7030 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7031 }
7032
7033 #[test]
7034 fn caw_changes_word_with_trailing_space() {
7035 let mut e = editor_with("hello world");
7036 run_keys(&mut e, "cawfoo<Esc>");
7037 assert_eq!(e.buffer().lines()[0], "fooworld");
7038 }
7039
7040 #[test]
7041 fn visual_char_yank_preserves_raw_text() {
7042 let mut e = editor_with("hello world");
7043 run_keys(&mut e, "vllly");
7044 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7045 }
7046
7047 #[test]
7048 fn single_line_visual_line_selects_full_line_on_yank() {
7049 let mut e = editor_with("hello world\nbye");
7050 run_keys(&mut e, "V");
7051 run_keys(&mut e, "y");
7054 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7055 }
7056
7057 #[test]
7058 fn visual_line_extends_both_directions() {
7059 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7060 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7062 assert_eq!(e.cursor(), (3, 0));
7063 run_keys(&mut e, "k");
7064 assert_eq!(e.cursor(), (2, 0));
7066 run_keys(&mut e, "k");
7067 assert_eq!(e.cursor(), (1, 0));
7068 }
7069
7070 #[test]
7071 fn visual_char_preserves_cursor_column() {
7072 let mut e = editor_with("hello world");
7073 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7075 assert_eq!(e.cursor(), (0, 5));
7076 run_keys(&mut e, "ll");
7077 assert_eq!(e.cursor(), (0, 7));
7078 }
7079
7080 #[test]
7081 fn visual_char_highlight_bounds_order() {
7082 let mut e = editor_with("abcdef");
7083 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7085 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7088 }
7089
7090 #[test]
7091 fn visual_line_highlight_bounds() {
7092 let mut e = editor_with("a\nb\nc");
7093 run_keys(&mut e, "V");
7094 assert_eq!(e.line_highlight(), Some((0, 0)));
7095 run_keys(&mut e, "j");
7096 assert_eq!(e.line_highlight(), Some((0, 1)));
7097 run_keys(&mut e, "j");
7098 assert_eq!(e.line_highlight(), Some((0, 2)));
7099 }
7100
7101 #[test]
7104 fn h_moves_left() {
7105 let mut e = editor_with("hello");
7106 e.jump_cursor(0, 3);
7107 run_keys(&mut e, "h");
7108 assert_eq!(e.cursor(), (0, 2));
7109 }
7110
7111 #[test]
7112 fn l_moves_right() {
7113 let mut e = editor_with("hello");
7114 run_keys(&mut e, "l");
7115 assert_eq!(e.cursor(), (0, 1));
7116 }
7117
7118 #[test]
7119 fn k_moves_up() {
7120 let mut e = editor_with("a\nb\nc");
7121 e.jump_cursor(2, 0);
7122 run_keys(&mut e, "k");
7123 assert_eq!(e.cursor(), (1, 0));
7124 }
7125
7126 #[test]
7127 fn zero_moves_to_line_start() {
7128 let mut e = editor_with(" hello");
7129 run_keys(&mut e, "$");
7130 run_keys(&mut e, "0");
7131 assert_eq!(e.cursor().1, 0);
7132 }
7133
7134 #[test]
7135 fn caret_moves_to_first_non_blank() {
7136 let mut e = editor_with(" hello");
7137 run_keys(&mut e, "0");
7138 run_keys(&mut e, "^");
7139 assert_eq!(e.cursor().1, 4);
7140 }
7141
7142 #[test]
7143 fn dollar_moves_to_last_char() {
7144 let mut e = editor_with("hello");
7145 run_keys(&mut e, "$");
7146 assert_eq!(e.cursor().1, 4);
7147 }
7148
7149 #[test]
7150 fn dollar_on_empty_line_stays_at_col_zero() {
7151 let mut e = editor_with("");
7152 run_keys(&mut e, "$");
7153 assert_eq!(e.cursor().1, 0);
7154 }
7155
7156 #[test]
7157 fn w_jumps_to_next_word() {
7158 let mut e = editor_with("foo bar baz");
7159 run_keys(&mut e, "w");
7160 assert_eq!(e.cursor().1, 4);
7161 }
7162
7163 #[test]
7164 fn b_jumps_back_a_word() {
7165 let mut e = editor_with("foo bar");
7166 e.jump_cursor(0, 6);
7167 run_keys(&mut e, "b");
7168 assert_eq!(e.cursor().1, 4);
7169 }
7170
7171 #[test]
7172 fn e_jumps_to_word_end() {
7173 let mut e = editor_with("foo bar");
7174 run_keys(&mut e, "e");
7175 assert_eq!(e.cursor().1, 2);
7176 }
7177
7178 #[test]
7181 fn d_dollar_deletes_to_eol() {
7182 let mut e = editor_with("hello world");
7183 e.jump_cursor(0, 5);
7184 run_keys(&mut e, "d$");
7185 assert_eq!(e.buffer().lines()[0], "hello");
7186 }
7187
7188 #[test]
7189 fn d_zero_deletes_to_line_start() {
7190 let mut e = editor_with("hello world");
7191 e.jump_cursor(0, 6);
7192 run_keys(&mut e, "d0");
7193 assert_eq!(e.buffer().lines()[0], "world");
7194 }
7195
7196 #[test]
7197 fn d_caret_deletes_to_first_non_blank() {
7198 let mut e = editor_with(" hello");
7199 e.jump_cursor(0, 6);
7200 run_keys(&mut e, "d^");
7201 assert_eq!(e.buffer().lines()[0], " llo");
7202 }
7203
7204 #[test]
7205 fn d_capital_g_deletes_to_end_of_file() {
7206 let mut e = editor_with("a\nb\nc\nd");
7207 e.jump_cursor(1, 0);
7208 run_keys(&mut e, "dG");
7209 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7210 }
7211
7212 #[test]
7213 fn d_gg_deletes_to_start_of_file() {
7214 let mut e = editor_with("a\nb\nc\nd");
7215 e.jump_cursor(2, 0);
7216 run_keys(&mut e, "dgg");
7217 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7218 }
7219
7220 #[test]
7221 fn cw_is_ce_quirk() {
7222 let mut e = editor_with("foo bar");
7225 run_keys(&mut e, "cwxyz<Esc>");
7226 assert_eq!(e.buffer().lines()[0], "xyz bar");
7227 }
7228
7229 #[test]
7232 fn big_d_deletes_to_eol() {
7233 let mut e = editor_with("hello world");
7234 e.jump_cursor(0, 5);
7235 run_keys(&mut e, "D");
7236 assert_eq!(e.buffer().lines()[0], "hello");
7237 }
7238
7239 #[test]
7240 fn big_c_deletes_to_eol_and_inserts() {
7241 let mut e = editor_with("hello world");
7242 e.jump_cursor(0, 5);
7243 run_keys(&mut e, "C!<Esc>");
7244 assert_eq!(e.buffer().lines()[0], "hello!");
7245 }
7246
7247 #[test]
7248 fn j_joins_next_line_with_space() {
7249 let mut e = editor_with("hello\nworld");
7250 run_keys(&mut e, "J");
7251 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7252 }
7253
7254 #[test]
7255 fn j_strips_leading_whitespace_on_join() {
7256 let mut e = editor_with("hello\n world");
7257 run_keys(&mut e, "J");
7258 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7259 }
7260
7261 #[test]
7262 fn big_x_deletes_char_before_cursor() {
7263 let mut e = editor_with("hello");
7264 e.jump_cursor(0, 3);
7265 run_keys(&mut e, "X");
7266 assert_eq!(e.buffer().lines()[0], "helo");
7267 }
7268
7269 #[test]
7270 fn s_substitutes_char_and_enters_insert() {
7271 let mut e = editor_with("hello");
7272 run_keys(&mut e, "sX<Esc>");
7273 assert_eq!(e.buffer().lines()[0], "Xello");
7274 }
7275
7276 #[test]
7277 fn count_x_deletes_many() {
7278 let mut e = editor_with("abcdef");
7279 run_keys(&mut e, "3x");
7280 assert_eq!(e.buffer().lines()[0], "def");
7281 }
7282
7283 #[test]
7286 fn p_pastes_charwise_after_cursor() {
7287 let mut e = editor_with("hello");
7288 run_keys(&mut e, "yw");
7289 run_keys(&mut e, "$p");
7290 assert_eq!(e.buffer().lines()[0], "hellohello");
7291 }
7292
7293 #[test]
7294 fn capital_p_pastes_charwise_before_cursor() {
7295 let mut e = editor_with("hello");
7296 run_keys(&mut e, "v");
7298 run_keys(&mut e, "l");
7299 run_keys(&mut e, "y");
7300 run_keys(&mut e, "$P");
7301 assert_eq!(e.buffer().lines()[0], "hellheo");
7304 }
7305
7306 #[test]
7307 fn p_pastes_linewise_below() {
7308 let mut e = editor_with("one\ntwo\nthree");
7309 run_keys(&mut e, "yy");
7310 run_keys(&mut e, "p");
7311 assert_eq!(
7312 e.buffer().lines(),
7313 &[
7314 "one".to_string(),
7315 "one".to_string(),
7316 "two".to_string(),
7317 "three".to_string()
7318 ]
7319 );
7320 }
7321
7322 #[test]
7323 fn capital_p_pastes_linewise_above() {
7324 let mut e = editor_with("one\ntwo");
7325 e.jump_cursor(1, 0);
7326 run_keys(&mut e, "yy");
7327 run_keys(&mut e, "P");
7328 assert_eq!(
7329 e.buffer().lines(),
7330 &["one".to_string(), "two".to_string(), "two".to_string()]
7331 );
7332 }
7333
7334 #[test]
7337 fn hash_finds_previous_occurrence() {
7338 let mut e = editor_with("foo bar foo baz foo");
7339 e.jump_cursor(0, 16);
7341 run_keys(&mut e, "#");
7342 assert_eq!(e.cursor().1, 8);
7343 }
7344
7345 #[test]
7348 fn visual_line_delete_removes_full_lines() {
7349 let mut e = editor_with("a\nb\nc\nd");
7350 run_keys(&mut e, "Vjd");
7351 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7352 }
7353
7354 #[test]
7355 fn visual_line_change_leaves_blank_line() {
7356 let mut e = editor_with("a\nb\nc");
7357 run_keys(&mut e, "Vjc");
7358 assert_eq!(e.vim_mode(), VimMode::Insert);
7359 run_keys(&mut e, "X<Esc>");
7360 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7364 }
7365
7366 #[test]
7367 fn cc_leaves_blank_line() {
7368 let mut e = editor_with("a\nb\nc");
7369 e.jump_cursor(1, 0);
7370 run_keys(&mut e, "ccX<Esc>");
7371 assert_eq!(
7372 e.buffer().lines(),
7373 &["a".to_string(), "X".to_string(), "c".to_string()]
7374 );
7375 }
7376
7377 #[test]
7382 fn big_w_skips_hyphens() {
7383 let mut e = editor_with("foo-bar baz");
7385 run_keys(&mut e, "W");
7386 assert_eq!(e.cursor().1, 8);
7387 }
7388
7389 #[test]
7390 fn big_w_crosses_lines() {
7391 let mut e = editor_with("foo-bar\nbaz-qux");
7392 run_keys(&mut e, "W");
7393 assert_eq!(e.cursor(), (1, 0));
7394 }
7395
7396 #[test]
7397 fn big_b_skips_hyphens() {
7398 let mut e = editor_with("foo-bar baz");
7399 e.jump_cursor(0, 9);
7400 run_keys(&mut e, "B");
7401 assert_eq!(e.cursor().1, 8);
7402 run_keys(&mut e, "B");
7403 assert_eq!(e.cursor().1, 0);
7404 }
7405
7406 #[test]
7407 fn big_e_jumps_to_big_word_end() {
7408 let mut e = editor_with("foo-bar baz");
7409 run_keys(&mut e, "E");
7410 assert_eq!(e.cursor().1, 6);
7411 run_keys(&mut e, "E");
7412 assert_eq!(e.cursor().1, 10);
7413 }
7414
7415 #[test]
7416 fn dw_with_big_word_variant() {
7417 let mut e = editor_with("foo-bar baz");
7419 run_keys(&mut e, "dW");
7420 assert_eq!(e.buffer().lines()[0], "baz");
7421 }
7422
7423 #[test]
7426 fn insert_ctrl_w_deletes_word_back() {
7427 let mut e = editor_with("");
7428 run_keys(&mut e, "i");
7429 for c in "hello world".chars() {
7430 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7431 }
7432 run_keys(&mut e, "<C-w>");
7433 assert_eq!(e.buffer().lines()[0], "hello ");
7434 }
7435
7436 #[test]
7437 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7438 let mut e = editor_with("hello\nworld");
7442 e.jump_cursor(1, 0);
7443 run_keys(&mut e, "i");
7444 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7445 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7448 assert_eq!(e.cursor(), (0, 0));
7449 }
7450
7451 #[test]
7452 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7453 let mut e = editor_with("foo bar\nbaz");
7454 e.jump_cursor(1, 0);
7455 run_keys(&mut e, "i");
7456 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7457 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7459 assert_eq!(e.cursor(), (0, 4));
7460 }
7461
7462 #[test]
7463 fn insert_ctrl_u_deletes_to_line_start() {
7464 let mut e = editor_with("");
7465 run_keys(&mut e, "i");
7466 for c in "hello world".chars() {
7467 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7468 }
7469 run_keys(&mut e, "<C-u>");
7470 assert_eq!(e.buffer().lines()[0], "");
7471 }
7472
7473 #[test]
7474 fn insert_ctrl_o_runs_one_normal_command() {
7475 let mut e = editor_with("hello world");
7476 run_keys(&mut e, "A");
7478 assert_eq!(e.vim_mode(), VimMode::Insert);
7479 e.jump_cursor(0, 0);
7481 run_keys(&mut e, "<C-o>");
7482 assert_eq!(e.vim_mode(), VimMode::Normal);
7483 run_keys(&mut e, "dw");
7484 assert_eq!(e.vim_mode(), VimMode::Insert);
7486 assert_eq!(e.buffer().lines()[0], "world");
7487 }
7488
7489 #[test]
7492 fn j_through_empty_line_preserves_column() {
7493 let mut e = editor_with("hello world\n\nanother line");
7494 run_keys(&mut e, "llllll");
7496 assert_eq!(e.cursor(), (0, 6));
7497 run_keys(&mut e, "j");
7500 assert_eq!(e.cursor(), (1, 0));
7501 run_keys(&mut e, "j");
7503 assert_eq!(e.cursor(), (2, 6));
7504 }
7505
7506 #[test]
7507 fn j_through_shorter_line_preserves_column() {
7508 let mut e = editor_with("hello world\nhi\nanother line");
7509 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7512 run_keys(&mut e, "j");
7513 assert_eq!(e.cursor(), (2, 7));
7514 }
7515
7516 #[test]
7517 fn esc_from_insert_sticky_matches_visible_cursor() {
7518 let mut e = editor_with(" this is a line\n another one of a similar size");
7522 e.jump_cursor(0, 12);
7523 run_keys(&mut e, "I");
7524 assert_eq!(e.cursor(), (0, 4));
7525 run_keys(&mut e, "X<Esc>");
7526 assert_eq!(e.cursor(), (0, 4));
7527 run_keys(&mut e, "j");
7528 assert_eq!(e.cursor(), (1, 4));
7529 }
7530
7531 #[test]
7532 fn esc_from_insert_sticky_tracks_inserted_chars() {
7533 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7534 run_keys(&mut e, "i");
7535 run_keys(&mut e, "abc<Esc>");
7536 assert_eq!(e.cursor(), (0, 2));
7537 run_keys(&mut e, "j");
7538 assert_eq!(e.cursor(), (1, 2));
7539 }
7540
7541 #[test]
7542 fn esc_from_insert_sticky_tracks_arrow_nav() {
7543 let mut e = editor_with("xxxxxx\nyyyyyy");
7544 run_keys(&mut e, "i");
7545 run_keys(&mut e, "abc");
7546 for _ in 0..2 {
7547 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7548 }
7549 run_keys(&mut e, "<Esc>");
7550 assert_eq!(e.cursor(), (0, 0));
7551 run_keys(&mut e, "j");
7552 assert_eq!(e.cursor(), (1, 0));
7553 }
7554
7555 #[test]
7556 fn esc_from_insert_at_col_14_followed_by_j() {
7557 let line = "x".repeat(30);
7560 let buf = format!("{line}\n{line}");
7561 let mut e = editor_with(&buf);
7562 e.jump_cursor(0, 14);
7563 run_keys(&mut e, "i");
7564 for c in "test ".chars() {
7565 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7566 }
7567 run_keys(&mut e, "<Esc>");
7568 assert_eq!(e.cursor(), (0, 18));
7569 run_keys(&mut e, "j");
7570 assert_eq!(e.cursor(), (1, 18));
7571 }
7572
7573 #[test]
7574 fn linewise_paste_resets_sticky_column() {
7575 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7579 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7581 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7585 run_keys(&mut e, "j");
7587 assert_eq!(e.cursor(), (3, 2));
7588 }
7589
7590 #[test]
7591 fn horizontal_motion_resyncs_sticky_column() {
7592 let mut e = editor_with("hello world\n\nanother line");
7596 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7599 assert_eq!(e.cursor(), (2, 3));
7600 }
7601
7602 #[test]
7605 fn ctrl_v_enters_visual_block() {
7606 let mut e = editor_with("aaa\nbbb\nccc");
7607 run_keys(&mut e, "<C-v>");
7608 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7609 }
7610
7611 #[test]
7612 fn visual_block_esc_returns_to_normal() {
7613 let mut e = editor_with("aaa\nbbb\nccc");
7614 run_keys(&mut e, "<C-v>");
7615 run_keys(&mut e, "<Esc>");
7616 assert_eq!(e.vim_mode(), VimMode::Normal);
7617 }
7618
7619 #[test]
7620 fn visual_exit_sets_lt_gt_marks() {
7621 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7624 run_keys(&mut e, "V");
7626 run_keys(&mut e, "j");
7627 run_keys(&mut e, "<Esc>");
7628 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7629 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7630 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7631 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7632 }
7633
7634 #[test]
7635 fn visual_exit_marks_use_lower_higher_order() {
7636 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7640 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7642 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7644 let lt = e.mark('<').unwrap();
7645 let gt = e.mark('>').unwrap();
7646 assert_eq!(lt.0, 2);
7647 assert_eq!(gt.0, 3);
7648 }
7649
7650 #[test]
7651 fn visualline_exit_marks_snap_to_line_edges() {
7652 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7654 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7656 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7658 let lt = e.mark('<').unwrap();
7659 let gt = e.mark('>').unwrap();
7660 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7661 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7663 }
7664
7665 #[test]
7666 fn visualblock_exit_marks_use_block_corners() {
7667 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7671 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7673 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7676 let lt = e.mark('<').unwrap();
7677 let gt = e.mark('>').unwrap();
7678 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7680 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7681 }
7682
7683 #[test]
7684 fn visual_block_delete_removes_column_range() {
7685 let mut e = editor_with("hello\nworld\nhappy");
7686 run_keys(&mut e, "l");
7688 run_keys(&mut e, "<C-v>");
7689 run_keys(&mut e, "jj");
7690 run_keys(&mut e, "ll");
7691 run_keys(&mut e, "d");
7692 assert_eq!(
7694 e.buffer().lines(),
7695 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7696 );
7697 }
7698
7699 #[test]
7700 fn visual_block_yank_joins_with_newlines() {
7701 let mut e = editor_with("hello\nworld\nhappy");
7702 run_keys(&mut e, "<C-v>");
7703 run_keys(&mut e, "jj");
7704 run_keys(&mut e, "ll");
7705 run_keys(&mut e, "y");
7706 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7707 }
7708
7709 #[test]
7710 fn visual_block_replace_fills_block() {
7711 let mut e = editor_with("hello\nworld\nhappy");
7712 run_keys(&mut e, "<C-v>");
7713 run_keys(&mut e, "jj");
7714 run_keys(&mut e, "ll");
7715 run_keys(&mut e, "rx");
7716 assert_eq!(
7717 e.buffer().lines(),
7718 &[
7719 "xxxlo".to_string(),
7720 "xxxld".to_string(),
7721 "xxxpy".to_string()
7722 ]
7723 );
7724 }
7725
7726 #[test]
7727 fn visual_block_insert_repeats_across_rows() {
7728 let mut e = editor_with("hello\nworld\nhappy");
7729 run_keys(&mut e, "<C-v>");
7730 run_keys(&mut e, "jj");
7731 run_keys(&mut e, "I");
7732 run_keys(&mut e, "# <Esc>");
7733 assert_eq!(
7734 e.buffer().lines(),
7735 &[
7736 "# hello".to_string(),
7737 "# world".to_string(),
7738 "# happy".to_string()
7739 ]
7740 );
7741 }
7742
7743 #[test]
7744 fn block_highlight_returns_none_outside_block_mode() {
7745 let mut e = editor_with("abc");
7746 assert!(e.block_highlight().is_none());
7747 run_keys(&mut e, "v");
7748 assert!(e.block_highlight().is_none());
7749 run_keys(&mut e, "<Esc>V");
7750 assert!(e.block_highlight().is_none());
7751 }
7752
7753 #[test]
7754 fn block_highlight_bounds_track_anchor_and_cursor() {
7755 let mut e = editor_with("aaaa\nbbbb\ncccc");
7756 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7758 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7761 }
7762
7763 #[test]
7764 fn visual_block_delete_handles_short_lines() {
7765 let mut e = editor_with("hello\nhi\nworld");
7767 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7769 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7771 assert_eq!(
7776 e.buffer().lines(),
7777 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7778 );
7779 }
7780
7781 #[test]
7782 fn visual_block_yank_pads_short_lines_with_empties() {
7783 let mut e = editor_with("hello\nhi\nworld");
7784 run_keys(&mut e, "l");
7785 run_keys(&mut e, "<C-v>");
7786 run_keys(&mut e, "jjll");
7787 run_keys(&mut e, "y");
7788 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7790 }
7791
7792 #[test]
7793 fn visual_block_replace_skips_past_eol() {
7794 let mut e = editor_with("ab\ncd\nef");
7797 run_keys(&mut e, "l");
7799 run_keys(&mut e, "<C-v>");
7800 run_keys(&mut e, "jjllllll");
7801 run_keys(&mut e, "rX");
7802 assert_eq!(
7805 e.buffer().lines(),
7806 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7807 );
7808 }
7809
7810 #[test]
7811 fn visual_block_with_empty_line_in_middle() {
7812 let mut e = editor_with("abcd\n\nefgh");
7813 run_keys(&mut e, "<C-v>");
7814 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7816 assert_eq!(
7819 e.buffer().lines(),
7820 &["d".to_string(), "".to_string(), "h".to_string()]
7821 );
7822 }
7823
7824 #[test]
7825 fn block_insert_pads_empty_lines_to_block_column() {
7826 let mut e = editor_with("this is a line\n\nthis is a line");
7829 e.jump_cursor(0, 3);
7830 run_keys(&mut e, "<C-v>");
7831 run_keys(&mut e, "jj");
7832 run_keys(&mut e, "I");
7833 run_keys(&mut e, "XX<Esc>");
7834 assert_eq!(
7835 e.buffer().lines(),
7836 &[
7837 "thiXXs is a line".to_string(),
7838 " XX".to_string(),
7839 "thiXXs is a line".to_string()
7840 ]
7841 );
7842 }
7843
7844 #[test]
7845 fn block_insert_pads_short_lines_to_block_column() {
7846 let mut e = editor_with("aaaaa\nbb\naaaaa");
7847 e.jump_cursor(0, 3);
7848 run_keys(&mut e, "<C-v>");
7849 run_keys(&mut e, "jj");
7850 run_keys(&mut e, "I");
7851 run_keys(&mut e, "Y<Esc>");
7852 assert_eq!(
7854 e.buffer().lines(),
7855 &[
7856 "aaaYaa".to_string(),
7857 "bb Y".to_string(),
7858 "aaaYaa".to_string()
7859 ]
7860 );
7861 }
7862
7863 #[test]
7864 fn visual_block_append_repeats_across_rows() {
7865 let mut e = editor_with("foo\nbar\nbaz");
7866 run_keys(&mut e, "<C-v>");
7867 run_keys(&mut e, "jj");
7868 run_keys(&mut e, "A");
7871 run_keys(&mut e, "!<Esc>");
7872 assert_eq!(
7873 e.buffer().lines(),
7874 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7875 );
7876 }
7877
7878 #[test]
7881 fn slash_opens_forward_search_prompt() {
7882 let mut e = editor_with("hello world");
7883 run_keys(&mut e, "/");
7884 let p = e.search_prompt().expect("prompt should be active");
7885 assert!(p.text.is_empty());
7886 assert!(p.forward);
7887 }
7888
7889 #[test]
7890 fn question_opens_backward_search_prompt() {
7891 let mut e = editor_with("hello world");
7892 run_keys(&mut e, "?");
7893 let p = e.search_prompt().expect("prompt should be active");
7894 assert!(!p.forward);
7895 }
7896
7897 #[test]
7898 fn search_prompt_typing_updates_pattern_live() {
7899 let mut e = editor_with("foo bar\nbaz");
7900 run_keys(&mut e, "/bar");
7901 assert_eq!(e.search_prompt().unwrap().text, "bar");
7902 assert!(e.search_state().pattern.is_some());
7904 }
7905
7906 #[test]
7907 fn search_prompt_backspace_and_enter() {
7908 let mut e = editor_with("hello world\nagain");
7909 run_keys(&mut e, "/worlx");
7910 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7911 assert_eq!(e.search_prompt().unwrap().text, "worl");
7912 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7913 assert!(e.search_prompt().is_none());
7915 assert_eq!(e.last_search(), Some("worl"));
7916 assert_eq!(e.cursor(), (0, 6));
7917 }
7918
7919 #[test]
7920 fn empty_search_prompt_enter_repeats_last_search() {
7921 let mut e = editor_with("foo bar foo baz foo");
7922 run_keys(&mut e, "/foo");
7923 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7924 assert_eq!(e.cursor().1, 8);
7925 run_keys(&mut e, "/");
7927 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7928 assert_eq!(e.cursor().1, 16);
7929 assert_eq!(e.last_search(), Some("foo"));
7930 }
7931
7932 #[test]
7933 fn search_history_records_committed_patterns() {
7934 let mut e = editor_with("alpha beta gamma");
7935 run_keys(&mut e, "/alpha<CR>");
7936 run_keys(&mut e, "/beta<CR>");
7937 let history = e.vim.search_history.clone();
7939 assert_eq!(history, vec!["alpha", "beta"]);
7940 }
7941
7942 #[test]
7943 fn search_history_dedupes_consecutive_repeats() {
7944 let mut e = editor_with("foo bar foo");
7945 run_keys(&mut e, "/foo<CR>");
7946 run_keys(&mut e, "/foo<CR>");
7947 run_keys(&mut e, "/bar<CR>");
7948 run_keys(&mut e, "/bar<CR>");
7949 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
7951 }
7952
7953 #[test]
7954 fn ctrl_p_walks_history_backward() {
7955 let mut e = editor_with("alpha beta gamma");
7956 run_keys(&mut e, "/alpha<CR>");
7957 run_keys(&mut e, "/beta<CR>");
7958 run_keys(&mut e, "/");
7960 assert_eq!(e.search_prompt().unwrap().text, "");
7961 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7962 assert_eq!(e.search_prompt().unwrap().text, "beta");
7963 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7964 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7965 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7967 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7968 }
7969
7970 #[test]
7971 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7972 let mut e = editor_with("a b c");
7973 run_keys(&mut e, "/a<CR>");
7974 run_keys(&mut e, "/b<CR>");
7975 run_keys(&mut e, "/c<CR>");
7976 run_keys(&mut e, "/");
7977 for _ in 0..3 {
7979 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7980 }
7981 assert_eq!(e.search_prompt().unwrap().text, "a");
7982 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7983 assert_eq!(e.search_prompt().unwrap().text, "b");
7984 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7985 assert_eq!(e.search_prompt().unwrap().text, "c");
7986 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7988 assert_eq!(e.search_prompt().unwrap().text, "c");
7989 }
7990
7991 #[test]
7992 fn typing_after_history_walk_resets_cursor() {
7993 let mut e = editor_with("foo");
7994 run_keys(&mut e, "/foo<CR>");
7995 run_keys(&mut e, "/");
7996 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7997 assert_eq!(e.search_prompt().unwrap().text, "foo");
7998 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8001 assert_eq!(e.search_prompt().unwrap().text, "foox");
8002 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8003 assert_eq!(e.search_prompt().unwrap().text, "foo");
8004 }
8005
8006 #[test]
8007 fn empty_backward_search_prompt_enter_repeats_last_search() {
8008 let mut e = editor_with("foo bar foo baz foo");
8009 run_keys(&mut e, "/foo");
8011 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8012 assert_eq!(e.cursor().1, 8);
8013 run_keys(&mut e, "?");
8014 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8015 assert_eq!(e.cursor().1, 0);
8016 assert_eq!(e.last_search(), Some("foo"));
8017 }
8018
8019 #[test]
8020 fn search_prompt_esc_cancels_but_keeps_last_search() {
8021 let mut e = editor_with("foo bar\nbaz");
8022 run_keys(&mut e, "/bar");
8023 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8024 assert!(e.search_prompt().is_none());
8025 assert_eq!(e.last_search(), Some("bar"));
8026 }
8027
8028 #[test]
8029 fn search_then_n_and_shift_n_navigate() {
8030 let mut e = editor_with("foo bar foo baz foo");
8031 run_keys(&mut e, "/foo");
8032 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8033 assert_eq!(e.cursor().1, 8);
8035 run_keys(&mut e, "n");
8036 assert_eq!(e.cursor().1, 16);
8037 run_keys(&mut e, "N");
8038 assert_eq!(e.cursor().1, 8);
8039 }
8040
8041 #[test]
8042 fn question_mark_searches_backward_on_enter() {
8043 let mut e = editor_with("foo bar foo baz");
8044 e.jump_cursor(0, 10);
8045 run_keys(&mut e, "?foo");
8046 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8047 assert_eq!(e.cursor(), (0, 8));
8049 }
8050
8051 #[test]
8054 fn big_y_yanks_to_end_of_line() {
8055 let mut e = editor_with("hello world");
8056 e.jump_cursor(0, 6);
8057 run_keys(&mut e, "Y");
8058 assert_eq!(e.last_yank.as_deref(), Some("world"));
8059 }
8060
8061 #[test]
8062 fn big_y_from_line_start_yanks_full_line() {
8063 let mut e = editor_with("hello world");
8064 run_keys(&mut e, "Y");
8065 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8066 }
8067
8068 #[test]
8069 fn gj_joins_without_inserting_space() {
8070 let mut e = editor_with("hello\n world");
8071 run_keys(&mut e, "gJ");
8072 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8074 }
8075
8076 #[test]
8077 fn gj_noop_on_last_line() {
8078 let mut e = editor_with("only");
8079 run_keys(&mut e, "gJ");
8080 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8081 }
8082
8083 #[test]
8084 fn ge_jumps_to_previous_word_end() {
8085 let mut e = editor_with("foo bar baz");
8086 e.jump_cursor(0, 5);
8087 run_keys(&mut e, "ge");
8088 assert_eq!(e.cursor(), (0, 2));
8089 }
8090
8091 #[test]
8092 fn ge_respects_word_class() {
8093 let mut e = editor_with("foo-bar baz");
8096 e.jump_cursor(0, 5);
8097 run_keys(&mut e, "ge");
8098 assert_eq!(e.cursor(), (0, 3));
8099 }
8100
8101 #[test]
8102 fn big_ge_treats_hyphens_as_part_of_word() {
8103 let mut e = editor_with("foo-bar baz");
8106 e.jump_cursor(0, 10);
8107 run_keys(&mut e, "gE");
8108 assert_eq!(e.cursor(), (0, 6));
8109 }
8110
8111 #[test]
8112 fn ge_crosses_line_boundary() {
8113 let mut e = editor_with("foo\nbar");
8114 e.jump_cursor(1, 0);
8115 run_keys(&mut e, "ge");
8116 assert_eq!(e.cursor(), (0, 2));
8117 }
8118
8119 #[test]
8120 fn dge_deletes_to_end_of_previous_word() {
8121 let mut e = editor_with("foo bar baz");
8122 e.jump_cursor(0, 8);
8123 run_keys(&mut e, "dge");
8126 assert_eq!(e.buffer().lines()[0], "foo baaz");
8127 }
8128
8129 #[test]
8130 fn ctrl_scroll_keys_do_not_panic() {
8131 let mut e = editor_with(
8134 (0..50)
8135 .map(|i| format!("line{i}"))
8136 .collect::<Vec<_>>()
8137 .join("\n")
8138 .as_str(),
8139 );
8140 run_keys(&mut e, "<C-f>");
8141 run_keys(&mut e, "<C-b>");
8142 assert!(!e.buffer().lines().is_empty());
8144 }
8145
8146 #[test]
8153 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8154 let mut e = Editor::new(
8155 hjkl_buffer::Buffer::new(),
8156 crate::types::DefaultHost::new(),
8157 crate::types::Options::default(),
8158 );
8159 e.set_content("row0\nrow1\nrow2");
8160 run_keys(&mut e, "3iX<Down><Esc>");
8162 assert!(e.buffer().lines()[0].contains('X'));
8164 assert!(
8167 !e.buffer().lines()[1].contains("row0"),
8168 "row1 leaked row0 contents: {:?}",
8169 e.buffer().lines()[1]
8170 );
8171 assert_eq!(e.buffer().lines().len(), 3);
8174 }
8175
8176 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8179 let mut e = Editor::new(
8180 hjkl_buffer::Buffer::new(),
8181 crate::types::DefaultHost::new(),
8182 crate::types::Options::default(),
8183 );
8184 let body = (0..n)
8185 .map(|i| format!(" line{}", i))
8186 .collect::<Vec<_>>()
8187 .join("\n");
8188 e.set_content(&body);
8189 e.set_viewport_height(viewport);
8190 e
8191 }
8192
8193 #[test]
8194 fn ctrl_d_moves_cursor_half_page_down() {
8195 let mut e = editor_with_rows(100, 20);
8196 run_keys(&mut e, "<C-d>");
8197 assert_eq!(e.cursor().0, 10);
8198 }
8199
8200 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8201 let mut e = Editor::new(
8202 hjkl_buffer::Buffer::new(),
8203 crate::types::DefaultHost::new(),
8204 crate::types::Options::default(),
8205 );
8206 e.set_content(&lines.join("\n"));
8207 e.set_viewport_height(viewport);
8208 let v = e.host_mut().viewport_mut();
8209 v.height = viewport;
8210 v.width = text_width;
8211 v.text_width = text_width;
8212 v.wrap = hjkl_buffer::Wrap::Char;
8213 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8214 e
8215 }
8216
8217 #[test]
8218 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8219 let lines = ["aaaabbbbcccc"; 10];
8223 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8224 e.jump_cursor(4, 0);
8225 e.ensure_cursor_in_scrolloff();
8226 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8227 assert!(csr <= 6, "csr={csr}");
8228 }
8229
8230 #[test]
8231 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8232 let lines = ["aaaabbbbcccc"; 10];
8233 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8234 e.jump_cursor(7, 0);
8237 e.ensure_cursor_in_scrolloff();
8238 e.jump_cursor(2, 0);
8239 e.ensure_cursor_in_scrolloff();
8240 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8241 assert!(csr >= 5, "csr={csr}");
8243 }
8244
8245 #[test]
8246 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8247 let lines = ["aaaabbbbcccc"; 5];
8248 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8249 e.jump_cursor(4, 11);
8250 e.ensure_cursor_in_scrolloff();
8251 let top = e.host().viewport().top_row;
8256 assert_eq!(top, 1);
8257 }
8258
8259 #[test]
8260 fn ctrl_u_moves_cursor_half_page_up() {
8261 let mut e = editor_with_rows(100, 20);
8262 e.jump_cursor(50, 0);
8263 run_keys(&mut e, "<C-u>");
8264 assert_eq!(e.cursor().0, 40);
8265 }
8266
8267 #[test]
8268 fn ctrl_f_moves_cursor_full_page_down() {
8269 let mut e = editor_with_rows(100, 20);
8270 run_keys(&mut e, "<C-f>");
8271 assert_eq!(e.cursor().0, 18);
8273 }
8274
8275 #[test]
8276 fn ctrl_b_moves_cursor_full_page_up() {
8277 let mut e = editor_with_rows(100, 20);
8278 e.jump_cursor(50, 0);
8279 run_keys(&mut e, "<C-b>");
8280 assert_eq!(e.cursor().0, 32);
8281 }
8282
8283 #[test]
8284 fn ctrl_d_lands_on_first_non_blank() {
8285 let mut e = editor_with_rows(100, 20);
8286 run_keys(&mut e, "<C-d>");
8287 assert_eq!(e.cursor().1, 2);
8289 }
8290
8291 #[test]
8292 fn ctrl_d_clamps_at_end_of_buffer() {
8293 let mut e = editor_with_rows(5, 20);
8294 run_keys(&mut e, "<C-d>");
8295 assert_eq!(e.cursor().0, 4);
8296 }
8297
8298 #[test]
8299 fn capital_h_jumps_to_viewport_top() {
8300 let mut e = editor_with_rows(100, 10);
8301 e.jump_cursor(50, 0);
8302 e.set_viewport_top(45);
8303 let top = e.host().viewport().top_row;
8304 run_keys(&mut e, "H");
8305 assert_eq!(e.cursor().0, top);
8306 assert_eq!(e.cursor().1, 2);
8307 }
8308
8309 #[test]
8310 fn capital_l_jumps_to_viewport_bottom() {
8311 let mut e = editor_with_rows(100, 10);
8312 e.jump_cursor(50, 0);
8313 e.set_viewport_top(45);
8314 let top = e.host().viewport().top_row;
8315 run_keys(&mut e, "L");
8316 assert_eq!(e.cursor().0, top + 9);
8317 }
8318
8319 #[test]
8320 fn capital_m_jumps_to_viewport_middle() {
8321 let mut e = editor_with_rows(100, 10);
8322 e.jump_cursor(50, 0);
8323 e.set_viewport_top(45);
8324 let top = e.host().viewport().top_row;
8325 run_keys(&mut e, "M");
8326 assert_eq!(e.cursor().0, top + 4);
8328 }
8329
8330 #[test]
8331 fn g_capital_m_lands_at_line_midpoint() {
8332 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8334 assert_eq!(e.cursor(), (0, 6));
8336 }
8337
8338 #[test]
8339 fn g_capital_m_on_empty_line_stays_at_zero() {
8340 let mut e = editor_with("");
8341 run_keys(&mut e, "gM");
8342 assert_eq!(e.cursor(), (0, 0));
8343 }
8344
8345 #[test]
8346 fn g_capital_m_uses_current_line_only() {
8347 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8350 run_keys(&mut e, "gM");
8351 assert_eq!(e.cursor(), (1, 6));
8352 }
8353
8354 #[test]
8355 fn capital_h_count_offsets_from_top() {
8356 let mut e = editor_with_rows(100, 10);
8357 e.jump_cursor(50, 0);
8358 e.set_viewport_top(45);
8359 let top = e.host().viewport().top_row;
8360 run_keys(&mut e, "3H");
8361 assert_eq!(e.cursor().0, top + 2);
8362 }
8363
8364 #[test]
8367 fn ctrl_o_returns_to_pre_g_position() {
8368 let mut e = editor_with_rows(50, 20);
8369 e.jump_cursor(5, 2);
8370 run_keys(&mut e, "G");
8371 assert_eq!(e.cursor().0, 49);
8372 run_keys(&mut e, "<C-o>");
8373 assert_eq!(e.cursor(), (5, 2));
8374 }
8375
8376 #[test]
8377 fn ctrl_i_redoes_jump_after_ctrl_o() {
8378 let mut e = editor_with_rows(50, 20);
8379 e.jump_cursor(5, 2);
8380 run_keys(&mut e, "G");
8381 let post = e.cursor();
8382 run_keys(&mut e, "<C-o>");
8383 run_keys(&mut e, "<C-i>");
8384 assert_eq!(e.cursor(), post);
8385 }
8386
8387 #[test]
8388 fn new_jump_clears_forward_stack() {
8389 let mut e = editor_with_rows(50, 20);
8390 e.jump_cursor(5, 2);
8391 run_keys(&mut e, "G");
8392 run_keys(&mut e, "<C-o>");
8393 run_keys(&mut e, "gg");
8394 run_keys(&mut e, "<C-i>");
8395 assert_eq!(e.cursor().0, 0);
8396 }
8397
8398 #[test]
8399 fn ctrl_o_on_empty_stack_is_noop() {
8400 let mut e = editor_with_rows(10, 20);
8401 e.jump_cursor(3, 1);
8402 run_keys(&mut e, "<C-o>");
8403 assert_eq!(e.cursor(), (3, 1));
8404 }
8405
8406 #[test]
8407 fn asterisk_search_pushes_jump() {
8408 let mut e = editor_with("foo bar\nbaz foo end");
8409 e.jump_cursor(0, 0);
8410 run_keys(&mut e, "*");
8411 let after = e.cursor();
8412 assert_ne!(after, (0, 0));
8413 run_keys(&mut e, "<C-o>");
8414 assert_eq!(e.cursor(), (0, 0));
8415 }
8416
8417 #[test]
8418 fn h_viewport_jump_is_recorded() {
8419 let mut e = editor_with_rows(100, 10);
8420 e.jump_cursor(50, 0);
8421 e.set_viewport_top(45);
8422 let pre = e.cursor();
8423 run_keys(&mut e, "H");
8424 assert_ne!(e.cursor(), pre);
8425 run_keys(&mut e, "<C-o>");
8426 assert_eq!(e.cursor(), pre);
8427 }
8428
8429 #[test]
8430 fn j_k_motion_does_not_push_jump() {
8431 let mut e = editor_with_rows(50, 20);
8432 e.jump_cursor(5, 0);
8433 run_keys(&mut e, "jjj");
8434 run_keys(&mut e, "<C-o>");
8435 assert_eq!(e.cursor().0, 8);
8436 }
8437
8438 #[test]
8439 fn jumplist_caps_at_100() {
8440 let mut e = editor_with_rows(200, 20);
8441 for i in 0..101 {
8442 e.jump_cursor(i, 0);
8443 run_keys(&mut e, "G");
8444 }
8445 assert!(e.vim.jump_back.len() <= 100);
8446 }
8447
8448 #[test]
8449 fn tab_acts_as_ctrl_i() {
8450 let mut e = editor_with_rows(50, 20);
8451 e.jump_cursor(5, 2);
8452 run_keys(&mut e, "G");
8453 let post = e.cursor();
8454 run_keys(&mut e, "<C-o>");
8455 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8456 assert_eq!(e.cursor(), post);
8457 }
8458
8459 #[test]
8462 fn ma_then_backtick_a_jumps_exact() {
8463 let mut e = editor_with_rows(50, 20);
8464 e.jump_cursor(5, 3);
8465 run_keys(&mut e, "ma");
8466 e.jump_cursor(20, 0);
8467 run_keys(&mut e, "`a");
8468 assert_eq!(e.cursor(), (5, 3));
8469 }
8470
8471 #[test]
8472 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8473 let mut e = editor_with_rows(50, 20);
8474 e.jump_cursor(5, 6);
8476 run_keys(&mut e, "ma");
8477 e.jump_cursor(30, 4);
8478 run_keys(&mut e, "'a");
8479 assert_eq!(e.cursor(), (5, 2));
8480 }
8481
8482 #[test]
8483 fn goto_mark_pushes_jumplist() {
8484 let mut e = editor_with_rows(50, 20);
8485 e.jump_cursor(10, 2);
8486 run_keys(&mut e, "mz");
8487 e.jump_cursor(3, 0);
8488 run_keys(&mut e, "`z");
8489 assert_eq!(e.cursor(), (10, 2));
8490 run_keys(&mut e, "<C-o>");
8491 assert_eq!(e.cursor(), (3, 0));
8492 }
8493
8494 #[test]
8495 fn goto_missing_mark_is_noop() {
8496 let mut e = editor_with_rows(50, 20);
8497 e.jump_cursor(3, 1);
8498 run_keys(&mut e, "`q");
8499 assert_eq!(e.cursor(), (3, 1));
8500 }
8501
8502 #[test]
8503 fn uppercase_mark_stored_under_uppercase_key() {
8504 let mut e = editor_with_rows(50, 20);
8505 e.jump_cursor(5, 3);
8506 run_keys(&mut e, "mA");
8507 assert_eq!(e.mark('A'), Some((5, 3)));
8510 assert!(e.mark('a').is_none());
8511 }
8512
8513 #[test]
8514 fn mark_survives_document_shrink_via_clamp() {
8515 let mut e = editor_with_rows(50, 20);
8516 e.jump_cursor(40, 4);
8517 run_keys(&mut e, "mx");
8518 e.set_content("a\nb\nc\nd\ne");
8520 run_keys(&mut e, "`x");
8521 let (r, _) = e.cursor();
8523 assert!(r <= 4);
8524 }
8525
8526 #[test]
8527 fn g_semicolon_walks_back_through_edits() {
8528 let mut e = editor_with("alpha\nbeta\ngamma");
8529 e.jump_cursor(0, 0);
8532 run_keys(&mut e, "iX<Esc>");
8533 e.jump_cursor(2, 0);
8534 run_keys(&mut e, "iY<Esc>");
8535 run_keys(&mut e, "g;");
8537 assert_eq!(e.cursor(), (2, 1));
8538 run_keys(&mut e, "g;");
8540 assert_eq!(e.cursor(), (0, 1));
8541 run_keys(&mut e, "g;");
8543 assert_eq!(e.cursor(), (0, 1));
8544 }
8545
8546 #[test]
8547 fn g_comma_walks_forward_after_g_semicolon() {
8548 let mut e = editor_with("a\nb\nc");
8549 e.jump_cursor(0, 0);
8550 run_keys(&mut e, "iX<Esc>");
8551 e.jump_cursor(2, 0);
8552 run_keys(&mut e, "iY<Esc>");
8553 run_keys(&mut e, "g;");
8554 run_keys(&mut e, "g;");
8555 assert_eq!(e.cursor(), (0, 1));
8556 run_keys(&mut e, "g,");
8557 assert_eq!(e.cursor(), (2, 1));
8558 }
8559
8560 #[test]
8561 fn new_edit_during_walk_trims_forward_entries() {
8562 let mut e = editor_with("a\nb\nc\nd");
8563 e.jump_cursor(0, 0);
8564 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8566 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8569 run_keys(&mut e, "g;");
8570 assert_eq!(e.cursor(), (0, 1));
8571 run_keys(&mut e, "iZ<Esc>");
8573 run_keys(&mut e, "g,");
8575 assert_ne!(e.cursor(), (2, 1));
8577 }
8578
8579 #[test]
8585 fn capital_mark_set_and_jump() {
8586 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8587 e.jump_cursor(2, 1);
8588 run_keys(&mut e, "mA");
8589 e.jump_cursor(0, 0);
8591 run_keys(&mut e, "'A");
8593 assert_eq!(e.cursor().0, 2);
8595 }
8596
8597 #[test]
8598 fn capital_mark_survives_set_content() {
8599 let mut e = editor_with("first buffer line\nsecond");
8600 e.jump_cursor(1, 3);
8601 run_keys(&mut e, "mA");
8602 e.set_content("totally different content\non many\nrows of text");
8604 e.jump_cursor(0, 0);
8606 run_keys(&mut e, "'A");
8607 assert_eq!(e.cursor().0, 1);
8608 }
8609
8610 #[test]
8615 fn capital_mark_shifts_with_edit() {
8616 let mut e = editor_with("a\nb\nc\nd");
8617 e.jump_cursor(3, 0);
8618 run_keys(&mut e, "mA");
8619 e.jump_cursor(0, 0);
8621 run_keys(&mut e, "dd");
8622 e.jump_cursor(0, 0);
8623 run_keys(&mut e, "'A");
8624 assert_eq!(e.cursor().0, 2);
8625 }
8626
8627 #[test]
8628 fn mark_below_delete_shifts_up() {
8629 let mut e = editor_with("a\nb\nc\nd\ne");
8630 e.jump_cursor(3, 0);
8632 run_keys(&mut e, "ma");
8633 e.jump_cursor(0, 0);
8635 run_keys(&mut e, "dd");
8636 e.jump_cursor(0, 0);
8638 run_keys(&mut e, "'a");
8639 assert_eq!(e.cursor().0, 2);
8640 assert_eq!(e.buffer().line(2).unwrap(), "d");
8641 }
8642
8643 #[test]
8644 fn mark_on_deleted_row_is_dropped() {
8645 let mut e = editor_with("a\nb\nc\nd");
8646 e.jump_cursor(1, 0);
8648 run_keys(&mut e, "ma");
8649 run_keys(&mut e, "dd");
8651 e.jump_cursor(2, 0);
8653 run_keys(&mut e, "'a");
8654 assert_eq!(e.cursor().0, 2);
8656 }
8657
8658 #[test]
8659 fn mark_above_edit_unchanged() {
8660 let mut e = editor_with("a\nb\nc\nd\ne");
8661 e.jump_cursor(0, 0);
8663 run_keys(&mut e, "ma");
8664 e.jump_cursor(3, 0);
8666 run_keys(&mut e, "dd");
8667 e.jump_cursor(2, 0);
8669 run_keys(&mut e, "'a");
8670 assert_eq!(e.cursor().0, 0);
8671 }
8672
8673 #[test]
8674 fn mark_shifts_down_after_insert() {
8675 let mut e = editor_with("a\nb\nc");
8676 e.jump_cursor(2, 0);
8678 run_keys(&mut e, "ma");
8679 e.jump_cursor(0, 0);
8681 run_keys(&mut e, "Onew<Esc>");
8682 e.jump_cursor(0, 0);
8685 run_keys(&mut e, "'a");
8686 assert_eq!(e.cursor().0, 3);
8687 assert_eq!(e.buffer().line(3).unwrap(), "c");
8688 }
8689
8690 #[test]
8693 fn forward_search_commit_pushes_jump() {
8694 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8695 e.jump_cursor(0, 0);
8696 run_keys(&mut e, "/target<CR>");
8697 assert_ne!(e.cursor(), (0, 0));
8699 run_keys(&mut e, "<C-o>");
8701 assert_eq!(e.cursor(), (0, 0));
8702 }
8703
8704 #[test]
8705 fn search_commit_no_match_does_not_push_jump() {
8706 let mut e = editor_with("alpha beta\nfoo end");
8707 e.jump_cursor(0, 3);
8708 let pre_len = e.vim.jump_back.len();
8709 run_keys(&mut e, "/zzznotfound<CR>");
8710 assert_eq!(e.vim.jump_back.len(), pre_len);
8712 }
8713
8714 #[test]
8717 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8718 let mut e = editor_with("hello world");
8719 run_keys(&mut e, "lll");
8720 let (row, col) = e.cursor();
8721 assert_eq!(e.buffer.cursor().row, row);
8722 assert_eq!(e.buffer.cursor().col, col);
8723 }
8724
8725 #[test]
8726 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8727 let mut e = editor_with("aaaa\nbbbb\ncccc");
8728 run_keys(&mut e, "jj");
8729 let (row, col) = e.cursor();
8730 assert_eq!(e.buffer.cursor().row, row);
8731 assert_eq!(e.buffer.cursor().col, col);
8732 }
8733
8734 #[test]
8735 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8736 let mut e = editor_with("foo bar baz");
8737 run_keys(&mut e, "ww");
8738 let (row, col) = e.cursor();
8739 assert_eq!(e.buffer.cursor().row, row);
8740 assert_eq!(e.buffer.cursor().col, col);
8741 }
8742
8743 #[test]
8744 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8745 let mut e = editor_with("a\nb\nc\nd\ne");
8746 run_keys(&mut e, "G");
8747 let (row, col) = e.cursor();
8748 assert_eq!(e.buffer.cursor().row, row);
8749 assert_eq!(e.buffer.cursor().col, col);
8750 }
8751
8752 #[test]
8753 fn editor_sticky_col_tracks_horizontal_motion() {
8754 let mut e = editor_with("longline\nhi\nlongline");
8755 run_keys(&mut e, "fl");
8760 let landed = e.cursor().1;
8761 assert!(landed > 0, "fl should have moved");
8762 run_keys(&mut e, "j");
8763 assert_eq!(e.sticky_col(), Some(landed));
8766 }
8767
8768 #[test]
8769 fn buffer_content_mirrors_textarea_after_insert() {
8770 let mut e = editor_with("hello");
8771 run_keys(&mut e, "iXYZ<Esc>");
8772 let text = e.buffer().lines().join("\n");
8773 assert_eq!(e.buffer.as_string(), text);
8774 }
8775
8776 #[test]
8777 fn buffer_content_mirrors_textarea_after_delete() {
8778 let mut e = editor_with("alpha bravo charlie");
8779 run_keys(&mut e, "dw");
8780 let text = e.buffer().lines().join("\n");
8781 assert_eq!(e.buffer.as_string(), text);
8782 }
8783
8784 #[test]
8785 fn buffer_content_mirrors_textarea_after_dd() {
8786 let mut e = editor_with("a\nb\nc\nd");
8787 run_keys(&mut e, "jdd");
8788 let text = e.buffer().lines().join("\n");
8789 assert_eq!(e.buffer.as_string(), text);
8790 }
8791
8792 #[test]
8793 fn buffer_content_mirrors_textarea_after_open_line() {
8794 let mut e = editor_with("foo\nbar");
8795 run_keys(&mut e, "oNEW<Esc>");
8796 let text = e.buffer().lines().join("\n");
8797 assert_eq!(e.buffer.as_string(), text);
8798 }
8799
8800 #[test]
8801 fn buffer_content_mirrors_textarea_after_paste() {
8802 let mut e = editor_with("hello");
8803 run_keys(&mut e, "yy");
8804 run_keys(&mut e, "p");
8805 let text = e.buffer().lines().join("\n");
8806 assert_eq!(e.buffer.as_string(), text);
8807 }
8808
8809 #[test]
8810 fn buffer_selection_none_in_normal_mode() {
8811 let e = editor_with("foo bar");
8812 assert!(e.buffer_selection().is_none());
8813 }
8814
8815 #[test]
8816 fn buffer_selection_char_in_visual_mode() {
8817 use hjkl_buffer::{Position, Selection};
8818 let mut e = editor_with("hello world");
8819 run_keys(&mut e, "vlll");
8820 assert_eq!(
8821 e.buffer_selection(),
8822 Some(Selection::Char {
8823 anchor: Position::new(0, 0),
8824 head: Position::new(0, 3),
8825 })
8826 );
8827 }
8828
8829 #[test]
8830 fn buffer_selection_line_in_visual_line_mode() {
8831 use hjkl_buffer::Selection;
8832 let mut e = editor_with("a\nb\nc\nd");
8833 run_keys(&mut e, "Vj");
8834 assert_eq!(
8835 e.buffer_selection(),
8836 Some(Selection::Line {
8837 anchor_row: 0,
8838 head_row: 1,
8839 })
8840 );
8841 }
8842
8843 #[test]
8844 fn wrapscan_off_blocks_wrap_around() {
8845 let mut e = editor_with("first\nsecond\nthird\n");
8846 e.settings_mut().wrapscan = false;
8847 e.jump_cursor(2, 0);
8849 run_keys(&mut e, "/first<CR>");
8850 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8852 e.settings_mut().wrapscan = true;
8854 run_keys(&mut e, "/first<CR>");
8855 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8856 }
8857
8858 #[test]
8859 fn smartcase_uppercase_pattern_stays_sensitive() {
8860 let mut e = editor_with("foo\nFoo\nBAR\n");
8861 e.settings_mut().ignore_case = true;
8862 e.settings_mut().smartcase = true;
8863 run_keys(&mut e, "/foo<CR>");
8866 let r1 = e
8867 .search_state()
8868 .pattern
8869 .as_ref()
8870 .unwrap()
8871 .as_str()
8872 .to_string();
8873 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8874 run_keys(&mut e, "/Foo<CR>");
8876 let r2 = e
8877 .search_state()
8878 .pattern
8879 .as_ref()
8880 .unwrap()
8881 .as_str()
8882 .to_string();
8883 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8884 }
8885
8886 #[test]
8887 fn enter_with_autoindent_copies_leading_whitespace() {
8888 let mut e = editor_with(" foo");
8889 e.jump_cursor(0, 7);
8890 run_keys(&mut e, "i<CR>");
8891 assert_eq!(e.buffer.line(1).unwrap(), " ");
8892 }
8893
8894 #[test]
8895 fn enter_without_autoindent_inserts_bare_newline() {
8896 let mut e = editor_with(" foo");
8897 e.settings_mut().autoindent = false;
8898 e.jump_cursor(0, 7);
8899 run_keys(&mut e, "i<CR>");
8900 assert_eq!(e.buffer.line(1).unwrap(), "");
8901 }
8902
8903 #[test]
8904 fn iskeyword_default_treats_alnum_underscore_as_word() {
8905 let mut e = editor_with("foo_bar baz");
8906 e.jump_cursor(0, 0);
8910 run_keys(&mut e, "*");
8911 let p = e
8912 .search_state()
8913 .pattern
8914 .as_ref()
8915 .unwrap()
8916 .as_str()
8917 .to_string();
8918 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8919 }
8920
8921 #[test]
8922 fn w_motion_respects_custom_iskeyword() {
8923 let mut e = editor_with("foo-bar baz");
8927 run_keys(&mut e, "w");
8928 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
8929 let mut e2 = editor_with("foo-bar baz");
8932 e2.set_iskeyword("@,_,45");
8933 run_keys(&mut e2, "w");
8934 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
8935 }
8936
8937 #[test]
8938 fn iskeyword_with_dash_treats_dash_as_word_char() {
8939 let mut e = editor_with("foo-bar baz");
8940 e.settings_mut().iskeyword = "@,_,45".to_string();
8941 e.jump_cursor(0, 0);
8942 run_keys(&mut e, "*");
8943 let p = e
8944 .search_state()
8945 .pattern
8946 .as_ref()
8947 .unwrap()
8948 .as_str()
8949 .to_string();
8950 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
8951 }
8952
8953 #[test]
8954 fn timeoutlen_drops_pending_g_prefix() {
8955 use std::time::{Duration, Instant};
8956 let mut e = editor_with("a\nb\nc");
8957 e.jump_cursor(2, 0);
8958 run_keys(&mut e, "g");
8960 assert!(matches!(e.vim.pending, super::Pending::G));
8961 e.settings.timeout_len = Duration::from_nanos(0);
8969 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
8970 e.vim.last_input_host_at = Some(Duration::ZERO);
8971 run_keys(&mut e, "g");
8975 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
8977 }
8978
8979 #[test]
8980 fn undobreak_on_breaks_group_at_arrow_motion() {
8981 let mut e = editor_with("");
8982 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8984 let line = e.buffer.line(0).unwrap_or("").to_string();
8987 assert!(line.contains("aaa"), "after undobreak: {line:?}");
8988 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
8989 }
8990
8991 #[test]
8992 fn undobreak_off_keeps_full_run_in_one_group() {
8993 let mut e = editor_with("");
8994 e.settings_mut().undo_break_on_motion = false;
8995 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8996 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
8999 }
9000
9001 #[test]
9002 fn undobreak_round_trips_through_options() {
9003 let e = editor_with("");
9004 let opts = e.current_options();
9005 assert!(opts.undo_break_on_motion);
9006 let mut e2 = editor_with("");
9007 let mut new_opts = opts.clone();
9008 new_opts.undo_break_on_motion = false;
9009 e2.apply_options(&new_opts);
9010 assert!(!e2.current_options().undo_break_on_motion);
9011 }
9012
9013 #[test]
9014 fn undo_levels_cap_drops_oldest() {
9015 let mut e = editor_with("abcde");
9016 e.settings_mut().undo_levels = 3;
9017 run_keys(&mut e, "ra");
9018 run_keys(&mut e, "lrb");
9019 run_keys(&mut e, "lrc");
9020 run_keys(&mut e, "lrd");
9021 run_keys(&mut e, "lre");
9022 assert_eq!(e.undo_stack_len(), 3);
9023 }
9024
9025 #[test]
9026 fn tab_inserts_literal_tab_when_noexpandtab() {
9027 let mut e = editor_with("");
9028 e.settings_mut().expandtab = false;
9031 e.settings_mut().softtabstop = 0;
9032 run_keys(&mut e, "i");
9033 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9034 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9035 }
9036
9037 #[test]
9038 fn tab_inserts_spaces_when_expandtab() {
9039 let mut e = editor_with("");
9040 e.settings_mut().expandtab = true;
9041 e.settings_mut().tabstop = 4;
9042 run_keys(&mut e, "i");
9043 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9044 assert_eq!(e.buffer.line(0).unwrap(), " ");
9045 }
9046
9047 #[test]
9048 fn tab_with_softtabstop_fills_to_next_boundary() {
9049 let mut e = editor_with("ab");
9051 e.settings_mut().expandtab = true;
9052 e.settings_mut().tabstop = 8;
9053 e.settings_mut().softtabstop = 4;
9054 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9056 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9057 }
9058
9059 #[test]
9060 fn backspace_deletes_softtab_run() {
9061 let mut e = editor_with(" x");
9064 e.settings_mut().softtabstop = 4;
9065 run_keys(&mut e, "fxi");
9067 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9068 assert_eq!(e.buffer.line(0).unwrap(), "x");
9069 }
9070
9071 #[test]
9072 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9073 let mut e = editor_with(" x");
9076 e.settings_mut().softtabstop = 4;
9077 run_keys(&mut e, "fxi");
9078 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9079 assert_eq!(e.buffer.line(0).unwrap(), " x");
9080 }
9081
9082 #[test]
9083 fn readonly_blocks_insert_mutation() {
9084 let mut e = editor_with("hello");
9085 e.settings_mut().readonly = true;
9086 run_keys(&mut e, "iX<Esc>");
9087 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9088 }
9089
9090 #[cfg(feature = "ratatui")]
9091 #[test]
9092 fn intern_ratatui_style_dedups_repeated_styles() {
9093 use ratatui::style::{Color, Style};
9094 let mut e = editor_with("");
9095 let red = Style::default().fg(Color::Red);
9096 let blue = Style::default().fg(Color::Blue);
9097 let id_r1 = e.intern_ratatui_style(red);
9098 let id_r2 = e.intern_ratatui_style(red);
9099 let id_b = e.intern_ratatui_style(blue);
9100 assert_eq!(id_r1, id_r2);
9101 assert_ne!(id_r1, id_b);
9102 assert_eq!(e.style_table().len(), 2);
9103 }
9104
9105 #[cfg(feature = "ratatui")]
9106 #[test]
9107 fn install_ratatui_syntax_spans_translates_styled_spans() {
9108 use ratatui::style::{Color, Style};
9109 let mut e = editor_with("SELECT foo");
9110 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9111 let by_row = e.buffer_spans();
9112 assert_eq!(by_row.len(), 1);
9113 assert_eq!(by_row[0].len(), 1);
9114 assert_eq!(by_row[0][0].start_byte, 0);
9115 assert_eq!(by_row[0][0].end_byte, 6);
9116 let id = by_row[0][0].style;
9117 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9118 }
9119
9120 #[cfg(feature = "ratatui")]
9121 #[test]
9122 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9123 use ratatui::style::{Color, Style};
9124 let mut e = editor_with("hello");
9125 e.install_ratatui_syntax_spans(vec![vec![(
9126 0,
9127 usize::MAX,
9128 Style::default().fg(Color::Blue),
9129 )]]);
9130 let by_row = e.buffer_spans();
9131 assert_eq!(by_row[0][0].end_byte, 5);
9132 }
9133
9134 #[cfg(feature = "ratatui")]
9135 #[test]
9136 fn install_ratatui_syntax_spans_drops_zero_width() {
9137 use ratatui::style::{Color, Style};
9138 let mut e = editor_with("abc");
9139 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9140 assert!(e.buffer_spans()[0].is_empty());
9141 }
9142
9143 #[test]
9144 fn named_register_yank_into_a_then_paste_from_a() {
9145 let mut e = editor_with("hello world\nsecond");
9146 run_keys(&mut e, "\"ayw");
9147 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9149 run_keys(&mut e, "j0\"aP");
9151 assert_eq!(e.buffer().lines()[1], "hello second");
9152 }
9153
9154 #[test]
9155 fn capital_r_overstrikes_chars() {
9156 let mut e = editor_with("hello");
9157 e.jump_cursor(0, 0);
9158 run_keys(&mut e, "RXY<Esc>");
9159 assert_eq!(e.buffer().lines()[0], "XYllo");
9161 }
9162
9163 #[test]
9164 fn capital_r_at_eol_appends() {
9165 let mut e = editor_with("hi");
9166 e.jump_cursor(0, 1);
9167 run_keys(&mut e, "RXYZ<Esc>");
9169 assert_eq!(e.buffer().lines()[0], "hXYZ");
9170 }
9171
9172 #[test]
9173 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9174 let mut e = editor_with("abc");
9178 e.jump_cursor(0, 0);
9179 run_keys(&mut e, "RX<Esc>");
9180 assert_eq!(e.buffer().lines()[0], "Xbc");
9181 }
9182
9183 #[test]
9184 fn ctrl_r_in_insert_pastes_named_register() {
9185 let mut e = editor_with("hello world");
9186 run_keys(&mut e, "\"ayw");
9188 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9189 run_keys(&mut e, "o");
9191 assert_eq!(e.vim_mode(), VimMode::Insert);
9192 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9193 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9194 assert_eq!(e.buffer().lines()[1], "hello ");
9195 assert_eq!(e.cursor(), (1, 6));
9197 assert_eq!(e.vim_mode(), VimMode::Insert);
9199 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9200 assert_eq!(e.buffer().lines()[1], "hello X");
9201 }
9202
9203 #[test]
9204 fn ctrl_r_with_unnamed_register() {
9205 let mut e = editor_with("foo");
9206 run_keys(&mut e, "yiw");
9207 run_keys(&mut e, "A ");
9208 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9210 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9211 assert_eq!(e.buffer().lines()[0], "foo foo");
9212 }
9213
9214 #[test]
9215 fn ctrl_r_unknown_selector_is_no_op() {
9216 let mut e = editor_with("abc");
9217 run_keys(&mut e, "A");
9218 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9219 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9222 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9223 assert_eq!(e.buffer().lines()[0], "abcZ");
9224 }
9225
9226 #[test]
9227 fn ctrl_r_multiline_register_pastes_with_newlines() {
9228 let mut e = editor_with("alpha\nbeta\ngamma");
9229 run_keys(&mut e, "\"byy");
9231 run_keys(&mut e, "j\"byy");
9232 run_keys(&mut e, "ggVj\"by");
9236 let payload = e.registers().read('b').unwrap().text.clone();
9237 assert!(payload.contains('\n'));
9238 run_keys(&mut e, "Go");
9239 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9240 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9241 let total_lines = e.buffer().lines().len();
9244 assert!(total_lines >= 5);
9245 }
9246
9247 #[test]
9248 fn yank_zero_holds_last_yank_after_delete() {
9249 let mut e = editor_with("hello world");
9250 run_keys(&mut e, "yw");
9251 let yanked = e.registers().read('0').unwrap().text.clone();
9252 assert!(!yanked.is_empty());
9253 run_keys(&mut e, "dw");
9255 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9256 assert!(!e.registers().read('1').unwrap().text.is_empty());
9258 }
9259
9260 #[test]
9261 fn delete_ring_rotates_through_one_through_nine() {
9262 let mut e = editor_with("a b c d e f g h i j");
9263 for _ in 0..3 {
9265 run_keys(&mut e, "dw");
9266 }
9267 let r1 = e.registers().read('1').unwrap().text.clone();
9269 let r2 = e.registers().read('2').unwrap().text.clone();
9270 let r3 = e.registers().read('3').unwrap().text.clone();
9271 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9272 assert_ne!(r1, r2);
9273 assert_ne!(r2, r3);
9274 }
9275
9276 #[test]
9277 fn capital_register_appends_to_lowercase() {
9278 let mut e = editor_with("foo bar");
9279 run_keys(&mut e, "\"ayw");
9280 let first = e.registers().read('a').unwrap().text.clone();
9281 assert!(first.contains("foo"));
9282 run_keys(&mut e, "w\"Ayw");
9284 let combined = e.registers().read('a').unwrap().text.clone();
9285 assert!(combined.starts_with(&first));
9286 assert!(combined.contains("bar"));
9287 }
9288
9289 #[test]
9290 fn zf_in_visual_line_creates_closed_fold() {
9291 let mut e = editor_with("a\nb\nc\nd\ne");
9292 e.jump_cursor(1, 0);
9294 run_keys(&mut e, "Vjjzf");
9295 assert_eq!(e.buffer().folds().len(), 1);
9296 let f = e.buffer().folds()[0];
9297 assert_eq!(f.start_row, 1);
9298 assert_eq!(f.end_row, 3);
9299 assert!(f.closed);
9300 }
9301
9302 #[test]
9303 fn zfj_in_normal_creates_two_row_fold() {
9304 let mut e = editor_with("a\nb\nc\nd\ne");
9305 e.jump_cursor(1, 0);
9306 run_keys(&mut e, "zfj");
9307 assert_eq!(e.buffer().folds().len(), 1);
9308 let f = e.buffer().folds()[0];
9309 assert_eq!(f.start_row, 1);
9310 assert_eq!(f.end_row, 2);
9311 assert!(f.closed);
9312 assert_eq!(e.cursor().0, 1);
9314 }
9315
9316 #[test]
9317 fn zf_with_count_folds_count_rows() {
9318 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9319 e.jump_cursor(0, 0);
9320 run_keys(&mut e, "zf3j");
9322 assert_eq!(e.buffer().folds().len(), 1);
9323 let f = e.buffer().folds()[0];
9324 assert_eq!(f.start_row, 0);
9325 assert_eq!(f.end_row, 3);
9326 }
9327
9328 #[test]
9329 fn zfk_folds_upward_range() {
9330 let mut e = editor_with("a\nb\nc\nd\ne");
9331 e.jump_cursor(3, 0);
9332 run_keys(&mut e, "zfk");
9333 let f = e.buffer().folds()[0];
9334 assert_eq!(f.start_row, 2);
9336 assert_eq!(f.end_row, 3);
9337 }
9338
9339 #[test]
9340 fn zf_capital_g_folds_to_bottom() {
9341 let mut e = editor_with("a\nb\nc\nd\ne");
9342 e.jump_cursor(1, 0);
9343 run_keys(&mut e, "zfG");
9345 let f = e.buffer().folds()[0];
9346 assert_eq!(f.start_row, 1);
9347 assert_eq!(f.end_row, 4);
9348 }
9349
9350 #[test]
9351 fn zfgg_folds_to_top_via_operator_pipeline() {
9352 let mut e = editor_with("a\nb\nc\nd\ne");
9353 e.jump_cursor(3, 0);
9354 run_keys(&mut e, "zfgg");
9358 let f = e.buffer().folds()[0];
9359 assert_eq!(f.start_row, 0);
9360 assert_eq!(f.end_row, 3);
9361 }
9362
9363 #[test]
9364 fn zfip_folds_paragraph_via_text_object() {
9365 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9366 e.jump_cursor(1, 0);
9367 run_keys(&mut e, "zfip");
9369 assert_eq!(e.buffer().folds().len(), 1);
9370 let f = e.buffer().folds()[0];
9371 assert_eq!(f.start_row, 0);
9372 assert_eq!(f.end_row, 2);
9373 }
9374
9375 #[test]
9376 fn zfap_folds_paragraph_with_trailing_blank() {
9377 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9378 e.jump_cursor(0, 0);
9379 run_keys(&mut e, "zfap");
9381 let f = e.buffer().folds()[0];
9382 assert_eq!(f.start_row, 0);
9383 assert_eq!(f.end_row, 3);
9384 }
9385
9386 #[test]
9387 fn zf_paragraph_motion_folds_to_blank() {
9388 let mut e = editor_with("alpha\nbeta\n\ngamma");
9389 e.jump_cursor(0, 0);
9390 run_keys(&mut e, "zf}");
9392 let f = e.buffer().folds()[0];
9393 assert_eq!(f.start_row, 0);
9394 assert_eq!(f.end_row, 2);
9395 }
9396
9397 #[test]
9398 fn za_toggles_fold_under_cursor() {
9399 let mut e = editor_with("a\nb\nc\nd");
9400 e.buffer_mut().add_fold(1, 2, true);
9401 e.jump_cursor(1, 0);
9402 run_keys(&mut e, "za");
9403 assert!(!e.buffer().folds()[0].closed);
9404 run_keys(&mut e, "za");
9405 assert!(e.buffer().folds()[0].closed);
9406 }
9407
9408 #[test]
9409 fn zr_opens_all_folds_zm_closes_all() {
9410 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9411 e.buffer_mut().add_fold(0, 1, true);
9412 e.buffer_mut().add_fold(2, 3, true);
9413 e.buffer_mut().add_fold(4, 5, true);
9414 run_keys(&mut e, "zR");
9415 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9416 run_keys(&mut e, "zM");
9417 assert!(e.buffer().folds().iter().all(|f| f.closed));
9418 }
9419
9420 #[test]
9421 fn ze_clears_all_folds() {
9422 let mut e = editor_with("a\nb\nc\nd");
9423 e.buffer_mut().add_fold(0, 1, true);
9424 e.buffer_mut().add_fold(2, 3, false);
9425 run_keys(&mut e, "zE");
9426 assert!(e.buffer().folds().is_empty());
9427 }
9428
9429 #[test]
9430 fn g_underscore_jumps_to_last_non_blank() {
9431 let mut e = editor_with("hello world ");
9432 run_keys(&mut e, "g_");
9433 assert_eq!(e.cursor().1, 10);
9435 }
9436
9437 #[test]
9438 fn gj_and_gk_alias_j_and_k() {
9439 let mut e = editor_with("a\nb\nc");
9440 run_keys(&mut e, "gj");
9441 assert_eq!(e.cursor().0, 1);
9442 run_keys(&mut e, "gk");
9443 assert_eq!(e.cursor().0, 0);
9444 }
9445
9446 #[test]
9447 fn paragraph_motions_walk_blank_lines() {
9448 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9449 run_keys(&mut e, "}");
9450 assert_eq!(e.cursor().0, 2);
9451 run_keys(&mut e, "}");
9452 assert_eq!(e.cursor().0, 5);
9453 run_keys(&mut e, "{");
9454 assert_eq!(e.cursor().0, 2);
9455 }
9456
9457 #[test]
9458 fn gv_reenters_last_visual_selection() {
9459 let mut e = editor_with("alpha\nbeta\ngamma");
9460 run_keys(&mut e, "Vj");
9461 run_keys(&mut e, "<Esc>");
9463 assert_eq!(e.vim_mode(), VimMode::Normal);
9464 run_keys(&mut e, "gv");
9466 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9467 }
9468
9469 #[test]
9470 fn o_in_visual_swaps_anchor_and_cursor() {
9471 let mut e = editor_with("hello world");
9472 run_keys(&mut e, "vllll");
9474 assert_eq!(e.cursor().1, 4);
9475 run_keys(&mut e, "o");
9477 assert_eq!(e.cursor().1, 0);
9478 assert_eq!(e.vim.visual_anchor, (0, 4));
9480 }
9481
9482 #[test]
9483 fn editing_inside_fold_invalidates_it() {
9484 let mut e = editor_with("a\nb\nc\nd");
9485 e.buffer_mut().add_fold(1, 2, true);
9486 e.jump_cursor(1, 0);
9487 run_keys(&mut e, "iX<Esc>");
9489 assert!(e.buffer().folds().is_empty());
9491 }
9492
9493 #[test]
9494 fn zd_removes_fold_under_cursor() {
9495 let mut e = editor_with("a\nb\nc\nd");
9496 e.buffer_mut().add_fold(1, 2, true);
9497 e.jump_cursor(2, 0);
9498 run_keys(&mut e, "zd");
9499 assert!(e.buffer().folds().is_empty());
9500 }
9501
9502 #[test]
9503 fn take_fold_ops_observes_z_keystroke_dispatch() {
9504 use crate::types::FoldOp;
9509 let mut e = editor_with("a\nb\nc\nd");
9510 e.buffer_mut().add_fold(1, 2, true);
9511 e.jump_cursor(1, 0);
9512 let _ = e.take_fold_ops();
9515 run_keys(&mut e, "zo");
9516 run_keys(&mut e, "zM");
9517 let ops = e.take_fold_ops();
9518 assert_eq!(ops.len(), 2);
9519 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9520 assert!(matches!(ops[1], FoldOp::CloseAll));
9521 assert!(e.take_fold_ops().is_empty());
9523 }
9524
9525 #[test]
9526 fn edit_pipeline_emits_invalidate_fold_op() {
9527 use crate::types::FoldOp;
9530 let mut e = editor_with("a\nb\nc\nd");
9531 e.buffer_mut().add_fold(1, 2, true);
9532 e.jump_cursor(1, 0);
9533 let _ = e.take_fold_ops();
9534 run_keys(&mut e, "iX<Esc>");
9535 let ops = e.take_fold_ops();
9536 assert!(
9537 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9538 "expected at least one Invalidate op, got {ops:?}"
9539 );
9540 }
9541
9542 #[test]
9543 fn dot_mark_jumps_to_last_edit_position() {
9544 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9545 e.jump_cursor(2, 0);
9546 run_keys(&mut e, "iX<Esc>");
9548 let after_edit = e.cursor();
9549 run_keys(&mut e, "gg");
9551 assert_eq!(e.cursor().0, 0);
9552 run_keys(&mut e, "'.");
9554 assert_eq!(e.cursor().0, after_edit.0);
9555 }
9556
9557 #[test]
9558 fn quote_quote_returns_to_pre_jump_position() {
9559 let mut e = editor_with_rows(50, 20);
9560 e.jump_cursor(10, 2);
9561 let before = e.cursor();
9562 run_keys(&mut e, "G");
9564 assert_ne!(e.cursor(), before);
9565 run_keys(&mut e, "''");
9567 assert_eq!(e.cursor().0, before.0);
9568 }
9569
9570 #[test]
9571 fn backtick_backtick_restores_exact_pre_jump_pos() {
9572 let mut e = editor_with_rows(50, 20);
9573 e.jump_cursor(7, 3);
9574 let before = e.cursor();
9575 run_keys(&mut e, "G");
9576 run_keys(&mut e, "``");
9577 assert_eq!(e.cursor(), before);
9578 }
9579
9580 #[test]
9581 fn macro_record_and_replay_basic() {
9582 let mut e = editor_with("foo\nbar\nbaz");
9583 run_keys(&mut e, "qaIX<Esc>jq");
9585 assert_eq!(e.buffer().lines()[0], "Xfoo");
9586 run_keys(&mut e, "@a");
9588 assert_eq!(e.buffer().lines()[1], "Xbar");
9589 run_keys(&mut e, "j@@");
9591 assert_eq!(e.buffer().lines()[2], "Xbaz");
9592 }
9593
9594 #[test]
9595 fn macro_count_replays_n_times() {
9596 let mut e = editor_with("a\nb\nc\nd\ne");
9597 run_keys(&mut e, "qajq");
9599 assert_eq!(e.cursor().0, 1);
9600 run_keys(&mut e, "3@a");
9602 assert_eq!(e.cursor().0, 4);
9603 }
9604
9605 #[test]
9606 fn macro_capital_q_appends_to_lowercase_register() {
9607 let mut e = editor_with("hello");
9608 run_keys(&mut e, "qall<Esc>q");
9609 run_keys(&mut e, "qAhh<Esc>q");
9610 let text = e.registers().read('a').unwrap().text.clone();
9613 assert!(text.contains("ll<Esc>"));
9614 assert!(text.contains("hh<Esc>"));
9615 }
9616
9617 #[test]
9618 fn buffer_selection_block_in_visual_block_mode() {
9619 use hjkl_buffer::{Position, Selection};
9620 let mut e = editor_with("aaaa\nbbbb\ncccc");
9621 run_keys(&mut e, "<C-v>jl");
9622 assert_eq!(
9623 e.buffer_selection(),
9624 Some(Selection::Block {
9625 anchor: Position::new(0, 0),
9626 head: Position::new(1, 1),
9627 })
9628 );
9629 }
9630
9631 #[test]
9634 fn n_after_question_mark_keeps_walking_backward() {
9635 let mut e = editor_with("foo bar foo baz foo end");
9638 e.jump_cursor(0, 22);
9639 run_keys(&mut e, "?foo<CR>");
9640 assert_eq!(e.cursor().1, 16);
9641 run_keys(&mut e, "n");
9642 assert_eq!(e.cursor().1, 8);
9643 run_keys(&mut e, "N");
9644 assert_eq!(e.cursor().1, 16);
9645 }
9646
9647 #[test]
9648 fn nested_macro_chord_records_literal_keys() {
9649 let mut e = editor_with("alpha\nbeta\ngamma");
9652 run_keys(&mut e, "qblq");
9654 run_keys(&mut e, "qaIX<Esc>q");
9657 e.jump_cursor(1, 0);
9659 run_keys(&mut e, "@a");
9660 assert_eq!(e.buffer().lines()[1], "Xbeta");
9661 }
9662
9663 #[test]
9664 fn shift_gt_motion_indents_one_line() {
9665 let mut e = editor_with("hello world");
9669 run_keys(&mut e, ">w");
9670 assert_eq!(e.buffer().lines()[0], " hello world");
9671 }
9672
9673 #[test]
9674 fn shift_lt_motion_outdents_one_line() {
9675 let mut e = editor_with(" hello world");
9676 run_keys(&mut e, "<lt>w");
9677 assert_eq!(e.buffer().lines()[0], " hello world");
9679 }
9680
9681 #[test]
9682 fn shift_gt_text_object_indents_paragraph() {
9683 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9684 e.jump_cursor(0, 0);
9685 run_keys(&mut e, ">ip");
9686 assert_eq!(e.buffer().lines()[0], " alpha");
9687 assert_eq!(e.buffer().lines()[1], " beta");
9688 assert_eq!(e.buffer().lines()[2], " gamma");
9689 assert_eq!(e.buffer().lines()[4], "rest");
9691 }
9692
9693 #[test]
9694 fn ctrl_o_runs_exactly_one_normal_command() {
9695 let mut e = editor_with("alpha beta gamma");
9698 e.jump_cursor(0, 0);
9699 run_keys(&mut e, "i");
9700 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9701 run_keys(&mut e, "dw");
9702 assert_eq!(e.vim_mode(), VimMode::Insert);
9704 run_keys(&mut e, "X");
9706 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9707 }
9708
9709 #[test]
9710 fn macro_replay_respects_mode_switching() {
9711 let mut e = editor_with("hi");
9715 run_keys(&mut e, "qaiX<Esc>0q");
9716 assert_eq!(e.vim_mode(), VimMode::Normal);
9717 e.set_content("yo");
9719 run_keys(&mut e, "@a");
9720 assert_eq!(e.vim_mode(), VimMode::Normal);
9721 assert_eq!(e.cursor().1, 0);
9722 assert_eq!(e.buffer().lines()[0], "Xyo");
9723 }
9724
9725 #[test]
9726 fn macro_recorded_text_round_trips_through_register() {
9727 let mut e = editor_with("");
9731 run_keys(&mut e, "qaiX<Esc>q");
9732 let text = e.registers().read('a').unwrap().text.clone();
9733 assert!(text.starts_with("iX"));
9734 run_keys(&mut e, "@a");
9736 assert_eq!(e.buffer().lines()[0], "XX");
9737 }
9738
9739 #[test]
9740 fn dot_after_macro_replays_macros_last_change() {
9741 let mut e = editor_with("ab\ncd\nef");
9744 run_keys(&mut e, "qaIX<Esc>jq");
9747 assert_eq!(e.buffer().lines()[0], "Xab");
9748 run_keys(&mut e, "@a");
9749 assert_eq!(e.buffer().lines()[1], "Xcd");
9750 let row_before_dot = e.cursor().0;
9753 run_keys(&mut e, ".");
9754 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9755 }
9756
9757 fn si_editor(content: &str) -> Editor {
9763 let opts = crate::types::Options {
9764 shiftwidth: 4,
9765 softtabstop: 4,
9766 expandtab: true,
9767 smartindent: true,
9768 autoindent: true,
9769 ..crate::types::Options::default()
9770 };
9771 let mut e = Editor::new(
9772 hjkl_buffer::Buffer::new(),
9773 crate::types::DefaultHost::new(),
9774 opts,
9775 );
9776 e.set_content(content);
9777 e
9778 }
9779
9780 #[test]
9781 fn smartindent_bumps_indent_after_open_brace() {
9782 let mut e = si_editor("fn foo() {");
9784 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9786 assert_eq!(
9787 e.buffer().lines()[1],
9788 " ",
9789 "smartindent should bump one shiftwidth after {{"
9790 );
9791 }
9792
9793 #[test]
9794 fn smartindent_no_bump_when_off() {
9795 let mut e = si_editor("fn foo() {");
9798 e.settings_mut().smartindent = false;
9799 e.jump_cursor(0, 10);
9800 run_keys(&mut e, "i<CR>");
9801 assert_eq!(
9802 e.buffer().lines()[1],
9803 "",
9804 "without smartindent, no bump: new line copies empty leading ws"
9805 );
9806 }
9807
9808 #[test]
9809 fn smartindent_uses_tab_when_noexpandtab() {
9810 let opts = crate::types::Options {
9812 shiftwidth: 4,
9813 softtabstop: 0,
9814 expandtab: false,
9815 smartindent: true,
9816 autoindent: true,
9817 ..crate::types::Options::default()
9818 };
9819 let mut e = Editor::new(
9820 hjkl_buffer::Buffer::new(),
9821 crate::types::DefaultHost::new(),
9822 opts,
9823 );
9824 e.set_content("fn foo() {");
9825 e.jump_cursor(0, 10);
9826 run_keys(&mut e, "i<CR>");
9827 assert_eq!(
9828 e.buffer().lines()[1],
9829 "\t",
9830 "noexpandtab: smartindent bump inserts a literal tab"
9831 );
9832 }
9833
9834 #[test]
9835 fn smartindent_dedent_on_close_brace() {
9836 let mut e = si_editor("fn foo() {");
9839 e.set_content("fn foo() {\n ");
9841 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9843 assert_eq!(
9844 e.buffer().lines()[1],
9845 "}",
9846 "close brace on whitespace-only line should dedent"
9847 );
9848 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9849 }
9850
9851 #[test]
9852 fn smartindent_no_dedent_when_off() {
9853 let mut e = si_editor("fn foo() {\n ");
9855 e.settings_mut().smartindent = false;
9856 e.jump_cursor(1, 4);
9857 run_keys(&mut e, "i}");
9858 assert_eq!(
9859 e.buffer().lines()[1],
9860 " }",
9861 "without smartindent, `}}` just appends at cursor"
9862 );
9863 }
9864
9865 #[test]
9866 fn smartindent_no_dedent_mid_line() {
9867 let mut e = si_editor(" let x = 1");
9870 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9872 assert_eq!(
9873 e.buffer().lines()[0],
9874 " let x = 1}",
9875 "mid-line `}}` should not dedent"
9876 );
9877 }
9878
9879 #[test]
9883 fn count_5x_fills_unnamed_register() {
9884 let mut e = editor_with("hello world\n");
9885 e.jump_cursor(0, 0);
9886 run_keys(&mut e, "5x");
9887 assert_eq!(e.buffer().lines()[0], " world");
9888 assert_eq!(e.cursor(), (0, 0));
9889 assert_eq!(e.yank(), "hello");
9890 }
9891
9892 #[test]
9893 fn x_fills_unnamed_register_single_char() {
9894 let mut e = editor_with("abc\n");
9895 e.jump_cursor(0, 0);
9896 run_keys(&mut e, "x");
9897 assert_eq!(e.buffer().lines()[0], "bc");
9898 assert_eq!(e.yank(), "a");
9899 }
9900
9901 #[test]
9902 fn big_x_fills_unnamed_register() {
9903 let mut e = editor_with("hello\n");
9904 e.jump_cursor(0, 3);
9905 run_keys(&mut e, "X");
9906 assert_eq!(e.buffer().lines()[0], "helo");
9907 assert_eq!(e.yank(), "l");
9908 }
9909
9910 #[test]
9912 fn g_motion_trailing_newline_lands_on_last_content_row() {
9913 let mut e = editor_with("foo\nbar\nbaz\n");
9914 e.jump_cursor(0, 0);
9915 run_keys(&mut e, "G");
9916 assert_eq!(
9918 e.cursor().0,
9919 2,
9920 "G should land on row 2 (baz), not row 3 (phantom empty)"
9921 );
9922 }
9923
9924 #[test]
9926 fn dd_last_line_clamps_cursor_to_new_last_row() {
9927 let mut e = editor_with("foo\nbar\n");
9928 e.jump_cursor(1, 0);
9929 run_keys(&mut e, "dd");
9930 assert_eq!(e.buffer().lines()[0], "foo");
9931 assert_eq!(
9932 e.cursor(),
9933 (0, 0),
9934 "cursor should clamp to row 0 after dd on last content line"
9935 );
9936 }
9937
9938 #[test]
9940 fn d_dollar_cursor_on_last_char() {
9941 let mut e = editor_with("hello world\n");
9942 e.jump_cursor(0, 5);
9943 run_keys(&mut e, "d$");
9944 assert_eq!(e.buffer().lines()[0], "hello");
9945 assert_eq!(
9946 e.cursor(),
9947 (0, 4),
9948 "d$ should leave cursor on col 4, not col 5"
9949 );
9950 }
9951
9952 #[test]
9954 fn undo_insert_clamps_cursor_to_last_valid_col() {
9955 let mut e = editor_with("hello\n");
9956 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
9958 assert_eq!(e.buffer().lines()[0], "hello");
9959 assert_eq!(
9960 e.cursor(),
9961 (0, 4),
9962 "undo should clamp cursor to col 4 on 'hello'"
9963 );
9964 }
9965
9966 #[test]
9968 fn da_doublequote_eats_trailing_whitespace() {
9969 let mut e = editor_with("say \"hello\" there\n");
9970 e.jump_cursor(0, 6);
9971 run_keys(&mut e, "da\"");
9972 assert_eq!(e.buffer().lines()[0], "say there");
9973 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
9974 }
9975
9976 #[test]
9978 fn dab_cursor_col_clamped_after_delete() {
9979 let mut e = editor_with("fn x() {\n body\n}\n");
9980 e.jump_cursor(1, 4);
9981 run_keys(&mut e, "daB");
9982 assert_eq!(e.buffer().lines()[0], "fn x() ");
9983 assert_eq!(
9984 e.cursor(),
9985 (0, 6),
9986 "daB should leave cursor at col 6, not 7"
9987 );
9988 }
9989
9990 #[test]
9992 fn dib_preserves_surrounding_newlines() {
9993 let mut e = editor_with("{\n body\n}\n");
9994 e.jump_cursor(1, 4);
9995 run_keys(&mut e, "diB");
9996 assert_eq!(e.buffer().lines()[0], "{");
9997 assert_eq!(e.buffer().lines()[1], "}");
9998 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
9999 }
10000
10001 #[test]
10002 fn is_chord_pending_tracks_replace_state() {
10003 let mut e = editor_with("abc\n");
10004 assert!(!e.is_chord_pending());
10005 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10007 assert!(e.is_chord_pending(), "engine should be pending after r");
10008 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10010 assert!(
10011 !e.is_chord_pending(),
10012 "engine pending should clear after replace"
10013 );
10014 }
10015
10016 #[test]
10019 fn yiw_sets_lbr_rbr_marks_around_word() {
10020 let mut e = editor_with("hello world");
10023 run_keys(&mut e, "yiw");
10024 let lo = e.mark('[').expect("'[' must be set after yiw");
10025 let hi = e.mark(']').expect("']' must be set after yiw");
10026 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10027 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10028 }
10029
10030 #[test]
10031 fn yj_linewise_sets_marks_at_line_edges() {
10032 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10035 run_keys(&mut e, "yj");
10036 let lo = e.mark('[').expect("'[' must be set after yj");
10037 let hi = e.mark(']').expect("']' must be set after yj");
10038 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10039 assert_eq!(
10040 hi,
10041 (1, 4),
10042 "'] snaps to (bot_row, last_col) for linewise yank"
10043 );
10044 }
10045
10046 #[test]
10047 fn dd_sets_lbr_rbr_marks_to_cursor() {
10048 let mut e = editor_with("aaa\nbbb");
10051 run_keys(&mut e, "dd");
10052 let lo = e.mark('[').expect("'[' must be set after dd");
10053 let hi = e.mark(']').expect("']' must be set after dd");
10054 assert_eq!(lo, hi, "after delete both marks are at the same position");
10055 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10056 }
10057
10058 #[test]
10059 fn dw_sets_lbr_rbr_marks_to_cursor() {
10060 let mut e = editor_with("hello world");
10063 run_keys(&mut e, "dw");
10064 let lo = e.mark('[').expect("'[' must be set after dw");
10065 let hi = e.mark(']').expect("']' must be set after dw");
10066 assert_eq!(lo, hi, "after delete both marks are at the same position");
10067 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10068 }
10069
10070 #[test]
10071 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10072 let mut e = editor_with("hello world");
10077 run_keys(&mut e, "cwfoo<Esc>");
10078 let lo = e.mark('[').expect("'[' must be set after cw");
10079 let hi = e.mark(']').expect("']' must be set after cw");
10080 assert_eq!(lo, (0, 0), "'[ should be start of change");
10081 assert_eq!(hi.0, 0, "'] should be on row 0");
10084 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10085 }
10086
10087 #[test]
10088 fn cw_with_no_insertion_sets_marks_at_change_start() {
10089 let mut e = editor_with("hello world");
10092 run_keys(&mut e, "cw<Esc>");
10093 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10094 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10095 assert_eq!(lo.0, 0, "'[ should be on row 0");
10096 assert_eq!(hi.0, 0, "'] should be on row 0");
10097 assert_eq!(lo, hi, "marks coincide when insert is empty");
10099 }
10100
10101 #[test]
10102 fn p_charwise_sets_marks_around_pasted_text() {
10103 let mut e = editor_with("abc xyz");
10106 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10109 let hi = e.mark(']').expect("']' set after charwise paste");
10110 assert!(lo <= hi, "'[ must not exceed ']'");
10111 assert_eq!(
10113 hi.1.wrapping_sub(lo.1),
10114 2,
10115 "'] - '[ should span 2 cols for a 3-char paste"
10116 );
10117 }
10118
10119 #[test]
10120 fn p_linewise_sets_marks_at_line_edges() {
10121 let mut e = editor_with("aaa\nbbb\nccc");
10124 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10128 let hi = e.mark(']').expect("']' set after linewise paste");
10129 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10130 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10131 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10132 }
10133
10134 #[test]
10135 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10136 let mut e = editor_with("hello world");
10140 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10144 assert_eq!(
10146 e.cursor(),
10147 (0, 4),
10148 "visual `[v`] should land on last yanked char"
10149 );
10150 assert_eq!(
10152 e.vim_mode(),
10153 crate::VimMode::Visual,
10154 "should be in Visual mode"
10155 );
10156 }
10157}