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) search_history: Vec<String>,
433 pub(super) search_history_cursor: Option<usize>,
438 pub(super) last_input_at: Option<std::time::Instant>,
447 pub(super) last_input_host_at: Option<core::time::Duration>,
451}
452
453const SEARCH_HISTORY_MAX: usize = 100;
454pub(crate) const CHANGE_LIST_MAX: usize = 100;
455
456#[derive(Debug, Clone)]
459pub struct SearchPrompt {
460 pub text: String,
461 pub cursor: usize,
462 pub forward: bool,
463}
464
465#[derive(Debug, Clone)]
466struct InsertSession {
467 count: usize,
468 row_min: usize,
470 row_max: usize,
471 before_lines: Vec<String>,
475 reason: InsertReason,
476}
477
478#[derive(Debug, Clone)]
479enum InsertReason {
480 Enter(InsertEntry),
482 Open { above: bool },
484 AfterChange,
487 DeleteToEol,
489 ReplayOnly,
492 BlockEdge { top: usize, bot: usize, col: usize },
496 Replace,
500}
501
502#[derive(Debug, Clone, Copy)]
512pub(super) struct LastVisual {
513 pub mode: Mode,
514 pub anchor: (usize, usize),
515 pub cursor: (usize, usize),
516 pub block_vcol: usize,
517}
518
519impl VimState {
520 pub fn public_mode(&self) -> VimMode {
521 match self.mode {
522 Mode::Normal => VimMode::Normal,
523 Mode::Insert => VimMode::Insert,
524 Mode::Visual => VimMode::Visual,
525 Mode::VisualLine => VimMode::VisualLine,
526 Mode::VisualBlock => VimMode::VisualBlock,
527 }
528 }
529
530 pub fn force_normal(&mut self) {
531 self.mode = Mode::Normal;
532 self.pending = Pending::None;
533 self.count = 0;
534 self.insert_session = None;
535 }
536
537 pub(crate) fn clear_pending_prefix(&mut self) {
547 self.pending = Pending::None;
548 self.count = 0;
549 self.pending_register = None;
550 self.insert_pending_register = false;
551 }
552
553 pub fn is_visual(&self) -> bool {
554 matches!(
555 self.mode,
556 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
557 )
558 }
559
560 pub fn is_visual_char(&self) -> bool {
561 self.mode == Mode::Visual
562 }
563
564 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
565 self.visual_anchor = anchor;
566 self.mode = Mode::Visual;
567 }
568
569 pub(crate) fn pending_count_val(&self) -> Option<u32> {
572 if self.count == 0 {
573 None
574 } else {
575 Some(self.count as u32)
576 }
577 }
578
579 pub(crate) fn is_chord_pending(&self) -> bool {
582 !matches!(self.pending, Pending::None)
583 }
584
585 pub(crate) fn pending_op_char(&self) -> Option<char> {
589 let op = match &self.pending {
590 Pending::Op { op, .. }
591 | Pending::OpTextObj { op, .. }
592 | Pending::OpG { op, .. }
593 | Pending::OpFind { op, .. } => Some(*op),
594 _ => None,
595 };
596 op.map(|o| match o {
597 Operator::Delete => 'd',
598 Operator::Change => 'c',
599 Operator::Yank => 'y',
600 Operator::Uppercase => 'U',
601 Operator::Lowercase => 'u',
602 Operator::ToggleCase => '~',
603 Operator::Indent => '>',
604 Operator::Outdent => '<',
605 Operator::Fold => 'z',
606 Operator::Reflow => 'q',
607 })
608 }
609}
610
611fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
617 ed.vim.search_prompt = Some(SearchPrompt {
618 text: String::new(),
619 cursor: 0,
620 forward,
621 });
622 ed.vim.search_history_cursor = None;
623 ed.set_search_pattern(None);
627}
628
629fn push_search_pattern<H: crate::types::Host>(
634 ed: &mut Editor<hjkl_buffer::Buffer, H>,
635 pattern: &str,
636) {
637 let compiled = if pattern.is_empty() {
638 None
639 } else {
640 let case_insensitive = ed.settings().ignore_case
647 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
648 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
649 std::borrow::Cow::Owned(format!("(?i){pattern}"))
650 } else {
651 std::borrow::Cow::Borrowed(pattern)
652 };
653 regex::Regex::new(&effective).ok()
654 };
655 let wrap = ed.settings().wrapscan;
656 ed.set_search_pattern(compiled);
660 ed.search_state_mut().wrap_around = wrap;
661}
662
663fn step_search_prompt<H: crate::types::Host>(
664 ed: &mut Editor<hjkl_buffer::Buffer, H>,
665 input: Input,
666) -> bool {
667 let history_dir = match (input.key, input.ctrl) {
671 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
672 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
673 _ => None,
674 };
675 if let Some(dir) = history_dir {
676 walk_search_history(ed, dir);
677 return true;
678 }
679 match input.key {
680 Key::Esc => {
681 let text = ed
684 .vim
685 .search_prompt
686 .take()
687 .map(|p| p.text)
688 .unwrap_or_default();
689 if !text.is_empty() {
690 ed.vim.last_search = Some(text);
691 }
692 ed.vim.search_history_cursor = None;
693 }
694 Key::Enter => {
695 let prompt = ed.vim.search_prompt.take();
696 if let Some(p) = prompt {
697 let pattern = if p.text.is_empty() {
700 ed.vim.last_search.clone()
701 } else {
702 Some(p.text.clone())
703 };
704 if let Some(pattern) = pattern {
705 push_search_pattern(ed, &pattern);
706 let pre = ed.cursor();
707 if p.forward {
708 ed.search_advance_forward(true);
709 } else {
710 ed.search_advance_backward(true);
711 }
712 ed.push_buffer_cursor_to_textarea();
713 if ed.cursor() != pre {
714 push_jump(ed, pre);
715 }
716 record_search_history(ed, &pattern);
717 ed.vim.last_search = Some(pattern);
718 ed.vim.last_search_forward = p.forward;
719 }
720 }
721 ed.vim.search_history_cursor = None;
722 }
723 Key::Backspace => {
724 ed.vim.search_history_cursor = None;
725 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
726 if p.text.pop().is_some() {
727 p.cursor = p.text.chars().count();
728 Some(p.text.clone())
729 } else {
730 None
731 }
732 });
733 if let Some(text) = new_text {
734 push_search_pattern(ed, &text);
735 }
736 }
737 Key::Char(c) => {
738 ed.vim.search_history_cursor = None;
739 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
740 p.text.push(c);
741 p.cursor = p.text.chars().count();
742 p.text.clone()
743 });
744 if let Some(text) = new_text {
745 push_search_pattern(ed, &text);
746 }
747 }
748 _ => {}
749 }
750 true
751}
752
753fn walk_change_list<H: crate::types::Host>(
757 ed: &mut Editor<hjkl_buffer::Buffer, H>,
758 dir: isize,
759 count: usize,
760) {
761 if ed.vim.change_list.is_empty() {
762 return;
763 }
764 let len = ed.vim.change_list.len();
765 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
766 (None, -1) => len as isize - 1,
767 (None, 1) => return, (Some(i), -1) => i as isize - 1,
769 (Some(i), 1) => i as isize + 1,
770 _ => return,
771 };
772 for _ in 1..count {
773 let next = idx + dir;
774 if next < 0 || next >= len as isize {
775 break;
776 }
777 idx = next;
778 }
779 if idx < 0 || idx >= len as isize {
780 return;
781 }
782 let idx = idx as usize;
783 ed.vim.change_list_cursor = Some(idx);
784 let (row, col) = ed.vim.change_list[idx];
785 ed.jump_cursor(row, col);
786}
787
788fn record_search_history<H: crate::types::Host>(
792 ed: &mut Editor<hjkl_buffer::Buffer, H>,
793 pattern: &str,
794) {
795 if pattern.is_empty() {
796 return;
797 }
798 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
799 return;
800 }
801 ed.vim.search_history.push(pattern.to_string());
802 let len = ed.vim.search_history.len();
803 if len > SEARCH_HISTORY_MAX {
804 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
805 }
806}
807
808fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
814 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
815 return;
816 }
817 let len = ed.vim.search_history.len();
818 let next_idx = match (ed.vim.search_history_cursor, dir) {
819 (None, -1) => Some(len - 1),
820 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
822 (Some(i), 1) if i + 1 < len => Some(i + 1),
823 _ => None,
824 };
825 let Some(idx) = next_idx else {
826 return;
827 };
828 ed.vim.search_history_cursor = Some(idx);
829 let text = ed.vim.search_history[idx].clone();
830 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
831 prompt.cursor = text.chars().count();
832 prompt.text = text.clone();
833 }
834 push_search_pattern(ed, &text);
835}
836
837pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
838 ed.sync_buffer_content_from_textarea();
843 let now = std::time::Instant::now();
851 let host_now = ed.host.now();
852 let timed_out = match ed.vim.last_input_host_at {
853 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
854 None => false,
855 };
856 if timed_out {
857 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
858 || ed.vim.count != 0
859 || ed.vim.pending_register.is_some()
860 || ed.vim.insert_pending_register;
861 if chord_in_flight {
862 ed.vim.clear_pending_prefix();
863 }
864 }
865 ed.vim.last_input_at = Some(now);
866 ed.vim.last_input_host_at = Some(host_now);
867 if ed.vim.recording_macro.is_some()
872 && !ed.vim.replaying_macro
873 && matches!(ed.vim.pending, Pending::None)
874 && ed.vim.mode != Mode::Insert
875 && input.key == Key::Char('q')
876 && !input.ctrl
877 && !input.alt
878 {
879 let reg = ed.vim.recording_macro.take().unwrap();
880 let keys = std::mem::take(&mut ed.vim.recording_keys);
881 let text = crate::input::encode_macro(&keys);
882 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
883 return true;
884 }
885 if ed.vim.search_prompt.is_some() {
887 return step_search_prompt(ed, input);
888 }
889 let pending_was_macro_chord = matches!(
893 ed.vim.pending,
894 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
895 );
896 let was_insert = ed.vim.mode == Mode::Insert;
897 let pre_visual_snapshot = match ed.vim.mode {
900 Mode::Visual => Some(LastVisual {
901 mode: Mode::Visual,
902 anchor: ed.vim.visual_anchor,
903 cursor: ed.cursor(),
904 block_vcol: 0,
905 }),
906 Mode::VisualLine => Some(LastVisual {
907 mode: Mode::VisualLine,
908 anchor: (ed.vim.visual_line_anchor, 0),
909 cursor: ed.cursor(),
910 block_vcol: 0,
911 }),
912 Mode::VisualBlock => Some(LastVisual {
913 mode: Mode::VisualBlock,
914 anchor: ed.vim.block_anchor,
915 cursor: ed.cursor(),
916 block_vcol: ed.vim.block_vcol,
917 }),
918 _ => None,
919 };
920 let consumed = match ed.vim.mode {
921 Mode::Insert => step_insert(ed, input),
922 _ => step_normal(ed, input),
923 };
924 if let Some(snap) = pre_visual_snapshot
925 && !matches!(
926 ed.vim.mode,
927 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
928 )
929 {
930 let (lo, hi) = match snap.mode {
946 Mode::Visual => {
947 if snap.anchor <= snap.cursor {
948 (snap.anchor, snap.cursor)
949 } else {
950 (snap.cursor, snap.anchor)
951 }
952 }
953 Mode::VisualLine => {
954 let r_lo = snap.anchor.0.min(snap.cursor.0);
955 let r_hi = snap.anchor.0.max(snap.cursor.0);
956 let last_col = ed
957 .buffer()
958 .lines()
959 .get(r_hi)
960 .map(|l| l.chars().count().saturating_sub(1))
961 .unwrap_or(0);
962 ((r_lo, 0), (r_hi, last_col))
963 }
964 Mode::VisualBlock => {
965 let (r1, c1) = snap.anchor;
966 let (r2, c2) = snap.cursor;
967 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
968 }
969 _ => {
970 if snap.anchor <= snap.cursor {
973 (snap.anchor, snap.cursor)
974 } else {
975 (snap.cursor, snap.anchor)
976 }
977 }
978 };
979 ed.set_mark('<', lo);
980 ed.set_mark('>', hi);
981 ed.vim.last_visual = Some(snap);
982 }
983 if !was_insert
987 && ed.vim.one_shot_normal
988 && ed.vim.mode == Mode::Normal
989 && matches!(ed.vim.pending, Pending::None)
990 {
991 ed.vim.one_shot_normal = false;
992 ed.vim.mode = Mode::Insert;
993 }
994 ed.sync_buffer_content_from_textarea();
1000 if !ed.vim.viewport_pinned {
1004 ed.ensure_cursor_in_scrolloff();
1005 }
1006 ed.vim.viewport_pinned = false;
1007 if ed.vim.recording_macro.is_some()
1012 && !ed.vim.replaying_macro
1013 && input.key != Key::Char('q')
1014 && !pending_was_macro_chord
1015 {
1016 ed.vim.recording_keys.push(input);
1017 }
1018 consumed
1019}
1020
1021fn step_insert<H: crate::types::Host>(
1024 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1025 input: Input,
1026) -> bool {
1027 if ed.vim.insert_pending_register {
1031 ed.vim.insert_pending_register = false;
1032 if let Key::Char(c) = input.key
1033 && !input.ctrl
1034 {
1035 insert_register_text(ed, c);
1036 }
1037 return true;
1038 }
1039
1040 if input.key == Key::Esc {
1041 finish_insert_session(ed);
1042 ed.vim.mode = Mode::Normal;
1043 let col = ed.cursor().1;
1048 if col > 0 {
1049 crate::motions::move_left(&mut ed.buffer, 1);
1050 ed.push_buffer_cursor_to_textarea();
1051 }
1052 ed.sticky_col = Some(ed.cursor().1);
1053 return true;
1054 }
1055
1056 if input.ctrl {
1058 match input.key {
1059 Key::Char('w') => {
1060 use hjkl_buffer::{Edit, MotionKind};
1061 ed.sync_buffer_content_from_textarea();
1062 let cursor = buf_cursor_pos(&ed.buffer);
1063 if cursor.row == 0 && cursor.col == 0 {
1064 return true;
1065 }
1066 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1069 let word_start = buf_cursor_pos(&ed.buffer);
1070 if word_start == cursor {
1071 return true;
1072 }
1073 buf_set_cursor_pos(&mut ed.buffer, cursor);
1074 ed.mutate_edit(Edit::DeleteRange {
1075 start: word_start,
1076 end: cursor,
1077 kind: MotionKind::Char,
1078 });
1079 ed.push_buffer_cursor_to_textarea();
1080 return true;
1081 }
1082 Key::Char('u') => {
1083 use hjkl_buffer::{Edit, MotionKind, Position};
1084 ed.sync_buffer_content_from_textarea();
1085 let cursor = buf_cursor_pos(&ed.buffer);
1086 if cursor.col > 0 {
1087 ed.mutate_edit(Edit::DeleteRange {
1088 start: Position::new(cursor.row, 0),
1089 end: cursor,
1090 kind: MotionKind::Char,
1091 });
1092 ed.push_buffer_cursor_to_textarea();
1093 }
1094 return true;
1095 }
1096 Key::Char('h') => {
1097 use hjkl_buffer::{Edit, MotionKind, Position};
1098 ed.sync_buffer_content_from_textarea();
1099 let cursor = buf_cursor_pos(&ed.buffer);
1100 if cursor.col > 0 {
1101 ed.mutate_edit(Edit::DeleteRange {
1102 start: Position::new(cursor.row, cursor.col - 1),
1103 end: cursor,
1104 kind: MotionKind::Char,
1105 });
1106 } else if cursor.row > 0 {
1107 let prev_row = cursor.row - 1;
1108 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1109 ed.mutate_edit(Edit::JoinLines {
1110 row: prev_row,
1111 count: 1,
1112 with_space: false,
1113 });
1114 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1115 }
1116 ed.push_buffer_cursor_to_textarea();
1117 return true;
1118 }
1119 Key::Char('o') => {
1120 ed.vim.one_shot_normal = true;
1123 ed.vim.mode = Mode::Normal;
1124 return true;
1125 }
1126 Key::Char('r') => {
1127 ed.vim.insert_pending_register = true;
1130 return true;
1131 }
1132 Key::Char('t') => {
1133 let (row, col) = ed.cursor();
1138 let sw = ed.settings().shiftwidth;
1139 indent_rows(ed, row, row, 1);
1140 ed.jump_cursor(row, col + sw);
1141 return true;
1142 }
1143 Key::Char('d') => {
1144 let (row, col) = ed.cursor();
1148 let before_len = buf_line_bytes(&ed.buffer, row);
1149 outdent_rows(ed, row, row, 1);
1150 let after_len = buf_line_bytes(&ed.buffer, row);
1151 let stripped = before_len.saturating_sub(after_len);
1152 let new_col = col.saturating_sub(stripped);
1153 ed.jump_cursor(row, new_col);
1154 return true;
1155 }
1156 _ => {}
1157 }
1158 }
1159
1160 let (row, _) = ed.cursor();
1163 if let Some(ref mut session) = ed.vim.insert_session {
1164 session.row_min = session.row_min.min(row);
1165 session.row_max = session.row_max.max(row);
1166 }
1167 let mutated = handle_insert_key(ed, input);
1168 if mutated {
1169 ed.mark_content_dirty();
1170 let (row, _) = ed.cursor();
1171 if let Some(ref mut session) = ed.vim.insert_session {
1172 session.row_min = session.row_min.min(row);
1173 session.row_max = session.row_max.max(row);
1174 }
1175 }
1176 true
1177}
1178
1179fn insert_register_text<H: crate::types::Host>(
1184 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1185 selector: char,
1186) {
1187 use hjkl_buffer::Edit;
1188 let text = match ed.registers().read(selector) {
1189 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1190 _ => return,
1191 };
1192 ed.sync_buffer_content_from_textarea();
1193 let cursor = buf_cursor_pos(&ed.buffer);
1194 ed.mutate_edit(Edit::InsertStr {
1195 at: cursor,
1196 text: text.clone(),
1197 });
1198 let mut row = cursor.row;
1201 let mut col = cursor.col;
1202 for ch in text.chars() {
1203 if ch == '\n' {
1204 row += 1;
1205 col = 0;
1206 } else {
1207 col += 1;
1208 }
1209 }
1210 buf_set_cursor_rc(&mut ed.buffer, row, col);
1211 ed.push_buffer_cursor_to_textarea();
1212 ed.mark_content_dirty();
1213 if let Some(ref mut session) = ed.vim.insert_session {
1214 session.row_min = session.row_min.min(row);
1215 session.row_max = session.row_max.max(row);
1216 }
1217}
1218
1219pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1238 if !settings.autoindent {
1239 return String::new();
1240 }
1241 let base: String = prev_line
1243 .chars()
1244 .take_while(|c| *c == ' ' || *c == '\t')
1245 .collect();
1246
1247 if settings.smartindent {
1248 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1252 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1253 let unit = if settings.expandtab {
1254 if settings.softtabstop > 0 {
1255 " ".repeat(settings.softtabstop)
1256 } else {
1257 " ".repeat(settings.shiftwidth)
1258 }
1259 } else {
1260 "\t".to_string()
1261 };
1262 return format!("{base}{unit}");
1263 }
1264 }
1265
1266 base
1267}
1268
1269fn try_dedent_close_bracket<H: crate::types::Host>(
1279 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1280 cursor: hjkl_buffer::Position,
1281 ch: char,
1282) -> bool {
1283 use hjkl_buffer::{Edit, MotionKind, Position};
1284
1285 if !ed.settings.smartindent {
1286 return false;
1287 }
1288 if !matches!(ch, '}' | ')' | ']') {
1289 return false;
1290 }
1291
1292 let line = match buf_line(&ed.buffer, cursor.row) {
1293 Some(l) => l.to_string(),
1294 None => return false,
1295 };
1296
1297 let before: String = line.chars().take(cursor.col).collect();
1299 if !before.chars().all(|c| c == ' ' || c == '\t') {
1300 return false;
1301 }
1302 if before.is_empty() {
1303 return false;
1305 }
1306
1307 let unit_len: usize = if ed.settings.expandtab {
1309 if ed.settings.softtabstop > 0 {
1310 ed.settings.softtabstop
1311 } else {
1312 ed.settings.shiftwidth
1313 }
1314 } else {
1315 1
1317 };
1318
1319 let strip_len = if ed.settings.expandtab {
1321 let spaces = before.chars().filter(|c| *c == ' ').count();
1323 if spaces < unit_len {
1324 return false;
1325 }
1326 unit_len
1327 } else {
1328 if !before.starts_with('\t') {
1330 return false;
1331 }
1332 1
1333 };
1334
1335 ed.mutate_edit(Edit::DeleteRange {
1337 start: Position::new(cursor.row, 0),
1338 end: Position::new(cursor.row, strip_len),
1339 kind: MotionKind::Char,
1340 });
1341 let new_col = cursor.col.saturating_sub(strip_len);
1346 ed.mutate_edit(Edit::InsertChar {
1347 at: Position::new(cursor.row, new_col),
1348 ch,
1349 });
1350 true
1351}
1352
1353fn handle_insert_key<H: crate::types::Host>(
1360 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1361 input: Input,
1362) -> bool {
1363 use hjkl_buffer::{Edit, MotionKind, Position};
1364 ed.sync_buffer_content_from_textarea();
1365 let cursor = buf_cursor_pos(&ed.buffer);
1366 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1367 let in_replace = matches!(
1371 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1372 Some(InsertReason::Replace)
1373 );
1374 let mutated = match input.key {
1375 Key::Char(c) if in_replace && cursor.col < line_chars => {
1376 ed.mutate_edit(Edit::DeleteRange {
1377 start: cursor,
1378 end: Position::new(cursor.row, cursor.col + 1),
1379 kind: MotionKind::Char,
1380 });
1381 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1382 true
1383 }
1384 Key::Char(c) => {
1385 if !try_dedent_close_bracket(ed, cursor, c) {
1386 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1387 }
1388 true
1389 }
1390 Key::Enter => {
1391 let prev_line = buf_line(&ed.buffer, cursor.row)
1392 .unwrap_or_default()
1393 .to_string();
1394 let indent = compute_enter_indent(&ed.settings, &prev_line);
1395 let text = format!("\n{indent}");
1396 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1397 true
1398 }
1399 Key::Tab => {
1400 if ed.settings.expandtab {
1401 let sts = ed.settings.softtabstop;
1404 let n = if sts > 0 {
1405 sts - (cursor.col % sts)
1406 } else {
1407 ed.settings.tabstop.max(1)
1408 };
1409 ed.mutate_edit(Edit::InsertStr {
1410 at: cursor,
1411 text: " ".repeat(n),
1412 });
1413 } else {
1414 ed.mutate_edit(Edit::InsertChar {
1415 at: cursor,
1416 ch: '\t',
1417 });
1418 }
1419 true
1420 }
1421 Key::Backspace => {
1422 let sts = ed.settings.softtabstop;
1426 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1427 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1428 let chars: Vec<char> = line.chars().collect();
1429 let run_start = cursor.col - sts;
1430 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1431 ed.mutate_edit(Edit::DeleteRange {
1432 start: Position::new(cursor.row, run_start),
1433 end: cursor,
1434 kind: MotionKind::Char,
1435 });
1436 return true;
1437 }
1438 }
1439 if cursor.col > 0 {
1440 ed.mutate_edit(Edit::DeleteRange {
1441 start: Position::new(cursor.row, cursor.col - 1),
1442 end: cursor,
1443 kind: MotionKind::Char,
1444 });
1445 true
1446 } else if cursor.row > 0 {
1447 let prev_row = cursor.row - 1;
1448 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1449 ed.mutate_edit(Edit::JoinLines {
1450 row: prev_row,
1451 count: 1,
1452 with_space: false,
1453 });
1454 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1455 true
1456 } else {
1457 false
1458 }
1459 }
1460 Key::Delete => {
1461 if cursor.col < line_chars {
1462 ed.mutate_edit(Edit::DeleteRange {
1463 start: cursor,
1464 end: Position::new(cursor.row, cursor.col + 1),
1465 kind: MotionKind::Char,
1466 });
1467 true
1468 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1469 ed.mutate_edit(Edit::JoinLines {
1470 row: cursor.row,
1471 count: 1,
1472 with_space: false,
1473 });
1474 buf_set_cursor_pos(&mut ed.buffer, cursor);
1475 true
1476 } else {
1477 false
1478 }
1479 }
1480 Key::Left => {
1481 crate::motions::move_left(&mut ed.buffer, 1);
1482 break_undo_group_in_insert(ed);
1483 false
1484 }
1485 Key::Right => {
1486 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1489 break_undo_group_in_insert(ed);
1490 false
1491 }
1492 Key::Up => {
1493 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1494 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1495 break_undo_group_in_insert(ed);
1496 false
1497 }
1498 Key::Down => {
1499 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1500 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1501 break_undo_group_in_insert(ed);
1502 false
1503 }
1504 Key::Home => {
1505 crate::motions::move_line_start(&mut ed.buffer);
1506 break_undo_group_in_insert(ed);
1507 false
1508 }
1509 Key::End => {
1510 crate::motions::move_line_end(&mut ed.buffer);
1511 break_undo_group_in_insert(ed);
1512 false
1513 }
1514 Key::PageUp => {
1515 let rows = viewport_full_rows(ed, 1) as isize;
1519 scroll_cursor_rows(ed, -rows);
1520 return false;
1521 }
1522 Key::PageDown => {
1523 let rows = viewport_full_rows(ed, 1) as isize;
1524 scroll_cursor_rows(ed, rows);
1525 return false;
1526 }
1527 _ => false,
1530 };
1531 ed.push_buffer_cursor_to_textarea();
1532 mutated
1533}
1534
1535fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1536 let Some(session) = ed.vim.insert_session.take() else {
1537 return;
1538 };
1539 let lines = buf_lines_to_vec(&ed.buffer);
1540 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1544 let before_end = session
1545 .row_max
1546 .min(session.before_lines.len().saturating_sub(1));
1547 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1548 session.before_lines[session.row_min..=before_end].join("\n")
1549 } else {
1550 String::new()
1551 };
1552 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1553 lines[session.row_min..=after_end].join("\n")
1554 } else {
1555 String::new()
1556 };
1557 let inserted = extract_inserted(&before, &after);
1558 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1559 use hjkl_buffer::{Edit, Position};
1560 for _ in 0..session.count - 1 {
1561 let (row, col) = ed.cursor();
1562 ed.mutate_edit(Edit::InsertStr {
1563 at: Position::new(row, col),
1564 text: inserted.clone(),
1565 });
1566 }
1567 }
1568 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1569 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1570 use hjkl_buffer::{Edit, Position};
1571 for r in (top + 1)..=bot {
1572 let line_len = buf_line_chars(&ed.buffer, r);
1573 if col > line_len {
1574 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1577 ed.mutate_edit(Edit::InsertStr {
1578 at: Position::new(r, line_len),
1579 text: pad,
1580 });
1581 }
1582 ed.mutate_edit(Edit::InsertStr {
1583 at: Position::new(r, col),
1584 text: inserted.clone(),
1585 });
1586 }
1587 buf_set_cursor_rc(&mut ed.buffer, top, col);
1588 ed.push_buffer_cursor_to_textarea();
1589 }
1590 return;
1591 }
1592 if ed.vim.replaying {
1593 return;
1594 }
1595 match session.reason {
1596 InsertReason::Enter(entry) => {
1597 ed.vim.last_change = Some(LastChange::InsertAt {
1598 entry,
1599 inserted,
1600 count: session.count,
1601 });
1602 }
1603 InsertReason::Open { above } => {
1604 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1605 }
1606 InsertReason::AfterChange => {
1607 if let Some(
1608 LastChange::OpMotion { inserted: ins, .. }
1609 | LastChange::OpTextObj { inserted: ins, .. }
1610 | LastChange::LineOp { inserted: ins, .. },
1611 ) = ed.vim.last_change.as_mut()
1612 {
1613 *ins = Some(inserted);
1614 }
1615 }
1616 InsertReason::DeleteToEol => {
1617 ed.vim.last_change = Some(LastChange::DeleteToEol {
1618 inserted: Some(inserted),
1619 });
1620 }
1621 InsertReason::ReplayOnly => {}
1622 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1623 InsertReason::Replace => {
1624 ed.vim.last_change = Some(LastChange::DeleteToEol {
1629 inserted: Some(inserted),
1630 });
1631 }
1632 }
1633}
1634
1635fn begin_insert<H: crate::types::Host>(
1636 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1637 count: usize,
1638 reason: InsertReason,
1639) {
1640 let record = !matches!(reason, InsertReason::ReplayOnly);
1641 if record {
1642 ed.push_undo();
1643 }
1644 let reason = if ed.vim.replaying {
1645 InsertReason::ReplayOnly
1646 } else {
1647 reason
1648 };
1649 let (row, _) = ed.cursor();
1650 ed.vim.insert_session = Some(InsertSession {
1651 count,
1652 row_min: row,
1653 row_max: row,
1654 before_lines: buf_lines_to_vec(&ed.buffer),
1655 reason,
1656 });
1657 ed.vim.mode = Mode::Insert;
1658}
1659
1660pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1675 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1676) {
1677 if !ed.settings.undo_break_on_motion {
1678 return;
1679 }
1680 if ed.vim.replaying {
1681 return;
1682 }
1683 if ed.vim.insert_session.is_none() {
1684 return;
1685 }
1686 ed.push_undo();
1687 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1688 let mut lines: Vec<String> = Vec::with_capacity(n);
1689 for r in 0..n {
1690 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1691 }
1692 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1693 if let Some(ref mut session) = ed.vim.insert_session {
1694 session.before_lines = lines;
1695 session.row_min = row;
1696 session.row_max = row;
1697 }
1698}
1699
1700fn step_normal<H: crate::types::Host>(
1703 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1704 input: Input,
1705) -> bool {
1706 if let Key::Char(d @ '0'..='9') = input.key
1708 && !input.ctrl
1709 && !input.alt
1710 && !matches!(
1711 ed.vim.pending,
1712 Pending::Replace
1713 | Pending::Find { .. }
1714 | Pending::OpFind { .. }
1715 | Pending::VisualTextObj { .. }
1716 )
1717 && (d != '0' || ed.vim.count > 0)
1718 {
1719 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1720 return true;
1721 }
1722
1723 match std::mem::take(&mut ed.vim.pending) {
1725 Pending::Replace => return handle_replace(ed, input),
1726 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1727 Pending::OpFind {
1728 op,
1729 count1,
1730 forward,
1731 till,
1732 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1733 Pending::G => return handle_after_g(ed, input),
1734 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1735 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1736 Pending::OpTextObj { op, count1, inner } => {
1737 return handle_text_object(ed, input, op, count1, inner);
1738 }
1739 Pending::VisualTextObj { inner } => {
1740 return handle_visual_text_obj(ed, input, inner);
1741 }
1742 Pending::Z => return handle_after_z(ed, input),
1743 Pending::SetMark => return handle_set_mark(ed, input),
1744 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1745 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1746 Pending::SelectRegister => return handle_select_register(ed, input),
1747 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1748 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1749 Pending::None => {}
1750 }
1751
1752 let count = take_count(&mut ed.vim);
1753
1754 match input.key {
1756 Key::Esc => {
1757 ed.vim.force_normal();
1758 return true;
1759 }
1760 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1761 ed.vim.visual_anchor = ed.cursor();
1762 ed.vim.mode = Mode::Visual;
1763 return true;
1764 }
1765 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1766 let (row, _) = ed.cursor();
1767 ed.vim.visual_line_anchor = row;
1768 ed.vim.mode = Mode::VisualLine;
1769 return true;
1770 }
1771 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1772 ed.vim.visual_anchor = ed.cursor();
1773 ed.vim.mode = Mode::Visual;
1774 return true;
1775 }
1776 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1777 let (row, _) = ed.cursor();
1778 ed.vim.visual_line_anchor = row;
1779 ed.vim.mode = Mode::VisualLine;
1780 return true;
1781 }
1782 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1783 let cur = ed.cursor();
1784 ed.vim.block_anchor = cur;
1785 ed.vim.block_vcol = cur.1;
1786 ed.vim.mode = Mode::VisualBlock;
1787 return true;
1788 }
1789 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1790 ed.vim.mode = Mode::Normal;
1792 return true;
1793 }
1794 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1797 Mode::Visual => {
1798 let cur = ed.cursor();
1799 let anchor = ed.vim.visual_anchor;
1800 ed.vim.visual_anchor = cur;
1801 ed.jump_cursor(anchor.0, anchor.1);
1802 return true;
1803 }
1804 Mode::VisualLine => {
1805 let cur_row = ed.cursor().0;
1806 let anchor_row = ed.vim.visual_line_anchor;
1807 ed.vim.visual_line_anchor = cur_row;
1808 ed.jump_cursor(anchor_row, 0);
1809 return true;
1810 }
1811 Mode::VisualBlock => {
1812 let cur = ed.cursor();
1813 let anchor = ed.vim.block_anchor;
1814 ed.vim.block_anchor = cur;
1815 ed.vim.block_vcol = anchor.1;
1816 ed.jump_cursor(anchor.0, anchor.1);
1817 return true;
1818 }
1819 _ => {}
1820 },
1821 _ => {}
1822 }
1823
1824 if ed.vim.is_visual()
1826 && let Some(op) = visual_operator(&input)
1827 {
1828 apply_visual_operator(ed, op);
1829 return true;
1830 }
1831
1832 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1836 match input.key {
1837 Key::Char('r') => {
1838 ed.vim.pending = Pending::Replace;
1839 return true;
1840 }
1841 Key::Char('I') => {
1842 let (top, bot, left, _right) = block_bounds(ed);
1843 ed.jump_cursor(top, left);
1844 ed.vim.mode = Mode::Normal;
1845 begin_insert(
1846 ed,
1847 1,
1848 InsertReason::BlockEdge {
1849 top,
1850 bot,
1851 col: left,
1852 },
1853 );
1854 return true;
1855 }
1856 Key::Char('A') => {
1857 let (top, bot, _left, right) = block_bounds(ed);
1858 let line_len = buf_line_chars(&ed.buffer, top);
1859 let col = (right + 1).min(line_len);
1860 ed.jump_cursor(top, col);
1861 ed.vim.mode = Mode::Normal;
1862 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1863 return true;
1864 }
1865 _ => {}
1866 }
1867 }
1868
1869 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1871 && !input.ctrl
1872 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1873 {
1874 let inner = matches!(input.key, Key::Char('i'));
1875 ed.vim.pending = Pending::VisualTextObj { inner };
1876 return true;
1877 }
1878
1879 if input.ctrl
1884 && let Key::Char(c) = input.key
1885 {
1886 match c {
1887 'd' => {
1888 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1889 return true;
1890 }
1891 'u' => {
1892 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1893 return true;
1894 }
1895 'f' => {
1896 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1897 return true;
1898 }
1899 'b' => {
1900 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1901 return true;
1902 }
1903 'r' => {
1904 do_redo(ed);
1905 return true;
1906 }
1907 'a' if ed.vim.mode == Mode::Normal => {
1908 adjust_number(ed, count.max(1) as i64);
1909 return true;
1910 }
1911 'x' if ed.vim.mode == Mode::Normal => {
1912 adjust_number(ed, -(count.max(1) as i64));
1913 return true;
1914 }
1915 'o' if ed.vim.mode == Mode::Normal => {
1916 for _ in 0..count.max(1) {
1917 jump_back(ed);
1918 }
1919 return true;
1920 }
1921 'i' if ed.vim.mode == Mode::Normal => {
1922 for _ in 0..count.max(1) {
1923 jump_forward(ed);
1924 }
1925 return true;
1926 }
1927 _ => {}
1928 }
1929 }
1930
1931 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1933 for _ in 0..count.max(1) {
1934 jump_forward(ed);
1935 }
1936 return true;
1937 }
1938
1939 if let Some(motion) = parse_motion(&input) {
1941 execute_motion(ed, motion.clone(), count);
1942 if ed.vim.mode == Mode::VisualBlock {
1944 update_block_vcol(ed, &motion);
1945 }
1946 if let Motion::Find { ch, forward, till } = motion {
1947 ed.vim.last_find = Some((ch, forward, till));
1948 }
1949 return true;
1950 }
1951
1952 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
1954 return true;
1955 }
1956
1957 if ed.vim.mode == Mode::Normal
1959 && let Key::Char(op_ch) = input.key
1960 && !input.ctrl
1961 && let Some(op) = char_to_operator(op_ch)
1962 {
1963 ed.vim.pending = Pending::Op { op, count1: count };
1964 return true;
1965 }
1966
1967 if ed.vim.mode == Mode::Normal
1969 && let Some((forward, till)) = find_entry(&input)
1970 {
1971 ed.vim.count = count;
1972 ed.vim.pending = Pending::Find { forward, till };
1973 return true;
1974 }
1975
1976 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
1978 ed.vim.count = count;
1979 ed.vim.pending = Pending::G;
1980 return true;
1981 }
1982
1983 if !input.ctrl
1985 && input.key == Key::Char('z')
1986 && matches!(
1987 ed.vim.mode,
1988 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
1989 )
1990 {
1991 ed.vim.pending = Pending::Z;
1992 return true;
1993 }
1994
1995 if !input.ctrl && ed.vim.mode == Mode::Normal {
1999 match input.key {
2000 Key::Char('m') => {
2001 ed.vim.pending = Pending::SetMark;
2002 return true;
2003 }
2004 Key::Char('\'') => {
2005 ed.vim.pending = Pending::GotoMarkLine;
2006 return true;
2007 }
2008 Key::Char('`') => {
2009 ed.vim.pending = Pending::GotoMarkChar;
2010 return true;
2011 }
2012 Key::Char('"') => {
2013 ed.vim.pending = Pending::SelectRegister;
2016 return true;
2017 }
2018 Key::Char('@') => {
2019 ed.vim.pending = Pending::PlayMacroTarget { count };
2023 return true;
2024 }
2025 Key::Char('q') if ed.vim.recording_macro.is_none() => {
2026 ed.vim.pending = Pending::RecordMacroTarget;
2031 return true;
2032 }
2033 _ => {}
2034 }
2035 }
2036
2037 true
2039}
2040
2041fn handle_set_mark<H: crate::types::Host>(
2042 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2043 input: Input,
2044) -> bool {
2045 if let Key::Char(c) = input.key
2046 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
2047 {
2048 let pos = ed.cursor();
2053 ed.set_mark(c, pos);
2054 }
2055 true
2056}
2057
2058fn handle_select_register<H: crate::types::Host>(
2062 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2063 input: Input,
2064) -> bool {
2065 if let Key::Char(c) = input.key
2066 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*'))
2067 {
2068 ed.vim.pending_register = Some(c);
2069 }
2070 true
2071}
2072
2073fn handle_record_macro_target<H: crate::types::Host>(
2078 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2079 input: Input,
2080) -> bool {
2081 if let Key::Char(c) = input.key
2082 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2083 {
2084 ed.vim.recording_macro = Some(c);
2085 if c.is_ascii_uppercase() {
2088 let lower = c.to_ascii_lowercase();
2089 let text = ed
2093 .registers()
2094 .read(lower)
2095 .map(|s| s.text.clone())
2096 .unwrap_or_default();
2097 ed.vim.recording_keys = crate::input::decode_macro(&text);
2098 } else {
2099 ed.vim.recording_keys.clear();
2100 }
2101 }
2102 true
2103}
2104
2105fn handle_play_macro_target<H: crate::types::Host>(
2111 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2112 input: Input,
2113 count: usize,
2114) -> bool {
2115 let reg = match input.key {
2116 Key::Char('@') => ed.vim.last_macro,
2117 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2118 Some(c.to_ascii_lowercase())
2119 }
2120 _ => None,
2121 };
2122 let Some(reg) = reg else {
2123 return true;
2124 };
2125 let text = match ed.registers().read(reg) {
2128 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2129 _ => return true,
2130 };
2131 let keys = crate::input::decode_macro(&text);
2132 ed.vim.last_macro = Some(reg);
2133 let times = count.max(1);
2134 let was_replaying = ed.vim.replaying_macro;
2135 ed.vim.replaying_macro = true;
2136 for _ in 0..times {
2137 for k in keys.iter().copied() {
2138 step(ed, k);
2139 }
2140 }
2141 ed.vim.replaying_macro = was_replaying;
2142 true
2143}
2144
2145fn handle_goto_mark<H: crate::types::Host>(
2146 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2147 input: Input,
2148 linewise: bool,
2149) -> bool {
2150 let Key::Char(c) = input.key else {
2151 return true;
2152 };
2153 let target = match c {
2160 'a'..='z' | 'A'..='Z' => ed.mark(c),
2161 '\'' | '`' => ed.vim.jump_back.last().copied(),
2162 '.' => ed.vim.last_edit_pos,
2163 _ => None,
2164 };
2165 let Some((row, col)) = target else {
2166 return true;
2167 };
2168 let pre = ed.cursor();
2169 let (r, c_clamped) = clamp_pos(ed, (row, col));
2170 if linewise {
2171 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2172 ed.push_buffer_cursor_to_textarea();
2173 move_first_non_whitespace(ed);
2174 } else {
2175 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2176 ed.push_buffer_cursor_to_textarea();
2177 }
2178 if ed.cursor() != pre {
2179 push_jump(ed, pre);
2180 }
2181 ed.sticky_col = Some(ed.cursor().1);
2182 true
2183}
2184
2185fn take_count(vim: &mut VimState) -> usize {
2186 if vim.count > 0 {
2187 let n = vim.count;
2188 vim.count = 0;
2189 n
2190 } else {
2191 1
2192 }
2193}
2194
2195fn char_to_operator(c: char) -> Option<Operator> {
2196 match c {
2197 'd' => Some(Operator::Delete),
2198 'c' => Some(Operator::Change),
2199 'y' => Some(Operator::Yank),
2200 '>' => Some(Operator::Indent),
2201 '<' => Some(Operator::Outdent),
2202 _ => None,
2203 }
2204}
2205
2206fn visual_operator(input: &Input) -> Option<Operator> {
2207 if input.ctrl {
2208 return None;
2209 }
2210 match input.key {
2211 Key::Char('y') => Some(Operator::Yank),
2212 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2213 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2214 Key::Char('U') => Some(Operator::Uppercase),
2216 Key::Char('u') => Some(Operator::Lowercase),
2217 Key::Char('~') => Some(Operator::ToggleCase),
2218 Key::Char('>') => Some(Operator::Indent),
2220 Key::Char('<') => Some(Operator::Outdent),
2221 _ => None,
2222 }
2223}
2224
2225fn find_entry(input: &Input) -> Option<(bool, bool)> {
2226 if input.ctrl {
2227 return None;
2228 }
2229 match input.key {
2230 Key::Char('f') => Some((true, false)),
2231 Key::Char('F') => Some((false, false)),
2232 Key::Char('t') => Some((true, true)),
2233 Key::Char('T') => Some((false, true)),
2234 _ => None,
2235 }
2236}
2237
2238const JUMPLIST_MAX: usize = 100;
2242
2243fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2248 ed.vim.jump_back.push(from);
2249 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2250 ed.vim.jump_back.remove(0);
2251 }
2252 ed.vim.jump_fwd.clear();
2253}
2254
2255fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2258 let Some(target) = ed.vim.jump_back.pop() else {
2259 return;
2260 };
2261 let cur = ed.cursor();
2262 ed.vim.jump_fwd.push(cur);
2263 let (r, c) = clamp_pos(ed, target);
2264 ed.jump_cursor(r, c);
2265 ed.sticky_col = Some(c);
2266}
2267
2268fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2271 let Some(target) = ed.vim.jump_fwd.pop() else {
2272 return;
2273 };
2274 let cur = ed.cursor();
2275 ed.vim.jump_back.push(cur);
2276 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2277 ed.vim.jump_back.remove(0);
2278 }
2279 let (r, c) = clamp_pos(ed, target);
2280 ed.jump_cursor(r, c);
2281 ed.sticky_col = Some(c);
2282}
2283
2284fn clamp_pos<H: crate::types::Host>(
2287 ed: &Editor<hjkl_buffer::Buffer, H>,
2288 pos: (usize, usize),
2289) -> (usize, usize) {
2290 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2291 let r = pos.0.min(last_row);
2292 let line_len = buf_line_chars(&ed.buffer, r);
2293 let c = pos.1.min(line_len.saturating_sub(1));
2294 (r, c)
2295}
2296
2297fn is_big_jump(motion: &Motion) -> bool {
2299 matches!(
2300 motion,
2301 Motion::FileTop
2302 | Motion::FileBottom
2303 | Motion::MatchBracket
2304 | Motion::WordAtCursor { .. }
2305 | Motion::SearchNext { .. }
2306 | Motion::ViewportTop
2307 | Motion::ViewportMiddle
2308 | Motion::ViewportBottom
2309 )
2310}
2311
2312fn viewport_half_rows<H: crate::types::Host>(
2317 ed: &Editor<hjkl_buffer::Buffer, H>,
2318 count: usize,
2319) -> usize {
2320 let h = ed.viewport_height_value() as usize;
2321 (h / 2).max(1).saturating_mul(count.max(1))
2322}
2323
2324fn viewport_full_rows<H: crate::types::Host>(
2327 ed: &Editor<hjkl_buffer::Buffer, H>,
2328 count: usize,
2329) -> usize {
2330 let h = ed.viewport_height_value() as usize;
2331 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2332}
2333
2334fn scroll_cursor_rows<H: crate::types::Host>(
2339 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2340 delta: isize,
2341) {
2342 if delta == 0 {
2343 return;
2344 }
2345 ed.sync_buffer_content_from_textarea();
2346 let (row, _) = ed.cursor();
2347 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2348 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2349 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2350 crate::motions::move_first_non_blank(&mut ed.buffer);
2351 ed.push_buffer_cursor_to_textarea();
2352 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2353}
2354
2355fn parse_motion(input: &Input) -> Option<Motion> {
2358 if input.ctrl {
2359 return None;
2360 }
2361 match input.key {
2362 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2363 Key::Char('l') | Key::Right => Some(Motion::Right),
2364 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2365 Key::Char('k') | Key::Up => Some(Motion::Up),
2366 Key::Char('w') => Some(Motion::WordFwd),
2367 Key::Char('W') => Some(Motion::BigWordFwd),
2368 Key::Char('b') => Some(Motion::WordBack),
2369 Key::Char('B') => Some(Motion::BigWordBack),
2370 Key::Char('e') => Some(Motion::WordEnd),
2371 Key::Char('E') => Some(Motion::BigWordEnd),
2372 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2373 Key::Char('^') => Some(Motion::FirstNonBlank),
2374 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2375 Key::Char('G') => Some(Motion::FileBottom),
2376 Key::Char('%') => Some(Motion::MatchBracket),
2377 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2378 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2379 Key::Char('*') => Some(Motion::WordAtCursor {
2380 forward: true,
2381 whole_word: true,
2382 }),
2383 Key::Char('#') => Some(Motion::WordAtCursor {
2384 forward: false,
2385 whole_word: true,
2386 }),
2387 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2388 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2389 Key::Char('H') => Some(Motion::ViewportTop),
2390 Key::Char('M') => Some(Motion::ViewportMiddle),
2391 Key::Char('L') => Some(Motion::ViewportBottom),
2392 Key::Char('{') => Some(Motion::ParagraphPrev),
2393 Key::Char('}') => Some(Motion::ParagraphNext),
2394 Key::Char('(') => Some(Motion::SentencePrev),
2395 Key::Char(')') => Some(Motion::SentenceNext),
2396 _ => None,
2397 }
2398}
2399
2400fn execute_motion<H: crate::types::Host>(
2403 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2404 motion: Motion,
2405 count: usize,
2406) {
2407 let count = count.max(1);
2408 let motion = match motion {
2410 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2411 Some((ch, forward, till)) => Motion::Find {
2412 ch,
2413 forward: if reverse { !forward } else { forward },
2414 till,
2415 },
2416 None => return,
2417 },
2418 other => other,
2419 };
2420 let pre_pos = ed.cursor();
2421 let pre_col = pre_pos.1;
2422 apply_motion_cursor(ed, &motion, count);
2423 let post_pos = ed.cursor();
2424 if is_big_jump(&motion) && pre_pos != post_pos {
2425 push_jump(ed, pre_pos);
2426 }
2427 apply_sticky_col(ed, &motion, pre_col);
2428 ed.sync_buffer_from_textarea();
2433}
2434
2435fn apply_sticky_col<H: crate::types::Host>(
2440 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2441 motion: &Motion,
2442 pre_col: usize,
2443) {
2444 if is_vertical_motion(motion) {
2445 let want = ed.sticky_col.unwrap_or(pre_col);
2446 ed.sticky_col = Some(want);
2449 let (row, _) = ed.cursor();
2450 let line_len = buf_line_chars(&ed.buffer, row);
2451 let max_col = line_len.saturating_sub(1);
2455 let target = want.min(max_col);
2456 ed.jump_cursor(row, target);
2457 } else {
2458 ed.sticky_col = Some(ed.cursor().1);
2461 }
2462}
2463
2464fn is_vertical_motion(motion: &Motion) -> bool {
2465 matches!(
2469 motion,
2470 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2471 )
2472}
2473
2474fn apply_motion_cursor<H: crate::types::Host>(
2475 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2476 motion: &Motion,
2477 count: usize,
2478) {
2479 apply_motion_cursor_ctx(ed, motion, count, false)
2480}
2481
2482fn apply_motion_cursor_ctx<H: crate::types::Host>(
2483 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2484 motion: &Motion,
2485 count: usize,
2486 as_operator: bool,
2487) {
2488 match motion {
2489 Motion::Left => {
2490 crate::motions::move_left(&mut ed.buffer, count);
2492 ed.push_buffer_cursor_to_textarea();
2493 }
2494 Motion::Right => {
2495 if as_operator {
2499 crate::motions::move_right_to_end(&mut ed.buffer, count);
2500 } else {
2501 crate::motions::move_right_in_line(&mut ed.buffer, count);
2502 }
2503 ed.push_buffer_cursor_to_textarea();
2504 }
2505 Motion::Up => {
2506 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2510 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2511 ed.push_buffer_cursor_to_textarea();
2512 }
2513 Motion::Down => {
2514 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2515 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2516 ed.push_buffer_cursor_to_textarea();
2517 }
2518 Motion::ScreenUp => {
2519 let v = *ed.host.viewport();
2520 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2521 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2522 ed.push_buffer_cursor_to_textarea();
2523 }
2524 Motion::ScreenDown => {
2525 let v = *ed.host.viewport();
2526 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2527 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2528 ed.push_buffer_cursor_to_textarea();
2529 }
2530 Motion::WordFwd => {
2531 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2532 ed.push_buffer_cursor_to_textarea();
2533 }
2534 Motion::WordBack => {
2535 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2536 ed.push_buffer_cursor_to_textarea();
2537 }
2538 Motion::WordEnd => {
2539 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2540 ed.push_buffer_cursor_to_textarea();
2541 }
2542 Motion::BigWordFwd => {
2543 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2544 ed.push_buffer_cursor_to_textarea();
2545 }
2546 Motion::BigWordBack => {
2547 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2548 ed.push_buffer_cursor_to_textarea();
2549 }
2550 Motion::BigWordEnd => {
2551 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2552 ed.push_buffer_cursor_to_textarea();
2553 }
2554 Motion::WordEndBack => {
2555 crate::motions::move_word_end_back(
2556 &mut ed.buffer,
2557 false,
2558 count,
2559 &ed.settings.iskeyword,
2560 );
2561 ed.push_buffer_cursor_to_textarea();
2562 }
2563 Motion::BigWordEndBack => {
2564 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2565 ed.push_buffer_cursor_to_textarea();
2566 }
2567 Motion::LineStart => {
2568 crate::motions::move_line_start(&mut ed.buffer);
2569 ed.push_buffer_cursor_to_textarea();
2570 }
2571 Motion::FirstNonBlank => {
2572 crate::motions::move_first_non_blank(&mut ed.buffer);
2573 ed.push_buffer_cursor_to_textarea();
2574 }
2575 Motion::LineEnd => {
2576 crate::motions::move_line_end(&mut ed.buffer);
2578 ed.push_buffer_cursor_to_textarea();
2579 }
2580 Motion::FileTop => {
2581 if count > 1 {
2584 crate::motions::move_bottom(&mut ed.buffer, count);
2585 } else {
2586 crate::motions::move_top(&mut ed.buffer);
2587 }
2588 ed.push_buffer_cursor_to_textarea();
2589 }
2590 Motion::FileBottom => {
2591 if count > 1 {
2594 crate::motions::move_bottom(&mut ed.buffer, count);
2595 } else {
2596 crate::motions::move_bottom(&mut ed.buffer, 0);
2597 }
2598 ed.push_buffer_cursor_to_textarea();
2599 }
2600 Motion::Find { ch, forward, till } => {
2601 for _ in 0..count {
2602 if !find_char_on_line(ed, *ch, *forward, *till) {
2603 break;
2604 }
2605 }
2606 }
2607 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2609 let _ = matching_bracket(ed);
2610 }
2611 Motion::WordAtCursor {
2612 forward,
2613 whole_word,
2614 } => {
2615 word_at_cursor_search(ed, *forward, *whole_word, count);
2616 }
2617 Motion::SearchNext { reverse } => {
2618 if let Some(pattern) = ed.vim.last_search.clone() {
2622 push_search_pattern(ed, &pattern);
2623 }
2624 if ed.search_state().pattern.is_none() {
2625 return;
2626 }
2627 let forward = ed.vim.last_search_forward != *reverse;
2631 for _ in 0..count.max(1) {
2632 if forward {
2633 ed.search_advance_forward(true);
2634 } else {
2635 ed.search_advance_backward(true);
2636 }
2637 }
2638 ed.push_buffer_cursor_to_textarea();
2639 }
2640 Motion::ViewportTop => {
2641 let v = *ed.host().viewport();
2642 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2643 ed.push_buffer_cursor_to_textarea();
2644 }
2645 Motion::ViewportMiddle => {
2646 let v = *ed.host().viewport();
2647 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2648 ed.push_buffer_cursor_to_textarea();
2649 }
2650 Motion::ViewportBottom => {
2651 let v = *ed.host().viewport();
2652 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2653 ed.push_buffer_cursor_to_textarea();
2654 }
2655 Motion::LastNonBlank => {
2656 crate::motions::move_last_non_blank(&mut ed.buffer);
2657 ed.push_buffer_cursor_to_textarea();
2658 }
2659 Motion::LineMiddle => {
2660 let row = ed.cursor().0;
2661 let line_chars = buf_line_chars(&ed.buffer, row);
2662 let target = line_chars / 2;
2665 ed.jump_cursor(row, target);
2666 }
2667 Motion::ParagraphPrev => {
2668 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2669 ed.push_buffer_cursor_to_textarea();
2670 }
2671 Motion::ParagraphNext => {
2672 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2673 ed.push_buffer_cursor_to_textarea();
2674 }
2675 Motion::SentencePrev => {
2676 for _ in 0..count.max(1) {
2677 if let Some((row, col)) = sentence_boundary(ed, false) {
2678 ed.jump_cursor(row, col);
2679 }
2680 }
2681 }
2682 Motion::SentenceNext => {
2683 for _ in 0..count.max(1) {
2684 if let Some((row, col)) = sentence_boundary(ed, true) {
2685 ed.jump_cursor(row, col);
2686 }
2687 }
2688 }
2689 }
2690}
2691
2692fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2693 ed.sync_buffer_content_from_textarea();
2699 crate::motions::move_first_non_blank(&mut ed.buffer);
2700 ed.push_buffer_cursor_to_textarea();
2701}
2702
2703fn find_char_on_line<H: crate::types::Host>(
2704 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2705 ch: char,
2706 forward: bool,
2707 till: bool,
2708) -> bool {
2709 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2710 if moved {
2711 ed.push_buffer_cursor_to_textarea();
2712 }
2713 moved
2714}
2715
2716fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2717 let moved = crate::motions::match_bracket(&mut ed.buffer);
2718 if moved {
2719 ed.push_buffer_cursor_to_textarea();
2720 }
2721 moved
2722}
2723
2724fn word_at_cursor_search<H: crate::types::Host>(
2725 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2726 forward: bool,
2727 whole_word: bool,
2728 count: usize,
2729) {
2730 let (row, col) = ed.cursor();
2731 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2732 let chars: Vec<char> = line.chars().collect();
2733 if chars.is_empty() {
2734 return;
2735 }
2736 let spec = ed.settings().iskeyword.clone();
2738 let is_word = |c: char| is_keyword_char(c, &spec);
2739 let mut start = col.min(chars.len().saturating_sub(1));
2740 while start > 0 && is_word(chars[start - 1]) {
2741 start -= 1;
2742 }
2743 let mut end = start;
2744 while end < chars.len() && is_word(chars[end]) {
2745 end += 1;
2746 }
2747 if end <= start {
2748 return;
2749 }
2750 let word: String = chars[start..end].iter().collect();
2751 let escaped = regex_escape(&word);
2752 let pattern = if whole_word {
2753 format!(r"\b{escaped}\b")
2754 } else {
2755 escaped
2756 };
2757 push_search_pattern(ed, &pattern);
2758 if ed.search_state().pattern.is_none() {
2759 return;
2760 }
2761 ed.vim.last_search = Some(pattern);
2763 ed.vim.last_search_forward = forward;
2764 for _ in 0..count.max(1) {
2765 if forward {
2766 ed.search_advance_forward(true);
2767 } else {
2768 ed.search_advance_backward(true);
2769 }
2770 }
2771 ed.push_buffer_cursor_to_textarea();
2772}
2773
2774fn regex_escape(s: &str) -> String {
2775 let mut out = String::with_capacity(s.len());
2776 for c in s.chars() {
2777 if matches!(
2778 c,
2779 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2780 ) {
2781 out.push('\\');
2782 }
2783 out.push(c);
2784 }
2785 out
2786}
2787
2788fn handle_after_op<H: crate::types::Host>(
2791 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2792 input: Input,
2793 op: Operator,
2794 count1: usize,
2795) -> bool {
2796 if let Key::Char(d @ '0'..='9') = input.key
2798 && !input.ctrl
2799 && (d != '0' || ed.vim.count > 0)
2800 {
2801 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2802 ed.vim.pending = Pending::Op { op, count1 };
2803 return true;
2804 }
2805
2806 if input.key == Key::Esc {
2808 ed.vim.count = 0;
2809 return true;
2810 }
2811
2812 let double_ch = match op {
2816 Operator::Delete => Some('d'),
2817 Operator::Change => Some('c'),
2818 Operator::Yank => Some('y'),
2819 Operator::Indent => Some('>'),
2820 Operator::Outdent => Some('<'),
2821 Operator::Uppercase => Some('U'),
2822 Operator::Lowercase => Some('u'),
2823 Operator::ToggleCase => Some('~'),
2824 Operator::Fold => None,
2825 Operator::Reflow => Some('q'),
2828 };
2829 if let Key::Char(c) = input.key
2830 && !input.ctrl
2831 && Some(c) == double_ch
2832 {
2833 let count2 = take_count(&mut ed.vim);
2834 let total = count1.max(1) * count2.max(1);
2835 execute_line_op(ed, op, total);
2836 if !ed.vim.replaying {
2837 ed.vim.last_change = Some(LastChange::LineOp {
2838 op,
2839 count: total,
2840 inserted: None,
2841 });
2842 }
2843 return true;
2844 }
2845
2846 if let Key::Char('i') | Key::Char('a') = input.key
2848 && !input.ctrl
2849 {
2850 let inner = matches!(input.key, Key::Char('i'));
2851 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2852 return true;
2853 }
2854
2855 if input.key == Key::Char('g') && !input.ctrl {
2857 ed.vim.pending = Pending::OpG { op, count1 };
2858 return true;
2859 }
2860
2861 if let Some((forward, till)) = find_entry(&input) {
2863 ed.vim.pending = Pending::OpFind {
2864 op,
2865 count1,
2866 forward,
2867 till,
2868 };
2869 return true;
2870 }
2871
2872 let count2 = take_count(&mut ed.vim);
2874 let total = count1.max(1) * count2.max(1);
2875 if let Some(motion) = parse_motion(&input) {
2876 let motion = match motion {
2877 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2878 Some((ch, forward, till)) => Motion::Find {
2879 ch,
2880 forward: if reverse { !forward } else { forward },
2881 till,
2882 },
2883 None => return true,
2884 },
2885 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2889 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2890 m => m,
2891 };
2892 apply_op_with_motion(ed, op, &motion, total);
2893 if let Motion::Find { ch, forward, till } = &motion {
2894 ed.vim.last_find = Some((*ch, *forward, *till));
2895 }
2896 if !ed.vim.replaying && op_is_change(op) {
2897 ed.vim.last_change = Some(LastChange::OpMotion {
2898 op,
2899 motion,
2900 count: total,
2901 inserted: None,
2902 });
2903 }
2904 return true;
2905 }
2906
2907 true
2909}
2910
2911fn handle_op_after_g<H: crate::types::Host>(
2912 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2913 input: Input,
2914 op: Operator,
2915 count1: usize,
2916) -> bool {
2917 if input.ctrl {
2918 return true;
2919 }
2920 let count2 = take_count(&mut ed.vim);
2921 let total = count1.max(1) * count2.max(1);
2922 if matches!(
2926 op,
2927 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2928 ) {
2929 let op_char = match op {
2930 Operator::Uppercase => 'U',
2931 Operator::Lowercase => 'u',
2932 Operator::ToggleCase => '~',
2933 _ => unreachable!(),
2934 };
2935 if input.key == Key::Char(op_char) {
2936 execute_line_op(ed, op, total);
2937 if !ed.vim.replaying {
2938 ed.vim.last_change = Some(LastChange::LineOp {
2939 op,
2940 count: total,
2941 inserted: None,
2942 });
2943 }
2944 return true;
2945 }
2946 }
2947 let motion = match input.key {
2948 Key::Char('g') => Motion::FileTop,
2949 Key::Char('e') => Motion::WordEndBack,
2950 Key::Char('E') => Motion::BigWordEndBack,
2951 Key::Char('j') => Motion::ScreenDown,
2952 Key::Char('k') => Motion::ScreenUp,
2953 _ => return true,
2954 };
2955 apply_op_with_motion(ed, op, &motion, total);
2956 if !ed.vim.replaying && op_is_change(op) {
2957 ed.vim.last_change = Some(LastChange::OpMotion {
2958 op,
2959 motion,
2960 count: total,
2961 inserted: None,
2962 });
2963 }
2964 true
2965}
2966
2967fn handle_after_g<H: crate::types::Host>(
2968 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2969 input: Input,
2970) -> bool {
2971 let count = take_count(&mut ed.vim);
2972 match input.key {
2973 Key::Char('g') => {
2974 let pre = ed.cursor();
2976 if count > 1 {
2977 ed.jump_cursor(count - 1, 0);
2978 } else {
2979 ed.jump_cursor(0, 0);
2980 }
2981 move_first_non_whitespace(ed);
2982 if ed.cursor() != pre {
2983 push_jump(ed, pre);
2984 }
2985 }
2986 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
2987 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
2988 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
2990 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
2992 Key::Char('v') => {
2994 if let Some(snap) = ed.vim.last_visual {
2995 match snap.mode {
2996 Mode::Visual => {
2997 ed.vim.visual_anchor = snap.anchor;
2998 ed.vim.mode = Mode::Visual;
2999 }
3000 Mode::VisualLine => {
3001 ed.vim.visual_line_anchor = snap.anchor.0;
3002 ed.vim.mode = Mode::VisualLine;
3003 }
3004 Mode::VisualBlock => {
3005 ed.vim.block_anchor = snap.anchor;
3006 ed.vim.block_vcol = snap.block_vcol;
3007 ed.vim.mode = Mode::VisualBlock;
3008 }
3009 _ => {}
3010 }
3011 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3012 }
3013 }
3014 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
3018 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
3019 Key::Char('U') => {
3023 ed.vim.pending = Pending::Op {
3024 op: Operator::Uppercase,
3025 count1: count,
3026 };
3027 }
3028 Key::Char('u') => {
3029 ed.vim.pending = Pending::Op {
3030 op: Operator::Lowercase,
3031 count1: count,
3032 };
3033 }
3034 Key::Char('~') => {
3035 ed.vim.pending = Pending::Op {
3036 op: Operator::ToggleCase,
3037 count1: count,
3038 };
3039 }
3040 Key::Char('q') => {
3041 ed.vim.pending = Pending::Op {
3044 op: Operator::Reflow,
3045 count1: count,
3046 };
3047 }
3048 Key::Char('J') => {
3049 for _ in 0..count.max(1) {
3051 ed.push_undo();
3052 join_line_raw(ed);
3053 }
3054 if !ed.vim.replaying {
3055 ed.vim.last_change = Some(LastChange::JoinLine {
3056 count: count.max(1),
3057 });
3058 }
3059 }
3060 Key::Char('d') => {
3061 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3066 }
3067 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
3070 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
3071 Key::Char('*') => execute_motion(
3075 ed,
3076 Motion::WordAtCursor {
3077 forward: true,
3078 whole_word: false,
3079 },
3080 count,
3081 ),
3082 Key::Char('#') => execute_motion(
3083 ed,
3084 Motion::WordAtCursor {
3085 forward: false,
3086 whole_word: false,
3087 },
3088 count,
3089 ),
3090 _ => {}
3091 }
3092 true
3093}
3094
3095fn handle_after_z<H: crate::types::Host>(
3096 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3097 input: Input,
3098) -> bool {
3099 use crate::editor::CursorScrollTarget;
3100 let row = ed.cursor().0;
3101 match input.key {
3102 Key::Char('z') => {
3103 ed.scroll_cursor_to(CursorScrollTarget::Center);
3104 ed.vim.viewport_pinned = true;
3105 }
3106 Key::Char('t') => {
3107 ed.scroll_cursor_to(CursorScrollTarget::Top);
3108 ed.vim.viewport_pinned = true;
3109 }
3110 Key::Char('b') => {
3111 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3112 ed.vim.viewport_pinned = true;
3113 }
3114 Key::Char('o') => {
3119 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3120 }
3121 Key::Char('c') => {
3122 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3123 }
3124 Key::Char('a') => {
3125 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3126 }
3127 Key::Char('R') => {
3128 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3129 }
3130 Key::Char('M') => {
3131 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3132 }
3133 Key::Char('E') => {
3134 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3135 }
3136 Key::Char('d') => {
3137 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3138 }
3139 Key::Char('f') => {
3140 if matches!(
3141 ed.vim.mode,
3142 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3143 ) {
3144 let anchor_row = match ed.vim.mode {
3147 Mode::VisualLine => ed.vim.visual_line_anchor,
3148 Mode::VisualBlock => ed.vim.block_anchor.0,
3149 _ => ed.vim.visual_anchor.0,
3150 };
3151 let cur = ed.cursor().0;
3152 let top = anchor_row.min(cur);
3153 let bot = anchor_row.max(cur);
3154 ed.apply_fold_op(crate::types::FoldOp::Add {
3155 start_row: top,
3156 end_row: bot,
3157 closed: true,
3158 });
3159 ed.vim.mode = Mode::Normal;
3160 } else {
3161 let count = take_count(&mut ed.vim);
3166 ed.vim.pending = Pending::Op {
3167 op: Operator::Fold,
3168 count1: count,
3169 };
3170 }
3171 }
3172 _ => {}
3173 }
3174 true
3175}
3176
3177fn handle_replace<H: crate::types::Host>(
3178 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3179 input: Input,
3180) -> bool {
3181 if let Key::Char(ch) = input.key {
3182 if ed.vim.mode == Mode::VisualBlock {
3183 block_replace(ed, ch);
3184 return true;
3185 }
3186 let count = take_count(&mut ed.vim);
3187 replace_char(ed, ch, count.max(1));
3188 if !ed.vim.replaying {
3189 ed.vim.last_change = Some(LastChange::ReplaceChar {
3190 ch,
3191 count: count.max(1),
3192 });
3193 }
3194 }
3195 true
3196}
3197
3198fn handle_find_target<H: crate::types::Host>(
3199 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3200 input: Input,
3201 forward: bool,
3202 till: bool,
3203) -> bool {
3204 let Key::Char(ch) = input.key else {
3205 return true;
3206 };
3207 let count = take_count(&mut ed.vim);
3208 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3209 ed.vim.last_find = Some((ch, forward, till));
3210 true
3211}
3212
3213fn handle_op_find_target<H: crate::types::Host>(
3214 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3215 input: Input,
3216 op: Operator,
3217 count1: usize,
3218 forward: bool,
3219 till: bool,
3220) -> bool {
3221 let Key::Char(ch) = input.key else {
3222 return true;
3223 };
3224 let count2 = take_count(&mut ed.vim);
3225 let total = count1.max(1) * count2.max(1);
3226 let motion = Motion::Find { ch, forward, till };
3227 apply_op_with_motion(ed, op, &motion, total);
3228 ed.vim.last_find = Some((ch, forward, till));
3229 if !ed.vim.replaying && op_is_change(op) {
3230 ed.vim.last_change = Some(LastChange::OpMotion {
3231 op,
3232 motion,
3233 count: total,
3234 inserted: None,
3235 });
3236 }
3237 true
3238}
3239
3240fn handle_text_object<H: crate::types::Host>(
3241 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3242 input: Input,
3243 op: Operator,
3244 _count1: usize,
3245 inner: bool,
3246) -> bool {
3247 let Key::Char(ch) = input.key else {
3248 return true;
3249 };
3250 let obj = match ch {
3251 'w' => TextObject::Word { big: false },
3252 'W' => TextObject::Word { big: true },
3253 '"' | '\'' | '`' => TextObject::Quote(ch),
3254 '(' | ')' | 'b' => TextObject::Bracket('('),
3255 '[' | ']' => TextObject::Bracket('['),
3256 '{' | '}' | 'B' => TextObject::Bracket('{'),
3257 '<' | '>' => TextObject::Bracket('<'),
3258 'p' => TextObject::Paragraph,
3259 't' => TextObject::XmlTag,
3260 's' => TextObject::Sentence,
3261 _ => return true,
3262 };
3263 apply_op_with_text_object(ed, op, obj, inner);
3264 if !ed.vim.replaying && op_is_change(op) {
3265 ed.vim.last_change = Some(LastChange::OpTextObj {
3266 op,
3267 obj,
3268 inner,
3269 inserted: None,
3270 });
3271 }
3272 true
3273}
3274
3275fn handle_visual_text_obj<H: crate::types::Host>(
3276 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3277 input: Input,
3278 inner: bool,
3279) -> bool {
3280 let Key::Char(ch) = input.key else {
3281 return true;
3282 };
3283 let obj = match ch {
3284 'w' => TextObject::Word { big: false },
3285 'W' => TextObject::Word { big: true },
3286 '"' | '\'' | '`' => TextObject::Quote(ch),
3287 '(' | ')' | 'b' => TextObject::Bracket('('),
3288 '[' | ']' => TextObject::Bracket('['),
3289 '{' | '}' | 'B' => TextObject::Bracket('{'),
3290 '<' | '>' => TextObject::Bracket('<'),
3291 'p' => TextObject::Paragraph,
3292 't' => TextObject::XmlTag,
3293 's' => TextObject::Sentence,
3294 _ => return true,
3295 };
3296 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3297 return true;
3298 };
3299 match kind {
3303 MotionKind::Linewise => {
3304 ed.vim.visual_line_anchor = start.0;
3305 ed.vim.mode = Mode::VisualLine;
3306 ed.jump_cursor(end.0, 0);
3307 }
3308 _ => {
3309 ed.vim.mode = Mode::Visual;
3310 ed.vim.visual_anchor = (start.0, start.1);
3311 let (er, ec) = retreat_one(ed, end);
3312 ed.jump_cursor(er, ec);
3313 }
3314 }
3315 true
3316}
3317
3318fn retreat_one<H: crate::types::Host>(
3320 ed: &Editor<hjkl_buffer::Buffer, H>,
3321 pos: (usize, usize),
3322) -> (usize, usize) {
3323 let (r, c) = pos;
3324 if c > 0 {
3325 (r, c - 1)
3326 } else if r > 0 {
3327 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3328 (r - 1, prev_len)
3329 } else {
3330 (0, 0)
3331 }
3332}
3333
3334fn op_is_change(op: Operator) -> bool {
3335 matches!(op, Operator::Delete | Operator::Change)
3336}
3337
3338fn handle_normal_only<H: crate::types::Host>(
3341 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3342 input: &Input,
3343 count: usize,
3344) -> bool {
3345 if input.ctrl {
3346 return false;
3347 }
3348 match input.key {
3349 Key::Char('i') => {
3350 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3351 true
3352 }
3353 Key::Char('I') => {
3354 move_first_non_whitespace(ed);
3355 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3356 true
3357 }
3358 Key::Char('a') => {
3359 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3360 ed.push_buffer_cursor_to_textarea();
3361 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3362 true
3363 }
3364 Key::Char('A') => {
3365 crate::motions::move_line_end(&mut ed.buffer);
3366 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3367 ed.push_buffer_cursor_to_textarea();
3368 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3369 true
3370 }
3371 Key::Char('R') => {
3372 begin_insert(ed, count.max(1), InsertReason::Replace);
3375 true
3376 }
3377 Key::Char('o') => {
3378 use hjkl_buffer::{Edit, Position};
3379 ed.push_undo();
3380 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3383 ed.sync_buffer_content_from_textarea();
3384 let row = buf_cursor_pos(&ed.buffer).row;
3385 let line_chars = buf_line_chars(&ed.buffer, row);
3386 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3389 let indent = compute_enter_indent(&ed.settings, prev_line);
3390 ed.mutate_edit(Edit::InsertStr {
3391 at: Position::new(row, line_chars),
3392 text: format!("\n{indent}"),
3393 });
3394 ed.push_buffer_cursor_to_textarea();
3395 true
3396 }
3397 Key::Char('O') => {
3398 use hjkl_buffer::{Edit, Position};
3399 ed.push_undo();
3400 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3401 ed.sync_buffer_content_from_textarea();
3402 let row = buf_cursor_pos(&ed.buffer).row;
3403 let indent = if row > 0 {
3407 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3408 compute_enter_indent(&ed.settings, above)
3409 } else {
3410 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3411 cur.chars()
3412 .take_while(|c| *c == ' ' || *c == '\t')
3413 .collect::<String>()
3414 };
3415 ed.mutate_edit(Edit::InsertStr {
3416 at: Position::new(row, 0),
3417 text: format!("{indent}\n"),
3418 });
3419 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3424 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3425 let new_row = buf_cursor_pos(&ed.buffer).row;
3426 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3427 ed.push_buffer_cursor_to_textarea();
3428 true
3429 }
3430 Key::Char('x') => {
3431 do_char_delete(ed, true, count.max(1));
3432 if !ed.vim.replaying {
3433 ed.vim.last_change = Some(LastChange::CharDel {
3434 forward: true,
3435 count: count.max(1),
3436 });
3437 }
3438 true
3439 }
3440 Key::Char('X') => {
3441 do_char_delete(ed, false, count.max(1));
3442 if !ed.vim.replaying {
3443 ed.vim.last_change = Some(LastChange::CharDel {
3444 forward: false,
3445 count: count.max(1),
3446 });
3447 }
3448 true
3449 }
3450 Key::Char('~') => {
3451 for _ in 0..count.max(1) {
3452 ed.push_undo();
3453 toggle_case_at_cursor(ed);
3454 }
3455 if !ed.vim.replaying {
3456 ed.vim.last_change = Some(LastChange::ToggleCase {
3457 count: count.max(1),
3458 });
3459 }
3460 true
3461 }
3462 Key::Char('J') => {
3463 for _ in 0..count.max(1) {
3464 ed.push_undo();
3465 join_line(ed);
3466 }
3467 if !ed.vim.replaying {
3468 ed.vim.last_change = Some(LastChange::JoinLine {
3469 count: count.max(1),
3470 });
3471 }
3472 true
3473 }
3474 Key::Char('D') => {
3475 ed.push_undo();
3476 delete_to_eol(ed);
3477 crate::motions::move_left(&mut ed.buffer, 1);
3479 ed.push_buffer_cursor_to_textarea();
3480 if !ed.vim.replaying {
3481 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3482 }
3483 true
3484 }
3485 Key::Char('Y') => {
3486 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3488 true
3489 }
3490 Key::Char('C') => {
3491 ed.push_undo();
3492 delete_to_eol(ed);
3493 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3494 true
3495 }
3496 Key::Char('s') => {
3497 use hjkl_buffer::{Edit, MotionKind, Position};
3498 ed.push_undo();
3499 ed.sync_buffer_content_from_textarea();
3500 for _ in 0..count.max(1) {
3501 let cursor = buf_cursor_pos(&ed.buffer);
3502 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3503 if cursor.col >= line_chars {
3504 break;
3505 }
3506 ed.mutate_edit(Edit::DeleteRange {
3507 start: cursor,
3508 end: Position::new(cursor.row, cursor.col + 1),
3509 kind: MotionKind::Char,
3510 });
3511 }
3512 ed.push_buffer_cursor_to_textarea();
3513 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3514 if !ed.vim.replaying {
3516 ed.vim.last_change = Some(LastChange::OpMotion {
3517 op: Operator::Change,
3518 motion: Motion::Right,
3519 count: count.max(1),
3520 inserted: None,
3521 });
3522 }
3523 true
3524 }
3525 Key::Char('p') => {
3526 do_paste(ed, false, count.max(1));
3527 if !ed.vim.replaying {
3528 ed.vim.last_change = Some(LastChange::Paste {
3529 before: false,
3530 count: count.max(1),
3531 });
3532 }
3533 true
3534 }
3535 Key::Char('P') => {
3536 do_paste(ed, true, count.max(1));
3537 if !ed.vim.replaying {
3538 ed.vim.last_change = Some(LastChange::Paste {
3539 before: true,
3540 count: count.max(1),
3541 });
3542 }
3543 true
3544 }
3545 Key::Char('u') => {
3546 do_undo(ed);
3547 true
3548 }
3549 Key::Char('r') => {
3550 ed.vim.count = count;
3551 ed.vim.pending = Pending::Replace;
3552 true
3553 }
3554 Key::Char('/') => {
3555 enter_search(ed, true);
3556 true
3557 }
3558 Key::Char('?') => {
3559 enter_search(ed, false);
3560 true
3561 }
3562 Key::Char('.') => {
3563 replay_last_change(ed, count);
3564 true
3565 }
3566 _ => false,
3567 }
3568}
3569
3570fn begin_insert_noundo<H: crate::types::Host>(
3572 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3573 count: usize,
3574 reason: InsertReason,
3575) {
3576 let reason = if ed.vim.replaying {
3577 InsertReason::ReplayOnly
3578 } else {
3579 reason
3580 };
3581 let (row, _) = ed.cursor();
3582 ed.vim.insert_session = Some(InsertSession {
3583 count,
3584 row_min: row,
3585 row_max: row,
3586 before_lines: buf_lines_to_vec(&ed.buffer),
3587 reason,
3588 });
3589 ed.vim.mode = Mode::Insert;
3590}
3591
3592fn apply_op_with_motion<H: crate::types::Host>(
3595 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3596 op: Operator,
3597 motion: &Motion,
3598 count: usize,
3599) {
3600 let start = ed.cursor();
3601 apply_motion_cursor_ctx(ed, motion, count, true);
3606 let end = ed.cursor();
3607 let kind = motion_kind(motion);
3608 ed.jump_cursor(start.0, start.1);
3610 run_operator_over_range(ed, op, start, end, kind);
3611}
3612
3613fn apply_op_with_text_object<H: crate::types::Host>(
3614 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3615 op: Operator,
3616 obj: TextObject,
3617 inner: bool,
3618) {
3619 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3620 return;
3621 };
3622 ed.jump_cursor(start.0, start.1);
3623 run_operator_over_range(ed, op, start, end, kind);
3624}
3625
3626fn motion_kind(motion: &Motion) -> MotionKind {
3627 match motion {
3628 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3629 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3630 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3631 MotionKind::Linewise
3632 }
3633 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3634 MotionKind::Inclusive
3635 }
3636 Motion::Find { .. } => MotionKind::Inclusive,
3637 Motion::MatchBracket => MotionKind::Inclusive,
3638 Motion::LineEnd => MotionKind::Inclusive,
3640 _ => MotionKind::Exclusive,
3641 }
3642}
3643
3644fn run_operator_over_range<H: crate::types::Host>(
3645 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3646 op: Operator,
3647 start: (usize, usize),
3648 end: (usize, usize),
3649 kind: MotionKind,
3650) {
3651 let (top, bot) = order(start, end);
3652 if top == bot {
3653 return;
3654 }
3655
3656 match op {
3657 Operator::Yank => {
3658 let text = read_vim_range(ed, top, bot, kind);
3659 if !text.is_empty() {
3660 ed.record_yank_to_host(text.clone());
3661 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3662 }
3663 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3664 ed.push_buffer_cursor_to_textarea();
3665 }
3666 Operator::Delete => {
3667 ed.push_undo();
3668 cut_vim_range(ed, top, bot, kind);
3669 if !matches!(kind, MotionKind::Linewise) {
3674 clamp_cursor_to_normal_mode(ed);
3675 }
3676 ed.vim.mode = Mode::Normal;
3677 }
3678 Operator::Change => {
3679 ed.push_undo();
3680 cut_vim_range(ed, top, bot, kind);
3681 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3682 }
3683 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3684 apply_case_op_to_selection(ed, op, top, bot, kind);
3685 }
3686 Operator::Indent | Operator::Outdent => {
3687 ed.push_undo();
3690 if op == Operator::Indent {
3691 indent_rows(ed, top.0, bot.0, 1);
3692 } else {
3693 outdent_rows(ed, top.0, bot.0, 1);
3694 }
3695 ed.vim.mode = Mode::Normal;
3696 }
3697 Operator::Fold => {
3698 if bot.0 >= top.0 {
3702 ed.apply_fold_op(crate::types::FoldOp::Add {
3703 start_row: top.0,
3704 end_row: bot.0,
3705 closed: true,
3706 });
3707 }
3708 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3709 ed.push_buffer_cursor_to_textarea();
3710 ed.vim.mode = Mode::Normal;
3711 }
3712 Operator::Reflow => {
3713 ed.push_undo();
3714 reflow_rows(ed, top.0, bot.0);
3715 ed.vim.mode = Mode::Normal;
3716 }
3717 }
3718}
3719
3720fn reflow_rows<H: crate::types::Host>(
3725 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3726 top: usize,
3727 bot: usize,
3728) {
3729 let width = ed.settings().textwidth.max(1);
3730 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3731 let bot = bot.min(lines.len().saturating_sub(1));
3732 if top > bot {
3733 return;
3734 }
3735 let original = lines[top..=bot].to_vec();
3736 let mut wrapped: Vec<String> = Vec::new();
3737 let mut paragraph: Vec<String> = Vec::new();
3738 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3739 if para.is_empty() {
3740 return;
3741 }
3742 let words = para.join(" ");
3743 let mut current = String::new();
3744 for word in words.split_whitespace() {
3745 let extra = if current.is_empty() {
3746 word.chars().count()
3747 } else {
3748 current.chars().count() + 1 + word.chars().count()
3749 };
3750 if extra > width && !current.is_empty() {
3751 out.push(std::mem::take(&mut current));
3752 current.push_str(word);
3753 } else if current.is_empty() {
3754 current.push_str(word);
3755 } else {
3756 current.push(' ');
3757 current.push_str(word);
3758 }
3759 }
3760 if !current.is_empty() {
3761 out.push(current);
3762 }
3763 para.clear();
3764 };
3765 for line in &original {
3766 if line.trim().is_empty() {
3767 flush(&mut paragraph, &mut wrapped, width);
3768 wrapped.push(String::new());
3769 } else {
3770 paragraph.push(line.clone());
3771 }
3772 }
3773 flush(&mut paragraph, &mut wrapped, width);
3774
3775 let after: Vec<String> = lines.split_off(bot + 1);
3777 lines.truncate(top);
3778 lines.extend(wrapped);
3779 lines.extend(after);
3780 ed.restore(lines, (top, 0));
3781 ed.mark_content_dirty();
3782}
3783
3784fn apply_case_op_to_selection<H: crate::types::Host>(
3790 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3791 op: Operator,
3792 top: (usize, usize),
3793 bot: (usize, usize),
3794 kind: MotionKind,
3795) {
3796 use hjkl_buffer::Edit;
3797 ed.push_undo();
3798 let saved_yank = ed.yank().to_string();
3799 let saved_yank_linewise = ed.vim.yank_linewise;
3800 let selection = cut_vim_range(ed, top, bot, kind);
3801 let transformed = match op {
3802 Operator::Uppercase => selection.to_uppercase(),
3803 Operator::Lowercase => selection.to_lowercase(),
3804 Operator::ToggleCase => toggle_case_str(&selection),
3805 _ => unreachable!(),
3806 };
3807 if !transformed.is_empty() {
3808 let cursor = buf_cursor_pos(&ed.buffer);
3809 ed.mutate_edit(Edit::InsertStr {
3810 at: cursor,
3811 text: transformed,
3812 });
3813 }
3814 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3815 ed.push_buffer_cursor_to_textarea();
3816 ed.set_yank(saved_yank);
3817 ed.vim.yank_linewise = saved_yank_linewise;
3818 ed.vim.mode = Mode::Normal;
3819}
3820
3821fn indent_rows<H: crate::types::Host>(
3826 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3827 top: usize,
3828 bot: usize,
3829 count: usize,
3830) {
3831 ed.sync_buffer_content_from_textarea();
3832 let width = ed.settings().shiftwidth * count.max(1);
3833 let pad: String = " ".repeat(width);
3834 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3835 let bot = bot.min(lines.len().saturating_sub(1));
3836 for line in lines.iter_mut().take(bot + 1).skip(top) {
3837 if !line.is_empty() {
3838 line.insert_str(0, &pad);
3839 }
3840 }
3841 ed.restore(lines, (top, 0));
3844 move_first_non_whitespace(ed);
3845}
3846
3847fn outdent_rows<H: crate::types::Host>(
3851 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3852 top: usize,
3853 bot: usize,
3854 count: usize,
3855) {
3856 ed.sync_buffer_content_from_textarea();
3857 let width = ed.settings().shiftwidth * count.max(1);
3858 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3859 let bot = bot.min(lines.len().saturating_sub(1));
3860 for line in lines.iter_mut().take(bot + 1).skip(top) {
3861 let strip: usize = line
3862 .chars()
3863 .take(width)
3864 .take_while(|c| *c == ' ' || *c == '\t')
3865 .count();
3866 if strip > 0 {
3867 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3868 line.drain(..byte_len);
3869 }
3870 }
3871 ed.restore(lines, (top, 0));
3872 move_first_non_whitespace(ed);
3873}
3874
3875fn toggle_case_str(s: &str) -> String {
3876 s.chars()
3877 .map(|c| {
3878 if c.is_lowercase() {
3879 c.to_uppercase().next().unwrap_or(c)
3880 } else if c.is_uppercase() {
3881 c.to_lowercase().next().unwrap_or(c)
3882 } else {
3883 c
3884 }
3885 })
3886 .collect()
3887}
3888
3889fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3890 if a <= b { (a, b) } else { (b, a) }
3891}
3892
3893fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3898 let (row, col) = ed.cursor();
3899 let line_chars = buf_line_chars(&ed.buffer, row);
3900 let max_col = line_chars.saturating_sub(1);
3901 if col > max_col {
3902 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
3903 ed.push_buffer_cursor_to_textarea();
3904 }
3905}
3906
3907fn execute_line_op<H: crate::types::Host>(
3910 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3911 op: Operator,
3912 count: usize,
3913) {
3914 let (row, col) = ed.cursor();
3915 let total = buf_row_count(&ed.buffer);
3916 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3917
3918 match op {
3919 Operator::Yank => {
3920 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3922 if !text.is_empty() {
3923 ed.record_yank_to_host(text.clone());
3924 ed.record_yank(text, true);
3925 }
3926 buf_set_cursor_rc(&mut ed.buffer, row, col);
3927 ed.push_buffer_cursor_to_textarea();
3928 ed.vim.mode = Mode::Normal;
3929 }
3930 Operator::Delete => {
3931 ed.push_undo();
3932 let deleted_through_last = end_row + 1 >= total;
3933 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3934 let total_after = buf_row_count(&ed.buffer);
3938 let raw_target = if deleted_through_last {
3939 row.saturating_sub(1).min(total_after.saturating_sub(1))
3940 } else {
3941 row.min(total_after.saturating_sub(1))
3942 };
3943 let target_row = if raw_target > 0
3949 && raw_target + 1 == total_after
3950 && buf_line(&ed.buffer, raw_target)
3951 .map(str::is_empty)
3952 .unwrap_or(false)
3953 {
3954 raw_target - 1
3955 } else {
3956 raw_target
3957 };
3958 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
3959 ed.push_buffer_cursor_to_textarea();
3960 move_first_non_whitespace(ed);
3961 ed.sticky_col = Some(ed.cursor().1);
3962 ed.vim.mode = Mode::Normal;
3963 }
3964 Operator::Change => {
3965 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3969 ed.push_undo();
3970 ed.sync_buffer_content_from_textarea();
3971 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3973 if end_row > row {
3974 ed.mutate_edit(Edit::DeleteRange {
3975 start: Position::new(row + 1, 0),
3976 end: Position::new(end_row, 0),
3977 kind: BufKind::Line,
3978 });
3979 }
3980 let line_chars = buf_line_chars(&ed.buffer, row);
3981 if line_chars > 0 {
3982 ed.mutate_edit(Edit::DeleteRange {
3983 start: Position::new(row, 0),
3984 end: Position::new(row, line_chars),
3985 kind: BufKind::Char,
3986 });
3987 }
3988 if !payload.is_empty() {
3989 ed.record_yank_to_host(payload.clone());
3990 ed.record_delete(payload, true);
3991 }
3992 buf_set_cursor_rc(&mut ed.buffer, row, 0);
3993 ed.push_buffer_cursor_to_textarea();
3994 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3995 }
3996 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3997 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4001 move_first_non_whitespace(ed);
4004 }
4005 Operator::Indent | Operator::Outdent => {
4006 ed.push_undo();
4008 if op == Operator::Indent {
4009 indent_rows(ed, row, end_row, 1);
4010 } else {
4011 outdent_rows(ed, row, end_row, 1);
4012 }
4013 ed.sticky_col = Some(ed.cursor().1);
4014 ed.vim.mode = Mode::Normal;
4015 }
4016 Operator::Fold => unreachable!("Fold has no line-op double"),
4018 Operator::Reflow => {
4019 ed.push_undo();
4021 reflow_rows(ed, row, end_row);
4022 move_first_non_whitespace(ed);
4023 ed.sticky_col = Some(ed.cursor().1);
4024 ed.vim.mode = Mode::Normal;
4025 }
4026 }
4027}
4028
4029fn apply_visual_operator<H: crate::types::Host>(
4032 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4033 op: Operator,
4034) {
4035 match ed.vim.mode {
4036 Mode::VisualLine => {
4037 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4038 let top = cursor_row.min(ed.vim.visual_line_anchor);
4039 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4040 ed.vim.yank_linewise = true;
4041 match op {
4042 Operator::Yank => {
4043 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4044 if !text.is_empty() {
4045 ed.record_yank_to_host(text.clone());
4046 ed.record_yank(text, true);
4047 }
4048 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4049 ed.push_buffer_cursor_to_textarea();
4050 ed.vim.mode = Mode::Normal;
4051 }
4052 Operator::Delete => {
4053 ed.push_undo();
4054 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4055 ed.vim.mode = Mode::Normal;
4056 }
4057 Operator::Change => {
4058 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4061 ed.push_undo();
4062 ed.sync_buffer_content_from_textarea();
4063 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4064 if bot > top {
4065 ed.mutate_edit(Edit::DeleteRange {
4066 start: Position::new(top + 1, 0),
4067 end: Position::new(bot, 0),
4068 kind: BufKind::Line,
4069 });
4070 }
4071 let line_chars = buf_line_chars(&ed.buffer, top);
4072 if line_chars > 0 {
4073 ed.mutate_edit(Edit::DeleteRange {
4074 start: Position::new(top, 0),
4075 end: Position::new(top, line_chars),
4076 kind: BufKind::Char,
4077 });
4078 }
4079 if !payload.is_empty() {
4080 ed.record_yank_to_host(payload.clone());
4081 ed.record_delete(payload, true);
4082 }
4083 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4084 ed.push_buffer_cursor_to_textarea();
4085 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4086 }
4087 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4088 let bot = buf_cursor_pos(&ed.buffer)
4089 .row
4090 .max(ed.vim.visual_line_anchor);
4091 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4092 move_first_non_whitespace(ed);
4093 }
4094 Operator::Indent | Operator::Outdent => {
4095 ed.push_undo();
4096 let (cursor_row, _) = ed.cursor();
4097 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4098 if op == Operator::Indent {
4099 indent_rows(ed, top, bot, 1);
4100 } else {
4101 outdent_rows(ed, top, bot, 1);
4102 }
4103 ed.vim.mode = Mode::Normal;
4104 }
4105 Operator::Reflow => {
4106 ed.push_undo();
4107 let (cursor_row, _) = ed.cursor();
4108 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4109 reflow_rows(ed, top, bot);
4110 ed.vim.mode = Mode::Normal;
4111 }
4112 Operator::Fold => unreachable!("Visual zf takes its own path"),
4115 }
4116 }
4117 Mode::Visual => {
4118 ed.vim.yank_linewise = false;
4119 let anchor = ed.vim.visual_anchor;
4120 let cursor = ed.cursor();
4121 let (top, bot) = order(anchor, cursor);
4122 match op {
4123 Operator::Yank => {
4124 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4125 if !text.is_empty() {
4126 ed.record_yank_to_host(text.clone());
4127 ed.record_yank(text, false);
4128 }
4129 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4130 ed.push_buffer_cursor_to_textarea();
4131 ed.vim.mode = Mode::Normal;
4132 }
4133 Operator::Delete => {
4134 ed.push_undo();
4135 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4136 ed.vim.mode = Mode::Normal;
4137 }
4138 Operator::Change => {
4139 ed.push_undo();
4140 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4141 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4142 }
4143 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4144 let anchor = ed.vim.visual_anchor;
4146 let cursor = ed.cursor();
4147 let (top, bot) = order(anchor, cursor);
4148 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4149 }
4150 Operator::Indent | Operator::Outdent => {
4151 ed.push_undo();
4152 let anchor = ed.vim.visual_anchor;
4153 let cursor = ed.cursor();
4154 let (top, bot) = order(anchor, cursor);
4155 if op == Operator::Indent {
4156 indent_rows(ed, top.0, bot.0, 1);
4157 } else {
4158 outdent_rows(ed, top.0, bot.0, 1);
4159 }
4160 ed.vim.mode = Mode::Normal;
4161 }
4162 Operator::Reflow => {
4163 ed.push_undo();
4164 let anchor = ed.vim.visual_anchor;
4165 let cursor = ed.cursor();
4166 let (top, bot) = order(anchor, cursor);
4167 reflow_rows(ed, top.0, bot.0);
4168 ed.vim.mode = Mode::Normal;
4169 }
4170 Operator::Fold => unreachable!("Visual zf takes its own path"),
4171 }
4172 }
4173 Mode::VisualBlock => apply_block_operator(ed, op),
4174 _ => {}
4175 }
4176}
4177
4178fn block_bounds<H: crate::types::Host>(
4183 ed: &Editor<hjkl_buffer::Buffer, H>,
4184) -> (usize, usize, usize, usize) {
4185 let (ar, ac) = ed.vim.block_anchor;
4186 let (cr, _) = ed.cursor();
4187 let cc = ed.vim.block_vcol;
4188 let top = ar.min(cr);
4189 let bot = ar.max(cr);
4190 let left = ac.min(cc);
4191 let right = ac.max(cc);
4192 (top, bot, left, right)
4193}
4194
4195fn update_block_vcol<H: crate::types::Host>(
4200 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4201 motion: &Motion,
4202) {
4203 match motion {
4204 Motion::Left
4205 | Motion::Right
4206 | Motion::WordFwd
4207 | Motion::BigWordFwd
4208 | Motion::WordBack
4209 | Motion::BigWordBack
4210 | Motion::WordEnd
4211 | Motion::BigWordEnd
4212 | Motion::WordEndBack
4213 | Motion::BigWordEndBack
4214 | Motion::LineStart
4215 | Motion::FirstNonBlank
4216 | Motion::LineEnd
4217 | Motion::Find { .. }
4218 | Motion::FindRepeat { .. }
4219 | Motion::MatchBracket => {
4220 ed.vim.block_vcol = ed.cursor().1;
4221 }
4222 _ => {}
4224 }
4225}
4226
4227fn apply_block_operator<H: crate::types::Host>(
4232 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4233 op: Operator,
4234) {
4235 let (top, bot, left, right) = block_bounds(ed);
4236 let yank = block_yank(ed, top, bot, left, right);
4238
4239 match op {
4240 Operator::Yank => {
4241 if !yank.is_empty() {
4242 ed.record_yank_to_host(yank.clone());
4243 ed.record_yank(yank, false);
4244 }
4245 ed.vim.mode = Mode::Normal;
4246 ed.jump_cursor(top, left);
4247 }
4248 Operator::Delete => {
4249 ed.push_undo();
4250 delete_block_contents(ed, top, bot, left, right);
4251 if !yank.is_empty() {
4252 ed.record_yank_to_host(yank.clone());
4253 ed.record_delete(yank, false);
4254 }
4255 ed.vim.mode = Mode::Normal;
4256 ed.jump_cursor(top, left);
4257 }
4258 Operator::Change => {
4259 ed.push_undo();
4260 delete_block_contents(ed, top, bot, left, right);
4261 if !yank.is_empty() {
4262 ed.record_yank_to_host(yank.clone());
4263 ed.record_delete(yank, false);
4264 }
4265 ed.jump_cursor(top, left);
4266 begin_insert_noundo(
4267 ed,
4268 1,
4269 InsertReason::BlockEdge {
4270 top,
4271 bot,
4272 col: left,
4273 },
4274 );
4275 }
4276 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4277 ed.push_undo();
4278 transform_block_case(ed, op, top, bot, left, right);
4279 ed.vim.mode = Mode::Normal;
4280 ed.jump_cursor(top, left);
4281 }
4282 Operator::Indent | Operator::Outdent => {
4283 ed.push_undo();
4287 if op == Operator::Indent {
4288 indent_rows(ed, top, bot, 1);
4289 } else {
4290 outdent_rows(ed, top, bot, 1);
4291 }
4292 ed.vim.mode = Mode::Normal;
4293 }
4294 Operator::Fold => unreachable!("Visual zf takes its own path"),
4295 Operator::Reflow => {
4296 ed.push_undo();
4300 reflow_rows(ed, top, bot);
4301 ed.vim.mode = Mode::Normal;
4302 }
4303 }
4304}
4305
4306fn transform_block_case<H: crate::types::Host>(
4310 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4311 op: Operator,
4312 top: usize,
4313 bot: usize,
4314 left: usize,
4315 right: usize,
4316) {
4317 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4318 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4319 let chars: Vec<char> = lines[r].chars().collect();
4320 if left >= chars.len() {
4321 continue;
4322 }
4323 let end = (right + 1).min(chars.len());
4324 let head: String = chars[..left].iter().collect();
4325 let mid: String = chars[left..end].iter().collect();
4326 let tail: String = chars[end..].iter().collect();
4327 let transformed = match op {
4328 Operator::Uppercase => mid.to_uppercase(),
4329 Operator::Lowercase => mid.to_lowercase(),
4330 Operator::ToggleCase => toggle_case_str(&mid),
4331 _ => mid,
4332 };
4333 lines[r] = format!("{head}{transformed}{tail}");
4334 }
4335 let saved_yank = ed.yank().to_string();
4336 let saved_linewise = ed.vim.yank_linewise;
4337 ed.restore(lines, (top, left));
4338 ed.set_yank(saved_yank);
4339 ed.vim.yank_linewise = saved_linewise;
4340}
4341
4342fn block_yank<H: crate::types::Host>(
4343 ed: &Editor<hjkl_buffer::Buffer, H>,
4344 top: usize,
4345 bot: usize,
4346 left: usize,
4347 right: usize,
4348) -> String {
4349 let lines = buf_lines_to_vec(&ed.buffer);
4350 let mut rows: Vec<String> = Vec::new();
4351 for r in top..=bot {
4352 let line = match lines.get(r) {
4353 Some(l) => l,
4354 None => break,
4355 };
4356 let chars: Vec<char> = line.chars().collect();
4357 let end = (right + 1).min(chars.len());
4358 if left >= chars.len() {
4359 rows.push(String::new());
4360 } else {
4361 rows.push(chars[left..end].iter().collect());
4362 }
4363 }
4364 rows.join("\n")
4365}
4366
4367fn delete_block_contents<H: crate::types::Host>(
4368 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4369 top: usize,
4370 bot: usize,
4371 left: usize,
4372 right: usize,
4373) {
4374 use hjkl_buffer::{Edit, MotionKind, Position};
4375 ed.sync_buffer_content_from_textarea();
4376 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4377 if last_row < top {
4378 return;
4379 }
4380 ed.mutate_edit(Edit::DeleteRange {
4381 start: Position::new(top, left),
4382 end: Position::new(last_row, right),
4383 kind: MotionKind::Block,
4384 });
4385 ed.push_buffer_cursor_to_textarea();
4386}
4387
4388fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4390 let (top, bot, left, right) = block_bounds(ed);
4391 ed.push_undo();
4392 ed.sync_buffer_content_from_textarea();
4393 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4394 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4395 let chars: Vec<char> = lines[r].chars().collect();
4396 if left >= chars.len() {
4397 continue;
4398 }
4399 let end = (right + 1).min(chars.len());
4400 let before: String = chars[..left].iter().collect();
4401 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4402 let after: String = chars[end..].iter().collect();
4403 lines[r] = format!("{before}{middle}{after}");
4404 }
4405 reset_textarea_lines(ed, lines);
4406 ed.vim.mode = Mode::Normal;
4407 ed.jump_cursor(top, left);
4408}
4409
4410fn reset_textarea_lines<H: crate::types::Host>(
4414 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4415 lines: Vec<String>,
4416) {
4417 let cursor = ed.cursor();
4418 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4419 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4420 ed.mark_content_dirty();
4421}
4422
4423type Pos = (usize, usize);
4429
4430fn text_object_range<H: crate::types::Host>(
4434 ed: &Editor<hjkl_buffer::Buffer, H>,
4435 obj: TextObject,
4436 inner: bool,
4437) -> Option<(Pos, Pos, MotionKind)> {
4438 match obj {
4439 TextObject::Word { big } => {
4440 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4441 }
4442 TextObject::Quote(q) => {
4443 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4444 }
4445 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4446 TextObject::Paragraph => {
4447 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4448 }
4449 TextObject::XmlTag => {
4450 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4451 }
4452 TextObject::Sentence => {
4453 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4454 }
4455 }
4456}
4457
4458fn sentence_boundary<H: crate::types::Host>(
4462 ed: &Editor<hjkl_buffer::Buffer, H>,
4463 forward: bool,
4464) -> Option<(usize, usize)> {
4465 let lines = buf_lines_to_vec(&ed.buffer);
4466 if lines.is_empty() {
4467 return None;
4468 }
4469 let pos_to_idx = |pos: (usize, usize)| -> usize {
4470 let mut idx = 0;
4471 for line in lines.iter().take(pos.0) {
4472 idx += line.chars().count() + 1;
4473 }
4474 idx + pos.1
4475 };
4476 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4477 for (r, line) in lines.iter().enumerate() {
4478 let len = line.chars().count();
4479 if idx <= len {
4480 return (r, idx);
4481 }
4482 idx -= len + 1;
4483 }
4484 let last = lines.len().saturating_sub(1);
4485 (last, lines[last].chars().count())
4486 };
4487 let mut chars: Vec<char> = Vec::new();
4488 for (r, line) in lines.iter().enumerate() {
4489 chars.extend(line.chars());
4490 if r + 1 < lines.len() {
4491 chars.push('\n');
4492 }
4493 }
4494 if chars.is_empty() {
4495 return None;
4496 }
4497 let total = chars.len();
4498 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4499 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4500
4501 if forward {
4502 let mut i = cursor_idx + 1;
4505 while i < total {
4506 if is_terminator(chars[i]) {
4507 while i + 1 < total && is_terminator(chars[i + 1]) {
4508 i += 1;
4509 }
4510 if i + 1 >= total {
4511 return None;
4512 }
4513 if chars[i + 1].is_whitespace() {
4514 let mut j = i + 1;
4515 while j < total && chars[j].is_whitespace() {
4516 j += 1;
4517 }
4518 if j >= total {
4519 return None;
4520 }
4521 return Some(idx_to_pos(j));
4522 }
4523 }
4524 i += 1;
4525 }
4526 None
4527 } else {
4528 let find_start = |from: usize| -> Option<usize> {
4532 let mut start = from;
4533 while start > 0 {
4534 let prev = chars[start - 1];
4535 if prev.is_whitespace() {
4536 let mut k = start - 1;
4537 while k > 0 && chars[k - 1].is_whitespace() {
4538 k -= 1;
4539 }
4540 if k > 0 && is_terminator(chars[k - 1]) {
4541 break;
4542 }
4543 }
4544 start -= 1;
4545 }
4546 while start < total && chars[start].is_whitespace() {
4547 start += 1;
4548 }
4549 (start < total).then_some(start)
4550 };
4551 let current_start = find_start(cursor_idx)?;
4552 if current_start < cursor_idx {
4553 return Some(idx_to_pos(current_start));
4554 }
4555 let mut k = current_start;
4558 while k > 0 && chars[k - 1].is_whitespace() {
4559 k -= 1;
4560 }
4561 if k == 0 {
4562 return None;
4563 }
4564 let prev_start = find_start(k - 1)?;
4565 Some(idx_to_pos(prev_start))
4566 }
4567}
4568
4569fn sentence_text_object<H: crate::types::Host>(
4575 ed: &Editor<hjkl_buffer::Buffer, H>,
4576 inner: bool,
4577) -> Option<((usize, usize), (usize, usize))> {
4578 let lines = buf_lines_to_vec(&ed.buffer);
4579 if lines.is_empty() {
4580 return None;
4581 }
4582 let pos_to_idx = |pos: (usize, usize)| -> usize {
4585 let mut idx = 0;
4586 for line in lines.iter().take(pos.0) {
4587 idx += line.chars().count() + 1;
4588 }
4589 idx + pos.1
4590 };
4591 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4592 for (r, line) in lines.iter().enumerate() {
4593 let len = line.chars().count();
4594 if idx <= len {
4595 return (r, idx);
4596 }
4597 idx -= len + 1;
4598 }
4599 let last = lines.len().saturating_sub(1);
4600 (last, lines[last].chars().count())
4601 };
4602 let mut chars: Vec<char> = Vec::new();
4603 for (r, line) in lines.iter().enumerate() {
4604 chars.extend(line.chars());
4605 if r + 1 < lines.len() {
4606 chars.push('\n');
4607 }
4608 }
4609 if chars.is_empty() {
4610 return None;
4611 }
4612
4613 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4614 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4615
4616 let mut start = cursor_idx;
4620 while start > 0 {
4621 let prev = chars[start - 1];
4622 if prev.is_whitespace() {
4623 let mut k = start - 1;
4627 while k > 0 && chars[k - 1].is_whitespace() {
4628 k -= 1;
4629 }
4630 if k > 0 && is_terminator(chars[k - 1]) {
4631 break;
4632 }
4633 }
4634 start -= 1;
4635 }
4636 while start < chars.len() && chars[start].is_whitespace() {
4639 start += 1;
4640 }
4641 if start >= chars.len() {
4642 return None;
4643 }
4644
4645 let mut end = start;
4648 while end < chars.len() {
4649 if is_terminator(chars[end]) {
4650 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4652 end += 1;
4653 }
4654 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4657 break;
4658 }
4659 }
4660 end += 1;
4661 }
4662 let end_idx = (end + 1).min(chars.len());
4664
4665 let final_end = if inner {
4666 end_idx
4667 } else {
4668 let mut e = end_idx;
4672 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4673 e += 1;
4674 }
4675 e
4676 };
4677
4678 Some((idx_to_pos(start), idx_to_pos(final_end)))
4679}
4680
4681fn tag_text_object<H: crate::types::Host>(
4685 ed: &Editor<hjkl_buffer::Buffer, H>,
4686 inner: bool,
4687) -> Option<((usize, usize), (usize, usize))> {
4688 let lines = buf_lines_to_vec(&ed.buffer);
4689 if lines.is_empty() {
4690 return None;
4691 }
4692 let pos_to_idx = |pos: (usize, usize)| -> usize {
4696 let mut idx = 0;
4697 for line in lines.iter().take(pos.0) {
4698 idx += line.chars().count() + 1;
4699 }
4700 idx + pos.1
4701 };
4702 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4703 for (r, line) in lines.iter().enumerate() {
4704 let len = line.chars().count();
4705 if idx <= len {
4706 return (r, idx);
4707 }
4708 idx -= len + 1;
4709 }
4710 let last = lines.len().saturating_sub(1);
4711 (last, lines[last].chars().count())
4712 };
4713 let mut chars: Vec<char> = Vec::new();
4714 for (r, line) in lines.iter().enumerate() {
4715 chars.extend(line.chars());
4716 if r + 1 < lines.len() {
4717 chars.push('\n');
4718 }
4719 }
4720 let cursor_idx = pos_to_idx(ed.cursor());
4721
4722 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4730 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4731 let mut i = 0;
4732 while i < chars.len() {
4733 if chars[i] != '<' {
4734 i += 1;
4735 continue;
4736 }
4737 let mut j = i + 1;
4738 while j < chars.len() && chars[j] != '>' {
4739 j += 1;
4740 }
4741 if j >= chars.len() {
4742 break;
4743 }
4744 let inside: String = chars[i + 1..j].iter().collect();
4745 let close_end = j + 1;
4746 let trimmed = inside.trim();
4747 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4748 i = close_end;
4749 continue;
4750 }
4751 if let Some(rest) = trimmed.strip_prefix('/') {
4752 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4753 if !name.is_empty()
4754 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4755 {
4756 let (open_start, content_start, _) = stack[stack_idx].clone();
4757 stack.truncate(stack_idx);
4758 let content_end = i;
4759 let candidate = (open_start, content_start, content_end, close_end);
4760 if cursor_idx >= content_start && cursor_idx <= content_end {
4761 innermost = match innermost {
4762 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4763 Some(candidate)
4764 }
4765 None => Some(candidate),
4766 existing => existing,
4767 };
4768 } else if open_start >= cursor_idx && next_after.is_none() {
4769 next_after = Some(candidate);
4770 }
4771 }
4772 } else if !trimmed.ends_with('/') {
4773 let name: String = trimmed
4774 .split(|c: char| c.is_whitespace() || c == '/')
4775 .next()
4776 .unwrap_or("")
4777 .to_string();
4778 if !name.is_empty() {
4779 stack.push((i, close_end, name));
4780 }
4781 }
4782 i = close_end;
4783 }
4784
4785 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4786 if inner {
4787 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4788 } else {
4789 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4790 }
4791}
4792
4793fn is_wordchar(c: char) -> bool {
4794 c.is_alphanumeric() || c == '_'
4795}
4796
4797pub(crate) use hjkl_buffer::is_keyword_char;
4801
4802fn word_text_object<H: crate::types::Host>(
4803 ed: &Editor<hjkl_buffer::Buffer, H>,
4804 inner: bool,
4805 big: bool,
4806) -> Option<((usize, usize), (usize, usize))> {
4807 let (row, col) = ed.cursor();
4808 let line = buf_line(&ed.buffer, row)?;
4809 let chars: Vec<char> = line.chars().collect();
4810 if chars.is_empty() {
4811 return None;
4812 }
4813 let at = col.min(chars.len().saturating_sub(1));
4814 let classify = |c: char| -> u8 {
4815 if c.is_whitespace() {
4816 0
4817 } else if big || is_wordchar(c) {
4818 1
4819 } else {
4820 2
4821 }
4822 };
4823 let cls = classify(chars[at]);
4824 let mut start = at;
4825 while start > 0 && classify(chars[start - 1]) == cls {
4826 start -= 1;
4827 }
4828 let mut end = at;
4829 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4830 end += 1;
4831 }
4832 let char_byte = |i: usize| {
4834 if i >= chars.len() {
4835 line.len()
4836 } else {
4837 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4838 }
4839 };
4840 let mut start_col = char_byte(start);
4841 let mut end_col = char_byte(end + 1);
4843 if !inner {
4844 let mut t = end + 1;
4846 let mut included_trailing = false;
4847 while t < chars.len() && chars[t].is_whitespace() {
4848 included_trailing = true;
4849 t += 1;
4850 }
4851 if included_trailing {
4852 end_col = char_byte(t);
4853 } else {
4854 let mut s = start;
4855 while s > 0 && chars[s - 1].is_whitespace() {
4856 s -= 1;
4857 }
4858 start_col = char_byte(s);
4859 }
4860 }
4861 Some(((row, start_col), (row, end_col)))
4862}
4863
4864fn quote_text_object<H: crate::types::Host>(
4865 ed: &Editor<hjkl_buffer::Buffer, H>,
4866 q: char,
4867 inner: bool,
4868) -> Option<((usize, usize), (usize, usize))> {
4869 let (row, col) = ed.cursor();
4870 let line = buf_line(&ed.buffer, row)?;
4871 let bytes = line.as_bytes();
4872 let q_byte = q as u8;
4873 let mut positions: Vec<usize> = Vec::new();
4875 for (i, &b) in bytes.iter().enumerate() {
4876 if b == q_byte {
4877 positions.push(i);
4878 }
4879 }
4880 if positions.len() < 2 {
4881 return None;
4882 }
4883 let mut open_idx: Option<usize> = None;
4884 let mut close_idx: Option<usize> = None;
4885 for pair in positions.chunks(2) {
4886 if pair.len() < 2 {
4887 break;
4888 }
4889 if col >= pair[0] && col <= pair[1] {
4890 open_idx = Some(pair[0]);
4891 close_idx = Some(pair[1]);
4892 break;
4893 }
4894 if col < pair[0] {
4895 open_idx = Some(pair[0]);
4896 close_idx = Some(pair[1]);
4897 break;
4898 }
4899 }
4900 let open = open_idx?;
4901 let close = close_idx?;
4902 if inner {
4904 if close <= open + 1 {
4905 return None;
4906 }
4907 Some(((row, open + 1), (row, close)))
4908 } else {
4909 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
4916 let mut end = after_close;
4918 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
4919 end += 1;
4920 }
4921 Some(((row, open), (row, end)))
4922 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
4923 let mut start = open;
4925 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
4926 start -= 1;
4927 }
4928 Some(((row, start), (row, close + 1)))
4929 } else {
4930 Some(((row, open), (row, close + 1)))
4931 }
4932 }
4933}
4934
4935fn bracket_text_object<H: crate::types::Host>(
4936 ed: &Editor<hjkl_buffer::Buffer, H>,
4937 open: char,
4938 inner: bool,
4939) -> Option<(Pos, Pos, MotionKind)> {
4940 let close = match open {
4941 '(' => ')',
4942 '[' => ']',
4943 '{' => '}',
4944 '<' => '>',
4945 _ => return None,
4946 };
4947 let (row, col) = ed.cursor();
4948 let lines = buf_lines_to_vec(&ed.buffer);
4949 let lines = lines.as_slice();
4950 let open_pos = find_open_bracket(lines, row, col, open, close)
4955 .or_else(|| find_next_open(lines, row, col, open))?;
4956 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
4957 if inner {
4959 if close_pos.0 > open_pos.0 + 1 {
4965 let inner_row_start = open_pos.0 + 1;
4967 let inner_row_end = close_pos.0 - 1;
4968 let end_col = lines
4969 .get(inner_row_end)
4970 .map(|l| l.chars().count())
4971 .unwrap_or(0);
4972 return Some((
4973 (inner_row_start, 0),
4974 (inner_row_end, end_col),
4975 MotionKind::Linewise,
4976 ));
4977 }
4978 let inner_start = advance_pos(lines, open_pos);
4979 if inner_start.0 > close_pos.0
4980 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
4981 {
4982 return None;
4983 }
4984 Some((inner_start, close_pos, MotionKind::Exclusive))
4985 } else {
4986 Some((
4987 open_pos,
4988 advance_pos(lines, close_pos),
4989 MotionKind::Exclusive,
4990 ))
4991 }
4992}
4993
4994fn find_open_bracket(
4995 lines: &[String],
4996 row: usize,
4997 col: usize,
4998 open: char,
4999 close: char,
5000) -> Option<(usize, usize)> {
5001 let mut depth: i32 = 0;
5002 let mut r = row;
5003 let mut c = col as isize;
5004 loop {
5005 let cur = &lines[r];
5006 let chars: Vec<char> = cur.chars().collect();
5007 if (c as usize) >= chars.len() {
5011 c = chars.len() as isize - 1;
5012 }
5013 while c >= 0 {
5014 let ch = chars[c as usize];
5015 if ch == close {
5016 depth += 1;
5017 } else if ch == open {
5018 if depth == 0 {
5019 return Some((r, c as usize));
5020 }
5021 depth -= 1;
5022 }
5023 c -= 1;
5024 }
5025 if r == 0 {
5026 return None;
5027 }
5028 r -= 1;
5029 c = lines[r].chars().count() as isize - 1;
5030 }
5031}
5032
5033fn find_close_bracket(
5034 lines: &[String],
5035 row: usize,
5036 start_col: usize,
5037 open: char,
5038 close: char,
5039) -> Option<(usize, usize)> {
5040 let mut depth: i32 = 0;
5041 let mut r = row;
5042 let mut c = start_col;
5043 loop {
5044 let cur = &lines[r];
5045 let chars: Vec<char> = cur.chars().collect();
5046 while c < chars.len() {
5047 let ch = chars[c];
5048 if ch == open {
5049 depth += 1;
5050 } else if ch == close {
5051 if depth == 0 {
5052 return Some((r, c));
5053 }
5054 depth -= 1;
5055 }
5056 c += 1;
5057 }
5058 if r + 1 >= lines.len() {
5059 return None;
5060 }
5061 r += 1;
5062 c = 0;
5063 }
5064}
5065
5066fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5070 let mut r = row;
5071 let mut c = col;
5072 while r < lines.len() {
5073 let chars: Vec<char> = lines[r].chars().collect();
5074 while c < chars.len() {
5075 if chars[c] == open {
5076 return Some((r, c));
5077 }
5078 c += 1;
5079 }
5080 r += 1;
5081 c = 0;
5082 }
5083 None
5084}
5085
5086fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5087 let (r, c) = pos;
5088 let line_len = lines[r].chars().count();
5089 if c < line_len {
5090 (r, c + 1)
5091 } else if r + 1 < lines.len() {
5092 (r + 1, 0)
5093 } else {
5094 pos
5095 }
5096}
5097
5098fn paragraph_text_object<H: crate::types::Host>(
5099 ed: &Editor<hjkl_buffer::Buffer, H>,
5100 inner: bool,
5101) -> Option<((usize, usize), (usize, usize))> {
5102 let (row, _) = ed.cursor();
5103 let lines = buf_lines_to_vec(&ed.buffer);
5104 if lines.is_empty() {
5105 return None;
5106 }
5107 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5109 if is_blank(row) {
5110 return None;
5111 }
5112 let mut top = row;
5113 while top > 0 && !is_blank(top - 1) {
5114 top -= 1;
5115 }
5116 let mut bot = row;
5117 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5118 bot += 1;
5119 }
5120 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5122 bot += 1;
5123 }
5124 let end_col = lines[bot].chars().count();
5125 Some(((top, 0), (bot, end_col)))
5126}
5127
5128fn read_vim_range<H: crate::types::Host>(
5134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5135 start: (usize, usize),
5136 end: (usize, usize),
5137 kind: MotionKind,
5138) -> String {
5139 let (top, bot) = order(start, end);
5140 ed.sync_buffer_content_from_textarea();
5141 let lines = buf_lines_to_vec(&ed.buffer);
5142 match kind {
5143 MotionKind::Linewise => {
5144 let lo = top.0;
5145 let hi = bot.0.min(lines.len().saturating_sub(1));
5146 let mut text = lines[lo..=hi].join("\n");
5147 text.push('\n');
5148 text
5149 }
5150 MotionKind::Inclusive | MotionKind::Exclusive => {
5151 let inclusive = matches!(kind, MotionKind::Inclusive);
5152 let mut out = String::new();
5154 for row in top.0..=bot.0 {
5155 let line = lines.get(row).map(String::as_str).unwrap_or("");
5156 let lo = if row == top.0 { top.1 } else { 0 };
5157 let hi_unclamped = if row == bot.0 {
5158 if inclusive { bot.1 + 1 } else { bot.1 }
5159 } else {
5160 line.chars().count() + 1
5161 };
5162 let row_chars: Vec<char> = line.chars().collect();
5163 let hi = hi_unclamped.min(row_chars.len());
5164 if lo < hi {
5165 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5166 }
5167 if row < bot.0 {
5168 out.push('\n');
5169 }
5170 }
5171 out
5172 }
5173 }
5174}
5175
5176fn cut_vim_range<H: crate::types::Host>(
5185 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5186 start: (usize, usize),
5187 end: (usize, usize),
5188 kind: MotionKind,
5189) -> String {
5190 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5191 let (top, bot) = order(start, end);
5192 ed.sync_buffer_content_from_textarea();
5193 let (buf_start, buf_end, buf_kind) = match kind {
5194 MotionKind::Linewise => (
5195 Position::new(top.0, 0),
5196 Position::new(bot.0, 0),
5197 BufKind::Line,
5198 ),
5199 MotionKind::Inclusive => {
5200 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5201 let next = if bot.1 < line_chars {
5205 Position::new(bot.0, bot.1 + 1)
5206 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5207 Position::new(bot.0 + 1, 0)
5208 } else {
5209 Position::new(bot.0, line_chars)
5210 };
5211 (Position::new(top.0, top.1), next, BufKind::Char)
5212 }
5213 MotionKind::Exclusive => (
5214 Position::new(top.0, top.1),
5215 Position::new(bot.0, bot.1),
5216 BufKind::Char,
5217 ),
5218 };
5219 let inverse = ed.mutate_edit(Edit::DeleteRange {
5220 start: buf_start,
5221 end: buf_end,
5222 kind: buf_kind,
5223 });
5224 let text = match inverse {
5225 Edit::InsertStr { text, .. } => text,
5226 _ => String::new(),
5227 };
5228 if !text.is_empty() {
5229 ed.record_yank_to_host(text.clone());
5230 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5231 }
5232 ed.push_buffer_cursor_to_textarea();
5233 text
5234}
5235
5236fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5242 use hjkl_buffer::{Edit, MotionKind, Position};
5243 ed.sync_buffer_content_from_textarea();
5244 let cursor = buf_cursor_pos(&ed.buffer);
5245 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5246 if cursor.col >= line_chars {
5247 return;
5248 }
5249 let inverse = ed.mutate_edit(Edit::DeleteRange {
5250 start: cursor,
5251 end: Position::new(cursor.row, line_chars),
5252 kind: MotionKind::Char,
5253 });
5254 if let Edit::InsertStr { text, .. } = inverse
5255 && !text.is_empty()
5256 {
5257 ed.record_yank_to_host(text.clone());
5258 ed.vim.yank_linewise = false;
5259 ed.set_yank(text);
5260 }
5261 buf_set_cursor_pos(&mut ed.buffer, cursor);
5262 ed.push_buffer_cursor_to_textarea();
5263}
5264
5265fn do_char_delete<H: crate::types::Host>(
5266 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5267 forward: bool,
5268 count: usize,
5269) {
5270 use hjkl_buffer::{Edit, MotionKind, Position};
5271 ed.push_undo();
5272 ed.sync_buffer_content_from_textarea();
5273 let mut deleted = String::new();
5276 for _ in 0..count {
5277 let cursor = buf_cursor_pos(&ed.buffer);
5278 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5279 if forward {
5280 if cursor.col >= line_chars {
5283 continue;
5284 }
5285 let inverse = ed.mutate_edit(Edit::DeleteRange {
5286 start: cursor,
5287 end: Position::new(cursor.row, cursor.col + 1),
5288 kind: MotionKind::Char,
5289 });
5290 if let Edit::InsertStr { text, .. } = inverse {
5291 deleted.push_str(&text);
5292 }
5293 } else {
5294 if cursor.col == 0 {
5296 continue;
5297 }
5298 let inverse = ed.mutate_edit(Edit::DeleteRange {
5299 start: Position::new(cursor.row, cursor.col - 1),
5300 end: cursor,
5301 kind: MotionKind::Char,
5302 });
5303 if let Edit::InsertStr { text, .. } = inverse {
5304 deleted = text + &deleted;
5307 }
5308 }
5309 }
5310 if !deleted.is_empty() {
5311 ed.record_yank_to_host(deleted.clone());
5312 ed.record_delete(deleted, false);
5313 }
5314 ed.push_buffer_cursor_to_textarea();
5315}
5316
5317fn adjust_number<H: crate::types::Host>(
5321 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5322 delta: i64,
5323) -> bool {
5324 use hjkl_buffer::{Edit, MotionKind, Position};
5325 ed.sync_buffer_content_from_textarea();
5326 let cursor = buf_cursor_pos(&ed.buffer);
5327 let row = cursor.row;
5328 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5329 Some(l) => l.chars().collect(),
5330 None => return false,
5331 };
5332 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5333 return false;
5334 };
5335 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5336 digit_start - 1
5337 } else {
5338 digit_start
5339 };
5340 let mut span_end = digit_start;
5341 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5342 span_end += 1;
5343 }
5344 let s: String = chars[span_start..span_end].iter().collect();
5345 let Ok(n) = s.parse::<i64>() else {
5346 return false;
5347 };
5348 let new_s = n.saturating_add(delta).to_string();
5349
5350 ed.push_undo();
5351 let span_start_pos = Position::new(row, span_start);
5352 let span_end_pos = Position::new(row, span_end);
5353 ed.mutate_edit(Edit::DeleteRange {
5354 start: span_start_pos,
5355 end: span_end_pos,
5356 kind: MotionKind::Char,
5357 });
5358 ed.mutate_edit(Edit::InsertStr {
5359 at: span_start_pos,
5360 text: new_s.clone(),
5361 });
5362 let new_len = new_s.chars().count();
5363 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5364 ed.push_buffer_cursor_to_textarea();
5365 true
5366}
5367
5368pub(crate) fn replace_char<H: crate::types::Host>(
5369 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5370 ch: char,
5371 count: usize,
5372) {
5373 use hjkl_buffer::{Edit, MotionKind, Position};
5374 ed.push_undo();
5375 ed.sync_buffer_content_from_textarea();
5376 for _ in 0..count {
5377 let cursor = buf_cursor_pos(&ed.buffer);
5378 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5379 if cursor.col >= line_chars {
5380 break;
5381 }
5382 ed.mutate_edit(Edit::DeleteRange {
5383 start: cursor,
5384 end: Position::new(cursor.row, cursor.col + 1),
5385 kind: MotionKind::Char,
5386 });
5387 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5388 }
5389 crate::motions::move_left(&mut ed.buffer, 1);
5391 ed.push_buffer_cursor_to_textarea();
5392}
5393
5394fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5395 use hjkl_buffer::{Edit, MotionKind, Position};
5396 ed.sync_buffer_content_from_textarea();
5397 let cursor = buf_cursor_pos(&ed.buffer);
5398 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5399 return;
5400 };
5401 let toggled = if c.is_uppercase() {
5402 c.to_lowercase().next().unwrap_or(c)
5403 } else {
5404 c.to_uppercase().next().unwrap_or(c)
5405 };
5406 ed.mutate_edit(Edit::DeleteRange {
5407 start: cursor,
5408 end: Position::new(cursor.row, cursor.col + 1),
5409 kind: MotionKind::Char,
5410 });
5411 ed.mutate_edit(Edit::InsertChar {
5412 at: cursor,
5413 ch: toggled,
5414 });
5415}
5416
5417fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5418 use hjkl_buffer::{Edit, Position};
5419 ed.sync_buffer_content_from_textarea();
5420 let row = buf_cursor_pos(&ed.buffer).row;
5421 if row + 1 >= buf_row_count(&ed.buffer) {
5422 return;
5423 }
5424 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5425 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5426 let next_trimmed = next_raw.trim_start();
5427 let cur_chars = cur_line.chars().count();
5428 let next_chars = next_raw.chars().count();
5429 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5432 " "
5433 } else {
5434 ""
5435 };
5436 let joined = format!("{cur_line}{separator}{next_trimmed}");
5437 ed.mutate_edit(Edit::Replace {
5438 start: Position::new(row, 0),
5439 end: Position::new(row + 1, next_chars),
5440 with: joined,
5441 });
5442 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5446 ed.push_buffer_cursor_to_textarea();
5447}
5448
5449fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5452 use hjkl_buffer::Edit;
5453 ed.sync_buffer_content_from_textarea();
5454 let row = buf_cursor_pos(&ed.buffer).row;
5455 if row + 1 >= buf_row_count(&ed.buffer) {
5456 return;
5457 }
5458 let join_col = buf_line_chars(&ed.buffer, row);
5459 ed.mutate_edit(Edit::JoinLines {
5460 row,
5461 count: 1,
5462 with_space: false,
5463 });
5464 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5466 ed.push_buffer_cursor_to_textarea();
5467}
5468
5469fn do_paste<H: crate::types::Host>(
5470 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5471 before: bool,
5472 count: usize,
5473) {
5474 use hjkl_buffer::{Edit, Position};
5475 ed.push_undo();
5476 let selector = ed.vim.pending_register.take();
5481 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5482 Some(slot) => (slot.text.clone(), slot.linewise),
5483 None => {
5489 let s = &ed.registers().unnamed;
5490 (s.text.clone(), s.linewise)
5491 }
5492 };
5493 for _ in 0..count {
5494 ed.sync_buffer_content_from_textarea();
5495 let yank = yank.clone();
5496 if yank.is_empty() {
5497 continue;
5498 }
5499 if linewise {
5500 let text = yank.trim_matches('\n').to_string();
5504 let row = buf_cursor_pos(&ed.buffer).row;
5505 let target_row = if before {
5506 ed.mutate_edit(Edit::InsertStr {
5507 at: Position::new(row, 0),
5508 text: format!("{text}\n"),
5509 });
5510 row
5511 } else {
5512 let line_chars = buf_line_chars(&ed.buffer, row);
5513 ed.mutate_edit(Edit::InsertStr {
5514 at: Position::new(row, line_chars),
5515 text: format!("\n{text}"),
5516 });
5517 row + 1
5518 };
5519 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5520 crate::motions::move_first_non_blank(&mut ed.buffer);
5521 ed.push_buffer_cursor_to_textarea();
5522 } else {
5523 let cursor = buf_cursor_pos(&ed.buffer);
5527 let at = if before {
5528 cursor
5529 } else {
5530 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5531 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5532 };
5533 ed.mutate_edit(Edit::InsertStr {
5534 at,
5535 text: yank.clone(),
5536 });
5537 crate::motions::move_left(&mut ed.buffer, 1);
5540 ed.push_buffer_cursor_to_textarea();
5541 }
5542 }
5543 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5545}
5546
5547pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5548 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5549 let current = ed.snapshot();
5550 ed.redo_stack.push(current);
5551 ed.restore(lines, cursor);
5552 }
5553 ed.vim.mode = Mode::Normal;
5554 clamp_cursor_to_normal_mode(ed);
5558}
5559
5560pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5561 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5562 let current = ed.snapshot();
5563 ed.undo_stack.push(current);
5564 ed.cap_undo();
5565 ed.restore(lines, cursor);
5566 }
5567 ed.vim.mode = Mode::Normal;
5568}
5569
5570fn replay_insert_and_finish<H: crate::types::Host>(
5577 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5578 text: &str,
5579) {
5580 use hjkl_buffer::{Edit, Position};
5581 let cursor = ed.cursor();
5582 ed.mutate_edit(Edit::InsertStr {
5583 at: Position::new(cursor.0, cursor.1),
5584 text: text.to_string(),
5585 });
5586 if ed.vim.insert_session.take().is_some() {
5587 if ed.cursor().1 > 0 {
5588 crate::motions::move_left(&mut ed.buffer, 1);
5589 ed.push_buffer_cursor_to_textarea();
5590 }
5591 ed.vim.mode = Mode::Normal;
5592 }
5593}
5594
5595fn replay_last_change<H: crate::types::Host>(
5596 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5597 outer_count: usize,
5598) {
5599 let Some(change) = ed.vim.last_change.clone() else {
5600 return;
5601 };
5602 ed.vim.replaying = true;
5603 let scale = if outer_count > 0 { outer_count } else { 1 };
5604 match change {
5605 LastChange::OpMotion {
5606 op,
5607 motion,
5608 count,
5609 inserted,
5610 } => {
5611 let total = count.max(1) * scale;
5612 apply_op_with_motion(ed, op, &motion, total);
5613 if let Some(text) = inserted {
5614 replay_insert_and_finish(ed, &text);
5615 }
5616 }
5617 LastChange::OpTextObj {
5618 op,
5619 obj,
5620 inner,
5621 inserted,
5622 } => {
5623 apply_op_with_text_object(ed, op, obj, inner);
5624 if let Some(text) = inserted {
5625 replay_insert_and_finish(ed, &text);
5626 }
5627 }
5628 LastChange::LineOp {
5629 op,
5630 count,
5631 inserted,
5632 } => {
5633 let total = count.max(1) * scale;
5634 execute_line_op(ed, op, total);
5635 if let Some(text) = inserted {
5636 replay_insert_and_finish(ed, &text);
5637 }
5638 }
5639 LastChange::CharDel { forward, count } => {
5640 do_char_delete(ed, forward, count * scale);
5641 }
5642 LastChange::ReplaceChar { ch, count } => {
5643 replace_char(ed, ch, count * scale);
5644 }
5645 LastChange::ToggleCase { count } => {
5646 for _ in 0..count * scale {
5647 ed.push_undo();
5648 toggle_case_at_cursor(ed);
5649 }
5650 }
5651 LastChange::JoinLine { count } => {
5652 for _ in 0..count * scale {
5653 ed.push_undo();
5654 join_line(ed);
5655 }
5656 }
5657 LastChange::Paste { before, count } => {
5658 do_paste(ed, before, count * scale);
5659 }
5660 LastChange::DeleteToEol { inserted } => {
5661 use hjkl_buffer::{Edit, Position};
5662 ed.push_undo();
5663 delete_to_eol(ed);
5664 if let Some(text) = inserted {
5665 let cursor = ed.cursor();
5666 ed.mutate_edit(Edit::InsertStr {
5667 at: Position::new(cursor.0, cursor.1),
5668 text,
5669 });
5670 }
5671 }
5672 LastChange::OpenLine { above, inserted } => {
5673 use hjkl_buffer::{Edit, Position};
5674 ed.push_undo();
5675 ed.sync_buffer_content_from_textarea();
5676 let row = buf_cursor_pos(&ed.buffer).row;
5677 if above {
5678 ed.mutate_edit(Edit::InsertStr {
5679 at: Position::new(row, 0),
5680 text: "\n".to_string(),
5681 });
5682 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5683 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5684 } else {
5685 let line_chars = buf_line_chars(&ed.buffer, row);
5686 ed.mutate_edit(Edit::InsertStr {
5687 at: Position::new(row, line_chars),
5688 text: "\n".to_string(),
5689 });
5690 }
5691 ed.push_buffer_cursor_to_textarea();
5692 let cursor = ed.cursor();
5693 ed.mutate_edit(Edit::InsertStr {
5694 at: Position::new(cursor.0, cursor.1),
5695 text: inserted,
5696 });
5697 }
5698 LastChange::InsertAt {
5699 entry,
5700 inserted,
5701 count,
5702 } => {
5703 use hjkl_buffer::{Edit, Position};
5704 ed.push_undo();
5705 match entry {
5706 InsertEntry::I => {}
5707 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5708 InsertEntry::A => {
5709 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5710 ed.push_buffer_cursor_to_textarea();
5711 }
5712 InsertEntry::ShiftA => {
5713 crate::motions::move_line_end(&mut ed.buffer);
5714 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5715 ed.push_buffer_cursor_to_textarea();
5716 }
5717 }
5718 for _ in 0..count.max(1) {
5719 let cursor = ed.cursor();
5720 ed.mutate_edit(Edit::InsertStr {
5721 at: Position::new(cursor.0, cursor.1),
5722 text: inserted.clone(),
5723 });
5724 }
5725 }
5726 }
5727 ed.vim.replaying = false;
5728}
5729
5730fn extract_inserted(before: &str, after: &str) -> String {
5733 let before_chars: Vec<char> = before.chars().collect();
5734 let after_chars: Vec<char> = after.chars().collect();
5735 if after_chars.len() <= before_chars.len() {
5736 return String::new();
5737 }
5738 let prefix = before_chars
5739 .iter()
5740 .zip(after_chars.iter())
5741 .take_while(|(a, b)| a == b)
5742 .count();
5743 let max_suffix = before_chars.len() - prefix;
5744 let suffix = before_chars
5745 .iter()
5746 .rev()
5747 .zip(after_chars.iter().rev())
5748 .take(max_suffix)
5749 .take_while(|(a, b)| a == b)
5750 .count();
5751 after_chars[prefix..after_chars.len() - suffix]
5752 .iter()
5753 .collect()
5754}
5755
5756#[cfg(all(test, feature = "crossterm"))]
5759mod tests {
5760 use crate::VimMode;
5761 use crate::editor::Editor;
5762 use crate::types::Host;
5763 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5764
5765 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5766 let mut iter = keys.chars().peekable();
5770 while let Some(c) = iter.next() {
5771 if c == '<' {
5772 let mut tag = String::new();
5773 for ch in iter.by_ref() {
5774 if ch == '>' {
5775 break;
5776 }
5777 tag.push(ch);
5778 }
5779 let ev = match tag.as_str() {
5780 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5781 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5782 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5783 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5784 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5785 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5786 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5787 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5788 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5792 s if s.starts_with("C-") => {
5793 let ch = s.chars().nth(2).unwrap();
5794 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5795 }
5796 _ => continue,
5797 };
5798 e.handle_key(ev);
5799 } else {
5800 let mods = if c.is_uppercase() {
5801 KeyModifiers::SHIFT
5802 } else {
5803 KeyModifiers::NONE
5804 };
5805 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5806 }
5807 }
5808 }
5809
5810 fn editor_with(content: &str) -> Editor {
5811 let opts = crate::types::Options {
5816 shiftwidth: 2,
5817 ..crate::types::Options::default()
5818 };
5819 let mut e = Editor::new(
5820 hjkl_buffer::Buffer::new(),
5821 crate::types::DefaultHost::new(),
5822 opts,
5823 );
5824 e.set_content(content);
5825 e
5826 }
5827
5828 #[test]
5829 fn f_char_jumps_on_line() {
5830 let mut e = editor_with("hello world");
5831 run_keys(&mut e, "fw");
5832 assert_eq!(e.cursor(), (0, 6));
5833 }
5834
5835 #[test]
5836 fn cap_f_jumps_backward() {
5837 let mut e = editor_with("hello world");
5838 e.jump_cursor(0, 10);
5839 run_keys(&mut e, "Fo");
5840 assert_eq!(e.cursor().1, 7);
5841 }
5842
5843 #[test]
5844 fn t_stops_before_char() {
5845 let mut e = editor_with("hello");
5846 run_keys(&mut e, "tl");
5847 assert_eq!(e.cursor(), (0, 1));
5848 }
5849
5850 #[test]
5851 fn semicolon_repeats_find() {
5852 let mut e = editor_with("aa.bb.cc");
5853 run_keys(&mut e, "f.");
5854 assert_eq!(e.cursor().1, 2);
5855 run_keys(&mut e, ";");
5856 assert_eq!(e.cursor().1, 5);
5857 }
5858
5859 #[test]
5860 fn comma_repeats_find_reverse() {
5861 let mut e = editor_with("aa.bb.cc");
5862 run_keys(&mut e, "f.");
5863 run_keys(&mut e, ";");
5864 run_keys(&mut e, ",");
5865 assert_eq!(e.cursor().1, 2);
5866 }
5867
5868 #[test]
5869 fn di_quote_deletes_content() {
5870 let mut e = editor_with("foo \"bar\" baz");
5871 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5873 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5874 }
5875
5876 #[test]
5877 fn da_quote_deletes_with_quotes() {
5878 let mut e = editor_with("foo \"bar\" baz");
5881 e.jump_cursor(0, 6);
5882 run_keys(&mut e, "da\"");
5883 assert_eq!(e.buffer().lines()[0], "foo baz");
5884 }
5885
5886 #[test]
5887 fn ci_paren_deletes_and_inserts() {
5888 let mut e = editor_with("fn(a, b, c)");
5889 e.jump_cursor(0, 5);
5890 run_keys(&mut e, "ci(");
5891 assert_eq!(e.vim_mode(), VimMode::Insert);
5892 assert_eq!(e.buffer().lines()[0], "fn()");
5893 }
5894
5895 #[test]
5896 fn diw_deletes_inner_word() {
5897 let mut e = editor_with("hello world");
5898 e.jump_cursor(0, 2);
5899 run_keys(&mut e, "diw");
5900 assert_eq!(e.buffer().lines()[0], " world");
5901 }
5902
5903 #[test]
5904 fn daw_deletes_word_with_trailing_space() {
5905 let mut e = editor_with("hello world");
5906 run_keys(&mut e, "daw");
5907 assert_eq!(e.buffer().lines()[0], "world");
5908 }
5909
5910 #[test]
5911 fn percent_jumps_to_matching_bracket() {
5912 let mut e = editor_with("foo(bar)");
5913 e.jump_cursor(0, 3);
5914 run_keys(&mut e, "%");
5915 assert_eq!(e.cursor().1, 7);
5916 run_keys(&mut e, "%");
5917 assert_eq!(e.cursor().1, 3);
5918 }
5919
5920 #[test]
5921 fn dot_repeats_last_change() {
5922 let mut e = editor_with("aaa bbb ccc");
5923 run_keys(&mut e, "dw");
5924 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5925 run_keys(&mut e, ".");
5926 assert_eq!(e.buffer().lines()[0], "ccc");
5927 }
5928
5929 #[test]
5930 fn dot_repeats_change_operator_with_text() {
5931 let mut e = editor_with("foo foo foo");
5932 run_keys(&mut e, "cwbar<Esc>");
5933 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5934 run_keys(&mut e, "w");
5936 run_keys(&mut e, ".");
5937 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5938 }
5939
5940 #[test]
5941 fn dot_repeats_x() {
5942 let mut e = editor_with("abcdef");
5943 run_keys(&mut e, "x");
5944 run_keys(&mut e, "..");
5945 assert_eq!(e.buffer().lines()[0], "def");
5946 }
5947
5948 #[test]
5949 fn count_operator_motion_compose() {
5950 let mut e = editor_with("one two three four five");
5951 run_keys(&mut e, "d3w");
5952 assert_eq!(e.buffer().lines()[0], "four five");
5953 }
5954
5955 #[test]
5956 fn two_dd_deletes_two_lines() {
5957 let mut e = editor_with("a\nb\nc");
5958 run_keys(&mut e, "2dd");
5959 assert_eq!(e.buffer().lines().len(), 1);
5960 assert_eq!(e.buffer().lines()[0], "c");
5961 }
5962
5963 #[test]
5968 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5969 let mut e = editor_with("one\ntwo\n three\nfour");
5970 e.jump_cursor(1, 2);
5971 run_keys(&mut e, "dd");
5972 assert_eq!(e.buffer().lines()[1], " three");
5974 assert_eq!(e.cursor(), (1, 4));
5975 }
5976
5977 #[test]
5978 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5979 let mut e = editor_with("one\n two\nthree");
5980 e.jump_cursor(2, 0);
5981 run_keys(&mut e, "dd");
5982 assert_eq!(e.buffer().lines().len(), 2);
5984 assert_eq!(e.cursor(), (1, 2));
5985 }
5986
5987 #[test]
5988 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5989 let mut e = editor_with("lonely");
5990 run_keys(&mut e, "dd");
5991 assert_eq!(e.buffer().lines().len(), 1);
5992 assert_eq!(e.buffer().lines()[0], "");
5993 assert_eq!(e.cursor(), (0, 0));
5994 }
5995
5996 #[test]
5997 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5998 let mut e = editor_with("a\nb\nc\n d\ne");
5999 e.jump_cursor(1, 0);
6001 run_keys(&mut e, "3dd");
6002 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6003 assert_eq!(e.cursor(), (1, 0));
6004 }
6005
6006 #[test]
6007 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6008 let mut e = editor_with(" line one\n line two\n xyz!");
6027 e.jump_cursor(0, 8);
6029 assert_eq!(e.cursor(), (0, 8));
6030 run_keys(&mut e, "dd");
6033 assert_eq!(
6034 e.cursor(),
6035 (0, 4),
6036 "dd must place cursor on first-non-blank"
6037 );
6038 run_keys(&mut e, "j");
6042 let (row, col) = e.cursor();
6043 assert_eq!(row, 1);
6044 assert_eq!(
6045 col, 4,
6046 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6047 );
6048 }
6049
6050 #[test]
6051 fn gu_lowercases_motion_range() {
6052 let mut e = editor_with("HELLO WORLD");
6053 run_keys(&mut e, "guw");
6054 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6055 assert_eq!(e.cursor(), (0, 0));
6056 }
6057
6058 #[test]
6059 fn g_u_uppercases_text_object() {
6060 let mut e = editor_with("hello world");
6061 run_keys(&mut e, "gUiw");
6063 assert_eq!(e.buffer().lines()[0], "HELLO world");
6064 assert_eq!(e.cursor(), (0, 0));
6065 }
6066
6067 #[test]
6068 fn g_tilde_toggles_case_of_range() {
6069 let mut e = editor_with("Hello World");
6070 run_keys(&mut e, "g~iw");
6071 assert_eq!(e.buffer().lines()[0], "hELLO World");
6072 }
6073
6074 #[test]
6075 fn g_uu_uppercases_current_line() {
6076 let mut e = editor_with("select 1\nselect 2");
6077 run_keys(&mut e, "gUU");
6078 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6079 assert_eq!(e.buffer().lines()[1], "select 2");
6080 }
6081
6082 #[test]
6083 fn gugu_lowercases_current_line() {
6084 let mut e = editor_with("FOO BAR\nBAZ");
6085 run_keys(&mut e, "gugu");
6086 assert_eq!(e.buffer().lines()[0], "foo bar");
6087 }
6088
6089 #[test]
6090 fn visual_u_uppercases_selection() {
6091 let mut e = editor_with("hello world");
6092 run_keys(&mut e, "veU");
6094 assert_eq!(e.buffer().lines()[0], "HELLO world");
6095 }
6096
6097 #[test]
6098 fn visual_line_u_lowercases_line() {
6099 let mut e = editor_with("HELLO WORLD\nOTHER");
6100 run_keys(&mut e, "Vu");
6101 assert_eq!(e.buffer().lines()[0], "hello world");
6102 assert_eq!(e.buffer().lines()[1], "OTHER");
6103 }
6104
6105 #[test]
6106 fn g_uu_with_count_uppercases_multiple_lines() {
6107 let mut e = editor_with("one\ntwo\nthree\nfour");
6108 run_keys(&mut e, "3gUU");
6110 assert_eq!(e.buffer().lines()[0], "ONE");
6111 assert_eq!(e.buffer().lines()[1], "TWO");
6112 assert_eq!(e.buffer().lines()[2], "THREE");
6113 assert_eq!(e.buffer().lines()[3], "four");
6114 }
6115
6116 #[test]
6117 fn double_gt_indents_current_line() {
6118 let mut e = editor_with("hello");
6119 run_keys(&mut e, ">>");
6120 assert_eq!(e.buffer().lines()[0], " hello");
6121 assert_eq!(e.cursor(), (0, 2));
6123 }
6124
6125 #[test]
6126 fn double_lt_outdents_current_line() {
6127 let mut e = editor_with(" hello");
6128 run_keys(&mut e, "<lt><lt>");
6129 assert_eq!(e.buffer().lines()[0], " hello");
6130 assert_eq!(e.cursor(), (0, 2));
6131 }
6132
6133 #[test]
6134 fn count_double_gt_indents_multiple_lines() {
6135 let mut e = editor_with("a\nb\nc\nd");
6136 run_keys(&mut e, "3>>");
6138 assert_eq!(e.buffer().lines()[0], " a");
6139 assert_eq!(e.buffer().lines()[1], " b");
6140 assert_eq!(e.buffer().lines()[2], " c");
6141 assert_eq!(e.buffer().lines()[3], "d");
6142 }
6143
6144 #[test]
6145 fn outdent_clips_ragged_leading_whitespace() {
6146 let mut e = editor_with(" x");
6149 run_keys(&mut e, "<lt><lt>");
6150 assert_eq!(e.buffer().lines()[0], "x");
6151 }
6152
6153 #[test]
6154 fn indent_motion_is_always_linewise() {
6155 let mut e = editor_with("foo bar");
6158 run_keys(&mut e, ">w");
6159 assert_eq!(e.buffer().lines()[0], " foo bar");
6160 }
6161
6162 #[test]
6163 fn indent_text_object_extends_over_paragraph() {
6164 let mut e = editor_with("a\nb\n\nc\nd");
6165 run_keys(&mut e, ">ap");
6167 assert_eq!(e.buffer().lines()[0], " a");
6168 assert_eq!(e.buffer().lines()[1], " b");
6169 assert_eq!(e.buffer().lines()[2], "");
6170 assert_eq!(e.buffer().lines()[3], "c");
6171 }
6172
6173 #[test]
6174 fn visual_line_indent_shifts_selected_rows() {
6175 let mut e = editor_with("x\ny\nz");
6176 run_keys(&mut e, "Vj>");
6178 assert_eq!(e.buffer().lines()[0], " x");
6179 assert_eq!(e.buffer().lines()[1], " y");
6180 assert_eq!(e.buffer().lines()[2], "z");
6181 }
6182
6183 #[test]
6184 fn outdent_empty_line_is_noop() {
6185 let mut e = editor_with("\nfoo");
6186 run_keys(&mut e, "<lt><lt>");
6187 assert_eq!(e.buffer().lines()[0], "");
6188 }
6189
6190 #[test]
6191 fn indent_skips_empty_lines() {
6192 let mut e = editor_with("");
6195 run_keys(&mut e, ">>");
6196 assert_eq!(e.buffer().lines()[0], "");
6197 }
6198
6199 #[test]
6200 fn insert_ctrl_t_indents_current_line() {
6201 let mut e = editor_with("x");
6202 run_keys(&mut e, "i<C-t>");
6204 assert_eq!(e.buffer().lines()[0], " x");
6205 assert_eq!(e.cursor(), (0, 2));
6208 }
6209
6210 #[test]
6211 fn insert_ctrl_d_outdents_current_line() {
6212 let mut e = editor_with(" x");
6213 run_keys(&mut e, "A<C-d>");
6215 assert_eq!(e.buffer().lines()[0], " x");
6216 }
6217
6218 #[test]
6219 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6220 let mut e = editor_with("first\nsecond");
6221 e.jump_cursor(1, 0);
6222 run_keys(&mut e, "h");
6223 assert_eq!(e.cursor(), (1, 0));
6225 }
6226
6227 #[test]
6228 fn l_at_last_char_does_not_wrap_to_next_line() {
6229 let mut e = editor_with("ab\ncd");
6230 e.jump_cursor(0, 1);
6232 run_keys(&mut e, "l");
6233 assert_eq!(e.cursor(), (0, 1));
6235 }
6236
6237 #[test]
6238 fn count_l_clamps_at_line_end() {
6239 let mut e = editor_with("abcde");
6240 run_keys(&mut e, "20l");
6243 assert_eq!(e.cursor(), (0, 4));
6244 }
6245
6246 #[test]
6247 fn count_h_clamps_at_col_zero() {
6248 let mut e = editor_with("abcde");
6249 e.jump_cursor(0, 3);
6250 run_keys(&mut e, "20h");
6251 assert_eq!(e.cursor(), (0, 0));
6252 }
6253
6254 #[test]
6255 fn dl_on_last_char_still_deletes_it() {
6256 let mut e = editor_with("ab");
6260 e.jump_cursor(0, 1);
6261 run_keys(&mut e, "dl");
6262 assert_eq!(e.buffer().lines()[0], "a");
6263 }
6264
6265 #[test]
6266 fn case_op_preserves_yank_register() {
6267 let mut e = editor_with("target");
6268 run_keys(&mut e, "yy");
6269 let yank_before = e.yank().to_string();
6270 run_keys(&mut e, "gUU");
6272 assert_eq!(e.buffer().lines()[0], "TARGET");
6273 assert_eq!(
6274 e.yank(),
6275 yank_before,
6276 "case ops must preserve the yank buffer"
6277 );
6278 }
6279
6280 #[test]
6281 fn dap_deletes_paragraph() {
6282 let mut e = editor_with("a\nb\n\nc\nd");
6283 run_keys(&mut e, "dap");
6284 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6285 }
6286
6287 #[test]
6288 fn dit_deletes_inner_tag_content() {
6289 let mut e = editor_with("<b>hello</b>");
6290 e.jump_cursor(0, 4);
6292 run_keys(&mut e, "dit");
6293 assert_eq!(e.buffer().lines()[0], "<b></b>");
6294 }
6295
6296 #[test]
6297 fn dat_deletes_around_tag() {
6298 let mut e = editor_with("hi <b>foo</b> bye");
6299 e.jump_cursor(0, 6);
6300 run_keys(&mut e, "dat");
6301 assert_eq!(e.buffer().lines()[0], "hi bye");
6302 }
6303
6304 #[test]
6305 fn dit_picks_innermost_tag() {
6306 let mut e = editor_with("<a><b>x</b></a>");
6307 e.jump_cursor(0, 6);
6309 run_keys(&mut e, "dit");
6310 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6312 }
6313
6314 #[test]
6315 fn dat_innermost_tag_pair() {
6316 let mut e = editor_with("<a><b>x</b></a>");
6317 e.jump_cursor(0, 6);
6318 run_keys(&mut e, "dat");
6319 assert_eq!(e.buffer().lines()[0], "<a></a>");
6320 }
6321
6322 #[test]
6323 fn dit_outside_any_tag_no_op() {
6324 let mut e = editor_with("plain text");
6325 e.jump_cursor(0, 3);
6326 run_keys(&mut e, "dit");
6327 assert_eq!(e.buffer().lines()[0], "plain text");
6329 }
6330
6331 #[test]
6332 fn cit_changes_inner_tag_content() {
6333 let mut e = editor_with("<b>hello</b>");
6334 e.jump_cursor(0, 4);
6335 run_keys(&mut e, "citNEW<Esc>");
6336 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6337 }
6338
6339 #[test]
6340 fn cat_changes_around_tag() {
6341 let mut e = editor_with("hi <b>foo</b> bye");
6342 e.jump_cursor(0, 6);
6343 run_keys(&mut e, "catBAR<Esc>");
6344 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6345 }
6346
6347 #[test]
6348 fn yit_yanks_inner_tag_content() {
6349 let mut e = editor_with("<b>hello</b>");
6350 e.jump_cursor(0, 4);
6351 run_keys(&mut e, "yit");
6352 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6353 }
6354
6355 #[test]
6356 fn yat_yanks_full_tag_pair() {
6357 let mut e = editor_with("hi <b>foo</b> bye");
6358 e.jump_cursor(0, 6);
6359 run_keys(&mut e, "yat");
6360 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6361 }
6362
6363 #[test]
6364 fn vit_visually_selects_inner_tag() {
6365 let mut e = editor_with("<b>hello</b>");
6366 e.jump_cursor(0, 4);
6367 run_keys(&mut e, "vit");
6368 assert_eq!(e.vim_mode(), VimMode::Visual);
6369 run_keys(&mut e, "y");
6370 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6371 }
6372
6373 #[test]
6374 fn vat_visually_selects_around_tag() {
6375 let mut e = editor_with("x<b>foo</b>y");
6376 e.jump_cursor(0, 5);
6377 run_keys(&mut e, "vat");
6378 assert_eq!(e.vim_mode(), VimMode::Visual);
6379 run_keys(&mut e, "y");
6380 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6381 }
6382
6383 #[test]
6386 #[allow(non_snake_case)]
6387 fn diW_deletes_inner_big_word() {
6388 let mut e = editor_with("foo.bar baz");
6389 e.jump_cursor(0, 2);
6390 run_keys(&mut e, "diW");
6391 assert_eq!(e.buffer().lines()[0], " baz");
6393 }
6394
6395 #[test]
6396 #[allow(non_snake_case)]
6397 fn daW_deletes_around_big_word() {
6398 let mut e = editor_with("foo.bar baz");
6399 e.jump_cursor(0, 2);
6400 run_keys(&mut e, "daW");
6401 assert_eq!(e.buffer().lines()[0], "baz");
6402 }
6403
6404 #[test]
6405 fn di_double_quote_deletes_inside() {
6406 let mut e = editor_with("a \"hello\" b");
6407 e.jump_cursor(0, 4);
6408 run_keys(&mut e, "di\"");
6409 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6410 }
6411
6412 #[test]
6413 fn da_double_quote_deletes_around() {
6414 let mut e = editor_with("a \"hello\" b");
6416 e.jump_cursor(0, 4);
6417 run_keys(&mut e, "da\"");
6418 assert_eq!(e.buffer().lines()[0], "a b");
6419 }
6420
6421 #[test]
6422 fn di_single_quote_deletes_inside() {
6423 let mut e = editor_with("x 'foo' y");
6424 e.jump_cursor(0, 4);
6425 run_keys(&mut e, "di'");
6426 assert_eq!(e.buffer().lines()[0], "x '' y");
6427 }
6428
6429 #[test]
6430 fn da_single_quote_deletes_around() {
6431 let mut e = editor_with("x 'foo' y");
6433 e.jump_cursor(0, 4);
6434 run_keys(&mut e, "da'");
6435 assert_eq!(e.buffer().lines()[0], "x y");
6436 }
6437
6438 #[test]
6439 fn di_backtick_deletes_inside() {
6440 let mut e = editor_with("p `q` r");
6441 e.jump_cursor(0, 3);
6442 run_keys(&mut e, "di`");
6443 assert_eq!(e.buffer().lines()[0], "p `` r");
6444 }
6445
6446 #[test]
6447 fn da_backtick_deletes_around() {
6448 let mut e = editor_with("p `q` r");
6450 e.jump_cursor(0, 3);
6451 run_keys(&mut e, "da`");
6452 assert_eq!(e.buffer().lines()[0], "p r");
6453 }
6454
6455 #[test]
6456 fn di_paren_deletes_inside() {
6457 let mut e = editor_with("f(arg)");
6458 e.jump_cursor(0, 3);
6459 run_keys(&mut e, "di(");
6460 assert_eq!(e.buffer().lines()[0], "f()");
6461 }
6462
6463 #[test]
6464 fn di_paren_alias_b_works() {
6465 let mut e = editor_with("f(arg)");
6466 e.jump_cursor(0, 3);
6467 run_keys(&mut e, "dib");
6468 assert_eq!(e.buffer().lines()[0], "f()");
6469 }
6470
6471 #[test]
6472 fn di_bracket_deletes_inside() {
6473 let mut e = editor_with("a[b,c]d");
6474 e.jump_cursor(0, 3);
6475 run_keys(&mut e, "di[");
6476 assert_eq!(e.buffer().lines()[0], "a[]d");
6477 }
6478
6479 #[test]
6480 fn da_bracket_deletes_around() {
6481 let mut e = editor_with("a[b,c]d");
6482 e.jump_cursor(0, 3);
6483 run_keys(&mut e, "da[");
6484 assert_eq!(e.buffer().lines()[0], "ad");
6485 }
6486
6487 #[test]
6488 fn di_brace_deletes_inside() {
6489 let mut e = editor_with("x{y}z");
6490 e.jump_cursor(0, 2);
6491 run_keys(&mut e, "di{");
6492 assert_eq!(e.buffer().lines()[0], "x{}z");
6493 }
6494
6495 #[test]
6496 fn da_brace_deletes_around() {
6497 let mut e = editor_with("x{y}z");
6498 e.jump_cursor(0, 2);
6499 run_keys(&mut e, "da{");
6500 assert_eq!(e.buffer().lines()[0], "xz");
6501 }
6502
6503 #[test]
6504 fn di_brace_alias_capital_b_works() {
6505 let mut e = editor_with("x{y}z");
6506 e.jump_cursor(0, 2);
6507 run_keys(&mut e, "diB");
6508 assert_eq!(e.buffer().lines()[0], "x{}z");
6509 }
6510
6511 #[test]
6512 fn di_angle_deletes_inside() {
6513 let mut e = editor_with("p<q>r");
6514 e.jump_cursor(0, 2);
6515 run_keys(&mut e, "di<lt>");
6517 assert_eq!(e.buffer().lines()[0], "p<>r");
6518 }
6519
6520 #[test]
6521 fn da_angle_deletes_around() {
6522 let mut e = editor_with("p<q>r");
6523 e.jump_cursor(0, 2);
6524 run_keys(&mut e, "da<lt>");
6525 assert_eq!(e.buffer().lines()[0], "pr");
6526 }
6527
6528 #[test]
6529 fn dip_deletes_inner_paragraph() {
6530 let mut e = editor_with("a\nb\nc\n\nd");
6531 e.jump_cursor(1, 0);
6532 run_keys(&mut e, "dip");
6533 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6536 }
6537
6538 #[test]
6541 fn sentence_motion_close_paren_jumps_forward() {
6542 let mut e = editor_with("Alpha. Beta. Gamma.");
6543 e.jump_cursor(0, 0);
6544 run_keys(&mut e, ")");
6545 assert_eq!(e.cursor(), (0, 7));
6547 run_keys(&mut e, ")");
6548 assert_eq!(e.cursor(), (0, 13));
6549 }
6550
6551 #[test]
6552 fn sentence_motion_open_paren_jumps_backward() {
6553 let mut e = editor_with("Alpha. Beta. Gamma.");
6554 e.jump_cursor(0, 13);
6555 run_keys(&mut e, "(");
6556 assert_eq!(e.cursor(), (0, 7));
6559 run_keys(&mut e, "(");
6560 assert_eq!(e.cursor(), (0, 0));
6561 }
6562
6563 #[test]
6564 fn sentence_motion_count() {
6565 let mut e = editor_with("A. B. C. D.");
6566 e.jump_cursor(0, 0);
6567 run_keys(&mut e, "3)");
6568 assert_eq!(e.cursor(), (0, 9));
6570 }
6571
6572 #[test]
6573 fn dis_deletes_inner_sentence() {
6574 let mut e = editor_with("First one. Second one. Third one.");
6575 e.jump_cursor(0, 13);
6576 run_keys(&mut e, "dis");
6577 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6579 }
6580
6581 #[test]
6582 fn das_deletes_around_sentence_with_trailing_space() {
6583 let mut e = editor_with("Alpha. Beta. Gamma.");
6584 e.jump_cursor(0, 8);
6585 run_keys(&mut e, "das");
6586 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6589 }
6590
6591 #[test]
6592 fn dis_handles_double_terminator() {
6593 let mut e = editor_with("Wow!? Next.");
6594 e.jump_cursor(0, 1);
6595 run_keys(&mut e, "dis");
6596 assert_eq!(e.buffer().lines()[0], " Next.");
6599 }
6600
6601 #[test]
6602 fn dis_first_sentence_from_cursor_at_zero() {
6603 let mut e = editor_with("Alpha. Beta.");
6604 e.jump_cursor(0, 0);
6605 run_keys(&mut e, "dis");
6606 assert_eq!(e.buffer().lines()[0], " Beta.");
6607 }
6608
6609 #[test]
6610 fn yis_yanks_inner_sentence() {
6611 let mut e = editor_with("Hello world. Bye.");
6612 e.jump_cursor(0, 5);
6613 run_keys(&mut e, "yis");
6614 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6615 }
6616
6617 #[test]
6618 fn vis_visually_selects_inner_sentence() {
6619 let mut e = editor_with("First. Second.");
6620 e.jump_cursor(0, 1);
6621 run_keys(&mut e, "vis");
6622 assert_eq!(e.vim_mode(), VimMode::Visual);
6623 run_keys(&mut e, "y");
6624 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6625 }
6626
6627 #[test]
6628 fn ciw_changes_inner_word() {
6629 let mut e = editor_with("hello world");
6630 e.jump_cursor(0, 1);
6631 run_keys(&mut e, "ciwHEY<Esc>");
6632 assert_eq!(e.buffer().lines()[0], "HEY world");
6633 }
6634
6635 #[test]
6636 fn yiw_yanks_inner_word() {
6637 let mut e = editor_with("hello world");
6638 e.jump_cursor(0, 1);
6639 run_keys(&mut e, "yiw");
6640 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6641 }
6642
6643 #[test]
6644 fn viw_selects_inner_word() {
6645 let mut e = editor_with("hello world");
6646 e.jump_cursor(0, 2);
6647 run_keys(&mut e, "viw");
6648 assert_eq!(e.vim_mode(), VimMode::Visual);
6649 run_keys(&mut e, "y");
6650 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6651 }
6652
6653 #[test]
6654 fn ci_paren_changes_inside() {
6655 let mut e = editor_with("f(old)");
6656 e.jump_cursor(0, 3);
6657 run_keys(&mut e, "ci(NEW<Esc>");
6658 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6659 }
6660
6661 #[test]
6662 fn yi_double_quote_yanks_inside() {
6663 let mut e = editor_with("say \"hi there\" then");
6664 e.jump_cursor(0, 6);
6665 run_keys(&mut e, "yi\"");
6666 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6667 }
6668
6669 #[test]
6670 fn vap_visual_selects_around_paragraph() {
6671 let mut e = editor_with("a\nb\n\nc");
6672 e.jump_cursor(0, 0);
6673 run_keys(&mut e, "vap");
6674 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6675 run_keys(&mut e, "y");
6676 let text = e.registers().read('"').unwrap().text.clone();
6678 assert!(text.starts_with("a\nb"));
6679 }
6680
6681 #[test]
6682 fn star_finds_next_occurrence() {
6683 let mut e = editor_with("foo bar foo baz");
6684 run_keys(&mut e, "*");
6685 assert_eq!(e.cursor().1, 8);
6686 }
6687
6688 #[test]
6689 fn star_skips_substring_match() {
6690 let mut e = editor_with("foo foobar baz");
6693 run_keys(&mut e, "*");
6694 assert_eq!(e.cursor().1, 0);
6695 }
6696
6697 #[test]
6698 fn g_star_matches_substring() {
6699 let mut e = editor_with("foo foobar baz");
6702 run_keys(&mut e, "g*");
6703 assert_eq!(e.cursor().1, 4);
6704 }
6705
6706 #[test]
6707 fn g_pound_matches_substring_backward() {
6708 let mut e = editor_with("foo foobar baz foo");
6711 run_keys(&mut e, "$b");
6712 assert_eq!(e.cursor().1, 15);
6713 run_keys(&mut e, "g#");
6714 assert_eq!(e.cursor().1, 4);
6715 }
6716
6717 #[test]
6718 fn n_repeats_last_search_forward() {
6719 let mut e = editor_with("foo bar foo baz foo");
6720 run_keys(&mut e, "/foo<CR>");
6723 assert_eq!(e.cursor().1, 8);
6724 run_keys(&mut e, "n");
6725 assert_eq!(e.cursor().1, 16);
6726 }
6727
6728 #[test]
6729 fn shift_n_reverses_search() {
6730 let mut e = editor_with("foo bar foo baz foo");
6731 run_keys(&mut e, "/foo<CR>");
6732 run_keys(&mut e, "n");
6733 assert_eq!(e.cursor().1, 16);
6734 run_keys(&mut e, "N");
6735 assert_eq!(e.cursor().1, 8);
6736 }
6737
6738 #[test]
6739 fn n_noop_without_pattern() {
6740 let mut e = editor_with("foo bar");
6741 run_keys(&mut e, "n");
6742 assert_eq!(e.cursor(), (0, 0));
6743 }
6744
6745 #[test]
6746 fn visual_line_preserves_cursor_column() {
6747 let mut e = editor_with("hello world\nanother one\nbye");
6750 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6752 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6753 assert_eq!(e.cursor(), (0, 5));
6754 run_keys(&mut e, "j");
6755 assert_eq!(e.cursor(), (1, 5));
6756 }
6757
6758 #[test]
6759 fn visual_line_yank_includes_trailing_newline() {
6760 let mut e = editor_with("aaa\nbbb\nccc");
6761 run_keys(&mut e, "Vjy");
6762 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6764 }
6765
6766 #[test]
6767 fn visual_line_yank_last_line_trailing_newline() {
6768 let mut e = editor_with("aaa\nbbb\nccc");
6769 run_keys(&mut e, "jj");
6771 run_keys(&mut e, "Vy");
6772 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6773 }
6774
6775 #[test]
6776 fn yy_on_last_line_has_trailing_newline() {
6777 let mut e = editor_with("aaa\nbbb\nccc");
6778 run_keys(&mut e, "jj");
6779 run_keys(&mut e, "yy");
6780 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6781 }
6782
6783 #[test]
6784 fn yy_in_middle_has_trailing_newline() {
6785 let mut e = editor_with("aaa\nbbb\nccc");
6786 run_keys(&mut e, "j");
6787 run_keys(&mut e, "yy");
6788 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6789 }
6790
6791 #[test]
6792 fn di_single_quote() {
6793 let mut e = editor_with("say 'hello world' now");
6794 e.jump_cursor(0, 7);
6795 run_keys(&mut e, "di'");
6796 assert_eq!(e.buffer().lines()[0], "say '' now");
6797 }
6798
6799 #[test]
6800 fn da_single_quote() {
6801 let mut e = editor_with("say 'hello' now");
6803 e.jump_cursor(0, 7);
6804 run_keys(&mut e, "da'");
6805 assert_eq!(e.buffer().lines()[0], "say now");
6806 }
6807
6808 #[test]
6809 fn di_backtick() {
6810 let mut e = editor_with("say `hi` now");
6811 e.jump_cursor(0, 5);
6812 run_keys(&mut e, "di`");
6813 assert_eq!(e.buffer().lines()[0], "say `` now");
6814 }
6815
6816 #[test]
6817 fn di_brace() {
6818 let mut e = editor_with("fn { a; b; c }");
6819 e.jump_cursor(0, 7);
6820 run_keys(&mut e, "di{");
6821 assert_eq!(e.buffer().lines()[0], "fn {}");
6822 }
6823
6824 #[test]
6825 fn di_bracket() {
6826 let mut e = editor_with("arr[1, 2, 3]");
6827 e.jump_cursor(0, 5);
6828 run_keys(&mut e, "di[");
6829 assert_eq!(e.buffer().lines()[0], "arr[]");
6830 }
6831
6832 #[test]
6833 fn dab_deletes_around_paren() {
6834 let mut e = editor_with("fn(a, b) + 1");
6835 e.jump_cursor(0, 4);
6836 run_keys(&mut e, "dab");
6837 assert_eq!(e.buffer().lines()[0], "fn + 1");
6838 }
6839
6840 #[test]
6841 fn da_big_b_deletes_around_brace() {
6842 let mut e = editor_with("x = {a: 1}");
6843 e.jump_cursor(0, 6);
6844 run_keys(&mut e, "daB");
6845 assert_eq!(e.buffer().lines()[0], "x = ");
6846 }
6847
6848 #[test]
6849 fn di_big_w_deletes_bigword() {
6850 let mut e = editor_with("foo-bar baz");
6851 e.jump_cursor(0, 2);
6852 run_keys(&mut e, "diW");
6853 assert_eq!(e.buffer().lines()[0], " baz");
6854 }
6855
6856 #[test]
6857 fn visual_select_inner_word() {
6858 let mut e = editor_with("hello world");
6859 e.jump_cursor(0, 2);
6860 run_keys(&mut e, "viw");
6861 assert_eq!(e.vim_mode(), VimMode::Visual);
6862 run_keys(&mut e, "y");
6863 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6864 }
6865
6866 #[test]
6867 fn visual_select_inner_quote() {
6868 let mut e = editor_with("foo \"bar\" baz");
6869 e.jump_cursor(0, 6);
6870 run_keys(&mut e, "vi\"");
6871 run_keys(&mut e, "y");
6872 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6873 }
6874
6875 #[test]
6876 fn visual_select_inner_paren() {
6877 let mut e = editor_with("fn(a, b)");
6878 e.jump_cursor(0, 4);
6879 run_keys(&mut e, "vi(");
6880 run_keys(&mut e, "y");
6881 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6882 }
6883
6884 #[test]
6885 fn visual_select_outer_brace() {
6886 let mut e = editor_with("{x}");
6887 e.jump_cursor(0, 1);
6888 run_keys(&mut e, "va{");
6889 run_keys(&mut e, "y");
6890 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6891 }
6892
6893 #[test]
6894 fn ci_paren_forward_scans_when_cursor_before_pair() {
6895 let mut e = editor_with("foo(bar)");
6898 e.jump_cursor(0, 0);
6899 run_keys(&mut e, "ci(NEW<Esc>");
6900 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
6901 }
6902
6903 #[test]
6904 fn ci_paren_forward_scans_across_lines() {
6905 let mut e = editor_with("first\nfoo(bar)\nlast");
6906 e.jump_cursor(0, 0);
6907 run_keys(&mut e, "ci(NEW<Esc>");
6908 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
6909 }
6910
6911 #[test]
6912 fn ci_brace_forward_scans_when_cursor_before_pair() {
6913 let mut e = editor_with("let x = {y};");
6914 e.jump_cursor(0, 0);
6915 run_keys(&mut e, "ci{NEW<Esc>");
6916 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
6917 }
6918
6919 #[test]
6920 fn cit_forward_scans_when_cursor_before_tag() {
6921 let mut e = editor_with("text <b>hello</b> rest");
6924 e.jump_cursor(0, 0);
6925 run_keys(&mut e, "citNEW<Esc>");
6926 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
6927 }
6928
6929 #[test]
6930 fn dat_forward_scans_when_cursor_before_tag() {
6931 let mut e = editor_with("text <b>hello</b> rest");
6933 e.jump_cursor(0, 0);
6934 run_keys(&mut e, "dat");
6935 assert_eq!(e.buffer().lines()[0], "text rest");
6936 }
6937
6938 #[test]
6939 fn ci_paren_still_works_when_cursor_inside() {
6940 let mut e = editor_with("fn(a, b)");
6943 e.jump_cursor(0, 4);
6944 run_keys(&mut e, "ci(NEW<Esc>");
6945 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
6946 }
6947
6948 #[test]
6949 fn caw_changes_word_with_trailing_space() {
6950 let mut e = editor_with("hello world");
6951 run_keys(&mut e, "cawfoo<Esc>");
6952 assert_eq!(e.buffer().lines()[0], "fooworld");
6953 }
6954
6955 #[test]
6956 fn visual_char_yank_preserves_raw_text() {
6957 let mut e = editor_with("hello world");
6958 run_keys(&mut e, "vllly");
6959 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6960 }
6961
6962 #[test]
6963 fn single_line_visual_line_selects_full_line_on_yank() {
6964 let mut e = editor_with("hello world\nbye");
6965 run_keys(&mut e, "V");
6966 run_keys(&mut e, "y");
6969 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6970 }
6971
6972 #[test]
6973 fn visual_line_extends_both_directions() {
6974 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6975 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6977 assert_eq!(e.cursor(), (3, 0));
6978 run_keys(&mut e, "k");
6979 assert_eq!(e.cursor(), (2, 0));
6981 run_keys(&mut e, "k");
6982 assert_eq!(e.cursor(), (1, 0));
6983 }
6984
6985 #[test]
6986 fn visual_char_preserves_cursor_column() {
6987 let mut e = editor_with("hello world");
6988 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6990 assert_eq!(e.cursor(), (0, 5));
6991 run_keys(&mut e, "ll");
6992 assert_eq!(e.cursor(), (0, 7));
6993 }
6994
6995 #[test]
6996 fn visual_char_highlight_bounds_order() {
6997 let mut e = editor_with("abcdef");
6998 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7000 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7003 }
7004
7005 #[test]
7006 fn visual_line_highlight_bounds() {
7007 let mut e = editor_with("a\nb\nc");
7008 run_keys(&mut e, "V");
7009 assert_eq!(e.line_highlight(), Some((0, 0)));
7010 run_keys(&mut e, "j");
7011 assert_eq!(e.line_highlight(), Some((0, 1)));
7012 run_keys(&mut e, "j");
7013 assert_eq!(e.line_highlight(), Some((0, 2)));
7014 }
7015
7016 #[test]
7019 fn h_moves_left() {
7020 let mut e = editor_with("hello");
7021 e.jump_cursor(0, 3);
7022 run_keys(&mut e, "h");
7023 assert_eq!(e.cursor(), (0, 2));
7024 }
7025
7026 #[test]
7027 fn l_moves_right() {
7028 let mut e = editor_with("hello");
7029 run_keys(&mut e, "l");
7030 assert_eq!(e.cursor(), (0, 1));
7031 }
7032
7033 #[test]
7034 fn k_moves_up() {
7035 let mut e = editor_with("a\nb\nc");
7036 e.jump_cursor(2, 0);
7037 run_keys(&mut e, "k");
7038 assert_eq!(e.cursor(), (1, 0));
7039 }
7040
7041 #[test]
7042 fn zero_moves_to_line_start() {
7043 let mut e = editor_with(" hello");
7044 run_keys(&mut e, "$");
7045 run_keys(&mut e, "0");
7046 assert_eq!(e.cursor().1, 0);
7047 }
7048
7049 #[test]
7050 fn caret_moves_to_first_non_blank() {
7051 let mut e = editor_with(" hello");
7052 run_keys(&mut e, "0");
7053 run_keys(&mut e, "^");
7054 assert_eq!(e.cursor().1, 4);
7055 }
7056
7057 #[test]
7058 fn dollar_moves_to_last_char() {
7059 let mut e = editor_with("hello");
7060 run_keys(&mut e, "$");
7061 assert_eq!(e.cursor().1, 4);
7062 }
7063
7064 #[test]
7065 fn dollar_on_empty_line_stays_at_col_zero() {
7066 let mut e = editor_with("");
7067 run_keys(&mut e, "$");
7068 assert_eq!(e.cursor().1, 0);
7069 }
7070
7071 #[test]
7072 fn w_jumps_to_next_word() {
7073 let mut e = editor_with("foo bar baz");
7074 run_keys(&mut e, "w");
7075 assert_eq!(e.cursor().1, 4);
7076 }
7077
7078 #[test]
7079 fn b_jumps_back_a_word() {
7080 let mut e = editor_with("foo bar");
7081 e.jump_cursor(0, 6);
7082 run_keys(&mut e, "b");
7083 assert_eq!(e.cursor().1, 4);
7084 }
7085
7086 #[test]
7087 fn e_jumps_to_word_end() {
7088 let mut e = editor_with("foo bar");
7089 run_keys(&mut e, "e");
7090 assert_eq!(e.cursor().1, 2);
7091 }
7092
7093 #[test]
7096 fn d_dollar_deletes_to_eol() {
7097 let mut e = editor_with("hello world");
7098 e.jump_cursor(0, 5);
7099 run_keys(&mut e, "d$");
7100 assert_eq!(e.buffer().lines()[0], "hello");
7101 }
7102
7103 #[test]
7104 fn d_zero_deletes_to_line_start() {
7105 let mut e = editor_with("hello world");
7106 e.jump_cursor(0, 6);
7107 run_keys(&mut e, "d0");
7108 assert_eq!(e.buffer().lines()[0], "world");
7109 }
7110
7111 #[test]
7112 fn d_caret_deletes_to_first_non_blank() {
7113 let mut e = editor_with(" hello");
7114 e.jump_cursor(0, 6);
7115 run_keys(&mut e, "d^");
7116 assert_eq!(e.buffer().lines()[0], " llo");
7117 }
7118
7119 #[test]
7120 fn d_capital_g_deletes_to_end_of_file() {
7121 let mut e = editor_with("a\nb\nc\nd");
7122 e.jump_cursor(1, 0);
7123 run_keys(&mut e, "dG");
7124 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7125 }
7126
7127 #[test]
7128 fn d_gg_deletes_to_start_of_file() {
7129 let mut e = editor_with("a\nb\nc\nd");
7130 e.jump_cursor(2, 0);
7131 run_keys(&mut e, "dgg");
7132 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7133 }
7134
7135 #[test]
7136 fn cw_is_ce_quirk() {
7137 let mut e = editor_with("foo bar");
7140 run_keys(&mut e, "cwxyz<Esc>");
7141 assert_eq!(e.buffer().lines()[0], "xyz bar");
7142 }
7143
7144 #[test]
7147 fn big_d_deletes_to_eol() {
7148 let mut e = editor_with("hello world");
7149 e.jump_cursor(0, 5);
7150 run_keys(&mut e, "D");
7151 assert_eq!(e.buffer().lines()[0], "hello");
7152 }
7153
7154 #[test]
7155 fn big_c_deletes_to_eol_and_inserts() {
7156 let mut e = editor_with("hello world");
7157 e.jump_cursor(0, 5);
7158 run_keys(&mut e, "C!<Esc>");
7159 assert_eq!(e.buffer().lines()[0], "hello!");
7160 }
7161
7162 #[test]
7163 fn j_joins_next_line_with_space() {
7164 let mut e = editor_with("hello\nworld");
7165 run_keys(&mut e, "J");
7166 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7167 }
7168
7169 #[test]
7170 fn j_strips_leading_whitespace_on_join() {
7171 let mut e = editor_with("hello\n world");
7172 run_keys(&mut e, "J");
7173 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7174 }
7175
7176 #[test]
7177 fn big_x_deletes_char_before_cursor() {
7178 let mut e = editor_with("hello");
7179 e.jump_cursor(0, 3);
7180 run_keys(&mut e, "X");
7181 assert_eq!(e.buffer().lines()[0], "helo");
7182 }
7183
7184 #[test]
7185 fn s_substitutes_char_and_enters_insert() {
7186 let mut e = editor_with("hello");
7187 run_keys(&mut e, "sX<Esc>");
7188 assert_eq!(e.buffer().lines()[0], "Xello");
7189 }
7190
7191 #[test]
7192 fn count_x_deletes_many() {
7193 let mut e = editor_with("abcdef");
7194 run_keys(&mut e, "3x");
7195 assert_eq!(e.buffer().lines()[0], "def");
7196 }
7197
7198 #[test]
7201 fn p_pastes_charwise_after_cursor() {
7202 let mut e = editor_with("hello");
7203 run_keys(&mut e, "yw");
7204 run_keys(&mut e, "$p");
7205 assert_eq!(e.buffer().lines()[0], "hellohello");
7206 }
7207
7208 #[test]
7209 fn capital_p_pastes_charwise_before_cursor() {
7210 let mut e = editor_with("hello");
7211 run_keys(&mut e, "v");
7213 run_keys(&mut e, "l");
7214 run_keys(&mut e, "y");
7215 run_keys(&mut e, "$P");
7216 assert_eq!(e.buffer().lines()[0], "hellheo");
7219 }
7220
7221 #[test]
7222 fn p_pastes_linewise_below() {
7223 let mut e = editor_with("one\ntwo\nthree");
7224 run_keys(&mut e, "yy");
7225 run_keys(&mut e, "p");
7226 assert_eq!(
7227 e.buffer().lines(),
7228 &[
7229 "one".to_string(),
7230 "one".to_string(),
7231 "two".to_string(),
7232 "three".to_string()
7233 ]
7234 );
7235 }
7236
7237 #[test]
7238 fn capital_p_pastes_linewise_above() {
7239 let mut e = editor_with("one\ntwo");
7240 e.jump_cursor(1, 0);
7241 run_keys(&mut e, "yy");
7242 run_keys(&mut e, "P");
7243 assert_eq!(
7244 e.buffer().lines(),
7245 &["one".to_string(), "two".to_string(), "two".to_string()]
7246 );
7247 }
7248
7249 #[test]
7252 fn hash_finds_previous_occurrence() {
7253 let mut e = editor_with("foo bar foo baz foo");
7254 e.jump_cursor(0, 16);
7256 run_keys(&mut e, "#");
7257 assert_eq!(e.cursor().1, 8);
7258 }
7259
7260 #[test]
7263 fn visual_line_delete_removes_full_lines() {
7264 let mut e = editor_with("a\nb\nc\nd");
7265 run_keys(&mut e, "Vjd");
7266 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7267 }
7268
7269 #[test]
7270 fn visual_line_change_leaves_blank_line() {
7271 let mut e = editor_with("a\nb\nc");
7272 run_keys(&mut e, "Vjc");
7273 assert_eq!(e.vim_mode(), VimMode::Insert);
7274 run_keys(&mut e, "X<Esc>");
7275 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7279 }
7280
7281 #[test]
7282 fn cc_leaves_blank_line() {
7283 let mut e = editor_with("a\nb\nc");
7284 e.jump_cursor(1, 0);
7285 run_keys(&mut e, "ccX<Esc>");
7286 assert_eq!(
7287 e.buffer().lines(),
7288 &["a".to_string(), "X".to_string(), "c".to_string()]
7289 );
7290 }
7291
7292 #[test]
7297 fn big_w_skips_hyphens() {
7298 let mut e = editor_with("foo-bar baz");
7300 run_keys(&mut e, "W");
7301 assert_eq!(e.cursor().1, 8);
7302 }
7303
7304 #[test]
7305 fn big_w_crosses_lines() {
7306 let mut e = editor_with("foo-bar\nbaz-qux");
7307 run_keys(&mut e, "W");
7308 assert_eq!(e.cursor(), (1, 0));
7309 }
7310
7311 #[test]
7312 fn big_b_skips_hyphens() {
7313 let mut e = editor_with("foo-bar baz");
7314 e.jump_cursor(0, 9);
7315 run_keys(&mut e, "B");
7316 assert_eq!(e.cursor().1, 8);
7317 run_keys(&mut e, "B");
7318 assert_eq!(e.cursor().1, 0);
7319 }
7320
7321 #[test]
7322 fn big_e_jumps_to_big_word_end() {
7323 let mut e = editor_with("foo-bar baz");
7324 run_keys(&mut e, "E");
7325 assert_eq!(e.cursor().1, 6);
7326 run_keys(&mut e, "E");
7327 assert_eq!(e.cursor().1, 10);
7328 }
7329
7330 #[test]
7331 fn dw_with_big_word_variant() {
7332 let mut e = editor_with("foo-bar baz");
7334 run_keys(&mut e, "dW");
7335 assert_eq!(e.buffer().lines()[0], "baz");
7336 }
7337
7338 #[test]
7341 fn insert_ctrl_w_deletes_word_back() {
7342 let mut e = editor_with("");
7343 run_keys(&mut e, "i");
7344 for c in "hello world".chars() {
7345 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7346 }
7347 run_keys(&mut e, "<C-w>");
7348 assert_eq!(e.buffer().lines()[0], "hello ");
7349 }
7350
7351 #[test]
7352 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7353 let mut e = editor_with("hello\nworld");
7357 e.jump_cursor(1, 0);
7358 run_keys(&mut e, "i");
7359 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7360 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7363 assert_eq!(e.cursor(), (0, 0));
7364 }
7365
7366 #[test]
7367 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7368 let mut e = editor_with("foo bar\nbaz");
7369 e.jump_cursor(1, 0);
7370 run_keys(&mut e, "i");
7371 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7372 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7374 assert_eq!(e.cursor(), (0, 4));
7375 }
7376
7377 #[test]
7378 fn insert_ctrl_u_deletes_to_line_start() {
7379 let mut e = editor_with("");
7380 run_keys(&mut e, "i");
7381 for c in "hello world".chars() {
7382 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7383 }
7384 run_keys(&mut e, "<C-u>");
7385 assert_eq!(e.buffer().lines()[0], "");
7386 }
7387
7388 #[test]
7389 fn insert_ctrl_o_runs_one_normal_command() {
7390 let mut e = editor_with("hello world");
7391 run_keys(&mut e, "A");
7393 assert_eq!(e.vim_mode(), VimMode::Insert);
7394 e.jump_cursor(0, 0);
7396 run_keys(&mut e, "<C-o>");
7397 assert_eq!(e.vim_mode(), VimMode::Normal);
7398 run_keys(&mut e, "dw");
7399 assert_eq!(e.vim_mode(), VimMode::Insert);
7401 assert_eq!(e.buffer().lines()[0], "world");
7402 }
7403
7404 #[test]
7407 fn j_through_empty_line_preserves_column() {
7408 let mut e = editor_with("hello world\n\nanother line");
7409 run_keys(&mut e, "llllll");
7411 assert_eq!(e.cursor(), (0, 6));
7412 run_keys(&mut e, "j");
7415 assert_eq!(e.cursor(), (1, 0));
7416 run_keys(&mut e, "j");
7418 assert_eq!(e.cursor(), (2, 6));
7419 }
7420
7421 #[test]
7422 fn j_through_shorter_line_preserves_column() {
7423 let mut e = editor_with("hello world\nhi\nanother line");
7424 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7427 run_keys(&mut e, "j");
7428 assert_eq!(e.cursor(), (2, 7));
7429 }
7430
7431 #[test]
7432 fn esc_from_insert_sticky_matches_visible_cursor() {
7433 let mut e = editor_with(" this is a line\n another one of a similar size");
7437 e.jump_cursor(0, 12);
7438 run_keys(&mut e, "I");
7439 assert_eq!(e.cursor(), (0, 4));
7440 run_keys(&mut e, "X<Esc>");
7441 assert_eq!(e.cursor(), (0, 4));
7442 run_keys(&mut e, "j");
7443 assert_eq!(e.cursor(), (1, 4));
7444 }
7445
7446 #[test]
7447 fn esc_from_insert_sticky_tracks_inserted_chars() {
7448 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7449 run_keys(&mut e, "i");
7450 run_keys(&mut e, "abc<Esc>");
7451 assert_eq!(e.cursor(), (0, 2));
7452 run_keys(&mut e, "j");
7453 assert_eq!(e.cursor(), (1, 2));
7454 }
7455
7456 #[test]
7457 fn esc_from_insert_sticky_tracks_arrow_nav() {
7458 let mut e = editor_with("xxxxxx\nyyyyyy");
7459 run_keys(&mut e, "i");
7460 run_keys(&mut e, "abc");
7461 for _ in 0..2 {
7462 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7463 }
7464 run_keys(&mut e, "<Esc>");
7465 assert_eq!(e.cursor(), (0, 0));
7466 run_keys(&mut e, "j");
7467 assert_eq!(e.cursor(), (1, 0));
7468 }
7469
7470 #[test]
7471 fn esc_from_insert_at_col_14_followed_by_j() {
7472 let line = "x".repeat(30);
7475 let buf = format!("{line}\n{line}");
7476 let mut e = editor_with(&buf);
7477 e.jump_cursor(0, 14);
7478 run_keys(&mut e, "i");
7479 for c in "test ".chars() {
7480 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7481 }
7482 run_keys(&mut e, "<Esc>");
7483 assert_eq!(e.cursor(), (0, 18));
7484 run_keys(&mut e, "j");
7485 assert_eq!(e.cursor(), (1, 18));
7486 }
7487
7488 #[test]
7489 fn linewise_paste_resets_sticky_column() {
7490 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7494 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7496 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7500 run_keys(&mut e, "j");
7502 assert_eq!(e.cursor(), (3, 2));
7503 }
7504
7505 #[test]
7506 fn horizontal_motion_resyncs_sticky_column() {
7507 let mut e = editor_with("hello world\n\nanother line");
7511 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7514 assert_eq!(e.cursor(), (2, 3));
7515 }
7516
7517 #[test]
7520 fn ctrl_v_enters_visual_block() {
7521 let mut e = editor_with("aaa\nbbb\nccc");
7522 run_keys(&mut e, "<C-v>");
7523 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7524 }
7525
7526 #[test]
7527 fn visual_block_esc_returns_to_normal() {
7528 let mut e = editor_with("aaa\nbbb\nccc");
7529 run_keys(&mut e, "<C-v>");
7530 run_keys(&mut e, "<Esc>");
7531 assert_eq!(e.vim_mode(), VimMode::Normal);
7532 }
7533
7534 #[test]
7535 fn visual_exit_sets_lt_gt_marks() {
7536 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7539 run_keys(&mut e, "V");
7541 run_keys(&mut e, "j");
7542 run_keys(&mut e, "<Esc>");
7543 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7544 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7545 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7546 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7547 }
7548
7549 #[test]
7550 fn visual_exit_marks_use_lower_higher_order() {
7551 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7555 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7557 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7559 let lt = e.mark('<').unwrap();
7560 let gt = e.mark('>').unwrap();
7561 assert_eq!(lt.0, 2);
7562 assert_eq!(gt.0, 3);
7563 }
7564
7565 #[test]
7566 fn visualline_exit_marks_snap_to_line_edges() {
7567 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7569 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7571 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7573 let lt = e.mark('<').unwrap();
7574 let gt = e.mark('>').unwrap();
7575 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7576 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7578 }
7579
7580 #[test]
7581 fn visualblock_exit_marks_use_block_corners() {
7582 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7586 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7588 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7591 let lt = e.mark('<').unwrap();
7592 let gt = e.mark('>').unwrap();
7593 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7595 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7596 }
7597
7598 #[test]
7599 fn visual_block_delete_removes_column_range() {
7600 let mut e = editor_with("hello\nworld\nhappy");
7601 run_keys(&mut e, "l");
7603 run_keys(&mut e, "<C-v>");
7604 run_keys(&mut e, "jj");
7605 run_keys(&mut e, "ll");
7606 run_keys(&mut e, "d");
7607 assert_eq!(
7609 e.buffer().lines(),
7610 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7611 );
7612 }
7613
7614 #[test]
7615 fn visual_block_yank_joins_with_newlines() {
7616 let mut e = editor_with("hello\nworld\nhappy");
7617 run_keys(&mut e, "<C-v>");
7618 run_keys(&mut e, "jj");
7619 run_keys(&mut e, "ll");
7620 run_keys(&mut e, "y");
7621 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7622 }
7623
7624 #[test]
7625 fn visual_block_replace_fills_block() {
7626 let mut e = editor_with("hello\nworld\nhappy");
7627 run_keys(&mut e, "<C-v>");
7628 run_keys(&mut e, "jj");
7629 run_keys(&mut e, "ll");
7630 run_keys(&mut e, "rx");
7631 assert_eq!(
7632 e.buffer().lines(),
7633 &[
7634 "xxxlo".to_string(),
7635 "xxxld".to_string(),
7636 "xxxpy".to_string()
7637 ]
7638 );
7639 }
7640
7641 #[test]
7642 fn visual_block_insert_repeats_across_rows() {
7643 let mut e = editor_with("hello\nworld\nhappy");
7644 run_keys(&mut e, "<C-v>");
7645 run_keys(&mut e, "jj");
7646 run_keys(&mut e, "I");
7647 run_keys(&mut e, "# <Esc>");
7648 assert_eq!(
7649 e.buffer().lines(),
7650 &[
7651 "# hello".to_string(),
7652 "# world".to_string(),
7653 "# happy".to_string()
7654 ]
7655 );
7656 }
7657
7658 #[test]
7659 fn block_highlight_returns_none_outside_block_mode() {
7660 let mut e = editor_with("abc");
7661 assert!(e.block_highlight().is_none());
7662 run_keys(&mut e, "v");
7663 assert!(e.block_highlight().is_none());
7664 run_keys(&mut e, "<Esc>V");
7665 assert!(e.block_highlight().is_none());
7666 }
7667
7668 #[test]
7669 fn block_highlight_bounds_track_anchor_and_cursor() {
7670 let mut e = editor_with("aaaa\nbbbb\ncccc");
7671 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7673 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7676 }
7677
7678 #[test]
7679 fn visual_block_delete_handles_short_lines() {
7680 let mut e = editor_with("hello\nhi\nworld");
7682 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7684 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7686 assert_eq!(
7691 e.buffer().lines(),
7692 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7693 );
7694 }
7695
7696 #[test]
7697 fn visual_block_yank_pads_short_lines_with_empties() {
7698 let mut e = editor_with("hello\nhi\nworld");
7699 run_keys(&mut e, "l");
7700 run_keys(&mut e, "<C-v>");
7701 run_keys(&mut e, "jjll");
7702 run_keys(&mut e, "y");
7703 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7705 }
7706
7707 #[test]
7708 fn visual_block_replace_skips_past_eol() {
7709 let mut e = editor_with("ab\ncd\nef");
7712 run_keys(&mut e, "l");
7714 run_keys(&mut e, "<C-v>");
7715 run_keys(&mut e, "jjllllll");
7716 run_keys(&mut e, "rX");
7717 assert_eq!(
7720 e.buffer().lines(),
7721 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7722 );
7723 }
7724
7725 #[test]
7726 fn visual_block_with_empty_line_in_middle() {
7727 let mut e = editor_with("abcd\n\nefgh");
7728 run_keys(&mut e, "<C-v>");
7729 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7731 assert_eq!(
7734 e.buffer().lines(),
7735 &["d".to_string(), "".to_string(), "h".to_string()]
7736 );
7737 }
7738
7739 #[test]
7740 fn block_insert_pads_empty_lines_to_block_column() {
7741 let mut e = editor_with("this is a line\n\nthis is a line");
7744 e.jump_cursor(0, 3);
7745 run_keys(&mut e, "<C-v>");
7746 run_keys(&mut e, "jj");
7747 run_keys(&mut e, "I");
7748 run_keys(&mut e, "XX<Esc>");
7749 assert_eq!(
7750 e.buffer().lines(),
7751 &[
7752 "thiXXs is a line".to_string(),
7753 " XX".to_string(),
7754 "thiXXs is a line".to_string()
7755 ]
7756 );
7757 }
7758
7759 #[test]
7760 fn block_insert_pads_short_lines_to_block_column() {
7761 let mut e = editor_with("aaaaa\nbb\naaaaa");
7762 e.jump_cursor(0, 3);
7763 run_keys(&mut e, "<C-v>");
7764 run_keys(&mut e, "jj");
7765 run_keys(&mut e, "I");
7766 run_keys(&mut e, "Y<Esc>");
7767 assert_eq!(
7769 e.buffer().lines(),
7770 &[
7771 "aaaYaa".to_string(),
7772 "bb Y".to_string(),
7773 "aaaYaa".to_string()
7774 ]
7775 );
7776 }
7777
7778 #[test]
7779 fn visual_block_append_repeats_across_rows() {
7780 let mut e = editor_with("foo\nbar\nbaz");
7781 run_keys(&mut e, "<C-v>");
7782 run_keys(&mut e, "jj");
7783 run_keys(&mut e, "A");
7786 run_keys(&mut e, "!<Esc>");
7787 assert_eq!(
7788 e.buffer().lines(),
7789 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7790 );
7791 }
7792
7793 #[test]
7796 fn slash_opens_forward_search_prompt() {
7797 let mut e = editor_with("hello world");
7798 run_keys(&mut e, "/");
7799 let p = e.search_prompt().expect("prompt should be active");
7800 assert!(p.text.is_empty());
7801 assert!(p.forward);
7802 }
7803
7804 #[test]
7805 fn question_opens_backward_search_prompt() {
7806 let mut e = editor_with("hello world");
7807 run_keys(&mut e, "?");
7808 let p = e.search_prompt().expect("prompt should be active");
7809 assert!(!p.forward);
7810 }
7811
7812 #[test]
7813 fn search_prompt_typing_updates_pattern_live() {
7814 let mut e = editor_with("foo bar\nbaz");
7815 run_keys(&mut e, "/bar");
7816 assert_eq!(e.search_prompt().unwrap().text, "bar");
7817 assert!(e.search_state().pattern.is_some());
7819 }
7820
7821 #[test]
7822 fn search_prompt_backspace_and_enter() {
7823 let mut e = editor_with("hello world\nagain");
7824 run_keys(&mut e, "/worlx");
7825 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7826 assert_eq!(e.search_prompt().unwrap().text, "worl");
7827 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7828 assert!(e.search_prompt().is_none());
7830 assert_eq!(e.last_search(), Some("worl"));
7831 assert_eq!(e.cursor(), (0, 6));
7832 }
7833
7834 #[test]
7835 fn empty_search_prompt_enter_repeats_last_search() {
7836 let mut e = editor_with("foo bar foo baz foo");
7837 run_keys(&mut e, "/foo");
7838 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7839 assert_eq!(e.cursor().1, 8);
7840 run_keys(&mut e, "/");
7842 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7843 assert_eq!(e.cursor().1, 16);
7844 assert_eq!(e.last_search(), Some("foo"));
7845 }
7846
7847 #[test]
7848 fn search_history_records_committed_patterns() {
7849 let mut e = editor_with("alpha beta gamma");
7850 run_keys(&mut e, "/alpha<CR>");
7851 run_keys(&mut e, "/beta<CR>");
7852 let history = e.vim.search_history.clone();
7854 assert_eq!(history, vec!["alpha", "beta"]);
7855 }
7856
7857 #[test]
7858 fn search_history_dedupes_consecutive_repeats() {
7859 let mut e = editor_with("foo bar foo");
7860 run_keys(&mut e, "/foo<CR>");
7861 run_keys(&mut e, "/foo<CR>");
7862 run_keys(&mut e, "/bar<CR>");
7863 run_keys(&mut e, "/bar<CR>");
7864 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
7866 }
7867
7868 #[test]
7869 fn ctrl_p_walks_history_backward() {
7870 let mut e = editor_with("alpha beta gamma");
7871 run_keys(&mut e, "/alpha<CR>");
7872 run_keys(&mut e, "/beta<CR>");
7873 run_keys(&mut e, "/");
7875 assert_eq!(e.search_prompt().unwrap().text, "");
7876 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7877 assert_eq!(e.search_prompt().unwrap().text, "beta");
7878 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7879 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7880 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7882 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7883 }
7884
7885 #[test]
7886 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7887 let mut e = editor_with("a b c");
7888 run_keys(&mut e, "/a<CR>");
7889 run_keys(&mut e, "/b<CR>");
7890 run_keys(&mut e, "/c<CR>");
7891 run_keys(&mut e, "/");
7892 for _ in 0..3 {
7894 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7895 }
7896 assert_eq!(e.search_prompt().unwrap().text, "a");
7897 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7898 assert_eq!(e.search_prompt().unwrap().text, "b");
7899 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7900 assert_eq!(e.search_prompt().unwrap().text, "c");
7901 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7903 assert_eq!(e.search_prompt().unwrap().text, "c");
7904 }
7905
7906 #[test]
7907 fn typing_after_history_walk_resets_cursor() {
7908 let mut e = editor_with("foo");
7909 run_keys(&mut e, "/foo<CR>");
7910 run_keys(&mut e, "/");
7911 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7912 assert_eq!(e.search_prompt().unwrap().text, "foo");
7913 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7916 assert_eq!(e.search_prompt().unwrap().text, "foox");
7917 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7918 assert_eq!(e.search_prompt().unwrap().text, "foo");
7919 }
7920
7921 #[test]
7922 fn empty_backward_search_prompt_enter_repeats_last_search() {
7923 let mut e = editor_with("foo bar foo baz foo");
7924 run_keys(&mut e, "/foo");
7926 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7927 assert_eq!(e.cursor().1, 8);
7928 run_keys(&mut e, "?");
7929 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7930 assert_eq!(e.cursor().1, 0);
7931 assert_eq!(e.last_search(), Some("foo"));
7932 }
7933
7934 #[test]
7935 fn search_prompt_esc_cancels_but_keeps_last_search() {
7936 let mut e = editor_with("foo bar\nbaz");
7937 run_keys(&mut e, "/bar");
7938 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7939 assert!(e.search_prompt().is_none());
7940 assert_eq!(e.last_search(), Some("bar"));
7941 }
7942
7943 #[test]
7944 fn search_then_n_and_shift_n_navigate() {
7945 let mut e = editor_with("foo bar foo baz foo");
7946 run_keys(&mut e, "/foo");
7947 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7948 assert_eq!(e.cursor().1, 8);
7950 run_keys(&mut e, "n");
7951 assert_eq!(e.cursor().1, 16);
7952 run_keys(&mut e, "N");
7953 assert_eq!(e.cursor().1, 8);
7954 }
7955
7956 #[test]
7957 fn question_mark_searches_backward_on_enter() {
7958 let mut e = editor_with("foo bar foo baz");
7959 e.jump_cursor(0, 10);
7960 run_keys(&mut e, "?foo");
7961 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7962 assert_eq!(e.cursor(), (0, 8));
7964 }
7965
7966 #[test]
7969 fn big_y_yanks_to_end_of_line() {
7970 let mut e = editor_with("hello world");
7971 e.jump_cursor(0, 6);
7972 run_keys(&mut e, "Y");
7973 assert_eq!(e.last_yank.as_deref(), Some("world"));
7974 }
7975
7976 #[test]
7977 fn big_y_from_line_start_yanks_full_line() {
7978 let mut e = editor_with("hello world");
7979 run_keys(&mut e, "Y");
7980 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7981 }
7982
7983 #[test]
7984 fn gj_joins_without_inserting_space() {
7985 let mut e = editor_with("hello\n world");
7986 run_keys(&mut e, "gJ");
7987 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7989 }
7990
7991 #[test]
7992 fn gj_noop_on_last_line() {
7993 let mut e = editor_with("only");
7994 run_keys(&mut e, "gJ");
7995 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7996 }
7997
7998 #[test]
7999 fn ge_jumps_to_previous_word_end() {
8000 let mut e = editor_with("foo bar baz");
8001 e.jump_cursor(0, 5);
8002 run_keys(&mut e, "ge");
8003 assert_eq!(e.cursor(), (0, 2));
8004 }
8005
8006 #[test]
8007 fn ge_respects_word_class() {
8008 let mut e = editor_with("foo-bar baz");
8011 e.jump_cursor(0, 5);
8012 run_keys(&mut e, "ge");
8013 assert_eq!(e.cursor(), (0, 3));
8014 }
8015
8016 #[test]
8017 fn big_ge_treats_hyphens_as_part_of_word() {
8018 let mut e = editor_with("foo-bar baz");
8021 e.jump_cursor(0, 10);
8022 run_keys(&mut e, "gE");
8023 assert_eq!(e.cursor(), (0, 6));
8024 }
8025
8026 #[test]
8027 fn ge_crosses_line_boundary() {
8028 let mut e = editor_with("foo\nbar");
8029 e.jump_cursor(1, 0);
8030 run_keys(&mut e, "ge");
8031 assert_eq!(e.cursor(), (0, 2));
8032 }
8033
8034 #[test]
8035 fn dge_deletes_to_end_of_previous_word() {
8036 let mut e = editor_with("foo bar baz");
8037 e.jump_cursor(0, 8);
8038 run_keys(&mut e, "dge");
8041 assert_eq!(e.buffer().lines()[0], "foo baaz");
8042 }
8043
8044 #[test]
8045 fn ctrl_scroll_keys_do_not_panic() {
8046 let mut e = editor_with(
8049 (0..50)
8050 .map(|i| format!("line{i}"))
8051 .collect::<Vec<_>>()
8052 .join("\n")
8053 .as_str(),
8054 );
8055 run_keys(&mut e, "<C-f>");
8056 run_keys(&mut e, "<C-b>");
8057 assert!(!e.buffer().lines().is_empty());
8059 }
8060
8061 #[test]
8068 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8069 let mut e = Editor::new(
8070 hjkl_buffer::Buffer::new(),
8071 crate::types::DefaultHost::new(),
8072 crate::types::Options::default(),
8073 );
8074 e.set_content("row0\nrow1\nrow2");
8075 run_keys(&mut e, "3iX<Down><Esc>");
8077 assert!(e.buffer().lines()[0].contains('X'));
8079 assert!(
8082 !e.buffer().lines()[1].contains("row0"),
8083 "row1 leaked row0 contents: {:?}",
8084 e.buffer().lines()[1]
8085 );
8086 assert_eq!(e.buffer().lines().len(), 3);
8089 }
8090
8091 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8094 let mut e = Editor::new(
8095 hjkl_buffer::Buffer::new(),
8096 crate::types::DefaultHost::new(),
8097 crate::types::Options::default(),
8098 );
8099 let body = (0..n)
8100 .map(|i| format!(" line{}", i))
8101 .collect::<Vec<_>>()
8102 .join("\n");
8103 e.set_content(&body);
8104 e.set_viewport_height(viewport);
8105 e
8106 }
8107
8108 #[test]
8109 fn ctrl_d_moves_cursor_half_page_down() {
8110 let mut e = editor_with_rows(100, 20);
8111 run_keys(&mut e, "<C-d>");
8112 assert_eq!(e.cursor().0, 10);
8113 }
8114
8115 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8116 let mut e = Editor::new(
8117 hjkl_buffer::Buffer::new(),
8118 crate::types::DefaultHost::new(),
8119 crate::types::Options::default(),
8120 );
8121 e.set_content(&lines.join("\n"));
8122 e.set_viewport_height(viewport);
8123 let v = e.host_mut().viewport_mut();
8124 v.height = viewport;
8125 v.width = text_width;
8126 v.text_width = text_width;
8127 v.wrap = hjkl_buffer::Wrap::Char;
8128 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8129 e
8130 }
8131
8132 #[test]
8133 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8134 let lines = ["aaaabbbbcccc"; 10];
8138 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8139 e.jump_cursor(4, 0);
8140 e.ensure_cursor_in_scrolloff();
8141 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8142 assert!(csr <= 6, "csr={csr}");
8143 }
8144
8145 #[test]
8146 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8147 let lines = ["aaaabbbbcccc"; 10];
8148 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8149 e.jump_cursor(7, 0);
8152 e.ensure_cursor_in_scrolloff();
8153 e.jump_cursor(2, 0);
8154 e.ensure_cursor_in_scrolloff();
8155 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8156 assert!(csr >= 5, "csr={csr}");
8158 }
8159
8160 #[test]
8161 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8162 let lines = ["aaaabbbbcccc"; 5];
8163 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8164 e.jump_cursor(4, 11);
8165 e.ensure_cursor_in_scrolloff();
8166 let top = e.host().viewport().top_row;
8171 assert_eq!(top, 1);
8172 }
8173
8174 #[test]
8175 fn ctrl_u_moves_cursor_half_page_up() {
8176 let mut e = editor_with_rows(100, 20);
8177 e.jump_cursor(50, 0);
8178 run_keys(&mut e, "<C-u>");
8179 assert_eq!(e.cursor().0, 40);
8180 }
8181
8182 #[test]
8183 fn ctrl_f_moves_cursor_full_page_down() {
8184 let mut e = editor_with_rows(100, 20);
8185 run_keys(&mut e, "<C-f>");
8186 assert_eq!(e.cursor().0, 18);
8188 }
8189
8190 #[test]
8191 fn ctrl_b_moves_cursor_full_page_up() {
8192 let mut e = editor_with_rows(100, 20);
8193 e.jump_cursor(50, 0);
8194 run_keys(&mut e, "<C-b>");
8195 assert_eq!(e.cursor().0, 32);
8196 }
8197
8198 #[test]
8199 fn ctrl_d_lands_on_first_non_blank() {
8200 let mut e = editor_with_rows(100, 20);
8201 run_keys(&mut e, "<C-d>");
8202 assert_eq!(e.cursor().1, 2);
8204 }
8205
8206 #[test]
8207 fn ctrl_d_clamps_at_end_of_buffer() {
8208 let mut e = editor_with_rows(5, 20);
8209 run_keys(&mut e, "<C-d>");
8210 assert_eq!(e.cursor().0, 4);
8211 }
8212
8213 #[test]
8214 fn capital_h_jumps_to_viewport_top() {
8215 let mut e = editor_with_rows(100, 10);
8216 e.jump_cursor(50, 0);
8217 e.set_viewport_top(45);
8218 let top = e.host().viewport().top_row;
8219 run_keys(&mut e, "H");
8220 assert_eq!(e.cursor().0, top);
8221 assert_eq!(e.cursor().1, 2);
8222 }
8223
8224 #[test]
8225 fn capital_l_jumps_to_viewport_bottom() {
8226 let mut e = editor_with_rows(100, 10);
8227 e.jump_cursor(50, 0);
8228 e.set_viewport_top(45);
8229 let top = e.host().viewport().top_row;
8230 run_keys(&mut e, "L");
8231 assert_eq!(e.cursor().0, top + 9);
8232 }
8233
8234 #[test]
8235 fn capital_m_jumps_to_viewport_middle() {
8236 let mut e = editor_with_rows(100, 10);
8237 e.jump_cursor(50, 0);
8238 e.set_viewport_top(45);
8239 let top = e.host().viewport().top_row;
8240 run_keys(&mut e, "M");
8241 assert_eq!(e.cursor().0, top + 4);
8243 }
8244
8245 #[test]
8246 fn g_capital_m_lands_at_line_midpoint() {
8247 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8249 assert_eq!(e.cursor(), (0, 6));
8251 }
8252
8253 #[test]
8254 fn g_capital_m_on_empty_line_stays_at_zero() {
8255 let mut e = editor_with("");
8256 run_keys(&mut e, "gM");
8257 assert_eq!(e.cursor(), (0, 0));
8258 }
8259
8260 #[test]
8261 fn g_capital_m_uses_current_line_only() {
8262 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8265 run_keys(&mut e, "gM");
8266 assert_eq!(e.cursor(), (1, 6));
8267 }
8268
8269 #[test]
8270 fn capital_h_count_offsets_from_top() {
8271 let mut e = editor_with_rows(100, 10);
8272 e.jump_cursor(50, 0);
8273 e.set_viewport_top(45);
8274 let top = e.host().viewport().top_row;
8275 run_keys(&mut e, "3H");
8276 assert_eq!(e.cursor().0, top + 2);
8277 }
8278
8279 #[test]
8282 fn ctrl_o_returns_to_pre_g_position() {
8283 let mut e = editor_with_rows(50, 20);
8284 e.jump_cursor(5, 2);
8285 run_keys(&mut e, "G");
8286 assert_eq!(e.cursor().0, 49);
8287 run_keys(&mut e, "<C-o>");
8288 assert_eq!(e.cursor(), (5, 2));
8289 }
8290
8291 #[test]
8292 fn ctrl_i_redoes_jump_after_ctrl_o() {
8293 let mut e = editor_with_rows(50, 20);
8294 e.jump_cursor(5, 2);
8295 run_keys(&mut e, "G");
8296 let post = e.cursor();
8297 run_keys(&mut e, "<C-o>");
8298 run_keys(&mut e, "<C-i>");
8299 assert_eq!(e.cursor(), post);
8300 }
8301
8302 #[test]
8303 fn new_jump_clears_forward_stack() {
8304 let mut e = editor_with_rows(50, 20);
8305 e.jump_cursor(5, 2);
8306 run_keys(&mut e, "G");
8307 run_keys(&mut e, "<C-o>");
8308 run_keys(&mut e, "gg");
8309 run_keys(&mut e, "<C-i>");
8310 assert_eq!(e.cursor().0, 0);
8311 }
8312
8313 #[test]
8314 fn ctrl_o_on_empty_stack_is_noop() {
8315 let mut e = editor_with_rows(10, 20);
8316 e.jump_cursor(3, 1);
8317 run_keys(&mut e, "<C-o>");
8318 assert_eq!(e.cursor(), (3, 1));
8319 }
8320
8321 #[test]
8322 fn asterisk_search_pushes_jump() {
8323 let mut e = editor_with("foo bar\nbaz foo end");
8324 e.jump_cursor(0, 0);
8325 run_keys(&mut e, "*");
8326 let after = e.cursor();
8327 assert_ne!(after, (0, 0));
8328 run_keys(&mut e, "<C-o>");
8329 assert_eq!(e.cursor(), (0, 0));
8330 }
8331
8332 #[test]
8333 fn h_viewport_jump_is_recorded() {
8334 let mut e = editor_with_rows(100, 10);
8335 e.jump_cursor(50, 0);
8336 e.set_viewport_top(45);
8337 let pre = e.cursor();
8338 run_keys(&mut e, "H");
8339 assert_ne!(e.cursor(), pre);
8340 run_keys(&mut e, "<C-o>");
8341 assert_eq!(e.cursor(), pre);
8342 }
8343
8344 #[test]
8345 fn j_k_motion_does_not_push_jump() {
8346 let mut e = editor_with_rows(50, 20);
8347 e.jump_cursor(5, 0);
8348 run_keys(&mut e, "jjj");
8349 run_keys(&mut e, "<C-o>");
8350 assert_eq!(e.cursor().0, 8);
8351 }
8352
8353 #[test]
8354 fn jumplist_caps_at_100() {
8355 let mut e = editor_with_rows(200, 20);
8356 for i in 0..101 {
8357 e.jump_cursor(i, 0);
8358 run_keys(&mut e, "G");
8359 }
8360 assert!(e.vim.jump_back.len() <= 100);
8361 }
8362
8363 #[test]
8364 fn tab_acts_as_ctrl_i() {
8365 let mut e = editor_with_rows(50, 20);
8366 e.jump_cursor(5, 2);
8367 run_keys(&mut e, "G");
8368 let post = e.cursor();
8369 run_keys(&mut e, "<C-o>");
8370 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8371 assert_eq!(e.cursor(), post);
8372 }
8373
8374 #[test]
8377 fn ma_then_backtick_a_jumps_exact() {
8378 let mut e = editor_with_rows(50, 20);
8379 e.jump_cursor(5, 3);
8380 run_keys(&mut e, "ma");
8381 e.jump_cursor(20, 0);
8382 run_keys(&mut e, "`a");
8383 assert_eq!(e.cursor(), (5, 3));
8384 }
8385
8386 #[test]
8387 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8388 let mut e = editor_with_rows(50, 20);
8389 e.jump_cursor(5, 6);
8391 run_keys(&mut e, "ma");
8392 e.jump_cursor(30, 4);
8393 run_keys(&mut e, "'a");
8394 assert_eq!(e.cursor(), (5, 2));
8395 }
8396
8397 #[test]
8398 fn goto_mark_pushes_jumplist() {
8399 let mut e = editor_with_rows(50, 20);
8400 e.jump_cursor(10, 2);
8401 run_keys(&mut e, "mz");
8402 e.jump_cursor(3, 0);
8403 run_keys(&mut e, "`z");
8404 assert_eq!(e.cursor(), (10, 2));
8405 run_keys(&mut e, "<C-o>");
8406 assert_eq!(e.cursor(), (3, 0));
8407 }
8408
8409 #[test]
8410 fn goto_missing_mark_is_noop() {
8411 let mut e = editor_with_rows(50, 20);
8412 e.jump_cursor(3, 1);
8413 run_keys(&mut e, "`q");
8414 assert_eq!(e.cursor(), (3, 1));
8415 }
8416
8417 #[test]
8418 fn uppercase_mark_stored_under_uppercase_key() {
8419 let mut e = editor_with_rows(50, 20);
8420 e.jump_cursor(5, 3);
8421 run_keys(&mut e, "mA");
8422 assert_eq!(e.mark('A'), Some((5, 3)));
8425 assert!(e.mark('a').is_none());
8426 }
8427
8428 #[test]
8429 fn mark_survives_document_shrink_via_clamp() {
8430 let mut e = editor_with_rows(50, 20);
8431 e.jump_cursor(40, 4);
8432 run_keys(&mut e, "mx");
8433 e.set_content("a\nb\nc\nd\ne");
8435 run_keys(&mut e, "`x");
8436 let (r, _) = e.cursor();
8438 assert!(r <= 4);
8439 }
8440
8441 #[test]
8442 fn g_semicolon_walks_back_through_edits() {
8443 let mut e = editor_with("alpha\nbeta\ngamma");
8444 e.jump_cursor(0, 0);
8447 run_keys(&mut e, "iX<Esc>");
8448 e.jump_cursor(2, 0);
8449 run_keys(&mut e, "iY<Esc>");
8450 run_keys(&mut e, "g;");
8452 assert_eq!(e.cursor(), (2, 1));
8453 run_keys(&mut e, "g;");
8455 assert_eq!(e.cursor(), (0, 1));
8456 run_keys(&mut e, "g;");
8458 assert_eq!(e.cursor(), (0, 1));
8459 }
8460
8461 #[test]
8462 fn g_comma_walks_forward_after_g_semicolon() {
8463 let mut e = editor_with("a\nb\nc");
8464 e.jump_cursor(0, 0);
8465 run_keys(&mut e, "iX<Esc>");
8466 e.jump_cursor(2, 0);
8467 run_keys(&mut e, "iY<Esc>");
8468 run_keys(&mut e, "g;");
8469 run_keys(&mut e, "g;");
8470 assert_eq!(e.cursor(), (0, 1));
8471 run_keys(&mut e, "g,");
8472 assert_eq!(e.cursor(), (2, 1));
8473 }
8474
8475 #[test]
8476 fn new_edit_during_walk_trims_forward_entries() {
8477 let mut e = editor_with("a\nb\nc\nd");
8478 e.jump_cursor(0, 0);
8479 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8481 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8484 run_keys(&mut e, "g;");
8485 assert_eq!(e.cursor(), (0, 1));
8486 run_keys(&mut e, "iZ<Esc>");
8488 run_keys(&mut e, "g,");
8490 assert_ne!(e.cursor(), (2, 1));
8492 }
8493
8494 #[test]
8500 fn capital_mark_set_and_jump() {
8501 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8502 e.jump_cursor(2, 1);
8503 run_keys(&mut e, "mA");
8504 e.jump_cursor(0, 0);
8506 run_keys(&mut e, "'A");
8508 assert_eq!(e.cursor().0, 2);
8510 }
8511
8512 #[test]
8513 fn capital_mark_survives_set_content() {
8514 let mut e = editor_with("first buffer line\nsecond");
8515 e.jump_cursor(1, 3);
8516 run_keys(&mut e, "mA");
8517 e.set_content("totally different content\non many\nrows of text");
8519 e.jump_cursor(0, 0);
8521 run_keys(&mut e, "'A");
8522 assert_eq!(e.cursor().0, 1);
8523 }
8524
8525 #[test]
8530 fn capital_mark_shifts_with_edit() {
8531 let mut e = editor_with("a\nb\nc\nd");
8532 e.jump_cursor(3, 0);
8533 run_keys(&mut e, "mA");
8534 e.jump_cursor(0, 0);
8536 run_keys(&mut e, "dd");
8537 e.jump_cursor(0, 0);
8538 run_keys(&mut e, "'A");
8539 assert_eq!(e.cursor().0, 2);
8540 }
8541
8542 #[test]
8543 fn mark_below_delete_shifts_up() {
8544 let mut e = editor_with("a\nb\nc\nd\ne");
8545 e.jump_cursor(3, 0);
8547 run_keys(&mut e, "ma");
8548 e.jump_cursor(0, 0);
8550 run_keys(&mut e, "dd");
8551 e.jump_cursor(0, 0);
8553 run_keys(&mut e, "'a");
8554 assert_eq!(e.cursor().0, 2);
8555 assert_eq!(e.buffer().line(2).unwrap(), "d");
8556 }
8557
8558 #[test]
8559 fn mark_on_deleted_row_is_dropped() {
8560 let mut e = editor_with("a\nb\nc\nd");
8561 e.jump_cursor(1, 0);
8563 run_keys(&mut e, "ma");
8564 run_keys(&mut e, "dd");
8566 e.jump_cursor(2, 0);
8568 run_keys(&mut e, "'a");
8569 assert_eq!(e.cursor().0, 2);
8571 }
8572
8573 #[test]
8574 fn mark_above_edit_unchanged() {
8575 let mut e = editor_with("a\nb\nc\nd\ne");
8576 e.jump_cursor(0, 0);
8578 run_keys(&mut e, "ma");
8579 e.jump_cursor(3, 0);
8581 run_keys(&mut e, "dd");
8582 e.jump_cursor(2, 0);
8584 run_keys(&mut e, "'a");
8585 assert_eq!(e.cursor().0, 0);
8586 }
8587
8588 #[test]
8589 fn mark_shifts_down_after_insert() {
8590 let mut e = editor_with("a\nb\nc");
8591 e.jump_cursor(2, 0);
8593 run_keys(&mut e, "ma");
8594 e.jump_cursor(0, 0);
8596 run_keys(&mut e, "Onew<Esc>");
8597 e.jump_cursor(0, 0);
8600 run_keys(&mut e, "'a");
8601 assert_eq!(e.cursor().0, 3);
8602 assert_eq!(e.buffer().line(3).unwrap(), "c");
8603 }
8604
8605 #[test]
8608 fn forward_search_commit_pushes_jump() {
8609 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8610 e.jump_cursor(0, 0);
8611 run_keys(&mut e, "/target<CR>");
8612 assert_ne!(e.cursor(), (0, 0));
8614 run_keys(&mut e, "<C-o>");
8616 assert_eq!(e.cursor(), (0, 0));
8617 }
8618
8619 #[test]
8620 fn search_commit_no_match_does_not_push_jump() {
8621 let mut e = editor_with("alpha beta\nfoo end");
8622 e.jump_cursor(0, 3);
8623 let pre_len = e.vim.jump_back.len();
8624 run_keys(&mut e, "/zzznotfound<CR>");
8625 assert_eq!(e.vim.jump_back.len(), pre_len);
8627 }
8628
8629 #[test]
8632 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8633 let mut e = editor_with("hello world");
8634 run_keys(&mut e, "lll");
8635 let (row, col) = e.cursor();
8636 assert_eq!(e.buffer.cursor().row, row);
8637 assert_eq!(e.buffer.cursor().col, col);
8638 }
8639
8640 #[test]
8641 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8642 let mut e = editor_with("aaaa\nbbbb\ncccc");
8643 run_keys(&mut e, "jj");
8644 let (row, col) = e.cursor();
8645 assert_eq!(e.buffer.cursor().row, row);
8646 assert_eq!(e.buffer.cursor().col, col);
8647 }
8648
8649 #[test]
8650 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8651 let mut e = editor_with("foo bar baz");
8652 run_keys(&mut e, "ww");
8653 let (row, col) = e.cursor();
8654 assert_eq!(e.buffer.cursor().row, row);
8655 assert_eq!(e.buffer.cursor().col, col);
8656 }
8657
8658 #[test]
8659 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8660 let mut e = editor_with("a\nb\nc\nd\ne");
8661 run_keys(&mut e, "G");
8662 let (row, col) = e.cursor();
8663 assert_eq!(e.buffer.cursor().row, row);
8664 assert_eq!(e.buffer.cursor().col, col);
8665 }
8666
8667 #[test]
8668 fn editor_sticky_col_tracks_horizontal_motion() {
8669 let mut e = editor_with("longline\nhi\nlongline");
8670 run_keys(&mut e, "fl");
8675 let landed = e.cursor().1;
8676 assert!(landed > 0, "fl should have moved");
8677 run_keys(&mut e, "j");
8678 assert_eq!(e.sticky_col(), Some(landed));
8681 }
8682
8683 #[test]
8684 fn buffer_content_mirrors_textarea_after_insert() {
8685 let mut e = editor_with("hello");
8686 run_keys(&mut e, "iXYZ<Esc>");
8687 let text = e.buffer().lines().join("\n");
8688 assert_eq!(e.buffer.as_string(), text);
8689 }
8690
8691 #[test]
8692 fn buffer_content_mirrors_textarea_after_delete() {
8693 let mut e = editor_with("alpha bravo charlie");
8694 run_keys(&mut e, "dw");
8695 let text = e.buffer().lines().join("\n");
8696 assert_eq!(e.buffer.as_string(), text);
8697 }
8698
8699 #[test]
8700 fn buffer_content_mirrors_textarea_after_dd() {
8701 let mut e = editor_with("a\nb\nc\nd");
8702 run_keys(&mut e, "jdd");
8703 let text = e.buffer().lines().join("\n");
8704 assert_eq!(e.buffer.as_string(), text);
8705 }
8706
8707 #[test]
8708 fn buffer_content_mirrors_textarea_after_open_line() {
8709 let mut e = editor_with("foo\nbar");
8710 run_keys(&mut e, "oNEW<Esc>");
8711 let text = e.buffer().lines().join("\n");
8712 assert_eq!(e.buffer.as_string(), text);
8713 }
8714
8715 #[test]
8716 fn buffer_content_mirrors_textarea_after_paste() {
8717 let mut e = editor_with("hello");
8718 run_keys(&mut e, "yy");
8719 run_keys(&mut e, "p");
8720 let text = e.buffer().lines().join("\n");
8721 assert_eq!(e.buffer.as_string(), text);
8722 }
8723
8724 #[test]
8725 fn buffer_selection_none_in_normal_mode() {
8726 let e = editor_with("foo bar");
8727 assert!(e.buffer_selection().is_none());
8728 }
8729
8730 #[test]
8731 fn buffer_selection_char_in_visual_mode() {
8732 use hjkl_buffer::{Position, Selection};
8733 let mut e = editor_with("hello world");
8734 run_keys(&mut e, "vlll");
8735 assert_eq!(
8736 e.buffer_selection(),
8737 Some(Selection::Char {
8738 anchor: Position::new(0, 0),
8739 head: Position::new(0, 3),
8740 })
8741 );
8742 }
8743
8744 #[test]
8745 fn buffer_selection_line_in_visual_line_mode() {
8746 use hjkl_buffer::Selection;
8747 let mut e = editor_with("a\nb\nc\nd");
8748 run_keys(&mut e, "Vj");
8749 assert_eq!(
8750 e.buffer_selection(),
8751 Some(Selection::Line {
8752 anchor_row: 0,
8753 head_row: 1,
8754 })
8755 );
8756 }
8757
8758 #[test]
8759 fn wrapscan_off_blocks_wrap_around() {
8760 let mut e = editor_with("first\nsecond\nthird\n");
8761 e.settings_mut().wrapscan = false;
8762 e.jump_cursor(2, 0);
8764 run_keys(&mut e, "/first<CR>");
8765 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8767 e.settings_mut().wrapscan = true;
8769 run_keys(&mut e, "/first<CR>");
8770 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8771 }
8772
8773 #[test]
8774 fn smartcase_uppercase_pattern_stays_sensitive() {
8775 let mut e = editor_with("foo\nFoo\nBAR\n");
8776 e.settings_mut().ignore_case = true;
8777 e.settings_mut().smartcase = true;
8778 run_keys(&mut e, "/foo<CR>");
8781 let r1 = e
8782 .search_state()
8783 .pattern
8784 .as_ref()
8785 .unwrap()
8786 .as_str()
8787 .to_string();
8788 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8789 run_keys(&mut e, "/Foo<CR>");
8791 let r2 = e
8792 .search_state()
8793 .pattern
8794 .as_ref()
8795 .unwrap()
8796 .as_str()
8797 .to_string();
8798 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8799 }
8800
8801 #[test]
8802 fn enter_with_autoindent_copies_leading_whitespace() {
8803 let mut e = editor_with(" foo");
8804 e.jump_cursor(0, 7);
8805 run_keys(&mut e, "i<CR>");
8806 assert_eq!(e.buffer.line(1).unwrap(), " ");
8807 }
8808
8809 #[test]
8810 fn enter_without_autoindent_inserts_bare_newline() {
8811 let mut e = editor_with(" foo");
8812 e.settings_mut().autoindent = false;
8813 e.jump_cursor(0, 7);
8814 run_keys(&mut e, "i<CR>");
8815 assert_eq!(e.buffer.line(1).unwrap(), "");
8816 }
8817
8818 #[test]
8819 fn iskeyword_default_treats_alnum_underscore_as_word() {
8820 let mut e = editor_with("foo_bar baz");
8821 e.jump_cursor(0, 0);
8825 run_keys(&mut e, "*");
8826 let p = e
8827 .search_state()
8828 .pattern
8829 .as_ref()
8830 .unwrap()
8831 .as_str()
8832 .to_string();
8833 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8834 }
8835
8836 #[test]
8837 fn w_motion_respects_custom_iskeyword() {
8838 let mut e = editor_with("foo-bar baz");
8842 run_keys(&mut e, "w");
8843 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
8844 let mut e2 = editor_with("foo-bar baz");
8847 e2.set_iskeyword("@,_,45");
8848 run_keys(&mut e2, "w");
8849 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
8850 }
8851
8852 #[test]
8853 fn iskeyword_with_dash_treats_dash_as_word_char() {
8854 let mut e = editor_with("foo-bar baz");
8855 e.settings_mut().iskeyword = "@,_,45".to_string();
8856 e.jump_cursor(0, 0);
8857 run_keys(&mut e, "*");
8858 let p = e
8859 .search_state()
8860 .pattern
8861 .as_ref()
8862 .unwrap()
8863 .as_str()
8864 .to_string();
8865 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
8866 }
8867
8868 #[test]
8869 fn timeoutlen_drops_pending_g_prefix() {
8870 use std::time::{Duration, Instant};
8871 let mut e = editor_with("a\nb\nc");
8872 e.jump_cursor(2, 0);
8873 run_keys(&mut e, "g");
8875 assert!(matches!(e.vim.pending, super::Pending::G));
8876 e.settings.timeout_len = Duration::from_nanos(0);
8884 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
8885 e.vim.last_input_host_at = Some(Duration::ZERO);
8886 run_keys(&mut e, "g");
8890 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
8892 }
8893
8894 #[test]
8895 fn undobreak_on_breaks_group_at_arrow_motion() {
8896 let mut e = editor_with("");
8897 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8899 let line = e.buffer.line(0).unwrap_or("").to_string();
8902 assert!(line.contains("aaa"), "after undobreak: {line:?}");
8903 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
8904 }
8905
8906 #[test]
8907 fn undobreak_off_keeps_full_run_in_one_group() {
8908 let mut e = editor_with("");
8909 e.settings_mut().undo_break_on_motion = false;
8910 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8911 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
8914 }
8915
8916 #[test]
8917 fn undobreak_round_trips_through_options() {
8918 let e = editor_with("");
8919 let opts = e.current_options();
8920 assert!(opts.undo_break_on_motion);
8921 let mut e2 = editor_with("");
8922 let mut new_opts = opts.clone();
8923 new_opts.undo_break_on_motion = false;
8924 e2.apply_options(&new_opts);
8925 assert!(!e2.current_options().undo_break_on_motion);
8926 }
8927
8928 #[test]
8929 fn undo_levels_cap_drops_oldest() {
8930 let mut e = editor_with("abcde");
8931 e.settings_mut().undo_levels = 3;
8932 run_keys(&mut e, "ra");
8933 run_keys(&mut e, "lrb");
8934 run_keys(&mut e, "lrc");
8935 run_keys(&mut e, "lrd");
8936 run_keys(&mut e, "lre");
8937 assert_eq!(e.undo_stack_len(), 3);
8938 }
8939
8940 #[test]
8941 fn tab_inserts_literal_tab_when_noexpandtab() {
8942 let mut e = editor_with("");
8943 e.settings_mut().expandtab = false;
8946 e.settings_mut().softtabstop = 0;
8947 run_keys(&mut e, "i");
8948 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8949 assert_eq!(e.buffer.line(0).unwrap(), "\t");
8950 }
8951
8952 #[test]
8953 fn tab_inserts_spaces_when_expandtab() {
8954 let mut e = editor_with("");
8955 e.settings_mut().expandtab = true;
8956 e.settings_mut().tabstop = 4;
8957 run_keys(&mut e, "i");
8958 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8959 assert_eq!(e.buffer.line(0).unwrap(), " ");
8960 }
8961
8962 #[test]
8963 fn tab_with_softtabstop_fills_to_next_boundary() {
8964 let mut e = editor_with("ab");
8966 e.settings_mut().expandtab = true;
8967 e.settings_mut().tabstop = 8;
8968 e.settings_mut().softtabstop = 4;
8969 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8971 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
8972 }
8973
8974 #[test]
8975 fn backspace_deletes_softtab_run() {
8976 let mut e = editor_with(" x");
8979 e.settings_mut().softtabstop = 4;
8980 run_keys(&mut e, "fxi");
8982 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8983 assert_eq!(e.buffer.line(0).unwrap(), "x");
8984 }
8985
8986 #[test]
8987 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
8988 let mut e = editor_with(" x");
8991 e.settings_mut().softtabstop = 4;
8992 run_keys(&mut e, "fxi");
8993 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8994 assert_eq!(e.buffer.line(0).unwrap(), " x");
8995 }
8996
8997 #[test]
8998 fn readonly_blocks_insert_mutation() {
8999 let mut e = editor_with("hello");
9000 e.settings_mut().readonly = true;
9001 run_keys(&mut e, "iX<Esc>");
9002 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9003 }
9004
9005 #[cfg(feature = "ratatui")]
9006 #[test]
9007 fn intern_ratatui_style_dedups_repeated_styles() {
9008 use ratatui::style::{Color, Style};
9009 let mut e = editor_with("");
9010 let red = Style::default().fg(Color::Red);
9011 let blue = Style::default().fg(Color::Blue);
9012 let id_r1 = e.intern_ratatui_style(red);
9013 let id_r2 = e.intern_ratatui_style(red);
9014 let id_b = e.intern_ratatui_style(blue);
9015 assert_eq!(id_r1, id_r2);
9016 assert_ne!(id_r1, id_b);
9017 assert_eq!(e.style_table().len(), 2);
9018 }
9019
9020 #[cfg(feature = "ratatui")]
9021 #[test]
9022 fn install_ratatui_syntax_spans_translates_styled_spans() {
9023 use ratatui::style::{Color, Style};
9024 let mut e = editor_with("SELECT foo");
9025 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9026 let by_row = e.buffer_spans();
9027 assert_eq!(by_row.len(), 1);
9028 assert_eq!(by_row[0].len(), 1);
9029 assert_eq!(by_row[0][0].start_byte, 0);
9030 assert_eq!(by_row[0][0].end_byte, 6);
9031 let id = by_row[0][0].style;
9032 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9033 }
9034
9035 #[cfg(feature = "ratatui")]
9036 #[test]
9037 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9038 use ratatui::style::{Color, Style};
9039 let mut e = editor_with("hello");
9040 e.install_ratatui_syntax_spans(vec![vec![(
9041 0,
9042 usize::MAX,
9043 Style::default().fg(Color::Blue),
9044 )]]);
9045 let by_row = e.buffer_spans();
9046 assert_eq!(by_row[0][0].end_byte, 5);
9047 }
9048
9049 #[cfg(feature = "ratatui")]
9050 #[test]
9051 fn install_ratatui_syntax_spans_drops_zero_width() {
9052 use ratatui::style::{Color, Style};
9053 let mut e = editor_with("abc");
9054 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9055 assert!(e.buffer_spans()[0].is_empty());
9056 }
9057
9058 #[test]
9059 fn named_register_yank_into_a_then_paste_from_a() {
9060 let mut e = editor_with("hello world\nsecond");
9061 run_keys(&mut e, "\"ayw");
9062 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9064 run_keys(&mut e, "j0\"aP");
9066 assert_eq!(e.buffer().lines()[1], "hello second");
9067 }
9068
9069 #[test]
9070 fn capital_r_overstrikes_chars() {
9071 let mut e = editor_with("hello");
9072 e.jump_cursor(0, 0);
9073 run_keys(&mut e, "RXY<Esc>");
9074 assert_eq!(e.buffer().lines()[0], "XYllo");
9076 }
9077
9078 #[test]
9079 fn capital_r_at_eol_appends() {
9080 let mut e = editor_with("hi");
9081 e.jump_cursor(0, 1);
9082 run_keys(&mut e, "RXYZ<Esc>");
9084 assert_eq!(e.buffer().lines()[0], "hXYZ");
9085 }
9086
9087 #[test]
9088 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9089 let mut e = editor_with("abc");
9093 e.jump_cursor(0, 0);
9094 run_keys(&mut e, "RX<Esc>");
9095 assert_eq!(e.buffer().lines()[0], "Xbc");
9096 }
9097
9098 #[test]
9099 fn ctrl_r_in_insert_pastes_named_register() {
9100 let mut e = editor_with("hello world");
9101 run_keys(&mut e, "\"ayw");
9103 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9104 run_keys(&mut e, "o");
9106 assert_eq!(e.vim_mode(), VimMode::Insert);
9107 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9108 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9109 assert_eq!(e.buffer().lines()[1], "hello ");
9110 assert_eq!(e.cursor(), (1, 6));
9112 assert_eq!(e.vim_mode(), VimMode::Insert);
9114 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9115 assert_eq!(e.buffer().lines()[1], "hello X");
9116 }
9117
9118 #[test]
9119 fn ctrl_r_with_unnamed_register() {
9120 let mut e = editor_with("foo");
9121 run_keys(&mut e, "yiw");
9122 run_keys(&mut e, "A ");
9123 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9125 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9126 assert_eq!(e.buffer().lines()[0], "foo foo");
9127 }
9128
9129 #[test]
9130 fn ctrl_r_unknown_selector_is_no_op() {
9131 let mut e = editor_with("abc");
9132 run_keys(&mut e, "A");
9133 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9134 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9137 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9138 assert_eq!(e.buffer().lines()[0], "abcZ");
9139 }
9140
9141 #[test]
9142 fn ctrl_r_multiline_register_pastes_with_newlines() {
9143 let mut e = editor_with("alpha\nbeta\ngamma");
9144 run_keys(&mut e, "\"byy");
9146 run_keys(&mut e, "j\"byy");
9147 run_keys(&mut e, "ggVj\"by");
9151 let payload = e.registers().read('b').unwrap().text.clone();
9152 assert!(payload.contains('\n'));
9153 run_keys(&mut e, "Go");
9154 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9155 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9156 let total_lines = e.buffer().lines().len();
9159 assert!(total_lines >= 5);
9160 }
9161
9162 #[test]
9163 fn yank_zero_holds_last_yank_after_delete() {
9164 let mut e = editor_with("hello world");
9165 run_keys(&mut e, "yw");
9166 let yanked = e.registers().read('0').unwrap().text.clone();
9167 assert!(!yanked.is_empty());
9168 run_keys(&mut e, "dw");
9170 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9171 assert!(!e.registers().read('1').unwrap().text.is_empty());
9173 }
9174
9175 #[test]
9176 fn delete_ring_rotates_through_one_through_nine() {
9177 let mut e = editor_with("a b c d e f g h i j");
9178 for _ in 0..3 {
9180 run_keys(&mut e, "dw");
9181 }
9182 let r1 = e.registers().read('1').unwrap().text.clone();
9184 let r2 = e.registers().read('2').unwrap().text.clone();
9185 let r3 = e.registers().read('3').unwrap().text.clone();
9186 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9187 assert_ne!(r1, r2);
9188 assert_ne!(r2, r3);
9189 }
9190
9191 #[test]
9192 fn capital_register_appends_to_lowercase() {
9193 let mut e = editor_with("foo bar");
9194 run_keys(&mut e, "\"ayw");
9195 let first = e.registers().read('a').unwrap().text.clone();
9196 assert!(first.contains("foo"));
9197 run_keys(&mut e, "w\"Ayw");
9199 let combined = e.registers().read('a').unwrap().text.clone();
9200 assert!(combined.starts_with(&first));
9201 assert!(combined.contains("bar"));
9202 }
9203
9204 #[test]
9205 fn zf_in_visual_line_creates_closed_fold() {
9206 let mut e = editor_with("a\nb\nc\nd\ne");
9207 e.jump_cursor(1, 0);
9209 run_keys(&mut e, "Vjjzf");
9210 assert_eq!(e.buffer().folds().len(), 1);
9211 let f = e.buffer().folds()[0];
9212 assert_eq!(f.start_row, 1);
9213 assert_eq!(f.end_row, 3);
9214 assert!(f.closed);
9215 }
9216
9217 #[test]
9218 fn zfj_in_normal_creates_two_row_fold() {
9219 let mut e = editor_with("a\nb\nc\nd\ne");
9220 e.jump_cursor(1, 0);
9221 run_keys(&mut e, "zfj");
9222 assert_eq!(e.buffer().folds().len(), 1);
9223 let f = e.buffer().folds()[0];
9224 assert_eq!(f.start_row, 1);
9225 assert_eq!(f.end_row, 2);
9226 assert!(f.closed);
9227 assert_eq!(e.cursor().0, 1);
9229 }
9230
9231 #[test]
9232 fn zf_with_count_folds_count_rows() {
9233 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9234 e.jump_cursor(0, 0);
9235 run_keys(&mut e, "zf3j");
9237 assert_eq!(e.buffer().folds().len(), 1);
9238 let f = e.buffer().folds()[0];
9239 assert_eq!(f.start_row, 0);
9240 assert_eq!(f.end_row, 3);
9241 }
9242
9243 #[test]
9244 fn zfk_folds_upward_range() {
9245 let mut e = editor_with("a\nb\nc\nd\ne");
9246 e.jump_cursor(3, 0);
9247 run_keys(&mut e, "zfk");
9248 let f = e.buffer().folds()[0];
9249 assert_eq!(f.start_row, 2);
9251 assert_eq!(f.end_row, 3);
9252 }
9253
9254 #[test]
9255 fn zf_capital_g_folds_to_bottom() {
9256 let mut e = editor_with("a\nb\nc\nd\ne");
9257 e.jump_cursor(1, 0);
9258 run_keys(&mut e, "zfG");
9260 let f = e.buffer().folds()[0];
9261 assert_eq!(f.start_row, 1);
9262 assert_eq!(f.end_row, 4);
9263 }
9264
9265 #[test]
9266 fn zfgg_folds_to_top_via_operator_pipeline() {
9267 let mut e = editor_with("a\nb\nc\nd\ne");
9268 e.jump_cursor(3, 0);
9269 run_keys(&mut e, "zfgg");
9273 let f = e.buffer().folds()[0];
9274 assert_eq!(f.start_row, 0);
9275 assert_eq!(f.end_row, 3);
9276 }
9277
9278 #[test]
9279 fn zfip_folds_paragraph_via_text_object() {
9280 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9281 e.jump_cursor(1, 0);
9282 run_keys(&mut e, "zfip");
9284 assert_eq!(e.buffer().folds().len(), 1);
9285 let f = e.buffer().folds()[0];
9286 assert_eq!(f.start_row, 0);
9287 assert_eq!(f.end_row, 2);
9288 }
9289
9290 #[test]
9291 fn zfap_folds_paragraph_with_trailing_blank() {
9292 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9293 e.jump_cursor(0, 0);
9294 run_keys(&mut e, "zfap");
9296 let f = e.buffer().folds()[0];
9297 assert_eq!(f.start_row, 0);
9298 assert_eq!(f.end_row, 3);
9299 }
9300
9301 #[test]
9302 fn zf_paragraph_motion_folds_to_blank() {
9303 let mut e = editor_with("alpha\nbeta\n\ngamma");
9304 e.jump_cursor(0, 0);
9305 run_keys(&mut e, "zf}");
9307 let f = e.buffer().folds()[0];
9308 assert_eq!(f.start_row, 0);
9309 assert_eq!(f.end_row, 2);
9310 }
9311
9312 #[test]
9313 fn za_toggles_fold_under_cursor() {
9314 let mut e = editor_with("a\nb\nc\nd");
9315 e.buffer_mut().add_fold(1, 2, true);
9316 e.jump_cursor(1, 0);
9317 run_keys(&mut e, "za");
9318 assert!(!e.buffer().folds()[0].closed);
9319 run_keys(&mut e, "za");
9320 assert!(e.buffer().folds()[0].closed);
9321 }
9322
9323 #[test]
9324 fn zr_opens_all_folds_zm_closes_all() {
9325 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9326 e.buffer_mut().add_fold(0, 1, true);
9327 e.buffer_mut().add_fold(2, 3, true);
9328 e.buffer_mut().add_fold(4, 5, true);
9329 run_keys(&mut e, "zR");
9330 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9331 run_keys(&mut e, "zM");
9332 assert!(e.buffer().folds().iter().all(|f| f.closed));
9333 }
9334
9335 #[test]
9336 fn ze_clears_all_folds() {
9337 let mut e = editor_with("a\nb\nc\nd");
9338 e.buffer_mut().add_fold(0, 1, true);
9339 e.buffer_mut().add_fold(2, 3, false);
9340 run_keys(&mut e, "zE");
9341 assert!(e.buffer().folds().is_empty());
9342 }
9343
9344 #[test]
9345 fn g_underscore_jumps_to_last_non_blank() {
9346 let mut e = editor_with("hello world ");
9347 run_keys(&mut e, "g_");
9348 assert_eq!(e.cursor().1, 10);
9350 }
9351
9352 #[test]
9353 fn gj_and_gk_alias_j_and_k() {
9354 let mut e = editor_with("a\nb\nc");
9355 run_keys(&mut e, "gj");
9356 assert_eq!(e.cursor().0, 1);
9357 run_keys(&mut e, "gk");
9358 assert_eq!(e.cursor().0, 0);
9359 }
9360
9361 #[test]
9362 fn paragraph_motions_walk_blank_lines() {
9363 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9364 run_keys(&mut e, "}");
9365 assert_eq!(e.cursor().0, 2);
9366 run_keys(&mut e, "}");
9367 assert_eq!(e.cursor().0, 5);
9368 run_keys(&mut e, "{");
9369 assert_eq!(e.cursor().0, 2);
9370 }
9371
9372 #[test]
9373 fn gv_reenters_last_visual_selection() {
9374 let mut e = editor_with("alpha\nbeta\ngamma");
9375 run_keys(&mut e, "Vj");
9376 run_keys(&mut e, "<Esc>");
9378 assert_eq!(e.vim_mode(), VimMode::Normal);
9379 run_keys(&mut e, "gv");
9381 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9382 }
9383
9384 #[test]
9385 fn o_in_visual_swaps_anchor_and_cursor() {
9386 let mut e = editor_with("hello world");
9387 run_keys(&mut e, "vllll");
9389 assert_eq!(e.cursor().1, 4);
9390 run_keys(&mut e, "o");
9392 assert_eq!(e.cursor().1, 0);
9393 assert_eq!(e.vim.visual_anchor, (0, 4));
9395 }
9396
9397 #[test]
9398 fn editing_inside_fold_invalidates_it() {
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, "iX<Esc>");
9404 assert!(e.buffer().folds().is_empty());
9406 }
9407
9408 #[test]
9409 fn zd_removes_fold_under_cursor() {
9410 let mut e = editor_with("a\nb\nc\nd");
9411 e.buffer_mut().add_fold(1, 2, true);
9412 e.jump_cursor(2, 0);
9413 run_keys(&mut e, "zd");
9414 assert!(e.buffer().folds().is_empty());
9415 }
9416
9417 #[test]
9418 fn take_fold_ops_observes_z_keystroke_dispatch() {
9419 use crate::types::FoldOp;
9424 let mut e = editor_with("a\nb\nc\nd");
9425 e.buffer_mut().add_fold(1, 2, true);
9426 e.jump_cursor(1, 0);
9427 let _ = e.take_fold_ops();
9430 run_keys(&mut e, "zo");
9431 run_keys(&mut e, "zM");
9432 let ops = e.take_fold_ops();
9433 assert_eq!(ops.len(), 2);
9434 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9435 assert!(matches!(ops[1], FoldOp::CloseAll));
9436 assert!(e.take_fold_ops().is_empty());
9438 }
9439
9440 #[test]
9441 fn edit_pipeline_emits_invalidate_fold_op() {
9442 use crate::types::FoldOp;
9445 let mut e = editor_with("a\nb\nc\nd");
9446 e.buffer_mut().add_fold(1, 2, true);
9447 e.jump_cursor(1, 0);
9448 let _ = e.take_fold_ops();
9449 run_keys(&mut e, "iX<Esc>");
9450 let ops = e.take_fold_ops();
9451 assert!(
9452 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9453 "expected at least one Invalidate op, got {ops:?}"
9454 );
9455 }
9456
9457 #[test]
9458 fn dot_mark_jumps_to_last_edit_position() {
9459 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9460 e.jump_cursor(2, 0);
9461 run_keys(&mut e, "iX<Esc>");
9463 let after_edit = e.cursor();
9464 run_keys(&mut e, "gg");
9466 assert_eq!(e.cursor().0, 0);
9467 run_keys(&mut e, "'.");
9469 assert_eq!(e.cursor().0, after_edit.0);
9470 }
9471
9472 #[test]
9473 fn quote_quote_returns_to_pre_jump_position() {
9474 let mut e = editor_with_rows(50, 20);
9475 e.jump_cursor(10, 2);
9476 let before = e.cursor();
9477 run_keys(&mut e, "G");
9479 assert_ne!(e.cursor(), before);
9480 run_keys(&mut e, "''");
9482 assert_eq!(e.cursor().0, before.0);
9483 }
9484
9485 #[test]
9486 fn backtick_backtick_restores_exact_pre_jump_pos() {
9487 let mut e = editor_with_rows(50, 20);
9488 e.jump_cursor(7, 3);
9489 let before = e.cursor();
9490 run_keys(&mut e, "G");
9491 run_keys(&mut e, "``");
9492 assert_eq!(e.cursor(), before);
9493 }
9494
9495 #[test]
9496 fn macro_record_and_replay_basic() {
9497 let mut e = editor_with("foo\nbar\nbaz");
9498 run_keys(&mut e, "qaIX<Esc>jq");
9500 assert_eq!(e.buffer().lines()[0], "Xfoo");
9501 run_keys(&mut e, "@a");
9503 assert_eq!(e.buffer().lines()[1], "Xbar");
9504 run_keys(&mut e, "j@@");
9506 assert_eq!(e.buffer().lines()[2], "Xbaz");
9507 }
9508
9509 #[test]
9510 fn macro_count_replays_n_times() {
9511 let mut e = editor_with("a\nb\nc\nd\ne");
9512 run_keys(&mut e, "qajq");
9514 assert_eq!(e.cursor().0, 1);
9515 run_keys(&mut e, "3@a");
9517 assert_eq!(e.cursor().0, 4);
9518 }
9519
9520 #[test]
9521 fn macro_capital_q_appends_to_lowercase_register() {
9522 let mut e = editor_with("hello");
9523 run_keys(&mut e, "qall<Esc>q");
9524 run_keys(&mut e, "qAhh<Esc>q");
9525 let text = e.registers().read('a').unwrap().text.clone();
9528 assert!(text.contains("ll<Esc>"));
9529 assert!(text.contains("hh<Esc>"));
9530 }
9531
9532 #[test]
9533 fn buffer_selection_block_in_visual_block_mode() {
9534 use hjkl_buffer::{Position, Selection};
9535 let mut e = editor_with("aaaa\nbbbb\ncccc");
9536 run_keys(&mut e, "<C-v>jl");
9537 assert_eq!(
9538 e.buffer_selection(),
9539 Some(Selection::Block {
9540 anchor: Position::new(0, 0),
9541 head: Position::new(1, 1),
9542 })
9543 );
9544 }
9545
9546 #[test]
9549 fn n_after_question_mark_keeps_walking_backward() {
9550 let mut e = editor_with("foo bar foo baz foo end");
9553 e.jump_cursor(0, 22);
9554 run_keys(&mut e, "?foo<CR>");
9555 assert_eq!(e.cursor().1, 16);
9556 run_keys(&mut e, "n");
9557 assert_eq!(e.cursor().1, 8);
9558 run_keys(&mut e, "N");
9559 assert_eq!(e.cursor().1, 16);
9560 }
9561
9562 #[test]
9563 fn nested_macro_chord_records_literal_keys() {
9564 let mut e = editor_with("alpha\nbeta\ngamma");
9567 run_keys(&mut e, "qblq");
9569 run_keys(&mut e, "qaIX<Esc>q");
9572 e.jump_cursor(1, 0);
9574 run_keys(&mut e, "@a");
9575 assert_eq!(e.buffer().lines()[1], "Xbeta");
9576 }
9577
9578 #[test]
9579 fn shift_gt_motion_indents_one_line() {
9580 let mut e = editor_with("hello world");
9584 run_keys(&mut e, ">w");
9585 assert_eq!(e.buffer().lines()[0], " hello world");
9586 }
9587
9588 #[test]
9589 fn shift_lt_motion_outdents_one_line() {
9590 let mut e = editor_with(" hello world");
9591 run_keys(&mut e, "<lt>w");
9592 assert_eq!(e.buffer().lines()[0], " hello world");
9594 }
9595
9596 #[test]
9597 fn shift_gt_text_object_indents_paragraph() {
9598 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9599 e.jump_cursor(0, 0);
9600 run_keys(&mut e, ">ip");
9601 assert_eq!(e.buffer().lines()[0], " alpha");
9602 assert_eq!(e.buffer().lines()[1], " beta");
9603 assert_eq!(e.buffer().lines()[2], " gamma");
9604 assert_eq!(e.buffer().lines()[4], "rest");
9606 }
9607
9608 #[test]
9609 fn ctrl_o_runs_exactly_one_normal_command() {
9610 let mut e = editor_with("alpha beta gamma");
9613 e.jump_cursor(0, 0);
9614 run_keys(&mut e, "i");
9615 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9616 run_keys(&mut e, "dw");
9617 assert_eq!(e.vim_mode(), VimMode::Insert);
9619 run_keys(&mut e, "X");
9621 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9622 }
9623
9624 #[test]
9625 fn macro_replay_respects_mode_switching() {
9626 let mut e = editor_with("hi");
9630 run_keys(&mut e, "qaiX<Esc>0q");
9631 assert_eq!(e.vim_mode(), VimMode::Normal);
9632 e.set_content("yo");
9634 run_keys(&mut e, "@a");
9635 assert_eq!(e.vim_mode(), VimMode::Normal);
9636 assert_eq!(e.cursor().1, 0);
9637 assert_eq!(e.buffer().lines()[0], "Xyo");
9638 }
9639
9640 #[test]
9641 fn macro_recorded_text_round_trips_through_register() {
9642 let mut e = editor_with("");
9646 run_keys(&mut e, "qaiX<Esc>q");
9647 let text = e.registers().read('a').unwrap().text.clone();
9648 assert!(text.starts_with("iX"));
9649 run_keys(&mut e, "@a");
9651 assert_eq!(e.buffer().lines()[0], "XX");
9652 }
9653
9654 #[test]
9655 fn dot_after_macro_replays_macros_last_change() {
9656 let mut e = editor_with("ab\ncd\nef");
9659 run_keys(&mut e, "qaIX<Esc>jq");
9662 assert_eq!(e.buffer().lines()[0], "Xab");
9663 run_keys(&mut e, "@a");
9664 assert_eq!(e.buffer().lines()[1], "Xcd");
9665 let row_before_dot = e.cursor().0;
9668 run_keys(&mut e, ".");
9669 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9670 }
9671
9672 fn si_editor(content: &str) -> Editor {
9678 let opts = crate::types::Options {
9679 shiftwidth: 4,
9680 softtabstop: 4,
9681 expandtab: true,
9682 smartindent: true,
9683 autoindent: true,
9684 ..crate::types::Options::default()
9685 };
9686 let mut e = Editor::new(
9687 hjkl_buffer::Buffer::new(),
9688 crate::types::DefaultHost::new(),
9689 opts,
9690 );
9691 e.set_content(content);
9692 e
9693 }
9694
9695 #[test]
9696 fn smartindent_bumps_indent_after_open_brace() {
9697 let mut e = si_editor("fn foo() {");
9699 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9701 assert_eq!(
9702 e.buffer().lines()[1],
9703 " ",
9704 "smartindent should bump one shiftwidth after {{"
9705 );
9706 }
9707
9708 #[test]
9709 fn smartindent_no_bump_when_off() {
9710 let mut e = si_editor("fn foo() {");
9713 e.settings_mut().smartindent = false;
9714 e.jump_cursor(0, 10);
9715 run_keys(&mut e, "i<CR>");
9716 assert_eq!(
9717 e.buffer().lines()[1],
9718 "",
9719 "without smartindent, no bump: new line copies empty leading ws"
9720 );
9721 }
9722
9723 #[test]
9724 fn smartindent_uses_tab_when_noexpandtab() {
9725 let opts = crate::types::Options {
9727 shiftwidth: 4,
9728 softtabstop: 0,
9729 expandtab: false,
9730 smartindent: true,
9731 autoindent: true,
9732 ..crate::types::Options::default()
9733 };
9734 let mut e = Editor::new(
9735 hjkl_buffer::Buffer::new(),
9736 crate::types::DefaultHost::new(),
9737 opts,
9738 );
9739 e.set_content("fn foo() {");
9740 e.jump_cursor(0, 10);
9741 run_keys(&mut e, "i<CR>");
9742 assert_eq!(
9743 e.buffer().lines()[1],
9744 "\t",
9745 "noexpandtab: smartindent bump inserts a literal tab"
9746 );
9747 }
9748
9749 #[test]
9750 fn smartindent_dedent_on_close_brace() {
9751 let mut e = si_editor("fn foo() {");
9754 e.set_content("fn foo() {\n ");
9756 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9758 assert_eq!(
9759 e.buffer().lines()[1],
9760 "}",
9761 "close brace on whitespace-only line should dedent"
9762 );
9763 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9764 }
9765
9766 #[test]
9767 fn smartindent_no_dedent_when_off() {
9768 let mut e = si_editor("fn foo() {\n ");
9770 e.settings_mut().smartindent = false;
9771 e.jump_cursor(1, 4);
9772 run_keys(&mut e, "i}");
9773 assert_eq!(
9774 e.buffer().lines()[1],
9775 " }",
9776 "without smartindent, `}}` just appends at cursor"
9777 );
9778 }
9779
9780 #[test]
9781 fn smartindent_no_dedent_mid_line() {
9782 let mut e = si_editor(" let x = 1");
9785 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9787 assert_eq!(
9788 e.buffer().lines()[0],
9789 " let x = 1}",
9790 "mid-line `}}` should not dedent"
9791 );
9792 }
9793
9794 #[test]
9798 fn count_5x_fills_unnamed_register() {
9799 let mut e = editor_with("hello world\n");
9800 e.jump_cursor(0, 0);
9801 run_keys(&mut e, "5x");
9802 assert_eq!(e.buffer().lines()[0], " world");
9803 assert_eq!(e.cursor(), (0, 0));
9804 assert_eq!(e.yank(), "hello");
9805 }
9806
9807 #[test]
9808 fn x_fills_unnamed_register_single_char() {
9809 let mut e = editor_with("abc\n");
9810 e.jump_cursor(0, 0);
9811 run_keys(&mut e, "x");
9812 assert_eq!(e.buffer().lines()[0], "bc");
9813 assert_eq!(e.yank(), "a");
9814 }
9815
9816 #[test]
9817 fn big_x_fills_unnamed_register() {
9818 let mut e = editor_with("hello\n");
9819 e.jump_cursor(0, 3);
9820 run_keys(&mut e, "X");
9821 assert_eq!(e.buffer().lines()[0], "helo");
9822 assert_eq!(e.yank(), "l");
9823 }
9824
9825 #[test]
9827 fn g_motion_trailing_newline_lands_on_last_content_row() {
9828 let mut e = editor_with("foo\nbar\nbaz\n");
9829 e.jump_cursor(0, 0);
9830 run_keys(&mut e, "G");
9831 assert_eq!(
9833 e.cursor().0,
9834 2,
9835 "G should land on row 2 (baz), not row 3 (phantom empty)"
9836 );
9837 }
9838
9839 #[test]
9841 fn dd_last_line_clamps_cursor_to_new_last_row() {
9842 let mut e = editor_with("foo\nbar\n");
9843 e.jump_cursor(1, 0);
9844 run_keys(&mut e, "dd");
9845 assert_eq!(e.buffer().lines()[0], "foo");
9846 assert_eq!(
9847 e.cursor(),
9848 (0, 0),
9849 "cursor should clamp to row 0 after dd on last content line"
9850 );
9851 }
9852
9853 #[test]
9855 fn d_dollar_cursor_on_last_char() {
9856 let mut e = editor_with("hello world\n");
9857 e.jump_cursor(0, 5);
9858 run_keys(&mut e, "d$");
9859 assert_eq!(e.buffer().lines()[0], "hello");
9860 assert_eq!(
9861 e.cursor(),
9862 (0, 4),
9863 "d$ should leave cursor on col 4, not col 5"
9864 );
9865 }
9866
9867 #[test]
9869 fn undo_insert_clamps_cursor_to_last_valid_col() {
9870 let mut e = editor_with("hello\n");
9871 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
9873 assert_eq!(e.buffer().lines()[0], "hello");
9874 assert_eq!(
9875 e.cursor(),
9876 (0, 4),
9877 "undo should clamp cursor to col 4 on 'hello'"
9878 );
9879 }
9880
9881 #[test]
9883 fn da_doublequote_eats_trailing_whitespace() {
9884 let mut e = editor_with("say \"hello\" there\n");
9885 e.jump_cursor(0, 6);
9886 run_keys(&mut e, "da\"");
9887 assert_eq!(e.buffer().lines()[0], "say there");
9888 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
9889 }
9890
9891 #[test]
9893 fn dab_cursor_col_clamped_after_delete() {
9894 let mut e = editor_with("fn x() {\n body\n}\n");
9895 e.jump_cursor(1, 4);
9896 run_keys(&mut e, "daB");
9897 assert_eq!(e.buffer().lines()[0], "fn x() ");
9898 assert_eq!(
9899 e.cursor(),
9900 (0, 6),
9901 "daB should leave cursor at col 6, not 7"
9902 );
9903 }
9904
9905 #[test]
9907 fn dib_preserves_surrounding_newlines() {
9908 let mut e = editor_with("{\n body\n}\n");
9909 e.jump_cursor(1, 4);
9910 run_keys(&mut e, "diB");
9911 assert_eq!(e.buffer().lines()[0], "{");
9912 assert_eq!(e.buffer().lines()[1], "}");
9913 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
9914 }
9915
9916 #[test]
9917 fn is_chord_pending_tracks_replace_state() {
9918 let mut e = editor_with("abc\n");
9919 assert!(!e.is_chord_pending());
9920 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
9922 assert!(e.is_chord_pending(), "engine should be pending after r");
9923 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
9925 assert!(
9926 !e.is_chord_pending(),
9927 "engine pending should clear after replace"
9928 );
9929 }
9930}