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
570fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
576 ed.vim.search_prompt = Some(SearchPrompt {
577 text: String::new(),
578 cursor: 0,
579 forward,
580 });
581 ed.vim.search_history_cursor = None;
582 ed.set_search_pattern(None);
586}
587
588fn push_search_pattern<H: crate::types::Host>(
593 ed: &mut Editor<hjkl_buffer::Buffer, H>,
594 pattern: &str,
595) {
596 let compiled = if pattern.is_empty() {
597 None
598 } else {
599 let case_insensitive = ed.settings().ignore_case
606 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
607 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
608 std::borrow::Cow::Owned(format!("(?i){pattern}"))
609 } else {
610 std::borrow::Cow::Borrowed(pattern)
611 };
612 regex::Regex::new(&effective).ok()
613 };
614 let wrap = ed.settings().wrapscan;
615 ed.set_search_pattern(compiled);
619 ed.search_state_mut().wrap_around = wrap;
620}
621
622fn step_search_prompt<H: crate::types::Host>(
623 ed: &mut Editor<hjkl_buffer::Buffer, H>,
624 input: Input,
625) -> bool {
626 let history_dir = match (input.key, input.ctrl) {
630 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
631 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
632 _ => None,
633 };
634 if let Some(dir) = history_dir {
635 walk_search_history(ed, dir);
636 return true;
637 }
638 match input.key {
639 Key::Esc => {
640 let text = ed
643 .vim
644 .search_prompt
645 .take()
646 .map(|p| p.text)
647 .unwrap_or_default();
648 if !text.is_empty() {
649 ed.vim.last_search = Some(text);
650 }
651 ed.vim.search_history_cursor = None;
652 }
653 Key::Enter => {
654 let prompt = ed.vim.search_prompt.take();
655 if let Some(p) = prompt {
656 let pattern = if p.text.is_empty() {
659 ed.vim.last_search.clone()
660 } else {
661 Some(p.text.clone())
662 };
663 if let Some(pattern) = pattern {
664 push_search_pattern(ed, &pattern);
665 let pre = ed.cursor();
666 if p.forward {
667 ed.search_advance_forward(true);
668 } else {
669 ed.search_advance_backward(true);
670 }
671 ed.push_buffer_cursor_to_textarea();
672 if ed.cursor() != pre {
673 push_jump(ed, pre);
674 }
675 record_search_history(ed, &pattern);
676 ed.vim.last_search = Some(pattern);
677 ed.vim.last_search_forward = p.forward;
678 }
679 }
680 ed.vim.search_history_cursor = None;
681 }
682 Key::Backspace => {
683 ed.vim.search_history_cursor = None;
684 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
685 if p.text.pop().is_some() {
686 p.cursor = p.text.chars().count();
687 Some(p.text.clone())
688 } else {
689 None
690 }
691 });
692 if let Some(text) = new_text {
693 push_search_pattern(ed, &text);
694 }
695 }
696 Key::Char(c) => {
697 ed.vim.search_history_cursor = None;
698 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
699 p.text.push(c);
700 p.cursor = p.text.chars().count();
701 p.text.clone()
702 });
703 if let Some(text) = new_text {
704 push_search_pattern(ed, &text);
705 }
706 }
707 _ => {}
708 }
709 true
710}
711
712fn walk_change_list<H: crate::types::Host>(
716 ed: &mut Editor<hjkl_buffer::Buffer, H>,
717 dir: isize,
718 count: usize,
719) {
720 if ed.vim.change_list.is_empty() {
721 return;
722 }
723 let len = ed.vim.change_list.len();
724 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
725 (None, -1) => len as isize - 1,
726 (None, 1) => return, (Some(i), -1) => i as isize - 1,
728 (Some(i), 1) => i as isize + 1,
729 _ => return,
730 };
731 for _ in 1..count {
732 let next = idx + dir;
733 if next < 0 || next >= len as isize {
734 break;
735 }
736 idx = next;
737 }
738 if idx < 0 || idx >= len as isize {
739 return;
740 }
741 let idx = idx as usize;
742 ed.vim.change_list_cursor = Some(idx);
743 let (row, col) = ed.vim.change_list[idx];
744 ed.jump_cursor(row, col);
745}
746
747fn record_search_history<H: crate::types::Host>(
751 ed: &mut Editor<hjkl_buffer::Buffer, H>,
752 pattern: &str,
753) {
754 if pattern.is_empty() {
755 return;
756 }
757 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
758 return;
759 }
760 ed.vim.search_history.push(pattern.to_string());
761 let len = ed.vim.search_history.len();
762 if len > SEARCH_HISTORY_MAX {
763 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
764 }
765}
766
767fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
773 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
774 return;
775 }
776 let len = ed.vim.search_history.len();
777 let next_idx = match (ed.vim.search_history_cursor, dir) {
778 (None, -1) => Some(len - 1),
779 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
781 (Some(i), 1) if i + 1 < len => Some(i + 1),
782 _ => None,
783 };
784 let Some(idx) = next_idx else {
785 return;
786 };
787 ed.vim.search_history_cursor = Some(idx);
788 let text = ed.vim.search_history[idx].clone();
789 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
790 prompt.cursor = text.chars().count();
791 prompt.text = text.clone();
792 }
793 push_search_pattern(ed, &text);
794}
795
796pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
797 ed.sync_buffer_content_from_textarea();
802 let now = std::time::Instant::now();
810 let host_now = ed.host.now();
811 let timed_out = match ed.vim.last_input_host_at {
812 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
813 None => false,
814 };
815 if timed_out {
816 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
817 || ed.vim.count != 0
818 || ed.vim.pending_register.is_some()
819 || ed.vim.insert_pending_register;
820 if chord_in_flight {
821 ed.vim.clear_pending_prefix();
822 }
823 }
824 ed.vim.last_input_at = Some(now);
825 ed.vim.last_input_host_at = Some(host_now);
826 if ed.vim.recording_macro.is_some()
831 && !ed.vim.replaying_macro
832 && matches!(ed.vim.pending, Pending::None)
833 && ed.vim.mode != Mode::Insert
834 && input.key == Key::Char('q')
835 && !input.ctrl
836 && !input.alt
837 {
838 let reg = ed.vim.recording_macro.take().unwrap();
839 let keys = std::mem::take(&mut ed.vim.recording_keys);
840 let text = crate::input::encode_macro(&keys);
841 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
842 return true;
843 }
844 if ed.vim.search_prompt.is_some() {
846 return step_search_prompt(ed, input);
847 }
848 let pending_was_macro_chord = matches!(
852 ed.vim.pending,
853 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
854 );
855 let was_insert = ed.vim.mode == Mode::Insert;
856 let pre_visual_snapshot = match ed.vim.mode {
859 Mode::Visual => Some(LastVisual {
860 mode: Mode::Visual,
861 anchor: ed.vim.visual_anchor,
862 cursor: ed.cursor(),
863 block_vcol: 0,
864 }),
865 Mode::VisualLine => Some(LastVisual {
866 mode: Mode::VisualLine,
867 anchor: (ed.vim.visual_line_anchor, 0),
868 cursor: ed.cursor(),
869 block_vcol: 0,
870 }),
871 Mode::VisualBlock => Some(LastVisual {
872 mode: Mode::VisualBlock,
873 anchor: ed.vim.block_anchor,
874 cursor: ed.cursor(),
875 block_vcol: ed.vim.block_vcol,
876 }),
877 _ => None,
878 };
879 let consumed = match ed.vim.mode {
880 Mode::Insert => step_insert(ed, input),
881 _ => step_normal(ed, input),
882 };
883 if let Some(snap) = pre_visual_snapshot
884 && !matches!(
885 ed.vim.mode,
886 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
887 )
888 {
889 ed.vim.last_visual = Some(snap);
890 }
891 if !was_insert
895 && ed.vim.one_shot_normal
896 && ed.vim.mode == Mode::Normal
897 && matches!(ed.vim.pending, Pending::None)
898 {
899 ed.vim.one_shot_normal = false;
900 ed.vim.mode = Mode::Insert;
901 }
902 ed.sync_buffer_content_from_textarea();
908 if !ed.vim.viewport_pinned {
912 ed.ensure_cursor_in_scrolloff();
913 }
914 ed.vim.viewport_pinned = false;
915 if ed.vim.recording_macro.is_some()
920 && !ed.vim.replaying_macro
921 && input.key != Key::Char('q')
922 && !pending_was_macro_chord
923 {
924 ed.vim.recording_keys.push(input);
925 }
926 consumed
927}
928
929fn step_insert<H: crate::types::Host>(
932 ed: &mut Editor<hjkl_buffer::Buffer, H>,
933 input: Input,
934) -> bool {
935 if ed.vim.insert_pending_register {
939 ed.vim.insert_pending_register = false;
940 if let Key::Char(c) = input.key
941 && !input.ctrl
942 {
943 insert_register_text(ed, c);
944 }
945 return true;
946 }
947
948 if input.key == Key::Esc {
949 finish_insert_session(ed);
950 ed.vim.mode = Mode::Normal;
951 let col = ed.cursor().1;
956 if col > 0 {
957 crate::motions::move_left(&mut ed.buffer, 1);
958 ed.push_buffer_cursor_to_textarea();
959 }
960 ed.sticky_col = Some(ed.cursor().1);
961 return true;
962 }
963
964 if input.ctrl {
966 match input.key {
967 Key::Char('w') => {
968 use hjkl_buffer::{Edit, MotionKind};
969 ed.sync_buffer_content_from_textarea();
970 let cursor = buf_cursor_pos(&ed.buffer);
971 if cursor.row == 0 && cursor.col == 0 {
972 return true;
973 }
974 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
977 let word_start = buf_cursor_pos(&ed.buffer);
978 if word_start == cursor {
979 return true;
980 }
981 buf_set_cursor_pos(&mut ed.buffer, cursor);
982 ed.mutate_edit(Edit::DeleteRange {
983 start: word_start,
984 end: cursor,
985 kind: MotionKind::Char,
986 });
987 ed.push_buffer_cursor_to_textarea();
988 return true;
989 }
990 Key::Char('u') => {
991 use hjkl_buffer::{Edit, MotionKind, Position};
992 ed.sync_buffer_content_from_textarea();
993 let cursor = buf_cursor_pos(&ed.buffer);
994 if cursor.col > 0 {
995 ed.mutate_edit(Edit::DeleteRange {
996 start: Position::new(cursor.row, 0),
997 end: cursor,
998 kind: MotionKind::Char,
999 });
1000 ed.push_buffer_cursor_to_textarea();
1001 }
1002 return true;
1003 }
1004 Key::Char('h') => {
1005 use hjkl_buffer::{Edit, MotionKind, Position};
1006 ed.sync_buffer_content_from_textarea();
1007 let cursor = buf_cursor_pos(&ed.buffer);
1008 if cursor.col > 0 {
1009 ed.mutate_edit(Edit::DeleteRange {
1010 start: Position::new(cursor.row, cursor.col - 1),
1011 end: cursor,
1012 kind: MotionKind::Char,
1013 });
1014 } else if cursor.row > 0 {
1015 let prev_row = cursor.row - 1;
1016 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1017 ed.mutate_edit(Edit::JoinLines {
1018 row: prev_row,
1019 count: 1,
1020 with_space: false,
1021 });
1022 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1023 }
1024 ed.push_buffer_cursor_to_textarea();
1025 return true;
1026 }
1027 Key::Char('o') => {
1028 ed.vim.one_shot_normal = true;
1031 ed.vim.mode = Mode::Normal;
1032 return true;
1033 }
1034 Key::Char('r') => {
1035 ed.vim.insert_pending_register = true;
1038 return true;
1039 }
1040 Key::Char('t') => {
1041 let (row, col) = ed.cursor();
1046 let sw = ed.settings().shiftwidth;
1047 indent_rows(ed, row, row, 1);
1048 ed.jump_cursor(row, col + sw);
1049 return true;
1050 }
1051 Key::Char('d') => {
1052 let (row, col) = ed.cursor();
1056 let before_len = buf_line_bytes(&ed.buffer, row);
1057 outdent_rows(ed, row, row, 1);
1058 let after_len = buf_line_bytes(&ed.buffer, row);
1059 let stripped = before_len.saturating_sub(after_len);
1060 let new_col = col.saturating_sub(stripped);
1061 ed.jump_cursor(row, new_col);
1062 return true;
1063 }
1064 _ => {}
1065 }
1066 }
1067
1068 let (row, _) = ed.cursor();
1071 if let Some(ref mut session) = ed.vim.insert_session {
1072 session.row_min = session.row_min.min(row);
1073 session.row_max = session.row_max.max(row);
1074 }
1075 let mutated = handle_insert_key(ed, input);
1076 if mutated {
1077 ed.mark_content_dirty();
1078 let (row, _) = ed.cursor();
1079 if let Some(ref mut session) = ed.vim.insert_session {
1080 session.row_min = session.row_min.min(row);
1081 session.row_max = session.row_max.max(row);
1082 }
1083 }
1084 true
1085}
1086
1087fn insert_register_text<H: crate::types::Host>(
1092 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1093 selector: char,
1094) {
1095 use hjkl_buffer::Edit;
1096 let text = match ed.registers().read(selector) {
1097 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1098 _ => return,
1099 };
1100 ed.sync_buffer_content_from_textarea();
1101 let cursor = buf_cursor_pos(&ed.buffer);
1102 ed.mutate_edit(Edit::InsertStr {
1103 at: cursor,
1104 text: text.clone(),
1105 });
1106 let mut row = cursor.row;
1109 let mut col = cursor.col;
1110 for ch in text.chars() {
1111 if ch == '\n' {
1112 row += 1;
1113 col = 0;
1114 } else {
1115 col += 1;
1116 }
1117 }
1118 buf_set_cursor_rc(&mut ed.buffer, row, col);
1119 ed.push_buffer_cursor_to_textarea();
1120 ed.mark_content_dirty();
1121 if let Some(ref mut session) = ed.vim.insert_session {
1122 session.row_min = session.row_min.min(row);
1123 session.row_max = session.row_max.max(row);
1124 }
1125}
1126
1127fn handle_insert_key<H: crate::types::Host>(
1134 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1135 input: Input,
1136) -> bool {
1137 use hjkl_buffer::{Edit, MotionKind, Position};
1138 ed.sync_buffer_content_from_textarea();
1139 let cursor = buf_cursor_pos(&ed.buffer);
1140 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1141 let in_replace = matches!(
1145 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1146 Some(InsertReason::Replace)
1147 );
1148 let mutated = match input.key {
1149 Key::Char(c) if in_replace && cursor.col < line_chars => {
1150 ed.mutate_edit(Edit::DeleteRange {
1151 start: cursor,
1152 end: Position::new(cursor.row, cursor.col + 1),
1153 kind: MotionKind::Char,
1154 });
1155 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1156 true
1157 }
1158 Key::Char(c) => {
1159 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1160 true
1161 }
1162 Key::Enter => {
1163 let indent: String = if ed.settings.autoindent {
1164 buf_line(&ed.buffer, cursor.row)
1165 .map(|l| l.chars().take_while(|c| *c == ' ' || *c == '\t').collect())
1166 .unwrap_or_default()
1167 } else {
1168 String::new()
1169 };
1170 let text = format!("\n{indent}");
1171 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1172 true
1173 }
1174 Key::Tab => {
1175 if ed.settings.expandtab {
1176 let n = ed.settings.tabstop.max(1);
1177 ed.mutate_edit(Edit::InsertStr {
1178 at: cursor,
1179 text: " ".repeat(n),
1180 });
1181 } else {
1182 ed.mutate_edit(Edit::InsertChar {
1183 at: cursor,
1184 ch: '\t',
1185 });
1186 }
1187 true
1188 }
1189 Key::Backspace => {
1190 if cursor.col > 0 {
1191 ed.mutate_edit(Edit::DeleteRange {
1192 start: Position::new(cursor.row, cursor.col - 1),
1193 end: cursor,
1194 kind: MotionKind::Char,
1195 });
1196 true
1197 } else if cursor.row > 0 {
1198 let prev_row = cursor.row - 1;
1199 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1200 ed.mutate_edit(Edit::JoinLines {
1201 row: prev_row,
1202 count: 1,
1203 with_space: false,
1204 });
1205 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1206 true
1207 } else {
1208 false
1209 }
1210 }
1211 Key::Delete => {
1212 if cursor.col < line_chars {
1213 ed.mutate_edit(Edit::DeleteRange {
1214 start: cursor,
1215 end: Position::new(cursor.row, cursor.col + 1),
1216 kind: MotionKind::Char,
1217 });
1218 true
1219 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1220 ed.mutate_edit(Edit::JoinLines {
1221 row: cursor.row,
1222 count: 1,
1223 with_space: false,
1224 });
1225 buf_set_cursor_pos(&mut ed.buffer, cursor);
1226 true
1227 } else {
1228 false
1229 }
1230 }
1231 Key::Left => {
1232 crate::motions::move_left(&mut ed.buffer, 1);
1233 break_undo_group_in_insert(ed);
1234 false
1235 }
1236 Key::Right => {
1237 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1240 break_undo_group_in_insert(ed);
1241 false
1242 }
1243 Key::Up => {
1244 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1245 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1246 break_undo_group_in_insert(ed);
1247 false
1248 }
1249 Key::Down => {
1250 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1251 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1252 break_undo_group_in_insert(ed);
1253 false
1254 }
1255 Key::Home => {
1256 crate::motions::move_line_start(&mut ed.buffer);
1257 break_undo_group_in_insert(ed);
1258 false
1259 }
1260 Key::End => {
1261 crate::motions::move_line_end(&mut ed.buffer);
1262 break_undo_group_in_insert(ed);
1263 false
1264 }
1265 Key::PageUp => {
1266 let rows = viewport_full_rows(ed, 1) as isize;
1270 scroll_cursor_rows(ed, -rows);
1271 return false;
1272 }
1273 Key::PageDown => {
1274 let rows = viewport_full_rows(ed, 1) as isize;
1275 scroll_cursor_rows(ed, rows);
1276 return false;
1277 }
1278 _ => false,
1281 };
1282 ed.push_buffer_cursor_to_textarea();
1283 mutated
1284}
1285
1286fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1287 let Some(session) = ed.vim.insert_session.take() else {
1288 return;
1289 };
1290 let lines = buf_lines_to_vec(&ed.buffer);
1291 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1295 let before_end = session
1296 .row_max
1297 .min(session.before_lines.len().saturating_sub(1));
1298 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1299 session.before_lines[session.row_min..=before_end].join("\n")
1300 } else {
1301 String::new()
1302 };
1303 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1304 lines[session.row_min..=after_end].join("\n")
1305 } else {
1306 String::new()
1307 };
1308 let inserted = extract_inserted(&before, &after);
1309 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1310 use hjkl_buffer::{Edit, Position};
1311 for _ in 0..session.count - 1 {
1312 let (row, col) = ed.cursor();
1313 ed.mutate_edit(Edit::InsertStr {
1314 at: Position::new(row, col),
1315 text: inserted.clone(),
1316 });
1317 }
1318 }
1319 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1320 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1321 use hjkl_buffer::{Edit, Position};
1322 for r in (top + 1)..=bot {
1323 let line_len = buf_line_chars(&ed.buffer, r);
1324 if col > line_len {
1325 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1328 ed.mutate_edit(Edit::InsertStr {
1329 at: Position::new(r, line_len),
1330 text: pad,
1331 });
1332 }
1333 ed.mutate_edit(Edit::InsertStr {
1334 at: Position::new(r, col),
1335 text: inserted.clone(),
1336 });
1337 }
1338 buf_set_cursor_rc(&mut ed.buffer, top, col);
1339 ed.push_buffer_cursor_to_textarea();
1340 }
1341 return;
1342 }
1343 if ed.vim.replaying {
1344 return;
1345 }
1346 match session.reason {
1347 InsertReason::Enter(entry) => {
1348 ed.vim.last_change = Some(LastChange::InsertAt {
1349 entry,
1350 inserted,
1351 count: session.count,
1352 });
1353 }
1354 InsertReason::Open { above } => {
1355 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1356 }
1357 InsertReason::AfterChange => {
1358 if let Some(
1359 LastChange::OpMotion { inserted: ins, .. }
1360 | LastChange::OpTextObj { inserted: ins, .. }
1361 | LastChange::LineOp { inserted: ins, .. },
1362 ) = ed.vim.last_change.as_mut()
1363 {
1364 *ins = Some(inserted);
1365 }
1366 }
1367 InsertReason::DeleteToEol => {
1368 ed.vim.last_change = Some(LastChange::DeleteToEol {
1369 inserted: Some(inserted),
1370 });
1371 }
1372 InsertReason::ReplayOnly => {}
1373 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1374 InsertReason::Replace => {
1375 ed.vim.last_change = Some(LastChange::DeleteToEol {
1380 inserted: Some(inserted),
1381 });
1382 }
1383 }
1384}
1385
1386fn begin_insert<H: crate::types::Host>(
1387 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1388 count: usize,
1389 reason: InsertReason,
1390) {
1391 let record = !matches!(reason, InsertReason::ReplayOnly);
1392 if record {
1393 ed.push_undo();
1394 }
1395 let reason = if ed.vim.replaying {
1396 InsertReason::ReplayOnly
1397 } else {
1398 reason
1399 };
1400 let (row, _) = ed.cursor();
1401 ed.vim.insert_session = Some(InsertSession {
1402 count,
1403 row_min: row,
1404 row_max: row,
1405 before_lines: buf_lines_to_vec(&ed.buffer),
1406 reason,
1407 });
1408 ed.vim.mode = Mode::Insert;
1409}
1410
1411pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1426 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1427) {
1428 if !ed.settings.undo_break_on_motion {
1429 return;
1430 }
1431 if ed.vim.replaying {
1432 return;
1433 }
1434 if ed.vim.insert_session.is_none() {
1435 return;
1436 }
1437 ed.push_undo();
1438 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1439 let mut lines: Vec<String> = Vec::with_capacity(n);
1440 for r in 0..n {
1441 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1442 }
1443 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1444 if let Some(ref mut session) = ed.vim.insert_session {
1445 session.before_lines = lines;
1446 session.row_min = row;
1447 session.row_max = row;
1448 }
1449}
1450
1451fn step_normal<H: crate::types::Host>(
1454 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1455 input: Input,
1456) -> bool {
1457 if let Key::Char(d @ '0'..='9') = input.key
1459 && !input.ctrl
1460 && !input.alt
1461 && !matches!(
1462 ed.vim.pending,
1463 Pending::Replace
1464 | Pending::Find { .. }
1465 | Pending::OpFind { .. }
1466 | Pending::VisualTextObj { .. }
1467 )
1468 && (d != '0' || ed.vim.count > 0)
1469 {
1470 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1471 return true;
1472 }
1473
1474 match std::mem::take(&mut ed.vim.pending) {
1476 Pending::Replace => return handle_replace(ed, input),
1477 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1478 Pending::OpFind {
1479 op,
1480 count1,
1481 forward,
1482 till,
1483 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1484 Pending::G => return handle_after_g(ed, input),
1485 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1486 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1487 Pending::OpTextObj { op, count1, inner } => {
1488 return handle_text_object(ed, input, op, count1, inner);
1489 }
1490 Pending::VisualTextObj { inner } => {
1491 return handle_visual_text_obj(ed, input, inner);
1492 }
1493 Pending::Z => return handle_after_z(ed, input),
1494 Pending::SetMark => return handle_set_mark(ed, input),
1495 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1496 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1497 Pending::SelectRegister => return handle_select_register(ed, input),
1498 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1499 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1500 Pending::None => {}
1501 }
1502
1503 let count = take_count(&mut ed.vim);
1504
1505 match input.key {
1507 Key::Esc => {
1508 ed.vim.force_normal();
1509 return true;
1510 }
1511 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1512 ed.vim.visual_anchor = ed.cursor();
1513 ed.vim.mode = Mode::Visual;
1514 return true;
1515 }
1516 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1517 let (row, _) = ed.cursor();
1518 ed.vim.visual_line_anchor = row;
1519 ed.vim.mode = Mode::VisualLine;
1520 return true;
1521 }
1522 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1523 ed.vim.visual_anchor = ed.cursor();
1524 ed.vim.mode = Mode::Visual;
1525 return true;
1526 }
1527 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1528 let (row, _) = ed.cursor();
1529 ed.vim.visual_line_anchor = row;
1530 ed.vim.mode = Mode::VisualLine;
1531 return true;
1532 }
1533 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1534 let cur = ed.cursor();
1535 ed.vim.block_anchor = cur;
1536 ed.vim.block_vcol = cur.1;
1537 ed.vim.mode = Mode::VisualBlock;
1538 return true;
1539 }
1540 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1541 ed.vim.mode = Mode::Normal;
1543 return true;
1544 }
1545 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1548 Mode::Visual => {
1549 let cur = ed.cursor();
1550 let anchor = ed.vim.visual_anchor;
1551 ed.vim.visual_anchor = cur;
1552 ed.jump_cursor(anchor.0, anchor.1);
1553 return true;
1554 }
1555 Mode::VisualLine => {
1556 let cur_row = ed.cursor().0;
1557 let anchor_row = ed.vim.visual_line_anchor;
1558 ed.vim.visual_line_anchor = cur_row;
1559 ed.jump_cursor(anchor_row, 0);
1560 return true;
1561 }
1562 Mode::VisualBlock => {
1563 let cur = ed.cursor();
1564 let anchor = ed.vim.block_anchor;
1565 ed.vim.block_anchor = cur;
1566 ed.vim.block_vcol = anchor.1;
1567 ed.jump_cursor(anchor.0, anchor.1);
1568 return true;
1569 }
1570 _ => {}
1571 },
1572 _ => {}
1573 }
1574
1575 if ed.vim.is_visual()
1577 && let Some(op) = visual_operator(&input)
1578 {
1579 apply_visual_operator(ed, op);
1580 return true;
1581 }
1582
1583 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1587 match input.key {
1588 Key::Char('r') => {
1589 ed.vim.pending = Pending::Replace;
1590 return true;
1591 }
1592 Key::Char('I') => {
1593 let (top, bot, left, _right) = block_bounds(ed);
1594 ed.jump_cursor(top, left);
1595 ed.vim.mode = Mode::Normal;
1596 begin_insert(
1597 ed,
1598 1,
1599 InsertReason::BlockEdge {
1600 top,
1601 bot,
1602 col: left,
1603 },
1604 );
1605 return true;
1606 }
1607 Key::Char('A') => {
1608 let (top, bot, _left, right) = block_bounds(ed);
1609 let line_len = buf_line_chars(&ed.buffer, top);
1610 let col = (right + 1).min(line_len);
1611 ed.jump_cursor(top, col);
1612 ed.vim.mode = Mode::Normal;
1613 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1614 return true;
1615 }
1616 _ => {}
1617 }
1618 }
1619
1620 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1622 && !input.ctrl
1623 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1624 {
1625 let inner = matches!(input.key, Key::Char('i'));
1626 ed.vim.pending = Pending::VisualTextObj { inner };
1627 return true;
1628 }
1629
1630 if input.ctrl
1635 && let Key::Char(c) = input.key
1636 {
1637 match c {
1638 'd' => {
1639 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1640 return true;
1641 }
1642 'u' => {
1643 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1644 return true;
1645 }
1646 'f' => {
1647 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1648 return true;
1649 }
1650 'b' => {
1651 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1652 return true;
1653 }
1654 'r' => {
1655 do_redo(ed);
1656 return true;
1657 }
1658 'a' if ed.vim.mode == Mode::Normal => {
1659 adjust_number(ed, count.max(1) as i64);
1660 return true;
1661 }
1662 'x' if ed.vim.mode == Mode::Normal => {
1663 adjust_number(ed, -(count.max(1) as i64));
1664 return true;
1665 }
1666 'o' if ed.vim.mode == Mode::Normal => {
1667 for _ in 0..count.max(1) {
1668 jump_back(ed);
1669 }
1670 return true;
1671 }
1672 'i' if ed.vim.mode == Mode::Normal => {
1673 for _ in 0..count.max(1) {
1674 jump_forward(ed);
1675 }
1676 return true;
1677 }
1678 _ => {}
1679 }
1680 }
1681
1682 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1684 for _ in 0..count.max(1) {
1685 jump_forward(ed);
1686 }
1687 return true;
1688 }
1689
1690 if let Some(motion) = parse_motion(&input) {
1692 execute_motion(ed, motion.clone(), count);
1693 if ed.vim.mode == Mode::VisualBlock {
1695 update_block_vcol(ed, &motion);
1696 }
1697 if let Motion::Find { ch, forward, till } = motion {
1698 ed.vim.last_find = Some((ch, forward, till));
1699 }
1700 return true;
1701 }
1702
1703 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
1705 return true;
1706 }
1707
1708 if ed.vim.mode == Mode::Normal
1710 && let Key::Char(op_ch) = input.key
1711 && !input.ctrl
1712 && let Some(op) = char_to_operator(op_ch)
1713 {
1714 ed.vim.pending = Pending::Op { op, count1: count };
1715 return true;
1716 }
1717
1718 if ed.vim.mode == Mode::Normal
1720 && let Some((forward, till)) = find_entry(&input)
1721 {
1722 ed.vim.count = count;
1723 ed.vim.pending = Pending::Find { forward, till };
1724 return true;
1725 }
1726
1727 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
1729 ed.vim.count = count;
1730 ed.vim.pending = Pending::G;
1731 return true;
1732 }
1733
1734 if !input.ctrl
1736 && input.key == Key::Char('z')
1737 && matches!(
1738 ed.vim.mode,
1739 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
1740 )
1741 {
1742 ed.vim.pending = Pending::Z;
1743 return true;
1744 }
1745
1746 if !input.ctrl && ed.vim.mode == Mode::Normal {
1750 match input.key {
1751 Key::Char('m') => {
1752 ed.vim.pending = Pending::SetMark;
1753 return true;
1754 }
1755 Key::Char('\'') => {
1756 ed.vim.pending = Pending::GotoMarkLine;
1757 return true;
1758 }
1759 Key::Char('`') => {
1760 ed.vim.pending = Pending::GotoMarkChar;
1761 return true;
1762 }
1763 Key::Char('"') => {
1764 ed.vim.pending = Pending::SelectRegister;
1767 return true;
1768 }
1769 Key::Char('@') => {
1770 ed.vim.pending = Pending::PlayMacroTarget { count };
1774 return true;
1775 }
1776 Key::Char('q') if ed.vim.recording_macro.is_none() => {
1777 ed.vim.pending = Pending::RecordMacroTarget;
1782 return true;
1783 }
1784 _ => {}
1785 }
1786 }
1787
1788 true
1790}
1791
1792fn handle_set_mark<H: crate::types::Host>(
1793 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1794 input: Input,
1795) -> bool {
1796 if let Key::Char(c) = input.key
1797 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
1798 {
1799 let pos = ed.cursor();
1804 ed.set_mark(c, pos);
1805 }
1806 true
1807}
1808
1809fn handle_select_register<H: crate::types::Host>(
1813 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1814 input: Input,
1815) -> bool {
1816 if let Key::Char(c) = input.key
1817 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*'))
1818 {
1819 ed.vim.pending_register = Some(c);
1820 }
1821 true
1822}
1823
1824fn handle_record_macro_target<H: crate::types::Host>(
1829 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1830 input: Input,
1831) -> bool {
1832 if let Key::Char(c) = input.key
1833 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
1834 {
1835 ed.vim.recording_macro = Some(c);
1836 if c.is_ascii_uppercase() {
1839 let lower = c.to_ascii_lowercase();
1840 let text = ed
1844 .registers()
1845 .read(lower)
1846 .map(|s| s.text.clone())
1847 .unwrap_or_default();
1848 ed.vim.recording_keys = crate::input::decode_macro(&text);
1849 } else {
1850 ed.vim.recording_keys.clear();
1851 }
1852 }
1853 true
1854}
1855
1856fn handle_play_macro_target<H: crate::types::Host>(
1862 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1863 input: Input,
1864 count: usize,
1865) -> bool {
1866 let reg = match input.key {
1867 Key::Char('@') => ed.vim.last_macro,
1868 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
1869 Some(c.to_ascii_lowercase())
1870 }
1871 _ => None,
1872 };
1873 let Some(reg) = reg else {
1874 return true;
1875 };
1876 let text = match ed.registers().read(reg) {
1879 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1880 _ => return true,
1881 };
1882 let keys = crate::input::decode_macro(&text);
1883 ed.vim.last_macro = Some(reg);
1884 let times = count.max(1);
1885 let was_replaying = ed.vim.replaying_macro;
1886 ed.vim.replaying_macro = true;
1887 for _ in 0..times {
1888 for k in keys.iter().copied() {
1889 step(ed, k);
1890 }
1891 }
1892 ed.vim.replaying_macro = was_replaying;
1893 true
1894}
1895
1896fn handle_goto_mark<H: crate::types::Host>(
1897 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1898 input: Input,
1899 linewise: bool,
1900) -> bool {
1901 let Key::Char(c) = input.key else {
1902 return true;
1903 };
1904 let target = match c {
1911 'a'..='z' | 'A'..='Z' => ed.mark(c),
1912 '\'' | '`' => ed.vim.jump_back.last().copied(),
1913 '.' => ed.vim.last_edit_pos,
1914 _ => None,
1915 };
1916 let Some((row, col)) = target else {
1917 return true;
1918 };
1919 let pre = ed.cursor();
1920 let (r, c_clamped) = clamp_pos(ed, (row, col));
1921 if linewise {
1922 buf_set_cursor_rc(&mut ed.buffer, r, 0);
1923 ed.push_buffer_cursor_to_textarea();
1924 move_first_non_whitespace(ed);
1925 } else {
1926 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
1927 ed.push_buffer_cursor_to_textarea();
1928 }
1929 if ed.cursor() != pre {
1930 push_jump(ed, pre);
1931 }
1932 ed.sticky_col = Some(ed.cursor().1);
1933 true
1934}
1935
1936fn take_count(vim: &mut VimState) -> usize {
1937 if vim.count > 0 {
1938 let n = vim.count;
1939 vim.count = 0;
1940 n
1941 } else {
1942 1
1943 }
1944}
1945
1946fn char_to_operator(c: char) -> Option<Operator> {
1947 match c {
1948 'd' => Some(Operator::Delete),
1949 'c' => Some(Operator::Change),
1950 'y' => Some(Operator::Yank),
1951 '>' => Some(Operator::Indent),
1952 '<' => Some(Operator::Outdent),
1953 _ => None,
1954 }
1955}
1956
1957fn visual_operator(input: &Input) -> Option<Operator> {
1958 if input.ctrl {
1959 return None;
1960 }
1961 match input.key {
1962 Key::Char('y') => Some(Operator::Yank),
1963 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
1964 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
1965 Key::Char('U') => Some(Operator::Uppercase),
1967 Key::Char('u') => Some(Operator::Lowercase),
1968 Key::Char('~') => Some(Operator::ToggleCase),
1969 Key::Char('>') => Some(Operator::Indent),
1971 Key::Char('<') => Some(Operator::Outdent),
1972 _ => None,
1973 }
1974}
1975
1976fn find_entry(input: &Input) -> Option<(bool, bool)> {
1977 if input.ctrl {
1978 return None;
1979 }
1980 match input.key {
1981 Key::Char('f') => Some((true, false)),
1982 Key::Char('F') => Some((false, false)),
1983 Key::Char('t') => Some((true, true)),
1984 Key::Char('T') => Some((false, true)),
1985 _ => None,
1986 }
1987}
1988
1989const JUMPLIST_MAX: usize = 100;
1993
1994fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
1999 ed.vim.jump_back.push(from);
2000 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2001 ed.vim.jump_back.remove(0);
2002 }
2003 ed.vim.jump_fwd.clear();
2004}
2005
2006fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2009 let Some(target) = ed.vim.jump_back.pop() else {
2010 return;
2011 };
2012 let cur = ed.cursor();
2013 ed.vim.jump_fwd.push(cur);
2014 let (r, c) = clamp_pos(ed, target);
2015 ed.jump_cursor(r, c);
2016 ed.sticky_col = Some(c);
2017}
2018
2019fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2022 let Some(target) = ed.vim.jump_fwd.pop() else {
2023 return;
2024 };
2025 let cur = ed.cursor();
2026 ed.vim.jump_back.push(cur);
2027 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2028 ed.vim.jump_back.remove(0);
2029 }
2030 let (r, c) = clamp_pos(ed, target);
2031 ed.jump_cursor(r, c);
2032 ed.sticky_col = Some(c);
2033}
2034
2035fn clamp_pos<H: crate::types::Host>(
2038 ed: &Editor<hjkl_buffer::Buffer, H>,
2039 pos: (usize, usize),
2040) -> (usize, usize) {
2041 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2042 let r = pos.0.min(last_row);
2043 let line_len = buf_line_chars(&ed.buffer, r);
2044 let c = pos.1.min(line_len.saturating_sub(1));
2045 (r, c)
2046}
2047
2048fn is_big_jump(motion: &Motion) -> bool {
2050 matches!(
2051 motion,
2052 Motion::FileTop
2053 | Motion::FileBottom
2054 | Motion::MatchBracket
2055 | Motion::WordAtCursor { .. }
2056 | Motion::SearchNext { .. }
2057 | Motion::ViewportTop
2058 | Motion::ViewportMiddle
2059 | Motion::ViewportBottom
2060 )
2061}
2062
2063fn viewport_half_rows<H: crate::types::Host>(
2068 ed: &Editor<hjkl_buffer::Buffer, H>,
2069 count: usize,
2070) -> usize {
2071 let h = ed.viewport_height_value() as usize;
2072 (h / 2).max(1).saturating_mul(count.max(1))
2073}
2074
2075fn viewport_full_rows<H: crate::types::Host>(
2078 ed: &Editor<hjkl_buffer::Buffer, H>,
2079 count: usize,
2080) -> usize {
2081 let h = ed.viewport_height_value() as usize;
2082 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2083}
2084
2085fn scroll_cursor_rows<H: crate::types::Host>(
2090 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2091 delta: isize,
2092) {
2093 if delta == 0 {
2094 return;
2095 }
2096 ed.sync_buffer_content_from_textarea();
2097 let (row, _) = ed.cursor();
2098 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2099 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2100 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2101 crate::motions::move_first_non_blank(&mut ed.buffer);
2102 ed.push_buffer_cursor_to_textarea();
2103 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2104}
2105
2106fn parse_motion(input: &Input) -> Option<Motion> {
2109 if input.ctrl {
2110 return None;
2111 }
2112 match input.key {
2113 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2114 Key::Char('l') | Key::Right => Some(Motion::Right),
2115 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2116 Key::Char('k') | Key::Up => Some(Motion::Up),
2117 Key::Char('w') => Some(Motion::WordFwd),
2118 Key::Char('W') => Some(Motion::BigWordFwd),
2119 Key::Char('b') => Some(Motion::WordBack),
2120 Key::Char('B') => Some(Motion::BigWordBack),
2121 Key::Char('e') => Some(Motion::WordEnd),
2122 Key::Char('E') => Some(Motion::BigWordEnd),
2123 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2124 Key::Char('^') => Some(Motion::FirstNonBlank),
2125 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2126 Key::Char('G') => Some(Motion::FileBottom),
2127 Key::Char('%') => Some(Motion::MatchBracket),
2128 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2129 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2130 Key::Char('*') => Some(Motion::WordAtCursor {
2131 forward: true,
2132 whole_word: true,
2133 }),
2134 Key::Char('#') => Some(Motion::WordAtCursor {
2135 forward: false,
2136 whole_word: true,
2137 }),
2138 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2139 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2140 Key::Char('H') => Some(Motion::ViewportTop),
2141 Key::Char('M') => Some(Motion::ViewportMiddle),
2142 Key::Char('L') => Some(Motion::ViewportBottom),
2143 Key::Char('{') => Some(Motion::ParagraphPrev),
2144 Key::Char('}') => Some(Motion::ParagraphNext),
2145 Key::Char('(') => Some(Motion::SentencePrev),
2146 Key::Char(')') => Some(Motion::SentenceNext),
2147 _ => None,
2148 }
2149}
2150
2151fn execute_motion<H: crate::types::Host>(
2154 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2155 motion: Motion,
2156 count: usize,
2157) {
2158 let count = count.max(1);
2159 let motion = match motion {
2161 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2162 Some((ch, forward, till)) => Motion::Find {
2163 ch,
2164 forward: if reverse { !forward } else { forward },
2165 till,
2166 },
2167 None => return,
2168 },
2169 other => other,
2170 };
2171 let pre_pos = ed.cursor();
2172 let pre_col = pre_pos.1;
2173 apply_motion_cursor(ed, &motion, count);
2174 let post_pos = ed.cursor();
2175 if is_big_jump(&motion) && pre_pos != post_pos {
2176 push_jump(ed, pre_pos);
2177 }
2178 apply_sticky_col(ed, &motion, pre_col);
2179 ed.sync_buffer_from_textarea();
2184}
2185
2186fn apply_sticky_col<H: crate::types::Host>(
2191 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2192 motion: &Motion,
2193 pre_col: usize,
2194) {
2195 if is_vertical_motion(motion) {
2196 let want = ed.sticky_col.unwrap_or(pre_col);
2197 ed.sticky_col = Some(want);
2200 let (row, _) = ed.cursor();
2201 let line_len = buf_line_chars(&ed.buffer, row);
2202 let max_col = line_len.saturating_sub(1);
2206 let target = want.min(max_col);
2207 ed.jump_cursor(row, target);
2208 } else {
2209 ed.sticky_col = Some(ed.cursor().1);
2212 }
2213}
2214
2215fn is_vertical_motion(motion: &Motion) -> bool {
2216 matches!(
2220 motion,
2221 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2222 )
2223}
2224
2225fn apply_motion_cursor<H: crate::types::Host>(
2226 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2227 motion: &Motion,
2228 count: usize,
2229) {
2230 apply_motion_cursor_ctx(ed, motion, count, false)
2231}
2232
2233fn apply_motion_cursor_ctx<H: crate::types::Host>(
2234 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2235 motion: &Motion,
2236 count: usize,
2237 as_operator: bool,
2238) {
2239 match motion {
2240 Motion::Left => {
2241 crate::motions::move_left(&mut ed.buffer, count);
2243 ed.push_buffer_cursor_to_textarea();
2244 }
2245 Motion::Right => {
2246 if as_operator {
2250 crate::motions::move_right_to_end(&mut ed.buffer, count);
2251 } else {
2252 crate::motions::move_right_in_line(&mut ed.buffer, count);
2253 }
2254 ed.push_buffer_cursor_to_textarea();
2255 }
2256 Motion::Up => {
2257 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2261 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2262 ed.push_buffer_cursor_to_textarea();
2263 }
2264 Motion::Down => {
2265 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2266 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2267 ed.push_buffer_cursor_to_textarea();
2268 }
2269 Motion::ScreenUp => {
2270 let v = *ed.host.viewport();
2271 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2272 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2273 ed.push_buffer_cursor_to_textarea();
2274 }
2275 Motion::ScreenDown => {
2276 let v = *ed.host.viewport();
2277 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2278 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2279 ed.push_buffer_cursor_to_textarea();
2280 }
2281 Motion::WordFwd => {
2282 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2283 ed.push_buffer_cursor_to_textarea();
2284 }
2285 Motion::WordBack => {
2286 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2287 ed.push_buffer_cursor_to_textarea();
2288 }
2289 Motion::WordEnd => {
2290 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2291 ed.push_buffer_cursor_to_textarea();
2292 }
2293 Motion::BigWordFwd => {
2294 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2295 ed.push_buffer_cursor_to_textarea();
2296 }
2297 Motion::BigWordBack => {
2298 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2299 ed.push_buffer_cursor_to_textarea();
2300 }
2301 Motion::BigWordEnd => {
2302 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2303 ed.push_buffer_cursor_to_textarea();
2304 }
2305 Motion::WordEndBack => {
2306 crate::motions::move_word_end_back(
2307 &mut ed.buffer,
2308 false,
2309 count,
2310 &ed.settings.iskeyword,
2311 );
2312 ed.push_buffer_cursor_to_textarea();
2313 }
2314 Motion::BigWordEndBack => {
2315 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2316 ed.push_buffer_cursor_to_textarea();
2317 }
2318 Motion::LineStart => {
2319 crate::motions::move_line_start(&mut ed.buffer);
2320 ed.push_buffer_cursor_to_textarea();
2321 }
2322 Motion::FirstNonBlank => {
2323 crate::motions::move_first_non_blank(&mut ed.buffer);
2324 ed.push_buffer_cursor_to_textarea();
2325 }
2326 Motion::LineEnd => {
2327 crate::motions::move_line_end(&mut ed.buffer);
2329 ed.push_buffer_cursor_to_textarea();
2330 }
2331 Motion::FileTop => {
2332 if count > 1 {
2335 crate::motions::move_bottom(&mut ed.buffer, count);
2336 } else {
2337 crate::motions::move_top(&mut ed.buffer);
2338 }
2339 ed.push_buffer_cursor_to_textarea();
2340 }
2341 Motion::FileBottom => {
2342 if count > 1 {
2345 crate::motions::move_bottom(&mut ed.buffer, count);
2346 } else {
2347 crate::motions::move_bottom(&mut ed.buffer, 0);
2348 }
2349 ed.push_buffer_cursor_to_textarea();
2350 }
2351 Motion::Find { ch, forward, till } => {
2352 for _ in 0..count {
2353 if !find_char_on_line(ed, *ch, *forward, *till) {
2354 break;
2355 }
2356 }
2357 }
2358 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2360 let _ = matching_bracket(ed);
2361 }
2362 Motion::WordAtCursor {
2363 forward,
2364 whole_word,
2365 } => {
2366 word_at_cursor_search(ed, *forward, *whole_word, count);
2367 }
2368 Motion::SearchNext { reverse } => {
2369 if let Some(pattern) = ed.vim.last_search.clone() {
2373 push_search_pattern(ed, &pattern);
2374 }
2375 if ed.search_state().pattern.is_none() {
2376 return;
2377 }
2378 let forward = ed.vim.last_search_forward != *reverse;
2382 for _ in 0..count.max(1) {
2383 if forward {
2384 ed.search_advance_forward(true);
2385 } else {
2386 ed.search_advance_backward(true);
2387 }
2388 }
2389 ed.push_buffer_cursor_to_textarea();
2390 }
2391 Motion::ViewportTop => {
2392 let v = *ed.host().viewport();
2393 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2394 ed.push_buffer_cursor_to_textarea();
2395 }
2396 Motion::ViewportMiddle => {
2397 let v = *ed.host().viewport();
2398 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2399 ed.push_buffer_cursor_to_textarea();
2400 }
2401 Motion::ViewportBottom => {
2402 let v = *ed.host().viewport();
2403 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2404 ed.push_buffer_cursor_to_textarea();
2405 }
2406 Motion::LastNonBlank => {
2407 crate::motions::move_last_non_blank(&mut ed.buffer);
2408 ed.push_buffer_cursor_to_textarea();
2409 }
2410 Motion::LineMiddle => {
2411 let row = ed.cursor().0;
2412 let line_chars = buf_line_chars(&ed.buffer, row);
2413 let target = line_chars / 2;
2416 ed.jump_cursor(row, target);
2417 }
2418 Motion::ParagraphPrev => {
2419 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2420 ed.push_buffer_cursor_to_textarea();
2421 }
2422 Motion::ParagraphNext => {
2423 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2424 ed.push_buffer_cursor_to_textarea();
2425 }
2426 Motion::SentencePrev => {
2427 for _ in 0..count.max(1) {
2428 if let Some((row, col)) = sentence_boundary(ed, false) {
2429 ed.jump_cursor(row, col);
2430 }
2431 }
2432 }
2433 Motion::SentenceNext => {
2434 for _ in 0..count.max(1) {
2435 if let Some((row, col)) = sentence_boundary(ed, true) {
2436 ed.jump_cursor(row, col);
2437 }
2438 }
2439 }
2440 }
2441}
2442
2443fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2444 ed.sync_buffer_content_from_textarea();
2450 crate::motions::move_first_non_blank(&mut ed.buffer);
2451 ed.push_buffer_cursor_to_textarea();
2452}
2453
2454fn find_char_on_line<H: crate::types::Host>(
2455 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2456 ch: char,
2457 forward: bool,
2458 till: bool,
2459) -> bool {
2460 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2461 if moved {
2462 ed.push_buffer_cursor_to_textarea();
2463 }
2464 moved
2465}
2466
2467fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2468 let moved = crate::motions::match_bracket(&mut ed.buffer);
2469 if moved {
2470 ed.push_buffer_cursor_to_textarea();
2471 }
2472 moved
2473}
2474
2475fn word_at_cursor_search<H: crate::types::Host>(
2476 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2477 forward: bool,
2478 whole_word: bool,
2479 count: usize,
2480) {
2481 let (row, col) = ed.cursor();
2482 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2483 let chars: Vec<char> = line.chars().collect();
2484 if chars.is_empty() {
2485 return;
2486 }
2487 let spec = ed.settings().iskeyword.clone();
2489 let is_word = |c: char| is_keyword_char(c, &spec);
2490 let mut start = col.min(chars.len().saturating_sub(1));
2491 while start > 0 && is_word(chars[start - 1]) {
2492 start -= 1;
2493 }
2494 let mut end = start;
2495 while end < chars.len() && is_word(chars[end]) {
2496 end += 1;
2497 }
2498 if end <= start {
2499 return;
2500 }
2501 let word: String = chars[start..end].iter().collect();
2502 let escaped = regex_escape(&word);
2503 let pattern = if whole_word {
2504 format!(r"\b{escaped}\b")
2505 } else {
2506 escaped
2507 };
2508 push_search_pattern(ed, &pattern);
2509 if ed.search_state().pattern.is_none() {
2510 return;
2511 }
2512 ed.vim.last_search = Some(pattern);
2514 ed.vim.last_search_forward = forward;
2515 for _ in 0..count.max(1) {
2516 if forward {
2517 ed.search_advance_forward(true);
2518 } else {
2519 ed.search_advance_backward(true);
2520 }
2521 }
2522 ed.push_buffer_cursor_to_textarea();
2523}
2524
2525fn regex_escape(s: &str) -> String {
2526 let mut out = String::with_capacity(s.len());
2527 for c in s.chars() {
2528 if matches!(
2529 c,
2530 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2531 ) {
2532 out.push('\\');
2533 }
2534 out.push(c);
2535 }
2536 out
2537}
2538
2539fn handle_after_op<H: crate::types::Host>(
2542 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2543 input: Input,
2544 op: Operator,
2545 count1: usize,
2546) -> bool {
2547 if let Key::Char(d @ '0'..='9') = input.key
2549 && !input.ctrl
2550 && (d != '0' || ed.vim.count > 0)
2551 {
2552 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2553 ed.vim.pending = Pending::Op { op, count1 };
2554 return true;
2555 }
2556
2557 if input.key == Key::Esc {
2559 ed.vim.count = 0;
2560 return true;
2561 }
2562
2563 let double_ch = match op {
2567 Operator::Delete => Some('d'),
2568 Operator::Change => Some('c'),
2569 Operator::Yank => Some('y'),
2570 Operator::Indent => Some('>'),
2571 Operator::Outdent => Some('<'),
2572 Operator::Uppercase => Some('U'),
2573 Operator::Lowercase => Some('u'),
2574 Operator::ToggleCase => Some('~'),
2575 Operator::Fold => None,
2576 Operator::Reflow => Some('q'),
2579 };
2580 if let Key::Char(c) = input.key
2581 && !input.ctrl
2582 && Some(c) == double_ch
2583 {
2584 let count2 = take_count(&mut ed.vim);
2585 let total = count1.max(1) * count2.max(1);
2586 execute_line_op(ed, op, total);
2587 if !ed.vim.replaying {
2588 ed.vim.last_change = Some(LastChange::LineOp {
2589 op,
2590 count: total,
2591 inserted: None,
2592 });
2593 }
2594 return true;
2595 }
2596
2597 if let Key::Char('i') | Key::Char('a') = input.key
2599 && !input.ctrl
2600 {
2601 let inner = matches!(input.key, Key::Char('i'));
2602 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2603 return true;
2604 }
2605
2606 if input.key == Key::Char('g') && !input.ctrl {
2608 ed.vim.pending = Pending::OpG { op, count1 };
2609 return true;
2610 }
2611
2612 if let Some((forward, till)) = find_entry(&input) {
2614 ed.vim.pending = Pending::OpFind {
2615 op,
2616 count1,
2617 forward,
2618 till,
2619 };
2620 return true;
2621 }
2622
2623 let count2 = take_count(&mut ed.vim);
2625 let total = count1.max(1) * count2.max(1);
2626 if let Some(motion) = parse_motion(&input) {
2627 let motion = match motion {
2628 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2629 Some((ch, forward, till)) => Motion::Find {
2630 ch,
2631 forward: if reverse { !forward } else { forward },
2632 till,
2633 },
2634 None => return true,
2635 },
2636 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2640 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2641 m => m,
2642 };
2643 apply_op_with_motion(ed, op, &motion, total);
2644 if let Motion::Find { ch, forward, till } = &motion {
2645 ed.vim.last_find = Some((*ch, *forward, *till));
2646 }
2647 if !ed.vim.replaying && op_is_change(op) {
2648 ed.vim.last_change = Some(LastChange::OpMotion {
2649 op,
2650 motion,
2651 count: total,
2652 inserted: None,
2653 });
2654 }
2655 return true;
2656 }
2657
2658 true
2660}
2661
2662fn handle_op_after_g<H: crate::types::Host>(
2663 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2664 input: Input,
2665 op: Operator,
2666 count1: usize,
2667) -> bool {
2668 if input.ctrl {
2669 return true;
2670 }
2671 let count2 = take_count(&mut ed.vim);
2672 let total = count1.max(1) * count2.max(1);
2673 if matches!(
2677 op,
2678 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
2679 ) {
2680 let op_char = match op {
2681 Operator::Uppercase => 'U',
2682 Operator::Lowercase => 'u',
2683 Operator::ToggleCase => '~',
2684 _ => unreachable!(),
2685 };
2686 if input.key == Key::Char(op_char) {
2687 execute_line_op(ed, op, total);
2688 if !ed.vim.replaying {
2689 ed.vim.last_change = Some(LastChange::LineOp {
2690 op,
2691 count: total,
2692 inserted: None,
2693 });
2694 }
2695 return true;
2696 }
2697 }
2698 let motion = match input.key {
2699 Key::Char('g') => Motion::FileTop,
2700 Key::Char('e') => Motion::WordEndBack,
2701 Key::Char('E') => Motion::BigWordEndBack,
2702 Key::Char('j') => Motion::ScreenDown,
2703 Key::Char('k') => Motion::ScreenUp,
2704 _ => return true,
2705 };
2706 apply_op_with_motion(ed, op, &motion, total);
2707 if !ed.vim.replaying && op_is_change(op) {
2708 ed.vim.last_change = Some(LastChange::OpMotion {
2709 op,
2710 motion,
2711 count: total,
2712 inserted: None,
2713 });
2714 }
2715 true
2716}
2717
2718fn handle_after_g<H: crate::types::Host>(
2719 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2720 input: Input,
2721) -> bool {
2722 let count = take_count(&mut ed.vim);
2723 match input.key {
2724 Key::Char('g') => {
2725 let pre = ed.cursor();
2727 if count > 1 {
2728 ed.jump_cursor(count - 1, 0);
2729 } else {
2730 ed.jump_cursor(0, 0);
2731 }
2732 move_first_non_whitespace(ed);
2733 if ed.cursor() != pre {
2734 push_jump(ed, pre);
2735 }
2736 }
2737 Key::Char('e') => execute_motion(ed, Motion::WordEndBack, count),
2738 Key::Char('E') => execute_motion(ed, Motion::BigWordEndBack, count),
2739 Key::Char('_') => execute_motion(ed, Motion::LastNonBlank, count),
2741 Key::Char('M') => execute_motion(ed, Motion::LineMiddle, count),
2743 Key::Char('v') => {
2745 if let Some(snap) = ed.vim.last_visual {
2746 match snap.mode {
2747 Mode::Visual => {
2748 ed.vim.visual_anchor = snap.anchor;
2749 ed.vim.mode = Mode::Visual;
2750 }
2751 Mode::VisualLine => {
2752 ed.vim.visual_line_anchor = snap.anchor.0;
2753 ed.vim.mode = Mode::VisualLine;
2754 }
2755 Mode::VisualBlock => {
2756 ed.vim.block_anchor = snap.anchor;
2757 ed.vim.block_vcol = snap.block_vcol;
2758 ed.vim.mode = Mode::VisualBlock;
2759 }
2760 _ => {}
2761 }
2762 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
2763 }
2764 }
2765 Key::Char('j') => execute_motion(ed, Motion::ScreenDown, count),
2769 Key::Char('k') => execute_motion(ed, Motion::ScreenUp, count),
2770 Key::Char('U') => {
2774 ed.vim.pending = Pending::Op {
2775 op: Operator::Uppercase,
2776 count1: count,
2777 };
2778 }
2779 Key::Char('u') => {
2780 ed.vim.pending = Pending::Op {
2781 op: Operator::Lowercase,
2782 count1: count,
2783 };
2784 }
2785 Key::Char('~') => {
2786 ed.vim.pending = Pending::Op {
2787 op: Operator::ToggleCase,
2788 count1: count,
2789 };
2790 }
2791 Key::Char('q') => {
2792 ed.vim.pending = Pending::Op {
2795 op: Operator::Reflow,
2796 count1: count,
2797 };
2798 }
2799 Key::Char('J') => {
2800 for _ in 0..count.max(1) {
2802 ed.push_undo();
2803 join_line_raw(ed);
2804 }
2805 if !ed.vim.replaying {
2806 ed.vim.last_change = Some(LastChange::JoinLine {
2807 count: count.max(1),
2808 });
2809 }
2810 }
2811 Key::Char('d') => {
2812 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
2817 }
2818 Key::Char(';') => walk_change_list(ed, -1, count.max(1)),
2821 Key::Char(',') => walk_change_list(ed, 1, count.max(1)),
2822 Key::Char('*') => execute_motion(
2826 ed,
2827 Motion::WordAtCursor {
2828 forward: true,
2829 whole_word: false,
2830 },
2831 count,
2832 ),
2833 Key::Char('#') => execute_motion(
2834 ed,
2835 Motion::WordAtCursor {
2836 forward: false,
2837 whole_word: false,
2838 },
2839 count,
2840 ),
2841 _ => {}
2842 }
2843 true
2844}
2845
2846fn handle_after_z<H: crate::types::Host>(
2847 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2848 input: Input,
2849) -> bool {
2850 use crate::editor::CursorScrollTarget;
2851 let row = ed.cursor().0;
2852 match input.key {
2853 Key::Char('z') => {
2854 ed.scroll_cursor_to(CursorScrollTarget::Center);
2855 ed.vim.viewport_pinned = true;
2856 }
2857 Key::Char('t') => {
2858 ed.scroll_cursor_to(CursorScrollTarget::Top);
2859 ed.vim.viewport_pinned = true;
2860 }
2861 Key::Char('b') => {
2862 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
2863 ed.vim.viewport_pinned = true;
2864 }
2865 Key::Char('o') => {
2870 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
2871 }
2872 Key::Char('c') => {
2873 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
2874 }
2875 Key::Char('a') => {
2876 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
2877 }
2878 Key::Char('R') => {
2879 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
2880 }
2881 Key::Char('M') => {
2882 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
2883 }
2884 Key::Char('E') => {
2885 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
2886 }
2887 Key::Char('d') => {
2888 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
2889 }
2890 Key::Char('f') => {
2891 if matches!(
2892 ed.vim.mode,
2893 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2894 ) {
2895 let anchor_row = match ed.vim.mode {
2898 Mode::VisualLine => ed.vim.visual_line_anchor,
2899 Mode::VisualBlock => ed.vim.block_anchor.0,
2900 _ => ed.vim.visual_anchor.0,
2901 };
2902 let cur = ed.cursor().0;
2903 let top = anchor_row.min(cur);
2904 let bot = anchor_row.max(cur);
2905 ed.apply_fold_op(crate::types::FoldOp::Add {
2906 start_row: top,
2907 end_row: bot,
2908 closed: true,
2909 });
2910 ed.vim.mode = Mode::Normal;
2911 } else {
2912 let count = take_count(&mut ed.vim);
2917 ed.vim.pending = Pending::Op {
2918 op: Operator::Fold,
2919 count1: count,
2920 };
2921 }
2922 }
2923 _ => {}
2924 }
2925 true
2926}
2927
2928fn handle_replace<H: crate::types::Host>(
2929 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2930 input: Input,
2931) -> bool {
2932 if let Key::Char(ch) = input.key {
2933 if ed.vim.mode == Mode::VisualBlock {
2934 block_replace(ed, ch);
2935 return true;
2936 }
2937 let count = take_count(&mut ed.vim);
2938 replace_char(ed, ch, count.max(1));
2939 if !ed.vim.replaying {
2940 ed.vim.last_change = Some(LastChange::ReplaceChar {
2941 ch,
2942 count: count.max(1),
2943 });
2944 }
2945 }
2946 true
2947}
2948
2949fn handle_find_target<H: crate::types::Host>(
2950 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2951 input: Input,
2952 forward: bool,
2953 till: bool,
2954) -> bool {
2955 let Key::Char(ch) = input.key else {
2956 return true;
2957 };
2958 let count = take_count(&mut ed.vim);
2959 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
2960 ed.vim.last_find = Some((ch, forward, till));
2961 true
2962}
2963
2964fn handle_op_find_target<H: crate::types::Host>(
2965 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2966 input: Input,
2967 op: Operator,
2968 count1: usize,
2969 forward: bool,
2970 till: bool,
2971) -> bool {
2972 let Key::Char(ch) = input.key else {
2973 return true;
2974 };
2975 let count2 = take_count(&mut ed.vim);
2976 let total = count1.max(1) * count2.max(1);
2977 let motion = Motion::Find { ch, forward, till };
2978 apply_op_with_motion(ed, op, &motion, total);
2979 ed.vim.last_find = Some((ch, forward, till));
2980 if !ed.vim.replaying && op_is_change(op) {
2981 ed.vim.last_change = Some(LastChange::OpMotion {
2982 op,
2983 motion,
2984 count: total,
2985 inserted: None,
2986 });
2987 }
2988 true
2989}
2990
2991fn handle_text_object<H: crate::types::Host>(
2992 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2993 input: Input,
2994 op: Operator,
2995 _count1: usize,
2996 inner: bool,
2997) -> bool {
2998 let Key::Char(ch) = input.key else {
2999 return true;
3000 };
3001 let obj = match ch {
3002 'w' => TextObject::Word { big: false },
3003 'W' => TextObject::Word { big: true },
3004 '"' | '\'' | '`' => TextObject::Quote(ch),
3005 '(' | ')' | 'b' => TextObject::Bracket('('),
3006 '[' | ']' => TextObject::Bracket('['),
3007 '{' | '}' | 'B' => TextObject::Bracket('{'),
3008 '<' | '>' => TextObject::Bracket('<'),
3009 'p' => TextObject::Paragraph,
3010 't' => TextObject::XmlTag,
3011 's' => TextObject::Sentence,
3012 _ => return true,
3013 };
3014 apply_op_with_text_object(ed, op, obj, inner);
3015 if !ed.vim.replaying && op_is_change(op) {
3016 ed.vim.last_change = Some(LastChange::OpTextObj {
3017 op,
3018 obj,
3019 inner,
3020 inserted: None,
3021 });
3022 }
3023 true
3024}
3025
3026fn handle_visual_text_obj<H: crate::types::Host>(
3027 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3028 input: Input,
3029 inner: bool,
3030) -> bool {
3031 let Key::Char(ch) = input.key else {
3032 return true;
3033 };
3034 let obj = match ch {
3035 'w' => TextObject::Word { big: false },
3036 'W' => TextObject::Word { big: true },
3037 '"' | '\'' | '`' => TextObject::Quote(ch),
3038 '(' | ')' | 'b' => TextObject::Bracket('('),
3039 '[' | ']' => TextObject::Bracket('['),
3040 '{' | '}' | 'B' => TextObject::Bracket('{'),
3041 '<' | '>' => TextObject::Bracket('<'),
3042 'p' => TextObject::Paragraph,
3043 't' => TextObject::XmlTag,
3044 's' => TextObject::Sentence,
3045 _ => return true,
3046 };
3047 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3048 return true;
3049 };
3050 match kind {
3054 MotionKind::Linewise => {
3055 ed.vim.visual_line_anchor = start.0;
3056 ed.vim.mode = Mode::VisualLine;
3057 ed.jump_cursor(end.0, 0);
3058 }
3059 _ => {
3060 ed.vim.mode = Mode::Visual;
3061 ed.vim.visual_anchor = (start.0, start.1);
3062 let (er, ec) = retreat_one(ed, end);
3063 ed.jump_cursor(er, ec);
3064 }
3065 }
3066 true
3067}
3068
3069fn retreat_one<H: crate::types::Host>(
3071 ed: &Editor<hjkl_buffer::Buffer, H>,
3072 pos: (usize, usize),
3073) -> (usize, usize) {
3074 let (r, c) = pos;
3075 if c > 0 {
3076 (r, c - 1)
3077 } else if r > 0 {
3078 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3079 (r - 1, prev_len)
3080 } else {
3081 (0, 0)
3082 }
3083}
3084
3085fn op_is_change(op: Operator) -> bool {
3086 matches!(op, Operator::Delete | Operator::Change)
3087}
3088
3089fn handle_normal_only<H: crate::types::Host>(
3092 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3093 input: &Input,
3094 count: usize,
3095) -> bool {
3096 if input.ctrl {
3097 return false;
3098 }
3099 match input.key {
3100 Key::Char('i') => {
3101 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3102 true
3103 }
3104 Key::Char('I') => {
3105 move_first_non_whitespace(ed);
3106 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3107 true
3108 }
3109 Key::Char('a') => {
3110 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3111 ed.push_buffer_cursor_to_textarea();
3112 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3113 true
3114 }
3115 Key::Char('A') => {
3116 crate::motions::move_line_end(&mut ed.buffer);
3117 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3118 ed.push_buffer_cursor_to_textarea();
3119 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3120 true
3121 }
3122 Key::Char('R') => {
3123 begin_insert(ed, count.max(1), InsertReason::Replace);
3126 true
3127 }
3128 Key::Char('o') => {
3129 use hjkl_buffer::{Edit, Position};
3130 ed.push_undo();
3131 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3134 ed.sync_buffer_content_from_textarea();
3135 let row = buf_cursor_pos(&ed.buffer).row;
3136 let line_chars = buf_line_chars(&ed.buffer, row);
3137 ed.mutate_edit(Edit::InsertStr {
3138 at: Position::new(row, line_chars),
3139 text: "\n".to_string(),
3140 });
3141 ed.push_buffer_cursor_to_textarea();
3142 true
3143 }
3144 Key::Char('O') => {
3145 use hjkl_buffer::{Edit, Position};
3146 ed.push_undo();
3147 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3148 ed.sync_buffer_content_from_textarea();
3149 let row = buf_cursor_pos(&ed.buffer).row;
3150 ed.mutate_edit(Edit::InsertStr {
3151 at: Position::new(row, 0),
3152 text: "\n".to_string(),
3153 });
3154 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3157 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3158 ed.push_buffer_cursor_to_textarea();
3159 true
3160 }
3161 Key::Char('x') => {
3162 do_char_delete(ed, true, count.max(1));
3163 if !ed.vim.replaying {
3164 ed.vim.last_change = Some(LastChange::CharDel {
3165 forward: true,
3166 count: count.max(1),
3167 });
3168 }
3169 true
3170 }
3171 Key::Char('X') => {
3172 do_char_delete(ed, false, count.max(1));
3173 if !ed.vim.replaying {
3174 ed.vim.last_change = Some(LastChange::CharDel {
3175 forward: false,
3176 count: count.max(1),
3177 });
3178 }
3179 true
3180 }
3181 Key::Char('~') => {
3182 for _ in 0..count.max(1) {
3183 ed.push_undo();
3184 toggle_case_at_cursor(ed);
3185 }
3186 if !ed.vim.replaying {
3187 ed.vim.last_change = Some(LastChange::ToggleCase {
3188 count: count.max(1),
3189 });
3190 }
3191 true
3192 }
3193 Key::Char('J') => {
3194 for _ in 0..count.max(1) {
3195 ed.push_undo();
3196 join_line(ed);
3197 }
3198 if !ed.vim.replaying {
3199 ed.vim.last_change = Some(LastChange::JoinLine {
3200 count: count.max(1),
3201 });
3202 }
3203 true
3204 }
3205 Key::Char('D') => {
3206 ed.push_undo();
3207 delete_to_eol(ed);
3208 crate::motions::move_left(&mut ed.buffer, 1);
3210 ed.push_buffer_cursor_to_textarea();
3211 if !ed.vim.replaying {
3212 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3213 }
3214 true
3215 }
3216 Key::Char('Y') => {
3217 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3219 true
3220 }
3221 Key::Char('C') => {
3222 ed.push_undo();
3223 delete_to_eol(ed);
3224 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3225 true
3226 }
3227 Key::Char('s') => {
3228 use hjkl_buffer::{Edit, MotionKind, Position};
3229 ed.push_undo();
3230 ed.sync_buffer_content_from_textarea();
3231 for _ in 0..count.max(1) {
3232 let cursor = buf_cursor_pos(&ed.buffer);
3233 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3234 if cursor.col >= line_chars {
3235 break;
3236 }
3237 ed.mutate_edit(Edit::DeleteRange {
3238 start: cursor,
3239 end: Position::new(cursor.row, cursor.col + 1),
3240 kind: MotionKind::Char,
3241 });
3242 }
3243 ed.push_buffer_cursor_to_textarea();
3244 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3245 if !ed.vim.replaying {
3247 ed.vim.last_change = Some(LastChange::OpMotion {
3248 op: Operator::Change,
3249 motion: Motion::Right,
3250 count: count.max(1),
3251 inserted: None,
3252 });
3253 }
3254 true
3255 }
3256 Key::Char('p') => {
3257 do_paste(ed, false, count.max(1));
3258 if !ed.vim.replaying {
3259 ed.vim.last_change = Some(LastChange::Paste {
3260 before: false,
3261 count: count.max(1),
3262 });
3263 }
3264 true
3265 }
3266 Key::Char('P') => {
3267 do_paste(ed, true, count.max(1));
3268 if !ed.vim.replaying {
3269 ed.vim.last_change = Some(LastChange::Paste {
3270 before: true,
3271 count: count.max(1),
3272 });
3273 }
3274 true
3275 }
3276 Key::Char('u') => {
3277 do_undo(ed);
3278 true
3279 }
3280 Key::Char('r') => {
3281 ed.vim.count = count;
3282 ed.vim.pending = Pending::Replace;
3283 true
3284 }
3285 Key::Char('/') => {
3286 enter_search(ed, true);
3287 true
3288 }
3289 Key::Char('?') => {
3290 enter_search(ed, false);
3291 true
3292 }
3293 Key::Char('.') => {
3294 replay_last_change(ed, count);
3295 true
3296 }
3297 _ => false,
3298 }
3299}
3300
3301fn begin_insert_noundo<H: crate::types::Host>(
3303 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3304 count: usize,
3305 reason: InsertReason,
3306) {
3307 let reason = if ed.vim.replaying {
3308 InsertReason::ReplayOnly
3309 } else {
3310 reason
3311 };
3312 let (row, _) = ed.cursor();
3313 ed.vim.insert_session = Some(InsertSession {
3314 count,
3315 row_min: row,
3316 row_max: row,
3317 before_lines: buf_lines_to_vec(&ed.buffer),
3318 reason,
3319 });
3320 ed.vim.mode = Mode::Insert;
3321}
3322
3323fn apply_op_with_motion<H: crate::types::Host>(
3326 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3327 op: Operator,
3328 motion: &Motion,
3329 count: usize,
3330) {
3331 let start = ed.cursor();
3332 apply_motion_cursor_ctx(ed, motion, count, true);
3337 let end = ed.cursor();
3338 let kind = motion_kind(motion);
3339 ed.jump_cursor(start.0, start.1);
3341 run_operator_over_range(ed, op, start, end, kind);
3342}
3343
3344fn apply_op_with_text_object<H: crate::types::Host>(
3345 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3346 op: Operator,
3347 obj: TextObject,
3348 inner: bool,
3349) {
3350 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3351 return;
3352 };
3353 ed.jump_cursor(start.0, start.1);
3354 run_operator_over_range(ed, op, start, end, kind);
3355}
3356
3357fn motion_kind(motion: &Motion) -> MotionKind {
3358 match motion {
3359 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3360 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3361 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3362 MotionKind::Linewise
3363 }
3364 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3365 MotionKind::Inclusive
3366 }
3367 Motion::Find { .. } => MotionKind::Inclusive,
3368 Motion::MatchBracket => MotionKind::Inclusive,
3369 Motion::LineEnd => MotionKind::Inclusive,
3371 _ => MotionKind::Exclusive,
3372 }
3373}
3374
3375fn run_operator_over_range<H: crate::types::Host>(
3376 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3377 op: Operator,
3378 start: (usize, usize),
3379 end: (usize, usize),
3380 kind: MotionKind,
3381) {
3382 let (top, bot) = order(start, end);
3383 if top == bot {
3384 return;
3385 }
3386
3387 match op {
3388 Operator::Yank => {
3389 let text = read_vim_range(ed, top, bot, kind);
3390 if !text.is_empty() {
3391 ed.record_yank_to_host(text.clone());
3392 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3393 }
3394 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3395 ed.push_buffer_cursor_to_textarea();
3396 }
3397 Operator::Delete => {
3398 ed.push_undo();
3399 cut_vim_range(ed, top, bot, kind);
3400 ed.vim.mode = Mode::Normal;
3401 }
3402 Operator::Change => {
3403 ed.push_undo();
3404 cut_vim_range(ed, top, bot, kind);
3405 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3406 }
3407 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3408 apply_case_op_to_selection(ed, op, top, bot, kind);
3409 }
3410 Operator::Indent | Operator::Outdent => {
3411 ed.push_undo();
3414 if op == Operator::Indent {
3415 indent_rows(ed, top.0, bot.0, 1);
3416 } else {
3417 outdent_rows(ed, top.0, bot.0, 1);
3418 }
3419 ed.vim.mode = Mode::Normal;
3420 }
3421 Operator::Fold => {
3422 if bot.0 >= top.0 {
3426 ed.apply_fold_op(crate::types::FoldOp::Add {
3427 start_row: top.0,
3428 end_row: bot.0,
3429 closed: true,
3430 });
3431 }
3432 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3433 ed.push_buffer_cursor_to_textarea();
3434 ed.vim.mode = Mode::Normal;
3435 }
3436 Operator::Reflow => {
3437 ed.push_undo();
3438 reflow_rows(ed, top.0, bot.0);
3439 ed.vim.mode = Mode::Normal;
3440 }
3441 }
3442}
3443
3444fn reflow_rows<H: crate::types::Host>(
3449 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3450 top: usize,
3451 bot: usize,
3452) {
3453 let width = ed.settings().textwidth.max(1);
3454 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3455 let bot = bot.min(lines.len().saturating_sub(1));
3456 if top > bot {
3457 return;
3458 }
3459 let original = lines[top..=bot].to_vec();
3460 let mut wrapped: Vec<String> = Vec::new();
3461 let mut paragraph: Vec<String> = Vec::new();
3462 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3463 if para.is_empty() {
3464 return;
3465 }
3466 let words = para.join(" ");
3467 let mut current = String::new();
3468 for word in words.split_whitespace() {
3469 let extra = if current.is_empty() {
3470 word.chars().count()
3471 } else {
3472 current.chars().count() + 1 + word.chars().count()
3473 };
3474 if extra > width && !current.is_empty() {
3475 out.push(std::mem::take(&mut current));
3476 current.push_str(word);
3477 } else if current.is_empty() {
3478 current.push_str(word);
3479 } else {
3480 current.push(' ');
3481 current.push_str(word);
3482 }
3483 }
3484 if !current.is_empty() {
3485 out.push(current);
3486 }
3487 para.clear();
3488 };
3489 for line in &original {
3490 if line.trim().is_empty() {
3491 flush(&mut paragraph, &mut wrapped, width);
3492 wrapped.push(String::new());
3493 } else {
3494 paragraph.push(line.clone());
3495 }
3496 }
3497 flush(&mut paragraph, &mut wrapped, width);
3498
3499 let after: Vec<String> = lines.split_off(bot + 1);
3501 lines.truncate(top);
3502 lines.extend(wrapped);
3503 lines.extend(after);
3504 ed.restore(lines, (top, 0));
3505 ed.mark_content_dirty();
3506}
3507
3508fn apply_case_op_to_selection<H: crate::types::Host>(
3514 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3515 op: Operator,
3516 top: (usize, usize),
3517 bot: (usize, usize),
3518 kind: MotionKind,
3519) {
3520 use hjkl_buffer::Edit;
3521 ed.push_undo();
3522 let saved_yank = ed.yank().to_string();
3523 let saved_yank_linewise = ed.vim.yank_linewise;
3524 let selection = cut_vim_range(ed, top, bot, kind);
3525 let transformed = match op {
3526 Operator::Uppercase => selection.to_uppercase(),
3527 Operator::Lowercase => selection.to_lowercase(),
3528 Operator::ToggleCase => toggle_case_str(&selection),
3529 _ => unreachable!(),
3530 };
3531 if !transformed.is_empty() {
3532 let cursor = buf_cursor_pos(&ed.buffer);
3533 ed.mutate_edit(Edit::InsertStr {
3534 at: cursor,
3535 text: transformed,
3536 });
3537 }
3538 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3539 ed.push_buffer_cursor_to_textarea();
3540 ed.set_yank(saved_yank);
3541 ed.vim.yank_linewise = saved_yank_linewise;
3542 ed.vim.mode = Mode::Normal;
3543}
3544
3545fn indent_rows<H: crate::types::Host>(
3550 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3551 top: usize,
3552 bot: usize,
3553 count: usize,
3554) {
3555 ed.sync_buffer_content_from_textarea();
3556 let width = ed.settings().shiftwidth * count.max(1);
3557 let pad: String = " ".repeat(width);
3558 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3559 let bot = bot.min(lines.len().saturating_sub(1));
3560 for line in lines.iter_mut().take(bot + 1).skip(top) {
3561 if !line.is_empty() {
3562 line.insert_str(0, &pad);
3563 }
3564 }
3565 ed.restore(lines, (top, 0));
3568 move_first_non_whitespace(ed);
3569}
3570
3571fn outdent_rows<H: crate::types::Host>(
3575 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3576 top: usize,
3577 bot: usize,
3578 count: usize,
3579) {
3580 ed.sync_buffer_content_from_textarea();
3581 let width = ed.settings().shiftwidth * count.max(1);
3582 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3583 let bot = bot.min(lines.len().saturating_sub(1));
3584 for line in lines.iter_mut().take(bot + 1).skip(top) {
3585 let strip: usize = line
3586 .chars()
3587 .take(width)
3588 .take_while(|c| *c == ' ' || *c == '\t')
3589 .count();
3590 if strip > 0 {
3591 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
3592 line.drain(..byte_len);
3593 }
3594 }
3595 ed.restore(lines, (top, 0));
3596 move_first_non_whitespace(ed);
3597}
3598
3599fn toggle_case_str(s: &str) -> String {
3600 s.chars()
3601 .map(|c| {
3602 if c.is_lowercase() {
3603 c.to_uppercase().next().unwrap_or(c)
3604 } else if c.is_uppercase() {
3605 c.to_lowercase().next().unwrap_or(c)
3606 } else {
3607 c
3608 }
3609 })
3610 .collect()
3611}
3612
3613fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
3614 if a <= b { (a, b) } else { (b, a) }
3615}
3616
3617fn execute_line_op<H: crate::types::Host>(
3620 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3621 op: Operator,
3622 count: usize,
3623) {
3624 let (row, col) = ed.cursor();
3625 let total = buf_row_count(&ed.buffer);
3626 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
3627
3628 match op {
3629 Operator::Yank => {
3630 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3632 if !text.is_empty() {
3633 ed.record_yank_to_host(text.clone());
3634 ed.record_yank(text, true);
3635 }
3636 buf_set_cursor_rc(&mut ed.buffer, row, col);
3637 ed.push_buffer_cursor_to_textarea();
3638 ed.vim.mode = Mode::Normal;
3639 }
3640 Operator::Delete => {
3641 ed.push_undo();
3642 let deleted_through_last = end_row + 1 >= total;
3643 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3644 let total_after = buf_row_count(&ed.buffer);
3648 let target_row = if deleted_through_last {
3649 row.saturating_sub(1).min(total_after.saturating_sub(1))
3650 } else {
3651 row.min(total_after.saturating_sub(1))
3652 };
3653 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
3654 ed.push_buffer_cursor_to_textarea();
3655 move_first_non_whitespace(ed);
3656 ed.vim.mode = Mode::Normal;
3657 }
3658 Operator::Change => {
3659 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3663 ed.push_undo();
3664 ed.sync_buffer_content_from_textarea();
3665 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
3667 if end_row > row {
3668 ed.mutate_edit(Edit::DeleteRange {
3669 start: Position::new(row + 1, 0),
3670 end: Position::new(end_row, 0),
3671 kind: BufKind::Line,
3672 });
3673 }
3674 let line_chars = buf_line_chars(&ed.buffer, row);
3675 if line_chars > 0 {
3676 ed.mutate_edit(Edit::DeleteRange {
3677 start: Position::new(row, 0),
3678 end: Position::new(row, line_chars),
3679 kind: BufKind::Char,
3680 });
3681 }
3682 if !payload.is_empty() {
3683 ed.record_yank_to_host(payload.clone());
3684 ed.record_delete(payload, true);
3685 }
3686 buf_set_cursor_rc(&mut ed.buffer, row, 0);
3687 ed.push_buffer_cursor_to_textarea();
3688 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3689 }
3690 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3691 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
3695 move_first_non_whitespace(ed);
3698 }
3699 Operator::Indent | Operator::Outdent => {
3700 ed.push_undo();
3702 if op == Operator::Indent {
3703 indent_rows(ed, row, end_row, 1);
3704 } else {
3705 outdent_rows(ed, row, end_row, 1);
3706 }
3707 ed.vim.mode = Mode::Normal;
3708 }
3709 Operator::Fold => unreachable!("Fold has no line-op double"),
3711 Operator::Reflow => {
3712 ed.push_undo();
3714 reflow_rows(ed, row, end_row);
3715 ed.vim.mode = Mode::Normal;
3716 }
3717 }
3718}
3719
3720fn apply_visual_operator<H: crate::types::Host>(
3723 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3724 op: Operator,
3725) {
3726 match ed.vim.mode {
3727 Mode::VisualLine => {
3728 let cursor_row = buf_cursor_pos(&ed.buffer).row;
3729 let top = cursor_row.min(ed.vim.visual_line_anchor);
3730 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3731 ed.vim.yank_linewise = true;
3732 match op {
3733 Operator::Yank => {
3734 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3735 if !text.is_empty() {
3736 ed.record_yank_to_host(text.clone());
3737 ed.record_yank(text, true);
3738 }
3739 buf_set_cursor_rc(&mut ed.buffer, top, 0);
3740 ed.push_buffer_cursor_to_textarea();
3741 ed.vim.mode = Mode::Normal;
3742 }
3743 Operator::Delete => {
3744 ed.push_undo();
3745 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3746 ed.vim.mode = Mode::Normal;
3747 }
3748 Operator::Change => {
3749 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
3752 ed.push_undo();
3753 ed.sync_buffer_content_from_textarea();
3754 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
3755 if bot > top {
3756 ed.mutate_edit(Edit::DeleteRange {
3757 start: Position::new(top + 1, 0),
3758 end: Position::new(bot, 0),
3759 kind: BufKind::Line,
3760 });
3761 }
3762 let line_chars = buf_line_chars(&ed.buffer, top);
3763 if line_chars > 0 {
3764 ed.mutate_edit(Edit::DeleteRange {
3765 start: Position::new(top, 0),
3766 end: Position::new(top, line_chars),
3767 kind: BufKind::Char,
3768 });
3769 }
3770 if !payload.is_empty() {
3771 ed.record_yank_to_host(payload.clone());
3772 ed.record_delete(payload, true);
3773 }
3774 buf_set_cursor_rc(&mut ed.buffer, top, 0);
3775 ed.push_buffer_cursor_to_textarea();
3776 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3777 }
3778 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3779 let bot = buf_cursor_pos(&ed.buffer)
3780 .row
3781 .max(ed.vim.visual_line_anchor);
3782 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
3783 move_first_non_whitespace(ed);
3784 }
3785 Operator::Indent | Operator::Outdent => {
3786 ed.push_undo();
3787 let (cursor_row, _) = ed.cursor();
3788 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3789 if op == Operator::Indent {
3790 indent_rows(ed, top, bot, 1);
3791 } else {
3792 outdent_rows(ed, top, bot, 1);
3793 }
3794 ed.vim.mode = Mode::Normal;
3795 }
3796 Operator::Reflow => {
3797 ed.push_undo();
3798 let (cursor_row, _) = ed.cursor();
3799 let bot = cursor_row.max(ed.vim.visual_line_anchor);
3800 reflow_rows(ed, top, bot);
3801 ed.vim.mode = Mode::Normal;
3802 }
3803 Operator::Fold => unreachable!("Visual zf takes its own path"),
3806 }
3807 }
3808 Mode::Visual => {
3809 ed.vim.yank_linewise = false;
3810 let anchor = ed.vim.visual_anchor;
3811 let cursor = ed.cursor();
3812 let (top, bot) = order(anchor, cursor);
3813 match op {
3814 Operator::Yank => {
3815 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
3816 if !text.is_empty() {
3817 ed.record_yank_to_host(text.clone());
3818 ed.record_yank(text, false);
3819 }
3820 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3821 ed.push_buffer_cursor_to_textarea();
3822 ed.vim.mode = Mode::Normal;
3823 }
3824 Operator::Delete => {
3825 ed.push_undo();
3826 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
3827 ed.vim.mode = Mode::Normal;
3828 }
3829 Operator::Change => {
3830 ed.push_undo();
3831 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
3832 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3833 }
3834 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3835 let anchor = ed.vim.visual_anchor;
3837 let cursor = ed.cursor();
3838 let (top, bot) = order(anchor, cursor);
3839 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
3840 }
3841 Operator::Indent | Operator::Outdent => {
3842 ed.push_undo();
3843 let anchor = ed.vim.visual_anchor;
3844 let cursor = ed.cursor();
3845 let (top, bot) = order(anchor, cursor);
3846 if op == Operator::Indent {
3847 indent_rows(ed, top.0, bot.0, 1);
3848 } else {
3849 outdent_rows(ed, top.0, bot.0, 1);
3850 }
3851 ed.vim.mode = Mode::Normal;
3852 }
3853 Operator::Reflow => {
3854 ed.push_undo();
3855 let anchor = ed.vim.visual_anchor;
3856 let cursor = ed.cursor();
3857 let (top, bot) = order(anchor, cursor);
3858 reflow_rows(ed, top.0, bot.0);
3859 ed.vim.mode = Mode::Normal;
3860 }
3861 Operator::Fold => unreachable!("Visual zf takes its own path"),
3862 }
3863 }
3864 Mode::VisualBlock => apply_block_operator(ed, op),
3865 _ => {}
3866 }
3867}
3868
3869fn block_bounds<H: crate::types::Host>(
3874 ed: &Editor<hjkl_buffer::Buffer, H>,
3875) -> (usize, usize, usize, usize) {
3876 let (ar, ac) = ed.vim.block_anchor;
3877 let (cr, _) = ed.cursor();
3878 let cc = ed.vim.block_vcol;
3879 let top = ar.min(cr);
3880 let bot = ar.max(cr);
3881 let left = ac.min(cc);
3882 let right = ac.max(cc);
3883 (top, bot, left, right)
3884}
3885
3886fn update_block_vcol<H: crate::types::Host>(
3891 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3892 motion: &Motion,
3893) {
3894 match motion {
3895 Motion::Left
3896 | Motion::Right
3897 | Motion::WordFwd
3898 | Motion::BigWordFwd
3899 | Motion::WordBack
3900 | Motion::BigWordBack
3901 | Motion::WordEnd
3902 | Motion::BigWordEnd
3903 | Motion::WordEndBack
3904 | Motion::BigWordEndBack
3905 | Motion::LineStart
3906 | Motion::FirstNonBlank
3907 | Motion::LineEnd
3908 | Motion::Find { .. }
3909 | Motion::FindRepeat { .. }
3910 | Motion::MatchBracket => {
3911 ed.vim.block_vcol = ed.cursor().1;
3912 }
3913 _ => {}
3915 }
3916}
3917
3918fn apply_block_operator<H: crate::types::Host>(
3923 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3924 op: Operator,
3925) {
3926 let (top, bot, left, right) = block_bounds(ed);
3927 let yank = block_yank(ed, top, bot, left, right);
3929
3930 match op {
3931 Operator::Yank => {
3932 if !yank.is_empty() {
3933 ed.record_yank_to_host(yank.clone());
3934 ed.record_yank(yank, false);
3935 }
3936 ed.vim.mode = Mode::Normal;
3937 ed.jump_cursor(top, left);
3938 }
3939 Operator::Delete => {
3940 ed.push_undo();
3941 delete_block_contents(ed, top, bot, left, right);
3942 if !yank.is_empty() {
3943 ed.record_yank_to_host(yank.clone());
3944 ed.record_delete(yank, false);
3945 }
3946 ed.vim.mode = Mode::Normal;
3947 ed.jump_cursor(top, left);
3948 }
3949 Operator::Change => {
3950 ed.push_undo();
3951 delete_block_contents(ed, top, bot, left, right);
3952 if !yank.is_empty() {
3953 ed.record_yank_to_host(yank.clone());
3954 ed.record_delete(yank, false);
3955 }
3956 ed.jump_cursor(top, left);
3957 begin_insert_noundo(
3958 ed,
3959 1,
3960 InsertReason::BlockEdge {
3961 top,
3962 bot,
3963 col: left,
3964 },
3965 );
3966 }
3967 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3968 ed.push_undo();
3969 transform_block_case(ed, op, top, bot, left, right);
3970 ed.vim.mode = Mode::Normal;
3971 ed.jump_cursor(top, left);
3972 }
3973 Operator::Indent | Operator::Outdent => {
3974 ed.push_undo();
3978 if op == Operator::Indent {
3979 indent_rows(ed, top, bot, 1);
3980 } else {
3981 outdent_rows(ed, top, bot, 1);
3982 }
3983 ed.vim.mode = Mode::Normal;
3984 }
3985 Operator::Fold => unreachable!("Visual zf takes its own path"),
3986 Operator::Reflow => {
3987 ed.push_undo();
3991 reflow_rows(ed, top, bot);
3992 ed.vim.mode = Mode::Normal;
3993 }
3994 }
3995}
3996
3997fn transform_block_case<H: crate::types::Host>(
4001 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4002 op: Operator,
4003 top: usize,
4004 bot: usize,
4005 left: usize,
4006 right: usize,
4007) {
4008 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4009 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4010 let chars: Vec<char> = lines[r].chars().collect();
4011 if left >= chars.len() {
4012 continue;
4013 }
4014 let end = (right + 1).min(chars.len());
4015 let head: String = chars[..left].iter().collect();
4016 let mid: String = chars[left..end].iter().collect();
4017 let tail: String = chars[end..].iter().collect();
4018 let transformed = match op {
4019 Operator::Uppercase => mid.to_uppercase(),
4020 Operator::Lowercase => mid.to_lowercase(),
4021 Operator::ToggleCase => toggle_case_str(&mid),
4022 _ => mid,
4023 };
4024 lines[r] = format!("{head}{transformed}{tail}");
4025 }
4026 let saved_yank = ed.yank().to_string();
4027 let saved_linewise = ed.vim.yank_linewise;
4028 ed.restore(lines, (top, left));
4029 ed.set_yank(saved_yank);
4030 ed.vim.yank_linewise = saved_linewise;
4031}
4032
4033fn block_yank<H: crate::types::Host>(
4034 ed: &Editor<hjkl_buffer::Buffer, H>,
4035 top: usize,
4036 bot: usize,
4037 left: usize,
4038 right: usize,
4039) -> String {
4040 let lines = buf_lines_to_vec(&ed.buffer);
4041 let mut rows: Vec<String> = Vec::new();
4042 for r in top..=bot {
4043 let line = match lines.get(r) {
4044 Some(l) => l,
4045 None => break,
4046 };
4047 let chars: Vec<char> = line.chars().collect();
4048 let end = (right + 1).min(chars.len());
4049 if left >= chars.len() {
4050 rows.push(String::new());
4051 } else {
4052 rows.push(chars[left..end].iter().collect());
4053 }
4054 }
4055 rows.join("\n")
4056}
4057
4058fn delete_block_contents<H: crate::types::Host>(
4059 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4060 top: usize,
4061 bot: usize,
4062 left: usize,
4063 right: usize,
4064) {
4065 use hjkl_buffer::{Edit, MotionKind, Position};
4066 ed.sync_buffer_content_from_textarea();
4067 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4068 if last_row < top {
4069 return;
4070 }
4071 ed.mutate_edit(Edit::DeleteRange {
4072 start: Position::new(top, left),
4073 end: Position::new(last_row, right),
4074 kind: MotionKind::Block,
4075 });
4076 ed.push_buffer_cursor_to_textarea();
4077}
4078
4079fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4081 let (top, bot, left, right) = block_bounds(ed);
4082 ed.push_undo();
4083 ed.sync_buffer_content_from_textarea();
4084 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4085 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4086 let chars: Vec<char> = lines[r].chars().collect();
4087 if left >= chars.len() {
4088 continue;
4089 }
4090 let end = (right + 1).min(chars.len());
4091 let before: String = chars[..left].iter().collect();
4092 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4093 let after: String = chars[end..].iter().collect();
4094 lines[r] = format!("{before}{middle}{after}");
4095 }
4096 reset_textarea_lines(ed, lines);
4097 ed.vim.mode = Mode::Normal;
4098 ed.jump_cursor(top, left);
4099}
4100
4101fn reset_textarea_lines<H: crate::types::Host>(
4105 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4106 lines: Vec<String>,
4107) {
4108 let cursor = ed.cursor();
4109 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4110 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4111 ed.mark_content_dirty();
4112}
4113
4114type Pos = (usize, usize);
4120
4121fn text_object_range<H: crate::types::Host>(
4125 ed: &Editor<hjkl_buffer::Buffer, H>,
4126 obj: TextObject,
4127 inner: bool,
4128) -> Option<(Pos, Pos, MotionKind)> {
4129 match obj {
4130 TextObject::Word { big } => {
4131 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4132 }
4133 TextObject::Quote(q) => {
4134 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4135 }
4136 TextObject::Bracket(open) => {
4137 bracket_text_object(ed, open, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4138 }
4139 TextObject::Paragraph => {
4140 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4141 }
4142 TextObject::XmlTag => {
4143 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4144 }
4145 TextObject::Sentence => {
4146 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4147 }
4148 }
4149}
4150
4151fn sentence_boundary<H: crate::types::Host>(
4155 ed: &Editor<hjkl_buffer::Buffer, H>,
4156 forward: bool,
4157) -> Option<(usize, usize)> {
4158 let lines = buf_lines_to_vec(&ed.buffer);
4159 if lines.is_empty() {
4160 return None;
4161 }
4162 let pos_to_idx = |pos: (usize, usize)| -> usize {
4163 let mut idx = 0;
4164 for line in lines.iter().take(pos.0) {
4165 idx += line.chars().count() + 1;
4166 }
4167 idx + pos.1
4168 };
4169 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4170 for (r, line) in lines.iter().enumerate() {
4171 let len = line.chars().count();
4172 if idx <= len {
4173 return (r, idx);
4174 }
4175 idx -= len + 1;
4176 }
4177 let last = lines.len().saturating_sub(1);
4178 (last, lines[last].chars().count())
4179 };
4180 let mut chars: Vec<char> = Vec::new();
4181 for (r, line) in lines.iter().enumerate() {
4182 chars.extend(line.chars());
4183 if r + 1 < lines.len() {
4184 chars.push('\n');
4185 }
4186 }
4187 if chars.is_empty() {
4188 return None;
4189 }
4190 let total = chars.len();
4191 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4192 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4193
4194 if forward {
4195 let mut i = cursor_idx + 1;
4198 while i < total {
4199 if is_terminator(chars[i]) {
4200 while i + 1 < total && is_terminator(chars[i + 1]) {
4201 i += 1;
4202 }
4203 if i + 1 >= total {
4204 return None;
4205 }
4206 if chars[i + 1].is_whitespace() {
4207 let mut j = i + 1;
4208 while j < total && chars[j].is_whitespace() {
4209 j += 1;
4210 }
4211 if j >= total {
4212 return None;
4213 }
4214 return Some(idx_to_pos(j));
4215 }
4216 }
4217 i += 1;
4218 }
4219 None
4220 } else {
4221 let find_start = |from: usize| -> Option<usize> {
4225 let mut start = from;
4226 while start > 0 {
4227 let prev = chars[start - 1];
4228 if prev.is_whitespace() {
4229 let mut k = start - 1;
4230 while k > 0 && chars[k - 1].is_whitespace() {
4231 k -= 1;
4232 }
4233 if k > 0 && is_terminator(chars[k - 1]) {
4234 break;
4235 }
4236 }
4237 start -= 1;
4238 }
4239 while start < total && chars[start].is_whitespace() {
4240 start += 1;
4241 }
4242 (start < total).then_some(start)
4243 };
4244 let current_start = find_start(cursor_idx)?;
4245 if current_start < cursor_idx {
4246 return Some(idx_to_pos(current_start));
4247 }
4248 let mut k = current_start;
4251 while k > 0 && chars[k - 1].is_whitespace() {
4252 k -= 1;
4253 }
4254 if k == 0 {
4255 return None;
4256 }
4257 let prev_start = find_start(k - 1)?;
4258 Some(idx_to_pos(prev_start))
4259 }
4260}
4261
4262fn sentence_text_object<H: crate::types::Host>(
4268 ed: &Editor<hjkl_buffer::Buffer, H>,
4269 inner: bool,
4270) -> Option<((usize, usize), (usize, usize))> {
4271 let lines = buf_lines_to_vec(&ed.buffer);
4272 if lines.is_empty() {
4273 return None;
4274 }
4275 let pos_to_idx = |pos: (usize, usize)| -> usize {
4278 let mut idx = 0;
4279 for line in lines.iter().take(pos.0) {
4280 idx += line.chars().count() + 1;
4281 }
4282 idx + pos.1
4283 };
4284 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4285 for (r, line) in lines.iter().enumerate() {
4286 let len = line.chars().count();
4287 if idx <= len {
4288 return (r, idx);
4289 }
4290 idx -= len + 1;
4291 }
4292 let last = lines.len().saturating_sub(1);
4293 (last, lines[last].chars().count())
4294 };
4295 let mut chars: Vec<char> = Vec::new();
4296 for (r, line) in lines.iter().enumerate() {
4297 chars.extend(line.chars());
4298 if r + 1 < lines.len() {
4299 chars.push('\n');
4300 }
4301 }
4302 if chars.is_empty() {
4303 return None;
4304 }
4305
4306 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4307 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4308
4309 let mut start = cursor_idx;
4313 while start > 0 {
4314 let prev = chars[start - 1];
4315 if prev.is_whitespace() {
4316 let mut k = start - 1;
4320 while k > 0 && chars[k - 1].is_whitespace() {
4321 k -= 1;
4322 }
4323 if k > 0 && is_terminator(chars[k - 1]) {
4324 break;
4325 }
4326 }
4327 start -= 1;
4328 }
4329 while start < chars.len() && chars[start].is_whitespace() {
4332 start += 1;
4333 }
4334 if start >= chars.len() {
4335 return None;
4336 }
4337
4338 let mut end = start;
4341 while end < chars.len() {
4342 if is_terminator(chars[end]) {
4343 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4345 end += 1;
4346 }
4347 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4350 break;
4351 }
4352 }
4353 end += 1;
4354 }
4355 let end_idx = (end + 1).min(chars.len());
4357
4358 let final_end = if inner {
4359 end_idx
4360 } else {
4361 let mut e = end_idx;
4365 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4366 e += 1;
4367 }
4368 e
4369 };
4370
4371 Some((idx_to_pos(start), idx_to_pos(final_end)))
4372}
4373
4374fn tag_text_object<H: crate::types::Host>(
4378 ed: &Editor<hjkl_buffer::Buffer, H>,
4379 inner: bool,
4380) -> Option<((usize, usize), (usize, usize))> {
4381 let lines = buf_lines_to_vec(&ed.buffer);
4382 if lines.is_empty() {
4383 return None;
4384 }
4385 let pos_to_idx = |pos: (usize, usize)| -> usize {
4389 let mut idx = 0;
4390 for line in lines.iter().take(pos.0) {
4391 idx += line.chars().count() + 1;
4392 }
4393 idx + pos.1
4394 };
4395 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4396 for (r, line) in lines.iter().enumerate() {
4397 let len = line.chars().count();
4398 if idx <= len {
4399 return (r, idx);
4400 }
4401 idx -= len + 1;
4402 }
4403 let last = lines.len().saturating_sub(1);
4404 (last, lines[last].chars().count())
4405 };
4406 let mut chars: Vec<char> = Vec::new();
4407 for (r, line) in lines.iter().enumerate() {
4408 chars.extend(line.chars());
4409 if r + 1 < lines.len() {
4410 chars.push('\n');
4411 }
4412 }
4413 let cursor_idx = pos_to_idx(ed.cursor());
4414
4415 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4421 let mut i = 0;
4422 while i < chars.len() {
4423 if chars[i] != '<' {
4424 i += 1;
4425 continue;
4426 }
4427 let mut j = i + 1;
4428 while j < chars.len() && chars[j] != '>' {
4429 j += 1;
4430 }
4431 if j >= chars.len() {
4432 break;
4433 }
4434 let inside: String = chars[i + 1..j].iter().collect();
4435 let close_end = j + 1;
4436 let trimmed = inside.trim();
4437 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4438 i = close_end;
4439 continue;
4440 }
4441 if let Some(rest) = trimmed.strip_prefix('/') {
4442 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4443 if !name.is_empty()
4444 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4445 {
4446 let (open_start, content_start, _) = stack[stack_idx].clone();
4447 stack.truncate(stack_idx);
4448 let content_end = i;
4449 if cursor_idx >= content_start && cursor_idx <= content_end {
4450 let candidate = (open_start, content_start, content_end, close_end);
4451 innermost = match innermost {
4452 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4453 Some(candidate)
4454 }
4455 None => Some(candidate),
4456 existing => existing,
4457 };
4458 }
4459 }
4460 } else if !trimmed.ends_with('/') {
4461 let name: String = trimmed
4462 .split(|c: char| c.is_whitespace() || c == '/')
4463 .next()
4464 .unwrap_or("")
4465 .to_string();
4466 if !name.is_empty() {
4467 stack.push((i, close_end, name));
4468 }
4469 }
4470 i = close_end;
4471 }
4472
4473 let (open_start, content_start, content_end, close_end) = innermost?;
4474 if inner {
4475 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4476 } else {
4477 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4478 }
4479}
4480
4481fn is_wordchar(c: char) -> bool {
4482 c.is_alphanumeric() || c == '_'
4483}
4484
4485pub(crate) use hjkl_buffer::is_keyword_char;
4489
4490fn word_text_object<H: crate::types::Host>(
4491 ed: &Editor<hjkl_buffer::Buffer, H>,
4492 inner: bool,
4493 big: bool,
4494) -> Option<((usize, usize), (usize, usize))> {
4495 let (row, col) = ed.cursor();
4496 let line = buf_line(&ed.buffer, row)?;
4497 let chars: Vec<char> = line.chars().collect();
4498 if chars.is_empty() {
4499 return None;
4500 }
4501 let at = col.min(chars.len().saturating_sub(1));
4502 let classify = |c: char| -> u8 {
4503 if c.is_whitespace() {
4504 0
4505 } else if big || is_wordchar(c) {
4506 1
4507 } else {
4508 2
4509 }
4510 };
4511 let cls = classify(chars[at]);
4512 let mut start = at;
4513 while start > 0 && classify(chars[start - 1]) == cls {
4514 start -= 1;
4515 }
4516 let mut end = at;
4517 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4518 end += 1;
4519 }
4520 let char_byte = |i: usize| {
4522 if i >= chars.len() {
4523 line.len()
4524 } else {
4525 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
4526 }
4527 };
4528 let mut start_col = char_byte(start);
4529 let mut end_col = char_byte(end + 1);
4531 if !inner {
4532 let mut t = end + 1;
4534 let mut included_trailing = false;
4535 while t < chars.len() && chars[t].is_whitespace() {
4536 included_trailing = true;
4537 t += 1;
4538 }
4539 if included_trailing {
4540 end_col = char_byte(t);
4541 } else {
4542 let mut s = start;
4543 while s > 0 && chars[s - 1].is_whitespace() {
4544 s -= 1;
4545 }
4546 start_col = char_byte(s);
4547 }
4548 }
4549 Some(((row, start_col), (row, end_col)))
4550}
4551
4552fn quote_text_object<H: crate::types::Host>(
4553 ed: &Editor<hjkl_buffer::Buffer, H>,
4554 q: char,
4555 inner: bool,
4556) -> Option<((usize, usize), (usize, usize))> {
4557 let (row, col) = ed.cursor();
4558 let line = buf_line(&ed.buffer, row)?;
4559 let bytes = line.as_bytes();
4560 let q_byte = q as u8;
4561 let mut positions: Vec<usize> = Vec::new();
4563 for (i, &b) in bytes.iter().enumerate() {
4564 if b == q_byte {
4565 positions.push(i);
4566 }
4567 }
4568 if positions.len() < 2 {
4569 return None;
4570 }
4571 let mut open_idx: Option<usize> = None;
4572 let mut close_idx: Option<usize> = None;
4573 for pair in positions.chunks(2) {
4574 if pair.len() < 2 {
4575 break;
4576 }
4577 if col >= pair[0] && col <= pair[1] {
4578 open_idx = Some(pair[0]);
4579 close_idx = Some(pair[1]);
4580 break;
4581 }
4582 if col < pair[0] {
4583 open_idx = Some(pair[0]);
4584 close_idx = Some(pair[1]);
4585 break;
4586 }
4587 }
4588 let open = open_idx?;
4589 let close = close_idx?;
4590 if inner {
4592 if close <= open + 1 {
4593 return None;
4594 }
4595 Some(((row, open + 1), (row, close)))
4596 } else {
4597 Some(((row, open), (row, close + 1)))
4598 }
4599}
4600
4601fn bracket_text_object<H: crate::types::Host>(
4602 ed: &Editor<hjkl_buffer::Buffer, H>,
4603 open: char,
4604 inner: bool,
4605) -> Option<((usize, usize), (usize, usize))> {
4606 let close = match open {
4607 '(' => ')',
4608 '[' => ']',
4609 '{' => '}',
4610 '<' => '>',
4611 _ => return None,
4612 };
4613 let (row, col) = ed.cursor();
4614 let lines = buf_lines_to_vec(&ed.buffer);
4615 let lines = lines.as_slice();
4616 let open_pos = find_open_bracket(lines, row, col, open, close)?;
4618 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
4619 if inner {
4621 let inner_start = advance_pos(lines, open_pos);
4622 if inner_start.0 > close_pos.0
4623 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
4624 {
4625 return None;
4626 }
4627 Some((inner_start, close_pos))
4628 } else {
4629 Some((open_pos, advance_pos(lines, close_pos)))
4630 }
4631}
4632
4633fn find_open_bracket(
4634 lines: &[String],
4635 row: usize,
4636 col: usize,
4637 open: char,
4638 close: char,
4639) -> Option<(usize, usize)> {
4640 let mut depth: i32 = 0;
4641 let mut r = row;
4642 let mut c = col as isize;
4643 loop {
4644 let cur = &lines[r];
4645 let chars: Vec<char> = cur.chars().collect();
4646 if (c as usize) >= chars.len() {
4650 c = chars.len() as isize - 1;
4651 }
4652 while c >= 0 {
4653 let ch = chars[c as usize];
4654 if ch == close {
4655 depth += 1;
4656 } else if ch == open {
4657 if depth == 0 {
4658 return Some((r, c as usize));
4659 }
4660 depth -= 1;
4661 }
4662 c -= 1;
4663 }
4664 if r == 0 {
4665 return None;
4666 }
4667 r -= 1;
4668 c = lines[r].chars().count() as isize - 1;
4669 }
4670}
4671
4672fn find_close_bracket(
4673 lines: &[String],
4674 row: usize,
4675 start_col: usize,
4676 open: char,
4677 close: char,
4678) -> Option<(usize, usize)> {
4679 let mut depth: i32 = 0;
4680 let mut r = row;
4681 let mut c = start_col;
4682 loop {
4683 let cur = &lines[r];
4684 let chars: Vec<char> = cur.chars().collect();
4685 while c < chars.len() {
4686 let ch = chars[c];
4687 if ch == open {
4688 depth += 1;
4689 } else if ch == close {
4690 if depth == 0 {
4691 return Some((r, c));
4692 }
4693 depth -= 1;
4694 }
4695 c += 1;
4696 }
4697 if r + 1 >= lines.len() {
4698 return None;
4699 }
4700 r += 1;
4701 c = 0;
4702 }
4703}
4704
4705fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
4706 let (r, c) = pos;
4707 let line_len = lines[r].chars().count();
4708 if c < line_len {
4709 (r, c + 1)
4710 } else if r + 1 < lines.len() {
4711 (r + 1, 0)
4712 } else {
4713 pos
4714 }
4715}
4716
4717fn paragraph_text_object<H: crate::types::Host>(
4718 ed: &Editor<hjkl_buffer::Buffer, H>,
4719 inner: bool,
4720) -> Option<((usize, usize), (usize, usize))> {
4721 let (row, _) = ed.cursor();
4722 let lines = buf_lines_to_vec(&ed.buffer);
4723 if lines.is_empty() {
4724 return None;
4725 }
4726 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
4728 if is_blank(row) {
4729 return None;
4730 }
4731 let mut top = row;
4732 while top > 0 && !is_blank(top - 1) {
4733 top -= 1;
4734 }
4735 let mut bot = row;
4736 while bot + 1 < lines.len() && !is_blank(bot + 1) {
4737 bot += 1;
4738 }
4739 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
4741 bot += 1;
4742 }
4743 let end_col = lines[bot].chars().count();
4744 Some(((top, 0), (bot, end_col)))
4745}
4746
4747fn read_vim_range<H: crate::types::Host>(
4753 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4754 start: (usize, usize),
4755 end: (usize, usize),
4756 kind: MotionKind,
4757) -> String {
4758 let (top, bot) = order(start, end);
4759 ed.sync_buffer_content_from_textarea();
4760 let lines = buf_lines_to_vec(&ed.buffer);
4761 match kind {
4762 MotionKind::Linewise => {
4763 let lo = top.0;
4764 let hi = bot.0.min(lines.len().saturating_sub(1));
4765 let mut text = lines[lo..=hi].join("\n");
4766 text.push('\n');
4767 text
4768 }
4769 MotionKind::Inclusive | MotionKind::Exclusive => {
4770 let inclusive = matches!(kind, MotionKind::Inclusive);
4771 let mut out = String::new();
4773 for row in top.0..=bot.0 {
4774 let line = lines.get(row).map(String::as_str).unwrap_or("");
4775 let lo = if row == top.0 { top.1 } else { 0 };
4776 let hi_unclamped = if row == bot.0 {
4777 if inclusive { bot.1 + 1 } else { bot.1 }
4778 } else {
4779 line.chars().count() + 1
4780 };
4781 let row_chars: Vec<char> = line.chars().collect();
4782 let hi = hi_unclamped.min(row_chars.len());
4783 if lo < hi {
4784 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
4785 }
4786 if row < bot.0 {
4787 out.push('\n');
4788 }
4789 }
4790 out
4791 }
4792 }
4793}
4794
4795fn cut_vim_range<H: crate::types::Host>(
4804 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4805 start: (usize, usize),
4806 end: (usize, usize),
4807 kind: MotionKind,
4808) -> String {
4809 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4810 let (top, bot) = order(start, end);
4811 ed.sync_buffer_content_from_textarea();
4812 let (buf_start, buf_end, buf_kind) = match kind {
4813 MotionKind::Linewise => (
4814 Position::new(top.0, 0),
4815 Position::new(bot.0, 0),
4816 BufKind::Line,
4817 ),
4818 MotionKind::Inclusive => {
4819 let line_chars = buf_line_chars(&ed.buffer, bot.0);
4820 let next = if bot.1 < line_chars {
4824 Position::new(bot.0, bot.1 + 1)
4825 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
4826 Position::new(bot.0 + 1, 0)
4827 } else {
4828 Position::new(bot.0, line_chars)
4829 };
4830 (Position::new(top.0, top.1), next, BufKind::Char)
4831 }
4832 MotionKind::Exclusive => (
4833 Position::new(top.0, top.1),
4834 Position::new(bot.0, bot.1),
4835 BufKind::Char,
4836 ),
4837 };
4838 let inverse = ed.mutate_edit(Edit::DeleteRange {
4839 start: buf_start,
4840 end: buf_end,
4841 kind: buf_kind,
4842 });
4843 let text = match inverse {
4844 Edit::InsertStr { text, .. } => text,
4845 _ => String::new(),
4846 };
4847 if !text.is_empty() {
4848 ed.record_yank_to_host(text.clone());
4849 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
4850 }
4851 ed.push_buffer_cursor_to_textarea();
4852 text
4853}
4854
4855fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4861 use hjkl_buffer::{Edit, MotionKind, Position};
4862 ed.sync_buffer_content_from_textarea();
4863 let cursor = buf_cursor_pos(&ed.buffer);
4864 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
4865 if cursor.col >= line_chars {
4866 return;
4867 }
4868 let inverse = ed.mutate_edit(Edit::DeleteRange {
4869 start: cursor,
4870 end: Position::new(cursor.row, line_chars),
4871 kind: MotionKind::Char,
4872 });
4873 if let Edit::InsertStr { text, .. } = inverse
4874 && !text.is_empty()
4875 {
4876 ed.record_yank_to_host(text.clone());
4877 ed.vim.yank_linewise = false;
4878 ed.set_yank(text);
4879 }
4880 buf_set_cursor_pos(&mut ed.buffer, cursor);
4881 ed.push_buffer_cursor_to_textarea();
4882}
4883
4884fn do_char_delete<H: crate::types::Host>(
4885 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4886 forward: bool,
4887 count: usize,
4888) {
4889 use hjkl_buffer::{Edit, MotionKind, Position};
4890 ed.push_undo();
4891 ed.sync_buffer_content_from_textarea();
4892 for _ in 0..count {
4893 let cursor = buf_cursor_pos(&ed.buffer);
4894 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
4895 if forward {
4896 if cursor.col >= line_chars {
4899 continue;
4900 }
4901 ed.mutate_edit(Edit::DeleteRange {
4902 start: cursor,
4903 end: Position::new(cursor.row, cursor.col + 1),
4904 kind: MotionKind::Char,
4905 });
4906 } else {
4907 if cursor.col == 0 {
4909 continue;
4910 }
4911 ed.mutate_edit(Edit::DeleteRange {
4912 start: Position::new(cursor.row, cursor.col - 1),
4913 end: cursor,
4914 kind: MotionKind::Char,
4915 });
4916 }
4917 }
4918 ed.push_buffer_cursor_to_textarea();
4919}
4920
4921fn adjust_number<H: crate::types::Host>(
4925 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4926 delta: i64,
4927) -> bool {
4928 use hjkl_buffer::{Edit, MotionKind, Position};
4929 ed.sync_buffer_content_from_textarea();
4930 let cursor = buf_cursor_pos(&ed.buffer);
4931 let row = cursor.row;
4932 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
4933 Some(l) => l.chars().collect(),
4934 None => return false,
4935 };
4936 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
4937 return false;
4938 };
4939 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
4940 digit_start - 1
4941 } else {
4942 digit_start
4943 };
4944 let mut span_end = digit_start;
4945 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
4946 span_end += 1;
4947 }
4948 let s: String = chars[span_start..span_end].iter().collect();
4949 let Ok(n) = s.parse::<i64>() else {
4950 return false;
4951 };
4952 let new_s = n.saturating_add(delta).to_string();
4953
4954 ed.push_undo();
4955 let span_start_pos = Position::new(row, span_start);
4956 let span_end_pos = Position::new(row, span_end);
4957 ed.mutate_edit(Edit::DeleteRange {
4958 start: span_start_pos,
4959 end: span_end_pos,
4960 kind: MotionKind::Char,
4961 });
4962 ed.mutate_edit(Edit::InsertStr {
4963 at: span_start_pos,
4964 text: new_s.clone(),
4965 });
4966 let new_len = new_s.chars().count();
4967 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
4968 ed.push_buffer_cursor_to_textarea();
4969 true
4970}
4971
4972fn replace_char<H: crate::types::Host>(
4973 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4974 ch: char,
4975 count: usize,
4976) {
4977 use hjkl_buffer::{Edit, MotionKind, Position};
4978 ed.push_undo();
4979 ed.sync_buffer_content_from_textarea();
4980 for _ in 0..count {
4981 let cursor = buf_cursor_pos(&ed.buffer);
4982 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
4983 if cursor.col >= line_chars {
4984 break;
4985 }
4986 ed.mutate_edit(Edit::DeleteRange {
4987 start: cursor,
4988 end: Position::new(cursor.row, cursor.col + 1),
4989 kind: MotionKind::Char,
4990 });
4991 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
4992 }
4993 crate::motions::move_left(&mut ed.buffer, 1);
4995 ed.push_buffer_cursor_to_textarea();
4996}
4997
4998fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4999 use hjkl_buffer::{Edit, MotionKind, Position};
5000 ed.sync_buffer_content_from_textarea();
5001 let cursor = buf_cursor_pos(&ed.buffer);
5002 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5003 return;
5004 };
5005 let toggled = if c.is_uppercase() {
5006 c.to_lowercase().next().unwrap_or(c)
5007 } else {
5008 c.to_uppercase().next().unwrap_or(c)
5009 };
5010 ed.mutate_edit(Edit::DeleteRange {
5011 start: cursor,
5012 end: Position::new(cursor.row, cursor.col + 1),
5013 kind: MotionKind::Char,
5014 });
5015 ed.mutate_edit(Edit::InsertChar {
5016 at: cursor,
5017 ch: toggled,
5018 });
5019}
5020
5021fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5022 use hjkl_buffer::{Edit, Position};
5023 ed.sync_buffer_content_from_textarea();
5024 let row = buf_cursor_pos(&ed.buffer).row;
5025 if row + 1 >= buf_row_count(&ed.buffer) {
5026 return;
5027 }
5028 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5029 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5030 let next_trimmed = next_raw.trim_start();
5031 let cur_chars = cur_line.chars().count();
5032 let next_chars = next_raw.chars().count();
5033 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5036 " "
5037 } else {
5038 ""
5039 };
5040 let joined = format!("{cur_line}{separator}{next_trimmed}");
5041 ed.mutate_edit(Edit::Replace {
5042 start: Position::new(row, 0),
5043 end: Position::new(row + 1, next_chars),
5044 with: joined,
5045 });
5046 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5050 ed.push_buffer_cursor_to_textarea();
5051}
5052
5053fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5056 use hjkl_buffer::Edit;
5057 ed.sync_buffer_content_from_textarea();
5058 let row = buf_cursor_pos(&ed.buffer).row;
5059 if row + 1 >= buf_row_count(&ed.buffer) {
5060 return;
5061 }
5062 let join_col = buf_line_chars(&ed.buffer, row);
5063 ed.mutate_edit(Edit::JoinLines {
5064 row,
5065 count: 1,
5066 with_space: false,
5067 });
5068 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5070 ed.push_buffer_cursor_to_textarea();
5071}
5072
5073fn do_paste<H: crate::types::Host>(
5074 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5075 before: bool,
5076 count: usize,
5077) {
5078 use hjkl_buffer::{Edit, Position};
5079 ed.push_undo();
5080 let selector = ed.vim.pending_register.take();
5085 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5086 Some(slot) => (slot.text.clone(), slot.linewise),
5087 None => (ed.yank().to_string(), ed.vim.yank_linewise),
5088 };
5089 for _ in 0..count {
5090 ed.sync_buffer_content_from_textarea();
5091 let yank = yank.clone();
5092 if yank.is_empty() {
5093 continue;
5094 }
5095 if linewise {
5096 let text = yank.trim_matches('\n').to_string();
5100 let row = buf_cursor_pos(&ed.buffer).row;
5101 let target_row = if before {
5102 ed.mutate_edit(Edit::InsertStr {
5103 at: Position::new(row, 0),
5104 text: format!("{text}\n"),
5105 });
5106 row
5107 } else {
5108 let line_chars = buf_line_chars(&ed.buffer, row);
5109 ed.mutate_edit(Edit::InsertStr {
5110 at: Position::new(row, line_chars),
5111 text: format!("\n{text}"),
5112 });
5113 row + 1
5114 };
5115 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5116 crate::motions::move_first_non_blank(&mut ed.buffer);
5117 ed.push_buffer_cursor_to_textarea();
5118 } else {
5119 let cursor = buf_cursor_pos(&ed.buffer);
5123 let at = if before {
5124 cursor
5125 } else {
5126 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5127 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5128 };
5129 ed.mutate_edit(Edit::InsertStr {
5130 at,
5131 text: yank.clone(),
5132 });
5133 crate::motions::move_left(&mut ed.buffer, 1);
5136 ed.push_buffer_cursor_to_textarea();
5137 }
5138 }
5139 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5141}
5142
5143pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5144 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5145 let current = ed.snapshot();
5146 ed.redo_stack.push(current);
5147 ed.restore(lines, cursor);
5148 }
5149 ed.vim.mode = Mode::Normal;
5150}
5151
5152pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5153 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5154 let current = ed.snapshot();
5155 ed.undo_stack.push(current);
5156 ed.cap_undo();
5157 ed.restore(lines, cursor);
5158 }
5159 ed.vim.mode = Mode::Normal;
5160}
5161
5162fn replay_insert_and_finish<H: crate::types::Host>(
5169 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5170 text: &str,
5171) {
5172 use hjkl_buffer::{Edit, Position};
5173 let cursor = ed.cursor();
5174 ed.mutate_edit(Edit::InsertStr {
5175 at: Position::new(cursor.0, cursor.1),
5176 text: text.to_string(),
5177 });
5178 if ed.vim.insert_session.take().is_some() {
5179 if ed.cursor().1 > 0 {
5180 crate::motions::move_left(&mut ed.buffer, 1);
5181 ed.push_buffer_cursor_to_textarea();
5182 }
5183 ed.vim.mode = Mode::Normal;
5184 }
5185}
5186
5187fn replay_last_change<H: crate::types::Host>(
5188 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5189 outer_count: usize,
5190) {
5191 let Some(change) = ed.vim.last_change.clone() else {
5192 return;
5193 };
5194 ed.vim.replaying = true;
5195 let scale = if outer_count > 0 { outer_count } else { 1 };
5196 match change {
5197 LastChange::OpMotion {
5198 op,
5199 motion,
5200 count,
5201 inserted,
5202 } => {
5203 let total = count.max(1) * scale;
5204 apply_op_with_motion(ed, op, &motion, total);
5205 if let Some(text) = inserted {
5206 replay_insert_and_finish(ed, &text);
5207 }
5208 }
5209 LastChange::OpTextObj {
5210 op,
5211 obj,
5212 inner,
5213 inserted,
5214 } => {
5215 apply_op_with_text_object(ed, op, obj, inner);
5216 if let Some(text) = inserted {
5217 replay_insert_and_finish(ed, &text);
5218 }
5219 }
5220 LastChange::LineOp {
5221 op,
5222 count,
5223 inserted,
5224 } => {
5225 let total = count.max(1) * scale;
5226 execute_line_op(ed, op, total);
5227 if let Some(text) = inserted {
5228 replay_insert_and_finish(ed, &text);
5229 }
5230 }
5231 LastChange::CharDel { forward, count } => {
5232 do_char_delete(ed, forward, count * scale);
5233 }
5234 LastChange::ReplaceChar { ch, count } => {
5235 replace_char(ed, ch, count * scale);
5236 }
5237 LastChange::ToggleCase { count } => {
5238 for _ in 0..count * scale {
5239 ed.push_undo();
5240 toggle_case_at_cursor(ed);
5241 }
5242 }
5243 LastChange::JoinLine { count } => {
5244 for _ in 0..count * scale {
5245 ed.push_undo();
5246 join_line(ed);
5247 }
5248 }
5249 LastChange::Paste { before, count } => {
5250 do_paste(ed, before, count * scale);
5251 }
5252 LastChange::DeleteToEol { inserted } => {
5253 use hjkl_buffer::{Edit, Position};
5254 ed.push_undo();
5255 delete_to_eol(ed);
5256 if let Some(text) = inserted {
5257 let cursor = ed.cursor();
5258 ed.mutate_edit(Edit::InsertStr {
5259 at: Position::new(cursor.0, cursor.1),
5260 text,
5261 });
5262 }
5263 }
5264 LastChange::OpenLine { above, inserted } => {
5265 use hjkl_buffer::{Edit, Position};
5266 ed.push_undo();
5267 ed.sync_buffer_content_from_textarea();
5268 let row = buf_cursor_pos(&ed.buffer).row;
5269 if above {
5270 ed.mutate_edit(Edit::InsertStr {
5271 at: Position::new(row, 0),
5272 text: "\n".to_string(),
5273 });
5274 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5275 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5276 } else {
5277 let line_chars = buf_line_chars(&ed.buffer, row);
5278 ed.mutate_edit(Edit::InsertStr {
5279 at: Position::new(row, line_chars),
5280 text: "\n".to_string(),
5281 });
5282 }
5283 ed.push_buffer_cursor_to_textarea();
5284 let cursor = ed.cursor();
5285 ed.mutate_edit(Edit::InsertStr {
5286 at: Position::new(cursor.0, cursor.1),
5287 text: inserted,
5288 });
5289 }
5290 LastChange::InsertAt {
5291 entry,
5292 inserted,
5293 count,
5294 } => {
5295 use hjkl_buffer::{Edit, Position};
5296 ed.push_undo();
5297 match entry {
5298 InsertEntry::I => {}
5299 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5300 InsertEntry::A => {
5301 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5302 ed.push_buffer_cursor_to_textarea();
5303 }
5304 InsertEntry::ShiftA => {
5305 crate::motions::move_line_end(&mut ed.buffer);
5306 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5307 ed.push_buffer_cursor_to_textarea();
5308 }
5309 }
5310 for _ in 0..count.max(1) {
5311 let cursor = ed.cursor();
5312 ed.mutate_edit(Edit::InsertStr {
5313 at: Position::new(cursor.0, cursor.1),
5314 text: inserted.clone(),
5315 });
5316 }
5317 }
5318 }
5319 ed.vim.replaying = false;
5320}
5321
5322fn extract_inserted(before: &str, after: &str) -> String {
5325 let before_chars: Vec<char> = before.chars().collect();
5326 let after_chars: Vec<char> = after.chars().collect();
5327 if after_chars.len() <= before_chars.len() {
5328 return String::new();
5329 }
5330 let prefix = before_chars
5331 .iter()
5332 .zip(after_chars.iter())
5333 .take_while(|(a, b)| a == b)
5334 .count();
5335 let max_suffix = before_chars.len() - prefix;
5336 let suffix = before_chars
5337 .iter()
5338 .rev()
5339 .zip(after_chars.iter().rev())
5340 .take(max_suffix)
5341 .take_while(|(a, b)| a == b)
5342 .count();
5343 after_chars[prefix..after_chars.len() - suffix]
5344 .iter()
5345 .collect()
5346}
5347
5348#[cfg(all(test, feature = "crossterm"))]
5351mod tests {
5352 use crate::VimMode;
5353 use crate::editor::Editor;
5354 use crate::types::Host;
5355 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5356
5357 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5358 let mut iter = keys.chars().peekable();
5362 while let Some(c) = iter.next() {
5363 if c == '<' {
5364 let mut tag = String::new();
5365 for ch in iter.by_ref() {
5366 if ch == '>' {
5367 break;
5368 }
5369 tag.push(ch);
5370 }
5371 let ev = match tag.as_str() {
5372 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5373 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5374 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5375 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5376 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5377 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5378 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5379 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5380 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5384 s if s.starts_with("C-") => {
5385 let ch = s.chars().nth(2).unwrap();
5386 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5387 }
5388 _ => continue,
5389 };
5390 e.handle_key(ev);
5391 } else {
5392 let mods = if c.is_uppercase() {
5393 KeyModifiers::SHIFT
5394 } else {
5395 KeyModifiers::NONE
5396 };
5397 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5398 }
5399 }
5400 }
5401
5402 fn editor_with(content: &str) -> Editor {
5403 let opts = crate::types::Options {
5408 shiftwidth: 2,
5409 ..crate::types::Options::default()
5410 };
5411 let mut e = Editor::new(
5412 hjkl_buffer::Buffer::new(),
5413 crate::types::DefaultHost::new(),
5414 opts,
5415 );
5416 e.set_content(content);
5417 e
5418 }
5419
5420 #[test]
5421 fn f_char_jumps_on_line() {
5422 let mut e = editor_with("hello world");
5423 run_keys(&mut e, "fw");
5424 assert_eq!(e.cursor(), (0, 6));
5425 }
5426
5427 #[test]
5428 fn cap_f_jumps_backward() {
5429 let mut e = editor_with("hello world");
5430 e.jump_cursor(0, 10);
5431 run_keys(&mut e, "Fo");
5432 assert_eq!(e.cursor().1, 7);
5433 }
5434
5435 #[test]
5436 fn t_stops_before_char() {
5437 let mut e = editor_with("hello");
5438 run_keys(&mut e, "tl");
5439 assert_eq!(e.cursor(), (0, 1));
5440 }
5441
5442 #[test]
5443 fn semicolon_repeats_find() {
5444 let mut e = editor_with("aa.bb.cc");
5445 run_keys(&mut e, "f.");
5446 assert_eq!(e.cursor().1, 2);
5447 run_keys(&mut e, ";");
5448 assert_eq!(e.cursor().1, 5);
5449 }
5450
5451 #[test]
5452 fn comma_repeats_find_reverse() {
5453 let mut e = editor_with("aa.bb.cc");
5454 run_keys(&mut e, "f.");
5455 run_keys(&mut e, ";");
5456 run_keys(&mut e, ",");
5457 assert_eq!(e.cursor().1, 2);
5458 }
5459
5460 #[test]
5461 fn di_quote_deletes_content() {
5462 let mut e = editor_with("foo \"bar\" baz");
5463 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
5465 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
5466 }
5467
5468 #[test]
5469 fn da_quote_deletes_with_quotes() {
5470 let mut e = editor_with("foo \"bar\" baz");
5471 e.jump_cursor(0, 6);
5472 run_keys(&mut e, "da\"");
5473 assert_eq!(e.buffer().lines()[0], "foo baz");
5474 }
5475
5476 #[test]
5477 fn ci_paren_deletes_and_inserts() {
5478 let mut e = editor_with("fn(a, b, c)");
5479 e.jump_cursor(0, 5);
5480 run_keys(&mut e, "ci(");
5481 assert_eq!(e.vim_mode(), VimMode::Insert);
5482 assert_eq!(e.buffer().lines()[0], "fn()");
5483 }
5484
5485 #[test]
5486 fn diw_deletes_inner_word() {
5487 let mut e = editor_with("hello world");
5488 e.jump_cursor(0, 2);
5489 run_keys(&mut e, "diw");
5490 assert_eq!(e.buffer().lines()[0], " world");
5491 }
5492
5493 #[test]
5494 fn daw_deletes_word_with_trailing_space() {
5495 let mut e = editor_with("hello world");
5496 run_keys(&mut e, "daw");
5497 assert_eq!(e.buffer().lines()[0], "world");
5498 }
5499
5500 #[test]
5501 fn percent_jumps_to_matching_bracket() {
5502 let mut e = editor_with("foo(bar)");
5503 e.jump_cursor(0, 3);
5504 run_keys(&mut e, "%");
5505 assert_eq!(e.cursor().1, 7);
5506 run_keys(&mut e, "%");
5507 assert_eq!(e.cursor().1, 3);
5508 }
5509
5510 #[test]
5511 fn dot_repeats_last_change() {
5512 let mut e = editor_with("aaa bbb ccc");
5513 run_keys(&mut e, "dw");
5514 assert_eq!(e.buffer().lines()[0], "bbb ccc");
5515 run_keys(&mut e, ".");
5516 assert_eq!(e.buffer().lines()[0], "ccc");
5517 }
5518
5519 #[test]
5520 fn dot_repeats_change_operator_with_text() {
5521 let mut e = editor_with("foo foo foo");
5522 run_keys(&mut e, "cwbar<Esc>");
5523 assert_eq!(e.buffer().lines()[0], "bar foo foo");
5524 run_keys(&mut e, "w");
5526 run_keys(&mut e, ".");
5527 assert_eq!(e.buffer().lines()[0], "bar bar foo");
5528 }
5529
5530 #[test]
5531 fn dot_repeats_x() {
5532 let mut e = editor_with("abcdef");
5533 run_keys(&mut e, "x");
5534 run_keys(&mut e, "..");
5535 assert_eq!(e.buffer().lines()[0], "def");
5536 }
5537
5538 #[test]
5539 fn count_operator_motion_compose() {
5540 let mut e = editor_with("one two three four five");
5541 run_keys(&mut e, "d3w");
5542 assert_eq!(e.buffer().lines()[0], "four five");
5543 }
5544
5545 #[test]
5546 fn two_dd_deletes_two_lines() {
5547 let mut e = editor_with("a\nb\nc");
5548 run_keys(&mut e, "2dd");
5549 assert_eq!(e.buffer().lines().len(), 1);
5550 assert_eq!(e.buffer().lines()[0], "c");
5551 }
5552
5553 #[test]
5558 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
5559 let mut e = editor_with("one\ntwo\n three\nfour");
5560 e.jump_cursor(1, 2);
5561 run_keys(&mut e, "dd");
5562 assert_eq!(e.buffer().lines()[1], " three");
5564 assert_eq!(e.cursor(), (1, 4));
5565 }
5566
5567 #[test]
5568 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
5569 let mut e = editor_with("one\n two\nthree");
5570 e.jump_cursor(2, 0);
5571 run_keys(&mut e, "dd");
5572 assert_eq!(e.buffer().lines().len(), 2);
5574 assert_eq!(e.cursor(), (1, 2));
5575 }
5576
5577 #[test]
5578 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
5579 let mut e = editor_with("lonely");
5580 run_keys(&mut e, "dd");
5581 assert_eq!(e.buffer().lines().len(), 1);
5582 assert_eq!(e.buffer().lines()[0], "");
5583 assert_eq!(e.cursor(), (0, 0));
5584 }
5585
5586 #[test]
5587 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
5588 let mut e = editor_with("a\nb\nc\n d\ne");
5589 e.jump_cursor(1, 0);
5591 run_keys(&mut e, "3dd");
5592 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
5593 assert_eq!(e.cursor(), (1, 0));
5594 }
5595
5596 #[test]
5597 fn gu_lowercases_motion_range() {
5598 let mut e = editor_with("HELLO WORLD");
5599 run_keys(&mut e, "guw");
5600 assert_eq!(e.buffer().lines()[0], "hello WORLD");
5601 assert_eq!(e.cursor(), (0, 0));
5602 }
5603
5604 #[test]
5605 fn g_u_uppercases_text_object() {
5606 let mut e = editor_with("hello world");
5607 run_keys(&mut e, "gUiw");
5609 assert_eq!(e.buffer().lines()[0], "HELLO world");
5610 assert_eq!(e.cursor(), (0, 0));
5611 }
5612
5613 #[test]
5614 fn g_tilde_toggles_case_of_range() {
5615 let mut e = editor_with("Hello World");
5616 run_keys(&mut e, "g~iw");
5617 assert_eq!(e.buffer().lines()[0], "hELLO World");
5618 }
5619
5620 #[test]
5621 fn g_uu_uppercases_current_line() {
5622 let mut e = editor_with("select 1\nselect 2");
5623 run_keys(&mut e, "gUU");
5624 assert_eq!(e.buffer().lines()[0], "SELECT 1");
5625 assert_eq!(e.buffer().lines()[1], "select 2");
5626 }
5627
5628 #[test]
5629 fn gugu_lowercases_current_line() {
5630 let mut e = editor_with("FOO BAR\nBAZ");
5631 run_keys(&mut e, "gugu");
5632 assert_eq!(e.buffer().lines()[0], "foo bar");
5633 }
5634
5635 #[test]
5636 fn visual_u_uppercases_selection() {
5637 let mut e = editor_with("hello world");
5638 run_keys(&mut e, "veU");
5640 assert_eq!(e.buffer().lines()[0], "HELLO world");
5641 }
5642
5643 #[test]
5644 fn visual_line_u_lowercases_line() {
5645 let mut e = editor_with("HELLO WORLD\nOTHER");
5646 run_keys(&mut e, "Vu");
5647 assert_eq!(e.buffer().lines()[0], "hello world");
5648 assert_eq!(e.buffer().lines()[1], "OTHER");
5649 }
5650
5651 #[test]
5652 fn g_uu_with_count_uppercases_multiple_lines() {
5653 let mut e = editor_with("one\ntwo\nthree\nfour");
5654 run_keys(&mut e, "3gUU");
5656 assert_eq!(e.buffer().lines()[0], "ONE");
5657 assert_eq!(e.buffer().lines()[1], "TWO");
5658 assert_eq!(e.buffer().lines()[2], "THREE");
5659 assert_eq!(e.buffer().lines()[3], "four");
5660 }
5661
5662 #[test]
5663 fn double_gt_indents_current_line() {
5664 let mut e = editor_with("hello");
5665 run_keys(&mut e, ">>");
5666 assert_eq!(e.buffer().lines()[0], " hello");
5667 assert_eq!(e.cursor(), (0, 2));
5669 }
5670
5671 #[test]
5672 fn double_lt_outdents_current_line() {
5673 let mut e = editor_with(" hello");
5674 run_keys(&mut e, "<lt><lt>");
5675 assert_eq!(e.buffer().lines()[0], " hello");
5676 assert_eq!(e.cursor(), (0, 2));
5677 }
5678
5679 #[test]
5680 fn count_double_gt_indents_multiple_lines() {
5681 let mut e = editor_with("a\nb\nc\nd");
5682 run_keys(&mut e, "3>>");
5684 assert_eq!(e.buffer().lines()[0], " a");
5685 assert_eq!(e.buffer().lines()[1], " b");
5686 assert_eq!(e.buffer().lines()[2], " c");
5687 assert_eq!(e.buffer().lines()[3], "d");
5688 }
5689
5690 #[test]
5691 fn outdent_clips_ragged_leading_whitespace() {
5692 let mut e = editor_with(" x");
5695 run_keys(&mut e, "<lt><lt>");
5696 assert_eq!(e.buffer().lines()[0], "x");
5697 }
5698
5699 #[test]
5700 fn indent_motion_is_always_linewise() {
5701 let mut e = editor_with("foo bar");
5704 run_keys(&mut e, ">w");
5705 assert_eq!(e.buffer().lines()[0], " foo bar");
5706 }
5707
5708 #[test]
5709 fn indent_text_object_extends_over_paragraph() {
5710 let mut e = editor_with("a\nb\n\nc\nd");
5711 run_keys(&mut e, ">ap");
5713 assert_eq!(e.buffer().lines()[0], " a");
5714 assert_eq!(e.buffer().lines()[1], " b");
5715 assert_eq!(e.buffer().lines()[2], "");
5716 assert_eq!(e.buffer().lines()[3], "c");
5717 }
5718
5719 #[test]
5720 fn visual_line_indent_shifts_selected_rows() {
5721 let mut e = editor_with("x\ny\nz");
5722 run_keys(&mut e, "Vj>");
5724 assert_eq!(e.buffer().lines()[0], " x");
5725 assert_eq!(e.buffer().lines()[1], " y");
5726 assert_eq!(e.buffer().lines()[2], "z");
5727 }
5728
5729 #[test]
5730 fn outdent_empty_line_is_noop() {
5731 let mut e = editor_with("\nfoo");
5732 run_keys(&mut e, "<lt><lt>");
5733 assert_eq!(e.buffer().lines()[0], "");
5734 }
5735
5736 #[test]
5737 fn indent_skips_empty_lines() {
5738 let mut e = editor_with("");
5741 run_keys(&mut e, ">>");
5742 assert_eq!(e.buffer().lines()[0], "");
5743 }
5744
5745 #[test]
5746 fn insert_ctrl_t_indents_current_line() {
5747 let mut e = editor_with("x");
5748 run_keys(&mut e, "i<C-t>");
5750 assert_eq!(e.buffer().lines()[0], " x");
5751 assert_eq!(e.cursor(), (0, 2));
5754 }
5755
5756 #[test]
5757 fn insert_ctrl_d_outdents_current_line() {
5758 let mut e = editor_with(" x");
5759 run_keys(&mut e, "A<C-d>");
5761 assert_eq!(e.buffer().lines()[0], " x");
5762 }
5763
5764 #[test]
5765 fn h_at_col_zero_does_not_wrap_to_prev_line() {
5766 let mut e = editor_with("first\nsecond");
5767 e.jump_cursor(1, 0);
5768 run_keys(&mut e, "h");
5769 assert_eq!(e.cursor(), (1, 0));
5771 }
5772
5773 #[test]
5774 fn l_at_last_char_does_not_wrap_to_next_line() {
5775 let mut e = editor_with("ab\ncd");
5776 e.jump_cursor(0, 1);
5778 run_keys(&mut e, "l");
5779 assert_eq!(e.cursor(), (0, 1));
5781 }
5782
5783 #[test]
5784 fn count_l_clamps_at_line_end() {
5785 let mut e = editor_with("abcde");
5786 run_keys(&mut e, "20l");
5789 assert_eq!(e.cursor(), (0, 4));
5790 }
5791
5792 #[test]
5793 fn count_h_clamps_at_col_zero() {
5794 let mut e = editor_with("abcde");
5795 e.jump_cursor(0, 3);
5796 run_keys(&mut e, "20h");
5797 assert_eq!(e.cursor(), (0, 0));
5798 }
5799
5800 #[test]
5801 fn dl_on_last_char_still_deletes_it() {
5802 let mut e = editor_with("ab");
5806 e.jump_cursor(0, 1);
5807 run_keys(&mut e, "dl");
5808 assert_eq!(e.buffer().lines()[0], "a");
5809 }
5810
5811 #[test]
5812 fn case_op_preserves_yank_register() {
5813 let mut e = editor_with("target");
5814 run_keys(&mut e, "yy");
5815 let yank_before = e.yank().to_string();
5816 run_keys(&mut e, "gUU");
5818 assert_eq!(e.buffer().lines()[0], "TARGET");
5819 assert_eq!(
5820 e.yank(),
5821 yank_before,
5822 "case ops must preserve the yank buffer"
5823 );
5824 }
5825
5826 #[test]
5827 fn dap_deletes_paragraph() {
5828 let mut e = editor_with("a\nb\n\nc\nd");
5829 run_keys(&mut e, "dap");
5830 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
5831 }
5832
5833 #[test]
5834 fn dit_deletes_inner_tag_content() {
5835 let mut e = editor_with("<b>hello</b>");
5836 e.jump_cursor(0, 4);
5838 run_keys(&mut e, "dit");
5839 assert_eq!(e.buffer().lines()[0], "<b></b>");
5840 }
5841
5842 #[test]
5843 fn dat_deletes_around_tag() {
5844 let mut e = editor_with("hi <b>foo</b> bye");
5845 e.jump_cursor(0, 6);
5846 run_keys(&mut e, "dat");
5847 assert_eq!(e.buffer().lines()[0], "hi bye");
5848 }
5849
5850 #[test]
5851 fn dit_picks_innermost_tag() {
5852 let mut e = editor_with("<a><b>x</b></a>");
5853 e.jump_cursor(0, 6);
5855 run_keys(&mut e, "dit");
5856 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
5858 }
5859
5860 #[test]
5861 fn dat_innermost_tag_pair() {
5862 let mut e = editor_with("<a><b>x</b></a>");
5863 e.jump_cursor(0, 6);
5864 run_keys(&mut e, "dat");
5865 assert_eq!(e.buffer().lines()[0], "<a></a>");
5866 }
5867
5868 #[test]
5869 fn dit_outside_any_tag_no_op() {
5870 let mut e = editor_with("plain text");
5871 e.jump_cursor(0, 3);
5872 run_keys(&mut e, "dit");
5873 assert_eq!(e.buffer().lines()[0], "plain text");
5875 }
5876
5877 #[test]
5878 fn cit_changes_inner_tag_content() {
5879 let mut e = editor_with("<b>hello</b>");
5880 e.jump_cursor(0, 4);
5881 run_keys(&mut e, "citNEW<Esc>");
5882 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
5883 }
5884
5885 #[test]
5886 fn cat_changes_around_tag() {
5887 let mut e = editor_with("hi <b>foo</b> bye");
5888 e.jump_cursor(0, 6);
5889 run_keys(&mut e, "catBAR<Esc>");
5890 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
5891 }
5892
5893 #[test]
5894 fn yit_yanks_inner_tag_content() {
5895 let mut e = editor_with("<b>hello</b>");
5896 e.jump_cursor(0, 4);
5897 run_keys(&mut e, "yit");
5898 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5899 }
5900
5901 #[test]
5902 fn yat_yanks_full_tag_pair() {
5903 let mut e = editor_with("hi <b>foo</b> bye");
5904 e.jump_cursor(0, 6);
5905 run_keys(&mut e, "yat");
5906 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5907 }
5908
5909 #[test]
5910 fn vit_visually_selects_inner_tag() {
5911 let mut e = editor_with("<b>hello</b>");
5912 e.jump_cursor(0, 4);
5913 run_keys(&mut e, "vit");
5914 assert_eq!(e.vim_mode(), VimMode::Visual);
5915 run_keys(&mut e, "y");
5916 assert_eq!(e.registers().read('"').unwrap().text, "hello");
5917 }
5918
5919 #[test]
5920 fn vat_visually_selects_around_tag() {
5921 let mut e = editor_with("x<b>foo</b>y");
5922 e.jump_cursor(0, 5);
5923 run_keys(&mut e, "vat");
5924 assert_eq!(e.vim_mode(), VimMode::Visual);
5925 run_keys(&mut e, "y");
5926 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
5927 }
5928
5929 #[test]
5932 #[allow(non_snake_case)]
5933 fn diW_deletes_inner_big_word() {
5934 let mut e = editor_with("foo.bar baz");
5935 e.jump_cursor(0, 2);
5936 run_keys(&mut e, "diW");
5937 assert_eq!(e.buffer().lines()[0], " baz");
5939 }
5940
5941 #[test]
5942 #[allow(non_snake_case)]
5943 fn daW_deletes_around_big_word() {
5944 let mut e = editor_with("foo.bar baz");
5945 e.jump_cursor(0, 2);
5946 run_keys(&mut e, "daW");
5947 assert_eq!(e.buffer().lines()[0], "baz");
5948 }
5949
5950 #[test]
5951 fn di_double_quote_deletes_inside() {
5952 let mut e = editor_with("a \"hello\" b");
5953 e.jump_cursor(0, 4);
5954 run_keys(&mut e, "di\"");
5955 assert_eq!(e.buffer().lines()[0], "a \"\" b");
5956 }
5957
5958 #[test]
5959 fn da_double_quote_deletes_around() {
5960 let mut e = editor_with("a \"hello\" b");
5961 e.jump_cursor(0, 4);
5962 run_keys(&mut e, "da\"");
5963 assert_eq!(e.buffer().lines()[0], "a b");
5964 }
5965
5966 #[test]
5967 fn di_single_quote_deletes_inside() {
5968 let mut e = editor_with("x 'foo' y");
5969 e.jump_cursor(0, 4);
5970 run_keys(&mut e, "di'");
5971 assert_eq!(e.buffer().lines()[0], "x '' y");
5972 }
5973
5974 #[test]
5975 fn da_single_quote_deletes_around() {
5976 let mut e = editor_with("x 'foo' y");
5977 e.jump_cursor(0, 4);
5978 run_keys(&mut e, "da'");
5979 assert_eq!(e.buffer().lines()[0], "x y");
5980 }
5981
5982 #[test]
5983 fn di_backtick_deletes_inside() {
5984 let mut e = editor_with("p `q` r");
5985 e.jump_cursor(0, 3);
5986 run_keys(&mut e, "di`");
5987 assert_eq!(e.buffer().lines()[0], "p `` r");
5988 }
5989
5990 #[test]
5991 fn da_backtick_deletes_around() {
5992 let mut e = editor_with("p `q` r");
5993 e.jump_cursor(0, 3);
5994 run_keys(&mut e, "da`");
5995 assert_eq!(e.buffer().lines()[0], "p r");
5996 }
5997
5998 #[test]
5999 fn di_paren_deletes_inside() {
6000 let mut e = editor_with("f(arg)");
6001 e.jump_cursor(0, 3);
6002 run_keys(&mut e, "di(");
6003 assert_eq!(e.buffer().lines()[0], "f()");
6004 }
6005
6006 #[test]
6007 fn di_paren_alias_b_works() {
6008 let mut e = editor_with("f(arg)");
6009 e.jump_cursor(0, 3);
6010 run_keys(&mut e, "dib");
6011 assert_eq!(e.buffer().lines()[0], "f()");
6012 }
6013
6014 #[test]
6015 fn di_bracket_deletes_inside() {
6016 let mut e = editor_with("a[b,c]d");
6017 e.jump_cursor(0, 3);
6018 run_keys(&mut e, "di[");
6019 assert_eq!(e.buffer().lines()[0], "a[]d");
6020 }
6021
6022 #[test]
6023 fn da_bracket_deletes_around() {
6024 let mut e = editor_with("a[b,c]d");
6025 e.jump_cursor(0, 3);
6026 run_keys(&mut e, "da[");
6027 assert_eq!(e.buffer().lines()[0], "ad");
6028 }
6029
6030 #[test]
6031 fn di_brace_deletes_inside() {
6032 let mut e = editor_with("x{y}z");
6033 e.jump_cursor(0, 2);
6034 run_keys(&mut e, "di{");
6035 assert_eq!(e.buffer().lines()[0], "x{}z");
6036 }
6037
6038 #[test]
6039 fn da_brace_deletes_around() {
6040 let mut e = editor_with("x{y}z");
6041 e.jump_cursor(0, 2);
6042 run_keys(&mut e, "da{");
6043 assert_eq!(e.buffer().lines()[0], "xz");
6044 }
6045
6046 #[test]
6047 fn di_brace_alias_capital_b_works() {
6048 let mut e = editor_with("x{y}z");
6049 e.jump_cursor(0, 2);
6050 run_keys(&mut e, "diB");
6051 assert_eq!(e.buffer().lines()[0], "x{}z");
6052 }
6053
6054 #[test]
6055 fn di_angle_deletes_inside() {
6056 let mut e = editor_with("p<q>r");
6057 e.jump_cursor(0, 2);
6058 run_keys(&mut e, "di<lt>");
6060 assert_eq!(e.buffer().lines()[0], "p<>r");
6061 }
6062
6063 #[test]
6064 fn da_angle_deletes_around() {
6065 let mut e = editor_with("p<q>r");
6066 e.jump_cursor(0, 2);
6067 run_keys(&mut e, "da<lt>");
6068 assert_eq!(e.buffer().lines()[0], "pr");
6069 }
6070
6071 #[test]
6072 fn dip_deletes_inner_paragraph() {
6073 let mut e = editor_with("a\nb\nc\n\nd");
6074 e.jump_cursor(1, 0);
6075 run_keys(&mut e, "dip");
6076 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6079 }
6080
6081 #[test]
6084 fn sentence_motion_close_paren_jumps_forward() {
6085 let mut e = editor_with("Alpha. Beta. Gamma.");
6086 e.jump_cursor(0, 0);
6087 run_keys(&mut e, ")");
6088 assert_eq!(e.cursor(), (0, 7));
6090 run_keys(&mut e, ")");
6091 assert_eq!(e.cursor(), (0, 13));
6092 }
6093
6094 #[test]
6095 fn sentence_motion_open_paren_jumps_backward() {
6096 let mut e = editor_with("Alpha. Beta. Gamma.");
6097 e.jump_cursor(0, 13);
6098 run_keys(&mut e, "(");
6099 assert_eq!(e.cursor(), (0, 7));
6102 run_keys(&mut e, "(");
6103 assert_eq!(e.cursor(), (0, 0));
6104 }
6105
6106 #[test]
6107 fn sentence_motion_count() {
6108 let mut e = editor_with("A. B. C. D.");
6109 e.jump_cursor(0, 0);
6110 run_keys(&mut e, "3)");
6111 assert_eq!(e.cursor(), (0, 9));
6113 }
6114
6115 #[test]
6116 fn dis_deletes_inner_sentence() {
6117 let mut e = editor_with("First one. Second one. Third one.");
6118 e.jump_cursor(0, 13);
6119 run_keys(&mut e, "dis");
6120 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6122 }
6123
6124 #[test]
6125 fn das_deletes_around_sentence_with_trailing_space() {
6126 let mut e = editor_with("Alpha. Beta. Gamma.");
6127 e.jump_cursor(0, 8);
6128 run_keys(&mut e, "das");
6129 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6132 }
6133
6134 #[test]
6135 fn dis_handles_double_terminator() {
6136 let mut e = editor_with("Wow!? Next.");
6137 e.jump_cursor(0, 1);
6138 run_keys(&mut e, "dis");
6139 assert_eq!(e.buffer().lines()[0], " Next.");
6142 }
6143
6144 #[test]
6145 fn dis_first_sentence_from_cursor_at_zero() {
6146 let mut e = editor_with("Alpha. Beta.");
6147 e.jump_cursor(0, 0);
6148 run_keys(&mut e, "dis");
6149 assert_eq!(e.buffer().lines()[0], " Beta.");
6150 }
6151
6152 #[test]
6153 fn yis_yanks_inner_sentence() {
6154 let mut e = editor_with("Hello world. Bye.");
6155 e.jump_cursor(0, 5);
6156 run_keys(&mut e, "yis");
6157 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6158 }
6159
6160 #[test]
6161 fn vis_visually_selects_inner_sentence() {
6162 let mut e = editor_with("First. Second.");
6163 e.jump_cursor(0, 1);
6164 run_keys(&mut e, "vis");
6165 assert_eq!(e.vim_mode(), VimMode::Visual);
6166 run_keys(&mut e, "y");
6167 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6168 }
6169
6170 #[test]
6171 fn ciw_changes_inner_word() {
6172 let mut e = editor_with("hello world");
6173 e.jump_cursor(0, 1);
6174 run_keys(&mut e, "ciwHEY<Esc>");
6175 assert_eq!(e.buffer().lines()[0], "HEY world");
6176 }
6177
6178 #[test]
6179 fn yiw_yanks_inner_word() {
6180 let mut e = editor_with("hello world");
6181 e.jump_cursor(0, 1);
6182 run_keys(&mut e, "yiw");
6183 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6184 }
6185
6186 #[test]
6187 fn viw_selects_inner_word() {
6188 let mut e = editor_with("hello world");
6189 e.jump_cursor(0, 2);
6190 run_keys(&mut e, "viw");
6191 assert_eq!(e.vim_mode(), VimMode::Visual);
6192 run_keys(&mut e, "y");
6193 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6194 }
6195
6196 #[test]
6197 fn ci_paren_changes_inside() {
6198 let mut e = editor_with("f(old)");
6199 e.jump_cursor(0, 3);
6200 run_keys(&mut e, "ci(NEW<Esc>");
6201 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6202 }
6203
6204 #[test]
6205 fn yi_double_quote_yanks_inside() {
6206 let mut e = editor_with("say \"hi there\" then");
6207 e.jump_cursor(0, 6);
6208 run_keys(&mut e, "yi\"");
6209 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6210 }
6211
6212 #[test]
6213 fn vap_visual_selects_around_paragraph() {
6214 let mut e = editor_with("a\nb\n\nc");
6215 e.jump_cursor(0, 0);
6216 run_keys(&mut e, "vap");
6217 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6218 run_keys(&mut e, "y");
6219 let text = e.registers().read('"').unwrap().text.clone();
6221 assert!(text.starts_with("a\nb"));
6222 }
6223
6224 #[test]
6225 fn star_finds_next_occurrence() {
6226 let mut e = editor_with("foo bar foo baz");
6227 run_keys(&mut e, "*");
6228 assert_eq!(e.cursor().1, 8);
6229 }
6230
6231 #[test]
6232 fn star_skips_substring_match() {
6233 let mut e = editor_with("foo foobar baz");
6236 run_keys(&mut e, "*");
6237 assert_eq!(e.cursor().1, 0);
6238 }
6239
6240 #[test]
6241 fn g_star_matches_substring() {
6242 let mut e = editor_with("foo foobar baz");
6245 run_keys(&mut e, "g*");
6246 assert_eq!(e.cursor().1, 4);
6247 }
6248
6249 #[test]
6250 fn g_pound_matches_substring_backward() {
6251 let mut e = editor_with("foo foobar baz foo");
6254 run_keys(&mut e, "$b");
6255 assert_eq!(e.cursor().1, 15);
6256 run_keys(&mut e, "g#");
6257 assert_eq!(e.cursor().1, 4);
6258 }
6259
6260 #[test]
6261 fn n_repeats_last_search_forward() {
6262 let mut e = editor_with("foo bar foo baz foo");
6263 run_keys(&mut e, "/foo<CR>");
6266 assert_eq!(e.cursor().1, 8);
6267 run_keys(&mut e, "n");
6268 assert_eq!(e.cursor().1, 16);
6269 }
6270
6271 #[test]
6272 fn shift_n_reverses_search() {
6273 let mut e = editor_with("foo bar foo baz foo");
6274 run_keys(&mut e, "/foo<CR>");
6275 run_keys(&mut e, "n");
6276 assert_eq!(e.cursor().1, 16);
6277 run_keys(&mut e, "N");
6278 assert_eq!(e.cursor().1, 8);
6279 }
6280
6281 #[test]
6282 fn n_noop_without_pattern() {
6283 let mut e = editor_with("foo bar");
6284 run_keys(&mut e, "n");
6285 assert_eq!(e.cursor(), (0, 0));
6286 }
6287
6288 #[test]
6289 fn visual_line_preserves_cursor_column() {
6290 let mut e = editor_with("hello world\nanother one\nbye");
6293 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6295 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6296 assert_eq!(e.cursor(), (0, 5));
6297 run_keys(&mut e, "j");
6298 assert_eq!(e.cursor(), (1, 5));
6299 }
6300
6301 #[test]
6302 fn visual_line_yank_includes_trailing_newline() {
6303 let mut e = editor_with("aaa\nbbb\nccc");
6304 run_keys(&mut e, "Vjy");
6305 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6307 }
6308
6309 #[test]
6310 fn visual_line_yank_last_line_trailing_newline() {
6311 let mut e = editor_with("aaa\nbbb\nccc");
6312 run_keys(&mut e, "jj");
6314 run_keys(&mut e, "Vy");
6315 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6316 }
6317
6318 #[test]
6319 fn yy_on_last_line_has_trailing_newline() {
6320 let mut e = editor_with("aaa\nbbb\nccc");
6321 run_keys(&mut e, "jj");
6322 run_keys(&mut e, "yy");
6323 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6324 }
6325
6326 #[test]
6327 fn yy_in_middle_has_trailing_newline() {
6328 let mut e = editor_with("aaa\nbbb\nccc");
6329 run_keys(&mut e, "j");
6330 run_keys(&mut e, "yy");
6331 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6332 }
6333
6334 #[test]
6335 fn di_single_quote() {
6336 let mut e = editor_with("say 'hello world' now");
6337 e.jump_cursor(0, 7);
6338 run_keys(&mut e, "di'");
6339 assert_eq!(e.buffer().lines()[0], "say '' now");
6340 }
6341
6342 #[test]
6343 fn da_single_quote() {
6344 let mut e = editor_with("say 'hello' now");
6345 e.jump_cursor(0, 7);
6346 run_keys(&mut e, "da'");
6347 assert_eq!(e.buffer().lines()[0], "say now");
6348 }
6349
6350 #[test]
6351 fn di_backtick() {
6352 let mut e = editor_with("say `hi` now");
6353 e.jump_cursor(0, 5);
6354 run_keys(&mut e, "di`");
6355 assert_eq!(e.buffer().lines()[0], "say `` now");
6356 }
6357
6358 #[test]
6359 fn di_brace() {
6360 let mut e = editor_with("fn { a; b; c }");
6361 e.jump_cursor(0, 7);
6362 run_keys(&mut e, "di{");
6363 assert_eq!(e.buffer().lines()[0], "fn {}");
6364 }
6365
6366 #[test]
6367 fn di_bracket() {
6368 let mut e = editor_with("arr[1, 2, 3]");
6369 e.jump_cursor(0, 5);
6370 run_keys(&mut e, "di[");
6371 assert_eq!(e.buffer().lines()[0], "arr[]");
6372 }
6373
6374 #[test]
6375 fn dab_deletes_around_paren() {
6376 let mut e = editor_with("fn(a, b) + 1");
6377 e.jump_cursor(0, 4);
6378 run_keys(&mut e, "dab");
6379 assert_eq!(e.buffer().lines()[0], "fn + 1");
6380 }
6381
6382 #[test]
6383 fn da_big_b_deletes_around_brace() {
6384 let mut e = editor_with("x = {a: 1}");
6385 e.jump_cursor(0, 6);
6386 run_keys(&mut e, "daB");
6387 assert_eq!(e.buffer().lines()[0], "x = ");
6388 }
6389
6390 #[test]
6391 fn di_big_w_deletes_bigword() {
6392 let mut e = editor_with("foo-bar baz");
6393 e.jump_cursor(0, 2);
6394 run_keys(&mut e, "diW");
6395 assert_eq!(e.buffer().lines()[0], " baz");
6396 }
6397
6398 #[test]
6399 fn visual_select_inner_word() {
6400 let mut e = editor_with("hello world");
6401 e.jump_cursor(0, 2);
6402 run_keys(&mut e, "viw");
6403 assert_eq!(e.vim_mode(), VimMode::Visual);
6404 run_keys(&mut e, "y");
6405 assert_eq!(e.last_yank.as_deref(), Some("hello"));
6406 }
6407
6408 #[test]
6409 fn visual_select_inner_quote() {
6410 let mut e = editor_with("foo \"bar\" baz");
6411 e.jump_cursor(0, 6);
6412 run_keys(&mut e, "vi\"");
6413 run_keys(&mut e, "y");
6414 assert_eq!(e.last_yank.as_deref(), Some("bar"));
6415 }
6416
6417 #[test]
6418 fn visual_select_inner_paren() {
6419 let mut e = editor_with("fn(a, b)");
6420 e.jump_cursor(0, 4);
6421 run_keys(&mut e, "vi(");
6422 run_keys(&mut e, "y");
6423 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
6424 }
6425
6426 #[test]
6427 fn visual_select_outer_brace() {
6428 let mut e = editor_with("{x}");
6429 e.jump_cursor(0, 1);
6430 run_keys(&mut e, "va{");
6431 run_keys(&mut e, "y");
6432 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
6433 }
6434
6435 #[test]
6436 fn caw_changes_word_with_trailing_space() {
6437 let mut e = editor_with("hello world");
6438 run_keys(&mut e, "cawfoo<Esc>");
6439 assert_eq!(e.buffer().lines()[0], "fooworld");
6440 }
6441
6442 #[test]
6443 fn visual_char_yank_preserves_raw_text() {
6444 let mut e = editor_with("hello world");
6445 run_keys(&mut e, "vllly");
6446 assert_eq!(e.last_yank.as_deref(), Some("hell"));
6447 }
6448
6449 #[test]
6450 fn single_line_visual_line_selects_full_line_on_yank() {
6451 let mut e = editor_with("hello world\nbye");
6452 run_keys(&mut e, "V");
6453 run_keys(&mut e, "y");
6456 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
6457 }
6458
6459 #[test]
6460 fn visual_line_extends_both_directions() {
6461 let mut e = editor_with("aaa\nbbb\nccc\nddd");
6462 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
6464 assert_eq!(e.cursor(), (3, 0));
6465 run_keys(&mut e, "k");
6466 assert_eq!(e.cursor(), (2, 0));
6468 run_keys(&mut e, "k");
6469 assert_eq!(e.cursor(), (1, 0));
6470 }
6471
6472 #[test]
6473 fn visual_char_preserves_cursor_column() {
6474 let mut e = editor_with("hello world");
6475 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
6477 assert_eq!(e.cursor(), (0, 5));
6478 run_keys(&mut e, "ll");
6479 assert_eq!(e.cursor(), (0, 7));
6480 }
6481
6482 #[test]
6483 fn visual_char_highlight_bounds_order() {
6484 let mut e = editor_with("abcdef");
6485 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
6487 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
6490 }
6491
6492 #[test]
6493 fn visual_line_highlight_bounds() {
6494 let mut e = editor_with("a\nb\nc");
6495 run_keys(&mut e, "V");
6496 assert_eq!(e.line_highlight(), Some((0, 0)));
6497 run_keys(&mut e, "j");
6498 assert_eq!(e.line_highlight(), Some((0, 1)));
6499 run_keys(&mut e, "j");
6500 assert_eq!(e.line_highlight(), Some((0, 2)));
6501 }
6502
6503 #[test]
6506 fn h_moves_left() {
6507 let mut e = editor_with("hello");
6508 e.jump_cursor(0, 3);
6509 run_keys(&mut e, "h");
6510 assert_eq!(e.cursor(), (0, 2));
6511 }
6512
6513 #[test]
6514 fn l_moves_right() {
6515 let mut e = editor_with("hello");
6516 run_keys(&mut e, "l");
6517 assert_eq!(e.cursor(), (0, 1));
6518 }
6519
6520 #[test]
6521 fn k_moves_up() {
6522 let mut e = editor_with("a\nb\nc");
6523 e.jump_cursor(2, 0);
6524 run_keys(&mut e, "k");
6525 assert_eq!(e.cursor(), (1, 0));
6526 }
6527
6528 #[test]
6529 fn zero_moves_to_line_start() {
6530 let mut e = editor_with(" hello");
6531 run_keys(&mut e, "$");
6532 run_keys(&mut e, "0");
6533 assert_eq!(e.cursor().1, 0);
6534 }
6535
6536 #[test]
6537 fn caret_moves_to_first_non_blank() {
6538 let mut e = editor_with(" hello");
6539 run_keys(&mut e, "0");
6540 run_keys(&mut e, "^");
6541 assert_eq!(e.cursor().1, 4);
6542 }
6543
6544 #[test]
6545 fn dollar_moves_to_last_char() {
6546 let mut e = editor_with("hello");
6547 run_keys(&mut e, "$");
6548 assert_eq!(e.cursor().1, 4);
6549 }
6550
6551 #[test]
6552 fn dollar_on_empty_line_stays_at_col_zero() {
6553 let mut e = editor_with("");
6554 run_keys(&mut e, "$");
6555 assert_eq!(e.cursor().1, 0);
6556 }
6557
6558 #[test]
6559 fn w_jumps_to_next_word() {
6560 let mut e = editor_with("foo bar baz");
6561 run_keys(&mut e, "w");
6562 assert_eq!(e.cursor().1, 4);
6563 }
6564
6565 #[test]
6566 fn b_jumps_back_a_word() {
6567 let mut e = editor_with("foo bar");
6568 e.jump_cursor(0, 6);
6569 run_keys(&mut e, "b");
6570 assert_eq!(e.cursor().1, 4);
6571 }
6572
6573 #[test]
6574 fn e_jumps_to_word_end() {
6575 let mut e = editor_with("foo bar");
6576 run_keys(&mut e, "e");
6577 assert_eq!(e.cursor().1, 2);
6578 }
6579
6580 #[test]
6583 fn d_dollar_deletes_to_eol() {
6584 let mut e = editor_with("hello world");
6585 e.jump_cursor(0, 5);
6586 run_keys(&mut e, "d$");
6587 assert_eq!(e.buffer().lines()[0], "hello");
6588 }
6589
6590 #[test]
6591 fn d_zero_deletes_to_line_start() {
6592 let mut e = editor_with("hello world");
6593 e.jump_cursor(0, 6);
6594 run_keys(&mut e, "d0");
6595 assert_eq!(e.buffer().lines()[0], "world");
6596 }
6597
6598 #[test]
6599 fn d_caret_deletes_to_first_non_blank() {
6600 let mut e = editor_with(" hello");
6601 e.jump_cursor(0, 6);
6602 run_keys(&mut e, "d^");
6603 assert_eq!(e.buffer().lines()[0], " llo");
6604 }
6605
6606 #[test]
6607 fn d_capital_g_deletes_to_end_of_file() {
6608 let mut e = editor_with("a\nb\nc\nd");
6609 e.jump_cursor(1, 0);
6610 run_keys(&mut e, "dG");
6611 assert_eq!(e.buffer().lines(), &["a".to_string()]);
6612 }
6613
6614 #[test]
6615 fn d_gg_deletes_to_start_of_file() {
6616 let mut e = editor_with("a\nb\nc\nd");
6617 e.jump_cursor(2, 0);
6618 run_keys(&mut e, "dgg");
6619 assert_eq!(e.buffer().lines(), &["d".to_string()]);
6620 }
6621
6622 #[test]
6623 fn cw_is_ce_quirk() {
6624 let mut e = editor_with("foo bar");
6627 run_keys(&mut e, "cwxyz<Esc>");
6628 assert_eq!(e.buffer().lines()[0], "xyz bar");
6629 }
6630
6631 #[test]
6634 fn big_d_deletes_to_eol() {
6635 let mut e = editor_with("hello world");
6636 e.jump_cursor(0, 5);
6637 run_keys(&mut e, "D");
6638 assert_eq!(e.buffer().lines()[0], "hello");
6639 }
6640
6641 #[test]
6642 fn big_c_deletes_to_eol_and_inserts() {
6643 let mut e = editor_with("hello world");
6644 e.jump_cursor(0, 5);
6645 run_keys(&mut e, "C!<Esc>");
6646 assert_eq!(e.buffer().lines()[0], "hello!");
6647 }
6648
6649 #[test]
6650 fn j_joins_next_line_with_space() {
6651 let mut e = editor_with("hello\nworld");
6652 run_keys(&mut e, "J");
6653 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6654 }
6655
6656 #[test]
6657 fn j_strips_leading_whitespace_on_join() {
6658 let mut e = editor_with("hello\n world");
6659 run_keys(&mut e, "J");
6660 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
6661 }
6662
6663 #[test]
6664 fn big_x_deletes_char_before_cursor() {
6665 let mut e = editor_with("hello");
6666 e.jump_cursor(0, 3);
6667 run_keys(&mut e, "X");
6668 assert_eq!(e.buffer().lines()[0], "helo");
6669 }
6670
6671 #[test]
6672 fn s_substitutes_char_and_enters_insert() {
6673 let mut e = editor_with("hello");
6674 run_keys(&mut e, "sX<Esc>");
6675 assert_eq!(e.buffer().lines()[0], "Xello");
6676 }
6677
6678 #[test]
6679 fn count_x_deletes_many() {
6680 let mut e = editor_with("abcdef");
6681 run_keys(&mut e, "3x");
6682 assert_eq!(e.buffer().lines()[0], "def");
6683 }
6684
6685 #[test]
6688 fn p_pastes_charwise_after_cursor() {
6689 let mut e = editor_with("hello");
6690 run_keys(&mut e, "yw");
6691 run_keys(&mut e, "$p");
6692 assert_eq!(e.buffer().lines()[0], "hellohello");
6693 }
6694
6695 #[test]
6696 fn capital_p_pastes_charwise_before_cursor() {
6697 let mut e = editor_with("hello");
6698 run_keys(&mut e, "v");
6700 run_keys(&mut e, "l");
6701 run_keys(&mut e, "y");
6702 run_keys(&mut e, "$P");
6703 assert_eq!(e.buffer().lines()[0], "hellheo");
6706 }
6707
6708 #[test]
6709 fn p_pastes_linewise_below() {
6710 let mut e = editor_with("one\ntwo\nthree");
6711 run_keys(&mut e, "yy");
6712 run_keys(&mut e, "p");
6713 assert_eq!(
6714 e.buffer().lines(),
6715 &[
6716 "one".to_string(),
6717 "one".to_string(),
6718 "two".to_string(),
6719 "three".to_string()
6720 ]
6721 );
6722 }
6723
6724 #[test]
6725 fn capital_p_pastes_linewise_above() {
6726 let mut e = editor_with("one\ntwo");
6727 e.jump_cursor(1, 0);
6728 run_keys(&mut e, "yy");
6729 run_keys(&mut e, "P");
6730 assert_eq!(
6731 e.buffer().lines(),
6732 &["one".to_string(), "two".to_string(), "two".to_string()]
6733 );
6734 }
6735
6736 #[test]
6739 fn hash_finds_previous_occurrence() {
6740 let mut e = editor_with("foo bar foo baz foo");
6741 e.jump_cursor(0, 16);
6743 run_keys(&mut e, "#");
6744 assert_eq!(e.cursor().1, 8);
6745 }
6746
6747 #[test]
6750 fn visual_line_delete_removes_full_lines() {
6751 let mut e = editor_with("a\nb\nc\nd");
6752 run_keys(&mut e, "Vjd");
6753 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
6754 }
6755
6756 #[test]
6757 fn visual_line_change_leaves_blank_line() {
6758 let mut e = editor_with("a\nb\nc");
6759 run_keys(&mut e, "Vjc");
6760 assert_eq!(e.vim_mode(), VimMode::Insert);
6761 run_keys(&mut e, "X<Esc>");
6762 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
6766 }
6767
6768 #[test]
6769 fn cc_leaves_blank_line() {
6770 let mut e = editor_with("a\nb\nc");
6771 e.jump_cursor(1, 0);
6772 run_keys(&mut e, "ccX<Esc>");
6773 assert_eq!(
6774 e.buffer().lines(),
6775 &["a".to_string(), "X".to_string(), "c".to_string()]
6776 );
6777 }
6778
6779 #[test]
6784 fn big_w_skips_hyphens() {
6785 let mut e = editor_with("foo-bar baz");
6787 run_keys(&mut e, "W");
6788 assert_eq!(e.cursor().1, 8);
6789 }
6790
6791 #[test]
6792 fn big_w_crosses_lines() {
6793 let mut e = editor_with("foo-bar\nbaz-qux");
6794 run_keys(&mut e, "W");
6795 assert_eq!(e.cursor(), (1, 0));
6796 }
6797
6798 #[test]
6799 fn big_b_skips_hyphens() {
6800 let mut e = editor_with("foo-bar baz");
6801 e.jump_cursor(0, 9);
6802 run_keys(&mut e, "B");
6803 assert_eq!(e.cursor().1, 8);
6804 run_keys(&mut e, "B");
6805 assert_eq!(e.cursor().1, 0);
6806 }
6807
6808 #[test]
6809 fn big_e_jumps_to_big_word_end() {
6810 let mut e = editor_with("foo-bar baz");
6811 run_keys(&mut e, "E");
6812 assert_eq!(e.cursor().1, 6);
6813 run_keys(&mut e, "E");
6814 assert_eq!(e.cursor().1, 10);
6815 }
6816
6817 #[test]
6818 fn dw_with_big_word_variant() {
6819 let mut e = editor_with("foo-bar baz");
6821 run_keys(&mut e, "dW");
6822 assert_eq!(e.buffer().lines()[0], "baz");
6823 }
6824
6825 #[test]
6828 fn insert_ctrl_w_deletes_word_back() {
6829 let mut e = editor_with("");
6830 run_keys(&mut e, "i");
6831 for c in "hello world".chars() {
6832 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6833 }
6834 run_keys(&mut e, "<C-w>");
6835 assert_eq!(e.buffer().lines()[0], "hello ");
6836 }
6837
6838 #[test]
6839 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
6840 let mut e = editor_with("hello\nworld");
6844 e.jump_cursor(1, 0);
6845 run_keys(&mut e, "i");
6846 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6847 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
6850 assert_eq!(e.cursor(), (0, 0));
6851 }
6852
6853 #[test]
6854 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
6855 let mut e = editor_with("foo bar\nbaz");
6856 e.jump_cursor(1, 0);
6857 run_keys(&mut e, "i");
6858 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
6859 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
6861 assert_eq!(e.cursor(), (0, 4));
6862 }
6863
6864 #[test]
6865 fn insert_ctrl_u_deletes_to_line_start() {
6866 let mut e = editor_with("");
6867 run_keys(&mut e, "i");
6868 for c in "hello world".chars() {
6869 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6870 }
6871 run_keys(&mut e, "<C-u>");
6872 assert_eq!(e.buffer().lines()[0], "");
6873 }
6874
6875 #[test]
6876 fn insert_ctrl_o_runs_one_normal_command() {
6877 let mut e = editor_with("hello world");
6878 run_keys(&mut e, "A");
6880 assert_eq!(e.vim_mode(), VimMode::Insert);
6881 e.jump_cursor(0, 0);
6883 run_keys(&mut e, "<C-o>");
6884 assert_eq!(e.vim_mode(), VimMode::Normal);
6885 run_keys(&mut e, "dw");
6886 assert_eq!(e.vim_mode(), VimMode::Insert);
6888 assert_eq!(e.buffer().lines()[0], "world");
6889 }
6890
6891 #[test]
6894 fn j_through_empty_line_preserves_column() {
6895 let mut e = editor_with("hello world\n\nanother line");
6896 run_keys(&mut e, "llllll");
6898 assert_eq!(e.cursor(), (0, 6));
6899 run_keys(&mut e, "j");
6902 assert_eq!(e.cursor(), (1, 0));
6903 run_keys(&mut e, "j");
6905 assert_eq!(e.cursor(), (2, 6));
6906 }
6907
6908 #[test]
6909 fn j_through_shorter_line_preserves_column() {
6910 let mut e = editor_with("hello world\nhi\nanother line");
6911 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
6914 run_keys(&mut e, "j");
6915 assert_eq!(e.cursor(), (2, 7));
6916 }
6917
6918 #[test]
6919 fn esc_from_insert_sticky_matches_visible_cursor() {
6920 let mut e = editor_with(" this is a line\n another one of a similar size");
6924 e.jump_cursor(0, 12);
6925 run_keys(&mut e, "I");
6926 assert_eq!(e.cursor(), (0, 4));
6927 run_keys(&mut e, "X<Esc>");
6928 assert_eq!(e.cursor(), (0, 4));
6929 run_keys(&mut e, "j");
6930 assert_eq!(e.cursor(), (1, 4));
6931 }
6932
6933 #[test]
6934 fn esc_from_insert_sticky_tracks_inserted_chars() {
6935 let mut e = editor_with("xxxxxxx\nyyyyyyy");
6936 run_keys(&mut e, "i");
6937 run_keys(&mut e, "abc<Esc>");
6938 assert_eq!(e.cursor(), (0, 2));
6939 run_keys(&mut e, "j");
6940 assert_eq!(e.cursor(), (1, 2));
6941 }
6942
6943 #[test]
6944 fn esc_from_insert_sticky_tracks_arrow_nav() {
6945 let mut e = editor_with("xxxxxx\nyyyyyy");
6946 run_keys(&mut e, "i");
6947 run_keys(&mut e, "abc");
6948 for _ in 0..2 {
6949 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
6950 }
6951 run_keys(&mut e, "<Esc>");
6952 assert_eq!(e.cursor(), (0, 0));
6953 run_keys(&mut e, "j");
6954 assert_eq!(e.cursor(), (1, 0));
6955 }
6956
6957 #[test]
6958 fn esc_from_insert_at_col_14_followed_by_j() {
6959 let line = "x".repeat(30);
6962 let buf = format!("{line}\n{line}");
6963 let mut e = editor_with(&buf);
6964 e.jump_cursor(0, 14);
6965 run_keys(&mut e, "i");
6966 for c in "test ".chars() {
6967 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
6968 }
6969 run_keys(&mut e, "<Esc>");
6970 assert_eq!(e.cursor(), (0, 18));
6971 run_keys(&mut e, "j");
6972 assert_eq!(e.cursor(), (1, 18));
6973 }
6974
6975 #[test]
6976 fn linewise_paste_resets_sticky_column() {
6977 let mut e = editor_with(" hello\naaaaaaaa\nbye");
6981 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
6983 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
6987 run_keys(&mut e, "j");
6989 assert_eq!(e.cursor(), (3, 2));
6990 }
6991
6992 #[test]
6993 fn horizontal_motion_resyncs_sticky_column() {
6994 let mut e = editor_with("hello world\n\nanother line");
6998 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7001 assert_eq!(e.cursor(), (2, 3));
7002 }
7003
7004 #[test]
7007 fn ctrl_v_enters_visual_block() {
7008 let mut e = editor_with("aaa\nbbb\nccc");
7009 run_keys(&mut e, "<C-v>");
7010 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7011 }
7012
7013 #[test]
7014 fn visual_block_esc_returns_to_normal() {
7015 let mut e = editor_with("aaa\nbbb\nccc");
7016 run_keys(&mut e, "<C-v>");
7017 run_keys(&mut e, "<Esc>");
7018 assert_eq!(e.vim_mode(), VimMode::Normal);
7019 }
7020
7021 #[test]
7022 fn visual_block_delete_removes_column_range() {
7023 let mut e = editor_with("hello\nworld\nhappy");
7024 run_keys(&mut e, "l");
7026 run_keys(&mut e, "<C-v>");
7027 run_keys(&mut e, "jj");
7028 run_keys(&mut e, "ll");
7029 run_keys(&mut e, "d");
7030 assert_eq!(
7032 e.buffer().lines(),
7033 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7034 );
7035 }
7036
7037 #[test]
7038 fn visual_block_yank_joins_with_newlines() {
7039 let mut e = editor_with("hello\nworld\nhappy");
7040 run_keys(&mut e, "<C-v>");
7041 run_keys(&mut e, "jj");
7042 run_keys(&mut e, "ll");
7043 run_keys(&mut e, "y");
7044 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7045 }
7046
7047 #[test]
7048 fn visual_block_replace_fills_block() {
7049 let mut e = editor_with("hello\nworld\nhappy");
7050 run_keys(&mut e, "<C-v>");
7051 run_keys(&mut e, "jj");
7052 run_keys(&mut e, "ll");
7053 run_keys(&mut e, "rx");
7054 assert_eq!(
7055 e.buffer().lines(),
7056 &[
7057 "xxxlo".to_string(),
7058 "xxxld".to_string(),
7059 "xxxpy".to_string()
7060 ]
7061 );
7062 }
7063
7064 #[test]
7065 fn visual_block_insert_repeats_across_rows() {
7066 let mut e = editor_with("hello\nworld\nhappy");
7067 run_keys(&mut e, "<C-v>");
7068 run_keys(&mut e, "jj");
7069 run_keys(&mut e, "I");
7070 run_keys(&mut e, "# <Esc>");
7071 assert_eq!(
7072 e.buffer().lines(),
7073 &[
7074 "# hello".to_string(),
7075 "# world".to_string(),
7076 "# happy".to_string()
7077 ]
7078 );
7079 }
7080
7081 #[test]
7082 fn block_highlight_returns_none_outside_block_mode() {
7083 let mut e = editor_with("abc");
7084 assert!(e.block_highlight().is_none());
7085 run_keys(&mut e, "v");
7086 assert!(e.block_highlight().is_none());
7087 run_keys(&mut e, "<Esc>V");
7088 assert!(e.block_highlight().is_none());
7089 }
7090
7091 #[test]
7092 fn block_highlight_bounds_track_anchor_and_cursor() {
7093 let mut e = editor_with("aaaa\nbbbb\ncccc");
7094 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7096 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7099 }
7100
7101 #[test]
7102 fn visual_block_delete_handles_short_lines() {
7103 let mut e = editor_with("hello\nhi\nworld");
7105 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7107 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7109 assert_eq!(
7114 e.buffer().lines(),
7115 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7116 );
7117 }
7118
7119 #[test]
7120 fn visual_block_yank_pads_short_lines_with_empties() {
7121 let mut e = editor_with("hello\nhi\nworld");
7122 run_keys(&mut e, "l");
7123 run_keys(&mut e, "<C-v>");
7124 run_keys(&mut e, "jjll");
7125 run_keys(&mut e, "y");
7126 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7128 }
7129
7130 #[test]
7131 fn visual_block_replace_skips_past_eol() {
7132 let mut e = editor_with("ab\ncd\nef");
7135 run_keys(&mut e, "l");
7137 run_keys(&mut e, "<C-v>");
7138 run_keys(&mut e, "jjllllll");
7139 run_keys(&mut e, "rX");
7140 assert_eq!(
7143 e.buffer().lines(),
7144 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7145 );
7146 }
7147
7148 #[test]
7149 fn visual_block_with_empty_line_in_middle() {
7150 let mut e = editor_with("abcd\n\nefgh");
7151 run_keys(&mut e, "<C-v>");
7152 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7154 assert_eq!(
7157 e.buffer().lines(),
7158 &["d".to_string(), "".to_string(), "h".to_string()]
7159 );
7160 }
7161
7162 #[test]
7163 fn block_insert_pads_empty_lines_to_block_column() {
7164 let mut e = editor_with("this is a line\n\nthis is a line");
7167 e.jump_cursor(0, 3);
7168 run_keys(&mut e, "<C-v>");
7169 run_keys(&mut e, "jj");
7170 run_keys(&mut e, "I");
7171 run_keys(&mut e, "XX<Esc>");
7172 assert_eq!(
7173 e.buffer().lines(),
7174 &[
7175 "thiXXs is a line".to_string(),
7176 " XX".to_string(),
7177 "thiXXs is a line".to_string()
7178 ]
7179 );
7180 }
7181
7182 #[test]
7183 fn block_insert_pads_short_lines_to_block_column() {
7184 let mut e = editor_with("aaaaa\nbb\naaaaa");
7185 e.jump_cursor(0, 3);
7186 run_keys(&mut e, "<C-v>");
7187 run_keys(&mut e, "jj");
7188 run_keys(&mut e, "I");
7189 run_keys(&mut e, "Y<Esc>");
7190 assert_eq!(
7192 e.buffer().lines(),
7193 &[
7194 "aaaYaa".to_string(),
7195 "bb Y".to_string(),
7196 "aaaYaa".to_string()
7197 ]
7198 );
7199 }
7200
7201 #[test]
7202 fn visual_block_append_repeats_across_rows() {
7203 let mut e = editor_with("foo\nbar\nbaz");
7204 run_keys(&mut e, "<C-v>");
7205 run_keys(&mut e, "jj");
7206 run_keys(&mut e, "A");
7209 run_keys(&mut e, "!<Esc>");
7210 assert_eq!(
7211 e.buffer().lines(),
7212 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
7213 );
7214 }
7215
7216 #[test]
7219 fn slash_opens_forward_search_prompt() {
7220 let mut e = editor_with("hello world");
7221 run_keys(&mut e, "/");
7222 let p = e.search_prompt().expect("prompt should be active");
7223 assert!(p.text.is_empty());
7224 assert!(p.forward);
7225 }
7226
7227 #[test]
7228 fn question_opens_backward_search_prompt() {
7229 let mut e = editor_with("hello world");
7230 run_keys(&mut e, "?");
7231 let p = e.search_prompt().expect("prompt should be active");
7232 assert!(!p.forward);
7233 }
7234
7235 #[test]
7236 fn search_prompt_typing_updates_pattern_live() {
7237 let mut e = editor_with("foo bar\nbaz");
7238 run_keys(&mut e, "/bar");
7239 assert_eq!(e.search_prompt().unwrap().text, "bar");
7240 assert!(e.search_state().pattern.is_some());
7242 }
7243
7244 #[test]
7245 fn search_prompt_backspace_and_enter() {
7246 let mut e = editor_with("hello world\nagain");
7247 run_keys(&mut e, "/worlx");
7248 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
7249 assert_eq!(e.search_prompt().unwrap().text, "worl");
7250 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7251 assert!(e.search_prompt().is_none());
7253 assert_eq!(e.last_search(), Some("worl"));
7254 assert_eq!(e.cursor(), (0, 6));
7255 }
7256
7257 #[test]
7258 fn empty_search_prompt_enter_repeats_last_search() {
7259 let mut e = editor_with("foo bar foo baz foo");
7260 run_keys(&mut e, "/foo");
7261 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7262 assert_eq!(e.cursor().1, 8);
7263 run_keys(&mut e, "/");
7265 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7266 assert_eq!(e.cursor().1, 16);
7267 assert_eq!(e.last_search(), Some("foo"));
7268 }
7269
7270 #[test]
7271 fn search_history_records_committed_patterns() {
7272 let mut e = editor_with("alpha beta gamma");
7273 run_keys(&mut e, "/alpha<CR>");
7274 run_keys(&mut e, "/beta<CR>");
7275 let history = e.vim.search_history.clone();
7277 assert_eq!(history, vec!["alpha", "beta"]);
7278 }
7279
7280 #[test]
7281 fn search_history_dedupes_consecutive_repeats() {
7282 let mut e = editor_with("foo bar foo");
7283 run_keys(&mut e, "/foo<CR>");
7284 run_keys(&mut e, "/foo<CR>");
7285 run_keys(&mut e, "/bar<CR>");
7286 run_keys(&mut e, "/bar<CR>");
7287 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
7289 }
7290
7291 #[test]
7292 fn ctrl_p_walks_history_backward() {
7293 let mut e = editor_with("alpha beta gamma");
7294 run_keys(&mut e, "/alpha<CR>");
7295 run_keys(&mut e, "/beta<CR>");
7296 run_keys(&mut e, "/");
7298 assert_eq!(e.search_prompt().unwrap().text, "");
7299 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7300 assert_eq!(e.search_prompt().unwrap().text, "beta");
7301 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7302 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7303 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7305 assert_eq!(e.search_prompt().unwrap().text, "alpha");
7306 }
7307
7308 #[test]
7309 fn ctrl_n_walks_history_forward_after_ctrl_p() {
7310 let mut e = editor_with("a b c");
7311 run_keys(&mut e, "/a<CR>");
7312 run_keys(&mut e, "/b<CR>");
7313 run_keys(&mut e, "/c<CR>");
7314 run_keys(&mut e, "/");
7315 for _ in 0..3 {
7317 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7318 }
7319 assert_eq!(e.search_prompt().unwrap().text, "a");
7320 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7321 assert_eq!(e.search_prompt().unwrap().text, "b");
7322 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7323 assert_eq!(e.search_prompt().unwrap().text, "c");
7324 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
7326 assert_eq!(e.search_prompt().unwrap().text, "c");
7327 }
7328
7329 #[test]
7330 fn typing_after_history_walk_resets_cursor() {
7331 let mut e = editor_with("foo");
7332 run_keys(&mut e, "/foo<CR>");
7333 run_keys(&mut e, "/");
7334 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7335 assert_eq!(e.search_prompt().unwrap().text, "foo");
7336 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
7339 assert_eq!(e.search_prompt().unwrap().text, "foox");
7340 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
7341 assert_eq!(e.search_prompt().unwrap().text, "foo");
7342 }
7343
7344 #[test]
7345 fn empty_backward_search_prompt_enter_repeats_last_search() {
7346 let mut e = editor_with("foo bar foo baz foo");
7347 run_keys(&mut e, "/foo");
7349 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7350 assert_eq!(e.cursor().1, 8);
7351 run_keys(&mut e, "?");
7352 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7353 assert_eq!(e.cursor().1, 0);
7354 assert_eq!(e.last_search(), Some("foo"));
7355 }
7356
7357 #[test]
7358 fn search_prompt_esc_cancels_but_keeps_last_search() {
7359 let mut e = editor_with("foo bar\nbaz");
7360 run_keys(&mut e, "/bar");
7361 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
7362 assert!(e.search_prompt().is_none());
7363 assert_eq!(e.last_search(), Some("bar"));
7364 }
7365
7366 #[test]
7367 fn search_then_n_and_shift_n_navigate() {
7368 let mut e = editor_with("foo bar foo baz foo");
7369 run_keys(&mut e, "/foo");
7370 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7371 assert_eq!(e.cursor().1, 8);
7373 run_keys(&mut e, "n");
7374 assert_eq!(e.cursor().1, 16);
7375 run_keys(&mut e, "N");
7376 assert_eq!(e.cursor().1, 8);
7377 }
7378
7379 #[test]
7380 fn question_mark_searches_backward_on_enter() {
7381 let mut e = editor_with("foo bar foo baz");
7382 e.jump_cursor(0, 10);
7383 run_keys(&mut e, "?foo");
7384 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
7385 assert_eq!(e.cursor(), (0, 8));
7387 }
7388
7389 #[test]
7392 fn big_y_yanks_to_end_of_line() {
7393 let mut e = editor_with("hello world");
7394 e.jump_cursor(0, 6);
7395 run_keys(&mut e, "Y");
7396 assert_eq!(e.last_yank.as_deref(), Some("world"));
7397 }
7398
7399 #[test]
7400 fn big_y_from_line_start_yanks_full_line() {
7401 let mut e = editor_with("hello world");
7402 run_keys(&mut e, "Y");
7403 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
7404 }
7405
7406 #[test]
7407 fn gj_joins_without_inserting_space() {
7408 let mut e = editor_with("hello\n world");
7409 run_keys(&mut e, "gJ");
7410 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7412 }
7413
7414 #[test]
7415 fn gj_noop_on_last_line() {
7416 let mut e = editor_with("only");
7417 run_keys(&mut e, "gJ");
7418 assert_eq!(e.buffer().lines(), &["only".to_string()]);
7419 }
7420
7421 #[test]
7422 fn ge_jumps_to_previous_word_end() {
7423 let mut e = editor_with("foo bar baz");
7424 e.jump_cursor(0, 5);
7425 run_keys(&mut e, "ge");
7426 assert_eq!(e.cursor(), (0, 2));
7427 }
7428
7429 #[test]
7430 fn ge_respects_word_class() {
7431 let mut e = editor_with("foo-bar baz");
7434 e.jump_cursor(0, 5);
7435 run_keys(&mut e, "ge");
7436 assert_eq!(e.cursor(), (0, 3));
7437 }
7438
7439 #[test]
7440 fn big_ge_treats_hyphens_as_part_of_word() {
7441 let mut e = editor_with("foo-bar baz");
7444 e.jump_cursor(0, 10);
7445 run_keys(&mut e, "gE");
7446 assert_eq!(e.cursor(), (0, 6));
7447 }
7448
7449 #[test]
7450 fn ge_crosses_line_boundary() {
7451 let mut e = editor_with("foo\nbar");
7452 e.jump_cursor(1, 0);
7453 run_keys(&mut e, "ge");
7454 assert_eq!(e.cursor(), (0, 2));
7455 }
7456
7457 #[test]
7458 fn dge_deletes_to_end_of_previous_word() {
7459 let mut e = editor_with("foo bar baz");
7460 e.jump_cursor(0, 8);
7461 run_keys(&mut e, "dge");
7464 assert_eq!(e.buffer().lines()[0], "foo baaz");
7465 }
7466
7467 #[test]
7468 fn ctrl_scroll_keys_do_not_panic() {
7469 let mut e = editor_with(
7472 (0..50)
7473 .map(|i| format!("line{i}"))
7474 .collect::<Vec<_>>()
7475 .join("\n")
7476 .as_str(),
7477 );
7478 run_keys(&mut e, "<C-f>");
7479 run_keys(&mut e, "<C-b>");
7480 assert!(!e.buffer().lines().is_empty());
7482 }
7483
7484 #[test]
7491 fn count_insert_with_arrow_nav_does_not_leak_rows() {
7492 let mut e = Editor::new(
7493 hjkl_buffer::Buffer::new(),
7494 crate::types::DefaultHost::new(),
7495 crate::types::Options::default(),
7496 );
7497 e.set_content("row0\nrow1\nrow2");
7498 run_keys(&mut e, "3iX<Down><Esc>");
7500 assert!(e.buffer().lines()[0].contains('X'));
7502 assert!(
7505 !e.buffer().lines()[1].contains("row0"),
7506 "row1 leaked row0 contents: {:?}",
7507 e.buffer().lines()[1]
7508 );
7509 assert_eq!(e.buffer().lines().len(), 3);
7512 }
7513
7514 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
7517 let mut e = Editor::new(
7518 hjkl_buffer::Buffer::new(),
7519 crate::types::DefaultHost::new(),
7520 crate::types::Options::default(),
7521 );
7522 let body = (0..n)
7523 .map(|i| format!(" line{}", i))
7524 .collect::<Vec<_>>()
7525 .join("\n");
7526 e.set_content(&body);
7527 e.set_viewport_height(viewport);
7528 e
7529 }
7530
7531 #[test]
7532 fn ctrl_d_moves_cursor_half_page_down() {
7533 let mut e = editor_with_rows(100, 20);
7534 run_keys(&mut e, "<C-d>");
7535 assert_eq!(e.cursor().0, 10);
7536 }
7537
7538 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
7539 let mut e = Editor::new(
7540 hjkl_buffer::Buffer::new(),
7541 crate::types::DefaultHost::new(),
7542 crate::types::Options::default(),
7543 );
7544 e.set_content(&lines.join("\n"));
7545 e.set_viewport_height(viewport);
7546 let v = e.host_mut().viewport_mut();
7547 v.height = viewport;
7548 v.width = text_width;
7549 v.text_width = text_width;
7550 v.wrap = hjkl_buffer::Wrap::Char;
7551 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
7552 e
7553 }
7554
7555 #[test]
7556 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
7557 let lines = ["aaaabbbbcccc"; 10];
7561 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7562 e.jump_cursor(4, 0);
7563 e.ensure_cursor_in_scrolloff();
7564 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
7565 assert!(csr <= 6, "csr={csr}");
7566 }
7567
7568 #[test]
7569 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
7570 let lines = ["aaaabbbbcccc"; 10];
7571 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7572 e.jump_cursor(7, 0);
7575 e.ensure_cursor_in_scrolloff();
7576 e.jump_cursor(2, 0);
7577 e.ensure_cursor_in_scrolloff();
7578 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
7579 assert!(csr >= 5, "csr={csr}");
7581 }
7582
7583 #[test]
7584 fn scrolloff_wrap_clamps_top_at_buffer_end() {
7585 let lines = ["aaaabbbbcccc"; 5];
7586 let mut e = editor_with_wrap_lines(&lines, 12, 4);
7587 e.jump_cursor(4, 11);
7588 e.ensure_cursor_in_scrolloff();
7589 let top = e.host().viewport().top_row;
7594 assert_eq!(top, 1);
7595 }
7596
7597 #[test]
7598 fn ctrl_u_moves_cursor_half_page_up() {
7599 let mut e = editor_with_rows(100, 20);
7600 e.jump_cursor(50, 0);
7601 run_keys(&mut e, "<C-u>");
7602 assert_eq!(e.cursor().0, 40);
7603 }
7604
7605 #[test]
7606 fn ctrl_f_moves_cursor_full_page_down() {
7607 let mut e = editor_with_rows(100, 20);
7608 run_keys(&mut e, "<C-f>");
7609 assert_eq!(e.cursor().0, 18);
7611 }
7612
7613 #[test]
7614 fn ctrl_b_moves_cursor_full_page_up() {
7615 let mut e = editor_with_rows(100, 20);
7616 e.jump_cursor(50, 0);
7617 run_keys(&mut e, "<C-b>");
7618 assert_eq!(e.cursor().0, 32);
7619 }
7620
7621 #[test]
7622 fn ctrl_d_lands_on_first_non_blank() {
7623 let mut e = editor_with_rows(100, 20);
7624 run_keys(&mut e, "<C-d>");
7625 assert_eq!(e.cursor().1, 2);
7627 }
7628
7629 #[test]
7630 fn ctrl_d_clamps_at_end_of_buffer() {
7631 let mut e = editor_with_rows(5, 20);
7632 run_keys(&mut e, "<C-d>");
7633 assert_eq!(e.cursor().0, 4);
7634 }
7635
7636 #[test]
7637 fn capital_h_jumps_to_viewport_top() {
7638 let mut e = editor_with_rows(100, 10);
7639 e.jump_cursor(50, 0);
7640 e.set_viewport_top(45);
7641 let top = e.host().viewport().top_row;
7642 run_keys(&mut e, "H");
7643 assert_eq!(e.cursor().0, top);
7644 assert_eq!(e.cursor().1, 2);
7645 }
7646
7647 #[test]
7648 fn capital_l_jumps_to_viewport_bottom() {
7649 let mut e = editor_with_rows(100, 10);
7650 e.jump_cursor(50, 0);
7651 e.set_viewport_top(45);
7652 let top = e.host().viewport().top_row;
7653 run_keys(&mut e, "L");
7654 assert_eq!(e.cursor().0, top + 9);
7655 }
7656
7657 #[test]
7658 fn capital_m_jumps_to_viewport_middle() {
7659 let mut e = editor_with_rows(100, 10);
7660 e.jump_cursor(50, 0);
7661 e.set_viewport_top(45);
7662 let top = e.host().viewport().top_row;
7663 run_keys(&mut e, "M");
7664 assert_eq!(e.cursor().0, top + 4);
7666 }
7667
7668 #[test]
7669 fn g_capital_m_lands_at_line_midpoint() {
7670 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
7672 assert_eq!(e.cursor(), (0, 6));
7674 }
7675
7676 #[test]
7677 fn g_capital_m_on_empty_line_stays_at_zero() {
7678 let mut e = editor_with("");
7679 run_keys(&mut e, "gM");
7680 assert_eq!(e.cursor(), (0, 0));
7681 }
7682
7683 #[test]
7684 fn g_capital_m_uses_current_line_only() {
7685 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
7688 run_keys(&mut e, "gM");
7689 assert_eq!(e.cursor(), (1, 6));
7690 }
7691
7692 #[test]
7693 fn capital_h_count_offsets_from_top() {
7694 let mut e = editor_with_rows(100, 10);
7695 e.jump_cursor(50, 0);
7696 e.set_viewport_top(45);
7697 let top = e.host().viewport().top_row;
7698 run_keys(&mut e, "3H");
7699 assert_eq!(e.cursor().0, top + 2);
7700 }
7701
7702 #[test]
7705 fn ctrl_o_returns_to_pre_g_position() {
7706 let mut e = editor_with_rows(50, 20);
7707 e.jump_cursor(5, 2);
7708 run_keys(&mut e, "G");
7709 assert_eq!(e.cursor().0, 49);
7710 run_keys(&mut e, "<C-o>");
7711 assert_eq!(e.cursor(), (5, 2));
7712 }
7713
7714 #[test]
7715 fn ctrl_i_redoes_jump_after_ctrl_o() {
7716 let mut e = editor_with_rows(50, 20);
7717 e.jump_cursor(5, 2);
7718 run_keys(&mut e, "G");
7719 let post = e.cursor();
7720 run_keys(&mut e, "<C-o>");
7721 run_keys(&mut e, "<C-i>");
7722 assert_eq!(e.cursor(), post);
7723 }
7724
7725 #[test]
7726 fn new_jump_clears_forward_stack() {
7727 let mut e = editor_with_rows(50, 20);
7728 e.jump_cursor(5, 2);
7729 run_keys(&mut e, "G");
7730 run_keys(&mut e, "<C-o>");
7731 run_keys(&mut e, "gg");
7732 run_keys(&mut e, "<C-i>");
7733 assert_eq!(e.cursor().0, 0);
7734 }
7735
7736 #[test]
7737 fn ctrl_o_on_empty_stack_is_noop() {
7738 let mut e = editor_with_rows(10, 20);
7739 e.jump_cursor(3, 1);
7740 run_keys(&mut e, "<C-o>");
7741 assert_eq!(e.cursor(), (3, 1));
7742 }
7743
7744 #[test]
7745 fn asterisk_search_pushes_jump() {
7746 let mut e = editor_with("foo bar\nbaz foo end");
7747 e.jump_cursor(0, 0);
7748 run_keys(&mut e, "*");
7749 let after = e.cursor();
7750 assert_ne!(after, (0, 0));
7751 run_keys(&mut e, "<C-o>");
7752 assert_eq!(e.cursor(), (0, 0));
7753 }
7754
7755 #[test]
7756 fn h_viewport_jump_is_recorded() {
7757 let mut e = editor_with_rows(100, 10);
7758 e.jump_cursor(50, 0);
7759 e.set_viewport_top(45);
7760 let pre = e.cursor();
7761 run_keys(&mut e, "H");
7762 assert_ne!(e.cursor(), pre);
7763 run_keys(&mut e, "<C-o>");
7764 assert_eq!(e.cursor(), pre);
7765 }
7766
7767 #[test]
7768 fn j_k_motion_does_not_push_jump() {
7769 let mut e = editor_with_rows(50, 20);
7770 e.jump_cursor(5, 0);
7771 run_keys(&mut e, "jjj");
7772 run_keys(&mut e, "<C-o>");
7773 assert_eq!(e.cursor().0, 8);
7774 }
7775
7776 #[test]
7777 fn jumplist_caps_at_100() {
7778 let mut e = editor_with_rows(200, 20);
7779 for i in 0..101 {
7780 e.jump_cursor(i, 0);
7781 run_keys(&mut e, "G");
7782 }
7783 assert!(e.vim.jump_back.len() <= 100);
7784 }
7785
7786 #[test]
7787 fn tab_acts_as_ctrl_i() {
7788 let mut e = editor_with_rows(50, 20);
7789 e.jump_cursor(5, 2);
7790 run_keys(&mut e, "G");
7791 let post = e.cursor();
7792 run_keys(&mut e, "<C-o>");
7793 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
7794 assert_eq!(e.cursor(), post);
7795 }
7796
7797 #[test]
7800 fn ma_then_backtick_a_jumps_exact() {
7801 let mut e = editor_with_rows(50, 20);
7802 e.jump_cursor(5, 3);
7803 run_keys(&mut e, "ma");
7804 e.jump_cursor(20, 0);
7805 run_keys(&mut e, "`a");
7806 assert_eq!(e.cursor(), (5, 3));
7807 }
7808
7809 #[test]
7810 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
7811 let mut e = editor_with_rows(50, 20);
7812 e.jump_cursor(5, 6);
7814 run_keys(&mut e, "ma");
7815 e.jump_cursor(30, 4);
7816 run_keys(&mut e, "'a");
7817 assert_eq!(e.cursor(), (5, 2));
7818 }
7819
7820 #[test]
7821 fn goto_mark_pushes_jumplist() {
7822 let mut e = editor_with_rows(50, 20);
7823 e.jump_cursor(10, 2);
7824 run_keys(&mut e, "mz");
7825 e.jump_cursor(3, 0);
7826 run_keys(&mut e, "`z");
7827 assert_eq!(e.cursor(), (10, 2));
7828 run_keys(&mut e, "<C-o>");
7829 assert_eq!(e.cursor(), (3, 0));
7830 }
7831
7832 #[test]
7833 fn goto_missing_mark_is_noop() {
7834 let mut e = editor_with_rows(50, 20);
7835 e.jump_cursor(3, 1);
7836 run_keys(&mut e, "`q");
7837 assert_eq!(e.cursor(), (3, 1));
7838 }
7839
7840 #[test]
7841 fn uppercase_mark_stored_under_uppercase_key() {
7842 let mut e = editor_with_rows(50, 20);
7843 e.jump_cursor(5, 3);
7844 run_keys(&mut e, "mA");
7845 assert_eq!(e.mark('A'), Some((5, 3)));
7848 assert!(e.mark('a').is_none());
7849 }
7850
7851 #[test]
7852 fn mark_survives_document_shrink_via_clamp() {
7853 let mut e = editor_with_rows(50, 20);
7854 e.jump_cursor(40, 4);
7855 run_keys(&mut e, "mx");
7856 e.set_content("a\nb\nc\nd\ne");
7858 run_keys(&mut e, "`x");
7859 let (r, _) = e.cursor();
7861 assert!(r <= 4);
7862 }
7863
7864 #[test]
7865 fn g_semicolon_walks_back_through_edits() {
7866 let mut e = editor_with("alpha\nbeta\ngamma");
7867 e.jump_cursor(0, 0);
7870 run_keys(&mut e, "iX<Esc>");
7871 e.jump_cursor(2, 0);
7872 run_keys(&mut e, "iY<Esc>");
7873 run_keys(&mut e, "g;");
7875 assert_eq!(e.cursor(), (2, 1));
7876 run_keys(&mut e, "g;");
7878 assert_eq!(e.cursor(), (0, 1));
7879 run_keys(&mut e, "g;");
7881 assert_eq!(e.cursor(), (0, 1));
7882 }
7883
7884 #[test]
7885 fn g_comma_walks_forward_after_g_semicolon() {
7886 let mut e = editor_with("a\nb\nc");
7887 e.jump_cursor(0, 0);
7888 run_keys(&mut e, "iX<Esc>");
7889 e.jump_cursor(2, 0);
7890 run_keys(&mut e, "iY<Esc>");
7891 run_keys(&mut e, "g;");
7892 run_keys(&mut e, "g;");
7893 assert_eq!(e.cursor(), (0, 1));
7894 run_keys(&mut e, "g,");
7895 assert_eq!(e.cursor(), (2, 1));
7896 }
7897
7898 #[test]
7899 fn new_edit_during_walk_trims_forward_entries() {
7900 let mut e = editor_with("a\nb\nc\nd");
7901 e.jump_cursor(0, 0);
7902 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
7904 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
7907 run_keys(&mut e, "g;");
7908 assert_eq!(e.cursor(), (0, 1));
7909 run_keys(&mut e, "iZ<Esc>");
7911 run_keys(&mut e, "g,");
7913 assert_ne!(e.cursor(), (2, 1));
7915 }
7916
7917 #[test]
7923 fn capital_mark_set_and_jump() {
7924 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
7925 e.jump_cursor(2, 1);
7926 run_keys(&mut e, "mA");
7927 e.jump_cursor(0, 0);
7929 run_keys(&mut e, "'A");
7931 assert_eq!(e.cursor().0, 2);
7933 }
7934
7935 #[test]
7936 fn capital_mark_survives_set_content() {
7937 let mut e = editor_with("first buffer line\nsecond");
7938 e.jump_cursor(1, 3);
7939 run_keys(&mut e, "mA");
7940 e.set_content("totally different content\non many\nrows of text");
7942 e.jump_cursor(0, 0);
7944 run_keys(&mut e, "'A");
7945 assert_eq!(e.cursor().0, 1);
7946 }
7947
7948 #[test]
7953 fn capital_mark_shifts_with_edit() {
7954 let mut e = editor_with("a\nb\nc\nd");
7955 e.jump_cursor(3, 0);
7956 run_keys(&mut e, "mA");
7957 e.jump_cursor(0, 0);
7959 run_keys(&mut e, "dd");
7960 e.jump_cursor(0, 0);
7961 run_keys(&mut e, "'A");
7962 assert_eq!(e.cursor().0, 2);
7963 }
7964
7965 #[test]
7966 fn mark_below_delete_shifts_up() {
7967 let mut e = editor_with("a\nb\nc\nd\ne");
7968 e.jump_cursor(3, 0);
7970 run_keys(&mut e, "ma");
7971 e.jump_cursor(0, 0);
7973 run_keys(&mut e, "dd");
7974 e.jump_cursor(0, 0);
7976 run_keys(&mut e, "'a");
7977 assert_eq!(e.cursor().0, 2);
7978 assert_eq!(e.buffer().line(2).unwrap(), "d");
7979 }
7980
7981 #[test]
7982 fn mark_on_deleted_row_is_dropped() {
7983 let mut e = editor_with("a\nb\nc\nd");
7984 e.jump_cursor(1, 0);
7986 run_keys(&mut e, "ma");
7987 run_keys(&mut e, "dd");
7989 e.jump_cursor(2, 0);
7991 run_keys(&mut e, "'a");
7992 assert_eq!(e.cursor().0, 2);
7994 }
7995
7996 #[test]
7997 fn mark_above_edit_unchanged() {
7998 let mut e = editor_with("a\nb\nc\nd\ne");
7999 e.jump_cursor(0, 0);
8001 run_keys(&mut e, "ma");
8002 e.jump_cursor(3, 0);
8004 run_keys(&mut e, "dd");
8005 e.jump_cursor(2, 0);
8007 run_keys(&mut e, "'a");
8008 assert_eq!(e.cursor().0, 0);
8009 }
8010
8011 #[test]
8012 fn mark_shifts_down_after_insert() {
8013 let mut e = editor_with("a\nb\nc");
8014 e.jump_cursor(2, 0);
8016 run_keys(&mut e, "ma");
8017 e.jump_cursor(0, 0);
8019 run_keys(&mut e, "Onew<Esc>");
8020 e.jump_cursor(0, 0);
8023 run_keys(&mut e, "'a");
8024 assert_eq!(e.cursor().0, 3);
8025 assert_eq!(e.buffer().line(3).unwrap(), "c");
8026 }
8027
8028 #[test]
8031 fn forward_search_commit_pushes_jump() {
8032 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8033 e.jump_cursor(0, 0);
8034 run_keys(&mut e, "/target<CR>");
8035 assert_ne!(e.cursor(), (0, 0));
8037 run_keys(&mut e, "<C-o>");
8039 assert_eq!(e.cursor(), (0, 0));
8040 }
8041
8042 #[test]
8043 fn search_commit_no_match_does_not_push_jump() {
8044 let mut e = editor_with("alpha beta\nfoo end");
8045 e.jump_cursor(0, 3);
8046 let pre_len = e.vim.jump_back.len();
8047 run_keys(&mut e, "/zzznotfound<CR>");
8048 assert_eq!(e.vim.jump_back.len(), pre_len);
8050 }
8051
8052 #[test]
8055 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8056 let mut e = editor_with("hello world");
8057 run_keys(&mut e, "lll");
8058 let (row, col) = e.cursor();
8059 assert_eq!(e.buffer.cursor().row, row);
8060 assert_eq!(e.buffer.cursor().col, col);
8061 }
8062
8063 #[test]
8064 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8065 let mut e = editor_with("aaaa\nbbbb\ncccc");
8066 run_keys(&mut e, "jj");
8067 let (row, col) = e.cursor();
8068 assert_eq!(e.buffer.cursor().row, row);
8069 assert_eq!(e.buffer.cursor().col, col);
8070 }
8071
8072 #[test]
8073 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8074 let mut e = editor_with("foo bar baz");
8075 run_keys(&mut e, "ww");
8076 let (row, col) = e.cursor();
8077 assert_eq!(e.buffer.cursor().row, row);
8078 assert_eq!(e.buffer.cursor().col, col);
8079 }
8080
8081 #[test]
8082 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8083 let mut e = editor_with("a\nb\nc\nd\ne");
8084 run_keys(&mut e, "G");
8085 let (row, col) = e.cursor();
8086 assert_eq!(e.buffer.cursor().row, row);
8087 assert_eq!(e.buffer.cursor().col, col);
8088 }
8089
8090 #[test]
8091 fn editor_sticky_col_tracks_horizontal_motion() {
8092 let mut e = editor_with("longline\nhi\nlongline");
8093 run_keys(&mut e, "fl");
8098 let landed = e.cursor().1;
8099 assert!(landed > 0, "fl should have moved");
8100 run_keys(&mut e, "j");
8101 assert_eq!(e.sticky_col(), Some(landed));
8104 }
8105
8106 #[test]
8107 fn buffer_content_mirrors_textarea_after_insert() {
8108 let mut e = editor_with("hello");
8109 run_keys(&mut e, "iXYZ<Esc>");
8110 let text = e.buffer().lines().join("\n");
8111 assert_eq!(e.buffer.as_string(), text);
8112 }
8113
8114 #[test]
8115 fn buffer_content_mirrors_textarea_after_delete() {
8116 let mut e = editor_with("alpha bravo charlie");
8117 run_keys(&mut e, "dw");
8118 let text = e.buffer().lines().join("\n");
8119 assert_eq!(e.buffer.as_string(), text);
8120 }
8121
8122 #[test]
8123 fn buffer_content_mirrors_textarea_after_dd() {
8124 let mut e = editor_with("a\nb\nc\nd");
8125 run_keys(&mut e, "jdd");
8126 let text = e.buffer().lines().join("\n");
8127 assert_eq!(e.buffer.as_string(), text);
8128 }
8129
8130 #[test]
8131 fn buffer_content_mirrors_textarea_after_open_line() {
8132 let mut e = editor_with("foo\nbar");
8133 run_keys(&mut e, "oNEW<Esc>");
8134 let text = e.buffer().lines().join("\n");
8135 assert_eq!(e.buffer.as_string(), text);
8136 }
8137
8138 #[test]
8139 fn buffer_content_mirrors_textarea_after_paste() {
8140 let mut e = editor_with("hello");
8141 run_keys(&mut e, "yy");
8142 run_keys(&mut e, "p");
8143 let text = e.buffer().lines().join("\n");
8144 assert_eq!(e.buffer.as_string(), text);
8145 }
8146
8147 #[test]
8148 fn buffer_selection_none_in_normal_mode() {
8149 let e = editor_with("foo bar");
8150 assert!(e.buffer_selection().is_none());
8151 }
8152
8153 #[test]
8154 fn buffer_selection_char_in_visual_mode() {
8155 use hjkl_buffer::{Position, Selection};
8156 let mut e = editor_with("hello world");
8157 run_keys(&mut e, "vlll");
8158 assert_eq!(
8159 e.buffer_selection(),
8160 Some(Selection::Char {
8161 anchor: Position::new(0, 0),
8162 head: Position::new(0, 3),
8163 })
8164 );
8165 }
8166
8167 #[test]
8168 fn buffer_selection_line_in_visual_line_mode() {
8169 use hjkl_buffer::Selection;
8170 let mut e = editor_with("a\nb\nc\nd");
8171 run_keys(&mut e, "Vj");
8172 assert_eq!(
8173 e.buffer_selection(),
8174 Some(Selection::Line {
8175 anchor_row: 0,
8176 head_row: 1,
8177 })
8178 );
8179 }
8180
8181 #[test]
8182 fn wrapscan_off_blocks_wrap_around() {
8183 let mut e = editor_with("first\nsecond\nthird\n");
8184 e.settings_mut().wrapscan = false;
8185 e.jump_cursor(2, 0);
8187 run_keys(&mut e, "/first<CR>");
8188 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8190 e.settings_mut().wrapscan = true;
8192 run_keys(&mut e, "/first<CR>");
8193 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8194 }
8195
8196 #[test]
8197 fn smartcase_uppercase_pattern_stays_sensitive() {
8198 let mut e = editor_with("foo\nFoo\nBAR\n");
8199 e.settings_mut().ignore_case = true;
8200 e.settings_mut().smartcase = true;
8201 run_keys(&mut e, "/foo<CR>");
8204 let r1 = e
8205 .search_state()
8206 .pattern
8207 .as_ref()
8208 .unwrap()
8209 .as_str()
8210 .to_string();
8211 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
8212 run_keys(&mut e, "/Foo<CR>");
8214 let r2 = e
8215 .search_state()
8216 .pattern
8217 .as_ref()
8218 .unwrap()
8219 .as_str()
8220 .to_string();
8221 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
8222 }
8223
8224 #[test]
8225 fn enter_with_autoindent_copies_leading_whitespace() {
8226 let mut e = editor_with(" foo");
8227 e.jump_cursor(0, 7);
8228 run_keys(&mut e, "i<CR>");
8229 assert_eq!(e.buffer.line(1).unwrap(), " ");
8230 }
8231
8232 #[test]
8233 fn enter_without_autoindent_inserts_bare_newline() {
8234 let mut e = editor_with(" foo");
8235 e.settings_mut().autoindent = false;
8236 e.jump_cursor(0, 7);
8237 run_keys(&mut e, "i<CR>");
8238 assert_eq!(e.buffer.line(1).unwrap(), "");
8239 }
8240
8241 #[test]
8242 fn iskeyword_default_treats_alnum_underscore_as_word() {
8243 let mut e = editor_with("foo_bar baz");
8244 e.jump_cursor(0, 0);
8248 run_keys(&mut e, "*");
8249 let p = e
8250 .search_state()
8251 .pattern
8252 .as_ref()
8253 .unwrap()
8254 .as_str()
8255 .to_string();
8256 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
8257 }
8258
8259 #[test]
8260 fn w_motion_respects_custom_iskeyword() {
8261 let mut e = editor_with("foo-bar baz");
8265 run_keys(&mut e, "w");
8266 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
8267 let mut e2 = editor_with("foo-bar baz");
8270 e2.set_iskeyword("@,_,45");
8271 run_keys(&mut e2, "w");
8272 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
8273 }
8274
8275 #[test]
8276 fn iskeyword_with_dash_treats_dash_as_word_char() {
8277 let mut e = editor_with("foo-bar baz");
8278 e.settings_mut().iskeyword = "@,_,45".to_string();
8279 e.jump_cursor(0, 0);
8280 run_keys(&mut e, "*");
8281 let p = e
8282 .search_state()
8283 .pattern
8284 .as_ref()
8285 .unwrap()
8286 .as_str()
8287 .to_string();
8288 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
8289 }
8290
8291 #[test]
8292 fn timeoutlen_drops_pending_g_prefix() {
8293 use std::time::{Duration, Instant};
8294 let mut e = editor_with("a\nb\nc");
8295 e.jump_cursor(2, 0);
8296 run_keys(&mut e, "g");
8298 assert!(matches!(e.vim.pending, super::Pending::G));
8299 e.settings.timeout_len = Duration::from_nanos(0);
8307 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
8308 e.vim.last_input_host_at = Some(Duration::ZERO);
8309 run_keys(&mut e, "g");
8313 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
8315 }
8316
8317 #[test]
8318 fn undobreak_on_breaks_group_at_arrow_motion() {
8319 let mut e = editor_with("");
8320 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8322 let line = e.buffer.line(0).unwrap_or("").to_string();
8325 assert!(line.contains("aaa"), "after undobreak: {line:?}");
8326 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
8327 }
8328
8329 #[test]
8330 fn undobreak_off_keeps_full_run_in_one_group() {
8331 let mut e = editor_with("");
8332 e.settings_mut().undo_break_on_motion = false;
8333 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
8334 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
8337 }
8338
8339 #[test]
8340 fn undobreak_round_trips_through_options() {
8341 let e = editor_with("");
8342 let opts = e.current_options();
8343 assert!(opts.undo_break_on_motion);
8344 let mut e2 = editor_with("");
8345 let mut new_opts = opts.clone();
8346 new_opts.undo_break_on_motion = false;
8347 e2.apply_options(&new_opts);
8348 assert!(!e2.current_options().undo_break_on_motion);
8349 }
8350
8351 #[test]
8352 fn undo_levels_cap_drops_oldest() {
8353 let mut e = editor_with("abcde");
8354 e.settings_mut().undo_levels = 3;
8355 run_keys(&mut e, "ra");
8356 run_keys(&mut e, "lrb");
8357 run_keys(&mut e, "lrc");
8358 run_keys(&mut e, "lrd");
8359 run_keys(&mut e, "lre");
8360 assert_eq!(e.undo_stack_len(), 3);
8361 }
8362
8363 #[test]
8364 fn tab_inserts_literal_tab_by_default() {
8365 let mut e = editor_with("");
8366 run_keys(&mut e, "i");
8367 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8368 assert_eq!(e.buffer.line(0).unwrap(), "\t");
8369 }
8370
8371 #[test]
8372 fn tab_inserts_spaces_when_expandtab() {
8373 let mut e = editor_with("");
8374 e.settings_mut().expandtab = true;
8375 e.settings_mut().tabstop = 4;
8376 run_keys(&mut e, "i");
8377 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8378 assert_eq!(e.buffer.line(0).unwrap(), " ");
8379 }
8380
8381 #[test]
8382 fn readonly_blocks_insert_mutation() {
8383 let mut e = editor_with("hello");
8384 e.settings_mut().readonly = true;
8385 run_keys(&mut e, "iX<Esc>");
8386 assert_eq!(e.buffer.line(0).unwrap(), "hello");
8387 }
8388
8389 #[cfg(feature = "ratatui")]
8390 #[test]
8391 fn intern_ratatui_style_dedups_repeated_styles() {
8392 use ratatui::style::{Color, Style};
8393 let mut e = editor_with("");
8394 let red = Style::default().fg(Color::Red);
8395 let blue = Style::default().fg(Color::Blue);
8396 let id_r1 = e.intern_ratatui_style(red);
8397 let id_r2 = e.intern_ratatui_style(red);
8398 let id_b = e.intern_ratatui_style(blue);
8399 assert_eq!(id_r1, id_r2);
8400 assert_ne!(id_r1, id_b);
8401 assert_eq!(e.style_table().len(), 2);
8402 }
8403
8404 #[cfg(feature = "ratatui")]
8405 #[test]
8406 fn install_ratatui_syntax_spans_translates_styled_spans() {
8407 use ratatui::style::{Color, Style};
8408 let mut e = editor_with("SELECT foo");
8409 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
8410 let by_row = e.buffer_spans();
8411 assert_eq!(by_row.len(), 1);
8412 assert_eq!(by_row[0].len(), 1);
8413 assert_eq!(by_row[0][0].start_byte, 0);
8414 assert_eq!(by_row[0][0].end_byte, 6);
8415 let id = by_row[0][0].style;
8416 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
8417 }
8418
8419 #[cfg(feature = "ratatui")]
8420 #[test]
8421 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
8422 use ratatui::style::{Color, Style};
8423 let mut e = editor_with("hello");
8424 e.install_ratatui_syntax_spans(vec![vec![(
8425 0,
8426 usize::MAX,
8427 Style::default().fg(Color::Blue),
8428 )]]);
8429 let by_row = e.buffer_spans();
8430 assert_eq!(by_row[0][0].end_byte, 5);
8431 }
8432
8433 #[cfg(feature = "ratatui")]
8434 #[test]
8435 fn install_ratatui_syntax_spans_drops_zero_width() {
8436 use ratatui::style::{Color, Style};
8437 let mut e = editor_with("abc");
8438 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
8439 assert!(e.buffer_spans()[0].is_empty());
8440 }
8441
8442 #[test]
8443 fn named_register_yank_into_a_then_paste_from_a() {
8444 let mut e = editor_with("hello world\nsecond");
8445 run_keys(&mut e, "\"ayw");
8446 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8448 run_keys(&mut e, "j0\"aP");
8450 assert_eq!(e.buffer().lines()[1], "hello second");
8451 }
8452
8453 #[test]
8454 fn capital_r_overstrikes_chars() {
8455 let mut e = editor_with("hello");
8456 e.jump_cursor(0, 0);
8457 run_keys(&mut e, "RXY<Esc>");
8458 assert_eq!(e.buffer().lines()[0], "XYllo");
8460 }
8461
8462 #[test]
8463 fn capital_r_at_eol_appends() {
8464 let mut e = editor_with("hi");
8465 e.jump_cursor(0, 1);
8466 run_keys(&mut e, "RXYZ<Esc>");
8468 assert_eq!(e.buffer().lines()[0], "hXYZ");
8469 }
8470
8471 #[test]
8472 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
8473 let mut e = editor_with("abc");
8477 e.jump_cursor(0, 0);
8478 run_keys(&mut e, "RX<Esc>");
8479 assert_eq!(e.buffer().lines()[0], "Xbc");
8480 }
8481
8482 #[test]
8483 fn ctrl_r_in_insert_pastes_named_register() {
8484 let mut e = editor_with("hello world");
8485 run_keys(&mut e, "\"ayw");
8487 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
8488 run_keys(&mut e, "o");
8490 assert_eq!(e.vim_mode(), VimMode::Insert);
8491 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8492 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
8493 assert_eq!(e.buffer().lines()[1], "hello ");
8494 assert_eq!(e.cursor(), (1, 6));
8496 assert_eq!(e.vim_mode(), VimMode::Insert);
8498 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
8499 assert_eq!(e.buffer().lines()[1], "hello X");
8500 }
8501
8502 #[test]
8503 fn ctrl_r_with_unnamed_register() {
8504 let mut e = editor_with("foo");
8505 run_keys(&mut e, "yiw");
8506 run_keys(&mut e, "A ");
8507 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8509 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
8510 assert_eq!(e.buffer().lines()[0], "foo foo");
8511 }
8512
8513 #[test]
8514 fn ctrl_r_unknown_selector_is_no_op() {
8515 let mut e = editor_with("abc");
8516 run_keys(&mut e, "A");
8517 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8518 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
8521 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
8522 assert_eq!(e.buffer().lines()[0], "abcZ");
8523 }
8524
8525 #[test]
8526 fn ctrl_r_multiline_register_pastes_with_newlines() {
8527 let mut e = editor_with("alpha\nbeta\ngamma");
8528 run_keys(&mut e, "\"byy");
8530 run_keys(&mut e, "j\"byy");
8531 run_keys(&mut e, "ggVj\"by");
8535 let payload = e.registers().read('b').unwrap().text.clone();
8536 assert!(payload.contains('\n'));
8537 run_keys(&mut e, "Go");
8538 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
8539 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
8540 let total_lines = e.buffer().lines().len();
8543 assert!(total_lines >= 5);
8544 }
8545
8546 #[test]
8547 fn yank_zero_holds_last_yank_after_delete() {
8548 let mut e = editor_with("hello world");
8549 run_keys(&mut e, "yw");
8550 let yanked = e.registers().read('0').unwrap().text.clone();
8551 assert!(!yanked.is_empty());
8552 run_keys(&mut e, "dw");
8554 assert_eq!(e.registers().read('0').unwrap().text, yanked);
8555 assert!(!e.registers().read('1').unwrap().text.is_empty());
8557 }
8558
8559 #[test]
8560 fn delete_ring_rotates_through_one_through_nine() {
8561 let mut e = editor_with("a b c d e f g h i j");
8562 for _ in 0..3 {
8564 run_keys(&mut e, "dw");
8565 }
8566 let r1 = e.registers().read('1').unwrap().text.clone();
8568 let r2 = e.registers().read('2').unwrap().text.clone();
8569 let r3 = e.registers().read('3').unwrap().text.clone();
8570 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
8571 assert_ne!(r1, r2);
8572 assert_ne!(r2, r3);
8573 }
8574
8575 #[test]
8576 fn capital_register_appends_to_lowercase() {
8577 let mut e = editor_with("foo bar");
8578 run_keys(&mut e, "\"ayw");
8579 let first = e.registers().read('a').unwrap().text.clone();
8580 assert!(first.contains("foo"));
8581 run_keys(&mut e, "w\"Ayw");
8583 let combined = e.registers().read('a').unwrap().text.clone();
8584 assert!(combined.starts_with(&first));
8585 assert!(combined.contains("bar"));
8586 }
8587
8588 #[test]
8589 fn zf_in_visual_line_creates_closed_fold() {
8590 let mut e = editor_with("a\nb\nc\nd\ne");
8591 e.jump_cursor(1, 0);
8593 run_keys(&mut e, "Vjjzf");
8594 assert_eq!(e.buffer().folds().len(), 1);
8595 let f = e.buffer().folds()[0];
8596 assert_eq!(f.start_row, 1);
8597 assert_eq!(f.end_row, 3);
8598 assert!(f.closed);
8599 }
8600
8601 #[test]
8602 fn zfj_in_normal_creates_two_row_fold() {
8603 let mut e = editor_with("a\nb\nc\nd\ne");
8604 e.jump_cursor(1, 0);
8605 run_keys(&mut e, "zfj");
8606 assert_eq!(e.buffer().folds().len(), 1);
8607 let f = e.buffer().folds()[0];
8608 assert_eq!(f.start_row, 1);
8609 assert_eq!(f.end_row, 2);
8610 assert!(f.closed);
8611 assert_eq!(e.cursor().0, 1);
8613 }
8614
8615 #[test]
8616 fn zf_with_count_folds_count_rows() {
8617 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8618 e.jump_cursor(0, 0);
8619 run_keys(&mut e, "zf3j");
8621 assert_eq!(e.buffer().folds().len(), 1);
8622 let f = e.buffer().folds()[0];
8623 assert_eq!(f.start_row, 0);
8624 assert_eq!(f.end_row, 3);
8625 }
8626
8627 #[test]
8628 fn zfk_folds_upward_range() {
8629 let mut e = editor_with("a\nb\nc\nd\ne");
8630 e.jump_cursor(3, 0);
8631 run_keys(&mut e, "zfk");
8632 let f = e.buffer().folds()[0];
8633 assert_eq!(f.start_row, 2);
8635 assert_eq!(f.end_row, 3);
8636 }
8637
8638 #[test]
8639 fn zf_capital_g_folds_to_bottom() {
8640 let mut e = editor_with("a\nb\nc\nd\ne");
8641 e.jump_cursor(1, 0);
8642 run_keys(&mut e, "zfG");
8644 let f = e.buffer().folds()[0];
8645 assert_eq!(f.start_row, 1);
8646 assert_eq!(f.end_row, 4);
8647 }
8648
8649 #[test]
8650 fn zfgg_folds_to_top_via_operator_pipeline() {
8651 let mut e = editor_with("a\nb\nc\nd\ne");
8652 e.jump_cursor(3, 0);
8653 run_keys(&mut e, "zfgg");
8657 let f = e.buffer().folds()[0];
8658 assert_eq!(f.start_row, 0);
8659 assert_eq!(f.end_row, 3);
8660 }
8661
8662 #[test]
8663 fn zfip_folds_paragraph_via_text_object() {
8664 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
8665 e.jump_cursor(1, 0);
8666 run_keys(&mut e, "zfip");
8668 assert_eq!(e.buffer().folds().len(), 1);
8669 let f = e.buffer().folds()[0];
8670 assert_eq!(f.start_row, 0);
8671 assert_eq!(f.end_row, 2);
8672 }
8673
8674 #[test]
8675 fn zfap_folds_paragraph_with_trailing_blank() {
8676 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
8677 e.jump_cursor(0, 0);
8678 run_keys(&mut e, "zfap");
8680 let f = e.buffer().folds()[0];
8681 assert_eq!(f.start_row, 0);
8682 assert_eq!(f.end_row, 3);
8683 }
8684
8685 #[test]
8686 fn zf_paragraph_motion_folds_to_blank() {
8687 let mut e = editor_with("alpha\nbeta\n\ngamma");
8688 e.jump_cursor(0, 0);
8689 run_keys(&mut e, "zf}");
8691 let f = e.buffer().folds()[0];
8692 assert_eq!(f.start_row, 0);
8693 assert_eq!(f.end_row, 2);
8694 }
8695
8696 #[test]
8697 fn za_toggles_fold_under_cursor() {
8698 let mut e = editor_with("a\nb\nc\nd");
8699 e.buffer_mut().add_fold(1, 2, true);
8700 e.jump_cursor(1, 0);
8701 run_keys(&mut e, "za");
8702 assert!(!e.buffer().folds()[0].closed);
8703 run_keys(&mut e, "za");
8704 assert!(e.buffer().folds()[0].closed);
8705 }
8706
8707 #[test]
8708 fn zr_opens_all_folds_zm_closes_all() {
8709 let mut e = editor_with("a\nb\nc\nd\ne\nf");
8710 e.buffer_mut().add_fold(0, 1, true);
8711 e.buffer_mut().add_fold(2, 3, true);
8712 e.buffer_mut().add_fold(4, 5, true);
8713 run_keys(&mut e, "zR");
8714 assert!(e.buffer().folds().iter().all(|f| !f.closed));
8715 run_keys(&mut e, "zM");
8716 assert!(e.buffer().folds().iter().all(|f| f.closed));
8717 }
8718
8719 #[test]
8720 fn ze_clears_all_folds() {
8721 let mut e = editor_with("a\nb\nc\nd");
8722 e.buffer_mut().add_fold(0, 1, true);
8723 e.buffer_mut().add_fold(2, 3, false);
8724 run_keys(&mut e, "zE");
8725 assert!(e.buffer().folds().is_empty());
8726 }
8727
8728 #[test]
8729 fn g_underscore_jumps_to_last_non_blank() {
8730 let mut e = editor_with("hello world ");
8731 run_keys(&mut e, "g_");
8732 assert_eq!(e.cursor().1, 10);
8734 }
8735
8736 #[test]
8737 fn gj_and_gk_alias_j_and_k() {
8738 let mut e = editor_with("a\nb\nc");
8739 run_keys(&mut e, "gj");
8740 assert_eq!(e.cursor().0, 1);
8741 run_keys(&mut e, "gk");
8742 assert_eq!(e.cursor().0, 0);
8743 }
8744
8745 #[test]
8746 fn paragraph_motions_walk_blank_lines() {
8747 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
8748 run_keys(&mut e, "}");
8749 assert_eq!(e.cursor().0, 2);
8750 run_keys(&mut e, "}");
8751 assert_eq!(e.cursor().0, 5);
8752 run_keys(&mut e, "{");
8753 assert_eq!(e.cursor().0, 2);
8754 }
8755
8756 #[test]
8757 fn gv_reenters_last_visual_selection() {
8758 let mut e = editor_with("alpha\nbeta\ngamma");
8759 run_keys(&mut e, "Vj");
8760 run_keys(&mut e, "<Esc>");
8762 assert_eq!(e.vim_mode(), VimMode::Normal);
8763 run_keys(&mut e, "gv");
8765 assert_eq!(e.vim_mode(), VimMode::VisualLine);
8766 }
8767
8768 #[test]
8769 fn o_in_visual_swaps_anchor_and_cursor() {
8770 let mut e = editor_with("hello world");
8771 run_keys(&mut e, "vllll");
8773 assert_eq!(e.cursor().1, 4);
8774 run_keys(&mut e, "o");
8776 assert_eq!(e.cursor().1, 0);
8777 assert_eq!(e.vim.visual_anchor, (0, 4));
8779 }
8780
8781 #[test]
8782 fn editing_inside_fold_invalidates_it() {
8783 let mut e = editor_with("a\nb\nc\nd");
8784 e.buffer_mut().add_fold(1, 2, true);
8785 e.jump_cursor(1, 0);
8786 run_keys(&mut e, "iX<Esc>");
8788 assert!(e.buffer().folds().is_empty());
8790 }
8791
8792 #[test]
8793 fn zd_removes_fold_under_cursor() {
8794 let mut e = editor_with("a\nb\nc\nd");
8795 e.buffer_mut().add_fold(1, 2, true);
8796 e.jump_cursor(2, 0);
8797 run_keys(&mut e, "zd");
8798 assert!(e.buffer().folds().is_empty());
8799 }
8800
8801 #[test]
8802 fn take_fold_ops_observes_z_keystroke_dispatch() {
8803 use crate::types::FoldOp;
8808 let mut e = editor_with("a\nb\nc\nd");
8809 e.buffer_mut().add_fold(1, 2, true);
8810 e.jump_cursor(1, 0);
8811 let _ = e.take_fold_ops();
8814 run_keys(&mut e, "zo");
8815 run_keys(&mut e, "zM");
8816 let ops = e.take_fold_ops();
8817 assert_eq!(ops.len(), 2);
8818 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
8819 assert!(matches!(ops[1], FoldOp::CloseAll));
8820 assert!(e.take_fold_ops().is_empty());
8822 }
8823
8824 #[test]
8825 fn edit_pipeline_emits_invalidate_fold_op() {
8826 use crate::types::FoldOp;
8829 let mut e = editor_with("a\nb\nc\nd");
8830 e.buffer_mut().add_fold(1, 2, true);
8831 e.jump_cursor(1, 0);
8832 let _ = e.take_fold_ops();
8833 run_keys(&mut e, "iX<Esc>");
8834 let ops = e.take_fold_ops();
8835 assert!(
8836 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
8837 "expected at least one Invalidate op, got {ops:?}"
8838 );
8839 }
8840
8841 #[test]
8842 fn dot_mark_jumps_to_last_edit_position() {
8843 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8844 e.jump_cursor(2, 0);
8845 run_keys(&mut e, "iX<Esc>");
8847 let after_edit = e.cursor();
8848 run_keys(&mut e, "gg");
8850 assert_eq!(e.cursor().0, 0);
8851 run_keys(&mut e, "'.");
8853 assert_eq!(e.cursor().0, after_edit.0);
8854 }
8855
8856 #[test]
8857 fn quote_quote_returns_to_pre_jump_position() {
8858 let mut e = editor_with_rows(50, 20);
8859 e.jump_cursor(10, 2);
8860 let before = e.cursor();
8861 run_keys(&mut e, "G");
8863 assert_ne!(e.cursor(), before);
8864 run_keys(&mut e, "''");
8866 assert_eq!(e.cursor().0, before.0);
8867 }
8868
8869 #[test]
8870 fn backtick_backtick_restores_exact_pre_jump_pos() {
8871 let mut e = editor_with_rows(50, 20);
8872 e.jump_cursor(7, 3);
8873 let before = e.cursor();
8874 run_keys(&mut e, "G");
8875 run_keys(&mut e, "``");
8876 assert_eq!(e.cursor(), before);
8877 }
8878
8879 #[test]
8880 fn macro_record_and_replay_basic() {
8881 let mut e = editor_with("foo\nbar\nbaz");
8882 run_keys(&mut e, "qaIX<Esc>jq");
8884 assert_eq!(e.buffer().lines()[0], "Xfoo");
8885 run_keys(&mut e, "@a");
8887 assert_eq!(e.buffer().lines()[1], "Xbar");
8888 run_keys(&mut e, "j@@");
8890 assert_eq!(e.buffer().lines()[2], "Xbaz");
8891 }
8892
8893 #[test]
8894 fn macro_count_replays_n_times() {
8895 let mut e = editor_with("a\nb\nc\nd\ne");
8896 run_keys(&mut e, "qajq");
8898 assert_eq!(e.cursor().0, 1);
8899 run_keys(&mut e, "3@a");
8901 assert_eq!(e.cursor().0, 4);
8902 }
8903
8904 #[test]
8905 fn macro_capital_q_appends_to_lowercase_register() {
8906 let mut e = editor_with("hello");
8907 run_keys(&mut e, "qall<Esc>q");
8908 run_keys(&mut e, "qAhh<Esc>q");
8909 let text = e.registers().read('a').unwrap().text.clone();
8912 assert!(text.contains("ll<Esc>"));
8913 assert!(text.contains("hh<Esc>"));
8914 }
8915
8916 #[test]
8917 fn buffer_selection_block_in_visual_block_mode() {
8918 use hjkl_buffer::{Position, Selection};
8919 let mut e = editor_with("aaaa\nbbbb\ncccc");
8920 run_keys(&mut e, "<C-v>jl");
8921 assert_eq!(
8922 e.buffer_selection(),
8923 Some(Selection::Block {
8924 anchor: Position::new(0, 0),
8925 head: Position::new(1, 1),
8926 })
8927 );
8928 }
8929
8930 #[test]
8933 fn n_after_question_mark_keeps_walking_backward() {
8934 let mut e = editor_with("foo bar foo baz foo end");
8937 e.jump_cursor(0, 22);
8938 run_keys(&mut e, "?foo<CR>");
8939 assert_eq!(e.cursor().1, 16);
8940 run_keys(&mut e, "n");
8941 assert_eq!(e.cursor().1, 8);
8942 run_keys(&mut e, "N");
8943 assert_eq!(e.cursor().1, 16);
8944 }
8945
8946 #[test]
8947 fn nested_macro_chord_records_literal_keys() {
8948 let mut e = editor_with("alpha\nbeta\ngamma");
8951 run_keys(&mut e, "qblq");
8953 run_keys(&mut e, "qaIX<Esc>q");
8956 e.jump_cursor(1, 0);
8958 run_keys(&mut e, "@a");
8959 assert_eq!(e.buffer().lines()[1], "Xbeta");
8960 }
8961
8962 #[test]
8963 fn shift_gt_motion_indents_one_line() {
8964 let mut e = editor_with("hello world");
8968 run_keys(&mut e, ">w");
8969 assert_eq!(e.buffer().lines()[0], " hello world");
8970 }
8971
8972 #[test]
8973 fn shift_lt_motion_outdents_one_line() {
8974 let mut e = editor_with(" hello world");
8975 run_keys(&mut e, "<lt>w");
8976 assert_eq!(e.buffer().lines()[0], " hello world");
8978 }
8979
8980 #[test]
8981 fn shift_gt_text_object_indents_paragraph() {
8982 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
8983 e.jump_cursor(0, 0);
8984 run_keys(&mut e, ">ip");
8985 assert_eq!(e.buffer().lines()[0], " alpha");
8986 assert_eq!(e.buffer().lines()[1], " beta");
8987 assert_eq!(e.buffer().lines()[2], " gamma");
8988 assert_eq!(e.buffer().lines()[4], "rest");
8990 }
8991
8992 #[test]
8993 fn ctrl_o_runs_exactly_one_normal_command() {
8994 let mut e = editor_with("alpha beta gamma");
8997 e.jump_cursor(0, 0);
8998 run_keys(&mut e, "i");
8999 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9000 run_keys(&mut e, "dw");
9001 assert_eq!(e.vim_mode(), VimMode::Insert);
9003 run_keys(&mut e, "X");
9005 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9006 }
9007
9008 #[test]
9009 fn macro_replay_respects_mode_switching() {
9010 let mut e = editor_with("hi");
9014 run_keys(&mut e, "qaiX<Esc>0q");
9015 assert_eq!(e.vim_mode(), VimMode::Normal);
9016 e.set_content("yo");
9018 run_keys(&mut e, "@a");
9019 assert_eq!(e.vim_mode(), VimMode::Normal);
9020 assert_eq!(e.cursor().1, 0);
9021 assert_eq!(e.buffer().lines()[0], "Xyo");
9022 }
9023
9024 #[test]
9025 fn macro_recorded_text_round_trips_through_register() {
9026 let mut e = editor_with("");
9030 run_keys(&mut e, "qaiX<Esc>q");
9031 let text = e.registers().read('a').unwrap().text.clone();
9032 assert!(text.starts_with("iX"));
9033 run_keys(&mut e, "@a");
9035 assert_eq!(e.buffer().lines()[0], "XX");
9036 }
9037
9038 #[test]
9039 fn dot_after_macro_replays_macros_last_change() {
9040 let mut e = editor_with("ab\ncd\nef");
9043 run_keys(&mut e, "qaIX<Esc>jq");
9046 assert_eq!(e.buffer().lines()[0], "Xab");
9047 run_keys(&mut e, "@a");
9048 assert_eq!(e.buffer().lines()[1], "Xcd");
9049 let row_before_dot = e.cursor().0;
9052 run_keys(&mut e, ".");
9053 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9054 }
9055}