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 pending_op_char(&self) -> Option<char> {
583 let op = match &self.pending {
584 Pending::Op { op, .. }
585 | Pending::OpTextObj { op, .. }
586 | Pending::OpG { op, .. }
587 | Pending::OpFind { op, .. } => Some(*op),
588 _ => None,
589 };
590 op.map(|o| match o {
591 Operator::Delete => 'd',
592 Operator::Change => 'c',
593 Operator::Yank => 'y',
594 Operator::Uppercase => 'U',
595 Operator::Lowercase => 'u',
596 Operator::ToggleCase => '~',
597 Operator::Indent => '>',
598 Operator::Outdent => '<',
599 Operator::Fold => 'z',
600 Operator::Reflow => 'q',
601 })
602 }
603}
604
605fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
611 ed.vim.search_prompt = Some(SearchPrompt {
612 text: String::new(),
613 cursor: 0,
614 forward,
615 });
616 ed.vim.search_history_cursor = None;
617 ed.set_search_pattern(None);
621}
622
623fn push_search_pattern<H: crate::types::Host>(
628 ed: &mut Editor<hjkl_buffer::Buffer, H>,
629 pattern: &str,
630) {
631 let compiled = if pattern.is_empty() {
632 None
633 } else {
634 let case_insensitive = ed.settings().ignore_case
641 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
642 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
643 std::borrow::Cow::Owned(format!("(?i){pattern}"))
644 } else {
645 std::borrow::Cow::Borrowed(pattern)
646 };
647 regex::Regex::new(&effective).ok()
648 };
649 let wrap = ed.settings().wrapscan;
650 ed.set_search_pattern(compiled);
654 ed.search_state_mut().wrap_around = wrap;
655}
656
657fn step_search_prompt<H: crate::types::Host>(
658 ed: &mut Editor<hjkl_buffer::Buffer, H>,
659 input: Input,
660) -> bool {
661 let history_dir = match (input.key, input.ctrl) {
665 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
666 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
667 _ => None,
668 };
669 if let Some(dir) = history_dir {
670 walk_search_history(ed, dir);
671 return true;
672 }
673 match input.key {
674 Key::Esc => {
675 let text = ed
678 .vim
679 .search_prompt
680 .take()
681 .map(|p| p.text)
682 .unwrap_or_default();
683 if !text.is_empty() {
684 ed.vim.last_search = Some(text);
685 }
686 ed.vim.search_history_cursor = None;
687 }
688 Key::Enter => {
689 let prompt = ed.vim.search_prompt.take();
690 if let Some(p) = prompt {
691 let pattern = if p.text.is_empty() {
694 ed.vim.last_search.clone()
695 } else {
696 Some(p.text.clone())
697 };
698 if let Some(pattern) = pattern {
699 push_search_pattern(ed, &pattern);
700 let pre = ed.cursor();
701 if p.forward {
702 ed.search_advance_forward(true);
703 } else {
704 ed.search_advance_backward(true);
705 }
706 ed.push_buffer_cursor_to_textarea();
707 if ed.cursor() != pre {
708 push_jump(ed, pre);
709 }
710 record_search_history(ed, &pattern);
711 ed.vim.last_search = Some(pattern);
712 ed.vim.last_search_forward = p.forward;
713 }
714 }
715 ed.vim.search_history_cursor = None;
716 }
717 Key::Backspace => {
718 ed.vim.search_history_cursor = None;
719 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
720 if p.text.pop().is_some() {
721 p.cursor = p.text.chars().count();
722 Some(p.text.clone())
723 } else {
724 None
725 }
726 });
727 if let Some(text) = new_text {
728 push_search_pattern(ed, &text);
729 }
730 }
731 Key::Char(c) => {
732 ed.vim.search_history_cursor = None;
733 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
734 p.text.push(c);
735 p.cursor = p.text.chars().count();
736 p.text.clone()
737 });
738 if let Some(text) = new_text {
739 push_search_pattern(ed, &text);
740 }
741 }
742 _ => {}
743 }
744 true
745}
746
747fn walk_change_list<H: crate::types::Host>(
751 ed: &mut Editor<hjkl_buffer::Buffer, H>,
752 dir: isize,
753 count: usize,
754) {
755 if ed.vim.change_list.is_empty() {
756 return;
757 }
758 let len = ed.vim.change_list.len();
759 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
760 (None, -1) => len as isize - 1,
761 (None, 1) => return, (Some(i), -1) => i as isize - 1,
763 (Some(i), 1) => i as isize + 1,
764 _ => return,
765 };
766 for _ in 1..count {
767 let next = idx + dir;
768 if next < 0 || next >= len as isize {
769 break;
770 }
771 idx = next;
772 }
773 if idx < 0 || idx >= len as isize {
774 return;
775 }
776 let idx = idx as usize;
777 ed.vim.change_list_cursor = Some(idx);
778 let (row, col) = ed.vim.change_list[idx];
779 ed.jump_cursor(row, col);
780}
781
782fn record_search_history<H: crate::types::Host>(
786 ed: &mut Editor<hjkl_buffer::Buffer, H>,
787 pattern: &str,
788) {
789 if pattern.is_empty() {
790 return;
791 }
792 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
793 return;
794 }
795 ed.vim.search_history.push(pattern.to_string());
796 let len = ed.vim.search_history.len();
797 if len > SEARCH_HISTORY_MAX {
798 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
799 }
800}
801
802fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
808 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
809 return;
810 }
811 let len = ed.vim.search_history.len();
812 let next_idx = match (ed.vim.search_history_cursor, dir) {
813 (None, -1) => Some(len - 1),
814 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
816 (Some(i), 1) if i + 1 < len => Some(i + 1),
817 _ => None,
818 };
819 let Some(idx) = next_idx else {
820 return;
821 };
822 ed.vim.search_history_cursor = Some(idx);
823 let text = ed.vim.search_history[idx].clone();
824 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
825 prompt.cursor = text.chars().count();
826 prompt.text = text.clone();
827 }
828 push_search_pattern(ed, &text);
829}
830
831pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
832 ed.sync_buffer_content_from_textarea();
837 let now = std::time::Instant::now();
845 let host_now = ed.host.now();
846 let timed_out = match ed.vim.last_input_host_at {
847 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
848 None => false,
849 };
850 if timed_out {
851 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
852 || ed.vim.count != 0
853 || ed.vim.pending_register.is_some()
854 || ed.vim.insert_pending_register;
855 if chord_in_flight {
856 ed.vim.clear_pending_prefix();
857 }
858 }
859 ed.vim.last_input_at = Some(now);
860 ed.vim.last_input_host_at = Some(host_now);
861 if ed.vim.recording_macro.is_some()
866 && !ed.vim.replaying_macro
867 && matches!(ed.vim.pending, Pending::None)
868 && ed.vim.mode != Mode::Insert
869 && input.key == Key::Char('q')
870 && !input.ctrl
871 && !input.alt
872 {
873 let reg = ed.vim.recording_macro.take().unwrap();
874 let keys = std::mem::take(&mut ed.vim.recording_keys);
875 let text = crate::input::encode_macro(&keys);
876 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
877 return true;
878 }
879 if ed.vim.search_prompt.is_some() {
881 return step_search_prompt(ed, input);
882 }
883 let pending_was_macro_chord = matches!(
887 ed.vim.pending,
888 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
889 );
890 let was_insert = ed.vim.mode == Mode::Insert;
891 let pre_visual_snapshot = match ed.vim.mode {
894 Mode::Visual => Some(LastVisual {
895 mode: Mode::Visual,
896 anchor: ed.vim.visual_anchor,
897 cursor: ed.cursor(),
898 block_vcol: 0,
899 }),
900 Mode::VisualLine => Some(LastVisual {
901 mode: Mode::VisualLine,
902 anchor: (ed.vim.visual_line_anchor, 0),
903 cursor: ed.cursor(),
904 block_vcol: 0,
905 }),
906 Mode::VisualBlock => Some(LastVisual {
907 mode: Mode::VisualBlock,
908 anchor: ed.vim.block_anchor,
909 cursor: ed.cursor(),
910 block_vcol: ed.vim.block_vcol,
911 }),
912 _ => None,
913 };
914 let consumed = match ed.vim.mode {
915 Mode::Insert => step_insert(ed, input),
916 _ => step_normal(ed, input),
917 };
918 if let Some(snap) = pre_visual_snapshot
919 && !matches!(
920 ed.vim.mode,
921 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
922 )
923 {
924 ed.vim.last_visual = Some(snap);
925 }
926 if !was_insert
930 && ed.vim.one_shot_normal
931 && ed.vim.mode == Mode::Normal
932 && matches!(ed.vim.pending, Pending::None)
933 {
934 ed.vim.one_shot_normal = false;
935 ed.vim.mode = Mode::Insert;
936 }
937 ed.sync_buffer_content_from_textarea();
943 if !ed.vim.viewport_pinned {
947 ed.ensure_cursor_in_scrolloff();
948 }
949 ed.vim.viewport_pinned = false;
950 if ed.vim.recording_macro.is_some()
955 && !ed.vim.replaying_macro
956 && input.key != Key::Char('q')
957 && !pending_was_macro_chord
958 {
959 ed.vim.recording_keys.push(input);
960 }
961 consumed
962}
963
964fn step_insert<H: crate::types::Host>(
967 ed: &mut Editor<hjkl_buffer::Buffer, H>,
968 input: Input,
969) -> bool {
970 if ed.vim.insert_pending_register {
974 ed.vim.insert_pending_register = false;
975 if let Key::Char(c) = input.key
976 && !input.ctrl
977 {
978 insert_register_text(ed, c);
979 }
980 return true;
981 }
982
983 if input.key == Key::Esc {
984 finish_insert_session(ed);
985 ed.vim.mode = Mode::Normal;
986 let col = ed.cursor().1;
991 if col > 0 {
992 crate::motions::move_left(&mut ed.buffer, 1);
993 ed.push_buffer_cursor_to_textarea();
994 }
995 ed.sticky_col = Some(ed.cursor().1);
996 return true;
997 }
998
999 if input.ctrl {
1001 match input.key {
1002 Key::Char('w') => {
1003 use hjkl_buffer::{Edit, MotionKind};
1004 ed.sync_buffer_content_from_textarea();
1005 let cursor = buf_cursor_pos(&ed.buffer);
1006 if cursor.row == 0 && cursor.col == 0 {
1007 return true;
1008 }
1009 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1012 let word_start = buf_cursor_pos(&ed.buffer);
1013 if word_start == cursor {
1014 return true;
1015 }
1016 buf_set_cursor_pos(&mut ed.buffer, cursor);
1017 ed.mutate_edit(Edit::DeleteRange {
1018 start: word_start,
1019 end: cursor,
1020 kind: MotionKind::Char,
1021 });
1022 ed.push_buffer_cursor_to_textarea();
1023 return true;
1024 }
1025 Key::Char('u') => {
1026 use hjkl_buffer::{Edit, MotionKind, Position};
1027 ed.sync_buffer_content_from_textarea();
1028 let cursor = buf_cursor_pos(&ed.buffer);
1029 if cursor.col > 0 {
1030 ed.mutate_edit(Edit::DeleteRange {
1031 start: Position::new(cursor.row, 0),
1032 end: cursor,
1033 kind: MotionKind::Char,
1034 });
1035 ed.push_buffer_cursor_to_textarea();
1036 }
1037 return true;
1038 }
1039 Key::Char('h') => {
1040 use hjkl_buffer::{Edit, MotionKind, Position};
1041 ed.sync_buffer_content_from_textarea();
1042 let cursor = buf_cursor_pos(&ed.buffer);
1043 if cursor.col > 0 {
1044 ed.mutate_edit(Edit::DeleteRange {
1045 start: Position::new(cursor.row, cursor.col - 1),
1046 end: cursor,
1047 kind: MotionKind::Char,
1048 });
1049 } else if cursor.row > 0 {
1050 let prev_row = cursor.row - 1;
1051 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1052 ed.mutate_edit(Edit::JoinLines {
1053 row: prev_row,
1054 count: 1,
1055 with_space: false,
1056 });
1057 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1058 }
1059 ed.push_buffer_cursor_to_textarea();
1060 return true;
1061 }
1062 Key::Char('o') => {
1063 ed.vim.one_shot_normal = true;
1066 ed.vim.mode = Mode::Normal;
1067 return true;
1068 }
1069 Key::Char('r') => {
1070 ed.vim.insert_pending_register = true;
1073 return true;
1074 }
1075 Key::Char('t') => {
1076 let (row, col) = ed.cursor();
1081 let sw = ed.settings().shiftwidth;
1082 indent_rows(ed, row, row, 1);
1083 ed.jump_cursor(row, col + sw);
1084 return true;
1085 }
1086 Key::Char('d') => {
1087 let (row, col) = ed.cursor();
1091 let before_len = buf_line_bytes(&ed.buffer, row);
1092 outdent_rows(ed, row, row, 1);
1093 let after_len = buf_line_bytes(&ed.buffer, row);
1094 let stripped = before_len.saturating_sub(after_len);
1095 let new_col = col.saturating_sub(stripped);
1096 ed.jump_cursor(row, new_col);
1097 return true;
1098 }
1099 _ => {}
1100 }
1101 }
1102
1103 let (row, _) = ed.cursor();
1106 if let Some(ref mut session) = ed.vim.insert_session {
1107 session.row_min = session.row_min.min(row);
1108 session.row_max = session.row_max.max(row);
1109 }
1110 let mutated = handle_insert_key(ed, input);
1111 if mutated {
1112 ed.mark_content_dirty();
1113 let (row, _) = ed.cursor();
1114 if let Some(ref mut session) = ed.vim.insert_session {
1115 session.row_min = session.row_min.min(row);
1116 session.row_max = session.row_max.max(row);
1117 }
1118 }
1119 true
1120}
1121
1122fn insert_register_text<H: crate::types::Host>(
1127 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1128 selector: char,
1129) {
1130 use hjkl_buffer::Edit;
1131 let text = match ed.registers().read(selector) {
1132 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1133 _ => return,
1134 };
1135 ed.sync_buffer_content_from_textarea();
1136 let cursor = buf_cursor_pos(&ed.buffer);
1137 ed.mutate_edit(Edit::InsertStr {
1138 at: cursor,
1139 text: text.clone(),
1140 });
1141 let mut row = cursor.row;
1144 let mut col = cursor.col;
1145 for ch in text.chars() {
1146 if ch == '\n' {
1147 row += 1;
1148 col = 0;
1149 } else {
1150 col += 1;
1151 }
1152 }
1153 buf_set_cursor_rc(&mut ed.buffer, row, col);
1154 ed.push_buffer_cursor_to_textarea();
1155 ed.mark_content_dirty();
1156 if let Some(ref mut session) = ed.vim.insert_session {
1157 session.row_min = session.row_min.min(row);
1158 session.row_max = session.row_max.max(row);
1159 }
1160}
1161
1162pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1181 if !settings.autoindent {
1182 return String::new();
1183 }
1184 let base: String = prev_line
1186 .chars()
1187 .take_while(|c| *c == ' ' || *c == '\t')
1188 .collect();
1189
1190 if settings.smartindent {
1191 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1195 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1196 let unit = if settings.expandtab {
1197 if settings.softtabstop > 0 {
1198 " ".repeat(settings.softtabstop)
1199 } else {
1200 " ".repeat(settings.shiftwidth)
1201 }
1202 } else {
1203 "\t".to_string()
1204 };
1205 return format!("{base}{unit}");
1206 }
1207 }
1208
1209 base
1210}
1211
1212fn try_dedent_close_bracket<H: crate::types::Host>(
1222 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1223 cursor: hjkl_buffer::Position,
1224 ch: char,
1225) -> bool {
1226 use hjkl_buffer::{Edit, MotionKind, Position};
1227
1228 if !ed.settings.smartindent {
1229 return false;
1230 }
1231 if !matches!(ch, '}' | ')' | ']') {
1232 return false;
1233 }
1234
1235 let line = match buf_line(&ed.buffer, cursor.row) {
1236 Some(l) => l.to_string(),
1237 None => return false,
1238 };
1239
1240 let before: String = line.chars().take(cursor.col).collect();
1242 if !before.chars().all(|c| c == ' ' || c == '\t') {
1243 return false;
1244 }
1245 if before.is_empty() {
1246 return false;
1248 }
1249
1250 let unit_len: usize = if ed.settings.expandtab {
1252 if ed.settings.softtabstop > 0 {
1253 ed.settings.softtabstop
1254 } else {
1255 ed.settings.shiftwidth
1256 }
1257 } else {
1258 1
1260 };
1261
1262 let strip_len = if ed.settings.expandtab {
1264 let spaces = before.chars().filter(|c| *c == ' ').count();
1266 if spaces < unit_len {
1267 return false;
1268 }
1269 unit_len
1270 } else {
1271 if !before.starts_with('\t') {
1273 return false;
1274 }
1275 1
1276 };
1277
1278 ed.mutate_edit(Edit::DeleteRange {
1280 start: Position::new(cursor.row, 0),
1281 end: Position::new(cursor.row, strip_len),
1282 kind: MotionKind::Char,
1283 });
1284 let new_col = cursor.col.saturating_sub(strip_len);
1289 ed.mutate_edit(Edit::InsertChar {
1290 at: Position::new(cursor.row, new_col),
1291 ch,
1292 });
1293 true
1294}
1295
1296fn handle_insert_key<H: crate::types::Host>(
1303 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1304 input: Input,
1305) -> bool {
1306 use hjkl_buffer::{Edit, MotionKind, Position};
1307 ed.sync_buffer_content_from_textarea();
1308 let cursor = buf_cursor_pos(&ed.buffer);
1309 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1310 let in_replace = matches!(
1314 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1315 Some(InsertReason::Replace)
1316 );
1317 let mutated = match input.key {
1318 Key::Char(c) if in_replace && cursor.col < line_chars => {
1319 ed.mutate_edit(Edit::DeleteRange {
1320 start: cursor,
1321 end: Position::new(cursor.row, cursor.col + 1),
1322 kind: MotionKind::Char,
1323 });
1324 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1325 true
1326 }
1327 Key::Char(c) => {
1328 if !try_dedent_close_bracket(ed, cursor, c) {
1329 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1330 }
1331 true
1332 }
1333 Key::Enter => {
1334 let prev_line = buf_line(&ed.buffer, cursor.row)
1335 .unwrap_or_default()
1336 .to_string();
1337 let indent = compute_enter_indent(&ed.settings, &prev_line);
1338 let text = format!("\n{indent}");
1339 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1340 true
1341 }
1342 Key::Tab => {
1343 if ed.settings.expandtab {
1344 let sts = ed.settings.softtabstop;
1347 let n = if sts > 0 {
1348 sts - (cursor.col % sts)
1349 } else {
1350 ed.settings.tabstop.max(1)
1351 };
1352 ed.mutate_edit(Edit::InsertStr {
1353 at: cursor,
1354 text: " ".repeat(n),
1355 });
1356 } else {
1357 ed.mutate_edit(Edit::InsertChar {
1358 at: cursor,
1359 ch: '\t',
1360 });
1361 }
1362 true
1363 }
1364 Key::Backspace => {
1365 let sts = ed.settings.softtabstop;
1369 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1370 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1371 let chars: Vec<char> = line.chars().collect();
1372 let run_start = cursor.col - sts;
1373 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1374 ed.mutate_edit(Edit::DeleteRange {
1375 start: Position::new(cursor.row, run_start),
1376 end: cursor,
1377 kind: MotionKind::Char,
1378 });
1379 return true;
1380 }
1381 }
1382 if cursor.col > 0 {
1383 ed.mutate_edit(Edit::DeleteRange {
1384 start: Position::new(cursor.row, cursor.col - 1),
1385 end: cursor,
1386 kind: MotionKind::Char,
1387 });
1388 true
1389 } else if cursor.row > 0 {
1390 let prev_row = cursor.row - 1;
1391 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1392 ed.mutate_edit(Edit::JoinLines {
1393 row: prev_row,
1394 count: 1,
1395 with_space: false,
1396 });
1397 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1398 true
1399 } else {
1400 false
1401 }
1402 }
1403 Key::Delete => {
1404 if cursor.col < line_chars {
1405 ed.mutate_edit(Edit::DeleteRange {
1406 start: cursor,
1407 end: Position::new(cursor.row, cursor.col + 1),
1408 kind: MotionKind::Char,
1409 });
1410 true
1411 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1412 ed.mutate_edit(Edit::JoinLines {
1413 row: cursor.row,
1414 count: 1,
1415 with_space: false,
1416 });
1417 buf_set_cursor_pos(&mut ed.buffer, cursor);
1418 true
1419 } else {
1420 false
1421 }
1422 }
1423 Key::Left => {
1424 crate::motions::move_left(&mut ed.buffer, 1);
1425 break_undo_group_in_insert(ed);
1426 false
1427 }
1428 Key::Right => {
1429 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1432 break_undo_group_in_insert(ed);
1433 false
1434 }
1435 Key::Up => {
1436 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1437 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1438 break_undo_group_in_insert(ed);
1439 false
1440 }
1441 Key::Down => {
1442 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1443 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1444 break_undo_group_in_insert(ed);
1445 false
1446 }
1447 Key::Home => {
1448 crate::motions::move_line_start(&mut ed.buffer);
1449 break_undo_group_in_insert(ed);
1450 false
1451 }
1452 Key::End => {
1453 crate::motions::move_line_end(&mut ed.buffer);
1454 break_undo_group_in_insert(ed);
1455 false
1456 }
1457 Key::PageUp => {
1458 let rows = viewport_full_rows(ed, 1) as isize;
1462 scroll_cursor_rows(ed, -rows);
1463 return false;
1464 }
1465 Key::PageDown => {
1466 let rows = viewport_full_rows(ed, 1) as isize;
1467 scroll_cursor_rows(ed, rows);
1468 return false;
1469 }
1470 _ => false,
1473 };
1474 ed.push_buffer_cursor_to_textarea();
1475 mutated
1476}
1477
1478fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1479 let Some(session) = ed.vim.insert_session.take() else {
1480 return;
1481 };
1482 let lines = buf_lines_to_vec(&ed.buffer);
1483 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1487 let before_end = session
1488 .row_max
1489 .min(session.before_lines.len().saturating_sub(1));
1490 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1491 session.before_lines[session.row_min..=before_end].join("\n")
1492 } else {
1493 String::new()
1494 };
1495 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1496 lines[session.row_min..=after_end].join("\n")
1497 } else {
1498 String::new()
1499 };
1500 let inserted = extract_inserted(&before, &after);
1501 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1502 use hjkl_buffer::{Edit, Position};
1503 for _ in 0..session.count - 1 {
1504 let (row, col) = ed.cursor();
1505 ed.mutate_edit(Edit::InsertStr {
1506 at: Position::new(row, col),
1507 text: inserted.clone(),
1508 });
1509 }
1510 }
1511 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1512 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1513 use hjkl_buffer::{Edit, Position};
1514 for r in (top + 1)..=bot {
1515 let line_len = buf_line_chars(&ed.buffer, r);
1516 if col > line_len {
1517 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1520 ed.mutate_edit(Edit::InsertStr {
1521 at: Position::new(r, line_len),
1522 text: pad,
1523 });
1524 }
1525 ed.mutate_edit(Edit::InsertStr {
1526 at: Position::new(r, col),
1527 text: inserted.clone(),
1528 });
1529 }
1530 buf_set_cursor_rc(&mut ed.buffer, top, col);
1531 ed.push_buffer_cursor_to_textarea();
1532 }
1533 return;
1534 }
1535 if ed.vim.replaying {
1536 return;
1537 }
1538 match session.reason {
1539 InsertReason::Enter(entry) => {
1540 ed.vim.last_change = Some(LastChange::InsertAt {
1541 entry,
1542 inserted,
1543 count: session.count,
1544 });
1545 }
1546 InsertReason::Open { above } => {
1547 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1548 }
1549 InsertReason::AfterChange => {
1550 if let Some(
1551 LastChange::OpMotion { inserted: ins, .. }
1552 | LastChange::OpTextObj { inserted: ins, .. }
1553 | LastChange::LineOp { inserted: ins, .. },
1554 ) = ed.vim.last_change.as_mut()
1555 {
1556 *ins = Some(inserted);
1557 }
1558 }
1559 InsertReason::DeleteToEol => {
1560 ed.vim.last_change = Some(LastChange::DeleteToEol {
1561 inserted: Some(inserted),
1562 });
1563 }
1564 InsertReason::ReplayOnly => {}
1565 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1566 InsertReason::Replace => {
1567 ed.vim.last_change = Some(LastChange::DeleteToEol {
1572 inserted: Some(inserted),
1573 });
1574 }
1575 }
1576}
1577
1578fn begin_insert<H: crate::types::Host>(
1579 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1580 count: usize,
1581 reason: InsertReason,
1582) {
1583 let record = !matches!(reason, InsertReason::ReplayOnly);
1584 if record {
1585 ed.push_undo();
1586 }
1587 let reason = if ed.vim.replaying {
1588 InsertReason::ReplayOnly
1589 } else {
1590 reason
1591 };
1592 let (row, _) = ed.cursor();
1593 ed.vim.insert_session = Some(InsertSession {
1594 count,
1595 row_min: row,
1596 row_max: row,
1597 before_lines: buf_lines_to_vec(&ed.buffer),
1598 reason,
1599 });
1600 ed.vim.mode = Mode::Insert;
1601}
1602
1603pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1618 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1619) {
1620 if !ed.settings.undo_break_on_motion {
1621 return;
1622 }
1623 if ed.vim.replaying {
1624 return;
1625 }
1626 if ed.vim.insert_session.is_none() {
1627 return;
1628 }
1629 ed.push_undo();
1630 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1631 let mut lines: Vec<String> = Vec::with_capacity(n);
1632 for r in 0..n {
1633 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1634 }
1635 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1636 if let Some(ref mut session) = ed.vim.insert_session {
1637 session.before_lines = lines;
1638 session.row_min = row;
1639 session.row_max = row;
1640 }
1641}
1642
1643fn step_normal<H: crate::types::Host>(
1646 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1647 input: Input,
1648) -> bool {
1649 if let Key::Char(d @ '0'..='9') = input.key
1651 && !input.ctrl
1652 && !input.alt
1653 && !matches!(
1654 ed.vim.pending,
1655 Pending::Replace
1656 | Pending::Find { .. }
1657 | Pending::OpFind { .. }
1658 | Pending::VisualTextObj { .. }
1659 )
1660 && (d != '0' || ed.vim.count > 0)
1661 {
1662 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1663 return true;
1664 }
1665
1666 match std::mem::take(&mut ed.vim.pending) {
1668 Pending::Replace => return handle_replace(ed, input),
1669 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1670 Pending::OpFind {
1671 op,
1672 count1,
1673 forward,
1674 till,
1675 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1676 Pending::G => return handle_after_g(ed, input),
1677 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1678 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1679 Pending::OpTextObj { op, count1, inner } => {
1680 return handle_text_object(ed, input, op, count1, inner);
1681 }
1682 Pending::VisualTextObj { inner } => {
1683 return handle_visual_text_obj(ed, input, inner);
1684 }
1685 Pending::Z => return handle_after_z(ed, input),
1686 Pending::SetMark => return handle_set_mark(ed, input),
1687 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1688 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1689 Pending::SelectRegister => return handle_select_register(ed, input),
1690 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1691 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1692 Pending::None => {}
1693 }
1694
1695 let count = take_count(&mut ed.vim);
1696
1697 match input.key {
1699 Key::Esc => {
1700 ed.vim.force_normal();
1701 return true;
1702 }
1703 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1704 ed.vim.visual_anchor = ed.cursor();
1705 ed.vim.mode = Mode::Visual;
1706 return true;
1707 }
1708 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1709 let (row, _) = ed.cursor();
1710 ed.vim.visual_line_anchor = row;
1711 ed.vim.mode = Mode::VisualLine;
1712 return true;
1713 }
1714 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1715 ed.vim.visual_anchor = ed.cursor();
1716 ed.vim.mode = Mode::Visual;
1717 return true;
1718 }
1719 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1720 let (row, _) = ed.cursor();
1721 ed.vim.visual_line_anchor = row;
1722 ed.vim.mode = Mode::VisualLine;
1723 return true;
1724 }
1725 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1726 let cur = ed.cursor();
1727 ed.vim.block_anchor = cur;
1728 ed.vim.block_vcol = cur.1;
1729 ed.vim.mode = Mode::VisualBlock;
1730 return true;
1731 }
1732 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1733 ed.vim.mode = Mode::Normal;
1735 return true;
1736 }
1737 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1740 Mode::Visual => {
1741 let cur = ed.cursor();
1742 let anchor = ed.vim.visual_anchor;
1743 ed.vim.visual_anchor = cur;
1744 ed.jump_cursor(anchor.0, anchor.1);
1745 return true;
1746 }
1747 Mode::VisualLine => {
1748 let cur_row = ed.cursor().0;
1749 let anchor_row = ed.vim.visual_line_anchor;
1750 ed.vim.visual_line_anchor = cur_row;
1751 ed.jump_cursor(anchor_row, 0);
1752 return true;
1753 }
1754 Mode::VisualBlock => {
1755 let cur = ed.cursor();
1756 let anchor = ed.vim.block_anchor;
1757 ed.vim.block_anchor = cur;
1758 ed.vim.block_vcol = anchor.1;
1759 ed.jump_cursor(anchor.0, anchor.1);
1760 return true;
1761 }
1762 _ => {}
1763 },
1764 _ => {}
1765 }
1766
1767 if ed.vim.is_visual()
1769 && let Some(op) = visual_operator(&input)
1770 {
1771 apply_visual_operator(ed, op);
1772 return true;
1773 }
1774
1775 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1779 match input.key {
1780 Key::Char('r') => {
1781 ed.vim.pending = Pending::Replace;
1782 return true;
1783 }
1784 Key::Char('I') => {
1785 let (top, bot, left, _right) = block_bounds(ed);
1786 ed.jump_cursor(top, left);
1787 ed.vim.mode = Mode::Normal;
1788 begin_insert(
1789 ed,
1790 1,
1791 InsertReason::BlockEdge {
1792 top,
1793 bot,
1794 col: left,
1795 },
1796 );
1797 return true;
1798 }
1799 Key::Char('A') => {
1800 let (top, bot, _left, right) = block_bounds(ed);
1801 let line_len = buf_line_chars(&ed.buffer, top);
1802 let col = (right + 1).min(line_len);
1803 ed.jump_cursor(top, col);
1804 ed.vim.mode = Mode::Normal;
1805 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1806 return true;
1807 }
1808 _ => {}
1809 }
1810 }
1811
1812 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1814 && !input.ctrl
1815 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1816 {
1817 let inner = matches!(input.key, Key::Char('i'));
1818 ed.vim.pending = Pending::VisualTextObj { inner };
1819 return true;
1820 }
1821
1822 if input.ctrl
1827 && let Key::Char(c) = input.key
1828 {
1829 match c {
1830 'd' => {
1831 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1832 return true;
1833 }
1834 'u' => {
1835 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1836 return true;
1837 }
1838 'f' => {
1839 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1840 return true;
1841 }
1842 'b' => {
1843 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1844 return true;
1845 }
1846 'r' => {
1847 do_redo(ed);
1848 return true;
1849 }
1850 'a' if ed.vim.mode == Mode::Normal => {
1851 adjust_number(ed, count.max(1) as i64);
1852 return true;
1853 }
1854 'x' if ed.vim.mode == Mode::Normal => {
1855 adjust_number(ed, -(count.max(1) as i64));
1856 return true;
1857 }
1858 'o' if ed.vim.mode == Mode::Normal => {
1859 for _ in 0..count.max(1) {
1860 jump_back(ed);
1861 }
1862 return true;
1863 }
1864 'i' if ed.vim.mode == Mode::Normal => {
1865 for _ in 0..count.max(1) {
1866 jump_forward(ed);
1867 }
1868 return true;
1869 }
1870 _ => {}
1871 }
1872 }
1873
1874 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1876 for _ in 0..count.max(1) {
1877 jump_forward(ed);
1878 }
1879 return true;
1880 }
1881
1882 if let Some(motion) = parse_motion(&input) {
1884 execute_motion(ed, motion.clone(), count);
1885 if ed.vim.mode == Mode::VisualBlock {
1887 update_block_vcol(ed, &motion);
1888 }
1889 if let Motion::Find { ch, forward, till } = motion {
1890 ed.vim.last_find = Some((ch, forward, till));
1891 }
1892 return true;
1893 }
1894
1895 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
1897 return true;
1898 }
1899
1900 if ed.vim.mode == Mode::Normal
1902 && let Key::Char(op_ch) = input.key
1903 && !input.ctrl
1904 && let Some(op) = char_to_operator(op_ch)
1905 {
1906 ed.vim.pending = Pending::Op { op, count1: count };
1907 return true;
1908 }
1909
1910 if ed.vim.mode == Mode::Normal
1912 && let Some((forward, till)) = find_entry(&input)
1913 {
1914 ed.vim.count = count;
1915 ed.vim.pending = Pending::Find { forward, till };
1916 return true;
1917 }
1918
1919 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
1921 ed.vim.count = count;
1922 ed.vim.pending = Pending::G;
1923 return true;
1924 }
1925
1926 if !input.ctrl
1928 && input.key == Key::Char('z')
1929 && matches!(
1930 ed.vim.mode,
1931 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
1932 )
1933 {
1934 ed.vim.pending = Pending::Z;
1935 return true;
1936 }
1937
1938 if !input.ctrl && ed.vim.mode == Mode::Normal {
1942 match input.key {
1943 Key::Char('m') => {
1944 ed.vim.pending = Pending::SetMark;
1945 return true;
1946 }
1947 Key::Char('\'') => {
1948 ed.vim.pending = Pending::GotoMarkLine;
1949 return true;
1950 }
1951 Key::Char('`') => {
1952 ed.vim.pending = Pending::GotoMarkChar;
1953 return true;
1954 }
1955 Key::Char('"') => {
1956 ed.vim.pending = Pending::SelectRegister;
1959 return true;
1960 }
1961 Key::Char('@') => {
1962 ed.vim.pending = Pending::PlayMacroTarget { count };
1966 return true;
1967 }
1968 Key::Char('q') if ed.vim.recording_macro.is_none() => {
1969 ed.vim.pending = Pending::RecordMacroTarget;
1974 return true;
1975 }
1976 _ => {}
1977 }
1978 }
1979
1980 true
1982}
1983
1984fn handle_set_mark<H: crate::types::Host>(
1985 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1986 input: Input,
1987) -> bool {
1988 if let Key::Char(c) = input.key
1989 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
1990 {
1991 let pos = ed.cursor();
1996 ed.set_mark(c, pos);
1997 }
1998 true
1999}
2000
2001fn handle_select_register<H: crate::types::Host>(
2005 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2006 input: Input,
2007) -> bool {
2008 if let Key::Char(c) = input.key
2009 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*'))
2010 {
2011 ed.vim.pending_register = Some(c);
2012 }
2013 true
2014}
2015
2016fn handle_record_macro_target<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_alphabetic() || c.is_ascii_digit())
2026 {
2027 ed.vim.recording_macro = Some(c);
2028 if c.is_ascii_uppercase() {
2031 let lower = c.to_ascii_lowercase();
2032 let text = ed
2036 .registers()
2037 .read(lower)
2038 .map(|s| s.text.clone())
2039 .unwrap_or_default();
2040 ed.vim.recording_keys = crate::input::decode_macro(&text);
2041 } else {
2042 ed.vim.recording_keys.clear();
2043 }
2044 }
2045 true
2046}
2047
2048fn handle_play_macro_target<H: crate::types::Host>(
2054 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2055 input: Input,
2056 count: usize,
2057) -> bool {
2058 let reg = match input.key {
2059 Key::Char('@') => ed.vim.last_macro,
2060 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2061 Some(c.to_ascii_lowercase())
2062 }
2063 _ => None,
2064 };
2065 let Some(reg) = reg else {
2066 return true;
2067 };
2068 let text = match ed.registers().read(reg) {
2071 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2072 _ => return true,
2073 };
2074 let keys = crate::input::decode_macro(&text);
2075 ed.vim.last_macro = Some(reg);
2076 let times = count.max(1);
2077 let was_replaying = ed.vim.replaying_macro;
2078 ed.vim.replaying_macro = true;
2079 for _ in 0..times {
2080 for k in keys.iter().copied() {
2081 step(ed, k);
2082 }
2083 }
2084 ed.vim.replaying_macro = was_replaying;
2085 true
2086}
2087
2088fn handle_goto_mark<H: crate::types::Host>(
2089 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2090 input: Input,
2091 linewise: bool,
2092) -> bool {
2093 let Key::Char(c) = input.key else {
2094 return true;
2095 };
2096 let target = match c {
2103 'a'..='z' | 'A'..='Z' => ed.mark(c),
2104 '\'' | '`' => ed.vim.jump_back.last().copied(),
2105 '.' => ed.vim.last_edit_pos,
2106 _ => None,
2107 };
2108 let Some((row, col)) = target else {
2109 return true;
2110 };
2111 let pre = ed.cursor();
2112 let (r, c_clamped) = clamp_pos(ed, (row, col));
2113 if linewise {
2114 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2115 ed.push_buffer_cursor_to_textarea();
2116 move_first_non_whitespace(ed);
2117 } else {
2118 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2119 ed.push_buffer_cursor_to_textarea();
2120 }
2121 if ed.cursor() != pre {
2122 push_jump(ed, pre);
2123 }
2124 ed.sticky_col = Some(ed.cursor().1);
2125 true
2126}
2127
2128fn take_count(vim: &mut VimState) -> usize {
2129 if vim.count > 0 {
2130 let n = vim.count;
2131 vim.count = 0;
2132 n
2133 } else {
2134 1
2135 }
2136}
2137
2138fn char_to_operator(c: char) -> Option<Operator> {
2139 match c {
2140 'd' => Some(Operator::Delete),
2141 'c' => Some(Operator::Change),
2142 'y' => Some(Operator::Yank),
2143 '>' => Some(Operator::Indent),
2144 '<' => Some(Operator::Outdent),
2145 _ => None,
2146 }
2147}
2148
2149fn visual_operator(input: &Input) -> Option<Operator> {
2150 if input.ctrl {
2151 return None;
2152 }
2153 match input.key {
2154 Key::Char('y') => Some(Operator::Yank),
2155 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2156 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2157 Key::Char('U') => Some(Operator::Uppercase),
2159 Key::Char('u') => Some(Operator::Lowercase),
2160 Key::Char('~') => Some(Operator::ToggleCase),
2161 Key::Char('>') => Some(Operator::Indent),
2163 Key::Char('<') => Some(Operator::Outdent),
2164 _ => None,
2165 }
2166}
2167
2168fn find_entry(input: &Input) -> Option<(bool, bool)> {
2169 if input.ctrl {
2170 return None;
2171 }
2172 match input.key {
2173 Key::Char('f') => Some((true, false)),
2174 Key::Char('F') => Some((false, false)),
2175 Key::Char('t') => Some((true, true)),
2176 Key::Char('T') => Some((false, true)),
2177 _ => None,
2178 }
2179}
2180
2181const JUMPLIST_MAX: usize = 100;
2185
2186fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2191 ed.vim.jump_back.push(from);
2192 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2193 ed.vim.jump_back.remove(0);
2194 }
2195 ed.vim.jump_fwd.clear();
2196}
2197
2198fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2201 let Some(target) = ed.vim.jump_back.pop() else {
2202 return;
2203 };
2204 let cur = ed.cursor();
2205 ed.vim.jump_fwd.push(cur);
2206 let (r, c) = clamp_pos(ed, target);
2207 ed.jump_cursor(r, c);
2208 ed.sticky_col = Some(c);
2209}
2210
2211fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2214 let Some(target) = ed.vim.jump_fwd.pop() else {
2215 return;
2216 };
2217 let cur = ed.cursor();
2218 ed.vim.jump_back.push(cur);
2219 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2220 ed.vim.jump_back.remove(0);
2221 }
2222 let (r, c) = clamp_pos(ed, target);
2223 ed.jump_cursor(r, c);
2224 ed.sticky_col = Some(c);
2225}
2226
2227fn clamp_pos<H: crate::types::Host>(
2230 ed: &Editor<hjkl_buffer::Buffer, H>,
2231 pos: (usize, usize),
2232) -> (usize, usize) {
2233 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2234 let r = pos.0.min(last_row);
2235 let line_len = buf_line_chars(&ed.buffer, r);
2236 let c = pos.1.min(line_len.saturating_sub(1));
2237 (r, c)
2238}
2239
2240fn is_big_jump(motion: &Motion) -> bool {
2242 matches!(
2243 motion,
2244 Motion::FileTop
2245 | Motion::FileBottom
2246 | Motion::MatchBracket
2247 | Motion::WordAtCursor { .. }
2248 | Motion::SearchNext { .. }
2249 | Motion::ViewportTop
2250 | Motion::ViewportMiddle
2251 | Motion::ViewportBottom
2252 )
2253}
2254
2255fn viewport_half_rows<H: crate::types::Host>(
2260 ed: &Editor<hjkl_buffer::Buffer, H>,
2261 count: usize,
2262) -> usize {
2263 let h = ed.viewport_height_value() as usize;
2264 (h / 2).max(1).saturating_mul(count.max(1))
2265}
2266
2267fn viewport_full_rows<H: crate::types::Host>(
2270 ed: &Editor<hjkl_buffer::Buffer, H>,
2271 count: usize,
2272) -> usize {
2273 let h = ed.viewport_height_value() as usize;
2274 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2275}
2276
2277fn scroll_cursor_rows<H: crate::types::Host>(
2282 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2283 delta: isize,
2284) {
2285 if delta == 0 {
2286 return;
2287 }
2288 ed.sync_buffer_content_from_textarea();
2289 let (row, _) = ed.cursor();
2290 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2291 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2292 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2293 crate::motions::move_first_non_blank(&mut ed.buffer);
2294 ed.push_buffer_cursor_to_textarea();
2295 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2296}
2297
2298fn parse_motion(input: &Input) -> Option<Motion> {
2301 if input.ctrl {
2302 return None;
2303 }
2304 match input.key {
2305 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2306 Key::Char('l') | Key::Right => Some(Motion::Right),
2307 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2308 Key::Char('k') | Key::Up => Some(Motion::Up),
2309 Key::Char('w') => Some(Motion::WordFwd),
2310 Key::Char('W') => Some(Motion::BigWordFwd),
2311 Key::Char('b') => Some(Motion::WordBack),
2312 Key::Char('B') => Some(Motion::BigWordBack),
2313 Key::Char('e') => Some(Motion::WordEnd),
2314 Key::Char('E') => Some(Motion::BigWordEnd),
2315 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2316 Key::Char('^') => Some(Motion::FirstNonBlank),
2317 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2318 Key::Char('G') => Some(Motion::FileBottom),
2319 Key::Char('%') => Some(Motion::MatchBracket),
2320 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2321 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2322 Key::Char('*') => Some(Motion::WordAtCursor {
2323 forward: true,
2324 whole_word: true,
2325 }),
2326 Key::Char('#') => Some(Motion::WordAtCursor {
2327 forward: false,
2328 whole_word: true,
2329 }),
2330 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2331 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2332 Key::Char('H') => Some(Motion::ViewportTop),
2333 Key::Char('M') => Some(Motion::ViewportMiddle),
2334 Key::Char('L') => Some(Motion::ViewportBottom),
2335 Key::Char('{') => Some(Motion::ParagraphPrev),
2336 Key::Char('}') => Some(Motion::ParagraphNext),
2337 Key::Char('(') => Some(Motion::SentencePrev),
2338 Key::Char(')') => Some(Motion::SentenceNext),
2339 _ => None,
2340 }
2341}
2342
2343fn execute_motion<H: crate::types::Host>(
2346 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2347 motion: Motion,
2348 count: usize,
2349) {
2350 let count = count.max(1);
2351 let motion = match motion {
2353 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2354 Some((ch, forward, till)) => Motion::Find {
2355 ch,
2356 forward: if reverse { !forward } else { forward },
2357 till,
2358 },
2359 None => return,
2360 },
2361 other => other,
2362 };
2363 let pre_pos = ed.cursor();
2364 let pre_col = pre_pos.1;
2365 apply_motion_cursor(ed, &motion, count);
2366 let post_pos = ed.cursor();
2367 if is_big_jump(&motion) && pre_pos != post_pos {
2368 push_jump(ed, pre_pos);
2369 }
2370 apply_sticky_col(ed, &motion, pre_col);
2371 ed.sync_buffer_from_textarea();
2376}
2377
2378fn apply_sticky_col<H: crate::types::Host>(
2383 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2384 motion: &Motion,
2385 pre_col: usize,
2386) {
2387 if is_vertical_motion(motion) {
2388 let want = ed.sticky_col.unwrap_or(pre_col);
2389 ed.sticky_col = Some(want);
2392 let (row, _) = ed.cursor();
2393 let line_len = buf_line_chars(&ed.buffer, row);
2394 let max_col = line_len.saturating_sub(1);
2398 let target = want.min(max_col);
2399 ed.jump_cursor(row, target);
2400 } else {
2401 ed.sticky_col = Some(ed.cursor().1);
2404 }
2405}
2406
2407fn is_vertical_motion(motion: &Motion) -> bool {
2408 matches!(
2412 motion,
2413 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2414 )
2415}
2416
2417fn apply_motion_cursor<H: crate::types::Host>(
2418 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2419 motion: &Motion,
2420 count: usize,
2421) {
2422 apply_motion_cursor_ctx(ed, motion, count, false)
2423}
2424
2425fn apply_motion_cursor_ctx<H: crate::types::Host>(
2426 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2427 motion: &Motion,
2428 count: usize,
2429 as_operator: bool,
2430) {
2431 match motion {
2432 Motion::Left => {
2433 crate::motions::move_left(&mut ed.buffer, count);
2435 ed.push_buffer_cursor_to_textarea();
2436 }
2437 Motion::Right => {
2438 if as_operator {
2442 crate::motions::move_right_to_end(&mut ed.buffer, count);
2443 } else {
2444 crate::motions::move_right_in_line(&mut ed.buffer, count);
2445 }
2446 ed.push_buffer_cursor_to_textarea();
2447 }
2448 Motion::Up => {
2449 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2453 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2454 ed.push_buffer_cursor_to_textarea();
2455 }
2456 Motion::Down => {
2457 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2458 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2459 ed.push_buffer_cursor_to_textarea();
2460 }
2461 Motion::ScreenUp => {
2462 let v = *ed.host.viewport();
2463 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2464 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2465 ed.push_buffer_cursor_to_textarea();
2466 }
2467 Motion::ScreenDown => {
2468 let v = *ed.host.viewport();
2469 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2470 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2471 ed.push_buffer_cursor_to_textarea();
2472 }
2473 Motion::WordFwd => {
2474 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2475 ed.push_buffer_cursor_to_textarea();
2476 }
2477 Motion::WordBack => {
2478 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2479 ed.push_buffer_cursor_to_textarea();
2480 }
2481 Motion::WordEnd => {
2482 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2483 ed.push_buffer_cursor_to_textarea();
2484 }
2485 Motion::BigWordFwd => {
2486 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2487 ed.push_buffer_cursor_to_textarea();
2488 }
2489 Motion::BigWordBack => {
2490 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2491 ed.push_buffer_cursor_to_textarea();
2492 }
2493 Motion::BigWordEnd => {
2494 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2495 ed.push_buffer_cursor_to_textarea();
2496 }
2497 Motion::WordEndBack => {
2498 crate::motions::move_word_end_back(
2499 &mut ed.buffer,
2500 false,
2501 count,
2502 &ed.settings.iskeyword,
2503 );
2504 ed.push_buffer_cursor_to_textarea();
2505 }
2506 Motion::BigWordEndBack => {
2507 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2508 ed.push_buffer_cursor_to_textarea();
2509 }
2510 Motion::LineStart => {
2511 crate::motions::move_line_start(&mut ed.buffer);
2512 ed.push_buffer_cursor_to_textarea();
2513 }
2514 Motion::FirstNonBlank => {
2515 crate::motions::move_first_non_blank(&mut ed.buffer);
2516 ed.push_buffer_cursor_to_textarea();
2517 }
2518 Motion::LineEnd => {
2519 crate::motions::move_line_end(&mut ed.buffer);
2521 ed.push_buffer_cursor_to_textarea();
2522 }
2523 Motion::FileTop => {
2524 if count > 1 {
2527 crate::motions::move_bottom(&mut ed.buffer, count);
2528 } else {
2529 crate::motions::move_top(&mut ed.buffer);
2530 }
2531 ed.push_buffer_cursor_to_textarea();
2532 }
2533 Motion::FileBottom => {
2534 if count > 1 {
2537 crate::motions::move_bottom(&mut ed.buffer, count);
2538 } else {
2539 crate::motions::move_bottom(&mut ed.buffer, 0);
2540 }
2541 ed.push_buffer_cursor_to_textarea();
2542 }
2543 Motion::Find { ch, forward, till } => {
2544 for _ in 0..count {
2545 if !find_char_on_line(ed, *ch, *forward, *till) {
2546 break;
2547 }
2548 }
2549 }
2550 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2552 let _ = matching_bracket(ed);
2553 }
2554 Motion::WordAtCursor {
2555 forward,
2556 whole_word,
2557 } => {
2558 word_at_cursor_search(ed, *forward, *whole_word, count);
2559 }
2560 Motion::SearchNext { reverse } => {
2561 if let Some(pattern) = ed.vim.last_search.clone() {
2565 push_search_pattern(ed, &pattern);
2566 }
2567 if ed.search_state().pattern.is_none() {
2568 return;
2569 }
2570 let forward = ed.vim.last_search_forward != *reverse;
2574 for _ in 0..count.max(1) {
2575 if forward {
2576 ed.search_advance_forward(true);
2577 } else {
2578 ed.search_advance_backward(true);
2579 }
2580 }
2581 ed.push_buffer_cursor_to_textarea();
2582 }
2583 Motion::ViewportTop => {
2584 let v = *ed.host().viewport();
2585 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2586 ed.push_buffer_cursor_to_textarea();
2587 }
2588 Motion::ViewportMiddle => {
2589 let v = *ed.host().viewport();
2590 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2591 ed.push_buffer_cursor_to_textarea();
2592 }
2593 Motion::ViewportBottom => {
2594 let v = *ed.host().viewport();
2595 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2596 ed.push_buffer_cursor_to_textarea();
2597 }
2598 Motion::LastNonBlank => {
2599 crate::motions::move_last_non_blank(&mut ed.buffer);
2600 ed.push_buffer_cursor_to_textarea();
2601 }
2602 Motion::LineMiddle => {
2603 let row = ed.cursor().0;
2604 let line_chars = buf_line_chars(&ed.buffer, row);
2605 let target = line_chars / 2;
2608 ed.jump_cursor(row, target);
2609 }
2610 Motion::ParagraphPrev => {
2611 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2612 ed.push_buffer_cursor_to_textarea();
2613 }
2614 Motion::ParagraphNext => {
2615 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2616 ed.push_buffer_cursor_to_textarea();
2617 }
2618 Motion::SentencePrev => {
2619 for _ in 0..count.max(1) {
2620 if let Some((row, col)) = sentence_boundary(ed, false) {
2621 ed.jump_cursor(row, col);
2622 }
2623 }
2624 }
2625 Motion::SentenceNext => {
2626 for _ in 0..count.max(1) {
2627 if let Some((row, col)) = sentence_boundary(ed, true) {
2628 ed.jump_cursor(row, col);
2629 }
2630 }
2631 }
2632 }
2633}
2634
2635fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2636 ed.sync_buffer_content_from_textarea();
2642 crate::motions::move_first_non_blank(&mut ed.buffer);
2643 ed.push_buffer_cursor_to_textarea();
2644}
2645
2646fn find_char_on_line<H: crate::types::Host>(
2647 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2648 ch: char,
2649 forward: bool,
2650 till: bool,
2651) -> bool {
2652 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2653 if moved {
2654 ed.push_buffer_cursor_to_textarea();
2655 }
2656 moved
2657}
2658
2659fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2660 let moved = crate::motions::match_bracket(&mut ed.buffer);
2661 if moved {
2662 ed.push_buffer_cursor_to_textarea();
2663 }
2664 moved
2665}
2666
2667fn word_at_cursor_search<H: crate::types::Host>(
2668 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2669 forward: bool,
2670 whole_word: bool,
2671 count: usize,
2672) {
2673 let (row, col) = ed.cursor();
2674 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2675 let chars: Vec<char> = line.chars().collect();
2676 if chars.is_empty() {
2677 return;
2678 }
2679 let spec = ed.settings().iskeyword.clone();
2681 let is_word = |c: char| is_keyword_char(c, &spec);
2682 let mut start = col.min(chars.len().saturating_sub(1));
2683 while start > 0 && is_word(chars[start - 1]) {
2684 start -= 1;
2685 }
2686 let mut end = start;
2687 while end < chars.len() && is_word(chars[end]) {
2688 end += 1;
2689 }
2690 if end <= start {
2691 return;
2692 }
2693 let word: String = chars[start..end].iter().collect();
2694 let escaped = regex_escape(&word);
2695 let pattern = if whole_word {
2696 format!(r"\b{escaped}\b")
2697 } else {
2698 escaped
2699 };
2700 push_search_pattern(ed, &pattern);
2701 if ed.search_state().pattern.is_none() {
2702 return;
2703 }
2704 ed.vim.last_search = Some(pattern);
2706 ed.vim.last_search_forward = forward;
2707 for _ in 0..count.max(1) {
2708 if forward {
2709 ed.search_advance_forward(true);
2710 } else {
2711 ed.search_advance_backward(true);
2712 }
2713 }
2714 ed.push_buffer_cursor_to_textarea();
2715}
2716
2717fn regex_escape(s: &str) -> String {
2718 let mut out = String::with_capacity(s.len());
2719 for c in s.chars() {
2720 if matches!(
2721 c,
2722 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2723 ) {
2724 out.push('\\');
2725 }
2726 out.push(c);
2727 }
2728 out
2729}
2730
2731fn handle_after_op<H: crate::types::Host>(
2734 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2735 input: Input,
2736 op: Operator,
2737 count1: usize,
2738) -> bool {
2739 if let Key::Char(d @ '0'..='9') = input.key
2741 && !input.ctrl
2742 && (d != '0' || ed.vim.count > 0)
2743 {
2744 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2745 ed.vim.pending = Pending::Op { op, count1 };
2746 return true;
2747 }
2748
2749 if input.key == Key::Esc {
2751 ed.vim.count = 0;
2752 return true;
2753 }
2754
2755 let double_ch = match op {
2759 Operator::Delete => Some('d'),
2760 Operator::Change => Some('c'),
2761 Operator::Yank => Some('y'),
2762 Operator::Indent => Some('>'),
2763 Operator::Outdent => Some('<'),
2764 Operator::Uppercase => Some('U'),
2765 Operator::Lowercase => Some('u'),
2766 Operator::ToggleCase => Some('~'),
2767 Operator::Fold => None,
2768 Operator::Reflow => Some('q'),
2771 };
2772 if let Key::Char(c) = input.key
2773 && !input.ctrl
2774 && Some(c) == double_ch
2775 {
2776 let count2 = take_count(&mut ed.vim);
2777 let total = count1.max(1) * count2.max(1);
2778 execute_line_op(ed, op, total);
2779 if !ed.vim.replaying {
2780 ed.vim.last_change = Some(LastChange::LineOp {
2781 op,
2782 count: total,
2783 inserted: None,
2784 });
2785 }
2786 return true;
2787 }
2788
2789 if let Key::Char('i') | Key::Char('a') = input.key
2791 && !input.ctrl
2792 {
2793 let inner = matches!(input.key, Key::Char('i'));
2794 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2795 return true;
2796 }
2797
2798 if input.key == Key::Char('g') && !input.ctrl {
2800 ed.vim.pending = Pending::OpG { op, count1 };
2801 return true;
2802 }
2803
2804 if let Some((forward, till)) = find_entry(&input) {
2806 ed.vim.pending = Pending::OpFind {
2807 op,
2808 count1,
2809 forward,
2810 till,
2811 };
2812 return true;
2813 }
2814
2815 let count2 = take_count(&mut ed.vim);
2817 let total = count1.max(1) * count2.max(1);
2818 if let Some(motion) = parse_motion(&input) {
2819 let motion = match motion {
2820 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2821 Some((ch, forward, till)) => Motion::Find {
2822 ch,
2823 forward: if reverse { !forward } else { forward },
2824 till,
2825 },
2826 None => return true,
2827 },
2828 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2832 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2833 m => m,
2834 };
2835 apply_op_with_motion(ed, op, &motion, total);
2836 if let Motion::Find { ch, forward, till } = &motion {
2837 ed.vim.last_find = Some((*ch, *forward, *till));
2838 }
2839 if !ed.vim.replaying && op_is_change(op) {
2840 ed.vim.last_change = Some(LastChange::OpMotion {
2841 op,
2842 motion,
2843 count: total,
2844 inserted: None,
2845 });
2846 }
2847 return true;
2848 }
2849
2850 true
2852}
2853
2854fn handle_op_after_g<H: crate::types::Host>(
2855 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2856 input: Input,
2857 op: Operator,
2858 count1: usize,
2859) -> bool {
2860 if input.ctrl {
2861 return true;
2862 }
2863 let count2 = take_count(&mut ed.vim);
2864 let total = count1.max(1) * count2.max(1);
2865 if matches!(
2869 op,
2870 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2871 ) {
2872 let op_char = match op {
2873 Operator::Uppercase => 'U',
2874 Operator::Lowercase => 'u',
2875 Operator::ToggleCase => '~',
2876 _ => unreachable!(),
2877 };
2878 if input.key == Key::Char(op_char) {
2879 execute_line_op(ed, op, total);
2880 if !ed.vim.replaying {
2881 ed.vim.last_change = Some(LastChange::LineOp {
2882 op,
2883 count: total,
2884 inserted: None,
2885 });
2886 }
2887 return true;
2888 }
2889 }
2890 let motion = match input.key {
2891 Key::Char('g') => Motion::FileTop,
2892 Key::Char('e') => Motion::WordEndBack,
2893 Key::Char('E') => Motion::BigWordEndBack,
2894 Key::Char('j') => Motion::ScreenDown,
2895 Key::Char('k') => Motion::ScreenUp,
2896 _ => return true,
2897 };
2898 apply_op_with_motion(ed, op, &motion, total);
2899 if !ed.vim.replaying && op_is_change(op) {
2900 ed.vim.last_change = Some(LastChange::OpMotion {
2901 op,
2902 motion,
2903 count: total,
2904 inserted: None,
2905 });
2906 }
2907 true
2908}
2909
2910fn handle_after_g<H: crate::types::Host>(
2911 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2912 input: Input,
2913) -> bool {
2914 let count = take_count(&mut ed.vim);
2915 match input.key {
2916 Key::Char('g') => {
2917 let pre = ed.cursor();
2919 if count > 1 {
2920 ed.jump_cursor(count - 1, 0);
2921 } else {
2922 ed.jump_cursor(0, 0);
2923 }
2924 move_first_non_whitespace(ed);
2925 if ed.cursor() != pre {
2926 push_jump(ed, pre);
2927 }
2928 }
2929 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
2930 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
2931 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
2933 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
2935 Key::Char('v') => {
2937 if let Some(snap) = ed.vim.last_visual {
2938 match snap.mode {
2939 Mode::Visual => {
2940 ed.vim.visual_anchor = snap.anchor;
2941 ed.vim.mode = Mode::Visual;
2942 }
2943 Mode::VisualLine => {
2944 ed.vim.visual_line_anchor = snap.anchor.0;
2945 ed.vim.mode = Mode::VisualLine;
2946 }
2947 Mode::VisualBlock => {
2948 ed.vim.block_anchor = snap.anchor;
2949 ed.vim.block_vcol = snap.block_vcol;
2950 ed.vim.mode = Mode::VisualBlock;
2951 }
2952 _ => {}
2953 }
2954 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2955 }
2956 }
2957 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
2961 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
2962 Key::Char('U') => {
2966 ed.vim.pending = Pending::Op {
2967 op: Operator::Uppercase,
2968 count1: count,
2969 };
2970 }
2971 Key::Char('u') => {
2972 ed.vim.pending = Pending::Op {
2973 op: Operator::Lowercase,
2974 count1: count,
2975 };
2976 }
2977 Key::Char('~') => {
2978 ed.vim.pending = Pending::Op {
2979 op: Operator::ToggleCase,
2980 count1: count,
2981 };
2982 }
2983 Key::Char('q') => {
2984 ed.vim.pending = Pending::Op {
2987 op: Operator::Reflow,
2988 count1: count,
2989 };
2990 }
2991 Key::Char('J') => {
2992 for _ in 0..count.max(1) {
2994 ed.push_undo();
2995 join_line_raw(ed);
2996 }
2997 if !ed.vim.replaying {
2998 ed.vim.last_change = Some(LastChange::JoinLine {
2999 count: count.max(1),
3000 });
3001 }
3002 }
3003 Key::Char('d') => {
3004 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3009 }
3010 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
3013 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
3014 Key::Char('*') => execute_motion(
3018 ed,
3019 Motion::WordAtCursor {
3020 forward: true,
3021 whole_word: false,
3022 },
3023 count,
3024 ),
3025 Key::Char('#') => execute_motion(
3026 ed,
3027 Motion::WordAtCursor {
3028 forward: false,
3029 whole_word: false,
3030 },
3031 count,
3032 ),
3033 _ => {}
3034 }
3035 true
3036}
3037
3038fn handle_after_z<H: crate::types::Host>(
3039 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3040 input: Input,
3041) -> bool {
3042 use crate::editor::CursorScrollTarget;
3043 let row = ed.cursor().0;
3044 match input.key {
3045 Key::Char('z') => {
3046 ed.scroll_cursor_to(CursorScrollTarget::Center);
3047 ed.vim.viewport_pinned = true;
3048 }
3049 Key::Char('t') => {
3050 ed.scroll_cursor_to(CursorScrollTarget::Top);
3051 ed.vim.viewport_pinned = true;
3052 }
3053 Key::Char('b') => {
3054 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3055 ed.vim.viewport_pinned = true;
3056 }
3057 Key::Char('o') => {
3062 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3063 }
3064 Key::Char('c') => {
3065 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3066 }
3067 Key::Char('a') => {
3068 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3069 }
3070 Key::Char('R') => {
3071 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3072 }
3073 Key::Char('M') => {
3074 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3075 }
3076 Key::Char('E') => {
3077 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3078 }
3079 Key::Char('d') => {
3080 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3081 }
3082 Key::Char('f') => {
3083 if matches!(
3084 ed.vim.mode,
3085 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3086 ) {
3087 let anchor_row = match ed.vim.mode {
3090 Mode::VisualLine => ed.vim.visual_line_anchor,
3091 Mode::VisualBlock => ed.vim.block_anchor.0,
3092 _ => ed.vim.visual_anchor.0,
3093 };
3094 let cur = ed.cursor().0;
3095 let top = anchor_row.min(cur);
3096 let bot = anchor_row.max(cur);
3097 ed.apply_fold_op(crate::types::FoldOp::Add {
3098 start_row: top,
3099 end_row: bot,
3100 closed: true,
3101 });
3102 ed.vim.mode = Mode::Normal;
3103 } else {
3104 let count = take_count(&mut ed.vim);
3109 ed.vim.pending = Pending::Op {
3110 op: Operator::Fold,
3111 count1: count,
3112 };
3113 }
3114 }
3115 _ => {}
3116 }
3117 true
3118}
3119
3120fn handle_replace<H: crate::types::Host>(
3121 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3122 input: Input,
3123) -> bool {
3124 if let Key::Char(ch) = input.key {
3125 if ed.vim.mode == Mode::VisualBlock {
3126 block_replace(ed, ch);
3127 return true;
3128 }
3129 let count = take_count(&mut ed.vim);
3130 replace_char(ed, ch, count.max(1));
3131 if !ed.vim.replaying {
3132 ed.vim.last_change = Some(LastChange::ReplaceChar {
3133 ch,
3134 count: count.max(1),
3135 });
3136 }
3137 }
3138 true
3139}
3140
3141fn handle_find_target<H: crate::types::Host>(
3142 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3143 input: Input,
3144 forward: bool,
3145 till: bool,
3146) -> bool {
3147 let Key::Char(ch) = input.key else {
3148 return true;
3149 };
3150 let count = take_count(&mut ed.vim);
3151 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3152 ed.vim.last_find = Some((ch, forward, till));
3153 true
3154}
3155
3156fn handle_op_find_target<H: crate::types::Host>(
3157 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3158 input: Input,
3159 op: Operator,
3160 count1: usize,
3161 forward: bool,
3162 till: bool,
3163) -> bool {
3164 let Key::Char(ch) = input.key else {
3165 return true;
3166 };
3167 let count2 = take_count(&mut ed.vim);
3168 let total = count1.max(1) * count2.max(1);
3169 let motion = Motion::Find { ch, forward, till };
3170 apply_op_with_motion(ed, op, &motion, total);
3171 ed.vim.last_find = Some((ch, forward, till));
3172 if !ed.vim.replaying && op_is_change(op) {
3173 ed.vim.last_change = Some(LastChange::OpMotion {
3174 op,
3175 motion,
3176 count: total,
3177 inserted: None,
3178 });
3179 }
3180 true
3181}
3182
3183fn handle_text_object<H: crate::types::Host>(
3184 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3185 input: Input,
3186 op: Operator,
3187 _count1: usize,
3188 inner: bool,
3189) -> bool {
3190 let Key::Char(ch) = input.key else {
3191 return true;
3192 };
3193 let obj = match ch {
3194 'w' => TextObject::Word { big: false },
3195 'W' => TextObject::Word { big: true },
3196 '"' | '\'' | '`' => TextObject::Quote(ch),
3197 '(' | ')' | 'b' => TextObject::Bracket('('),
3198 '[' | ']' => TextObject::Bracket('['),
3199 '{' | '}' | 'B' => TextObject::Bracket('{'),
3200 '<' | '>' => TextObject::Bracket('<'),
3201 'p' => TextObject::Paragraph,
3202 't' => TextObject::XmlTag,
3203 's' => TextObject::Sentence,
3204 _ => return true,
3205 };
3206 apply_op_with_text_object(ed, op, obj, inner);
3207 if !ed.vim.replaying && op_is_change(op) {
3208 ed.vim.last_change = Some(LastChange::OpTextObj {
3209 op,
3210 obj,
3211 inner,
3212 inserted: None,
3213 });
3214 }
3215 true
3216}
3217
3218fn handle_visual_text_obj<H: crate::types::Host>(
3219 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3220 input: Input,
3221 inner: bool,
3222) -> bool {
3223 let Key::Char(ch) = input.key else {
3224 return true;
3225 };
3226 let obj = match ch {
3227 'w' => TextObject::Word { big: false },
3228 'W' => TextObject::Word { big: true },
3229 '"' | '\'' | '`' => TextObject::Quote(ch),
3230 '(' | ')' | 'b' => TextObject::Bracket('('),
3231 '[' | ']' => TextObject::Bracket('['),
3232 '{' | '}' | 'B' => TextObject::Bracket('{'),
3233 '<' | '>' => TextObject::Bracket('<'),
3234 'p' => TextObject::Paragraph,
3235 't' => TextObject::XmlTag,
3236 's' => TextObject::Sentence,
3237 _ => return true,
3238 };
3239 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3240 return true;
3241 };
3242 match kind {
3246 MotionKind::Linewise => {
3247 ed.vim.visual_line_anchor = start.0;
3248 ed.vim.mode = Mode::VisualLine;
3249 ed.jump_cursor(end.0, 0);
3250 }
3251 _ => {
3252 ed.vim.mode = Mode::Visual;
3253 ed.vim.visual_anchor = (start.0, start.1);
3254 let (er, ec) = retreat_one(ed, end);
3255 ed.jump_cursor(er, ec);
3256 }
3257 }
3258 true
3259}
3260
3261fn retreat_one<H: crate::types::Host>(
3263 ed: &Editor<hjkl_buffer::Buffer, H>,
3264 pos: (usize, usize),
3265) -> (usize, usize) {
3266 let (r, c) = pos;
3267 if c > 0 {
3268 (r, c - 1)
3269 } else if r > 0 {
3270 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3271 (r - 1, prev_len)
3272 } else {
3273 (0, 0)
3274 }
3275}
3276
3277fn op_is_change(op: Operator) -> bool {
3278 matches!(op, Operator::Delete | Operator::Change)
3279}
3280
3281fn handle_normal_only<H: crate::types::Host>(
3284 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3285 input: &Input,
3286 count: usize,
3287) -> bool {
3288 if input.ctrl {
3289 return false;
3290 }
3291 match input.key {
3292 Key::Char('i') => {
3293 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3294 true
3295 }
3296 Key::Char('I') => {
3297 move_first_non_whitespace(ed);
3298 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3299 true
3300 }
3301 Key::Char('a') => {
3302 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3303 ed.push_buffer_cursor_to_textarea();
3304 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3305 true
3306 }
3307 Key::Char('A') => {
3308 crate::motions::move_line_end(&mut ed.buffer);
3309 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3310 ed.push_buffer_cursor_to_textarea();
3311 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3312 true
3313 }
3314 Key::Char('R') => {
3315 begin_insert(ed, count.max(1), InsertReason::Replace);
3318 true
3319 }
3320 Key::Char('o') => {
3321 use hjkl_buffer::{Edit, Position};
3322 ed.push_undo();
3323 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3326 ed.sync_buffer_content_from_textarea();
3327 let row = buf_cursor_pos(&ed.buffer).row;
3328 let line_chars = buf_line_chars(&ed.buffer, row);
3329 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3332 let indent = compute_enter_indent(&ed.settings, prev_line);
3333 ed.mutate_edit(Edit::InsertStr {
3334 at: Position::new(row, line_chars),
3335 text: format!("\n{indent}"),
3336 });
3337 ed.push_buffer_cursor_to_textarea();
3338 true
3339 }
3340 Key::Char('O') => {
3341 use hjkl_buffer::{Edit, Position};
3342 ed.push_undo();
3343 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3344 ed.sync_buffer_content_from_textarea();
3345 let row = buf_cursor_pos(&ed.buffer).row;
3346 let indent = if row > 0 {
3350 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3351 compute_enter_indent(&ed.settings, above)
3352 } else {
3353 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3354 cur.chars()
3355 .take_while(|c| *c == ' ' || *c == '\t')
3356 .collect::<String>()
3357 };
3358 ed.mutate_edit(Edit::InsertStr {
3359 at: Position::new(row, 0),
3360 text: format!("{indent}\n"),
3361 });
3362 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3367 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3368 let new_row = buf_cursor_pos(&ed.buffer).row;
3369 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3370 ed.push_buffer_cursor_to_textarea();
3371 true
3372 }
3373 Key::Char('x') => {
3374 do_char_delete(ed, true, count.max(1));
3375 if !ed.vim.replaying {
3376 ed.vim.last_change = Some(LastChange::CharDel {
3377 forward: true,
3378 count: count.max(1),
3379 });
3380 }
3381 true
3382 }
3383 Key::Char('X') => {
3384 do_char_delete(ed, false, count.max(1));
3385 if !ed.vim.replaying {
3386 ed.vim.last_change = Some(LastChange::CharDel {
3387 forward: false,
3388 count: count.max(1),
3389 });
3390 }
3391 true
3392 }
3393 Key::Char('~') => {
3394 for _ in 0..count.max(1) {
3395 ed.push_undo();
3396 toggle_case_at_cursor(ed);
3397 }
3398 if !ed.vim.replaying {
3399 ed.vim.last_change = Some(LastChange::ToggleCase {
3400 count: count.max(1),
3401 });
3402 }
3403 true
3404 }
3405 Key::Char('J') => {
3406 for _ in 0..count.max(1) {
3407 ed.push_undo();
3408 join_line(ed);
3409 }
3410 if !ed.vim.replaying {
3411 ed.vim.last_change = Some(LastChange::JoinLine {
3412 count: count.max(1),
3413 });
3414 }
3415 true
3416 }
3417 Key::Char('D') => {
3418 ed.push_undo();
3419 delete_to_eol(ed);
3420 crate::motions::move_left(&mut ed.buffer, 1);
3422 ed.push_buffer_cursor_to_textarea();
3423 if !ed.vim.replaying {
3424 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3425 }
3426 true
3427 }
3428 Key::Char('Y') => {
3429 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3431 true
3432 }
3433 Key::Char('C') => {
3434 ed.push_undo();
3435 delete_to_eol(ed);
3436 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3437 true
3438 }
3439 Key::Char('s') => {
3440 use hjkl_buffer::{Edit, MotionKind, Position};
3441 ed.push_undo();
3442 ed.sync_buffer_content_from_textarea();
3443 for _ in 0..count.max(1) {
3444 let cursor = buf_cursor_pos(&ed.buffer);
3445 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3446 if cursor.col >= line_chars {
3447 break;
3448 }
3449 ed.mutate_edit(Edit::DeleteRange {
3450 start: cursor,
3451 end: Position::new(cursor.row, cursor.col + 1),
3452 kind: MotionKind::Char,
3453 });
3454 }
3455 ed.push_buffer_cursor_to_textarea();
3456 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3457 if !ed.vim.replaying {
3459 ed.vim.last_change = Some(LastChange::OpMotion {
3460 op: Operator::Change,
3461 motion: Motion::Right,
3462 count: count.max(1),
3463 inserted: None,
3464 });
3465 }
3466 true
3467 }
3468 Key::Char('p') => {
3469 do_paste(ed, false, count.max(1));
3470 if !ed.vim.replaying {
3471 ed.vim.last_change = Some(LastChange::Paste {
3472 before: false,
3473 count: count.max(1),
3474 });
3475 }
3476 true
3477 }
3478 Key::Char('P') => {
3479 do_paste(ed, true, count.max(1));
3480 if !ed.vim.replaying {
3481 ed.vim.last_change = Some(LastChange::Paste {
3482 before: true,
3483 count: count.max(1),
3484 });
3485 }
3486 true
3487 }
3488 Key::Char('u') => {
3489 do_undo(ed);
3490 true
3491 }
3492 Key::Char('r') => {
3493 ed.vim.count = count;
3494 ed.vim.pending = Pending::Replace;
3495 true
3496 }
3497 Key::Char('/') => {
3498 enter_search(ed, true);
3499 true
3500 }
3501 Key::Char('?') => {
3502 enter_search(ed, false);
3503 true
3504 }
3505 Key::Char('.') => {
3506 replay_last_change(ed, count);
3507 true
3508 }
3509 _ => false,
3510 }
3511}
3512
3513fn begin_insert_noundo<H: crate::types::Host>(
3515 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3516 count: usize,
3517 reason: InsertReason,
3518) {
3519 let reason = if ed.vim.replaying {
3520 InsertReason::ReplayOnly
3521 } else {
3522 reason
3523 };
3524 let (row, _) = ed.cursor();
3525 ed.vim.insert_session = Some(InsertSession {
3526 count,
3527 row_min: row,
3528 row_max: row,
3529 before_lines: buf_lines_to_vec(&ed.buffer),
3530 reason,
3531 });
3532 ed.vim.mode = Mode::Insert;
3533}
3534
3535fn apply_op_with_motion<H: crate::types::Host>(
3538 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3539 op: Operator,
3540 motion: &Motion,
3541 count: usize,
3542) {
3543 let start = ed.cursor();
3544 apply_motion_cursor_ctx(ed, motion, count, true);
3549 let end = ed.cursor();
3550 let kind = motion_kind(motion);
3551 ed.jump_cursor(start.0, start.1);
3553 run_operator_over_range(ed, op, start, end, kind);
3554}
3555
3556fn apply_op_with_text_object<H: crate::types::Host>(
3557 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3558 op: Operator,
3559 obj: TextObject,
3560 inner: bool,
3561) {
3562 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3563 return;
3564 };
3565 ed.jump_cursor(start.0, start.1);
3566 run_operator_over_range(ed, op, start, end, kind);
3567}
3568
3569fn motion_kind(motion: &Motion) -> MotionKind {
3570 match motion {
3571 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3572 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3573 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3574 MotionKind::Linewise
3575 }
3576 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3577 MotionKind::Inclusive
3578 }
3579 Motion::Find { .. } => MotionKind::Inclusive,
3580 Motion::MatchBracket => MotionKind::Inclusive,
3581 Motion::LineEnd => MotionKind::Inclusive,
3583 _ => MotionKind::Exclusive,
3584 }
3585}
3586
3587fn run_operator_over_range<H: crate::types::Host>(
3588 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3589 op: Operator,
3590 start: (usize, usize),
3591 end: (usize, usize),
3592 kind: MotionKind,
3593) {
3594 let (top, bot) = order(start, end);
3595 if top == bot {
3596 return;
3597 }
3598
3599 match op {
3600 Operator::Yank => {
3601 let text = read_vim_range(ed, top, bot, kind);
3602 if !text.is_empty() {
3603 ed.record_yank_to_host(text.clone());
3604 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3605 }
3606 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3607 ed.push_buffer_cursor_to_textarea();
3608 }
3609 Operator::Delete => {
3610 ed.push_undo();
3611 cut_vim_range(ed, top, bot, kind);
3612 ed.vim.mode = Mode::Normal;
3613 }
3614 Operator::Change => {
3615 ed.push_undo();
3616 cut_vim_range(ed, top, bot, kind);
3617 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3618 }
3619 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3620 apply_case_op_to_selection(ed, op, top, bot, kind);
3621 }
3622 Operator::Indent | Operator::Outdent => {
3623 ed.push_undo();
3626 if op == Operator::Indent {
3627 indent_rows(ed, top.0, bot.0, 1);
3628 } else {
3629 outdent_rows(ed, top.0, bot.0, 1);
3630 }
3631 ed.vim.mode = Mode::Normal;
3632 }
3633 Operator::Fold => {
3634 if bot.0 >= top.0 {
3638 ed.apply_fold_op(crate::types::FoldOp::Add {
3639 start_row: top.0,
3640 end_row: bot.0,
3641 closed: true,
3642 });
3643 }
3644 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3645 ed.push_buffer_cursor_to_textarea();
3646 ed.vim.mode = Mode::Normal;
3647 }
3648 Operator::Reflow => {
3649 ed.push_undo();
3650 reflow_rows(ed, top.0, bot.0);
3651 ed.vim.mode = Mode::Normal;
3652 }
3653 }
3654}
3655
3656fn reflow_rows<H: crate::types::Host>(
3661 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3662 top: usize,
3663 bot: usize,
3664) {
3665 let width = ed.settings().textwidth.max(1);
3666 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3667 let bot = bot.min(lines.len().saturating_sub(1));
3668 if top > bot {
3669 return;
3670 }
3671 let original = lines[top..=bot].to_vec();
3672 let mut wrapped: Vec<String> = Vec::new();
3673 let mut paragraph: Vec<String> = Vec::new();
3674 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3675 if para.is_empty() {
3676 return;
3677 }
3678 let words = para.join(" ");
3679 let mut current = String::new();
3680 for word in words.split_whitespace() {
3681 let extra = if current.is_empty() {
3682 word.chars().count()
3683 } else {
3684 current.chars().count() + 1 + word.chars().count()
3685 };
3686 if extra > width && !current.is_empty() {
3687 out.push(std::mem::take(&mut current));
3688 current.push_str(word);
3689 } else if current.is_empty() {
3690 current.push_str(word);
3691 } else {
3692 current.push(' ');
3693 current.push_str(word);
3694 }
3695 }
3696 if !current.is_empty() {
3697 out.push(current);
3698 }
3699 para.clear();
3700 };
3701 for line in &original {
3702 if line.trim().is_empty() {
3703 flush(&mut paragraph, &mut wrapped, width);
3704 wrapped.push(String::new());
3705 } else {
3706 paragraph.push(line.clone());
3707 }
3708 }
3709 flush(&mut paragraph, &mut wrapped, width);
3710
3711 let after: Vec<String> = lines.split_off(bot + 1);
3713 lines.truncate(top);
3714 lines.extend(wrapped);
3715 lines.extend(after);
3716 ed.restore(lines, (top, 0));
3717 ed.mark_content_dirty();
3718}
3719
3720fn apply_case_op_to_selection<H: crate::types::Host>(
3726 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3727 op: Operator,
3728 top: (usize, usize),
3729 bot: (usize, usize),
3730 kind: MotionKind,
3731) {
3732 use hjkl_buffer::Edit;
3733 ed.push_undo();
3734 let saved_yank = ed.yank().to_string();
3735 let saved_yank_linewise = ed.vim.yank_linewise;
3736 let selection = cut_vim_range(ed, top, bot, kind);
3737 let transformed = match op {
3738 Operator::Uppercase => selection.to_uppercase(),
3739 Operator::Lowercase => selection.to_lowercase(),
3740 Operator::ToggleCase => toggle_case_str(&selection),
3741 _ => unreachable!(),
3742 };
3743 if !transformed.is_empty() {
3744 let cursor = buf_cursor_pos(&ed.buffer);
3745 ed.mutate_edit(Edit::InsertStr {
3746 at: cursor,
3747 text: transformed,
3748 });
3749 }
3750 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3751 ed.push_buffer_cursor_to_textarea();
3752 ed.set_yank(saved_yank);
3753 ed.vim.yank_linewise = saved_yank_linewise;
3754 ed.vim.mode = Mode::Normal;
3755}
3756
3757fn indent_rows<H: crate::types::Host>(
3762 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3763 top: usize,
3764 bot: usize,
3765 count: usize,
3766) {
3767 ed.sync_buffer_content_from_textarea();
3768 let width = ed.settings().shiftwidth * count.max(1);
3769 let pad: String = " ".repeat(width);
3770 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3771 let bot = bot.min(lines.len().saturating_sub(1));
3772 for line in lines.iter_mut().take(bot + 1).skip(top) {
3773 if !line.is_empty() {
3774 line.insert_str(0, &pad);
3775 }
3776 }
3777 ed.restore(lines, (top, 0));
3780 move_first_non_whitespace(ed);
3781}
3782
3783fn outdent_rows<H: crate::types::Host>(
3787 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3788 top: usize,
3789 bot: usize,
3790 count: usize,
3791) {
3792 ed.sync_buffer_content_from_textarea();
3793 let width = ed.settings().shiftwidth * count.max(1);
3794 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3795 let bot = bot.min(lines.len().saturating_sub(1));
3796 for line in lines.iter_mut().take(bot + 1).skip(top) {
3797 let strip: usize = line
3798 .chars()
3799 .take(width)
3800 .take_while(|c| *c == ' ' || *c == '\t')
3801 .count();
3802 if strip > 0 {
3803 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3804 line.drain(..byte_len);
3805 }
3806 }
3807 ed.restore(lines, (top, 0));
3808 move_first_non_whitespace(ed);
3809}
3810
3811fn toggle_case_str(s: &str) -> String {
3812 s.chars()
3813 .map(|c| {
3814 if c.is_lowercase() {
3815 c.to_uppercase().next().unwrap_or(c)
3816 } else if c.is_uppercase() {
3817 c.to_lowercase().next().unwrap_or(c)
3818 } else {
3819 c
3820 }
3821 })
3822 .collect()
3823}
3824
3825fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3826 if a <= b { (a, b) } else { (b, a) }
3827}
3828
3829fn execute_line_op<H: crate::types::Host>(
3832 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3833 op: Operator,
3834 count: usize,
3835) {
3836 let (row, col) = ed.cursor();
3837 let total = buf_row_count(&ed.buffer);
3838 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3839
3840 match op {
3841 Operator::Yank => {
3842 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3844 if !text.is_empty() {
3845 ed.record_yank_to_host(text.clone());
3846 ed.record_yank(text, true);
3847 }
3848 buf_set_cursor_rc(&mut ed.buffer, row, col);
3849 ed.push_buffer_cursor_to_textarea();
3850 ed.vim.mode = Mode::Normal;
3851 }
3852 Operator::Delete => {
3853 ed.push_undo();
3854 let deleted_through_last = end_row + 1 >= total;
3855 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3856 let total_after = buf_row_count(&ed.buffer);
3860 let target_row = if deleted_through_last {
3861 row.saturating_sub(1).min(total_after.saturating_sub(1))
3862 } else {
3863 row.min(total_after.saturating_sub(1))
3864 };
3865 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
3866 ed.push_buffer_cursor_to_textarea();
3867 move_first_non_whitespace(ed);
3868 ed.sticky_col = Some(ed.cursor().1);
3869 ed.vim.mode = Mode::Normal;
3870 }
3871 Operator::Change => {
3872 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3876 ed.push_undo();
3877 ed.sync_buffer_content_from_textarea();
3878 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3880 if end_row > row {
3881 ed.mutate_edit(Edit::DeleteRange {
3882 start: Position::new(row + 1, 0),
3883 end: Position::new(end_row, 0),
3884 kind: BufKind::Line,
3885 });
3886 }
3887 let line_chars = buf_line_chars(&ed.buffer, row);
3888 if line_chars > 0 {
3889 ed.mutate_edit(Edit::DeleteRange {
3890 start: Position::new(row, 0),
3891 end: Position::new(row, line_chars),
3892 kind: BufKind::Char,
3893 });
3894 }
3895 if !payload.is_empty() {
3896 ed.record_yank_to_host(payload.clone());
3897 ed.record_delete(payload, true);
3898 }
3899 buf_set_cursor_rc(&mut ed.buffer, row, 0);
3900 ed.push_buffer_cursor_to_textarea();
3901 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3902 }
3903 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3904 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
3908 move_first_non_whitespace(ed);
3911 }
3912 Operator::Indent | Operator::Outdent => {
3913 ed.push_undo();
3915 if op == Operator::Indent {
3916 indent_rows(ed, row, end_row, 1);
3917 } else {
3918 outdent_rows(ed, row, end_row, 1);
3919 }
3920 ed.sticky_col = Some(ed.cursor().1);
3921 ed.vim.mode = Mode::Normal;
3922 }
3923 Operator::Fold => unreachable!("Fold has no line-op double"),
3925 Operator::Reflow => {
3926 ed.push_undo();
3928 reflow_rows(ed, row, end_row);
3929 move_first_non_whitespace(ed);
3930 ed.sticky_col = Some(ed.cursor().1);
3931 ed.vim.mode = Mode::Normal;
3932 }
3933 }
3934}
3935
3936fn apply_visual_operator<H: crate::types::Host>(
3939 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3940 op: Operator,
3941) {
3942 match ed.vim.mode {
3943 Mode::VisualLine => {
3944 let cursor_row = buf_cursor_pos(&ed.buffer).row;
3945 let top = cursor_row.min(ed.vim.visual_line_anchor);
3946 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3947 ed.vim.yank_linewise = true;
3948 match op {
3949 Operator::Yank => {
3950 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3951 if !text.is_empty() {
3952 ed.record_yank_to_host(text.clone());
3953 ed.record_yank(text, true);
3954 }
3955 buf_set_cursor_rc(&mut ed.buffer, top, 0);
3956 ed.push_buffer_cursor_to_textarea();
3957 ed.vim.mode = Mode::Normal;
3958 }
3959 Operator::Delete => {
3960 ed.push_undo();
3961 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3962 ed.vim.mode = Mode::Normal;
3963 }
3964 Operator::Change => {
3965 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3968 ed.push_undo();
3969 ed.sync_buffer_content_from_textarea();
3970 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3971 if bot > top {
3972 ed.mutate_edit(Edit::DeleteRange {
3973 start: Position::new(top + 1, 0),
3974 end: Position::new(bot, 0),
3975 kind: BufKind::Line,
3976 });
3977 }
3978 let line_chars = buf_line_chars(&ed.buffer, top);
3979 if line_chars > 0 {
3980 ed.mutate_edit(Edit::DeleteRange {
3981 start: Position::new(top, 0),
3982 end: Position::new(top, line_chars),
3983 kind: BufKind::Char,
3984 });
3985 }
3986 if !payload.is_empty() {
3987 ed.record_yank_to_host(payload.clone());
3988 ed.record_delete(payload, true);
3989 }
3990 buf_set_cursor_rc(&mut ed.buffer, top, 0);
3991 ed.push_buffer_cursor_to_textarea();
3992 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3993 }
3994 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3995 let bot = buf_cursor_pos(&ed.buffer)
3996 .row
3997 .max(ed.vim.visual_line_anchor);
3998 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
3999 move_first_non_whitespace(ed);
4000 }
4001 Operator::Indent | Operator::Outdent => {
4002 ed.push_undo();
4003 let (cursor_row, _) = ed.cursor();
4004 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4005 if op == Operator::Indent {
4006 indent_rows(ed, top, bot, 1);
4007 } else {
4008 outdent_rows(ed, top, bot, 1);
4009 }
4010 ed.vim.mode = Mode::Normal;
4011 }
4012 Operator::Reflow => {
4013 ed.push_undo();
4014 let (cursor_row, _) = ed.cursor();
4015 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4016 reflow_rows(ed, top, bot);
4017 ed.vim.mode = Mode::Normal;
4018 }
4019 Operator::Fold => unreachable!("Visual zf takes its own path"),
4022 }
4023 }
4024 Mode::Visual => {
4025 ed.vim.yank_linewise = false;
4026 let anchor = ed.vim.visual_anchor;
4027 let cursor = ed.cursor();
4028 let (top, bot) = order(anchor, cursor);
4029 match op {
4030 Operator::Yank => {
4031 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4032 if !text.is_empty() {
4033 ed.record_yank_to_host(text.clone());
4034 ed.record_yank(text, false);
4035 }
4036 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4037 ed.push_buffer_cursor_to_textarea();
4038 ed.vim.mode = Mode::Normal;
4039 }
4040 Operator::Delete => {
4041 ed.push_undo();
4042 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4043 ed.vim.mode = Mode::Normal;
4044 }
4045 Operator::Change => {
4046 ed.push_undo();
4047 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4048 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4049 }
4050 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4051 let anchor = ed.vim.visual_anchor;
4053 let cursor = ed.cursor();
4054 let (top, bot) = order(anchor, cursor);
4055 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4056 }
4057 Operator::Indent | Operator::Outdent => {
4058 ed.push_undo();
4059 let anchor = ed.vim.visual_anchor;
4060 let cursor = ed.cursor();
4061 let (top, bot) = order(anchor, cursor);
4062 if op == Operator::Indent {
4063 indent_rows(ed, top.0, bot.0, 1);
4064 } else {
4065 outdent_rows(ed, top.0, bot.0, 1);
4066 }
4067 ed.vim.mode = Mode::Normal;
4068 }
4069 Operator::Reflow => {
4070 ed.push_undo();
4071 let anchor = ed.vim.visual_anchor;
4072 let cursor = ed.cursor();
4073 let (top, bot) = order(anchor, cursor);
4074 reflow_rows(ed, top.0, bot.0);
4075 ed.vim.mode = Mode::Normal;
4076 }
4077 Operator::Fold => unreachable!("Visual zf takes its own path"),
4078 }
4079 }
4080 Mode::VisualBlock => apply_block_operator(ed, op),
4081 _ => {}
4082 }
4083}
4084
4085fn block_bounds<H: crate::types::Host>(
4090 ed: &Editor<hjkl_buffer::Buffer, H>,
4091) -> (usize, usize, usize, usize) {
4092 let (ar, ac) = ed.vim.block_anchor;
4093 let (cr, _) = ed.cursor();
4094 let cc = ed.vim.block_vcol;
4095 let top = ar.min(cr);
4096 let bot = ar.max(cr);
4097 let left = ac.min(cc);
4098 let right = ac.max(cc);
4099 (top, bot, left, right)
4100}
4101
4102fn update_block_vcol<H: crate::types::Host>(
4107 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4108 motion: &Motion,
4109) {
4110 match motion {
4111 Motion::Left
4112 | Motion::Right
4113 | Motion::WordFwd
4114 | Motion::BigWordFwd
4115 | Motion::WordBack
4116 | Motion::BigWordBack
4117 | Motion::WordEnd
4118 | Motion::BigWordEnd
4119 | Motion::WordEndBack
4120 | Motion::BigWordEndBack
4121 | Motion::LineStart
4122 | Motion::FirstNonBlank
4123 | Motion::LineEnd
4124 | Motion::Find { .. }
4125 | Motion::FindRepeat { .. }
4126 | Motion::MatchBracket => {
4127 ed.vim.block_vcol = ed.cursor().1;
4128 }
4129 _ => {}
4131 }
4132}
4133
4134fn apply_block_operator<H: crate::types::Host>(
4139 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4140 op: Operator,
4141) {
4142 let (top, bot, left, right) = block_bounds(ed);
4143 let yank = block_yank(ed, top, bot, left, right);
4145
4146 match op {
4147 Operator::Yank => {
4148 if !yank.is_empty() {
4149 ed.record_yank_to_host(yank.clone());
4150 ed.record_yank(yank, false);
4151 }
4152 ed.vim.mode = Mode::Normal;
4153 ed.jump_cursor(top, left);
4154 }
4155 Operator::Delete => {
4156 ed.push_undo();
4157 delete_block_contents(ed, top, bot, left, right);
4158 if !yank.is_empty() {
4159 ed.record_yank_to_host(yank.clone());
4160 ed.record_delete(yank, false);
4161 }
4162 ed.vim.mode = Mode::Normal;
4163 ed.jump_cursor(top, left);
4164 }
4165 Operator::Change => {
4166 ed.push_undo();
4167 delete_block_contents(ed, top, bot, left, right);
4168 if !yank.is_empty() {
4169 ed.record_yank_to_host(yank.clone());
4170 ed.record_delete(yank, false);
4171 }
4172 ed.jump_cursor(top, left);
4173 begin_insert_noundo(
4174 ed,
4175 1,
4176 InsertReason::BlockEdge {
4177 top,
4178 bot,
4179 col: left,
4180 },
4181 );
4182 }
4183 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4184 ed.push_undo();
4185 transform_block_case(ed, op, top, bot, left, right);
4186 ed.vim.mode = Mode::Normal;
4187 ed.jump_cursor(top, left);
4188 }
4189 Operator::Indent | Operator::Outdent => {
4190 ed.push_undo();
4194 if op == Operator::Indent {
4195 indent_rows(ed, top, bot, 1);
4196 } else {
4197 outdent_rows(ed, top, bot, 1);
4198 }
4199 ed.vim.mode = Mode::Normal;
4200 }
4201 Operator::Fold => unreachable!("Visual zf takes its own path"),
4202 Operator::Reflow => {
4203 ed.push_undo();
4207 reflow_rows(ed, top, bot);
4208 ed.vim.mode = Mode::Normal;
4209 }
4210 }
4211}
4212
4213fn transform_block_case<H: crate::types::Host>(
4217 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4218 op: Operator,
4219 top: usize,
4220 bot: usize,
4221 left: usize,
4222 right: usize,
4223) {
4224 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4225 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4226 let chars: Vec<char> = lines[r].chars().collect();
4227 if left >= chars.len() {
4228 continue;
4229 }
4230 let end = (right + 1).min(chars.len());
4231 let head: String = chars[..left].iter().collect();
4232 let mid: String = chars[left..end].iter().collect();
4233 let tail: String = chars[end..].iter().collect();
4234 let transformed = match op {
4235 Operator::Uppercase => mid.to_uppercase(),
4236 Operator::Lowercase => mid.to_lowercase(),
4237 Operator::ToggleCase => toggle_case_str(&mid),
4238 _ => mid,
4239 };
4240 lines[r] = format!("{head}{transformed}{tail}");
4241 }
4242 let saved_yank = ed.yank().to_string();
4243 let saved_linewise = ed.vim.yank_linewise;
4244 ed.restore(lines, (top, left));
4245 ed.set_yank(saved_yank);
4246 ed.vim.yank_linewise = saved_linewise;
4247}
4248
4249fn block_yank<H: crate::types::Host>(
4250 ed: &Editor<hjkl_buffer::Buffer, H>,
4251 top: usize,
4252 bot: usize,
4253 left: usize,
4254 right: usize,
4255) -> String {
4256 let lines = buf_lines_to_vec(&ed.buffer);
4257 let mut rows: Vec<String> = Vec::new();
4258 for r in top..=bot {
4259 let line = match lines.get(r) {
4260 Some(l) => l,
4261 None => break,
4262 };
4263 let chars: Vec<char> = line.chars().collect();
4264 let end = (right + 1).min(chars.len());
4265 if left >= chars.len() {
4266 rows.push(String::new());
4267 } else {
4268 rows.push(chars[left..end].iter().collect());
4269 }
4270 }
4271 rows.join("\n")
4272}
4273
4274fn delete_block_contents<H: crate::types::Host>(
4275 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4276 top: usize,
4277 bot: usize,
4278 left: usize,
4279 right: usize,
4280) {
4281 use hjkl_buffer::{Edit, MotionKind, Position};
4282 ed.sync_buffer_content_from_textarea();
4283 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4284 if last_row < top {
4285 return;
4286 }
4287 ed.mutate_edit(Edit::DeleteRange {
4288 start: Position::new(top, left),
4289 end: Position::new(last_row, right),
4290 kind: MotionKind::Block,
4291 });
4292 ed.push_buffer_cursor_to_textarea();
4293}
4294
4295fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4297 let (top, bot, left, right) = block_bounds(ed);
4298 ed.push_undo();
4299 ed.sync_buffer_content_from_textarea();
4300 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4301 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4302 let chars: Vec<char> = lines[r].chars().collect();
4303 if left >= chars.len() {
4304 continue;
4305 }
4306 let end = (right + 1).min(chars.len());
4307 let before: String = chars[..left].iter().collect();
4308 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4309 let after: String = chars[end..].iter().collect();
4310 lines[r] = format!("{before}{middle}{after}");
4311 }
4312 reset_textarea_lines(ed, lines);
4313 ed.vim.mode = Mode::Normal;
4314 ed.jump_cursor(top, left);
4315}
4316
4317fn reset_textarea_lines<H: crate::types::Host>(
4321 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4322 lines: Vec<String>,
4323) {
4324 let cursor = ed.cursor();
4325 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4326 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4327 ed.mark_content_dirty();
4328}
4329
4330type Pos = (usize, usize);
4336
4337fn text_object_range<H: crate::types::Host>(
4341 ed: &Editor<hjkl_buffer::Buffer, H>,
4342 obj: TextObject,
4343 inner: bool,
4344) -> Option<(Pos, Pos, MotionKind)> {
4345 match obj {
4346 TextObject::Word { big } => {
4347 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4348 }
4349 TextObject::Quote(q) => {
4350 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4351 }
4352 TextObject::Bracket(open) => {
4353 bracket_text_object(ed, open, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4354 }
4355 TextObject::Paragraph => {
4356 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4357 }
4358 TextObject::XmlTag => {
4359 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4360 }
4361 TextObject::Sentence => {
4362 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4363 }
4364 }
4365}
4366
4367fn sentence_boundary<H: crate::types::Host>(
4371 ed: &Editor<hjkl_buffer::Buffer, H>,
4372 forward: bool,
4373) -> Option<(usize, usize)> {
4374 let lines = buf_lines_to_vec(&ed.buffer);
4375 if lines.is_empty() {
4376 return None;
4377 }
4378 let pos_to_idx = |pos: (usize, usize)| -> usize {
4379 let mut idx = 0;
4380 for line in lines.iter().take(pos.0) {
4381 idx += line.chars().count() + 1;
4382 }
4383 idx + pos.1
4384 };
4385 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4386 for (r, line) in lines.iter().enumerate() {
4387 let len = line.chars().count();
4388 if idx <= len {
4389 return (r, idx);
4390 }
4391 idx -= len + 1;
4392 }
4393 let last = lines.len().saturating_sub(1);
4394 (last, lines[last].chars().count())
4395 };
4396 let mut chars: Vec<char> = Vec::new();
4397 for (r, line) in lines.iter().enumerate() {
4398 chars.extend(line.chars());
4399 if r + 1 < lines.len() {
4400 chars.push('\n');
4401 }
4402 }
4403 if chars.is_empty() {
4404 return None;
4405 }
4406 let total = chars.len();
4407 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4408 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4409
4410 if forward {
4411 let mut i = cursor_idx + 1;
4414 while i < total {
4415 if is_terminator(chars[i]) {
4416 while i + 1 < total && is_terminator(chars[i + 1]) {
4417 i += 1;
4418 }
4419 if i + 1 >= total {
4420 return None;
4421 }
4422 if chars[i + 1].is_whitespace() {
4423 let mut j = i + 1;
4424 while j < total && chars[j].is_whitespace() {
4425 j += 1;
4426 }
4427 if j >= total {
4428 return None;
4429 }
4430 return Some(idx_to_pos(j));
4431 }
4432 }
4433 i += 1;
4434 }
4435 None
4436 } else {
4437 let find_start = |from: usize| -> Option<usize> {
4441 let mut start = from;
4442 while start > 0 {
4443 let prev = chars[start - 1];
4444 if prev.is_whitespace() {
4445 let mut k = start - 1;
4446 while k > 0 && chars[k - 1].is_whitespace() {
4447 k -= 1;
4448 }
4449 if k > 0 && is_terminator(chars[k - 1]) {
4450 break;
4451 }
4452 }
4453 start -= 1;
4454 }
4455 while start < total && chars[start].is_whitespace() {
4456 start += 1;
4457 }
4458 (start < total).then_some(start)
4459 };
4460 let current_start = find_start(cursor_idx)?;
4461 if current_start < cursor_idx {
4462 return Some(idx_to_pos(current_start));
4463 }
4464 let mut k = current_start;
4467 while k > 0 && chars[k - 1].is_whitespace() {
4468 k -= 1;
4469 }
4470 if k == 0 {
4471 return None;
4472 }
4473 let prev_start = find_start(k - 1)?;
4474 Some(idx_to_pos(prev_start))
4475 }
4476}
4477
4478fn sentence_text_object<H: crate::types::Host>(
4484 ed: &Editor<hjkl_buffer::Buffer, H>,
4485 inner: bool,
4486) -> Option<((usize, usize), (usize, usize))> {
4487 let lines = buf_lines_to_vec(&ed.buffer);
4488 if lines.is_empty() {
4489 return None;
4490 }
4491 let pos_to_idx = |pos: (usize, usize)| -> usize {
4494 let mut idx = 0;
4495 for line in lines.iter().take(pos.0) {
4496 idx += line.chars().count() + 1;
4497 }
4498 idx + pos.1
4499 };
4500 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4501 for (r, line) in lines.iter().enumerate() {
4502 let len = line.chars().count();
4503 if idx <= len {
4504 return (r, idx);
4505 }
4506 idx -= len + 1;
4507 }
4508 let last = lines.len().saturating_sub(1);
4509 (last, lines[last].chars().count())
4510 };
4511 let mut chars: Vec<char> = Vec::new();
4512 for (r, line) in lines.iter().enumerate() {
4513 chars.extend(line.chars());
4514 if r + 1 < lines.len() {
4515 chars.push('\n');
4516 }
4517 }
4518 if chars.is_empty() {
4519 return None;
4520 }
4521
4522 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4523 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4524
4525 let mut start = cursor_idx;
4529 while start > 0 {
4530 let prev = chars[start - 1];
4531 if prev.is_whitespace() {
4532 let mut k = start - 1;
4536 while k > 0 && chars[k - 1].is_whitespace() {
4537 k -= 1;
4538 }
4539 if k > 0 && is_terminator(chars[k - 1]) {
4540 break;
4541 }
4542 }
4543 start -= 1;
4544 }
4545 while start < chars.len() && chars[start].is_whitespace() {
4548 start += 1;
4549 }
4550 if start >= chars.len() {
4551 return None;
4552 }
4553
4554 let mut end = start;
4557 while end < chars.len() {
4558 if is_terminator(chars[end]) {
4559 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4561 end += 1;
4562 }
4563 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4566 break;
4567 }
4568 }
4569 end += 1;
4570 }
4571 let end_idx = (end + 1).min(chars.len());
4573
4574 let final_end = if inner {
4575 end_idx
4576 } else {
4577 let mut e = end_idx;
4581 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4582 e += 1;
4583 }
4584 e
4585 };
4586
4587 Some((idx_to_pos(start), idx_to_pos(final_end)))
4588}
4589
4590fn tag_text_object<H: crate::types::Host>(
4594 ed: &Editor<hjkl_buffer::Buffer, H>,
4595 inner: bool,
4596) -> Option<((usize, usize), (usize, usize))> {
4597 let lines = buf_lines_to_vec(&ed.buffer);
4598 if lines.is_empty() {
4599 return None;
4600 }
4601 let pos_to_idx = |pos: (usize, usize)| -> usize {
4605 let mut idx = 0;
4606 for line in lines.iter().take(pos.0) {
4607 idx += line.chars().count() + 1;
4608 }
4609 idx + pos.1
4610 };
4611 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4612 for (r, line) in lines.iter().enumerate() {
4613 let len = line.chars().count();
4614 if idx <= len {
4615 return (r, idx);
4616 }
4617 idx -= len + 1;
4618 }
4619 let last = lines.len().saturating_sub(1);
4620 (last, lines[last].chars().count())
4621 };
4622 let mut chars: Vec<char> = Vec::new();
4623 for (r, line) in lines.iter().enumerate() {
4624 chars.extend(line.chars());
4625 if r + 1 < lines.len() {
4626 chars.push('\n');
4627 }
4628 }
4629 let cursor_idx = pos_to_idx(ed.cursor());
4630
4631 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4639 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4640 let mut i = 0;
4641 while i < chars.len() {
4642 if chars[i] != '<' {
4643 i += 1;
4644 continue;
4645 }
4646 let mut j = i + 1;
4647 while j < chars.len() && chars[j] != '>' {
4648 j += 1;
4649 }
4650 if j >= chars.len() {
4651 break;
4652 }
4653 let inside: String = chars[i + 1..j].iter().collect();
4654 let close_end = j + 1;
4655 let trimmed = inside.trim();
4656 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4657 i = close_end;
4658 continue;
4659 }
4660 if let Some(rest) = trimmed.strip_prefix('/') {
4661 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4662 if !name.is_empty()
4663 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4664 {
4665 let (open_start, content_start, _) = stack[stack_idx].clone();
4666 stack.truncate(stack_idx);
4667 let content_end = i;
4668 let candidate = (open_start, content_start, content_end, close_end);
4669 if cursor_idx >= content_start && cursor_idx <= content_end {
4670 innermost = match innermost {
4671 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4672 Some(candidate)
4673 }
4674 None => Some(candidate),
4675 existing => existing,
4676 };
4677 } else if open_start >= cursor_idx && next_after.is_none() {
4678 next_after = Some(candidate);
4679 }
4680 }
4681 } else if !trimmed.ends_with('/') {
4682 let name: String = trimmed
4683 .split(|c: char| c.is_whitespace() || c == '/')
4684 .next()
4685 .unwrap_or("")
4686 .to_string();
4687 if !name.is_empty() {
4688 stack.push((i, close_end, name));
4689 }
4690 }
4691 i = close_end;
4692 }
4693
4694 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4695 if inner {
4696 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4697 } else {
4698 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4699 }
4700}
4701
4702fn is_wordchar(c: char) -> bool {
4703 c.is_alphanumeric() || c == '_'
4704}
4705
4706pub(crate) use hjkl_buffer::is_keyword_char;
4710
4711fn word_text_object<H: crate::types::Host>(
4712 ed: &Editor<hjkl_buffer::Buffer, H>,
4713 inner: bool,
4714 big: bool,
4715) -> Option<((usize, usize), (usize, usize))> {
4716 let (row, col) = ed.cursor();
4717 let line = buf_line(&ed.buffer, row)?;
4718 let chars: Vec<char> = line.chars().collect();
4719 if chars.is_empty() {
4720 return None;
4721 }
4722 let at = col.min(chars.len().saturating_sub(1));
4723 let classify = |c: char| -> u8 {
4724 if c.is_whitespace() {
4725 0
4726 } else if big || is_wordchar(c) {
4727 1
4728 } else {
4729 2
4730 }
4731 };
4732 let cls = classify(chars[at]);
4733 let mut start = at;
4734 while start > 0 && classify(chars[start - 1]) == cls {
4735 start -= 1;
4736 }
4737 let mut end = at;
4738 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4739 end += 1;
4740 }
4741 let char_byte = |i: usize| {
4743 if i >= chars.len() {
4744 line.len()
4745 } else {
4746 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4747 }
4748 };
4749 let mut start_col = char_byte(start);
4750 let mut end_col = char_byte(end + 1);
4752 if !inner {
4753 let mut t = end + 1;
4755 let mut included_trailing = false;
4756 while t < chars.len() && chars[t].is_whitespace() {
4757 included_trailing = true;
4758 t += 1;
4759 }
4760 if included_trailing {
4761 end_col = char_byte(t);
4762 } else {
4763 let mut s = start;
4764 while s > 0 && chars[s - 1].is_whitespace() {
4765 s -= 1;
4766 }
4767 start_col = char_byte(s);
4768 }
4769 }
4770 Some(((row, start_col), (row, end_col)))
4771}
4772
4773fn quote_text_object<H: crate::types::Host>(
4774 ed: &Editor<hjkl_buffer::Buffer, H>,
4775 q: char,
4776 inner: bool,
4777) -> Option<((usize, usize), (usize, usize))> {
4778 let (row, col) = ed.cursor();
4779 let line = buf_line(&ed.buffer, row)?;
4780 let bytes = line.as_bytes();
4781 let q_byte = q as u8;
4782 let mut positions: Vec<usize> = Vec::new();
4784 for (i, &b) in bytes.iter().enumerate() {
4785 if b == q_byte {
4786 positions.push(i);
4787 }
4788 }
4789 if positions.len() < 2 {
4790 return None;
4791 }
4792 let mut open_idx: Option<usize> = None;
4793 let mut close_idx: Option<usize> = None;
4794 for pair in positions.chunks(2) {
4795 if pair.len() < 2 {
4796 break;
4797 }
4798 if col >= pair[0] && col <= pair[1] {
4799 open_idx = Some(pair[0]);
4800 close_idx = Some(pair[1]);
4801 break;
4802 }
4803 if col < pair[0] {
4804 open_idx = Some(pair[0]);
4805 close_idx = Some(pair[1]);
4806 break;
4807 }
4808 }
4809 let open = open_idx?;
4810 let close = close_idx?;
4811 if inner {
4813 if close <= open + 1 {
4814 return None;
4815 }
4816 Some(((row, open + 1), (row, close)))
4817 } else {
4818 Some(((row, open), (row, close + 1)))
4819 }
4820}
4821
4822fn bracket_text_object<H: crate::types::Host>(
4823 ed: &Editor<hjkl_buffer::Buffer, H>,
4824 open: char,
4825 inner: bool,
4826) -> Option<((usize, usize), (usize, usize))> {
4827 let close = match open {
4828 '(' => ')',
4829 '[' => ']',
4830 '{' => '}',
4831 '<' => '>',
4832 _ => return None,
4833 };
4834 let (row, col) = ed.cursor();
4835 let lines = buf_lines_to_vec(&ed.buffer);
4836 let lines = lines.as_slice();
4837 let open_pos = find_open_bracket(lines, row, col, open, close)
4842 .or_else(|| find_next_open(lines, row, col, open))?;
4843 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
4844 if inner {
4846 let inner_start = advance_pos(lines, open_pos);
4847 if inner_start.0 > close_pos.0
4848 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
4849 {
4850 return None;
4851 }
4852 Some((inner_start, close_pos))
4853 } else {
4854 Some((open_pos, advance_pos(lines, close_pos)))
4855 }
4856}
4857
4858fn find_open_bracket(
4859 lines: &[String],
4860 row: usize,
4861 col: usize,
4862 open: char,
4863 close: char,
4864) -> Option<(usize, usize)> {
4865 let mut depth: i32 = 0;
4866 let mut r = row;
4867 let mut c = col as isize;
4868 loop {
4869 let cur = &lines[r];
4870 let chars: Vec<char> = cur.chars().collect();
4871 if (c as usize) >= chars.len() {
4875 c = chars.len() as isize - 1;
4876 }
4877 while c >= 0 {
4878 let ch = chars[c as usize];
4879 if ch == close {
4880 depth += 1;
4881 } else if ch == open {
4882 if depth == 0 {
4883 return Some((r, c as usize));
4884 }
4885 depth -= 1;
4886 }
4887 c -= 1;
4888 }
4889 if r == 0 {
4890 return None;
4891 }
4892 r -= 1;
4893 c = lines[r].chars().count() as isize - 1;
4894 }
4895}
4896
4897fn find_close_bracket(
4898 lines: &[String],
4899 row: usize,
4900 start_col: usize,
4901 open: char,
4902 close: char,
4903) -> Option<(usize, usize)> {
4904 let mut depth: i32 = 0;
4905 let mut r = row;
4906 let mut c = start_col;
4907 loop {
4908 let cur = &lines[r];
4909 let chars: Vec<char> = cur.chars().collect();
4910 while c < chars.len() {
4911 let ch = chars[c];
4912 if ch == open {
4913 depth += 1;
4914 } else if ch == close {
4915 if depth == 0 {
4916 return Some((r, c));
4917 }
4918 depth -= 1;
4919 }
4920 c += 1;
4921 }
4922 if r + 1 >= lines.len() {
4923 return None;
4924 }
4925 r += 1;
4926 c = 0;
4927 }
4928}
4929
4930fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
4934 let mut r = row;
4935 let mut c = col;
4936 while r < lines.len() {
4937 let chars: Vec<char> = lines[r].chars().collect();
4938 while c < chars.len() {
4939 if chars[c] == open {
4940 return Some((r, c));
4941 }
4942 c += 1;
4943 }
4944 r += 1;
4945 c = 0;
4946 }
4947 None
4948}
4949
4950fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
4951 let (r, c) = pos;
4952 let line_len = lines[r].chars().count();
4953 if c < line_len {
4954 (r, c + 1)
4955 } else if r + 1 < lines.len() {
4956 (r + 1, 0)
4957 } else {
4958 pos
4959 }
4960}
4961
4962fn paragraph_text_object<H: crate::types::Host>(
4963 ed: &Editor<hjkl_buffer::Buffer, H>,
4964 inner: bool,
4965) -> Option<((usize, usize), (usize, usize))> {
4966 let (row, _) = ed.cursor();
4967 let lines = buf_lines_to_vec(&ed.buffer);
4968 if lines.is_empty() {
4969 return None;
4970 }
4971 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
4973 if is_blank(row) {
4974 return None;
4975 }
4976 let mut top = row;
4977 while top > 0 && !is_blank(top - 1) {
4978 top -= 1;
4979 }
4980 let mut bot = row;
4981 while bot + 1 < lines.len() && !is_blank(bot + 1) {
4982 bot += 1;
4983 }
4984 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
4986 bot += 1;
4987 }
4988 let end_col = lines[bot].chars().count();
4989 Some(((top, 0), (bot, end_col)))
4990}
4991
4992fn read_vim_range<H: crate::types::Host>(
4998 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4999 start: (usize, usize),
5000 end: (usize, usize),
5001 kind: MotionKind,
5002) -> String {
5003 let (top, bot) = order(start, end);
5004 ed.sync_buffer_content_from_textarea();
5005 let lines = buf_lines_to_vec(&ed.buffer);
5006 match kind {
5007 MotionKind::Linewise => {
5008 let lo = top.0;
5009 let hi = bot.0.min(lines.len().saturating_sub(1));
5010 let mut text = lines[lo..=hi].join("\n");
5011 text.push('\n');
5012 text
5013 }
5014 MotionKind::Inclusive | MotionKind::Exclusive => {
5015 let inclusive = matches!(kind, MotionKind::Inclusive);
5016 let mut out = String::new();
5018 for row in top.0..=bot.0 {
5019 let line = lines.get(row).map(String::as_str).unwrap_or("");
5020 let lo = if row == top.0 { top.1 } else { 0 };
5021 let hi_unclamped = if row == bot.0 {
5022 if inclusive { bot.1 + 1 } else { bot.1 }
5023 } else {
5024 line.chars().count() + 1
5025 };
5026 let row_chars: Vec<char> = line.chars().collect();
5027 let hi = hi_unclamped.min(row_chars.len());
5028 if lo < hi {
5029 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5030 }
5031 if row < bot.0 {
5032 out.push('\n');
5033 }
5034 }
5035 out
5036 }
5037 }
5038}
5039
5040fn cut_vim_range<H: crate::types::Host>(
5049 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5050 start: (usize, usize),
5051 end: (usize, usize),
5052 kind: MotionKind,
5053) -> String {
5054 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5055 let (top, bot) = order(start, end);
5056 ed.sync_buffer_content_from_textarea();
5057 let (buf_start, buf_end, buf_kind) = match kind {
5058 MotionKind::Linewise => (
5059 Position::new(top.0, 0),
5060 Position::new(bot.0, 0),
5061 BufKind::Line,
5062 ),
5063 MotionKind::Inclusive => {
5064 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5065 let next = if bot.1 < line_chars {
5069 Position::new(bot.0, bot.1 + 1)
5070 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5071 Position::new(bot.0 + 1, 0)
5072 } else {
5073 Position::new(bot.0, line_chars)
5074 };
5075 (Position::new(top.0, top.1), next, BufKind::Char)
5076 }
5077 MotionKind::Exclusive => (
5078 Position::new(top.0, top.1),
5079 Position::new(bot.0, bot.1),
5080 BufKind::Char,
5081 ),
5082 };
5083 let inverse = ed.mutate_edit(Edit::DeleteRange {
5084 start: buf_start,
5085 end: buf_end,
5086 kind: buf_kind,
5087 });
5088 let text = match inverse {
5089 Edit::InsertStr { text, .. } => text,
5090 _ => String::new(),
5091 };
5092 if !text.is_empty() {
5093 ed.record_yank_to_host(text.clone());
5094 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5095 }
5096 ed.push_buffer_cursor_to_textarea();
5097 text
5098}
5099
5100fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5106 use hjkl_buffer::{Edit, MotionKind, Position};
5107 ed.sync_buffer_content_from_textarea();
5108 let cursor = buf_cursor_pos(&ed.buffer);
5109 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5110 if cursor.col >= line_chars {
5111 return;
5112 }
5113 let inverse = ed.mutate_edit(Edit::DeleteRange {
5114 start: cursor,
5115 end: Position::new(cursor.row, line_chars),
5116 kind: MotionKind::Char,
5117 });
5118 if let Edit::InsertStr { text, .. } = inverse
5119 && !text.is_empty()
5120 {
5121 ed.record_yank_to_host(text.clone());
5122 ed.vim.yank_linewise = false;
5123 ed.set_yank(text);
5124 }
5125 buf_set_cursor_pos(&mut ed.buffer, cursor);
5126 ed.push_buffer_cursor_to_textarea();
5127}
5128
5129fn do_char_delete<H: crate::types::Host>(
5130 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5131 forward: bool,
5132 count: usize,
5133) {
5134 use hjkl_buffer::{Edit, MotionKind, Position};
5135 ed.push_undo();
5136 ed.sync_buffer_content_from_textarea();
5137 for _ in 0..count {
5138 let cursor = buf_cursor_pos(&ed.buffer);
5139 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5140 if forward {
5141 if cursor.col >= line_chars {
5144 continue;
5145 }
5146 ed.mutate_edit(Edit::DeleteRange {
5147 start: cursor,
5148 end: Position::new(cursor.row, cursor.col + 1),
5149 kind: MotionKind::Char,
5150 });
5151 } else {
5152 if cursor.col == 0 {
5154 continue;
5155 }
5156 ed.mutate_edit(Edit::DeleteRange {
5157 start: Position::new(cursor.row, cursor.col - 1),
5158 end: cursor,
5159 kind: MotionKind::Char,
5160 });
5161 }
5162 }
5163 ed.push_buffer_cursor_to_textarea();
5164}
5165
5166fn adjust_number<H: crate::types::Host>(
5170 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5171 delta: i64,
5172) -> bool {
5173 use hjkl_buffer::{Edit, MotionKind, Position};
5174 ed.sync_buffer_content_from_textarea();
5175 let cursor = buf_cursor_pos(&ed.buffer);
5176 let row = cursor.row;
5177 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5178 Some(l) => l.chars().collect(),
5179 None => return false,
5180 };
5181 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5182 return false;
5183 };
5184 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5185 digit_start - 1
5186 } else {
5187 digit_start
5188 };
5189 let mut span_end = digit_start;
5190 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5191 span_end += 1;
5192 }
5193 let s: String = chars[span_start..span_end].iter().collect();
5194 let Ok(n) = s.parse::<i64>() else {
5195 return false;
5196 };
5197 let new_s = n.saturating_add(delta).to_string();
5198
5199 ed.push_undo();
5200 let span_start_pos = Position::new(row, span_start);
5201 let span_end_pos = Position::new(row, span_end);
5202 ed.mutate_edit(Edit::DeleteRange {
5203 start: span_start_pos,
5204 end: span_end_pos,
5205 kind: MotionKind::Char,
5206 });
5207 ed.mutate_edit(Edit::InsertStr {
5208 at: span_start_pos,
5209 text: new_s.clone(),
5210 });
5211 let new_len = new_s.chars().count();
5212 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5213 ed.push_buffer_cursor_to_textarea();
5214 true
5215}
5216
5217fn replace_char<H: crate::types::Host>(
5218 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5219 ch: char,
5220 count: usize,
5221) {
5222 use hjkl_buffer::{Edit, MotionKind, Position};
5223 ed.push_undo();
5224 ed.sync_buffer_content_from_textarea();
5225 for _ in 0..count {
5226 let cursor = buf_cursor_pos(&ed.buffer);
5227 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5228 if cursor.col >= line_chars {
5229 break;
5230 }
5231 ed.mutate_edit(Edit::DeleteRange {
5232 start: cursor,
5233 end: Position::new(cursor.row, cursor.col + 1),
5234 kind: MotionKind::Char,
5235 });
5236 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5237 }
5238 crate::motions::move_left(&mut ed.buffer, 1);
5240 ed.push_buffer_cursor_to_textarea();
5241}
5242
5243fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5244 use hjkl_buffer::{Edit, MotionKind, Position};
5245 ed.sync_buffer_content_from_textarea();
5246 let cursor = buf_cursor_pos(&ed.buffer);
5247 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5248 return;
5249 };
5250 let toggled = if c.is_uppercase() {
5251 c.to_lowercase().next().unwrap_or(c)
5252 } else {
5253 c.to_uppercase().next().unwrap_or(c)
5254 };
5255 ed.mutate_edit(Edit::DeleteRange {
5256 start: cursor,
5257 end: Position::new(cursor.row, cursor.col + 1),
5258 kind: MotionKind::Char,
5259 });
5260 ed.mutate_edit(Edit::InsertChar {
5261 at: cursor,
5262 ch: toggled,
5263 });
5264}
5265
5266fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5267 use hjkl_buffer::{Edit, Position};
5268 ed.sync_buffer_content_from_textarea();
5269 let row = buf_cursor_pos(&ed.buffer).row;
5270 if row + 1 >= buf_row_count(&ed.buffer) {
5271 return;
5272 }
5273 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5274 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5275 let next_trimmed = next_raw.trim_start();
5276 let cur_chars = cur_line.chars().count();
5277 let next_chars = next_raw.chars().count();
5278 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5281 " "
5282 } else {
5283 ""
5284 };
5285 let joined = format!("{cur_line}{separator}{next_trimmed}");
5286 ed.mutate_edit(Edit::Replace {
5287 start: Position::new(row, 0),
5288 end: Position::new(row + 1, next_chars),
5289 with: joined,
5290 });
5291 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5295 ed.push_buffer_cursor_to_textarea();
5296}
5297
5298fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5301 use hjkl_buffer::Edit;
5302 ed.sync_buffer_content_from_textarea();
5303 let row = buf_cursor_pos(&ed.buffer).row;
5304 if row + 1 >= buf_row_count(&ed.buffer) {
5305 return;
5306 }
5307 let join_col = buf_line_chars(&ed.buffer, row);
5308 ed.mutate_edit(Edit::JoinLines {
5309 row,
5310 count: 1,
5311 with_space: false,
5312 });
5313 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5315 ed.push_buffer_cursor_to_textarea();
5316}
5317
5318fn do_paste<H: crate::types::Host>(
5319 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5320 before: bool,
5321 count: usize,
5322) {
5323 use hjkl_buffer::{Edit, Position};
5324 ed.push_undo();
5325 let selector = ed.vim.pending_register.take();
5330 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5331 Some(slot) => (slot.text.clone(), slot.linewise),
5332 None => {
5338 let s = &ed.registers().unnamed;
5339 (s.text.clone(), s.linewise)
5340 }
5341 };
5342 for _ in 0..count {
5343 ed.sync_buffer_content_from_textarea();
5344 let yank = yank.clone();
5345 if yank.is_empty() {
5346 continue;
5347 }
5348 if linewise {
5349 let text = yank.trim_matches('\n').to_string();
5353 let row = buf_cursor_pos(&ed.buffer).row;
5354 let target_row = if before {
5355 ed.mutate_edit(Edit::InsertStr {
5356 at: Position::new(row, 0),
5357 text: format!("{text}\n"),
5358 });
5359 row
5360 } else {
5361 let line_chars = buf_line_chars(&ed.buffer, row);
5362 ed.mutate_edit(Edit::InsertStr {
5363 at: Position::new(row, line_chars),
5364 text: format!("\n{text}"),
5365 });
5366 row + 1
5367 };
5368 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5369 crate::motions::move_first_non_blank(&mut ed.buffer);
5370 ed.push_buffer_cursor_to_textarea();
5371 } else {
5372 let cursor = buf_cursor_pos(&ed.buffer);
5376 let at = if before {
5377 cursor
5378 } else {
5379 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5380 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5381 };
5382 ed.mutate_edit(Edit::InsertStr {
5383 at,
5384 text: yank.clone(),
5385 });
5386 crate::motions::move_left(&mut ed.buffer, 1);
5389 ed.push_buffer_cursor_to_textarea();
5390 }
5391 }
5392 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5394}
5395
5396pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5397 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5398 let current = ed.snapshot();
5399 ed.redo_stack.push(current);
5400 ed.restore(lines, cursor);
5401 }
5402 ed.vim.mode = Mode::Normal;
5403}
5404
5405pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5406 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5407 let current = ed.snapshot();
5408 ed.undo_stack.push(current);
5409 ed.cap_undo();
5410 ed.restore(lines, cursor);
5411 }
5412 ed.vim.mode = Mode::Normal;
5413}
5414
5415fn replay_insert_and_finish<H: crate::types::Host>(
5422 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5423 text: &str,
5424) {
5425 use hjkl_buffer::{Edit, Position};
5426 let cursor = ed.cursor();
5427 ed.mutate_edit(Edit::InsertStr {
5428 at: Position::new(cursor.0, cursor.1),
5429 text: text.to_string(),
5430 });
5431 if ed.vim.insert_session.take().is_some() {
5432 if ed.cursor().1 > 0 {
5433 crate::motions::move_left(&mut ed.buffer, 1);
5434 ed.push_buffer_cursor_to_textarea();
5435 }
5436 ed.vim.mode = Mode::Normal;
5437 }
5438}
5439
5440fn replay_last_change<H: crate::types::Host>(
5441 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5442 outer_count: usize,
5443) {
5444 let Some(change) = ed.vim.last_change.clone() else {
5445 return;
5446 };
5447 ed.vim.replaying = true;
5448 let scale = if outer_count > 0 { outer_count } else { 1 };
5449 match change {
5450 LastChange::OpMotion {
5451 op,
5452 motion,
5453 count,
5454 inserted,
5455 } => {
5456 let total = count.max(1) * scale;
5457 apply_op_with_motion(ed, op, &motion, total);
5458 if let Some(text) = inserted {
5459 replay_insert_and_finish(ed, &text);
5460 }
5461 }
5462 LastChange::OpTextObj {
5463 op,
5464 obj,
5465 inner,
5466 inserted,
5467 } => {
5468 apply_op_with_text_object(ed, op, obj, inner);
5469 if let Some(text) = inserted {
5470 replay_insert_and_finish(ed, &text);
5471 }
5472 }
5473 LastChange::LineOp {
5474 op,
5475 count,
5476 inserted,
5477 } => {
5478 let total = count.max(1) * scale;
5479 execute_line_op(ed, op, total);
5480 if let Some(text) = inserted {
5481 replay_insert_and_finish(ed, &text);
5482 }
5483 }
5484 LastChange::CharDel { forward, count } => {
5485 do_char_delete(ed, forward, count * scale);
5486 }
5487 LastChange::ReplaceChar { ch, count } => {
5488 replace_char(ed, ch, count * scale);
5489 }
5490 LastChange::ToggleCase { count } => {
5491 for _ in 0..count * scale {
5492 ed.push_undo();
5493 toggle_case_at_cursor(ed);
5494 }
5495 }
5496 LastChange::JoinLine { count } => {
5497 for _ in 0..count * scale {
5498 ed.push_undo();
5499 join_line(ed);
5500 }
5501 }
5502 LastChange::Paste { before, count } => {
5503 do_paste(ed, before, count * scale);
5504 }
5505 LastChange::DeleteToEol { inserted } => {
5506 use hjkl_buffer::{Edit, Position};
5507 ed.push_undo();
5508 delete_to_eol(ed);
5509 if let Some(text) = inserted {
5510 let cursor = ed.cursor();
5511 ed.mutate_edit(Edit::InsertStr {
5512 at: Position::new(cursor.0, cursor.1),
5513 text,
5514 });
5515 }
5516 }
5517 LastChange::OpenLine { above, inserted } => {
5518 use hjkl_buffer::{Edit, Position};
5519 ed.push_undo();
5520 ed.sync_buffer_content_from_textarea();
5521 let row = buf_cursor_pos(&ed.buffer).row;
5522 if above {
5523 ed.mutate_edit(Edit::InsertStr {
5524 at: Position::new(row, 0),
5525 text: "\n".to_string(),
5526 });
5527 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5528 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5529 } else {
5530 let line_chars = buf_line_chars(&ed.buffer, row);
5531 ed.mutate_edit(Edit::InsertStr {
5532 at: Position::new(row, line_chars),
5533 text: "\n".to_string(),
5534 });
5535 }
5536 ed.push_buffer_cursor_to_textarea();
5537 let cursor = ed.cursor();
5538 ed.mutate_edit(Edit::InsertStr {
5539 at: Position::new(cursor.0, cursor.1),
5540 text: inserted,
5541 });
5542 }
5543 LastChange::InsertAt {
5544 entry,
5545 inserted,
5546 count,
5547 } => {
5548 use hjkl_buffer::{Edit, Position};
5549 ed.push_undo();
5550 match entry {
5551 InsertEntry::I => {}
5552 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5553 InsertEntry::A => {
5554 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5555 ed.push_buffer_cursor_to_textarea();
5556 }
5557 InsertEntry::ShiftA => {
5558 crate::motions::move_line_end(&mut ed.buffer);
5559 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5560 ed.push_buffer_cursor_to_textarea();
5561 }
5562 }
5563 for _ in 0..count.max(1) {
5564 let cursor = ed.cursor();
5565 ed.mutate_edit(Edit::InsertStr {
5566 at: Position::new(cursor.0, cursor.1),
5567 text: inserted.clone(),
5568 });
5569 }
5570 }
5571 }
5572 ed.vim.replaying = false;
5573}
5574
5575fn extract_inserted(before: &str, after: &str) -> String {
5578 let before_chars: Vec<char> = before.chars().collect();
5579 let after_chars: Vec<char> = after.chars().collect();
5580 if after_chars.len() <= before_chars.len() {
5581 return String::new();
5582 }
5583 let prefix = before_chars
5584 .iter()
5585 .zip(after_chars.iter())
5586 .take_while(|(a, b)| a == b)
5587 .count();
5588 let max_suffix = before_chars.len() - prefix;
5589 let suffix = before_chars
5590 .iter()
5591 .rev()
5592 .zip(after_chars.iter().rev())
5593 .take(max_suffix)
5594 .take_while(|(a, b)| a == b)
5595 .count();
5596 after_chars[prefix..after_chars.len() - suffix]
5597 .iter()
5598 .collect()
5599}
5600
5601#[cfg(all(test, feature = "crossterm"))]
5604mod tests {
5605 use crate::VimMode;
5606 use crate::editor::Editor;
5607 use crate::types::Host;
5608 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5609
5610 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5611 let mut iter = keys.chars().peekable();
5615 while let Some(c) = iter.next() {
5616 if c == '<' {
5617 let mut tag = String::new();
5618 for ch in iter.by_ref() {
5619 if ch == '>' {
5620 break;
5621 }
5622 tag.push(ch);
5623 }
5624 let ev = match tag.as_str() {
5625 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5626 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5627 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5628 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5629 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5630 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5631 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5632 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5633 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5637 s if s.starts_with("C-") => {
5638 let ch = s.chars().nth(2).unwrap();
5639 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5640 }
5641 _ => continue,
5642 };
5643 e.handle_key(ev);
5644 } else {
5645 let mods = if c.is_uppercase() {
5646 KeyModifiers::SHIFT
5647 } else {
5648 KeyModifiers::NONE
5649 };
5650 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5651 }
5652 }
5653 }
5654
5655 fn editor_with(content: &str) -> Editor {
5656 let opts = crate::types::Options {
5661 shiftwidth: 2,
5662 ..crate::types::Options::default()
5663 };
5664 let mut e = Editor::new(
5665 hjkl_buffer::Buffer::new(),
5666 crate::types::DefaultHost::new(),
5667 opts,
5668 );
5669 e.set_content(content);
5670 e
5671 }
5672
5673 #[test]
5674 fn f_char_jumps_on_line() {
5675 let mut e = editor_with("hello world");
5676 run_keys(&mut e, "fw");
5677 assert_eq!(e.cursor(), (0, 6));
5678 }
5679
5680 #[test]
5681 fn cap_f_jumps_backward() {
5682 let mut e = editor_with("hello world");
5683 e.jump_cursor(0, 10);
5684 run_keys(&mut e, "Fo");
5685 assert_eq!(e.cursor().1, 7);
5686 }
5687
5688 #[test]
5689 fn t_stops_before_char() {
5690 let mut e = editor_with("hello");
5691 run_keys(&mut e, "tl");
5692 assert_eq!(e.cursor(), (0, 1));
5693 }
5694
5695 #[test]
5696 fn semicolon_repeats_find() {
5697 let mut e = editor_with("aa.bb.cc");
5698 run_keys(&mut e, "f.");
5699 assert_eq!(e.cursor().1, 2);
5700 run_keys(&mut e, ";");
5701 assert_eq!(e.cursor().1, 5);
5702 }
5703
5704 #[test]
5705 fn comma_repeats_find_reverse() {
5706 let mut e = editor_with("aa.bb.cc");
5707 run_keys(&mut e, "f.");
5708 run_keys(&mut e, ";");
5709 run_keys(&mut e, ",");
5710 assert_eq!(e.cursor().1, 2);
5711 }
5712
5713 #[test]
5714 fn di_quote_deletes_content() {
5715 let mut e = editor_with("foo \"bar\" baz");
5716 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5718 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5719 }
5720
5721 #[test]
5722 fn da_quote_deletes_with_quotes() {
5723 let mut e = editor_with("foo \"bar\" baz");
5724 e.jump_cursor(0, 6);
5725 run_keys(&mut e, "da\"");
5726 assert_eq!(e.buffer().lines()[0], "foo baz");
5727 }
5728
5729 #[test]
5730 fn ci_paren_deletes_and_inserts() {
5731 let mut e = editor_with("fn(a, b, c)");
5732 e.jump_cursor(0, 5);
5733 run_keys(&mut e, "ci(");
5734 assert_eq!(e.vim_mode(), VimMode::Insert);
5735 assert_eq!(e.buffer().lines()[0], "fn()");
5736 }
5737
5738 #[test]
5739 fn diw_deletes_inner_word() {
5740 let mut e = editor_with("hello world");
5741 e.jump_cursor(0, 2);
5742 run_keys(&mut e, "diw");
5743 assert_eq!(e.buffer().lines()[0], " world");
5744 }
5745
5746 #[test]
5747 fn daw_deletes_word_with_trailing_space() {
5748 let mut e = editor_with("hello world");
5749 run_keys(&mut e, "daw");
5750 assert_eq!(e.buffer().lines()[0], "world");
5751 }
5752
5753 #[test]
5754 fn percent_jumps_to_matching_bracket() {
5755 let mut e = editor_with("foo(bar)");
5756 e.jump_cursor(0, 3);
5757 run_keys(&mut e, "%");
5758 assert_eq!(e.cursor().1, 7);
5759 run_keys(&mut e, "%");
5760 assert_eq!(e.cursor().1, 3);
5761 }
5762
5763 #[test]
5764 fn dot_repeats_last_change() {
5765 let mut e = editor_with("aaa bbb ccc");
5766 run_keys(&mut e, "dw");
5767 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5768 run_keys(&mut e, ".");
5769 assert_eq!(e.buffer().lines()[0], "ccc");
5770 }
5771
5772 #[test]
5773 fn dot_repeats_change_operator_with_text() {
5774 let mut e = editor_with("foo foo foo");
5775 run_keys(&mut e, "cwbar<Esc>");
5776 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5777 run_keys(&mut e, "w");
5779 run_keys(&mut e, ".");
5780 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5781 }
5782
5783 #[test]
5784 fn dot_repeats_x() {
5785 let mut e = editor_with("abcdef");
5786 run_keys(&mut e, "x");
5787 run_keys(&mut e, "..");
5788 assert_eq!(e.buffer().lines()[0], "def");
5789 }
5790
5791 #[test]
5792 fn count_operator_motion_compose() {
5793 let mut e = editor_with("one two three four five");
5794 run_keys(&mut e, "d3w");
5795 assert_eq!(e.buffer().lines()[0], "four five");
5796 }
5797
5798 #[test]
5799 fn two_dd_deletes_two_lines() {
5800 let mut e = editor_with("a\nb\nc");
5801 run_keys(&mut e, "2dd");
5802 assert_eq!(e.buffer().lines().len(), 1);
5803 assert_eq!(e.buffer().lines()[0], "c");
5804 }
5805
5806 #[test]
5811 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5812 let mut e = editor_with("one\ntwo\n three\nfour");
5813 e.jump_cursor(1, 2);
5814 run_keys(&mut e, "dd");
5815 assert_eq!(e.buffer().lines()[1], " three");
5817 assert_eq!(e.cursor(), (1, 4));
5818 }
5819
5820 #[test]
5821 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5822 let mut e = editor_with("one\n two\nthree");
5823 e.jump_cursor(2, 0);
5824 run_keys(&mut e, "dd");
5825 assert_eq!(e.buffer().lines().len(), 2);
5827 assert_eq!(e.cursor(), (1, 2));
5828 }
5829
5830 #[test]
5831 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5832 let mut e = editor_with("lonely");
5833 run_keys(&mut e, "dd");
5834 assert_eq!(e.buffer().lines().len(), 1);
5835 assert_eq!(e.buffer().lines()[0], "");
5836 assert_eq!(e.cursor(), (0, 0));
5837 }
5838
5839 #[test]
5840 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5841 let mut e = editor_with("a\nb\nc\n d\ne");
5842 e.jump_cursor(1, 0);
5844 run_keys(&mut e, "3dd");
5845 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
5846 assert_eq!(e.cursor(), (1, 0));
5847 }
5848
5849 #[test]
5850 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
5851 let mut e = editor_with(" line one\n line two\n xyz!");
5870 e.jump_cursor(0, 8);
5872 assert_eq!(e.cursor(), (0, 8));
5873 run_keys(&mut e, "dd");
5876 assert_eq!(
5877 e.cursor(),
5878 (0, 4),
5879 "dd must place cursor on first-non-blank"
5880 );
5881 run_keys(&mut e, "j");
5885 let (row, col) = e.cursor();
5886 assert_eq!(row, 1);
5887 assert_eq!(
5888 col, 4,
5889 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
5890 );
5891 }
5892
5893 #[test]
5894 fn gu_lowercases_motion_range() {
5895 let mut e = editor_with("HELLO WORLD");
5896 run_keys(&mut e, "guw");
5897 assert_eq!(e.buffer().lines()[0], "hello WORLD");
5898 assert_eq!(e.cursor(), (0, 0));
5899 }
5900
5901 #[test]
5902 fn g_u_uppercases_text_object() {
5903 let mut e = editor_with("hello world");
5904 run_keys(&mut e, "gUiw");
5906 assert_eq!(e.buffer().lines()[0], "HELLO world");
5907 assert_eq!(e.cursor(), (0, 0));
5908 }
5909
5910 #[test]
5911 fn g_tilde_toggles_case_of_range() {
5912 let mut e = editor_with("Hello World");
5913 run_keys(&mut e, "g~iw");
5914 assert_eq!(e.buffer().lines()[0], "hELLO World");
5915 }
5916
5917 #[test]
5918 fn g_uu_uppercases_current_line() {
5919 let mut e = editor_with("select 1\nselect 2");
5920 run_keys(&mut e, "gUU");
5921 assert_eq!(e.buffer().lines()[0], "SELECT 1");
5922 assert_eq!(e.buffer().lines()[1], "select 2");
5923 }
5924
5925 #[test]
5926 fn gugu_lowercases_current_line() {
5927 let mut e = editor_with("FOO BAR\nBAZ");
5928 run_keys(&mut e, "gugu");
5929 assert_eq!(e.buffer().lines()[0], "foo bar");
5930 }
5931
5932 #[test]
5933 fn visual_u_uppercases_selection() {
5934 let mut e = editor_with("hello world");
5935 run_keys(&mut e, "veU");
5937 assert_eq!(e.buffer().lines()[0], "HELLO world");
5938 }
5939
5940 #[test]
5941 fn visual_line_u_lowercases_line() {
5942 let mut e = editor_with("HELLO WORLD\nOTHER");
5943 run_keys(&mut e, "Vu");
5944 assert_eq!(e.buffer().lines()[0], "hello world");
5945 assert_eq!(e.buffer().lines()[1], "OTHER");
5946 }
5947
5948 #[test]
5949 fn g_uu_with_count_uppercases_multiple_lines() {
5950 let mut e = editor_with("one\ntwo\nthree\nfour");
5951 run_keys(&mut e, "3gUU");
5953 assert_eq!(e.buffer().lines()[0], "ONE");
5954 assert_eq!(e.buffer().lines()[1], "TWO");
5955 assert_eq!(e.buffer().lines()[2], "THREE");
5956 assert_eq!(e.buffer().lines()[3], "four");
5957 }
5958
5959 #[test]
5960 fn double_gt_indents_current_line() {
5961 let mut e = editor_with("hello");
5962 run_keys(&mut e, ">>");
5963 assert_eq!(e.buffer().lines()[0], " hello");
5964 assert_eq!(e.cursor(), (0, 2));
5966 }
5967
5968 #[test]
5969 fn double_lt_outdents_current_line() {
5970 let mut e = editor_with(" hello");
5971 run_keys(&mut e, "<lt><lt>");
5972 assert_eq!(e.buffer().lines()[0], " hello");
5973 assert_eq!(e.cursor(), (0, 2));
5974 }
5975
5976 #[test]
5977 fn count_double_gt_indents_multiple_lines() {
5978 let mut e = editor_with("a\nb\nc\nd");
5979 run_keys(&mut e, "3>>");
5981 assert_eq!(e.buffer().lines()[0], " a");
5982 assert_eq!(e.buffer().lines()[1], " b");
5983 assert_eq!(e.buffer().lines()[2], " c");
5984 assert_eq!(e.buffer().lines()[3], "d");
5985 }
5986
5987 #[test]
5988 fn outdent_clips_ragged_leading_whitespace() {
5989 let mut e = editor_with(" x");
5992 run_keys(&mut e, "<lt><lt>");
5993 assert_eq!(e.buffer().lines()[0], "x");
5994 }
5995
5996 #[test]
5997 fn indent_motion_is_always_linewise() {
5998 let mut e = editor_with("foo bar");
6001 run_keys(&mut e, ">w");
6002 assert_eq!(e.buffer().lines()[0], " foo bar");
6003 }
6004
6005 #[test]
6006 fn indent_text_object_extends_over_paragraph() {
6007 let mut e = editor_with("a\nb\n\nc\nd");
6008 run_keys(&mut e, ">ap");
6010 assert_eq!(e.buffer().lines()[0], " a");
6011 assert_eq!(e.buffer().lines()[1], " b");
6012 assert_eq!(e.buffer().lines()[2], "");
6013 assert_eq!(e.buffer().lines()[3], "c");
6014 }
6015
6016 #[test]
6017 fn visual_line_indent_shifts_selected_rows() {
6018 let mut e = editor_with("x\ny\nz");
6019 run_keys(&mut e, "Vj>");
6021 assert_eq!(e.buffer().lines()[0], " x");
6022 assert_eq!(e.buffer().lines()[1], " y");
6023 assert_eq!(e.buffer().lines()[2], "z");
6024 }
6025
6026 #[test]
6027 fn outdent_empty_line_is_noop() {
6028 let mut e = editor_with("\nfoo");
6029 run_keys(&mut e, "<lt><lt>");
6030 assert_eq!(e.buffer().lines()[0], "");
6031 }
6032
6033 #[test]
6034 fn indent_skips_empty_lines() {
6035 let mut e = editor_with("");
6038 run_keys(&mut e, ">>");
6039 assert_eq!(e.buffer().lines()[0], "");
6040 }
6041
6042 #[test]
6043 fn insert_ctrl_t_indents_current_line() {
6044 let mut e = editor_with("x");
6045 run_keys(&mut e, "i<C-t>");
6047 assert_eq!(e.buffer().lines()[0], " x");
6048 assert_eq!(e.cursor(), (0, 2));
6051 }
6052
6053 #[test]
6054 fn insert_ctrl_d_outdents_current_line() {
6055 let mut e = editor_with(" x");
6056 run_keys(&mut e, "A<C-d>");
6058 assert_eq!(e.buffer().lines()[0], " x");
6059 }
6060
6061 #[test]
6062 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6063 let mut e = editor_with("first\nsecond");
6064 e.jump_cursor(1, 0);
6065 run_keys(&mut e, "h");
6066 assert_eq!(e.cursor(), (1, 0));
6068 }
6069
6070 #[test]
6071 fn l_at_last_char_does_not_wrap_to_next_line() {
6072 let mut e = editor_with("ab\ncd");
6073 e.jump_cursor(0, 1);
6075 run_keys(&mut e, "l");
6076 assert_eq!(e.cursor(), (0, 1));
6078 }
6079
6080 #[test]
6081 fn count_l_clamps_at_line_end() {
6082 let mut e = editor_with("abcde");
6083 run_keys(&mut e, "20l");
6086 assert_eq!(e.cursor(), (0, 4));
6087 }
6088
6089 #[test]
6090 fn count_h_clamps_at_col_zero() {
6091 let mut e = editor_with("abcde");
6092 e.jump_cursor(0, 3);
6093 run_keys(&mut e, "20h");
6094 assert_eq!(e.cursor(), (0, 0));
6095 }
6096
6097 #[test]
6098 fn dl_on_last_char_still_deletes_it() {
6099 let mut e = editor_with("ab");
6103 e.jump_cursor(0, 1);
6104 run_keys(&mut e, "dl");
6105 assert_eq!(e.buffer().lines()[0], "a");
6106 }
6107
6108 #[test]
6109 fn case_op_preserves_yank_register() {
6110 let mut e = editor_with("target");
6111 run_keys(&mut e, "yy");
6112 let yank_before = e.yank().to_string();
6113 run_keys(&mut e, "gUU");
6115 assert_eq!(e.buffer().lines()[0], "TARGET");
6116 assert_eq!(
6117 e.yank(),
6118 yank_before,
6119 "case ops must preserve the yank buffer"
6120 );
6121 }
6122
6123 #[test]
6124 fn dap_deletes_paragraph() {
6125 let mut e = editor_with("a\nb\n\nc\nd");
6126 run_keys(&mut e, "dap");
6127 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6128 }
6129
6130 #[test]
6131 fn dit_deletes_inner_tag_content() {
6132 let mut e = editor_with("<b>hello</b>");
6133 e.jump_cursor(0, 4);
6135 run_keys(&mut e, "dit");
6136 assert_eq!(e.buffer().lines()[0], "<b></b>");
6137 }
6138
6139 #[test]
6140 fn dat_deletes_around_tag() {
6141 let mut e = editor_with("hi <b>foo</b> bye");
6142 e.jump_cursor(0, 6);
6143 run_keys(&mut e, "dat");
6144 assert_eq!(e.buffer().lines()[0], "hi bye");
6145 }
6146
6147 #[test]
6148 fn dit_picks_innermost_tag() {
6149 let mut e = editor_with("<a><b>x</b></a>");
6150 e.jump_cursor(0, 6);
6152 run_keys(&mut e, "dit");
6153 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6155 }
6156
6157 #[test]
6158 fn dat_innermost_tag_pair() {
6159 let mut e = editor_with("<a><b>x</b></a>");
6160 e.jump_cursor(0, 6);
6161 run_keys(&mut e, "dat");
6162 assert_eq!(e.buffer().lines()[0], "<a></a>");
6163 }
6164
6165 #[test]
6166 fn dit_outside_any_tag_no_op() {
6167 let mut e = editor_with("plain text");
6168 e.jump_cursor(0, 3);
6169 run_keys(&mut e, "dit");
6170 assert_eq!(e.buffer().lines()[0], "plain text");
6172 }
6173
6174 #[test]
6175 fn cit_changes_inner_tag_content() {
6176 let mut e = editor_with("<b>hello</b>");
6177 e.jump_cursor(0, 4);
6178 run_keys(&mut e, "citNEW<Esc>");
6179 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6180 }
6181
6182 #[test]
6183 fn cat_changes_around_tag() {
6184 let mut e = editor_with("hi <b>foo</b> bye");
6185 e.jump_cursor(0, 6);
6186 run_keys(&mut e, "catBAR<Esc>");
6187 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6188 }
6189
6190 #[test]
6191 fn yit_yanks_inner_tag_content() {
6192 let mut e = editor_with("<b>hello</b>");
6193 e.jump_cursor(0, 4);
6194 run_keys(&mut e, "yit");
6195 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6196 }
6197
6198 #[test]
6199 fn yat_yanks_full_tag_pair() {
6200 let mut e = editor_with("hi <b>foo</b> bye");
6201 e.jump_cursor(0, 6);
6202 run_keys(&mut e, "yat");
6203 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6204 }
6205
6206 #[test]
6207 fn vit_visually_selects_inner_tag() {
6208 let mut e = editor_with("<b>hello</b>");
6209 e.jump_cursor(0, 4);
6210 run_keys(&mut e, "vit");
6211 assert_eq!(e.vim_mode(), VimMode::Visual);
6212 run_keys(&mut e, "y");
6213 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6214 }
6215
6216 #[test]
6217 fn vat_visually_selects_around_tag() {
6218 let mut e = editor_with("x<b>foo</b>y");
6219 e.jump_cursor(0, 5);
6220 run_keys(&mut e, "vat");
6221 assert_eq!(e.vim_mode(), VimMode::Visual);
6222 run_keys(&mut e, "y");
6223 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6224 }
6225
6226 #[test]
6229 #[allow(non_snake_case)]
6230 fn diW_deletes_inner_big_word() {
6231 let mut e = editor_with("foo.bar baz");
6232 e.jump_cursor(0, 2);
6233 run_keys(&mut e, "diW");
6234 assert_eq!(e.buffer().lines()[0], " baz");
6236 }
6237
6238 #[test]
6239 #[allow(non_snake_case)]
6240 fn daW_deletes_around_big_word() {
6241 let mut e = editor_with("foo.bar baz");
6242 e.jump_cursor(0, 2);
6243 run_keys(&mut e, "daW");
6244 assert_eq!(e.buffer().lines()[0], "baz");
6245 }
6246
6247 #[test]
6248 fn di_double_quote_deletes_inside() {
6249 let mut e = editor_with("a \"hello\" b");
6250 e.jump_cursor(0, 4);
6251 run_keys(&mut e, "di\"");
6252 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6253 }
6254
6255 #[test]
6256 fn da_double_quote_deletes_around() {
6257 let mut e = editor_with("a \"hello\" b");
6258 e.jump_cursor(0, 4);
6259 run_keys(&mut e, "da\"");
6260 assert_eq!(e.buffer().lines()[0], "a b");
6261 }
6262
6263 #[test]
6264 fn di_single_quote_deletes_inside() {
6265 let mut e = editor_with("x 'foo' y");
6266 e.jump_cursor(0, 4);
6267 run_keys(&mut e, "di'");
6268 assert_eq!(e.buffer().lines()[0], "x '' y");
6269 }
6270
6271 #[test]
6272 fn da_single_quote_deletes_around() {
6273 let mut e = editor_with("x 'foo' y");
6274 e.jump_cursor(0, 4);
6275 run_keys(&mut e, "da'");
6276 assert_eq!(e.buffer().lines()[0], "x y");
6277 }
6278
6279 #[test]
6280 fn di_backtick_deletes_inside() {
6281 let mut e = editor_with("p `q` r");
6282 e.jump_cursor(0, 3);
6283 run_keys(&mut e, "di`");
6284 assert_eq!(e.buffer().lines()[0], "p `` r");
6285 }
6286
6287 #[test]
6288 fn da_backtick_deletes_around() {
6289 let mut e = editor_with("p `q` r");
6290 e.jump_cursor(0, 3);
6291 run_keys(&mut e, "da`");
6292 assert_eq!(e.buffer().lines()[0], "p r");
6293 }
6294
6295 #[test]
6296 fn di_paren_deletes_inside() {
6297 let mut e = editor_with("f(arg)");
6298 e.jump_cursor(0, 3);
6299 run_keys(&mut e, "di(");
6300 assert_eq!(e.buffer().lines()[0], "f()");
6301 }
6302
6303 #[test]
6304 fn di_paren_alias_b_works() {
6305 let mut e = editor_with("f(arg)");
6306 e.jump_cursor(0, 3);
6307 run_keys(&mut e, "dib");
6308 assert_eq!(e.buffer().lines()[0], "f()");
6309 }
6310
6311 #[test]
6312 fn di_bracket_deletes_inside() {
6313 let mut e = editor_with("a[b,c]d");
6314 e.jump_cursor(0, 3);
6315 run_keys(&mut e, "di[");
6316 assert_eq!(e.buffer().lines()[0], "a[]d");
6317 }
6318
6319 #[test]
6320 fn da_bracket_deletes_around() {
6321 let mut e = editor_with("a[b,c]d");
6322 e.jump_cursor(0, 3);
6323 run_keys(&mut e, "da[");
6324 assert_eq!(e.buffer().lines()[0], "ad");
6325 }
6326
6327 #[test]
6328 fn di_brace_deletes_inside() {
6329 let mut e = editor_with("x{y}z");
6330 e.jump_cursor(0, 2);
6331 run_keys(&mut e, "di{");
6332 assert_eq!(e.buffer().lines()[0], "x{}z");
6333 }
6334
6335 #[test]
6336 fn da_brace_deletes_around() {
6337 let mut e = editor_with("x{y}z");
6338 e.jump_cursor(0, 2);
6339 run_keys(&mut e, "da{");
6340 assert_eq!(e.buffer().lines()[0], "xz");
6341 }
6342
6343 #[test]
6344 fn di_brace_alias_capital_b_works() {
6345 let mut e = editor_with("x{y}z");
6346 e.jump_cursor(0, 2);
6347 run_keys(&mut e, "diB");
6348 assert_eq!(e.buffer().lines()[0], "x{}z");
6349 }
6350
6351 #[test]
6352 fn di_angle_deletes_inside() {
6353 let mut e = editor_with("p<q>r");
6354 e.jump_cursor(0, 2);
6355 run_keys(&mut e, "di<lt>");
6357 assert_eq!(e.buffer().lines()[0], "p<>r");
6358 }
6359
6360 #[test]
6361 fn da_angle_deletes_around() {
6362 let mut e = editor_with("p<q>r");
6363 e.jump_cursor(0, 2);
6364 run_keys(&mut e, "da<lt>");
6365 assert_eq!(e.buffer().lines()[0], "pr");
6366 }
6367
6368 #[test]
6369 fn dip_deletes_inner_paragraph() {
6370 let mut e = editor_with("a\nb\nc\n\nd");
6371 e.jump_cursor(1, 0);
6372 run_keys(&mut e, "dip");
6373 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6376 }
6377
6378 #[test]
6381 fn sentence_motion_close_paren_jumps_forward() {
6382 let mut e = editor_with("Alpha. Beta. Gamma.");
6383 e.jump_cursor(0, 0);
6384 run_keys(&mut e, ")");
6385 assert_eq!(e.cursor(), (0, 7));
6387 run_keys(&mut e, ")");
6388 assert_eq!(e.cursor(), (0, 13));
6389 }
6390
6391 #[test]
6392 fn sentence_motion_open_paren_jumps_backward() {
6393 let mut e = editor_with("Alpha. Beta. Gamma.");
6394 e.jump_cursor(0, 13);
6395 run_keys(&mut e, "(");
6396 assert_eq!(e.cursor(), (0, 7));
6399 run_keys(&mut e, "(");
6400 assert_eq!(e.cursor(), (0, 0));
6401 }
6402
6403 #[test]
6404 fn sentence_motion_count() {
6405 let mut e = editor_with("A. B. C. D.");
6406 e.jump_cursor(0, 0);
6407 run_keys(&mut e, "3)");
6408 assert_eq!(e.cursor(), (0, 9));
6410 }
6411
6412 #[test]
6413 fn dis_deletes_inner_sentence() {
6414 let mut e = editor_with("First one. Second one. Third one.");
6415 e.jump_cursor(0, 13);
6416 run_keys(&mut e, "dis");
6417 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6419 }
6420
6421 #[test]
6422 fn das_deletes_around_sentence_with_trailing_space() {
6423 let mut e = editor_with("Alpha. Beta. Gamma.");
6424 e.jump_cursor(0, 8);
6425 run_keys(&mut e, "das");
6426 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6429 }
6430
6431 #[test]
6432 fn dis_handles_double_terminator() {
6433 let mut e = editor_with("Wow!? Next.");
6434 e.jump_cursor(0, 1);
6435 run_keys(&mut e, "dis");
6436 assert_eq!(e.buffer().lines()[0], " Next.");
6439 }
6440
6441 #[test]
6442 fn dis_first_sentence_from_cursor_at_zero() {
6443 let mut e = editor_with("Alpha. Beta.");
6444 e.jump_cursor(0, 0);
6445 run_keys(&mut e, "dis");
6446 assert_eq!(e.buffer().lines()[0], " Beta.");
6447 }
6448
6449 #[test]
6450 fn yis_yanks_inner_sentence() {
6451 let mut e = editor_with("Hello world. Bye.");
6452 e.jump_cursor(0, 5);
6453 run_keys(&mut e, "yis");
6454 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6455 }
6456
6457 #[test]
6458 fn vis_visually_selects_inner_sentence() {
6459 let mut e = editor_with("First. Second.");
6460 e.jump_cursor(0, 1);
6461 run_keys(&mut e, "vis");
6462 assert_eq!(e.vim_mode(), VimMode::Visual);
6463 run_keys(&mut e, "y");
6464 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6465 }
6466
6467 #[test]
6468 fn ciw_changes_inner_word() {
6469 let mut e = editor_with("hello world");
6470 e.jump_cursor(0, 1);
6471 run_keys(&mut e, "ciwHEY<Esc>");
6472 assert_eq!(e.buffer().lines()[0], "HEY world");
6473 }
6474
6475 #[test]
6476 fn yiw_yanks_inner_word() {
6477 let mut e = editor_with("hello world");
6478 e.jump_cursor(0, 1);
6479 run_keys(&mut e, "yiw");
6480 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6481 }
6482
6483 #[test]
6484 fn viw_selects_inner_word() {
6485 let mut e = editor_with("hello world");
6486 e.jump_cursor(0, 2);
6487 run_keys(&mut e, "viw");
6488 assert_eq!(e.vim_mode(), VimMode::Visual);
6489 run_keys(&mut e, "y");
6490 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6491 }
6492
6493 #[test]
6494 fn ci_paren_changes_inside() {
6495 let mut e = editor_with("f(old)");
6496 e.jump_cursor(0, 3);
6497 run_keys(&mut e, "ci(NEW<Esc>");
6498 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6499 }
6500
6501 #[test]
6502 fn yi_double_quote_yanks_inside() {
6503 let mut e = editor_with("say \"hi there\" then");
6504 e.jump_cursor(0, 6);
6505 run_keys(&mut e, "yi\"");
6506 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6507 }
6508
6509 #[test]
6510 fn vap_visual_selects_around_paragraph() {
6511 let mut e = editor_with("a\nb\n\nc");
6512 e.jump_cursor(0, 0);
6513 run_keys(&mut e, "vap");
6514 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6515 run_keys(&mut e, "y");
6516 let text = e.registers().read('"').unwrap().text.clone();
6518 assert!(text.starts_with("a\nb"));
6519 }
6520
6521 #[test]
6522 fn star_finds_next_occurrence() {
6523 let mut e = editor_with("foo bar foo baz");
6524 run_keys(&mut e, "*");
6525 assert_eq!(e.cursor().1, 8);
6526 }
6527
6528 #[test]
6529 fn star_skips_substring_match() {
6530 let mut e = editor_with("foo foobar baz");
6533 run_keys(&mut e, "*");
6534 assert_eq!(e.cursor().1, 0);
6535 }
6536
6537 #[test]
6538 fn g_star_matches_substring() {
6539 let mut e = editor_with("foo foobar baz");
6542 run_keys(&mut e, "g*");
6543 assert_eq!(e.cursor().1, 4);
6544 }
6545
6546 #[test]
6547 fn g_pound_matches_substring_backward() {
6548 let mut e = editor_with("foo foobar baz foo");
6551 run_keys(&mut e, "$b");
6552 assert_eq!(e.cursor().1, 15);
6553 run_keys(&mut e, "g#");
6554 assert_eq!(e.cursor().1, 4);
6555 }
6556
6557 #[test]
6558 fn n_repeats_last_search_forward() {
6559 let mut e = editor_with("foo bar foo baz foo");
6560 run_keys(&mut e, "/foo<CR>");
6563 assert_eq!(e.cursor().1, 8);
6564 run_keys(&mut e, "n");
6565 assert_eq!(e.cursor().1, 16);
6566 }
6567
6568 #[test]
6569 fn shift_n_reverses_search() {
6570 let mut e = editor_with("foo bar foo baz foo");
6571 run_keys(&mut e, "/foo<CR>");
6572 run_keys(&mut e, "n");
6573 assert_eq!(e.cursor().1, 16);
6574 run_keys(&mut e, "N");
6575 assert_eq!(e.cursor().1, 8);
6576 }
6577
6578 #[test]
6579 fn n_noop_without_pattern() {
6580 let mut e = editor_with("foo bar");
6581 run_keys(&mut e, "n");
6582 assert_eq!(e.cursor(), (0, 0));
6583 }
6584
6585 #[test]
6586 fn visual_line_preserves_cursor_column() {
6587 let mut e = editor_with("hello world\nanother one\nbye");
6590 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6592 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6593 assert_eq!(e.cursor(), (0, 5));
6594 run_keys(&mut e, "j");
6595 assert_eq!(e.cursor(), (1, 5));
6596 }
6597
6598 #[test]
6599 fn visual_line_yank_includes_trailing_newline() {
6600 let mut e = editor_with("aaa\nbbb\nccc");
6601 run_keys(&mut e, "Vjy");
6602 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6604 }
6605
6606 #[test]
6607 fn visual_line_yank_last_line_trailing_newline() {
6608 let mut e = editor_with("aaa\nbbb\nccc");
6609 run_keys(&mut e, "jj");
6611 run_keys(&mut e, "Vy");
6612 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6613 }
6614
6615 #[test]
6616 fn yy_on_last_line_has_trailing_newline() {
6617 let mut e = editor_with("aaa\nbbb\nccc");
6618 run_keys(&mut e, "jj");
6619 run_keys(&mut e, "yy");
6620 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6621 }
6622
6623 #[test]
6624 fn yy_in_middle_has_trailing_newline() {
6625 let mut e = editor_with("aaa\nbbb\nccc");
6626 run_keys(&mut e, "j");
6627 run_keys(&mut e, "yy");
6628 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6629 }
6630
6631 #[test]
6632 fn di_single_quote() {
6633 let mut e = editor_with("say 'hello world' now");
6634 e.jump_cursor(0, 7);
6635 run_keys(&mut e, "di'");
6636 assert_eq!(e.buffer().lines()[0], "say '' now");
6637 }
6638
6639 #[test]
6640 fn da_single_quote() {
6641 let mut e = editor_with("say 'hello' now");
6642 e.jump_cursor(0, 7);
6643 run_keys(&mut e, "da'");
6644 assert_eq!(e.buffer().lines()[0], "say now");
6645 }
6646
6647 #[test]
6648 fn di_backtick() {
6649 let mut e = editor_with("say `hi` now");
6650 e.jump_cursor(0, 5);
6651 run_keys(&mut e, "di`");
6652 assert_eq!(e.buffer().lines()[0], "say `` now");
6653 }
6654
6655 #[test]
6656 fn di_brace() {
6657 let mut e = editor_with("fn { a; b; c }");
6658 e.jump_cursor(0, 7);
6659 run_keys(&mut e, "di{");
6660 assert_eq!(e.buffer().lines()[0], "fn {}");
6661 }
6662
6663 #[test]
6664 fn di_bracket() {
6665 let mut e = editor_with("arr[1, 2, 3]");
6666 e.jump_cursor(0, 5);
6667 run_keys(&mut e, "di[");
6668 assert_eq!(e.buffer().lines()[0], "arr[]");
6669 }
6670
6671 #[test]
6672 fn dab_deletes_around_paren() {
6673 let mut e = editor_with("fn(a, b) + 1");
6674 e.jump_cursor(0, 4);
6675 run_keys(&mut e, "dab");
6676 assert_eq!(e.buffer().lines()[0], "fn + 1");
6677 }
6678
6679 #[test]
6680 fn da_big_b_deletes_around_brace() {
6681 let mut e = editor_with("x = {a: 1}");
6682 e.jump_cursor(0, 6);
6683 run_keys(&mut e, "daB");
6684 assert_eq!(e.buffer().lines()[0], "x = ");
6685 }
6686
6687 #[test]
6688 fn di_big_w_deletes_bigword() {
6689 let mut e = editor_with("foo-bar baz");
6690 e.jump_cursor(0, 2);
6691 run_keys(&mut e, "diW");
6692 assert_eq!(e.buffer().lines()[0], " baz");
6693 }
6694
6695 #[test]
6696 fn visual_select_inner_word() {
6697 let mut e = editor_with("hello world");
6698 e.jump_cursor(0, 2);
6699 run_keys(&mut e, "viw");
6700 assert_eq!(e.vim_mode(), VimMode::Visual);
6701 run_keys(&mut e, "y");
6702 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6703 }
6704
6705 #[test]
6706 fn visual_select_inner_quote() {
6707 let mut e = editor_with("foo \"bar\" baz");
6708 e.jump_cursor(0, 6);
6709 run_keys(&mut e, "vi\"");
6710 run_keys(&mut e, "y");
6711 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6712 }
6713
6714 #[test]
6715 fn visual_select_inner_paren() {
6716 let mut e = editor_with("fn(a, b)");
6717 e.jump_cursor(0, 4);
6718 run_keys(&mut e, "vi(");
6719 run_keys(&mut e, "y");
6720 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6721 }
6722
6723 #[test]
6724 fn visual_select_outer_brace() {
6725 let mut e = editor_with("{x}");
6726 e.jump_cursor(0, 1);
6727 run_keys(&mut e, "va{");
6728 run_keys(&mut e, "y");
6729 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6730 }
6731
6732 #[test]
6733 fn ci_paren_forward_scans_when_cursor_before_pair() {
6734 let mut e = editor_with("foo(bar)");
6737 e.jump_cursor(0, 0);
6738 run_keys(&mut e, "ci(NEW<Esc>");
6739 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
6740 }
6741
6742 #[test]
6743 fn ci_paren_forward_scans_across_lines() {
6744 let mut e = editor_with("first\nfoo(bar)\nlast");
6745 e.jump_cursor(0, 0);
6746 run_keys(&mut e, "ci(NEW<Esc>");
6747 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
6748 }
6749
6750 #[test]
6751 fn ci_brace_forward_scans_when_cursor_before_pair() {
6752 let mut e = editor_with("let x = {y};");
6753 e.jump_cursor(0, 0);
6754 run_keys(&mut e, "ci{NEW<Esc>");
6755 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
6756 }
6757
6758 #[test]
6759 fn cit_forward_scans_when_cursor_before_tag() {
6760 let mut e = editor_with("text <b>hello</b> rest");
6763 e.jump_cursor(0, 0);
6764 run_keys(&mut e, "citNEW<Esc>");
6765 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
6766 }
6767
6768 #[test]
6769 fn dat_forward_scans_when_cursor_before_tag() {
6770 let mut e = editor_with("text <b>hello</b> rest");
6772 e.jump_cursor(0, 0);
6773 run_keys(&mut e, "dat");
6774 assert_eq!(e.buffer().lines()[0], "text rest");
6775 }
6776
6777 #[test]
6778 fn ci_paren_still_works_when_cursor_inside() {
6779 let mut e = editor_with("fn(a, b)");
6782 e.jump_cursor(0, 4);
6783 run_keys(&mut e, "ci(NEW<Esc>");
6784 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
6785 }
6786
6787 #[test]
6788 fn caw_changes_word_with_trailing_space() {
6789 let mut e = editor_with("hello world");
6790 run_keys(&mut e, "cawfoo<Esc>");
6791 assert_eq!(e.buffer().lines()[0], "fooworld");
6792 }
6793
6794 #[test]
6795 fn visual_char_yank_preserves_raw_text() {
6796 let mut e = editor_with("hello world");
6797 run_keys(&mut e, "vllly");
6798 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6799 }
6800
6801 #[test]
6802 fn single_line_visual_line_selects_full_line_on_yank() {
6803 let mut e = editor_with("hello world\nbye");
6804 run_keys(&mut e, "V");
6805 run_keys(&mut e, "y");
6808 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6809 }
6810
6811 #[test]
6812 fn visual_line_extends_both_directions() {
6813 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6814 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6816 assert_eq!(e.cursor(), (3, 0));
6817 run_keys(&mut e, "k");
6818 assert_eq!(e.cursor(), (2, 0));
6820 run_keys(&mut e, "k");
6821 assert_eq!(e.cursor(), (1, 0));
6822 }
6823
6824 #[test]
6825 fn visual_char_preserves_cursor_column() {
6826 let mut e = editor_with("hello world");
6827 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6829 assert_eq!(e.cursor(), (0, 5));
6830 run_keys(&mut e, "ll");
6831 assert_eq!(e.cursor(), (0, 7));
6832 }
6833
6834 #[test]
6835 fn visual_char_highlight_bounds_order() {
6836 let mut e = editor_with("abcdef");
6837 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
6839 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
6842 }
6843
6844 #[test]
6845 fn visual_line_highlight_bounds() {
6846 let mut e = editor_with("a\nb\nc");
6847 run_keys(&mut e, "V");
6848 assert_eq!(e.line_highlight(), Some((0, 0)));
6849 run_keys(&mut e, "j");
6850 assert_eq!(e.line_highlight(), Some((0, 1)));
6851 run_keys(&mut e, "j");
6852 assert_eq!(e.line_highlight(), Some((0, 2)));
6853 }
6854
6855 #[test]
6858 fn h_moves_left() {
6859 let mut e = editor_with("hello");
6860 e.jump_cursor(0, 3);
6861 run_keys(&mut e, "h");
6862 assert_eq!(e.cursor(), (0, 2));
6863 }
6864
6865 #[test]
6866 fn l_moves_right() {
6867 let mut e = editor_with("hello");
6868 run_keys(&mut e, "l");
6869 assert_eq!(e.cursor(), (0, 1));
6870 }
6871
6872 #[test]
6873 fn k_moves_up() {
6874 let mut e = editor_with("a\nb\nc");
6875 e.jump_cursor(2, 0);
6876 run_keys(&mut e, "k");
6877 assert_eq!(e.cursor(), (1, 0));
6878 }
6879
6880 #[test]
6881 fn zero_moves_to_line_start() {
6882 let mut e = editor_with(" hello");
6883 run_keys(&mut e, "$");
6884 run_keys(&mut e, "0");
6885 assert_eq!(e.cursor().1, 0);
6886 }
6887
6888 #[test]
6889 fn caret_moves_to_first_non_blank() {
6890 let mut e = editor_with(" hello");
6891 run_keys(&mut e, "0");
6892 run_keys(&mut e, "^");
6893 assert_eq!(e.cursor().1, 4);
6894 }
6895
6896 #[test]
6897 fn dollar_moves_to_last_char() {
6898 let mut e = editor_with("hello");
6899 run_keys(&mut e, "$");
6900 assert_eq!(e.cursor().1, 4);
6901 }
6902
6903 #[test]
6904 fn dollar_on_empty_line_stays_at_col_zero() {
6905 let mut e = editor_with("");
6906 run_keys(&mut e, "$");
6907 assert_eq!(e.cursor().1, 0);
6908 }
6909
6910 #[test]
6911 fn w_jumps_to_next_word() {
6912 let mut e = editor_with("foo bar baz");
6913 run_keys(&mut e, "w");
6914 assert_eq!(e.cursor().1, 4);
6915 }
6916
6917 #[test]
6918 fn b_jumps_back_a_word() {
6919 let mut e = editor_with("foo bar");
6920 e.jump_cursor(0, 6);
6921 run_keys(&mut e, "b");
6922 assert_eq!(e.cursor().1, 4);
6923 }
6924
6925 #[test]
6926 fn e_jumps_to_word_end() {
6927 let mut e = editor_with("foo bar");
6928 run_keys(&mut e, "e");
6929 assert_eq!(e.cursor().1, 2);
6930 }
6931
6932 #[test]
6935 fn d_dollar_deletes_to_eol() {
6936 let mut e = editor_with("hello world");
6937 e.jump_cursor(0, 5);
6938 run_keys(&mut e, "d$");
6939 assert_eq!(e.buffer().lines()[0], "hello");
6940 }
6941
6942 #[test]
6943 fn d_zero_deletes_to_line_start() {
6944 let mut e = editor_with("hello world");
6945 e.jump_cursor(0, 6);
6946 run_keys(&mut e, "d0");
6947 assert_eq!(e.buffer().lines()[0], "world");
6948 }
6949
6950 #[test]
6951 fn d_caret_deletes_to_first_non_blank() {
6952 let mut e = editor_with(" hello");
6953 e.jump_cursor(0, 6);
6954 run_keys(&mut e, "d^");
6955 assert_eq!(e.buffer().lines()[0], " llo");
6956 }
6957
6958 #[test]
6959 fn d_capital_g_deletes_to_end_of_file() {
6960 let mut e = editor_with("a\nb\nc\nd");
6961 e.jump_cursor(1, 0);
6962 run_keys(&mut e, "dG");
6963 assert_eq!(e.buffer().lines(), &["a".to_string()]);
6964 }
6965
6966 #[test]
6967 fn d_gg_deletes_to_start_of_file() {
6968 let mut e = editor_with("a\nb\nc\nd");
6969 e.jump_cursor(2, 0);
6970 run_keys(&mut e, "dgg");
6971 assert_eq!(e.buffer().lines(), &["d".to_string()]);
6972 }
6973
6974 #[test]
6975 fn cw_is_ce_quirk() {
6976 let mut e = editor_with("foo bar");
6979 run_keys(&mut e, "cwxyz<Esc>");
6980 assert_eq!(e.buffer().lines()[0], "xyz bar");
6981 }
6982
6983 #[test]
6986 fn big_d_deletes_to_eol() {
6987 let mut e = editor_with("hello world");
6988 e.jump_cursor(0, 5);
6989 run_keys(&mut e, "D");
6990 assert_eq!(e.buffer().lines()[0], "hello");
6991 }
6992
6993 #[test]
6994 fn big_c_deletes_to_eol_and_inserts() {
6995 let mut e = editor_with("hello world");
6996 e.jump_cursor(0, 5);
6997 run_keys(&mut e, "C!<Esc>");
6998 assert_eq!(e.buffer().lines()[0], "hello!");
6999 }
7000
7001 #[test]
7002 fn j_joins_next_line_with_space() {
7003 let mut e = editor_with("hello\nworld");
7004 run_keys(&mut e, "J");
7005 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7006 }
7007
7008 #[test]
7009 fn j_strips_leading_whitespace_on_join() {
7010 let mut e = editor_with("hello\n world");
7011 run_keys(&mut e, "J");
7012 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7013 }
7014
7015 #[test]
7016 fn big_x_deletes_char_before_cursor() {
7017 let mut e = editor_with("hello");
7018 e.jump_cursor(0, 3);
7019 run_keys(&mut e, "X");
7020 assert_eq!(e.buffer().lines()[0], "helo");
7021 }
7022
7023 #[test]
7024 fn s_substitutes_char_and_enters_insert() {
7025 let mut e = editor_with("hello");
7026 run_keys(&mut e, "sX<Esc>");
7027 assert_eq!(e.buffer().lines()[0], "Xello");
7028 }
7029
7030 #[test]
7031 fn count_x_deletes_many() {
7032 let mut e = editor_with("abcdef");
7033 run_keys(&mut e, "3x");
7034 assert_eq!(e.buffer().lines()[0], "def");
7035 }
7036
7037 #[test]
7040 fn p_pastes_charwise_after_cursor() {
7041 let mut e = editor_with("hello");
7042 run_keys(&mut e, "yw");
7043 run_keys(&mut e, "$p");
7044 assert_eq!(e.buffer().lines()[0], "hellohello");
7045 }
7046
7047 #[test]
7048 fn capital_p_pastes_charwise_before_cursor() {
7049 let mut e = editor_with("hello");
7050 run_keys(&mut e, "v");
7052 run_keys(&mut e, "l");
7053 run_keys(&mut e, "y");
7054 run_keys(&mut e, "$P");
7055 assert_eq!(e.buffer().lines()[0], "hellheo");
7058 }
7059
7060 #[test]
7061 fn p_pastes_linewise_below() {
7062 let mut e = editor_with("one\ntwo\nthree");
7063 run_keys(&mut e, "yy");
7064 run_keys(&mut e, "p");
7065 assert_eq!(
7066 e.buffer().lines(),
7067 &[
7068 "one".to_string(),
7069 "one".to_string(),
7070 "two".to_string(),
7071 "three".to_string()
7072 ]
7073 );
7074 }
7075
7076 #[test]
7077 fn capital_p_pastes_linewise_above() {
7078 let mut e = editor_with("one\ntwo");
7079 e.jump_cursor(1, 0);
7080 run_keys(&mut e, "yy");
7081 run_keys(&mut e, "P");
7082 assert_eq!(
7083 e.buffer().lines(),
7084 &["one".to_string(), "two".to_string(), "two".to_string()]
7085 );
7086 }
7087
7088 #[test]
7091 fn hash_finds_previous_occurrence() {
7092 let mut e = editor_with("foo bar foo baz foo");
7093 e.jump_cursor(0, 16);
7095 run_keys(&mut e, "#");
7096 assert_eq!(e.cursor().1, 8);
7097 }
7098
7099 #[test]
7102 fn visual_line_delete_removes_full_lines() {
7103 let mut e = editor_with("a\nb\nc\nd");
7104 run_keys(&mut e, "Vjd");
7105 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7106 }
7107
7108 #[test]
7109 fn visual_line_change_leaves_blank_line() {
7110 let mut e = editor_with("a\nb\nc");
7111 run_keys(&mut e, "Vjc");
7112 assert_eq!(e.vim_mode(), VimMode::Insert);
7113 run_keys(&mut e, "X<Esc>");
7114 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7118 }
7119
7120 #[test]
7121 fn cc_leaves_blank_line() {
7122 let mut e = editor_with("a\nb\nc");
7123 e.jump_cursor(1, 0);
7124 run_keys(&mut e, "ccX<Esc>");
7125 assert_eq!(
7126 e.buffer().lines(),
7127 &["a".to_string(), "X".to_string(), "c".to_string()]
7128 );
7129 }
7130
7131 #[test]
7136 fn big_w_skips_hyphens() {
7137 let mut e = editor_with("foo-bar baz");
7139 run_keys(&mut e, "W");
7140 assert_eq!(e.cursor().1, 8);
7141 }
7142
7143 #[test]
7144 fn big_w_crosses_lines() {
7145 let mut e = editor_with("foo-bar\nbaz-qux");
7146 run_keys(&mut e, "W");
7147 assert_eq!(e.cursor(), (1, 0));
7148 }
7149
7150 #[test]
7151 fn big_b_skips_hyphens() {
7152 let mut e = editor_with("foo-bar baz");
7153 e.jump_cursor(0, 9);
7154 run_keys(&mut e, "B");
7155 assert_eq!(e.cursor().1, 8);
7156 run_keys(&mut e, "B");
7157 assert_eq!(e.cursor().1, 0);
7158 }
7159
7160 #[test]
7161 fn big_e_jumps_to_big_word_end() {
7162 let mut e = editor_with("foo-bar baz");
7163 run_keys(&mut e, "E");
7164 assert_eq!(e.cursor().1, 6);
7165 run_keys(&mut e, "E");
7166 assert_eq!(e.cursor().1, 10);
7167 }
7168
7169 #[test]
7170 fn dw_with_big_word_variant() {
7171 let mut e = editor_with("foo-bar baz");
7173 run_keys(&mut e, "dW");
7174 assert_eq!(e.buffer().lines()[0], "baz");
7175 }
7176
7177 #[test]
7180 fn insert_ctrl_w_deletes_word_back() {
7181 let mut e = editor_with("");
7182 run_keys(&mut e, "i");
7183 for c in "hello world".chars() {
7184 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7185 }
7186 run_keys(&mut e, "<C-w>");
7187 assert_eq!(e.buffer().lines()[0], "hello ");
7188 }
7189
7190 #[test]
7191 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7192 let mut e = editor_with("hello\nworld");
7196 e.jump_cursor(1, 0);
7197 run_keys(&mut e, "i");
7198 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7199 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7202 assert_eq!(e.cursor(), (0, 0));
7203 }
7204
7205 #[test]
7206 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7207 let mut e = editor_with("foo bar\nbaz");
7208 e.jump_cursor(1, 0);
7209 run_keys(&mut e, "i");
7210 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7211 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7213 assert_eq!(e.cursor(), (0, 4));
7214 }
7215
7216 #[test]
7217 fn insert_ctrl_u_deletes_to_line_start() {
7218 let mut e = editor_with("");
7219 run_keys(&mut e, "i");
7220 for c in "hello world".chars() {
7221 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7222 }
7223 run_keys(&mut e, "<C-u>");
7224 assert_eq!(e.buffer().lines()[0], "");
7225 }
7226
7227 #[test]
7228 fn insert_ctrl_o_runs_one_normal_command() {
7229 let mut e = editor_with("hello world");
7230 run_keys(&mut e, "A");
7232 assert_eq!(e.vim_mode(), VimMode::Insert);
7233 e.jump_cursor(0, 0);
7235 run_keys(&mut e, "<C-o>");
7236 assert_eq!(e.vim_mode(), VimMode::Normal);
7237 run_keys(&mut e, "dw");
7238 assert_eq!(e.vim_mode(), VimMode::Insert);
7240 assert_eq!(e.buffer().lines()[0], "world");
7241 }
7242
7243 #[test]
7246 fn j_through_empty_line_preserves_column() {
7247 let mut e = editor_with("hello world\n\nanother line");
7248 run_keys(&mut e, "llllll");
7250 assert_eq!(e.cursor(), (0, 6));
7251 run_keys(&mut e, "j");
7254 assert_eq!(e.cursor(), (1, 0));
7255 run_keys(&mut e, "j");
7257 assert_eq!(e.cursor(), (2, 6));
7258 }
7259
7260 #[test]
7261 fn j_through_shorter_line_preserves_column() {
7262 let mut e = editor_with("hello world\nhi\nanother line");
7263 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7266 run_keys(&mut e, "j");
7267 assert_eq!(e.cursor(), (2, 7));
7268 }
7269
7270 #[test]
7271 fn esc_from_insert_sticky_matches_visible_cursor() {
7272 let mut e = editor_with(" this is a line\n another one of a similar size");
7276 e.jump_cursor(0, 12);
7277 run_keys(&mut e, "I");
7278 assert_eq!(e.cursor(), (0, 4));
7279 run_keys(&mut e, "X<Esc>");
7280 assert_eq!(e.cursor(), (0, 4));
7281 run_keys(&mut e, "j");
7282 assert_eq!(e.cursor(), (1, 4));
7283 }
7284
7285 #[test]
7286 fn esc_from_insert_sticky_tracks_inserted_chars() {
7287 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7288 run_keys(&mut e, "i");
7289 run_keys(&mut e, "abc<Esc>");
7290 assert_eq!(e.cursor(), (0, 2));
7291 run_keys(&mut e, "j");
7292 assert_eq!(e.cursor(), (1, 2));
7293 }
7294
7295 #[test]
7296 fn esc_from_insert_sticky_tracks_arrow_nav() {
7297 let mut e = editor_with("xxxxxx\nyyyyyy");
7298 run_keys(&mut e, "i");
7299 run_keys(&mut e, "abc");
7300 for _ in 0..2 {
7301 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7302 }
7303 run_keys(&mut e, "<Esc>");
7304 assert_eq!(e.cursor(), (0, 0));
7305 run_keys(&mut e, "j");
7306 assert_eq!(e.cursor(), (1, 0));
7307 }
7308
7309 #[test]
7310 fn esc_from_insert_at_col_14_followed_by_j() {
7311 let line = "x".repeat(30);
7314 let buf = format!("{line}\n{line}");
7315 let mut e = editor_with(&buf);
7316 e.jump_cursor(0, 14);
7317 run_keys(&mut e, "i");
7318 for c in "test ".chars() {
7319 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7320 }
7321 run_keys(&mut e, "<Esc>");
7322 assert_eq!(e.cursor(), (0, 18));
7323 run_keys(&mut e, "j");
7324 assert_eq!(e.cursor(), (1, 18));
7325 }
7326
7327 #[test]
7328 fn linewise_paste_resets_sticky_column() {
7329 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7333 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7335 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7339 run_keys(&mut e, "j");
7341 assert_eq!(e.cursor(), (3, 2));
7342 }
7343
7344 #[test]
7345 fn horizontal_motion_resyncs_sticky_column() {
7346 let mut e = editor_with("hello world\n\nanother line");
7350 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7353 assert_eq!(e.cursor(), (2, 3));
7354 }
7355
7356 #[test]
7359 fn ctrl_v_enters_visual_block() {
7360 let mut e = editor_with("aaa\nbbb\nccc");
7361 run_keys(&mut e, "<C-v>");
7362 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7363 }
7364
7365 #[test]
7366 fn visual_block_esc_returns_to_normal() {
7367 let mut e = editor_with("aaa\nbbb\nccc");
7368 run_keys(&mut e, "<C-v>");
7369 run_keys(&mut e, "<Esc>");
7370 assert_eq!(e.vim_mode(), VimMode::Normal);
7371 }
7372
7373 #[test]
7374 fn visual_block_delete_removes_column_range() {
7375 let mut e = editor_with("hello\nworld\nhappy");
7376 run_keys(&mut e, "l");
7378 run_keys(&mut e, "<C-v>");
7379 run_keys(&mut e, "jj");
7380 run_keys(&mut e, "ll");
7381 run_keys(&mut e, "d");
7382 assert_eq!(
7384 e.buffer().lines(),
7385 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7386 );
7387 }
7388
7389 #[test]
7390 fn visual_block_yank_joins_with_newlines() {
7391 let mut e = editor_with("hello\nworld\nhappy");
7392 run_keys(&mut e, "<C-v>");
7393 run_keys(&mut e, "jj");
7394 run_keys(&mut e, "ll");
7395 run_keys(&mut e, "y");
7396 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7397 }
7398
7399 #[test]
7400 fn visual_block_replace_fills_block() {
7401 let mut e = editor_with("hello\nworld\nhappy");
7402 run_keys(&mut e, "<C-v>");
7403 run_keys(&mut e, "jj");
7404 run_keys(&mut e, "ll");
7405 run_keys(&mut e, "rx");
7406 assert_eq!(
7407 e.buffer().lines(),
7408 &[
7409 "xxxlo".to_string(),
7410 "xxxld".to_string(),
7411 "xxxpy".to_string()
7412 ]
7413 );
7414 }
7415
7416 #[test]
7417 fn visual_block_insert_repeats_across_rows() {
7418 let mut e = editor_with("hello\nworld\nhappy");
7419 run_keys(&mut e, "<C-v>");
7420 run_keys(&mut e, "jj");
7421 run_keys(&mut e, "I");
7422 run_keys(&mut e, "# <Esc>");
7423 assert_eq!(
7424 e.buffer().lines(),
7425 &[
7426 "# hello".to_string(),
7427 "# world".to_string(),
7428 "# happy".to_string()
7429 ]
7430 );
7431 }
7432
7433 #[test]
7434 fn block_highlight_returns_none_outside_block_mode() {
7435 let mut e = editor_with("abc");
7436 assert!(e.block_highlight().is_none());
7437 run_keys(&mut e, "v");
7438 assert!(e.block_highlight().is_none());
7439 run_keys(&mut e, "<Esc>V");
7440 assert!(e.block_highlight().is_none());
7441 }
7442
7443 #[test]
7444 fn block_highlight_bounds_track_anchor_and_cursor() {
7445 let mut e = editor_with("aaaa\nbbbb\ncccc");
7446 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7448 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7451 }
7452
7453 #[test]
7454 fn visual_block_delete_handles_short_lines() {
7455 let mut e = editor_with("hello\nhi\nworld");
7457 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7459 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7461 assert_eq!(
7466 e.buffer().lines(),
7467 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7468 );
7469 }
7470
7471 #[test]
7472 fn visual_block_yank_pads_short_lines_with_empties() {
7473 let mut e = editor_with("hello\nhi\nworld");
7474 run_keys(&mut e, "l");
7475 run_keys(&mut e, "<C-v>");
7476 run_keys(&mut e, "jjll");
7477 run_keys(&mut e, "y");
7478 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7480 }
7481
7482 #[test]
7483 fn visual_block_replace_skips_past_eol() {
7484 let mut e = editor_with("ab\ncd\nef");
7487 run_keys(&mut e, "l");
7489 run_keys(&mut e, "<C-v>");
7490 run_keys(&mut e, "jjllllll");
7491 run_keys(&mut e, "rX");
7492 assert_eq!(
7495 e.buffer().lines(),
7496 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7497 );
7498 }
7499
7500 #[test]
7501 fn visual_block_with_empty_line_in_middle() {
7502 let mut e = editor_with("abcd\n\nefgh");
7503 run_keys(&mut e, "<C-v>");
7504 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7506 assert_eq!(
7509 e.buffer().lines(),
7510 &["d".to_string(), "".to_string(), "h".to_string()]
7511 );
7512 }
7513
7514 #[test]
7515 fn block_insert_pads_empty_lines_to_block_column() {
7516 let mut e = editor_with("this is a line\n\nthis is a line");
7519 e.jump_cursor(0, 3);
7520 run_keys(&mut e, "<C-v>");
7521 run_keys(&mut e, "jj");
7522 run_keys(&mut e, "I");
7523 run_keys(&mut e, "XX<Esc>");
7524 assert_eq!(
7525 e.buffer().lines(),
7526 &[
7527 "thiXXs is a line".to_string(),
7528 " XX".to_string(),
7529 "thiXXs is a line".to_string()
7530 ]
7531 );
7532 }
7533
7534 #[test]
7535 fn block_insert_pads_short_lines_to_block_column() {
7536 let mut e = editor_with("aaaaa\nbb\naaaaa");
7537 e.jump_cursor(0, 3);
7538 run_keys(&mut e, "<C-v>");
7539 run_keys(&mut e, "jj");
7540 run_keys(&mut e, "I");
7541 run_keys(&mut e, "Y<Esc>");
7542 assert_eq!(
7544 e.buffer().lines(),
7545 &[
7546 "aaaYaa".to_string(),
7547 "bb Y".to_string(),
7548 "aaaYaa".to_string()
7549 ]
7550 );
7551 }
7552
7553 #[test]
7554 fn visual_block_append_repeats_across_rows() {
7555 let mut e = editor_with("foo\nbar\nbaz");
7556 run_keys(&mut e, "<C-v>");
7557 run_keys(&mut e, "jj");
7558 run_keys(&mut e, "A");
7561 run_keys(&mut e, "!<Esc>");
7562 assert_eq!(
7563 e.buffer().lines(),
7564 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7565 );
7566 }
7567
7568 #[test]
7571 fn slash_opens_forward_search_prompt() {
7572 let mut e = editor_with("hello world");
7573 run_keys(&mut e, "/");
7574 let p = e.search_prompt().expect("prompt should be active");
7575 assert!(p.text.is_empty());
7576 assert!(p.forward);
7577 }
7578
7579 #[test]
7580 fn question_opens_backward_search_prompt() {
7581 let mut e = editor_with("hello world");
7582 run_keys(&mut e, "?");
7583 let p = e.search_prompt().expect("prompt should be active");
7584 assert!(!p.forward);
7585 }
7586
7587 #[test]
7588 fn search_prompt_typing_updates_pattern_live() {
7589 let mut e = editor_with("foo bar\nbaz");
7590 run_keys(&mut e, "/bar");
7591 assert_eq!(e.search_prompt().unwrap().text, "bar");
7592 assert!(e.search_state().pattern.is_some());
7594 }
7595
7596 #[test]
7597 fn search_prompt_backspace_and_enter() {
7598 let mut e = editor_with("hello world\nagain");
7599 run_keys(&mut e, "/worlx");
7600 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7601 assert_eq!(e.search_prompt().unwrap().text, "worl");
7602 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7603 assert!(e.search_prompt().is_none());
7605 assert_eq!(e.last_search(), Some("worl"));
7606 assert_eq!(e.cursor(), (0, 6));
7607 }
7608
7609 #[test]
7610 fn empty_search_prompt_enter_repeats_last_search() {
7611 let mut e = editor_with("foo bar foo baz foo");
7612 run_keys(&mut e, "/foo");
7613 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7614 assert_eq!(e.cursor().1, 8);
7615 run_keys(&mut e, "/");
7617 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7618 assert_eq!(e.cursor().1, 16);
7619 assert_eq!(e.last_search(), Some("foo"));
7620 }
7621
7622 #[test]
7623 fn search_history_records_committed_patterns() {
7624 let mut e = editor_with("alpha beta gamma");
7625 run_keys(&mut e, "/alpha<CR>");
7626 run_keys(&mut e, "/beta<CR>");
7627 let history = e.vim.search_history.clone();
7629 assert_eq!(history, vec!["alpha", "beta"]);
7630 }
7631
7632 #[test]
7633 fn search_history_dedupes_consecutive_repeats() {
7634 let mut e = editor_with("foo bar foo");
7635 run_keys(&mut e, "/foo<CR>");
7636 run_keys(&mut e, "/foo<CR>");
7637 run_keys(&mut e, "/bar<CR>");
7638 run_keys(&mut e, "/bar<CR>");
7639 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
7641 }
7642
7643 #[test]
7644 fn ctrl_p_walks_history_backward() {
7645 let mut e = editor_with("alpha beta gamma");
7646 run_keys(&mut e, "/alpha<CR>");
7647 run_keys(&mut e, "/beta<CR>");
7648 run_keys(&mut e, "/");
7650 assert_eq!(e.search_prompt().unwrap().text, "");
7651 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7652 assert_eq!(e.search_prompt().unwrap().text, "beta");
7653 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7654 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7655 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7657 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7658 }
7659
7660 #[test]
7661 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7662 let mut e = editor_with("a b c");
7663 run_keys(&mut e, "/a<CR>");
7664 run_keys(&mut e, "/b<CR>");
7665 run_keys(&mut e, "/c<CR>");
7666 run_keys(&mut e, "/");
7667 for _ in 0..3 {
7669 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7670 }
7671 assert_eq!(e.search_prompt().unwrap().text, "a");
7672 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7673 assert_eq!(e.search_prompt().unwrap().text, "b");
7674 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7675 assert_eq!(e.search_prompt().unwrap().text, "c");
7676 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7678 assert_eq!(e.search_prompt().unwrap().text, "c");
7679 }
7680
7681 #[test]
7682 fn typing_after_history_walk_resets_cursor() {
7683 let mut e = editor_with("foo");
7684 run_keys(&mut e, "/foo<CR>");
7685 run_keys(&mut e, "/");
7686 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7687 assert_eq!(e.search_prompt().unwrap().text, "foo");
7688 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7691 assert_eq!(e.search_prompt().unwrap().text, "foox");
7692 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7693 assert_eq!(e.search_prompt().unwrap().text, "foo");
7694 }
7695
7696 #[test]
7697 fn empty_backward_search_prompt_enter_repeats_last_search() {
7698 let mut e = editor_with("foo bar foo baz foo");
7699 run_keys(&mut e, "/foo");
7701 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7702 assert_eq!(e.cursor().1, 8);
7703 run_keys(&mut e, "?");
7704 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7705 assert_eq!(e.cursor().1, 0);
7706 assert_eq!(e.last_search(), Some("foo"));
7707 }
7708
7709 #[test]
7710 fn search_prompt_esc_cancels_but_keeps_last_search() {
7711 let mut e = editor_with("foo bar\nbaz");
7712 run_keys(&mut e, "/bar");
7713 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7714 assert!(e.search_prompt().is_none());
7715 assert_eq!(e.last_search(), Some("bar"));
7716 }
7717
7718 #[test]
7719 fn search_then_n_and_shift_n_navigate() {
7720 let mut e = editor_with("foo bar foo baz foo");
7721 run_keys(&mut e, "/foo");
7722 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7723 assert_eq!(e.cursor().1, 8);
7725 run_keys(&mut e, "n");
7726 assert_eq!(e.cursor().1, 16);
7727 run_keys(&mut e, "N");
7728 assert_eq!(e.cursor().1, 8);
7729 }
7730
7731 #[test]
7732 fn question_mark_searches_backward_on_enter() {
7733 let mut e = editor_with("foo bar foo baz");
7734 e.jump_cursor(0, 10);
7735 run_keys(&mut e, "?foo");
7736 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7737 assert_eq!(e.cursor(), (0, 8));
7739 }
7740
7741 #[test]
7744 fn big_y_yanks_to_end_of_line() {
7745 let mut e = editor_with("hello world");
7746 e.jump_cursor(0, 6);
7747 run_keys(&mut e, "Y");
7748 assert_eq!(e.last_yank.as_deref(), Some("world"));
7749 }
7750
7751 #[test]
7752 fn big_y_from_line_start_yanks_full_line() {
7753 let mut e = editor_with("hello world");
7754 run_keys(&mut e, "Y");
7755 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7756 }
7757
7758 #[test]
7759 fn gj_joins_without_inserting_space() {
7760 let mut e = editor_with("hello\n world");
7761 run_keys(&mut e, "gJ");
7762 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7764 }
7765
7766 #[test]
7767 fn gj_noop_on_last_line() {
7768 let mut e = editor_with("only");
7769 run_keys(&mut e, "gJ");
7770 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7771 }
7772
7773 #[test]
7774 fn ge_jumps_to_previous_word_end() {
7775 let mut e = editor_with("foo bar baz");
7776 e.jump_cursor(0, 5);
7777 run_keys(&mut e, "ge");
7778 assert_eq!(e.cursor(), (0, 2));
7779 }
7780
7781 #[test]
7782 fn ge_respects_word_class() {
7783 let mut e = editor_with("foo-bar baz");
7786 e.jump_cursor(0, 5);
7787 run_keys(&mut e, "ge");
7788 assert_eq!(e.cursor(), (0, 3));
7789 }
7790
7791 #[test]
7792 fn big_ge_treats_hyphens_as_part_of_word() {
7793 let mut e = editor_with("foo-bar baz");
7796 e.jump_cursor(0, 10);
7797 run_keys(&mut e, "gE");
7798 assert_eq!(e.cursor(), (0, 6));
7799 }
7800
7801 #[test]
7802 fn ge_crosses_line_boundary() {
7803 let mut e = editor_with("foo\nbar");
7804 e.jump_cursor(1, 0);
7805 run_keys(&mut e, "ge");
7806 assert_eq!(e.cursor(), (0, 2));
7807 }
7808
7809 #[test]
7810 fn dge_deletes_to_end_of_previous_word() {
7811 let mut e = editor_with("foo bar baz");
7812 e.jump_cursor(0, 8);
7813 run_keys(&mut e, "dge");
7816 assert_eq!(e.buffer().lines()[0], "foo baaz");
7817 }
7818
7819 #[test]
7820 fn ctrl_scroll_keys_do_not_panic() {
7821 let mut e = editor_with(
7824 (0..50)
7825 .map(|i| format!("line{i}"))
7826 .collect::<Vec<_>>()
7827 .join("\n")
7828 .as_str(),
7829 );
7830 run_keys(&mut e, "<C-f>");
7831 run_keys(&mut e, "<C-b>");
7832 assert!(!e.buffer().lines().is_empty());
7834 }
7835
7836 #[test]
7843 fn count_insert_with_arrow_nav_does_not_leak_rows() {
7844 let mut e = Editor::new(
7845 hjkl_buffer::Buffer::new(),
7846 crate::types::DefaultHost::new(),
7847 crate::types::Options::default(),
7848 );
7849 e.set_content("row0\nrow1\nrow2");
7850 run_keys(&mut e, "3iX<Down><Esc>");
7852 assert!(e.buffer().lines()[0].contains('X'));
7854 assert!(
7857 !e.buffer().lines()[1].contains("row0"),
7858 "row1 leaked row0 contents: {:?}",
7859 e.buffer().lines()[1]
7860 );
7861 assert_eq!(e.buffer().lines().len(), 3);
7864 }
7865
7866 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
7869 let mut e = Editor::new(
7870 hjkl_buffer::Buffer::new(),
7871 crate::types::DefaultHost::new(),
7872 crate::types::Options::default(),
7873 );
7874 let body = (0..n)
7875 .map(|i| format!(" line{}", i))
7876 .collect::<Vec<_>>()
7877 .join("\n");
7878 e.set_content(&body);
7879 e.set_viewport_height(viewport);
7880 e
7881 }
7882
7883 #[test]
7884 fn ctrl_d_moves_cursor_half_page_down() {
7885 let mut e = editor_with_rows(100, 20);
7886 run_keys(&mut e, "<C-d>");
7887 assert_eq!(e.cursor().0, 10);
7888 }
7889
7890 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
7891 let mut e = Editor::new(
7892 hjkl_buffer::Buffer::new(),
7893 crate::types::DefaultHost::new(),
7894 crate::types::Options::default(),
7895 );
7896 e.set_content(&lines.join("\n"));
7897 e.set_viewport_height(viewport);
7898 let v = e.host_mut().viewport_mut();
7899 v.height = viewport;
7900 v.width = text_width;
7901 v.text_width = text_width;
7902 v.wrap = hjkl_buffer::Wrap::Char;
7903 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
7904 e
7905 }
7906
7907 #[test]
7908 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
7909 let lines = ["aaaabbbbcccc"; 10];
7913 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7914 e.jump_cursor(4, 0);
7915 e.ensure_cursor_in_scrolloff();
7916 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
7917 assert!(csr <= 6, "csr={csr}");
7918 }
7919
7920 #[test]
7921 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
7922 let lines = ["aaaabbbbcccc"; 10];
7923 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7924 e.jump_cursor(7, 0);
7927 e.ensure_cursor_in_scrolloff();
7928 e.jump_cursor(2, 0);
7929 e.ensure_cursor_in_scrolloff();
7930 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
7931 assert!(csr >= 5, "csr={csr}");
7933 }
7934
7935 #[test]
7936 fn scrolloff_wrap_clamps_top_at_buffer_end() {
7937 let lines = ["aaaabbbbcccc"; 5];
7938 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7939 e.jump_cursor(4, 11);
7940 e.ensure_cursor_in_scrolloff();
7941 let top = e.host().viewport().top_row;
7946 assert_eq!(top, 1);
7947 }
7948
7949 #[test]
7950 fn ctrl_u_moves_cursor_half_page_up() {
7951 let mut e = editor_with_rows(100, 20);
7952 e.jump_cursor(50, 0);
7953 run_keys(&mut e, "<C-u>");
7954 assert_eq!(e.cursor().0, 40);
7955 }
7956
7957 #[test]
7958 fn ctrl_f_moves_cursor_full_page_down() {
7959 let mut e = editor_with_rows(100, 20);
7960 run_keys(&mut e, "<C-f>");
7961 assert_eq!(e.cursor().0, 18);
7963 }
7964
7965 #[test]
7966 fn ctrl_b_moves_cursor_full_page_up() {
7967 let mut e = editor_with_rows(100, 20);
7968 e.jump_cursor(50, 0);
7969 run_keys(&mut e, "<C-b>");
7970 assert_eq!(e.cursor().0, 32);
7971 }
7972
7973 #[test]
7974 fn ctrl_d_lands_on_first_non_blank() {
7975 let mut e = editor_with_rows(100, 20);
7976 run_keys(&mut e, "<C-d>");
7977 assert_eq!(e.cursor().1, 2);
7979 }
7980
7981 #[test]
7982 fn ctrl_d_clamps_at_end_of_buffer() {
7983 let mut e = editor_with_rows(5, 20);
7984 run_keys(&mut e, "<C-d>");
7985 assert_eq!(e.cursor().0, 4);
7986 }
7987
7988 #[test]
7989 fn capital_h_jumps_to_viewport_top() {
7990 let mut e = editor_with_rows(100, 10);
7991 e.jump_cursor(50, 0);
7992 e.set_viewport_top(45);
7993 let top = e.host().viewport().top_row;
7994 run_keys(&mut e, "H");
7995 assert_eq!(e.cursor().0, top);
7996 assert_eq!(e.cursor().1, 2);
7997 }
7998
7999 #[test]
8000 fn capital_l_jumps_to_viewport_bottom() {
8001 let mut e = editor_with_rows(100, 10);
8002 e.jump_cursor(50, 0);
8003 e.set_viewport_top(45);
8004 let top = e.host().viewport().top_row;
8005 run_keys(&mut e, "L");
8006 assert_eq!(e.cursor().0, top + 9);
8007 }
8008
8009 #[test]
8010 fn capital_m_jumps_to_viewport_middle() {
8011 let mut e = editor_with_rows(100, 10);
8012 e.jump_cursor(50, 0);
8013 e.set_viewport_top(45);
8014 let top = e.host().viewport().top_row;
8015 run_keys(&mut e, "M");
8016 assert_eq!(e.cursor().0, top + 4);
8018 }
8019
8020 #[test]
8021 fn g_capital_m_lands_at_line_midpoint() {
8022 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8024 assert_eq!(e.cursor(), (0, 6));
8026 }
8027
8028 #[test]
8029 fn g_capital_m_on_empty_line_stays_at_zero() {
8030 let mut e = editor_with("");
8031 run_keys(&mut e, "gM");
8032 assert_eq!(e.cursor(), (0, 0));
8033 }
8034
8035 #[test]
8036 fn g_capital_m_uses_current_line_only() {
8037 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8040 run_keys(&mut e, "gM");
8041 assert_eq!(e.cursor(), (1, 6));
8042 }
8043
8044 #[test]
8045 fn capital_h_count_offsets_from_top() {
8046 let mut e = editor_with_rows(100, 10);
8047 e.jump_cursor(50, 0);
8048 e.set_viewport_top(45);
8049 let top = e.host().viewport().top_row;
8050 run_keys(&mut e, "3H");
8051 assert_eq!(e.cursor().0, top + 2);
8052 }
8053
8054 #[test]
8057 fn ctrl_o_returns_to_pre_g_position() {
8058 let mut e = editor_with_rows(50, 20);
8059 e.jump_cursor(5, 2);
8060 run_keys(&mut e, "G");
8061 assert_eq!(e.cursor().0, 49);
8062 run_keys(&mut e, "<C-o>");
8063 assert_eq!(e.cursor(), (5, 2));
8064 }
8065
8066 #[test]
8067 fn ctrl_i_redoes_jump_after_ctrl_o() {
8068 let mut e = editor_with_rows(50, 20);
8069 e.jump_cursor(5, 2);
8070 run_keys(&mut e, "G");
8071 let post = e.cursor();
8072 run_keys(&mut e, "<C-o>");
8073 run_keys(&mut e, "<C-i>");
8074 assert_eq!(e.cursor(), post);
8075 }
8076
8077 #[test]
8078 fn new_jump_clears_forward_stack() {
8079 let mut e = editor_with_rows(50, 20);
8080 e.jump_cursor(5, 2);
8081 run_keys(&mut e, "G");
8082 run_keys(&mut e, "<C-o>");
8083 run_keys(&mut e, "gg");
8084 run_keys(&mut e, "<C-i>");
8085 assert_eq!(e.cursor().0, 0);
8086 }
8087
8088 #[test]
8089 fn ctrl_o_on_empty_stack_is_noop() {
8090 let mut e = editor_with_rows(10, 20);
8091 e.jump_cursor(3, 1);
8092 run_keys(&mut e, "<C-o>");
8093 assert_eq!(e.cursor(), (3, 1));
8094 }
8095
8096 #[test]
8097 fn asterisk_search_pushes_jump() {
8098 let mut e = editor_with("foo bar\nbaz foo end");
8099 e.jump_cursor(0, 0);
8100 run_keys(&mut e, "*");
8101 let after = e.cursor();
8102 assert_ne!(after, (0, 0));
8103 run_keys(&mut e, "<C-o>");
8104 assert_eq!(e.cursor(), (0, 0));
8105 }
8106
8107 #[test]
8108 fn h_viewport_jump_is_recorded() {
8109 let mut e = editor_with_rows(100, 10);
8110 e.jump_cursor(50, 0);
8111 e.set_viewport_top(45);
8112 let pre = e.cursor();
8113 run_keys(&mut e, "H");
8114 assert_ne!(e.cursor(), pre);
8115 run_keys(&mut e, "<C-o>");
8116 assert_eq!(e.cursor(), pre);
8117 }
8118
8119 #[test]
8120 fn j_k_motion_does_not_push_jump() {
8121 let mut e = editor_with_rows(50, 20);
8122 e.jump_cursor(5, 0);
8123 run_keys(&mut e, "jjj");
8124 run_keys(&mut e, "<C-o>");
8125 assert_eq!(e.cursor().0, 8);
8126 }
8127
8128 #[test]
8129 fn jumplist_caps_at_100() {
8130 let mut e = editor_with_rows(200, 20);
8131 for i in 0..101 {
8132 e.jump_cursor(i, 0);
8133 run_keys(&mut e, "G");
8134 }
8135 assert!(e.vim.jump_back.len() <= 100);
8136 }
8137
8138 #[test]
8139 fn tab_acts_as_ctrl_i() {
8140 let mut e = editor_with_rows(50, 20);
8141 e.jump_cursor(5, 2);
8142 run_keys(&mut e, "G");
8143 let post = e.cursor();
8144 run_keys(&mut e, "<C-o>");
8145 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8146 assert_eq!(e.cursor(), post);
8147 }
8148
8149 #[test]
8152 fn ma_then_backtick_a_jumps_exact() {
8153 let mut e = editor_with_rows(50, 20);
8154 e.jump_cursor(5, 3);
8155 run_keys(&mut e, "ma");
8156 e.jump_cursor(20, 0);
8157 run_keys(&mut e, "`a");
8158 assert_eq!(e.cursor(), (5, 3));
8159 }
8160
8161 #[test]
8162 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8163 let mut e = editor_with_rows(50, 20);
8164 e.jump_cursor(5, 6);
8166 run_keys(&mut e, "ma");
8167 e.jump_cursor(30, 4);
8168 run_keys(&mut e, "'a");
8169 assert_eq!(e.cursor(), (5, 2));
8170 }
8171
8172 #[test]
8173 fn goto_mark_pushes_jumplist() {
8174 let mut e = editor_with_rows(50, 20);
8175 e.jump_cursor(10, 2);
8176 run_keys(&mut e, "mz");
8177 e.jump_cursor(3, 0);
8178 run_keys(&mut e, "`z");
8179 assert_eq!(e.cursor(), (10, 2));
8180 run_keys(&mut e, "<C-o>");
8181 assert_eq!(e.cursor(), (3, 0));
8182 }
8183
8184 #[test]
8185 fn goto_missing_mark_is_noop() {
8186 let mut e = editor_with_rows(50, 20);
8187 e.jump_cursor(3, 1);
8188 run_keys(&mut e, "`q");
8189 assert_eq!(e.cursor(), (3, 1));
8190 }
8191
8192 #[test]
8193 fn uppercase_mark_stored_under_uppercase_key() {
8194 let mut e = editor_with_rows(50, 20);
8195 e.jump_cursor(5, 3);
8196 run_keys(&mut e, "mA");
8197 assert_eq!(e.mark('A'), Some((5, 3)));
8200 assert!(e.mark('a').is_none());
8201 }
8202
8203 #[test]
8204 fn mark_survives_document_shrink_via_clamp() {
8205 let mut e = editor_with_rows(50, 20);
8206 e.jump_cursor(40, 4);
8207 run_keys(&mut e, "mx");
8208 e.set_content("a\nb\nc\nd\ne");
8210 run_keys(&mut e, "`x");
8211 let (r, _) = e.cursor();
8213 assert!(r <= 4);
8214 }
8215
8216 #[test]
8217 fn g_semicolon_walks_back_through_edits() {
8218 let mut e = editor_with("alpha\nbeta\ngamma");
8219 e.jump_cursor(0, 0);
8222 run_keys(&mut e, "iX<Esc>");
8223 e.jump_cursor(2, 0);
8224 run_keys(&mut e, "iY<Esc>");
8225 run_keys(&mut e, "g;");
8227 assert_eq!(e.cursor(), (2, 1));
8228 run_keys(&mut e, "g;");
8230 assert_eq!(e.cursor(), (0, 1));
8231 run_keys(&mut e, "g;");
8233 assert_eq!(e.cursor(), (0, 1));
8234 }
8235
8236 #[test]
8237 fn g_comma_walks_forward_after_g_semicolon() {
8238 let mut e = editor_with("a\nb\nc");
8239 e.jump_cursor(0, 0);
8240 run_keys(&mut e, "iX<Esc>");
8241 e.jump_cursor(2, 0);
8242 run_keys(&mut e, "iY<Esc>");
8243 run_keys(&mut e, "g;");
8244 run_keys(&mut e, "g;");
8245 assert_eq!(e.cursor(), (0, 1));
8246 run_keys(&mut e, "g,");
8247 assert_eq!(e.cursor(), (2, 1));
8248 }
8249
8250 #[test]
8251 fn new_edit_during_walk_trims_forward_entries() {
8252 let mut e = editor_with("a\nb\nc\nd");
8253 e.jump_cursor(0, 0);
8254 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8256 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8259 run_keys(&mut e, "g;");
8260 assert_eq!(e.cursor(), (0, 1));
8261 run_keys(&mut e, "iZ<Esc>");
8263 run_keys(&mut e, "g,");
8265 assert_ne!(e.cursor(), (2, 1));
8267 }
8268
8269 #[test]
8275 fn capital_mark_set_and_jump() {
8276 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8277 e.jump_cursor(2, 1);
8278 run_keys(&mut e, "mA");
8279 e.jump_cursor(0, 0);
8281 run_keys(&mut e, "'A");
8283 assert_eq!(e.cursor().0, 2);
8285 }
8286
8287 #[test]
8288 fn capital_mark_survives_set_content() {
8289 let mut e = editor_with("first buffer line\nsecond");
8290 e.jump_cursor(1, 3);
8291 run_keys(&mut e, "mA");
8292 e.set_content("totally different content\non many\nrows of text");
8294 e.jump_cursor(0, 0);
8296 run_keys(&mut e, "'A");
8297 assert_eq!(e.cursor().0, 1);
8298 }
8299
8300 #[test]
8305 fn capital_mark_shifts_with_edit() {
8306 let mut e = editor_with("a\nb\nc\nd");
8307 e.jump_cursor(3, 0);
8308 run_keys(&mut e, "mA");
8309 e.jump_cursor(0, 0);
8311 run_keys(&mut e, "dd");
8312 e.jump_cursor(0, 0);
8313 run_keys(&mut e, "'A");
8314 assert_eq!(e.cursor().0, 2);
8315 }
8316
8317 #[test]
8318 fn mark_below_delete_shifts_up() {
8319 let mut e = editor_with("a\nb\nc\nd\ne");
8320 e.jump_cursor(3, 0);
8322 run_keys(&mut e, "ma");
8323 e.jump_cursor(0, 0);
8325 run_keys(&mut e, "dd");
8326 e.jump_cursor(0, 0);
8328 run_keys(&mut e, "'a");
8329 assert_eq!(e.cursor().0, 2);
8330 assert_eq!(e.buffer().line(2).unwrap(), "d");
8331 }
8332
8333 #[test]
8334 fn mark_on_deleted_row_is_dropped() {
8335 let mut e = editor_with("a\nb\nc\nd");
8336 e.jump_cursor(1, 0);
8338 run_keys(&mut e, "ma");
8339 run_keys(&mut e, "dd");
8341 e.jump_cursor(2, 0);
8343 run_keys(&mut e, "'a");
8344 assert_eq!(e.cursor().0, 2);
8346 }
8347
8348 #[test]
8349 fn mark_above_edit_unchanged() {
8350 let mut e = editor_with("a\nb\nc\nd\ne");
8351 e.jump_cursor(0, 0);
8353 run_keys(&mut e, "ma");
8354 e.jump_cursor(3, 0);
8356 run_keys(&mut e, "dd");
8357 e.jump_cursor(2, 0);
8359 run_keys(&mut e, "'a");
8360 assert_eq!(e.cursor().0, 0);
8361 }
8362
8363 #[test]
8364 fn mark_shifts_down_after_insert() {
8365 let mut e = editor_with("a\nb\nc");
8366 e.jump_cursor(2, 0);
8368 run_keys(&mut e, "ma");
8369 e.jump_cursor(0, 0);
8371 run_keys(&mut e, "Onew<Esc>");
8372 e.jump_cursor(0, 0);
8375 run_keys(&mut e, "'a");
8376 assert_eq!(e.cursor().0, 3);
8377 assert_eq!(e.buffer().line(3).unwrap(), "c");
8378 }
8379
8380 #[test]
8383 fn forward_search_commit_pushes_jump() {
8384 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8385 e.jump_cursor(0, 0);
8386 run_keys(&mut e, "/target<CR>");
8387 assert_ne!(e.cursor(), (0, 0));
8389 run_keys(&mut e, "<C-o>");
8391 assert_eq!(e.cursor(), (0, 0));
8392 }
8393
8394 #[test]
8395 fn search_commit_no_match_does_not_push_jump() {
8396 let mut e = editor_with("alpha beta\nfoo end");
8397 e.jump_cursor(0, 3);
8398 let pre_len = e.vim.jump_back.len();
8399 run_keys(&mut e, "/zzznotfound<CR>");
8400 assert_eq!(e.vim.jump_back.len(), pre_len);
8402 }
8403
8404 #[test]
8407 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8408 let mut e = editor_with("hello world");
8409 run_keys(&mut e, "lll");
8410 let (row, col) = e.cursor();
8411 assert_eq!(e.buffer.cursor().row, row);
8412 assert_eq!(e.buffer.cursor().col, col);
8413 }
8414
8415 #[test]
8416 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8417 let mut e = editor_with("aaaa\nbbbb\ncccc");
8418 run_keys(&mut e, "jj");
8419 let (row, col) = e.cursor();
8420 assert_eq!(e.buffer.cursor().row, row);
8421 assert_eq!(e.buffer.cursor().col, col);
8422 }
8423
8424 #[test]
8425 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8426 let mut e = editor_with("foo bar baz");
8427 run_keys(&mut e, "ww");
8428 let (row, col) = e.cursor();
8429 assert_eq!(e.buffer.cursor().row, row);
8430 assert_eq!(e.buffer.cursor().col, col);
8431 }
8432
8433 #[test]
8434 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8435 let mut e = editor_with("a\nb\nc\nd\ne");
8436 run_keys(&mut e, "G");
8437 let (row, col) = e.cursor();
8438 assert_eq!(e.buffer.cursor().row, row);
8439 assert_eq!(e.buffer.cursor().col, col);
8440 }
8441
8442 #[test]
8443 fn editor_sticky_col_tracks_horizontal_motion() {
8444 let mut e = editor_with("longline\nhi\nlongline");
8445 run_keys(&mut e, "fl");
8450 let landed = e.cursor().1;
8451 assert!(landed > 0, "fl should have moved");
8452 run_keys(&mut e, "j");
8453 assert_eq!(e.sticky_col(), Some(landed));
8456 }
8457
8458 #[test]
8459 fn buffer_content_mirrors_textarea_after_insert() {
8460 let mut e = editor_with("hello");
8461 run_keys(&mut e, "iXYZ<Esc>");
8462 let text = e.buffer().lines().join("\n");
8463 assert_eq!(e.buffer.as_string(), text);
8464 }
8465
8466 #[test]
8467 fn buffer_content_mirrors_textarea_after_delete() {
8468 let mut e = editor_with("alpha bravo charlie");
8469 run_keys(&mut e, "dw");
8470 let text = e.buffer().lines().join("\n");
8471 assert_eq!(e.buffer.as_string(), text);
8472 }
8473
8474 #[test]
8475 fn buffer_content_mirrors_textarea_after_dd() {
8476 let mut e = editor_with("a\nb\nc\nd");
8477 run_keys(&mut e, "jdd");
8478 let text = e.buffer().lines().join("\n");
8479 assert_eq!(e.buffer.as_string(), text);
8480 }
8481
8482 #[test]
8483 fn buffer_content_mirrors_textarea_after_open_line() {
8484 let mut e = editor_with("foo\nbar");
8485 run_keys(&mut e, "oNEW<Esc>");
8486 let text = e.buffer().lines().join("\n");
8487 assert_eq!(e.buffer.as_string(), text);
8488 }
8489
8490 #[test]
8491 fn buffer_content_mirrors_textarea_after_paste() {
8492 let mut e = editor_with("hello");
8493 run_keys(&mut e, "yy");
8494 run_keys(&mut e, "p");
8495 let text = e.buffer().lines().join("\n");
8496 assert_eq!(e.buffer.as_string(), text);
8497 }
8498
8499 #[test]
8500 fn buffer_selection_none_in_normal_mode() {
8501 let e = editor_with("foo bar");
8502 assert!(e.buffer_selection().is_none());
8503 }
8504
8505 #[test]
8506 fn buffer_selection_char_in_visual_mode() {
8507 use hjkl_buffer::{Position, Selection};
8508 let mut e = editor_with("hello world");
8509 run_keys(&mut e, "vlll");
8510 assert_eq!(
8511 e.buffer_selection(),
8512 Some(Selection::Char {
8513 anchor: Position::new(0, 0),
8514 head: Position::new(0, 3),
8515 })
8516 );
8517 }
8518
8519 #[test]
8520 fn buffer_selection_line_in_visual_line_mode() {
8521 use hjkl_buffer::Selection;
8522 let mut e = editor_with("a\nb\nc\nd");
8523 run_keys(&mut e, "Vj");
8524 assert_eq!(
8525 e.buffer_selection(),
8526 Some(Selection::Line {
8527 anchor_row: 0,
8528 head_row: 1,
8529 })
8530 );
8531 }
8532
8533 #[test]
8534 fn wrapscan_off_blocks_wrap_around() {
8535 let mut e = editor_with("first\nsecond\nthird\n");
8536 e.settings_mut().wrapscan = false;
8537 e.jump_cursor(2, 0);
8539 run_keys(&mut e, "/first<CR>");
8540 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8542 e.settings_mut().wrapscan = true;
8544 run_keys(&mut e, "/first<CR>");
8545 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8546 }
8547
8548 #[test]
8549 fn smartcase_uppercase_pattern_stays_sensitive() {
8550 let mut e = editor_with("foo\nFoo\nBAR\n");
8551 e.settings_mut().ignore_case = true;
8552 e.settings_mut().smartcase = true;
8553 run_keys(&mut e, "/foo<CR>");
8556 let r1 = e
8557 .search_state()
8558 .pattern
8559 .as_ref()
8560 .unwrap()
8561 .as_str()
8562 .to_string();
8563 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8564 run_keys(&mut e, "/Foo<CR>");
8566 let r2 = e
8567 .search_state()
8568 .pattern
8569 .as_ref()
8570 .unwrap()
8571 .as_str()
8572 .to_string();
8573 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8574 }
8575
8576 #[test]
8577 fn enter_with_autoindent_copies_leading_whitespace() {
8578 let mut e = editor_with(" foo");
8579 e.jump_cursor(0, 7);
8580 run_keys(&mut e, "i<CR>");
8581 assert_eq!(e.buffer.line(1).unwrap(), " ");
8582 }
8583
8584 #[test]
8585 fn enter_without_autoindent_inserts_bare_newline() {
8586 let mut e = editor_with(" foo");
8587 e.settings_mut().autoindent = false;
8588 e.jump_cursor(0, 7);
8589 run_keys(&mut e, "i<CR>");
8590 assert_eq!(e.buffer.line(1).unwrap(), "");
8591 }
8592
8593 #[test]
8594 fn iskeyword_default_treats_alnum_underscore_as_word() {
8595 let mut e = editor_with("foo_bar baz");
8596 e.jump_cursor(0, 0);
8600 run_keys(&mut e, "*");
8601 let p = e
8602 .search_state()
8603 .pattern
8604 .as_ref()
8605 .unwrap()
8606 .as_str()
8607 .to_string();
8608 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8609 }
8610
8611 #[test]
8612 fn w_motion_respects_custom_iskeyword() {
8613 let mut e = editor_with("foo-bar baz");
8617 run_keys(&mut e, "w");
8618 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
8619 let mut e2 = editor_with("foo-bar baz");
8622 e2.set_iskeyword("@,_,45");
8623 run_keys(&mut e2, "w");
8624 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
8625 }
8626
8627 #[test]
8628 fn iskeyword_with_dash_treats_dash_as_word_char() {
8629 let mut e = editor_with("foo-bar baz");
8630 e.settings_mut().iskeyword = "@,_,45".to_string();
8631 e.jump_cursor(0, 0);
8632 run_keys(&mut e, "*");
8633 let p = e
8634 .search_state()
8635 .pattern
8636 .as_ref()
8637 .unwrap()
8638 .as_str()
8639 .to_string();
8640 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
8641 }
8642
8643 #[test]
8644 fn timeoutlen_drops_pending_g_prefix() {
8645 use std::time::{Duration, Instant};
8646 let mut e = editor_with("a\nb\nc");
8647 e.jump_cursor(2, 0);
8648 run_keys(&mut e, "g");
8650 assert!(matches!(e.vim.pending, super::Pending::G));
8651 e.settings.timeout_len = Duration::from_nanos(0);
8659 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
8660 e.vim.last_input_host_at = Some(Duration::ZERO);
8661 run_keys(&mut e, "g");
8665 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
8667 }
8668
8669 #[test]
8670 fn undobreak_on_breaks_group_at_arrow_motion() {
8671 let mut e = editor_with("");
8672 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8674 let line = e.buffer.line(0).unwrap_or("").to_string();
8677 assert!(line.contains("aaa"), "after undobreak: {line:?}");
8678 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
8679 }
8680
8681 #[test]
8682 fn undobreak_off_keeps_full_run_in_one_group() {
8683 let mut e = editor_with("");
8684 e.settings_mut().undo_break_on_motion = false;
8685 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8686 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
8689 }
8690
8691 #[test]
8692 fn undobreak_round_trips_through_options() {
8693 let e = editor_with("");
8694 let opts = e.current_options();
8695 assert!(opts.undo_break_on_motion);
8696 let mut e2 = editor_with("");
8697 let mut new_opts = opts.clone();
8698 new_opts.undo_break_on_motion = false;
8699 e2.apply_options(&new_opts);
8700 assert!(!e2.current_options().undo_break_on_motion);
8701 }
8702
8703 #[test]
8704 fn undo_levels_cap_drops_oldest() {
8705 let mut e = editor_with("abcde");
8706 e.settings_mut().undo_levels = 3;
8707 run_keys(&mut e, "ra");
8708 run_keys(&mut e, "lrb");
8709 run_keys(&mut e, "lrc");
8710 run_keys(&mut e, "lrd");
8711 run_keys(&mut e, "lre");
8712 assert_eq!(e.undo_stack_len(), 3);
8713 }
8714
8715 #[test]
8716 fn tab_inserts_literal_tab_when_noexpandtab() {
8717 let mut e = editor_with("");
8718 e.settings_mut().expandtab = false;
8721 e.settings_mut().softtabstop = 0;
8722 run_keys(&mut e, "i");
8723 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8724 assert_eq!(e.buffer.line(0).unwrap(), "\t");
8725 }
8726
8727 #[test]
8728 fn tab_inserts_spaces_when_expandtab() {
8729 let mut e = editor_with("");
8730 e.settings_mut().expandtab = true;
8731 e.settings_mut().tabstop = 4;
8732 run_keys(&mut e, "i");
8733 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8734 assert_eq!(e.buffer.line(0).unwrap(), " ");
8735 }
8736
8737 #[test]
8738 fn tab_with_softtabstop_fills_to_next_boundary() {
8739 let mut e = editor_with("ab");
8741 e.settings_mut().expandtab = true;
8742 e.settings_mut().tabstop = 8;
8743 e.settings_mut().softtabstop = 4;
8744 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8746 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
8747 }
8748
8749 #[test]
8750 fn backspace_deletes_softtab_run() {
8751 let mut e = editor_with(" x");
8754 e.settings_mut().softtabstop = 4;
8755 run_keys(&mut e, "fxi");
8757 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8758 assert_eq!(e.buffer.line(0).unwrap(), "x");
8759 }
8760
8761 #[test]
8762 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
8763 let mut e = editor_with(" x");
8766 e.settings_mut().softtabstop = 4;
8767 run_keys(&mut e, "fxi");
8768 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8769 assert_eq!(e.buffer.line(0).unwrap(), " x");
8770 }
8771
8772 #[test]
8773 fn readonly_blocks_insert_mutation() {
8774 let mut e = editor_with("hello");
8775 e.settings_mut().readonly = true;
8776 run_keys(&mut e, "iX<Esc>");
8777 assert_eq!(e.buffer.line(0).unwrap(), "hello");
8778 }
8779
8780 #[cfg(feature = "ratatui")]
8781 #[test]
8782 fn intern_ratatui_style_dedups_repeated_styles() {
8783 use ratatui::style::{Color, Style};
8784 let mut e = editor_with("");
8785 let red = Style::default().fg(Color::Red);
8786 let blue = Style::default().fg(Color::Blue);
8787 let id_r1 = e.intern_ratatui_style(red);
8788 let id_r2 = e.intern_ratatui_style(red);
8789 let id_b = e.intern_ratatui_style(blue);
8790 assert_eq!(id_r1, id_r2);
8791 assert_ne!(id_r1, id_b);
8792 assert_eq!(e.style_table().len(), 2);
8793 }
8794
8795 #[cfg(feature = "ratatui")]
8796 #[test]
8797 fn install_ratatui_syntax_spans_translates_styled_spans() {
8798 use ratatui::style::{Color, Style};
8799 let mut e = editor_with("SELECT foo");
8800 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
8801 let by_row = e.buffer_spans();
8802 assert_eq!(by_row.len(), 1);
8803 assert_eq!(by_row[0].len(), 1);
8804 assert_eq!(by_row[0][0].start_byte, 0);
8805 assert_eq!(by_row[0][0].end_byte, 6);
8806 let id = by_row[0][0].style;
8807 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
8808 }
8809
8810 #[cfg(feature = "ratatui")]
8811 #[test]
8812 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
8813 use ratatui::style::{Color, Style};
8814 let mut e = editor_with("hello");
8815 e.install_ratatui_syntax_spans(vec![vec![(
8816 0,
8817 usize::MAX,
8818 Style::default().fg(Color::Blue),
8819 )]]);
8820 let by_row = e.buffer_spans();
8821 assert_eq!(by_row[0][0].end_byte, 5);
8822 }
8823
8824 #[cfg(feature = "ratatui")]
8825 #[test]
8826 fn install_ratatui_syntax_spans_drops_zero_width() {
8827 use ratatui::style::{Color, Style};
8828 let mut e = editor_with("abc");
8829 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
8830 assert!(e.buffer_spans()[0].is_empty());
8831 }
8832
8833 #[test]
8834 fn named_register_yank_into_a_then_paste_from_a() {
8835 let mut e = editor_with("hello world\nsecond");
8836 run_keys(&mut e, "\"ayw");
8837 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8839 run_keys(&mut e, "j0\"aP");
8841 assert_eq!(e.buffer().lines()[1], "hello second");
8842 }
8843
8844 #[test]
8845 fn capital_r_overstrikes_chars() {
8846 let mut e = editor_with("hello");
8847 e.jump_cursor(0, 0);
8848 run_keys(&mut e, "RXY<Esc>");
8849 assert_eq!(e.buffer().lines()[0], "XYllo");
8851 }
8852
8853 #[test]
8854 fn capital_r_at_eol_appends() {
8855 let mut e = editor_with("hi");
8856 e.jump_cursor(0, 1);
8857 run_keys(&mut e, "RXYZ<Esc>");
8859 assert_eq!(e.buffer().lines()[0], "hXYZ");
8860 }
8861
8862 #[test]
8863 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
8864 let mut e = editor_with("abc");
8868 e.jump_cursor(0, 0);
8869 run_keys(&mut e, "RX<Esc>");
8870 assert_eq!(e.buffer().lines()[0], "Xbc");
8871 }
8872
8873 #[test]
8874 fn ctrl_r_in_insert_pastes_named_register() {
8875 let mut e = editor_with("hello world");
8876 run_keys(&mut e, "\"ayw");
8878 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8879 run_keys(&mut e, "o");
8881 assert_eq!(e.vim_mode(), VimMode::Insert);
8882 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8883 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
8884 assert_eq!(e.buffer().lines()[1], "hello ");
8885 assert_eq!(e.cursor(), (1, 6));
8887 assert_eq!(e.vim_mode(), VimMode::Insert);
8889 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
8890 assert_eq!(e.buffer().lines()[1], "hello X");
8891 }
8892
8893 #[test]
8894 fn ctrl_r_with_unnamed_register() {
8895 let mut e = editor_with("foo");
8896 run_keys(&mut e, "yiw");
8897 run_keys(&mut e, "A ");
8898 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8900 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
8901 assert_eq!(e.buffer().lines()[0], "foo foo");
8902 }
8903
8904 #[test]
8905 fn ctrl_r_unknown_selector_is_no_op() {
8906 let mut e = editor_with("abc");
8907 run_keys(&mut e, "A");
8908 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8909 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
8912 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
8913 assert_eq!(e.buffer().lines()[0], "abcZ");
8914 }
8915
8916 #[test]
8917 fn ctrl_r_multiline_register_pastes_with_newlines() {
8918 let mut e = editor_with("alpha\nbeta\ngamma");
8919 run_keys(&mut e, "\"byy");
8921 run_keys(&mut e, "j\"byy");
8922 run_keys(&mut e, "ggVj\"by");
8926 let payload = e.registers().read('b').unwrap().text.clone();
8927 assert!(payload.contains('\n'));
8928 run_keys(&mut e, "Go");
8929 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8930 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
8931 let total_lines = e.buffer().lines().len();
8934 assert!(total_lines >= 5);
8935 }
8936
8937 #[test]
8938 fn yank_zero_holds_last_yank_after_delete() {
8939 let mut e = editor_with("hello world");
8940 run_keys(&mut e, "yw");
8941 let yanked = e.registers().read('0').unwrap().text.clone();
8942 assert!(!yanked.is_empty());
8943 run_keys(&mut e, "dw");
8945 assert_eq!(e.registers().read('0').unwrap().text, yanked);
8946 assert!(!e.registers().read('1').unwrap().text.is_empty());
8948 }
8949
8950 #[test]
8951 fn delete_ring_rotates_through_one_through_nine() {
8952 let mut e = editor_with("a b c d e f g h i j");
8953 for _ in 0..3 {
8955 run_keys(&mut e, "dw");
8956 }
8957 let r1 = e.registers().read('1').unwrap().text.clone();
8959 let r2 = e.registers().read('2').unwrap().text.clone();
8960 let r3 = e.registers().read('3').unwrap().text.clone();
8961 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
8962 assert_ne!(r1, r2);
8963 assert_ne!(r2, r3);
8964 }
8965
8966 #[test]
8967 fn capital_register_appends_to_lowercase() {
8968 let mut e = editor_with("foo bar");
8969 run_keys(&mut e, "\"ayw");
8970 let first = e.registers().read('a').unwrap().text.clone();
8971 assert!(first.contains("foo"));
8972 run_keys(&mut e, "w\"Ayw");
8974 let combined = e.registers().read('a').unwrap().text.clone();
8975 assert!(combined.starts_with(&first));
8976 assert!(combined.contains("bar"));
8977 }
8978
8979 #[test]
8980 fn zf_in_visual_line_creates_closed_fold() {
8981 let mut e = editor_with("a\nb\nc\nd\ne");
8982 e.jump_cursor(1, 0);
8984 run_keys(&mut e, "Vjjzf");
8985 assert_eq!(e.buffer().folds().len(), 1);
8986 let f = e.buffer().folds()[0];
8987 assert_eq!(f.start_row, 1);
8988 assert_eq!(f.end_row, 3);
8989 assert!(f.closed);
8990 }
8991
8992 #[test]
8993 fn zfj_in_normal_creates_two_row_fold() {
8994 let mut e = editor_with("a\nb\nc\nd\ne");
8995 e.jump_cursor(1, 0);
8996 run_keys(&mut e, "zfj");
8997 assert_eq!(e.buffer().folds().len(), 1);
8998 let f = e.buffer().folds()[0];
8999 assert_eq!(f.start_row, 1);
9000 assert_eq!(f.end_row, 2);
9001 assert!(f.closed);
9002 assert_eq!(e.cursor().0, 1);
9004 }
9005
9006 #[test]
9007 fn zf_with_count_folds_count_rows() {
9008 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9009 e.jump_cursor(0, 0);
9010 run_keys(&mut e, "zf3j");
9012 assert_eq!(e.buffer().folds().len(), 1);
9013 let f = e.buffer().folds()[0];
9014 assert_eq!(f.start_row, 0);
9015 assert_eq!(f.end_row, 3);
9016 }
9017
9018 #[test]
9019 fn zfk_folds_upward_range() {
9020 let mut e = editor_with("a\nb\nc\nd\ne");
9021 e.jump_cursor(3, 0);
9022 run_keys(&mut e, "zfk");
9023 let f = e.buffer().folds()[0];
9024 assert_eq!(f.start_row, 2);
9026 assert_eq!(f.end_row, 3);
9027 }
9028
9029 #[test]
9030 fn zf_capital_g_folds_to_bottom() {
9031 let mut e = editor_with("a\nb\nc\nd\ne");
9032 e.jump_cursor(1, 0);
9033 run_keys(&mut e, "zfG");
9035 let f = e.buffer().folds()[0];
9036 assert_eq!(f.start_row, 1);
9037 assert_eq!(f.end_row, 4);
9038 }
9039
9040 #[test]
9041 fn zfgg_folds_to_top_via_operator_pipeline() {
9042 let mut e = editor_with("a\nb\nc\nd\ne");
9043 e.jump_cursor(3, 0);
9044 run_keys(&mut e, "zfgg");
9048 let f = e.buffer().folds()[0];
9049 assert_eq!(f.start_row, 0);
9050 assert_eq!(f.end_row, 3);
9051 }
9052
9053 #[test]
9054 fn zfip_folds_paragraph_via_text_object() {
9055 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9056 e.jump_cursor(1, 0);
9057 run_keys(&mut e, "zfip");
9059 assert_eq!(e.buffer().folds().len(), 1);
9060 let f = e.buffer().folds()[0];
9061 assert_eq!(f.start_row, 0);
9062 assert_eq!(f.end_row, 2);
9063 }
9064
9065 #[test]
9066 fn zfap_folds_paragraph_with_trailing_blank() {
9067 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9068 e.jump_cursor(0, 0);
9069 run_keys(&mut e, "zfap");
9071 let f = e.buffer().folds()[0];
9072 assert_eq!(f.start_row, 0);
9073 assert_eq!(f.end_row, 3);
9074 }
9075
9076 #[test]
9077 fn zf_paragraph_motion_folds_to_blank() {
9078 let mut e = editor_with("alpha\nbeta\n\ngamma");
9079 e.jump_cursor(0, 0);
9080 run_keys(&mut e, "zf}");
9082 let f = e.buffer().folds()[0];
9083 assert_eq!(f.start_row, 0);
9084 assert_eq!(f.end_row, 2);
9085 }
9086
9087 #[test]
9088 fn za_toggles_fold_under_cursor() {
9089 let mut e = editor_with("a\nb\nc\nd");
9090 e.buffer_mut().add_fold(1, 2, true);
9091 e.jump_cursor(1, 0);
9092 run_keys(&mut e, "za");
9093 assert!(!e.buffer().folds()[0].closed);
9094 run_keys(&mut e, "za");
9095 assert!(e.buffer().folds()[0].closed);
9096 }
9097
9098 #[test]
9099 fn zr_opens_all_folds_zm_closes_all() {
9100 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9101 e.buffer_mut().add_fold(0, 1, true);
9102 e.buffer_mut().add_fold(2, 3, true);
9103 e.buffer_mut().add_fold(4, 5, true);
9104 run_keys(&mut e, "zR");
9105 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9106 run_keys(&mut e, "zM");
9107 assert!(e.buffer().folds().iter().all(|f| f.closed));
9108 }
9109
9110 #[test]
9111 fn ze_clears_all_folds() {
9112 let mut e = editor_with("a\nb\nc\nd");
9113 e.buffer_mut().add_fold(0, 1, true);
9114 e.buffer_mut().add_fold(2, 3, false);
9115 run_keys(&mut e, "zE");
9116 assert!(e.buffer().folds().is_empty());
9117 }
9118
9119 #[test]
9120 fn g_underscore_jumps_to_last_non_blank() {
9121 let mut e = editor_with("hello world ");
9122 run_keys(&mut e, "g_");
9123 assert_eq!(e.cursor().1, 10);
9125 }
9126
9127 #[test]
9128 fn gj_and_gk_alias_j_and_k() {
9129 let mut e = editor_with("a\nb\nc");
9130 run_keys(&mut e, "gj");
9131 assert_eq!(e.cursor().0, 1);
9132 run_keys(&mut e, "gk");
9133 assert_eq!(e.cursor().0, 0);
9134 }
9135
9136 #[test]
9137 fn paragraph_motions_walk_blank_lines() {
9138 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9139 run_keys(&mut e, "}");
9140 assert_eq!(e.cursor().0, 2);
9141 run_keys(&mut e, "}");
9142 assert_eq!(e.cursor().0, 5);
9143 run_keys(&mut e, "{");
9144 assert_eq!(e.cursor().0, 2);
9145 }
9146
9147 #[test]
9148 fn gv_reenters_last_visual_selection() {
9149 let mut e = editor_with("alpha\nbeta\ngamma");
9150 run_keys(&mut e, "Vj");
9151 run_keys(&mut e, "<Esc>");
9153 assert_eq!(e.vim_mode(), VimMode::Normal);
9154 run_keys(&mut e, "gv");
9156 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9157 }
9158
9159 #[test]
9160 fn o_in_visual_swaps_anchor_and_cursor() {
9161 let mut e = editor_with("hello world");
9162 run_keys(&mut e, "vllll");
9164 assert_eq!(e.cursor().1, 4);
9165 run_keys(&mut e, "o");
9167 assert_eq!(e.cursor().1, 0);
9168 assert_eq!(e.vim.visual_anchor, (0, 4));
9170 }
9171
9172 #[test]
9173 fn editing_inside_fold_invalidates_it() {
9174 let mut e = editor_with("a\nb\nc\nd");
9175 e.buffer_mut().add_fold(1, 2, true);
9176 e.jump_cursor(1, 0);
9177 run_keys(&mut e, "iX<Esc>");
9179 assert!(e.buffer().folds().is_empty());
9181 }
9182
9183 #[test]
9184 fn zd_removes_fold_under_cursor() {
9185 let mut e = editor_with("a\nb\nc\nd");
9186 e.buffer_mut().add_fold(1, 2, true);
9187 e.jump_cursor(2, 0);
9188 run_keys(&mut e, "zd");
9189 assert!(e.buffer().folds().is_empty());
9190 }
9191
9192 #[test]
9193 fn take_fold_ops_observes_z_keystroke_dispatch() {
9194 use crate::types::FoldOp;
9199 let mut e = editor_with("a\nb\nc\nd");
9200 e.buffer_mut().add_fold(1, 2, true);
9201 e.jump_cursor(1, 0);
9202 let _ = e.take_fold_ops();
9205 run_keys(&mut e, "zo");
9206 run_keys(&mut e, "zM");
9207 let ops = e.take_fold_ops();
9208 assert_eq!(ops.len(), 2);
9209 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9210 assert!(matches!(ops[1], FoldOp::CloseAll));
9211 assert!(e.take_fold_ops().is_empty());
9213 }
9214
9215 #[test]
9216 fn edit_pipeline_emits_invalidate_fold_op() {
9217 use crate::types::FoldOp;
9220 let mut e = editor_with("a\nb\nc\nd");
9221 e.buffer_mut().add_fold(1, 2, true);
9222 e.jump_cursor(1, 0);
9223 let _ = e.take_fold_ops();
9224 run_keys(&mut e, "iX<Esc>");
9225 let ops = e.take_fold_ops();
9226 assert!(
9227 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9228 "expected at least one Invalidate op, got {ops:?}"
9229 );
9230 }
9231
9232 #[test]
9233 fn dot_mark_jumps_to_last_edit_position() {
9234 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9235 e.jump_cursor(2, 0);
9236 run_keys(&mut e, "iX<Esc>");
9238 let after_edit = e.cursor();
9239 run_keys(&mut e, "gg");
9241 assert_eq!(e.cursor().0, 0);
9242 run_keys(&mut e, "'.");
9244 assert_eq!(e.cursor().0, after_edit.0);
9245 }
9246
9247 #[test]
9248 fn quote_quote_returns_to_pre_jump_position() {
9249 let mut e = editor_with_rows(50, 20);
9250 e.jump_cursor(10, 2);
9251 let before = e.cursor();
9252 run_keys(&mut e, "G");
9254 assert_ne!(e.cursor(), before);
9255 run_keys(&mut e, "''");
9257 assert_eq!(e.cursor().0, before.0);
9258 }
9259
9260 #[test]
9261 fn backtick_backtick_restores_exact_pre_jump_pos() {
9262 let mut e = editor_with_rows(50, 20);
9263 e.jump_cursor(7, 3);
9264 let before = e.cursor();
9265 run_keys(&mut e, "G");
9266 run_keys(&mut e, "``");
9267 assert_eq!(e.cursor(), before);
9268 }
9269
9270 #[test]
9271 fn macro_record_and_replay_basic() {
9272 let mut e = editor_with("foo\nbar\nbaz");
9273 run_keys(&mut e, "qaIX<Esc>jq");
9275 assert_eq!(e.buffer().lines()[0], "Xfoo");
9276 run_keys(&mut e, "@a");
9278 assert_eq!(e.buffer().lines()[1], "Xbar");
9279 run_keys(&mut e, "j@@");
9281 assert_eq!(e.buffer().lines()[2], "Xbaz");
9282 }
9283
9284 #[test]
9285 fn macro_count_replays_n_times() {
9286 let mut e = editor_with("a\nb\nc\nd\ne");
9287 run_keys(&mut e, "qajq");
9289 assert_eq!(e.cursor().0, 1);
9290 run_keys(&mut e, "3@a");
9292 assert_eq!(e.cursor().0, 4);
9293 }
9294
9295 #[test]
9296 fn macro_capital_q_appends_to_lowercase_register() {
9297 let mut e = editor_with("hello");
9298 run_keys(&mut e, "qall<Esc>q");
9299 run_keys(&mut e, "qAhh<Esc>q");
9300 let text = e.registers().read('a').unwrap().text.clone();
9303 assert!(text.contains("ll<Esc>"));
9304 assert!(text.contains("hh<Esc>"));
9305 }
9306
9307 #[test]
9308 fn buffer_selection_block_in_visual_block_mode() {
9309 use hjkl_buffer::{Position, Selection};
9310 let mut e = editor_with("aaaa\nbbbb\ncccc");
9311 run_keys(&mut e, "<C-v>jl");
9312 assert_eq!(
9313 e.buffer_selection(),
9314 Some(Selection::Block {
9315 anchor: Position::new(0, 0),
9316 head: Position::new(1, 1),
9317 })
9318 );
9319 }
9320
9321 #[test]
9324 fn n_after_question_mark_keeps_walking_backward() {
9325 let mut e = editor_with("foo bar foo baz foo end");
9328 e.jump_cursor(0, 22);
9329 run_keys(&mut e, "?foo<CR>");
9330 assert_eq!(e.cursor().1, 16);
9331 run_keys(&mut e, "n");
9332 assert_eq!(e.cursor().1, 8);
9333 run_keys(&mut e, "N");
9334 assert_eq!(e.cursor().1, 16);
9335 }
9336
9337 #[test]
9338 fn nested_macro_chord_records_literal_keys() {
9339 let mut e = editor_with("alpha\nbeta\ngamma");
9342 run_keys(&mut e, "qblq");
9344 run_keys(&mut e, "qaIX<Esc>q");
9347 e.jump_cursor(1, 0);
9349 run_keys(&mut e, "@a");
9350 assert_eq!(e.buffer().lines()[1], "Xbeta");
9351 }
9352
9353 #[test]
9354 fn shift_gt_motion_indents_one_line() {
9355 let mut e = editor_with("hello world");
9359 run_keys(&mut e, ">w");
9360 assert_eq!(e.buffer().lines()[0], " hello world");
9361 }
9362
9363 #[test]
9364 fn shift_lt_motion_outdents_one_line() {
9365 let mut e = editor_with(" hello world");
9366 run_keys(&mut e, "<lt>w");
9367 assert_eq!(e.buffer().lines()[0], " hello world");
9369 }
9370
9371 #[test]
9372 fn shift_gt_text_object_indents_paragraph() {
9373 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9374 e.jump_cursor(0, 0);
9375 run_keys(&mut e, ">ip");
9376 assert_eq!(e.buffer().lines()[0], " alpha");
9377 assert_eq!(e.buffer().lines()[1], " beta");
9378 assert_eq!(e.buffer().lines()[2], " gamma");
9379 assert_eq!(e.buffer().lines()[4], "rest");
9381 }
9382
9383 #[test]
9384 fn ctrl_o_runs_exactly_one_normal_command() {
9385 let mut e = editor_with("alpha beta gamma");
9388 e.jump_cursor(0, 0);
9389 run_keys(&mut e, "i");
9390 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9391 run_keys(&mut e, "dw");
9392 assert_eq!(e.vim_mode(), VimMode::Insert);
9394 run_keys(&mut e, "X");
9396 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9397 }
9398
9399 #[test]
9400 fn macro_replay_respects_mode_switching() {
9401 let mut e = editor_with("hi");
9405 run_keys(&mut e, "qaiX<Esc>0q");
9406 assert_eq!(e.vim_mode(), VimMode::Normal);
9407 e.set_content("yo");
9409 run_keys(&mut e, "@a");
9410 assert_eq!(e.vim_mode(), VimMode::Normal);
9411 assert_eq!(e.cursor().1, 0);
9412 assert_eq!(e.buffer().lines()[0], "Xyo");
9413 }
9414
9415 #[test]
9416 fn macro_recorded_text_round_trips_through_register() {
9417 let mut e = editor_with("");
9421 run_keys(&mut e, "qaiX<Esc>q");
9422 let text = e.registers().read('a').unwrap().text.clone();
9423 assert!(text.starts_with("iX"));
9424 run_keys(&mut e, "@a");
9426 assert_eq!(e.buffer().lines()[0], "XX");
9427 }
9428
9429 #[test]
9430 fn dot_after_macro_replays_macros_last_change() {
9431 let mut e = editor_with("ab\ncd\nef");
9434 run_keys(&mut e, "qaIX<Esc>jq");
9437 assert_eq!(e.buffer().lines()[0], "Xab");
9438 run_keys(&mut e, "@a");
9439 assert_eq!(e.buffer().lines()[1], "Xcd");
9440 let row_before_dot = e.cursor().0;
9443 run_keys(&mut e, ".");
9444 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9445 }
9446
9447 fn si_editor(content: &str) -> Editor {
9453 let opts = crate::types::Options {
9454 shiftwidth: 4,
9455 softtabstop: 4,
9456 expandtab: true,
9457 smartindent: true,
9458 autoindent: true,
9459 ..crate::types::Options::default()
9460 };
9461 let mut e = Editor::new(
9462 hjkl_buffer::Buffer::new(),
9463 crate::types::DefaultHost::new(),
9464 opts,
9465 );
9466 e.set_content(content);
9467 e
9468 }
9469
9470 #[test]
9471 fn smartindent_bumps_indent_after_open_brace() {
9472 let mut e = si_editor("fn foo() {");
9474 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9476 assert_eq!(
9477 e.buffer().lines()[1],
9478 " ",
9479 "smartindent should bump one shiftwidth after {{"
9480 );
9481 }
9482
9483 #[test]
9484 fn smartindent_no_bump_when_off() {
9485 let mut e = si_editor("fn foo() {");
9488 e.settings_mut().smartindent = false;
9489 e.jump_cursor(0, 10);
9490 run_keys(&mut e, "i<CR>");
9491 assert_eq!(
9492 e.buffer().lines()[1],
9493 "",
9494 "without smartindent, no bump: new line copies empty leading ws"
9495 );
9496 }
9497
9498 #[test]
9499 fn smartindent_uses_tab_when_noexpandtab() {
9500 let opts = crate::types::Options {
9502 shiftwidth: 4,
9503 softtabstop: 0,
9504 expandtab: false,
9505 smartindent: true,
9506 autoindent: true,
9507 ..crate::types::Options::default()
9508 };
9509 let mut e = Editor::new(
9510 hjkl_buffer::Buffer::new(),
9511 crate::types::DefaultHost::new(),
9512 opts,
9513 );
9514 e.set_content("fn foo() {");
9515 e.jump_cursor(0, 10);
9516 run_keys(&mut e, "i<CR>");
9517 assert_eq!(
9518 e.buffer().lines()[1],
9519 "\t",
9520 "noexpandtab: smartindent bump inserts a literal tab"
9521 );
9522 }
9523
9524 #[test]
9525 fn smartindent_dedent_on_close_brace() {
9526 let mut e = si_editor("fn foo() {");
9529 e.set_content("fn foo() {\n ");
9531 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9533 assert_eq!(
9534 e.buffer().lines()[1],
9535 "}",
9536 "close brace on whitespace-only line should dedent"
9537 );
9538 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9539 }
9540
9541 #[test]
9542 fn smartindent_no_dedent_when_off() {
9543 let mut e = si_editor("fn foo() {\n ");
9545 e.settings_mut().smartindent = false;
9546 e.jump_cursor(1, 4);
9547 run_keys(&mut e, "i}");
9548 assert_eq!(
9549 e.buffer().lines()[1],
9550 " }",
9551 "without smartindent, `}}` just appends at cursor"
9552 );
9553 }
9554
9555 #[test]
9556 fn smartindent_no_dedent_mid_line() {
9557 let mut e = si_editor(" let x = 1");
9560 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9562 assert_eq!(
9563 e.buffer().lines()[0],
9564 " let x = 1}",
9565 "mid-line `}}` should not dedent"
9566 );
9567 }
9568}