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) = if snap.anchor <= snap.cursor {
934 (snap.anchor, snap.cursor)
935 } else {
936 (snap.cursor, snap.anchor)
937 };
938 ed.set_mark('<', lo);
939 ed.set_mark('>', hi);
940 ed.vim.last_visual = Some(snap);
941 }
942 if !was_insert
946 && ed.vim.one_shot_normal
947 && ed.vim.mode == Mode::Normal
948 && matches!(ed.vim.pending, Pending::None)
949 {
950 ed.vim.one_shot_normal = false;
951 ed.vim.mode = Mode::Insert;
952 }
953 ed.sync_buffer_content_from_textarea();
959 if !ed.vim.viewport_pinned {
963 ed.ensure_cursor_in_scrolloff();
964 }
965 ed.vim.viewport_pinned = false;
966 if ed.vim.recording_macro.is_some()
971 && !ed.vim.replaying_macro
972 && input.key != Key::Char('q')
973 && !pending_was_macro_chord
974 {
975 ed.vim.recording_keys.push(input);
976 }
977 consumed
978}
979
980fn step_insert<H: crate::types::Host>(
983 ed: &mut Editor<hjkl_buffer::Buffer, H>,
984 input: Input,
985) -> bool {
986 if ed.vim.insert_pending_register {
990 ed.vim.insert_pending_register = false;
991 if let Key::Char(c) = input.key
992 && !input.ctrl
993 {
994 insert_register_text(ed, c);
995 }
996 return true;
997 }
998
999 if input.key == Key::Esc {
1000 finish_insert_session(ed);
1001 ed.vim.mode = Mode::Normal;
1002 let col = ed.cursor().1;
1007 if col > 0 {
1008 crate::motions::move_left(&mut ed.buffer, 1);
1009 ed.push_buffer_cursor_to_textarea();
1010 }
1011 ed.sticky_col = Some(ed.cursor().1);
1012 return true;
1013 }
1014
1015 if input.ctrl {
1017 match input.key {
1018 Key::Char('w') => {
1019 use hjkl_buffer::{Edit, MotionKind};
1020 ed.sync_buffer_content_from_textarea();
1021 let cursor = buf_cursor_pos(&ed.buffer);
1022 if cursor.row == 0 && cursor.col == 0 {
1023 return true;
1024 }
1025 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1028 let word_start = buf_cursor_pos(&ed.buffer);
1029 if word_start == cursor {
1030 return true;
1031 }
1032 buf_set_cursor_pos(&mut ed.buffer, cursor);
1033 ed.mutate_edit(Edit::DeleteRange {
1034 start: word_start,
1035 end: cursor,
1036 kind: MotionKind::Char,
1037 });
1038 ed.push_buffer_cursor_to_textarea();
1039 return true;
1040 }
1041 Key::Char('u') => {
1042 use hjkl_buffer::{Edit, MotionKind, Position};
1043 ed.sync_buffer_content_from_textarea();
1044 let cursor = buf_cursor_pos(&ed.buffer);
1045 if cursor.col > 0 {
1046 ed.mutate_edit(Edit::DeleteRange {
1047 start: Position::new(cursor.row, 0),
1048 end: cursor,
1049 kind: MotionKind::Char,
1050 });
1051 ed.push_buffer_cursor_to_textarea();
1052 }
1053 return true;
1054 }
1055 Key::Char('h') => {
1056 use hjkl_buffer::{Edit, MotionKind, Position};
1057 ed.sync_buffer_content_from_textarea();
1058 let cursor = buf_cursor_pos(&ed.buffer);
1059 if cursor.col > 0 {
1060 ed.mutate_edit(Edit::DeleteRange {
1061 start: Position::new(cursor.row, cursor.col - 1),
1062 end: cursor,
1063 kind: MotionKind::Char,
1064 });
1065 } else if cursor.row > 0 {
1066 let prev_row = cursor.row - 1;
1067 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1068 ed.mutate_edit(Edit::JoinLines {
1069 row: prev_row,
1070 count: 1,
1071 with_space: false,
1072 });
1073 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1074 }
1075 ed.push_buffer_cursor_to_textarea();
1076 return true;
1077 }
1078 Key::Char('o') => {
1079 ed.vim.one_shot_normal = true;
1082 ed.vim.mode = Mode::Normal;
1083 return true;
1084 }
1085 Key::Char('r') => {
1086 ed.vim.insert_pending_register = true;
1089 return true;
1090 }
1091 Key::Char('t') => {
1092 let (row, col) = ed.cursor();
1097 let sw = ed.settings().shiftwidth;
1098 indent_rows(ed, row, row, 1);
1099 ed.jump_cursor(row, col + sw);
1100 return true;
1101 }
1102 Key::Char('d') => {
1103 let (row, col) = ed.cursor();
1107 let before_len = buf_line_bytes(&ed.buffer, row);
1108 outdent_rows(ed, row, row, 1);
1109 let after_len = buf_line_bytes(&ed.buffer, row);
1110 let stripped = before_len.saturating_sub(after_len);
1111 let new_col = col.saturating_sub(stripped);
1112 ed.jump_cursor(row, new_col);
1113 return true;
1114 }
1115 _ => {}
1116 }
1117 }
1118
1119 let (row, _) = ed.cursor();
1122 if let Some(ref mut session) = ed.vim.insert_session {
1123 session.row_min = session.row_min.min(row);
1124 session.row_max = session.row_max.max(row);
1125 }
1126 let mutated = handle_insert_key(ed, input);
1127 if mutated {
1128 ed.mark_content_dirty();
1129 let (row, _) = ed.cursor();
1130 if let Some(ref mut session) = ed.vim.insert_session {
1131 session.row_min = session.row_min.min(row);
1132 session.row_max = session.row_max.max(row);
1133 }
1134 }
1135 true
1136}
1137
1138fn insert_register_text<H: crate::types::Host>(
1143 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1144 selector: char,
1145) {
1146 use hjkl_buffer::Edit;
1147 let text = match ed.registers().read(selector) {
1148 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1149 _ => return,
1150 };
1151 ed.sync_buffer_content_from_textarea();
1152 let cursor = buf_cursor_pos(&ed.buffer);
1153 ed.mutate_edit(Edit::InsertStr {
1154 at: cursor,
1155 text: text.clone(),
1156 });
1157 let mut row = cursor.row;
1160 let mut col = cursor.col;
1161 for ch in text.chars() {
1162 if ch == '\n' {
1163 row += 1;
1164 col = 0;
1165 } else {
1166 col += 1;
1167 }
1168 }
1169 buf_set_cursor_rc(&mut ed.buffer, row, col);
1170 ed.push_buffer_cursor_to_textarea();
1171 ed.mark_content_dirty();
1172 if let Some(ref mut session) = ed.vim.insert_session {
1173 session.row_min = session.row_min.min(row);
1174 session.row_max = session.row_max.max(row);
1175 }
1176}
1177
1178pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1197 if !settings.autoindent {
1198 return String::new();
1199 }
1200 let base: String = prev_line
1202 .chars()
1203 .take_while(|c| *c == ' ' || *c == '\t')
1204 .collect();
1205
1206 if settings.smartindent {
1207 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1211 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1212 let unit = if settings.expandtab {
1213 if settings.softtabstop > 0 {
1214 " ".repeat(settings.softtabstop)
1215 } else {
1216 " ".repeat(settings.shiftwidth)
1217 }
1218 } else {
1219 "\t".to_string()
1220 };
1221 return format!("{base}{unit}");
1222 }
1223 }
1224
1225 base
1226}
1227
1228fn try_dedent_close_bracket<H: crate::types::Host>(
1238 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1239 cursor: hjkl_buffer::Position,
1240 ch: char,
1241) -> bool {
1242 use hjkl_buffer::{Edit, MotionKind, Position};
1243
1244 if !ed.settings.smartindent {
1245 return false;
1246 }
1247 if !matches!(ch, '}' | ')' | ']') {
1248 return false;
1249 }
1250
1251 let line = match buf_line(&ed.buffer, cursor.row) {
1252 Some(l) => l.to_string(),
1253 None => return false,
1254 };
1255
1256 let before: String = line.chars().take(cursor.col).collect();
1258 if !before.chars().all(|c| c == ' ' || c == '\t') {
1259 return false;
1260 }
1261 if before.is_empty() {
1262 return false;
1264 }
1265
1266 let unit_len: usize = if ed.settings.expandtab {
1268 if ed.settings.softtabstop > 0 {
1269 ed.settings.softtabstop
1270 } else {
1271 ed.settings.shiftwidth
1272 }
1273 } else {
1274 1
1276 };
1277
1278 let strip_len = if ed.settings.expandtab {
1280 let spaces = before.chars().filter(|c| *c == ' ').count();
1282 if spaces < unit_len {
1283 return false;
1284 }
1285 unit_len
1286 } else {
1287 if !before.starts_with('\t') {
1289 return false;
1290 }
1291 1
1292 };
1293
1294 ed.mutate_edit(Edit::DeleteRange {
1296 start: Position::new(cursor.row, 0),
1297 end: Position::new(cursor.row, strip_len),
1298 kind: MotionKind::Char,
1299 });
1300 let new_col = cursor.col.saturating_sub(strip_len);
1305 ed.mutate_edit(Edit::InsertChar {
1306 at: Position::new(cursor.row, new_col),
1307 ch,
1308 });
1309 true
1310}
1311
1312fn handle_insert_key<H: crate::types::Host>(
1319 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1320 input: Input,
1321) -> bool {
1322 use hjkl_buffer::{Edit, MotionKind, Position};
1323 ed.sync_buffer_content_from_textarea();
1324 let cursor = buf_cursor_pos(&ed.buffer);
1325 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1326 let in_replace = matches!(
1330 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1331 Some(InsertReason::Replace)
1332 );
1333 let mutated = match input.key {
1334 Key::Char(c) if in_replace && cursor.col < line_chars => {
1335 ed.mutate_edit(Edit::DeleteRange {
1336 start: cursor,
1337 end: Position::new(cursor.row, cursor.col + 1),
1338 kind: MotionKind::Char,
1339 });
1340 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1341 true
1342 }
1343 Key::Char(c) => {
1344 if !try_dedent_close_bracket(ed, cursor, c) {
1345 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1346 }
1347 true
1348 }
1349 Key::Enter => {
1350 let prev_line = buf_line(&ed.buffer, cursor.row)
1351 .unwrap_or_default()
1352 .to_string();
1353 let indent = compute_enter_indent(&ed.settings, &prev_line);
1354 let text = format!("\n{indent}");
1355 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1356 true
1357 }
1358 Key::Tab => {
1359 if ed.settings.expandtab {
1360 let sts = ed.settings.softtabstop;
1363 let n = if sts > 0 {
1364 sts - (cursor.col % sts)
1365 } else {
1366 ed.settings.tabstop.max(1)
1367 };
1368 ed.mutate_edit(Edit::InsertStr {
1369 at: cursor,
1370 text: " ".repeat(n),
1371 });
1372 } else {
1373 ed.mutate_edit(Edit::InsertChar {
1374 at: cursor,
1375 ch: '\t',
1376 });
1377 }
1378 true
1379 }
1380 Key::Backspace => {
1381 let sts = ed.settings.softtabstop;
1385 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1386 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1387 let chars: Vec<char> = line.chars().collect();
1388 let run_start = cursor.col - sts;
1389 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1390 ed.mutate_edit(Edit::DeleteRange {
1391 start: Position::new(cursor.row, run_start),
1392 end: cursor,
1393 kind: MotionKind::Char,
1394 });
1395 return true;
1396 }
1397 }
1398 if cursor.col > 0 {
1399 ed.mutate_edit(Edit::DeleteRange {
1400 start: Position::new(cursor.row, cursor.col - 1),
1401 end: cursor,
1402 kind: MotionKind::Char,
1403 });
1404 true
1405 } else if cursor.row > 0 {
1406 let prev_row = cursor.row - 1;
1407 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1408 ed.mutate_edit(Edit::JoinLines {
1409 row: prev_row,
1410 count: 1,
1411 with_space: false,
1412 });
1413 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1414 true
1415 } else {
1416 false
1417 }
1418 }
1419 Key::Delete => {
1420 if cursor.col < line_chars {
1421 ed.mutate_edit(Edit::DeleteRange {
1422 start: cursor,
1423 end: Position::new(cursor.row, cursor.col + 1),
1424 kind: MotionKind::Char,
1425 });
1426 true
1427 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1428 ed.mutate_edit(Edit::JoinLines {
1429 row: cursor.row,
1430 count: 1,
1431 with_space: false,
1432 });
1433 buf_set_cursor_pos(&mut ed.buffer, cursor);
1434 true
1435 } else {
1436 false
1437 }
1438 }
1439 Key::Left => {
1440 crate::motions::move_left(&mut ed.buffer, 1);
1441 break_undo_group_in_insert(ed);
1442 false
1443 }
1444 Key::Right => {
1445 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1448 break_undo_group_in_insert(ed);
1449 false
1450 }
1451 Key::Up => {
1452 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1453 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1454 break_undo_group_in_insert(ed);
1455 false
1456 }
1457 Key::Down => {
1458 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1459 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1460 break_undo_group_in_insert(ed);
1461 false
1462 }
1463 Key::Home => {
1464 crate::motions::move_line_start(&mut ed.buffer);
1465 break_undo_group_in_insert(ed);
1466 false
1467 }
1468 Key::End => {
1469 crate::motions::move_line_end(&mut ed.buffer);
1470 break_undo_group_in_insert(ed);
1471 false
1472 }
1473 Key::PageUp => {
1474 let rows = viewport_full_rows(ed, 1) as isize;
1478 scroll_cursor_rows(ed, -rows);
1479 return false;
1480 }
1481 Key::PageDown => {
1482 let rows = viewport_full_rows(ed, 1) as isize;
1483 scroll_cursor_rows(ed, rows);
1484 return false;
1485 }
1486 _ => false,
1489 };
1490 ed.push_buffer_cursor_to_textarea();
1491 mutated
1492}
1493
1494fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1495 let Some(session) = ed.vim.insert_session.take() else {
1496 return;
1497 };
1498 let lines = buf_lines_to_vec(&ed.buffer);
1499 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1503 let before_end = session
1504 .row_max
1505 .min(session.before_lines.len().saturating_sub(1));
1506 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1507 session.before_lines[session.row_min..=before_end].join("\n")
1508 } else {
1509 String::new()
1510 };
1511 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1512 lines[session.row_min..=after_end].join("\n")
1513 } else {
1514 String::new()
1515 };
1516 let inserted = extract_inserted(&before, &after);
1517 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1518 use hjkl_buffer::{Edit, Position};
1519 for _ in 0..session.count - 1 {
1520 let (row, col) = ed.cursor();
1521 ed.mutate_edit(Edit::InsertStr {
1522 at: Position::new(row, col),
1523 text: inserted.clone(),
1524 });
1525 }
1526 }
1527 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1528 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1529 use hjkl_buffer::{Edit, Position};
1530 for r in (top + 1)..=bot {
1531 let line_len = buf_line_chars(&ed.buffer, r);
1532 if col > line_len {
1533 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1536 ed.mutate_edit(Edit::InsertStr {
1537 at: Position::new(r, line_len),
1538 text: pad,
1539 });
1540 }
1541 ed.mutate_edit(Edit::InsertStr {
1542 at: Position::new(r, col),
1543 text: inserted.clone(),
1544 });
1545 }
1546 buf_set_cursor_rc(&mut ed.buffer, top, col);
1547 ed.push_buffer_cursor_to_textarea();
1548 }
1549 return;
1550 }
1551 if ed.vim.replaying {
1552 return;
1553 }
1554 match session.reason {
1555 InsertReason::Enter(entry) => {
1556 ed.vim.last_change = Some(LastChange::InsertAt {
1557 entry,
1558 inserted,
1559 count: session.count,
1560 });
1561 }
1562 InsertReason::Open { above } => {
1563 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1564 }
1565 InsertReason::AfterChange => {
1566 if let Some(
1567 LastChange::OpMotion { inserted: ins, .. }
1568 | LastChange::OpTextObj { inserted: ins, .. }
1569 | LastChange::LineOp { inserted: ins, .. },
1570 ) = ed.vim.last_change.as_mut()
1571 {
1572 *ins = Some(inserted);
1573 }
1574 }
1575 InsertReason::DeleteToEol => {
1576 ed.vim.last_change = Some(LastChange::DeleteToEol {
1577 inserted: Some(inserted),
1578 });
1579 }
1580 InsertReason::ReplayOnly => {}
1581 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1582 InsertReason::Replace => {
1583 ed.vim.last_change = Some(LastChange::DeleteToEol {
1588 inserted: Some(inserted),
1589 });
1590 }
1591 }
1592}
1593
1594fn begin_insert<H: crate::types::Host>(
1595 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1596 count: usize,
1597 reason: InsertReason,
1598) {
1599 let record = !matches!(reason, InsertReason::ReplayOnly);
1600 if record {
1601 ed.push_undo();
1602 }
1603 let reason = if ed.vim.replaying {
1604 InsertReason::ReplayOnly
1605 } else {
1606 reason
1607 };
1608 let (row, _) = ed.cursor();
1609 ed.vim.insert_session = Some(InsertSession {
1610 count,
1611 row_min: row,
1612 row_max: row,
1613 before_lines: buf_lines_to_vec(&ed.buffer),
1614 reason,
1615 });
1616 ed.vim.mode = Mode::Insert;
1617}
1618
1619pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1634 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1635) {
1636 if !ed.settings.undo_break_on_motion {
1637 return;
1638 }
1639 if ed.vim.replaying {
1640 return;
1641 }
1642 if ed.vim.insert_session.is_none() {
1643 return;
1644 }
1645 ed.push_undo();
1646 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1647 let mut lines: Vec<String> = Vec::with_capacity(n);
1648 for r in 0..n {
1649 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1650 }
1651 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1652 if let Some(ref mut session) = ed.vim.insert_session {
1653 session.before_lines = lines;
1654 session.row_min = row;
1655 session.row_max = row;
1656 }
1657}
1658
1659fn step_normal<H: crate::types::Host>(
1662 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1663 input: Input,
1664) -> bool {
1665 if let Key::Char(d @ '0'..='9') = input.key
1667 && !input.ctrl
1668 && !input.alt
1669 && !matches!(
1670 ed.vim.pending,
1671 Pending::Replace
1672 | Pending::Find { .. }
1673 | Pending::OpFind { .. }
1674 | Pending::VisualTextObj { .. }
1675 )
1676 && (d != '0' || ed.vim.count > 0)
1677 {
1678 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1679 return true;
1680 }
1681
1682 match std::mem::take(&mut ed.vim.pending) {
1684 Pending::Replace => return handle_replace(ed, input),
1685 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1686 Pending::OpFind {
1687 op,
1688 count1,
1689 forward,
1690 till,
1691 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1692 Pending::G => return handle_after_g(ed, input),
1693 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1694 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1695 Pending::OpTextObj { op, count1, inner } => {
1696 return handle_text_object(ed, input, op, count1, inner);
1697 }
1698 Pending::VisualTextObj { inner } => {
1699 return handle_visual_text_obj(ed, input, inner);
1700 }
1701 Pending::Z => return handle_after_z(ed, input),
1702 Pending::SetMark => return handle_set_mark(ed, input),
1703 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1704 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1705 Pending::SelectRegister => return handle_select_register(ed, input),
1706 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1707 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1708 Pending::None => {}
1709 }
1710
1711 let count = take_count(&mut ed.vim);
1712
1713 match input.key {
1715 Key::Esc => {
1716 ed.vim.force_normal();
1717 return true;
1718 }
1719 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1720 ed.vim.visual_anchor = ed.cursor();
1721 ed.vim.mode = Mode::Visual;
1722 return true;
1723 }
1724 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1725 let (row, _) = ed.cursor();
1726 ed.vim.visual_line_anchor = row;
1727 ed.vim.mode = Mode::VisualLine;
1728 return true;
1729 }
1730 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1731 ed.vim.visual_anchor = ed.cursor();
1732 ed.vim.mode = Mode::Visual;
1733 return true;
1734 }
1735 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1736 let (row, _) = ed.cursor();
1737 ed.vim.visual_line_anchor = row;
1738 ed.vim.mode = Mode::VisualLine;
1739 return true;
1740 }
1741 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1742 let cur = ed.cursor();
1743 ed.vim.block_anchor = cur;
1744 ed.vim.block_vcol = cur.1;
1745 ed.vim.mode = Mode::VisualBlock;
1746 return true;
1747 }
1748 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1749 ed.vim.mode = Mode::Normal;
1751 return true;
1752 }
1753 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1756 Mode::Visual => {
1757 let cur = ed.cursor();
1758 let anchor = ed.vim.visual_anchor;
1759 ed.vim.visual_anchor = cur;
1760 ed.jump_cursor(anchor.0, anchor.1);
1761 return true;
1762 }
1763 Mode::VisualLine => {
1764 let cur_row = ed.cursor().0;
1765 let anchor_row = ed.vim.visual_line_anchor;
1766 ed.vim.visual_line_anchor = cur_row;
1767 ed.jump_cursor(anchor_row, 0);
1768 return true;
1769 }
1770 Mode::VisualBlock => {
1771 let cur = ed.cursor();
1772 let anchor = ed.vim.block_anchor;
1773 ed.vim.block_anchor = cur;
1774 ed.vim.block_vcol = anchor.1;
1775 ed.jump_cursor(anchor.0, anchor.1);
1776 return true;
1777 }
1778 _ => {}
1779 },
1780 _ => {}
1781 }
1782
1783 if ed.vim.is_visual()
1785 && let Some(op) = visual_operator(&input)
1786 {
1787 apply_visual_operator(ed, op);
1788 return true;
1789 }
1790
1791 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1795 match input.key {
1796 Key::Char('r') => {
1797 ed.vim.pending = Pending::Replace;
1798 return true;
1799 }
1800 Key::Char('I') => {
1801 let (top, bot, left, _right) = block_bounds(ed);
1802 ed.jump_cursor(top, left);
1803 ed.vim.mode = Mode::Normal;
1804 begin_insert(
1805 ed,
1806 1,
1807 InsertReason::BlockEdge {
1808 top,
1809 bot,
1810 col: left,
1811 },
1812 );
1813 return true;
1814 }
1815 Key::Char('A') => {
1816 let (top, bot, _left, right) = block_bounds(ed);
1817 let line_len = buf_line_chars(&ed.buffer, top);
1818 let col = (right + 1).min(line_len);
1819 ed.jump_cursor(top, col);
1820 ed.vim.mode = Mode::Normal;
1821 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1822 return true;
1823 }
1824 _ => {}
1825 }
1826 }
1827
1828 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1830 && !input.ctrl
1831 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1832 {
1833 let inner = matches!(input.key, Key::Char('i'));
1834 ed.vim.pending = Pending::VisualTextObj { inner };
1835 return true;
1836 }
1837
1838 if input.ctrl
1843 && let Key::Char(c) = input.key
1844 {
1845 match c {
1846 'd' => {
1847 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1848 return true;
1849 }
1850 'u' => {
1851 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1852 return true;
1853 }
1854 'f' => {
1855 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1856 return true;
1857 }
1858 'b' => {
1859 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1860 return true;
1861 }
1862 'r' => {
1863 do_redo(ed);
1864 return true;
1865 }
1866 'a' if ed.vim.mode == Mode::Normal => {
1867 adjust_number(ed, count.max(1) as i64);
1868 return true;
1869 }
1870 'x' if ed.vim.mode == Mode::Normal => {
1871 adjust_number(ed, -(count.max(1) as i64));
1872 return true;
1873 }
1874 'o' if ed.vim.mode == Mode::Normal => {
1875 for _ in 0..count.max(1) {
1876 jump_back(ed);
1877 }
1878 return true;
1879 }
1880 'i' if ed.vim.mode == Mode::Normal => {
1881 for _ in 0..count.max(1) {
1882 jump_forward(ed);
1883 }
1884 return true;
1885 }
1886 _ => {}
1887 }
1888 }
1889
1890 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1892 for _ in 0..count.max(1) {
1893 jump_forward(ed);
1894 }
1895 return true;
1896 }
1897
1898 if let Some(motion) = parse_motion(&input) {
1900 execute_motion(ed, motion.clone(), count);
1901 if ed.vim.mode == Mode::VisualBlock {
1903 update_block_vcol(ed, &motion);
1904 }
1905 if let Motion::Find { ch, forward, till } = motion {
1906 ed.vim.last_find = Some((ch, forward, till));
1907 }
1908 return true;
1909 }
1910
1911 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
1913 return true;
1914 }
1915
1916 if ed.vim.mode == Mode::Normal
1918 && let Key::Char(op_ch) = input.key
1919 && !input.ctrl
1920 && let Some(op) = char_to_operator(op_ch)
1921 {
1922 ed.vim.pending = Pending::Op { op, count1: count };
1923 return true;
1924 }
1925
1926 if ed.vim.mode == Mode::Normal
1928 && let Some((forward, till)) = find_entry(&input)
1929 {
1930 ed.vim.count = count;
1931 ed.vim.pending = Pending::Find { forward, till };
1932 return true;
1933 }
1934
1935 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
1937 ed.vim.count = count;
1938 ed.vim.pending = Pending::G;
1939 return true;
1940 }
1941
1942 if !input.ctrl
1944 && input.key == Key::Char('z')
1945 && matches!(
1946 ed.vim.mode,
1947 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
1948 )
1949 {
1950 ed.vim.pending = Pending::Z;
1951 return true;
1952 }
1953
1954 if !input.ctrl && ed.vim.mode == Mode::Normal {
1958 match input.key {
1959 Key::Char('m') => {
1960 ed.vim.pending = Pending::SetMark;
1961 return true;
1962 }
1963 Key::Char('\'') => {
1964 ed.vim.pending = Pending::GotoMarkLine;
1965 return true;
1966 }
1967 Key::Char('`') => {
1968 ed.vim.pending = Pending::GotoMarkChar;
1969 return true;
1970 }
1971 Key::Char('"') => {
1972 ed.vim.pending = Pending::SelectRegister;
1975 return true;
1976 }
1977 Key::Char('@') => {
1978 ed.vim.pending = Pending::PlayMacroTarget { count };
1982 return true;
1983 }
1984 Key::Char('q') if ed.vim.recording_macro.is_none() => {
1985 ed.vim.pending = Pending::RecordMacroTarget;
1990 return true;
1991 }
1992 _ => {}
1993 }
1994 }
1995
1996 true
1998}
1999
2000fn handle_set_mark<H: crate::types::Host>(
2001 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2002 input: Input,
2003) -> bool {
2004 if let Key::Char(c) = input.key
2005 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
2006 {
2007 let pos = ed.cursor();
2012 ed.set_mark(c, pos);
2013 }
2014 true
2015}
2016
2017fn handle_select_register<H: crate::types::Host>(
2021 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2022 input: Input,
2023) -> bool {
2024 if let Key::Char(c) = input.key
2025 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*'))
2026 {
2027 ed.vim.pending_register = Some(c);
2028 }
2029 true
2030}
2031
2032fn handle_record_macro_target<H: crate::types::Host>(
2037 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2038 input: Input,
2039) -> bool {
2040 if let Key::Char(c) = input.key
2041 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2042 {
2043 ed.vim.recording_macro = Some(c);
2044 if c.is_ascii_uppercase() {
2047 let lower = c.to_ascii_lowercase();
2048 let text = ed
2052 .registers()
2053 .read(lower)
2054 .map(|s| s.text.clone())
2055 .unwrap_or_default();
2056 ed.vim.recording_keys = crate::input::decode_macro(&text);
2057 } else {
2058 ed.vim.recording_keys.clear();
2059 }
2060 }
2061 true
2062}
2063
2064fn handle_play_macro_target<H: crate::types::Host>(
2070 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2071 input: Input,
2072 count: usize,
2073) -> bool {
2074 let reg = match input.key {
2075 Key::Char('@') => ed.vim.last_macro,
2076 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2077 Some(c.to_ascii_lowercase())
2078 }
2079 _ => None,
2080 };
2081 let Some(reg) = reg else {
2082 return true;
2083 };
2084 let text = match ed.registers().read(reg) {
2087 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2088 _ => return true,
2089 };
2090 let keys = crate::input::decode_macro(&text);
2091 ed.vim.last_macro = Some(reg);
2092 let times = count.max(1);
2093 let was_replaying = ed.vim.replaying_macro;
2094 ed.vim.replaying_macro = true;
2095 for _ in 0..times {
2096 for k in keys.iter().copied() {
2097 step(ed, k);
2098 }
2099 }
2100 ed.vim.replaying_macro = was_replaying;
2101 true
2102}
2103
2104fn handle_goto_mark<H: crate::types::Host>(
2105 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2106 input: Input,
2107 linewise: bool,
2108) -> bool {
2109 let Key::Char(c) = input.key else {
2110 return true;
2111 };
2112 let target = match c {
2119 'a'..='z' | 'A'..='Z' => ed.mark(c),
2120 '\'' | '`' => ed.vim.jump_back.last().copied(),
2121 '.' => ed.vim.last_edit_pos,
2122 _ => None,
2123 };
2124 let Some((row, col)) = target else {
2125 return true;
2126 };
2127 let pre = ed.cursor();
2128 let (r, c_clamped) = clamp_pos(ed, (row, col));
2129 if linewise {
2130 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2131 ed.push_buffer_cursor_to_textarea();
2132 move_first_non_whitespace(ed);
2133 } else {
2134 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2135 ed.push_buffer_cursor_to_textarea();
2136 }
2137 if ed.cursor() != pre {
2138 push_jump(ed, pre);
2139 }
2140 ed.sticky_col = Some(ed.cursor().1);
2141 true
2142}
2143
2144fn take_count(vim: &mut VimState) -> usize {
2145 if vim.count > 0 {
2146 let n = vim.count;
2147 vim.count = 0;
2148 n
2149 } else {
2150 1
2151 }
2152}
2153
2154fn char_to_operator(c: char) -> Option<Operator> {
2155 match c {
2156 'd' => Some(Operator::Delete),
2157 'c' => Some(Operator::Change),
2158 'y' => Some(Operator::Yank),
2159 '>' => Some(Operator::Indent),
2160 '<' => Some(Operator::Outdent),
2161 _ => None,
2162 }
2163}
2164
2165fn visual_operator(input: &Input) -> Option<Operator> {
2166 if input.ctrl {
2167 return None;
2168 }
2169 match input.key {
2170 Key::Char('y') => Some(Operator::Yank),
2171 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2172 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2173 Key::Char('U') => Some(Operator::Uppercase),
2175 Key::Char('u') => Some(Operator::Lowercase),
2176 Key::Char('~') => Some(Operator::ToggleCase),
2177 Key::Char('>') => Some(Operator::Indent),
2179 Key::Char('<') => Some(Operator::Outdent),
2180 _ => None,
2181 }
2182}
2183
2184fn find_entry(input: &Input) -> Option<(bool, bool)> {
2185 if input.ctrl {
2186 return None;
2187 }
2188 match input.key {
2189 Key::Char('f') => Some((true, false)),
2190 Key::Char('F') => Some((false, false)),
2191 Key::Char('t') => Some((true, true)),
2192 Key::Char('T') => Some((false, true)),
2193 _ => None,
2194 }
2195}
2196
2197const JUMPLIST_MAX: usize = 100;
2201
2202fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2207 ed.vim.jump_back.push(from);
2208 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2209 ed.vim.jump_back.remove(0);
2210 }
2211 ed.vim.jump_fwd.clear();
2212}
2213
2214fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2217 let Some(target) = ed.vim.jump_back.pop() else {
2218 return;
2219 };
2220 let cur = ed.cursor();
2221 ed.vim.jump_fwd.push(cur);
2222 let (r, c) = clamp_pos(ed, target);
2223 ed.jump_cursor(r, c);
2224 ed.sticky_col = Some(c);
2225}
2226
2227fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2230 let Some(target) = ed.vim.jump_fwd.pop() else {
2231 return;
2232 };
2233 let cur = ed.cursor();
2234 ed.vim.jump_back.push(cur);
2235 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2236 ed.vim.jump_back.remove(0);
2237 }
2238 let (r, c) = clamp_pos(ed, target);
2239 ed.jump_cursor(r, c);
2240 ed.sticky_col = Some(c);
2241}
2242
2243fn clamp_pos<H: crate::types::Host>(
2246 ed: &Editor<hjkl_buffer::Buffer, H>,
2247 pos: (usize, usize),
2248) -> (usize, usize) {
2249 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2250 let r = pos.0.min(last_row);
2251 let line_len = buf_line_chars(&ed.buffer, r);
2252 let c = pos.1.min(line_len.saturating_sub(1));
2253 (r, c)
2254}
2255
2256fn is_big_jump(motion: &Motion) -> bool {
2258 matches!(
2259 motion,
2260 Motion::FileTop
2261 | Motion::FileBottom
2262 | Motion::MatchBracket
2263 | Motion::WordAtCursor { .. }
2264 | Motion::SearchNext { .. }
2265 | Motion::ViewportTop
2266 | Motion::ViewportMiddle
2267 | Motion::ViewportBottom
2268 )
2269}
2270
2271fn viewport_half_rows<H: crate::types::Host>(
2276 ed: &Editor<hjkl_buffer::Buffer, H>,
2277 count: usize,
2278) -> usize {
2279 let h = ed.viewport_height_value() as usize;
2280 (h / 2).max(1).saturating_mul(count.max(1))
2281}
2282
2283fn viewport_full_rows<H: crate::types::Host>(
2286 ed: &Editor<hjkl_buffer::Buffer, H>,
2287 count: usize,
2288) -> usize {
2289 let h = ed.viewport_height_value() as usize;
2290 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2291}
2292
2293fn scroll_cursor_rows<H: crate::types::Host>(
2298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2299 delta: isize,
2300) {
2301 if delta == 0 {
2302 return;
2303 }
2304 ed.sync_buffer_content_from_textarea();
2305 let (row, _) = ed.cursor();
2306 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2307 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2308 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2309 crate::motions::move_first_non_blank(&mut ed.buffer);
2310 ed.push_buffer_cursor_to_textarea();
2311 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2312}
2313
2314fn parse_motion(input: &Input) -> Option<Motion> {
2317 if input.ctrl {
2318 return None;
2319 }
2320 match input.key {
2321 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2322 Key::Char('l') | Key::Right => Some(Motion::Right),
2323 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2324 Key::Char('k') | Key::Up => Some(Motion::Up),
2325 Key::Char('w') => Some(Motion::WordFwd),
2326 Key::Char('W') => Some(Motion::BigWordFwd),
2327 Key::Char('b') => Some(Motion::WordBack),
2328 Key::Char('B') => Some(Motion::BigWordBack),
2329 Key::Char('e') => Some(Motion::WordEnd),
2330 Key::Char('E') => Some(Motion::BigWordEnd),
2331 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2332 Key::Char('^') => Some(Motion::FirstNonBlank),
2333 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2334 Key::Char('G') => Some(Motion::FileBottom),
2335 Key::Char('%') => Some(Motion::MatchBracket),
2336 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2337 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2338 Key::Char('*') => Some(Motion::WordAtCursor {
2339 forward: true,
2340 whole_word: true,
2341 }),
2342 Key::Char('#') => Some(Motion::WordAtCursor {
2343 forward: false,
2344 whole_word: true,
2345 }),
2346 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2347 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2348 Key::Char('H') => Some(Motion::ViewportTop),
2349 Key::Char('M') => Some(Motion::ViewportMiddle),
2350 Key::Char('L') => Some(Motion::ViewportBottom),
2351 Key::Char('{') => Some(Motion::ParagraphPrev),
2352 Key::Char('}') => Some(Motion::ParagraphNext),
2353 Key::Char('(') => Some(Motion::SentencePrev),
2354 Key::Char(')') => Some(Motion::SentenceNext),
2355 _ => None,
2356 }
2357}
2358
2359fn execute_motion<H: crate::types::Host>(
2362 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2363 motion: Motion,
2364 count: usize,
2365) {
2366 let count = count.max(1);
2367 let motion = match motion {
2369 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2370 Some((ch, forward, till)) => Motion::Find {
2371 ch,
2372 forward: if reverse { !forward } else { forward },
2373 till,
2374 },
2375 None => return,
2376 },
2377 other => other,
2378 };
2379 let pre_pos = ed.cursor();
2380 let pre_col = pre_pos.1;
2381 apply_motion_cursor(ed, &motion, count);
2382 let post_pos = ed.cursor();
2383 if is_big_jump(&motion) && pre_pos != post_pos {
2384 push_jump(ed, pre_pos);
2385 }
2386 apply_sticky_col(ed, &motion, pre_col);
2387 ed.sync_buffer_from_textarea();
2392}
2393
2394fn apply_sticky_col<H: crate::types::Host>(
2399 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2400 motion: &Motion,
2401 pre_col: usize,
2402) {
2403 if is_vertical_motion(motion) {
2404 let want = ed.sticky_col.unwrap_or(pre_col);
2405 ed.sticky_col = Some(want);
2408 let (row, _) = ed.cursor();
2409 let line_len = buf_line_chars(&ed.buffer, row);
2410 let max_col = line_len.saturating_sub(1);
2414 let target = want.min(max_col);
2415 ed.jump_cursor(row, target);
2416 } else {
2417 ed.sticky_col = Some(ed.cursor().1);
2420 }
2421}
2422
2423fn is_vertical_motion(motion: &Motion) -> bool {
2424 matches!(
2428 motion,
2429 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2430 )
2431}
2432
2433fn apply_motion_cursor<H: crate::types::Host>(
2434 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2435 motion: &Motion,
2436 count: usize,
2437) {
2438 apply_motion_cursor_ctx(ed, motion, count, false)
2439}
2440
2441fn apply_motion_cursor_ctx<H: crate::types::Host>(
2442 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2443 motion: &Motion,
2444 count: usize,
2445 as_operator: bool,
2446) {
2447 match motion {
2448 Motion::Left => {
2449 crate::motions::move_left(&mut ed.buffer, count);
2451 ed.push_buffer_cursor_to_textarea();
2452 }
2453 Motion::Right => {
2454 if as_operator {
2458 crate::motions::move_right_to_end(&mut ed.buffer, count);
2459 } else {
2460 crate::motions::move_right_in_line(&mut ed.buffer, count);
2461 }
2462 ed.push_buffer_cursor_to_textarea();
2463 }
2464 Motion::Up => {
2465 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2469 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2470 ed.push_buffer_cursor_to_textarea();
2471 }
2472 Motion::Down => {
2473 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2474 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2475 ed.push_buffer_cursor_to_textarea();
2476 }
2477 Motion::ScreenUp => {
2478 let v = *ed.host.viewport();
2479 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2480 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2481 ed.push_buffer_cursor_to_textarea();
2482 }
2483 Motion::ScreenDown => {
2484 let v = *ed.host.viewport();
2485 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2486 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2487 ed.push_buffer_cursor_to_textarea();
2488 }
2489 Motion::WordFwd => {
2490 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2491 ed.push_buffer_cursor_to_textarea();
2492 }
2493 Motion::WordBack => {
2494 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2495 ed.push_buffer_cursor_to_textarea();
2496 }
2497 Motion::WordEnd => {
2498 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2499 ed.push_buffer_cursor_to_textarea();
2500 }
2501 Motion::BigWordFwd => {
2502 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2503 ed.push_buffer_cursor_to_textarea();
2504 }
2505 Motion::BigWordBack => {
2506 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2507 ed.push_buffer_cursor_to_textarea();
2508 }
2509 Motion::BigWordEnd => {
2510 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2511 ed.push_buffer_cursor_to_textarea();
2512 }
2513 Motion::WordEndBack => {
2514 crate::motions::move_word_end_back(
2515 &mut ed.buffer,
2516 false,
2517 count,
2518 &ed.settings.iskeyword,
2519 );
2520 ed.push_buffer_cursor_to_textarea();
2521 }
2522 Motion::BigWordEndBack => {
2523 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2524 ed.push_buffer_cursor_to_textarea();
2525 }
2526 Motion::LineStart => {
2527 crate::motions::move_line_start(&mut ed.buffer);
2528 ed.push_buffer_cursor_to_textarea();
2529 }
2530 Motion::FirstNonBlank => {
2531 crate::motions::move_first_non_blank(&mut ed.buffer);
2532 ed.push_buffer_cursor_to_textarea();
2533 }
2534 Motion::LineEnd => {
2535 crate::motions::move_line_end(&mut ed.buffer);
2537 ed.push_buffer_cursor_to_textarea();
2538 }
2539 Motion::FileTop => {
2540 if count > 1 {
2543 crate::motions::move_bottom(&mut ed.buffer, count);
2544 } else {
2545 crate::motions::move_top(&mut ed.buffer);
2546 }
2547 ed.push_buffer_cursor_to_textarea();
2548 }
2549 Motion::FileBottom => {
2550 if count > 1 {
2553 crate::motions::move_bottom(&mut ed.buffer, count);
2554 } else {
2555 crate::motions::move_bottom(&mut ed.buffer, 0);
2556 }
2557 ed.push_buffer_cursor_to_textarea();
2558 }
2559 Motion::Find { ch, forward, till } => {
2560 for _ in 0..count {
2561 if !find_char_on_line(ed, *ch, *forward, *till) {
2562 break;
2563 }
2564 }
2565 }
2566 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2568 let _ = matching_bracket(ed);
2569 }
2570 Motion::WordAtCursor {
2571 forward,
2572 whole_word,
2573 } => {
2574 word_at_cursor_search(ed, *forward, *whole_word, count);
2575 }
2576 Motion::SearchNext { reverse } => {
2577 if let Some(pattern) = ed.vim.last_search.clone() {
2581 push_search_pattern(ed, &pattern);
2582 }
2583 if ed.search_state().pattern.is_none() {
2584 return;
2585 }
2586 let forward = ed.vim.last_search_forward != *reverse;
2590 for _ in 0..count.max(1) {
2591 if forward {
2592 ed.search_advance_forward(true);
2593 } else {
2594 ed.search_advance_backward(true);
2595 }
2596 }
2597 ed.push_buffer_cursor_to_textarea();
2598 }
2599 Motion::ViewportTop => {
2600 let v = *ed.host().viewport();
2601 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2602 ed.push_buffer_cursor_to_textarea();
2603 }
2604 Motion::ViewportMiddle => {
2605 let v = *ed.host().viewport();
2606 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2607 ed.push_buffer_cursor_to_textarea();
2608 }
2609 Motion::ViewportBottom => {
2610 let v = *ed.host().viewport();
2611 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2612 ed.push_buffer_cursor_to_textarea();
2613 }
2614 Motion::LastNonBlank => {
2615 crate::motions::move_last_non_blank(&mut ed.buffer);
2616 ed.push_buffer_cursor_to_textarea();
2617 }
2618 Motion::LineMiddle => {
2619 let row = ed.cursor().0;
2620 let line_chars = buf_line_chars(&ed.buffer, row);
2621 let target = line_chars / 2;
2624 ed.jump_cursor(row, target);
2625 }
2626 Motion::ParagraphPrev => {
2627 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2628 ed.push_buffer_cursor_to_textarea();
2629 }
2630 Motion::ParagraphNext => {
2631 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2632 ed.push_buffer_cursor_to_textarea();
2633 }
2634 Motion::SentencePrev => {
2635 for _ in 0..count.max(1) {
2636 if let Some((row, col)) = sentence_boundary(ed, false) {
2637 ed.jump_cursor(row, col);
2638 }
2639 }
2640 }
2641 Motion::SentenceNext => {
2642 for _ in 0..count.max(1) {
2643 if let Some((row, col)) = sentence_boundary(ed, true) {
2644 ed.jump_cursor(row, col);
2645 }
2646 }
2647 }
2648 }
2649}
2650
2651fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2652 ed.sync_buffer_content_from_textarea();
2658 crate::motions::move_first_non_blank(&mut ed.buffer);
2659 ed.push_buffer_cursor_to_textarea();
2660}
2661
2662fn find_char_on_line<H: crate::types::Host>(
2663 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2664 ch: char,
2665 forward: bool,
2666 till: bool,
2667) -> bool {
2668 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2669 if moved {
2670 ed.push_buffer_cursor_to_textarea();
2671 }
2672 moved
2673}
2674
2675fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2676 let moved = crate::motions::match_bracket(&mut ed.buffer);
2677 if moved {
2678 ed.push_buffer_cursor_to_textarea();
2679 }
2680 moved
2681}
2682
2683fn word_at_cursor_search<H: crate::types::Host>(
2684 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2685 forward: bool,
2686 whole_word: bool,
2687 count: usize,
2688) {
2689 let (row, col) = ed.cursor();
2690 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2691 let chars: Vec<char> = line.chars().collect();
2692 if chars.is_empty() {
2693 return;
2694 }
2695 let spec = ed.settings().iskeyword.clone();
2697 let is_word = |c: char| is_keyword_char(c, &spec);
2698 let mut start = col.min(chars.len().saturating_sub(1));
2699 while start > 0 && is_word(chars[start - 1]) {
2700 start -= 1;
2701 }
2702 let mut end = start;
2703 while end < chars.len() && is_word(chars[end]) {
2704 end += 1;
2705 }
2706 if end <= start {
2707 return;
2708 }
2709 let word: String = chars[start..end].iter().collect();
2710 let escaped = regex_escape(&word);
2711 let pattern = if whole_word {
2712 format!(r"\b{escaped}\b")
2713 } else {
2714 escaped
2715 };
2716 push_search_pattern(ed, &pattern);
2717 if ed.search_state().pattern.is_none() {
2718 return;
2719 }
2720 ed.vim.last_search = Some(pattern);
2722 ed.vim.last_search_forward = forward;
2723 for _ in 0..count.max(1) {
2724 if forward {
2725 ed.search_advance_forward(true);
2726 } else {
2727 ed.search_advance_backward(true);
2728 }
2729 }
2730 ed.push_buffer_cursor_to_textarea();
2731}
2732
2733fn regex_escape(s: &str) -> String {
2734 let mut out = String::with_capacity(s.len());
2735 for c in s.chars() {
2736 if matches!(
2737 c,
2738 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2739 ) {
2740 out.push('\\');
2741 }
2742 out.push(c);
2743 }
2744 out
2745}
2746
2747fn handle_after_op<H: crate::types::Host>(
2750 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2751 input: Input,
2752 op: Operator,
2753 count1: usize,
2754) -> bool {
2755 if let Key::Char(d @ '0'..='9') = input.key
2757 && !input.ctrl
2758 && (d != '0' || ed.vim.count > 0)
2759 {
2760 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2761 ed.vim.pending = Pending::Op { op, count1 };
2762 return true;
2763 }
2764
2765 if input.key == Key::Esc {
2767 ed.vim.count = 0;
2768 return true;
2769 }
2770
2771 let double_ch = match op {
2775 Operator::Delete => Some('d'),
2776 Operator::Change => Some('c'),
2777 Operator::Yank => Some('y'),
2778 Operator::Indent => Some('>'),
2779 Operator::Outdent => Some('<'),
2780 Operator::Uppercase => Some('U'),
2781 Operator::Lowercase => Some('u'),
2782 Operator::ToggleCase => Some('~'),
2783 Operator::Fold => None,
2784 Operator::Reflow => Some('q'),
2787 };
2788 if let Key::Char(c) = input.key
2789 && !input.ctrl
2790 && Some(c) == double_ch
2791 {
2792 let count2 = take_count(&mut ed.vim);
2793 let total = count1.max(1) * count2.max(1);
2794 execute_line_op(ed, op, total);
2795 if !ed.vim.replaying {
2796 ed.vim.last_change = Some(LastChange::LineOp {
2797 op,
2798 count: total,
2799 inserted: None,
2800 });
2801 }
2802 return true;
2803 }
2804
2805 if let Key::Char('i') | Key::Char('a') = input.key
2807 && !input.ctrl
2808 {
2809 let inner = matches!(input.key, Key::Char('i'));
2810 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2811 return true;
2812 }
2813
2814 if input.key == Key::Char('g') && !input.ctrl {
2816 ed.vim.pending = Pending::OpG { op, count1 };
2817 return true;
2818 }
2819
2820 if let Some((forward, till)) = find_entry(&input) {
2822 ed.vim.pending = Pending::OpFind {
2823 op,
2824 count1,
2825 forward,
2826 till,
2827 };
2828 return true;
2829 }
2830
2831 let count2 = take_count(&mut ed.vim);
2833 let total = count1.max(1) * count2.max(1);
2834 if let Some(motion) = parse_motion(&input) {
2835 let motion = match motion {
2836 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2837 Some((ch, forward, till)) => Motion::Find {
2838 ch,
2839 forward: if reverse { !forward } else { forward },
2840 till,
2841 },
2842 None => return true,
2843 },
2844 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2848 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2849 m => m,
2850 };
2851 apply_op_with_motion(ed, op, &motion, total);
2852 if let Motion::Find { ch, forward, till } = &motion {
2853 ed.vim.last_find = Some((*ch, *forward, *till));
2854 }
2855 if !ed.vim.replaying && op_is_change(op) {
2856 ed.vim.last_change = Some(LastChange::OpMotion {
2857 op,
2858 motion,
2859 count: total,
2860 inserted: None,
2861 });
2862 }
2863 return true;
2864 }
2865
2866 true
2868}
2869
2870fn handle_op_after_g<H: crate::types::Host>(
2871 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2872 input: Input,
2873 op: Operator,
2874 count1: usize,
2875) -> bool {
2876 if input.ctrl {
2877 return true;
2878 }
2879 let count2 = take_count(&mut ed.vim);
2880 let total = count1.max(1) * count2.max(1);
2881 if matches!(
2885 op,
2886 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2887 ) {
2888 let op_char = match op {
2889 Operator::Uppercase => 'U',
2890 Operator::Lowercase => 'u',
2891 Operator::ToggleCase => '~',
2892 _ => unreachable!(),
2893 };
2894 if input.key == Key::Char(op_char) {
2895 execute_line_op(ed, op, total);
2896 if !ed.vim.replaying {
2897 ed.vim.last_change = Some(LastChange::LineOp {
2898 op,
2899 count: total,
2900 inserted: None,
2901 });
2902 }
2903 return true;
2904 }
2905 }
2906 let motion = match input.key {
2907 Key::Char('g') => Motion::FileTop,
2908 Key::Char('e') => Motion::WordEndBack,
2909 Key::Char('E') => Motion::BigWordEndBack,
2910 Key::Char('j') => Motion::ScreenDown,
2911 Key::Char('k') => Motion::ScreenUp,
2912 _ => return true,
2913 };
2914 apply_op_with_motion(ed, op, &motion, total);
2915 if !ed.vim.replaying && op_is_change(op) {
2916 ed.vim.last_change = Some(LastChange::OpMotion {
2917 op,
2918 motion,
2919 count: total,
2920 inserted: None,
2921 });
2922 }
2923 true
2924}
2925
2926fn handle_after_g<H: crate::types::Host>(
2927 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2928 input: Input,
2929) -> bool {
2930 let count = take_count(&mut ed.vim);
2931 match input.key {
2932 Key::Char('g') => {
2933 let pre = ed.cursor();
2935 if count > 1 {
2936 ed.jump_cursor(count - 1, 0);
2937 } else {
2938 ed.jump_cursor(0, 0);
2939 }
2940 move_first_non_whitespace(ed);
2941 if ed.cursor() != pre {
2942 push_jump(ed, pre);
2943 }
2944 }
2945 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
2946 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
2947 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
2949 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
2951 Key::Char('v') => {
2953 if let Some(snap) = ed.vim.last_visual {
2954 match snap.mode {
2955 Mode::Visual => {
2956 ed.vim.visual_anchor = snap.anchor;
2957 ed.vim.mode = Mode::Visual;
2958 }
2959 Mode::VisualLine => {
2960 ed.vim.visual_line_anchor = snap.anchor.0;
2961 ed.vim.mode = Mode::VisualLine;
2962 }
2963 Mode::VisualBlock => {
2964 ed.vim.block_anchor = snap.anchor;
2965 ed.vim.block_vcol = snap.block_vcol;
2966 ed.vim.mode = Mode::VisualBlock;
2967 }
2968 _ => {}
2969 }
2970 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2971 }
2972 }
2973 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
2977 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
2978 Key::Char('U') => {
2982 ed.vim.pending = Pending::Op {
2983 op: Operator::Uppercase,
2984 count1: count,
2985 };
2986 }
2987 Key::Char('u') => {
2988 ed.vim.pending = Pending::Op {
2989 op: Operator::Lowercase,
2990 count1: count,
2991 };
2992 }
2993 Key::Char('~') => {
2994 ed.vim.pending = Pending::Op {
2995 op: Operator::ToggleCase,
2996 count1: count,
2997 };
2998 }
2999 Key::Char('q') => {
3000 ed.vim.pending = Pending::Op {
3003 op: Operator::Reflow,
3004 count1: count,
3005 };
3006 }
3007 Key::Char('J') => {
3008 for _ in 0..count.max(1) {
3010 ed.push_undo();
3011 join_line_raw(ed);
3012 }
3013 if !ed.vim.replaying {
3014 ed.vim.last_change = Some(LastChange::JoinLine {
3015 count: count.max(1),
3016 });
3017 }
3018 }
3019 Key::Char('d') => {
3020 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3025 }
3026 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
3029 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
3030 Key::Char('*') => execute_motion(
3034 ed,
3035 Motion::WordAtCursor {
3036 forward: true,
3037 whole_word: false,
3038 },
3039 count,
3040 ),
3041 Key::Char('#') => execute_motion(
3042 ed,
3043 Motion::WordAtCursor {
3044 forward: false,
3045 whole_word: false,
3046 },
3047 count,
3048 ),
3049 _ => {}
3050 }
3051 true
3052}
3053
3054fn handle_after_z<H: crate::types::Host>(
3055 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3056 input: Input,
3057) -> bool {
3058 use crate::editor::CursorScrollTarget;
3059 let row = ed.cursor().0;
3060 match input.key {
3061 Key::Char('z') => {
3062 ed.scroll_cursor_to(CursorScrollTarget::Center);
3063 ed.vim.viewport_pinned = true;
3064 }
3065 Key::Char('t') => {
3066 ed.scroll_cursor_to(CursorScrollTarget::Top);
3067 ed.vim.viewport_pinned = true;
3068 }
3069 Key::Char('b') => {
3070 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3071 ed.vim.viewport_pinned = true;
3072 }
3073 Key::Char('o') => {
3078 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3079 }
3080 Key::Char('c') => {
3081 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3082 }
3083 Key::Char('a') => {
3084 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3085 }
3086 Key::Char('R') => {
3087 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3088 }
3089 Key::Char('M') => {
3090 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3091 }
3092 Key::Char('E') => {
3093 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3094 }
3095 Key::Char('d') => {
3096 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3097 }
3098 Key::Char('f') => {
3099 if matches!(
3100 ed.vim.mode,
3101 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3102 ) {
3103 let anchor_row = match ed.vim.mode {
3106 Mode::VisualLine => ed.vim.visual_line_anchor,
3107 Mode::VisualBlock => ed.vim.block_anchor.0,
3108 _ => ed.vim.visual_anchor.0,
3109 };
3110 let cur = ed.cursor().0;
3111 let top = anchor_row.min(cur);
3112 let bot = anchor_row.max(cur);
3113 ed.apply_fold_op(crate::types::FoldOp::Add {
3114 start_row: top,
3115 end_row: bot,
3116 closed: true,
3117 });
3118 ed.vim.mode = Mode::Normal;
3119 } else {
3120 let count = take_count(&mut ed.vim);
3125 ed.vim.pending = Pending::Op {
3126 op: Operator::Fold,
3127 count1: count,
3128 };
3129 }
3130 }
3131 _ => {}
3132 }
3133 true
3134}
3135
3136fn handle_replace<H: crate::types::Host>(
3137 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3138 input: Input,
3139) -> bool {
3140 if let Key::Char(ch) = input.key {
3141 if ed.vim.mode == Mode::VisualBlock {
3142 block_replace(ed, ch);
3143 return true;
3144 }
3145 let count = take_count(&mut ed.vim);
3146 replace_char(ed, ch, count.max(1));
3147 if !ed.vim.replaying {
3148 ed.vim.last_change = Some(LastChange::ReplaceChar {
3149 ch,
3150 count: count.max(1),
3151 });
3152 }
3153 }
3154 true
3155}
3156
3157fn handle_find_target<H: crate::types::Host>(
3158 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3159 input: Input,
3160 forward: bool,
3161 till: bool,
3162) -> bool {
3163 let Key::Char(ch) = input.key else {
3164 return true;
3165 };
3166 let count = take_count(&mut ed.vim);
3167 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3168 ed.vim.last_find = Some((ch, forward, till));
3169 true
3170}
3171
3172fn handle_op_find_target<H: crate::types::Host>(
3173 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3174 input: Input,
3175 op: Operator,
3176 count1: usize,
3177 forward: bool,
3178 till: bool,
3179) -> bool {
3180 let Key::Char(ch) = input.key else {
3181 return true;
3182 };
3183 let count2 = take_count(&mut ed.vim);
3184 let total = count1.max(1) * count2.max(1);
3185 let motion = Motion::Find { ch, forward, till };
3186 apply_op_with_motion(ed, op, &motion, total);
3187 ed.vim.last_find = Some((ch, forward, till));
3188 if !ed.vim.replaying && op_is_change(op) {
3189 ed.vim.last_change = Some(LastChange::OpMotion {
3190 op,
3191 motion,
3192 count: total,
3193 inserted: None,
3194 });
3195 }
3196 true
3197}
3198
3199fn handle_text_object<H: crate::types::Host>(
3200 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3201 input: Input,
3202 op: Operator,
3203 _count1: usize,
3204 inner: bool,
3205) -> bool {
3206 let Key::Char(ch) = input.key else {
3207 return true;
3208 };
3209 let obj = match ch {
3210 'w' => TextObject::Word { big: false },
3211 'W' => TextObject::Word { big: true },
3212 '"' | '\'' | '`' => TextObject::Quote(ch),
3213 '(' | ')' | 'b' => TextObject::Bracket('('),
3214 '[' | ']' => TextObject::Bracket('['),
3215 '{' | '}' | 'B' => TextObject::Bracket('{'),
3216 '<' | '>' => TextObject::Bracket('<'),
3217 'p' => TextObject::Paragraph,
3218 't' => TextObject::XmlTag,
3219 's' => TextObject::Sentence,
3220 _ => return true,
3221 };
3222 apply_op_with_text_object(ed, op, obj, inner);
3223 if !ed.vim.replaying && op_is_change(op) {
3224 ed.vim.last_change = Some(LastChange::OpTextObj {
3225 op,
3226 obj,
3227 inner,
3228 inserted: None,
3229 });
3230 }
3231 true
3232}
3233
3234fn handle_visual_text_obj<H: crate::types::Host>(
3235 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3236 input: Input,
3237 inner: bool,
3238) -> bool {
3239 let Key::Char(ch) = input.key else {
3240 return true;
3241 };
3242 let obj = match ch {
3243 'w' => TextObject::Word { big: false },
3244 'W' => TextObject::Word { big: true },
3245 '"' | '\'' | '`' => TextObject::Quote(ch),
3246 '(' | ')' | 'b' => TextObject::Bracket('('),
3247 '[' | ']' => TextObject::Bracket('['),
3248 '{' | '}' | 'B' => TextObject::Bracket('{'),
3249 '<' | '>' => TextObject::Bracket('<'),
3250 'p' => TextObject::Paragraph,
3251 't' => TextObject::XmlTag,
3252 's' => TextObject::Sentence,
3253 _ => return true,
3254 };
3255 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3256 return true;
3257 };
3258 match kind {
3262 MotionKind::Linewise => {
3263 ed.vim.visual_line_anchor = start.0;
3264 ed.vim.mode = Mode::VisualLine;
3265 ed.jump_cursor(end.0, 0);
3266 }
3267 _ => {
3268 ed.vim.mode = Mode::Visual;
3269 ed.vim.visual_anchor = (start.0, start.1);
3270 let (er, ec) = retreat_one(ed, end);
3271 ed.jump_cursor(er, ec);
3272 }
3273 }
3274 true
3275}
3276
3277fn retreat_one<H: crate::types::Host>(
3279 ed: &Editor<hjkl_buffer::Buffer, H>,
3280 pos: (usize, usize),
3281) -> (usize, usize) {
3282 let (r, c) = pos;
3283 if c > 0 {
3284 (r, c - 1)
3285 } else if r > 0 {
3286 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3287 (r - 1, prev_len)
3288 } else {
3289 (0, 0)
3290 }
3291}
3292
3293fn op_is_change(op: Operator) -> bool {
3294 matches!(op, Operator::Delete | Operator::Change)
3295}
3296
3297fn handle_normal_only<H: crate::types::Host>(
3300 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3301 input: &Input,
3302 count: usize,
3303) -> bool {
3304 if input.ctrl {
3305 return false;
3306 }
3307 match input.key {
3308 Key::Char('i') => {
3309 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3310 true
3311 }
3312 Key::Char('I') => {
3313 move_first_non_whitespace(ed);
3314 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3315 true
3316 }
3317 Key::Char('a') => {
3318 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3319 ed.push_buffer_cursor_to_textarea();
3320 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3321 true
3322 }
3323 Key::Char('A') => {
3324 crate::motions::move_line_end(&mut ed.buffer);
3325 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3326 ed.push_buffer_cursor_to_textarea();
3327 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3328 true
3329 }
3330 Key::Char('R') => {
3331 begin_insert(ed, count.max(1), InsertReason::Replace);
3334 true
3335 }
3336 Key::Char('o') => {
3337 use hjkl_buffer::{Edit, Position};
3338 ed.push_undo();
3339 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3342 ed.sync_buffer_content_from_textarea();
3343 let row = buf_cursor_pos(&ed.buffer).row;
3344 let line_chars = buf_line_chars(&ed.buffer, row);
3345 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3348 let indent = compute_enter_indent(&ed.settings, prev_line);
3349 ed.mutate_edit(Edit::InsertStr {
3350 at: Position::new(row, line_chars),
3351 text: format!("\n{indent}"),
3352 });
3353 ed.push_buffer_cursor_to_textarea();
3354 true
3355 }
3356 Key::Char('O') => {
3357 use hjkl_buffer::{Edit, Position};
3358 ed.push_undo();
3359 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3360 ed.sync_buffer_content_from_textarea();
3361 let row = buf_cursor_pos(&ed.buffer).row;
3362 let indent = if row > 0 {
3366 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3367 compute_enter_indent(&ed.settings, above)
3368 } else {
3369 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3370 cur.chars()
3371 .take_while(|c| *c == ' ' || *c == '\t')
3372 .collect::<String>()
3373 };
3374 ed.mutate_edit(Edit::InsertStr {
3375 at: Position::new(row, 0),
3376 text: format!("{indent}\n"),
3377 });
3378 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3383 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3384 let new_row = buf_cursor_pos(&ed.buffer).row;
3385 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3386 ed.push_buffer_cursor_to_textarea();
3387 true
3388 }
3389 Key::Char('x') => {
3390 do_char_delete(ed, true, count.max(1));
3391 if !ed.vim.replaying {
3392 ed.vim.last_change = Some(LastChange::CharDel {
3393 forward: true,
3394 count: count.max(1),
3395 });
3396 }
3397 true
3398 }
3399 Key::Char('X') => {
3400 do_char_delete(ed, false, count.max(1));
3401 if !ed.vim.replaying {
3402 ed.vim.last_change = Some(LastChange::CharDel {
3403 forward: false,
3404 count: count.max(1),
3405 });
3406 }
3407 true
3408 }
3409 Key::Char('~') => {
3410 for _ in 0..count.max(1) {
3411 ed.push_undo();
3412 toggle_case_at_cursor(ed);
3413 }
3414 if !ed.vim.replaying {
3415 ed.vim.last_change = Some(LastChange::ToggleCase {
3416 count: count.max(1),
3417 });
3418 }
3419 true
3420 }
3421 Key::Char('J') => {
3422 for _ in 0..count.max(1) {
3423 ed.push_undo();
3424 join_line(ed);
3425 }
3426 if !ed.vim.replaying {
3427 ed.vim.last_change = Some(LastChange::JoinLine {
3428 count: count.max(1),
3429 });
3430 }
3431 true
3432 }
3433 Key::Char('D') => {
3434 ed.push_undo();
3435 delete_to_eol(ed);
3436 crate::motions::move_left(&mut ed.buffer, 1);
3438 ed.push_buffer_cursor_to_textarea();
3439 if !ed.vim.replaying {
3440 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3441 }
3442 true
3443 }
3444 Key::Char('Y') => {
3445 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3447 true
3448 }
3449 Key::Char('C') => {
3450 ed.push_undo();
3451 delete_to_eol(ed);
3452 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3453 true
3454 }
3455 Key::Char('s') => {
3456 use hjkl_buffer::{Edit, MotionKind, Position};
3457 ed.push_undo();
3458 ed.sync_buffer_content_from_textarea();
3459 for _ in 0..count.max(1) {
3460 let cursor = buf_cursor_pos(&ed.buffer);
3461 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3462 if cursor.col >= line_chars {
3463 break;
3464 }
3465 ed.mutate_edit(Edit::DeleteRange {
3466 start: cursor,
3467 end: Position::new(cursor.row, cursor.col + 1),
3468 kind: MotionKind::Char,
3469 });
3470 }
3471 ed.push_buffer_cursor_to_textarea();
3472 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3473 if !ed.vim.replaying {
3475 ed.vim.last_change = Some(LastChange::OpMotion {
3476 op: Operator::Change,
3477 motion: Motion::Right,
3478 count: count.max(1),
3479 inserted: None,
3480 });
3481 }
3482 true
3483 }
3484 Key::Char('p') => {
3485 do_paste(ed, false, count.max(1));
3486 if !ed.vim.replaying {
3487 ed.vim.last_change = Some(LastChange::Paste {
3488 before: false,
3489 count: count.max(1),
3490 });
3491 }
3492 true
3493 }
3494 Key::Char('P') => {
3495 do_paste(ed, true, count.max(1));
3496 if !ed.vim.replaying {
3497 ed.vim.last_change = Some(LastChange::Paste {
3498 before: true,
3499 count: count.max(1),
3500 });
3501 }
3502 true
3503 }
3504 Key::Char('u') => {
3505 do_undo(ed);
3506 true
3507 }
3508 Key::Char('r') => {
3509 ed.vim.count = count;
3510 ed.vim.pending = Pending::Replace;
3511 true
3512 }
3513 Key::Char('/') => {
3514 enter_search(ed, true);
3515 true
3516 }
3517 Key::Char('?') => {
3518 enter_search(ed, false);
3519 true
3520 }
3521 Key::Char('.') => {
3522 replay_last_change(ed, count);
3523 true
3524 }
3525 _ => false,
3526 }
3527}
3528
3529fn begin_insert_noundo<H: crate::types::Host>(
3531 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3532 count: usize,
3533 reason: InsertReason,
3534) {
3535 let reason = if ed.vim.replaying {
3536 InsertReason::ReplayOnly
3537 } else {
3538 reason
3539 };
3540 let (row, _) = ed.cursor();
3541 ed.vim.insert_session = Some(InsertSession {
3542 count,
3543 row_min: row,
3544 row_max: row,
3545 before_lines: buf_lines_to_vec(&ed.buffer),
3546 reason,
3547 });
3548 ed.vim.mode = Mode::Insert;
3549}
3550
3551fn apply_op_with_motion<H: crate::types::Host>(
3554 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3555 op: Operator,
3556 motion: &Motion,
3557 count: usize,
3558) {
3559 let start = ed.cursor();
3560 apply_motion_cursor_ctx(ed, motion, count, true);
3565 let end = ed.cursor();
3566 let kind = motion_kind(motion);
3567 ed.jump_cursor(start.0, start.1);
3569 run_operator_over_range(ed, op, start, end, kind);
3570}
3571
3572fn apply_op_with_text_object<H: crate::types::Host>(
3573 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3574 op: Operator,
3575 obj: TextObject,
3576 inner: bool,
3577) {
3578 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3579 return;
3580 };
3581 ed.jump_cursor(start.0, start.1);
3582 run_operator_over_range(ed, op, start, end, kind);
3583}
3584
3585fn motion_kind(motion: &Motion) -> MotionKind {
3586 match motion {
3587 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3588 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3589 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3590 MotionKind::Linewise
3591 }
3592 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3593 MotionKind::Inclusive
3594 }
3595 Motion::Find { .. } => MotionKind::Inclusive,
3596 Motion::MatchBracket => MotionKind::Inclusive,
3597 Motion::LineEnd => MotionKind::Inclusive,
3599 _ => MotionKind::Exclusive,
3600 }
3601}
3602
3603fn run_operator_over_range<H: crate::types::Host>(
3604 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3605 op: Operator,
3606 start: (usize, usize),
3607 end: (usize, usize),
3608 kind: MotionKind,
3609) {
3610 let (top, bot) = order(start, end);
3611 if top == bot {
3612 return;
3613 }
3614
3615 match op {
3616 Operator::Yank => {
3617 let text = read_vim_range(ed, top, bot, kind);
3618 if !text.is_empty() {
3619 ed.record_yank_to_host(text.clone());
3620 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3621 }
3622 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3623 ed.push_buffer_cursor_to_textarea();
3624 }
3625 Operator::Delete => {
3626 ed.push_undo();
3627 cut_vim_range(ed, top, bot, kind);
3628 if !matches!(kind, MotionKind::Linewise) {
3633 clamp_cursor_to_normal_mode(ed);
3634 }
3635 ed.vim.mode = Mode::Normal;
3636 }
3637 Operator::Change => {
3638 ed.push_undo();
3639 cut_vim_range(ed, top, bot, kind);
3640 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3641 }
3642 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3643 apply_case_op_to_selection(ed, op, top, bot, kind);
3644 }
3645 Operator::Indent | Operator::Outdent => {
3646 ed.push_undo();
3649 if op == Operator::Indent {
3650 indent_rows(ed, top.0, bot.0, 1);
3651 } else {
3652 outdent_rows(ed, top.0, bot.0, 1);
3653 }
3654 ed.vim.mode = Mode::Normal;
3655 }
3656 Operator::Fold => {
3657 if bot.0 >= top.0 {
3661 ed.apply_fold_op(crate::types::FoldOp::Add {
3662 start_row: top.0,
3663 end_row: bot.0,
3664 closed: true,
3665 });
3666 }
3667 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3668 ed.push_buffer_cursor_to_textarea();
3669 ed.vim.mode = Mode::Normal;
3670 }
3671 Operator::Reflow => {
3672 ed.push_undo();
3673 reflow_rows(ed, top.0, bot.0);
3674 ed.vim.mode = Mode::Normal;
3675 }
3676 }
3677}
3678
3679fn reflow_rows<H: crate::types::Host>(
3684 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3685 top: usize,
3686 bot: usize,
3687) {
3688 let width = ed.settings().textwidth.max(1);
3689 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3690 let bot = bot.min(lines.len().saturating_sub(1));
3691 if top > bot {
3692 return;
3693 }
3694 let original = lines[top..=bot].to_vec();
3695 let mut wrapped: Vec<String> = Vec::new();
3696 let mut paragraph: Vec<String> = Vec::new();
3697 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3698 if para.is_empty() {
3699 return;
3700 }
3701 let words = para.join(" ");
3702 let mut current = String::new();
3703 for word in words.split_whitespace() {
3704 let extra = if current.is_empty() {
3705 word.chars().count()
3706 } else {
3707 current.chars().count() + 1 + word.chars().count()
3708 };
3709 if extra > width && !current.is_empty() {
3710 out.push(std::mem::take(&mut current));
3711 current.push_str(word);
3712 } else if current.is_empty() {
3713 current.push_str(word);
3714 } else {
3715 current.push(' ');
3716 current.push_str(word);
3717 }
3718 }
3719 if !current.is_empty() {
3720 out.push(current);
3721 }
3722 para.clear();
3723 };
3724 for line in &original {
3725 if line.trim().is_empty() {
3726 flush(&mut paragraph, &mut wrapped, width);
3727 wrapped.push(String::new());
3728 } else {
3729 paragraph.push(line.clone());
3730 }
3731 }
3732 flush(&mut paragraph, &mut wrapped, width);
3733
3734 let after: Vec<String> = lines.split_off(bot + 1);
3736 lines.truncate(top);
3737 lines.extend(wrapped);
3738 lines.extend(after);
3739 ed.restore(lines, (top, 0));
3740 ed.mark_content_dirty();
3741}
3742
3743fn apply_case_op_to_selection<H: crate::types::Host>(
3749 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3750 op: Operator,
3751 top: (usize, usize),
3752 bot: (usize, usize),
3753 kind: MotionKind,
3754) {
3755 use hjkl_buffer::Edit;
3756 ed.push_undo();
3757 let saved_yank = ed.yank().to_string();
3758 let saved_yank_linewise = ed.vim.yank_linewise;
3759 let selection = cut_vim_range(ed, top, bot, kind);
3760 let transformed = match op {
3761 Operator::Uppercase => selection.to_uppercase(),
3762 Operator::Lowercase => selection.to_lowercase(),
3763 Operator::ToggleCase => toggle_case_str(&selection),
3764 _ => unreachable!(),
3765 };
3766 if !transformed.is_empty() {
3767 let cursor = buf_cursor_pos(&ed.buffer);
3768 ed.mutate_edit(Edit::InsertStr {
3769 at: cursor,
3770 text: transformed,
3771 });
3772 }
3773 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3774 ed.push_buffer_cursor_to_textarea();
3775 ed.set_yank(saved_yank);
3776 ed.vim.yank_linewise = saved_yank_linewise;
3777 ed.vim.mode = Mode::Normal;
3778}
3779
3780fn indent_rows<H: crate::types::Host>(
3785 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3786 top: usize,
3787 bot: usize,
3788 count: usize,
3789) {
3790 ed.sync_buffer_content_from_textarea();
3791 let width = ed.settings().shiftwidth * count.max(1);
3792 let pad: String = " ".repeat(width);
3793 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3794 let bot = bot.min(lines.len().saturating_sub(1));
3795 for line in lines.iter_mut().take(bot + 1).skip(top) {
3796 if !line.is_empty() {
3797 line.insert_str(0, &pad);
3798 }
3799 }
3800 ed.restore(lines, (top, 0));
3803 move_first_non_whitespace(ed);
3804}
3805
3806fn outdent_rows<H: crate::types::Host>(
3810 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3811 top: usize,
3812 bot: usize,
3813 count: usize,
3814) {
3815 ed.sync_buffer_content_from_textarea();
3816 let width = ed.settings().shiftwidth * count.max(1);
3817 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3818 let bot = bot.min(lines.len().saturating_sub(1));
3819 for line in lines.iter_mut().take(bot + 1).skip(top) {
3820 let strip: usize = line
3821 .chars()
3822 .take(width)
3823 .take_while(|c| *c == ' ' || *c == '\t')
3824 .count();
3825 if strip > 0 {
3826 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3827 line.drain(..byte_len);
3828 }
3829 }
3830 ed.restore(lines, (top, 0));
3831 move_first_non_whitespace(ed);
3832}
3833
3834fn toggle_case_str(s: &str) -> String {
3835 s.chars()
3836 .map(|c| {
3837 if c.is_lowercase() {
3838 c.to_uppercase().next().unwrap_or(c)
3839 } else if c.is_uppercase() {
3840 c.to_lowercase().next().unwrap_or(c)
3841 } else {
3842 c
3843 }
3844 })
3845 .collect()
3846}
3847
3848fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3849 if a <= b { (a, b) } else { (b, a) }
3850}
3851
3852fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3857 let (row, col) = ed.cursor();
3858 let line_chars = buf_line_chars(&ed.buffer, row);
3859 let max_col = line_chars.saturating_sub(1);
3860 if col > max_col {
3861 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
3862 ed.push_buffer_cursor_to_textarea();
3863 }
3864}
3865
3866fn execute_line_op<H: crate::types::Host>(
3869 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3870 op: Operator,
3871 count: usize,
3872) {
3873 let (row, col) = ed.cursor();
3874 let total = buf_row_count(&ed.buffer);
3875 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3876
3877 match op {
3878 Operator::Yank => {
3879 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3881 if !text.is_empty() {
3882 ed.record_yank_to_host(text.clone());
3883 ed.record_yank(text, true);
3884 }
3885 buf_set_cursor_rc(&mut ed.buffer, row, col);
3886 ed.push_buffer_cursor_to_textarea();
3887 ed.vim.mode = Mode::Normal;
3888 }
3889 Operator::Delete => {
3890 ed.push_undo();
3891 let deleted_through_last = end_row + 1 >= total;
3892 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3893 let total_after = buf_row_count(&ed.buffer);
3897 let raw_target = if deleted_through_last {
3898 row.saturating_sub(1).min(total_after.saturating_sub(1))
3899 } else {
3900 row.min(total_after.saturating_sub(1))
3901 };
3902 let target_row = if raw_target > 0
3908 && raw_target + 1 == total_after
3909 && buf_line(&ed.buffer, raw_target)
3910 .map(str::is_empty)
3911 .unwrap_or(false)
3912 {
3913 raw_target - 1
3914 } else {
3915 raw_target
3916 };
3917 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
3918 ed.push_buffer_cursor_to_textarea();
3919 move_first_non_whitespace(ed);
3920 ed.sticky_col = Some(ed.cursor().1);
3921 ed.vim.mode = Mode::Normal;
3922 }
3923 Operator::Change => {
3924 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3928 ed.push_undo();
3929 ed.sync_buffer_content_from_textarea();
3930 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3932 if end_row > row {
3933 ed.mutate_edit(Edit::DeleteRange {
3934 start: Position::new(row + 1, 0),
3935 end: Position::new(end_row, 0),
3936 kind: BufKind::Line,
3937 });
3938 }
3939 let line_chars = buf_line_chars(&ed.buffer, row);
3940 if line_chars > 0 {
3941 ed.mutate_edit(Edit::DeleteRange {
3942 start: Position::new(row, 0),
3943 end: Position::new(row, line_chars),
3944 kind: BufKind::Char,
3945 });
3946 }
3947 if !payload.is_empty() {
3948 ed.record_yank_to_host(payload.clone());
3949 ed.record_delete(payload, true);
3950 }
3951 buf_set_cursor_rc(&mut ed.buffer, row, 0);
3952 ed.push_buffer_cursor_to_textarea();
3953 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3954 }
3955 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3956 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
3960 move_first_non_whitespace(ed);
3963 }
3964 Operator::Indent | Operator::Outdent => {
3965 ed.push_undo();
3967 if op == Operator::Indent {
3968 indent_rows(ed, row, end_row, 1);
3969 } else {
3970 outdent_rows(ed, row, end_row, 1);
3971 }
3972 ed.sticky_col = Some(ed.cursor().1);
3973 ed.vim.mode = Mode::Normal;
3974 }
3975 Operator::Fold => unreachable!("Fold has no line-op double"),
3977 Operator::Reflow => {
3978 ed.push_undo();
3980 reflow_rows(ed, row, end_row);
3981 move_first_non_whitespace(ed);
3982 ed.sticky_col = Some(ed.cursor().1);
3983 ed.vim.mode = Mode::Normal;
3984 }
3985 }
3986}
3987
3988fn apply_visual_operator<H: crate::types::Host>(
3991 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3992 op: Operator,
3993) {
3994 match ed.vim.mode {
3995 Mode::VisualLine => {
3996 let cursor_row = buf_cursor_pos(&ed.buffer).row;
3997 let top = cursor_row.min(ed.vim.visual_line_anchor);
3998 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3999 ed.vim.yank_linewise = true;
4000 match op {
4001 Operator::Yank => {
4002 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4003 if !text.is_empty() {
4004 ed.record_yank_to_host(text.clone());
4005 ed.record_yank(text, true);
4006 }
4007 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4008 ed.push_buffer_cursor_to_textarea();
4009 ed.vim.mode = Mode::Normal;
4010 }
4011 Operator::Delete => {
4012 ed.push_undo();
4013 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4014 ed.vim.mode = Mode::Normal;
4015 }
4016 Operator::Change => {
4017 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4020 ed.push_undo();
4021 ed.sync_buffer_content_from_textarea();
4022 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4023 if bot > top {
4024 ed.mutate_edit(Edit::DeleteRange {
4025 start: Position::new(top + 1, 0),
4026 end: Position::new(bot, 0),
4027 kind: BufKind::Line,
4028 });
4029 }
4030 let line_chars = buf_line_chars(&ed.buffer, top);
4031 if line_chars > 0 {
4032 ed.mutate_edit(Edit::DeleteRange {
4033 start: Position::new(top, 0),
4034 end: Position::new(top, line_chars),
4035 kind: BufKind::Char,
4036 });
4037 }
4038 if !payload.is_empty() {
4039 ed.record_yank_to_host(payload.clone());
4040 ed.record_delete(payload, true);
4041 }
4042 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4043 ed.push_buffer_cursor_to_textarea();
4044 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4045 }
4046 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4047 let bot = buf_cursor_pos(&ed.buffer)
4048 .row
4049 .max(ed.vim.visual_line_anchor);
4050 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4051 move_first_non_whitespace(ed);
4052 }
4053 Operator::Indent | Operator::Outdent => {
4054 ed.push_undo();
4055 let (cursor_row, _) = ed.cursor();
4056 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4057 if op == Operator::Indent {
4058 indent_rows(ed, top, bot, 1);
4059 } else {
4060 outdent_rows(ed, top, bot, 1);
4061 }
4062 ed.vim.mode = Mode::Normal;
4063 }
4064 Operator::Reflow => {
4065 ed.push_undo();
4066 let (cursor_row, _) = ed.cursor();
4067 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4068 reflow_rows(ed, top, bot);
4069 ed.vim.mode = Mode::Normal;
4070 }
4071 Operator::Fold => unreachable!("Visual zf takes its own path"),
4074 }
4075 }
4076 Mode::Visual => {
4077 ed.vim.yank_linewise = false;
4078 let anchor = ed.vim.visual_anchor;
4079 let cursor = ed.cursor();
4080 let (top, bot) = order(anchor, cursor);
4081 match op {
4082 Operator::Yank => {
4083 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4084 if !text.is_empty() {
4085 ed.record_yank_to_host(text.clone());
4086 ed.record_yank(text, false);
4087 }
4088 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4089 ed.push_buffer_cursor_to_textarea();
4090 ed.vim.mode = Mode::Normal;
4091 }
4092 Operator::Delete => {
4093 ed.push_undo();
4094 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4095 ed.vim.mode = Mode::Normal;
4096 }
4097 Operator::Change => {
4098 ed.push_undo();
4099 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4100 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4101 }
4102 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4103 let anchor = ed.vim.visual_anchor;
4105 let cursor = ed.cursor();
4106 let (top, bot) = order(anchor, cursor);
4107 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4108 }
4109 Operator::Indent | Operator::Outdent => {
4110 ed.push_undo();
4111 let anchor = ed.vim.visual_anchor;
4112 let cursor = ed.cursor();
4113 let (top, bot) = order(anchor, cursor);
4114 if op == Operator::Indent {
4115 indent_rows(ed, top.0, bot.0, 1);
4116 } else {
4117 outdent_rows(ed, top.0, bot.0, 1);
4118 }
4119 ed.vim.mode = Mode::Normal;
4120 }
4121 Operator::Reflow => {
4122 ed.push_undo();
4123 let anchor = ed.vim.visual_anchor;
4124 let cursor = ed.cursor();
4125 let (top, bot) = order(anchor, cursor);
4126 reflow_rows(ed, top.0, bot.0);
4127 ed.vim.mode = Mode::Normal;
4128 }
4129 Operator::Fold => unreachable!("Visual zf takes its own path"),
4130 }
4131 }
4132 Mode::VisualBlock => apply_block_operator(ed, op),
4133 _ => {}
4134 }
4135}
4136
4137fn block_bounds<H: crate::types::Host>(
4142 ed: &Editor<hjkl_buffer::Buffer, H>,
4143) -> (usize, usize, usize, usize) {
4144 let (ar, ac) = ed.vim.block_anchor;
4145 let (cr, _) = ed.cursor();
4146 let cc = ed.vim.block_vcol;
4147 let top = ar.min(cr);
4148 let bot = ar.max(cr);
4149 let left = ac.min(cc);
4150 let right = ac.max(cc);
4151 (top, bot, left, right)
4152}
4153
4154fn update_block_vcol<H: crate::types::Host>(
4159 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4160 motion: &Motion,
4161) {
4162 match motion {
4163 Motion::Left
4164 | Motion::Right
4165 | Motion::WordFwd
4166 | Motion::BigWordFwd
4167 | Motion::WordBack
4168 | Motion::BigWordBack
4169 | Motion::WordEnd
4170 | Motion::BigWordEnd
4171 | Motion::WordEndBack
4172 | Motion::BigWordEndBack
4173 | Motion::LineStart
4174 | Motion::FirstNonBlank
4175 | Motion::LineEnd
4176 | Motion::Find { .. }
4177 | Motion::FindRepeat { .. }
4178 | Motion::MatchBracket => {
4179 ed.vim.block_vcol = ed.cursor().1;
4180 }
4181 _ => {}
4183 }
4184}
4185
4186fn apply_block_operator<H: crate::types::Host>(
4191 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4192 op: Operator,
4193) {
4194 let (top, bot, left, right) = block_bounds(ed);
4195 let yank = block_yank(ed, top, bot, left, right);
4197
4198 match op {
4199 Operator::Yank => {
4200 if !yank.is_empty() {
4201 ed.record_yank_to_host(yank.clone());
4202 ed.record_yank(yank, false);
4203 }
4204 ed.vim.mode = Mode::Normal;
4205 ed.jump_cursor(top, left);
4206 }
4207 Operator::Delete => {
4208 ed.push_undo();
4209 delete_block_contents(ed, top, bot, left, right);
4210 if !yank.is_empty() {
4211 ed.record_yank_to_host(yank.clone());
4212 ed.record_delete(yank, false);
4213 }
4214 ed.vim.mode = Mode::Normal;
4215 ed.jump_cursor(top, left);
4216 }
4217 Operator::Change => {
4218 ed.push_undo();
4219 delete_block_contents(ed, top, bot, left, right);
4220 if !yank.is_empty() {
4221 ed.record_yank_to_host(yank.clone());
4222 ed.record_delete(yank, false);
4223 }
4224 ed.jump_cursor(top, left);
4225 begin_insert_noundo(
4226 ed,
4227 1,
4228 InsertReason::BlockEdge {
4229 top,
4230 bot,
4231 col: left,
4232 },
4233 );
4234 }
4235 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4236 ed.push_undo();
4237 transform_block_case(ed, op, top, bot, left, right);
4238 ed.vim.mode = Mode::Normal;
4239 ed.jump_cursor(top, left);
4240 }
4241 Operator::Indent | Operator::Outdent => {
4242 ed.push_undo();
4246 if op == Operator::Indent {
4247 indent_rows(ed, top, bot, 1);
4248 } else {
4249 outdent_rows(ed, top, bot, 1);
4250 }
4251 ed.vim.mode = Mode::Normal;
4252 }
4253 Operator::Fold => unreachable!("Visual zf takes its own path"),
4254 Operator::Reflow => {
4255 ed.push_undo();
4259 reflow_rows(ed, top, bot);
4260 ed.vim.mode = Mode::Normal;
4261 }
4262 }
4263}
4264
4265fn transform_block_case<H: crate::types::Host>(
4269 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4270 op: Operator,
4271 top: usize,
4272 bot: usize,
4273 left: usize,
4274 right: usize,
4275) {
4276 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4277 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4278 let chars: Vec<char> = lines[r].chars().collect();
4279 if left >= chars.len() {
4280 continue;
4281 }
4282 let end = (right + 1).min(chars.len());
4283 let head: String = chars[..left].iter().collect();
4284 let mid: String = chars[left..end].iter().collect();
4285 let tail: String = chars[end..].iter().collect();
4286 let transformed = match op {
4287 Operator::Uppercase => mid.to_uppercase(),
4288 Operator::Lowercase => mid.to_lowercase(),
4289 Operator::ToggleCase => toggle_case_str(&mid),
4290 _ => mid,
4291 };
4292 lines[r] = format!("{head}{transformed}{tail}");
4293 }
4294 let saved_yank = ed.yank().to_string();
4295 let saved_linewise = ed.vim.yank_linewise;
4296 ed.restore(lines, (top, left));
4297 ed.set_yank(saved_yank);
4298 ed.vim.yank_linewise = saved_linewise;
4299}
4300
4301fn block_yank<H: crate::types::Host>(
4302 ed: &Editor<hjkl_buffer::Buffer, H>,
4303 top: usize,
4304 bot: usize,
4305 left: usize,
4306 right: usize,
4307) -> String {
4308 let lines = buf_lines_to_vec(&ed.buffer);
4309 let mut rows: Vec<String> = Vec::new();
4310 for r in top..=bot {
4311 let line = match lines.get(r) {
4312 Some(l) => l,
4313 None => break,
4314 };
4315 let chars: Vec<char> = line.chars().collect();
4316 let end = (right + 1).min(chars.len());
4317 if left >= chars.len() {
4318 rows.push(String::new());
4319 } else {
4320 rows.push(chars[left..end].iter().collect());
4321 }
4322 }
4323 rows.join("\n")
4324}
4325
4326fn delete_block_contents<H: crate::types::Host>(
4327 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4328 top: usize,
4329 bot: usize,
4330 left: usize,
4331 right: usize,
4332) {
4333 use hjkl_buffer::{Edit, MotionKind, Position};
4334 ed.sync_buffer_content_from_textarea();
4335 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4336 if last_row < top {
4337 return;
4338 }
4339 ed.mutate_edit(Edit::DeleteRange {
4340 start: Position::new(top, left),
4341 end: Position::new(last_row, right),
4342 kind: MotionKind::Block,
4343 });
4344 ed.push_buffer_cursor_to_textarea();
4345}
4346
4347fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4349 let (top, bot, left, right) = block_bounds(ed);
4350 ed.push_undo();
4351 ed.sync_buffer_content_from_textarea();
4352 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4353 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4354 let chars: Vec<char> = lines[r].chars().collect();
4355 if left >= chars.len() {
4356 continue;
4357 }
4358 let end = (right + 1).min(chars.len());
4359 let before: String = chars[..left].iter().collect();
4360 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4361 let after: String = chars[end..].iter().collect();
4362 lines[r] = format!("{before}{middle}{after}");
4363 }
4364 reset_textarea_lines(ed, lines);
4365 ed.vim.mode = Mode::Normal;
4366 ed.jump_cursor(top, left);
4367}
4368
4369fn reset_textarea_lines<H: crate::types::Host>(
4373 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4374 lines: Vec<String>,
4375) {
4376 let cursor = ed.cursor();
4377 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4378 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4379 ed.mark_content_dirty();
4380}
4381
4382type Pos = (usize, usize);
4388
4389fn text_object_range<H: crate::types::Host>(
4393 ed: &Editor<hjkl_buffer::Buffer, H>,
4394 obj: TextObject,
4395 inner: bool,
4396) -> Option<(Pos, Pos, MotionKind)> {
4397 match obj {
4398 TextObject::Word { big } => {
4399 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4400 }
4401 TextObject::Quote(q) => {
4402 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4403 }
4404 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4405 TextObject::Paragraph => {
4406 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4407 }
4408 TextObject::XmlTag => {
4409 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4410 }
4411 TextObject::Sentence => {
4412 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4413 }
4414 }
4415}
4416
4417fn sentence_boundary<H: crate::types::Host>(
4421 ed: &Editor<hjkl_buffer::Buffer, H>,
4422 forward: bool,
4423) -> Option<(usize, usize)> {
4424 let lines = buf_lines_to_vec(&ed.buffer);
4425 if lines.is_empty() {
4426 return None;
4427 }
4428 let pos_to_idx = |pos: (usize, usize)| -> usize {
4429 let mut idx = 0;
4430 for line in lines.iter().take(pos.0) {
4431 idx += line.chars().count() + 1;
4432 }
4433 idx + pos.1
4434 };
4435 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4436 for (r, line) in lines.iter().enumerate() {
4437 let len = line.chars().count();
4438 if idx <= len {
4439 return (r, idx);
4440 }
4441 idx -= len + 1;
4442 }
4443 let last = lines.len().saturating_sub(1);
4444 (last, lines[last].chars().count())
4445 };
4446 let mut chars: Vec<char> = Vec::new();
4447 for (r, line) in lines.iter().enumerate() {
4448 chars.extend(line.chars());
4449 if r + 1 < lines.len() {
4450 chars.push('\n');
4451 }
4452 }
4453 if chars.is_empty() {
4454 return None;
4455 }
4456 let total = chars.len();
4457 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4458 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4459
4460 if forward {
4461 let mut i = cursor_idx + 1;
4464 while i < total {
4465 if is_terminator(chars[i]) {
4466 while i + 1 < total && is_terminator(chars[i + 1]) {
4467 i += 1;
4468 }
4469 if i + 1 >= total {
4470 return None;
4471 }
4472 if chars[i + 1].is_whitespace() {
4473 let mut j = i + 1;
4474 while j < total && chars[j].is_whitespace() {
4475 j += 1;
4476 }
4477 if j >= total {
4478 return None;
4479 }
4480 return Some(idx_to_pos(j));
4481 }
4482 }
4483 i += 1;
4484 }
4485 None
4486 } else {
4487 let find_start = |from: usize| -> Option<usize> {
4491 let mut start = from;
4492 while start > 0 {
4493 let prev = chars[start - 1];
4494 if prev.is_whitespace() {
4495 let mut k = start - 1;
4496 while k > 0 && chars[k - 1].is_whitespace() {
4497 k -= 1;
4498 }
4499 if k > 0 && is_terminator(chars[k - 1]) {
4500 break;
4501 }
4502 }
4503 start -= 1;
4504 }
4505 while start < total && chars[start].is_whitespace() {
4506 start += 1;
4507 }
4508 (start < total).then_some(start)
4509 };
4510 let current_start = find_start(cursor_idx)?;
4511 if current_start < cursor_idx {
4512 return Some(idx_to_pos(current_start));
4513 }
4514 let mut k = current_start;
4517 while k > 0 && chars[k - 1].is_whitespace() {
4518 k -= 1;
4519 }
4520 if k == 0 {
4521 return None;
4522 }
4523 let prev_start = find_start(k - 1)?;
4524 Some(idx_to_pos(prev_start))
4525 }
4526}
4527
4528fn sentence_text_object<H: crate::types::Host>(
4534 ed: &Editor<hjkl_buffer::Buffer, H>,
4535 inner: bool,
4536) -> Option<((usize, usize), (usize, usize))> {
4537 let lines = buf_lines_to_vec(&ed.buffer);
4538 if lines.is_empty() {
4539 return None;
4540 }
4541 let pos_to_idx = |pos: (usize, usize)| -> usize {
4544 let mut idx = 0;
4545 for line in lines.iter().take(pos.0) {
4546 idx += line.chars().count() + 1;
4547 }
4548 idx + pos.1
4549 };
4550 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4551 for (r, line) in lines.iter().enumerate() {
4552 let len = line.chars().count();
4553 if idx <= len {
4554 return (r, idx);
4555 }
4556 idx -= len + 1;
4557 }
4558 let last = lines.len().saturating_sub(1);
4559 (last, lines[last].chars().count())
4560 };
4561 let mut chars: Vec<char> = Vec::new();
4562 for (r, line) in lines.iter().enumerate() {
4563 chars.extend(line.chars());
4564 if r + 1 < lines.len() {
4565 chars.push('\n');
4566 }
4567 }
4568 if chars.is_empty() {
4569 return None;
4570 }
4571
4572 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4573 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4574
4575 let mut start = cursor_idx;
4579 while start > 0 {
4580 let prev = chars[start - 1];
4581 if prev.is_whitespace() {
4582 let mut k = start - 1;
4586 while k > 0 && chars[k - 1].is_whitespace() {
4587 k -= 1;
4588 }
4589 if k > 0 && is_terminator(chars[k - 1]) {
4590 break;
4591 }
4592 }
4593 start -= 1;
4594 }
4595 while start < chars.len() && chars[start].is_whitespace() {
4598 start += 1;
4599 }
4600 if start >= chars.len() {
4601 return None;
4602 }
4603
4604 let mut end = start;
4607 while end < chars.len() {
4608 if is_terminator(chars[end]) {
4609 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4611 end += 1;
4612 }
4613 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4616 break;
4617 }
4618 }
4619 end += 1;
4620 }
4621 let end_idx = (end + 1).min(chars.len());
4623
4624 let final_end = if inner {
4625 end_idx
4626 } else {
4627 let mut e = end_idx;
4631 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4632 e += 1;
4633 }
4634 e
4635 };
4636
4637 Some((idx_to_pos(start), idx_to_pos(final_end)))
4638}
4639
4640fn tag_text_object<H: crate::types::Host>(
4644 ed: &Editor<hjkl_buffer::Buffer, H>,
4645 inner: bool,
4646) -> Option<((usize, usize), (usize, usize))> {
4647 let lines = buf_lines_to_vec(&ed.buffer);
4648 if lines.is_empty() {
4649 return None;
4650 }
4651 let pos_to_idx = |pos: (usize, usize)| -> usize {
4655 let mut idx = 0;
4656 for line in lines.iter().take(pos.0) {
4657 idx += line.chars().count() + 1;
4658 }
4659 idx + pos.1
4660 };
4661 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4662 for (r, line) in lines.iter().enumerate() {
4663 let len = line.chars().count();
4664 if idx <= len {
4665 return (r, idx);
4666 }
4667 idx -= len + 1;
4668 }
4669 let last = lines.len().saturating_sub(1);
4670 (last, lines[last].chars().count())
4671 };
4672 let mut chars: Vec<char> = Vec::new();
4673 for (r, line) in lines.iter().enumerate() {
4674 chars.extend(line.chars());
4675 if r + 1 < lines.len() {
4676 chars.push('\n');
4677 }
4678 }
4679 let cursor_idx = pos_to_idx(ed.cursor());
4680
4681 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4689 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4690 let mut i = 0;
4691 while i < chars.len() {
4692 if chars[i] != '<' {
4693 i += 1;
4694 continue;
4695 }
4696 let mut j = i + 1;
4697 while j < chars.len() && chars[j] != '>' {
4698 j += 1;
4699 }
4700 if j >= chars.len() {
4701 break;
4702 }
4703 let inside: String = chars[i + 1..j].iter().collect();
4704 let close_end = j + 1;
4705 let trimmed = inside.trim();
4706 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4707 i = close_end;
4708 continue;
4709 }
4710 if let Some(rest) = trimmed.strip_prefix('/') {
4711 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4712 if !name.is_empty()
4713 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4714 {
4715 let (open_start, content_start, _) = stack[stack_idx].clone();
4716 stack.truncate(stack_idx);
4717 let content_end = i;
4718 let candidate = (open_start, content_start, content_end, close_end);
4719 if cursor_idx >= content_start && cursor_idx <= content_end {
4720 innermost = match innermost {
4721 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4722 Some(candidate)
4723 }
4724 None => Some(candidate),
4725 existing => existing,
4726 };
4727 } else if open_start >= cursor_idx && next_after.is_none() {
4728 next_after = Some(candidate);
4729 }
4730 }
4731 } else if !trimmed.ends_with('/') {
4732 let name: String = trimmed
4733 .split(|c: char| c.is_whitespace() || c == '/')
4734 .next()
4735 .unwrap_or("")
4736 .to_string();
4737 if !name.is_empty() {
4738 stack.push((i, close_end, name));
4739 }
4740 }
4741 i = close_end;
4742 }
4743
4744 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4745 if inner {
4746 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4747 } else {
4748 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4749 }
4750}
4751
4752fn is_wordchar(c: char) -> bool {
4753 c.is_alphanumeric() || c == '_'
4754}
4755
4756pub(crate) use hjkl_buffer::is_keyword_char;
4760
4761fn word_text_object<H: crate::types::Host>(
4762 ed: &Editor<hjkl_buffer::Buffer, H>,
4763 inner: bool,
4764 big: bool,
4765) -> Option<((usize, usize), (usize, usize))> {
4766 let (row, col) = ed.cursor();
4767 let line = buf_line(&ed.buffer, row)?;
4768 let chars: Vec<char> = line.chars().collect();
4769 if chars.is_empty() {
4770 return None;
4771 }
4772 let at = col.min(chars.len().saturating_sub(1));
4773 let classify = |c: char| -> u8 {
4774 if c.is_whitespace() {
4775 0
4776 } else if big || is_wordchar(c) {
4777 1
4778 } else {
4779 2
4780 }
4781 };
4782 let cls = classify(chars[at]);
4783 let mut start = at;
4784 while start > 0 && classify(chars[start - 1]) == cls {
4785 start -= 1;
4786 }
4787 let mut end = at;
4788 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4789 end += 1;
4790 }
4791 let char_byte = |i: usize| {
4793 if i >= chars.len() {
4794 line.len()
4795 } else {
4796 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4797 }
4798 };
4799 let mut start_col = char_byte(start);
4800 let mut end_col = char_byte(end + 1);
4802 if !inner {
4803 let mut t = end + 1;
4805 let mut included_trailing = false;
4806 while t < chars.len() && chars[t].is_whitespace() {
4807 included_trailing = true;
4808 t += 1;
4809 }
4810 if included_trailing {
4811 end_col = char_byte(t);
4812 } else {
4813 let mut s = start;
4814 while s > 0 && chars[s - 1].is_whitespace() {
4815 s -= 1;
4816 }
4817 start_col = char_byte(s);
4818 }
4819 }
4820 Some(((row, start_col), (row, end_col)))
4821}
4822
4823fn quote_text_object<H: crate::types::Host>(
4824 ed: &Editor<hjkl_buffer::Buffer, H>,
4825 q: char,
4826 inner: bool,
4827) -> Option<((usize, usize), (usize, usize))> {
4828 let (row, col) = ed.cursor();
4829 let line = buf_line(&ed.buffer, row)?;
4830 let bytes = line.as_bytes();
4831 let q_byte = q as u8;
4832 let mut positions: Vec<usize> = Vec::new();
4834 for (i, &b) in bytes.iter().enumerate() {
4835 if b == q_byte {
4836 positions.push(i);
4837 }
4838 }
4839 if positions.len() < 2 {
4840 return None;
4841 }
4842 let mut open_idx: Option<usize> = None;
4843 let mut close_idx: Option<usize> = None;
4844 for pair in positions.chunks(2) {
4845 if pair.len() < 2 {
4846 break;
4847 }
4848 if col >= pair[0] && col <= pair[1] {
4849 open_idx = Some(pair[0]);
4850 close_idx = Some(pair[1]);
4851 break;
4852 }
4853 if col < pair[0] {
4854 open_idx = Some(pair[0]);
4855 close_idx = Some(pair[1]);
4856 break;
4857 }
4858 }
4859 let open = open_idx?;
4860 let close = close_idx?;
4861 if inner {
4863 if close <= open + 1 {
4864 return None;
4865 }
4866 Some(((row, open + 1), (row, close)))
4867 } else {
4868 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
4875 let mut end = after_close;
4877 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
4878 end += 1;
4879 }
4880 Some(((row, open), (row, end)))
4881 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
4882 let mut start = open;
4884 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
4885 start -= 1;
4886 }
4887 Some(((row, start), (row, close + 1)))
4888 } else {
4889 Some(((row, open), (row, close + 1)))
4890 }
4891 }
4892}
4893
4894fn bracket_text_object<H: crate::types::Host>(
4895 ed: &Editor<hjkl_buffer::Buffer, H>,
4896 open: char,
4897 inner: bool,
4898) -> Option<(Pos, Pos, MotionKind)> {
4899 let close = match open {
4900 '(' => ')',
4901 '[' => ']',
4902 '{' => '}',
4903 '<' => '>',
4904 _ => return None,
4905 };
4906 let (row, col) = ed.cursor();
4907 let lines = buf_lines_to_vec(&ed.buffer);
4908 let lines = lines.as_slice();
4909 let open_pos = find_open_bracket(lines, row, col, open, close)
4914 .or_else(|| find_next_open(lines, row, col, open))?;
4915 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
4916 if inner {
4918 if close_pos.0 > open_pos.0 + 1 {
4924 let inner_row_start = open_pos.0 + 1;
4926 let inner_row_end = close_pos.0 - 1;
4927 let end_col = lines
4928 .get(inner_row_end)
4929 .map(|l| l.chars().count())
4930 .unwrap_or(0);
4931 return Some((
4932 (inner_row_start, 0),
4933 (inner_row_end, end_col),
4934 MotionKind::Linewise,
4935 ));
4936 }
4937 let inner_start = advance_pos(lines, open_pos);
4938 if inner_start.0 > close_pos.0
4939 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
4940 {
4941 return None;
4942 }
4943 Some((inner_start, close_pos, MotionKind::Exclusive))
4944 } else {
4945 Some((
4946 open_pos,
4947 advance_pos(lines, close_pos),
4948 MotionKind::Exclusive,
4949 ))
4950 }
4951}
4952
4953fn find_open_bracket(
4954 lines: &[String],
4955 row: usize,
4956 col: usize,
4957 open: char,
4958 close: char,
4959) -> Option<(usize, usize)> {
4960 let mut depth: i32 = 0;
4961 let mut r = row;
4962 let mut c = col as isize;
4963 loop {
4964 let cur = &lines[r];
4965 let chars: Vec<char> = cur.chars().collect();
4966 if (c as usize) >= chars.len() {
4970 c = chars.len() as isize - 1;
4971 }
4972 while c >= 0 {
4973 let ch = chars[c as usize];
4974 if ch == close {
4975 depth += 1;
4976 } else if ch == open {
4977 if depth == 0 {
4978 return Some((r, c as usize));
4979 }
4980 depth -= 1;
4981 }
4982 c -= 1;
4983 }
4984 if r == 0 {
4985 return None;
4986 }
4987 r -= 1;
4988 c = lines[r].chars().count() as isize - 1;
4989 }
4990}
4991
4992fn find_close_bracket(
4993 lines: &[String],
4994 row: usize,
4995 start_col: usize,
4996 open: char,
4997 close: char,
4998) -> Option<(usize, usize)> {
4999 let mut depth: i32 = 0;
5000 let mut r = row;
5001 let mut c = start_col;
5002 loop {
5003 let cur = &lines[r];
5004 let chars: Vec<char> = cur.chars().collect();
5005 while c < chars.len() {
5006 let ch = chars[c];
5007 if ch == open {
5008 depth += 1;
5009 } else if ch == close {
5010 if depth == 0 {
5011 return Some((r, c));
5012 }
5013 depth -= 1;
5014 }
5015 c += 1;
5016 }
5017 if r + 1 >= lines.len() {
5018 return None;
5019 }
5020 r += 1;
5021 c = 0;
5022 }
5023}
5024
5025fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5029 let mut r = row;
5030 let mut c = col;
5031 while r < lines.len() {
5032 let chars: Vec<char> = lines[r].chars().collect();
5033 while c < chars.len() {
5034 if chars[c] == open {
5035 return Some((r, c));
5036 }
5037 c += 1;
5038 }
5039 r += 1;
5040 c = 0;
5041 }
5042 None
5043}
5044
5045fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5046 let (r, c) = pos;
5047 let line_len = lines[r].chars().count();
5048 if c < line_len {
5049 (r, c + 1)
5050 } else if r + 1 < lines.len() {
5051 (r + 1, 0)
5052 } else {
5053 pos
5054 }
5055}
5056
5057fn paragraph_text_object<H: crate::types::Host>(
5058 ed: &Editor<hjkl_buffer::Buffer, H>,
5059 inner: bool,
5060) -> Option<((usize, usize), (usize, usize))> {
5061 let (row, _) = ed.cursor();
5062 let lines = buf_lines_to_vec(&ed.buffer);
5063 if lines.is_empty() {
5064 return None;
5065 }
5066 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5068 if is_blank(row) {
5069 return None;
5070 }
5071 let mut top = row;
5072 while top > 0 && !is_blank(top - 1) {
5073 top -= 1;
5074 }
5075 let mut bot = row;
5076 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5077 bot += 1;
5078 }
5079 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5081 bot += 1;
5082 }
5083 let end_col = lines[bot].chars().count();
5084 Some(((top, 0), (bot, end_col)))
5085}
5086
5087fn read_vim_range<H: crate::types::Host>(
5093 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5094 start: (usize, usize),
5095 end: (usize, usize),
5096 kind: MotionKind,
5097) -> String {
5098 let (top, bot) = order(start, end);
5099 ed.sync_buffer_content_from_textarea();
5100 let lines = buf_lines_to_vec(&ed.buffer);
5101 match kind {
5102 MotionKind::Linewise => {
5103 let lo = top.0;
5104 let hi = bot.0.min(lines.len().saturating_sub(1));
5105 let mut text = lines[lo..=hi].join("\n");
5106 text.push('\n');
5107 text
5108 }
5109 MotionKind::Inclusive | MotionKind::Exclusive => {
5110 let inclusive = matches!(kind, MotionKind::Inclusive);
5111 let mut out = String::new();
5113 for row in top.0..=bot.0 {
5114 let line = lines.get(row).map(String::as_str).unwrap_or("");
5115 let lo = if row == top.0 { top.1 } else { 0 };
5116 let hi_unclamped = if row == bot.0 {
5117 if inclusive { bot.1 + 1 } else { bot.1 }
5118 } else {
5119 line.chars().count() + 1
5120 };
5121 let row_chars: Vec<char> = line.chars().collect();
5122 let hi = hi_unclamped.min(row_chars.len());
5123 if lo < hi {
5124 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5125 }
5126 if row < bot.0 {
5127 out.push('\n');
5128 }
5129 }
5130 out
5131 }
5132 }
5133}
5134
5135fn cut_vim_range<H: crate::types::Host>(
5144 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5145 start: (usize, usize),
5146 end: (usize, usize),
5147 kind: MotionKind,
5148) -> String {
5149 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5150 let (top, bot) = order(start, end);
5151 ed.sync_buffer_content_from_textarea();
5152 let (buf_start, buf_end, buf_kind) = match kind {
5153 MotionKind::Linewise => (
5154 Position::new(top.0, 0),
5155 Position::new(bot.0, 0),
5156 BufKind::Line,
5157 ),
5158 MotionKind::Inclusive => {
5159 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5160 let next = if bot.1 < line_chars {
5164 Position::new(bot.0, bot.1 + 1)
5165 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5166 Position::new(bot.0 + 1, 0)
5167 } else {
5168 Position::new(bot.0, line_chars)
5169 };
5170 (Position::new(top.0, top.1), next, BufKind::Char)
5171 }
5172 MotionKind::Exclusive => (
5173 Position::new(top.0, top.1),
5174 Position::new(bot.0, bot.1),
5175 BufKind::Char,
5176 ),
5177 };
5178 let inverse = ed.mutate_edit(Edit::DeleteRange {
5179 start: buf_start,
5180 end: buf_end,
5181 kind: buf_kind,
5182 });
5183 let text = match inverse {
5184 Edit::InsertStr { text, .. } => text,
5185 _ => String::new(),
5186 };
5187 if !text.is_empty() {
5188 ed.record_yank_to_host(text.clone());
5189 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5190 }
5191 ed.push_buffer_cursor_to_textarea();
5192 text
5193}
5194
5195fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5201 use hjkl_buffer::{Edit, MotionKind, Position};
5202 ed.sync_buffer_content_from_textarea();
5203 let cursor = buf_cursor_pos(&ed.buffer);
5204 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5205 if cursor.col >= line_chars {
5206 return;
5207 }
5208 let inverse = ed.mutate_edit(Edit::DeleteRange {
5209 start: cursor,
5210 end: Position::new(cursor.row, line_chars),
5211 kind: MotionKind::Char,
5212 });
5213 if let Edit::InsertStr { text, .. } = inverse
5214 && !text.is_empty()
5215 {
5216 ed.record_yank_to_host(text.clone());
5217 ed.vim.yank_linewise = false;
5218 ed.set_yank(text);
5219 }
5220 buf_set_cursor_pos(&mut ed.buffer, cursor);
5221 ed.push_buffer_cursor_to_textarea();
5222}
5223
5224fn do_char_delete<H: crate::types::Host>(
5225 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5226 forward: bool,
5227 count: usize,
5228) {
5229 use hjkl_buffer::{Edit, MotionKind, Position};
5230 ed.push_undo();
5231 ed.sync_buffer_content_from_textarea();
5232 let mut deleted = String::new();
5235 for _ in 0..count {
5236 let cursor = buf_cursor_pos(&ed.buffer);
5237 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5238 if forward {
5239 if cursor.col >= line_chars {
5242 continue;
5243 }
5244 let inverse = ed.mutate_edit(Edit::DeleteRange {
5245 start: cursor,
5246 end: Position::new(cursor.row, cursor.col + 1),
5247 kind: MotionKind::Char,
5248 });
5249 if let Edit::InsertStr { text, .. } = inverse {
5250 deleted.push_str(&text);
5251 }
5252 } else {
5253 if cursor.col == 0 {
5255 continue;
5256 }
5257 let inverse = ed.mutate_edit(Edit::DeleteRange {
5258 start: Position::new(cursor.row, cursor.col - 1),
5259 end: cursor,
5260 kind: MotionKind::Char,
5261 });
5262 if let Edit::InsertStr { text, .. } = inverse {
5263 deleted = text + &deleted;
5266 }
5267 }
5268 }
5269 if !deleted.is_empty() {
5270 ed.record_yank_to_host(deleted.clone());
5271 ed.record_delete(deleted, false);
5272 }
5273 ed.push_buffer_cursor_to_textarea();
5274}
5275
5276fn adjust_number<H: crate::types::Host>(
5280 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5281 delta: i64,
5282) -> bool {
5283 use hjkl_buffer::{Edit, MotionKind, Position};
5284 ed.sync_buffer_content_from_textarea();
5285 let cursor = buf_cursor_pos(&ed.buffer);
5286 let row = cursor.row;
5287 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5288 Some(l) => l.chars().collect(),
5289 None => return false,
5290 };
5291 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5292 return false;
5293 };
5294 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5295 digit_start - 1
5296 } else {
5297 digit_start
5298 };
5299 let mut span_end = digit_start;
5300 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5301 span_end += 1;
5302 }
5303 let s: String = chars[span_start..span_end].iter().collect();
5304 let Ok(n) = s.parse::<i64>() else {
5305 return false;
5306 };
5307 let new_s = n.saturating_add(delta).to_string();
5308
5309 ed.push_undo();
5310 let span_start_pos = Position::new(row, span_start);
5311 let span_end_pos = Position::new(row, span_end);
5312 ed.mutate_edit(Edit::DeleteRange {
5313 start: span_start_pos,
5314 end: span_end_pos,
5315 kind: MotionKind::Char,
5316 });
5317 ed.mutate_edit(Edit::InsertStr {
5318 at: span_start_pos,
5319 text: new_s.clone(),
5320 });
5321 let new_len = new_s.chars().count();
5322 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5323 ed.push_buffer_cursor_to_textarea();
5324 true
5325}
5326
5327fn replace_char<H: crate::types::Host>(
5328 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5329 ch: char,
5330 count: usize,
5331) {
5332 use hjkl_buffer::{Edit, MotionKind, Position};
5333 ed.push_undo();
5334 ed.sync_buffer_content_from_textarea();
5335 for _ in 0..count {
5336 let cursor = buf_cursor_pos(&ed.buffer);
5337 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5338 if cursor.col >= line_chars {
5339 break;
5340 }
5341 ed.mutate_edit(Edit::DeleteRange {
5342 start: cursor,
5343 end: Position::new(cursor.row, cursor.col + 1),
5344 kind: MotionKind::Char,
5345 });
5346 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5347 }
5348 crate::motions::move_left(&mut ed.buffer, 1);
5350 ed.push_buffer_cursor_to_textarea();
5351}
5352
5353fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5354 use hjkl_buffer::{Edit, MotionKind, Position};
5355 ed.sync_buffer_content_from_textarea();
5356 let cursor = buf_cursor_pos(&ed.buffer);
5357 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5358 return;
5359 };
5360 let toggled = if c.is_uppercase() {
5361 c.to_lowercase().next().unwrap_or(c)
5362 } else {
5363 c.to_uppercase().next().unwrap_or(c)
5364 };
5365 ed.mutate_edit(Edit::DeleteRange {
5366 start: cursor,
5367 end: Position::new(cursor.row, cursor.col + 1),
5368 kind: MotionKind::Char,
5369 });
5370 ed.mutate_edit(Edit::InsertChar {
5371 at: cursor,
5372 ch: toggled,
5373 });
5374}
5375
5376fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5377 use hjkl_buffer::{Edit, Position};
5378 ed.sync_buffer_content_from_textarea();
5379 let row = buf_cursor_pos(&ed.buffer).row;
5380 if row + 1 >= buf_row_count(&ed.buffer) {
5381 return;
5382 }
5383 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5384 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5385 let next_trimmed = next_raw.trim_start();
5386 let cur_chars = cur_line.chars().count();
5387 let next_chars = next_raw.chars().count();
5388 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5391 " "
5392 } else {
5393 ""
5394 };
5395 let joined = format!("{cur_line}{separator}{next_trimmed}");
5396 ed.mutate_edit(Edit::Replace {
5397 start: Position::new(row, 0),
5398 end: Position::new(row + 1, next_chars),
5399 with: joined,
5400 });
5401 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5405 ed.push_buffer_cursor_to_textarea();
5406}
5407
5408fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5411 use hjkl_buffer::Edit;
5412 ed.sync_buffer_content_from_textarea();
5413 let row = buf_cursor_pos(&ed.buffer).row;
5414 if row + 1 >= buf_row_count(&ed.buffer) {
5415 return;
5416 }
5417 let join_col = buf_line_chars(&ed.buffer, row);
5418 ed.mutate_edit(Edit::JoinLines {
5419 row,
5420 count: 1,
5421 with_space: false,
5422 });
5423 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5425 ed.push_buffer_cursor_to_textarea();
5426}
5427
5428fn do_paste<H: crate::types::Host>(
5429 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5430 before: bool,
5431 count: usize,
5432) {
5433 use hjkl_buffer::{Edit, Position};
5434 ed.push_undo();
5435 let selector = ed.vim.pending_register.take();
5440 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5441 Some(slot) => (slot.text.clone(), slot.linewise),
5442 None => {
5448 let s = &ed.registers().unnamed;
5449 (s.text.clone(), s.linewise)
5450 }
5451 };
5452 for _ in 0..count {
5453 ed.sync_buffer_content_from_textarea();
5454 let yank = yank.clone();
5455 if yank.is_empty() {
5456 continue;
5457 }
5458 if linewise {
5459 let text = yank.trim_matches('\n').to_string();
5463 let row = buf_cursor_pos(&ed.buffer).row;
5464 let target_row = if before {
5465 ed.mutate_edit(Edit::InsertStr {
5466 at: Position::new(row, 0),
5467 text: format!("{text}\n"),
5468 });
5469 row
5470 } else {
5471 let line_chars = buf_line_chars(&ed.buffer, row);
5472 ed.mutate_edit(Edit::InsertStr {
5473 at: Position::new(row, line_chars),
5474 text: format!("\n{text}"),
5475 });
5476 row + 1
5477 };
5478 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5479 crate::motions::move_first_non_blank(&mut ed.buffer);
5480 ed.push_buffer_cursor_to_textarea();
5481 } else {
5482 let cursor = buf_cursor_pos(&ed.buffer);
5486 let at = if before {
5487 cursor
5488 } else {
5489 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5490 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5491 };
5492 ed.mutate_edit(Edit::InsertStr {
5493 at,
5494 text: yank.clone(),
5495 });
5496 crate::motions::move_left(&mut ed.buffer, 1);
5499 ed.push_buffer_cursor_to_textarea();
5500 }
5501 }
5502 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5504}
5505
5506pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5507 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5508 let current = ed.snapshot();
5509 ed.redo_stack.push(current);
5510 ed.restore(lines, cursor);
5511 }
5512 ed.vim.mode = Mode::Normal;
5513 clamp_cursor_to_normal_mode(ed);
5517}
5518
5519pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5520 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5521 let current = ed.snapshot();
5522 ed.undo_stack.push(current);
5523 ed.cap_undo();
5524 ed.restore(lines, cursor);
5525 }
5526 ed.vim.mode = Mode::Normal;
5527}
5528
5529fn replay_insert_and_finish<H: crate::types::Host>(
5536 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5537 text: &str,
5538) {
5539 use hjkl_buffer::{Edit, Position};
5540 let cursor = ed.cursor();
5541 ed.mutate_edit(Edit::InsertStr {
5542 at: Position::new(cursor.0, cursor.1),
5543 text: text.to_string(),
5544 });
5545 if ed.vim.insert_session.take().is_some() {
5546 if ed.cursor().1 > 0 {
5547 crate::motions::move_left(&mut ed.buffer, 1);
5548 ed.push_buffer_cursor_to_textarea();
5549 }
5550 ed.vim.mode = Mode::Normal;
5551 }
5552}
5553
5554fn replay_last_change<H: crate::types::Host>(
5555 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5556 outer_count: usize,
5557) {
5558 let Some(change) = ed.vim.last_change.clone() else {
5559 return;
5560 };
5561 ed.vim.replaying = true;
5562 let scale = if outer_count > 0 { outer_count } else { 1 };
5563 match change {
5564 LastChange::OpMotion {
5565 op,
5566 motion,
5567 count,
5568 inserted,
5569 } => {
5570 let total = count.max(1) * scale;
5571 apply_op_with_motion(ed, op, &motion, total);
5572 if let Some(text) = inserted {
5573 replay_insert_and_finish(ed, &text);
5574 }
5575 }
5576 LastChange::OpTextObj {
5577 op,
5578 obj,
5579 inner,
5580 inserted,
5581 } => {
5582 apply_op_with_text_object(ed, op, obj, inner);
5583 if let Some(text) = inserted {
5584 replay_insert_and_finish(ed, &text);
5585 }
5586 }
5587 LastChange::LineOp {
5588 op,
5589 count,
5590 inserted,
5591 } => {
5592 let total = count.max(1) * scale;
5593 execute_line_op(ed, op, total);
5594 if let Some(text) = inserted {
5595 replay_insert_and_finish(ed, &text);
5596 }
5597 }
5598 LastChange::CharDel { forward, count } => {
5599 do_char_delete(ed, forward, count * scale);
5600 }
5601 LastChange::ReplaceChar { ch, count } => {
5602 replace_char(ed, ch, count * scale);
5603 }
5604 LastChange::ToggleCase { count } => {
5605 for _ in 0..count * scale {
5606 ed.push_undo();
5607 toggle_case_at_cursor(ed);
5608 }
5609 }
5610 LastChange::JoinLine { count } => {
5611 for _ in 0..count * scale {
5612 ed.push_undo();
5613 join_line(ed);
5614 }
5615 }
5616 LastChange::Paste { before, count } => {
5617 do_paste(ed, before, count * scale);
5618 }
5619 LastChange::DeleteToEol { inserted } => {
5620 use hjkl_buffer::{Edit, Position};
5621 ed.push_undo();
5622 delete_to_eol(ed);
5623 if let Some(text) = inserted {
5624 let cursor = ed.cursor();
5625 ed.mutate_edit(Edit::InsertStr {
5626 at: Position::new(cursor.0, cursor.1),
5627 text,
5628 });
5629 }
5630 }
5631 LastChange::OpenLine { above, inserted } => {
5632 use hjkl_buffer::{Edit, Position};
5633 ed.push_undo();
5634 ed.sync_buffer_content_from_textarea();
5635 let row = buf_cursor_pos(&ed.buffer).row;
5636 if above {
5637 ed.mutate_edit(Edit::InsertStr {
5638 at: Position::new(row, 0),
5639 text: "\n".to_string(),
5640 });
5641 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5642 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5643 } else {
5644 let line_chars = buf_line_chars(&ed.buffer, row);
5645 ed.mutate_edit(Edit::InsertStr {
5646 at: Position::new(row, line_chars),
5647 text: "\n".to_string(),
5648 });
5649 }
5650 ed.push_buffer_cursor_to_textarea();
5651 let cursor = ed.cursor();
5652 ed.mutate_edit(Edit::InsertStr {
5653 at: Position::new(cursor.0, cursor.1),
5654 text: inserted,
5655 });
5656 }
5657 LastChange::InsertAt {
5658 entry,
5659 inserted,
5660 count,
5661 } => {
5662 use hjkl_buffer::{Edit, Position};
5663 ed.push_undo();
5664 match entry {
5665 InsertEntry::I => {}
5666 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5667 InsertEntry::A => {
5668 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5669 ed.push_buffer_cursor_to_textarea();
5670 }
5671 InsertEntry::ShiftA => {
5672 crate::motions::move_line_end(&mut ed.buffer);
5673 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5674 ed.push_buffer_cursor_to_textarea();
5675 }
5676 }
5677 for _ in 0..count.max(1) {
5678 let cursor = ed.cursor();
5679 ed.mutate_edit(Edit::InsertStr {
5680 at: Position::new(cursor.0, cursor.1),
5681 text: inserted.clone(),
5682 });
5683 }
5684 }
5685 }
5686 ed.vim.replaying = false;
5687}
5688
5689fn extract_inserted(before: &str, after: &str) -> String {
5692 let before_chars: Vec<char> = before.chars().collect();
5693 let after_chars: Vec<char> = after.chars().collect();
5694 if after_chars.len() <= before_chars.len() {
5695 return String::new();
5696 }
5697 let prefix = before_chars
5698 .iter()
5699 .zip(after_chars.iter())
5700 .take_while(|(a, b)| a == b)
5701 .count();
5702 let max_suffix = before_chars.len() - prefix;
5703 let suffix = before_chars
5704 .iter()
5705 .rev()
5706 .zip(after_chars.iter().rev())
5707 .take(max_suffix)
5708 .take_while(|(a, b)| a == b)
5709 .count();
5710 after_chars[prefix..after_chars.len() - suffix]
5711 .iter()
5712 .collect()
5713}
5714
5715#[cfg(all(test, feature = "crossterm"))]
5718mod tests {
5719 use crate::VimMode;
5720 use crate::editor::Editor;
5721 use crate::types::Host;
5722 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5723
5724 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5725 let mut iter = keys.chars().peekable();
5729 while let Some(c) = iter.next() {
5730 if c == '<' {
5731 let mut tag = String::new();
5732 for ch in iter.by_ref() {
5733 if ch == '>' {
5734 break;
5735 }
5736 tag.push(ch);
5737 }
5738 let ev = match tag.as_str() {
5739 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5740 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5741 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5742 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5743 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5744 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5745 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5746 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5747 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5751 s if s.starts_with("C-") => {
5752 let ch = s.chars().nth(2).unwrap();
5753 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5754 }
5755 _ => continue,
5756 };
5757 e.handle_key(ev);
5758 } else {
5759 let mods = if c.is_uppercase() {
5760 KeyModifiers::SHIFT
5761 } else {
5762 KeyModifiers::NONE
5763 };
5764 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5765 }
5766 }
5767 }
5768
5769 fn editor_with(content: &str) -> Editor {
5770 let opts = crate::types::Options {
5775 shiftwidth: 2,
5776 ..crate::types::Options::default()
5777 };
5778 let mut e = Editor::new(
5779 hjkl_buffer::Buffer::new(),
5780 crate::types::DefaultHost::new(),
5781 opts,
5782 );
5783 e.set_content(content);
5784 e
5785 }
5786
5787 #[test]
5788 fn f_char_jumps_on_line() {
5789 let mut e = editor_with("hello world");
5790 run_keys(&mut e, "fw");
5791 assert_eq!(e.cursor(), (0, 6));
5792 }
5793
5794 #[test]
5795 fn cap_f_jumps_backward() {
5796 let mut e = editor_with("hello world");
5797 e.jump_cursor(0, 10);
5798 run_keys(&mut e, "Fo");
5799 assert_eq!(e.cursor().1, 7);
5800 }
5801
5802 #[test]
5803 fn t_stops_before_char() {
5804 let mut e = editor_with("hello");
5805 run_keys(&mut e, "tl");
5806 assert_eq!(e.cursor(), (0, 1));
5807 }
5808
5809 #[test]
5810 fn semicolon_repeats_find() {
5811 let mut e = editor_with("aa.bb.cc");
5812 run_keys(&mut e, "f.");
5813 assert_eq!(e.cursor().1, 2);
5814 run_keys(&mut e, ";");
5815 assert_eq!(e.cursor().1, 5);
5816 }
5817
5818 #[test]
5819 fn comma_repeats_find_reverse() {
5820 let mut e = editor_with("aa.bb.cc");
5821 run_keys(&mut e, "f.");
5822 run_keys(&mut e, ";");
5823 run_keys(&mut e, ",");
5824 assert_eq!(e.cursor().1, 2);
5825 }
5826
5827 #[test]
5828 fn di_quote_deletes_content() {
5829 let mut e = editor_with("foo \"bar\" baz");
5830 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5832 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5833 }
5834
5835 #[test]
5836 fn da_quote_deletes_with_quotes() {
5837 let mut e = editor_with("foo \"bar\" baz");
5840 e.jump_cursor(0, 6);
5841 run_keys(&mut e, "da\"");
5842 assert_eq!(e.buffer().lines()[0], "foo baz");
5843 }
5844
5845 #[test]
5846 fn ci_paren_deletes_and_inserts() {
5847 let mut e = editor_with("fn(a, b, c)");
5848 e.jump_cursor(0, 5);
5849 run_keys(&mut e, "ci(");
5850 assert_eq!(e.vim_mode(), VimMode::Insert);
5851 assert_eq!(e.buffer().lines()[0], "fn()");
5852 }
5853
5854 #[test]
5855 fn diw_deletes_inner_word() {
5856 let mut e = editor_with("hello world");
5857 e.jump_cursor(0, 2);
5858 run_keys(&mut e, "diw");
5859 assert_eq!(e.buffer().lines()[0], " world");
5860 }
5861
5862 #[test]
5863 fn daw_deletes_word_with_trailing_space() {
5864 let mut e = editor_with("hello world");
5865 run_keys(&mut e, "daw");
5866 assert_eq!(e.buffer().lines()[0], "world");
5867 }
5868
5869 #[test]
5870 fn percent_jumps_to_matching_bracket() {
5871 let mut e = editor_with("foo(bar)");
5872 e.jump_cursor(0, 3);
5873 run_keys(&mut e, "%");
5874 assert_eq!(e.cursor().1, 7);
5875 run_keys(&mut e, "%");
5876 assert_eq!(e.cursor().1, 3);
5877 }
5878
5879 #[test]
5880 fn dot_repeats_last_change() {
5881 let mut e = editor_with("aaa bbb ccc");
5882 run_keys(&mut e, "dw");
5883 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5884 run_keys(&mut e, ".");
5885 assert_eq!(e.buffer().lines()[0], "ccc");
5886 }
5887
5888 #[test]
5889 fn dot_repeats_change_operator_with_text() {
5890 let mut e = editor_with("foo foo foo");
5891 run_keys(&mut e, "cwbar<Esc>");
5892 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5893 run_keys(&mut e, "w");
5895 run_keys(&mut e, ".");
5896 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5897 }
5898
5899 #[test]
5900 fn dot_repeats_x() {
5901 let mut e = editor_with("abcdef");
5902 run_keys(&mut e, "x");
5903 run_keys(&mut e, "..");
5904 assert_eq!(e.buffer().lines()[0], "def");
5905 }
5906
5907 #[test]
5908 fn count_operator_motion_compose() {
5909 let mut e = editor_with("one two three four five");
5910 run_keys(&mut e, "d3w");
5911 assert_eq!(e.buffer().lines()[0], "four five");
5912 }
5913
5914 #[test]
5915 fn two_dd_deletes_two_lines() {
5916 let mut e = editor_with("a\nb\nc");
5917 run_keys(&mut e, "2dd");
5918 assert_eq!(e.buffer().lines().len(), 1);
5919 assert_eq!(e.buffer().lines()[0], "c");
5920 }
5921
5922 #[test]
5927 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5928 let mut e = editor_with("one\ntwo\n three\nfour");
5929 e.jump_cursor(1, 2);
5930 run_keys(&mut e, "dd");
5931 assert_eq!(e.buffer().lines()[1], " three");
5933 assert_eq!(e.cursor(), (1, 4));
5934 }
5935
5936 #[test]
5937 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5938 let mut e = editor_with("one\n two\nthree");
5939 e.jump_cursor(2, 0);
5940 run_keys(&mut e, "dd");
5941 assert_eq!(e.buffer().lines().len(), 2);
5943 assert_eq!(e.cursor(), (1, 2));
5944 }
5945
5946 #[test]
5947 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5948 let mut e = editor_with("lonely");
5949 run_keys(&mut e, "dd");
5950 assert_eq!(e.buffer().lines().len(), 1);
5951 assert_eq!(e.buffer().lines()[0], "");
5952 assert_eq!(e.cursor(), (0, 0));
5953 }
5954
5955 #[test]
5956 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5957 let mut e = editor_with("a\nb\nc\n d\ne");
5958 e.jump_cursor(1, 0);
5960 run_keys(&mut e, "3dd");
5961 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
5962 assert_eq!(e.cursor(), (1, 0));
5963 }
5964
5965 #[test]
5966 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
5967 let mut e = editor_with(" line one\n line two\n xyz!");
5986 e.jump_cursor(0, 8);
5988 assert_eq!(e.cursor(), (0, 8));
5989 run_keys(&mut e, "dd");
5992 assert_eq!(
5993 e.cursor(),
5994 (0, 4),
5995 "dd must place cursor on first-non-blank"
5996 );
5997 run_keys(&mut e, "j");
6001 let (row, col) = e.cursor();
6002 assert_eq!(row, 1);
6003 assert_eq!(
6004 col, 4,
6005 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6006 );
6007 }
6008
6009 #[test]
6010 fn gu_lowercases_motion_range() {
6011 let mut e = editor_with("HELLO WORLD");
6012 run_keys(&mut e, "guw");
6013 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6014 assert_eq!(e.cursor(), (0, 0));
6015 }
6016
6017 #[test]
6018 fn g_u_uppercases_text_object() {
6019 let mut e = editor_with("hello world");
6020 run_keys(&mut e, "gUiw");
6022 assert_eq!(e.buffer().lines()[0], "HELLO world");
6023 assert_eq!(e.cursor(), (0, 0));
6024 }
6025
6026 #[test]
6027 fn g_tilde_toggles_case_of_range() {
6028 let mut e = editor_with("Hello World");
6029 run_keys(&mut e, "g~iw");
6030 assert_eq!(e.buffer().lines()[0], "hELLO World");
6031 }
6032
6033 #[test]
6034 fn g_uu_uppercases_current_line() {
6035 let mut e = editor_with("select 1\nselect 2");
6036 run_keys(&mut e, "gUU");
6037 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6038 assert_eq!(e.buffer().lines()[1], "select 2");
6039 }
6040
6041 #[test]
6042 fn gugu_lowercases_current_line() {
6043 let mut e = editor_with("FOO BAR\nBAZ");
6044 run_keys(&mut e, "gugu");
6045 assert_eq!(e.buffer().lines()[0], "foo bar");
6046 }
6047
6048 #[test]
6049 fn visual_u_uppercases_selection() {
6050 let mut e = editor_with("hello world");
6051 run_keys(&mut e, "veU");
6053 assert_eq!(e.buffer().lines()[0], "HELLO world");
6054 }
6055
6056 #[test]
6057 fn visual_line_u_lowercases_line() {
6058 let mut e = editor_with("HELLO WORLD\nOTHER");
6059 run_keys(&mut e, "Vu");
6060 assert_eq!(e.buffer().lines()[0], "hello world");
6061 assert_eq!(e.buffer().lines()[1], "OTHER");
6062 }
6063
6064 #[test]
6065 fn g_uu_with_count_uppercases_multiple_lines() {
6066 let mut e = editor_with("one\ntwo\nthree\nfour");
6067 run_keys(&mut e, "3gUU");
6069 assert_eq!(e.buffer().lines()[0], "ONE");
6070 assert_eq!(e.buffer().lines()[1], "TWO");
6071 assert_eq!(e.buffer().lines()[2], "THREE");
6072 assert_eq!(e.buffer().lines()[3], "four");
6073 }
6074
6075 #[test]
6076 fn double_gt_indents_current_line() {
6077 let mut e = editor_with("hello");
6078 run_keys(&mut e, ">>");
6079 assert_eq!(e.buffer().lines()[0], " hello");
6080 assert_eq!(e.cursor(), (0, 2));
6082 }
6083
6084 #[test]
6085 fn double_lt_outdents_current_line() {
6086 let mut e = editor_with(" hello");
6087 run_keys(&mut e, "<lt><lt>");
6088 assert_eq!(e.buffer().lines()[0], " hello");
6089 assert_eq!(e.cursor(), (0, 2));
6090 }
6091
6092 #[test]
6093 fn count_double_gt_indents_multiple_lines() {
6094 let mut e = editor_with("a\nb\nc\nd");
6095 run_keys(&mut e, "3>>");
6097 assert_eq!(e.buffer().lines()[0], " a");
6098 assert_eq!(e.buffer().lines()[1], " b");
6099 assert_eq!(e.buffer().lines()[2], " c");
6100 assert_eq!(e.buffer().lines()[3], "d");
6101 }
6102
6103 #[test]
6104 fn outdent_clips_ragged_leading_whitespace() {
6105 let mut e = editor_with(" x");
6108 run_keys(&mut e, "<lt><lt>");
6109 assert_eq!(e.buffer().lines()[0], "x");
6110 }
6111
6112 #[test]
6113 fn indent_motion_is_always_linewise() {
6114 let mut e = editor_with("foo bar");
6117 run_keys(&mut e, ">w");
6118 assert_eq!(e.buffer().lines()[0], " foo bar");
6119 }
6120
6121 #[test]
6122 fn indent_text_object_extends_over_paragraph() {
6123 let mut e = editor_with("a\nb\n\nc\nd");
6124 run_keys(&mut e, ">ap");
6126 assert_eq!(e.buffer().lines()[0], " a");
6127 assert_eq!(e.buffer().lines()[1], " b");
6128 assert_eq!(e.buffer().lines()[2], "");
6129 assert_eq!(e.buffer().lines()[3], "c");
6130 }
6131
6132 #[test]
6133 fn visual_line_indent_shifts_selected_rows() {
6134 let mut e = editor_with("x\ny\nz");
6135 run_keys(&mut e, "Vj>");
6137 assert_eq!(e.buffer().lines()[0], " x");
6138 assert_eq!(e.buffer().lines()[1], " y");
6139 assert_eq!(e.buffer().lines()[2], "z");
6140 }
6141
6142 #[test]
6143 fn outdent_empty_line_is_noop() {
6144 let mut e = editor_with("\nfoo");
6145 run_keys(&mut e, "<lt><lt>");
6146 assert_eq!(e.buffer().lines()[0], "");
6147 }
6148
6149 #[test]
6150 fn indent_skips_empty_lines() {
6151 let mut e = editor_with("");
6154 run_keys(&mut e, ">>");
6155 assert_eq!(e.buffer().lines()[0], "");
6156 }
6157
6158 #[test]
6159 fn insert_ctrl_t_indents_current_line() {
6160 let mut e = editor_with("x");
6161 run_keys(&mut e, "i<C-t>");
6163 assert_eq!(e.buffer().lines()[0], " x");
6164 assert_eq!(e.cursor(), (0, 2));
6167 }
6168
6169 #[test]
6170 fn insert_ctrl_d_outdents_current_line() {
6171 let mut e = editor_with(" x");
6172 run_keys(&mut e, "A<C-d>");
6174 assert_eq!(e.buffer().lines()[0], " x");
6175 }
6176
6177 #[test]
6178 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6179 let mut e = editor_with("first\nsecond");
6180 e.jump_cursor(1, 0);
6181 run_keys(&mut e, "h");
6182 assert_eq!(e.cursor(), (1, 0));
6184 }
6185
6186 #[test]
6187 fn l_at_last_char_does_not_wrap_to_next_line() {
6188 let mut e = editor_with("ab\ncd");
6189 e.jump_cursor(0, 1);
6191 run_keys(&mut e, "l");
6192 assert_eq!(e.cursor(), (0, 1));
6194 }
6195
6196 #[test]
6197 fn count_l_clamps_at_line_end() {
6198 let mut e = editor_with("abcde");
6199 run_keys(&mut e, "20l");
6202 assert_eq!(e.cursor(), (0, 4));
6203 }
6204
6205 #[test]
6206 fn count_h_clamps_at_col_zero() {
6207 let mut e = editor_with("abcde");
6208 e.jump_cursor(0, 3);
6209 run_keys(&mut e, "20h");
6210 assert_eq!(e.cursor(), (0, 0));
6211 }
6212
6213 #[test]
6214 fn dl_on_last_char_still_deletes_it() {
6215 let mut e = editor_with("ab");
6219 e.jump_cursor(0, 1);
6220 run_keys(&mut e, "dl");
6221 assert_eq!(e.buffer().lines()[0], "a");
6222 }
6223
6224 #[test]
6225 fn case_op_preserves_yank_register() {
6226 let mut e = editor_with("target");
6227 run_keys(&mut e, "yy");
6228 let yank_before = e.yank().to_string();
6229 run_keys(&mut e, "gUU");
6231 assert_eq!(e.buffer().lines()[0], "TARGET");
6232 assert_eq!(
6233 e.yank(),
6234 yank_before,
6235 "case ops must preserve the yank buffer"
6236 );
6237 }
6238
6239 #[test]
6240 fn dap_deletes_paragraph() {
6241 let mut e = editor_with("a\nb\n\nc\nd");
6242 run_keys(&mut e, "dap");
6243 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6244 }
6245
6246 #[test]
6247 fn dit_deletes_inner_tag_content() {
6248 let mut e = editor_with("<b>hello</b>");
6249 e.jump_cursor(0, 4);
6251 run_keys(&mut e, "dit");
6252 assert_eq!(e.buffer().lines()[0], "<b></b>");
6253 }
6254
6255 #[test]
6256 fn dat_deletes_around_tag() {
6257 let mut e = editor_with("hi <b>foo</b> bye");
6258 e.jump_cursor(0, 6);
6259 run_keys(&mut e, "dat");
6260 assert_eq!(e.buffer().lines()[0], "hi bye");
6261 }
6262
6263 #[test]
6264 fn dit_picks_innermost_tag() {
6265 let mut e = editor_with("<a><b>x</b></a>");
6266 e.jump_cursor(0, 6);
6268 run_keys(&mut e, "dit");
6269 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6271 }
6272
6273 #[test]
6274 fn dat_innermost_tag_pair() {
6275 let mut e = editor_with("<a><b>x</b></a>");
6276 e.jump_cursor(0, 6);
6277 run_keys(&mut e, "dat");
6278 assert_eq!(e.buffer().lines()[0], "<a></a>");
6279 }
6280
6281 #[test]
6282 fn dit_outside_any_tag_no_op() {
6283 let mut e = editor_with("plain text");
6284 e.jump_cursor(0, 3);
6285 run_keys(&mut e, "dit");
6286 assert_eq!(e.buffer().lines()[0], "plain text");
6288 }
6289
6290 #[test]
6291 fn cit_changes_inner_tag_content() {
6292 let mut e = editor_with("<b>hello</b>");
6293 e.jump_cursor(0, 4);
6294 run_keys(&mut e, "citNEW<Esc>");
6295 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6296 }
6297
6298 #[test]
6299 fn cat_changes_around_tag() {
6300 let mut e = editor_with("hi <b>foo</b> bye");
6301 e.jump_cursor(0, 6);
6302 run_keys(&mut e, "catBAR<Esc>");
6303 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6304 }
6305
6306 #[test]
6307 fn yit_yanks_inner_tag_content() {
6308 let mut e = editor_with("<b>hello</b>");
6309 e.jump_cursor(0, 4);
6310 run_keys(&mut e, "yit");
6311 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6312 }
6313
6314 #[test]
6315 fn yat_yanks_full_tag_pair() {
6316 let mut e = editor_with("hi <b>foo</b> bye");
6317 e.jump_cursor(0, 6);
6318 run_keys(&mut e, "yat");
6319 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6320 }
6321
6322 #[test]
6323 fn vit_visually_selects_inner_tag() {
6324 let mut e = editor_with("<b>hello</b>");
6325 e.jump_cursor(0, 4);
6326 run_keys(&mut e, "vit");
6327 assert_eq!(e.vim_mode(), VimMode::Visual);
6328 run_keys(&mut e, "y");
6329 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6330 }
6331
6332 #[test]
6333 fn vat_visually_selects_around_tag() {
6334 let mut e = editor_with("x<b>foo</b>y");
6335 e.jump_cursor(0, 5);
6336 run_keys(&mut e, "vat");
6337 assert_eq!(e.vim_mode(), VimMode::Visual);
6338 run_keys(&mut e, "y");
6339 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6340 }
6341
6342 #[test]
6345 #[allow(non_snake_case)]
6346 fn diW_deletes_inner_big_word() {
6347 let mut e = editor_with("foo.bar baz");
6348 e.jump_cursor(0, 2);
6349 run_keys(&mut e, "diW");
6350 assert_eq!(e.buffer().lines()[0], " baz");
6352 }
6353
6354 #[test]
6355 #[allow(non_snake_case)]
6356 fn daW_deletes_around_big_word() {
6357 let mut e = editor_with("foo.bar baz");
6358 e.jump_cursor(0, 2);
6359 run_keys(&mut e, "daW");
6360 assert_eq!(e.buffer().lines()[0], "baz");
6361 }
6362
6363 #[test]
6364 fn di_double_quote_deletes_inside() {
6365 let mut e = editor_with("a \"hello\" b");
6366 e.jump_cursor(0, 4);
6367 run_keys(&mut e, "di\"");
6368 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6369 }
6370
6371 #[test]
6372 fn da_double_quote_deletes_around() {
6373 let mut e = editor_with("a \"hello\" b");
6375 e.jump_cursor(0, 4);
6376 run_keys(&mut e, "da\"");
6377 assert_eq!(e.buffer().lines()[0], "a b");
6378 }
6379
6380 #[test]
6381 fn di_single_quote_deletes_inside() {
6382 let mut e = editor_with("x 'foo' y");
6383 e.jump_cursor(0, 4);
6384 run_keys(&mut e, "di'");
6385 assert_eq!(e.buffer().lines()[0], "x '' y");
6386 }
6387
6388 #[test]
6389 fn da_single_quote_deletes_around() {
6390 let mut e = editor_with("x 'foo' y");
6392 e.jump_cursor(0, 4);
6393 run_keys(&mut e, "da'");
6394 assert_eq!(e.buffer().lines()[0], "x y");
6395 }
6396
6397 #[test]
6398 fn di_backtick_deletes_inside() {
6399 let mut e = editor_with("p `q` r");
6400 e.jump_cursor(0, 3);
6401 run_keys(&mut e, "di`");
6402 assert_eq!(e.buffer().lines()[0], "p `` r");
6403 }
6404
6405 #[test]
6406 fn da_backtick_deletes_around() {
6407 let mut e = editor_with("p `q` r");
6409 e.jump_cursor(0, 3);
6410 run_keys(&mut e, "da`");
6411 assert_eq!(e.buffer().lines()[0], "p r");
6412 }
6413
6414 #[test]
6415 fn di_paren_deletes_inside() {
6416 let mut e = editor_with("f(arg)");
6417 e.jump_cursor(0, 3);
6418 run_keys(&mut e, "di(");
6419 assert_eq!(e.buffer().lines()[0], "f()");
6420 }
6421
6422 #[test]
6423 fn di_paren_alias_b_works() {
6424 let mut e = editor_with("f(arg)");
6425 e.jump_cursor(0, 3);
6426 run_keys(&mut e, "dib");
6427 assert_eq!(e.buffer().lines()[0], "f()");
6428 }
6429
6430 #[test]
6431 fn di_bracket_deletes_inside() {
6432 let mut e = editor_with("a[b,c]d");
6433 e.jump_cursor(0, 3);
6434 run_keys(&mut e, "di[");
6435 assert_eq!(e.buffer().lines()[0], "a[]d");
6436 }
6437
6438 #[test]
6439 fn da_bracket_deletes_around() {
6440 let mut e = editor_with("a[b,c]d");
6441 e.jump_cursor(0, 3);
6442 run_keys(&mut e, "da[");
6443 assert_eq!(e.buffer().lines()[0], "ad");
6444 }
6445
6446 #[test]
6447 fn di_brace_deletes_inside() {
6448 let mut e = editor_with("x{y}z");
6449 e.jump_cursor(0, 2);
6450 run_keys(&mut e, "di{");
6451 assert_eq!(e.buffer().lines()[0], "x{}z");
6452 }
6453
6454 #[test]
6455 fn da_brace_deletes_around() {
6456 let mut e = editor_with("x{y}z");
6457 e.jump_cursor(0, 2);
6458 run_keys(&mut e, "da{");
6459 assert_eq!(e.buffer().lines()[0], "xz");
6460 }
6461
6462 #[test]
6463 fn di_brace_alias_capital_b_works() {
6464 let mut e = editor_with("x{y}z");
6465 e.jump_cursor(0, 2);
6466 run_keys(&mut e, "diB");
6467 assert_eq!(e.buffer().lines()[0], "x{}z");
6468 }
6469
6470 #[test]
6471 fn di_angle_deletes_inside() {
6472 let mut e = editor_with("p<q>r");
6473 e.jump_cursor(0, 2);
6474 run_keys(&mut e, "di<lt>");
6476 assert_eq!(e.buffer().lines()[0], "p<>r");
6477 }
6478
6479 #[test]
6480 fn da_angle_deletes_around() {
6481 let mut e = editor_with("p<q>r");
6482 e.jump_cursor(0, 2);
6483 run_keys(&mut e, "da<lt>");
6484 assert_eq!(e.buffer().lines()[0], "pr");
6485 }
6486
6487 #[test]
6488 fn dip_deletes_inner_paragraph() {
6489 let mut e = editor_with("a\nb\nc\n\nd");
6490 e.jump_cursor(1, 0);
6491 run_keys(&mut e, "dip");
6492 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6495 }
6496
6497 #[test]
6500 fn sentence_motion_close_paren_jumps_forward() {
6501 let mut e = editor_with("Alpha. Beta. Gamma.");
6502 e.jump_cursor(0, 0);
6503 run_keys(&mut e, ")");
6504 assert_eq!(e.cursor(), (0, 7));
6506 run_keys(&mut e, ")");
6507 assert_eq!(e.cursor(), (0, 13));
6508 }
6509
6510 #[test]
6511 fn sentence_motion_open_paren_jumps_backward() {
6512 let mut e = editor_with("Alpha. Beta. Gamma.");
6513 e.jump_cursor(0, 13);
6514 run_keys(&mut e, "(");
6515 assert_eq!(e.cursor(), (0, 7));
6518 run_keys(&mut e, "(");
6519 assert_eq!(e.cursor(), (0, 0));
6520 }
6521
6522 #[test]
6523 fn sentence_motion_count() {
6524 let mut e = editor_with("A. B. C. D.");
6525 e.jump_cursor(0, 0);
6526 run_keys(&mut e, "3)");
6527 assert_eq!(e.cursor(), (0, 9));
6529 }
6530
6531 #[test]
6532 fn dis_deletes_inner_sentence() {
6533 let mut e = editor_with("First one. Second one. Third one.");
6534 e.jump_cursor(0, 13);
6535 run_keys(&mut e, "dis");
6536 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6538 }
6539
6540 #[test]
6541 fn das_deletes_around_sentence_with_trailing_space() {
6542 let mut e = editor_with("Alpha. Beta. Gamma.");
6543 e.jump_cursor(0, 8);
6544 run_keys(&mut e, "das");
6545 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6548 }
6549
6550 #[test]
6551 fn dis_handles_double_terminator() {
6552 let mut e = editor_with("Wow!? Next.");
6553 e.jump_cursor(0, 1);
6554 run_keys(&mut e, "dis");
6555 assert_eq!(e.buffer().lines()[0], " Next.");
6558 }
6559
6560 #[test]
6561 fn dis_first_sentence_from_cursor_at_zero() {
6562 let mut e = editor_with("Alpha. Beta.");
6563 e.jump_cursor(0, 0);
6564 run_keys(&mut e, "dis");
6565 assert_eq!(e.buffer().lines()[0], " Beta.");
6566 }
6567
6568 #[test]
6569 fn yis_yanks_inner_sentence() {
6570 let mut e = editor_with("Hello world. Bye.");
6571 e.jump_cursor(0, 5);
6572 run_keys(&mut e, "yis");
6573 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6574 }
6575
6576 #[test]
6577 fn vis_visually_selects_inner_sentence() {
6578 let mut e = editor_with("First. Second.");
6579 e.jump_cursor(0, 1);
6580 run_keys(&mut e, "vis");
6581 assert_eq!(e.vim_mode(), VimMode::Visual);
6582 run_keys(&mut e, "y");
6583 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6584 }
6585
6586 #[test]
6587 fn ciw_changes_inner_word() {
6588 let mut e = editor_with("hello world");
6589 e.jump_cursor(0, 1);
6590 run_keys(&mut e, "ciwHEY<Esc>");
6591 assert_eq!(e.buffer().lines()[0], "HEY world");
6592 }
6593
6594 #[test]
6595 fn yiw_yanks_inner_word() {
6596 let mut e = editor_with("hello world");
6597 e.jump_cursor(0, 1);
6598 run_keys(&mut e, "yiw");
6599 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6600 }
6601
6602 #[test]
6603 fn viw_selects_inner_word() {
6604 let mut e = editor_with("hello world");
6605 e.jump_cursor(0, 2);
6606 run_keys(&mut e, "viw");
6607 assert_eq!(e.vim_mode(), VimMode::Visual);
6608 run_keys(&mut e, "y");
6609 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6610 }
6611
6612 #[test]
6613 fn ci_paren_changes_inside() {
6614 let mut e = editor_with("f(old)");
6615 e.jump_cursor(0, 3);
6616 run_keys(&mut e, "ci(NEW<Esc>");
6617 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6618 }
6619
6620 #[test]
6621 fn yi_double_quote_yanks_inside() {
6622 let mut e = editor_with("say \"hi there\" then");
6623 e.jump_cursor(0, 6);
6624 run_keys(&mut e, "yi\"");
6625 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6626 }
6627
6628 #[test]
6629 fn vap_visual_selects_around_paragraph() {
6630 let mut e = editor_with("a\nb\n\nc");
6631 e.jump_cursor(0, 0);
6632 run_keys(&mut e, "vap");
6633 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6634 run_keys(&mut e, "y");
6635 let text = e.registers().read('"').unwrap().text.clone();
6637 assert!(text.starts_with("a\nb"));
6638 }
6639
6640 #[test]
6641 fn star_finds_next_occurrence() {
6642 let mut e = editor_with("foo bar foo baz");
6643 run_keys(&mut e, "*");
6644 assert_eq!(e.cursor().1, 8);
6645 }
6646
6647 #[test]
6648 fn star_skips_substring_match() {
6649 let mut e = editor_with("foo foobar baz");
6652 run_keys(&mut e, "*");
6653 assert_eq!(e.cursor().1, 0);
6654 }
6655
6656 #[test]
6657 fn g_star_matches_substring() {
6658 let mut e = editor_with("foo foobar baz");
6661 run_keys(&mut e, "g*");
6662 assert_eq!(e.cursor().1, 4);
6663 }
6664
6665 #[test]
6666 fn g_pound_matches_substring_backward() {
6667 let mut e = editor_with("foo foobar baz foo");
6670 run_keys(&mut e, "$b");
6671 assert_eq!(e.cursor().1, 15);
6672 run_keys(&mut e, "g#");
6673 assert_eq!(e.cursor().1, 4);
6674 }
6675
6676 #[test]
6677 fn n_repeats_last_search_forward() {
6678 let mut e = editor_with("foo bar foo baz foo");
6679 run_keys(&mut e, "/foo<CR>");
6682 assert_eq!(e.cursor().1, 8);
6683 run_keys(&mut e, "n");
6684 assert_eq!(e.cursor().1, 16);
6685 }
6686
6687 #[test]
6688 fn shift_n_reverses_search() {
6689 let mut e = editor_with("foo bar foo baz foo");
6690 run_keys(&mut e, "/foo<CR>");
6691 run_keys(&mut e, "n");
6692 assert_eq!(e.cursor().1, 16);
6693 run_keys(&mut e, "N");
6694 assert_eq!(e.cursor().1, 8);
6695 }
6696
6697 #[test]
6698 fn n_noop_without_pattern() {
6699 let mut e = editor_with("foo bar");
6700 run_keys(&mut e, "n");
6701 assert_eq!(e.cursor(), (0, 0));
6702 }
6703
6704 #[test]
6705 fn visual_line_preserves_cursor_column() {
6706 let mut e = editor_with("hello world\nanother one\nbye");
6709 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6711 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6712 assert_eq!(e.cursor(), (0, 5));
6713 run_keys(&mut e, "j");
6714 assert_eq!(e.cursor(), (1, 5));
6715 }
6716
6717 #[test]
6718 fn visual_line_yank_includes_trailing_newline() {
6719 let mut e = editor_with("aaa\nbbb\nccc");
6720 run_keys(&mut e, "Vjy");
6721 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6723 }
6724
6725 #[test]
6726 fn visual_line_yank_last_line_trailing_newline() {
6727 let mut e = editor_with("aaa\nbbb\nccc");
6728 run_keys(&mut e, "jj");
6730 run_keys(&mut e, "Vy");
6731 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6732 }
6733
6734 #[test]
6735 fn yy_on_last_line_has_trailing_newline() {
6736 let mut e = editor_with("aaa\nbbb\nccc");
6737 run_keys(&mut e, "jj");
6738 run_keys(&mut e, "yy");
6739 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6740 }
6741
6742 #[test]
6743 fn yy_in_middle_has_trailing_newline() {
6744 let mut e = editor_with("aaa\nbbb\nccc");
6745 run_keys(&mut e, "j");
6746 run_keys(&mut e, "yy");
6747 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6748 }
6749
6750 #[test]
6751 fn di_single_quote() {
6752 let mut e = editor_with("say 'hello world' now");
6753 e.jump_cursor(0, 7);
6754 run_keys(&mut e, "di'");
6755 assert_eq!(e.buffer().lines()[0], "say '' now");
6756 }
6757
6758 #[test]
6759 fn da_single_quote() {
6760 let mut e = editor_with("say 'hello' now");
6762 e.jump_cursor(0, 7);
6763 run_keys(&mut e, "da'");
6764 assert_eq!(e.buffer().lines()[0], "say now");
6765 }
6766
6767 #[test]
6768 fn di_backtick() {
6769 let mut e = editor_with("say `hi` now");
6770 e.jump_cursor(0, 5);
6771 run_keys(&mut e, "di`");
6772 assert_eq!(e.buffer().lines()[0], "say `` now");
6773 }
6774
6775 #[test]
6776 fn di_brace() {
6777 let mut e = editor_with("fn { a; b; c }");
6778 e.jump_cursor(0, 7);
6779 run_keys(&mut e, "di{");
6780 assert_eq!(e.buffer().lines()[0], "fn {}");
6781 }
6782
6783 #[test]
6784 fn di_bracket() {
6785 let mut e = editor_with("arr[1, 2, 3]");
6786 e.jump_cursor(0, 5);
6787 run_keys(&mut e, "di[");
6788 assert_eq!(e.buffer().lines()[0], "arr[]");
6789 }
6790
6791 #[test]
6792 fn dab_deletes_around_paren() {
6793 let mut e = editor_with("fn(a, b) + 1");
6794 e.jump_cursor(0, 4);
6795 run_keys(&mut e, "dab");
6796 assert_eq!(e.buffer().lines()[0], "fn + 1");
6797 }
6798
6799 #[test]
6800 fn da_big_b_deletes_around_brace() {
6801 let mut e = editor_with("x = {a: 1}");
6802 e.jump_cursor(0, 6);
6803 run_keys(&mut e, "daB");
6804 assert_eq!(e.buffer().lines()[0], "x = ");
6805 }
6806
6807 #[test]
6808 fn di_big_w_deletes_bigword() {
6809 let mut e = editor_with("foo-bar baz");
6810 e.jump_cursor(0, 2);
6811 run_keys(&mut e, "diW");
6812 assert_eq!(e.buffer().lines()[0], " baz");
6813 }
6814
6815 #[test]
6816 fn visual_select_inner_word() {
6817 let mut e = editor_with("hello world");
6818 e.jump_cursor(0, 2);
6819 run_keys(&mut e, "viw");
6820 assert_eq!(e.vim_mode(), VimMode::Visual);
6821 run_keys(&mut e, "y");
6822 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6823 }
6824
6825 #[test]
6826 fn visual_select_inner_quote() {
6827 let mut e = editor_with("foo \"bar\" baz");
6828 e.jump_cursor(0, 6);
6829 run_keys(&mut e, "vi\"");
6830 run_keys(&mut e, "y");
6831 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6832 }
6833
6834 #[test]
6835 fn visual_select_inner_paren() {
6836 let mut e = editor_with("fn(a, b)");
6837 e.jump_cursor(0, 4);
6838 run_keys(&mut e, "vi(");
6839 run_keys(&mut e, "y");
6840 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6841 }
6842
6843 #[test]
6844 fn visual_select_outer_brace() {
6845 let mut e = editor_with("{x}");
6846 e.jump_cursor(0, 1);
6847 run_keys(&mut e, "va{");
6848 run_keys(&mut e, "y");
6849 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6850 }
6851
6852 #[test]
6853 fn ci_paren_forward_scans_when_cursor_before_pair() {
6854 let mut e = editor_with("foo(bar)");
6857 e.jump_cursor(0, 0);
6858 run_keys(&mut e, "ci(NEW<Esc>");
6859 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
6860 }
6861
6862 #[test]
6863 fn ci_paren_forward_scans_across_lines() {
6864 let mut e = editor_with("first\nfoo(bar)\nlast");
6865 e.jump_cursor(0, 0);
6866 run_keys(&mut e, "ci(NEW<Esc>");
6867 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
6868 }
6869
6870 #[test]
6871 fn ci_brace_forward_scans_when_cursor_before_pair() {
6872 let mut e = editor_with("let x = {y};");
6873 e.jump_cursor(0, 0);
6874 run_keys(&mut e, "ci{NEW<Esc>");
6875 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
6876 }
6877
6878 #[test]
6879 fn cit_forward_scans_when_cursor_before_tag() {
6880 let mut e = editor_with("text <b>hello</b> rest");
6883 e.jump_cursor(0, 0);
6884 run_keys(&mut e, "citNEW<Esc>");
6885 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
6886 }
6887
6888 #[test]
6889 fn dat_forward_scans_when_cursor_before_tag() {
6890 let mut e = editor_with("text <b>hello</b> rest");
6892 e.jump_cursor(0, 0);
6893 run_keys(&mut e, "dat");
6894 assert_eq!(e.buffer().lines()[0], "text rest");
6895 }
6896
6897 #[test]
6898 fn ci_paren_still_works_when_cursor_inside() {
6899 let mut e = editor_with("fn(a, b)");
6902 e.jump_cursor(0, 4);
6903 run_keys(&mut e, "ci(NEW<Esc>");
6904 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
6905 }
6906
6907 #[test]
6908 fn caw_changes_word_with_trailing_space() {
6909 let mut e = editor_with("hello world");
6910 run_keys(&mut e, "cawfoo<Esc>");
6911 assert_eq!(e.buffer().lines()[0], "fooworld");
6912 }
6913
6914 #[test]
6915 fn visual_char_yank_preserves_raw_text() {
6916 let mut e = editor_with("hello world");
6917 run_keys(&mut e, "vllly");
6918 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6919 }
6920
6921 #[test]
6922 fn single_line_visual_line_selects_full_line_on_yank() {
6923 let mut e = editor_with("hello world\nbye");
6924 run_keys(&mut e, "V");
6925 run_keys(&mut e, "y");
6928 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6929 }
6930
6931 #[test]
6932 fn visual_line_extends_both_directions() {
6933 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6934 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6936 assert_eq!(e.cursor(), (3, 0));
6937 run_keys(&mut e, "k");
6938 assert_eq!(e.cursor(), (2, 0));
6940 run_keys(&mut e, "k");
6941 assert_eq!(e.cursor(), (1, 0));
6942 }
6943
6944 #[test]
6945 fn visual_char_preserves_cursor_column() {
6946 let mut e = editor_with("hello world");
6947 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6949 assert_eq!(e.cursor(), (0, 5));
6950 run_keys(&mut e, "ll");
6951 assert_eq!(e.cursor(), (0, 7));
6952 }
6953
6954 #[test]
6955 fn visual_char_highlight_bounds_order() {
6956 let mut e = editor_with("abcdef");
6957 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
6959 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
6962 }
6963
6964 #[test]
6965 fn visual_line_highlight_bounds() {
6966 let mut e = editor_with("a\nb\nc");
6967 run_keys(&mut e, "V");
6968 assert_eq!(e.line_highlight(), Some((0, 0)));
6969 run_keys(&mut e, "j");
6970 assert_eq!(e.line_highlight(), Some((0, 1)));
6971 run_keys(&mut e, "j");
6972 assert_eq!(e.line_highlight(), Some((0, 2)));
6973 }
6974
6975 #[test]
6978 fn h_moves_left() {
6979 let mut e = editor_with("hello");
6980 e.jump_cursor(0, 3);
6981 run_keys(&mut e, "h");
6982 assert_eq!(e.cursor(), (0, 2));
6983 }
6984
6985 #[test]
6986 fn l_moves_right() {
6987 let mut e = editor_with("hello");
6988 run_keys(&mut e, "l");
6989 assert_eq!(e.cursor(), (0, 1));
6990 }
6991
6992 #[test]
6993 fn k_moves_up() {
6994 let mut e = editor_with("a\nb\nc");
6995 e.jump_cursor(2, 0);
6996 run_keys(&mut e, "k");
6997 assert_eq!(e.cursor(), (1, 0));
6998 }
6999
7000 #[test]
7001 fn zero_moves_to_line_start() {
7002 let mut e = editor_with(" hello");
7003 run_keys(&mut e, "$");
7004 run_keys(&mut e, "0");
7005 assert_eq!(e.cursor().1, 0);
7006 }
7007
7008 #[test]
7009 fn caret_moves_to_first_non_blank() {
7010 let mut e = editor_with(" hello");
7011 run_keys(&mut e, "0");
7012 run_keys(&mut e, "^");
7013 assert_eq!(e.cursor().1, 4);
7014 }
7015
7016 #[test]
7017 fn dollar_moves_to_last_char() {
7018 let mut e = editor_with("hello");
7019 run_keys(&mut e, "$");
7020 assert_eq!(e.cursor().1, 4);
7021 }
7022
7023 #[test]
7024 fn dollar_on_empty_line_stays_at_col_zero() {
7025 let mut e = editor_with("");
7026 run_keys(&mut e, "$");
7027 assert_eq!(e.cursor().1, 0);
7028 }
7029
7030 #[test]
7031 fn w_jumps_to_next_word() {
7032 let mut e = editor_with("foo bar baz");
7033 run_keys(&mut e, "w");
7034 assert_eq!(e.cursor().1, 4);
7035 }
7036
7037 #[test]
7038 fn b_jumps_back_a_word() {
7039 let mut e = editor_with("foo bar");
7040 e.jump_cursor(0, 6);
7041 run_keys(&mut e, "b");
7042 assert_eq!(e.cursor().1, 4);
7043 }
7044
7045 #[test]
7046 fn e_jumps_to_word_end() {
7047 let mut e = editor_with("foo bar");
7048 run_keys(&mut e, "e");
7049 assert_eq!(e.cursor().1, 2);
7050 }
7051
7052 #[test]
7055 fn d_dollar_deletes_to_eol() {
7056 let mut e = editor_with("hello world");
7057 e.jump_cursor(0, 5);
7058 run_keys(&mut e, "d$");
7059 assert_eq!(e.buffer().lines()[0], "hello");
7060 }
7061
7062 #[test]
7063 fn d_zero_deletes_to_line_start() {
7064 let mut e = editor_with("hello world");
7065 e.jump_cursor(0, 6);
7066 run_keys(&mut e, "d0");
7067 assert_eq!(e.buffer().lines()[0], "world");
7068 }
7069
7070 #[test]
7071 fn d_caret_deletes_to_first_non_blank() {
7072 let mut e = editor_with(" hello");
7073 e.jump_cursor(0, 6);
7074 run_keys(&mut e, "d^");
7075 assert_eq!(e.buffer().lines()[0], " llo");
7076 }
7077
7078 #[test]
7079 fn d_capital_g_deletes_to_end_of_file() {
7080 let mut e = editor_with("a\nb\nc\nd");
7081 e.jump_cursor(1, 0);
7082 run_keys(&mut e, "dG");
7083 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7084 }
7085
7086 #[test]
7087 fn d_gg_deletes_to_start_of_file() {
7088 let mut e = editor_with("a\nb\nc\nd");
7089 e.jump_cursor(2, 0);
7090 run_keys(&mut e, "dgg");
7091 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7092 }
7093
7094 #[test]
7095 fn cw_is_ce_quirk() {
7096 let mut e = editor_with("foo bar");
7099 run_keys(&mut e, "cwxyz<Esc>");
7100 assert_eq!(e.buffer().lines()[0], "xyz bar");
7101 }
7102
7103 #[test]
7106 fn big_d_deletes_to_eol() {
7107 let mut e = editor_with("hello world");
7108 e.jump_cursor(0, 5);
7109 run_keys(&mut e, "D");
7110 assert_eq!(e.buffer().lines()[0], "hello");
7111 }
7112
7113 #[test]
7114 fn big_c_deletes_to_eol_and_inserts() {
7115 let mut e = editor_with("hello world");
7116 e.jump_cursor(0, 5);
7117 run_keys(&mut e, "C!<Esc>");
7118 assert_eq!(e.buffer().lines()[0], "hello!");
7119 }
7120
7121 #[test]
7122 fn j_joins_next_line_with_space() {
7123 let mut e = editor_with("hello\nworld");
7124 run_keys(&mut e, "J");
7125 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7126 }
7127
7128 #[test]
7129 fn j_strips_leading_whitespace_on_join() {
7130 let mut e = editor_with("hello\n world");
7131 run_keys(&mut e, "J");
7132 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7133 }
7134
7135 #[test]
7136 fn big_x_deletes_char_before_cursor() {
7137 let mut e = editor_with("hello");
7138 e.jump_cursor(0, 3);
7139 run_keys(&mut e, "X");
7140 assert_eq!(e.buffer().lines()[0], "helo");
7141 }
7142
7143 #[test]
7144 fn s_substitutes_char_and_enters_insert() {
7145 let mut e = editor_with("hello");
7146 run_keys(&mut e, "sX<Esc>");
7147 assert_eq!(e.buffer().lines()[0], "Xello");
7148 }
7149
7150 #[test]
7151 fn count_x_deletes_many() {
7152 let mut e = editor_with("abcdef");
7153 run_keys(&mut e, "3x");
7154 assert_eq!(e.buffer().lines()[0], "def");
7155 }
7156
7157 #[test]
7160 fn p_pastes_charwise_after_cursor() {
7161 let mut e = editor_with("hello");
7162 run_keys(&mut e, "yw");
7163 run_keys(&mut e, "$p");
7164 assert_eq!(e.buffer().lines()[0], "hellohello");
7165 }
7166
7167 #[test]
7168 fn capital_p_pastes_charwise_before_cursor() {
7169 let mut e = editor_with("hello");
7170 run_keys(&mut e, "v");
7172 run_keys(&mut e, "l");
7173 run_keys(&mut e, "y");
7174 run_keys(&mut e, "$P");
7175 assert_eq!(e.buffer().lines()[0], "hellheo");
7178 }
7179
7180 #[test]
7181 fn p_pastes_linewise_below() {
7182 let mut e = editor_with("one\ntwo\nthree");
7183 run_keys(&mut e, "yy");
7184 run_keys(&mut e, "p");
7185 assert_eq!(
7186 e.buffer().lines(),
7187 &[
7188 "one".to_string(),
7189 "one".to_string(),
7190 "two".to_string(),
7191 "three".to_string()
7192 ]
7193 );
7194 }
7195
7196 #[test]
7197 fn capital_p_pastes_linewise_above() {
7198 let mut e = editor_with("one\ntwo");
7199 e.jump_cursor(1, 0);
7200 run_keys(&mut e, "yy");
7201 run_keys(&mut e, "P");
7202 assert_eq!(
7203 e.buffer().lines(),
7204 &["one".to_string(), "two".to_string(), "two".to_string()]
7205 );
7206 }
7207
7208 #[test]
7211 fn hash_finds_previous_occurrence() {
7212 let mut e = editor_with("foo bar foo baz foo");
7213 e.jump_cursor(0, 16);
7215 run_keys(&mut e, "#");
7216 assert_eq!(e.cursor().1, 8);
7217 }
7218
7219 #[test]
7222 fn visual_line_delete_removes_full_lines() {
7223 let mut e = editor_with("a\nb\nc\nd");
7224 run_keys(&mut e, "Vjd");
7225 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7226 }
7227
7228 #[test]
7229 fn visual_line_change_leaves_blank_line() {
7230 let mut e = editor_with("a\nb\nc");
7231 run_keys(&mut e, "Vjc");
7232 assert_eq!(e.vim_mode(), VimMode::Insert);
7233 run_keys(&mut e, "X<Esc>");
7234 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7238 }
7239
7240 #[test]
7241 fn cc_leaves_blank_line() {
7242 let mut e = editor_with("a\nb\nc");
7243 e.jump_cursor(1, 0);
7244 run_keys(&mut e, "ccX<Esc>");
7245 assert_eq!(
7246 e.buffer().lines(),
7247 &["a".to_string(), "X".to_string(), "c".to_string()]
7248 );
7249 }
7250
7251 #[test]
7256 fn big_w_skips_hyphens() {
7257 let mut e = editor_with("foo-bar baz");
7259 run_keys(&mut e, "W");
7260 assert_eq!(e.cursor().1, 8);
7261 }
7262
7263 #[test]
7264 fn big_w_crosses_lines() {
7265 let mut e = editor_with("foo-bar\nbaz-qux");
7266 run_keys(&mut e, "W");
7267 assert_eq!(e.cursor(), (1, 0));
7268 }
7269
7270 #[test]
7271 fn big_b_skips_hyphens() {
7272 let mut e = editor_with("foo-bar baz");
7273 e.jump_cursor(0, 9);
7274 run_keys(&mut e, "B");
7275 assert_eq!(e.cursor().1, 8);
7276 run_keys(&mut e, "B");
7277 assert_eq!(e.cursor().1, 0);
7278 }
7279
7280 #[test]
7281 fn big_e_jumps_to_big_word_end() {
7282 let mut e = editor_with("foo-bar baz");
7283 run_keys(&mut e, "E");
7284 assert_eq!(e.cursor().1, 6);
7285 run_keys(&mut e, "E");
7286 assert_eq!(e.cursor().1, 10);
7287 }
7288
7289 #[test]
7290 fn dw_with_big_word_variant() {
7291 let mut e = editor_with("foo-bar baz");
7293 run_keys(&mut e, "dW");
7294 assert_eq!(e.buffer().lines()[0], "baz");
7295 }
7296
7297 #[test]
7300 fn insert_ctrl_w_deletes_word_back() {
7301 let mut e = editor_with("");
7302 run_keys(&mut e, "i");
7303 for c in "hello world".chars() {
7304 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7305 }
7306 run_keys(&mut e, "<C-w>");
7307 assert_eq!(e.buffer().lines()[0], "hello ");
7308 }
7309
7310 #[test]
7311 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7312 let mut e = editor_with("hello\nworld");
7316 e.jump_cursor(1, 0);
7317 run_keys(&mut e, "i");
7318 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7319 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7322 assert_eq!(e.cursor(), (0, 0));
7323 }
7324
7325 #[test]
7326 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7327 let mut e = editor_with("foo bar\nbaz");
7328 e.jump_cursor(1, 0);
7329 run_keys(&mut e, "i");
7330 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7331 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7333 assert_eq!(e.cursor(), (0, 4));
7334 }
7335
7336 #[test]
7337 fn insert_ctrl_u_deletes_to_line_start() {
7338 let mut e = editor_with("");
7339 run_keys(&mut e, "i");
7340 for c in "hello world".chars() {
7341 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7342 }
7343 run_keys(&mut e, "<C-u>");
7344 assert_eq!(e.buffer().lines()[0], "");
7345 }
7346
7347 #[test]
7348 fn insert_ctrl_o_runs_one_normal_command() {
7349 let mut e = editor_with("hello world");
7350 run_keys(&mut e, "A");
7352 assert_eq!(e.vim_mode(), VimMode::Insert);
7353 e.jump_cursor(0, 0);
7355 run_keys(&mut e, "<C-o>");
7356 assert_eq!(e.vim_mode(), VimMode::Normal);
7357 run_keys(&mut e, "dw");
7358 assert_eq!(e.vim_mode(), VimMode::Insert);
7360 assert_eq!(e.buffer().lines()[0], "world");
7361 }
7362
7363 #[test]
7366 fn j_through_empty_line_preserves_column() {
7367 let mut e = editor_with("hello world\n\nanother line");
7368 run_keys(&mut e, "llllll");
7370 assert_eq!(e.cursor(), (0, 6));
7371 run_keys(&mut e, "j");
7374 assert_eq!(e.cursor(), (1, 0));
7375 run_keys(&mut e, "j");
7377 assert_eq!(e.cursor(), (2, 6));
7378 }
7379
7380 #[test]
7381 fn j_through_shorter_line_preserves_column() {
7382 let mut e = editor_with("hello world\nhi\nanother line");
7383 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7386 run_keys(&mut e, "j");
7387 assert_eq!(e.cursor(), (2, 7));
7388 }
7389
7390 #[test]
7391 fn esc_from_insert_sticky_matches_visible_cursor() {
7392 let mut e = editor_with(" this is a line\n another one of a similar size");
7396 e.jump_cursor(0, 12);
7397 run_keys(&mut e, "I");
7398 assert_eq!(e.cursor(), (0, 4));
7399 run_keys(&mut e, "X<Esc>");
7400 assert_eq!(e.cursor(), (0, 4));
7401 run_keys(&mut e, "j");
7402 assert_eq!(e.cursor(), (1, 4));
7403 }
7404
7405 #[test]
7406 fn esc_from_insert_sticky_tracks_inserted_chars() {
7407 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7408 run_keys(&mut e, "i");
7409 run_keys(&mut e, "abc<Esc>");
7410 assert_eq!(e.cursor(), (0, 2));
7411 run_keys(&mut e, "j");
7412 assert_eq!(e.cursor(), (1, 2));
7413 }
7414
7415 #[test]
7416 fn esc_from_insert_sticky_tracks_arrow_nav() {
7417 let mut e = editor_with("xxxxxx\nyyyyyy");
7418 run_keys(&mut e, "i");
7419 run_keys(&mut e, "abc");
7420 for _ in 0..2 {
7421 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7422 }
7423 run_keys(&mut e, "<Esc>");
7424 assert_eq!(e.cursor(), (0, 0));
7425 run_keys(&mut e, "j");
7426 assert_eq!(e.cursor(), (1, 0));
7427 }
7428
7429 #[test]
7430 fn esc_from_insert_at_col_14_followed_by_j() {
7431 let line = "x".repeat(30);
7434 let buf = format!("{line}\n{line}");
7435 let mut e = editor_with(&buf);
7436 e.jump_cursor(0, 14);
7437 run_keys(&mut e, "i");
7438 for c in "test ".chars() {
7439 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7440 }
7441 run_keys(&mut e, "<Esc>");
7442 assert_eq!(e.cursor(), (0, 18));
7443 run_keys(&mut e, "j");
7444 assert_eq!(e.cursor(), (1, 18));
7445 }
7446
7447 #[test]
7448 fn linewise_paste_resets_sticky_column() {
7449 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7453 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7455 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7459 run_keys(&mut e, "j");
7461 assert_eq!(e.cursor(), (3, 2));
7462 }
7463
7464 #[test]
7465 fn horizontal_motion_resyncs_sticky_column() {
7466 let mut e = editor_with("hello world\n\nanother line");
7470 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7473 assert_eq!(e.cursor(), (2, 3));
7474 }
7475
7476 #[test]
7479 fn ctrl_v_enters_visual_block() {
7480 let mut e = editor_with("aaa\nbbb\nccc");
7481 run_keys(&mut e, "<C-v>");
7482 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7483 }
7484
7485 #[test]
7486 fn visual_block_esc_returns_to_normal() {
7487 let mut e = editor_with("aaa\nbbb\nccc");
7488 run_keys(&mut e, "<C-v>");
7489 run_keys(&mut e, "<Esc>");
7490 assert_eq!(e.vim_mode(), VimMode::Normal);
7491 }
7492
7493 #[test]
7494 fn visual_exit_sets_lt_gt_marks() {
7495 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7498 run_keys(&mut e, "V");
7500 run_keys(&mut e, "j");
7501 run_keys(&mut e, "<Esc>");
7502 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7503 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7504 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7505 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7506 }
7507
7508 #[test]
7509 fn visual_exit_marks_use_lower_higher_order() {
7510 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7514 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7516 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7518 let lt = e.mark('<').unwrap();
7519 let gt = e.mark('>').unwrap();
7520 assert_eq!(lt.0, 2);
7521 assert_eq!(gt.0, 3);
7522 }
7523
7524 #[test]
7525 fn visual_block_delete_removes_column_range() {
7526 let mut e = editor_with("hello\nworld\nhappy");
7527 run_keys(&mut e, "l");
7529 run_keys(&mut e, "<C-v>");
7530 run_keys(&mut e, "jj");
7531 run_keys(&mut e, "ll");
7532 run_keys(&mut e, "d");
7533 assert_eq!(
7535 e.buffer().lines(),
7536 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7537 );
7538 }
7539
7540 #[test]
7541 fn visual_block_yank_joins_with_newlines() {
7542 let mut e = editor_with("hello\nworld\nhappy");
7543 run_keys(&mut e, "<C-v>");
7544 run_keys(&mut e, "jj");
7545 run_keys(&mut e, "ll");
7546 run_keys(&mut e, "y");
7547 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7548 }
7549
7550 #[test]
7551 fn visual_block_replace_fills_block() {
7552 let mut e = editor_with("hello\nworld\nhappy");
7553 run_keys(&mut e, "<C-v>");
7554 run_keys(&mut e, "jj");
7555 run_keys(&mut e, "ll");
7556 run_keys(&mut e, "rx");
7557 assert_eq!(
7558 e.buffer().lines(),
7559 &[
7560 "xxxlo".to_string(),
7561 "xxxld".to_string(),
7562 "xxxpy".to_string()
7563 ]
7564 );
7565 }
7566
7567 #[test]
7568 fn visual_block_insert_repeats_across_rows() {
7569 let mut e = editor_with("hello\nworld\nhappy");
7570 run_keys(&mut e, "<C-v>");
7571 run_keys(&mut e, "jj");
7572 run_keys(&mut e, "I");
7573 run_keys(&mut e, "# <Esc>");
7574 assert_eq!(
7575 e.buffer().lines(),
7576 &[
7577 "# hello".to_string(),
7578 "# world".to_string(),
7579 "# happy".to_string()
7580 ]
7581 );
7582 }
7583
7584 #[test]
7585 fn block_highlight_returns_none_outside_block_mode() {
7586 let mut e = editor_with("abc");
7587 assert!(e.block_highlight().is_none());
7588 run_keys(&mut e, "v");
7589 assert!(e.block_highlight().is_none());
7590 run_keys(&mut e, "<Esc>V");
7591 assert!(e.block_highlight().is_none());
7592 }
7593
7594 #[test]
7595 fn block_highlight_bounds_track_anchor_and_cursor() {
7596 let mut e = editor_with("aaaa\nbbbb\ncccc");
7597 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7599 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7602 }
7603
7604 #[test]
7605 fn visual_block_delete_handles_short_lines() {
7606 let mut e = editor_with("hello\nhi\nworld");
7608 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7610 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7612 assert_eq!(
7617 e.buffer().lines(),
7618 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7619 );
7620 }
7621
7622 #[test]
7623 fn visual_block_yank_pads_short_lines_with_empties() {
7624 let mut e = editor_with("hello\nhi\nworld");
7625 run_keys(&mut e, "l");
7626 run_keys(&mut e, "<C-v>");
7627 run_keys(&mut e, "jjll");
7628 run_keys(&mut e, "y");
7629 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7631 }
7632
7633 #[test]
7634 fn visual_block_replace_skips_past_eol() {
7635 let mut e = editor_with("ab\ncd\nef");
7638 run_keys(&mut e, "l");
7640 run_keys(&mut e, "<C-v>");
7641 run_keys(&mut e, "jjllllll");
7642 run_keys(&mut e, "rX");
7643 assert_eq!(
7646 e.buffer().lines(),
7647 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7648 );
7649 }
7650
7651 #[test]
7652 fn visual_block_with_empty_line_in_middle() {
7653 let mut e = editor_with("abcd\n\nefgh");
7654 run_keys(&mut e, "<C-v>");
7655 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7657 assert_eq!(
7660 e.buffer().lines(),
7661 &["d".to_string(), "".to_string(), "h".to_string()]
7662 );
7663 }
7664
7665 #[test]
7666 fn block_insert_pads_empty_lines_to_block_column() {
7667 let mut e = editor_with("this is a line\n\nthis is a line");
7670 e.jump_cursor(0, 3);
7671 run_keys(&mut e, "<C-v>");
7672 run_keys(&mut e, "jj");
7673 run_keys(&mut e, "I");
7674 run_keys(&mut e, "XX<Esc>");
7675 assert_eq!(
7676 e.buffer().lines(),
7677 &[
7678 "thiXXs is a line".to_string(),
7679 " XX".to_string(),
7680 "thiXXs is a line".to_string()
7681 ]
7682 );
7683 }
7684
7685 #[test]
7686 fn block_insert_pads_short_lines_to_block_column() {
7687 let mut e = editor_with("aaaaa\nbb\naaaaa");
7688 e.jump_cursor(0, 3);
7689 run_keys(&mut e, "<C-v>");
7690 run_keys(&mut e, "jj");
7691 run_keys(&mut e, "I");
7692 run_keys(&mut e, "Y<Esc>");
7693 assert_eq!(
7695 e.buffer().lines(),
7696 &[
7697 "aaaYaa".to_string(),
7698 "bb Y".to_string(),
7699 "aaaYaa".to_string()
7700 ]
7701 );
7702 }
7703
7704 #[test]
7705 fn visual_block_append_repeats_across_rows() {
7706 let mut e = editor_with("foo\nbar\nbaz");
7707 run_keys(&mut e, "<C-v>");
7708 run_keys(&mut e, "jj");
7709 run_keys(&mut e, "A");
7712 run_keys(&mut e, "!<Esc>");
7713 assert_eq!(
7714 e.buffer().lines(),
7715 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7716 );
7717 }
7718
7719 #[test]
7722 fn slash_opens_forward_search_prompt() {
7723 let mut e = editor_with("hello world");
7724 run_keys(&mut e, "/");
7725 let p = e.search_prompt().expect("prompt should be active");
7726 assert!(p.text.is_empty());
7727 assert!(p.forward);
7728 }
7729
7730 #[test]
7731 fn question_opens_backward_search_prompt() {
7732 let mut e = editor_with("hello world");
7733 run_keys(&mut e, "?");
7734 let p = e.search_prompt().expect("prompt should be active");
7735 assert!(!p.forward);
7736 }
7737
7738 #[test]
7739 fn search_prompt_typing_updates_pattern_live() {
7740 let mut e = editor_with("foo bar\nbaz");
7741 run_keys(&mut e, "/bar");
7742 assert_eq!(e.search_prompt().unwrap().text, "bar");
7743 assert!(e.search_state().pattern.is_some());
7745 }
7746
7747 #[test]
7748 fn search_prompt_backspace_and_enter() {
7749 let mut e = editor_with("hello world\nagain");
7750 run_keys(&mut e, "/worlx");
7751 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7752 assert_eq!(e.search_prompt().unwrap().text, "worl");
7753 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7754 assert!(e.search_prompt().is_none());
7756 assert_eq!(e.last_search(), Some("worl"));
7757 assert_eq!(e.cursor(), (0, 6));
7758 }
7759
7760 #[test]
7761 fn empty_search_prompt_enter_repeats_last_search() {
7762 let mut e = editor_with("foo bar foo baz foo");
7763 run_keys(&mut e, "/foo");
7764 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7765 assert_eq!(e.cursor().1, 8);
7766 run_keys(&mut e, "/");
7768 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7769 assert_eq!(e.cursor().1, 16);
7770 assert_eq!(e.last_search(), Some("foo"));
7771 }
7772
7773 #[test]
7774 fn search_history_records_committed_patterns() {
7775 let mut e = editor_with("alpha beta gamma");
7776 run_keys(&mut e, "/alpha<CR>");
7777 run_keys(&mut e, "/beta<CR>");
7778 let history = e.vim.search_history.clone();
7780 assert_eq!(history, vec!["alpha", "beta"]);
7781 }
7782
7783 #[test]
7784 fn search_history_dedupes_consecutive_repeats() {
7785 let mut e = editor_with("foo bar foo");
7786 run_keys(&mut e, "/foo<CR>");
7787 run_keys(&mut e, "/foo<CR>");
7788 run_keys(&mut e, "/bar<CR>");
7789 run_keys(&mut e, "/bar<CR>");
7790 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
7792 }
7793
7794 #[test]
7795 fn ctrl_p_walks_history_backward() {
7796 let mut e = editor_with("alpha beta gamma");
7797 run_keys(&mut e, "/alpha<CR>");
7798 run_keys(&mut e, "/beta<CR>");
7799 run_keys(&mut e, "/");
7801 assert_eq!(e.search_prompt().unwrap().text, "");
7802 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7803 assert_eq!(e.search_prompt().unwrap().text, "beta");
7804 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7805 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7806 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7808 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7809 }
7810
7811 #[test]
7812 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7813 let mut e = editor_with("a b c");
7814 run_keys(&mut e, "/a<CR>");
7815 run_keys(&mut e, "/b<CR>");
7816 run_keys(&mut e, "/c<CR>");
7817 run_keys(&mut e, "/");
7818 for _ in 0..3 {
7820 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7821 }
7822 assert_eq!(e.search_prompt().unwrap().text, "a");
7823 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7824 assert_eq!(e.search_prompt().unwrap().text, "b");
7825 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7826 assert_eq!(e.search_prompt().unwrap().text, "c");
7827 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7829 assert_eq!(e.search_prompt().unwrap().text, "c");
7830 }
7831
7832 #[test]
7833 fn typing_after_history_walk_resets_cursor() {
7834 let mut e = editor_with("foo");
7835 run_keys(&mut e, "/foo<CR>");
7836 run_keys(&mut e, "/");
7837 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7838 assert_eq!(e.search_prompt().unwrap().text, "foo");
7839 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7842 assert_eq!(e.search_prompt().unwrap().text, "foox");
7843 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7844 assert_eq!(e.search_prompt().unwrap().text, "foo");
7845 }
7846
7847 #[test]
7848 fn empty_backward_search_prompt_enter_repeats_last_search() {
7849 let mut e = editor_with("foo bar foo baz foo");
7850 run_keys(&mut e, "/foo");
7852 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7853 assert_eq!(e.cursor().1, 8);
7854 run_keys(&mut e, "?");
7855 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7856 assert_eq!(e.cursor().1, 0);
7857 assert_eq!(e.last_search(), Some("foo"));
7858 }
7859
7860 #[test]
7861 fn search_prompt_esc_cancels_but_keeps_last_search() {
7862 let mut e = editor_with("foo bar\nbaz");
7863 run_keys(&mut e, "/bar");
7864 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7865 assert!(e.search_prompt().is_none());
7866 assert_eq!(e.last_search(), Some("bar"));
7867 }
7868
7869 #[test]
7870 fn search_then_n_and_shift_n_navigate() {
7871 let mut e = editor_with("foo bar foo baz foo");
7872 run_keys(&mut e, "/foo");
7873 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7874 assert_eq!(e.cursor().1, 8);
7876 run_keys(&mut e, "n");
7877 assert_eq!(e.cursor().1, 16);
7878 run_keys(&mut e, "N");
7879 assert_eq!(e.cursor().1, 8);
7880 }
7881
7882 #[test]
7883 fn question_mark_searches_backward_on_enter() {
7884 let mut e = editor_with("foo bar foo baz");
7885 e.jump_cursor(0, 10);
7886 run_keys(&mut e, "?foo");
7887 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7888 assert_eq!(e.cursor(), (0, 8));
7890 }
7891
7892 #[test]
7895 fn big_y_yanks_to_end_of_line() {
7896 let mut e = editor_with("hello world");
7897 e.jump_cursor(0, 6);
7898 run_keys(&mut e, "Y");
7899 assert_eq!(e.last_yank.as_deref(), Some("world"));
7900 }
7901
7902 #[test]
7903 fn big_y_from_line_start_yanks_full_line() {
7904 let mut e = editor_with("hello world");
7905 run_keys(&mut e, "Y");
7906 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7907 }
7908
7909 #[test]
7910 fn gj_joins_without_inserting_space() {
7911 let mut e = editor_with("hello\n world");
7912 run_keys(&mut e, "gJ");
7913 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7915 }
7916
7917 #[test]
7918 fn gj_noop_on_last_line() {
7919 let mut e = editor_with("only");
7920 run_keys(&mut e, "gJ");
7921 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7922 }
7923
7924 #[test]
7925 fn ge_jumps_to_previous_word_end() {
7926 let mut e = editor_with("foo bar baz");
7927 e.jump_cursor(0, 5);
7928 run_keys(&mut e, "ge");
7929 assert_eq!(e.cursor(), (0, 2));
7930 }
7931
7932 #[test]
7933 fn ge_respects_word_class() {
7934 let mut e = editor_with("foo-bar baz");
7937 e.jump_cursor(0, 5);
7938 run_keys(&mut e, "ge");
7939 assert_eq!(e.cursor(), (0, 3));
7940 }
7941
7942 #[test]
7943 fn big_ge_treats_hyphens_as_part_of_word() {
7944 let mut e = editor_with("foo-bar baz");
7947 e.jump_cursor(0, 10);
7948 run_keys(&mut e, "gE");
7949 assert_eq!(e.cursor(), (0, 6));
7950 }
7951
7952 #[test]
7953 fn ge_crosses_line_boundary() {
7954 let mut e = editor_with("foo\nbar");
7955 e.jump_cursor(1, 0);
7956 run_keys(&mut e, "ge");
7957 assert_eq!(e.cursor(), (0, 2));
7958 }
7959
7960 #[test]
7961 fn dge_deletes_to_end_of_previous_word() {
7962 let mut e = editor_with("foo bar baz");
7963 e.jump_cursor(0, 8);
7964 run_keys(&mut e, "dge");
7967 assert_eq!(e.buffer().lines()[0], "foo baaz");
7968 }
7969
7970 #[test]
7971 fn ctrl_scroll_keys_do_not_panic() {
7972 let mut e = editor_with(
7975 (0..50)
7976 .map(|i| format!("line{i}"))
7977 .collect::<Vec<_>>()
7978 .join("\n")
7979 .as_str(),
7980 );
7981 run_keys(&mut e, "<C-f>");
7982 run_keys(&mut e, "<C-b>");
7983 assert!(!e.buffer().lines().is_empty());
7985 }
7986
7987 #[test]
7994 fn count_insert_with_arrow_nav_does_not_leak_rows() {
7995 let mut e = Editor::new(
7996 hjkl_buffer::Buffer::new(),
7997 crate::types::DefaultHost::new(),
7998 crate::types::Options::default(),
7999 );
8000 e.set_content("row0\nrow1\nrow2");
8001 run_keys(&mut e, "3iX<Down><Esc>");
8003 assert!(e.buffer().lines()[0].contains('X'));
8005 assert!(
8008 !e.buffer().lines()[1].contains("row0"),
8009 "row1 leaked row0 contents: {:?}",
8010 e.buffer().lines()[1]
8011 );
8012 assert_eq!(e.buffer().lines().len(), 3);
8015 }
8016
8017 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8020 let mut e = Editor::new(
8021 hjkl_buffer::Buffer::new(),
8022 crate::types::DefaultHost::new(),
8023 crate::types::Options::default(),
8024 );
8025 let body = (0..n)
8026 .map(|i| format!(" line{}", i))
8027 .collect::<Vec<_>>()
8028 .join("\n");
8029 e.set_content(&body);
8030 e.set_viewport_height(viewport);
8031 e
8032 }
8033
8034 #[test]
8035 fn ctrl_d_moves_cursor_half_page_down() {
8036 let mut e = editor_with_rows(100, 20);
8037 run_keys(&mut e, "<C-d>");
8038 assert_eq!(e.cursor().0, 10);
8039 }
8040
8041 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8042 let mut e = Editor::new(
8043 hjkl_buffer::Buffer::new(),
8044 crate::types::DefaultHost::new(),
8045 crate::types::Options::default(),
8046 );
8047 e.set_content(&lines.join("\n"));
8048 e.set_viewport_height(viewport);
8049 let v = e.host_mut().viewport_mut();
8050 v.height = viewport;
8051 v.width = text_width;
8052 v.text_width = text_width;
8053 v.wrap = hjkl_buffer::Wrap::Char;
8054 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8055 e
8056 }
8057
8058 #[test]
8059 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8060 let lines = ["aaaabbbbcccc"; 10];
8064 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8065 e.jump_cursor(4, 0);
8066 e.ensure_cursor_in_scrolloff();
8067 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8068 assert!(csr <= 6, "csr={csr}");
8069 }
8070
8071 #[test]
8072 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8073 let lines = ["aaaabbbbcccc"; 10];
8074 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8075 e.jump_cursor(7, 0);
8078 e.ensure_cursor_in_scrolloff();
8079 e.jump_cursor(2, 0);
8080 e.ensure_cursor_in_scrolloff();
8081 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8082 assert!(csr >= 5, "csr={csr}");
8084 }
8085
8086 #[test]
8087 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8088 let lines = ["aaaabbbbcccc"; 5];
8089 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8090 e.jump_cursor(4, 11);
8091 e.ensure_cursor_in_scrolloff();
8092 let top = e.host().viewport().top_row;
8097 assert_eq!(top, 1);
8098 }
8099
8100 #[test]
8101 fn ctrl_u_moves_cursor_half_page_up() {
8102 let mut e = editor_with_rows(100, 20);
8103 e.jump_cursor(50, 0);
8104 run_keys(&mut e, "<C-u>");
8105 assert_eq!(e.cursor().0, 40);
8106 }
8107
8108 #[test]
8109 fn ctrl_f_moves_cursor_full_page_down() {
8110 let mut e = editor_with_rows(100, 20);
8111 run_keys(&mut e, "<C-f>");
8112 assert_eq!(e.cursor().0, 18);
8114 }
8115
8116 #[test]
8117 fn ctrl_b_moves_cursor_full_page_up() {
8118 let mut e = editor_with_rows(100, 20);
8119 e.jump_cursor(50, 0);
8120 run_keys(&mut e, "<C-b>");
8121 assert_eq!(e.cursor().0, 32);
8122 }
8123
8124 #[test]
8125 fn ctrl_d_lands_on_first_non_blank() {
8126 let mut e = editor_with_rows(100, 20);
8127 run_keys(&mut e, "<C-d>");
8128 assert_eq!(e.cursor().1, 2);
8130 }
8131
8132 #[test]
8133 fn ctrl_d_clamps_at_end_of_buffer() {
8134 let mut e = editor_with_rows(5, 20);
8135 run_keys(&mut e, "<C-d>");
8136 assert_eq!(e.cursor().0, 4);
8137 }
8138
8139 #[test]
8140 fn capital_h_jumps_to_viewport_top() {
8141 let mut e = editor_with_rows(100, 10);
8142 e.jump_cursor(50, 0);
8143 e.set_viewport_top(45);
8144 let top = e.host().viewport().top_row;
8145 run_keys(&mut e, "H");
8146 assert_eq!(e.cursor().0, top);
8147 assert_eq!(e.cursor().1, 2);
8148 }
8149
8150 #[test]
8151 fn capital_l_jumps_to_viewport_bottom() {
8152 let mut e = editor_with_rows(100, 10);
8153 e.jump_cursor(50, 0);
8154 e.set_viewport_top(45);
8155 let top = e.host().viewport().top_row;
8156 run_keys(&mut e, "L");
8157 assert_eq!(e.cursor().0, top + 9);
8158 }
8159
8160 #[test]
8161 fn capital_m_jumps_to_viewport_middle() {
8162 let mut e = editor_with_rows(100, 10);
8163 e.jump_cursor(50, 0);
8164 e.set_viewport_top(45);
8165 let top = e.host().viewport().top_row;
8166 run_keys(&mut e, "M");
8167 assert_eq!(e.cursor().0, top + 4);
8169 }
8170
8171 #[test]
8172 fn g_capital_m_lands_at_line_midpoint() {
8173 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8175 assert_eq!(e.cursor(), (0, 6));
8177 }
8178
8179 #[test]
8180 fn g_capital_m_on_empty_line_stays_at_zero() {
8181 let mut e = editor_with("");
8182 run_keys(&mut e, "gM");
8183 assert_eq!(e.cursor(), (0, 0));
8184 }
8185
8186 #[test]
8187 fn g_capital_m_uses_current_line_only() {
8188 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8191 run_keys(&mut e, "gM");
8192 assert_eq!(e.cursor(), (1, 6));
8193 }
8194
8195 #[test]
8196 fn capital_h_count_offsets_from_top() {
8197 let mut e = editor_with_rows(100, 10);
8198 e.jump_cursor(50, 0);
8199 e.set_viewport_top(45);
8200 let top = e.host().viewport().top_row;
8201 run_keys(&mut e, "3H");
8202 assert_eq!(e.cursor().0, top + 2);
8203 }
8204
8205 #[test]
8208 fn ctrl_o_returns_to_pre_g_position() {
8209 let mut e = editor_with_rows(50, 20);
8210 e.jump_cursor(5, 2);
8211 run_keys(&mut e, "G");
8212 assert_eq!(e.cursor().0, 49);
8213 run_keys(&mut e, "<C-o>");
8214 assert_eq!(e.cursor(), (5, 2));
8215 }
8216
8217 #[test]
8218 fn ctrl_i_redoes_jump_after_ctrl_o() {
8219 let mut e = editor_with_rows(50, 20);
8220 e.jump_cursor(5, 2);
8221 run_keys(&mut e, "G");
8222 let post = e.cursor();
8223 run_keys(&mut e, "<C-o>");
8224 run_keys(&mut e, "<C-i>");
8225 assert_eq!(e.cursor(), post);
8226 }
8227
8228 #[test]
8229 fn new_jump_clears_forward_stack() {
8230 let mut e = editor_with_rows(50, 20);
8231 e.jump_cursor(5, 2);
8232 run_keys(&mut e, "G");
8233 run_keys(&mut e, "<C-o>");
8234 run_keys(&mut e, "gg");
8235 run_keys(&mut e, "<C-i>");
8236 assert_eq!(e.cursor().0, 0);
8237 }
8238
8239 #[test]
8240 fn ctrl_o_on_empty_stack_is_noop() {
8241 let mut e = editor_with_rows(10, 20);
8242 e.jump_cursor(3, 1);
8243 run_keys(&mut e, "<C-o>");
8244 assert_eq!(e.cursor(), (3, 1));
8245 }
8246
8247 #[test]
8248 fn asterisk_search_pushes_jump() {
8249 let mut e = editor_with("foo bar\nbaz foo end");
8250 e.jump_cursor(0, 0);
8251 run_keys(&mut e, "*");
8252 let after = e.cursor();
8253 assert_ne!(after, (0, 0));
8254 run_keys(&mut e, "<C-o>");
8255 assert_eq!(e.cursor(), (0, 0));
8256 }
8257
8258 #[test]
8259 fn h_viewport_jump_is_recorded() {
8260 let mut e = editor_with_rows(100, 10);
8261 e.jump_cursor(50, 0);
8262 e.set_viewport_top(45);
8263 let pre = e.cursor();
8264 run_keys(&mut e, "H");
8265 assert_ne!(e.cursor(), pre);
8266 run_keys(&mut e, "<C-o>");
8267 assert_eq!(e.cursor(), pre);
8268 }
8269
8270 #[test]
8271 fn j_k_motion_does_not_push_jump() {
8272 let mut e = editor_with_rows(50, 20);
8273 e.jump_cursor(5, 0);
8274 run_keys(&mut e, "jjj");
8275 run_keys(&mut e, "<C-o>");
8276 assert_eq!(e.cursor().0, 8);
8277 }
8278
8279 #[test]
8280 fn jumplist_caps_at_100() {
8281 let mut e = editor_with_rows(200, 20);
8282 for i in 0..101 {
8283 e.jump_cursor(i, 0);
8284 run_keys(&mut e, "G");
8285 }
8286 assert!(e.vim.jump_back.len() <= 100);
8287 }
8288
8289 #[test]
8290 fn tab_acts_as_ctrl_i() {
8291 let mut e = editor_with_rows(50, 20);
8292 e.jump_cursor(5, 2);
8293 run_keys(&mut e, "G");
8294 let post = e.cursor();
8295 run_keys(&mut e, "<C-o>");
8296 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8297 assert_eq!(e.cursor(), post);
8298 }
8299
8300 #[test]
8303 fn ma_then_backtick_a_jumps_exact() {
8304 let mut e = editor_with_rows(50, 20);
8305 e.jump_cursor(5, 3);
8306 run_keys(&mut e, "ma");
8307 e.jump_cursor(20, 0);
8308 run_keys(&mut e, "`a");
8309 assert_eq!(e.cursor(), (5, 3));
8310 }
8311
8312 #[test]
8313 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8314 let mut e = editor_with_rows(50, 20);
8315 e.jump_cursor(5, 6);
8317 run_keys(&mut e, "ma");
8318 e.jump_cursor(30, 4);
8319 run_keys(&mut e, "'a");
8320 assert_eq!(e.cursor(), (5, 2));
8321 }
8322
8323 #[test]
8324 fn goto_mark_pushes_jumplist() {
8325 let mut e = editor_with_rows(50, 20);
8326 e.jump_cursor(10, 2);
8327 run_keys(&mut e, "mz");
8328 e.jump_cursor(3, 0);
8329 run_keys(&mut e, "`z");
8330 assert_eq!(e.cursor(), (10, 2));
8331 run_keys(&mut e, "<C-o>");
8332 assert_eq!(e.cursor(), (3, 0));
8333 }
8334
8335 #[test]
8336 fn goto_missing_mark_is_noop() {
8337 let mut e = editor_with_rows(50, 20);
8338 e.jump_cursor(3, 1);
8339 run_keys(&mut e, "`q");
8340 assert_eq!(e.cursor(), (3, 1));
8341 }
8342
8343 #[test]
8344 fn uppercase_mark_stored_under_uppercase_key() {
8345 let mut e = editor_with_rows(50, 20);
8346 e.jump_cursor(5, 3);
8347 run_keys(&mut e, "mA");
8348 assert_eq!(e.mark('A'), Some((5, 3)));
8351 assert!(e.mark('a').is_none());
8352 }
8353
8354 #[test]
8355 fn mark_survives_document_shrink_via_clamp() {
8356 let mut e = editor_with_rows(50, 20);
8357 e.jump_cursor(40, 4);
8358 run_keys(&mut e, "mx");
8359 e.set_content("a\nb\nc\nd\ne");
8361 run_keys(&mut e, "`x");
8362 let (r, _) = e.cursor();
8364 assert!(r <= 4);
8365 }
8366
8367 #[test]
8368 fn g_semicolon_walks_back_through_edits() {
8369 let mut e = editor_with("alpha\nbeta\ngamma");
8370 e.jump_cursor(0, 0);
8373 run_keys(&mut e, "iX<Esc>");
8374 e.jump_cursor(2, 0);
8375 run_keys(&mut e, "iY<Esc>");
8376 run_keys(&mut e, "g;");
8378 assert_eq!(e.cursor(), (2, 1));
8379 run_keys(&mut e, "g;");
8381 assert_eq!(e.cursor(), (0, 1));
8382 run_keys(&mut e, "g;");
8384 assert_eq!(e.cursor(), (0, 1));
8385 }
8386
8387 #[test]
8388 fn g_comma_walks_forward_after_g_semicolon() {
8389 let mut e = editor_with("a\nb\nc");
8390 e.jump_cursor(0, 0);
8391 run_keys(&mut e, "iX<Esc>");
8392 e.jump_cursor(2, 0);
8393 run_keys(&mut e, "iY<Esc>");
8394 run_keys(&mut e, "g;");
8395 run_keys(&mut e, "g;");
8396 assert_eq!(e.cursor(), (0, 1));
8397 run_keys(&mut e, "g,");
8398 assert_eq!(e.cursor(), (2, 1));
8399 }
8400
8401 #[test]
8402 fn new_edit_during_walk_trims_forward_entries() {
8403 let mut e = editor_with("a\nb\nc\nd");
8404 e.jump_cursor(0, 0);
8405 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8407 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8410 run_keys(&mut e, "g;");
8411 assert_eq!(e.cursor(), (0, 1));
8412 run_keys(&mut e, "iZ<Esc>");
8414 run_keys(&mut e, "g,");
8416 assert_ne!(e.cursor(), (2, 1));
8418 }
8419
8420 #[test]
8426 fn capital_mark_set_and_jump() {
8427 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8428 e.jump_cursor(2, 1);
8429 run_keys(&mut e, "mA");
8430 e.jump_cursor(0, 0);
8432 run_keys(&mut e, "'A");
8434 assert_eq!(e.cursor().0, 2);
8436 }
8437
8438 #[test]
8439 fn capital_mark_survives_set_content() {
8440 let mut e = editor_with("first buffer line\nsecond");
8441 e.jump_cursor(1, 3);
8442 run_keys(&mut e, "mA");
8443 e.set_content("totally different content\non many\nrows of text");
8445 e.jump_cursor(0, 0);
8447 run_keys(&mut e, "'A");
8448 assert_eq!(e.cursor().0, 1);
8449 }
8450
8451 #[test]
8456 fn capital_mark_shifts_with_edit() {
8457 let mut e = editor_with("a\nb\nc\nd");
8458 e.jump_cursor(3, 0);
8459 run_keys(&mut e, "mA");
8460 e.jump_cursor(0, 0);
8462 run_keys(&mut e, "dd");
8463 e.jump_cursor(0, 0);
8464 run_keys(&mut e, "'A");
8465 assert_eq!(e.cursor().0, 2);
8466 }
8467
8468 #[test]
8469 fn mark_below_delete_shifts_up() {
8470 let mut e = editor_with("a\nb\nc\nd\ne");
8471 e.jump_cursor(3, 0);
8473 run_keys(&mut e, "ma");
8474 e.jump_cursor(0, 0);
8476 run_keys(&mut e, "dd");
8477 e.jump_cursor(0, 0);
8479 run_keys(&mut e, "'a");
8480 assert_eq!(e.cursor().0, 2);
8481 assert_eq!(e.buffer().line(2).unwrap(), "d");
8482 }
8483
8484 #[test]
8485 fn mark_on_deleted_row_is_dropped() {
8486 let mut e = editor_with("a\nb\nc\nd");
8487 e.jump_cursor(1, 0);
8489 run_keys(&mut e, "ma");
8490 run_keys(&mut e, "dd");
8492 e.jump_cursor(2, 0);
8494 run_keys(&mut e, "'a");
8495 assert_eq!(e.cursor().0, 2);
8497 }
8498
8499 #[test]
8500 fn mark_above_edit_unchanged() {
8501 let mut e = editor_with("a\nb\nc\nd\ne");
8502 e.jump_cursor(0, 0);
8504 run_keys(&mut e, "ma");
8505 e.jump_cursor(3, 0);
8507 run_keys(&mut e, "dd");
8508 e.jump_cursor(2, 0);
8510 run_keys(&mut e, "'a");
8511 assert_eq!(e.cursor().0, 0);
8512 }
8513
8514 #[test]
8515 fn mark_shifts_down_after_insert() {
8516 let mut e = editor_with("a\nb\nc");
8517 e.jump_cursor(2, 0);
8519 run_keys(&mut e, "ma");
8520 e.jump_cursor(0, 0);
8522 run_keys(&mut e, "Onew<Esc>");
8523 e.jump_cursor(0, 0);
8526 run_keys(&mut e, "'a");
8527 assert_eq!(e.cursor().0, 3);
8528 assert_eq!(e.buffer().line(3).unwrap(), "c");
8529 }
8530
8531 #[test]
8534 fn forward_search_commit_pushes_jump() {
8535 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8536 e.jump_cursor(0, 0);
8537 run_keys(&mut e, "/target<CR>");
8538 assert_ne!(e.cursor(), (0, 0));
8540 run_keys(&mut e, "<C-o>");
8542 assert_eq!(e.cursor(), (0, 0));
8543 }
8544
8545 #[test]
8546 fn search_commit_no_match_does_not_push_jump() {
8547 let mut e = editor_with("alpha beta\nfoo end");
8548 e.jump_cursor(0, 3);
8549 let pre_len = e.vim.jump_back.len();
8550 run_keys(&mut e, "/zzznotfound<CR>");
8551 assert_eq!(e.vim.jump_back.len(), pre_len);
8553 }
8554
8555 #[test]
8558 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8559 let mut e = editor_with("hello world");
8560 run_keys(&mut e, "lll");
8561 let (row, col) = e.cursor();
8562 assert_eq!(e.buffer.cursor().row, row);
8563 assert_eq!(e.buffer.cursor().col, col);
8564 }
8565
8566 #[test]
8567 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8568 let mut e = editor_with("aaaa\nbbbb\ncccc");
8569 run_keys(&mut e, "jj");
8570 let (row, col) = e.cursor();
8571 assert_eq!(e.buffer.cursor().row, row);
8572 assert_eq!(e.buffer.cursor().col, col);
8573 }
8574
8575 #[test]
8576 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8577 let mut e = editor_with("foo bar baz");
8578 run_keys(&mut e, "ww");
8579 let (row, col) = e.cursor();
8580 assert_eq!(e.buffer.cursor().row, row);
8581 assert_eq!(e.buffer.cursor().col, col);
8582 }
8583
8584 #[test]
8585 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8586 let mut e = editor_with("a\nb\nc\nd\ne");
8587 run_keys(&mut e, "G");
8588 let (row, col) = e.cursor();
8589 assert_eq!(e.buffer.cursor().row, row);
8590 assert_eq!(e.buffer.cursor().col, col);
8591 }
8592
8593 #[test]
8594 fn editor_sticky_col_tracks_horizontal_motion() {
8595 let mut e = editor_with("longline\nhi\nlongline");
8596 run_keys(&mut e, "fl");
8601 let landed = e.cursor().1;
8602 assert!(landed > 0, "fl should have moved");
8603 run_keys(&mut e, "j");
8604 assert_eq!(e.sticky_col(), Some(landed));
8607 }
8608
8609 #[test]
8610 fn buffer_content_mirrors_textarea_after_insert() {
8611 let mut e = editor_with("hello");
8612 run_keys(&mut e, "iXYZ<Esc>");
8613 let text = e.buffer().lines().join("\n");
8614 assert_eq!(e.buffer.as_string(), text);
8615 }
8616
8617 #[test]
8618 fn buffer_content_mirrors_textarea_after_delete() {
8619 let mut e = editor_with("alpha bravo charlie");
8620 run_keys(&mut e, "dw");
8621 let text = e.buffer().lines().join("\n");
8622 assert_eq!(e.buffer.as_string(), text);
8623 }
8624
8625 #[test]
8626 fn buffer_content_mirrors_textarea_after_dd() {
8627 let mut e = editor_with("a\nb\nc\nd");
8628 run_keys(&mut e, "jdd");
8629 let text = e.buffer().lines().join("\n");
8630 assert_eq!(e.buffer.as_string(), text);
8631 }
8632
8633 #[test]
8634 fn buffer_content_mirrors_textarea_after_open_line() {
8635 let mut e = editor_with("foo\nbar");
8636 run_keys(&mut e, "oNEW<Esc>");
8637 let text = e.buffer().lines().join("\n");
8638 assert_eq!(e.buffer.as_string(), text);
8639 }
8640
8641 #[test]
8642 fn buffer_content_mirrors_textarea_after_paste() {
8643 let mut e = editor_with("hello");
8644 run_keys(&mut e, "yy");
8645 run_keys(&mut e, "p");
8646 let text = e.buffer().lines().join("\n");
8647 assert_eq!(e.buffer.as_string(), text);
8648 }
8649
8650 #[test]
8651 fn buffer_selection_none_in_normal_mode() {
8652 let e = editor_with("foo bar");
8653 assert!(e.buffer_selection().is_none());
8654 }
8655
8656 #[test]
8657 fn buffer_selection_char_in_visual_mode() {
8658 use hjkl_buffer::{Position, Selection};
8659 let mut e = editor_with("hello world");
8660 run_keys(&mut e, "vlll");
8661 assert_eq!(
8662 e.buffer_selection(),
8663 Some(Selection::Char {
8664 anchor: Position::new(0, 0),
8665 head: Position::new(0, 3),
8666 })
8667 );
8668 }
8669
8670 #[test]
8671 fn buffer_selection_line_in_visual_line_mode() {
8672 use hjkl_buffer::Selection;
8673 let mut e = editor_with("a\nb\nc\nd");
8674 run_keys(&mut e, "Vj");
8675 assert_eq!(
8676 e.buffer_selection(),
8677 Some(Selection::Line {
8678 anchor_row: 0,
8679 head_row: 1,
8680 })
8681 );
8682 }
8683
8684 #[test]
8685 fn wrapscan_off_blocks_wrap_around() {
8686 let mut e = editor_with("first\nsecond\nthird\n");
8687 e.settings_mut().wrapscan = false;
8688 e.jump_cursor(2, 0);
8690 run_keys(&mut e, "/first<CR>");
8691 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8693 e.settings_mut().wrapscan = true;
8695 run_keys(&mut e, "/first<CR>");
8696 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8697 }
8698
8699 #[test]
8700 fn smartcase_uppercase_pattern_stays_sensitive() {
8701 let mut e = editor_with("foo\nFoo\nBAR\n");
8702 e.settings_mut().ignore_case = true;
8703 e.settings_mut().smartcase = true;
8704 run_keys(&mut e, "/foo<CR>");
8707 let r1 = e
8708 .search_state()
8709 .pattern
8710 .as_ref()
8711 .unwrap()
8712 .as_str()
8713 .to_string();
8714 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8715 run_keys(&mut e, "/Foo<CR>");
8717 let r2 = e
8718 .search_state()
8719 .pattern
8720 .as_ref()
8721 .unwrap()
8722 .as_str()
8723 .to_string();
8724 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8725 }
8726
8727 #[test]
8728 fn enter_with_autoindent_copies_leading_whitespace() {
8729 let mut e = editor_with(" foo");
8730 e.jump_cursor(0, 7);
8731 run_keys(&mut e, "i<CR>");
8732 assert_eq!(e.buffer.line(1).unwrap(), " ");
8733 }
8734
8735 #[test]
8736 fn enter_without_autoindent_inserts_bare_newline() {
8737 let mut e = editor_with(" foo");
8738 e.settings_mut().autoindent = false;
8739 e.jump_cursor(0, 7);
8740 run_keys(&mut e, "i<CR>");
8741 assert_eq!(e.buffer.line(1).unwrap(), "");
8742 }
8743
8744 #[test]
8745 fn iskeyword_default_treats_alnum_underscore_as_word() {
8746 let mut e = editor_with("foo_bar baz");
8747 e.jump_cursor(0, 0);
8751 run_keys(&mut e, "*");
8752 let p = e
8753 .search_state()
8754 .pattern
8755 .as_ref()
8756 .unwrap()
8757 .as_str()
8758 .to_string();
8759 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8760 }
8761
8762 #[test]
8763 fn w_motion_respects_custom_iskeyword() {
8764 let mut e = editor_with("foo-bar baz");
8768 run_keys(&mut e, "w");
8769 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
8770 let mut e2 = editor_with("foo-bar baz");
8773 e2.set_iskeyword("@,_,45");
8774 run_keys(&mut e2, "w");
8775 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
8776 }
8777
8778 #[test]
8779 fn iskeyword_with_dash_treats_dash_as_word_char() {
8780 let mut e = editor_with("foo-bar baz");
8781 e.settings_mut().iskeyword = "@,_,45".to_string();
8782 e.jump_cursor(0, 0);
8783 run_keys(&mut e, "*");
8784 let p = e
8785 .search_state()
8786 .pattern
8787 .as_ref()
8788 .unwrap()
8789 .as_str()
8790 .to_string();
8791 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
8792 }
8793
8794 #[test]
8795 fn timeoutlen_drops_pending_g_prefix() {
8796 use std::time::{Duration, Instant};
8797 let mut e = editor_with("a\nb\nc");
8798 e.jump_cursor(2, 0);
8799 run_keys(&mut e, "g");
8801 assert!(matches!(e.vim.pending, super::Pending::G));
8802 e.settings.timeout_len = Duration::from_nanos(0);
8810 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
8811 e.vim.last_input_host_at = Some(Duration::ZERO);
8812 run_keys(&mut e, "g");
8816 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
8818 }
8819
8820 #[test]
8821 fn undobreak_on_breaks_group_at_arrow_motion() {
8822 let mut e = editor_with("");
8823 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8825 let line = e.buffer.line(0).unwrap_or("").to_string();
8828 assert!(line.contains("aaa"), "after undobreak: {line:?}");
8829 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
8830 }
8831
8832 #[test]
8833 fn undobreak_off_keeps_full_run_in_one_group() {
8834 let mut e = editor_with("");
8835 e.settings_mut().undo_break_on_motion = false;
8836 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8837 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
8840 }
8841
8842 #[test]
8843 fn undobreak_round_trips_through_options() {
8844 let e = editor_with("");
8845 let opts = e.current_options();
8846 assert!(opts.undo_break_on_motion);
8847 let mut e2 = editor_with("");
8848 let mut new_opts = opts.clone();
8849 new_opts.undo_break_on_motion = false;
8850 e2.apply_options(&new_opts);
8851 assert!(!e2.current_options().undo_break_on_motion);
8852 }
8853
8854 #[test]
8855 fn undo_levels_cap_drops_oldest() {
8856 let mut e = editor_with("abcde");
8857 e.settings_mut().undo_levels = 3;
8858 run_keys(&mut e, "ra");
8859 run_keys(&mut e, "lrb");
8860 run_keys(&mut e, "lrc");
8861 run_keys(&mut e, "lrd");
8862 run_keys(&mut e, "lre");
8863 assert_eq!(e.undo_stack_len(), 3);
8864 }
8865
8866 #[test]
8867 fn tab_inserts_literal_tab_when_noexpandtab() {
8868 let mut e = editor_with("");
8869 e.settings_mut().expandtab = false;
8872 e.settings_mut().softtabstop = 0;
8873 run_keys(&mut e, "i");
8874 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8875 assert_eq!(e.buffer.line(0).unwrap(), "\t");
8876 }
8877
8878 #[test]
8879 fn tab_inserts_spaces_when_expandtab() {
8880 let mut e = editor_with("");
8881 e.settings_mut().expandtab = true;
8882 e.settings_mut().tabstop = 4;
8883 run_keys(&mut e, "i");
8884 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8885 assert_eq!(e.buffer.line(0).unwrap(), " ");
8886 }
8887
8888 #[test]
8889 fn tab_with_softtabstop_fills_to_next_boundary() {
8890 let mut e = editor_with("ab");
8892 e.settings_mut().expandtab = true;
8893 e.settings_mut().tabstop = 8;
8894 e.settings_mut().softtabstop = 4;
8895 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8897 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
8898 }
8899
8900 #[test]
8901 fn backspace_deletes_softtab_run() {
8902 let mut e = editor_with(" x");
8905 e.settings_mut().softtabstop = 4;
8906 run_keys(&mut e, "fxi");
8908 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8909 assert_eq!(e.buffer.line(0).unwrap(), "x");
8910 }
8911
8912 #[test]
8913 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
8914 let mut e = editor_with(" x");
8917 e.settings_mut().softtabstop = 4;
8918 run_keys(&mut e, "fxi");
8919 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8920 assert_eq!(e.buffer.line(0).unwrap(), " x");
8921 }
8922
8923 #[test]
8924 fn readonly_blocks_insert_mutation() {
8925 let mut e = editor_with("hello");
8926 e.settings_mut().readonly = true;
8927 run_keys(&mut e, "iX<Esc>");
8928 assert_eq!(e.buffer.line(0).unwrap(), "hello");
8929 }
8930
8931 #[cfg(feature = "ratatui")]
8932 #[test]
8933 fn intern_ratatui_style_dedups_repeated_styles() {
8934 use ratatui::style::{Color, Style};
8935 let mut e = editor_with("");
8936 let red = Style::default().fg(Color::Red);
8937 let blue = Style::default().fg(Color::Blue);
8938 let id_r1 = e.intern_ratatui_style(red);
8939 let id_r2 = e.intern_ratatui_style(red);
8940 let id_b = e.intern_ratatui_style(blue);
8941 assert_eq!(id_r1, id_r2);
8942 assert_ne!(id_r1, id_b);
8943 assert_eq!(e.style_table().len(), 2);
8944 }
8945
8946 #[cfg(feature = "ratatui")]
8947 #[test]
8948 fn install_ratatui_syntax_spans_translates_styled_spans() {
8949 use ratatui::style::{Color, Style};
8950 let mut e = editor_with("SELECT foo");
8951 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
8952 let by_row = e.buffer_spans();
8953 assert_eq!(by_row.len(), 1);
8954 assert_eq!(by_row[0].len(), 1);
8955 assert_eq!(by_row[0][0].start_byte, 0);
8956 assert_eq!(by_row[0][0].end_byte, 6);
8957 let id = by_row[0][0].style;
8958 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
8959 }
8960
8961 #[cfg(feature = "ratatui")]
8962 #[test]
8963 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
8964 use ratatui::style::{Color, Style};
8965 let mut e = editor_with("hello");
8966 e.install_ratatui_syntax_spans(vec![vec![(
8967 0,
8968 usize::MAX,
8969 Style::default().fg(Color::Blue),
8970 )]]);
8971 let by_row = e.buffer_spans();
8972 assert_eq!(by_row[0][0].end_byte, 5);
8973 }
8974
8975 #[cfg(feature = "ratatui")]
8976 #[test]
8977 fn install_ratatui_syntax_spans_drops_zero_width() {
8978 use ratatui::style::{Color, Style};
8979 let mut e = editor_with("abc");
8980 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
8981 assert!(e.buffer_spans()[0].is_empty());
8982 }
8983
8984 #[test]
8985 fn named_register_yank_into_a_then_paste_from_a() {
8986 let mut e = editor_with("hello world\nsecond");
8987 run_keys(&mut e, "\"ayw");
8988 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8990 run_keys(&mut e, "j0\"aP");
8992 assert_eq!(e.buffer().lines()[1], "hello second");
8993 }
8994
8995 #[test]
8996 fn capital_r_overstrikes_chars() {
8997 let mut e = editor_with("hello");
8998 e.jump_cursor(0, 0);
8999 run_keys(&mut e, "RXY<Esc>");
9000 assert_eq!(e.buffer().lines()[0], "XYllo");
9002 }
9003
9004 #[test]
9005 fn capital_r_at_eol_appends() {
9006 let mut e = editor_with("hi");
9007 e.jump_cursor(0, 1);
9008 run_keys(&mut e, "RXYZ<Esc>");
9010 assert_eq!(e.buffer().lines()[0], "hXYZ");
9011 }
9012
9013 #[test]
9014 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9015 let mut e = editor_with("abc");
9019 e.jump_cursor(0, 0);
9020 run_keys(&mut e, "RX<Esc>");
9021 assert_eq!(e.buffer().lines()[0], "Xbc");
9022 }
9023
9024 #[test]
9025 fn ctrl_r_in_insert_pastes_named_register() {
9026 let mut e = editor_with("hello world");
9027 run_keys(&mut e, "\"ayw");
9029 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9030 run_keys(&mut e, "o");
9032 assert_eq!(e.vim_mode(), VimMode::Insert);
9033 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9034 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9035 assert_eq!(e.buffer().lines()[1], "hello ");
9036 assert_eq!(e.cursor(), (1, 6));
9038 assert_eq!(e.vim_mode(), VimMode::Insert);
9040 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9041 assert_eq!(e.buffer().lines()[1], "hello X");
9042 }
9043
9044 #[test]
9045 fn ctrl_r_with_unnamed_register() {
9046 let mut e = editor_with("foo");
9047 run_keys(&mut e, "yiw");
9048 run_keys(&mut e, "A ");
9049 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9051 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9052 assert_eq!(e.buffer().lines()[0], "foo foo");
9053 }
9054
9055 #[test]
9056 fn ctrl_r_unknown_selector_is_no_op() {
9057 let mut e = editor_with("abc");
9058 run_keys(&mut e, "A");
9059 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9060 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9063 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9064 assert_eq!(e.buffer().lines()[0], "abcZ");
9065 }
9066
9067 #[test]
9068 fn ctrl_r_multiline_register_pastes_with_newlines() {
9069 let mut e = editor_with("alpha\nbeta\ngamma");
9070 run_keys(&mut e, "\"byy");
9072 run_keys(&mut e, "j\"byy");
9073 run_keys(&mut e, "ggVj\"by");
9077 let payload = e.registers().read('b').unwrap().text.clone();
9078 assert!(payload.contains('\n'));
9079 run_keys(&mut e, "Go");
9080 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9081 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9082 let total_lines = e.buffer().lines().len();
9085 assert!(total_lines >= 5);
9086 }
9087
9088 #[test]
9089 fn yank_zero_holds_last_yank_after_delete() {
9090 let mut e = editor_with("hello world");
9091 run_keys(&mut e, "yw");
9092 let yanked = e.registers().read('0').unwrap().text.clone();
9093 assert!(!yanked.is_empty());
9094 run_keys(&mut e, "dw");
9096 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9097 assert!(!e.registers().read('1').unwrap().text.is_empty());
9099 }
9100
9101 #[test]
9102 fn delete_ring_rotates_through_one_through_nine() {
9103 let mut e = editor_with("a b c d e f g h i j");
9104 for _ in 0..3 {
9106 run_keys(&mut e, "dw");
9107 }
9108 let r1 = e.registers().read('1').unwrap().text.clone();
9110 let r2 = e.registers().read('2').unwrap().text.clone();
9111 let r3 = e.registers().read('3').unwrap().text.clone();
9112 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9113 assert_ne!(r1, r2);
9114 assert_ne!(r2, r3);
9115 }
9116
9117 #[test]
9118 fn capital_register_appends_to_lowercase() {
9119 let mut e = editor_with("foo bar");
9120 run_keys(&mut e, "\"ayw");
9121 let first = e.registers().read('a').unwrap().text.clone();
9122 assert!(first.contains("foo"));
9123 run_keys(&mut e, "w\"Ayw");
9125 let combined = e.registers().read('a').unwrap().text.clone();
9126 assert!(combined.starts_with(&first));
9127 assert!(combined.contains("bar"));
9128 }
9129
9130 #[test]
9131 fn zf_in_visual_line_creates_closed_fold() {
9132 let mut e = editor_with("a\nb\nc\nd\ne");
9133 e.jump_cursor(1, 0);
9135 run_keys(&mut e, "Vjjzf");
9136 assert_eq!(e.buffer().folds().len(), 1);
9137 let f = e.buffer().folds()[0];
9138 assert_eq!(f.start_row, 1);
9139 assert_eq!(f.end_row, 3);
9140 assert!(f.closed);
9141 }
9142
9143 #[test]
9144 fn zfj_in_normal_creates_two_row_fold() {
9145 let mut e = editor_with("a\nb\nc\nd\ne");
9146 e.jump_cursor(1, 0);
9147 run_keys(&mut e, "zfj");
9148 assert_eq!(e.buffer().folds().len(), 1);
9149 let f = e.buffer().folds()[0];
9150 assert_eq!(f.start_row, 1);
9151 assert_eq!(f.end_row, 2);
9152 assert!(f.closed);
9153 assert_eq!(e.cursor().0, 1);
9155 }
9156
9157 #[test]
9158 fn zf_with_count_folds_count_rows() {
9159 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9160 e.jump_cursor(0, 0);
9161 run_keys(&mut e, "zf3j");
9163 assert_eq!(e.buffer().folds().len(), 1);
9164 let f = e.buffer().folds()[0];
9165 assert_eq!(f.start_row, 0);
9166 assert_eq!(f.end_row, 3);
9167 }
9168
9169 #[test]
9170 fn zfk_folds_upward_range() {
9171 let mut e = editor_with("a\nb\nc\nd\ne");
9172 e.jump_cursor(3, 0);
9173 run_keys(&mut e, "zfk");
9174 let f = e.buffer().folds()[0];
9175 assert_eq!(f.start_row, 2);
9177 assert_eq!(f.end_row, 3);
9178 }
9179
9180 #[test]
9181 fn zf_capital_g_folds_to_bottom() {
9182 let mut e = editor_with("a\nb\nc\nd\ne");
9183 e.jump_cursor(1, 0);
9184 run_keys(&mut e, "zfG");
9186 let f = e.buffer().folds()[0];
9187 assert_eq!(f.start_row, 1);
9188 assert_eq!(f.end_row, 4);
9189 }
9190
9191 #[test]
9192 fn zfgg_folds_to_top_via_operator_pipeline() {
9193 let mut e = editor_with("a\nb\nc\nd\ne");
9194 e.jump_cursor(3, 0);
9195 run_keys(&mut e, "zfgg");
9199 let f = e.buffer().folds()[0];
9200 assert_eq!(f.start_row, 0);
9201 assert_eq!(f.end_row, 3);
9202 }
9203
9204 #[test]
9205 fn zfip_folds_paragraph_via_text_object() {
9206 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9207 e.jump_cursor(1, 0);
9208 run_keys(&mut e, "zfip");
9210 assert_eq!(e.buffer().folds().len(), 1);
9211 let f = e.buffer().folds()[0];
9212 assert_eq!(f.start_row, 0);
9213 assert_eq!(f.end_row, 2);
9214 }
9215
9216 #[test]
9217 fn zfap_folds_paragraph_with_trailing_blank() {
9218 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9219 e.jump_cursor(0, 0);
9220 run_keys(&mut e, "zfap");
9222 let f = e.buffer().folds()[0];
9223 assert_eq!(f.start_row, 0);
9224 assert_eq!(f.end_row, 3);
9225 }
9226
9227 #[test]
9228 fn zf_paragraph_motion_folds_to_blank() {
9229 let mut e = editor_with("alpha\nbeta\n\ngamma");
9230 e.jump_cursor(0, 0);
9231 run_keys(&mut e, "zf}");
9233 let f = e.buffer().folds()[0];
9234 assert_eq!(f.start_row, 0);
9235 assert_eq!(f.end_row, 2);
9236 }
9237
9238 #[test]
9239 fn za_toggles_fold_under_cursor() {
9240 let mut e = editor_with("a\nb\nc\nd");
9241 e.buffer_mut().add_fold(1, 2, true);
9242 e.jump_cursor(1, 0);
9243 run_keys(&mut e, "za");
9244 assert!(!e.buffer().folds()[0].closed);
9245 run_keys(&mut e, "za");
9246 assert!(e.buffer().folds()[0].closed);
9247 }
9248
9249 #[test]
9250 fn zr_opens_all_folds_zm_closes_all() {
9251 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9252 e.buffer_mut().add_fold(0, 1, true);
9253 e.buffer_mut().add_fold(2, 3, true);
9254 e.buffer_mut().add_fold(4, 5, true);
9255 run_keys(&mut e, "zR");
9256 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9257 run_keys(&mut e, "zM");
9258 assert!(e.buffer().folds().iter().all(|f| f.closed));
9259 }
9260
9261 #[test]
9262 fn ze_clears_all_folds() {
9263 let mut e = editor_with("a\nb\nc\nd");
9264 e.buffer_mut().add_fold(0, 1, true);
9265 e.buffer_mut().add_fold(2, 3, false);
9266 run_keys(&mut e, "zE");
9267 assert!(e.buffer().folds().is_empty());
9268 }
9269
9270 #[test]
9271 fn g_underscore_jumps_to_last_non_blank() {
9272 let mut e = editor_with("hello world ");
9273 run_keys(&mut e, "g_");
9274 assert_eq!(e.cursor().1, 10);
9276 }
9277
9278 #[test]
9279 fn gj_and_gk_alias_j_and_k() {
9280 let mut e = editor_with("a\nb\nc");
9281 run_keys(&mut e, "gj");
9282 assert_eq!(e.cursor().0, 1);
9283 run_keys(&mut e, "gk");
9284 assert_eq!(e.cursor().0, 0);
9285 }
9286
9287 #[test]
9288 fn paragraph_motions_walk_blank_lines() {
9289 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9290 run_keys(&mut e, "}");
9291 assert_eq!(e.cursor().0, 2);
9292 run_keys(&mut e, "}");
9293 assert_eq!(e.cursor().0, 5);
9294 run_keys(&mut e, "{");
9295 assert_eq!(e.cursor().0, 2);
9296 }
9297
9298 #[test]
9299 fn gv_reenters_last_visual_selection() {
9300 let mut e = editor_with("alpha\nbeta\ngamma");
9301 run_keys(&mut e, "Vj");
9302 run_keys(&mut e, "<Esc>");
9304 assert_eq!(e.vim_mode(), VimMode::Normal);
9305 run_keys(&mut e, "gv");
9307 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9308 }
9309
9310 #[test]
9311 fn o_in_visual_swaps_anchor_and_cursor() {
9312 let mut e = editor_with("hello world");
9313 run_keys(&mut e, "vllll");
9315 assert_eq!(e.cursor().1, 4);
9316 run_keys(&mut e, "o");
9318 assert_eq!(e.cursor().1, 0);
9319 assert_eq!(e.vim.visual_anchor, (0, 4));
9321 }
9322
9323 #[test]
9324 fn editing_inside_fold_invalidates_it() {
9325 let mut e = editor_with("a\nb\nc\nd");
9326 e.buffer_mut().add_fold(1, 2, true);
9327 e.jump_cursor(1, 0);
9328 run_keys(&mut e, "iX<Esc>");
9330 assert!(e.buffer().folds().is_empty());
9332 }
9333
9334 #[test]
9335 fn zd_removes_fold_under_cursor() {
9336 let mut e = editor_with("a\nb\nc\nd");
9337 e.buffer_mut().add_fold(1, 2, true);
9338 e.jump_cursor(2, 0);
9339 run_keys(&mut e, "zd");
9340 assert!(e.buffer().folds().is_empty());
9341 }
9342
9343 #[test]
9344 fn take_fold_ops_observes_z_keystroke_dispatch() {
9345 use crate::types::FoldOp;
9350 let mut e = editor_with("a\nb\nc\nd");
9351 e.buffer_mut().add_fold(1, 2, true);
9352 e.jump_cursor(1, 0);
9353 let _ = e.take_fold_ops();
9356 run_keys(&mut e, "zo");
9357 run_keys(&mut e, "zM");
9358 let ops = e.take_fold_ops();
9359 assert_eq!(ops.len(), 2);
9360 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9361 assert!(matches!(ops[1], FoldOp::CloseAll));
9362 assert!(e.take_fold_ops().is_empty());
9364 }
9365
9366 #[test]
9367 fn edit_pipeline_emits_invalidate_fold_op() {
9368 use crate::types::FoldOp;
9371 let mut e = editor_with("a\nb\nc\nd");
9372 e.buffer_mut().add_fold(1, 2, true);
9373 e.jump_cursor(1, 0);
9374 let _ = e.take_fold_ops();
9375 run_keys(&mut e, "iX<Esc>");
9376 let ops = e.take_fold_ops();
9377 assert!(
9378 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9379 "expected at least one Invalidate op, got {ops:?}"
9380 );
9381 }
9382
9383 #[test]
9384 fn dot_mark_jumps_to_last_edit_position() {
9385 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9386 e.jump_cursor(2, 0);
9387 run_keys(&mut e, "iX<Esc>");
9389 let after_edit = e.cursor();
9390 run_keys(&mut e, "gg");
9392 assert_eq!(e.cursor().0, 0);
9393 run_keys(&mut e, "'.");
9395 assert_eq!(e.cursor().0, after_edit.0);
9396 }
9397
9398 #[test]
9399 fn quote_quote_returns_to_pre_jump_position() {
9400 let mut e = editor_with_rows(50, 20);
9401 e.jump_cursor(10, 2);
9402 let before = e.cursor();
9403 run_keys(&mut e, "G");
9405 assert_ne!(e.cursor(), before);
9406 run_keys(&mut e, "''");
9408 assert_eq!(e.cursor().0, before.0);
9409 }
9410
9411 #[test]
9412 fn backtick_backtick_restores_exact_pre_jump_pos() {
9413 let mut e = editor_with_rows(50, 20);
9414 e.jump_cursor(7, 3);
9415 let before = e.cursor();
9416 run_keys(&mut e, "G");
9417 run_keys(&mut e, "``");
9418 assert_eq!(e.cursor(), before);
9419 }
9420
9421 #[test]
9422 fn macro_record_and_replay_basic() {
9423 let mut e = editor_with("foo\nbar\nbaz");
9424 run_keys(&mut e, "qaIX<Esc>jq");
9426 assert_eq!(e.buffer().lines()[0], "Xfoo");
9427 run_keys(&mut e, "@a");
9429 assert_eq!(e.buffer().lines()[1], "Xbar");
9430 run_keys(&mut e, "j@@");
9432 assert_eq!(e.buffer().lines()[2], "Xbaz");
9433 }
9434
9435 #[test]
9436 fn macro_count_replays_n_times() {
9437 let mut e = editor_with("a\nb\nc\nd\ne");
9438 run_keys(&mut e, "qajq");
9440 assert_eq!(e.cursor().0, 1);
9441 run_keys(&mut e, "3@a");
9443 assert_eq!(e.cursor().0, 4);
9444 }
9445
9446 #[test]
9447 fn macro_capital_q_appends_to_lowercase_register() {
9448 let mut e = editor_with("hello");
9449 run_keys(&mut e, "qall<Esc>q");
9450 run_keys(&mut e, "qAhh<Esc>q");
9451 let text = e.registers().read('a').unwrap().text.clone();
9454 assert!(text.contains("ll<Esc>"));
9455 assert!(text.contains("hh<Esc>"));
9456 }
9457
9458 #[test]
9459 fn buffer_selection_block_in_visual_block_mode() {
9460 use hjkl_buffer::{Position, Selection};
9461 let mut e = editor_with("aaaa\nbbbb\ncccc");
9462 run_keys(&mut e, "<C-v>jl");
9463 assert_eq!(
9464 e.buffer_selection(),
9465 Some(Selection::Block {
9466 anchor: Position::new(0, 0),
9467 head: Position::new(1, 1),
9468 })
9469 );
9470 }
9471
9472 #[test]
9475 fn n_after_question_mark_keeps_walking_backward() {
9476 let mut e = editor_with("foo bar foo baz foo end");
9479 e.jump_cursor(0, 22);
9480 run_keys(&mut e, "?foo<CR>");
9481 assert_eq!(e.cursor().1, 16);
9482 run_keys(&mut e, "n");
9483 assert_eq!(e.cursor().1, 8);
9484 run_keys(&mut e, "N");
9485 assert_eq!(e.cursor().1, 16);
9486 }
9487
9488 #[test]
9489 fn nested_macro_chord_records_literal_keys() {
9490 let mut e = editor_with("alpha\nbeta\ngamma");
9493 run_keys(&mut e, "qblq");
9495 run_keys(&mut e, "qaIX<Esc>q");
9498 e.jump_cursor(1, 0);
9500 run_keys(&mut e, "@a");
9501 assert_eq!(e.buffer().lines()[1], "Xbeta");
9502 }
9503
9504 #[test]
9505 fn shift_gt_motion_indents_one_line() {
9506 let mut e = editor_with("hello world");
9510 run_keys(&mut e, ">w");
9511 assert_eq!(e.buffer().lines()[0], " hello world");
9512 }
9513
9514 #[test]
9515 fn shift_lt_motion_outdents_one_line() {
9516 let mut e = editor_with(" hello world");
9517 run_keys(&mut e, "<lt>w");
9518 assert_eq!(e.buffer().lines()[0], " hello world");
9520 }
9521
9522 #[test]
9523 fn shift_gt_text_object_indents_paragraph() {
9524 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9525 e.jump_cursor(0, 0);
9526 run_keys(&mut e, ">ip");
9527 assert_eq!(e.buffer().lines()[0], " alpha");
9528 assert_eq!(e.buffer().lines()[1], " beta");
9529 assert_eq!(e.buffer().lines()[2], " gamma");
9530 assert_eq!(e.buffer().lines()[4], "rest");
9532 }
9533
9534 #[test]
9535 fn ctrl_o_runs_exactly_one_normal_command() {
9536 let mut e = editor_with("alpha beta gamma");
9539 e.jump_cursor(0, 0);
9540 run_keys(&mut e, "i");
9541 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9542 run_keys(&mut e, "dw");
9543 assert_eq!(e.vim_mode(), VimMode::Insert);
9545 run_keys(&mut e, "X");
9547 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9548 }
9549
9550 #[test]
9551 fn macro_replay_respects_mode_switching() {
9552 let mut e = editor_with("hi");
9556 run_keys(&mut e, "qaiX<Esc>0q");
9557 assert_eq!(e.vim_mode(), VimMode::Normal);
9558 e.set_content("yo");
9560 run_keys(&mut e, "@a");
9561 assert_eq!(e.vim_mode(), VimMode::Normal);
9562 assert_eq!(e.cursor().1, 0);
9563 assert_eq!(e.buffer().lines()[0], "Xyo");
9564 }
9565
9566 #[test]
9567 fn macro_recorded_text_round_trips_through_register() {
9568 let mut e = editor_with("");
9572 run_keys(&mut e, "qaiX<Esc>q");
9573 let text = e.registers().read('a').unwrap().text.clone();
9574 assert!(text.starts_with("iX"));
9575 run_keys(&mut e, "@a");
9577 assert_eq!(e.buffer().lines()[0], "XX");
9578 }
9579
9580 #[test]
9581 fn dot_after_macro_replays_macros_last_change() {
9582 let mut e = editor_with("ab\ncd\nef");
9585 run_keys(&mut e, "qaIX<Esc>jq");
9588 assert_eq!(e.buffer().lines()[0], "Xab");
9589 run_keys(&mut e, "@a");
9590 assert_eq!(e.buffer().lines()[1], "Xcd");
9591 let row_before_dot = e.cursor().0;
9594 run_keys(&mut e, ".");
9595 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9596 }
9597
9598 fn si_editor(content: &str) -> Editor {
9604 let opts = crate::types::Options {
9605 shiftwidth: 4,
9606 softtabstop: 4,
9607 expandtab: true,
9608 smartindent: true,
9609 autoindent: true,
9610 ..crate::types::Options::default()
9611 };
9612 let mut e = Editor::new(
9613 hjkl_buffer::Buffer::new(),
9614 crate::types::DefaultHost::new(),
9615 opts,
9616 );
9617 e.set_content(content);
9618 e
9619 }
9620
9621 #[test]
9622 fn smartindent_bumps_indent_after_open_brace() {
9623 let mut e = si_editor("fn foo() {");
9625 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9627 assert_eq!(
9628 e.buffer().lines()[1],
9629 " ",
9630 "smartindent should bump one shiftwidth after {{"
9631 );
9632 }
9633
9634 #[test]
9635 fn smartindent_no_bump_when_off() {
9636 let mut e = si_editor("fn foo() {");
9639 e.settings_mut().smartindent = false;
9640 e.jump_cursor(0, 10);
9641 run_keys(&mut e, "i<CR>");
9642 assert_eq!(
9643 e.buffer().lines()[1],
9644 "",
9645 "without smartindent, no bump: new line copies empty leading ws"
9646 );
9647 }
9648
9649 #[test]
9650 fn smartindent_uses_tab_when_noexpandtab() {
9651 let opts = crate::types::Options {
9653 shiftwidth: 4,
9654 softtabstop: 0,
9655 expandtab: false,
9656 smartindent: true,
9657 autoindent: true,
9658 ..crate::types::Options::default()
9659 };
9660 let mut e = Editor::new(
9661 hjkl_buffer::Buffer::new(),
9662 crate::types::DefaultHost::new(),
9663 opts,
9664 );
9665 e.set_content("fn foo() {");
9666 e.jump_cursor(0, 10);
9667 run_keys(&mut e, "i<CR>");
9668 assert_eq!(
9669 e.buffer().lines()[1],
9670 "\t",
9671 "noexpandtab: smartindent bump inserts a literal tab"
9672 );
9673 }
9674
9675 #[test]
9676 fn smartindent_dedent_on_close_brace() {
9677 let mut e = si_editor("fn foo() {");
9680 e.set_content("fn foo() {\n ");
9682 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9684 assert_eq!(
9685 e.buffer().lines()[1],
9686 "}",
9687 "close brace on whitespace-only line should dedent"
9688 );
9689 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9690 }
9691
9692 #[test]
9693 fn smartindent_no_dedent_when_off() {
9694 let mut e = si_editor("fn foo() {\n ");
9696 e.settings_mut().smartindent = false;
9697 e.jump_cursor(1, 4);
9698 run_keys(&mut e, "i}");
9699 assert_eq!(
9700 e.buffer().lines()[1],
9701 " }",
9702 "without smartindent, `}}` just appends at cursor"
9703 );
9704 }
9705
9706 #[test]
9707 fn smartindent_no_dedent_mid_line() {
9708 let mut e = si_editor(" let x = 1");
9711 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9713 assert_eq!(
9714 e.buffer().lines()[0],
9715 " let x = 1}",
9716 "mid-line `}}` should not dedent"
9717 );
9718 }
9719
9720 #[test]
9724 fn count_5x_fills_unnamed_register() {
9725 let mut e = editor_with("hello world\n");
9726 e.jump_cursor(0, 0);
9727 run_keys(&mut e, "5x");
9728 assert_eq!(e.buffer().lines()[0], " world");
9729 assert_eq!(e.cursor(), (0, 0));
9730 assert_eq!(e.yank(), "hello");
9731 }
9732
9733 #[test]
9734 fn x_fills_unnamed_register_single_char() {
9735 let mut e = editor_with("abc\n");
9736 e.jump_cursor(0, 0);
9737 run_keys(&mut e, "x");
9738 assert_eq!(e.buffer().lines()[0], "bc");
9739 assert_eq!(e.yank(), "a");
9740 }
9741
9742 #[test]
9743 fn big_x_fills_unnamed_register() {
9744 let mut e = editor_with("hello\n");
9745 e.jump_cursor(0, 3);
9746 run_keys(&mut e, "X");
9747 assert_eq!(e.buffer().lines()[0], "helo");
9748 assert_eq!(e.yank(), "l");
9749 }
9750
9751 #[test]
9753 fn g_motion_trailing_newline_lands_on_last_content_row() {
9754 let mut e = editor_with("foo\nbar\nbaz\n");
9755 e.jump_cursor(0, 0);
9756 run_keys(&mut e, "G");
9757 assert_eq!(
9759 e.cursor().0,
9760 2,
9761 "G should land on row 2 (baz), not row 3 (phantom empty)"
9762 );
9763 }
9764
9765 #[test]
9767 fn dd_last_line_clamps_cursor_to_new_last_row() {
9768 let mut e = editor_with("foo\nbar\n");
9769 e.jump_cursor(1, 0);
9770 run_keys(&mut e, "dd");
9771 assert_eq!(e.buffer().lines()[0], "foo");
9772 assert_eq!(
9773 e.cursor(),
9774 (0, 0),
9775 "cursor should clamp to row 0 after dd on last content line"
9776 );
9777 }
9778
9779 #[test]
9781 fn d_dollar_cursor_on_last_char() {
9782 let mut e = editor_with("hello world\n");
9783 e.jump_cursor(0, 5);
9784 run_keys(&mut e, "d$");
9785 assert_eq!(e.buffer().lines()[0], "hello");
9786 assert_eq!(
9787 e.cursor(),
9788 (0, 4),
9789 "d$ should leave cursor on col 4, not col 5"
9790 );
9791 }
9792
9793 #[test]
9795 fn undo_insert_clamps_cursor_to_last_valid_col() {
9796 let mut e = editor_with("hello\n");
9797 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
9799 assert_eq!(e.buffer().lines()[0], "hello");
9800 assert_eq!(
9801 e.cursor(),
9802 (0, 4),
9803 "undo should clamp cursor to col 4 on 'hello'"
9804 );
9805 }
9806
9807 #[test]
9809 fn da_doublequote_eats_trailing_whitespace() {
9810 let mut e = editor_with("say \"hello\" there\n");
9811 e.jump_cursor(0, 6);
9812 run_keys(&mut e, "da\"");
9813 assert_eq!(e.buffer().lines()[0], "say there");
9814 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
9815 }
9816
9817 #[test]
9819 fn dab_cursor_col_clamped_after_delete() {
9820 let mut e = editor_with("fn x() {\n body\n}\n");
9821 e.jump_cursor(1, 4);
9822 run_keys(&mut e, "daB");
9823 assert_eq!(e.buffer().lines()[0], "fn x() ");
9824 assert_eq!(
9825 e.cursor(),
9826 (0, 6),
9827 "daB should leave cursor at col 6, not 7"
9828 );
9829 }
9830
9831 #[test]
9833 fn dib_preserves_surrounding_newlines() {
9834 let mut e = editor_with("{\n body\n}\n");
9835 e.jump_cursor(1, 4);
9836 run_keys(&mut e, "diB");
9837 assert_eq!(e.buffer().lines()[0], "{");
9838 assert_eq!(e.buffer().lines()[1], "}");
9839 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
9840 }
9841
9842 #[test]
9843 fn is_chord_pending_tracks_replace_state() {
9844 let mut e = editor_with("abc\n");
9845 assert!(!e.is_chord_pending());
9846 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
9848 assert!(e.is_chord_pending(), "engine should be pending after r");
9849 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
9851 assert!(
9852 !e.is_chord_pending(),
9853 "engine pending should clear after replace"
9854 );
9855 }
9856}