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