1use crate::VimMode;
75use crate::input::{Input, Key};
76
77use crate::buf_helpers::{
78 buf_cursor_pos, buf_line, buf_line_bytes, buf_line_chars, buf_lines_to_vec, buf_row_count,
79 buf_set_cursor_pos, buf_set_cursor_rc,
80};
81use crate::editor::Editor;
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86pub enum Mode {
87 #[default]
88 Normal,
89 Insert,
90 Visual,
91 VisualLine,
92 VisualBlock,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Default)]
100enum Pending {
101 #[default]
102 None,
103 Op { op: Operator, count1: usize },
106 OpTextObj {
108 op: Operator,
109 count1: usize,
110 inner: bool,
111 },
112 OpG { op: Operator, count1: usize },
114 G,
116 Find { forward: bool, till: bool },
118 OpFind {
120 op: Operator,
121 count1: usize,
122 forward: bool,
123 till: bool,
124 },
125 Replace,
127 VisualTextObj { inner: bool },
130 Z,
132 SetMark,
134 GotoMarkLine,
137 GotoMarkChar,
140 SelectRegister,
143 RecordMacroTarget,
147 PlayMacroTarget { count: usize },
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum Operator {
157 Delete,
158 Change,
159 Yank,
160 Uppercase,
163 Lowercase,
165 ToggleCase,
169 Indent,
174 Outdent,
177 Fold,
181 Reflow,
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub enum Motion {
190 Left,
191 Right,
192 Up,
193 Down,
194 WordFwd,
195 BigWordFwd,
196 WordBack,
197 BigWordBack,
198 WordEnd,
199 BigWordEnd,
200 WordEndBack,
202 BigWordEndBack,
204 LineStart,
205 FirstNonBlank,
206 LineEnd,
207 FileTop,
208 FileBottom,
209 Find {
210 ch: char,
211 forward: bool,
212 till: bool,
213 },
214 FindRepeat {
215 reverse: bool,
216 },
217 MatchBracket,
218 WordAtCursor {
219 forward: bool,
220 whole_word: bool,
223 },
224 SearchNext {
226 reverse: bool,
227 },
228 ViewportTop,
230 ViewportMiddle,
232 ViewportBottom,
234 LastNonBlank,
236 LineMiddle,
239 ParagraphPrev,
241 ParagraphNext,
243 SentencePrev,
245 SentenceNext,
247 ScreenDown,
250 ScreenUp,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum TextObject {
256 Word {
257 big: bool,
258 },
259 Quote(char),
260 Bracket(char),
261 Paragraph,
262 XmlTag,
266 Sentence,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum MotionKind {
276 Exclusive,
278 Inclusive,
280 Linewise,
282}
283
284#[derive(Debug, Clone)]
288enum LastChange {
289 OpMotion {
291 op: Operator,
292 motion: Motion,
293 count: usize,
294 inserted: Option<String>,
295 },
296 OpTextObj {
298 op: Operator,
299 obj: TextObject,
300 inner: bool,
301 inserted: Option<String>,
302 },
303 LineOp {
305 op: Operator,
306 count: usize,
307 inserted: Option<String>,
308 },
309 CharDel { forward: bool, count: usize },
311 ReplaceChar { ch: char, count: usize },
313 ToggleCase { count: usize },
315 JoinLine { count: usize },
317 Paste { before: bool, count: usize },
319 DeleteToEol { inserted: Option<String> },
321 OpenLine { above: bool, inserted: String },
323 InsertAt {
325 entry: InsertEntry,
326 inserted: String,
327 count: usize,
328 },
329}
330
331#[derive(Debug, Clone, Copy, PartialEq, Eq)]
332enum InsertEntry {
333 I,
334 A,
335 ShiftI,
336 ShiftA,
337}
338
339#[derive(Default)]
342pub struct VimState {
343 mode: Mode,
344 pending: Pending,
345 count: usize,
346 last_find: Option<(char, bool, bool)>,
348 last_change: Option<LastChange>,
349 insert_session: Option<InsertSession>,
351 pub(super) visual_anchor: (usize, usize),
355 pub(super) visual_line_anchor: usize,
357 pub(super) block_anchor: (usize, usize),
360 pub(super) block_vcol: usize,
366 pub(super) yank_linewise: bool,
368 pub(super) pending_register: Option<char>,
371 pub(super) recording_macro: Option<char>,
375 pub(super) recording_keys: Vec<crate::input::Input>,
380 pub(super) replaying_macro: bool,
383 pub(super) last_macro: Option<char>,
385 pub(super) last_edit_pos: Option<(usize, usize)>,
388 pub(super) change_list: Vec<(usize, usize)>,
392 pub(super) change_list_cursor: Option<usize>,
395 pub(super) last_visual: Option<LastVisual>,
398 pub(super) viewport_pinned: bool,
402 replaying: bool,
404 one_shot_normal: bool,
407 pub(super) search_prompt: Option<SearchPrompt>,
409 pub(super) last_search: Option<String>,
413 pub(super) last_search_forward: bool,
417 pub(super) jump_back: Vec<(usize, usize)>,
422 pub(super) jump_fwd: Vec<(usize, usize)>,
425 pub(super) insert_pending_register: bool,
429 pub(super) search_history: Vec<String>,
433 pub(super) search_history_cursor: Option<usize>,
438 pub(super) last_input_at: Option<std::time::Instant>,
447 pub(super) last_input_host_at: Option<core::time::Duration>,
451}
452
453const SEARCH_HISTORY_MAX: usize = 100;
454pub(crate) const CHANGE_LIST_MAX: usize = 100;
455
456#[derive(Debug, Clone)]
459pub struct SearchPrompt {
460 pub text: String,
461 pub cursor: usize,
462 pub forward: bool,
463}
464
465#[derive(Debug, Clone)]
466struct InsertSession {
467 count: usize,
468 row_min: usize,
470 row_max: usize,
471 before_lines: Vec<String>,
475 reason: InsertReason,
476}
477
478#[derive(Debug, Clone)]
479enum InsertReason {
480 Enter(InsertEntry),
482 Open { above: bool },
484 AfterChange,
487 DeleteToEol,
489 ReplayOnly,
492 BlockEdge { top: usize, bot: usize, col: usize },
496 Replace,
500}
501
502#[derive(Debug, Clone, Copy)]
512pub(super) struct LastVisual {
513 pub mode: Mode,
514 pub anchor: (usize, usize),
515 pub cursor: (usize, usize),
516 pub block_vcol: usize,
517}
518
519impl VimState {
520 pub fn public_mode(&self) -> VimMode {
521 match self.mode {
522 Mode::Normal => VimMode::Normal,
523 Mode::Insert => VimMode::Insert,
524 Mode::Visual => VimMode::Visual,
525 Mode::VisualLine => VimMode::VisualLine,
526 Mode::VisualBlock => VimMode::VisualBlock,
527 }
528 }
529
530 pub fn force_normal(&mut self) {
531 self.mode = Mode::Normal;
532 self.pending = Pending::None;
533 self.count = 0;
534 self.insert_session = None;
535 }
536
537 pub(crate) fn clear_pending_prefix(&mut self) {
547 self.pending = Pending::None;
548 self.count = 0;
549 self.pending_register = None;
550 self.insert_pending_register = false;
551 }
552
553 pub fn is_visual(&self) -> bool {
554 matches!(
555 self.mode,
556 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
557 )
558 }
559
560 pub fn is_visual_char(&self) -> bool {
561 self.mode == Mode::Visual
562 }
563
564 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
565 self.visual_anchor = anchor;
566 self.mode = Mode::Visual;
567 }
568
569 pub(crate) fn pending_count_val(&self) -> Option<u32> {
572 if self.count == 0 {
573 None
574 } else {
575 Some(self.count as u32)
576 }
577 }
578
579 pub(crate) fn is_chord_pending(&self) -> bool {
582 !matches!(self.pending, Pending::None)
583 }
584
585 pub(crate) fn pending_op_char(&self) -> Option<char> {
589 let op = match &self.pending {
590 Pending::Op { op, .. }
591 | Pending::OpTextObj { op, .. }
592 | Pending::OpG { op, .. }
593 | Pending::OpFind { op, .. } => Some(*op),
594 _ => None,
595 };
596 op.map(|o| match o {
597 Operator::Delete => 'd',
598 Operator::Change => 'c',
599 Operator::Yank => 'y',
600 Operator::Uppercase => 'U',
601 Operator::Lowercase => 'u',
602 Operator::ToggleCase => '~',
603 Operator::Indent => '>',
604 Operator::Outdent => '<',
605 Operator::Fold => 'z',
606 Operator::Reflow => 'q',
607 })
608 }
609}
610
611fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
617 ed.vim.search_prompt = Some(SearchPrompt {
618 text: String::new(),
619 cursor: 0,
620 forward,
621 });
622 ed.vim.search_history_cursor = None;
623 ed.set_search_pattern(None);
627}
628
629fn push_search_pattern<H: crate::types::Host>(
634 ed: &mut Editor<hjkl_buffer::Buffer, H>,
635 pattern: &str,
636) {
637 let compiled = if pattern.is_empty() {
638 None
639 } else {
640 let case_insensitive = ed.settings().ignore_case
647 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
648 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
649 std::borrow::Cow::Owned(format!("(?i){pattern}"))
650 } else {
651 std::borrow::Cow::Borrowed(pattern)
652 };
653 regex::Regex::new(&effective).ok()
654 };
655 let wrap = ed.settings().wrapscan;
656 ed.set_search_pattern(compiled);
660 ed.search_state_mut().wrap_around = wrap;
661}
662
663fn step_search_prompt<H: crate::types::Host>(
664 ed: &mut Editor<hjkl_buffer::Buffer, H>,
665 input: Input,
666) -> bool {
667 let history_dir = match (input.key, input.ctrl) {
671 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
672 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
673 _ => None,
674 };
675 if let Some(dir) = history_dir {
676 walk_search_history(ed, dir);
677 return true;
678 }
679 match input.key {
680 Key::Esc => {
681 let text = ed
684 .vim
685 .search_prompt
686 .take()
687 .map(|p| p.text)
688 .unwrap_or_default();
689 if !text.is_empty() {
690 ed.vim.last_search = Some(text);
691 }
692 ed.vim.search_history_cursor = None;
693 }
694 Key::Enter => {
695 let prompt = ed.vim.search_prompt.take();
696 if let Some(p) = prompt {
697 let pattern = if p.text.is_empty() {
700 ed.vim.last_search.clone()
701 } else {
702 Some(p.text.clone())
703 };
704 if let Some(pattern) = pattern {
705 push_search_pattern(ed, &pattern);
706 let pre = ed.cursor();
707 if p.forward {
708 ed.search_advance_forward(true);
709 } else {
710 ed.search_advance_backward(true);
711 }
712 ed.push_buffer_cursor_to_textarea();
713 if ed.cursor() != pre {
714 push_jump(ed, pre);
715 }
716 record_search_history(ed, &pattern);
717 ed.vim.last_search = Some(pattern);
718 ed.vim.last_search_forward = p.forward;
719 }
720 }
721 ed.vim.search_history_cursor = None;
722 }
723 Key::Backspace => {
724 ed.vim.search_history_cursor = None;
725 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
726 if p.text.pop().is_some() {
727 p.cursor = p.text.chars().count();
728 Some(p.text.clone())
729 } else {
730 None
731 }
732 });
733 if let Some(text) = new_text {
734 push_search_pattern(ed, &text);
735 }
736 }
737 Key::Char(c) => {
738 ed.vim.search_history_cursor = None;
739 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
740 p.text.push(c);
741 p.cursor = p.text.chars().count();
742 p.text.clone()
743 });
744 if let Some(text) = new_text {
745 push_search_pattern(ed, &text);
746 }
747 }
748 _ => {}
749 }
750 true
751}
752
753fn walk_change_list<H: crate::types::Host>(
757 ed: &mut Editor<hjkl_buffer::Buffer, H>,
758 dir: isize,
759 count: usize,
760) {
761 if ed.vim.change_list.is_empty() {
762 return;
763 }
764 let len = ed.vim.change_list.len();
765 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
766 (None, -1) => len as isize - 1,
767 (None, 1) => return, (Some(i), -1) => i as isize - 1,
769 (Some(i), 1) => i as isize + 1,
770 _ => return,
771 };
772 for _ in 1..count {
773 let next = idx + dir;
774 if next < 0 || next >= len as isize {
775 break;
776 }
777 idx = next;
778 }
779 if idx < 0 || idx >= len as isize {
780 return;
781 }
782 let idx = idx as usize;
783 ed.vim.change_list_cursor = Some(idx);
784 let (row, col) = ed.vim.change_list[idx];
785 ed.jump_cursor(row, col);
786}
787
788fn record_search_history<H: crate::types::Host>(
792 ed: &mut Editor<hjkl_buffer::Buffer, H>,
793 pattern: &str,
794) {
795 if pattern.is_empty() {
796 return;
797 }
798 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
799 return;
800 }
801 ed.vim.search_history.push(pattern.to_string());
802 let len = ed.vim.search_history.len();
803 if len > SEARCH_HISTORY_MAX {
804 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
805 }
806}
807
808fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
814 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
815 return;
816 }
817 let len = ed.vim.search_history.len();
818 let next_idx = match (ed.vim.search_history_cursor, dir) {
819 (None, -1) => Some(len - 1),
820 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
822 (Some(i), 1) if i + 1 < len => Some(i + 1),
823 _ => None,
824 };
825 let Some(idx) = next_idx else {
826 return;
827 };
828 ed.vim.search_history_cursor = Some(idx);
829 let text = ed.vim.search_history[idx].clone();
830 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
831 prompt.cursor = text.chars().count();
832 prompt.text = text.clone();
833 }
834 push_search_pattern(ed, &text);
835}
836
837pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
838 ed.sync_buffer_content_from_textarea();
843 let now = std::time::Instant::now();
851 let host_now = ed.host.now();
852 let timed_out = match ed.vim.last_input_host_at {
853 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
854 None => false,
855 };
856 if timed_out {
857 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
858 || ed.vim.count != 0
859 || ed.vim.pending_register.is_some()
860 || ed.vim.insert_pending_register;
861 if chord_in_flight {
862 ed.vim.clear_pending_prefix();
863 }
864 }
865 ed.vim.last_input_at = Some(now);
866 ed.vim.last_input_host_at = Some(host_now);
867 if ed.vim.recording_macro.is_some()
872 && !ed.vim.replaying_macro
873 && matches!(ed.vim.pending, Pending::None)
874 && ed.vim.mode != Mode::Insert
875 && input.key == Key::Char('q')
876 && !input.ctrl
877 && !input.alt
878 {
879 let reg = ed.vim.recording_macro.take().unwrap();
880 let keys = std::mem::take(&mut ed.vim.recording_keys);
881 let text = crate::input::encode_macro(&keys);
882 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
883 return true;
884 }
885 if ed.vim.search_prompt.is_some() {
887 return step_search_prompt(ed, input);
888 }
889 let pending_was_macro_chord = matches!(
893 ed.vim.pending,
894 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
895 );
896 let was_insert = ed.vim.mode == Mode::Insert;
897 let pre_visual_snapshot = match ed.vim.mode {
900 Mode::Visual => Some(LastVisual {
901 mode: Mode::Visual,
902 anchor: ed.vim.visual_anchor,
903 cursor: ed.cursor(),
904 block_vcol: 0,
905 }),
906 Mode::VisualLine => Some(LastVisual {
907 mode: Mode::VisualLine,
908 anchor: (ed.vim.visual_line_anchor, 0),
909 cursor: ed.cursor(),
910 block_vcol: 0,
911 }),
912 Mode::VisualBlock => Some(LastVisual {
913 mode: Mode::VisualBlock,
914 anchor: ed.vim.block_anchor,
915 cursor: ed.cursor(),
916 block_vcol: ed.vim.block_vcol,
917 }),
918 _ => None,
919 };
920 let consumed = match ed.vim.mode {
921 Mode::Insert => step_insert(ed, input),
922 _ => step_normal(ed, input),
923 };
924 if let Some(snap) = pre_visual_snapshot
925 && !matches!(
926 ed.vim.mode,
927 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
928 )
929 {
930 ed.vim.last_visual = Some(snap);
931 }
932 if !was_insert
936 && ed.vim.one_shot_normal
937 && ed.vim.mode == Mode::Normal
938 && matches!(ed.vim.pending, Pending::None)
939 {
940 ed.vim.one_shot_normal = false;
941 ed.vim.mode = Mode::Insert;
942 }
943 ed.sync_buffer_content_from_textarea();
949 if !ed.vim.viewport_pinned {
953 ed.ensure_cursor_in_scrolloff();
954 }
955 ed.vim.viewport_pinned = false;
956 if ed.vim.recording_macro.is_some()
961 && !ed.vim.replaying_macro
962 && input.key != Key::Char('q')
963 && !pending_was_macro_chord
964 {
965 ed.vim.recording_keys.push(input);
966 }
967 consumed
968}
969
970fn step_insert<H: crate::types::Host>(
973 ed: &mut Editor<hjkl_buffer::Buffer, H>,
974 input: Input,
975) -> bool {
976 if ed.vim.insert_pending_register {
980 ed.vim.insert_pending_register = false;
981 if let Key::Char(c) = input.key
982 && !input.ctrl
983 {
984 insert_register_text(ed, c);
985 }
986 return true;
987 }
988
989 if input.key == Key::Esc {
990 finish_insert_session(ed);
991 ed.vim.mode = Mode::Normal;
992 let col = ed.cursor().1;
997 if col > 0 {
998 crate::motions::move_left(&mut ed.buffer, 1);
999 ed.push_buffer_cursor_to_textarea();
1000 }
1001 ed.sticky_col = Some(ed.cursor().1);
1002 return true;
1003 }
1004
1005 if input.ctrl {
1007 match input.key {
1008 Key::Char('w') => {
1009 use hjkl_buffer::{Edit, MotionKind};
1010 ed.sync_buffer_content_from_textarea();
1011 let cursor = buf_cursor_pos(&ed.buffer);
1012 if cursor.row == 0 && cursor.col == 0 {
1013 return true;
1014 }
1015 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1018 let word_start = buf_cursor_pos(&ed.buffer);
1019 if word_start == cursor {
1020 return true;
1021 }
1022 buf_set_cursor_pos(&mut ed.buffer, cursor);
1023 ed.mutate_edit(Edit::DeleteRange {
1024 start: word_start,
1025 end: cursor,
1026 kind: MotionKind::Char,
1027 });
1028 ed.push_buffer_cursor_to_textarea();
1029 return true;
1030 }
1031 Key::Char('u') => {
1032 use hjkl_buffer::{Edit, MotionKind, Position};
1033 ed.sync_buffer_content_from_textarea();
1034 let cursor = buf_cursor_pos(&ed.buffer);
1035 if cursor.col > 0 {
1036 ed.mutate_edit(Edit::DeleteRange {
1037 start: Position::new(cursor.row, 0),
1038 end: cursor,
1039 kind: MotionKind::Char,
1040 });
1041 ed.push_buffer_cursor_to_textarea();
1042 }
1043 return true;
1044 }
1045 Key::Char('h') => {
1046 use hjkl_buffer::{Edit, MotionKind, Position};
1047 ed.sync_buffer_content_from_textarea();
1048 let cursor = buf_cursor_pos(&ed.buffer);
1049 if cursor.col > 0 {
1050 ed.mutate_edit(Edit::DeleteRange {
1051 start: Position::new(cursor.row, cursor.col - 1),
1052 end: cursor,
1053 kind: MotionKind::Char,
1054 });
1055 } else if cursor.row > 0 {
1056 let prev_row = cursor.row - 1;
1057 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1058 ed.mutate_edit(Edit::JoinLines {
1059 row: prev_row,
1060 count: 1,
1061 with_space: false,
1062 });
1063 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1064 }
1065 ed.push_buffer_cursor_to_textarea();
1066 return true;
1067 }
1068 Key::Char('o') => {
1069 ed.vim.one_shot_normal = true;
1072 ed.vim.mode = Mode::Normal;
1073 return true;
1074 }
1075 Key::Char('r') => {
1076 ed.vim.insert_pending_register = true;
1079 return true;
1080 }
1081 Key::Char('t') => {
1082 let (row, col) = ed.cursor();
1087 let sw = ed.settings().shiftwidth;
1088 indent_rows(ed, row, row, 1);
1089 ed.jump_cursor(row, col + sw);
1090 return true;
1091 }
1092 Key::Char('d') => {
1093 let (row, col) = ed.cursor();
1097 let before_len = buf_line_bytes(&ed.buffer, row);
1098 outdent_rows(ed, row, row, 1);
1099 let after_len = buf_line_bytes(&ed.buffer, row);
1100 let stripped = before_len.saturating_sub(after_len);
1101 let new_col = col.saturating_sub(stripped);
1102 ed.jump_cursor(row, new_col);
1103 return true;
1104 }
1105 _ => {}
1106 }
1107 }
1108
1109 let (row, _) = ed.cursor();
1112 if let Some(ref mut session) = ed.vim.insert_session {
1113 session.row_min = session.row_min.min(row);
1114 session.row_max = session.row_max.max(row);
1115 }
1116 let mutated = handle_insert_key(ed, input);
1117 if mutated {
1118 ed.mark_content_dirty();
1119 let (row, _) = ed.cursor();
1120 if let Some(ref mut session) = ed.vim.insert_session {
1121 session.row_min = session.row_min.min(row);
1122 session.row_max = session.row_max.max(row);
1123 }
1124 }
1125 true
1126}
1127
1128fn insert_register_text<H: crate::types::Host>(
1133 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1134 selector: char,
1135) {
1136 use hjkl_buffer::Edit;
1137 let text = match ed.registers().read(selector) {
1138 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1139 _ => return,
1140 };
1141 ed.sync_buffer_content_from_textarea();
1142 let cursor = buf_cursor_pos(&ed.buffer);
1143 ed.mutate_edit(Edit::InsertStr {
1144 at: cursor,
1145 text: text.clone(),
1146 });
1147 let mut row = cursor.row;
1150 let mut col = cursor.col;
1151 for ch in text.chars() {
1152 if ch == '\n' {
1153 row += 1;
1154 col = 0;
1155 } else {
1156 col += 1;
1157 }
1158 }
1159 buf_set_cursor_rc(&mut ed.buffer, row, col);
1160 ed.push_buffer_cursor_to_textarea();
1161 ed.mark_content_dirty();
1162 if let Some(ref mut session) = ed.vim.insert_session {
1163 session.row_min = session.row_min.min(row);
1164 session.row_max = session.row_max.max(row);
1165 }
1166}
1167
1168pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1187 if !settings.autoindent {
1188 return String::new();
1189 }
1190 let base: String = prev_line
1192 .chars()
1193 .take_while(|c| *c == ' ' || *c == '\t')
1194 .collect();
1195
1196 if settings.smartindent {
1197 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1201 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1202 let unit = if settings.expandtab {
1203 if settings.softtabstop > 0 {
1204 " ".repeat(settings.softtabstop)
1205 } else {
1206 " ".repeat(settings.shiftwidth)
1207 }
1208 } else {
1209 "\t".to_string()
1210 };
1211 return format!("{base}{unit}");
1212 }
1213 }
1214
1215 base
1216}
1217
1218fn try_dedent_close_bracket<H: crate::types::Host>(
1228 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1229 cursor: hjkl_buffer::Position,
1230 ch: char,
1231) -> bool {
1232 use hjkl_buffer::{Edit, MotionKind, Position};
1233
1234 if !ed.settings.smartindent {
1235 return false;
1236 }
1237 if !matches!(ch, '}' | ')' | ']') {
1238 return false;
1239 }
1240
1241 let line = match buf_line(&ed.buffer, cursor.row) {
1242 Some(l) => l.to_string(),
1243 None => return false,
1244 };
1245
1246 let before: String = line.chars().take(cursor.col).collect();
1248 if !before.chars().all(|c| c == ' ' || c == '\t') {
1249 return false;
1250 }
1251 if before.is_empty() {
1252 return false;
1254 }
1255
1256 let unit_len: usize = if ed.settings.expandtab {
1258 if ed.settings.softtabstop > 0 {
1259 ed.settings.softtabstop
1260 } else {
1261 ed.settings.shiftwidth
1262 }
1263 } else {
1264 1
1266 };
1267
1268 let strip_len = if ed.settings.expandtab {
1270 let spaces = before.chars().filter(|c| *c == ' ').count();
1272 if spaces < unit_len {
1273 return false;
1274 }
1275 unit_len
1276 } else {
1277 if !before.starts_with('\t') {
1279 return false;
1280 }
1281 1
1282 };
1283
1284 ed.mutate_edit(Edit::DeleteRange {
1286 start: Position::new(cursor.row, 0),
1287 end: Position::new(cursor.row, strip_len),
1288 kind: MotionKind::Char,
1289 });
1290 let new_col = cursor.col.saturating_sub(strip_len);
1295 ed.mutate_edit(Edit::InsertChar {
1296 at: Position::new(cursor.row, new_col),
1297 ch,
1298 });
1299 true
1300}
1301
1302fn handle_insert_key<H: crate::types::Host>(
1309 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1310 input: Input,
1311) -> bool {
1312 use hjkl_buffer::{Edit, MotionKind, Position};
1313 ed.sync_buffer_content_from_textarea();
1314 let cursor = buf_cursor_pos(&ed.buffer);
1315 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1316 let in_replace = matches!(
1320 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1321 Some(InsertReason::Replace)
1322 );
1323 let mutated = match input.key {
1324 Key::Char(c) if in_replace && cursor.col < line_chars => {
1325 ed.mutate_edit(Edit::DeleteRange {
1326 start: cursor,
1327 end: Position::new(cursor.row, cursor.col + 1),
1328 kind: MotionKind::Char,
1329 });
1330 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1331 true
1332 }
1333 Key::Char(c) => {
1334 if !try_dedent_close_bracket(ed, cursor, c) {
1335 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1336 }
1337 true
1338 }
1339 Key::Enter => {
1340 let prev_line = buf_line(&ed.buffer, cursor.row)
1341 .unwrap_or_default()
1342 .to_string();
1343 let indent = compute_enter_indent(&ed.settings, &prev_line);
1344 let text = format!("\n{indent}");
1345 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1346 true
1347 }
1348 Key::Tab => {
1349 if ed.settings.expandtab {
1350 let sts = ed.settings.softtabstop;
1353 let n = if sts > 0 {
1354 sts - (cursor.col % sts)
1355 } else {
1356 ed.settings.tabstop.max(1)
1357 };
1358 ed.mutate_edit(Edit::InsertStr {
1359 at: cursor,
1360 text: " ".repeat(n),
1361 });
1362 } else {
1363 ed.mutate_edit(Edit::InsertChar {
1364 at: cursor,
1365 ch: '\t',
1366 });
1367 }
1368 true
1369 }
1370 Key::Backspace => {
1371 let sts = ed.settings.softtabstop;
1375 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1376 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1377 let chars: Vec<char> = line.chars().collect();
1378 let run_start = cursor.col - sts;
1379 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1380 ed.mutate_edit(Edit::DeleteRange {
1381 start: Position::new(cursor.row, run_start),
1382 end: cursor,
1383 kind: MotionKind::Char,
1384 });
1385 return true;
1386 }
1387 }
1388 if cursor.col > 0 {
1389 ed.mutate_edit(Edit::DeleteRange {
1390 start: Position::new(cursor.row, cursor.col - 1),
1391 end: cursor,
1392 kind: MotionKind::Char,
1393 });
1394 true
1395 } else if cursor.row > 0 {
1396 let prev_row = cursor.row - 1;
1397 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1398 ed.mutate_edit(Edit::JoinLines {
1399 row: prev_row,
1400 count: 1,
1401 with_space: false,
1402 });
1403 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1404 true
1405 } else {
1406 false
1407 }
1408 }
1409 Key::Delete => {
1410 if cursor.col < line_chars {
1411 ed.mutate_edit(Edit::DeleteRange {
1412 start: cursor,
1413 end: Position::new(cursor.row, cursor.col + 1),
1414 kind: MotionKind::Char,
1415 });
1416 true
1417 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1418 ed.mutate_edit(Edit::JoinLines {
1419 row: cursor.row,
1420 count: 1,
1421 with_space: false,
1422 });
1423 buf_set_cursor_pos(&mut ed.buffer, cursor);
1424 true
1425 } else {
1426 false
1427 }
1428 }
1429 Key::Left => {
1430 crate::motions::move_left(&mut ed.buffer, 1);
1431 break_undo_group_in_insert(ed);
1432 false
1433 }
1434 Key::Right => {
1435 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1438 break_undo_group_in_insert(ed);
1439 false
1440 }
1441 Key::Up => {
1442 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1443 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1444 break_undo_group_in_insert(ed);
1445 false
1446 }
1447 Key::Down => {
1448 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1449 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1450 break_undo_group_in_insert(ed);
1451 false
1452 }
1453 Key::Home => {
1454 crate::motions::move_line_start(&mut ed.buffer);
1455 break_undo_group_in_insert(ed);
1456 false
1457 }
1458 Key::End => {
1459 crate::motions::move_line_end(&mut ed.buffer);
1460 break_undo_group_in_insert(ed);
1461 false
1462 }
1463 Key::PageUp => {
1464 let rows = viewport_full_rows(ed, 1) as isize;
1468 scroll_cursor_rows(ed, -rows);
1469 return false;
1470 }
1471 Key::PageDown => {
1472 let rows = viewport_full_rows(ed, 1) as isize;
1473 scroll_cursor_rows(ed, rows);
1474 return false;
1475 }
1476 _ => false,
1479 };
1480 ed.push_buffer_cursor_to_textarea();
1481 mutated
1482}
1483
1484fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1485 let Some(session) = ed.vim.insert_session.take() else {
1486 return;
1487 };
1488 let lines = buf_lines_to_vec(&ed.buffer);
1489 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1493 let before_end = session
1494 .row_max
1495 .min(session.before_lines.len().saturating_sub(1));
1496 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1497 session.before_lines[session.row_min..=before_end].join("\n")
1498 } else {
1499 String::new()
1500 };
1501 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1502 lines[session.row_min..=after_end].join("\n")
1503 } else {
1504 String::new()
1505 };
1506 let inserted = extract_inserted(&before, &after);
1507 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1508 use hjkl_buffer::{Edit, Position};
1509 for _ in 0..session.count - 1 {
1510 let (row, col) = ed.cursor();
1511 ed.mutate_edit(Edit::InsertStr {
1512 at: Position::new(row, col),
1513 text: inserted.clone(),
1514 });
1515 }
1516 }
1517 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1518 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1519 use hjkl_buffer::{Edit, Position};
1520 for r in (top + 1)..=bot {
1521 let line_len = buf_line_chars(&ed.buffer, r);
1522 if col > line_len {
1523 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1526 ed.mutate_edit(Edit::InsertStr {
1527 at: Position::new(r, line_len),
1528 text: pad,
1529 });
1530 }
1531 ed.mutate_edit(Edit::InsertStr {
1532 at: Position::new(r, col),
1533 text: inserted.clone(),
1534 });
1535 }
1536 buf_set_cursor_rc(&mut ed.buffer, top, col);
1537 ed.push_buffer_cursor_to_textarea();
1538 }
1539 return;
1540 }
1541 if ed.vim.replaying {
1542 return;
1543 }
1544 match session.reason {
1545 InsertReason::Enter(entry) => {
1546 ed.vim.last_change = Some(LastChange::InsertAt {
1547 entry,
1548 inserted,
1549 count: session.count,
1550 });
1551 }
1552 InsertReason::Open { above } => {
1553 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1554 }
1555 InsertReason::AfterChange => {
1556 if let Some(
1557 LastChange::OpMotion { inserted: ins, .. }
1558 | LastChange::OpTextObj { inserted: ins, .. }
1559 | LastChange::LineOp { inserted: ins, .. },
1560 ) = ed.vim.last_change.as_mut()
1561 {
1562 *ins = Some(inserted);
1563 }
1564 }
1565 InsertReason::DeleteToEol => {
1566 ed.vim.last_change = Some(LastChange::DeleteToEol {
1567 inserted: Some(inserted),
1568 });
1569 }
1570 InsertReason::ReplayOnly => {}
1571 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1572 InsertReason::Replace => {
1573 ed.vim.last_change = Some(LastChange::DeleteToEol {
1578 inserted: Some(inserted),
1579 });
1580 }
1581 }
1582}
1583
1584fn begin_insert<H: crate::types::Host>(
1585 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1586 count: usize,
1587 reason: InsertReason,
1588) {
1589 let record = !matches!(reason, InsertReason::ReplayOnly);
1590 if record {
1591 ed.push_undo();
1592 }
1593 let reason = if ed.vim.replaying {
1594 InsertReason::ReplayOnly
1595 } else {
1596 reason
1597 };
1598 let (row, _) = ed.cursor();
1599 ed.vim.insert_session = Some(InsertSession {
1600 count,
1601 row_min: row,
1602 row_max: row,
1603 before_lines: buf_lines_to_vec(&ed.buffer),
1604 reason,
1605 });
1606 ed.vim.mode = Mode::Insert;
1607}
1608
1609pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1624 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1625) {
1626 if !ed.settings.undo_break_on_motion {
1627 return;
1628 }
1629 if ed.vim.replaying {
1630 return;
1631 }
1632 if ed.vim.insert_session.is_none() {
1633 return;
1634 }
1635 ed.push_undo();
1636 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1637 let mut lines: Vec<String> = Vec::with_capacity(n);
1638 for r in 0..n {
1639 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1640 }
1641 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1642 if let Some(ref mut session) = ed.vim.insert_session {
1643 session.before_lines = lines;
1644 session.row_min = row;
1645 session.row_max = row;
1646 }
1647}
1648
1649fn step_normal<H: crate::types::Host>(
1652 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1653 input: Input,
1654) -> bool {
1655 if let Key::Char(d @ '0'..='9') = input.key
1657 && !input.ctrl
1658 && !input.alt
1659 && !matches!(
1660 ed.vim.pending,
1661 Pending::Replace
1662 | Pending::Find { .. }
1663 | Pending::OpFind { .. }
1664 | Pending::VisualTextObj { .. }
1665 )
1666 && (d != '0' || ed.vim.count > 0)
1667 {
1668 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1669 return true;
1670 }
1671
1672 match std::mem::take(&mut ed.vim.pending) {
1674 Pending::Replace => return handle_replace(ed, input),
1675 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1676 Pending::OpFind {
1677 op,
1678 count1,
1679 forward,
1680 till,
1681 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1682 Pending::G => return handle_after_g(ed, input),
1683 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1684 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1685 Pending::OpTextObj { op, count1, inner } => {
1686 return handle_text_object(ed, input, op, count1, inner);
1687 }
1688 Pending::VisualTextObj { inner } => {
1689 return handle_visual_text_obj(ed, input, inner);
1690 }
1691 Pending::Z => return handle_after_z(ed, input),
1692 Pending::SetMark => return handle_set_mark(ed, input),
1693 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1694 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1695 Pending::SelectRegister => return handle_select_register(ed, input),
1696 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1697 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1698 Pending::None => {}
1699 }
1700
1701 let count = take_count(&mut ed.vim);
1702
1703 match input.key {
1705 Key::Esc => {
1706 ed.vim.force_normal();
1707 return true;
1708 }
1709 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1710 ed.vim.visual_anchor = ed.cursor();
1711 ed.vim.mode = Mode::Visual;
1712 return true;
1713 }
1714 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1715 let (row, _) = ed.cursor();
1716 ed.vim.visual_line_anchor = row;
1717 ed.vim.mode = Mode::VisualLine;
1718 return true;
1719 }
1720 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1721 ed.vim.visual_anchor = ed.cursor();
1722 ed.vim.mode = Mode::Visual;
1723 return true;
1724 }
1725 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1726 let (row, _) = ed.cursor();
1727 ed.vim.visual_line_anchor = row;
1728 ed.vim.mode = Mode::VisualLine;
1729 return true;
1730 }
1731 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1732 let cur = ed.cursor();
1733 ed.vim.block_anchor = cur;
1734 ed.vim.block_vcol = cur.1;
1735 ed.vim.mode = Mode::VisualBlock;
1736 return true;
1737 }
1738 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1739 ed.vim.mode = Mode::Normal;
1741 return true;
1742 }
1743 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1746 Mode::Visual => {
1747 let cur = ed.cursor();
1748 let anchor = ed.vim.visual_anchor;
1749 ed.vim.visual_anchor = cur;
1750 ed.jump_cursor(anchor.0, anchor.1);
1751 return true;
1752 }
1753 Mode::VisualLine => {
1754 let cur_row = ed.cursor().0;
1755 let anchor_row = ed.vim.visual_line_anchor;
1756 ed.vim.visual_line_anchor = cur_row;
1757 ed.jump_cursor(anchor_row, 0);
1758 return true;
1759 }
1760 Mode::VisualBlock => {
1761 let cur = ed.cursor();
1762 let anchor = ed.vim.block_anchor;
1763 ed.vim.block_anchor = cur;
1764 ed.vim.block_vcol = anchor.1;
1765 ed.jump_cursor(anchor.0, anchor.1);
1766 return true;
1767 }
1768 _ => {}
1769 },
1770 _ => {}
1771 }
1772
1773 if ed.vim.is_visual()
1775 && let Some(op) = visual_operator(&input)
1776 {
1777 apply_visual_operator(ed, op);
1778 return true;
1779 }
1780
1781 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1785 match input.key {
1786 Key::Char('r') => {
1787 ed.vim.pending = Pending::Replace;
1788 return true;
1789 }
1790 Key::Char('I') => {
1791 let (top, bot, left, _right) = block_bounds(ed);
1792 ed.jump_cursor(top, left);
1793 ed.vim.mode = Mode::Normal;
1794 begin_insert(
1795 ed,
1796 1,
1797 InsertReason::BlockEdge {
1798 top,
1799 bot,
1800 col: left,
1801 },
1802 );
1803 return true;
1804 }
1805 Key::Char('A') => {
1806 let (top, bot, _left, right) = block_bounds(ed);
1807 let line_len = buf_line_chars(&ed.buffer, top);
1808 let col = (right + 1).min(line_len);
1809 ed.jump_cursor(top, col);
1810 ed.vim.mode = Mode::Normal;
1811 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1812 return true;
1813 }
1814 _ => {}
1815 }
1816 }
1817
1818 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1820 && !input.ctrl
1821 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1822 {
1823 let inner = matches!(input.key, Key::Char('i'));
1824 ed.vim.pending = Pending::VisualTextObj { inner };
1825 return true;
1826 }
1827
1828 if input.ctrl
1833 && let Key::Char(c) = input.key
1834 {
1835 match c {
1836 'd' => {
1837 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1838 return true;
1839 }
1840 'u' => {
1841 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1842 return true;
1843 }
1844 'f' => {
1845 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1846 return true;
1847 }
1848 'b' => {
1849 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1850 return true;
1851 }
1852 'r' => {
1853 do_redo(ed);
1854 return true;
1855 }
1856 'a' if ed.vim.mode == Mode::Normal => {
1857 adjust_number(ed, count.max(1) as i64);
1858 return true;
1859 }
1860 'x' if ed.vim.mode == Mode::Normal => {
1861 adjust_number(ed, -(count.max(1) as i64));
1862 return true;
1863 }
1864 'o' if ed.vim.mode == Mode::Normal => {
1865 for _ in 0..count.max(1) {
1866 jump_back(ed);
1867 }
1868 return true;
1869 }
1870 'i' if ed.vim.mode == Mode::Normal => {
1871 for _ in 0..count.max(1) {
1872 jump_forward(ed);
1873 }
1874 return true;
1875 }
1876 _ => {}
1877 }
1878 }
1879
1880 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1882 for _ in 0..count.max(1) {
1883 jump_forward(ed);
1884 }
1885 return true;
1886 }
1887
1888 if let Some(motion) = parse_motion(&input) {
1890 execute_motion(ed, motion.clone(), count);
1891 if ed.vim.mode == Mode::VisualBlock {
1893 update_block_vcol(ed, &motion);
1894 }
1895 if let Motion::Find { ch, forward, till } = motion {
1896 ed.vim.last_find = Some((ch, forward, till));
1897 }
1898 return true;
1899 }
1900
1901 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
1903 return true;
1904 }
1905
1906 if ed.vim.mode == Mode::Normal
1908 && let Key::Char(op_ch) = input.key
1909 && !input.ctrl
1910 && let Some(op) = char_to_operator(op_ch)
1911 {
1912 ed.vim.pending = Pending::Op { op, count1: count };
1913 return true;
1914 }
1915
1916 if ed.vim.mode == Mode::Normal
1918 && let Some((forward, till)) = find_entry(&input)
1919 {
1920 ed.vim.count = count;
1921 ed.vim.pending = Pending::Find { forward, till };
1922 return true;
1923 }
1924
1925 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
1927 ed.vim.count = count;
1928 ed.vim.pending = Pending::G;
1929 return true;
1930 }
1931
1932 if !input.ctrl
1934 && input.key == Key::Char('z')
1935 && matches!(
1936 ed.vim.mode,
1937 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
1938 )
1939 {
1940 ed.vim.pending = Pending::Z;
1941 return true;
1942 }
1943
1944 if !input.ctrl && ed.vim.mode == Mode::Normal {
1948 match input.key {
1949 Key::Char('m') => {
1950 ed.vim.pending = Pending::SetMark;
1951 return true;
1952 }
1953 Key::Char('\'') => {
1954 ed.vim.pending = Pending::GotoMarkLine;
1955 return true;
1956 }
1957 Key::Char('`') => {
1958 ed.vim.pending = Pending::GotoMarkChar;
1959 return true;
1960 }
1961 Key::Char('"') => {
1962 ed.vim.pending = Pending::SelectRegister;
1965 return true;
1966 }
1967 Key::Char('@') => {
1968 ed.vim.pending = Pending::PlayMacroTarget { count };
1972 return true;
1973 }
1974 Key::Char('q') if ed.vim.recording_macro.is_none() => {
1975 ed.vim.pending = Pending::RecordMacroTarget;
1980 return true;
1981 }
1982 _ => {}
1983 }
1984 }
1985
1986 true
1988}
1989
1990fn handle_set_mark<H: crate::types::Host>(
1991 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1992 input: Input,
1993) -> bool {
1994 if let Key::Char(c) = input.key
1995 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
1996 {
1997 let pos = ed.cursor();
2002 ed.set_mark(c, pos);
2003 }
2004 true
2005}
2006
2007fn handle_select_register<H: crate::types::Host>(
2011 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2012 input: Input,
2013) -> bool {
2014 if let Key::Char(c) = input.key
2015 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*'))
2016 {
2017 ed.vim.pending_register = Some(c);
2018 }
2019 true
2020}
2021
2022fn handle_record_macro_target<H: crate::types::Host>(
2027 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2028 input: Input,
2029) -> bool {
2030 if let Key::Char(c) = input.key
2031 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2032 {
2033 ed.vim.recording_macro = Some(c);
2034 if c.is_ascii_uppercase() {
2037 let lower = c.to_ascii_lowercase();
2038 let text = ed
2042 .registers()
2043 .read(lower)
2044 .map(|s| s.text.clone())
2045 .unwrap_or_default();
2046 ed.vim.recording_keys = crate::input::decode_macro(&text);
2047 } else {
2048 ed.vim.recording_keys.clear();
2049 }
2050 }
2051 true
2052}
2053
2054fn handle_play_macro_target<H: crate::types::Host>(
2060 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2061 input: Input,
2062 count: usize,
2063) -> bool {
2064 let reg = match input.key {
2065 Key::Char('@') => ed.vim.last_macro,
2066 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2067 Some(c.to_ascii_lowercase())
2068 }
2069 _ => None,
2070 };
2071 let Some(reg) = reg else {
2072 return true;
2073 };
2074 let text = match ed.registers().read(reg) {
2077 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2078 _ => return true,
2079 };
2080 let keys = crate::input::decode_macro(&text);
2081 ed.vim.last_macro = Some(reg);
2082 let times = count.max(1);
2083 let was_replaying = ed.vim.replaying_macro;
2084 ed.vim.replaying_macro = true;
2085 for _ in 0..times {
2086 for k in keys.iter().copied() {
2087 step(ed, k);
2088 }
2089 }
2090 ed.vim.replaying_macro = was_replaying;
2091 true
2092}
2093
2094fn handle_goto_mark<H: crate::types::Host>(
2095 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2096 input: Input,
2097 linewise: bool,
2098) -> bool {
2099 let Key::Char(c) = input.key else {
2100 return true;
2101 };
2102 let target = match c {
2109 'a'..='z' | 'A'..='Z' => ed.mark(c),
2110 '\'' | '`' => ed.vim.jump_back.last().copied(),
2111 '.' => ed.vim.last_edit_pos,
2112 _ => None,
2113 };
2114 let Some((row, col)) = target else {
2115 return true;
2116 };
2117 let pre = ed.cursor();
2118 let (r, c_clamped) = clamp_pos(ed, (row, col));
2119 if linewise {
2120 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2121 ed.push_buffer_cursor_to_textarea();
2122 move_first_non_whitespace(ed);
2123 } else {
2124 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2125 ed.push_buffer_cursor_to_textarea();
2126 }
2127 if ed.cursor() != pre {
2128 push_jump(ed, pre);
2129 }
2130 ed.sticky_col = Some(ed.cursor().1);
2131 true
2132}
2133
2134fn take_count(vim: &mut VimState) -> usize {
2135 if vim.count > 0 {
2136 let n = vim.count;
2137 vim.count = 0;
2138 n
2139 } else {
2140 1
2141 }
2142}
2143
2144fn char_to_operator(c: char) -> Option<Operator> {
2145 match c {
2146 'd' => Some(Operator::Delete),
2147 'c' => Some(Operator::Change),
2148 'y' => Some(Operator::Yank),
2149 '>' => Some(Operator::Indent),
2150 '<' => Some(Operator::Outdent),
2151 _ => None,
2152 }
2153}
2154
2155fn visual_operator(input: &Input) -> Option<Operator> {
2156 if input.ctrl {
2157 return None;
2158 }
2159 match input.key {
2160 Key::Char('y') => Some(Operator::Yank),
2161 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2162 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2163 Key::Char('U') => Some(Operator::Uppercase),
2165 Key::Char('u') => Some(Operator::Lowercase),
2166 Key::Char('~') => Some(Operator::ToggleCase),
2167 Key::Char('>') => Some(Operator::Indent),
2169 Key::Char('<') => Some(Operator::Outdent),
2170 _ => None,
2171 }
2172}
2173
2174fn find_entry(input: &Input) -> Option<(bool, bool)> {
2175 if input.ctrl {
2176 return None;
2177 }
2178 match input.key {
2179 Key::Char('f') => Some((true, false)),
2180 Key::Char('F') => Some((false, false)),
2181 Key::Char('t') => Some((true, true)),
2182 Key::Char('T') => Some((false, true)),
2183 _ => None,
2184 }
2185}
2186
2187const JUMPLIST_MAX: usize = 100;
2191
2192fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2197 ed.vim.jump_back.push(from);
2198 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2199 ed.vim.jump_back.remove(0);
2200 }
2201 ed.vim.jump_fwd.clear();
2202}
2203
2204fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2207 let Some(target) = ed.vim.jump_back.pop() else {
2208 return;
2209 };
2210 let cur = ed.cursor();
2211 ed.vim.jump_fwd.push(cur);
2212 let (r, c) = clamp_pos(ed, target);
2213 ed.jump_cursor(r, c);
2214 ed.sticky_col = Some(c);
2215}
2216
2217fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2220 let Some(target) = ed.vim.jump_fwd.pop() else {
2221 return;
2222 };
2223 let cur = ed.cursor();
2224 ed.vim.jump_back.push(cur);
2225 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2226 ed.vim.jump_back.remove(0);
2227 }
2228 let (r, c) = clamp_pos(ed, target);
2229 ed.jump_cursor(r, c);
2230 ed.sticky_col = Some(c);
2231}
2232
2233fn clamp_pos<H: crate::types::Host>(
2236 ed: &Editor<hjkl_buffer::Buffer, H>,
2237 pos: (usize, usize),
2238) -> (usize, usize) {
2239 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2240 let r = pos.0.min(last_row);
2241 let line_len = buf_line_chars(&ed.buffer, r);
2242 let c = pos.1.min(line_len.saturating_sub(1));
2243 (r, c)
2244}
2245
2246fn is_big_jump(motion: &Motion) -> bool {
2248 matches!(
2249 motion,
2250 Motion::FileTop
2251 | Motion::FileBottom
2252 | Motion::MatchBracket
2253 | Motion::WordAtCursor { .. }
2254 | Motion::SearchNext { .. }
2255 | Motion::ViewportTop
2256 | Motion::ViewportMiddle
2257 | Motion::ViewportBottom
2258 )
2259}
2260
2261fn viewport_half_rows<H: crate::types::Host>(
2266 ed: &Editor<hjkl_buffer::Buffer, H>,
2267 count: usize,
2268) -> usize {
2269 let h = ed.viewport_height_value() as usize;
2270 (h / 2).max(1).saturating_mul(count.max(1))
2271}
2272
2273fn viewport_full_rows<H: crate::types::Host>(
2276 ed: &Editor<hjkl_buffer::Buffer, H>,
2277 count: usize,
2278) -> usize {
2279 let h = ed.viewport_height_value() as usize;
2280 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2281}
2282
2283fn scroll_cursor_rows<H: crate::types::Host>(
2288 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2289 delta: isize,
2290) {
2291 if delta == 0 {
2292 return;
2293 }
2294 ed.sync_buffer_content_from_textarea();
2295 let (row, _) = ed.cursor();
2296 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2297 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2298 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2299 crate::motions::move_first_non_blank(&mut ed.buffer);
2300 ed.push_buffer_cursor_to_textarea();
2301 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2302}
2303
2304fn parse_motion(input: &Input) -> Option<Motion> {
2307 if input.ctrl {
2308 return None;
2309 }
2310 match input.key {
2311 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2312 Key::Char('l') | Key::Right => Some(Motion::Right),
2313 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2314 Key::Char('k') | Key::Up => Some(Motion::Up),
2315 Key::Char('w') => Some(Motion::WordFwd),
2316 Key::Char('W') => Some(Motion::BigWordFwd),
2317 Key::Char('b') => Some(Motion::WordBack),
2318 Key::Char('B') => Some(Motion::BigWordBack),
2319 Key::Char('e') => Some(Motion::WordEnd),
2320 Key::Char('E') => Some(Motion::BigWordEnd),
2321 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2322 Key::Char('^') => Some(Motion::FirstNonBlank),
2323 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2324 Key::Char('G') => Some(Motion::FileBottom),
2325 Key::Char('%') => Some(Motion::MatchBracket),
2326 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2327 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2328 Key::Char('*') => Some(Motion::WordAtCursor {
2329 forward: true,
2330 whole_word: true,
2331 }),
2332 Key::Char('#') => Some(Motion::WordAtCursor {
2333 forward: false,
2334 whole_word: true,
2335 }),
2336 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2337 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2338 Key::Char('H') => Some(Motion::ViewportTop),
2339 Key::Char('M') => Some(Motion::ViewportMiddle),
2340 Key::Char('L') => Some(Motion::ViewportBottom),
2341 Key::Char('{') => Some(Motion::ParagraphPrev),
2342 Key::Char('}') => Some(Motion::ParagraphNext),
2343 Key::Char('(') => Some(Motion::SentencePrev),
2344 Key::Char(')') => Some(Motion::SentenceNext),
2345 _ => None,
2346 }
2347}
2348
2349fn execute_motion<H: crate::types::Host>(
2352 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2353 motion: Motion,
2354 count: usize,
2355) {
2356 let count = count.max(1);
2357 let motion = match motion {
2359 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2360 Some((ch, forward, till)) => Motion::Find {
2361 ch,
2362 forward: if reverse { !forward } else { forward },
2363 till,
2364 },
2365 None => return,
2366 },
2367 other => other,
2368 };
2369 let pre_pos = ed.cursor();
2370 let pre_col = pre_pos.1;
2371 apply_motion_cursor(ed, &motion, count);
2372 let post_pos = ed.cursor();
2373 if is_big_jump(&motion) && pre_pos != post_pos {
2374 push_jump(ed, pre_pos);
2375 }
2376 apply_sticky_col(ed, &motion, pre_col);
2377 ed.sync_buffer_from_textarea();
2382}
2383
2384fn apply_sticky_col<H: crate::types::Host>(
2389 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2390 motion: &Motion,
2391 pre_col: usize,
2392) {
2393 if is_vertical_motion(motion) {
2394 let want = ed.sticky_col.unwrap_or(pre_col);
2395 ed.sticky_col = Some(want);
2398 let (row, _) = ed.cursor();
2399 let line_len = buf_line_chars(&ed.buffer, row);
2400 let max_col = line_len.saturating_sub(1);
2404 let target = want.min(max_col);
2405 ed.jump_cursor(row, target);
2406 } else {
2407 ed.sticky_col = Some(ed.cursor().1);
2410 }
2411}
2412
2413fn is_vertical_motion(motion: &Motion) -> bool {
2414 matches!(
2418 motion,
2419 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2420 )
2421}
2422
2423fn apply_motion_cursor<H: crate::types::Host>(
2424 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2425 motion: &Motion,
2426 count: usize,
2427) {
2428 apply_motion_cursor_ctx(ed, motion, count, false)
2429}
2430
2431fn apply_motion_cursor_ctx<H: crate::types::Host>(
2432 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2433 motion: &Motion,
2434 count: usize,
2435 as_operator: bool,
2436) {
2437 match motion {
2438 Motion::Left => {
2439 crate::motions::move_left(&mut ed.buffer, count);
2441 ed.push_buffer_cursor_to_textarea();
2442 }
2443 Motion::Right => {
2444 if as_operator {
2448 crate::motions::move_right_to_end(&mut ed.buffer, count);
2449 } else {
2450 crate::motions::move_right_in_line(&mut ed.buffer, count);
2451 }
2452 ed.push_buffer_cursor_to_textarea();
2453 }
2454 Motion::Up => {
2455 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2459 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2460 ed.push_buffer_cursor_to_textarea();
2461 }
2462 Motion::Down => {
2463 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2464 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2465 ed.push_buffer_cursor_to_textarea();
2466 }
2467 Motion::ScreenUp => {
2468 let v = *ed.host.viewport();
2469 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2470 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2471 ed.push_buffer_cursor_to_textarea();
2472 }
2473 Motion::ScreenDown => {
2474 let v = *ed.host.viewport();
2475 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2476 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2477 ed.push_buffer_cursor_to_textarea();
2478 }
2479 Motion::WordFwd => {
2480 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2481 ed.push_buffer_cursor_to_textarea();
2482 }
2483 Motion::WordBack => {
2484 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2485 ed.push_buffer_cursor_to_textarea();
2486 }
2487 Motion::WordEnd => {
2488 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2489 ed.push_buffer_cursor_to_textarea();
2490 }
2491 Motion::BigWordFwd => {
2492 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2493 ed.push_buffer_cursor_to_textarea();
2494 }
2495 Motion::BigWordBack => {
2496 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2497 ed.push_buffer_cursor_to_textarea();
2498 }
2499 Motion::BigWordEnd => {
2500 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2501 ed.push_buffer_cursor_to_textarea();
2502 }
2503 Motion::WordEndBack => {
2504 crate::motions::move_word_end_back(
2505 &mut ed.buffer,
2506 false,
2507 count,
2508 &ed.settings.iskeyword,
2509 );
2510 ed.push_buffer_cursor_to_textarea();
2511 }
2512 Motion::BigWordEndBack => {
2513 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2514 ed.push_buffer_cursor_to_textarea();
2515 }
2516 Motion::LineStart => {
2517 crate::motions::move_line_start(&mut ed.buffer);
2518 ed.push_buffer_cursor_to_textarea();
2519 }
2520 Motion::FirstNonBlank => {
2521 crate::motions::move_first_non_blank(&mut ed.buffer);
2522 ed.push_buffer_cursor_to_textarea();
2523 }
2524 Motion::LineEnd => {
2525 crate::motions::move_line_end(&mut ed.buffer);
2527 ed.push_buffer_cursor_to_textarea();
2528 }
2529 Motion::FileTop => {
2530 if count > 1 {
2533 crate::motions::move_bottom(&mut ed.buffer, count);
2534 } else {
2535 crate::motions::move_top(&mut ed.buffer);
2536 }
2537 ed.push_buffer_cursor_to_textarea();
2538 }
2539 Motion::FileBottom => {
2540 if count > 1 {
2543 crate::motions::move_bottom(&mut ed.buffer, count);
2544 } else {
2545 crate::motions::move_bottom(&mut ed.buffer, 0);
2546 }
2547 ed.push_buffer_cursor_to_textarea();
2548 }
2549 Motion::Find { ch, forward, till } => {
2550 for _ in 0..count {
2551 if !find_char_on_line(ed, *ch, *forward, *till) {
2552 break;
2553 }
2554 }
2555 }
2556 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2558 let _ = matching_bracket(ed);
2559 }
2560 Motion::WordAtCursor {
2561 forward,
2562 whole_word,
2563 } => {
2564 word_at_cursor_search(ed, *forward, *whole_word, count);
2565 }
2566 Motion::SearchNext { reverse } => {
2567 if let Some(pattern) = ed.vim.last_search.clone() {
2571 push_search_pattern(ed, &pattern);
2572 }
2573 if ed.search_state().pattern.is_none() {
2574 return;
2575 }
2576 let forward = ed.vim.last_search_forward != *reverse;
2580 for _ in 0..count.max(1) {
2581 if forward {
2582 ed.search_advance_forward(true);
2583 } else {
2584 ed.search_advance_backward(true);
2585 }
2586 }
2587 ed.push_buffer_cursor_to_textarea();
2588 }
2589 Motion::ViewportTop => {
2590 let v = *ed.host().viewport();
2591 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2592 ed.push_buffer_cursor_to_textarea();
2593 }
2594 Motion::ViewportMiddle => {
2595 let v = *ed.host().viewport();
2596 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2597 ed.push_buffer_cursor_to_textarea();
2598 }
2599 Motion::ViewportBottom => {
2600 let v = *ed.host().viewport();
2601 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2602 ed.push_buffer_cursor_to_textarea();
2603 }
2604 Motion::LastNonBlank => {
2605 crate::motions::move_last_non_blank(&mut ed.buffer);
2606 ed.push_buffer_cursor_to_textarea();
2607 }
2608 Motion::LineMiddle => {
2609 let row = ed.cursor().0;
2610 let line_chars = buf_line_chars(&ed.buffer, row);
2611 let target = line_chars / 2;
2614 ed.jump_cursor(row, target);
2615 }
2616 Motion::ParagraphPrev => {
2617 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2618 ed.push_buffer_cursor_to_textarea();
2619 }
2620 Motion::ParagraphNext => {
2621 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2622 ed.push_buffer_cursor_to_textarea();
2623 }
2624 Motion::SentencePrev => {
2625 for _ in 0..count.max(1) {
2626 if let Some((row, col)) = sentence_boundary(ed, false) {
2627 ed.jump_cursor(row, col);
2628 }
2629 }
2630 }
2631 Motion::SentenceNext => {
2632 for _ in 0..count.max(1) {
2633 if let Some((row, col)) = sentence_boundary(ed, true) {
2634 ed.jump_cursor(row, col);
2635 }
2636 }
2637 }
2638 }
2639}
2640
2641fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2642 ed.sync_buffer_content_from_textarea();
2648 crate::motions::move_first_non_blank(&mut ed.buffer);
2649 ed.push_buffer_cursor_to_textarea();
2650}
2651
2652fn find_char_on_line<H: crate::types::Host>(
2653 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2654 ch: char,
2655 forward: bool,
2656 till: bool,
2657) -> bool {
2658 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2659 if moved {
2660 ed.push_buffer_cursor_to_textarea();
2661 }
2662 moved
2663}
2664
2665fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2666 let moved = crate::motions::match_bracket(&mut ed.buffer);
2667 if moved {
2668 ed.push_buffer_cursor_to_textarea();
2669 }
2670 moved
2671}
2672
2673fn word_at_cursor_search<H: crate::types::Host>(
2674 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2675 forward: bool,
2676 whole_word: bool,
2677 count: usize,
2678) {
2679 let (row, col) = ed.cursor();
2680 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2681 let chars: Vec<char> = line.chars().collect();
2682 if chars.is_empty() {
2683 return;
2684 }
2685 let spec = ed.settings().iskeyword.clone();
2687 let is_word = |c: char| is_keyword_char(c, &spec);
2688 let mut start = col.min(chars.len().saturating_sub(1));
2689 while start > 0 && is_word(chars[start - 1]) {
2690 start -= 1;
2691 }
2692 let mut end = start;
2693 while end < chars.len() && is_word(chars[end]) {
2694 end += 1;
2695 }
2696 if end <= start {
2697 return;
2698 }
2699 let word: String = chars[start..end].iter().collect();
2700 let escaped = regex_escape(&word);
2701 let pattern = if whole_word {
2702 format!(r"\b{escaped}\b")
2703 } else {
2704 escaped
2705 };
2706 push_search_pattern(ed, &pattern);
2707 if ed.search_state().pattern.is_none() {
2708 return;
2709 }
2710 ed.vim.last_search = Some(pattern);
2712 ed.vim.last_search_forward = forward;
2713 for _ in 0..count.max(1) {
2714 if forward {
2715 ed.search_advance_forward(true);
2716 } else {
2717 ed.search_advance_backward(true);
2718 }
2719 }
2720 ed.push_buffer_cursor_to_textarea();
2721}
2722
2723fn regex_escape(s: &str) -> String {
2724 let mut out = String::with_capacity(s.len());
2725 for c in s.chars() {
2726 if matches!(
2727 c,
2728 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2729 ) {
2730 out.push('\\');
2731 }
2732 out.push(c);
2733 }
2734 out
2735}
2736
2737fn handle_after_op<H: crate::types::Host>(
2740 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2741 input: Input,
2742 op: Operator,
2743 count1: usize,
2744) -> bool {
2745 if let Key::Char(d @ '0'..='9') = input.key
2747 && !input.ctrl
2748 && (d != '0' || ed.vim.count > 0)
2749 {
2750 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2751 ed.vim.pending = Pending::Op { op, count1 };
2752 return true;
2753 }
2754
2755 if input.key == Key::Esc {
2757 ed.vim.count = 0;
2758 return true;
2759 }
2760
2761 let double_ch = match op {
2765 Operator::Delete => Some('d'),
2766 Operator::Change => Some('c'),
2767 Operator::Yank => Some('y'),
2768 Operator::Indent => Some('>'),
2769 Operator::Outdent => Some('<'),
2770 Operator::Uppercase => Some('U'),
2771 Operator::Lowercase => Some('u'),
2772 Operator::ToggleCase => Some('~'),
2773 Operator::Fold => None,
2774 Operator::Reflow => Some('q'),
2777 };
2778 if let Key::Char(c) = input.key
2779 && !input.ctrl
2780 && Some(c) == double_ch
2781 {
2782 let count2 = take_count(&mut ed.vim);
2783 let total = count1.max(1) * count2.max(1);
2784 execute_line_op(ed, op, total);
2785 if !ed.vim.replaying {
2786 ed.vim.last_change = Some(LastChange::LineOp {
2787 op,
2788 count: total,
2789 inserted: None,
2790 });
2791 }
2792 return true;
2793 }
2794
2795 if let Key::Char('i') | Key::Char('a') = input.key
2797 && !input.ctrl
2798 {
2799 let inner = matches!(input.key, Key::Char('i'));
2800 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2801 return true;
2802 }
2803
2804 if input.key == Key::Char('g') && !input.ctrl {
2806 ed.vim.pending = Pending::OpG { op, count1 };
2807 return true;
2808 }
2809
2810 if let Some((forward, till)) = find_entry(&input) {
2812 ed.vim.pending = Pending::OpFind {
2813 op,
2814 count1,
2815 forward,
2816 till,
2817 };
2818 return true;
2819 }
2820
2821 let count2 = take_count(&mut ed.vim);
2823 let total = count1.max(1) * count2.max(1);
2824 if let Some(motion) = parse_motion(&input) {
2825 let motion = match motion {
2826 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2827 Some((ch, forward, till)) => Motion::Find {
2828 ch,
2829 forward: if reverse { !forward } else { forward },
2830 till,
2831 },
2832 None => return true,
2833 },
2834 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2838 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2839 m => m,
2840 };
2841 apply_op_with_motion(ed, op, &motion, total);
2842 if let Motion::Find { ch, forward, till } = &motion {
2843 ed.vim.last_find = Some((*ch, *forward, *till));
2844 }
2845 if !ed.vim.replaying && op_is_change(op) {
2846 ed.vim.last_change = Some(LastChange::OpMotion {
2847 op,
2848 motion,
2849 count: total,
2850 inserted: None,
2851 });
2852 }
2853 return true;
2854 }
2855
2856 true
2858}
2859
2860fn handle_op_after_g<H: crate::types::Host>(
2861 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2862 input: Input,
2863 op: Operator,
2864 count1: usize,
2865) -> bool {
2866 if input.ctrl {
2867 return true;
2868 }
2869 let count2 = take_count(&mut ed.vim);
2870 let total = count1.max(1) * count2.max(1);
2871 if matches!(
2875 op,
2876 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2877 ) {
2878 let op_char = match op {
2879 Operator::Uppercase => 'U',
2880 Operator::Lowercase => 'u',
2881 Operator::ToggleCase => '~',
2882 _ => unreachable!(),
2883 };
2884 if input.key == Key::Char(op_char) {
2885 execute_line_op(ed, op, total);
2886 if !ed.vim.replaying {
2887 ed.vim.last_change = Some(LastChange::LineOp {
2888 op,
2889 count: total,
2890 inserted: None,
2891 });
2892 }
2893 return true;
2894 }
2895 }
2896 let motion = match input.key {
2897 Key::Char('g') => Motion::FileTop,
2898 Key::Char('e') => Motion::WordEndBack,
2899 Key::Char('E') => Motion::BigWordEndBack,
2900 Key::Char('j') => Motion::ScreenDown,
2901 Key::Char('k') => Motion::ScreenUp,
2902 _ => return true,
2903 };
2904 apply_op_with_motion(ed, op, &motion, total);
2905 if !ed.vim.replaying && op_is_change(op) {
2906 ed.vim.last_change = Some(LastChange::OpMotion {
2907 op,
2908 motion,
2909 count: total,
2910 inserted: None,
2911 });
2912 }
2913 true
2914}
2915
2916fn handle_after_g<H: crate::types::Host>(
2917 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2918 input: Input,
2919) -> bool {
2920 let count = take_count(&mut ed.vim);
2921 match input.key {
2922 Key::Char('g') => {
2923 let pre = ed.cursor();
2925 if count > 1 {
2926 ed.jump_cursor(count - 1, 0);
2927 } else {
2928 ed.jump_cursor(0, 0);
2929 }
2930 move_first_non_whitespace(ed);
2931 if ed.cursor() != pre {
2932 push_jump(ed, pre);
2933 }
2934 }
2935 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
2936 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
2937 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
2939 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
2941 Key::Char('v') => {
2943 if let Some(snap) = ed.vim.last_visual {
2944 match snap.mode {
2945 Mode::Visual => {
2946 ed.vim.visual_anchor = snap.anchor;
2947 ed.vim.mode = Mode::Visual;
2948 }
2949 Mode::VisualLine => {
2950 ed.vim.visual_line_anchor = snap.anchor.0;
2951 ed.vim.mode = Mode::VisualLine;
2952 }
2953 Mode::VisualBlock => {
2954 ed.vim.block_anchor = snap.anchor;
2955 ed.vim.block_vcol = snap.block_vcol;
2956 ed.vim.mode = Mode::VisualBlock;
2957 }
2958 _ => {}
2959 }
2960 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2961 }
2962 }
2963 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
2967 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
2968 Key::Char('U') => {
2972 ed.vim.pending = Pending::Op {
2973 op: Operator::Uppercase,
2974 count1: count,
2975 };
2976 }
2977 Key::Char('u') => {
2978 ed.vim.pending = Pending::Op {
2979 op: Operator::Lowercase,
2980 count1: count,
2981 };
2982 }
2983 Key::Char('~') => {
2984 ed.vim.pending = Pending::Op {
2985 op: Operator::ToggleCase,
2986 count1: count,
2987 };
2988 }
2989 Key::Char('q') => {
2990 ed.vim.pending = Pending::Op {
2993 op: Operator::Reflow,
2994 count1: count,
2995 };
2996 }
2997 Key::Char('J') => {
2998 for _ in 0..count.max(1) {
3000 ed.push_undo();
3001 join_line_raw(ed);
3002 }
3003 if !ed.vim.replaying {
3004 ed.vim.last_change = Some(LastChange::JoinLine {
3005 count: count.max(1),
3006 });
3007 }
3008 }
3009 Key::Char('d') => {
3010 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3015 }
3016 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
3019 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
3020 Key::Char('*') => execute_motion(
3024 ed,
3025 Motion::WordAtCursor {
3026 forward: true,
3027 whole_word: false,
3028 },
3029 count,
3030 ),
3031 Key::Char('#') => execute_motion(
3032 ed,
3033 Motion::WordAtCursor {
3034 forward: false,
3035 whole_word: false,
3036 },
3037 count,
3038 ),
3039 _ => {}
3040 }
3041 true
3042}
3043
3044fn handle_after_z<H: crate::types::Host>(
3045 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3046 input: Input,
3047) -> bool {
3048 use crate::editor::CursorScrollTarget;
3049 let row = ed.cursor().0;
3050 match input.key {
3051 Key::Char('z') => {
3052 ed.scroll_cursor_to(CursorScrollTarget::Center);
3053 ed.vim.viewport_pinned = true;
3054 }
3055 Key::Char('t') => {
3056 ed.scroll_cursor_to(CursorScrollTarget::Top);
3057 ed.vim.viewport_pinned = true;
3058 }
3059 Key::Char('b') => {
3060 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3061 ed.vim.viewport_pinned = true;
3062 }
3063 Key::Char('o') => {
3068 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3069 }
3070 Key::Char('c') => {
3071 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3072 }
3073 Key::Char('a') => {
3074 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3075 }
3076 Key::Char('R') => {
3077 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3078 }
3079 Key::Char('M') => {
3080 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3081 }
3082 Key::Char('E') => {
3083 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3084 }
3085 Key::Char('d') => {
3086 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3087 }
3088 Key::Char('f') => {
3089 if matches!(
3090 ed.vim.mode,
3091 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3092 ) {
3093 let anchor_row = match ed.vim.mode {
3096 Mode::VisualLine => ed.vim.visual_line_anchor,
3097 Mode::VisualBlock => ed.vim.block_anchor.0,
3098 _ => ed.vim.visual_anchor.0,
3099 };
3100 let cur = ed.cursor().0;
3101 let top = anchor_row.min(cur);
3102 let bot = anchor_row.max(cur);
3103 ed.apply_fold_op(crate::types::FoldOp::Add {
3104 start_row: top,
3105 end_row: bot,
3106 closed: true,
3107 });
3108 ed.vim.mode = Mode::Normal;
3109 } else {
3110 let count = take_count(&mut ed.vim);
3115 ed.vim.pending = Pending::Op {
3116 op: Operator::Fold,
3117 count1: count,
3118 };
3119 }
3120 }
3121 _ => {}
3122 }
3123 true
3124}
3125
3126fn handle_replace<H: crate::types::Host>(
3127 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3128 input: Input,
3129) -> bool {
3130 if let Key::Char(ch) = input.key {
3131 if ed.vim.mode == Mode::VisualBlock {
3132 block_replace(ed, ch);
3133 return true;
3134 }
3135 let count = take_count(&mut ed.vim);
3136 replace_char(ed, ch, count.max(1));
3137 if !ed.vim.replaying {
3138 ed.vim.last_change = Some(LastChange::ReplaceChar {
3139 ch,
3140 count: count.max(1),
3141 });
3142 }
3143 }
3144 true
3145}
3146
3147fn handle_find_target<H: crate::types::Host>(
3148 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3149 input: Input,
3150 forward: bool,
3151 till: bool,
3152) -> bool {
3153 let Key::Char(ch) = input.key else {
3154 return true;
3155 };
3156 let count = take_count(&mut ed.vim);
3157 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3158 ed.vim.last_find = Some((ch, forward, till));
3159 true
3160}
3161
3162fn handle_op_find_target<H: crate::types::Host>(
3163 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3164 input: Input,
3165 op: Operator,
3166 count1: usize,
3167 forward: bool,
3168 till: bool,
3169) -> bool {
3170 let Key::Char(ch) = input.key else {
3171 return true;
3172 };
3173 let count2 = take_count(&mut ed.vim);
3174 let total = count1.max(1) * count2.max(1);
3175 let motion = Motion::Find { ch, forward, till };
3176 apply_op_with_motion(ed, op, &motion, total);
3177 ed.vim.last_find = Some((ch, forward, till));
3178 if !ed.vim.replaying && op_is_change(op) {
3179 ed.vim.last_change = Some(LastChange::OpMotion {
3180 op,
3181 motion,
3182 count: total,
3183 inserted: None,
3184 });
3185 }
3186 true
3187}
3188
3189fn handle_text_object<H: crate::types::Host>(
3190 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3191 input: Input,
3192 op: Operator,
3193 _count1: usize,
3194 inner: bool,
3195) -> bool {
3196 let Key::Char(ch) = input.key else {
3197 return true;
3198 };
3199 let obj = match ch {
3200 'w' => TextObject::Word { big: false },
3201 'W' => TextObject::Word { big: true },
3202 '"' | '\'' | '`' => TextObject::Quote(ch),
3203 '(' | ')' | 'b' => TextObject::Bracket('('),
3204 '[' | ']' => TextObject::Bracket('['),
3205 '{' | '}' | 'B' => TextObject::Bracket('{'),
3206 '<' | '>' => TextObject::Bracket('<'),
3207 'p' => TextObject::Paragraph,
3208 't' => TextObject::XmlTag,
3209 's' => TextObject::Sentence,
3210 _ => return true,
3211 };
3212 apply_op_with_text_object(ed, op, obj, inner);
3213 if !ed.vim.replaying && op_is_change(op) {
3214 ed.vim.last_change = Some(LastChange::OpTextObj {
3215 op,
3216 obj,
3217 inner,
3218 inserted: None,
3219 });
3220 }
3221 true
3222}
3223
3224fn handle_visual_text_obj<H: crate::types::Host>(
3225 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3226 input: Input,
3227 inner: bool,
3228) -> bool {
3229 let Key::Char(ch) = input.key else {
3230 return true;
3231 };
3232 let obj = match ch {
3233 'w' => TextObject::Word { big: false },
3234 'W' => TextObject::Word { big: true },
3235 '"' | '\'' | '`' => TextObject::Quote(ch),
3236 '(' | ')' | 'b' => TextObject::Bracket('('),
3237 '[' | ']' => TextObject::Bracket('['),
3238 '{' | '}' | 'B' => TextObject::Bracket('{'),
3239 '<' | '>' => TextObject::Bracket('<'),
3240 'p' => TextObject::Paragraph,
3241 't' => TextObject::XmlTag,
3242 's' => TextObject::Sentence,
3243 _ => return true,
3244 };
3245 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3246 return true;
3247 };
3248 match kind {
3252 MotionKind::Linewise => {
3253 ed.vim.visual_line_anchor = start.0;
3254 ed.vim.mode = Mode::VisualLine;
3255 ed.jump_cursor(end.0, 0);
3256 }
3257 _ => {
3258 ed.vim.mode = Mode::Visual;
3259 ed.vim.visual_anchor = (start.0, start.1);
3260 let (er, ec) = retreat_one(ed, end);
3261 ed.jump_cursor(er, ec);
3262 }
3263 }
3264 true
3265}
3266
3267fn retreat_one<H: crate::types::Host>(
3269 ed: &Editor<hjkl_buffer::Buffer, H>,
3270 pos: (usize, usize),
3271) -> (usize, usize) {
3272 let (r, c) = pos;
3273 if c > 0 {
3274 (r, c - 1)
3275 } else if r > 0 {
3276 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3277 (r - 1, prev_len)
3278 } else {
3279 (0, 0)
3280 }
3281}
3282
3283fn op_is_change(op: Operator) -> bool {
3284 matches!(op, Operator::Delete | Operator::Change)
3285}
3286
3287fn handle_normal_only<H: crate::types::Host>(
3290 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3291 input: &Input,
3292 count: usize,
3293) -> bool {
3294 if input.ctrl {
3295 return false;
3296 }
3297 match input.key {
3298 Key::Char('i') => {
3299 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3300 true
3301 }
3302 Key::Char('I') => {
3303 move_first_non_whitespace(ed);
3304 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3305 true
3306 }
3307 Key::Char('a') => {
3308 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3309 ed.push_buffer_cursor_to_textarea();
3310 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3311 true
3312 }
3313 Key::Char('A') => {
3314 crate::motions::move_line_end(&mut ed.buffer);
3315 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3316 ed.push_buffer_cursor_to_textarea();
3317 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3318 true
3319 }
3320 Key::Char('R') => {
3321 begin_insert(ed, count.max(1), InsertReason::Replace);
3324 true
3325 }
3326 Key::Char('o') => {
3327 use hjkl_buffer::{Edit, Position};
3328 ed.push_undo();
3329 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3332 ed.sync_buffer_content_from_textarea();
3333 let row = buf_cursor_pos(&ed.buffer).row;
3334 let line_chars = buf_line_chars(&ed.buffer, row);
3335 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3338 let indent = compute_enter_indent(&ed.settings, prev_line);
3339 ed.mutate_edit(Edit::InsertStr {
3340 at: Position::new(row, line_chars),
3341 text: format!("\n{indent}"),
3342 });
3343 ed.push_buffer_cursor_to_textarea();
3344 true
3345 }
3346 Key::Char('O') => {
3347 use hjkl_buffer::{Edit, Position};
3348 ed.push_undo();
3349 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3350 ed.sync_buffer_content_from_textarea();
3351 let row = buf_cursor_pos(&ed.buffer).row;
3352 let indent = if row > 0 {
3356 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3357 compute_enter_indent(&ed.settings, above)
3358 } else {
3359 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3360 cur.chars()
3361 .take_while(|c| *c == ' ' || *c == '\t')
3362 .collect::<String>()
3363 };
3364 ed.mutate_edit(Edit::InsertStr {
3365 at: Position::new(row, 0),
3366 text: format!("{indent}\n"),
3367 });
3368 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3373 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3374 let new_row = buf_cursor_pos(&ed.buffer).row;
3375 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3376 ed.push_buffer_cursor_to_textarea();
3377 true
3378 }
3379 Key::Char('x') => {
3380 do_char_delete(ed, true, count.max(1));
3381 if !ed.vim.replaying {
3382 ed.vim.last_change = Some(LastChange::CharDel {
3383 forward: true,
3384 count: count.max(1),
3385 });
3386 }
3387 true
3388 }
3389 Key::Char('X') => {
3390 do_char_delete(ed, false, count.max(1));
3391 if !ed.vim.replaying {
3392 ed.vim.last_change = Some(LastChange::CharDel {
3393 forward: false,
3394 count: count.max(1),
3395 });
3396 }
3397 true
3398 }
3399 Key::Char('~') => {
3400 for _ in 0..count.max(1) {
3401 ed.push_undo();
3402 toggle_case_at_cursor(ed);
3403 }
3404 if !ed.vim.replaying {
3405 ed.vim.last_change = Some(LastChange::ToggleCase {
3406 count: count.max(1),
3407 });
3408 }
3409 true
3410 }
3411 Key::Char('J') => {
3412 for _ in 0..count.max(1) {
3413 ed.push_undo();
3414 join_line(ed);
3415 }
3416 if !ed.vim.replaying {
3417 ed.vim.last_change = Some(LastChange::JoinLine {
3418 count: count.max(1),
3419 });
3420 }
3421 true
3422 }
3423 Key::Char('D') => {
3424 ed.push_undo();
3425 delete_to_eol(ed);
3426 crate::motions::move_left(&mut ed.buffer, 1);
3428 ed.push_buffer_cursor_to_textarea();
3429 if !ed.vim.replaying {
3430 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3431 }
3432 true
3433 }
3434 Key::Char('Y') => {
3435 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3437 true
3438 }
3439 Key::Char('C') => {
3440 ed.push_undo();
3441 delete_to_eol(ed);
3442 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3443 true
3444 }
3445 Key::Char('s') => {
3446 use hjkl_buffer::{Edit, MotionKind, Position};
3447 ed.push_undo();
3448 ed.sync_buffer_content_from_textarea();
3449 for _ in 0..count.max(1) {
3450 let cursor = buf_cursor_pos(&ed.buffer);
3451 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3452 if cursor.col >= line_chars {
3453 break;
3454 }
3455 ed.mutate_edit(Edit::DeleteRange {
3456 start: cursor,
3457 end: Position::new(cursor.row, cursor.col + 1),
3458 kind: MotionKind::Char,
3459 });
3460 }
3461 ed.push_buffer_cursor_to_textarea();
3462 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3463 if !ed.vim.replaying {
3465 ed.vim.last_change = Some(LastChange::OpMotion {
3466 op: Operator::Change,
3467 motion: Motion::Right,
3468 count: count.max(1),
3469 inserted: None,
3470 });
3471 }
3472 true
3473 }
3474 Key::Char('p') => {
3475 do_paste(ed, false, count.max(1));
3476 if !ed.vim.replaying {
3477 ed.vim.last_change = Some(LastChange::Paste {
3478 before: false,
3479 count: count.max(1),
3480 });
3481 }
3482 true
3483 }
3484 Key::Char('P') => {
3485 do_paste(ed, true, count.max(1));
3486 if !ed.vim.replaying {
3487 ed.vim.last_change = Some(LastChange::Paste {
3488 before: true,
3489 count: count.max(1),
3490 });
3491 }
3492 true
3493 }
3494 Key::Char('u') => {
3495 do_undo(ed);
3496 true
3497 }
3498 Key::Char('r') => {
3499 ed.vim.count = count;
3500 ed.vim.pending = Pending::Replace;
3501 true
3502 }
3503 Key::Char('/') => {
3504 enter_search(ed, true);
3505 true
3506 }
3507 Key::Char('?') => {
3508 enter_search(ed, false);
3509 true
3510 }
3511 Key::Char('.') => {
3512 replay_last_change(ed, count);
3513 true
3514 }
3515 _ => false,
3516 }
3517}
3518
3519fn begin_insert_noundo<H: crate::types::Host>(
3521 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3522 count: usize,
3523 reason: InsertReason,
3524) {
3525 let reason = if ed.vim.replaying {
3526 InsertReason::ReplayOnly
3527 } else {
3528 reason
3529 };
3530 let (row, _) = ed.cursor();
3531 ed.vim.insert_session = Some(InsertSession {
3532 count,
3533 row_min: row,
3534 row_max: row,
3535 before_lines: buf_lines_to_vec(&ed.buffer),
3536 reason,
3537 });
3538 ed.vim.mode = Mode::Insert;
3539}
3540
3541fn apply_op_with_motion<H: crate::types::Host>(
3544 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3545 op: Operator,
3546 motion: &Motion,
3547 count: usize,
3548) {
3549 let start = ed.cursor();
3550 apply_motion_cursor_ctx(ed, motion, count, true);
3555 let end = ed.cursor();
3556 let kind = motion_kind(motion);
3557 ed.jump_cursor(start.0, start.1);
3559 run_operator_over_range(ed, op, start, end, kind);
3560}
3561
3562fn apply_op_with_text_object<H: crate::types::Host>(
3563 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3564 op: Operator,
3565 obj: TextObject,
3566 inner: bool,
3567) {
3568 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3569 return;
3570 };
3571 ed.jump_cursor(start.0, start.1);
3572 run_operator_over_range(ed, op, start, end, kind);
3573}
3574
3575fn motion_kind(motion: &Motion) -> MotionKind {
3576 match motion {
3577 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3578 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3579 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3580 MotionKind::Linewise
3581 }
3582 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3583 MotionKind::Inclusive
3584 }
3585 Motion::Find { .. } => MotionKind::Inclusive,
3586 Motion::MatchBracket => MotionKind::Inclusive,
3587 Motion::LineEnd => MotionKind::Inclusive,
3589 _ => MotionKind::Exclusive,
3590 }
3591}
3592
3593fn run_operator_over_range<H: crate::types::Host>(
3594 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3595 op: Operator,
3596 start: (usize, usize),
3597 end: (usize, usize),
3598 kind: MotionKind,
3599) {
3600 let (top, bot) = order(start, end);
3601 if top == bot {
3602 return;
3603 }
3604
3605 match op {
3606 Operator::Yank => {
3607 let text = read_vim_range(ed, top, bot, kind);
3608 if !text.is_empty() {
3609 ed.record_yank_to_host(text.clone());
3610 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3611 }
3612 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3613 ed.push_buffer_cursor_to_textarea();
3614 }
3615 Operator::Delete => {
3616 ed.push_undo();
3617 cut_vim_range(ed, top, bot, kind);
3618 if !matches!(kind, MotionKind::Linewise) {
3623 clamp_cursor_to_normal_mode(ed);
3624 }
3625 ed.vim.mode = Mode::Normal;
3626 }
3627 Operator::Change => {
3628 ed.push_undo();
3629 cut_vim_range(ed, top, bot, kind);
3630 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3631 }
3632 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3633 apply_case_op_to_selection(ed, op, top, bot, kind);
3634 }
3635 Operator::Indent | Operator::Outdent => {
3636 ed.push_undo();
3639 if op == Operator::Indent {
3640 indent_rows(ed, top.0, bot.0, 1);
3641 } else {
3642 outdent_rows(ed, top.0, bot.0, 1);
3643 }
3644 ed.vim.mode = Mode::Normal;
3645 }
3646 Operator::Fold => {
3647 if bot.0 >= top.0 {
3651 ed.apply_fold_op(crate::types::FoldOp::Add {
3652 start_row: top.0,
3653 end_row: bot.0,
3654 closed: true,
3655 });
3656 }
3657 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3658 ed.push_buffer_cursor_to_textarea();
3659 ed.vim.mode = Mode::Normal;
3660 }
3661 Operator::Reflow => {
3662 ed.push_undo();
3663 reflow_rows(ed, top.0, bot.0);
3664 ed.vim.mode = Mode::Normal;
3665 }
3666 }
3667}
3668
3669fn reflow_rows<H: crate::types::Host>(
3674 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3675 top: usize,
3676 bot: usize,
3677) {
3678 let width = ed.settings().textwidth.max(1);
3679 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3680 let bot = bot.min(lines.len().saturating_sub(1));
3681 if top > bot {
3682 return;
3683 }
3684 let original = lines[top..=bot].to_vec();
3685 let mut wrapped: Vec<String> = Vec::new();
3686 let mut paragraph: Vec<String> = Vec::new();
3687 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3688 if para.is_empty() {
3689 return;
3690 }
3691 let words = para.join(" ");
3692 let mut current = String::new();
3693 for word in words.split_whitespace() {
3694 let extra = if current.is_empty() {
3695 word.chars().count()
3696 } else {
3697 current.chars().count() + 1 + word.chars().count()
3698 };
3699 if extra > width && !current.is_empty() {
3700 out.push(std::mem::take(&mut current));
3701 current.push_str(word);
3702 } else if current.is_empty() {
3703 current.push_str(word);
3704 } else {
3705 current.push(' ');
3706 current.push_str(word);
3707 }
3708 }
3709 if !current.is_empty() {
3710 out.push(current);
3711 }
3712 para.clear();
3713 };
3714 for line in &original {
3715 if line.trim().is_empty() {
3716 flush(&mut paragraph, &mut wrapped, width);
3717 wrapped.push(String::new());
3718 } else {
3719 paragraph.push(line.clone());
3720 }
3721 }
3722 flush(&mut paragraph, &mut wrapped, width);
3723
3724 let after: Vec<String> = lines.split_off(bot + 1);
3726 lines.truncate(top);
3727 lines.extend(wrapped);
3728 lines.extend(after);
3729 ed.restore(lines, (top, 0));
3730 ed.mark_content_dirty();
3731}
3732
3733fn apply_case_op_to_selection<H: crate::types::Host>(
3739 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3740 op: Operator,
3741 top: (usize, usize),
3742 bot: (usize, usize),
3743 kind: MotionKind,
3744) {
3745 use hjkl_buffer::Edit;
3746 ed.push_undo();
3747 let saved_yank = ed.yank().to_string();
3748 let saved_yank_linewise = ed.vim.yank_linewise;
3749 let selection = cut_vim_range(ed, top, bot, kind);
3750 let transformed = match op {
3751 Operator::Uppercase => selection.to_uppercase(),
3752 Operator::Lowercase => selection.to_lowercase(),
3753 Operator::ToggleCase => toggle_case_str(&selection),
3754 _ => unreachable!(),
3755 };
3756 if !transformed.is_empty() {
3757 let cursor = buf_cursor_pos(&ed.buffer);
3758 ed.mutate_edit(Edit::InsertStr {
3759 at: cursor,
3760 text: transformed,
3761 });
3762 }
3763 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3764 ed.push_buffer_cursor_to_textarea();
3765 ed.set_yank(saved_yank);
3766 ed.vim.yank_linewise = saved_yank_linewise;
3767 ed.vim.mode = Mode::Normal;
3768}
3769
3770fn indent_rows<H: crate::types::Host>(
3775 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3776 top: usize,
3777 bot: usize,
3778 count: usize,
3779) {
3780 ed.sync_buffer_content_from_textarea();
3781 let width = ed.settings().shiftwidth * count.max(1);
3782 let pad: String = " ".repeat(width);
3783 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3784 let bot = bot.min(lines.len().saturating_sub(1));
3785 for line in lines.iter_mut().take(bot + 1).skip(top) {
3786 if !line.is_empty() {
3787 line.insert_str(0, &pad);
3788 }
3789 }
3790 ed.restore(lines, (top, 0));
3793 move_first_non_whitespace(ed);
3794}
3795
3796fn outdent_rows<H: crate::types::Host>(
3800 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3801 top: usize,
3802 bot: usize,
3803 count: usize,
3804) {
3805 ed.sync_buffer_content_from_textarea();
3806 let width = ed.settings().shiftwidth * count.max(1);
3807 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3808 let bot = bot.min(lines.len().saturating_sub(1));
3809 for line in lines.iter_mut().take(bot + 1).skip(top) {
3810 let strip: usize = line
3811 .chars()
3812 .take(width)
3813 .take_while(|c| *c == ' ' || *c == '\t')
3814 .count();
3815 if strip > 0 {
3816 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3817 line.drain(..byte_len);
3818 }
3819 }
3820 ed.restore(lines, (top, 0));
3821 move_first_non_whitespace(ed);
3822}
3823
3824fn toggle_case_str(s: &str) -> String {
3825 s.chars()
3826 .map(|c| {
3827 if c.is_lowercase() {
3828 c.to_uppercase().next().unwrap_or(c)
3829 } else if c.is_uppercase() {
3830 c.to_lowercase().next().unwrap_or(c)
3831 } else {
3832 c
3833 }
3834 })
3835 .collect()
3836}
3837
3838fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3839 if a <= b { (a, b) } else { (b, a) }
3840}
3841
3842fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
3847 let (row, col) = ed.cursor();
3848 let line_chars = buf_line_chars(&ed.buffer, row);
3849 let max_col = line_chars.saturating_sub(1);
3850 if col > max_col {
3851 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
3852 ed.push_buffer_cursor_to_textarea();
3853 }
3854}
3855
3856fn execute_line_op<H: crate::types::Host>(
3859 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3860 op: Operator,
3861 count: usize,
3862) {
3863 let (row, col) = ed.cursor();
3864 let total = buf_row_count(&ed.buffer);
3865 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3866
3867 match op {
3868 Operator::Yank => {
3869 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3871 if !text.is_empty() {
3872 ed.record_yank_to_host(text.clone());
3873 ed.record_yank(text, true);
3874 }
3875 buf_set_cursor_rc(&mut ed.buffer, row, col);
3876 ed.push_buffer_cursor_to_textarea();
3877 ed.vim.mode = Mode::Normal;
3878 }
3879 Operator::Delete => {
3880 ed.push_undo();
3881 let deleted_through_last = end_row + 1 >= total;
3882 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3883 let total_after = buf_row_count(&ed.buffer);
3887 let raw_target = if deleted_through_last {
3888 row.saturating_sub(1).min(total_after.saturating_sub(1))
3889 } else {
3890 row.min(total_after.saturating_sub(1))
3891 };
3892 let target_row = if raw_target > 0
3898 && raw_target + 1 == total_after
3899 && buf_line(&ed.buffer, raw_target)
3900 .map(str::is_empty)
3901 .unwrap_or(false)
3902 {
3903 raw_target - 1
3904 } else {
3905 raw_target
3906 };
3907 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
3908 ed.push_buffer_cursor_to_textarea();
3909 move_first_non_whitespace(ed);
3910 ed.sticky_col = Some(ed.cursor().1);
3911 ed.vim.mode = Mode::Normal;
3912 }
3913 Operator::Change => {
3914 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3918 ed.push_undo();
3919 ed.sync_buffer_content_from_textarea();
3920 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3922 if end_row > row {
3923 ed.mutate_edit(Edit::DeleteRange {
3924 start: Position::new(row + 1, 0),
3925 end: Position::new(end_row, 0),
3926 kind: BufKind::Line,
3927 });
3928 }
3929 let line_chars = buf_line_chars(&ed.buffer, row);
3930 if line_chars > 0 {
3931 ed.mutate_edit(Edit::DeleteRange {
3932 start: Position::new(row, 0),
3933 end: Position::new(row, line_chars),
3934 kind: BufKind::Char,
3935 });
3936 }
3937 if !payload.is_empty() {
3938 ed.record_yank_to_host(payload.clone());
3939 ed.record_delete(payload, true);
3940 }
3941 buf_set_cursor_rc(&mut ed.buffer, row, 0);
3942 ed.push_buffer_cursor_to_textarea();
3943 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3944 }
3945 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3946 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
3950 move_first_non_whitespace(ed);
3953 }
3954 Operator::Indent | Operator::Outdent => {
3955 ed.push_undo();
3957 if op == Operator::Indent {
3958 indent_rows(ed, row, end_row, 1);
3959 } else {
3960 outdent_rows(ed, row, end_row, 1);
3961 }
3962 ed.sticky_col = Some(ed.cursor().1);
3963 ed.vim.mode = Mode::Normal;
3964 }
3965 Operator::Fold => unreachable!("Fold has no line-op double"),
3967 Operator::Reflow => {
3968 ed.push_undo();
3970 reflow_rows(ed, row, end_row);
3971 move_first_non_whitespace(ed);
3972 ed.sticky_col = Some(ed.cursor().1);
3973 ed.vim.mode = Mode::Normal;
3974 }
3975 }
3976}
3977
3978fn apply_visual_operator<H: crate::types::Host>(
3981 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3982 op: Operator,
3983) {
3984 match ed.vim.mode {
3985 Mode::VisualLine => {
3986 let cursor_row = buf_cursor_pos(&ed.buffer).row;
3987 let top = cursor_row.min(ed.vim.visual_line_anchor);
3988 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3989 ed.vim.yank_linewise = true;
3990 match op {
3991 Operator::Yank => {
3992 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3993 if !text.is_empty() {
3994 ed.record_yank_to_host(text.clone());
3995 ed.record_yank(text, true);
3996 }
3997 buf_set_cursor_rc(&mut ed.buffer, top, 0);
3998 ed.push_buffer_cursor_to_textarea();
3999 ed.vim.mode = Mode::Normal;
4000 }
4001 Operator::Delete => {
4002 ed.push_undo();
4003 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4004 ed.vim.mode = Mode::Normal;
4005 }
4006 Operator::Change => {
4007 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4010 ed.push_undo();
4011 ed.sync_buffer_content_from_textarea();
4012 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4013 if bot > top {
4014 ed.mutate_edit(Edit::DeleteRange {
4015 start: Position::new(top + 1, 0),
4016 end: Position::new(bot, 0),
4017 kind: BufKind::Line,
4018 });
4019 }
4020 let line_chars = buf_line_chars(&ed.buffer, top);
4021 if line_chars > 0 {
4022 ed.mutate_edit(Edit::DeleteRange {
4023 start: Position::new(top, 0),
4024 end: Position::new(top, line_chars),
4025 kind: BufKind::Char,
4026 });
4027 }
4028 if !payload.is_empty() {
4029 ed.record_yank_to_host(payload.clone());
4030 ed.record_delete(payload, true);
4031 }
4032 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4033 ed.push_buffer_cursor_to_textarea();
4034 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4035 }
4036 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4037 let bot = buf_cursor_pos(&ed.buffer)
4038 .row
4039 .max(ed.vim.visual_line_anchor);
4040 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4041 move_first_non_whitespace(ed);
4042 }
4043 Operator::Indent | Operator::Outdent => {
4044 ed.push_undo();
4045 let (cursor_row, _) = ed.cursor();
4046 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4047 if op == Operator::Indent {
4048 indent_rows(ed, top, bot, 1);
4049 } else {
4050 outdent_rows(ed, top, bot, 1);
4051 }
4052 ed.vim.mode = Mode::Normal;
4053 }
4054 Operator::Reflow => {
4055 ed.push_undo();
4056 let (cursor_row, _) = ed.cursor();
4057 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4058 reflow_rows(ed, top, bot);
4059 ed.vim.mode = Mode::Normal;
4060 }
4061 Operator::Fold => unreachable!("Visual zf takes its own path"),
4064 }
4065 }
4066 Mode::Visual => {
4067 ed.vim.yank_linewise = false;
4068 let anchor = ed.vim.visual_anchor;
4069 let cursor = ed.cursor();
4070 let (top, bot) = order(anchor, cursor);
4071 match op {
4072 Operator::Yank => {
4073 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4074 if !text.is_empty() {
4075 ed.record_yank_to_host(text.clone());
4076 ed.record_yank(text, false);
4077 }
4078 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4079 ed.push_buffer_cursor_to_textarea();
4080 ed.vim.mode = Mode::Normal;
4081 }
4082 Operator::Delete => {
4083 ed.push_undo();
4084 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4085 ed.vim.mode = Mode::Normal;
4086 }
4087 Operator::Change => {
4088 ed.push_undo();
4089 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4090 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4091 }
4092 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4093 let anchor = ed.vim.visual_anchor;
4095 let cursor = ed.cursor();
4096 let (top, bot) = order(anchor, cursor);
4097 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4098 }
4099 Operator::Indent | Operator::Outdent => {
4100 ed.push_undo();
4101 let anchor = ed.vim.visual_anchor;
4102 let cursor = ed.cursor();
4103 let (top, bot) = order(anchor, cursor);
4104 if op == Operator::Indent {
4105 indent_rows(ed, top.0, bot.0, 1);
4106 } else {
4107 outdent_rows(ed, top.0, bot.0, 1);
4108 }
4109 ed.vim.mode = Mode::Normal;
4110 }
4111 Operator::Reflow => {
4112 ed.push_undo();
4113 let anchor = ed.vim.visual_anchor;
4114 let cursor = ed.cursor();
4115 let (top, bot) = order(anchor, cursor);
4116 reflow_rows(ed, top.0, bot.0);
4117 ed.vim.mode = Mode::Normal;
4118 }
4119 Operator::Fold => unreachable!("Visual zf takes its own path"),
4120 }
4121 }
4122 Mode::VisualBlock => apply_block_operator(ed, op),
4123 _ => {}
4124 }
4125}
4126
4127fn block_bounds<H: crate::types::Host>(
4132 ed: &Editor<hjkl_buffer::Buffer, H>,
4133) -> (usize, usize, usize, usize) {
4134 let (ar, ac) = ed.vim.block_anchor;
4135 let (cr, _) = ed.cursor();
4136 let cc = ed.vim.block_vcol;
4137 let top = ar.min(cr);
4138 let bot = ar.max(cr);
4139 let left = ac.min(cc);
4140 let right = ac.max(cc);
4141 (top, bot, left, right)
4142}
4143
4144fn update_block_vcol<H: crate::types::Host>(
4149 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4150 motion: &Motion,
4151) {
4152 match motion {
4153 Motion::Left
4154 | Motion::Right
4155 | Motion::WordFwd
4156 | Motion::BigWordFwd
4157 | Motion::WordBack
4158 | Motion::BigWordBack
4159 | Motion::WordEnd
4160 | Motion::BigWordEnd
4161 | Motion::WordEndBack
4162 | Motion::BigWordEndBack
4163 | Motion::LineStart
4164 | Motion::FirstNonBlank
4165 | Motion::LineEnd
4166 | Motion::Find { .. }
4167 | Motion::FindRepeat { .. }
4168 | Motion::MatchBracket => {
4169 ed.vim.block_vcol = ed.cursor().1;
4170 }
4171 _ => {}
4173 }
4174}
4175
4176fn apply_block_operator<H: crate::types::Host>(
4181 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4182 op: Operator,
4183) {
4184 let (top, bot, left, right) = block_bounds(ed);
4185 let yank = block_yank(ed, top, bot, left, right);
4187
4188 match op {
4189 Operator::Yank => {
4190 if !yank.is_empty() {
4191 ed.record_yank_to_host(yank.clone());
4192 ed.record_yank(yank, false);
4193 }
4194 ed.vim.mode = Mode::Normal;
4195 ed.jump_cursor(top, left);
4196 }
4197 Operator::Delete => {
4198 ed.push_undo();
4199 delete_block_contents(ed, top, bot, left, right);
4200 if !yank.is_empty() {
4201 ed.record_yank_to_host(yank.clone());
4202 ed.record_delete(yank, false);
4203 }
4204 ed.vim.mode = Mode::Normal;
4205 ed.jump_cursor(top, left);
4206 }
4207 Operator::Change => {
4208 ed.push_undo();
4209 delete_block_contents(ed, top, bot, left, right);
4210 if !yank.is_empty() {
4211 ed.record_yank_to_host(yank.clone());
4212 ed.record_delete(yank, false);
4213 }
4214 ed.jump_cursor(top, left);
4215 begin_insert_noundo(
4216 ed,
4217 1,
4218 InsertReason::BlockEdge {
4219 top,
4220 bot,
4221 col: left,
4222 },
4223 );
4224 }
4225 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4226 ed.push_undo();
4227 transform_block_case(ed, op, top, bot, left, right);
4228 ed.vim.mode = Mode::Normal;
4229 ed.jump_cursor(top, left);
4230 }
4231 Operator::Indent | Operator::Outdent => {
4232 ed.push_undo();
4236 if op == Operator::Indent {
4237 indent_rows(ed, top, bot, 1);
4238 } else {
4239 outdent_rows(ed, top, bot, 1);
4240 }
4241 ed.vim.mode = Mode::Normal;
4242 }
4243 Operator::Fold => unreachable!("Visual zf takes its own path"),
4244 Operator::Reflow => {
4245 ed.push_undo();
4249 reflow_rows(ed, top, bot);
4250 ed.vim.mode = Mode::Normal;
4251 }
4252 }
4253}
4254
4255fn transform_block_case<H: crate::types::Host>(
4259 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4260 op: Operator,
4261 top: usize,
4262 bot: usize,
4263 left: usize,
4264 right: usize,
4265) {
4266 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4267 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4268 let chars: Vec<char> = lines[r].chars().collect();
4269 if left >= chars.len() {
4270 continue;
4271 }
4272 let end = (right + 1).min(chars.len());
4273 let head: String = chars[..left].iter().collect();
4274 let mid: String = chars[left..end].iter().collect();
4275 let tail: String = chars[end..].iter().collect();
4276 let transformed = match op {
4277 Operator::Uppercase => mid.to_uppercase(),
4278 Operator::Lowercase => mid.to_lowercase(),
4279 Operator::ToggleCase => toggle_case_str(&mid),
4280 _ => mid,
4281 };
4282 lines[r] = format!("{head}{transformed}{tail}");
4283 }
4284 let saved_yank = ed.yank().to_string();
4285 let saved_linewise = ed.vim.yank_linewise;
4286 ed.restore(lines, (top, left));
4287 ed.set_yank(saved_yank);
4288 ed.vim.yank_linewise = saved_linewise;
4289}
4290
4291fn block_yank<H: crate::types::Host>(
4292 ed: &Editor<hjkl_buffer::Buffer, H>,
4293 top: usize,
4294 bot: usize,
4295 left: usize,
4296 right: usize,
4297) -> String {
4298 let lines = buf_lines_to_vec(&ed.buffer);
4299 let mut rows: Vec<String> = Vec::new();
4300 for r in top..=bot {
4301 let line = match lines.get(r) {
4302 Some(l) => l,
4303 None => break,
4304 };
4305 let chars: Vec<char> = line.chars().collect();
4306 let end = (right + 1).min(chars.len());
4307 if left >= chars.len() {
4308 rows.push(String::new());
4309 } else {
4310 rows.push(chars[left..end].iter().collect());
4311 }
4312 }
4313 rows.join("\n")
4314}
4315
4316fn delete_block_contents<H: crate::types::Host>(
4317 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4318 top: usize,
4319 bot: usize,
4320 left: usize,
4321 right: usize,
4322) {
4323 use hjkl_buffer::{Edit, MotionKind, Position};
4324 ed.sync_buffer_content_from_textarea();
4325 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4326 if last_row < top {
4327 return;
4328 }
4329 ed.mutate_edit(Edit::DeleteRange {
4330 start: Position::new(top, left),
4331 end: Position::new(last_row, right),
4332 kind: MotionKind::Block,
4333 });
4334 ed.push_buffer_cursor_to_textarea();
4335}
4336
4337fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4339 let (top, bot, left, right) = block_bounds(ed);
4340 ed.push_undo();
4341 ed.sync_buffer_content_from_textarea();
4342 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4343 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4344 let chars: Vec<char> = lines[r].chars().collect();
4345 if left >= chars.len() {
4346 continue;
4347 }
4348 let end = (right + 1).min(chars.len());
4349 let before: String = chars[..left].iter().collect();
4350 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4351 let after: String = chars[end..].iter().collect();
4352 lines[r] = format!("{before}{middle}{after}");
4353 }
4354 reset_textarea_lines(ed, lines);
4355 ed.vim.mode = Mode::Normal;
4356 ed.jump_cursor(top, left);
4357}
4358
4359fn reset_textarea_lines<H: crate::types::Host>(
4363 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4364 lines: Vec<String>,
4365) {
4366 let cursor = ed.cursor();
4367 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4368 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4369 ed.mark_content_dirty();
4370}
4371
4372type Pos = (usize, usize);
4378
4379fn text_object_range<H: crate::types::Host>(
4383 ed: &Editor<hjkl_buffer::Buffer, H>,
4384 obj: TextObject,
4385 inner: bool,
4386) -> Option<(Pos, Pos, MotionKind)> {
4387 match obj {
4388 TextObject::Word { big } => {
4389 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4390 }
4391 TextObject::Quote(q) => {
4392 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4393 }
4394 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4395 TextObject::Paragraph => {
4396 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4397 }
4398 TextObject::XmlTag => {
4399 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4400 }
4401 TextObject::Sentence => {
4402 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4403 }
4404 }
4405}
4406
4407fn sentence_boundary<H: crate::types::Host>(
4411 ed: &Editor<hjkl_buffer::Buffer, H>,
4412 forward: bool,
4413) -> Option<(usize, usize)> {
4414 let lines = buf_lines_to_vec(&ed.buffer);
4415 if lines.is_empty() {
4416 return None;
4417 }
4418 let pos_to_idx = |pos: (usize, usize)| -> usize {
4419 let mut idx = 0;
4420 for line in lines.iter().take(pos.0) {
4421 idx += line.chars().count() + 1;
4422 }
4423 idx + pos.1
4424 };
4425 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4426 for (r, line) in lines.iter().enumerate() {
4427 let len = line.chars().count();
4428 if idx <= len {
4429 return (r, idx);
4430 }
4431 idx -= len + 1;
4432 }
4433 let last = lines.len().saturating_sub(1);
4434 (last, lines[last].chars().count())
4435 };
4436 let mut chars: Vec<char> = Vec::new();
4437 for (r, line) in lines.iter().enumerate() {
4438 chars.extend(line.chars());
4439 if r + 1 < lines.len() {
4440 chars.push('\n');
4441 }
4442 }
4443 if chars.is_empty() {
4444 return None;
4445 }
4446 let total = chars.len();
4447 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4448 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4449
4450 if forward {
4451 let mut i = cursor_idx + 1;
4454 while i < total {
4455 if is_terminator(chars[i]) {
4456 while i + 1 < total && is_terminator(chars[i + 1]) {
4457 i += 1;
4458 }
4459 if i + 1 >= total {
4460 return None;
4461 }
4462 if chars[i + 1].is_whitespace() {
4463 let mut j = i + 1;
4464 while j < total && chars[j].is_whitespace() {
4465 j += 1;
4466 }
4467 if j >= total {
4468 return None;
4469 }
4470 return Some(idx_to_pos(j));
4471 }
4472 }
4473 i += 1;
4474 }
4475 None
4476 } else {
4477 let find_start = |from: usize| -> Option<usize> {
4481 let mut start = from;
4482 while start > 0 {
4483 let prev = chars[start - 1];
4484 if prev.is_whitespace() {
4485 let mut k = start - 1;
4486 while k > 0 && chars[k - 1].is_whitespace() {
4487 k -= 1;
4488 }
4489 if k > 0 && is_terminator(chars[k - 1]) {
4490 break;
4491 }
4492 }
4493 start -= 1;
4494 }
4495 while start < total && chars[start].is_whitespace() {
4496 start += 1;
4497 }
4498 (start < total).then_some(start)
4499 };
4500 let current_start = find_start(cursor_idx)?;
4501 if current_start < cursor_idx {
4502 return Some(idx_to_pos(current_start));
4503 }
4504 let mut k = current_start;
4507 while k > 0 && chars[k - 1].is_whitespace() {
4508 k -= 1;
4509 }
4510 if k == 0 {
4511 return None;
4512 }
4513 let prev_start = find_start(k - 1)?;
4514 Some(idx_to_pos(prev_start))
4515 }
4516}
4517
4518fn sentence_text_object<H: crate::types::Host>(
4524 ed: &Editor<hjkl_buffer::Buffer, H>,
4525 inner: bool,
4526) -> Option<((usize, usize), (usize, usize))> {
4527 let lines = buf_lines_to_vec(&ed.buffer);
4528 if lines.is_empty() {
4529 return None;
4530 }
4531 let pos_to_idx = |pos: (usize, usize)| -> usize {
4534 let mut idx = 0;
4535 for line in lines.iter().take(pos.0) {
4536 idx += line.chars().count() + 1;
4537 }
4538 idx + pos.1
4539 };
4540 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4541 for (r, line) in lines.iter().enumerate() {
4542 let len = line.chars().count();
4543 if idx <= len {
4544 return (r, idx);
4545 }
4546 idx -= len + 1;
4547 }
4548 let last = lines.len().saturating_sub(1);
4549 (last, lines[last].chars().count())
4550 };
4551 let mut chars: Vec<char> = Vec::new();
4552 for (r, line) in lines.iter().enumerate() {
4553 chars.extend(line.chars());
4554 if r + 1 < lines.len() {
4555 chars.push('\n');
4556 }
4557 }
4558 if chars.is_empty() {
4559 return None;
4560 }
4561
4562 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4563 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4564
4565 let mut start = cursor_idx;
4569 while start > 0 {
4570 let prev = chars[start - 1];
4571 if prev.is_whitespace() {
4572 let mut k = start - 1;
4576 while k > 0 && chars[k - 1].is_whitespace() {
4577 k -= 1;
4578 }
4579 if k > 0 && is_terminator(chars[k - 1]) {
4580 break;
4581 }
4582 }
4583 start -= 1;
4584 }
4585 while start < chars.len() && chars[start].is_whitespace() {
4588 start += 1;
4589 }
4590 if start >= chars.len() {
4591 return None;
4592 }
4593
4594 let mut end = start;
4597 while end < chars.len() {
4598 if is_terminator(chars[end]) {
4599 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4601 end += 1;
4602 }
4603 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4606 break;
4607 }
4608 }
4609 end += 1;
4610 }
4611 let end_idx = (end + 1).min(chars.len());
4613
4614 let final_end = if inner {
4615 end_idx
4616 } else {
4617 let mut e = end_idx;
4621 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4622 e += 1;
4623 }
4624 e
4625 };
4626
4627 Some((idx_to_pos(start), idx_to_pos(final_end)))
4628}
4629
4630fn tag_text_object<H: crate::types::Host>(
4634 ed: &Editor<hjkl_buffer::Buffer, H>,
4635 inner: bool,
4636) -> Option<((usize, usize), (usize, usize))> {
4637 let lines = buf_lines_to_vec(&ed.buffer);
4638 if lines.is_empty() {
4639 return None;
4640 }
4641 let pos_to_idx = |pos: (usize, usize)| -> usize {
4645 let mut idx = 0;
4646 for line in lines.iter().take(pos.0) {
4647 idx += line.chars().count() + 1;
4648 }
4649 idx + pos.1
4650 };
4651 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4652 for (r, line) in lines.iter().enumerate() {
4653 let len = line.chars().count();
4654 if idx <= len {
4655 return (r, idx);
4656 }
4657 idx -= len + 1;
4658 }
4659 let last = lines.len().saturating_sub(1);
4660 (last, lines[last].chars().count())
4661 };
4662 let mut chars: Vec<char> = Vec::new();
4663 for (r, line) in lines.iter().enumerate() {
4664 chars.extend(line.chars());
4665 if r + 1 < lines.len() {
4666 chars.push('\n');
4667 }
4668 }
4669 let cursor_idx = pos_to_idx(ed.cursor());
4670
4671 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4679 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4680 let mut i = 0;
4681 while i < chars.len() {
4682 if chars[i] != '<' {
4683 i += 1;
4684 continue;
4685 }
4686 let mut j = i + 1;
4687 while j < chars.len() && chars[j] != '>' {
4688 j += 1;
4689 }
4690 if j >= chars.len() {
4691 break;
4692 }
4693 let inside: String = chars[i + 1..j].iter().collect();
4694 let close_end = j + 1;
4695 let trimmed = inside.trim();
4696 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4697 i = close_end;
4698 continue;
4699 }
4700 if let Some(rest) = trimmed.strip_prefix('/') {
4701 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4702 if !name.is_empty()
4703 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4704 {
4705 let (open_start, content_start, _) = stack[stack_idx].clone();
4706 stack.truncate(stack_idx);
4707 let content_end = i;
4708 let candidate = (open_start, content_start, content_end, close_end);
4709 if cursor_idx >= content_start && cursor_idx <= content_end {
4710 innermost = match innermost {
4711 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4712 Some(candidate)
4713 }
4714 None => Some(candidate),
4715 existing => existing,
4716 };
4717 } else if open_start >= cursor_idx && next_after.is_none() {
4718 next_after = Some(candidate);
4719 }
4720 }
4721 } else if !trimmed.ends_with('/') {
4722 let name: String = trimmed
4723 .split(|c: char| c.is_whitespace() || c == '/')
4724 .next()
4725 .unwrap_or("")
4726 .to_string();
4727 if !name.is_empty() {
4728 stack.push((i, close_end, name));
4729 }
4730 }
4731 i = close_end;
4732 }
4733
4734 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4735 if inner {
4736 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4737 } else {
4738 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4739 }
4740}
4741
4742fn is_wordchar(c: char) -> bool {
4743 c.is_alphanumeric() || c == '_'
4744}
4745
4746pub(crate) use hjkl_buffer::is_keyword_char;
4750
4751fn word_text_object<H: crate::types::Host>(
4752 ed: &Editor<hjkl_buffer::Buffer, H>,
4753 inner: bool,
4754 big: bool,
4755) -> Option<((usize, usize), (usize, usize))> {
4756 let (row, col) = ed.cursor();
4757 let line = buf_line(&ed.buffer, row)?;
4758 let chars: Vec<char> = line.chars().collect();
4759 if chars.is_empty() {
4760 return None;
4761 }
4762 let at = col.min(chars.len().saturating_sub(1));
4763 let classify = |c: char| -> u8 {
4764 if c.is_whitespace() {
4765 0
4766 } else if big || is_wordchar(c) {
4767 1
4768 } else {
4769 2
4770 }
4771 };
4772 let cls = classify(chars[at]);
4773 let mut start = at;
4774 while start > 0 && classify(chars[start - 1]) == cls {
4775 start -= 1;
4776 }
4777 let mut end = at;
4778 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4779 end += 1;
4780 }
4781 let char_byte = |i: usize| {
4783 if i >= chars.len() {
4784 line.len()
4785 } else {
4786 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4787 }
4788 };
4789 let mut start_col = char_byte(start);
4790 let mut end_col = char_byte(end + 1);
4792 if !inner {
4793 let mut t = end + 1;
4795 let mut included_trailing = false;
4796 while t < chars.len() && chars[t].is_whitespace() {
4797 included_trailing = true;
4798 t += 1;
4799 }
4800 if included_trailing {
4801 end_col = char_byte(t);
4802 } else {
4803 let mut s = start;
4804 while s > 0 && chars[s - 1].is_whitespace() {
4805 s -= 1;
4806 }
4807 start_col = char_byte(s);
4808 }
4809 }
4810 Some(((row, start_col), (row, end_col)))
4811}
4812
4813fn quote_text_object<H: crate::types::Host>(
4814 ed: &Editor<hjkl_buffer::Buffer, H>,
4815 q: char,
4816 inner: bool,
4817) -> Option<((usize, usize), (usize, usize))> {
4818 let (row, col) = ed.cursor();
4819 let line = buf_line(&ed.buffer, row)?;
4820 let bytes = line.as_bytes();
4821 let q_byte = q as u8;
4822 let mut positions: Vec<usize> = Vec::new();
4824 for (i, &b) in bytes.iter().enumerate() {
4825 if b == q_byte {
4826 positions.push(i);
4827 }
4828 }
4829 if positions.len() < 2 {
4830 return None;
4831 }
4832 let mut open_idx: Option<usize> = None;
4833 let mut close_idx: Option<usize> = None;
4834 for pair in positions.chunks(2) {
4835 if pair.len() < 2 {
4836 break;
4837 }
4838 if col >= pair[0] && col <= pair[1] {
4839 open_idx = Some(pair[0]);
4840 close_idx = Some(pair[1]);
4841 break;
4842 }
4843 if col < pair[0] {
4844 open_idx = Some(pair[0]);
4845 close_idx = Some(pair[1]);
4846 break;
4847 }
4848 }
4849 let open = open_idx?;
4850 let close = close_idx?;
4851 if inner {
4853 if close <= open + 1 {
4854 return None;
4855 }
4856 Some(((row, open + 1), (row, close)))
4857 } else {
4858 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
4865 let mut end = after_close;
4867 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
4868 end += 1;
4869 }
4870 Some(((row, open), (row, end)))
4871 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
4872 let mut start = open;
4874 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
4875 start -= 1;
4876 }
4877 Some(((row, start), (row, close + 1)))
4878 } else {
4879 Some(((row, open), (row, close + 1)))
4880 }
4881 }
4882}
4883
4884fn bracket_text_object<H: crate::types::Host>(
4885 ed: &Editor<hjkl_buffer::Buffer, H>,
4886 open: char,
4887 inner: bool,
4888) -> Option<(Pos, Pos, MotionKind)> {
4889 let close = match open {
4890 '(' => ')',
4891 '[' => ']',
4892 '{' => '}',
4893 '<' => '>',
4894 _ => return None,
4895 };
4896 let (row, col) = ed.cursor();
4897 let lines = buf_lines_to_vec(&ed.buffer);
4898 let lines = lines.as_slice();
4899 let open_pos = find_open_bracket(lines, row, col, open, close)
4904 .or_else(|| find_next_open(lines, row, col, open))?;
4905 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
4906 if inner {
4908 if close_pos.0 > open_pos.0 + 1 {
4914 let inner_row_start = open_pos.0 + 1;
4916 let inner_row_end = close_pos.0 - 1;
4917 let end_col = lines
4918 .get(inner_row_end)
4919 .map(|l| l.chars().count())
4920 .unwrap_or(0);
4921 return Some((
4922 (inner_row_start, 0),
4923 (inner_row_end, end_col),
4924 MotionKind::Linewise,
4925 ));
4926 }
4927 let inner_start = advance_pos(lines, open_pos);
4928 if inner_start.0 > close_pos.0
4929 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
4930 {
4931 return None;
4932 }
4933 Some((inner_start, close_pos, MotionKind::Exclusive))
4934 } else {
4935 Some((
4936 open_pos,
4937 advance_pos(lines, close_pos),
4938 MotionKind::Exclusive,
4939 ))
4940 }
4941}
4942
4943fn find_open_bracket(
4944 lines: &[String],
4945 row: usize,
4946 col: usize,
4947 open: char,
4948 close: char,
4949) -> Option<(usize, usize)> {
4950 let mut depth: i32 = 0;
4951 let mut r = row;
4952 let mut c = col as isize;
4953 loop {
4954 let cur = &lines[r];
4955 let chars: Vec<char> = cur.chars().collect();
4956 if (c as usize) >= chars.len() {
4960 c = chars.len() as isize - 1;
4961 }
4962 while c >= 0 {
4963 let ch = chars[c as usize];
4964 if ch == close {
4965 depth += 1;
4966 } else if ch == open {
4967 if depth == 0 {
4968 return Some((r, c as usize));
4969 }
4970 depth -= 1;
4971 }
4972 c -= 1;
4973 }
4974 if r == 0 {
4975 return None;
4976 }
4977 r -= 1;
4978 c = lines[r].chars().count() as isize - 1;
4979 }
4980}
4981
4982fn find_close_bracket(
4983 lines: &[String],
4984 row: usize,
4985 start_col: usize,
4986 open: char,
4987 close: char,
4988) -> Option<(usize, usize)> {
4989 let mut depth: i32 = 0;
4990 let mut r = row;
4991 let mut c = start_col;
4992 loop {
4993 let cur = &lines[r];
4994 let chars: Vec<char> = cur.chars().collect();
4995 while c < chars.len() {
4996 let ch = chars[c];
4997 if ch == open {
4998 depth += 1;
4999 } else if ch == close {
5000 if depth == 0 {
5001 return Some((r, c));
5002 }
5003 depth -= 1;
5004 }
5005 c += 1;
5006 }
5007 if r + 1 >= lines.len() {
5008 return None;
5009 }
5010 r += 1;
5011 c = 0;
5012 }
5013}
5014
5015fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5019 let mut r = row;
5020 let mut c = col;
5021 while r < lines.len() {
5022 let chars: Vec<char> = lines[r].chars().collect();
5023 while c < chars.len() {
5024 if chars[c] == open {
5025 return Some((r, c));
5026 }
5027 c += 1;
5028 }
5029 r += 1;
5030 c = 0;
5031 }
5032 None
5033}
5034
5035fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5036 let (r, c) = pos;
5037 let line_len = lines[r].chars().count();
5038 if c < line_len {
5039 (r, c + 1)
5040 } else if r + 1 < lines.len() {
5041 (r + 1, 0)
5042 } else {
5043 pos
5044 }
5045}
5046
5047fn paragraph_text_object<H: crate::types::Host>(
5048 ed: &Editor<hjkl_buffer::Buffer, H>,
5049 inner: bool,
5050) -> Option<((usize, usize), (usize, usize))> {
5051 let (row, _) = ed.cursor();
5052 let lines = buf_lines_to_vec(&ed.buffer);
5053 if lines.is_empty() {
5054 return None;
5055 }
5056 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5058 if is_blank(row) {
5059 return None;
5060 }
5061 let mut top = row;
5062 while top > 0 && !is_blank(top - 1) {
5063 top -= 1;
5064 }
5065 let mut bot = row;
5066 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5067 bot += 1;
5068 }
5069 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5071 bot += 1;
5072 }
5073 let end_col = lines[bot].chars().count();
5074 Some(((top, 0), (bot, end_col)))
5075}
5076
5077fn read_vim_range<H: crate::types::Host>(
5083 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5084 start: (usize, usize),
5085 end: (usize, usize),
5086 kind: MotionKind,
5087) -> String {
5088 let (top, bot) = order(start, end);
5089 ed.sync_buffer_content_from_textarea();
5090 let lines = buf_lines_to_vec(&ed.buffer);
5091 match kind {
5092 MotionKind::Linewise => {
5093 let lo = top.0;
5094 let hi = bot.0.min(lines.len().saturating_sub(1));
5095 let mut text = lines[lo..=hi].join("\n");
5096 text.push('\n');
5097 text
5098 }
5099 MotionKind::Inclusive | MotionKind::Exclusive => {
5100 let inclusive = matches!(kind, MotionKind::Inclusive);
5101 let mut out = String::new();
5103 for row in top.0..=bot.0 {
5104 let line = lines.get(row).map(String::as_str).unwrap_or("");
5105 let lo = if row == top.0 { top.1 } else { 0 };
5106 let hi_unclamped = if row == bot.0 {
5107 if inclusive { bot.1 + 1 } else { bot.1 }
5108 } else {
5109 line.chars().count() + 1
5110 };
5111 let row_chars: Vec<char> = line.chars().collect();
5112 let hi = hi_unclamped.min(row_chars.len());
5113 if lo < hi {
5114 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5115 }
5116 if row < bot.0 {
5117 out.push('\n');
5118 }
5119 }
5120 out
5121 }
5122 }
5123}
5124
5125fn cut_vim_range<H: crate::types::Host>(
5134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5135 start: (usize, usize),
5136 end: (usize, usize),
5137 kind: MotionKind,
5138) -> String {
5139 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5140 let (top, bot) = order(start, end);
5141 ed.sync_buffer_content_from_textarea();
5142 let (buf_start, buf_end, buf_kind) = match kind {
5143 MotionKind::Linewise => (
5144 Position::new(top.0, 0),
5145 Position::new(bot.0, 0),
5146 BufKind::Line,
5147 ),
5148 MotionKind::Inclusive => {
5149 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5150 let next = if bot.1 < line_chars {
5154 Position::new(bot.0, bot.1 + 1)
5155 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5156 Position::new(bot.0 + 1, 0)
5157 } else {
5158 Position::new(bot.0, line_chars)
5159 };
5160 (Position::new(top.0, top.1), next, BufKind::Char)
5161 }
5162 MotionKind::Exclusive => (
5163 Position::new(top.0, top.1),
5164 Position::new(bot.0, bot.1),
5165 BufKind::Char,
5166 ),
5167 };
5168 let inverse = ed.mutate_edit(Edit::DeleteRange {
5169 start: buf_start,
5170 end: buf_end,
5171 kind: buf_kind,
5172 });
5173 let text = match inverse {
5174 Edit::InsertStr { text, .. } => text,
5175 _ => String::new(),
5176 };
5177 if !text.is_empty() {
5178 ed.record_yank_to_host(text.clone());
5179 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5180 }
5181 ed.push_buffer_cursor_to_textarea();
5182 text
5183}
5184
5185fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5191 use hjkl_buffer::{Edit, MotionKind, Position};
5192 ed.sync_buffer_content_from_textarea();
5193 let cursor = buf_cursor_pos(&ed.buffer);
5194 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5195 if cursor.col >= line_chars {
5196 return;
5197 }
5198 let inverse = ed.mutate_edit(Edit::DeleteRange {
5199 start: cursor,
5200 end: Position::new(cursor.row, line_chars),
5201 kind: MotionKind::Char,
5202 });
5203 if let Edit::InsertStr { text, .. } = inverse
5204 && !text.is_empty()
5205 {
5206 ed.record_yank_to_host(text.clone());
5207 ed.vim.yank_linewise = false;
5208 ed.set_yank(text);
5209 }
5210 buf_set_cursor_pos(&mut ed.buffer, cursor);
5211 ed.push_buffer_cursor_to_textarea();
5212}
5213
5214fn do_char_delete<H: crate::types::Host>(
5215 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5216 forward: bool,
5217 count: usize,
5218) {
5219 use hjkl_buffer::{Edit, MotionKind, Position};
5220 ed.push_undo();
5221 ed.sync_buffer_content_from_textarea();
5222 let mut deleted = String::new();
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 forward {
5229 if cursor.col >= line_chars {
5232 continue;
5233 }
5234 let inverse = ed.mutate_edit(Edit::DeleteRange {
5235 start: cursor,
5236 end: Position::new(cursor.row, cursor.col + 1),
5237 kind: MotionKind::Char,
5238 });
5239 if let Edit::InsertStr { text, .. } = inverse {
5240 deleted.push_str(&text);
5241 }
5242 } else {
5243 if cursor.col == 0 {
5245 continue;
5246 }
5247 let inverse = ed.mutate_edit(Edit::DeleteRange {
5248 start: Position::new(cursor.row, cursor.col - 1),
5249 end: cursor,
5250 kind: MotionKind::Char,
5251 });
5252 if let Edit::InsertStr { text, .. } = inverse {
5253 deleted = text + &deleted;
5256 }
5257 }
5258 }
5259 if !deleted.is_empty() {
5260 ed.record_yank_to_host(deleted.clone());
5261 ed.record_delete(deleted, false);
5262 }
5263 ed.push_buffer_cursor_to_textarea();
5264}
5265
5266fn adjust_number<H: crate::types::Host>(
5270 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5271 delta: i64,
5272) -> bool {
5273 use hjkl_buffer::{Edit, MotionKind, Position};
5274 ed.sync_buffer_content_from_textarea();
5275 let cursor = buf_cursor_pos(&ed.buffer);
5276 let row = cursor.row;
5277 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5278 Some(l) => l.chars().collect(),
5279 None => return false,
5280 };
5281 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5282 return false;
5283 };
5284 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5285 digit_start - 1
5286 } else {
5287 digit_start
5288 };
5289 let mut span_end = digit_start;
5290 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5291 span_end += 1;
5292 }
5293 let s: String = chars[span_start..span_end].iter().collect();
5294 let Ok(n) = s.parse::<i64>() else {
5295 return false;
5296 };
5297 let new_s = n.saturating_add(delta).to_string();
5298
5299 ed.push_undo();
5300 let span_start_pos = Position::new(row, span_start);
5301 let span_end_pos = Position::new(row, span_end);
5302 ed.mutate_edit(Edit::DeleteRange {
5303 start: span_start_pos,
5304 end: span_end_pos,
5305 kind: MotionKind::Char,
5306 });
5307 ed.mutate_edit(Edit::InsertStr {
5308 at: span_start_pos,
5309 text: new_s.clone(),
5310 });
5311 let new_len = new_s.chars().count();
5312 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5313 ed.push_buffer_cursor_to_textarea();
5314 true
5315}
5316
5317fn replace_char<H: crate::types::Host>(
5318 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5319 ch: char,
5320 count: usize,
5321) {
5322 use hjkl_buffer::{Edit, MotionKind, Position};
5323 ed.push_undo();
5324 ed.sync_buffer_content_from_textarea();
5325 for _ in 0..count {
5326 let cursor = buf_cursor_pos(&ed.buffer);
5327 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5328 if cursor.col >= line_chars {
5329 break;
5330 }
5331 ed.mutate_edit(Edit::DeleteRange {
5332 start: cursor,
5333 end: Position::new(cursor.row, cursor.col + 1),
5334 kind: MotionKind::Char,
5335 });
5336 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5337 }
5338 crate::motions::move_left(&mut ed.buffer, 1);
5340 ed.push_buffer_cursor_to_textarea();
5341}
5342
5343fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5344 use hjkl_buffer::{Edit, MotionKind, Position};
5345 ed.sync_buffer_content_from_textarea();
5346 let cursor = buf_cursor_pos(&ed.buffer);
5347 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5348 return;
5349 };
5350 let toggled = if c.is_uppercase() {
5351 c.to_lowercase().next().unwrap_or(c)
5352 } else {
5353 c.to_uppercase().next().unwrap_or(c)
5354 };
5355 ed.mutate_edit(Edit::DeleteRange {
5356 start: cursor,
5357 end: Position::new(cursor.row, cursor.col + 1),
5358 kind: MotionKind::Char,
5359 });
5360 ed.mutate_edit(Edit::InsertChar {
5361 at: cursor,
5362 ch: toggled,
5363 });
5364}
5365
5366fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5367 use hjkl_buffer::{Edit, Position};
5368 ed.sync_buffer_content_from_textarea();
5369 let row = buf_cursor_pos(&ed.buffer).row;
5370 if row + 1 >= buf_row_count(&ed.buffer) {
5371 return;
5372 }
5373 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5374 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5375 let next_trimmed = next_raw.trim_start();
5376 let cur_chars = cur_line.chars().count();
5377 let next_chars = next_raw.chars().count();
5378 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5381 " "
5382 } else {
5383 ""
5384 };
5385 let joined = format!("{cur_line}{separator}{next_trimmed}");
5386 ed.mutate_edit(Edit::Replace {
5387 start: Position::new(row, 0),
5388 end: Position::new(row + 1, next_chars),
5389 with: joined,
5390 });
5391 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5395 ed.push_buffer_cursor_to_textarea();
5396}
5397
5398fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5401 use hjkl_buffer::Edit;
5402 ed.sync_buffer_content_from_textarea();
5403 let row = buf_cursor_pos(&ed.buffer).row;
5404 if row + 1 >= buf_row_count(&ed.buffer) {
5405 return;
5406 }
5407 let join_col = buf_line_chars(&ed.buffer, row);
5408 ed.mutate_edit(Edit::JoinLines {
5409 row,
5410 count: 1,
5411 with_space: false,
5412 });
5413 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5415 ed.push_buffer_cursor_to_textarea();
5416}
5417
5418fn do_paste<H: crate::types::Host>(
5419 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5420 before: bool,
5421 count: usize,
5422) {
5423 use hjkl_buffer::{Edit, Position};
5424 ed.push_undo();
5425 let selector = ed.vim.pending_register.take();
5430 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5431 Some(slot) => (slot.text.clone(), slot.linewise),
5432 None => {
5438 let s = &ed.registers().unnamed;
5439 (s.text.clone(), s.linewise)
5440 }
5441 };
5442 for _ in 0..count {
5443 ed.sync_buffer_content_from_textarea();
5444 let yank = yank.clone();
5445 if yank.is_empty() {
5446 continue;
5447 }
5448 if linewise {
5449 let text = yank.trim_matches('\n').to_string();
5453 let row = buf_cursor_pos(&ed.buffer).row;
5454 let target_row = if before {
5455 ed.mutate_edit(Edit::InsertStr {
5456 at: Position::new(row, 0),
5457 text: format!("{text}\n"),
5458 });
5459 row
5460 } else {
5461 let line_chars = buf_line_chars(&ed.buffer, row);
5462 ed.mutate_edit(Edit::InsertStr {
5463 at: Position::new(row, line_chars),
5464 text: format!("\n{text}"),
5465 });
5466 row + 1
5467 };
5468 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5469 crate::motions::move_first_non_blank(&mut ed.buffer);
5470 ed.push_buffer_cursor_to_textarea();
5471 } else {
5472 let cursor = buf_cursor_pos(&ed.buffer);
5476 let at = if before {
5477 cursor
5478 } else {
5479 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5480 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5481 };
5482 ed.mutate_edit(Edit::InsertStr {
5483 at,
5484 text: yank.clone(),
5485 });
5486 crate::motions::move_left(&mut ed.buffer, 1);
5489 ed.push_buffer_cursor_to_textarea();
5490 }
5491 }
5492 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5494}
5495
5496pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5497 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5498 let current = ed.snapshot();
5499 ed.redo_stack.push(current);
5500 ed.restore(lines, cursor);
5501 }
5502 ed.vim.mode = Mode::Normal;
5503 clamp_cursor_to_normal_mode(ed);
5507}
5508
5509pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5510 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5511 let current = ed.snapshot();
5512 ed.undo_stack.push(current);
5513 ed.cap_undo();
5514 ed.restore(lines, cursor);
5515 }
5516 ed.vim.mode = Mode::Normal;
5517}
5518
5519fn replay_insert_and_finish<H: crate::types::Host>(
5526 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5527 text: &str,
5528) {
5529 use hjkl_buffer::{Edit, Position};
5530 let cursor = ed.cursor();
5531 ed.mutate_edit(Edit::InsertStr {
5532 at: Position::new(cursor.0, cursor.1),
5533 text: text.to_string(),
5534 });
5535 if ed.vim.insert_session.take().is_some() {
5536 if ed.cursor().1 > 0 {
5537 crate::motions::move_left(&mut ed.buffer, 1);
5538 ed.push_buffer_cursor_to_textarea();
5539 }
5540 ed.vim.mode = Mode::Normal;
5541 }
5542}
5543
5544fn replay_last_change<H: crate::types::Host>(
5545 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5546 outer_count: usize,
5547) {
5548 let Some(change) = ed.vim.last_change.clone() else {
5549 return;
5550 };
5551 ed.vim.replaying = true;
5552 let scale = if outer_count > 0 { outer_count } else { 1 };
5553 match change {
5554 LastChange::OpMotion {
5555 op,
5556 motion,
5557 count,
5558 inserted,
5559 } => {
5560 let total = count.max(1) * scale;
5561 apply_op_with_motion(ed, op, &motion, total);
5562 if let Some(text) = inserted {
5563 replay_insert_and_finish(ed, &text);
5564 }
5565 }
5566 LastChange::OpTextObj {
5567 op,
5568 obj,
5569 inner,
5570 inserted,
5571 } => {
5572 apply_op_with_text_object(ed, op, obj, inner);
5573 if let Some(text) = inserted {
5574 replay_insert_and_finish(ed, &text);
5575 }
5576 }
5577 LastChange::LineOp {
5578 op,
5579 count,
5580 inserted,
5581 } => {
5582 let total = count.max(1) * scale;
5583 execute_line_op(ed, op, total);
5584 if let Some(text) = inserted {
5585 replay_insert_and_finish(ed, &text);
5586 }
5587 }
5588 LastChange::CharDel { forward, count } => {
5589 do_char_delete(ed, forward, count * scale);
5590 }
5591 LastChange::ReplaceChar { ch, count } => {
5592 replace_char(ed, ch, count * scale);
5593 }
5594 LastChange::ToggleCase { count } => {
5595 for _ in 0..count * scale {
5596 ed.push_undo();
5597 toggle_case_at_cursor(ed);
5598 }
5599 }
5600 LastChange::JoinLine { count } => {
5601 for _ in 0..count * scale {
5602 ed.push_undo();
5603 join_line(ed);
5604 }
5605 }
5606 LastChange::Paste { before, count } => {
5607 do_paste(ed, before, count * scale);
5608 }
5609 LastChange::DeleteToEol { inserted } => {
5610 use hjkl_buffer::{Edit, Position};
5611 ed.push_undo();
5612 delete_to_eol(ed);
5613 if let Some(text) = inserted {
5614 let cursor = ed.cursor();
5615 ed.mutate_edit(Edit::InsertStr {
5616 at: Position::new(cursor.0, cursor.1),
5617 text,
5618 });
5619 }
5620 }
5621 LastChange::OpenLine { above, inserted } => {
5622 use hjkl_buffer::{Edit, Position};
5623 ed.push_undo();
5624 ed.sync_buffer_content_from_textarea();
5625 let row = buf_cursor_pos(&ed.buffer).row;
5626 if above {
5627 ed.mutate_edit(Edit::InsertStr {
5628 at: Position::new(row, 0),
5629 text: "\n".to_string(),
5630 });
5631 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5632 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5633 } else {
5634 let line_chars = buf_line_chars(&ed.buffer, row);
5635 ed.mutate_edit(Edit::InsertStr {
5636 at: Position::new(row, line_chars),
5637 text: "\n".to_string(),
5638 });
5639 }
5640 ed.push_buffer_cursor_to_textarea();
5641 let cursor = ed.cursor();
5642 ed.mutate_edit(Edit::InsertStr {
5643 at: Position::new(cursor.0, cursor.1),
5644 text: inserted,
5645 });
5646 }
5647 LastChange::InsertAt {
5648 entry,
5649 inserted,
5650 count,
5651 } => {
5652 use hjkl_buffer::{Edit, Position};
5653 ed.push_undo();
5654 match entry {
5655 InsertEntry::I => {}
5656 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5657 InsertEntry::A => {
5658 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5659 ed.push_buffer_cursor_to_textarea();
5660 }
5661 InsertEntry::ShiftA => {
5662 crate::motions::move_line_end(&mut ed.buffer);
5663 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5664 ed.push_buffer_cursor_to_textarea();
5665 }
5666 }
5667 for _ in 0..count.max(1) {
5668 let cursor = ed.cursor();
5669 ed.mutate_edit(Edit::InsertStr {
5670 at: Position::new(cursor.0, cursor.1),
5671 text: inserted.clone(),
5672 });
5673 }
5674 }
5675 }
5676 ed.vim.replaying = false;
5677}
5678
5679fn extract_inserted(before: &str, after: &str) -> String {
5682 let before_chars: Vec<char> = before.chars().collect();
5683 let after_chars: Vec<char> = after.chars().collect();
5684 if after_chars.len() <= before_chars.len() {
5685 return String::new();
5686 }
5687 let prefix = before_chars
5688 .iter()
5689 .zip(after_chars.iter())
5690 .take_while(|(a, b)| a == b)
5691 .count();
5692 let max_suffix = before_chars.len() - prefix;
5693 let suffix = before_chars
5694 .iter()
5695 .rev()
5696 .zip(after_chars.iter().rev())
5697 .take(max_suffix)
5698 .take_while(|(a, b)| a == b)
5699 .count();
5700 after_chars[prefix..after_chars.len() - suffix]
5701 .iter()
5702 .collect()
5703}
5704
5705#[cfg(all(test, feature = "crossterm"))]
5708mod tests {
5709 use crate::VimMode;
5710 use crate::editor::Editor;
5711 use crate::types::Host;
5712 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5713
5714 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5715 let mut iter = keys.chars().peekable();
5719 while let Some(c) = iter.next() {
5720 if c == '<' {
5721 let mut tag = String::new();
5722 for ch in iter.by_ref() {
5723 if ch == '>' {
5724 break;
5725 }
5726 tag.push(ch);
5727 }
5728 let ev = match tag.as_str() {
5729 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5730 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5731 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5732 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5733 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5734 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5735 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5736 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5737 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5741 s if s.starts_with("C-") => {
5742 let ch = s.chars().nth(2).unwrap();
5743 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5744 }
5745 _ => continue,
5746 };
5747 e.handle_key(ev);
5748 } else {
5749 let mods = if c.is_uppercase() {
5750 KeyModifiers::SHIFT
5751 } else {
5752 KeyModifiers::NONE
5753 };
5754 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5755 }
5756 }
5757 }
5758
5759 fn editor_with(content: &str) -> Editor {
5760 let opts = crate::types::Options {
5765 shiftwidth: 2,
5766 ..crate::types::Options::default()
5767 };
5768 let mut e = Editor::new(
5769 hjkl_buffer::Buffer::new(),
5770 crate::types::DefaultHost::new(),
5771 opts,
5772 );
5773 e.set_content(content);
5774 e
5775 }
5776
5777 #[test]
5778 fn f_char_jumps_on_line() {
5779 let mut e = editor_with("hello world");
5780 run_keys(&mut e, "fw");
5781 assert_eq!(e.cursor(), (0, 6));
5782 }
5783
5784 #[test]
5785 fn cap_f_jumps_backward() {
5786 let mut e = editor_with("hello world");
5787 e.jump_cursor(0, 10);
5788 run_keys(&mut e, "Fo");
5789 assert_eq!(e.cursor().1, 7);
5790 }
5791
5792 #[test]
5793 fn t_stops_before_char() {
5794 let mut e = editor_with("hello");
5795 run_keys(&mut e, "tl");
5796 assert_eq!(e.cursor(), (0, 1));
5797 }
5798
5799 #[test]
5800 fn semicolon_repeats_find() {
5801 let mut e = editor_with("aa.bb.cc");
5802 run_keys(&mut e, "f.");
5803 assert_eq!(e.cursor().1, 2);
5804 run_keys(&mut e, ";");
5805 assert_eq!(e.cursor().1, 5);
5806 }
5807
5808 #[test]
5809 fn comma_repeats_find_reverse() {
5810 let mut e = editor_with("aa.bb.cc");
5811 run_keys(&mut e, "f.");
5812 run_keys(&mut e, ";");
5813 run_keys(&mut e, ",");
5814 assert_eq!(e.cursor().1, 2);
5815 }
5816
5817 #[test]
5818 fn di_quote_deletes_content() {
5819 let mut e = editor_with("foo \"bar\" baz");
5820 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5822 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5823 }
5824
5825 #[test]
5826 fn da_quote_deletes_with_quotes() {
5827 let mut e = editor_with("foo \"bar\" baz");
5830 e.jump_cursor(0, 6);
5831 run_keys(&mut e, "da\"");
5832 assert_eq!(e.buffer().lines()[0], "foo baz");
5833 }
5834
5835 #[test]
5836 fn ci_paren_deletes_and_inserts() {
5837 let mut e = editor_with("fn(a, b, c)");
5838 e.jump_cursor(0, 5);
5839 run_keys(&mut e, "ci(");
5840 assert_eq!(e.vim_mode(), VimMode::Insert);
5841 assert_eq!(e.buffer().lines()[0], "fn()");
5842 }
5843
5844 #[test]
5845 fn diw_deletes_inner_word() {
5846 let mut e = editor_with("hello world");
5847 e.jump_cursor(0, 2);
5848 run_keys(&mut e, "diw");
5849 assert_eq!(e.buffer().lines()[0], " world");
5850 }
5851
5852 #[test]
5853 fn daw_deletes_word_with_trailing_space() {
5854 let mut e = editor_with("hello world");
5855 run_keys(&mut e, "daw");
5856 assert_eq!(e.buffer().lines()[0], "world");
5857 }
5858
5859 #[test]
5860 fn percent_jumps_to_matching_bracket() {
5861 let mut e = editor_with("foo(bar)");
5862 e.jump_cursor(0, 3);
5863 run_keys(&mut e, "%");
5864 assert_eq!(e.cursor().1, 7);
5865 run_keys(&mut e, "%");
5866 assert_eq!(e.cursor().1, 3);
5867 }
5868
5869 #[test]
5870 fn dot_repeats_last_change() {
5871 let mut e = editor_with("aaa bbb ccc");
5872 run_keys(&mut e, "dw");
5873 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5874 run_keys(&mut e, ".");
5875 assert_eq!(e.buffer().lines()[0], "ccc");
5876 }
5877
5878 #[test]
5879 fn dot_repeats_change_operator_with_text() {
5880 let mut e = editor_with("foo foo foo");
5881 run_keys(&mut e, "cwbar<Esc>");
5882 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5883 run_keys(&mut e, "w");
5885 run_keys(&mut e, ".");
5886 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5887 }
5888
5889 #[test]
5890 fn dot_repeats_x() {
5891 let mut e = editor_with("abcdef");
5892 run_keys(&mut e, "x");
5893 run_keys(&mut e, "..");
5894 assert_eq!(e.buffer().lines()[0], "def");
5895 }
5896
5897 #[test]
5898 fn count_operator_motion_compose() {
5899 let mut e = editor_with("one two three four five");
5900 run_keys(&mut e, "d3w");
5901 assert_eq!(e.buffer().lines()[0], "four five");
5902 }
5903
5904 #[test]
5905 fn two_dd_deletes_two_lines() {
5906 let mut e = editor_with("a\nb\nc");
5907 run_keys(&mut e, "2dd");
5908 assert_eq!(e.buffer().lines().len(), 1);
5909 assert_eq!(e.buffer().lines()[0], "c");
5910 }
5911
5912 #[test]
5917 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5918 let mut e = editor_with("one\ntwo\n three\nfour");
5919 e.jump_cursor(1, 2);
5920 run_keys(&mut e, "dd");
5921 assert_eq!(e.buffer().lines()[1], " three");
5923 assert_eq!(e.cursor(), (1, 4));
5924 }
5925
5926 #[test]
5927 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5928 let mut e = editor_with("one\n two\nthree");
5929 e.jump_cursor(2, 0);
5930 run_keys(&mut e, "dd");
5931 assert_eq!(e.buffer().lines().len(), 2);
5933 assert_eq!(e.cursor(), (1, 2));
5934 }
5935
5936 #[test]
5937 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5938 let mut e = editor_with("lonely");
5939 run_keys(&mut e, "dd");
5940 assert_eq!(e.buffer().lines().len(), 1);
5941 assert_eq!(e.buffer().lines()[0], "");
5942 assert_eq!(e.cursor(), (0, 0));
5943 }
5944
5945 #[test]
5946 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5947 let mut e = editor_with("a\nb\nc\n d\ne");
5948 e.jump_cursor(1, 0);
5950 run_keys(&mut e, "3dd");
5951 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
5952 assert_eq!(e.cursor(), (1, 0));
5953 }
5954
5955 #[test]
5956 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
5957 let mut e = editor_with(" line one\n line two\n xyz!");
5976 e.jump_cursor(0, 8);
5978 assert_eq!(e.cursor(), (0, 8));
5979 run_keys(&mut e, "dd");
5982 assert_eq!(
5983 e.cursor(),
5984 (0, 4),
5985 "dd must place cursor on first-non-blank"
5986 );
5987 run_keys(&mut e, "j");
5991 let (row, col) = e.cursor();
5992 assert_eq!(row, 1);
5993 assert_eq!(
5994 col, 4,
5995 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
5996 );
5997 }
5998
5999 #[test]
6000 fn gu_lowercases_motion_range() {
6001 let mut e = editor_with("HELLO WORLD");
6002 run_keys(&mut e, "guw");
6003 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6004 assert_eq!(e.cursor(), (0, 0));
6005 }
6006
6007 #[test]
6008 fn g_u_uppercases_text_object() {
6009 let mut e = editor_with("hello world");
6010 run_keys(&mut e, "gUiw");
6012 assert_eq!(e.buffer().lines()[0], "HELLO world");
6013 assert_eq!(e.cursor(), (0, 0));
6014 }
6015
6016 #[test]
6017 fn g_tilde_toggles_case_of_range() {
6018 let mut e = editor_with("Hello World");
6019 run_keys(&mut e, "g~iw");
6020 assert_eq!(e.buffer().lines()[0], "hELLO World");
6021 }
6022
6023 #[test]
6024 fn g_uu_uppercases_current_line() {
6025 let mut e = editor_with("select 1\nselect 2");
6026 run_keys(&mut e, "gUU");
6027 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6028 assert_eq!(e.buffer().lines()[1], "select 2");
6029 }
6030
6031 #[test]
6032 fn gugu_lowercases_current_line() {
6033 let mut e = editor_with("FOO BAR\nBAZ");
6034 run_keys(&mut e, "gugu");
6035 assert_eq!(e.buffer().lines()[0], "foo bar");
6036 }
6037
6038 #[test]
6039 fn visual_u_uppercases_selection() {
6040 let mut e = editor_with("hello world");
6041 run_keys(&mut e, "veU");
6043 assert_eq!(e.buffer().lines()[0], "HELLO world");
6044 }
6045
6046 #[test]
6047 fn visual_line_u_lowercases_line() {
6048 let mut e = editor_with("HELLO WORLD\nOTHER");
6049 run_keys(&mut e, "Vu");
6050 assert_eq!(e.buffer().lines()[0], "hello world");
6051 assert_eq!(e.buffer().lines()[1], "OTHER");
6052 }
6053
6054 #[test]
6055 fn g_uu_with_count_uppercases_multiple_lines() {
6056 let mut e = editor_with("one\ntwo\nthree\nfour");
6057 run_keys(&mut e, "3gUU");
6059 assert_eq!(e.buffer().lines()[0], "ONE");
6060 assert_eq!(e.buffer().lines()[1], "TWO");
6061 assert_eq!(e.buffer().lines()[2], "THREE");
6062 assert_eq!(e.buffer().lines()[3], "four");
6063 }
6064
6065 #[test]
6066 fn double_gt_indents_current_line() {
6067 let mut e = editor_with("hello");
6068 run_keys(&mut e, ">>");
6069 assert_eq!(e.buffer().lines()[0], " hello");
6070 assert_eq!(e.cursor(), (0, 2));
6072 }
6073
6074 #[test]
6075 fn double_lt_outdents_current_line() {
6076 let mut e = editor_with(" hello");
6077 run_keys(&mut e, "<lt><lt>");
6078 assert_eq!(e.buffer().lines()[0], " hello");
6079 assert_eq!(e.cursor(), (0, 2));
6080 }
6081
6082 #[test]
6083 fn count_double_gt_indents_multiple_lines() {
6084 let mut e = editor_with("a\nb\nc\nd");
6085 run_keys(&mut e, "3>>");
6087 assert_eq!(e.buffer().lines()[0], " a");
6088 assert_eq!(e.buffer().lines()[1], " b");
6089 assert_eq!(e.buffer().lines()[2], " c");
6090 assert_eq!(e.buffer().lines()[3], "d");
6091 }
6092
6093 #[test]
6094 fn outdent_clips_ragged_leading_whitespace() {
6095 let mut e = editor_with(" x");
6098 run_keys(&mut e, "<lt><lt>");
6099 assert_eq!(e.buffer().lines()[0], "x");
6100 }
6101
6102 #[test]
6103 fn indent_motion_is_always_linewise() {
6104 let mut e = editor_with("foo bar");
6107 run_keys(&mut e, ">w");
6108 assert_eq!(e.buffer().lines()[0], " foo bar");
6109 }
6110
6111 #[test]
6112 fn indent_text_object_extends_over_paragraph() {
6113 let mut e = editor_with("a\nb\n\nc\nd");
6114 run_keys(&mut e, ">ap");
6116 assert_eq!(e.buffer().lines()[0], " a");
6117 assert_eq!(e.buffer().lines()[1], " b");
6118 assert_eq!(e.buffer().lines()[2], "");
6119 assert_eq!(e.buffer().lines()[3], "c");
6120 }
6121
6122 #[test]
6123 fn visual_line_indent_shifts_selected_rows() {
6124 let mut e = editor_with("x\ny\nz");
6125 run_keys(&mut e, "Vj>");
6127 assert_eq!(e.buffer().lines()[0], " x");
6128 assert_eq!(e.buffer().lines()[1], " y");
6129 assert_eq!(e.buffer().lines()[2], "z");
6130 }
6131
6132 #[test]
6133 fn outdent_empty_line_is_noop() {
6134 let mut e = editor_with("\nfoo");
6135 run_keys(&mut e, "<lt><lt>");
6136 assert_eq!(e.buffer().lines()[0], "");
6137 }
6138
6139 #[test]
6140 fn indent_skips_empty_lines() {
6141 let mut e = editor_with("");
6144 run_keys(&mut e, ">>");
6145 assert_eq!(e.buffer().lines()[0], "");
6146 }
6147
6148 #[test]
6149 fn insert_ctrl_t_indents_current_line() {
6150 let mut e = editor_with("x");
6151 run_keys(&mut e, "i<C-t>");
6153 assert_eq!(e.buffer().lines()[0], " x");
6154 assert_eq!(e.cursor(), (0, 2));
6157 }
6158
6159 #[test]
6160 fn insert_ctrl_d_outdents_current_line() {
6161 let mut e = editor_with(" x");
6162 run_keys(&mut e, "A<C-d>");
6164 assert_eq!(e.buffer().lines()[0], " x");
6165 }
6166
6167 #[test]
6168 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6169 let mut e = editor_with("first\nsecond");
6170 e.jump_cursor(1, 0);
6171 run_keys(&mut e, "h");
6172 assert_eq!(e.cursor(), (1, 0));
6174 }
6175
6176 #[test]
6177 fn l_at_last_char_does_not_wrap_to_next_line() {
6178 let mut e = editor_with("ab\ncd");
6179 e.jump_cursor(0, 1);
6181 run_keys(&mut e, "l");
6182 assert_eq!(e.cursor(), (0, 1));
6184 }
6185
6186 #[test]
6187 fn count_l_clamps_at_line_end() {
6188 let mut e = editor_with("abcde");
6189 run_keys(&mut e, "20l");
6192 assert_eq!(e.cursor(), (0, 4));
6193 }
6194
6195 #[test]
6196 fn count_h_clamps_at_col_zero() {
6197 let mut e = editor_with("abcde");
6198 e.jump_cursor(0, 3);
6199 run_keys(&mut e, "20h");
6200 assert_eq!(e.cursor(), (0, 0));
6201 }
6202
6203 #[test]
6204 fn dl_on_last_char_still_deletes_it() {
6205 let mut e = editor_with("ab");
6209 e.jump_cursor(0, 1);
6210 run_keys(&mut e, "dl");
6211 assert_eq!(e.buffer().lines()[0], "a");
6212 }
6213
6214 #[test]
6215 fn case_op_preserves_yank_register() {
6216 let mut e = editor_with("target");
6217 run_keys(&mut e, "yy");
6218 let yank_before = e.yank().to_string();
6219 run_keys(&mut e, "gUU");
6221 assert_eq!(e.buffer().lines()[0], "TARGET");
6222 assert_eq!(
6223 e.yank(),
6224 yank_before,
6225 "case ops must preserve the yank buffer"
6226 );
6227 }
6228
6229 #[test]
6230 fn dap_deletes_paragraph() {
6231 let mut e = editor_with("a\nb\n\nc\nd");
6232 run_keys(&mut e, "dap");
6233 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6234 }
6235
6236 #[test]
6237 fn dit_deletes_inner_tag_content() {
6238 let mut e = editor_with("<b>hello</b>");
6239 e.jump_cursor(0, 4);
6241 run_keys(&mut e, "dit");
6242 assert_eq!(e.buffer().lines()[0], "<b></b>");
6243 }
6244
6245 #[test]
6246 fn dat_deletes_around_tag() {
6247 let mut e = editor_with("hi <b>foo</b> bye");
6248 e.jump_cursor(0, 6);
6249 run_keys(&mut e, "dat");
6250 assert_eq!(e.buffer().lines()[0], "hi bye");
6251 }
6252
6253 #[test]
6254 fn dit_picks_innermost_tag() {
6255 let mut e = editor_with("<a><b>x</b></a>");
6256 e.jump_cursor(0, 6);
6258 run_keys(&mut e, "dit");
6259 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6261 }
6262
6263 #[test]
6264 fn dat_innermost_tag_pair() {
6265 let mut e = editor_with("<a><b>x</b></a>");
6266 e.jump_cursor(0, 6);
6267 run_keys(&mut e, "dat");
6268 assert_eq!(e.buffer().lines()[0], "<a></a>");
6269 }
6270
6271 #[test]
6272 fn dit_outside_any_tag_no_op() {
6273 let mut e = editor_with("plain text");
6274 e.jump_cursor(0, 3);
6275 run_keys(&mut e, "dit");
6276 assert_eq!(e.buffer().lines()[0], "plain text");
6278 }
6279
6280 #[test]
6281 fn cit_changes_inner_tag_content() {
6282 let mut e = editor_with("<b>hello</b>");
6283 e.jump_cursor(0, 4);
6284 run_keys(&mut e, "citNEW<Esc>");
6285 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6286 }
6287
6288 #[test]
6289 fn cat_changes_around_tag() {
6290 let mut e = editor_with("hi <b>foo</b> bye");
6291 e.jump_cursor(0, 6);
6292 run_keys(&mut e, "catBAR<Esc>");
6293 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6294 }
6295
6296 #[test]
6297 fn yit_yanks_inner_tag_content() {
6298 let mut e = editor_with("<b>hello</b>");
6299 e.jump_cursor(0, 4);
6300 run_keys(&mut e, "yit");
6301 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6302 }
6303
6304 #[test]
6305 fn yat_yanks_full_tag_pair() {
6306 let mut e = editor_with("hi <b>foo</b> bye");
6307 e.jump_cursor(0, 6);
6308 run_keys(&mut e, "yat");
6309 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6310 }
6311
6312 #[test]
6313 fn vit_visually_selects_inner_tag() {
6314 let mut e = editor_with("<b>hello</b>");
6315 e.jump_cursor(0, 4);
6316 run_keys(&mut e, "vit");
6317 assert_eq!(e.vim_mode(), VimMode::Visual);
6318 run_keys(&mut e, "y");
6319 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6320 }
6321
6322 #[test]
6323 fn vat_visually_selects_around_tag() {
6324 let mut e = editor_with("x<b>foo</b>y");
6325 e.jump_cursor(0, 5);
6326 run_keys(&mut e, "vat");
6327 assert_eq!(e.vim_mode(), VimMode::Visual);
6328 run_keys(&mut e, "y");
6329 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6330 }
6331
6332 #[test]
6335 #[allow(non_snake_case)]
6336 fn diW_deletes_inner_big_word() {
6337 let mut e = editor_with("foo.bar baz");
6338 e.jump_cursor(0, 2);
6339 run_keys(&mut e, "diW");
6340 assert_eq!(e.buffer().lines()[0], " baz");
6342 }
6343
6344 #[test]
6345 #[allow(non_snake_case)]
6346 fn daW_deletes_around_big_word() {
6347 let mut e = editor_with("foo.bar baz");
6348 e.jump_cursor(0, 2);
6349 run_keys(&mut e, "daW");
6350 assert_eq!(e.buffer().lines()[0], "baz");
6351 }
6352
6353 #[test]
6354 fn di_double_quote_deletes_inside() {
6355 let mut e = editor_with("a \"hello\" b");
6356 e.jump_cursor(0, 4);
6357 run_keys(&mut e, "di\"");
6358 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6359 }
6360
6361 #[test]
6362 fn da_double_quote_deletes_around() {
6363 let mut e = editor_with("a \"hello\" b");
6365 e.jump_cursor(0, 4);
6366 run_keys(&mut e, "da\"");
6367 assert_eq!(e.buffer().lines()[0], "a b");
6368 }
6369
6370 #[test]
6371 fn di_single_quote_deletes_inside() {
6372 let mut e = editor_with("x 'foo' y");
6373 e.jump_cursor(0, 4);
6374 run_keys(&mut e, "di'");
6375 assert_eq!(e.buffer().lines()[0], "x '' y");
6376 }
6377
6378 #[test]
6379 fn da_single_quote_deletes_around() {
6380 let mut e = editor_with("x 'foo' y");
6382 e.jump_cursor(0, 4);
6383 run_keys(&mut e, "da'");
6384 assert_eq!(e.buffer().lines()[0], "x y");
6385 }
6386
6387 #[test]
6388 fn di_backtick_deletes_inside() {
6389 let mut e = editor_with("p `q` r");
6390 e.jump_cursor(0, 3);
6391 run_keys(&mut e, "di`");
6392 assert_eq!(e.buffer().lines()[0], "p `` r");
6393 }
6394
6395 #[test]
6396 fn da_backtick_deletes_around() {
6397 let mut e = editor_with("p `q` r");
6399 e.jump_cursor(0, 3);
6400 run_keys(&mut e, "da`");
6401 assert_eq!(e.buffer().lines()[0], "p r");
6402 }
6403
6404 #[test]
6405 fn di_paren_deletes_inside() {
6406 let mut e = editor_with("f(arg)");
6407 e.jump_cursor(0, 3);
6408 run_keys(&mut e, "di(");
6409 assert_eq!(e.buffer().lines()[0], "f()");
6410 }
6411
6412 #[test]
6413 fn di_paren_alias_b_works() {
6414 let mut e = editor_with("f(arg)");
6415 e.jump_cursor(0, 3);
6416 run_keys(&mut e, "dib");
6417 assert_eq!(e.buffer().lines()[0], "f()");
6418 }
6419
6420 #[test]
6421 fn di_bracket_deletes_inside() {
6422 let mut e = editor_with("a[b,c]d");
6423 e.jump_cursor(0, 3);
6424 run_keys(&mut e, "di[");
6425 assert_eq!(e.buffer().lines()[0], "a[]d");
6426 }
6427
6428 #[test]
6429 fn da_bracket_deletes_around() {
6430 let mut e = editor_with("a[b,c]d");
6431 e.jump_cursor(0, 3);
6432 run_keys(&mut e, "da[");
6433 assert_eq!(e.buffer().lines()[0], "ad");
6434 }
6435
6436 #[test]
6437 fn di_brace_deletes_inside() {
6438 let mut e = editor_with("x{y}z");
6439 e.jump_cursor(0, 2);
6440 run_keys(&mut e, "di{");
6441 assert_eq!(e.buffer().lines()[0], "x{}z");
6442 }
6443
6444 #[test]
6445 fn da_brace_deletes_around() {
6446 let mut e = editor_with("x{y}z");
6447 e.jump_cursor(0, 2);
6448 run_keys(&mut e, "da{");
6449 assert_eq!(e.buffer().lines()[0], "xz");
6450 }
6451
6452 #[test]
6453 fn di_brace_alias_capital_b_works() {
6454 let mut e = editor_with("x{y}z");
6455 e.jump_cursor(0, 2);
6456 run_keys(&mut e, "diB");
6457 assert_eq!(e.buffer().lines()[0], "x{}z");
6458 }
6459
6460 #[test]
6461 fn di_angle_deletes_inside() {
6462 let mut e = editor_with("p<q>r");
6463 e.jump_cursor(0, 2);
6464 run_keys(&mut e, "di<lt>");
6466 assert_eq!(e.buffer().lines()[0], "p<>r");
6467 }
6468
6469 #[test]
6470 fn da_angle_deletes_around() {
6471 let mut e = editor_with("p<q>r");
6472 e.jump_cursor(0, 2);
6473 run_keys(&mut e, "da<lt>");
6474 assert_eq!(e.buffer().lines()[0], "pr");
6475 }
6476
6477 #[test]
6478 fn dip_deletes_inner_paragraph() {
6479 let mut e = editor_with("a\nb\nc\n\nd");
6480 e.jump_cursor(1, 0);
6481 run_keys(&mut e, "dip");
6482 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6485 }
6486
6487 #[test]
6490 fn sentence_motion_close_paren_jumps_forward() {
6491 let mut e = editor_with("Alpha. Beta. Gamma.");
6492 e.jump_cursor(0, 0);
6493 run_keys(&mut e, ")");
6494 assert_eq!(e.cursor(), (0, 7));
6496 run_keys(&mut e, ")");
6497 assert_eq!(e.cursor(), (0, 13));
6498 }
6499
6500 #[test]
6501 fn sentence_motion_open_paren_jumps_backward() {
6502 let mut e = editor_with("Alpha. Beta. Gamma.");
6503 e.jump_cursor(0, 13);
6504 run_keys(&mut e, "(");
6505 assert_eq!(e.cursor(), (0, 7));
6508 run_keys(&mut e, "(");
6509 assert_eq!(e.cursor(), (0, 0));
6510 }
6511
6512 #[test]
6513 fn sentence_motion_count() {
6514 let mut e = editor_with("A. B. C. D.");
6515 e.jump_cursor(0, 0);
6516 run_keys(&mut e, "3)");
6517 assert_eq!(e.cursor(), (0, 9));
6519 }
6520
6521 #[test]
6522 fn dis_deletes_inner_sentence() {
6523 let mut e = editor_with("First one. Second one. Third one.");
6524 e.jump_cursor(0, 13);
6525 run_keys(&mut e, "dis");
6526 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6528 }
6529
6530 #[test]
6531 fn das_deletes_around_sentence_with_trailing_space() {
6532 let mut e = editor_with("Alpha. Beta. Gamma.");
6533 e.jump_cursor(0, 8);
6534 run_keys(&mut e, "das");
6535 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6538 }
6539
6540 #[test]
6541 fn dis_handles_double_terminator() {
6542 let mut e = editor_with("Wow!? Next.");
6543 e.jump_cursor(0, 1);
6544 run_keys(&mut e, "dis");
6545 assert_eq!(e.buffer().lines()[0], " Next.");
6548 }
6549
6550 #[test]
6551 fn dis_first_sentence_from_cursor_at_zero() {
6552 let mut e = editor_with("Alpha. Beta.");
6553 e.jump_cursor(0, 0);
6554 run_keys(&mut e, "dis");
6555 assert_eq!(e.buffer().lines()[0], " Beta.");
6556 }
6557
6558 #[test]
6559 fn yis_yanks_inner_sentence() {
6560 let mut e = editor_with("Hello world. Bye.");
6561 e.jump_cursor(0, 5);
6562 run_keys(&mut e, "yis");
6563 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6564 }
6565
6566 #[test]
6567 fn vis_visually_selects_inner_sentence() {
6568 let mut e = editor_with("First. Second.");
6569 e.jump_cursor(0, 1);
6570 run_keys(&mut e, "vis");
6571 assert_eq!(e.vim_mode(), VimMode::Visual);
6572 run_keys(&mut e, "y");
6573 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6574 }
6575
6576 #[test]
6577 fn ciw_changes_inner_word() {
6578 let mut e = editor_with("hello world");
6579 e.jump_cursor(0, 1);
6580 run_keys(&mut e, "ciwHEY<Esc>");
6581 assert_eq!(e.buffer().lines()[0], "HEY world");
6582 }
6583
6584 #[test]
6585 fn yiw_yanks_inner_word() {
6586 let mut e = editor_with("hello world");
6587 e.jump_cursor(0, 1);
6588 run_keys(&mut e, "yiw");
6589 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6590 }
6591
6592 #[test]
6593 fn viw_selects_inner_word() {
6594 let mut e = editor_with("hello world");
6595 e.jump_cursor(0, 2);
6596 run_keys(&mut e, "viw");
6597 assert_eq!(e.vim_mode(), VimMode::Visual);
6598 run_keys(&mut e, "y");
6599 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6600 }
6601
6602 #[test]
6603 fn ci_paren_changes_inside() {
6604 let mut e = editor_with("f(old)");
6605 e.jump_cursor(0, 3);
6606 run_keys(&mut e, "ci(NEW<Esc>");
6607 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6608 }
6609
6610 #[test]
6611 fn yi_double_quote_yanks_inside() {
6612 let mut e = editor_with("say \"hi there\" then");
6613 e.jump_cursor(0, 6);
6614 run_keys(&mut e, "yi\"");
6615 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6616 }
6617
6618 #[test]
6619 fn vap_visual_selects_around_paragraph() {
6620 let mut e = editor_with("a\nb\n\nc");
6621 e.jump_cursor(0, 0);
6622 run_keys(&mut e, "vap");
6623 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6624 run_keys(&mut e, "y");
6625 let text = e.registers().read('"').unwrap().text.clone();
6627 assert!(text.starts_with("a\nb"));
6628 }
6629
6630 #[test]
6631 fn star_finds_next_occurrence() {
6632 let mut e = editor_with("foo bar foo baz");
6633 run_keys(&mut e, "*");
6634 assert_eq!(e.cursor().1, 8);
6635 }
6636
6637 #[test]
6638 fn star_skips_substring_match() {
6639 let mut e = editor_with("foo foobar baz");
6642 run_keys(&mut e, "*");
6643 assert_eq!(e.cursor().1, 0);
6644 }
6645
6646 #[test]
6647 fn g_star_matches_substring() {
6648 let mut e = editor_with("foo foobar baz");
6651 run_keys(&mut e, "g*");
6652 assert_eq!(e.cursor().1, 4);
6653 }
6654
6655 #[test]
6656 fn g_pound_matches_substring_backward() {
6657 let mut e = editor_with("foo foobar baz foo");
6660 run_keys(&mut e, "$b");
6661 assert_eq!(e.cursor().1, 15);
6662 run_keys(&mut e, "g#");
6663 assert_eq!(e.cursor().1, 4);
6664 }
6665
6666 #[test]
6667 fn n_repeats_last_search_forward() {
6668 let mut e = editor_with("foo bar foo baz foo");
6669 run_keys(&mut e, "/foo<CR>");
6672 assert_eq!(e.cursor().1, 8);
6673 run_keys(&mut e, "n");
6674 assert_eq!(e.cursor().1, 16);
6675 }
6676
6677 #[test]
6678 fn shift_n_reverses_search() {
6679 let mut e = editor_with("foo bar foo baz foo");
6680 run_keys(&mut e, "/foo<CR>");
6681 run_keys(&mut e, "n");
6682 assert_eq!(e.cursor().1, 16);
6683 run_keys(&mut e, "N");
6684 assert_eq!(e.cursor().1, 8);
6685 }
6686
6687 #[test]
6688 fn n_noop_without_pattern() {
6689 let mut e = editor_with("foo bar");
6690 run_keys(&mut e, "n");
6691 assert_eq!(e.cursor(), (0, 0));
6692 }
6693
6694 #[test]
6695 fn visual_line_preserves_cursor_column() {
6696 let mut e = editor_with("hello world\nanother one\nbye");
6699 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6701 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6702 assert_eq!(e.cursor(), (0, 5));
6703 run_keys(&mut e, "j");
6704 assert_eq!(e.cursor(), (1, 5));
6705 }
6706
6707 #[test]
6708 fn visual_line_yank_includes_trailing_newline() {
6709 let mut e = editor_with("aaa\nbbb\nccc");
6710 run_keys(&mut e, "Vjy");
6711 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6713 }
6714
6715 #[test]
6716 fn visual_line_yank_last_line_trailing_newline() {
6717 let mut e = editor_with("aaa\nbbb\nccc");
6718 run_keys(&mut e, "jj");
6720 run_keys(&mut e, "Vy");
6721 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6722 }
6723
6724 #[test]
6725 fn yy_on_last_line_has_trailing_newline() {
6726 let mut e = editor_with("aaa\nbbb\nccc");
6727 run_keys(&mut e, "jj");
6728 run_keys(&mut e, "yy");
6729 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6730 }
6731
6732 #[test]
6733 fn yy_in_middle_has_trailing_newline() {
6734 let mut e = editor_with("aaa\nbbb\nccc");
6735 run_keys(&mut e, "j");
6736 run_keys(&mut e, "yy");
6737 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6738 }
6739
6740 #[test]
6741 fn di_single_quote() {
6742 let mut e = editor_with("say 'hello world' now");
6743 e.jump_cursor(0, 7);
6744 run_keys(&mut e, "di'");
6745 assert_eq!(e.buffer().lines()[0], "say '' now");
6746 }
6747
6748 #[test]
6749 fn da_single_quote() {
6750 let mut e = editor_with("say 'hello' now");
6752 e.jump_cursor(0, 7);
6753 run_keys(&mut e, "da'");
6754 assert_eq!(e.buffer().lines()[0], "say now");
6755 }
6756
6757 #[test]
6758 fn di_backtick() {
6759 let mut e = editor_with("say `hi` now");
6760 e.jump_cursor(0, 5);
6761 run_keys(&mut e, "di`");
6762 assert_eq!(e.buffer().lines()[0], "say `` now");
6763 }
6764
6765 #[test]
6766 fn di_brace() {
6767 let mut e = editor_with("fn { a; b; c }");
6768 e.jump_cursor(0, 7);
6769 run_keys(&mut e, "di{");
6770 assert_eq!(e.buffer().lines()[0], "fn {}");
6771 }
6772
6773 #[test]
6774 fn di_bracket() {
6775 let mut e = editor_with("arr[1, 2, 3]");
6776 e.jump_cursor(0, 5);
6777 run_keys(&mut e, "di[");
6778 assert_eq!(e.buffer().lines()[0], "arr[]");
6779 }
6780
6781 #[test]
6782 fn dab_deletes_around_paren() {
6783 let mut e = editor_with("fn(a, b) + 1");
6784 e.jump_cursor(0, 4);
6785 run_keys(&mut e, "dab");
6786 assert_eq!(e.buffer().lines()[0], "fn + 1");
6787 }
6788
6789 #[test]
6790 fn da_big_b_deletes_around_brace() {
6791 let mut e = editor_with("x = {a: 1}");
6792 e.jump_cursor(0, 6);
6793 run_keys(&mut e, "daB");
6794 assert_eq!(e.buffer().lines()[0], "x = ");
6795 }
6796
6797 #[test]
6798 fn di_big_w_deletes_bigword() {
6799 let mut e = editor_with("foo-bar baz");
6800 e.jump_cursor(0, 2);
6801 run_keys(&mut e, "diW");
6802 assert_eq!(e.buffer().lines()[0], " baz");
6803 }
6804
6805 #[test]
6806 fn visual_select_inner_word() {
6807 let mut e = editor_with("hello world");
6808 e.jump_cursor(0, 2);
6809 run_keys(&mut e, "viw");
6810 assert_eq!(e.vim_mode(), VimMode::Visual);
6811 run_keys(&mut e, "y");
6812 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6813 }
6814
6815 #[test]
6816 fn visual_select_inner_quote() {
6817 let mut e = editor_with("foo \"bar\" baz");
6818 e.jump_cursor(0, 6);
6819 run_keys(&mut e, "vi\"");
6820 run_keys(&mut e, "y");
6821 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6822 }
6823
6824 #[test]
6825 fn visual_select_inner_paren() {
6826 let mut e = editor_with("fn(a, b)");
6827 e.jump_cursor(0, 4);
6828 run_keys(&mut e, "vi(");
6829 run_keys(&mut e, "y");
6830 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6831 }
6832
6833 #[test]
6834 fn visual_select_outer_brace() {
6835 let mut e = editor_with("{x}");
6836 e.jump_cursor(0, 1);
6837 run_keys(&mut e, "va{");
6838 run_keys(&mut e, "y");
6839 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6840 }
6841
6842 #[test]
6843 fn ci_paren_forward_scans_when_cursor_before_pair() {
6844 let mut e = editor_with("foo(bar)");
6847 e.jump_cursor(0, 0);
6848 run_keys(&mut e, "ci(NEW<Esc>");
6849 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
6850 }
6851
6852 #[test]
6853 fn ci_paren_forward_scans_across_lines() {
6854 let mut e = editor_with("first\nfoo(bar)\nlast");
6855 e.jump_cursor(0, 0);
6856 run_keys(&mut e, "ci(NEW<Esc>");
6857 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
6858 }
6859
6860 #[test]
6861 fn ci_brace_forward_scans_when_cursor_before_pair() {
6862 let mut e = editor_with("let x = {y};");
6863 e.jump_cursor(0, 0);
6864 run_keys(&mut e, "ci{NEW<Esc>");
6865 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
6866 }
6867
6868 #[test]
6869 fn cit_forward_scans_when_cursor_before_tag() {
6870 let mut e = editor_with("text <b>hello</b> rest");
6873 e.jump_cursor(0, 0);
6874 run_keys(&mut e, "citNEW<Esc>");
6875 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
6876 }
6877
6878 #[test]
6879 fn dat_forward_scans_when_cursor_before_tag() {
6880 let mut e = editor_with("text <b>hello</b> rest");
6882 e.jump_cursor(0, 0);
6883 run_keys(&mut e, "dat");
6884 assert_eq!(e.buffer().lines()[0], "text rest");
6885 }
6886
6887 #[test]
6888 fn ci_paren_still_works_when_cursor_inside() {
6889 let mut e = editor_with("fn(a, b)");
6892 e.jump_cursor(0, 4);
6893 run_keys(&mut e, "ci(NEW<Esc>");
6894 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
6895 }
6896
6897 #[test]
6898 fn caw_changes_word_with_trailing_space() {
6899 let mut e = editor_with("hello world");
6900 run_keys(&mut e, "cawfoo<Esc>");
6901 assert_eq!(e.buffer().lines()[0], "fooworld");
6902 }
6903
6904 #[test]
6905 fn visual_char_yank_preserves_raw_text() {
6906 let mut e = editor_with("hello world");
6907 run_keys(&mut e, "vllly");
6908 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6909 }
6910
6911 #[test]
6912 fn single_line_visual_line_selects_full_line_on_yank() {
6913 let mut e = editor_with("hello world\nbye");
6914 run_keys(&mut e, "V");
6915 run_keys(&mut e, "y");
6918 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6919 }
6920
6921 #[test]
6922 fn visual_line_extends_both_directions() {
6923 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6924 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6926 assert_eq!(e.cursor(), (3, 0));
6927 run_keys(&mut e, "k");
6928 assert_eq!(e.cursor(), (2, 0));
6930 run_keys(&mut e, "k");
6931 assert_eq!(e.cursor(), (1, 0));
6932 }
6933
6934 #[test]
6935 fn visual_char_preserves_cursor_column() {
6936 let mut e = editor_with("hello world");
6937 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6939 assert_eq!(e.cursor(), (0, 5));
6940 run_keys(&mut e, "ll");
6941 assert_eq!(e.cursor(), (0, 7));
6942 }
6943
6944 #[test]
6945 fn visual_char_highlight_bounds_order() {
6946 let mut e = editor_with("abcdef");
6947 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
6949 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
6952 }
6953
6954 #[test]
6955 fn visual_line_highlight_bounds() {
6956 let mut e = editor_with("a\nb\nc");
6957 run_keys(&mut e, "V");
6958 assert_eq!(e.line_highlight(), Some((0, 0)));
6959 run_keys(&mut e, "j");
6960 assert_eq!(e.line_highlight(), Some((0, 1)));
6961 run_keys(&mut e, "j");
6962 assert_eq!(e.line_highlight(), Some((0, 2)));
6963 }
6964
6965 #[test]
6968 fn h_moves_left() {
6969 let mut e = editor_with("hello");
6970 e.jump_cursor(0, 3);
6971 run_keys(&mut e, "h");
6972 assert_eq!(e.cursor(), (0, 2));
6973 }
6974
6975 #[test]
6976 fn l_moves_right() {
6977 let mut e = editor_with("hello");
6978 run_keys(&mut e, "l");
6979 assert_eq!(e.cursor(), (0, 1));
6980 }
6981
6982 #[test]
6983 fn k_moves_up() {
6984 let mut e = editor_with("a\nb\nc");
6985 e.jump_cursor(2, 0);
6986 run_keys(&mut e, "k");
6987 assert_eq!(e.cursor(), (1, 0));
6988 }
6989
6990 #[test]
6991 fn zero_moves_to_line_start() {
6992 let mut e = editor_with(" hello");
6993 run_keys(&mut e, "$");
6994 run_keys(&mut e, "0");
6995 assert_eq!(e.cursor().1, 0);
6996 }
6997
6998 #[test]
6999 fn caret_moves_to_first_non_blank() {
7000 let mut e = editor_with(" hello");
7001 run_keys(&mut e, "0");
7002 run_keys(&mut e, "^");
7003 assert_eq!(e.cursor().1, 4);
7004 }
7005
7006 #[test]
7007 fn dollar_moves_to_last_char() {
7008 let mut e = editor_with("hello");
7009 run_keys(&mut e, "$");
7010 assert_eq!(e.cursor().1, 4);
7011 }
7012
7013 #[test]
7014 fn dollar_on_empty_line_stays_at_col_zero() {
7015 let mut e = editor_with("");
7016 run_keys(&mut e, "$");
7017 assert_eq!(e.cursor().1, 0);
7018 }
7019
7020 #[test]
7021 fn w_jumps_to_next_word() {
7022 let mut e = editor_with("foo bar baz");
7023 run_keys(&mut e, "w");
7024 assert_eq!(e.cursor().1, 4);
7025 }
7026
7027 #[test]
7028 fn b_jumps_back_a_word() {
7029 let mut e = editor_with("foo bar");
7030 e.jump_cursor(0, 6);
7031 run_keys(&mut e, "b");
7032 assert_eq!(e.cursor().1, 4);
7033 }
7034
7035 #[test]
7036 fn e_jumps_to_word_end() {
7037 let mut e = editor_with("foo bar");
7038 run_keys(&mut e, "e");
7039 assert_eq!(e.cursor().1, 2);
7040 }
7041
7042 #[test]
7045 fn d_dollar_deletes_to_eol() {
7046 let mut e = editor_with("hello world");
7047 e.jump_cursor(0, 5);
7048 run_keys(&mut e, "d$");
7049 assert_eq!(e.buffer().lines()[0], "hello");
7050 }
7051
7052 #[test]
7053 fn d_zero_deletes_to_line_start() {
7054 let mut e = editor_with("hello world");
7055 e.jump_cursor(0, 6);
7056 run_keys(&mut e, "d0");
7057 assert_eq!(e.buffer().lines()[0], "world");
7058 }
7059
7060 #[test]
7061 fn d_caret_deletes_to_first_non_blank() {
7062 let mut e = editor_with(" hello");
7063 e.jump_cursor(0, 6);
7064 run_keys(&mut e, "d^");
7065 assert_eq!(e.buffer().lines()[0], " llo");
7066 }
7067
7068 #[test]
7069 fn d_capital_g_deletes_to_end_of_file() {
7070 let mut e = editor_with("a\nb\nc\nd");
7071 e.jump_cursor(1, 0);
7072 run_keys(&mut e, "dG");
7073 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7074 }
7075
7076 #[test]
7077 fn d_gg_deletes_to_start_of_file() {
7078 let mut e = editor_with("a\nb\nc\nd");
7079 e.jump_cursor(2, 0);
7080 run_keys(&mut e, "dgg");
7081 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7082 }
7083
7084 #[test]
7085 fn cw_is_ce_quirk() {
7086 let mut e = editor_with("foo bar");
7089 run_keys(&mut e, "cwxyz<Esc>");
7090 assert_eq!(e.buffer().lines()[0], "xyz bar");
7091 }
7092
7093 #[test]
7096 fn big_d_deletes_to_eol() {
7097 let mut e = editor_with("hello world");
7098 e.jump_cursor(0, 5);
7099 run_keys(&mut e, "D");
7100 assert_eq!(e.buffer().lines()[0], "hello");
7101 }
7102
7103 #[test]
7104 fn big_c_deletes_to_eol_and_inserts() {
7105 let mut e = editor_with("hello world");
7106 e.jump_cursor(0, 5);
7107 run_keys(&mut e, "C!<Esc>");
7108 assert_eq!(e.buffer().lines()[0], "hello!");
7109 }
7110
7111 #[test]
7112 fn j_joins_next_line_with_space() {
7113 let mut e = editor_with("hello\nworld");
7114 run_keys(&mut e, "J");
7115 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7116 }
7117
7118 #[test]
7119 fn j_strips_leading_whitespace_on_join() {
7120 let mut e = editor_with("hello\n world");
7121 run_keys(&mut e, "J");
7122 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7123 }
7124
7125 #[test]
7126 fn big_x_deletes_char_before_cursor() {
7127 let mut e = editor_with("hello");
7128 e.jump_cursor(0, 3);
7129 run_keys(&mut e, "X");
7130 assert_eq!(e.buffer().lines()[0], "helo");
7131 }
7132
7133 #[test]
7134 fn s_substitutes_char_and_enters_insert() {
7135 let mut e = editor_with("hello");
7136 run_keys(&mut e, "sX<Esc>");
7137 assert_eq!(e.buffer().lines()[0], "Xello");
7138 }
7139
7140 #[test]
7141 fn count_x_deletes_many() {
7142 let mut e = editor_with("abcdef");
7143 run_keys(&mut e, "3x");
7144 assert_eq!(e.buffer().lines()[0], "def");
7145 }
7146
7147 #[test]
7150 fn p_pastes_charwise_after_cursor() {
7151 let mut e = editor_with("hello");
7152 run_keys(&mut e, "yw");
7153 run_keys(&mut e, "$p");
7154 assert_eq!(e.buffer().lines()[0], "hellohello");
7155 }
7156
7157 #[test]
7158 fn capital_p_pastes_charwise_before_cursor() {
7159 let mut e = editor_with("hello");
7160 run_keys(&mut e, "v");
7162 run_keys(&mut e, "l");
7163 run_keys(&mut e, "y");
7164 run_keys(&mut e, "$P");
7165 assert_eq!(e.buffer().lines()[0], "hellheo");
7168 }
7169
7170 #[test]
7171 fn p_pastes_linewise_below() {
7172 let mut e = editor_with("one\ntwo\nthree");
7173 run_keys(&mut e, "yy");
7174 run_keys(&mut e, "p");
7175 assert_eq!(
7176 e.buffer().lines(),
7177 &[
7178 "one".to_string(),
7179 "one".to_string(),
7180 "two".to_string(),
7181 "three".to_string()
7182 ]
7183 );
7184 }
7185
7186 #[test]
7187 fn capital_p_pastes_linewise_above() {
7188 let mut e = editor_with("one\ntwo");
7189 e.jump_cursor(1, 0);
7190 run_keys(&mut e, "yy");
7191 run_keys(&mut e, "P");
7192 assert_eq!(
7193 e.buffer().lines(),
7194 &["one".to_string(), "two".to_string(), "two".to_string()]
7195 );
7196 }
7197
7198 #[test]
7201 fn hash_finds_previous_occurrence() {
7202 let mut e = editor_with("foo bar foo baz foo");
7203 e.jump_cursor(0, 16);
7205 run_keys(&mut e, "#");
7206 assert_eq!(e.cursor().1, 8);
7207 }
7208
7209 #[test]
7212 fn visual_line_delete_removes_full_lines() {
7213 let mut e = editor_with("a\nb\nc\nd");
7214 run_keys(&mut e, "Vjd");
7215 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7216 }
7217
7218 #[test]
7219 fn visual_line_change_leaves_blank_line() {
7220 let mut e = editor_with("a\nb\nc");
7221 run_keys(&mut e, "Vjc");
7222 assert_eq!(e.vim_mode(), VimMode::Insert);
7223 run_keys(&mut e, "X<Esc>");
7224 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7228 }
7229
7230 #[test]
7231 fn cc_leaves_blank_line() {
7232 let mut e = editor_with("a\nb\nc");
7233 e.jump_cursor(1, 0);
7234 run_keys(&mut e, "ccX<Esc>");
7235 assert_eq!(
7236 e.buffer().lines(),
7237 &["a".to_string(), "X".to_string(), "c".to_string()]
7238 );
7239 }
7240
7241 #[test]
7246 fn big_w_skips_hyphens() {
7247 let mut e = editor_with("foo-bar baz");
7249 run_keys(&mut e, "W");
7250 assert_eq!(e.cursor().1, 8);
7251 }
7252
7253 #[test]
7254 fn big_w_crosses_lines() {
7255 let mut e = editor_with("foo-bar\nbaz-qux");
7256 run_keys(&mut e, "W");
7257 assert_eq!(e.cursor(), (1, 0));
7258 }
7259
7260 #[test]
7261 fn big_b_skips_hyphens() {
7262 let mut e = editor_with("foo-bar baz");
7263 e.jump_cursor(0, 9);
7264 run_keys(&mut e, "B");
7265 assert_eq!(e.cursor().1, 8);
7266 run_keys(&mut e, "B");
7267 assert_eq!(e.cursor().1, 0);
7268 }
7269
7270 #[test]
7271 fn big_e_jumps_to_big_word_end() {
7272 let mut e = editor_with("foo-bar baz");
7273 run_keys(&mut e, "E");
7274 assert_eq!(e.cursor().1, 6);
7275 run_keys(&mut e, "E");
7276 assert_eq!(e.cursor().1, 10);
7277 }
7278
7279 #[test]
7280 fn dw_with_big_word_variant() {
7281 let mut e = editor_with("foo-bar baz");
7283 run_keys(&mut e, "dW");
7284 assert_eq!(e.buffer().lines()[0], "baz");
7285 }
7286
7287 #[test]
7290 fn insert_ctrl_w_deletes_word_back() {
7291 let mut e = editor_with("");
7292 run_keys(&mut e, "i");
7293 for c in "hello world".chars() {
7294 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7295 }
7296 run_keys(&mut e, "<C-w>");
7297 assert_eq!(e.buffer().lines()[0], "hello ");
7298 }
7299
7300 #[test]
7301 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7302 let mut e = editor_with("hello\nworld");
7306 e.jump_cursor(1, 0);
7307 run_keys(&mut e, "i");
7308 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7309 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7312 assert_eq!(e.cursor(), (0, 0));
7313 }
7314
7315 #[test]
7316 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7317 let mut e = editor_with("foo bar\nbaz");
7318 e.jump_cursor(1, 0);
7319 run_keys(&mut e, "i");
7320 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7321 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7323 assert_eq!(e.cursor(), (0, 4));
7324 }
7325
7326 #[test]
7327 fn insert_ctrl_u_deletes_to_line_start() {
7328 let mut e = editor_with("");
7329 run_keys(&mut e, "i");
7330 for c in "hello world".chars() {
7331 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7332 }
7333 run_keys(&mut e, "<C-u>");
7334 assert_eq!(e.buffer().lines()[0], "");
7335 }
7336
7337 #[test]
7338 fn insert_ctrl_o_runs_one_normal_command() {
7339 let mut e = editor_with("hello world");
7340 run_keys(&mut e, "A");
7342 assert_eq!(e.vim_mode(), VimMode::Insert);
7343 e.jump_cursor(0, 0);
7345 run_keys(&mut e, "<C-o>");
7346 assert_eq!(e.vim_mode(), VimMode::Normal);
7347 run_keys(&mut e, "dw");
7348 assert_eq!(e.vim_mode(), VimMode::Insert);
7350 assert_eq!(e.buffer().lines()[0], "world");
7351 }
7352
7353 #[test]
7356 fn j_through_empty_line_preserves_column() {
7357 let mut e = editor_with("hello world\n\nanother line");
7358 run_keys(&mut e, "llllll");
7360 assert_eq!(e.cursor(), (0, 6));
7361 run_keys(&mut e, "j");
7364 assert_eq!(e.cursor(), (1, 0));
7365 run_keys(&mut e, "j");
7367 assert_eq!(e.cursor(), (2, 6));
7368 }
7369
7370 #[test]
7371 fn j_through_shorter_line_preserves_column() {
7372 let mut e = editor_with("hello world\nhi\nanother line");
7373 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7376 run_keys(&mut e, "j");
7377 assert_eq!(e.cursor(), (2, 7));
7378 }
7379
7380 #[test]
7381 fn esc_from_insert_sticky_matches_visible_cursor() {
7382 let mut e = editor_with(" this is a line\n another one of a similar size");
7386 e.jump_cursor(0, 12);
7387 run_keys(&mut e, "I");
7388 assert_eq!(e.cursor(), (0, 4));
7389 run_keys(&mut e, "X<Esc>");
7390 assert_eq!(e.cursor(), (0, 4));
7391 run_keys(&mut e, "j");
7392 assert_eq!(e.cursor(), (1, 4));
7393 }
7394
7395 #[test]
7396 fn esc_from_insert_sticky_tracks_inserted_chars() {
7397 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7398 run_keys(&mut e, "i");
7399 run_keys(&mut e, "abc<Esc>");
7400 assert_eq!(e.cursor(), (0, 2));
7401 run_keys(&mut e, "j");
7402 assert_eq!(e.cursor(), (1, 2));
7403 }
7404
7405 #[test]
7406 fn esc_from_insert_sticky_tracks_arrow_nav() {
7407 let mut e = editor_with("xxxxxx\nyyyyyy");
7408 run_keys(&mut e, "i");
7409 run_keys(&mut e, "abc");
7410 for _ in 0..2 {
7411 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7412 }
7413 run_keys(&mut e, "<Esc>");
7414 assert_eq!(e.cursor(), (0, 0));
7415 run_keys(&mut e, "j");
7416 assert_eq!(e.cursor(), (1, 0));
7417 }
7418
7419 #[test]
7420 fn esc_from_insert_at_col_14_followed_by_j() {
7421 let line = "x".repeat(30);
7424 let buf = format!("{line}\n{line}");
7425 let mut e = editor_with(&buf);
7426 e.jump_cursor(0, 14);
7427 run_keys(&mut e, "i");
7428 for c in "test ".chars() {
7429 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7430 }
7431 run_keys(&mut e, "<Esc>");
7432 assert_eq!(e.cursor(), (0, 18));
7433 run_keys(&mut e, "j");
7434 assert_eq!(e.cursor(), (1, 18));
7435 }
7436
7437 #[test]
7438 fn linewise_paste_resets_sticky_column() {
7439 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7443 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7445 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7449 run_keys(&mut e, "j");
7451 assert_eq!(e.cursor(), (3, 2));
7452 }
7453
7454 #[test]
7455 fn horizontal_motion_resyncs_sticky_column() {
7456 let mut e = editor_with("hello world\n\nanother line");
7460 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7463 assert_eq!(e.cursor(), (2, 3));
7464 }
7465
7466 #[test]
7469 fn ctrl_v_enters_visual_block() {
7470 let mut e = editor_with("aaa\nbbb\nccc");
7471 run_keys(&mut e, "<C-v>");
7472 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7473 }
7474
7475 #[test]
7476 fn visual_block_esc_returns_to_normal() {
7477 let mut e = editor_with("aaa\nbbb\nccc");
7478 run_keys(&mut e, "<C-v>");
7479 run_keys(&mut e, "<Esc>");
7480 assert_eq!(e.vim_mode(), VimMode::Normal);
7481 }
7482
7483 #[test]
7484 fn visual_block_delete_removes_column_range() {
7485 let mut e = editor_with("hello\nworld\nhappy");
7486 run_keys(&mut e, "l");
7488 run_keys(&mut e, "<C-v>");
7489 run_keys(&mut e, "jj");
7490 run_keys(&mut e, "ll");
7491 run_keys(&mut e, "d");
7492 assert_eq!(
7494 e.buffer().lines(),
7495 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7496 );
7497 }
7498
7499 #[test]
7500 fn visual_block_yank_joins_with_newlines() {
7501 let mut e = editor_with("hello\nworld\nhappy");
7502 run_keys(&mut e, "<C-v>");
7503 run_keys(&mut e, "jj");
7504 run_keys(&mut e, "ll");
7505 run_keys(&mut e, "y");
7506 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7507 }
7508
7509 #[test]
7510 fn visual_block_replace_fills_block() {
7511 let mut e = editor_with("hello\nworld\nhappy");
7512 run_keys(&mut e, "<C-v>");
7513 run_keys(&mut e, "jj");
7514 run_keys(&mut e, "ll");
7515 run_keys(&mut e, "rx");
7516 assert_eq!(
7517 e.buffer().lines(),
7518 &[
7519 "xxxlo".to_string(),
7520 "xxxld".to_string(),
7521 "xxxpy".to_string()
7522 ]
7523 );
7524 }
7525
7526 #[test]
7527 fn visual_block_insert_repeats_across_rows() {
7528 let mut e = editor_with("hello\nworld\nhappy");
7529 run_keys(&mut e, "<C-v>");
7530 run_keys(&mut e, "jj");
7531 run_keys(&mut e, "I");
7532 run_keys(&mut e, "# <Esc>");
7533 assert_eq!(
7534 e.buffer().lines(),
7535 &[
7536 "# hello".to_string(),
7537 "# world".to_string(),
7538 "# happy".to_string()
7539 ]
7540 );
7541 }
7542
7543 #[test]
7544 fn block_highlight_returns_none_outside_block_mode() {
7545 let mut e = editor_with("abc");
7546 assert!(e.block_highlight().is_none());
7547 run_keys(&mut e, "v");
7548 assert!(e.block_highlight().is_none());
7549 run_keys(&mut e, "<Esc>V");
7550 assert!(e.block_highlight().is_none());
7551 }
7552
7553 #[test]
7554 fn block_highlight_bounds_track_anchor_and_cursor() {
7555 let mut e = editor_with("aaaa\nbbbb\ncccc");
7556 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7558 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7561 }
7562
7563 #[test]
7564 fn visual_block_delete_handles_short_lines() {
7565 let mut e = editor_with("hello\nhi\nworld");
7567 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7569 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7571 assert_eq!(
7576 e.buffer().lines(),
7577 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7578 );
7579 }
7580
7581 #[test]
7582 fn visual_block_yank_pads_short_lines_with_empties() {
7583 let mut e = editor_with("hello\nhi\nworld");
7584 run_keys(&mut e, "l");
7585 run_keys(&mut e, "<C-v>");
7586 run_keys(&mut e, "jjll");
7587 run_keys(&mut e, "y");
7588 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7590 }
7591
7592 #[test]
7593 fn visual_block_replace_skips_past_eol() {
7594 let mut e = editor_with("ab\ncd\nef");
7597 run_keys(&mut e, "l");
7599 run_keys(&mut e, "<C-v>");
7600 run_keys(&mut e, "jjllllll");
7601 run_keys(&mut e, "rX");
7602 assert_eq!(
7605 e.buffer().lines(),
7606 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7607 );
7608 }
7609
7610 #[test]
7611 fn visual_block_with_empty_line_in_middle() {
7612 let mut e = editor_with("abcd\n\nefgh");
7613 run_keys(&mut e, "<C-v>");
7614 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7616 assert_eq!(
7619 e.buffer().lines(),
7620 &["d".to_string(), "".to_string(), "h".to_string()]
7621 );
7622 }
7623
7624 #[test]
7625 fn block_insert_pads_empty_lines_to_block_column() {
7626 let mut e = editor_with("this is a line\n\nthis is a line");
7629 e.jump_cursor(0, 3);
7630 run_keys(&mut e, "<C-v>");
7631 run_keys(&mut e, "jj");
7632 run_keys(&mut e, "I");
7633 run_keys(&mut e, "XX<Esc>");
7634 assert_eq!(
7635 e.buffer().lines(),
7636 &[
7637 "thiXXs is a line".to_string(),
7638 " XX".to_string(),
7639 "thiXXs is a line".to_string()
7640 ]
7641 );
7642 }
7643
7644 #[test]
7645 fn block_insert_pads_short_lines_to_block_column() {
7646 let mut e = editor_with("aaaaa\nbb\naaaaa");
7647 e.jump_cursor(0, 3);
7648 run_keys(&mut e, "<C-v>");
7649 run_keys(&mut e, "jj");
7650 run_keys(&mut e, "I");
7651 run_keys(&mut e, "Y<Esc>");
7652 assert_eq!(
7654 e.buffer().lines(),
7655 &[
7656 "aaaYaa".to_string(),
7657 "bb Y".to_string(),
7658 "aaaYaa".to_string()
7659 ]
7660 );
7661 }
7662
7663 #[test]
7664 fn visual_block_append_repeats_across_rows() {
7665 let mut e = editor_with("foo\nbar\nbaz");
7666 run_keys(&mut e, "<C-v>");
7667 run_keys(&mut e, "jj");
7668 run_keys(&mut e, "A");
7671 run_keys(&mut e, "!<Esc>");
7672 assert_eq!(
7673 e.buffer().lines(),
7674 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7675 );
7676 }
7677
7678 #[test]
7681 fn slash_opens_forward_search_prompt() {
7682 let mut e = editor_with("hello world");
7683 run_keys(&mut e, "/");
7684 let p = e.search_prompt().expect("prompt should be active");
7685 assert!(p.text.is_empty());
7686 assert!(p.forward);
7687 }
7688
7689 #[test]
7690 fn question_opens_backward_search_prompt() {
7691 let mut e = editor_with("hello world");
7692 run_keys(&mut e, "?");
7693 let p = e.search_prompt().expect("prompt should be active");
7694 assert!(!p.forward);
7695 }
7696
7697 #[test]
7698 fn search_prompt_typing_updates_pattern_live() {
7699 let mut e = editor_with("foo bar\nbaz");
7700 run_keys(&mut e, "/bar");
7701 assert_eq!(e.search_prompt().unwrap().text, "bar");
7702 assert!(e.search_state().pattern.is_some());
7704 }
7705
7706 #[test]
7707 fn search_prompt_backspace_and_enter() {
7708 let mut e = editor_with("hello world\nagain");
7709 run_keys(&mut e, "/worlx");
7710 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7711 assert_eq!(e.search_prompt().unwrap().text, "worl");
7712 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7713 assert!(e.search_prompt().is_none());
7715 assert_eq!(e.last_search(), Some("worl"));
7716 assert_eq!(e.cursor(), (0, 6));
7717 }
7718
7719 #[test]
7720 fn empty_search_prompt_enter_repeats_last_search() {
7721 let mut e = editor_with("foo bar foo baz foo");
7722 run_keys(&mut e, "/foo");
7723 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7724 assert_eq!(e.cursor().1, 8);
7725 run_keys(&mut e, "/");
7727 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7728 assert_eq!(e.cursor().1, 16);
7729 assert_eq!(e.last_search(), Some("foo"));
7730 }
7731
7732 #[test]
7733 fn search_history_records_committed_patterns() {
7734 let mut e = editor_with("alpha beta gamma");
7735 run_keys(&mut e, "/alpha<CR>");
7736 run_keys(&mut e, "/beta<CR>");
7737 let history = e.vim.search_history.clone();
7739 assert_eq!(history, vec!["alpha", "beta"]);
7740 }
7741
7742 #[test]
7743 fn search_history_dedupes_consecutive_repeats() {
7744 let mut e = editor_with("foo bar foo");
7745 run_keys(&mut e, "/foo<CR>");
7746 run_keys(&mut e, "/foo<CR>");
7747 run_keys(&mut e, "/bar<CR>");
7748 run_keys(&mut e, "/bar<CR>");
7749 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
7751 }
7752
7753 #[test]
7754 fn ctrl_p_walks_history_backward() {
7755 let mut e = editor_with("alpha beta gamma");
7756 run_keys(&mut e, "/alpha<CR>");
7757 run_keys(&mut e, "/beta<CR>");
7758 run_keys(&mut e, "/");
7760 assert_eq!(e.search_prompt().unwrap().text, "");
7761 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7762 assert_eq!(e.search_prompt().unwrap().text, "beta");
7763 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7764 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7765 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7767 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7768 }
7769
7770 #[test]
7771 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7772 let mut e = editor_with("a b c");
7773 run_keys(&mut e, "/a<CR>");
7774 run_keys(&mut e, "/b<CR>");
7775 run_keys(&mut e, "/c<CR>");
7776 run_keys(&mut e, "/");
7777 for _ in 0..3 {
7779 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7780 }
7781 assert_eq!(e.search_prompt().unwrap().text, "a");
7782 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7783 assert_eq!(e.search_prompt().unwrap().text, "b");
7784 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7785 assert_eq!(e.search_prompt().unwrap().text, "c");
7786 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7788 assert_eq!(e.search_prompt().unwrap().text, "c");
7789 }
7790
7791 #[test]
7792 fn typing_after_history_walk_resets_cursor() {
7793 let mut e = editor_with("foo");
7794 run_keys(&mut e, "/foo<CR>");
7795 run_keys(&mut e, "/");
7796 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7797 assert_eq!(e.search_prompt().unwrap().text, "foo");
7798 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7801 assert_eq!(e.search_prompt().unwrap().text, "foox");
7802 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7803 assert_eq!(e.search_prompt().unwrap().text, "foo");
7804 }
7805
7806 #[test]
7807 fn empty_backward_search_prompt_enter_repeats_last_search() {
7808 let mut e = editor_with("foo bar foo baz foo");
7809 run_keys(&mut e, "/foo");
7811 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7812 assert_eq!(e.cursor().1, 8);
7813 run_keys(&mut e, "?");
7814 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7815 assert_eq!(e.cursor().1, 0);
7816 assert_eq!(e.last_search(), Some("foo"));
7817 }
7818
7819 #[test]
7820 fn search_prompt_esc_cancels_but_keeps_last_search() {
7821 let mut e = editor_with("foo bar\nbaz");
7822 run_keys(&mut e, "/bar");
7823 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7824 assert!(e.search_prompt().is_none());
7825 assert_eq!(e.last_search(), Some("bar"));
7826 }
7827
7828 #[test]
7829 fn search_then_n_and_shift_n_navigate() {
7830 let mut e = editor_with("foo bar foo baz foo");
7831 run_keys(&mut e, "/foo");
7832 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7833 assert_eq!(e.cursor().1, 8);
7835 run_keys(&mut e, "n");
7836 assert_eq!(e.cursor().1, 16);
7837 run_keys(&mut e, "N");
7838 assert_eq!(e.cursor().1, 8);
7839 }
7840
7841 #[test]
7842 fn question_mark_searches_backward_on_enter() {
7843 let mut e = editor_with("foo bar foo baz");
7844 e.jump_cursor(0, 10);
7845 run_keys(&mut e, "?foo");
7846 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7847 assert_eq!(e.cursor(), (0, 8));
7849 }
7850
7851 #[test]
7854 fn big_y_yanks_to_end_of_line() {
7855 let mut e = editor_with("hello world");
7856 e.jump_cursor(0, 6);
7857 run_keys(&mut e, "Y");
7858 assert_eq!(e.last_yank.as_deref(), Some("world"));
7859 }
7860
7861 #[test]
7862 fn big_y_from_line_start_yanks_full_line() {
7863 let mut e = editor_with("hello world");
7864 run_keys(&mut e, "Y");
7865 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7866 }
7867
7868 #[test]
7869 fn gj_joins_without_inserting_space() {
7870 let mut e = editor_with("hello\n world");
7871 run_keys(&mut e, "gJ");
7872 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7874 }
7875
7876 #[test]
7877 fn gj_noop_on_last_line() {
7878 let mut e = editor_with("only");
7879 run_keys(&mut e, "gJ");
7880 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7881 }
7882
7883 #[test]
7884 fn ge_jumps_to_previous_word_end() {
7885 let mut e = editor_with("foo bar baz");
7886 e.jump_cursor(0, 5);
7887 run_keys(&mut e, "ge");
7888 assert_eq!(e.cursor(), (0, 2));
7889 }
7890
7891 #[test]
7892 fn ge_respects_word_class() {
7893 let mut e = editor_with("foo-bar baz");
7896 e.jump_cursor(0, 5);
7897 run_keys(&mut e, "ge");
7898 assert_eq!(e.cursor(), (0, 3));
7899 }
7900
7901 #[test]
7902 fn big_ge_treats_hyphens_as_part_of_word() {
7903 let mut e = editor_with("foo-bar baz");
7906 e.jump_cursor(0, 10);
7907 run_keys(&mut e, "gE");
7908 assert_eq!(e.cursor(), (0, 6));
7909 }
7910
7911 #[test]
7912 fn ge_crosses_line_boundary() {
7913 let mut e = editor_with("foo\nbar");
7914 e.jump_cursor(1, 0);
7915 run_keys(&mut e, "ge");
7916 assert_eq!(e.cursor(), (0, 2));
7917 }
7918
7919 #[test]
7920 fn dge_deletes_to_end_of_previous_word() {
7921 let mut e = editor_with("foo bar baz");
7922 e.jump_cursor(0, 8);
7923 run_keys(&mut e, "dge");
7926 assert_eq!(e.buffer().lines()[0], "foo baaz");
7927 }
7928
7929 #[test]
7930 fn ctrl_scroll_keys_do_not_panic() {
7931 let mut e = editor_with(
7934 (0..50)
7935 .map(|i| format!("line{i}"))
7936 .collect::<Vec<_>>()
7937 .join("\n")
7938 .as_str(),
7939 );
7940 run_keys(&mut e, "<C-f>");
7941 run_keys(&mut e, "<C-b>");
7942 assert!(!e.buffer().lines().is_empty());
7944 }
7945
7946 #[test]
7953 fn count_insert_with_arrow_nav_does_not_leak_rows() {
7954 let mut e = Editor::new(
7955 hjkl_buffer::Buffer::new(),
7956 crate::types::DefaultHost::new(),
7957 crate::types::Options::default(),
7958 );
7959 e.set_content("row0\nrow1\nrow2");
7960 run_keys(&mut e, "3iX<Down><Esc>");
7962 assert!(e.buffer().lines()[0].contains('X'));
7964 assert!(
7967 !e.buffer().lines()[1].contains("row0"),
7968 "row1 leaked row0 contents: {:?}",
7969 e.buffer().lines()[1]
7970 );
7971 assert_eq!(e.buffer().lines().len(), 3);
7974 }
7975
7976 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
7979 let mut e = Editor::new(
7980 hjkl_buffer::Buffer::new(),
7981 crate::types::DefaultHost::new(),
7982 crate::types::Options::default(),
7983 );
7984 let body = (0..n)
7985 .map(|i| format!(" line{}", i))
7986 .collect::<Vec<_>>()
7987 .join("\n");
7988 e.set_content(&body);
7989 e.set_viewport_height(viewport);
7990 e
7991 }
7992
7993 #[test]
7994 fn ctrl_d_moves_cursor_half_page_down() {
7995 let mut e = editor_with_rows(100, 20);
7996 run_keys(&mut e, "<C-d>");
7997 assert_eq!(e.cursor().0, 10);
7998 }
7999
8000 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8001 let mut e = Editor::new(
8002 hjkl_buffer::Buffer::new(),
8003 crate::types::DefaultHost::new(),
8004 crate::types::Options::default(),
8005 );
8006 e.set_content(&lines.join("\n"));
8007 e.set_viewport_height(viewport);
8008 let v = e.host_mut().viewport_mut();
8009 v.height = viewport;
8010 v.width = text_width;
8011 v.text_width = text_width;
8012 v.wrap = hjkl_buffer::Wrap::Char;
8013 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8014 e
8015 }
8016
8017 #[test]
8018 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8019 let lines = ["aaaabbbbcccc"; 10];
8023 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8024 e.jump_cursor(4, 0);
8025 e.ensure_cursor_in_scrolloff();
8026 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8027 assert!(csr <= 6, "csr={csr}");
8028 }
8029
8030 #[test]
8031 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8032 let lines = ["aaaabbbbcccc"; 10];
8033 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8034 e.jump_cursor(7, 0);
8037 e.ensure_cursor_in_scrolloff();
8038 e.jump_cursor(2, 0);
8039 e.ensure_cursor_in_scrolloff();
8040 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8041 assert!(csr >= 5, "csr={csr}");
8043 }
8044
8045 #[test]
8046 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8047 let lines = ["aaaabbbbcccc"; 5];
8048 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8049 e.jump_cursor(4, 11);
8050 e.ensure_cursor_in_scrolloff();
8051 let top = e.host().viewport().top_row;
8056 assert_eq!(top, 1);
8057 }
8058
8059 #[test]
8060 fn ctrl_u_moves_cursor_half_page_up() {
8061 let mut e = editor_with_rows(100, 20);
8062 e.jump_cursor(50, 0);
8063 run_keys(&mut e, "<C-u>");
8064 assert_eq!(e.cursor().0, 40);
8065 }
8066
8067 #[test]
8068 fn ctrl_f_moves_cursor_full_page_down() {
8069 let mut e = editor_with_rows(100, 20);
8070 run_keys(&mut e, "<C-f>");
8071 assert_eq!(e.cursor().0, 18);
8073 }
8074
8075 #[test]
8076 fn ctrl_b_moves_cursor_full_page_up() {
8077 let mut e = editor_with_rows(100, 20);
8078 e.jump_cursor(50, 0);
8079 run_keys(&mut e, "<C-b>");
8080 assert_eq!(e.cursor().0, 32);
8081 }
8082
8083 #[test]
8084 fn ctrl_d_lands_on_first_non_blank() {
8085 let mut e = editor_with_rows(100, 20);
8086 run_keys(&mut e, "<C-d>");
8087 assert_eq!(e.cursor().1, 2);
8089 }
8090
8091 #[test]
8092 fn ctrl_d_clamps_at_end_of_buffer() {
8093 let mut e = editor_with_rows(5, 20);
8094 run_keys(&mut e, "<C-d>");
8095 assert_eq!(e.cursor().0, 4);
8096 }
8097
8098 #[test]
8099 fn capital_h_jumps_to_viewport_top() {
8100 let mut e = editor_with_rows(100, 10);
8101 e.jump_cursor(50, 0);
8102 e.set_viewport_top(45);
8103 let top = e.host().viewport().top_row;
8104 run_keys(&mut e, "H");
8105 assert_eq!(e.cursor().0, top);
8106 assert_eq!(e.cursor().1, 2);
8107 }
8108
8109 #[test]
8110 fn capital_l_jumps_to_viewport_bottom() {
8111 let mut e = editor_with_rows(100, 10);
8112 e.jump_cursor(50, 0);
8113 e.set_viewport_top(45);
8114 let top = e.host().viewport().top_row;
8115 run_keys(&mut e, "L");
8116 assert_eq!(e.cursor().0, top + 9);
8117 }
8118
8119 #[test]
8120 fn capital_m_jumps_to_viewport_middle() {
8121 let mut e = editor_with_rows(100, 10);
8122 e.jump_cursor(50, 0);
8123 e.set_viewport_top(45);
8124 let top = e.host().viewport().top_row;
8125 run_keys(&mut e, "M");
8126 assert_eq!(e.cursor().0, top + 4);
8128 }
8129
8130 #[test]
8131 fn g_capital_m_lands_at_line_midpoint() {
8132 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8134 assert_eq!(e.cursor(), (0, 6));
8136 }
8137
8138 #[test]
8139 fn g_capital_m_on_empty_line_stays_at_zero() {
8140 let mut e = editor_with("");
8141 run_keys(&mut e, "gM");
8142 assert_eq!(e.cursor(), (0, 0));
8143 }
8144
8145 #[test]
8146 fn g_capital_m_uses_current_line_only() {
8147 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8150 run_keys(&mut e, "gM");
8151 assert_eq!(e.cursor(), (1, 6));
8152 }
8153
8154 #[test]
8155 fn capital_h_count_offsets_from_top() {
8156 let mut e = editor_with_rows(100, 10);
8157 e.jump_cursor(50, 0);
8158 e.set_viewport_top(45);
8159 let top = e.host().viewport().top_row;
8160 run_keys(&mut e, "3H");
8161 assert_eq!(e.cursor().0, top + 2);
8162 }
8163
8164 #[test]
8167 fn ctrl_o_returns_to_pre_g_position() {
8168 let mut e = editor_with_rows(50, 20);
8169 e.jump_cursor(5, 2);
8170 run_keys(&mut e, "G");
8171 assert_eq!(e.cursor().0, 49);
8172 run_keys(&mut e, "<C-o>");
8173 assert_eq!(e.cursor(), (5, 2));
8174 }
8175
8176 #[test]
8177 fn ctrl_i_redoes_jump_after_ctrl_o() {
8178 let mut e = editor_with_rows(50, 20);
8179 e.jump_cursor(5, 2);
8180 run_keys(&mut e, "G");
8181 let post = e.cursor();
8182 run_keys(&mut e, "<C-o>");
8183 run_keys(&mut e, "<C-i>");
8184 assert_eq!(e.cursor(), post);
8185 }
8186
8187 #[test]
8188 fn new_jump_clears_forward_stack() {
8189 let mut e = editor_with_rows(50, 20);
8190 e.jump_cursor(5, 2);
8191 run_keys(&mut e, "G");
8192 run_keys(&mut e, "<C-o>");
8193 run_keys(&mut e, "gg");
8194 run_keys(&mut e, "<C-i>");
8195 assert_eq!(e.cursor().0, 0);
8196 }
8197
8198 #[test]
8199 fn ctrl_o_on_empty_stack_is_noop() {
8200 let mut e = editor_with_rows(10, 20);
8201 e.jump_cursor(3, 1);
8202 run_keys(&mut e, "<C-o>");
8203 assert_eq!(e.cursor(), (3, 1));
8204 }
8205
8206 #[test]
8207 fn asterisk_search_pushes_jump() {
8208 let mut e = editor_with("foo bar\nbaz foo end");
8209 e.jump_cursor(0, 0);
8210 run_keys(&mut e, "*");
8211 let after = e.cursor();
8212 assert_ne!(after, (0, 0));
8213 run_keys(&mut e, "<C-o>");
8214 assert_eq!(e.cursor(), (0, 0));
8215 }
8216
8217 #[test]
8218 fn h_viewport_jump_is_recorded() {
8219 let mut e = editor_with_rows(100, 10);
8220 e.jump_cursor(50, 0);
8221 e.set_viewport_top(45);
8222 let pre = e.cursor();
8223 run_keys(&mut e, "H");
8224 assert_ne!(e.cursor(), pre);
8225 run_keys(&mut e, "<C-o>");
8226 assert_eq!(e.cursor(), pre);
8227 }
8228
8229 #[test]
8230 fn j_k_motion_does_not_push_jump() {
8231 let mut e = editor_with_rows(50, 20);
8232 e.jump_cursor(5, 0);
8233 run_keys(&mut e, "jjj");
8234 run_keys(&mut e, "<C-o>");
8235 assert_eq!(e.cursor().0, 8);
8236 }
8237
8238 #[test]
8239 fn jumplist_caps_at_100() {
8240 let mut e = editor_with_rows(200, 20);
8241 for i in 0..101 {
8242 e.jump_cursor(i, 0);
8243 run_keys(&mut e, "G");
8244 }
8245 assert!(e.vim.jump_back.len() <= 100);
8246 }
8247
8248 #[test]
8249 fn tab_acts_as_ctrl_i() {
8250 let mut e = editor_with_rows(50, 20);
8251 e.jump_cursor(5, 2);
8252 run_keys(&mut e, "G");
8253 let post = e.cursor();
8254 run_keys(&mut e, "<C-o>");
8255 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8256 assert_eq!(e.cursor(), post);
8257 }
8258
8259 #[test]
8262 fn ma_then_backtick_a_jumps_exact() {
8263 let mut e = editor_with_rows(50, 20);
8264 e.jump_cursor(5, 3);
8265 run_keys(&mut e, "ma");
8266 e.jump_cursor(20, 0);
8267 run_keys(&mut e, "`a");
8268 assert_eq!(e.cursor(), (5, 3));
8269 }
8270
8271 #[test]
8272 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8273 let mut e = editor_with_rows(50, 20);
8274 e.jump_cursor(5, 6);
8276 run_keys(&mut e, "ma");
8277 e.jump_cursor(30, 4);
8278 run_keys(&mut e, "'a");
8279 assert_eq!(e.cursor(), (5, 2));
8280 }
8281
8282 #[test]
8283 fn goto_mark_pushes_jumplist() {
8284 let mut e = editor_with_rows(50, 20);
8285 e.jump_cursor(10, 2);
8286 run_keys(&mut e, "mz");
8287 e.jump_cursor(3, 0);
8288 run_keys(&mut e, "`z");
8289 assert_eq!(e.cursor(), (10, 2));
8290 run_keys(&mut e, "<C-o>");
8291 assert_eq!(e.cursor(), (3, 0));
8292 }
8293
8294 #[test]
8295 fn goto_missing_mark_is_noop() {
8296 let mut e = editor_with_rows(50, 20);
8297 e.jump_cursor(3, 1);
8298 run_keys(&mut e, "`q");
8299 assert_eq!(e.cursor(), (3, 1));
8300 }
8301
8302 #[test]
8303 fn uppercase_mark_stored_under_uppercase_key() {
8304 let mut e = editor_with_rows(50, 20);
8305 e.jump_cursor(5, 3);
8306 run_keys(&mut e, "mA");
8307 assert_eq!(e.mark('A'), Some((5, 3)));
8310 assert!(e.mark('a').is_none());
8311 }
8312
8313 #[test]
8314 fn mark_survives_document_shrink_via_clamp() {
8315 let mut e = editor_with_rows(50, 20);
8316 e.jump_cursor(40, 4);
8317 run_keys(&mut e, "mx");
8318 e.set_content("a\nb\nc\nd\ne");
8320 run_keys(&mut e, "`x");
8321 let (r, _) = e.cursor();
8323 assert!(r <= 4);
8324 }
8325
8326 #[test]
8327 fn g_semicolon_walks_back_through_edits() {
8328 let mut e = editor_with("alpha\nbeta\ngamma");
8329 e.jump_cursor(0, 0);
8332 run_keys(&mut e, "iX<Esc>");
8333 e.jump_cursor(2, 0);
8334 run_keys(&mut e, "iY<Esc>");
8335 run_keys(&mut e, "g;");
8337 assert_eq!(e.cursor(), (2, 1));
8338 run_keys(&mut e, "g;");
8340 assert_eq!(e.cursor(), (0, 1));
8341 run_keys(&mut e, "g;");
8343 assert_eq!(e.cursor(), (0, 1));
8344 }
8345
8346 #[test]
8347 fn g_comma_walks_forward_after_g_semicolon() {
8348 let mut e = editor_with("a\nb\nc");
8349 e.jump_cursor(0, 0);
8350 run_keys(&mut e, "iX<Esc>");
8351 e.jump_cursor(2, 0);
8352 run_keys(&mut e, "iY<Esc>");
8353 run_keys(&mut e, "g;");
8354 run_keys(&mut e, "g;");
8355 assert_eq!(e.cursor(), (0, 1));
8356 run_keys(&mut e, "g,");
8357 assert_eq!(e.cursor(), (2, 1));
8358 }
8359
8360 #[test]
8361 fn new_edit_during_walk_trims_forward_entries() {
8362 let mut e = editor_with("a\nb\nc\nd");
8363 e.jump_cursor(0, 0);
8364 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8366 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8369 run_keys(&mut e, "g;");
8370 assert_eq!(e.cursor(), (0, 1));
8371 run_keys(&mut e, "iZ<Esc>");
8373 run_keys(&mut e, "g,");
8375 assert_ne!(e.cursor(), (2, 1));
8377 }
8378
8379 #[test]
8385 fn capital_mark_set_and_jump() {
8386 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8387 e.jump_cursor(2, 1);
8388 run_keys(&mut e, "mA");
8389 e.jump_cursor(0, 0);
8391 run_keys(&mut e, "'A");
8393 assert_eq!(e.cursor().0, 2);
8395 }
8396
8397 #[test]
8398 fn capital_mark_survives_set_content() {
8399 let mut e = editor_with("first buffer line\nsecond");
8400 e.jump_cursor(1, 3);
8401 run_keys(&mut e, "mA");
8402 e.set_content("totally different content\non many\nrows of text");
8404 e.jump_cursor(0, 0);
8406 run_keys(&mut e, "'A");
8407 assert_eq!(e.cursor().0, 1);
8408 }
8409
8410 #[test]
8415 fn capital_mark_shifts_with_edit() {
8416 let mut e = editor_with("a\nb\nc\nd");
8417 e.jump_cursor(3, 0);
8418 run_keys(&mut e, "mA");
8419 e.jump_cursor(0, 0);
8421 run_keys(&mut e, "dd");
8422 e.jump_cursor(0, 0);
8423 run_keys(&mut e, "'A");
8424 assert_eq!(e.cursor().0, 2);
8425 }
8426
8427 #[test]
8428 fn mark_below_delete_shifts_up() {
8429 let mut e = editor_with("a\nb\nc\nd\ne");
8430 e.jump_cursor(3, 0);
8432 run_keys(&mut e, "ma");
8433 e.jump_cursor(0, 0);
8435 run_keys(&mut e, "dd");
8436 e.jump_cursor(0, 0);
8438 run_keys(&mut e, "'a");
8439 assert_eq!(e.cursor().0, 2);
8440 assert_eq!(e.buffer().line(2).unwrap(), "d");
8441 }
8442
8443 #[test]
8444 fn mark_on_deleted_row_is_dropped() {
8445 let mut e = editor_with("a\nb\nc\nd");
8446 e.jump_cursor(1, 0);
8448 run_keys(&mut e, "ma");
8449 run_keys(&mut e, "dd");
8451 e.jump_cursor(2, 0);
8453 run_keys(&mut e, "'a");
8454 assert_eq!(e.cursor().0, 2);
8456 }
8457
8458 #[test]
8459 fn mark_above_edit_unchanged() {
8460 let mut e = editor_with("a\nb\nc\nd\ne");
8461 e.jump_cursor(0, 0);
8463 run_keys(&mut e, "ma");
8464 e.jump_cursor(3, 0);
8466 run_keys(&mut e, "dd");
8467 e.jump_cursor(2, 0);
8469 run_keys(&mut e, "'a");
8470 assert_eq!(e.cursor().0, 0);
8471 }
8472
8473 #[test]
8474 fn mark_shifts_down_after_insert() {
8475 let mut e = editor_with("a\nb\nc");
8476 e.jump_cursor(2, 0);
8478 run_keys(&mut e, "ma");
8479 e.jump_cursor(0, 0);
8481 run_keys(&mut e, "Onew<Esc>");
8482 e.jump_cursor(0, 0);
8485 run_keys(&mut e, "'a");
8486 assert_eq!(e.cursor().0, 3);
8487 assert_eq!(e.buffer().line(3).unwrap(), "c");
8488 }
8489
8490 #[test]
8493 fn forward_search_commit_pushes_jump() {
8494 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8495 e.jump_cursor(0, 0);
8496 run_keys(&mut e, "/target<CR>");
8497 assert_ne!(e.cursor(), (0, 0));
8499 run_keys(&mut e, "<C-o>");
8501 assert_eq!(e.cursor(), (0, 0));
8502 }
8503
8504 #[test]
8505 fn search_commit_no_match_does_not_push_jump() {
8506 let mut e = editor_with("alpha beta\nfoo end");
8507 e.jump_cursor(0, 3);
8508 let pre_len = e.vim.jump_back.len();
8509 run_keys(&mut e, "/zzznotfound<CR>");
8510 assert_eq!(e.vim.jump_back.len(), pre_len);
8512 }
8513
8514 #[test]
8517 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8518 let mut e = editor_with("hello world");
8519 run_keys(&mut e, "lll");
8520 let (row, col) = e.cursor();
8521 assert_eq!(e.buffer.cursor().row, row);
8522 assert_eq!(e.buffer.cursor().col, col);
8523 }
8524
8525 #[test]
8526 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8527 let mut e = editor_with("aaaa\nbbbb\ncccc");
8528 run_keys(&mut e, "jj");
8529 let (row, col) = e.cursor();
8530 assert_eq!(e.buffer.cursor().row, row);
8531 assert_eq!(e.buffer.cursor().col, col);
8532 }
8533
8534 #[test]
8535 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8536 let mut e = editor_with("foo bar baz");
8537 run_keys(&mut e, "ww");
8538 let (row, col) = e.cursor();
8539 assert_eq!(e.buffer.cursor().row, row);
8540 assert_eq!(e.buffer.cursor().col, col);
8541 }
8542
8543 #[test]
8544 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8545 let mut e = editor_with("a\nb\nc\nd\ne");
8546 run_keys(&mut e, "G");
8547 let (row, col) = e.cursor();
8548 assert_eq!(e.buffer.cursor().row, row);
8549 assert_eq!(e.buffer.cursor().col, col);
8550 }
8551
8552 #[test]
8553 fn editor_sticky_col_tracks_horizontal_motion() {
8554 let mut e = editor_with("longline\nhi\nlongline");
8555 run_keys(&mut e, "fl");
8560 let landed = e.cursor().1;
8561 assert!(landed > 0, "fl should have moved");
8562 run_keys(&mut e, "j");
8563 assert_eq!(e.sticky_col(), Some(landed));
8566 }
8567
8568 #[test]
8569 fn buffer_content_mirrors_textarea_after_insert() {
8570 let mut e = editor_with("hello");
8571 run_keys(&mut e, "iXYZ<Esc>");
8572 let text = e.buffer().lines().join("\n");
8573 assert_eq!(e.buffer.as_string(), text);
8574 }
8575
8576 #[test]
8577 fn buffer_content_mirrors_textarea_after_delete() {
8578 let mut e = editor_with("alpha bravo charlie");
8579 run_keys(&mut e, "dw");
8580 let text = e.buffer().lines().join("\n");
8581 assert_eq!(e.buffer.as_string(), text);
8582 }
8583
8584 #[test]
8585 fn buffer_content_mirrors_textarea_after_dd() {
8586 let mut e = editor_with("a\nb\nc\nd");
8587 run_keys(&mut e, "jdd");
8588 let text = e.buffer().lines().join("\n");
8589 assert_eq!(e.buffer.as_string(), text);
8590 }
8591
8592 #[test]
8593 fn buffer_content_mirrors_textarea_after_open_line() {
8594 let mut e = editor_with("foo\nbar");
8595 run_keys(&mut e, "oNEW<Esc>");
8596 let text = e.buffer().lines().join("\n");
8597 assert_eq!(e.buffer.as_string(), text);
8598 }
8599
8600 #[test]
8601 fn buffer_content_mirrors_textarea_after_paste() {
8602 let mut e = editor_with("hello");
8603 run_keys(&mut e, "yy");
8604 run_keys(&mut e, "p");
8605 let text = e.buffer().lines().join("\n");
8606 assert_eq!(e.buffer.as_string(), text);
8607 }
8608
8609 #[test]
8610 fn buffer_selection_none_in_normal_mode() {
8611 let e = editor_with("foo bar");
8612 assert!(e.buffer_selection().is_none());
8613 }
8614
8615 #[test]
8616 fn buffer_selection_char_in_visual_mode() {
8617 use hjkl_buffer::{Position, Selection};
8618 let mut e = editor_with("hello world");
8619 run_keys(&mut e, "vlll");
8620 assert_eq!(
8621 e.buffer_selection(),
8622 Some(Selection::Char {
8623 anchor: Position::new(0, 0),
8624 head: Position::new(0, 3),
8625 })
8626 );
8627 }
8628
8629 #[test]
8630 fn buffer_selection_line_in_visual_line_mode() {
8631 use hjkl_buffer::Selection;
8632 let mut e = editor_with("a\nb\nc\nd");
8633 run_keys(&mut e, "Vj");
8634 assert_eq!(
8635 e.buffer_selection(),
8636 Some(Selection::Line {
8637 anchor_row: 0,
8638 head_row: 1,
8639 })
8640 );
8641 }
8642
8643 #[test]
8644 fn wrapscan_off_blocks_wrap_around() {
8645 let mut e = editor_with("first\nsecond\nthird\n");
8646 e.settings_mut().wrapscan = false;
8647 e.jump_cursor(2, 0);
8649 run_keys(&mut e, "/first<CR>");
8650 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8652 e.settings_mut().wrapscan = true;
8654 run_keys(&mut e, "/first<CR>");
8655 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8656 }
8657
8658 #[test]
8659 fn smartcase_uppercase_pattern_stays_sensitive() {
8660 let mut e = editor_with("foo\nFoo\nBAR\n");
8661 e.settings_mut().ignore_case = true;
8662 e.settings_mut().smartcase = true;
8663 run_keys(&mut e, "/foo<CR>");
8666 let r1 = e
8667 .search_state()
8668 .pattern
8669 .as_ref()
8670 .unwrap()
8671 .as_str()
8672 .to_string();
8673 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8674 run_keys(&mut e, "/Foo<CR>");
8676 let r2 = e
8677 .search_state()
8678 .pattern
8679 .as_ref()
8680 .unwrap()
8681 .as_str()
8682 .to_string();
8683 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8684 }
8685
8686 #[test]
8687 fn enter_with_autoindent_copies_leading_whitespace() {
8688 let mut e = editor_with(" foo");
8689 e.jump_cursor(0, 7);
8690 run_keys(&mut e, "i<CR>");
8691 assert_eq!(e.buffer.line(1).unwrap(), " ");
8692 }
8693
8694 #[test]
8695 fn enter_without_autoindent_inserts_bare_newline() {
8696 let mut e = editor_with(" foo");
8697 e.settings_mut().autoindent = false;
8698 e.jump_cursor(0, 7);
8699 run_keys(&mut e, "i<CR>");
8700 assert_eq!(e.buffer.line(1).unwrap(), "");
8701 }
8702
8703 #[test]
8704 fn iskeyword_default_treats_alnum_underscore_as_word() {
8705 let mut e = editor_with("foo_bar baz");
8706 e.jump_cursor(0, 0);
8710 run_keys(&mut e, "*");
8711 let p = e
8712 .search_state()
8713 .pattern
8714 .as_ref()
8715 .unwrap()
8716 .as_str()
8717 .to_string();
8718 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8719 }
8720
8721 #[test]
8722 fn w_motion_respects_custom_iskeyword() {
8723 let mut e = editor_with("foo-bar baz");
8727 run_keys(&mut e, "w");
8728 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
8729 let mut e2 = editor_with("foo-bar baz");
8732 e2.set_iskeyword("@,_,45");
8733 run_keys(&mut e2, "w");
8734 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
8735 }
8736
8737 #[test]
8738 fn iskeyword_with_dash_treats_dash_as_word_char() {
8739 let mut e = editor_with("foo-bar baz");
8740 e.settings_mut().iskeyword = "@,_,45".to_string();
8741 e.jump_cursor(0, 0);
8742 run_keys(&mut e, "*");
8743 let p = e
8744 .search_state()
8745 .pattern
8746 .as_ref()
8747 .unwrap()
8748 .as_str()
8749 .to_string();
8750 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
8751 }
8752
8753 #[test]
8754 fn timeoutlen_drops_pending_g_prefix() {
8755 use std::time::{Duration, Instant};
8756 let mut e = editor_with("a\nb\nc");
8757 e.jump_cursor(2, 0);
8758 run_keys(&mut e, "g");
8760 assert!(matches!(e.vim.pending, super::Pending::G));
8761 e.settings.timeout_len = Duration::from_nanos(0);
8769 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
8770 e.vim.last_input_host_at = Some(Duration::ZERO);
8771 run_keys(&mut e, "g");
8775 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
8777 }
8778
8779 #[test]
8780 fn undobreak_on_breaks_group_at_arrow_motion() {
8781 let mut e = editor_with("");
8782 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8784 let line = e.buffer.line(0).unwrap_or("").to_string();
8787 assert!(line.contains("aaa"), "after undobreak: {line:?}");
8788 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
8789 }
8790
8791 #[test]
8792 fn undobreak_off_keeps_full_run_in_one_group() {
8793 let mut e = editor_with("");
8794 e.settings_mut().undo_break_on_motion = false;
8795 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8796 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
8799 }
8800
8801 #[test]
8802 fn undobreak_round_trips_through_options() {
8803 let e = editor_with("");
8804 let opts = e.current_options();
8805 assert!(opts.undo_break_on_motion);
8806 let mut e2 = editor_with("");
8807 let mut new_opts = opts.clone();
8808 new_opts.undo_break_on_motion = false;
8809 e2.apply_options(&new_opts);
8810 assert!(!e2.current_options().undo_break_on_motion);
8811 }
8812
8813 #[test]
8814 fn undo_levels_cap_drops_oldest() {
8815 let mut e = editor_with("abcde");
8816 e.settings_mut().undo_levels = 3;
8817 run_keys(&mut e, "ra");
8818 run_keys(&mut e, "lrb");
8819 run_keys(&mut e, "lrc");
8820 run_keys(&mut e, "lrd");
8821 run_keys(&mut e, "lre");
8822 assert_eq!(e.undo_stack_len(), 3);
8823 }
8824
8825 #[test]
8826 fn tab_inserts_literal_tab_when_noexpandtab() {
8827 let mut e = editor_with("");
8828 e.settings_mut().expandtab = false;
8831 e.settings_mut().softtabstop = 0;
8832 run_keys(&mut e, "i");
8833 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8834 assert_eq!(e.buffer.line(0).unwrap(), "\t");
8835 }
8836
8837 #[test]
8838 fn tab_inserts_spaces_when_expandtab() {
8839 let mut e = editor_with("");
8840 e.settings_mut().expandtab = true;
8841 e.settings_mut().tabstop = 4;
8842 run_keys(&mut e, "i");
8843 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8844 assert_eq!(e.buffer.line(0).unwrap(), " ");
8845 }
8846
8847 #[test]
8848 fn tab_with_softtabstop_fills_to_next_boundary() {
8849 let mut e = editor_with("ab");
8851 e.settings_mut().expandtab = true;
8852 e.settings_mut().tabstop = 8;
8853 e.settings_mut().softtabstop = 4;
8854 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8856 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
8857 }
8858
8859 #[test]
8860 fn backspace_deletes_softtab_run() {
8861 let mut e = editor_with(" x");
8864 e.settings_mut().softtabstop = 4;
8865 run_keys(&mut e, "fxi");
8867 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8868 assert_eq!(e.buffer.line(0).unwrap(), "x");
8869 }
8870
8871 #[test]
8872 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
8873 let mut e = editor_with(" x");
8876 e.settings_mut().softtabstop = 4;
8877 run_keys(&mut e, "fxi");
8878 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8879 assert_eq!(e.buffer.line(0).unwrap(), " x");
8880 }
8881
8882 #[test]
8883 fn readonly_blocks_insert_mutation() {
8884 let mut e = editor_with("hello");
8885 e.settings_mut().readonly = true;
8886 run_keys(&mut e, "iX<Esc>");
8887 assert_eq!(e.buffer.line(0).unwrap(), "hello");
8888 }
8889
8890 #[cfg(feature = "ratatui")]
8891 #[test]
8892 fn intern_ratatui_style_dedups_repeated_styles() {
8893 use ratatui::style::{Color, Style};
8894 let mut e = editor_with("");
8895 let red = Style::default().fg(Color::Red);
8896 let blue = Style::default().fg(Color::Blue);
8897 let id_r1 = e.intern_ratatui_style(red);
8898 let id_r2 = e.intern_ratatui_style(red);
8899 let id_b = e.intern_ratatui_style(blue);
8900 assert_eq!(id_r1, id_r2);
8901 assert_ne!(id_r1, id_b);
8902 assert_eq!(e.style_table().len(), 2);
8903 }
8904
8905 #[cfg(feature = "ratatui")]
8906 #[test]
8907 fn install_ratatui_syntax_spans_translates_styled_spans() {
8908 use ratatui::style::{Color, Style};
8909 let mut e = editor_with("SELECT foo");
8910 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
8911 let by_row = e.buffer_spans();
8912 assert_eq!(by_row.len(), 1);
8913 assert_eq!(by_row[0].len(), 1);
8914 assert_eq!(by_row[0][0].start_byte, 0);
8915 assert_eq!(by_row[0][0].end_byte, 6);
8916 let id = by_row[0][0].style;
8917 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
8918 }
8919
8920 #[cfg(feature = "ratatui")]
8921 #[test]
8922 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
8923 use ratatui::style::{Color, Style};
8924 let mut e = editor_with("hello");
8925 e.install_ratatui_syntax_spans(vec![vec![(
8926 0,
8927 usize::MAX,
8928 Style::default().fg(Color::Blue),
8929 )]]);
8930 let by_row = e.buffer_spans();
8931 assert_eq!(by_row[0][0].end_byte, 5);
8932 }
8933
8934 #[cfg(feature = "ratatui")]
8935 #[test]
8936 fn install_ratatui_syntax_spans_drops_zero_width() {
8937 use ratatui::style::{Color, Style};
8938 let mut e = editor_with("abc");
8939 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
8940 assert!(e.buffer_spans()[0].is_empty());
8941 }
8942
8943 #[test]
8944 fn named_register_yank_into_a_then_paste_from_a() {
8945 let mut e = editor_with("hello world\nsecond");
8946 run_keys(&mut e, "\"ayw");
8947 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8949 run_keys(&mut e, "j0\"aP");
8951 assert_eq!(e.buffer().lines()[1], "hello second");
8952 }
8953
8954 #[test]
8955 fn capital_r_overstrikes_chars() {
8956 let mut e = editor_with("hello");
8957 e.jump_cursor(0, 0);
8958 run_keys(&mut e, "RXY<Esc>");
8959 assert_eq!(e.buffer().lines()[0], "XYllo");
8961 }
8962
8963 #[test]
8964 fn capital_r_at_eol_appends() {
8965 let mut e = editor_with("hi");
8966 e.jump_cursor(0, 1);
8967 run_keys(&mut e, "RXYZ<Esc>");
8969 assert_eq!(e.buffer().lines()[0], "hXYZ");
8970 }
8971
8972 #[test]
8973 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
8974 let mut e = editor_with("abc");
8978 e.jump_cursor(0, 0);
8979 run_keys(&mut e, "RX<Esc>");
8980 assert_eq!(e.buffer().lines()[0], "Xbc");
8981 }
8982
8983 #[test]
8984 fn ctrl_r_in_insert_pastes_named_register() {
8985 let mut e = editor_with("hello world");
8986 run_keys(&mut e, "\"ayw");
8988 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8989 run_keys(&mut e, "o");
8991 assert_eq!(e.vim_mode(), VimMode::Insert);
8992 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8993 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
8994 assert_eq!(e.buffer().lines()[1], "hello ");
8995 assert_eq!(e.cursor(), (1, 6));
8997 assert_eq!(e.vim_mode(), VimMode::Insert);
8999 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9000 assert_eq!(e.buffer().lines()[1], "hello X");
9001 }
9002
9003 #[test]
9004 fn ctrl_r_with_unnamed_register() {
9005 let mut e = editor_with("foo");
9006 run_keys(&mut e, "yiw");
9007 run_keys(&mut e, "A ");
9008 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9010 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9011 assert_eq!(e.buffer().lines()[0], "foo foo");
9012 }
9013
9014 #[test]
9015 fn ctrl_r_unknown_selector_is_no_op() {
9016 let mut e = editor_with("abc");
9017 run_keys(&mut e, "A");
9018 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9019 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9022 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9023 assert_eq!(e.buffer().lines()[0], "abcZ");
9024 }
9025
9026 #[test]
9027 fn ctrl_r_multiline_register_pastes_with_newlines() {
9028 let mut e = editor_with("alpha\nbeta\ngamma");
9029 run_keys(&mut e, "\"byy");
9031 run_keys(&mut e, "j\"byy");
9032 run_keys(&mut e, "ggVj\"by");
9036 let payload = e.registers().read('b').unwrap().text.clone();
9037 assert!(payload.contains('\n'));
9038 run_keys(&mut e, "Go");
9039 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9040 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9041 let total_lines = e.buffer().lines().len();
9044 assert!(total_lines >= 5);
9045 }
9046
9047 #[test]
9048 fn yank_zero_holds_last_yank_after_delete() {
9049 let mut e = editor_with("hello world");
9050 run_keys(&mut e, "yw");
9051 let yanked = e.registers().read('0').unwrap().text.clone();
9052 assert!(!yanked.is_empty());
9053 run_keys(&mut e, "dw");
9055 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9056 assert!(!e.registers().read('1').unwrap().text.is_empty());
9058 }
9059
9060 #[test]
9061 fn delete_ring_rotates_through_one_through_nine() {
9062 let mut e = editor_with("a b c d e f g h i j");
9063 for _ in 0..3 {
9065 run_keys(&mut e, "dw");
9066 }
9067 let r1 = e.registers().read('1').unwrap().text.clone();
9069 let r2 = e.registers().read('2').unwrap().text.clone();
9070 let r3 = e.registers().read('3').unwrap().text.clone();
9071 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9072 assert_ne!(r1, r2);
9073 assert_ne!(r2, r3);
9074 }
9075
9076 #[test]
9077 fn capital_register_appends_to_lowercase() {
9078 let mut e = editor_with("foo bar");
9079 run_keys(&mut e, "\"ayw");
9080 let first = e.registers().read('a').unwrap().text.clone();
9081 assert!(first.contains("foo"));
9082 run_keys(&mut e, "w\"Ayw");
9084 let combined = e.registers().read('a').unwrap().text.clone();
9085 assert!(combined.starts_with(&first));
9086 assert!(combined.contains("bar"));
9087 }
9088
9089 #[test]
9090 fn zf_in_visual_line_creates_closed_fold() {
9091 let mut e = editor_with("a\nb\nc\nd\ne");
9092 e.jump_cursor(1, 0);
9094 run_keys(&mut e, "Vjjzf");
9095 assert_eq!(e.buffer().folds().len(), 1);
9096 let f = e.buffer().folds()[0];
9097 assert_eq!(f.start_row, 1);
9098 assert_eq!(f.end_row, 3);
9099 assert!(f.closed);
9100 }
9101
9102 #[test]
9103 fn zfj_in_normal_creates_two_row_fold() {
9104 let mut e = editor_with("a\nb\nc\nd\ne");
9105 e.jump_cursor(1, 0);
9106 run_keys(&mut e, "zfj");
9107 assert_eq!(e.buffer().folds().len(), 1);
9108 let f = e.buffer().folds()[0];
9109 assert_eq!(f.start_row, 1);
9110 assert_eq!(f.end_row, 2);
9111 assert!(f.closed);
9112 assert_eq!(e.cursor().0, 1);
9114 }
9115
9116 #[test]
9117 fn zf_with_count_folds_count_rows() {
9118 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9119 e.jump_cursor(0, 0);
9120 run_keys(&mut e, "zf3j");
9122 assert_eq!(e.buffer().folds().len(), 1);
9123 let f = e.buffer().folds()[0];
9124 assert_eq!(f.start_row, 0);
9125 assert_eq!(f.end_row, 3);
9126 }
9127
9128 #[test]
9129 fn zfk_folds_upward_range() {
9130 let mut e = editor_with("a\nb\nc\nd\ne");
9131 e.jump_cursor(3, 0);
9132 run_keys(&mut e, "zfk");
9133 let f = e.buffer().folds()[0];
9134 assert_eq!(f.start_row, 2);
9136 assert_eq!(f.end_row, 3);
9137 }
9138
9139 #[test]
9140 fn zf_capital_g_folds_to_bottom() {
9141 let mut e = editor_with("a\nb\nc\nd\ne");
9142 e.jump_cursor(1, 0);
9143 run_keys(&mut e, "zfG");
9145 let f = e.buffer().folds()[0];
9146 assert_eq!(f.start_row, 1);
9147 assert_eq!(f.end_row, 4);
9148 }
9149
9150 #[test]
9151 fn zfgg_folds_to_top_via_operator_pipeline() {
9152 let mut e = editor_with("a\nb\nc\nd\ne");
9153 e.jump_cursor(3, 0);
9154 run_keys(&mut e, "zfgg");
9158 let f = e.buffer().folds()[0];
9159 assert_eq!(f.start_row, 0);
9160 assert_eq!(f.end_row, 3);
9161 }
9162
9163 #[test]
9164 fn zfip_folds_paragraph_via_text_object() {
9165 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9166 e.jump_cursor(1, 0);
9167 run_keys(&mut e, "zfip");
9169 assert_eq!(e.buffer().folds().len(), 1);
9170 let f = e.buffer().folds()[0];
9171 assert_eq!(f.start_row, 0);
9172 assert_eq!(f.end_row, 2);
9173 }
9174
9175 #[test]
9176 fn zfap_folds_paragraph_with_trailing_blank() {
9177 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9178 e.jump_cursor(0, 0);
9179 run_keys(&mut e, "zfap");
9181 let f = e.buffer().folds()[0];
9182 assert_eq!(f.start_row, 0);
9183 assert_eq!(f.end_row, 3);
9184 }
9185
9186 #[test]
9187 fn zf_paragraph_motion_folds_to_blank() {
9188 let mut e = editor_with("alpha\nbeta\n\ngamma");
9189 e.jump_cursor(0, 0);
9190 run_keys(&mut e, "zf}");
9192 let f = e.buffer().folds()[0];
9193 assert_eq!(f.start_row, 0);
9194 assert_eq!(f.end_row, 2);
9195 }
9196
9197 #[test]
9198 fn za_toggles_fold_under_cursor() {
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 run_keys(&mut e, "za");
9203 assert!(!e.buffer().folds()[0].closed);
9204 run_keys(&mut e, "za");
9205 assert!(e.buffer().folds()[0].closed);
9206 }
9207
9208 #[test]
9209 fn zr_opens_all_folds_zm_closes_all() {
9210 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9211 e.buffer_mut().add_fold(0, 1, true);
9212 e.buffer_mut().add_fold(2, 3, true);
9213 e.buffer_mut().add_fold(4, 5, true);
9214 run_keys(&mut e, "zR");
9215 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9216 run_keys(&mut e, "zM");
9217 assert!(e.buffer().folds().iter().all(|f| f.closed));
9218 }
9219
9220 #[test]
9221 fn ze_clears_all_folds() {
9222 let mut e = editor_with("a\nb\nc\nd");
9223 e.buffer_mut().add_fold(0, 1, true);
9224 e.buffer_mut().add_fold(2, 3, false);
9225 run_keys(&mut e, "zE");
9226 assert!(e.buffer().folds().is_empty());
9227 }
9228
9229 #[test]
9230 fn g_underscore_jumps_to_last_non_blank() {
9231 let mut e = editor_with("hello world ");
9232 run_keys(&mut e, "g_");
9233 assert_eq!(e.cursor().1, 10);
9235 }
9236
9237 #[test]
9238 fn gj_and_gk_alias_j_and_k() {
9239 let mut e = editor_with("a\nb\nc");
9240 run_keys(&mut e, "gj");
9241 assert_eq!(e.cursor().0, 1);
9242 run_keys(&mut e, "gk");
9243 assert_eq!(e.cursor().0, 0);
9244 }
9245
9246 #[test]
9247 fn paragraph_motions_walk_blank_lines() {
9248 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9249 run_keys(&mut e, "}");
9250 assert_eq!(e.cursor().0, 2);
9251 run_keys(&mut e, "}");
9252 assert_eq!(e.cursor().0, 5);
9253 run_keys(&mut e, "{");
9254 assert_eq!(e.cursor().0, 2);
9255 }
9256
9257 #[test]
9258 fn gv_reenters_last_visual_selection() {
9259 let mut e = editor_with("alpha\nbeta\ngamma");
9260 run_keys(&mut e, "Vj");
9261 run_keys(&mut e, "<Esc>");
9263 assert_eq!(e.vim_mode(), VimMode::Normal);
9264 run_keys(&mut e, "gv");
9266 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9267 }
9268
9269 #[test]
9270 fn o_in_visual_swaps_anchor_and_cursor() {
9271 let mut e = editor_with("hello world");
9272 run_keys(&mut e, "vllll");
9274 assert_eq!(e.cursor().1, 4);
9275 run_keys(&mut e, "o");
9277 assert_eq!(e.cursor().1, 0);
9278 assert_eq!(e.vim.visual_anchor, (0, 4));
9280 }
9281
9282 #[test]
9283 fn editing_inside_fold_invalidates_it() {
9284 let mut e = editor_with("a\nb\nc\nd");
9285 e.buffer_mut().add_fold(1, 2, true);
9286 e.jump_cursor(1, 0);
9287 run_keys(&mut e, "iX<Esc>");
9289 assert!(e.buffer().folds().is_empty());
9291 }
9292
9293 #[test]
9294 fn zd_removes_fold_under_cursor() {
9295 let mut e = editor_with("a\nb\nc\nd");
9296 e.buffer_mut().add_fold(1, 2, true);
9297 e.jump_cursor(2, 0);
9298 run_keys(&mut e, "zd");
9299 assert!(e.buffer().folds().is_empty());
9300 }
9301
9302 #[test]
9303 fn take_fold_ops_observes_z_keystroke_dispatch() {
9304 use crate::types::FoldOp;
9309 let mut e = editor_with("a\nb\nc\nd");
9310 e.buffer_mut().add_fold(1, 2, true);
9311 e.jump_cursor(1, 0);
9312 let _ = e.take_fold_ops();
9315 run_keys(&mut e, "zo");
9316 run_keys(&mut e, "zM");
9317 let ops = e.take_fold_ops();
9318 assert_eq!(ops.len(), 2);
9319 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9320 assert!(matches!(ops[1], FoldOp::CloseAll));
9321 assert!(e.take_fold_ops().is_empty());
9323 }
9324
9325 #[test]
9326 fn edit_pipeline_emits_invalidate_fold_op() {
9327 use crate::types::FoldOp;
9330 let mut e = editor_with("a\nb\nc\nd");
9331 e.buffer_mut().add_fold(1, 2, true);
9332 e.jump_cursor(1, 0);
9333 let _ = e.take_fold_ops();
9334 run_keys(&mut e, "iX<Esc>");
9335 let ops = e.take_fold_ops();
9336 assert!(
9337 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9338 "expected at least one Invalidate op, got {ops:?}"
9339 );
9340 }
9341
9342 #[test]
9343 fn dot_mark_jumps_to_last_edit_position() {
9344 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9345 e.jump_cursor(2, 0);
9346 run_keys(&mut e, "iX<Esc>");
9348 let after_edit = e.cursor();
9349 run_keys(&mut e, "gg");
9351 assert_eq!(e.cursor().0, 0);
9352 run_keys(&mut e, "'.");
9354 assert_eq!(e.cursor().0, after_edit.0);
9355 }
9356
9357 #[test]
9358 fn quote_quote_returns_to_pre_jump_position() {
9359 let mut e = editor_with_rows(50, 20);
9360 e.jump_cursor(10, 2);
9361 let before = e.cursor();
9362 run_keys(&mut e, "G");
9364 assert_ne!(e.cursor(), before);
9365 run_keys(&mut e, "''");
9367 assert_eq!(e.cursor().0, before.0);
9368 }
9369
9370 #[test]
9371 fn backtick_backtick_restores_exact_pre_jump_pos() {
9372 let mut e = editor_with_rows(50, 20);
9373 e.jump_cursor(7, 3);
9374 let before = e.cursor();
9375 run_keys(&mut e, "G");
9376 run_keys(&mut e, "``");
9377 assert_eq!(e.cursor(), before);
9378 }
9379
9380 #[test]
9381 fn macro_record_and_replay_basic() {
9382 let mut e = editor_with("foo\nbar\nbaz");
9383 run_keys(&mut e, "qaIX<Esc>jq");
9385 assert_eq!(e.buffer().lines()[0], "Xfoo");
9386 run_keys(&mut e, "@a");
9388 assert_eq!(e.buffer().lines()[1], "Xbar");
9389 run_keys(&mut e, "j@@");
9391 assert_eq!(e.buffer().lines()[2], "Xbaz");
9392 }
9393
9394 #[test]
9395 fn macro_count_replays_n_times() {
9396 let mut e = editor_with("a\nb\nc\nd\ne");
9397 run_keys(&mut e, "qajq");
9399 assert_eq!(e.cursor().0, 1);
9400 run_keys(&mut e, "3@a");
9402 assert_eq!(e.cursor().0, 4);
9403 }
9404
9405 #[test]
9406 fn macro_capital_q_appends_to_lowercase_register() {
9407 let mut e = editor_with("hello");
9408 run_keys(&mut e, "qall<Esc>q");
9409 run_keys(&mut e, "qAhh<Esc>q");
9410 let text = e.registers().read('a').unwrap().text.clone();
9413 assert!(text.contains("ll<Esc>"));
9414 assert!(text.contains("hh<Esc>"));
9415 }
9416
9417 #[test]
9418 fn buffer_selection_block_in_visual_block_mode() {
9419 use hjkl_buffer::{Position, Selection};
9420 let mut e = editor_with("aaaa\nbbbb\ncccc");
9421 run_keys(&mut e, "<C-v>jl");
9422 assert_eq!(
9423 e.buffer_selection(),
9424 Some(Selection::Block {
9425 anchor: Position::new(0, 0),
9426 head: Position::new(1, 1),
9427 })
9428 );
9429 }
9430
9431 #[test]
9434 fn n_after_question_mark_keeps_walking_backward() {
9435 let mut e = editor_with("foo bar foo baz foo end");
9438 e.jump_cursor(0, 22);
9439 run_keys(&mut e, "?foo<CR>");
9440 assert_eq!(e.cursor().1, 16);
9441 run_keys(&mut e, "n");
9442 assert_eq!(e.cursor().1, 8);
9443 run_keys(&mut e, "N");
9444 assert_eq!(e.cursor().1, 16);
9445 }
9446
9447 #[test]
9448 fn nested_macro_chord_records_literal_keys() {
9449 let mut e = editor_with("alpha\nbeta\ngamma");
9452 run_keys(&mut e, "qblq");
9454 run_keys(&mut e, "qaIX<Esc>q");
9457 e.jump_cursor(1, 0);
9459 run_keys(&mut e, "@a");
9460 assert_eq!(e.buffer().lines()[1], "Xbeta");
9461 }
9462
9463 #[test]
9464 fn shift_gt_motion_indents_one_line() {
9465 let mut e = editor_with("hello world");
9469 run_keys(&mut e, ">w");
9470 assert_eq!(e.buffer().lines()[0], " hello world");
9471 }
9472
9473 #[test]
9474 fn shift_lt_motion_outdents_one_line() {
9475 let mut e = editor_with(" hello world");
9476 run_keys(&mut e, "<lt>w");
9477 assert_eq!(e.buffer().lines()[0], " hello world");
9479 }
9480
9481 #[test]
9482 fn shift_gt_text_object_indents_paragraph() {
9483 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9484 e.jump_cursor(0, 0);
9485 run_keys(&mut e, ">ip");
9486 assert_eq!(e.buffer().lines()[0], " alpha");
9487 assert_eq!(e.buffer().lines()[1], " beta");
9488 assert_eq!(e.buffer().lines()[2], " gamma");
9489 assert_eq!(e.buffer().lines()[4], "rest");
9491 }
9492
9493 #[test]
9494 fn ctrl_o_runs_exactly_one_normal_command() {
9495 let mut e = editor_with("alpha beta gamma");
9498 e.jump_cursor(0, 0);
9499 run_keys(&mut e, "i");
9500 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9501 run_keys(&mut e, "dw");
9502 assert_eq!(e.vim_mode(), VimMode::Insert);
9504 run_keys(&mut e, "X");
9506 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9507 }
9508
9509 #[test]
9510 fn macro_replay_respects_mode_switching() {
9511 let mut e = editor_with("hi");
9515 run_keys(&mut e, "qaiX<Esc>0q");
9516 assert_eq!(e.vim_mode(), VimMode::Normal);
9517 e.set_content("yo");
9519 run_keys(&mut e, "@a");
9520 assert_eq!(e.vim_mode(), VimMode::Normal);
9521 assert_eq!(e.cursor().1, 0);
9522 assert_eq!(e.buffer().lines()[0], "Xyo");
9523 }
9524
9525 #[test]
9526 fn macro_recorded_text_round_trips_through_register() {
9527 let mut e = editor_with("");
9531 run_keys(&mut e, "qaiX<Esc>q");
9532 let text = e.registers().read('a').unwrap().text.clone();
9533 assert!(text.starts_with("iX"));
9534 run_keys(&mut e, "@a");
9536 assert_eq!(e.buffer().lines()[0], "XX");
9537 }
9538
9539 #[test]
9540 fn dot_after_macro_replays_macros_last_change() {
9541 let mut e = editor_with("ab\ncd\nef");
9544 run_keys(&mut e, "qaIX<Esc>jq");
9547 assert_eq!(e.buffer().lines()[0], "Xab");
9548 run_keys(&mut e, "@a");
9549 assert_eq!(e.buffer().lines()[1], "Xcd");
9550 let row_before_dot = e.cursor().0;
9553 run_keys(&mut e, ".");
9554 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9555 }
9556
9557 fn si_editor(content: &str) -> Editor {
9563 let opts = crate::types::Options {
9564 shiftwidth: 4,
9565 softtabstop: 4,
9566 expandtab: true,
9567 smartindent: true,
9568 autoindent: true,
9569 ..crate::types::Options::default()
9570 };
9571 let mut e = Editor::new(
9572 hjkl_buffer::Buffer::new(),
9573 crate::types::DefaultHost::new(),
9574 opts,
9575 );
9576 e.set_content(content);
9577 e
9578 }
9579
9580 #[test]
9581 fn smartindent_bumps_indent_after_open_brace() {
9582 let mut e = si_editor("fn foo() {");
9584 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9586 assert_eq!(
9587 e.buffer().lines()[1],
9588 " ",
9589 "smartindent should bump one shiftwidth after {{"
9590 );
9591 }
9592
9593 #[test]
9594 fn smartindent_no_bump_when_off() {
9595 let mut e = si_editor("fn foo() {");
9598 e.settings_mut().smartindent = false;
9599 e.jump_cursor(0, 10);
9600 run_keys(&mut e, "i<CR>");
9601 assert_eq!(
9602 e.buffer().lines()[1],
9603 "",
9604 "without smartindent, no bump: new line copies empty leading ws"
9605 );
9606 }
9607
9608 #[test]
9609 fn smartindent_uses_tab_when_noexpandtab() {
9610 let opts = crate::types::Options {
9612 shiftwidth: 4,
9613 softtabstop: 0,
9614 expandtab: false,
9615 smartindent: true,
9616 autoindent: true,
9617 ..crate::types::Options::default()
9618 };
9619 let mut e = Editor::new(
9620 hjkl_buffer::Buffer::new(),
9621 crate::types::DefaultHost::new(),
9622 opts,
9623 );
9624 e.set_content("fn foo() {");
9625 e.jump_cursor(0, 10);
9626 run_keys(&mut e, "i<CR>");
9627 assert_eq!(
9628 e.buffer().lines()[1],
9629 "\t",
9630 "noexpandtab: smartindent bump inserts a literal tab"
9631 );
9632 }
9633
9634 #[test]
9635 fn smartindent_dedent_on_close_brace() {
9636 let mut e = si_editor("fn foo() {");
9639 e.set_content("fn foo() {\n ");
9641 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9643 assert_eq!(
9644 e.buffer().lines()[1],
9645 "}",
9646 "close brace on whitespace-only line should dedent"
9647 );
9648 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9649 }
9650
9651 #[test]
9652 fn smartindent_no_dedent_when_off() {
9653 let mut e = si_editor("fn foo() {\n ");
9655 e.settings_mut().smartindent = false;
9656 e.jump_cursor(1, 4);
9657 run_keys(&mut e, "i}");
9658 assert_eq!(
9659 e.buffer().lines()[1],
9660 " }",
9661 "without smartindent, `}}` just appends at cursor"
9662 );
9663 }
9664
9665 #[test]
9666 fn smartindent_no_dedent_mid_line() {
9667 let mut e = si_editor(" let x = 1");
9670 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9672 assert_eq!(
9673 e.buffer().lines()[0],
9674 " let x = 1}",
9675 "mid-line `}}` should not dedent"
9676 );
9677 }
9678
9679 #[test]
9683 fn count_5x_fills_unnamed_register() {
9684 let mut e = editor_with("hello world\n");
9685 e.jump_cursor(0, 0);
9686 run_keys(&mut e, "5x");
9687 assert_eq!(e.buffer().lines()[0], " world");
9688 assert_eq!(e.cursor(), (0, 0));
9689 assert_eq!(e.yank(), "hello");
9690 }
9691
9692 #[test]
9693 fn x_fills_unnamed_register_single_char() {
9694 let mut e = editor_with("abc\n");
9695 e.jump_cursor(0, 0);
9696 run_keys(&mut e, "x");
9697 assert_eq!(e.buffer().lines()[0], "bc");
9698 assert_eq!(e.yank(), "a");
9699 }
9700
9701 #[test]
9702 fn big_x_fills_unnamed_register() {
9703 let mut e = editor_with("hello\n");
9704 e.jump_cursor(0, 3);
9705 run_keys(&mut e, "X");
9706 assert_eq!(e.buffer().lines()[0], "helo");
9707 assert_eq!(e.yank(), "l");
9708 }
9709
9710 #[test]
9712 fn g_motion_trailing_newline_lands_on_last_content_row() {
9713 let mut e = editor_with("foo\nbar\nbaz\n");
9714 e.jump_cursor(0, 0);
9715 run_keys(&mut e, "G");
9716 assert_eq!(
9718 e.cursor().0,
9719 2,
9720 "G should land on row 2 (baz), not row 3 (phantom empty)"
9721 );
9722 }
9723
9724 #[test]
9726 fn dd_last_line_clamps_cursor_to_new_last_row() {
9727 let mut e = editor_with("foo\nbar\n");
9728 e.jump_cursor(1, 0);
9729 run_keys(&mut e, "dd");
9730 assert_eq!(e.buffer().lines()[0], "foo");
9731 assert_eq!(
9732 e.cursor(),
9733 (0, 0),
9734 "cursor should clamp to row 0 after dd on last content line"
9735 );
9736 }
9737
9738 #[test]
9740 fn d_dollar_cursor_on_last_char() {
9741 let mut e = editor_with("hello world\n");
9742 e.jump_cursor(0, 5);
9743 run_keys(&mut e, "d$");
9744 assert_eq!(e.buffer().lines()[0], "hello");
9745 assert_eq!(
9746 e.cursor(),
9747 (0, 4),
9748 "d$ should leave cursor on col 4, not col 5"
9749 );
9750 }
9751
9752 #[test]
9754 fn undo_insert_clamps_cursor_to_last_valid_col() {
9755 let mut e = editor_with("hello\n");
9756 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
9758 assert_eq!(e.buffer().lines()[0], "hello");
9759 assert_eq!(
9760 e.cursor(),
9761 (0, 4),
9762 "undo should clamp cursor to col 4 on 'hello'"
9763 );
9764 }
9765
9766 #[test]
9768 fn da_doublequote_eats_trailing_whitespace() {
9769 let mut e = editor_with("say \"hello\" there\n");
9770 e.jump_cursor(0, 6);
9771 run_keys(&mut e, "da\"");
9772 assert_eq!(e.buffer().lines()[0], "say there");
9773 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
9774 }
9775
9776 #[test]
9778 fn dab_cursor_col_clamped_after_delete() {
9779 let mut e = editor_with("fn x() {\n body\n}\n");
9780 e.jump_cursor(1, 4);
9781 run_keys(&mut e, "daB");
9782 assert_eq!(e.buffer().lines()[0], "fn x() ");
9783 assert_eq!(
9784 e.cursor(),
9785 (0, 6),
9786 "daB should leave cursor at col 6, not 7"
9787 );
9788 }
9789
9790 #[test]
9792 fn dib_preserves_surrounding_newlines() {
9793 let mut e = editor_with("{\n body\n}\n");
9794 e.jump_cursor(1, 4);
9795 run_keys(&mut e, "diB");
9796 assert_eq!(e.buffer().lines()[0], "{");
9797 assert_eq!(e.buffer().lines()[1], "}");
9798 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
9799 }
9800
9801 #[test]
9802 fn is_chord_pending_tracks_replace_state() {
9803 let mut e = editor_with("abc\n");
9804 assert!(!e.is_chord_pending());
9805 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
9807 assert!(e.is_chord_pending(), "engine should be pending after r");
9808 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
9810 assert!(
9811 !e.is_chord_pending(),
9812 "engine pending should clear after replace"
9813 );
9814 }
9815}