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) last_insert_pos: Option<(usize, usize)>,
392 pub(super) change_list: Vec<(usize, usize)>,
396 pub(super) change_list_cursor: Option<usize>,
399 pub(super) last_visual: Option<LastVisual>,
402 pub(super) viewport_pinned: bool,
406 replaying: bool,
408 one_shot_normal: bool,
411 pub(super) search_prompt: Option<SearchPrompt>,
413 pub(super) last_search: Option<String>,
417 pub(super) last_search_forward: bool,
421 pub(super) jump_back: Vec<(usize, usize)>,
426 pub(super) jump_fwd: Vec<(usize, usize)>,
429 pub(super) insert_pending_register: bool,
433 pub(super) change_mark_start: Option<(usize, usize)>,
439 pub(super) search_history: Vec<String>,
443 pub(super) search_history_cursor: Option<usize>,
448 pub(super) last_input_at: Option<std::time::Instant>,
457 pub(super) last_input_host_at: Option<core::time::Duration>,
461}
462
463const SEARCH_HISTORY_MAX: usize = 100;
464pub(crate) const CHANGE_LIST_MAX: usize = 100;
465
466#[derive(Debug, Clone)]
469pub struct SearchPrompt {
470 pub text: String,
471 pub cursor: usize,
472 pub forward: bool,
473}
474
475#[derive(Debug, Clone)]
476struct InsertSession {
477 count: usize,
478 row_min: usize,
480 row_max: usize,
481 before_lines: Vec<String>,
485 reason: InsertReason,
486}
487
488#[derive(Debug, Clone)]
489enum InsertReason {
490 Enter(InsertEntry),
492 Open { above: bool },
494 AfterChange,
497 DeleteToEol,
499 ReplayOnly,
502 BlockEdge { top: usize, bot: usize, col: usize },
506 BlockChange { top: usize, bot: usize, col: usize },
511 Replace,
515}
516
517#[derive(Debug, Clone, Copy)]
527pub(super) struct LastVisual {
528 pub mode: Mode,
529 pub anchor: (usize, usize),
530 pub cursor: (usize, usize),
531 pub block_vcol: usize,
532}
533
534impl VimState {
535 pub fn public_mode(&self) -> VimMode {
536 match self.mode {
537 Mode::Normal => VimMode::Normal,
538 Mode::Insert => VimMode::Insert,
539 Mode::Visual => VimMode::Visual,
540 Mode::VisualLine => VimMode::VisualLine,
541 Mode::VisualBlock => VimMode::VisualBlock,
542 }
543 }
544
545 pub fn force_normal(&mut self) {
546 self.mode = Mode::Normal;
547 self.pending = Pending::None;
548 self.count = 0;
549 self.insert_session = None;
550 }
551
552 pub(crate) fn clear_pending_prefix(&mut self) {
562 self.pending = Pending::None;
563 self.count = 0;
564 self.pending_register = None;
565 self.insert_pending_register = false;
566 }
567
568 pub fn is_visual(&self) -> bool {
569 matches!(
570 self.mode,
571 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
572 )
573 }
574
575 pub fn is_visual_char(&self) -> bool {
576 self.mode == Mode::Visual
577 }
578
579 pub fn enter_visual(&mut self, anchor: (usize, usize)) {
580 self.visual_anchor = anchor;
581 self.mode = Mode::Visual;
582 }
583
584 pub(crate) fn pending_count_val(&self) -> Option<u32> {
587 if self.count == 0 {
588 None
589 } else {
590 Some(self.count as u32)
591 }
592 }
593
594 pub(crate) fn is_chord_pending(&self) -> bool {
597 !matches!(self.pending, Pending::None)
598 }
599
600 pub(crate) fn pending_op_char(&self) -> Option<char> {
604 let op = match &self.pending {
605 Pending::Op { op, .. }
606 | Pending::OpTextObj { op, .. }
607 | Pending::OpG { op, .. }
608 | Pending::OpFind { op, .. } => Some(*op),
609 _ => None,
610 };
611 op.map(|o| match o {
612 Operator::Delete => 'd',
613 Operator::Change => 'c',
614 Operator::Yank => 'y',
615 Operator::Uppercase => 'U',
616 Operator::Lowercase => 'u',
617 Operator::ToggleCase => '~',
618 Operator::Indent => '>',
619 Operator::Outdent => '<',
620 Operator::Fold => 'z',
621 Operator::Reflow => 'q',
622 })
623 }
624}
625
626fn enter_search<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, forward: bool) {
632 ed.vim.search_prompt = Some(SearchPrompt {
633 text: String::new(),
634 cursor: 0,
635 forward,
636 });
637 ed.vim.search_history_cursor = None;
638 ed.set_search_pattern(None);
642}
643
644fn push_search_pattern<H: crate::types::Host>(
649 ed: &mut Editor<hjkl_buffer::Buffer, H>,
650 pattern: &str,
651) {
652 let compiled = if pattern.is_empty() {
653 None
654 } else {
655 let case_insensitive = ed.settings().ignore_case
662 && !(ed.settings().smartcase && pattern.chars().any(|c| c.is_uppercase()));
663 let effective: std::borrow::Cow<'_, str> = if case_insensitive {
664 std::borrow::Cow::Owned(format!("(?i){pattern}"))
665 } else {
666 std::borrow::Cow::Borrowed(pattern)
667 };
668 regex::Regex::new(&effective).ok()
669 };
670 let wrap = ed.settings().wrapscan;
671 ed.set_search_pattern(compiled);
675 ed.search_state_mut().wrap_around = wrap;
676}
677
678fn step_search_prompt<H: crate::types::Host>(
679 ed: &mut Editor<hjkl_buffer::Buffer, H>,
680 input: Input,
681) -> bool {
682 let history_dir = match (input.key, input.ctrl) {
686 (Key::Char('p'), true) | (Key::Up, _) => Some(-1),
687 (Key::Char('n'), true) | (Key::Down, _) => Some(1),
688 _ => None,
689 };
690 if let Some(dir) = history_dir {
691 walk_search_history(ed, dir);
692 return true;
693 }
694 match input.key {
695 Key::Esc => {
696 let text = ed
699 .vim
700 .search_prompt
701 .take()
702 .map(|p| p.text)
703 .unwrap_or_default();
704 if !text.is_empty() {
705 ed.vim.last_search = Some(text);
706 }
707 ed.vim.search_history_cursor = None;
708 }
709 Key::Enter => {
710 let prompt = ed.vim.search_prompt.take();
711 if let Some(p) = prompt {
712 let pattern = if p.text.is_empty() {
715 ed.vim.last_search.clone()
716 } else {
717 Some(p.text.clone())
718 };
719 if let Some(pattern) = pattern {
720 push_search_pattern(ed, &pattern);
721 let pre = ed.cursor();
722 if p.forward {
723 ed.search_advance_forward(true);
724 } else {
725 ed.search_advance_backward(true);
726 }
727 ed.push_buffer_cursor_to_textarea();
728 if ed.cursor() != pre {
729 push_jump(ed, pre);
730 }
731 record_search_history(ed, &pattern);
732 ed.vim.last_search = Some(pattern);
733 ed.vim.last_search_forward = p.forward;
734 }
735 }
736 ed.vim.search_history_cursor = None;
737 }
738 Key::Backspace => {
739 ed.vim.search_history_cursor = None;
740 let new_text = ed.vim.search_prompt.as_mut().and_then(|p| {
741 if p.text.pop().is_some() {
742 p.cursor = p.text.chars().count();
743 Some(p.text.clone())
744 } else {
745 None
746 }
747 });
748 if let Some(text) = new_text {
749 push_search_pattern(ed, &text);
750 }
751 }
752 Key::Char(c) => {
753 ed.vim.search_history_cursor = None;
754 let new_text = ed.vim.search_prompt.as_mut().map(|p| {
755 p.text.push(c);
756 p.cursor = p.text.chars().count();
757 p.text.clone()
758 });
759 if let Some(text) = new_text {
760 push_search_pattern(ed, &text);
761 }
762 }
763 _ => {}
764 }
765 true
766}
767
768fn walk_change_list<H: crate::types::Host>(
772 ed: &mut Editor<hjkl_buffer::Buffer, H>,
773 dir: isize,
774 count: usize,
775) {
776 if ed.vim.change_list.is_empty() {
777 return;
778 }
779 let len = ed.vim.change_list.len();
780 let mut idx: isize = match (ed.vim.change_list_cursor, dir) {
781 (None, -1) => len as isize - 1,
782 (None, 1) => return, (Some(i), -1) => i as isize - 1,
784 (Some(i), 1) => i as isize + 1,
785 _ => return,
786 };
787 for _ in 1..count {
788 let next = idx + dir;
789 if next < 0 || next >= len as isize {
790 break;
791 }
792 idx = next;
793 }
794 if idx < 0 || idx >= len as isize {
795 return;
796 }
797 let idx = idx as usize;
798 ed.vim.change_list_cursor = Some(idx);
799 let (row, col) = ed.vim.change_list[idx];
800 ed.jump_cursor(row, col);
801}
802
803fn record_search_history<H: crate::types::Host>(
807 ed: &mut Editor<hjkl_buffer::Buffer, H>,
808 pattern: &str,
809) {
810 if pattern.is_empty() {
811 return;
812 }
813 if ed.vim.search_history.last().map(String::as_str) == Some(pattern) {
814 return;
815 }
816 ed.vim.search_history.push(pattern.to_string());
817 let len = ed.vim.search_history.len();
818 if len > SEARCH_HISTORY_MAX {
819 ed.vim.search_history.drain(0..len - SEARCH_HISTORY_MAX);
820 }
821}
822
823fn walk_search_history<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, dir: isize) {
829 if ed.vim.search_history.is_empty() || ed.vim.search_prompt.is_none() {
830 return;
831 }
832 let len = ed.vim.search_history.len();
833 let next_idx = match (ed.vim.search_history_cursor, dir) {
834 (None, -1) => Some(len - 1),
835 (None, 1) => return, (Some(i), -1) => i.checked_sub(1),
837 (Some(i), 1) if i + 1 < len => Some(i + 1),
838 _ => None,
839 };
840 let Some(idx) = next_idx else {
841 return;
842 };
843 ed.vim.search_history_cursor = Some(idx);
844 let text = ed.vim.search_history[idx].clone();
845 if let Some(prompt) = ed.vim.search_prompt.as_mut() {
846 prompt.cursor = text.chars().count();
847 prompt.text = text.clone();
848 }
849 push_search_pattern(ed, &text);
850}
851
852pub fn step<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, input: Input) -> bool {
853 ed.sync_buffer_content_from_textarea();
858 let now = std::time::Instant::now();
866 let host_now = ed.host.now();
867 let timed_out = match ed.vim.last_input_host_at {
868 Some(prev) => host_now.saturating_sub(prev) > ed.settings.timeout_len,
869 None => false,
870 };
871 if timed_out {
872 let chord_in_flight = !matches!(ed.vim.pending, Pending::None)
873 || ed.vim.count != 0
874 || ed.vim.pending_register.is_some()
875 || ed.vim.insert_pending_register;
876 if chord_in_flight {
877 ed.vim.clear_pending_prefix();
878 }
879 }
880 ed.vim.last_input_at = Some(now);
881 ed.vim.last_input_host_at = Some(host_now);
882 if ed.vim.recording_macro.is_some()
887 && !ed.vim.replaying_macro
888 && matches!(ed.vim.pending, Pending::None)
889 && ed.vim.mode != Mode::Insert
890 && input.key == Key::Char('q')
891 && !input.ctrl
892 && !input.alt
893 {
894 let reg = ed.vim.recording_macro.take().unwrap();
895 let keys = std::mem::take(&mut ed.vim.recording_keys);
896 let text = crate::input::encode_macro(&keys);
897 ed.set_named_register_text(reg.to_ascii_lowercase(), text);
898 return true;
899 }
900 if ed.vim.search_prompt.is_some() {
902 return step_search_prompt(ed, input);
903 }
904 let pending_was_macro_chord = matches!(
908 ed.vim.pending,
909 Pending::RecordMacroTarget | Pending::PlayMacroTarget { .. }
910 );
911 let was_insert = ed.vim.mode == Mode::Insert;
912 let pre_visual_snapshot = match ed.vim.mode {
915 Mode::Visual => Some(LastVisual {
916 mode: Mode::Visual,
917 anchor: ed.vim.visual_anchor,
918 cursor: ed.cursor(),
919 block_vcol: 0,
920 }),
921 Mode::VisualLine => Some(LastVisual {
922 mode: Mode::VisualLine,
923 anchor: (ed.vim.visual_line_anchor, 0),
924 cursor: ed.cursor(),
925 block_vcol: 0,
926 }),
927 Mode::VisualBlock => Some(LastVisual {
928 mode: Mode::VisualBlock,
929 anchor: ed.vim.block_anchor,
930 cursor: ed.cursor(),
931 block_vcol: ed.vim.block_vcol,
932 }),
933 _ => None,
934 };
935 let consumed = match ed.vim.mode {
936 Mode::Insert => step_insert(ed, input),
937 _ => step_normal(ed, input),
938 };
939 if let Some(snap) = pre_visual_snapshot
940 && !matches!(
941 ed.vim.mode,
942 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
943 )
944 {
945 let (lo, hi) = match snap.mode {
961 Mode::Visual => {
962 if snap.anchor <= snap.cursor {
963 (snap.anchor, snap.cursor)
964 } else {
965 (snap.cursor, snap.anchor)
966 }
967 }
968 Mode::VisualLine => {
969 let r_lo = snap.anchor.0.min(snap.cursor.0);
970 let r_hi = snap.anchor.0.max(snap.cursor.0);
971 let last_col = ed
972 .buffer()
973 .lines()
974 .get(r_hi)
975 .map(|l| l.chars().count().saturating_sub(1))
976 .unwrap_or(0);
977 ((r_lo, 0), (r_hi, last_col))
978 }
979 Mode::VisualBlock => {
980 let (r1, c1) = snap.anchor;
981 let (r2, c2) = snap.cursor;
982 ((r1.min(r2), c1.min(c2)), (r1.max(r2), c1.max(c2)))
983 }
984 _ => {
985 if snap.anchor <= snap.cursor {
988 (snap.anchor, snap.cursor)
989 } else {
990 (snap.cursor, snap.anchor)
991 }
992 }
993 };
994 ed.set_mark('<', lo);
995 ed.set_mark('>', hi);
996 ed.vim.last_visual = Some(snap);
997 }
998 if !was_insert
1002 && ed.vim.one_shot_normal
1003 && ed.vim.mode == Mode::Normal
1004 && matches!(ed.vim.pending, Pending::None)
1005 {
1006 ed.vim.one_shot_normal = false;
1007 ed.vim.mode = Mode::Insert;
1008 }
1009 ed.sync_buffer_content_from_textarea();
1015 if !ed.vim.viewport_pinned {
1019 ed.ensure_cursor_in_scrolloff();
1020 }
1021 ed.vim.viewport_pinned = false;
1022 if ed.vim.recording_macro.is_some()
1027 && !ed.vim.replaying_macro
1028 && input.key != Key::Char('q')
1029 && !pending_was_macro_chord
1030 {
1031 ed.vim.recording_keys.push(input);
1032 }
1033 consumed
1034}
1035
1036fn step_insert<H: crate::types::Host>(
1039 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1040 input: Input,
1041) -> bool {
1042 if ed.vim.insert_pending_register {
1046 ed.vim.insert_pending_register = false;
1047 if let Key::Char(c) = input.key
1048 && !input.ctrl
1049 {
1050 insert_register_text(ed, c);
1051 }
1052 return true;
1053 }
1054
1055 if input.key == Key::Esc {
1056 finish_insert_session(ed);
1057 ed.vim.mode = Mode::Normal;
1058 let col = ed.cursor().1;
1063 ed.vim.last_insert_pos = Some(ed.cursor());
1067 if col > 0 {
1068 crate::motions::move_left(&mut ed.buffer, 1);
1069 ed.push_buffer_cursor_to_textarea();
1070 }
1071 ed.sticky_col = Some(ed.cursor().1);
1072 return true;
1073 }
1074
1075 if input.ctrl {
1077 match input.key {
1078 Key::Char('w') => {
1079 use hjkl_buffer::{Edit, MotionKind};
1080 ed.sync_buffer_content_from_textarea();
1081 let cursor = buf_cursor_pos(&ed.buffer);
1082 if cursor.row == 0 && cursor.col == 0 {
1083 return true;
1084 }
1085 crate::motions::move_word_back(&mut ed.buffer, false, 1, &ed.settings.iskeyword);
1088 let word_start = buf_cursor_pos(&ed.buffer);
1089 if word_start == cursor {
1090 return true;
1091 }
1092 buf_set_cursor_pos(&mut ed.buffer, cursor);
1093 ed.mutate_edit(Edit::DeleteRange {
1094 start: word_start,
1095 end: cursor,
1096 kind: MotionKind::Char,
1097 });
1098 ed.push_buffer_cursor_to_textarea();
1099 return true;
1100 }
1101 Key::Char('u') => {
1102 use hjkl_buffer::{Edit, MotionKind, Position};
1103 ed.sync_buffer_content_from_textarea();
1104 let cursor = buf_cursor_pos(&ed.buffer);
1105 if cursor.col > 0 {
1106 ed.mutate_edit(Edit::DeleteRange {
1107 start: Position::new(cursor.row, 0),
1108 end: cursor,
1109 kind: MotionKind::Char,
1110 });
1111 ed.push_buffer_cursor_to_textarea();
1112 }
1113 return true;
1114 }
1115 Key::Char('h') => {
1116 use hjkl_buffer::{Edit, MotionKind, Position};
1117 ed.sync_buffer_content_from_textarea();
1118 let cursor = buf_cursor_pos(&ed.buffer);
1119 if cursor.col > 0 {
1120 ed.mutate_edit(Edit::DeleteRange {
1121 start: Position::new(cursor.row, cursor.col - 1),
1122 end: cursor,
1123 kind: MotionKind::Char,
1124 });
1125 } else if cursor.row > 0 {
1126 let prev_row = cursor.row - 1;
1127 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1128 ed.mutate_edit(Edit::JoinLines {
1129 row: prev_row,
1130 count: 1,
1131 with_space: false,
1132 });
1133 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1134 }
1135 ed.push_buffer_cursor_to_textarea();
1136 return true;
1137 }
1138 Key::Char('o') => {
1139 ed.vim.one_shot_normal = true;
1142 ed.vim.mode = Mode::Normal;
1143 return true;
1144 }
1145 Key::Char('r') => {
1146 ed.vim.insert_pending_register = true;
1149 return true;
1150 }
1151 Key::Char('t') => {
1152 let (row, col) = ed.cursor();
1157 let sw = ed.settings().shiftwidth;
1158 indent_rows(ed, row, row, 1);
1159 ed.jump_cursor(row, col + sw);
1160 return true;
1161 }
1162 Key::Char('d') => {
1163 let (row, col) = ed.cursor();
1167 let before_len = buf_line_bytes(&ed.buffer, row);
1168 outdent_rows(ed, row, row, 1);
1169 let after_len = buf_line_bytes(&ed.buffer, row);
1170 let stripped = before_len.saturating_sub(after_len);
1171 let new_col = col.saturating_sub(stripped);
1172 ed.jump_cursor(row, new_col);
1173 return true;
1174 }
1175 _ => {}
1176 }
1177 }
1178
1179 let (row, _) = ed.cursor();
1182 if let Some(ref mut session) = ed.vim.insert_session {
1183 session.row_min = session.row_min.min(row);
1184 session.row_max = session.row_max.max(row);
1185 }
1186 let mutated = handle_insert_key(ed, input);
1187 if mutated {
1188 ed.mark_content_dirty();
1189 let (row, _) = ed.cursor();
1190 if let Some(ref mut session) = ed.vim.insert_session {
1191 session.row_min = session.row_min.min(row);
1192 session.row_max = session.row_max.max(row);
1193 }
1194 }
1195 true
1196}
1197
1198fn insert_register_text<H: crate::types::Host>(
1203 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1204 selector: char,
1205) {
1206 use hjkl_buffer::Edit;
1207 let text = match ed.registers().read(selector) {
1208 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
1209 _ => return,
1210 };
1211 ed.sync_buffer_content_from_textarea();
1212 let cursor = buf_cursor_pos(&ed.buffer);
1213 ed.mutate_edit(Edit::InsertStr {
1214 at: cursor,
1215 text: text.clone(),
1216 });
1217 let mut row = cursor.row;
1220 let mut col = cursor.col;
1221 for ch in text.chars() {
1222 if ch == '\n' {
1223 row += 1;
1224 col = 0;
1225 } else {
1226 col += 1;
1227 }
1228 }
1229 buf_set_cursor_rc(&mut ed.buffer, row, col);
1230 ed.push_buffer_cursor_to_textarea();
1231 ed.mark_content_dirty();
1232 if let Some(ref mut session) = ed.vim.insert_session {
1233 session.row_min = session.row_min.min(row);
1234 session.row_max = session.row_max.max(row);
1235 }
1236}
1237
1238pub(super) fn compute_enter_indent(settings: &crate::editor::Settings, prev_line: &str) -> String {
1257 if !settings.autoindent {
1258 return String::new();
1259 }
1260 let base: String = prev_line
1262 .chars()
1263 .take_while(|c| *c == ' ' || *c == '\t')
1264 .collect();
1265
1266 if settings.smartindent {
1267 let last_non_ws = prev_line.chars().rev().find(|c| !c.is_whitespace());
1271 if matches!(last_non_ws, Some('{' | '(' | '[')) {
1272 let unit = if settings.expandtab {
1273 if settings.softtabstop > 0 {
1274 " ".repeat(settings.softtabstop)
1275 } else {
1276 " ".repeat(settings.shiftwidth)
1277 }
1278 } else {
1279 "\t".to_string()
1280 };
1281 return format!("{base}{unit}");
1282 }
1283 }
1284
1285 base
1286}
1287
1288fn try_dedent_close_bracket<H: crate::types::Host>(
1298 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1299 cursor: hjkl_buffer::Position,
1300 ch: char,
1301) -> bool {
1302 use hjkl_buffer::{Edit, MotionKind, Position};
1303
1304 if !ed.settings.smartindent {
1305 return false;
1306 }
1307 if !matches!(ch, '}' | ')' | ']') {
1308 return false;
1309 }
1310
1311 let line = match buf_line(&ed.buffer, cursor.row) {
1312 Some(l) => l.to_string(),
1313 None => return false,
1314 };
1315
1316 let before: String = line.chars().take(cursor.col).collect();
1318 if !before.chars().all(|c| c == ' ' || c == '\t') {
1319 return false;
1320 }
1321 if before.is_empty() {
1322 return false;
1324 }
1325
1326 let unit_len: usize = if ed.settings.expandtab {
1328 if ed.settings.softtabstop > 0 {
1329 ed.settings.softtabstop
1330 } else {
1331 ed.settings.shiftwidth
1332 }
1333 } else {
1334 1
1336 };
1337
1338 let strip_len = if ed.settings.expandtab {
1340 let spaces = before.chars().filter(|c| *c == ' ').count();
1342 if spaces < unit_len {
1343 return false;
1344 }
1345 unit_len
1346 } else {
1347 if !before.starts_with('\t') {
1349 return false;
1350 }
1351 1
1352 };
1353
1354 ed.mutate_edit(Edit::DeleteRange {
1356 start: Position::new(cursor.row, 0),
1357 end: Position::new(cursor.row, strip_len),
1358 kind: MotionKind::Char,
1359 });
1360 let new_col = cursor.col.saturating_sub(strip_len);
1365 ed.mutate_edit(Edit::InsertChar {
1366 at: Position::new(cursor.row, new_col),
1367 ch,
1368 });
1369 true
1370}
1371
1372fn handle_insert_key<H: crate::types::Host>(
1379 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1380 input: Input,
1381) -> bool {
1382 use hjkl_buffer::{Edit, MotionKind, Position};
1383 ed.sync_buffer_content_from_textarea();
1384 let cursor = buf_cursor_pos(&ed.buffer);
1385 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
1386 let in_replace = matches!(
1390 ed.vim.insert_session.as_ref().map(|s| &s.reason),
1391 Some(InsertReason::Replace)
1392 );
1393 let mutated = match input.key {
1394 Key::Char(c) if in_replace && cursor.col < line_chars => {
1395 ed.mutate_edit(Edit::DeleteRange {
1396 start: cursor,
1397 end: Position::new(cursor.row, cursor.col + 1),
1398 kind: MotionKind::Char,
1399 });
1400 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1401 true
1402 }
1403 Key::Char(c) => {
1404 if !try_dedent_close_bracket(ed, cursor, c) {
1405 ed.mutate_edit(Edit::InsertChar { at: cursor, ch: c });
1406 }
1407 true
1408 }
1409 Key::Enter => {
1410 let prev_line = buf_line(&ed.buffer, cursor.row)
1411 .unwrap_or_default()
1412 .to_string();
1413 let indent = compute_enter_indent(&ed.settings, &prev_line);
1414 let text = format!("\n{indent}");
1415 ed.mutate_edit(Edit::InsertStr { at: cursor, text });
1416 true
1417 }
1418 Key::Tab => {
1419 if ed.settings.expandtab {
1420 let sts = ed.settings.softtabstop;
1423 let n = if sts > 0 {
1424 sts - (cursor.col % sts)
1425 } else {
1426 ed.settings.tabstop.max(1)
1427 };
1428 ed.mutate_edit(Edit::InsertStr {
1429 at: cursor,
1430 text: " ".repeat(n),
1431 });
1432 } else {
1433 ed.mutate_edit(Edit::InsertChar {
1434 at: cursor,
1435 ch: '\t',
1436 });
1437 }
1438 true
1439 }
1440 Key::Backspace => {
1441 let sts = ed.settings.softtabstop;
1445 if sts > 0 && cursor.col >= sts && cursor.col.is_multiple_of(sts) {
1446 let line = buf_line(&ed.buffer, cursor.row).unwrap_or_default();
1447 let chars: Vec<char> = line.chars().collect();
1448 let run_start = cursor.col - sts;
1449 if (run_start..cursor.col).all(|i| chars.get(i).copied() == Some(' ')) {
1450 ed.mutate_edit(Edit::DeleteRange {
1451 start: Position::new(cursor.row, run_start),
1452 end: cursor,
1453 kind: MotionKind::Char,
1454 });
1455 return true;
1456 }
1457 }
1458 if cursor.col > 0 {
1459 ed.mutate_edit(Edit::DeleteRange {
1460 start: Position::new(cursor.row, cursor.col - 1),
1461 end: cursor,
1462 kind: MotionKind::Char,
1463 });
1464 true
1465 } else if cursor.row > 0 {
1466 let prev_row = cursor.row - 1;
1467 let prev_chars = buf_line_chars(&ed.buffer, prev_row);
1468 ed.mutate_edit(Edit::JoinLines {
1469 row: prev_row,
1470 count: 1,
1471 with_space: false,
1472 });
1473 buf_set_cursor_rc(&mut ed.buffer, prev_row, prev_chars);
1474 true
1475 } else {
1476 false
1477 }
1478 }
1479 Key::Delete => {
1480 if cursor.col < line_chars {
1481 ed.mutate_edit(Edit::DeleteRange {
1482 start: cursor,
1483 end: Position::new(cursor.row, cursor.col + 1),
1484 kind: MotionKind::Char,
1485 });
1486 true
1487 } else if cursor.row + 1 < buf_row_count(&ed.buffer) {
1488 ed.mutate_edit(Edit::JoinLines {
1489 row: cursor.row,
1490 count: 1,
1491 with_space: false,
1492 });
1493 buf_set_cursor_pos(&mut ed.buffer, cursor);
1494 true
1495 } else {
1496 false
1497 }
1498 }
1499 Key::Left => {
1500 crate::motions::move_left(&mut ed.buffer, 1);
1501 break_undo_group_in_insert(ed);
1502 false
1503 }
1504 Key::Right => {
1505 crate::motions::move_right_to_end(&mut ed.buffer, 1);
1508 break_undo_group_in_insert(ed);
1509 false
1510 }
1511 Key::Up => {
1512 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1513 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1514 break_undo_group_in_insert(ed);
1515 false
1516 }
1517 Key::Down => {
1518 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
1519 crate::motions::move_down(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
1520 break_undo_group_in_insert(ed);
1521 false
1522 }
1523 Key::Home => {
1524 crate::motions::move_line_start(&mut ed.buffer);
1525 break_undo_group_in_insert(ed);
1526 false
1527 }
1528 Key::End => {
1529 crate::motions::move_line_end(&mut ed.buffer);
1530 break_undo_group_in_insert(ed);
1531 false
1532 }
1533 Key::PageUp => {
1534 let rows = viewport_full_rows(ed, 1) as isize;
1538 scroll_cursor_rows(ed, -rows);
1539 return false;
1540 }
1541 Key::PageDown => {
1542 let rows = viewport_full_rows(ed, 1) as isize;
1543 scroll_cursor_rows(ed, rows);
1544 return false;
1545 }
1546 _ => false,
1549 };
1550 ed.push_buffer_cursor_to_textarea();
1551 mutated
1552}
1553
1554fn finish_insert_session<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
1555 let Some(session) = ed.vim.insert_session.take() else {
1556 return;
1557 };
1558 let lines = buf_lines_to_vec(&ed.buffer);
1559 let after_end = session.row_max.min(lines.len().saturating_sub(1));
1563 let before_end = session
1564 .row_max
1565 .min(session.before_lines.len().saturating_sub(1));
1566 let before = if before_end >= session.row_min && session.row_min < session.before_lines.len() {
1567 session.before_lines[session.row_min..=before_end].join("\n")
1568 } else {
1569 String::new()
1570 };
1571 let after = if after_end >= session.row_min && session.row_min < lines.len() {
1572 lines[session.row_min..=after_end].join("\n")
1573 } else {
1574 String::new()
1575 };
1576 let inserted = extract_inserted(&before, &after);
1577 if !inserted.is_empty() && session.count > 1 && !ed.vim.replaying {
1578 use hjkl_buffer::{Edit, Position};
1579 for _ in 0..session.count - 1 {
1580 let (row, col) = ed.cursor();
1581 ed.mutate_edit(Edit::InsertStr {
1582 at: Position::new(row, col),
1583 text: inserted.clone(),
1584 });
1585 }
1586 }
1587 fn replicate_block_text<H: crate::types::Host>(
1591 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1592 inserted: &str,
1593 top: usize,
1594 bot: usize,
1595 col: usize,
1596 ) {
1597 use hjkl_buffer::{Edit, Position};
1598 for r in (top + 1)..=bot {
1599 let line_len = buf_line_chars(&ed.buffer, r);
1600 if col > line_len {
1601 let pad: String = std::iter::repeat_n(' ', col - line_len).collect();
1602 ed.mutate_edit(Edit::InsertStr {
1603 at: Position::new(r, line_len),
1604 text: pad,
1605 });
1606 }
1607 ed.mutate_edit(Edit::InsertStr {
1608 at: Position::new(r, col),
1609 text: inserted.to_string(),
1610 });
1611 }
1612 }
1613
1614 if let InsertReason::BlockEdge { top, bot, col } = session.reason {
1615 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1618 replicate_block_text(ed, &inserted, top, bot, col);
1619 buf_set_cursor_rc(&mut ed.buffer, top, col);
1620 ed.push_buffer_cursor_to_textarea();
1621 }
1622 return;
1623 }
1624 if let InsertReason::BlockChange { top, bot, col } = session.reason {
1625 if !inserted.is_empty() && top < bot && !ed.vim.replaying {
1629 replicate_block_text(ed, &inserted, top, bot, col);
1630 let ins_chars = inserted.chars().count();
1631 let line_len = buf_line_chars(&ed.buffer, top);
1632 let target_col = (col + ins_chars).min(line_len);
1633 buf_set_cursor_rc(&mut ed.buffer, top, target_col);
1634 ed.push_buffer_cursor_to_textarea();
1635 }
1636 return;
1637 }
1638 if ed.vim.replaying {
1639 return;
1640 }
1641 match session.reason {
1642 InsertReason::Enter(entry) => {
1643 ed.vim.last_change = Some(LastChange::InsertAt {
1644 entry,
1645 inserted,
1646 count: session.count,
1647 });
1648 }
1649 InsertReason::Open { above } => {
1650 ed.vim.last_change = Some(LastChange::OpenLine { above, inserted });
1651 }
1652 InsertReason::AfterChange => {
1653 if let Some(
1654 LastChange::OpMotion { inserted: ins, .. }
1655 | LastChange::OpTextObj { inserted: ins, .. }
1656 | LastChange::LineOp { inserted: ins, .. },
1657 ) = ed.vim.last_change.as_mut()
1658 {
1659 *ins = Some(inserted);
1660 }
1661 if let Some(start) = ed.vim.change_mark_start.take() {
1667 let end = ed.cursor();
1668 ed.set_mark('[', start);
1669 ed.set_mark(']', end);
1670 }
1671 }
1672 InsertReason::DeleteToEol => {
1673 ed.vim.last_change = Some(LastChange::DeleteToEol {
1674 inserted: Some(inserted),
1675 });
1676 }
1677 InsertReason::ReplayOnly => {}
1678 InsertReason::BlockEdge { .. } => unreachable!("handled above"),
1679 InsertReason::BlockChange { .. } => unreachable!("handled above"),
1680 InsertReason::Replace => {
1681 ed.vim.last_change = Some(LastChange::DeleteToEol {
1686 inserted: Some(inserted),
1687 });
1688 }
1689 }
1690}
1691
1692fn begin_insert<H: crate::types::Host>(
1693 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1694 count: usize,
1695 reason: InsertReason,
1696) {
1697 let record = !matches!(reason, InsertReason::ReplayOnly);
1698 if record {
1699 ed.push_undo();
1700 }
1701 let reason = if ed.vim.replaying {
1702 InsertReason::ReplayOnly
1703 } else {
1704 reason
1705 };
1706 let (row, _) = ed.cursor();
1707 ed.vim.insert_session = Some(InsertSession {
1708 count,
1709 row_min: row,
1710 row_max: row,
1711 before_lines: buf_lines_to_vec(&ed.buffer),
1712 reason,
1713 });
1714 ed.vim.mode = Mode::Insert;
1715}
1716
1717pub(crate) fn break_undo_group_in_insert<H: crate::types::Host>(
1732 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1733) {
1734 if !ed.settings.undo_break_on_motion {
1735 return;
1736 }
1737 if ed.vim.replaying {
1738 return;
1739 }
1740 if ed.vim.insert_session.is_none() {
1741 return;
1742 }
1743 ed.push_undo();
1744 let n = crate::types::Query::line_count(&ed.buffer) as usize;
1745 let mut lines: Vec<String> = Vec::with_capacity(n);
1746 for r in 0..n {
1747 lines.push(crate::types::Query::line(&ed.buffer, r as u32).to_string());
1748 }
1749 let row = crate::types::Cursor::cursor(&ed.buffer).line as usize;
1750 if let Some(ref mut session) = ed.vim.insert_session {
1751 session.before_lines = lines;
1752 session.row_min = row;
1753 session.row_max = row;
1754 }
1755}
1756
1757fn step_normal<H: crate::types::Host>(
1760 ed: &mut Editor<hjkl_buffer::Buffer, H>,
1761 input: Input,
1762) -> bool {
1763 if let Key::Char(d @ '0'..='9') = input.key
1765 && !input.ctrl
1766 && !input.alt
1767 && !matches!(
1768 ed.vim.pending,
1769 Pending::Replace
1770 | Pending::Find { .. }
1771 | Pending::OpFind { .. }
1772 | Pending::VisualTextObj { .. }
1773 )
1774 && (d != '0' || ed.vim.count > 0)
1775 {
1776 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
1777 return true;
1778 }
1779
1780 match std::mem::take(&mut ed.vim.pending) {
1782 Pending::Replace => return handle_replace(ed, input),
1783 Pending::Find { forward, till } => return handle_find_target(ed, input, forward, till),
1784 Pending::OpFind {
1785 op,
1786 count1,
1787 forward,
1788 till,
1789 } => return handle_op_find_target(ed, input, op, count1, forward, till),
1790 Pending::G => return handle_after_g(ed, input),
1791 Pending::OpG { op, count1 } => return handle_op_after_g(ed, input, op, count1),
1792 Pending::Op { op, count1 } => return handle_after_op(ed, input, op, count1),
1793 Pending::OpTextObj { op, count1, inner } => {
1794 return handle_text_object(ed, input, op, count1, inner);
1795 }
1796 Pending::VisualTextObj { inner } => {
1797 return handle_visual_text_obj(ed, input, inner);
1798 }
1799 Pending::Z => return handle_after_z(ed, input),
1800 Pending::SetMark => return handle_set_mark(ed, input),
1801 Pending::GotoMarkLine => return handle_goto_mark(ed, input, true),
1802 Pending::GotoMarkChar => return handle_goto_mark(ed, input, false),
1803 Pending::SelectRegister => return handle_select_register(ed, input),
1804 Pending::RecordMacroTarget => return handle_record_macro_target(ed, input),
1805 Pending::PlayMacroTarget { count } => return handle_play_macro_target(ed, input, count),
1806 Pending::None => {}
1807 }
1808
1809 let count = take_count(&mut ed.vim);
1810
1811 match input.key {
1813 Key::Esc => {
1814 ed.vim.force_normal();
1815 return true;
1816 }
1817 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1818 ed.vim.visual_anchor = ed.cursor();
1819 ed.vim.mode = Mode::Visual;
1820 return true;
1821 }
1822 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Normal => {
1823 let (row, _) = ed.cursor();
1824 ed.vim.visual_line_anchor = row;
1825 ed.vim.mode = Mode::VisualLine;
1826 return true;
1827 }
1828 Key::Char('v') if !input.ctrl && ed.vim.mode == Mode::VisualLine => {
1829 ed.vim.visual_anchor = ed.cursor();
1830 ed.vim.mode = Mode::Visual;
1831 return true;
1832 }
1833 Key::Char('V') if !input.ctrl && ed.vim.mode == Mode::Visual => {
1834 let (row, _) = ed.cursor();
1835 ed.vim.visual_line_anchor = row;
1836 ed.vim.mode = Mode::VisualLine;
1837 return true;
1838 }
1839 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::Normal => {
1840 let cur = ed.cursor();
1841 ed.vim.block_anchor = cur;
1842 ed.vim.block_vcol = cur.1;
1843 ed.vim.mode = Mode::VisualBlock;
1844 return true;
1845 }
1846 Key::Char('v') if input.ctrl && ed.vim.mode == Mode::VisualBlock => {
1847 ed.vim.mode = Mode::Normal;
1849 return true;
1850 }
1851 Key::Char('o') if !input.ctrl => match ed.vim.mode {
1854 Mode::Visual => {
1855 let cur = ed.cursor();
1856 let anchor = ed.vim.visual_anchor;
1857 ed.vim.visual_anchor = cur;
1858 ed.jump_cursor(anchor.0, anchor.1);
1859 return true;
1860 }
1861 Mode::VisualLine => {
1862 let cur_row = ed.cursor().0;
1863 let anchor_row = ed.vim.visual_line_anchor;
1864 ed.vim.visual_line_anchor = cur_row;
1865 ed.jump_cursor(anchor_row, 0);
1866 return true;
1867 }
1868 Mode::VisualBlock => {
1869 let cur = ed.cursor();
1870 let anchor = ed.vim.block_anchor;
1871 ed.vim.block_anchor = cur;
1872 ed.vim.block_vcol = anchor.1;
1873 ed.jump_cursor(anchor.0, anchor.1);
1874 return true;
1875 }
1876 _ => {}
1877 },
1878 _ => {}
1879 }
1880
1881 if ed.vim.is_visual()
1883 && let Some(op) = visual_operator(&input)
1884 {
1885 apply_visual_operator(ed, op);
1886 return true;
1887 }
1888
1889 if ed.vim.mode == Mode::VisualBlock && !input.ctrl {
1893 match input.key {
1894 Key::Char('r') => {
1895 ed.vim.pending = Pending::Replace;
1896 return true;
1897 }
1898 Key::Char('I') => {
1899 let (top, bot, left, _right) = block_bounds(ed);
1900 ed.jump_cursor(top, left);
1901 ed.vim.mode = Mode::Normal;
1902 begin_insert(
1903 ed,
1904 1,
1905 InsertReason::BlockEdge {
1906 top,
1907 bot,
1908 col: left,
1909 },
1910 );
1911 return true;
1912 }
1913 Key::Char('A') => {
1914 let (top, bot, _left, right) = block_bounds(ed);
1915 let line_len = buf_line_chars(&ed.buffer, top);
1916 let col = (right + 1).min(line_len);
1917 ed.jump_cursor(top, col);
1918 ed.vim.mode = Mode::Normal;
1919 begin_insert(ed, 1, InsertReason::BlockEdge { top, bot, col });
1920 return true;
1921 }
1922 _ => {}
1923 }
1924 }
1925
1926 if matches!(ed.vim.mode, Mode::Visual | Mode::VisualLine)
1928 && !input.ctrl
1929 && matches!(input.key, Key::Char('i') | Key::Char('a'))
1930 {
1931 let inner = matches!(input.key, Key::Char('i'));
1932 ed.vim.pending = Pending::VisualTextObj { inner };
1933 return true;
1934 }
1935
1936 if input.ctrl
1941 && let Key::Char(c) = input.key
1942 {
1943 match c {
1944 'd' => {
1945 scroll_cursor_rows(ed, viewport_half_rows(ed, count) as isize);
1946 return true;
1947 }
1948 'u' => {
1949 scroll_cursor_rows(ed, -(viewport_half_rows(ed, count) as isize));
1950 return true;
1951 }
1952 'f' => {
1953 scroll_cursor_rows(ed, viewport_full_rows(ed, count) as isize);
1954 return true;
1955 }
1956 'b' => {
1957 scroll_cursor_rows(ed, -(viewport_full_rows(ed, count) as isize));
1958 return true;
1959 }
1960 'r' => {
1961 do_redo(ed);
1962 return true;
1963 }
1964 'a' if ed.vim.mode == Mode::Normal => {
1965 adjust_number(ed, count.max(1) as i64);
1966 return true;
1967 }
1968 'x' if ed.vim.mode == Mode::Normal => {
1969 adjust_number(ed, -(count.max(1) as i64));
1970 return true;
1971 }
1972 'o' if ed.vim.mode == Mode::Normal => {
1973 for _ in 0..count.max(1) {
1974 jump_back(ed);
1975 }
1976 return true;
1977 }
1978 'i' if ed.vim.mode == Mode::Normal => {
1979 for _ in 0..count.max(1) {
1980 jump_forward(ed);
1981 }
1982 return true;
1983 }
1984 _ => {}
1985 }
1986 }
1987
1988 if !input.ctrl && input.key == Key::Tab && ed.vim.mode == Mode::Normal {
1990 for _ in 0..count.max(1) {
1991 jump_forward(ed);
1992 }
1993 return true;
1994 }
1995
1996 if let Some(motion) = parse_motion(&input) {
1998 execute_motion(ed, motion.clone(), count);
1999 if ed.vim.mode == Mode::VisualBlock {
2001 update_block_vcol(ed, &motion);
2002 }
2003 if let Motion::Find { ch, forward, till } = motion {
2004 ed.vim.last_find = Some((ch, forward, till));
2005 }
2006 return true;
2007 }
2008
2009 if ed.vim.mode == Mode::Normal && handle_normal_only(ed, &input, count) {
2011 return true;
2012 }
2013
2014 if ed.vim.mode == Mode::Normal
2016 && let Key::Char(op_ch) = input.key
2017 && !input.ctrl
2018 && let Some(op) = char_to_operator(op_ch)
2019 {
2020 ed.vim.pending = Pending::Op { op, count1: count };
2021 return true;
2022 }
2023
2024 if ed.vim.mode == Mode::Normal
2026 && let Some((forward, till)) = find_entry(&input)
2027 {
2028 ed.vim.count = count;
2029 ed.vim.pending = Pending::Find { forward, till };
2030 return true;
2031 }
2032
2033 if !input.ctrl && input.key == Key::Char('g') && ed.vim.mode == Mode::Normal {
2035 ed.vim.count = count;
2036 ed.vim.pending = Pending::G;
2037 return true;
2038 }
2039
2040 if !input.ctrl
2042 && input.key == Key::Char('z')
2043 && matches!(
2044 ed.vim.mode,
2045 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2046 )
2047 {
2048 ed.vim.pending = Pending::Z;
2049 return true;
2050 }
2051
2052 if !input.ctrl
2058 && matches!(
2059 ed.vim.mode,
2060 Mode::Normal | Mode::Visual | Mode::VisualLine | Mode::VisualBlock
2061 )
2062 && input.key == Key::Char('`')
2063 {
2064 ed.vim.pending = Pending::GotoMarkChar;
2065 return true;
2066 }
2067 if !input.ctrl && ed.vim.mode == Mode::Normal {
2068 match input.key {
2069 Key::Char('m') => {
2070 ed.vim.pending = Pending::SetMark;
2071 return true;
2072 }
2073 Key::Char('\'') => {
2074 ed.vim.pending = Pending::GotoMarkLine;
2075 return true;
2076 }
2077 Key::Char('`') => {
2078 ed.vim.pending = Pending::GotoMarkChar;
2080 return true;
2081 }
2082 Key::Char('"') => {
2083 ed.vim.pending = Pending::SelectRegister;
2086 return true;
2087 }
2088 Key::Char('@') => {
2089 ed.vim.pending = Pending::PlayMacroTarget { count };
2093 return true;
2094 }
2095 Key::Char('q') if ed.vim.recording_macro.is_none() => {
2096 ed.vim.pending = Pending::RecordMacroTarget;
2101 return true;
2102 }
2103 _ => {}
2104 }
2105 }
2106
2107 true
2109}
2110
2111fn handle_set_mark<H: crate::types::Host>(
2112 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2113 input: Input,
2114) -> bool {
2115 if let Key::Char(c) = input.key
2116 && (c.is_ascii_lowercase() || c.is_ascii_uppercase())
2117 {
2118 let pos = ed.cursor();
2123 ed.set_mark(c, pos);
2124 }
2125 true
2126}
2127
2128fn handle_select_register<H: crate::types::Host>(
2132 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2133 input: Input,
2134) -> bool {
2135 if let Key::Char(c) = input.key
2136 && (c.is_ascii_alphanumeric() || matches!(c, '"' | '+' | '*' | '_'))
2137 {
2138 ed.vim.pending_register = Some(c);
2139 }
2140 true
2141}
2142
2143fn handle_record_macro_target<H: crate::types::Host>(
2148 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2149 input: Input,
2150) -> bool {
2151 if let Key::Char(c) = input.key
2152 && (c.is_ascii_alphabetic() || c.is_ascii_digit())
2153 {
2154 ed.vim.recording_macro = Some(c);
2155 if c.is_ascii_uppercase() {
2158 let lower = c.to_ascii_lowercase();
2159 let text = ed
2163 .registers()
2164 .read(lower)
2165 .map(|s| s.text.clone())
2166 .unwrap_or_default();
2167 ed.vim.recording_keys = crate::input::decode_macro(&text);
2168 } else {
2169 ed.vim.recording_keys.clear();
2170 }
2171 }
2172 true
2173}
2174
2175fn handle_play_macro_target<H: crate::types::Host>(
2181 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2182 input: Input,
2183 count: usize,
2184) -> bool {
2185 let reg = match input.key {
2186 Key::Char('@') => ed.vim.last_macro,
2187 Key::Char(c) if c.is_ascii_alphabetic() || c.is_ascii_digit() => {
2188 Some(c.to_ascii_lowercase())
2189 }
2190 _ => None,
2191 };
2192 let Some(reg) = reg else {
2193 return true;
2194 };
2195 let text = match ed.registers().read(reg) {
2198 Some(slot) if !slot.text.is_empty() => slot.text.clone(),
2199 _ => return true,
2200 };
2201 let keys = crate::input::decode_macro(&text);
2202 ed.vim.last_macro = Some(reg);
2203 let times = count.max(1);
2204 let was_replaying = ed.vim.replaying_macro;
2205 ed.vim.replaying_macro = true;
2206 for _ in 0..times {
2207 for k in keys.iter().copied() {
2208 step(ed, k);
2209 }
2210 }
2211 ed.vim.replaying_macro = was_replaying;
2212 true
2213}
2214
2215fn handle_goto_mark<H: crate::types::Host>(
2216 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2217 input: Input,
2218 linewise: bool,
2219) -> bool {
2220 let Key::Char(c) = input.key else {
2221 return true;
2222 };
2223 let target = match c {
2230 'a'..='z' | 'A'..='Z' => ed.mark(c),
2231 '\'' | '`' => ed.vim.jump_back.last().copied(),
2232 '.' => ed.vim.last_edit_pos,
2233 '[' | ']' | '<' | '>' => ed.mark(c),
2238 _ => None,
2239 };
2240 let Some((row, col)) = target else {
2241 return true;
2242 };
2243 let pre = ed.cursor();
2244 let (r, c_clamped) = clamp_pos(ed, (row, col));
2245 if linewise {
2246 buf_set_cursor_rc(&mut ed.buffer, r, 0);
2247 ed.push_buffer_cursor_to_textarea();
2248 move_first_non_whitespace(ed);
2249 } else {
2250 buf_set_cursor_rc(&mut ed.buffer, r, c_clamped);
2251 ed.push_buffer_cursor_to_textarea();
2252 }
2253 if ed.cursor() != pre {
2254 push_jump(ed, pre);
2255 }
2256 ed.sticky_col = Some(ed.cursor().1);
2257 true
2258}
2259
2260fn take_count(vim: &mut VimState) -> usize {
2261 if vim.count > 0 {
2262 let n = vim.count;
2263 vim.count = 0;
2264 n
2265 } else {
2266 1
2267 }
2268}
2269
2270fn char_to_operator(c: char) -> Option<Operator> {
2271 match c {
2272 'd' => Some(Operator::Delete),
2273 'c' => Some(Operator::Change),
2274 'y' => Some(Operator::Yank),
2275 '>' => Some(Operator::Indent),
2276 '<' => Some(Operator::Outdent),
2277 _ => None,
2278 }
2279}
2280
2281fn visual_operator(input: &Input) -> Option<Operator> {
2282 if input.ctrl {
2283 return None;
2284 }
2285 match input.key {
2286 Key::Char('y') => Some(Operator::Yank),
2287 Key::Char('d') | Key::Char('x') => Some(Operator::Delete),
2288 Key::Char('c') | Key::Char('s') => Some(Operator::Change),
2289 Key::Char('U') => Some(Operator::Uppercase),
2291 Key::Char('u') => Some(Operator::Lowercase),
2292 Key::Char('~') => Some(Operator::ToggleCase),
2293 Key::Char('>') => Some(Operator::Indent),
2295 Key::Char('<') => Some(Operator::Outdent),
2296 _ => None,
2297 }
2298}
2299
2300fn find_entry(input: &Input) -> Option<(bool, bool)> {
2301 if input.ctrl {
2302 return None;
2303 }
2304 match input.key {
2305 Key::Char('f') => Some((true, false)),
2306 Key::Char('F') => Some((false, false)),
2307 Key::Char('t') => Some((true, true)),
2308 Key::Char('T') => Some((false, true)),
2309 _ => None,
2310 }
2311}
2312
2313const JUMPLIST_MAX: usize = 100;
2317
2318fn push_jump<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, from: (usize, usize)) {
2323 ed.vim.jump_back.push(from);
2324 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2325 ed.vim.jump_back.remove(0);
2326 }
2327 ed.vim.jump_fwd.clear();
2328}
2329
2330fn jump_back<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2333 let Some(target) = ed.vim.jump_back.pop() else {
2334 return;
2335 };
2336 let cur = ed.cursor();
2337 ed.vim.jump_fwd.push(cur);
2338 let (r, c) = clamp_pos(ed, target);
2339 ed.jump_cursor(r, c);
2340 ed.sticky_col = Some(c);
2341}
2342
2343fn jump_forward<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2346 let Some(target) = ed.vim.jump_fwd.pop() else {
2347 return;
2348 };
2349 let cur = ed.cursor();
2350 ed.vim.jump_back.push(cur);
2351 if ed.vim.jump_back.len() > JUMPLIST_MAX {
2352 ed.vim.jump_back.remove(0);
2353 }
2354 let (r, c) = clamp_pos(ed, target);
2355 ed.jump_cursor(r, c);
2356 ed.sticky_col = Some(c);
2357}
2358
2359fn clamp_pos<H: crate::types::Host>(
2362 ed: &Editor<hjkl_buffer::Buffer, H>,
2363 pos: (usize, usize),
2364) -> (usize, usize) {
2365 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2366 let r = pos.0.min(last_row);
2367 let line_len = buf_line_chars(&ed.buffer, r);
2368 let c = pos.1.min(line_len.saturating_sub(1));
2369 (r, c)
2370}
2371
2372fn is_big_jump(motion: &Motion) -> bool {
2374 matches!(
2375 motion,
2376 Motion::FileTop
2377 | Motion::FileBottom
2378 | Motion::MatchBracket
2379 | Motion::WordAtCursor { .. }
2380 | Motion::SearchNext { .. }
2381 | Motion::ViewportTop
2382 | Motion::ViewportMiddle
2383 | Motion::ViewportBottom
2384 )
2385}
2386
2387fn viewport_half_rows<H: crate::types::Host>(
2392 ed: &Editor<hjkl_buffer::Buffer, H>,
2393 count: usize,
2394) -> usize {
2395 let h = ed.viewport_height_value() as usize;
2396 (h / 2).max(1).saturating_mul(count.max(1))
2397}
2398
2399fn viewport_full_rows<H: crate::types::Host>(
2402 ed: &Editor<hjkl_buffer::Buffer, H>,
2403 count: usize,
2404) -> usize {
2405 let h = ed.viewport_height_value() as usize;
2406 h.saturating_sub(2).max(1).saturating_mul(count.max(1))
2407}
2408
2409fn scroll_cursor_rows<H: crate::types::Host>(
2414 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2415 delta: isize,
2416) {
2417 if delta == 0 {
2418 return;
2419 }
2420 ed.sync_buffer_content_from_textarea();
2421 let (row, _) = ed.cursor();
2422 let last_row = buf_row_count(&ed.buffer).saturating_sub(1);
2423 let target = (row as isize + delta).max(0).min(last_row as isize) as usize;
2424 buf_set_cursor_rc(&mut ed.buffer, target, 0);
2425 crate::motions::move_first_non_blank(&mut ed.buffer);
2426 ed.push_buffer_cursor_to_textarea();
2427 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
2428}
2429
2430fn parse_motion(input: &Input) -> Option<Motion> {
2433 if input.ctrl {
2434 return None;
2435 }
2436 match input.key {
2437 Key::Char('h') | Key::Backspace | Key::Left => Some(Motion::Left),
2438 Key::Char('l') | Key::Right => Some(Motion::Right),
2439 Key::Char('j') | Key::Down | Key::Enter => Some(Motion::Down),
2440 Key::Char('k') | Key::Up => Some(Motion::Up),
2441 Key::Char('w') => Some(Motion::WordFwd),
2442 Key::Char('W') => Some(Motion::BigWordFwd),
2443 Key::Char('b') => Some(Motion::WordBack),
2444 Key::Char('B') => Some(Motion::BigWordBack),
2445 Key::Char('e') => Some(Motion::WordEnd),
2446 Key::Char('E') => Some(Motion::BigWordEnd),
2447 Key::Char('0') | Key::Home => Some(Motion::LineStart),
2448 Key::Char('^') => Some(Motion::FirstNonBlank),
2449 Key::Char('$') | Key::End => Some(Motion::LineEnd),
2450 Key::Char('G') => Some(Motion::FileBottom),
2451 Key::Char('%') => Some(Motion::MatchBracket),
2452 Key::Char(';') => Some(Motion::FindRepeat { reverse: false }),
2453 Key::Char(',') => Some(Motion::FindRepeat { reverse: true }),
2454 Key::Char('*') => Some(Motion::WordAtCursor {
2455 forward: true,
2456 whole_word: true,
2457 }),
2458 Key::Char('#') => Some(Motion::WordAtCursor {
2459 forward: false,
2460 whole_word: true,
2461 }),
2462 Key::Char('n') => Some(Motion::SearchNext { reverse: false }),
2463 Key::Char('N') => Some(Motion::SearchNext { reverse: true }),
2464 Key::Char('H') => Some(Motion::ViewportTop),
2465 Key::Char('M') => Some(Motion::ViewportMiddle),
2466 Key::Char('L') => Some(Motion::ViewportBottom),
2467 Key::Char('{') => Some(Motion::ParagraphPrev),
2468 Key::Char('}') => Some(Motion::ParagraphNext),
2469 Key::Char('(') => Some(Motion::SentencePrev),
2470 Key::Char(')') => Some(Motion::SentenceNext),
2471 _ => None,
2472 }
2473}
2474
2475pub(crate) fn execute_motion<H: crate::types::Host>(
2478 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2479 motion: Motion,
2480 count: usize,
2481) {
2482 let count = count.max(1);
2483 let motion = match motion {
2485 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2486 Some((ch, forward, till)) => Motion::Find {
2487 ch,
2488 forward: if reverse { !forward } else { forward },
2489 till,
2490 },
2491 None => return,
2492 },
2493 other => other,
2494 };
2495 let pre_pos = ed.cursor();
2496 let pre_col = pre_pos.1;
2497 apply_motion_cursor(ed, &motion, count);
2498 let post_pos = ed.cursor();
2499 if is_big_jump(&motion) && pre_pos != post_pos {
2500 push_jump(ed, pre_pos);
2501 }
2502 apply_sticky_col(ed, &motion, pre_col);
2503 ed.sync_buffer_from_textarea();
2508}
2509
2510fn apply_sticky_col<H: crate::types::Host>(
2515 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2516 motion: &Motion,
2517 pre_col: usize,
2518) {
2519 if is_vertical_motion(motion) {
2520 let want = ed.sticky_col.unwrap_or(pre_col);
2521 ed.sticky_col = Some(want);
2524 let (row, _) = ed.cursor();
2525 let line_len = buf_line_chars(&ed.buffer, row);
2526 let max_col = line_len.saturating_sub(1);
2530 let target = want.min(max_col);
2531 ed.jump_cursor(row, target);
2532 } else {
2533 ed.sticky_col = Some(ed.cursor().1);
2536 }
2537}
2538
2539fn is_vertical_motion(motion: &Motion) -> bool {
2540 matches!(
2544 motion,
2545 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown
2546 )
2547}
2548
2549fn apply_motion_cursor<H: crate::types::Host>(
2550 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2551 motion: &Motion,
2552 count: usize,
2553) {
2554 apply_motion_cursor_ctx(ed, motion, count, false)
2555}
2556
2557fn apply_motion_cursor_ctx<H: crate::types::Host>(
2558 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2559 motion: &Motion,
2560 count: usize,
2561 as_operator: bool,
2562) {
2563 match motion {
2564 Motion::Left => {
2565 crate::motions::move_left(&mut ed.buffer, count);
2567 ed.push_buffer_cursor_to_textarea();
2568 }
2569 Motion::Right => {
2570 if as_operator {
2574 crate::motions::move_right_to_end(&mut ed.buffer, count);
2575 } else {
2576 crate::motions::move_right_in_line(&mut ed.buffer, count);
2577 }
2578 ed.push_buffer_cursor_to_textarea();
2579 }
2580 Motion::Up => {
2581 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2585 crate::motions::move_up(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2586 ed.push_buffer_cursor_to_textarea();
2587 }
2588 Motion::Down => {
2589 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2590 crate::motions::move_down(&mut ed.buffer, &folds, count, &mut ed.sticky_col);
2591 ed.push_buffer_cursor_to_textarea();
2592 }
2593 Motion::ScreenUp => {
2594 let v = *ed.host.viewport();
2595 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2596 crate::motions::move_screen_up(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2597 ed.push_buffer_cursor_to_textarea();
2598 }
2599 Motion::ScreenDown => {
2600 let v = *ed.host.viewport();
2601 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
2602 crate::motions::move_screen_down(&mut ed.buffer, &folds, &v, count, &mut ed.sticky_col);
2603 ed.push_buffer_cursor_to_textarea();
2604 }
2605 Motion::WordFwd => {
2606 crate::motions::move_word_fwd(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2607 ed.push_buffer_cursor_to_textarea();
2608 }
2609 Motion::WordBack => {
2610 crate::motions::move_word_back(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2611 ed.push_buffer_cursor_to_textarea();
2612 }
2613 Motion::WordEnd => {
2614 crate::motions::move_word_end(&mut ed.buffer, false, count, &ed.settings.iskeyword);
2615 ed.push_buffer_cursor_to_textarea();
2616 }
2617 Motion::BigWordFwd => {
2618 crate::motions::move_word_fwd(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2619 ed.push_buffer_cursor_to_textarea();
2620 }
2621 Motion::BigWordBack => {
2622 crate::motions::move_word_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2623 ed.push_buffer_cursor_to_textarea();
2624 }
2625 Motion::BigWordEnd => {
2626 crate::motions::move_word_end(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2627 ed.push_buffer_cursor_to_textarea();
2628 }
2629 Motion::WordEndBack => {
2630 crate::motions::move_word_end_back(
2631 &mut ed.buffer,
2632 false,
2633 count,
2634 &ed.settings.iskeyword,
2635 );
2636 ed.push_buffer_cursor_to_textarea();
2637 }
2638 Motion::BigWordEndBack => {
2639 crate::motions::move_word_end_back(&mut ed.buffer, true, count, &ed.settings.iskeyword);
2640 ed.push_buffer_cursor_to_textarea();
2641 }
2642 Motion::LineStart => {
2643 crate::motions::move_line_start(&mut ed.buffer);
2644 ed.push_buffer_cursor_to_textarea();
2645 }
2646 Motion::FirstNonBlank => {
2647 crate::motions::move_first_non_blank(&mut ed.buffer);
2648 ed.push_buffer_cursor_to_textarea();
2649 }
2650 Motion::LineEnd => {
2651 crate::motions::move_line_end(&mut ed.buffer);
2653 ed.push_buffer_cursor_to_textarea();
2654 }
2655 Motion::FileTop => {
2656 if count > 1 {
2659 crate::motions::move_bottom(&mut ed.buffer, count);
2660 } else {
2661 crate::motions::move_top(&mut ed.buffer);
2662 }
2663 ed.push_buffer_cursor_to_textarea();
2664 }
2665 Motion::FileBottom => {
2666 if count > 1 {
2669 crate::motions::move_bottom(&mut ed.buffer, count);
2670 } else {
2671 crate::motions::move_bottom(&mut ed.buffer, 0);
2672 }
2673 ed.push_buffer_cursor_to_textarea();
2674 }
2675 Motion::Find { ch, forward, till } => {
2676 for _ in 0..count {
2677 if !find_char_on_line(ed, *ch, *forward, *till) {
2678 break;
2679 }
2680 }
2681 }
2682 Motion::FindRepeat { .. } => {} Motion::MatchBracket => {
2684 let _ = matching_bracket(ed);
2685 }
2686 Motion::WordAtCursor {
2687 forward,
2688 whole_word,
2689 } => {
2690 word_at_cursor_search(ed, *forward, *whole_word, count);
2691 }
2692 Motion::SearchNext { reverse } => {
2693 if let Some(pattern) = ed.vim.last_search.clone() {
2697 push_search_pattern(ed, &pattern);
2698 }
2699 if ed.search_state().pattern.is_none() {
2700 return;
2701 }
2702 let forward = ed.vim.last_search_forward != *reverse;
2706 for _ in 0..count.max(1) {
2707 if forward {
2708 ed.search_advance_forward(true);
2709 } else {
2710 ed.search_advance_backward(true);
2711 }
2712 }
2713 ed.push_buffer_cursor_to_textarea();
2714 }
2715 Motion::ViewportTop => {
2716 let v = *ed.host().viewport();
2717 crate::motions::move_viewport_top(&mut ed.buffer, &v, count.saturating_sub(1));
2718 ed.push_buffer_cursor_to_textarea();
2719 }
2720 Motion::ViewportMiddle => {
2721 let v = *ed.host().viewport();
2722 crate::motions::move_viewport_middle(&mut ed.buffer, &v);
2723 ed.push_buffer_cursor_to_textarea();
2724 }
2725 Motion::ViewportBottom => {
2726 let v = *ed.host().viewport();
2727 crate::motions::move_viewport_bottom(&mut ed.buffer, &v, count.saturating_sub(1));
2728 ed.push_buffer_cursor_to_textarea();
2729 }
2730 Motion::LastNonBlank => {
2731 crate::motions::move_last_non_blank(&mut ed.buffer);
2732 ed.push_buffer_cursor_to_textarea();
2733 }
2734 Motion::LineMiddle => {
2735 let row = ed.cursor().0;
2736 let line_chars = buf_line_chars(&ed.buffer, row);
2737 let target = line_chars / 2;
2740 ed.jump_cursor(row, target);
2741 }
2742 Motion::ParagraphPrev => {
2743 crate::motions::move_paragraph_prev(&mut ed.buffer, count);
2744 ed.push_buffer_cursor_to_textarea();
2745 }
2746 Motion::ParagraphNext => {
2747 crate::motions::move_paragraph_next(&mut ed.buffer, count);
2748 ed.push_buffer_cursor_to_textarea();
2749 }
2750 Motion::SentencePrev => {
2751 for _ in 0..count.max(1) {
2752 if let Some((row, col)) = sentence_boundary(ed, false) {
2753 ed.jump_cursor(row, col);
2754 }
2755 }
2756 }
2757 Motion::SentenceNext => {
2758 for _ in 0..count.max(1) {
2759 if let Some((row, col)) = sentence_boundary(ed, true) {
2760 ed.jump_cursor(row, col);
2761 }
2762 }
2763 }
2764 }
2765}
2766
2767fn move_first_non_whitespace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
2768 ed.sync_buffer_content_from_textarea();
2774 crate::motions::move_first_non_blank(&mut ed.buffer);
2775 ed.push_buffer_cursor_to_textarea();
2776}
2777
2778fn find_char_on_line<H: crate::types::Host>(
2779 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2780 ch: char,
2781 forward: bool,
2782 till: bool,
2783) -> bool {
2784 let moved = crate::motions::find_char_on_line(&mut ed.buffer, ch, forward, till);
2785 if moved {
2786 ed.push_buffer_cursor_to_textarea();
2787 }
2788 moved
2789}
2790
2791fn matching_bracket<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) -> bool {
2792 let moved = crate::motions::match_bracket(&mut ed.buffer);
2793 if moved {
2794 ed.push_buffer_cursor_to_textarea();
2795 }
2796 moved
2797}
2798
2799fn word_at_cursor_search<H: crate::types::Host>(
2800 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2801 forward: bool,
2802 whole_word: bool,
2803 count: usize,
2804) {
2805 let (row, col) = ed.cursor();
2806 let line: String = buf_line(&ed.buffer, row).unwrap_or("").to_string();
2807 let chars: Vec<char> = line.chars().collect();
2808 if chars.is_empty() {
2809 return;
2810 }
2811 let spec = ed.settings().iskeyword.clone();
2813 let is_word = |c: char| is_keyword_char(c, &spec);
2814 let mut start = col.min(chars.len().saturating_sub(1));
2815 while start > 0 && is_word(chars[start - 1]) {
2816 start -= 1;
2817 }
2818 let mut end = start;
2819 while end < chars.len() && is_word(chars[end]) {
2820 end += 1;
2821 }
2822 if end <= start {
2823 return;
2824 }
2825 let word: String = chars[start..end].iter().collect();
2826 let escaped = regex_escape(&word);
2827 let pattern = if whole_word {
2828 format!(r"\b{escaped}\b")
2829 } else {
2830 escaped
2831 };
2832 push_search_pattern(ed, &pattern);
2833 if ed.search_state().pattern.is_none() {
2834 return;
2835 }
2836 ed.vim.last_search = Some(pattern);
2838 ed.vim.last_search_forward = forward;
2839 for _ in 0..count.max(1) {
2840 if forward {
2841 ed.search_advance_forward(true);
2842 } else {
2843 ed.search_advance_backward(true);
2844 }
2845 }
2846 ed.push_buffer_cursor_to_textarea();
2847}
2848
2849fn regex_escape(s: &str) -> String {
2850 let mut out = String::with_capacity(s.len());
2851 for c in s.chars() {
2852 if matches!(
2853 c,
2854 '.' | '+' | '*' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
2855 ) {
2856 out.push('\\');
2857 }
2858 out.push(c);
2859 }
2860 out
2861}
2862
2863fn handle_after_op<H: crate::types::Host>(
2866 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2867 input: Input,
2868 op: Operator,
2869 count1: usize,
2870) -> bool {
2871 if let Key::Char(d @ '0'..='9') = input.key
2873 && !input.ctrl
2874 && (d != '0' || ed.vim.count > 0)
2875 {
2876 ed.vim.count = ed.vim.count.saturating_mul(10) + (d as usize - '0' as usize);
2877 ed.vim.pending = Pending::Op { op, count1 };
2878 return true;
2879 }
2880
2881 if input.key == Key::Esc {
2883 ed.vim.count = 0;
2884 return true;
2885 }
2886
2887 let double_ch = match op {
2891 Operator::Delete => Some('d'),
2892 Operator::Change => Some('c'),
2893 Operator::Yank => Some('y'),
2894 Operator::Indent => Some('>'),
2895 Operator::Outdent => Some('<'),
2896 Operator::Uppercase => Some('U'),
2897 Operator::Lowercase => Some('u'),
2898 Operator::ToggleCase => Some('~'),
2899 Operator::Fold => None,
2900 Operator::Reflow => Some('q'),
2903 };
2904 if let Key::Char(c) = input.key
2905 && !input.ctrl
2906 && Some(c) == double_ch
2907 {
2908 let count2 = take_count(&mut ed.vim);
2909 let total = count1.max(1) * count2.max(1);
2910 execute_line_op(ed, op, total);
2911 if !ed.vim.replaying {
2912 ed.vim.last_change = Some(LastChange::LineOp {
2913 op,
2914 count: total,
2915 inserted: None,
2916 });
2917 }
2918 return true;
2919 }
2920
2921 if let Key::Char('i') | Key::Char('a') = input.key
2923 && !input.ctrl
2924 {
2925 let inner = matches!(input.key, Key::Char('i'));
2926 ed.vim.pending = Pending::OpTextObj { op, count1, inner };
2927 return true;
2928 }
2929
2930 if input.key == Key::Char('g') && !input.ctrl {
2932 ed.vim.pending = Pending::OpG { op, count1 };
2933 return true;
2934 }
2935
2936 if let Some((forward, till)) = find_entry(&input) {
2938 ed.vim.pending = Pending::OpFind {
2939 op,
2940 count1,
2941 forward,
2942 till,
2943 };
2944 return true;
2945 }
2946
2947 let count2 = take_count(&mut ed.vim);
2949 let total = count1.max(1) * count2.max(1);
2950 if let Some(motion) = parse_motion(&input) {
2951 let motion = match motion {
2952 Motion::FindRepeat { reverse } => match ed.vim.last_find {
2953 Some((ch, forward, till)) => Motion::Find {
2954 ch,
2955 forward: if reverse { !forward } else { forward },
2956 till,
2957 },
2958 None => return true,
2959 },
2960 Motion::WordFwd if op == Operator::Change => Motion::WordEnd,
2964 Motion::BigWordFwd if op == Operator::Change => Motion::BigWordEnd,
2965 m => m,
2966 };
2967 apply_op_with_motion(ed, op, &motion, total);
2968 if let Motion::Find { ch, forward, till } = &motion {
2969 ed.vim.last_find = Some((*ch, *forward, *till));
2970 }
2971 if !ed.vim.replaying && op_is_change(op) {
2972 ed.vim.last_change = Some(LastChange::OpMotion {
2973 op,
2974 motion,
2975 count: total,
2976 inserted: None,
2977 });
2978 }
2979 return true;
2980 }
2981
2982 true
2984}
2985
2986fn handle_op_after_g<H: crate::types::Host>(
2987 ed: &mut Editor<hjkl_buffer::Buffer, H>,
2988 input: Input,
2989 op: Operator,
2990 count1: usize,
2991) -> bool {
2992 if input.ctrl {
2993 return true;
2994 }
2995 let count2 = take_count(&mut ed.vim);
2996 let total = count1.max(1) * count2.max(1);
2997 if matches!(
3001 op,
3002 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase
3003 ) {
3004 let op_char = match op {
3005 Operator::Uppercase => 'U',
3006 Operator::Lowercase => 'u',
3007 Operator::ToggleCase => '~',
3008 _ => unreachable!(),
3009 };
3010 if input.key == Key::Char(op_char) {
3011 execute_line_op(ed, op, total);
3012 if !ed.vim.replaying {
3013 ed.vim.last_change = Some(LastChange::LineOp {
3014 op,
3015 count: total,
3016 inserted: None,
3017 });
3018 }
3019 return true;
3020 }
3021 }
3022 let motion = match input.key {
3023 Key::Char('g') => Motion::FileTop,
3024 Key::Char('e') => Motion::WordEndBack,
3025 Key::Char('E') => Motion::BigWordEndBack,
3026 Key::Char('j') => Motion::ScreenDown,
3027 Key::Char('k') => Motion::ScreenUp,
3028 _ => return true,
3029 };
3030 apply_op_with_motion(ed, op, &motion, total);
3031 if !ed.vim.replaying && op_is_change(op) {
3032 ed.vim.last_change = Some(LastChange::OpMotion {
3033 op,
3034 motion,
3035 count: total,
3036 inserted: None,
3037 });
3038 }
3039 true
3040}
3041
3042fn handle_after_g<H: crate::types::Host>(
3043 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3044 input: Input,
3045) -> bool {
3046 let count = take_count(&mut ed.vim);
3047 if let Key::Char(ch) = input.key {
3050 apply_after_g(ed, ch, count);
3051 }
3052 true
3053}
3054
3055pub(crate) fn apply_after_g<H: crate::types::Host>(
3060 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3061 ch: char,
3062 count: usize,
3063) {
3064 match ch {
3065 'g' => {
3066 let pre = ed.cursor();
3068 if count > 1 {
3069 ed.jump_cursor(count - 1, 0);
3070 } else {
3071 ed.jump_cursor(0, 0);
3072 }
3073 move_first_non_whitespace(ed);
3074 if ed.cursor() != pre {
3075 push_jump(ed, pre);
3076 }
3077 }
3078 'e' => execute_motion(ed, Motion::WordEndBack, count),
3079 'E' => execute_motion(ed, Motion::BigWordEndBack, count),
3080 '_' => execute_motion(ed, Motion::LastNonBlank, count),
3082 'M' => execute_motion(ed, Motion::LineMiddle, count),
3084 'v' => {
3086 if let Some(snap) = ed.vim.last_visual {
3087 match snap.mode {
3088 Mode::Visual => {
3089 ed.vim.visual_anchor = snap.anchor;
3090 ed.vim.mode = Mode::Visual;
3091 }
3092 Mode::VisualLine => {
3093 ed.vim.visual_line_anchor = snap.anchor.0;
3094 ed.vim.mode = Mode::VisualLine;
3095 }
3096 Mode::VisualBlock => {
3097 ed.vim.block_anchor = snap.anchor;
3098 ed.vim.block_vcol = snap.block_vcol;
3099 ed.vim.mode = Mode::VisualBlock;
3100 }
3101 _ => {}
3102 }
3103 ed.jump_cursor(snap.cursor.0, snap.cursor.1);
3104 }
3105 }
3106 'j' => execute_motion(ed, Motion::ScreenDown, count),
3110 'k' => execute_motion(ed, Motion::ScreenUp, count),
3111 'U' => {
3115 ed.vim.pending = Pending::Op {
3116 op: Operator::Uppercase,
3117 count1: count,
3118 };
3119 }
3120 'u' => {
3121 ed.vim.pending = Pending::Op {
3122 op: Operator::Lowercase,
3123 count1: count,
3124 };
3125 }
3126 '~' => {
3127 ed.vim.pending = Pending::Op {
3128 op: Operator::ToggleCase,
3129 count1: count,
3130 };
3131 }
3132 'q' => {
3133 ed.vim.pending = Pending::Op {
3136 op: Operator::Reflow,
3137 count1: count,
3138 };
3139 }
3140 'J' => {
3141 for _ in 0..count.max(1) {
3143 ed.push_undo();
3144 join_line_raw(ed);
3145 }
3146 if !ed.vim.replaying {
3147 ed.vim.last_change = Some(LastChange::JoinLine {
3148 count: count.max(1),
3149 });
3150 }
3151 }
3152 'd' => {
3153 ed.pending_lsp = Some(crate::editor::LspIntent::GotoDefinition);
3158 }
3159 'i' => {
3164 if let Some((row, col)) = ed.vim.last_insert_pos {
3165 ed.jump_cursor(row, col);
3166 }
3167 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3168 }
3169 ';' => walk_change_list(ed, -1, count.max(1)),
3172 ',' => walk_change_list(ed, 1, count.max(1)),
3173 '*' => execute_motion(
3177 ed,
3178 Motion::WordAtCursor {
3179 forward: true,
3180 whole_word: false,
3181 },
3182 count,
3183 ),
3184 '#' => execute_motion(
3185 ed,
3186 Motion::WordAtCursor {
3187 forward: false,
3188 whole_word: false,
3189 },
3190 count,
3191 ),
3192 _ => {}
3193 }
3194}
3195
3196fn handle_after_z<H: crate::types::Host>(
3197 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3198 input: Input,
3199) -> bool {
3200 let count = take_count(&mut ed.vim);
3201 if let Key::Char(ch) = input.key {
3204 apply_after_z(ed, ch, count);
3205 }
3206 true
3207}
3208
3209pub(crate) fn apply_after_z<H: crate::types::Host>(
3214 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3215 ch: char,
3216 count: usize,
3217) {
3218 use crate::editor::CursorScrollTarget;
3219 let row = ed.cursor().0;
3220 match ch {
3221 'z' => {
3222 ed.scroll_cursor_to(CursorScrollTarget::Center);
3223 ed.vim.viewport_pinned = true;
3224 }
3225 't' => {
3226 ed.scroll_cursor_to(CursorScrollTarget::Top);
3227 ed.vim.viewport_pinned = true;
3228 }
3229 'b' => {
3230 ed.scroll_cursor_to(CursorScrollTarget::Bottom);
3231 ed.vim.viewport_pinned = true;
3232 }
3233 'o' => {
3238 ed.apply_fold_op(crate::types::FoldOp::OpenAt(row));
3239 }
3240 'c' => {
3241 ed.apply_fold_op(crate::types::FoldOp::CloseAt(row));
3242 }
3243 'a' => {
3244 ed.apply_fold_op(crate::types::FoldOp::ToggleAt(row));
3245 }
3246 'R' => {
3247 ed.apply_fold_op(crate::types::FoldOp::OpenAll);
3248 }
3249 'M' => {
3250 ed.apply_fold_op(crate::types::FoldOp::CloseAll);
3251 }
3252 'E' => {
3253 ed.apply_fold_op(crate::types::FoldOp::ClearAll);
3254 }
3255 'd' => {
3256 ed.apply_fold_op(crate::types::FoldOp::RemoveAt(row));
3257 }
3258 'f' => {
3259 if matches!(
3260 ed.vim.mode,
3261 Mode::Visual | Mode::VisualLine | Mode::VisualBlock
3262 ) {
3263 let anchor_row = match ed.vim.mode {
3266 Mode::VisualLine => ed.vim.visual_line_anchor,
3267 Mode::VisualBlock => ed.vim.block_anchor.0,
3268 _ => ed.vim.visual_anchor.0,
3269 };
3270 let cur = ed.cursor().0;
3271 let top = anchor_row.min(cur);
3272 let bot = anchor_row.max(cur);
3273 ed.apply_fold_op(crate::types::FoldOp::Add {
3274 start_row: top,
3275 end_row: bot,
3276 closed: true,
3277 });
3278 ed.vim.mode = Mode::Normal;
3279 } else {
3280 ed.vim.pending = Pending::Op {
3285 op: Operator::Fold,
3286 count1: count,
3287 };
3288 }
3289 }
3290 _ => {}
3291 }
3292}
3293
3294fn handle_replace<H: crate::types::Host>(
3295 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3296 input: Input,
3297) -> bool {
3298 if let Key::Char(ch) = input.key {
3299 if ed.vim.mode == Mode::VisualBlock {
3300 block_replace(ed, ch);
3301 return true;
3302 }
3303 let count = take_count(&mut ed.vim);
3304 replace_char(ed, ch, count.max(1));
3305 if !ed.vim.replaying {
3306 ed.vim.last_change = Some(LastChange::ReplaceChar {
3307 ch,
3308 count: count.max(1),
3309 });
3310 }
3311 }
3312 true
3313}
3314
3315fn handle_find_target<H: crate::types::Host>(
3316 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3317 input: Input,
3318 forward: bool,
3319 till: bool,
3320) -> bool {
3321 let Key::Char(ch) = input.key else {
3322 return true;
3323 };
3324 let count = take_count(&mut ed.vim);
3325 apply_find_char(ed, ch, forward, till, count.max(1));
3326 true
3327}
3328
3329pub(crate) fn apply_find_char<H: crate::types::Host>(
3335 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3336 ch: char,
3337 forward: bool,
3338 till: bool,
3339 count: usize,
3340) {
3341 execute_motion(ed, Motion::Find { ch, forward, till }, count.max(1));
3342 ed.vim.last_find = Some((ch, forward, till));
3343}
3344
3345fn handle_op_find_target<H: crate::types::Host>(
3346 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3347 input: Input,
3348 op: Operator,
3349 count1: usize,
3350 forward: bool,
3351 till: bool,
3352) -> bool {
3353 let Key::Char(ch) = input.key else {
3354 return true;
3355 };
3356 let count2 = take_count(&mut ed.vim);
3357 let total = count1.max(1) * count2.max(1);
3358 let motion = Motion::Find { ch, forward, till };
3359 apply_op_with_motion(ed, op, &motion, total);
3360 ed.vim.last_find = Some((ch, forward, till));
3361 if !ed.vim.replaying && op_is_change(op) {
3362 ed.vim.last_change = Some(LastChange::OpMotion {
3363 op,
3364 motion,
3365 count: total,
3366 inserted: None,
3367 });
3368 }
3369 true
3370}
3371
3372fn handle_text_object<H: crate::types::Host>(
3373 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3374 input: Input,
3375 op: Operator,
3376 _count1: usize,
3377 inner: bool,
3378) -> bool {
3379 let Key::Char(ch) = input.key else {
3380 return true;
3381 };
3382 let obj = match ch {
3383 'w' => TextObject::Word { big: false },
3384 'W' => TextObject::Word { big: true },
3385 '"' | '\'' | '`' => TextObject::Quote(ch),
3386 '(' | ')' | 'b' => TextObject::Bracket('('),
3387 '[' | ']' => TextObject::Bracket('['),
3388 '{' | '}' | 'B' => TextObject::Bracket('{'),
3389 '<' | '>' => TextObject::Bracket('<'),
3390 'p' => TextObject::Paragraph,
3391 't' => TextObject::XmlTag,
3392 's' => TextObject::Sentence,
3393 _ => return true,
3394 };
3395 apply_op_with_text_object(ed, op, obj, inner);
3396 if !ed.vim.replaying && op_is_change(op) {
3397 ed.vim.last_change = Some(LastChange::OpTextObj {
3398 op,
3399 obj,
3400 inner,
3401 inserted: None,
3402 });
3403 }
3404 true
3405}
3406
3407fn handle_visual_text_obj<H: crate::types::Host>(
3408 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3409 input: Input,
3410 inner: bool,
3411) -> bool {
3412 let Key::Char(ch) = input.key else {
3413 return true;
3414 };
3415 let obj = match ch {
3416 'w' => TextObject::Word { big: false },
3417 'W' => TextObject::Word { big: true },
3418 '"' | '\'' | '`' => TextObject::Quote(ch),
3419 '(' | ')' | 'b' => TextObject::Bracket('('),
3420 '[' | ']' => TextObject::Bracket('['),
3421 '{' | '}' | 'B' => TextObject::Bracket('{'),
3422 '<' | '>' => TextObject::Bracket('<'),
3423 'p' => TextObject::Paragraph,
3424 't' => TextObject::XmlTag,
3425 's' => TextObject::Sentence,
3426 _ => return true,
3427 };
3428 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3429 return true;
3430 };
3431 match kind {
3435 MotionKind::Linewise => {
3436 ed.vim.visual_line_anchor = start.0;
3437 ed.vim.mode = Mode::VisualLine;
3438 ed.jump_cursor(end.0, 0);
3439 }
3440 _ => {
3441 ed.vim.mode = Mode::Visual;
3442 ed.vim.visual_anchor = (start.0, start.1);
3443 let (er, ec) = retreat_one(ed, end);
3444 ed.jump_cursor(er, ec);
3445 }
3446 }
3447 true
3448}
3449
3450fn retreat_one<H: crate::types::Host>(
3452 ed: &Editor<hjkl_buffer::Buffer, H>,
3453 pos: (usize, usize),
3454) -> (usize, usize) {
3455 let (r, c) = pos;
3456 if c > 0 {
3457 (r, c - 1)
3458 } else if r > 0 {
3459 let prev_len = buf_line_bytes(&ed.buffer, r - 1);
3460 (r - 1, prev_len)
3461 } else {
3462 (0, 0)
3463 }
3464}
3465
3466fn op_is_change(op: Operator) -> bool {
3467 matches!(op, Operator::Delete | Operator::Change)
3468}
3469
3470fn handle_normal_only<H: crate::types::Host>(
3473 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3474 input: &Input,
3475 count: usize,
3476) -> bool {
3477 if input.ctrl {
3478 return false;
3479 }
3480 match input.key {
3481 Key::Char('i') => {
3482 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::I));
3483 true
3484 }
3485 Key::Char('I') => {
3486 move_first_non_whitespace(ed);
3487 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftI));
3488 true
3489 }
3490 Key::Char('a') => {
3491 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3492 ed.push_buffer_cursor_to_textarea();
3493 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::A));
3494 true
3495 }
3496 Key::Char('A') => {
3497 crate::motions::move_line_end(&mut ed.buffer);
3498 crate::motions::move_right_to_end(&mut ed.buffer, 1);
3499 ed.push_buffer_cursor_to_textarea();
3500 begin_insert(ed, count.max(1), InsertReason::Enter(InsertEntry::ShiftA));
3501 true
3502 }
3503 Key::Char('R') => {
3504 begin_insert(ed, count.max(1), InsertReason::Replace);
3507 true
3508 }
3509 Key::Char('o') => {
3510 use hjkl_buffer::{Edit, Position};
3511 ed.push_undo();
3512 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: false });
3515 ed.sync_buffer_content_from_textarea();
3516 let row = buf_cursor_pos(&ed.buffer).row;
3517 let line_chars = buf_line_chars(&ed.buffer, row);
3518 let prev_line = buf_line(&ed.buffer, row).unwrap_or_default();
3521 let indent = compute_enter_indent(&ed.settings, prev_line);
3522 ed.mutate_edit(Edit::InsertStr {
3523 at: Position::new(row, line_chars),
3524 text: format!("\n{indent}"),
3525 });
3526 ed.push_buffer_cursor_to_textarea();
3527 true
3528 }
3529 Key::Char('O') => {
3530 use hjkl_buffer::{Edit, Position};
3531 ed.push_undo();
3532 begin_insert_noundo(ed, count.max(1), InsertReason::Open { above: true });
3533 ed.sync_buffer_content_from_textarea();
3534 let row = buf_cursor_pos(&ed.buffer).row;
3535 let indent = if row > 0 {
3539 let above = buf_line(&ed.buffer, row - 1).unwrap_or_default();
3540 compute_enter_indent(&ed.settings, above)
3541 } else {
3542 let cur = buf_line(&ed.buffer, row).unwrap_or_default();
3543 cur.chars()
3544 .take_while(|c| *c == ' ' || *c == '\t')
3545 .collect::<String>()
3546 };
3547 ed.mutate_edit(Edit::InsertStr {
3548 at: Position::new(row, 0),
3549 text: format!("{indent}\n"),
3550 });
3551 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
3556 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
3557 let new_row = buf_cursor_pos(&ed.buffer).row;
3558 buf_set_cursor_rc(&mut ed.buffer, new_row, indent.chars().count());
3559 ed.push_buffer_cursor_to_textarea();
3560 true
3561 }
3562 Key::Char('x') => {
3563 do_char_delete(ed, true, count.max(1));
3564 if !ed.vim.replaying {
3565 ed.vim.last_change = Some(LastChange::CharDel {
3566 forward: true,
3567 count: count.max(1),
3568 });
3569 }
3570 true
3571 }
3572 Key::Char('X') => {
3573 do_char_delete(ed, false, count.max(1));
3574 if !ed.vim.replaying {
3575 ed.vim.last_change = Some(LastChange::CharDel {
3576 forward: false,
3577 count: count.max(1),
3578 });
3579 }
3580 true
3581 }
3582 Key::Char('~') => {
3583 for _ in 0..count.max(1) {
3584 ed.push_undo();
3585 toggle_case_at_cursor(ed);
3586 }
3587 if !ed.vim.replaying {
3588 ed.vim.last_change = Some(LastChange::ToggleCase {
3589 count: count.max(1),
3590 });
3591 }
3592 true
3593 }
3594 Key::Char('J') => {
3595 for _ in 0..count.max(1) {
3596 ed.push_undo();
3597 join_line(ed);
3598 }
3599 if !ed.vim.replaying {
3600 ed.vim.last_change = Some(LastChange::JoinLine {
3601 count: count.max(1),
3602 });
3603 }
3604 true
3605 }
3606 Key::Char('D') => {
3607 ed.push_undo();
3608 delete_to_eol(ed);
3609 crate::motions::move_left(&mut ed.buffer, 1);
3611 ed.push_buffer_cursor_to_textarea();
3612 if !ed.vim.replaying {
3613 ed.vim.last_change = Some(LastChange::DeleteToEol { inserted: None });
3614 }
3615 true
3616 }
3617 Key::Char('Y') => {
3618 apply_op_with_motion(ed, Operator::Yank, &Motion::LineEnd, count.max(1));
3620 true
3621 }
3622 Key::Char('C') => {
3623 ed.push_undo();
3624 delete_to_eol(ed);
3625 begin_insert_noundo(ed, 1, InsertReason::DeleteToEol);
3626 true
3627 }
3628 Key::Char('s') => {
3629 use hjkl_buffer::{Edit, MotionKind, Position};
3630 ed.push_undo();
3631 ed.sync_buffer_content_from_textarea();
3632 for _ in 0..count.max(1) {
3633 let cursor = buf_cursor_pos(&ed.buffer);
3634 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
3635 if cursor.col >= line_chars {
3636 break;
3637 }
3638 ed.mutate_edit(Edit::DeleteRange {
3639 start: cursor,
3640 end: Position::new(cursor.row, cursor.col + 1),
3641 kind: MotionKind::Char,
3642 });
3643 }
3644 ed.push_buffer_cursor_to_textarea();
3645 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3646 if !ed.vim.replaying {
3648 ed.vim.last_change = Some(LastChange::OpMotion {
3649 op: Operator::Change,
3650 motion: Motion::Right,
3651 count: count.max(1),
3652 inserted: None,
3653 });
3654 }
3655 true
3656 }
3657 Key::Char('p') => {
3658 do_paste(ed, false, count.max(1));
3659 if !ed.vim.replaying {
3660 ed.vim.last_change = Some(LastChange::Paste {
3661 before: false,
3662 count: count.max(1),
3663 });
3664 }
3665 true
3666 }
3667 Key::Char('P') => {
3668 do_paste(ed, true, count.max(1));
3669 if !ed.vim.replaying {
3670 ed.vim.last_change = Some(LastChange::Paste {
3671 before: true,
3672 count: count.max(1),
3673 });
3674 }
3675 true
3676 }
3677 Key::Char('u') => {
3678 do_undo(ed);
3679 true
3680 }
3681 Key::Char('r') => {
3682 ed.vim.count = count;
3683 ed.vim.pending = Pending::Replace;
3684 true
3685 }
3686 Key::Char('/') => {
3687 enter_search(ed, true);
3688 true
3689 }
3690 Key::Char('?') => {
3691 enter_search(ed, false);
3692 true
3693 }
3694 Key::Char('.') => {
3695 replay_last_change(ed, count);
3696 true
3697 }
3698 _ => false,
3699 }
3700}
3701
3702fn begin_insert_noundo<H: crate::types::Host>(
3704 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3705 count: usize,
3706 reason: InsertReason,
3707) {
3708 let reason = if ed.vim.replaying {
3709 InsertReason::ReplayOnly
3710 } else {
3711 reason
3712 };
3713 let (row, _) = ed.cursor();
3714 ed.vim.insert_session = Some(InsertSession {
3715 count,
3716 row_min: row,
3717 row_max: row,
3718 before_lines: buf_lines_to_vec(&ed.buffer),
3719 reason,
3720 });
3721 ed.vim.mode = Mode::Insert;
3722}
3723
3724fn apply_op_with_motion<H: crate::types::Host>(
3727 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3728 op: Operator,
3729 motion: &Motion,
3730 count: usize,
3731) {
3732 let start = ed.cursor();
3733 apply_motion_cursor_ctx(ed, motion, count, true);
3738 let end = ed.cursor();
3739 let kind = motion_kind(motion);
3740 ed.jump_cursor(start.0, start.1);
3742 run_operator_over_range(ed, op, start, end, kind);
3743}
3744
3745fn apply_op_with_text_object<H: crate::types::Host>(
3746 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3747 op: Operator,
3748 obj: TextObject,
3749 inner: bool,
3750) {
3751 let Some((start, end, kind)) = text_object_range(ed, obj, inner) else {
3752 return;
3753 };
3754 ed.jump_cursor(start.0, start.1);
3755 run_operator_over_range(ed, op, start, end, kind);
3756}
3757
3758fn motion_kind(motion: &Motion) -> MotionKind {
3759 match motion {
3760 Motion::Up | Motion::Down | Motion::ScreenUp | Motion::ScreenDown => MotionKind::Linewise,
3761 Motion::FileTop | Motion::FileBottom => MotionKind::Linewise,
3762 Motion::ViewportTop | Motion::ViewportMiddle | Motion::ViewportBottom => {
3763 MotionKind::Linewise
3764 }
3765 Motion::WordEnd | Motion::BigWordEnd | Motion::WordEndBack | Motion::BigWordEndBack => {
3766 MotionKind::Inclusive
3767 }
3768 Motion::Find { .. } => MotionKind::Inclusive,
3769 Motion::MatchBracket => MotionKind::Inclusive,
3770 Motion::LineEnd => MotionKind::Inclusive,
3772 _ => MotionKind::Exclusive,
3773 }
3774}
3775
3776fn run_operator_over_range<H: crate::types::Host>(
3777 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3778 op: Operator,
3779 start: (usize, usize),
3780 end: (usize, usize),
3781 kind: MotionKind,
3782) {
3783 let (top, bot) = order(start, end);
3784 if top == bot {
3785 return;
3786 }
3787
3788 match op {
3789 Operator::Yank => {
3790 let text = read_vim_range(ed, top, bot, kind);
3791 if !text.is_empty() {
3792 ed.record_yank_to_host(text.clone());
3793 ed.record_yank(text, matches!(kind, MotionKind::Linewise));
3794 }
3795 let rbr = match kind {
3799 MotionKind::Linewise => {
3800 let last_col = buf_line_chars(&ed.buffer, bot.0).saturating_sub(1);
3801 (bot.0, last_col)
3802 }
3803 MotionKind::Inclusive => (bot.0, bot.1),
3804 MotionKind::Exclusive => (bot.0, bot.1.saturating_sub(1)),
3805 };
3806 ed.set_mark('[', top);
3807 ed.set_mark(']', rbr);
3808 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3809 ed.push_buffer_cursor_to_textarea();
3810 }
3811 Operator::Delete => {
3812 ed.push_undo();
3813 cut_vim_range(ed, top, bot, kind);
3814 if !matches!(kind, MotionKind::Linewise) {
3819 clamp_cursor_to_normal_mode(ed);
3820 }
3821 ed.vim.mode = Mode::Normal;
3822 let pos = ed.cursor();
3826 ed.set_mark('[', pos);
3827 ed.set_mark(']', pos);
3828 }
3829 Operator::Change => {
3830 ed.vim.change_mark_start = Some(top);
3835 ed.push_undo();
3836 cut_vim_range(ed, top, bot, kind);
3837 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
3838 }
3839 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
3840 apply_case_op_to_selection(ed, op, top, bot, kind);
3841 }
3842 Operator::Indent | Operator::Outdent => {
3843 ed.push_undo();
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::Fold => {
3854 if bot.0 >= top.0 {
3858 ed.apply_fold_op(crate::types::FoldOp::Add {
3859 start_row: top.0,
3860 end_row: bot.0,
3861 closed: true,
3862 });
3863 }
3864 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3865 ed.push_buffer_cursor_to_textarea();
3866 ed.vim.mode = Mode::Normal;
3867 }
3868 Operator::Reflow => {
3869 ed.push_undo();
3870 reflow_rows(ed, top.0, bot.0);
3871 ed.vim.mode = Mode::Normal;
3872 }
3873 }
3874}
3875
3876fn reflow_rows<H: crate::types::Host>(
3881 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3882 top: usize,
3883 bot: usize,
3884) {
3885 let width = ed.settings().textwidth.max(1);
3886 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3887 let bot = bot.min(lines.len().saturating_sub(1));
3888 if top > bot {
3889 return;
3890 }
3891 let original = lines[top..=bot].to_vec();
3892 let mut wrapped: Vec<String> = Vec::new();
3893 let mut paragraph: Vec<String> = Vec::new();
3894 let flush = |para: &mut Vec<String>, out: &mut Vec<String>, width: usize| {
3895 if para.is_empty() {
3896 return;
3897 }
3898 let words = para.join(" ");
3899 let mut current = String::new();
3900 for word in words.split_whitespace() {
3901 let extra = if current.is_empty() {
3902 word.chars().count()
3903 } else {
3904 current.chars().count() + 1 + word.chars().count()
3905 };
3906 if extra > width && !current.is_empty() {
3907 out.push(std::mem::take(&mut current));
3908 current.push_str(word);
3909 } else if current.is_empty() {
3910 current.push_str(word);
3911 } else {
3912 current.push(' ');
3913 current.push_str(word);
3914 }
3915 }
3916 if !current.is_empty() {
3917 out.push(current);
3918 }
3919 para.clear();
3920 };
3921 for line in &original {
3922 if line.trim().is_empty() {
3923 flush(&mut paragraph, &mut wrapped, width);
3924 wrapped.push(String::new());
3925 } else {
3926 paragraph.push(line.clone());
3927 }
3928 }
3929 flush(&mut paragraph, &mut wrapped, width);
3930
3931 let after: Vec<String> = lines.split_off(bot + 1);
3933 lines.truncate(top);
3934 lines.extend(wrapped);
3935 lines.extend(after);
3936 ed.restore(lines, (top, 0));
3937 ed.mark_content_dirty();
3938}
3939
3940fn apply_case_op_to_selection<H: crate::types::Host>(
3946 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3947 op: Operator,
3948 top: (usize, usize),
3949 bot: (usize, usize),
3950 kind: MotionKind,
3951) {
3952 use hjkl_buffer::Edit;
3953 ed.push_undo();
3954 let saved_yank = ed.yank().to_string();
3955 let saved_yank_linewise = ed.vim.yank_linewise;
3956 let selection = cut_vim_range(ed, top, bot, kind);
3957 let transformed = match op {
3958 Operator::Uppercase => selection.to_uppercase(),
3959 Operator::Lowercase => selection.to_lowercase(),
3960 Operator::ToggleCase => toggle_case_str(&selection),
3961 _ => unreachable!(),
3962 };
3963 if !transformed.is_empty() {
3964 let cursor = buf_cursor_pos(&ed.buffer);
3965 ed.mutate_edit(Edit::InsertStr {
3966 at: cursor,
3967 text: transformed,
3968 });
3969 }
3970 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
3971 ed.push_buffer_cursor_to_textarea();
3972 ed.set_yank(saved_yank);
3973 ed.vim.yank_linewise = saved_yank_linewise;
3974 ed.vim.mode = Mode::Normal;
3975}
3976
3977fn indent_rows<H: crate::types::Host>(
3982 ed: &mut Editor<hjkl_buffer::Buffer, H>,
3983 top: usize,
3984 bot: usize,
3985 count: usize,
3986) {
3987 ed.sync_buffer_content_from_textarea();
3988 let width = ed.settings().shiftwidth * count.max(1);
3989 let pad: String = " ".repeat(width);
3990 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
3991 let bot = bot.min(lines.len().saturating_sub(1));
3992 for line in lines.iter_mut().take(bot + 1).skip(top) {
3993 if !line.is_empty() {
3994 line.insert_str(0, &pad);
3995 }
3996 }
3997 ed.restore(lines, (top, 0));
4000 move_first_non_whitespace(ed);
4001}
4002
4003fn outdent_rows<H: crate::types::Host>(
4007 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4008 top: usize,
4009 bot: usize,
4010 count: usize,
4011) {
4012 ed.sync_buffer_content_from_textarea();
4013 let width = ed.settings().shiftwidth * count.max(1);
4014 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4015 let bot = bot.min(lines.len().saturating_sub(1));
4016 for line in lines.iter_mut().take(bot + 1).skip(top) {
4017 let strip: usize = line
4018 .chars()
4019 .take(width)
4020 .take_while(|c| *c == ' ' || *c == '\t')
4021 .count();
4022 if strip > 0 {
4023 let byte_len: usize = line.chars().take(strip).map(|c| c.len_utf8()).sum();
4024 line.drain(..byte_len);
4025 }
4026 }
4027 ed.restore(lines, (top, 0));
4028 move_first_non_whitespace(ed);
4029}
4030
4031fn toggle_case_str(s: &str) -> String {
4032 s.chars()
4033 .map(|c| {
4034 if c.is_lowercase() {
4035 c.to_uppercase().next().unwrap_or(c)
4036 } else if c.is_uppercase() {
4037 c.to_lowercase().next().unwrap_or(c)
4038 } else {
4039 c
4040 }
4041 })
4042 .collect()
4043}
4044
4045fn order(a: (usize, usize), b: (usize, usize)) -> ((usize, usize), (usize, usize)) {
4046 if a <= b { (a, b) } else { (b, a) }
4047}
4048
4049fn clamp_cursor_to_normal_mode<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
4054 let (row, col) = ed.cursor();
4055 let line_chars = buf_line_chars(&ed.buffer, row);
4056 let max_col = line_chars.saturating_sub(1);
4057 if col > max_col {
4058 buf_set_cursor_rc(&mut ed.buffer, row, max_col);
4059 ed.push_buffer_cursor_to_textarea();
4060 }
4061}
4062
4063fn execute_line_op<H: crate::types::Host>(
4066 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4067 op: Operator,
4068 count: usize,
4069) {
4070 let (row, col) = ed.cursor();
4071 let total = buf_row_count(&ed.buffer);
4072 let end_row = (row + count.saturating_sub(1)).min(total.saturating_sub(1));
4073
4074 match op {
4075 Operator::Yank => {
4076 let text = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4078 if !text.is_empty() {
4079 ed.record_yank_to_host(text.clone());
4080 ed.record_yank(text, true);
4081 }
4082 let last_col = buf_line_chars(&ed.buffer, end_row).saturating_sub(1);
4085 ed.set_mark('[', (row, 0));
4086 ed.set_mark(']', (end_row, last_col));
4087 buf_set_cursor_rc(&mut ed.buffer, row, col);
4088 ed.push_buffer_cursor_to_textarea();
4089 ed.vim.mode = Mode::Normal;
4090 }
4091 Operator::Delete => {
4092 ed.push_undo();
4093 let deleted_through_last = end_row + 1 >= total;
4094 cut_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4095 let total_after = buf_row_count(&ed.buffer);
4099 let raw_target = if deleted_through_last {
4100 row.saturating_sub(1).min(total_after.saturating_sub(1))
4101 } else {
4102 row.min(total_after.saturating_sub(1))
4103 };
4104 let target_row = if raw_target > 0
4110 && raw_target + 1 == total_after
4111 && buf_line(&ed.buffer, raw_target)
4112 .map(str::is_empty)
4113 .unwrap_or(false)
4114 {
4115 raw_target - 1
4116 } else {
4117 raw_target
4118 };
4119 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
4120 ed.push_buffer_cursor_to_textarea();
4121 move_first_non_whitespace(ed);
4122 ed.sticky_col = Some(ed.cursor().1);
4123 ed.vim.mode = Mode::Normal;
4124 let pos = ed.cursor();
4127 ed.set_mark('[', pos);
4128 ed.set_mark(']', pos);
4129 }
4130 Operator::Change => {
4131 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4135 ed.vim.change_mark_start = Some((row, 0));
4137 ed.push_undo();
4138 ed.sync_buffer_content_from_textarea();
4139 let payload = read_vim_range(ed, (row, col), (end_row, 0), MotionKind::Linewise);
4141 if end_row > row {
4142 ed.mutate_edit(Edit::DeleteRange {
4143 start: Position::new(row + 1, 0),
4144 end: Position::new(end_row, 0),
4145 kind: BufKind::Line,
4146 });
4147 }
4148 let line_chars = buf_line_chars(&ed.buffer, row);
4149 if line_chars > 0 {
4150 ed.mutate_edit(Edit::DeleteRange {
4151 start: Position::new(row, 0),
4152 end: Position::new(row, line_chars),
4153 kind: BufKind::Char,
4154 });
4155 }
4156 if !payload.is_empty() {
4157 ed.record_yank_to_host(payload.clone());
4158 ed.record_delete(payload, true);
4159 }
4160 buf_set_cursor_rc(&mut ed.buffer, row, 0);
4161 ed.push_buffer_cursor_to_textarea();
4162 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4163 }
4164 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4165 apply_case_op_to_selection(ed, op, (row, col), (end_row, 0), MotionKind::Linewise);
4169 move_first_non_whitespace(ed);
4172 }
4173 Operator::Indent | Operator::Outdent => {
4174 ed.push_undo();
4176 if op == Operator::Indent {
4177 indent_rows(ed, row, end_row, 1);
4178 } else {
4179 outdent_rows(ed, row, end_row, 1);
4180 }
4181 ed.sticky_col = Some(ed.cursor().1);
4182 ed.vim.mode = Mode::Normal;
4183 }
4184 Operator::Fold => unreachable!("Fold has no line-op double"),
4186 Operator::Reflow => {
4187 ed.push_undo();
4189 reflow_rows(ed, row, end_row);
4190 move_first_non_whitespace(ed);
4191 ed.sticky_col = Some(ed.cursor().1);
4192 ed.vim.mode = Mode::Normal;
4193 }
4194 }
4195}
4196
4197fn apply_visual_operator<H: crate::types::Host>(
4200 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4201 op: Operator,
4202) {
4203 match ed.vim.mode {
4204 Mode::VisualLine => {
4205 let cursor_row = buf_cursor_pos(&ed.buffer).row;
4206 let top = cursor_row.min(ed.vim.visual_line_anchor);
4207 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4208 ed.vim.yank_linewise = true;
4209 match op {
4210 Operator::Yank => {
4211 let text = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4212 if !text.is_empty() {
4213 ed.record_yank_to_host(text.clone());
4214 ed.record_yank(text, true);
4215 }
4216 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4217 ed.push_buffer_cursor_to_textarea();
4218 ed.vim.mode = Mode::Normal;
4219 }
4220 Operator::Delete => {
4221 ed.push_undo();
4222 cut_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4223 ed.vim.mode = Mode::Normal;
4224 }
4225 Operator::Change => {
4226 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
4229 ed.push_undo();
4230 ed.sync_buffer_content_from_textarea();
4231 let payload = read_vim_range(ed, (top, 0), (bot, 0), MotionKind::Linewise);
4232 if bot > top {
4233 ed.mutate_edit(Edit::DeleteRange {
4234 start: Position::new(top + 1, 0),
4235 end: Position::new(bot, 0),
4236 kind: BufKind::Line,
4237 });
4238 }
4239 let line_chars = buf_line_chars(&ed.buffer, top);
4240 if line_chars > 0 {
4241 ed.mutate_edit(Edit::DeleteRange {
4242 start: Position::new(top, 0),
4243 end: Position::new(top, line_chars),
4244 kind: BufKind::Char,
4245 });
4246 }
4247 if !payload.is_empty() {
4248 ed.record_yank_to_host(payload.clone());
4249 ed.record_delete(payload, true);
4250 }
4251 buf_set_cursor_rc(&mut ed.buffer, top, 0);
4252 ed.push_buffer_cursor_to_textarea();
4253 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4254 }
4255 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4256 let bot = buf_cursor_pos(&ed.buffer)
4257 .row
4258 .max(ed.vim.visual_line_anchor);
4259 apply_case_op_to_selection(ed, op, (top, 0), (bot, 0), MotionKind::Linewise);
4260 move_first_non_whitespace(ed);
4261 }
4262 Operator::Indent | Operator::Outdent => {
4263 ed.push_undo();
4264 let (cursor_row, _) = ed.cursor();
4265 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4266 if op == Operator::Indent {
4267 indent_rows(ed, top, bot, 1);
4268 } else {
4269 outdent_rows(ed, top, bot, 1);
4270 }
4271 ed.vim.mode = Mode::Normal;
4272 }
4273 Operator::Reflow => {
4274 ed.push_undo();
4275 let (cursor_row, _) = ed.cursor();
4276 let bot = cursor_row.max(ed.vim.visual_line_anchor);
4277 reflow_rows(ed, top, bot);
4278 ed.vim.mode = Mode::Normal;
4279 }
4280 Operator::Fold => unreachable!("Visual zf takes its own path"),
4283 }
4284 }
4285 Mode::Visual => {
4286 ed.vim.yank_linewise = false;
4287 let anchor = ed.vim.visual_anchor;
4288 let cursor = ed.cursor();
4289 let (top, bot) = order(anchor, cursor);
4290 match op {
4291 Operator::Yank => {
4292 let text = read_vim_range(ed, top, bot, MotionKind::Inclusive);
4293 if !text.is_empty() {
4294 ed.record_yank_to_host(text.clone());
4295 ed.record_yank(text, false);
4296 }
4297 buf_set_cursor_rc(&mut ed.buffer, top.0, top.1);
4298 ed.push_buffer_cursor_to_textarea();
4299 ed.vim.mode = Mode::Normal;
4300 }
4301 Operator::Delete => {
4302 ed.push_undo();
4303 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4304 ed.vim.mode = Mode::Normal;
4305 }
4306 Operator::Change => {
4307 ed.push_undo();
4308 cut_vim_range(ed, top, bot, MotionKind::Inclusive);
4309 begin_insert_noundo(ed, 1, InsertReason::AfterChange);
4310 }
4311 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4312 let anchor = ed.vim.visual_anchor;
4314 let cursor = ed.cursor();
4315 let (top, bot) = order(anchor, cursor);
4316 apply_case_op_to_selection(ed, op, top, bot, MotionKind::Inclusive);
4317 }
4318 Operator::Indent | Operator::Outdent => {
4319 ed.push_undo();
4320 let anchor = ed.vim.visual_anchor;
4321 let cursor = ed.cursor();
4322 let (top, bot) = order(anchor, cursor);
4323 if op == Operator::Indent {
4324 indent_rows(ed, top.0, bot.0, 1);
4325 } else {
4326 outdent_rows(ed, top.0, bot.0, 1);
4327 }
4328 ed.vim.mode = Mode::Normal;
4329 }
4330 Operator::Reflow => {
4331 ed.push_undo();
4332 let anchor = ed.vim.visual_anchor;
4333 let cursor = ed.cursor();
4334 let (top, bot) = order(anchor, cursor);
4335 reflow_rows(ed, top.0, bot.0);
4336 ed.vim.mode = Mode::Normal;
4337 }
4338 Operator::Fold => unreachable!("Visual zf takes its own path"),
4339 }
4340 }
4341 Mode::VisualBlock => apply_block_operator(ed, op),
4342 _ => {}
4343 }
4344}
4345
4346fn block_bounds<H: crate::types::Host>(
4351 ed: &Editor<hjkl_buffer::Buffer, H>,
4352) -> (usize, usize, usize, usize) {
4353 let (ar, ac) = ed.vim.block_anchor;
4354 let (cr, _) = ed.cursor();
4355 let cc = ed.vim.block_vcol;
4356 let top = ar.min(cr);
4357 let bot = ar.max(cr);
4358 let left = ac.min(cc);
4359 let right = ac.max(cc);
4360 (top, bot, left, right)
4361}
4362
4363fn update_block_vcol<H: crate::types::Host>(
4368 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4369 motion: &Motion,
4370) {
4371 match motion {
4372 Motion::Left
4373 | Motion::Right
4374 | Motion::WordFwd
4375 | Motion::BigWordFwd
4376 | Motion::WordBack
4377 | Motion::BigWordBack
4378 | Motion::WordEnd
4379 | Motion::BigWordEnd
4380 | Motion::WordEndBack
4381 | Motion::BigWordEndBack
4382 | Motion::LineStart
4383 | Motion::FirstNonBlank
4384 | Motion::LineEnd
4385 | Motion::Find { .. }
4386 | Motion::FindRepeat { .. }
4387 | Motion::MatchBracket => {
4388 ed.vim.block_vcol = ed.cursor().1;
4389 }
4390 _ => {}
4392 }
4393}
4394
4395fn apply_block_operator<H: crate::types::Host>(
4400 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4401 op: Operator,
4402) {
4403 let (top, bot, left, right) = block_bounds(ed);
4404 let yank = block_yank(ed, top, bot, left, right);
4406
4407 match op {
4408 Operator::Yank => {
4409 if !yank.is_empty() {
4410 ed.record_yank_to_host(yank.clone());
4411 ed.record_yank(yank, false);
4412 }
4413 ed.vim.mode = Mode::Normal;
4414 ed.jump_cursor(top, left);
4415 }
4416 Operator::Delete => {
4417 ed.push_undo();
4418 delete_block_contents(ed, top, bot, left, right);
4419 if !yank.is_empty() {
4420 ed.record_yank_to_host(yank.clone());
4421 ed.record_delete(yank, false);
4422 }
4423 ed.vim.mode = Mode::Normal;
4424 ed.jump_cursor(top, left);
4425 }
4426 Operator::Change => {
4427 ed.push_undo();
4428 delete_block_contents(ed, top, bot, left, right);
4429 if !yank.is_empty() {
4430 ed.record_yank_to_host(yank.clone());
4431 ed.record_delete(yank, false);
4432 }
4433 ed.jump_cursor(top, left);
4434 begin_insert_noundo(
4435 ed,
4436 1,
4437 InsertReason::BlockChange {
4438 top,
4439 bot,
4440 col: left,
4441 },
4442 );
4443 }
4444 Operator::Uppercase | Operator::Lowercase | Operator::ToggleCase => {
4445 ed.push_undo();
4446 transform_block_case(ed, op, top, bot, left, right);
4447 ed.vim.mode = Mode::Normal;
4448 ed.jump_cursor(top, left);
4449 }
4450 Operator::Indent | Operator::Outdent => {
4451 ed.push_undo();
4455 if op == Operator::Indent {
4456 indent_rows(ed, top, bot, 1);
4457 } else {
4458 outdent_rows(ed, top, bot, 1);
4459 }
4460 ed.vim.mode = Mode::Normal;
4461 }
4462 Operator::Fold => unreachable!("Visual zf takes its own path"),
4463 Operator::Reflow => {
4464 ed.push_undo();
4468 reflow_rows(ed, top, bot);
4469 ed.vim.mode = Mode::Normal;
4470 }
4471 }
4472}
4473
4474fn transform_block_case<H: crate::types::Host>(
4478 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4479 op: Operator,
4480 top: usize,
4481 bot: usize,
4482 left: usize,
4483 right: usize,
4484) {
4485 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4486 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4487 let chars: Vec<char> = lines[r].chars().collect();
4488 if left >= chars.len() {
4489 continue;
4490 }
4491 let end = (right + 1).min(chars.len());
4492 let head: String = chars[..left].iter().collect();
4493 let mid: String = chars[left..end].iter().collect();
4494 let tail: String = chars[end..].iter().collect();
4495 let transformed = match op {
4496 Operator::Uppercase => mid.to_uppercase(),
4497 Operator::Lowercase => mid.to_lowercase(),
4498 Operator::ToggleCase => toggle_case_str(&mid),
4499 _ => mid,
4500 };
4501 lines[r] = format!("{head}{transformed}{tail}");
4502 }
4503 let saved_yank = ed.yank().to_string();
4504 let saved_linewise = ed.vim.yank_linewise;
4505 ed.restore(lines, (top, left));
4506 ed.set_yank(saved_yank);
4507 ed.vim.yank_linewise = saved_linewise;
4508}
4509
4510fn block_yank<H: crate::types::Host>(
4511 ed: &Editor<hjkl_buffer::Buffer, H>,
4512 top: usize,
4513 bot: usize,
4514 left: usize,
4515 right: usize,
4516) -> String {
4517 let lines = buf_lines_to_vec(&ed.buffer);
4518 let mut rows: Vec<String> = Vec::new();
4519 for r in top..=bot {
4520 let line = match lines.get(r) {
4521 Some(l) => l,
4522 None => break,
4523 };
4524 let chars: Vec<char> = line.chars().collect();
4525 let end = (right + 1).min(chars.len());
4526 if left >= chars.len() {
4527 rows.push(String::new());
4528 } else {
4529 rows.push(chars[left..end].iter().collect());
4530 }
4531 }
4532 rows.join("\n")
4533}
4534
4535fn delete_block_contents<H: crate::types::Host>(
4536 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4537 top: usize,
4538 bot: usize,
4539 left: usize,
4540 right: usize,
4541) {
4542 use hjkl_buffer::{Edit, MotionKind, Position};
4543 ed.sync_buffer_content_from_textarea();
4544 let last_row = bot.min(buf_row_count(&ed.buffer).saturating_sub(1));
4545 if last_row < top {
4546 return;
4547 }
4548 ed.mutate_edit(Edit::DeleteRange {
4549 start: Position::new(top, left),
4550 end: Position::new(last_row, right),
4551 kind: MotionKind::Block,
4552 });
4553 ed.push_buffer_cursor_to_textarea();
4554}
4555
4556fn block_replace<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>, ch: char) {
4558 let (top, bot, left, right) = block_bounds(ed);
4559 ed.push_undo();
4560 ed.sync_buffer_content_from_textarea();
4561 let mut lines: Vec<String> = buf_lines_to_vec(&ed.buffer);
4562 for r in top..=bot.min(lines.len().saturating_sub(1)) {
4563 let chars: Vec<char> = lines[r].chars().collect();
4564 if left >= chars.len() {
4565 continue;
4566 }
4567 let end = (right + 1).min(chars.len());
4568 let before: String = chars[..left].iter().collect();
4569 let middle: String = std::iter::repeat_n(ch, end - left).collect();
4570 let after: String = chars[end..].iter().collect();
4571 lines[r] = format!("{before}{middle}{after}");
4572 }
4573 reset_textarea_lines(ed, lines);
4574 ed.vim.mode = Mode::Normal;
4575 ed.jump_cursor(top, left);
4576}
4577
4578fn reset_textarea_lines<H: crate::types::Host>(
4582 ed: &mut Editor<hjkl_buffer::Buffer, H>,
4583 lines: Vec<String>,
4584) {
4585 let cursor = ed.cursor();
4586 crate::types::BufferEdit::replace_all(&mut ed.buffer, &lines.join("\n"));
4587 buf_set_cursor_rc(&mut ed.buffer, cursor.0, cursor.1);
4588 ed.mark_content_dirty();
4589}
4590
4591type Pos = (usize, usize);
4597
4598fn text_object_range<H: crate::types::Host>(
4602 ed: &Editor<hjkl_buffer::Buffer, H>,
4603 obj: TextObject,
4604 inner: bool,
4605) -> Option<(Pos, Pos, MotionKind)> {
4606 match obj {
4607 TextObject::Word { big } => {
4608 word_text_object(ed, inner, big).map(|(s, e)| (s, e, MotionKind::Exclusive))
4609 }
4610 TextObject::Quote(q) => {
4611 quote_text_object(ed, q, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4612 }
4613 TextObject::Bracket(open) => bracket_text_object(ed, open, inner),
4614 TextObject::Paragraph => {
4615 paragraph_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Linewise))
4616 }
4617 TextObject::XmlTag => {
4618 tag_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4619 }
4620 TextObject::Sentence => {
4621 sentence_text_object(ed, inner).map(|(s, e)| (s, e, MotionKind::Exclusive))
4622 }
4623 }
4624}
4625
4626fn sentence_boundary<H: crate::types::Host>(
4630 ed: &Editor<hjkl_buffer::Buffer, H>,
4631 forward: bool,
4632) -> Option<(usize, usize)> {
4633 let lines = buf_lines_to_vec(&ed.buffer);
4634 if lines.is_empty() {
4635 return None;
4636 }
4637 let pos_to_idx = |pos: (usize, usize)| -> usize {
4638 let mut idx = 0;
4639 for line in lines.iter().take(pos.0) {
4640 idx += line.chars().count() + 1;
4641 }
4642 idx + pos.1
4643 };
4644 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4645 for (r, line) in lines.iter().enumerate() {
4646 let len = line.chars().count();
4647 if idx <= len {
4648 return (r, idx);
4649 }
4650 idx -= len + 1;
4651 }
4652 let last = lines.len().saturating_sub(1);
4653 (last, lines[last].chars().count())
4654 };
4655 let mut chars: Vec<char> = Vec::new();
4656 for (r, line) in lines.iter().enumerate() {
4657 chars.extend(line.chars());
4658 if r + 1 < lines.len() {
4659 chars.push('\n');
4660 }
4661 }
4662 if chars.is_empty() {
4663 return None;
4664 }
4665 let total = chars.len();
4666 let cursor_idx = pos_to_idx(ed.cursor()).min(total - 1);
4667 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4668
4669 if forward {
4670 let mut i = cursor_idx + 1;
4673 while i < total {
4674 if is_terminator(chars[i]) {
4675 while i + 1 < total && is_terminator(chars[i + 1]) {
4676 i += 1;
4677 }
4678 if i + 1 >= total {
4679 return None;
4680 }
4681 if chars[i + 1].is_whitespace() {
4682 let mut j = i + 1;
4683 while j < total && chars[j].is_whitespace() {
4684 j += 1;
4685 }
4686 if j >= total {
4687 return None;
4688 }
4689 return Some(idx_to_pos(j));
4690 }
4691 }
4692 i += 1;
4693 }
4694 None
4695 } else {
4696 let find_start = |from: usize| -> Option<usize> {
4700 let mut start = from;
4701 while start > 0 {
4702 let prev = chars[start - 1];
4703 if prev.is_whitespace() {
4704 let mut k = start - 1;
4705 while k > 0 && chars[k - 1].is_whitespace() {
4706 k -= 1;
4707 }
4708 if k > 0 && is_terminator(chars[k - 1]) {
4709 break;
4710 }
4711 }
4712 start -= 1;
4713 }
4714 while start < total && chars[start].is_whitespace() {
4715 start += 1;
4716 }
4717 (start < total).then_some(start)
4718 };
4719 let current_start = find_start(cursor_idx)?;
4720 if current_start < cursor_idx {
4721 return Some(idx_to_pos(current_start));
4722 }
4723 let mut k = current_start;
4726 while k > 0 && chars[k - 1].is_whitespace() {
4727 k -= 1;
4728 }
4729 if k == 0 {
4730 return None;
4731 }
4732 let prev_start = find_start(k - 1)?;
4733 Some(idx_to_pos(prev_start))
4734 }
4735}
4736
4737fn sentence_text_object<H: crate::types::Host>(
4743 ed: &Editor<hjkl_buffer::Buffer, H>,
4744 inner: bool,
4745) -> Option<((usize, usize), (usize, usize))> {
4746 let lines = buf_lines_to_vec(&ed.buffer);
4747 if lines.is_empty() {
4748 return None;
4749 }
4750 let pos_to_idx = |pos: (usize, usize)| -> usize {
4753 let mut idx = 0;
4754 for line in lines.iter().take(pos.0) {
4755 idx += line.chars().count() + 1;
4756 }
4757 idx + pos.1
4758 };
4759 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4760 for (r, line) in lines.iter().enumerate() {
4761 let len = line.chars().count();
4762 if idx <= len {
4763 return (r, idx);
4764 }
4765 idx -= len + 1;
4766 }
4767 let last = lines.len().saturating_sub(1);
4768 (last, lines[last].chars().count())
4769 };
4770 let mut chars: Vec<char> = Vec::new();
4771 for (r, line) in lines.iter().enumerate() {
4772 chars.extend(line.chars());
4773 if r + 1 < lines.len() {
4774 chars.push('\n');
4775 }
4776 }
4777 if chars.is_empty() {
4778 return None;
4779 }
4780
4781 let cursor_idx = pos_to_idx(ed.cursor()).min(chars.len() - 1);
4782 let is_terminator = |c: char| matches!(c, '.' | '?' | '!');
4783
4784 let mut start = cursor_idx;
4788 while start > 0 {
4789 let prev = chars[start - 1];
4790 if prev.is_whitespace() {
4791 let mut k = start - 1;
4795 while k > 0 && chars[k - 1].is_whitespace() {
4796 k -= 1;
4797 }
4798 if k > 0 && is_terminator(chars[k - 1]) {
4799 break;
4800 }
4801 }
4802 start -= 1;
4803 }
4804 while start < chars.len() && chars[start].is_whitespace() {
4807 start += 1;
4808 }
4809 if start >= chars.len() {
4810 return None;
4811 }
4812
4813 let mut end = start;
4816 while end < chars.len() {
4817 if is_terminator(chars[end]) {
4818 while end + 1 < chars.len() && is_terminator(chars[end + 1]) {
4820 end += 1;
4821 }
4822 if end + 1 >= chars.len() || chars[end + 1].is_whitespace() {
4825 break;
4826 }
4827 }
4828 end += 1;
4829 }
4830 let end_idx = (end + 1).min(chars.len());
4832
4833 let final_end = if inner {
4834 end_idx
4835 } else {
4836 let mut e = end_idx;
4840 while e < chars.len() && chars[e].is_whitespace() && chars[e] != '\n' {
4841 e += 1;
4842 }
4843 e
4844 };
4845
4846 Some((idx_to_pos(start), idx_to_pos(final_end)))
4847}
4848
4849fn tag_text_object<H: crate::types::Host>(
4853 ed: &Editor<hjkl_buffer::Buffer, H>,
4854 inner: bool,
4855) -> Option<((usize, usize), (usize, usize))> {
4856 let lines = buf_lines_to_vec(&ed.buffer);
4857 if lines.is_empty() {
4858 return None;
4859 }
4860 let pos_to_idx = |pos: (usize, usize)| -> usize {
4864 let mut idx = 0;
4865 for line in lines.iter().take(pos.0) {
4866 idx += line.chars().count() + 1;
4867 }
4868 idx + pos.1
4869 };
4870 let idx_to_pos = |mut idx: usize| -> (usize, usize) {
4871 for (r, line) in lines.iter().enumerate() {
4872 let len = line.chars().count();
4873 if idx <= len {
4874 return (r, idx);
4875 }
4876 idx -= len + 1;
4877 }
4878 let last = lines.len().saturating_sub(1);
4879 (last, lines[last].chars().count())
4880 };
4881 let mut chars: Vec<char> = Vec::new();
4882 for (r, line) in lines.iter().enumerate() {
4883 chars.extend(line.chars());
4884 if r + 1 < lines.len() {
4885 chars.push('\n');
4886 }
4887 }
4888 let cursor_idx = pos_to_idx(ed.cursor());
4889
4890 let mut stack: Vec<(usize, usize, String)> = Vec::new(); let mut innermost: Option<(usize, usize, usize, usize)> = None;
4898 let mut next_after: Option<(usize, usize, usize, usize)> = None;
4899 let mut i = 0;
4900 while i < chars.len() {
4901 if chars[i] != '<' {
4902 i += 1;
4903 continue;
4904 }
4905 let mut j = i + 1;
4906 while j < chars.len() && chars[j] != '>' {
4907 j += 1;
4908 }
4909 if j >= chars.len() {
4910 break;
4911 }
4912 let inside: String = chars[i + 1..j].iter().collect();
4913 let close_end = j + 1;
4914 let trimmed = inside.trim();
4915 if trimmed.starts_with('!') || trimmed.starts_with('?') {
4916 i = close_end;
4917 continue;
4918 }
4919 if let Some(rest) = trimmed.strip_prefix('/') {
4920 let name = rest.split_whitespace().next().unwrap_or("").to_string();
4921 if !name.is_empty()
4922 && let Some(stack_idx) = stack.iter().rposition(|(_, _, n)| *n == name)
4923 {
4924 let (open_start, content_start, _) = stack[stack_idx].clone();
4925 stack.truncate(stack_idx);
4926 let content_end = i;
4927 let candidate = (open_start, content_start, content_end, close_end);
4928 if cursor_idx >= content_start && cursor_idx <= content_end {
4929 innermost = match innermost {
4930 Some((_, cs, ce, _)) if cs <= content_start && content_end <= ce => {
4931 Some(candidate)
4932 }
4933 None => Some(candidate),
4934 existing => existing,
4935 };
4936 } else if open_start >= cursor_idx && next_after.is_none() {
4937 next_after = Some(candidate);
4938 }
4939 }
4940 } else if !trimmed.ends_with('/') {
4941 let name: String = trimmed
4942 .split(|c: char| c.is_whitespace() || c == '/')
4943 .next()
4944 .unwrap_or("")
4945 .to_string();
4946 if !name.is_empty() {
4947 stack.push((i, close_end, name));
4948 }
4949 }
4950 i = close_end;
4951 }
4952
4953 let (open_start, content_start, content_end, close_end) = innermost.or(next_after)?;
4954 if inner {
4955 Some((idx_to_pos(content_start), idx_to_pos(content_end)))
4956 } else {
4957 Some((idx_to_pos(open_start), idx_to_pos(close_end)))
4958 }
4959}
4960
4961fn is_wordchar(c: char) -> bool {
4962 c.is_alphanumeric() || c == '_'
4963}
4964
4965pub(crate) use hjkl_buffer::is_keyword_char;
4969
4970fn word_text_object<H: crate::types::Host>(
4971 ed: &Editor<hjkl_buffer::Buffer, H>,
4972 inner: bool,
4973 big: bool,
4974) -> Option<((usize, usize), (usize, usize))> {
4975 let (row, col) = ed.cursor();
4976 let line = buf_line(&ed.buffer, row)?;
4977 let chars: Vec<char> = line.chars().collect();
4978 if chars.is_empty() {
4979 return None;
4980 }
4981 let at = col.min(chars.len().saturating_sub(1));
4982 let classify = |c: char| -> u8 {
4983 if c.is_whitespace() {
4984 0
4985 } else if big || is_wordchar(c) {
4986 1
4987 } else {
4988 2
4989 }
4990 };
4991 let cls = classify(chars[at]);
4992 let mut start = at;
4993 while start > 0 && classify(chars[start - 1]) == cls {
4994 start -= 1;
4995 }
4996 let mut end = at;
4997 while end + 1 < chars.len() && classify(chars[end + 1]) == cls {
4998 end += 1;
4999 }
5000 let char_byte = |i: usize| {
5002 if i >= chars.len() {
5003 line.len()
5004 } else {
5005 line.char_indices().nth(i).map(|(b, _)| b).unwrap_or(0)
5006 }
5007 };
5008 let mut start_col = char_byte(start);
5009 let mut end_col = char_byte(end + 1);
5011 if !inner {
5012 let mut t = end + 1;
5014 let mut included_trailing = false;
5015 while t < chars.len() && chars[t].is_whitespace() {
5016 included_trailing = true;
5017 t += 1;
5018 }
5019 if included_trailing {
5020 end_col = char_byte(t);
5021 } else {
5022 let mut s = start;
5023 while s > 0 && chars[s - 1].is_whitespace() {
5024 s -= 1;
5025 }
5026 start_col = char_byte(s);
5027 }
5028 }
5029 Some(((row, start_col), (row, end_col)))
5030}
5031
5032fn quote_text_object<H: crate::types::Host>(
5033 ed: &Editor<hjkl_buffer::Buffer, H>,
5034 q: char,
5035 inner: bool,
5036) -> Option<((usize, usize), (usize, usize))> {
5037 let (row, col) = ed.cursor();
5038 let line = buf_line(&ed.buffer, row)?;
5039 let bytes = line.as_bytes();
5040 let q_byte = q as u8;
5041 let mut positions: Vec<usize> = Vec::new();
5043 for (i, &b) in bytes.iter().enumerate() {
5044 if b == q_byte {
5045 positions.push(i);
5046 }
5047 }
5048 if positions.len() < 2 {
5049 return None;
5050 }
5051 let mut open_idx: Option<usize> = None;
5052 let mut close_idx: Option<usize> = None;
5053 for pair in positions.chunks(2) {
5054 if pair.len() < 2 {
5055 break;
5056 }
5057 if col >= pair[0] && col <= pair[1] {
5058 open_idx = Some(pair[0]);
5059 close_idx = Some(pair[1]);
5060 break;
5061 }
5062 if col < pair[0] {
5063 open_idx = Some(pair[0]);
5064 close_idx = Some(pair[1]);
5065 break;
5066 }
5067 }
5068 let open = open_idx?;
5069 let close = close_idx?;
5070 if inner {
5072 if close <= open + 1 {
5073 return None;
5074 }
5075 Some(((row, open + 1), (row, close)))
5076 } else {
5077 let after_close = close + 1; if after_close < bytes.len() && bytes[after_close].is_ascii_whitespace() {
5084 let mut end = after_close;
5086 while end < bytes.len() && bytes[end].is_ascii_whitespace() {
5087 end += 1;
5088 }
5089 Some(((row, open), (row, end)))
5090 } else if open > 0 && bytes[open - 1].is_ascii_whitespace() {
5091 let mut start = open;
5093 while start > 0 && bytes[start - 1].is_ascii_whitespace() {
5094 start -= 1;
5095 }
5096 Some(((row, start), (row, close + 1)))
5097 } else {
5098 Some(((row, open), (row, close + 1)))
5099 }
5100 }
5101}
5102
5103fn bracket_text_object<H: crate::types::Host>(
5104 ed: &Editor<hjkl_buffer::Buffer, H>,
5105 open: char,
5106 inner: bool,
5107) -> Option<(Pos, Pos, MotionKind)> {
5108 let close = match open {
5109 '(' => ')',
5110 '[' => ']',
5111 '{' => '}',
5112 '<' => '>',
5113 _ => return None,
5114 };
5115 let (row, col) = ed.cursor();
5116 let lines = buf_lines_to_vec(&ed.buffer);
5117 let lines = lines.as_slice();
5118 let open_pos = find_open_bracket(lines, row, col, open, close)
5123 .or_else(|| find_next_open(lines, row, col, open))?;
5124 let close_pos = find_close_bracket(lines, open_pos.0, open_pos.1 + 1, open, close)?;
5125 if inner {
5127 if close_pos.0 > open_pos.0 + 1 {
5133 let inner_row_start = open_pos.0 + 1;
5135 let inner_row_end = close_pos.0 - 1;
5136 let end_col = lines
5137 .get(inner_row_end)
5138 .map(|l| l.chars().count())
5139 .unwrap_or(0);
5140 return Some((
5141 (inner_row_start, 0),
5142 (inner_row_end, end_col),
5143 MotionKind::Linewise,
5144 ));
5145 }
5146 let inner_start = advance_pos(lines, open_pos);
5147 if inner_start.0 > close_pos.0
5148 || (inner_start.0 == close_pos.0 && inner_start.1 >= close_pos.1)
5149 {
5150 return None;
5151 }
5152 Some((inner_start, close_pos, MotionKind::Exclusive))
5153 } else {
5154 Some((
5155 open_pos,
5156 advance_pos(lines, close_pos),
5157 MotionKind::Exclusive,
5158 ))
5159 }
5160}
5161
5162fn find_open_bracket(
5163 lines: &[String],
5164 row: usize,
5165 col: usize,
5166 open: char,
5167 close: char,
5168) -> Option<(usize, usize)> {
5169 let mut depth: i32 = 0;
5170 let mut r = row;
5171 let mut c = col as isize;
5172 loop {
5173 let cur = &lines[r];
5174 let chars: Vec<char> = cur.chars().collect();
5175 if (c as usize) >= chars.len() {
5179 c = chars.len() as isize - 1;
5180 }
5181 while c >= 0 {
5182 let ch = chars[c as usize];
5183 if ch == close {
5184 depth += 1;
5185 } else if ch == open {
5186 if depth == 0 {
5187 return Some((r, c as usize));
5188 }
5189 depth -= 1;
5190 }
5191 c -= 1;
5192 }
5193 if r == 0 {
5194 return None;
5195 }
5196 r -= 1;
5197 c = lines[r].chars().count() as isize - 1;
5198 }
5199}
5200
5201fn find_close_bracket(
5202 lines: &[String],
5203 row: usize,
5204 start_col: usize,
5205 open: char,
5206 close: char,
5207) -> Option<(usize, usize)> {
5208 let mut depth: i32 = 0;
5209 let mut r = row;
5210 let mut c = start_col;
5211 loop {
5212 let cur = &lines[r];
5213 let chars: Vec<char> = cur.chars().collect();
5214 while c < chars.len() {
5215 let ch = chars[c];
5216 if ch == open {
5217 depth += 1;
5218 } else if ch == close {
5219 if depth == 0 {
5220 return Some((r, c));
5221 }
5222 depth -= 1;
5223 }
5224 c += 1;
5225 }
5226 if r + 1 >= lines.len() {
5227 return None;
5228 }
5229 r += 1;
5230 c = 0;
5231 }
5232}
5233
5234fn find_next_open(lines: &[String], row: usize, col: usize, open: char) -> Option<(usize, usize)> {
5238 let mut r = row;
5239 let mut c = col;
5240 while r < lines.len() {
5241 let chars: Vec<char> = lines[r].chars().collect();
5242 while c < chars.len() {
5243 if chars[c] == open {
5244 return Some((r, c));
5245 }
5246 c += 1;
5247 }
5248 r += 1;
5249 c = 0;
5250 }
5251 None
5252}
5253
5254fn advance_pos(lines: &[String], pos: (usize, usize)) -> (usize, usize) {
5255 let (r, c) = pos;
5256 let line_len = lines[r].chars().count();
5257 if c < line_len {
5258 (r, c + 1)
5259 } else if r + 1 < lines.len() {
5260 (r + 1, 0)
5261 } else {
5262 pos
5263 }
5264}
5265
5266fn paragraph_text_object<H: crate::types::Host>(
5267 ed: &Editor<hjkl_buffer::Buffer, H>,
5268 inner: bool,
5269) -> Option<((usize, usize), (usize, usize))> {
5270 let (row, _) = ed.cursor();
5271 let lines = buf_lines_to_vec(&ed.buffer);
5272 if lines.is_empty() {
5273 return None;
5274 }
5275 let is_blank = |r: usize| lines.get(r).map(|s| s.trim().is_empty()).unwrap_or(true);
5277 if is_blank(row) {
5278 return None;
5279 }
5280 let mut top = row;
5281 while top > 0 && !is_blank(top - 1) {
5282 top -= 1;
5283 }
5284 let mut bot = row;
5285 while bot + 1 < lines.len() && !is_blank(bot + 1) {
5286 bot += 1;
5287 }
5288 if !inner && bot + 1 < lines.len() && is_blank(bot + 1) {
5290 bot += 1;
5291 }
5292 let end_col = lines[bot].chars().count();
5293 Some(((top, 0), (bot, end_col)))
5294}
5295
5296fn read_vim_range<H: crate::types::Host>(
5302 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5303 start: (usize, usize),
5304 end: (usize, usize),
5305 kind: MotionKind,
5306) -> String {
5307 let (top, bot) = order(start, end);
5308 ed.sync_buffer_content_from_textarea();
5309 let lines = buf_lines_to_vec(&ed.buffer);
5310 match kind {
5311 MotionKind::Linewise => {
5312 let lo = top.0;
5313 let hi = bot.0.min(lines.len().saturating_sub(1));
5314 let mut text = lines[lo..=hi].join("\n");
5315 text.push('\n');
5316 text
5317 }
5318 MotionKind::Inclusive | MotionKind::Exclusive => {
5319 let inclusive = matches!(kind, MotionKind::Inclusive);
5320 let mut out = String::new();
5322 for row in top.0..=bot.0 {
5323 let line = lines.get(row).map(String::as_str).unwrap_or("");
5324 let lo = if row == top.0 { top.1 } else { 0 };
5325 let hi_unclamped = if row == bot.0 {
5326 if inclusive { bot.1 + 1 } else { bot.1 }
5327 } else {
5328 line.chars().count() + 1
5329 };
5330 let row_chars: Vec<char> = line.chars().collect();
5331 let hi = hi_unclamped.min(row_chars.len());
5332 if lo < hi {
5333 out.push_str(&row_chars[lo..hi].iter().collect::<String>());
5334 }
5335 if row < bot.0 {
5336 out.push('\n');
5337 }
5338 }
5339 out
5340 }
5341 }
5342}
5343
5344fn cut_vim_range<H: crate::types::Host>(
5353 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5354 start: (usize, usize),
5355 end: (usize, usize),
5356 kind: MotionKind,
5357) -> String {
5358 use hjkl_buffer::{Edit, MotionKind as BufKind, Position};
5359 let (top, bot) = order(start, end);
5360 ed.sync_buffer_content_from_textarea();
5361 let (buf_start, buf_end, buf_kind) = match kind {
5362 MotionKind::Linewise => (
5363 Position::new(top.0, 0),
5364 Position::new(bot.0, 0),
5365 BufKind::Line,
5366 ),
5367 MotionKind::Inclusive => {
5368 let line_chars = buf_line_chars(&ed.buffer, bot.0);
5369 let next = if bot.1 < line_chars {
5373 Position::new(bot.0, bot.1 + 1)
5374 } else if bot.0 + 1 < buf_row_count(&ed.buffer) {
5375 Position::new(bot.0 + 1, 0)
5376 } else {
5377 Position::new(bot.0, line_chars)
5378 };
5379 (Position::new(top.0, top.1), next, BufKind::Char)
5380 }
5381 MotionKind::Exclusive => (
5382 Position::new(top.0, top.1),
5383 Position::new(bot.0, bot.1),
5384 BufKind::Char,
5385 ),
5386 };
5387 let inverse = ed.mutate_edit(Edit::DeleteRange {
5388 start: buf_start,
5389 end: buf_end,
5390 kind: buf_kind,
5391 });
5392 let text = match inverse {
5393 Edit::InsertStr { text, .. } => text,
5394 _ => String::new(),
5395 };
5396 if !text.is_empty() {
5397 ed.record_yank_to_host(text.clone());
5398 ed.record_delete(text.clone(), matches!(kind, MotionKind::Linewise));
5399 }
5400 ed.push_buffer_cursor_to_textarea();
5401 text
5402}
5403
5404fn delete_to_eol<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5410 use hjkl_buffer::{Edit, MotionKind, Position};
5411 ed.sync_buffer_content_from_textarea();
5412 let cursor = buf_cursor_pos(&ed.buffer);
5413 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5414 if cursor.col >= line_chars {
5415 return;
5416 }
5417 let inverse = ed.mutate_edit(Edit::DeleteRange {
5418 start: cursor,
5419 end: Position::new(cursor.row, line_chars),
5420 kind: MotionKind::Char,
5421 });
5422 if let Edit::InsertStr { text, .. } = inverse
5423 && !text.is_empty()
5424 {
5425 ed.record_yank_to_host(text.clone());
5426 ed.vim.yank_linewise = false;
5427 ed.set_yank(text);
5428 }
5429 buf_set_cursor_pos(&mut ed.buffer, cursor);
5430 ed.push_buffer_cursor_to_textarea();
5431}
5432
5433fn do_char_delete<H: crate::types::Host>(
5434 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5435 forward: bool,
5436 count: usize,
5437) {
5438 use hjkl_buffer::{Edit, MotionKind, Position};
5439 ed.push_undo();
5440 ed.sync_buffer_content_from_textarea();
5441 let mut deleted = String::new();
5444 for _ in 0..count {
5445 let cursor = buf_cursor_pos(&ed.buffer);
5446 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5447 if forward {
5448 if cursor.col >= line_chars {
5451 continue;
5452 }
5453 let inverse = ed.mutate_edit(Edit::DeleteRange {
5454 start: cursor,
5455 end: Position::new(cursor.row, cursor.col + 1),
5456 kind: MotionKind::Char,
5457 });
5458 if let Edit::InsertStr { text, .. } = inverse {
5459 deleted.push_str(&text);
5460 }
5461 } else {
5462 if cursor.col == 0 {
5464 continue;
5465 }
5466 let inverse = ed.mutate_edit(Edit::DeleteRange {
5467 start: Position::new(cursor.row, cursor.col - 1),
5468 end: cursor,
5469 kind: MotionKind::Char,
5470 });
5471 if let Edit::InsertStr { text, .. } = inverse {
5472 deleted = text + &deleted;
5475 }
5476 }
5477 }
5478 if !deleted.is_empty() {
5479 ed.record_yank_to_host(deleted.clone());
5480 ed.record_delete(deleted, false);
5481 }
5482 ed.push_buffer_cursor_to_textarea();
5483}
5484
5485fn adjust_number<H: crate::types::Host>(
5489 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5490 delta: i64,
5491) -> bool {
5492 use hjkl_buffer::{Edit, MotionKind, Position};
5493 ed.sync_buffer_content_from_textarea();
5494 let cursor = buf_cursor_pos(&ed.buffer);
5495 let row = cursor.row;
5496 let chars: Vec<char> = match buf_line(&ed.buffer, row) {
5497 Some(l) => l.chars().collect(),
5498 None => return false,
5499 };
5500 let Some(digit_start) = (cursor.col..chars.len()).find(|&i| chars[i].is_ascii_digit()) else {
5501 return false;
5502 };
5503 let span_start = if digit_start > 0 && chars[digit_start - 1] == '-' {
5504 digit_start - 1
5505 } else {
5506 digit_start
5507 };
5508 let mut span_end = digit_start;
5509 while span_end < chars.len() && chars[span_end].is_ascii_digit() {
5510 span_end += 1;
5511 }
5512 let s: String = chars[span_start..span_end].iter().collect();
5513 let Ok(n) = s.parse::<i64>() else {
5514 return false;
5515 };
5516 let new_s = n.saturating_add(delta).to_string();
5517
5518 ed.push_undo();
5519 let span_start_pos = Position::new(row, span_start);
5520 let span_end_pos = Position::new(row, span_end);
5521 ed.mutate_edit(Edit::DeleteRange {
5522 start: span_start_pos,
5523 end: span_end_pos,
5524 kind: MotionKind::Char,
5525 });
5526 ed.mutate_edit(Edit::InsertStr {
5527 at: span_start_pos,
5528 text: new_s.clone(),
5529 });
5530 let new_len = new_s.chars().count();
5531 buf_set_cursor_rc(&mut ed.buffer, row, span_start + new_len.saturating_sub(1));
5532 ed.push_buffer_cursor_to_textarea();
5533 true
5534}
5535
5536pub(crate) fn replace_char<H: crate::types::Host>(
5537 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5538 ch: char,
5539 count: usize,
5540) {
5541 use hjkl_buffer::{Edit, MotionKind, Position};
5542 ed.push_undo();
5543 ed.sync_buffer_content_from_textarea();
5544 for _ in 0..count {
5545 let cursor = buf_cursor_pos(&ed.buffer);
5546 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5547 if cursor.col >= line_chars {
5548 break;
5549 }
5550 ed.mutate_edit(Edit::DeleteRange {
5551 start: cursor,
5552 end: Position::new(cursor.row, cursor.col + 1),
5553 kind: MotionKind::Char,
5554 });
5555 ed.mutate_edit(Edit::InsertChar { at: cursor, ch });
5556 }
5557 crate::motions::move_left(&mut ed.buffer, 1);
5559 ed.push_buffer_cursor_to_textarea();
5560}
5561
5562fn toggle_case_at_cursor<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5563 use hjkl_buffer::{Edit, MotionKind, Position};
5564 ed.sync_buffer_content_from_textarea();
5565 let cursor = buf_cursor_pos(&ed.buffer);
5566 let Some(c) = buf_line(&ed.buffer, cursor.row).and_then(|l| l.chars().nth(cursor.col)) else {
5567 return;
5568 };
5569 let toggled = if c.is_uppercase() {
5570 c.to_lowercase().next().unwrap_or(c)
5571 } else {
5572 c.to_uppercase().next().unwrap_or(c)
5573 };
5574 ed.mutate_edit(Edit::DeleteRange {
5575 start: cursor,
5576 end: Position::new(cursor.row, cursor.col + 1),
5577 kind: MotionKind::Char,
5578 });
5579 ed.mutate_edit(Edit::InsertChar {
5580 at: cursor,
5581 ch: toggled,
5582 });
5583}
5584
5585fn join_line<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5586 use hjkl_buffer::{Edit, Position};
5587 ed.sync_buffer_content_from_textarea();
5588 let row = buf_cursor_pos(&ed.buffer).row;
5589 if row + 1 >= buf_row_count(&ed.buffer) {
5590 return;
5591 }
5592 let cur_line = buf_line(&ed.buffer, row).unwrap_or("").to_string();
5593 let next_raw = buf_line(&ed.buffer, row + 1).unwrap_or("").to_string();
5594 let next_trimmed = next_raw.trim_start();
5595 let cur_chars = cur_line.chars().count();
5596 let next_chars = next_raw.chars().count();
5597 let separator = if !cur_line.is_empty() && !next_trimmed.is_empty() {
5600 " "
5601 } else {
5602 ""
5603 };
5604 let joined = format!("{cur_line}{separator}{next_trimmed}");
5605 ed.mutate_edit(Edit::Replace {
5606 start: Position::new(row, 0),
5607 end: Position::new(row + 1, next_chars),
5608 with: joined,
5609 });
5610 buf_set_cursor_rc(&mut ed.buffer, row, cur_chars);
5614 ed.push_buffer_cursor_to_textarea();
5615}
5616
5617fn join_line_raw<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5620 use hjkl_buffer::Edit;
5621 ed.sync_buffer_content_from_textarea();
5622 let row = buf_cursor_pos(&ed.buffer).row;
5623 if row + 1 >= buf_row_count(&ed.buffer) {
5624 return;
5625 }
5626 let join_col = buf_line_chars(&ed.buffer, row);
5627 ed.mutate_edit(Edit::JoinLines {
5628 row,
5629 count: 1,
5630 with_space: false,
5631 });
5632 buf_set_cursor_rc(&mut ed.buffer, row, join_col);
5634 ed.push_buffer_cursor_to_textarea();
5635}
5636
5637fn do_paste<H: crate::types::Host>(
5638 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5639 before: bool,
5640 count: usize,
5641) {
5642 use hjkl_buffer::{Edit, Position};
5643 ed.push_undo();
5644 let selector = ed.vim.pending_register.take();
5649 let (yank, linewise) = match selector.and_then(|c| ed.registers().read(c)) {
5650 Some(slot) => (slot.text.clone(), slot.linewise),
5651 None => {
5657 let s = &ed.registers().unnamed;
5658 (s.text.clone(), s.linewise)
5659 }
5660 };
5661 let mut paste_mark: Option<((usize, usize), (usize, usize))> = None;
5665 for _ in 0..count {
5666 ed.sync_buffer_content_from_textarea();
5667 let yank = yank.clone();
5668 if yank.is_empty() {
5669 continue;
5670 }
5671 if linewise {
5672 let text = yank.trim_matches('\n').to_string();
5676 let row = buf_cursor_pos(&ed.buffer).row;
5677 let target_row = if before {
5678 ed.mutate_edit(Edit::InsertStr {
5679 at: Position::new(row, 0),
5680 text: format!("{text}\n"),
5681 });
5682 row
5683 } else {
5684 let line_chars = buf_line_chars(&ed.buffer, row);
5685 ed.mutate_edit(Edit::InsertStr {
5686 at: Position::new(row, line_chars),
5687 text: format!("\n{text}"),
5688 });
5689 row + 1
5690 };
5691 buf_set_cursor_rc(&mut ed.buffer, target_row, 0);
5692 crate::motions::move_first_non_blank(&mut ed.buffer);
5693 ed.push_buffer_cursor_to_textarea();
5694 let payload_lines = text.lines().count().max(1);
5696 let bot_row = target_row + payload_lines - 1;
5697 let bot_last_col = buf_line_chars(&ed.buffer, bot_row).saturating_sub(1);
5698 paste_mark = Some(((target_row, 0), (bot_row, bot_last_col)));
5699 } else {
5700 let cursor = buf_cursor_pos(&ed.buffer);
5704 let at = if before {
5705 cursor
5706 } else {
5707 let line_chars = buf_line_chars(&ed.buffer, cursor.row);
5708 Position::new(cursor.row, (cursor.col + 1).min(line_chars))
5709 };
5710 ed.mutate_edit(Edit::InsertStr {
5711 at,
5712 text: yank.clone(),
5713 });
5714 crate::motions::move_left(&mut ed.buffer, 1);
5717 ed.push_buffer_cursor_to_textarea();
5718 let lo = (at.row, at.col);
5720 let hi = ed.cursor();
5721 paste_mark = Some((lo, hi));
5722 }
5723 }
5724 if let Some((lo, hi)) = paste_mark {
5725 ed.set_mark('[', lo);
5726 ed.set_mark(']', hi);
5727 }
5728 ed.sticky_col = Some(buf_cursor_pos(&ed.buffer).col);
5730}
5731
5732pub(crate) fn do_undo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5733 if let Some((lines, cursor)) = ed.undo_stack.pop() {
5734 let current = ed.snapshot();
5735 ed.redo_stack.push(current);
5736 ed.restore(lines, cursor);
5737 }
5738 ed.vim.mode = Mode::Normal;
5739 clamp_cursor_to_normal_mode(ed);
5743}
5744
5745pub(crate) fn do_redo<H: crate::types::Host>(ed: &mut Editor<hjkl_buffer::Buffer, H>) {
5746 if let Some((lines, cursor)) = ed.redo_stack.pop() {
5747 let current = ed.snapshot();
5748 ed.undo_stack.push(current);
5749 ed.cap_undo();
5750 ed.restore(lines, cursor);
5751 }
5752 ed.vim.mode = Mode::Normal;
5753}
5754
5755fn replay_insert_and_finish<H: crate::types::Host>(
5762 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5763 text: &str,
5764) {
5765 use hjkl_buffer::{Edit, Position};
5766 let cursor = ed.cursor();
5767 ed.mutate_edit(Edit::InsertStr {
5768 at: Position::new(cursor.0, cursor.1),
5769 text: text.to_string(),
5770 });
5771 if ed.vim.insert_session.take().is_some() {
5772 if ed.cursor().1 > 0 {
5773 crate::motions::move_left(&mut ed.buffer, 1);
5774 ed.push_buffer_cursor_to_textarea();
5775 }
5776 ed.vim.mode = Mode::Normal;
5777 }
5778}
5779
5780fn replay_last_change<H: crate::types::Host>(
5781 ed: &mut Editor<hjkl_buffer::Buffer, H>,
5782 outer_count: usize,
5783) {
5784 let Some(change) = ed.vim.last_change.clone() else {
5785 return;
5786 };
5787 ed.vim.replaying = true;
5788 let scale = if outer_count > 0 { outer_count } else { 1 };
5789 match change {
5790 LastChange::OpMotion {
5791 op,
5792 motion,
5793 count,
5794 inserted,
5795 } => {
5796 let total = count.max(1) * scale;
5797 apply_op_with_motion(ed, op, &motion, total);
5798 if let Some(text) = inserted {
5799 replay_insert_and_finish(ed, &text);
5800 }
5801 }
5802 LastChange::OpTextObj {
5803 op,
5804 obj,
5805 inner,
5806 inserted,
5807 } => {
5808 apply_op_with_text_object(ed, op, obj, inner);
5809 if let Some(text) = inserted {
5810 replay_insert_and_finish(ed, &text);
5811 }
5812 }
5813 LastChange::LineOp {
5814 op,
5815 count,
5816 inserted,
5817 } => {
5818 let total = count.max(1) * scale;
5819 execute_line_op(ed, op, total);
5820 if let Some(text) = inserted {
5821 replay_insert_and_finish(ed, &text);
5822 }
5823 }
5824 LastChange::CharDel { forward, count } => {
5825 do_char_delete(ed, forward, count * scale);
5826 }
5827 LastChange::ReplaceChar { ch, count } => {
5828 replace_char(ed, ch, count * scale);
5829 }
5830 LastChange::ToggleCase { count } => {
5831 for _ in 0..count * scale {
5832 ed.push_undo();
5833 toggle_case_at_cursor(ed);
5834 }
5835 }
5836 LastChange::JoinLine { count } => {
5837 for _ in 0..count * scale {
5838 ed.push_undo();
5839 join_line(ed);
5840 }
5841 }
5842 LastChange::Paste { before, count } => {
5843 do_paste(ed, before, count * scale);
5844 }
5845 LastChange::DeleteToEol { inserted } => {
5846 use hjkl_buffer::{Edit, Position};
5847 ed.push_undo();
5848 delete_to_eol(ed);
5849 if let Some(text) = inserted {
5850 let cursor = ed.cursor();
5851 ed.mutate_edit(Edit::InsertStr {
5852 at: Position::new(cursor.0, cursor.1),
5853 text,
5854 });
5855 }
5856 }
5857 LastChange::OpenLine { above, inserted } => {
5858 use hjkl_buffer::{Edit, Position};
5859 ed.push_undo();
5860 ed.sync_buffer_content_from_textarea();
5861 let row = buf_cursor_pos(&ed.buffer).row;
5862 if above {
5863 ed.mutate_edit(Edit::InsertStr {
5864 at: Position::new(row, 0),
5865 text: "\n".to_string(),
5866 });
5867 let folds = crate::buffer_impl::SnapshotFoldProvider::from_buffer(&ed.buffer);
5868 crate::motions::move_up(&mut ed.buffer, &folds, 1, &mut ed.sticky_col);
5869 } else {
5870 let line_chars = buf_line_chars(&ed.buffer, row);
5871 ed.mutate_edit(Edit::InsertStr {
5872 at: Position::new(row, line_chars),
5873 text: "\n".to_string(),
5874 });
5875 }
5876 ed.push_buffer_cursor_to_textarea();
5877 let cursor = ed.cursor();
5878 ed.mutate_edit(Edit::InsertStr {
5879 at: Position::new(cursor.0, cursor.1),
5880 text: inserted,
5881 });
5882 }
5883 LastChange::InsertAt {
5884 entry,
5885 inserted,
5886 count,
5887 } => {
5888 use hjkl_buffer::{Edit, Position};
5889 ed.push_undo();
5890 match entry {
5891 InsertEntry::I => {}
5892 InsertEntry::ShiftI => move_first_non_whitespace(ed),
5893 InsertEntry::A => {
5894 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5895 ed.push_buffer_cursor_to_textarea();
5896 }
5897 InsertEntry::ShiftA => {
5898 crate::motions::move_line_end(&mut ed.buffer);
5899 crate::motions::move_right_to_end(&mut ed.buffer, 1);
5900 ed.push_buffer_cursor_to_textarea();
5901 }
5902 }
5903 for _ in 0..count.max(1) {
5904 let cursor = ed.cursor();
5905 ed.mutate_edit(Edit::InsertStr {
5906 at: Position::new(cursor.0, cursor.1),
5907 text: inserted.clone(),
5908 });
5909 }
5910 }
5911 }
5912 ed.vim.replaying = false;
5913}
5914
5915fn extract_inserted(before: &str, after: &str) -> String {
5918 let before_chars: Vec<char> = before.chars().collect();
5919 let after_chars: Vec<char> = after.chars().collect();
5920 if after_chars.len() <= before_chars.len() {
5921 return String::new();
5922 }
5923 let prefix = before_chars
5924 .iter()
5925 .zip(after_chars.iter())
5926 .take_while(|(a, b)| a == b)
5927 .count();
5928 let max_suffix = before_chars.len() - prefix;
5929 let suffix = before_chars
5930 .iter()
5931 .rev()
5932 .zip(after_chars.iter().rev())
5933 .take(max_suffix)
5934 .take_while(|(a, b)| a == b)
5935 .count();
5936 after_chars[prefix..after_chars.len() - suffix]
5937 .iter()
5938 .collect()
5939}
5940
5941#[cfg(all(test, feature = "crossterm"))]
5944mod tests {
5945 use crate::VimMode;
5946 use crate::editor::Editor;
5947 use crate::types::Host;
5948 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5949
5950 fn run_keys<H: crate::types::Host>(e: &mut Editor<hjkl_buffer::Buffer, H>, keys: &str) {
5951 let mut iter = keys.chars().peekable();
5955 while let Some(c) = iter.next() {
5956 if c == '<' {
5957 let mut tag = String::new();
5958 for ch in iter.by_ref() {
5959 if ch == '>' {
5960 break;
5961 }
5962 tag.push(ch);
5963 }
5964 let ev = match tag.as_str() {
5965 "Esc" => KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE),
5966 "CR" => KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE),
5967 "BS" => KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE),
5968 "Space" => KeyEvent::new(KeyCode::Char(' '), KeyModifiers::NONE),
5969 "Up" => KeyEvent::new(KeyCode::Up, KeyModifiers::NONE),
5970 "Down" => KeyEvent::new(KeyCode::Down, KeyModifiers::NONE),
5971 "Left" => KeyEvent::new(KeyCode::Left, KeyModifiers::NONE),
5972 "Right" => KeyEvent::new(KeyCode::Right, KeyModifiers::NONE),
5973 "lt" => KeyEvent::new(KeyCode::Char('<'), KeyModifiers::NONE),
5977 s if s.starts_with("C-") => {
5978 let ch = s.chars().nth(2).unwrap();
5979 KeyEvent::new(KeyCode::Char(ch), KeyModifiers::CONTROL)
5980 }
5981 _ => continue,
5982 };
5983 e.handle_key(ev);
5984 } else {
5985 let mods = if c.is_uppercase() {
5986 KeyModifiers::SHIFT
5987 } else {
5988 KeyModifiers::NONE
5989 };
5990 e.handle_key(KeyEvent::new(KeyCode::Char(c), mods));
5991 }
5992 }
5993 }
5994
5995 fn editor_with(content: &str) -> Editor {
5996 let opts = crate::types::Options {
6001 shiftwidth: 2,
6002 ..crate::types::Options::default()
6003 };
6004 let mut e = Editor::new(
6005 hjkl_buffer::Buffer::new(),
6006 crate::types::DefaultHost::new(),
6007 opts,
6008 );
6009 e.set_content(content);
6010 e
6011 }
6012
6013 #[test]
6014 fn f_char_jumps_on_line() {
6015 let mut e = editor_with("hello world");
6016 run_keys(&mut e, "fw");
6017 assert_eq!(e.cursor(), (0, 6));
6018 }
6019
6020 #[test]
6021 fn cap_f_jumps_backward() {
6022 let mut e = editor_with("hello world");
6023 e.jump_cursor(0, 10);
6024 run_keys(&mut e, "Fo");
6025 assert_eq!(e.cursor().1, 7);
6026 }
6027
6028 #[test]
6029 fn t_stops_before_char() {
6030 let mut e = editor_with("hello");
6031 run_keys(&mut e, "tl");
6032 assert_eq!(e.cursor(), (0, 1));
6033 }
6034
6035 #[test]
6036 fn semicolon_repeats_find() {
6037 let mut e = editor_with("aa.bb.cc");
6038 run_keys(&mut e, "f.");
6039 assert_eq!(e.cursor().1, 2);
6040 run_keys(&mut e, ";");
6041 assert_eq!(e.cursor().1, 5);
6042 }
6043
6044 #[test]
6045 fn comma_repeats_find_reverse() {
6046 let mut e = editor_with("aa.bb.cc");
6047 run_keys(&mut e, "f.");
6048 run_keys(&mut e, ";");
6049 run_keys(&mut e, ",");
6050 assert_eq!(e.cursor().1, 2);
6051 }
6052
6053 #[test]
6054 fn di_quote_deletes_content() {
6055 let mut e = editor_with("foo \"bar\" baz");
6056 e.jump_cursor(0, 6); run_keys(&mut e, "di\"");
6058 assert_eq!(e.buffer().lines()[0], "foo \"\" baz");
6059 }
6060
6061 #[test]
6062 fn da_quote_deletes_with_quotes() {
6063 let mut e = editor_with("foo \"bar\" baz");
6066 e.jump_cursor(0, 6);
6067 run_keys(&mut e, "da\"");
6068 assert_eq!(e.buffer().lines()[0], "foo baz");
6069 }
6070
6071 #[test]
6072 fn ci_paren_deletes_and_inserts() {
6073 let mut e = editor_with("fn(a, b, c)");
6074 e.jump_cursor(0, 5);
6075 run_keys(&mut e, "ci(");
6076 assert_eq!(e.vim_mode(), VimMode::Insert);
6077 assert_eq!(e.buffer().lines()[0], "fn()");
6078 }
6079
6080 #[test]
6081 fn diw_deletes_inner_word() {
6082 let mut e = editor_with("hello world");
6083 e.jump_cursor(0, 2);
6084 run_keys(&mut e, "diw");
6085 assert_eq!(e.buffer().lines()[0], " world");
6086 }
6087
6088 #[test]
6089 fn daw_deletes_word_with_trailing_space() {
6090 let mut e = editor_with("hello world");
6091 run_keys(&mut e, "daw");
6092 assert_eq!(e.buffer().lines()[0], "world");
6093 }
6094
6095 #[test]
6096 fn percent_jumps_to_matching_bracket() {
6097 let mut e = editor_with("foo(bar)");
6098 e.jump_cursor(0, 3);
6099 run_keys(&mut e, "%");
6100 assert_eq!(e.cursor().1, 7);
6101 run_keys(&mut e, "%");
6102 assert_eq!(e.cursor().1, 3);
6103 }
6104
6105 #[test]
6106 fn dot_repeats_last_change() {
6107 let mut e = editor_with("aaa bbb ccc");
6108 run_keys(&mut e, "dw");
6109 assert_eq!(e.buffer().lines()[0], "bbb ccc");
6110 run_keys(&mut e, ".");
6111 assert_eq!(e.buffer().lines()[0], "ccc");
6112 }
6113
6114 #[test]
6115 fn dot_repeats_change_operator_with_text() {
6116 let mut e = editor_with("foo foo foo");
6117 run_keys(&mut e, "cwbar<Esc>");
6118 assert_eq!(e.buffer().lines()[0], "bar foo foo");
6119 run_keys(&mut e, "w");
6121 run_keys(&mut e, ".");
6122 assert_eq!(e.buffer().lines()[0], "bar bar foo");
6123 }
6124
6125 #[test]
6126 fn dot_repeats_x() {
6127 let mut e = editor_with("abcdef");
6128 run_keys(&mut e, "x");
6129 run_keys(&mut e, "..");
6130 assert_eq!(e.buffer().lines()[0], "def");
6131 }
6132
6133 #[test]
6134 fn count_operator_motion_compose() {
6135 let mut e = editor_with("one two three four five");
6136 run_keys(&mut e, "d3w");
6137 assert_eq!(e.buffer().lines()[0], "four five");
6138 }
6139
6140 #[test]
6141 fn two_dd_deletes_two_lines() {
6142 let mut e = editor_with("a\nb\nc");
6143 run_keys(&mut e, "2dd");
6144 assert_eq!(e.buffer().lines().len(), 1);
6145 assert_eq!(e.buffer().lines()[0], "c");
6146 }
6147
6148 #[test]
6153 fn dd_in_middle_puts_cursor_on_first_non_blank_of_next() {
6154 let mut e = editor_with("one\ntwo\n three\nfour");
6155 e.jump_cursor(1, 2);
6156 run_keys(&mut e, "dd");
6157 assert_eq!(e.buffer().lines()[1], " three");
6159 assert_eq!(e.cursor(), (1, 4));
6160 }
6161
6162 #[test]
6163 fn dd_on_last_line_puts_cursor_on_first_non_blank_of_prev() {
6164 let mut e = editor_with("one\n two\nthree");
6165 e.jump_cursor(2, 0);
6166 run_keys(&mut e, "dd");
6167 assert_eq!(e.buffer().lines().len(), 2);
6169 assert_eq!(e.cursor(), (1, 2));
6170 }
6171
6172 #[test]
6173 fn dd_on_only_line_leaves_empty_buffer_and_cursor_at_zero() {
6174 let mut e = editor_with("lonely");
6175 run_keys(&mut e, "dd");
6176 assert_eq!(e.buffer().lines().len(), 1);
6177 assert_eq!(e.buffer().lines()[0], "");
6178 assert_eq!(e.cursor(), (0, 0));
6179 }
6180
6181 #[test]
6182 fn count_dd_puts_cursor_on_first_non_blank_of_remaining() {
6183 let mut e = editor_with("a\nb\nc\n d\ne");
6184 e.jump_cursor(1, 0);
6186 run_keys(&mut e, "3dd");
6187 assert_eq!(e.buffer().lines(), &["a".to_string(), "e".to_string()]);
6188 assert_eq!(e.cursor(), (1, 0));
6189 }
6190
6191 #[test]
6192 fn dd_then_j_uses_first_non_blank_not_sticky_col() {
6193 let mut e = editor_with(" line one\n line two\n xyz!");
6212 e.jump_cursor(0, 8);
6214 assert_eq!(e.cursor(), (0, 8));
6215 run_keys(&mut e, "dd");
6218 assert_eq!(
6219 e.cursor(),
6220 (0, 4),
6221 "dd must place cursor on first-non-blank"
6222 );
6223 run_keys(&mut e, "j");
6227 let (row, col) = e.cursor();
6228 assert_eq!(row, 1);
6229 assert_eq!(
6230 col, 4,
6231 "after dd, j should use the column dd established (4), not pre-dd sticky_col (8)"
6232 );
6233 }
6234
6235 #[test]
6236 fn gu_lowercases_motion_range() {
6237 let mut e = editor_with("HELLO WORLD");
6238 run_keys(&mut e, "guw");
6239 assert_eq!(e.buffer().lines()[0], "hello WORLD");
6240 assert_eq!(e.cursor(), (0, 0));
6241 }
6242
6243 #[test]
6244 fn g_u_uppercases_text_object() {
6245 let mut e = editor_with("hello world");
6246 run_keys(&mut e, "gUiw");
6248 assert_eq!(e.buffer().lines()[0], "HELLO world");
6249 assert_eq!(e.cursor(), (0, 0));
6250 }
6251
6252 #[test]
6253 fn g_tilde_toggles_case_of_range() {
6254 let mut e = editor_with("Hello World");
6255 run_keys(&mut e, "g~iw");
6256 assert_eq!(e.buffer().lines()[0], "hELLO World");
6257 }
6258
6259 #[test]
6260 fn g_uu_uppercases_current_line() {
6261 let mut e = editor_with("select 1\nselect 2");
6262 run_keys(&mut e, "gUU");
6263 assert_eq!(e.buffer().lines()[0], "SELECT 1");
6264 assert_eq!(e.buffer().lines()[1], "select 2");
6265 }
6266
6267 #[test]
6268 fn gugu_lowercases_current_line() {
6269 let mut e = editor_with("FOO BAR\nBAZ");
6270 run_keys(&mut e, "gugu");
6271 assert_eq!(e.buffer().lines()[0], "foo bar");
6272 }
6273
6274 #[test]
6275 fn visual_u_uppercases_selection() {
6276 let mut e = editor_with("hello world");
6277 run_keys(&mut e, "veU");
6279 assert_eq!(e.buffer().lines()[0], "HELLO world");
6280 }
6281
6282 #[test]
6283 fn visual_line_u_lowercases_line() {
6284 let mut e = editor_with("HELLO WORLD\nOTHER");
6285 run_keys(&mut e, "Vu");
6286 assert_eq!(e.buffer().lines()[0], "hello world");
6287 assert_eq!(e.buffer().lines()[1], "OTHER");
6288 }
6289
6290 #[test]
6291 fn g_uu_with_count_uppercases_multiple_lines() {
6292 let mut e = editor_with("one\ntwo\nthree\nfour");
6293 run_keys(&mut e, "3gUU");
6295 assert_eq!(e.buffer().lines()[0], "ONE");
6296 assert_eq!(e.buffer().lines()[1], "TWO");
6297 assert_eq!(e.buffer().lines()[2], "THREE");
6298 assert_eq!(e.buffer().lines()[3], "four");
6299 }
6300
6301 #[test]
6302 fn double_gt_indents_current_line() {
6303 let mut e = editor_with("hello");
6304 run_keys(&mut e, ">>");
6305 assert_eq!(e.buffer().lines()[0], " hello");
6306 assert_eq!(e.cursor(), (0, 2));
6308 }
6309
6310 #[test]
6311 fn double_lt_outdents_current_line() {
6312 let mut e = editor_with(" hello");
6313 run_keys(&mut e, "<lt><lt>");
6314 assert_eq!(e.buffer().lines()[0], " hello");
6315 assert_eq!(e.cursor(), (0, 2));
6316 }
6317
6318 #[test]
6319 fn count_double_gt_indents_multiple_lines() {
6320 let mut e = editor_with("a\nb\nc\nd");
6321 run_keys(&mut e, "3>>");
6323 assert_eq!(e.buffer().lines()[0], " a");
6324 assert_eq!(e.buffer().lines()[1], " b");
6325 assert_eq!(e.buffer().lines()[2], " c");
6326 assert_eq!(e.buffer().lines()[3], "d");
6327 }
6328
6329 #[test]
6330 fn outdent_clips_ragged_leading_whitespace() {
6331 let mut e = editor_with(" x");
6334 run_keys(&mut e, "<lt><lt>");
6335 assert_eq!(e.buffer().lines()[0], "x");
6336 }
6337
6338 #[test]
6339 fn indent_motion_is_always_linewise() {
6340 let mut e = editor_with("foo bar");
6343 run_keys(&mut e, ">w");
6344 assert_eq!(e.buffer().lines()[0], " foo bar");
6345 }
6346
6347 #[test]
6348 fn indent_text_object_extends_over_paragraph() {
6349 let mut e = editor_with("a\nb\n\nc\nd");
6350 run_keys(&mut e, ">ap");
6352 assert_eq!(e.buffer().lines()[0], " a");
6353 assert_eq!(e.buffer().lines()[1], " b");
6354 assert_eq!(e.buffer().lines()[2], "");
6355 assert_eq!(e.buffer().lines()[3], "c");
6356 }
6357
6358 #[test]
6359 fn visual_line_indent_shifts_selected_rows() {
6360 let mut e = editor_with("x\ny\nz");
6361 run_keys(&mut e, "Vj>");
6363 assert_eq!(e.buffer().lines()[0], " x");
6364 assert_eq!(e.buffer().lines()[1], " y");
6365 assert_eq!(e.buffer().lines()[2], "z");
6366 }
6367
6368 #[test]
6369 fn outdent_empty_line_is_noop() {
6370 let mut e = editor_with("\nfoo");
6371 run_keys(&mut e, "<lt><lt>");
6372 assert_eq!(e.buffer().lines()[0], "");
6373 }
6374
6375 #[test]
6376 fn indent_skips_empty_lines() {
6377 let mut e = editor_with("");
6380 run_keys(&mut e, ">>");
6381 assert_eq!(e.buffer().lines()[0], "");
6382 }
6383
6384 #[test]
6385 fn insert_ctrl_t_indents_current_line() {
6386 let mut e = editor_with("x");
6387 run_keys(&mut e, "i<C-t>");
6389 assert_eq!(e.buffer().lines()[0], " x");
6390 assert_eq!(e.cursor(), (0, 2));
6393 }
6394
6395 #[test]
6396 fn insert_ctrl_d_outdents_current_line() {
6397 let mut e = editor_with(" x");
6398 run_keys(&mut e, "A<C-d>");
6400 assert_eq!(e.buffer().lines()[0], " x");
6401 }
6402
6403 #[test]
6404 fn h_at_col_zero_does_not_wrap_to_prev_line() {
6405 let mut e = editor_with("first\nsecond");
6406 e.jump_cursor(1, 0);
6407 run_keys(&mut e, "h");
6408 assert_eq!(e.cursor(), (1, 0));
6410 }
6411
6412 #[test]
6413 fn l_at_last_char_does_not_wrap_to_next_line() {
6414 let mut e = editor_with("ab\ncd");
6415 e.jump_cursor(0, 1);
6417 run_keys(&mut e, "l");
6418 assert_eq!(e.cursor(), (0, 1));
6420 }
6421
6422 #[test]
6423 fn count_l_clamps_at_line_end() {
6424 let mut e = editor_with("abcde");
6425 run_keys(&mut e, "20l");
6428 assert_eq!(e.cursor(), (0, 4));
6429 }
6430
6431 #[test]
6432 fn count_h_clamps_at_col_zero() {
6433 let mut e = editor_with("abcde");
6434 e.jump_cursor(0, 3);
6435 run_keys(&mut e, "20h");
6436 assert_eq!(e.cursor(), (0, 0));
6437 }
6438
6439 #[test]
6440 fn dl_on_last_char_still_deletes_it() {
6441 let mut e = editor_with("ab");
6445 e.jump_cursor(0, 1);
6446 run_keys(&mut e, "dl");
6447 assert_eq!(e.buffer().lines()[0], "a");
6448 }
6449
6450 #[test]
6451 fn case_op_preserves_yank_register() {
6452 let mut e = editor_with("target");
6453 run_keys(&mut e, "yy");
6454 let yank_before = e.yank().to_string();
6455 run_keys(&mut e, "gUU");
6457 assert_eq!(e.buffer().lines()[0], "TARGET");
6458 assert_eq!(
6459 e.yank(),
6460 yank_before,
6461 "case ops must preserve the yank buffer"
6462 );
6463 }
6464
6465 #[test]
6466 fn dap_deletes_paragraph() {
6467 let mut e = editor_with("a\nb\n\nc\nd");
6468 run_keys(&mut e, "dap");
6469 assert_eq!(e.buffer().lines().first().map(String::as_str), Some("c"));
6470 }
6471
6472 #[test]
6473 fn dit_deletes_inner_tag_content() {
6474 let mut e = editor_with("<b>hello</b>");
6475 e.jump_cursor(0, 4);
6477 run_keys(&mut e, "dit");
6478 assert_eq!(e.buffer().lines()[0], "<b></b>");
6479 }
6480
6481 #[test]
6482 fn dat_deletes_around_tag() {
6483 let mut e = editor_with("hi <b>foo</b> bye");
6484 e.jump_cursor(0, 6);
6485 run_keys(&mut e, "dat");
6486 assert_eq!(e.buffer().lines()[0], "hi bye");
6487 }
6488
6489 #[test]
6490 fn dit_picks_innermost_tag() {
6491 let mut e = editor_with("<a><b>x</b></a>");
6492 e.jump_cursor(0, 6);
6494 run_keys(&mut e, "dit");
6495 assert_eq!(e.buffer().lines()[0], "<a><b></b></a>");
6497 }
6498
6499 #[test]
6500 fn dat_innermost_tag_pair() {
6501 let mut e = editor_with("<a><b>x</b></a>");
6502 e.jump_cursor(0, 6);
6503 run_keys(&mut e, "dat");
6504 assert_eq!(e.buffer().lines()[0], "<a></a>");
6505 }
6506
6507 #[test]
6508 fn dit_outside_any_tag_no_op() {
6509 let mut e = editor_with("plain text");
6510 e.jump_cursor(0, 3);
6511 run_keys(&mut e, "dit");
6512 assert_eq!(e.buffer().lines()[0], "plain text");
6514 }
6515
6516 #[test]
6517 fn cit_changes_inner_tag_content() {
6518 let mut e = editor_with("<b>hello</b>");
6519 e.jump_cursor(0, 4);
6520 run_keys(&mut e, "citNEW<Esc>");
6521 assert_eq!(e.buffer().lines()[0], "<b>NEW</b>");
6522 }
6523
6524 #[test]
6525 fn cat_changes_around_tag() {
6526 let mut e = editor_with("hi <b>foo</b> bye");
6527 e.jump_cursor(0, 6);
6528 run_keys(&mut e, "catBAR<Esc>");
6529 assert_eq!(e.buffer().lines()[0], "hi BAR bye");
6530 }
6531
6532 #[test]
6533 fn yit_yanks_inner_tag_content() {
6534 let mut e = editor_with("<b>hello</b>");
6535 e.jump_cursor(0, 4);
6536 run_keys(&mut e, "yit");
6537 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6538 }
6539
6540 #[test]
6541 fn yat_yanks_full_tag_pair() {
6542 let mut e = editor_with("hi <b>foo</b> bye");
6543 e.jump_cursor(0, 6);
6544 run_keys(&mut e, "yat");
6545 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6546 }
6547
6548 #[test]
6549 fn vit_visually_selects_inner_tag() {
6550 let mut e = editor_with("<b>hello</b>");
6551 e.jump_cursor(0, 4);
6552 run_keys(&mut e, "vit");
6553 assert_eq!(e.vim_mode(), VimMode::Visual);
6554 run_keys(&mut e, "y");
6555 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6556 }
6557
6558 #[test]
6559 fn vat_visually_selects_around_tag() {
6560 let mut e = editor_with("x<b>foo</b>y");
6561 e.jump_cursor(0, 5);
6562 run_keys(&mut e, "vat");
6563 assert_eq!(e.vim_mode(), VimMode::Visual);
6564 run_keys(&mut e, "y");
6565 assert_eq!(e.registers().read('"').unwrap().text, "<b>foo</b>");
6566 }
6567
6568 #[test]
6571 #[allow(non_snake_case)]
6572 fn diW_deletes_inner_big_word() {
6573 let mut e = editor_with("foo.bar baz");
6574 e.jump_cursor(0, 2);
6575 run_keys(&mut e, "diW");
6576 assert_eq!(e.buffer().lines()[0], " baz");
6578 }
6579
6580 #[test]
6581 #[allow(non_snake_case)]
6582 fn daW_deletes_around_big_word() {
6583 let mut e = editor_with("foo.bar baz");
6584 e.jump_cursor(0, 2);
6585 run_keys(&mut e, "daW");
6586 assert_eq!(e.buffer().lines()[0], "baz");
6587 }
6588
6589 #[test]
6590 fn di_double_quote_deletes_inside() {
6591 let mut e = editor_with("a \"hello\" b");
6592 e.jump_cursor(0, 4);
6593 run_keys(&mut e, "di\"");
6594 assert_eq!(e.buffer().lines()[0], "a \"\" b");
6595 }
6596
6597 #[test]
6598 fn da_double_quote_deletes_around() {
6599 let mut e = editor_with("a \"hello\" b");
6601 e.jump_cursor(0, 4);
6602 run_keys(&mut e, "da\"");
6603 assert_eq!(e.buffer().lines()[0], "a b");
6604 }
6605
6606 #[test]
6607 fn di_single_quote_deletes_inside() {
6608 let mut e = editor_with("x 'foo' y");
6609 e.jump_cursor(0, 4);
6610 run_keys(&mut e, "di'");
6611 assert_eq!(e.buffer().lines()[0], "x '' y");
6612 }
6613
6614 #[test]
6615 fn da_single_quote_deletes_around() {
6616 let mut e = editor_with("x 'foo' y");
6618 e.jump_cursor(0, 4);
6619 run_keys(&mut e, "da'");
6620 assert_eq!(e.buffer().lines()[0], "x y");
6621 }
6622
6623 #[test]
6624 fn di_backtick_deletes_inside() {
6625 let mut e = editor_with("p `q` r");
6626 e.jump_cursor(0, 3);
6627 run_keys(&mut e, "di`");
6628 assert_eq!(e.buffer().lines()[0], "p `` r");
6629 }
6630
6631 #[test]
6632 fn da_backtick_deletes_around() {
6633 let mut e = editor_with("p `q` r");
6635 e.jump_cursor(0, 3);
6636 run_keys(&mut e, "da`");
6637 assert_eq!(e.buffer().lines()[0], "p r");
6638 }
6639
6640 #[test]
6641 fn di_paren_deletes_inside() {
6642 let mut e = editor_with("f(arg)");
6643 e.jump_cursor(0, 3);
6644 run_keys(&mut e, "di(");
6645 assert_eq!(e.buffer().lines()[0], "f()");
6646 }
6647
6648 #[test]
6649 fn di_paren_alias_b_works() {
6650 let mut e = editor_with("f(arg)");
6651 e.jump_cursor(0, 3);
6652 run_keys(&mut e, "dib");
6653 assert_eq!(e.buffer().lines()[0], "f()");
6654 }
6655
6656 #[test]
6657 fn di_bracket_deletes_inside() {
6658 let mut e = editor_with("a[b,c]d");
6659 e.jump_cursor(0, 3);
6660 run_keys(&mut e, "di[");
6661 assert_eq!(e.buffer().lines()[0], "a[]d");
6662 }
6663
6664 #[test]
6665 fn da_bracket_deletes_around() {
6666 let mut e = editor_with("a[b,c]d");
6667 e.jump_cursor(0, 3);
6668 run_keys(&mut e, "da[");
6669 assert_eq!(e.buffer().lines()[0], "ad");
6670 }
6671
6672 #[test]
6673 fn di_brace_deletes_inside() {
6674 let mut e = editor_with("x{y}z");
6675 e.jump_cursor(0, 2);
6676 run_keys(&mut e, "di{");
6677 assert_eq!(e.buffer().lines()[0], "x{}z");
6678 }
6679
6680 #[test]
6681 fn da_brace_deletes_around() {
6682 let mut e = editor_with("x{y}z");
6683 e.jump_cursor(0, 2);
6684 run_keys(&mut e, "da{");
6685 assert_eq!(e.buffer().lines()[0], "xz");
6686 }
6687
6688 #[test]
6689 fn di_brace_alias_capital_b_works() {
6690 let mut e = editor_with("x{y}z");
6691 e.jump_cursor(0, 2);
6692 run_keys(&mut e, "diB");
6693 assert_eq!(e.buffer().lines()[0], "x{}z");
6694 }
6695
6696 #[test]
6697 fn di_angle_deletes_inside() {
6698 let mut e = editor_with("p<q>r");
6699 e.jump_cursor(0, 2);
6700 run_keys(&mut e, "di<lt>");
6702 assert_eq!(e.buffer().lines()[0], "p<>r");
6703 }
6704
6705 #[test]
6706 fn da_angle_deletes_around() {
6707 let mut e = editor_with("p<q>r");
6708 e.jump_cursor(0, 2);
6709 run_keys(&mut e, "da<lt>");
6710 assert_eq!(e.buffer().lines()[0], "pr");
6711 }
6712
6713 #[test]
6714 fn dip_deletes_inner_paragraph() {
6715 let mut e = editor_with("a\nb\nc\n\nd");
6716 e.jump_cursor(1, 0);
6717 run_keys(&mut e, "dip");
6718 assert_eq!(e.buffer().lines(), vec!["".to_string(), "d".into()]);
6721 }
6722
6723 #[test]
6726 fn sentence_motion_close_paren_jumps_forward() {
6727 let mut e = editor_with("Alpha. Beta. Gamma.");
6728 e.jump_cursor(0, 0);
6729 run_keys(&mut e, ")");
6730 assert_eq!(e.cursor(), (0, 7));
6732 run_keys(&mut e, ")");
6733 assert_eq!(e.cursor(), (0, 13));
6734 }
6735
6736 #[test]
6737 fn sentence_motion_open_paren_jumps_backward() {
6738 let mut e = editor_with("Alpha. Beta. Gamma.");
6739 e.jump_cursor(0, 13);
6740 run_keys(&mut e, "(");
6741 assert_eq!(e.cursor(), (0, 7));
6744 run_keys(&mut e, "(");
6745 assert_eq!(e.cursor(), (0, 0));
6746 }
6747
6748 #[test]
6749 fn sentence_motion_count() {
6750 let mut e = editor_with("A. B. C. D.");
6751 e.jump_cursor(0, 0);
6752 run_keys(&mut e, "3)");
6753 assert_eq!(e.cursor(), (0, 9));
6755 }
6756
6757 #[test]
6758 fn dis_deletes_inner_sentence() {
6759 let mut e = editor_with("First one. Second one. Third one.");
6760 e.jump_cursor(0, 13);
6761 run_keys(&mut e, "dis");
6762 assert_eq!(e.buffer().lines()[0], "First one. Third one.");
6764 }
6765
6766 #[test]
6767 fn das_deletes_around_sentence_with_trailing_space() {
6768 let mut e = editor_with("Alpha. Beta. Gamma.");
6769 e.jump_cursor(0, 8);
6770 run_keys(&mut e, "das");
6771 assert_eq!(e.buffer().lines()[0], "Alpha. Gamma.");
6774 }
6775
6776 #[test]
6777 fn dis_handles_double_terminator() {
6778 let mut e = editor_with("Wow!? Next.");
6779 e.jump_cursor(0, 1);
6780 run_keys(&mut e, "dis");
6781 assert_eq!(e.buffer().lines()[0], " Next.");
6784 }
6785
6786 #[test]
6787 fn dis_first_sentence_from_cursor_at_zero() {
6788 let mut e = editor_with("Alpha. Beta.");
6789 e.jump_cursor(0, 0);
6790 run_keys(&mut e, "dis");
6791 assert_eq!(e.buffer().lines()[0], " Beta.");
6792 }
6793
6794 #[test]
6795 fn yis_yanks_inner_sentence() {
6796 let mut e = editor_with("Hello world. Bye.");
6797 e.jump_cursor(0, 5);
6798 run_keys(&mut e, "yis");
6799 assert_eq!(e.registers().read('"').unwrap().text, "Hello world.");
6800 }
6801
6802 #[test]
6803 fn vis_visually_selects_inner_sentence() {
6804 let mut e = editor_with("First. Second.");
6805 e.jump_cursor(0, 1);
6806 run_keys(&mut e, "vis");
6807 assert_eq!(e.vim_mode(), VimMode::Visual);
6808 run_keys(&mut e, "y");
6809 assert_eq!(e.registers().read('"').unwrap().text, "First.");
6810 }
6811
6812 #[test]
6813 fn ciw_changes_inner_word() {
6814 let mut e = editor_with("hello world");
6815 e.jump_cursor(0, 1);
6816 run_keys(&mut e, "ciwHEY<Esc>");
6817 assert_eq!(e.buffer().lines()[0], "HEY world");
6818 }
6819
6820 #[test]
6821 fn yiw_yanks_inner_word() {
6822 let mut e = editor_with("hello world");
6823 e.jump_cursor(0, 1);
6824 run_keys(&mut e, "yiw");
6825 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6826 }
6827
6828 #[test]
6829 fn viw_selects_inner_word() {
6830 let mut e = editor_with("hello world");
6831 e.jump_cursor(0, 2);
6832 run_keys(&mut e, "viw");
6833 assert_eq!(e.vim_mode(), VimMode::Visual);
6834 run_keys(&mut e, "y");
6835 assert_eq!(e.registers().read('"').unwrap().text, "hello");
6836 }
6837
6838 #[test]
6839 fn ci_paren_changes_inside() {
6840 let mut e = editor_with("f(old)");
6841 e.jump_cursor(0, 3);
6842 run_keys(&mut e, "ci(NEW<Esc>");
6843 assert_eq!(e.buffer().lines()[0], "f(NEW)");
6844 }
6845
6846 #[test]
6847 fn yi_double_quote_yanks_inside() {
6848 let mut e = editor_with("say \"hi there\" then");
6849 e.jump_cursor(0, 6);
6850 run_keys(&mut e, "yi\"");
6851 assert_eq!(e.registers().read('"').unwrap().text, "hi there");
6852 }
6853
6854 #[test]
6855 fn vap_visual_selects_around_paragraph() {
6856 let mut e = editor_with("a\nb\n\nc");
6857 e.jump_cursor(0, 0);
6858 run_keys(&mut e, "vap");
6859 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6860 run_keys(&mut e, "y");
6861 let text = e.registers().read('"').unwrap().text.clone();
6863 assert!(text.starts_with("a\nb"));
6864 }
6865
6866 #[test]
6867 fn star_finds_next_occurrence() {
6868 let mut e = editor_with("foo bar foo baz");
6869 run_keys(&mut e, "*");
6870 assert_eq!(e.cursor().1, 8);
6871 }
6872
6873 #[test]
6874 fn star_skips_substring_match() {
6875 let mut e = editor_with("foo foobar baz");
6878 run_keys(&mut e, "*");
6879 assert_eq!(e.cursor().1, 0);
6880 }
6881
6882 #[test]
6883 fn g_star_matches_substring() {
6884 let mut e = editor_with("foo foobar baz");
6887 run_keys(&mut e, "g*");
6888 assert_eq!(e.cursor().1, 4);
6889 }
6890
6891 #[test]
6892 fn g_pound_matches_substring_backward() {
6893 let mut e = editor_with("foo foobar baz foo");
6896 run_keys(&mut e, "$b");
6897 assert_eq!(e.cursor().1, 15);
6898 run_keys(&mut e, "g#");
6899 assert_eq!(e.cursor().1, 4);
6900 }
6901
6902 #[test]
6903 fn n_repeats_last_search_forward() {
6904 let mut e = editor_with("foo bar foo baz foo");
6905 run_keys(&mut e, "/foo<CR>");
6908 assert_eq!(e.cursor().1, 8);
6909 run_keys(&mut e, "n");
6910 assert_eq!(e.cursor().1, 16);
6911 }
6912
6913 #[test]
6914 fn shift_n_reverses_search() {
6915 let mut e = editor_with("foo bar foo baz foo");
6916 run_keys(&mut e, "/foo<CR>");
6917 run_keys(&mut e, "n");
6918 assert_eq!(e.cursor().1, 16);
6919 run_keys(&mut e, "N");
6920 assert_eq!(e.cursor().1, 8);
6921 }
6922
6923 #[test]
6924 fn n_noop_without_pattern() {
6925 let mut e = editor_with("foo bar");
6926 run_keys(&mut e, "n");
6927 assert_eq!(e.cursor(), (0, 0));
6928 }
6929
6930 #[test]
6931 fn visual_line_preserves_cursor_column() {
6932 let mut e = editor_with("hello world\nanother one\nbye");
6935 run_keys(&mut e, "lllll"); run_keys(&mut e, "V");
6937 assert_eq!(e.vim_mode(), VimMode::VisualLine);
6938 assert_eq!(e.cursor(), (0, 5));
6939 run_keys(&mut e, "j");
6940 assert_eq!(e.cursor(), (1, 5));
6941 }
6942
6943 #[test]
6944 fn visual_line_yank_includes_trailing_newline() {
6945 let mut e = editor_with("aaa\nbbb\nccc");
6946 run_keys(&mut e, "Vjy");
6947 assert_eq!(e.last_yank.as_deref(), Some("aaa\nbbb\n"));
6949 }
6950
6951 #[test]
6952 fn visual_line_yank_last_line_trailing_newline() {
6953 let mut e = editor_with("aaa\nbbb\nccc");
6954 run_keys(&mut e, "jj");
6956 run_keys(&mut e, "Vy");
6957 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6958 }
6959
6960 #[test]
6961 fn yy_on_last_line_has_trailing_newline() {
6962 let mut e = editor_with("aaa\nbbb\nccc");
6963 run_keys(&mut e, "jj");
6964 run_keys(&mut e, "yy");
6965 assert_eq!(e.last_yank.as_deref(), Some("ccc\n"));
6966 }
6967
6968 #[test]
6969 fn yy_in_middle_has_trailing_newline() {
6970 let mut e = editor_with("aaa\nbbb\nccc");
6971 run_keys(&mut e, "j");
6972 run_keys(&mut e, "yy");
6973 assert_eq!(e.last_yank.as_deref(), Some("bbb\n"));
6974 }
6975
6976 #[test]
6977 fn di_single_quote() {
6978 let mut e = editor_with("say 'hello world' now");
6979 e.jump_cursor(0, 7);
6980 run_keys(&mut e, "di'");
6981 assert_eq!(e.buffer().lines()[0], "say '' now");
6982 }
6983
6984 #[test]
6985 fn da_single_quote() {
6986 let mut e = editor_with("say 'hello' now");
6988 e.jump_cursor(0, 7);
6989 run_keys(&mut e, "da'");
6990 assert_eq!(e.buffer().lines()[0], "say now");
6991 }
6992
6993 #[test]
6994 fn di_backtick() {
6995 let mut e = editor_with("say `hi` now");
6996 e.jump_cursor(0, 5);
6997 run_keys(&mut e, "di`");
6998 assert_eq!(e.buffer().lines()[0], "say `` now");
6999 }
7000
7001 #[test]
7002 fn di_brace() {
7003 let mut e = editor_with("fn { a; b; c }");
7004 e.jump_cursor(0, 7);
7005 run_keys(&mut e, "di{");
7006 assert_eq!(e.buffer().lines()[0], "fn {}");
7007 }
7008
7009 #[test]
7010 fn di_bracket() {
7011 let mut e = editor_with("arr[1, 2, 3]");
7012 e.jump_cursor(0, 5);
7013 run_keys(&mut e, "di[");
7014 assert_eq!(e.buffer().lines()[0], "arr[]");
7015 }
7016
7017 #[test]
7018 fn dab_deletes_around_paren() {
7019 let mut e = editor_with("fn(a, b) + 1");
7020 e.jump_cursor(0, 4);
7021 run_keys(&mut e, "dab");
7022 assert_eq!(e.buffer().lines()[0], "fn + 1");
7023 }
7024
7025 #[test]
7026 fn da_big_b_deletes_around_brace() {
7027 let mut e = editor_with("x = {a: 1}");
7028 e.jump_cursor(0, 6);
7029 run_keys(&mut e, "daB");
7030 assert_eq!(e.buffer().lines()[0], "x = ");
7031 }
7032
7033 #[test]
7034 fn di_big_w_deletes_bigword() {
7035 let mut e = editor_with("foo-bar baz");
7036 e.jump_cursor(0, 2);
7037 run_keys(&mut e, "diW");
7038 assert_eq!(e.buffer().lines()[0], " baz");
7039 }
7040
7041 #[test]
7042 fn visual_select_inner_word() {
7043 let mut e = editor_with("hello world");
7044 e.jump_cursor(0, 2);
7045 run_keys(&mut e, "viw");
7046 assert_eq!(e.vim_mode(), VimMode::Visual);
7047 run_keys(&mut e, "y");
7048 assert_eq!(e.last_yank.as_deref(), Some("hello"));
7049 }
7050
7051 #[test]
7052 fn visual_select_inner_quote() {
7053 let mut e = editor_with("foo \"bar\" baz");
7054 e.jump_cursor(0, 6);
7055 run_keys(&mut e, "vi\"");
7056 run_keys(&mut e, "y");
7057 assert_eq!(e.last_yank.as_deref(), Some("bar"));
7058 }
7059
7060 #[test]
7061 fn visual_select_inner_paren() {
7062 let mut e = editor_with("fn(a, b)");
7063 e.jump_cursor(0, 4);
7064 run_keys(&mut e, "vi(");
7065 run_keys(&mut e, "y");
7066 assert_eq!(e.last_yank.as_deref(), Some("a, b"));
7067 }
7068
7069 #[test]
7070 fn visual_select_outer_brace() {
7071 let mut e = editor_with("{x}");
7072 e.jump_cursor(0, 1);
7073 run_keys(&mut e, "va{");
7074 run_keys(&mut e, "y");
7075 assert_eq!(e.last_yank.as_deref(), Some("{x}"));
7076 }
7077
7078 #[test]
7079 fn ci_paren_forward_scans_when_cursor_before_pair() {
7080 let mut e = editor_with("foo(bar)");
7083 e.jump_cursor(0, 0);
7084 run_keys(&mut e, "ci(NEW<Esc>");
7085 assert_eq!(e.buffer().lines()[0], "foo(NEW)");
7086 }
7087
7088 #[test]
7089 fn ci_paren_forward_scans_across_lines() {
7090 let mut e = editor_with("first\nfoo(bar)\nlast");
7091 e.jump_cursor(0, 0);
7092 run_keys(&mut e, "ci(NEW<Esc>");
7093 assert_eq!(e.buffer().lines()[1], "foo(NEW)");
7094 }
7095
7096 #[test]
7097 fn ci_brace_forward_scans_when_cursor_before_pair() {
7098 let mut e = editor_with("let x = {y};");
7099 e.jump_cursor(0, 0);
7100 run_keys(&mut e, "ci{NEW<Esc>");
7101 assert_eq!(e.buffer().lines()[0], "let x = {NEW};");
7102 }
7103
7104 #[test]
7105 fn cit_forward_scans_when_cursor_before_tag() {
7106 let mut e = editor_with("text <b>hello</b> rest");
7109 e.jump_cursor(0, 0);
7110 run_keys(&mut e, "citNEW<Esc>");
7111 assert_eq!(e.buffer().lines()[0], "text <b>NEW</b> rest");
7112 }
7113
7114 #[test]
7115 fn dat_forward_scans_when_cursor_before_tag() {
7116 let mut e = editor_with("text <b>hello</b> rest");
7118 e.jump_cursor(0, 0);
7119 run_keys(&mut e, "dat");
7120 assert_eq!(e.buffer().lines()[0], "text rest");
7121 }
7122
7123 #[test]
7124 fn ci_paren_still_works_when_cursor_inside() {
7125 let mut e = editor_with("fn(a, b)");
7128 e.jump_cursor(0, 4);
7129 run_keys(&mut e, "ci(NEW<Esc>");
7130 assert_eq!(e.buffer().lines()[0], "fn(NEW)");
7131 }
7132
7133 #[test]
7134 fn caw_changes_word_with_trailing_space() {
7135 let mut e = editor_with("hello world");
7136 run_keys(&mut e, "cawfoo<Esc>");
7137 assert_eq!(e.buffer().lines()[0], "fooworld");
7138 }
7139
7140 #[test]
7141 fn visual_char_yank_preserves_raw_text() {
7142 let mut e = editor_with("hello world");
7143 run_keys(&mut e, "vllly");
7144 assert_eq!(e.last_yank.as_deref(), Some("hell"));
7145 }
7146
7147 #[test]
7148 fn single_line_visual_line_selects_full_line_on_yank() {
7149 let mut e = editor_with("hello world\nbye");
7150 run_keys(&mut e, "V");
7151 run_keys(&mut e, "y");
7154 assert_eq!(e.last_yank.as_deref(), Some("hello world\n"));
7155 }
7156
7157 #[test]
7158 fn visual_line_extends_both_directions() {
7159 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7160 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7162 assert_eq!(e.cursor(), (3, 0));
7163 run_keys(&mut e, "k");
7164 assert_eq!(e.cursor(), (2, 0));
7166 run_keys(&mut e, "k");
7167 assert_eq!(e.cursor(), (1, 0));
7168 }
7169
7170 #[test]
7171 fn visual_char_preserves_cursor_column() {
7172 let mut e = editor_with("hello world");
7173 run_keys(&mut e, "lllll"); run_keys(&mut e, "v");
7175 assert_eq!(e.cursor(), (0, 5));
7176 run_keys(&mut e, "ll");
7177 assert_eq!(e.cursor(), (0, 7));
7178 }
7179
7180 #[test]
7181 fn visual_char_highlight_bounds_order() {
7182 let mut e = editor_with("abcdef");
7183 run_keys(&mut e, "lll"); run_keys(&mut e, "v");
7185 run_keys(&mut e, "hh"); assert_eq!(e.char_highlight(), Some(((0, 1), (0, 3))));
7188 }
7189
7190 #[test]
7191 fn visual_line_highlight_bounds() {
7192 let mut e = editor_with("a\nb\nc");
7193 run_keys(&mut e, "V");
7194 assert_eq!(e.line_highlight(), Some((0, 0)));
7195 run_keys(&mut e, "j");
7196 assert_eq!(e.line_highlight(), Some((0, 1)));
7197 run_keys(&mut e, "j");
7198 assert_eq!(e.line_highlight(), Some((0, 2)));
7199 }
7200
7201 #[test]
7204 fn h_moves_left() {
7205 let mut e = editor_with("hello");
7206 e.jump_cursor(0, 3);
7207 run_keys(&mut e, "h");
7208 assert_eq!(e.cursor(), (0, 2));
7209 }
7210
7211 #[test]
7212 fn l_moves_right() {
7213 let mut e = editor_with("hello");
7214 run_keys(&mut e, "l");
7215 assert_eq!(e.cursor(), (0, 1));
7216 }
7217
7218 #[test]
7219 fn k_moves_up() {
7220 let mut e = editor_with("a\nb\nc");
7221 e.jump_cursor(2, 0);
7222 run_keys(&mut e, "k");
7223 assert_eq!(e.cursor(), (1, 0));
7224 }
7225
7226 #[test]
7227 fn zero_moves_to_line_start() {
7228 let mut e = editor_with(" hello");
7229 run_keys(&mut e, "$");
7230 run_keys(&mut e, "0");
7231 assert_eq!(e.cursor().1, 0);
7232 }
7233
7234 #[test]
7235 fn caret_moves_to_first_non_blank() {
7236 let mut e = editor_with(" hello");
7237 run_keys(&mut e, "0");
7238 run_keys(&mut e, "^");
7239 assert_eq!(e.cursor().1, 4);
7240 }
7241
7242 #[test]
7243 fn dollar_moves_to_last_char() {
7244 let mut e = editor_with("hello");
7245 run_keys(&mut e, "$");
7246 assert_eq!(e.cursor().1, 4);
7247 }
7248
7249 #[test]
7250 fn dollar_on_empty_line_stays_at_col_zero() {
7251 let mut e = editor_with("");
7252 run_keys(&mut e, "$");
7253 assert_eq!(e.cursor().1, 0);
7254 }
7255
7256 #[test]
7257 fn w_jumps_to_next_word() {
7258 let mut e = editor_with("foo bar baz");
7259 run_keys(&mut e, "w");
7260 assert_eq!(e.cursor().1, 4);
7261 }
7262
7263 #[test]
7264 fn b_jumps_back_a_word() {
7265 let mut e = editor_with("foo bar");
7266 e.jump_cursor(0, 6);
7267 run_keys(&mut e, "b");
7268 assert_eq!(e.cursor().1, 4);
7269 }
7270
7271 #[test]
7272 fn e_jumps_to_word_end() {
7273 let mut e = editor_with("foo bar");
7274 run_keys(&mut e, "e");
7275 assert_eq!(e.cursor().1, 2);
7276 }
7277
7278 #[test]
7281 fn d_dollar_deletes_to_eol() {
7282 let mut e = editor_with("hello world");
7283 e.jump_cursor(0, 5);
7284 run_keys(&mut e, "d$");
7285 assert_eq!(e.buffer().lines()[0], "hello");
7286 }
7287
7288 #[test]
7289 fn d_zero_deletes_to_line_start() {
7290 let mut e = editor_with("hello world");
7291 e.jump_cursor(0, 6);
7292 run_keys(&mut e, "d0");
7293 assert_eq!(e.buffer().lines()[0], "world");
7294 }
7295
7296 #[test]
7297 fn d_caret_deletes_to_first_non_blank() {
7298 let mut e = editor_with(" hello");
7299 e.jump_cursor(0, 6);
7300 run_keys(&mut e, "d^");
7301 assert_eq!(e.buffer().lines()[0], " llo");
7302 }
7303
7304 #[test]
7305 fn d_capital_g_deletes_to_end_of_file() {
7306 let mut e = editor_with("a\nb\nc\nd");
7307 e.jump_cursor(1, 0);
7308 run_keys(&mut e, "dG");
7309 assert_eq!(e.buffer().lines(), &["a".to_string()]);
7310 }
7311
7312 #[test]
7313 fn d_gg_deletes_to_start_of_file() {
7314 let mut e = editor_with("a\nb\nc\nd");
7315 e.jump_cursor(2, 0);
7316 run_keys(&mut e, "dgg");
7317 assert_eq!(e.buffer().lines(), &["d".to_string()]);
7318 }
7319
7320 #[test]
7321 fn cw_is_ce_quirk() {
7322 let mut e = editor_with("foo bar");
7325 run_keys(&mut e, "cwxyz<Esc>");
7326 assert_eq!(e.buffer().lines()[0], "xyz bar");
7327 }
7328
7329 #[test]
7332 fn big_d_deletes_to_eol() {
7333 let mut e = editor_with("hello world");
7334 e.jump_cursor(0, 5);
7335 run_keys(&mut e, "D");
7336 assert_eq!(e.buffer().lines()[0], "hello");
7337 }
7338
7339 #[test]
7340 fn big_c_deletes_to_eol_and_inserts() {
7341 let mut e = editor_with("hello world");
7342 e.jump_cursor(0, 5);
7343 run_keys(&mut e, "C!<Esc>");
7344 assert_eq!(e.buffer().lines()[0], "hello!");
7345 }
7346
7347 #[test]
7348 fn j_joins_next_line_with_space() {
7349 let mut e = editor_with("hello\nworld");
7350 run_keys(&mut e, "J");
7351 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7352 }
7353
7354 #[test]
7355 fn j_strips_leading_whitespace_on_join() {
7356 let mut e = editor_with("hello\n world");
7357 run_keys(&mut e, "J");
7358 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
7359 }
7360
7361 #[test]
7362 fn big_x_deletes_char_before_cursor() {
7363 let mut e = editor_with("hello");
7364 e.jump_cursor(0, 3);
7365 run_keys(&mut e, "X");
7366 assert_eq!(e.buffer().lines()[0], "helo");
7367 }
7368
7369 #[test]
7370 fn s_substitutes_char_and_enters_insert() {
7371 let mut e = editor_with("hello");
7372 run_keys(&mut e, "sX<Esc>");
7373 assert_eq!(e.buffer().lines()[0], "Xello");
7374 }
7375
7376 #[test]
7377 fn count_x_deletes_many() {
7378 let mut e = editor_with("abcdef");
7379 run_keys(&mut e, "3x");
7380 assert_eq!(e.buffer().lines()[0], "def");
7381 }
7382
7383 #[test]
7386 fn p_pastes_charwise_after_cursor() {
7387 let mut e = editor_with("hello");
7388 run_keys(&mut e, "yw");
7389 run_keys(&mut e, "$p");
7390 assert_eq!(e.buffer().lines()[0], "hellohello");
7391 }
7392
7393 #[test]
7394 fn capital_p_pastes_charwise_before_cursor() {
7395 let mut e = editor_with("hello");
7396 run_keys(&mut e, "v");
7398 run_keys(&mut e, "l");
7399 run_keys(&mut e, "y");
7400 run_keys(&mut e, "$P");
7401 assert_eq!(e.buffer().lines()[0], "hellheo");
7404 }
7405
7406 #[test]
7407 fn p_pastes_linewise_below() {
7408 let mut e = editor_with("one\ntwo\nthree");
7409 run_keys(&mut e, "yy");
7410 run_keys(&mut e, "p");
7411 assert_eq!(
7412 e.buffer().lines(),
7413 &[
7414 "one".to_string(),
7415 "one".to_string(),
7416 "two".to_string(),
7417 "three".to_string()
7418 ]
7419 );
7420 }
7421
7422 #[test]
7423 fn capital_p_pastes_linewise_above() {
7424 let mut e = editor_with("one\ntwo");
7425 e.jump_cursor(1, 0);
7426 run_keys(&mut e, "yy");
7427 run_keys(&mut e, "P");
7428 assert_eq!(
7429 e.buffer().lines(),
7430 &["one".to_string(), "two".to_string(), "two".to_string()]
7431 );
7432 }
7433
7434 #[test]
7437 fn hash_finds_previous_occurrence() {
7438 let mut e = editor_with("foo bar foo baz foo");
7439 e.jump_cursor(0, 16);
7441 run_keys(&mut e, "#");
7442 assert_eq!(e.cursor().1, 8);
7443 }
7444
7445 #[test]
7448 fn visual_line_delete_removes_full_lines() {
7449 let mut e = editor_with("a\nb\nc\nd");
7450 run_keys(&mut e, "Vjd");
7451 assert_eq!(e.buffer().lines(), &["c".to_string(), "d".to_string()]);
7452 }
7453
7454 #[test]
7455 fn visual_line_change_leaves_blank_line() {
7456 let mut e = editor_with("a\nb\nc");
7457 run_keys(&mut e, "Vjc");
7458 assert_eq!(e.vim_mode(), VimMode::Insert);
7459 run_keys(&mut e, "X<Esc>");
7460 assert_eq!(e.buffer().lines(), &["X".to_string(), "c".to_string()]);
7464 }
7465
7466 #[test]
7467 fn cc_leaves_blank_line() {
7468 let mut e = editor_with("a\nb\nc");
7469 e.jump_cursor(1, 0);
7470 run_keys(&mut e, "ccX<Esc>");
7471 assert_eq!(
7472 e.buffer().lines(),
7473 &["a".to_string(), "X".to_string(), "c".to_string()]
7474 );
7475 }
7476
7477 #[test]
7482 fn big_w_skips_hyphens() {
7483 let mut e = editor_with("foo-bar baz");
7485 run_keys(&mut e, "W");
7486 assert_eq!(e.cursor().1, 8);
7487 }
7488
7489 #[test]
7490 fn big_w_crosses_lines() {
7491 let mut e = editor_with("foo-bar\nbaz-qux");
7492 run_keys(&mut e, "W");
7493 assert_eq!(e.cursor(), (1, 0));
7494 }
7495
7496 #[test]
7497 fn big_b_skips_hyphens() {
7498 let mut e = editor_with("foo-bar baz");
7499 e.jump_cursor(0, 9);
7500 run_keys(&mut e, "B");
7501 assert_eq!(e.cursor().1, 8);
7502 run_keys(&mut e, "B");
7503 assert_eq!(e.cursor().1, 0);
7504 }
7505
7506 #[test]
7507 fn big_e_jumps_to_big_word_end() {
7508 let mut e = editor_with("foo-bar baz");
7509 run_keys(&mut e, "E");
7510 assert_eq!(e.cursor().1, 6);
7511 run_keys(&mut e, "E");
7512 assert_eq!(e.cursor().1, 10);
7513 }
7514
7515 #[test]
7516 fn dw_with_big_word_variant() {
7517 let mut e = editor_with("foo-bar baz");
7519 run_keys(&mut e, "dW");
7520 assert_eq!(e.buffer().lines()[0], "baz");
7521 }
7522
7523 #[test]
7526 fn insert_ctrl_w_deletes_word_back() {
7527 let mut e = editor_with("");
7528 run_keys(&mut e, "i");
7529 for c in "hello world".chars() {
7530 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7531 }
7532 run_keys(&mut e, "<C-w>");
7533 assert_eq!(e.buffer().lines()[0], "hello ");
7534 }
7535
7536 #[test]
7537 fn insert_ctrl_w_at_col0_joins_with_prev_word() {
7538 let mut e = editor_with("hello\nworld");
7542 e.jump_cursor(1, 0);
7543 run_keys(&mut e, "i");
7544 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7545 assert_eq!(e.buffer().lines(), vec!["world".to_string()]);
7548 assert_eq!(e.cursor(), (0, 0));
7549 }
7550
7551 #[test]
7552 fn insert_ctrl_w_at_col0_keeps_prefix_words() {
7553 let mut e = editor_with("foo bar\nbaz");
7554 e.jump_cursor(1, 0);
7555 run_keys(&mut e, "i");
7556 e.handle_key(KeyEvent::new(KeyCode::Char('w'), KeyModifiers::CONTROL));
7557 assert_eq!(e.buffer().lines(), vec!["foo baz".to_string()]);
7559 assert_eq!(e.cursor(), (0, 4));
7560 }
7561
7562 #[test]
7563 fn insert_ctrl_u_deletes_to_line_start() {
7564 let mut e = editor_with("");
7565 run_keys(&mut e, "i");
7566 for c in "hello world".chars() {
7567 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7568 }
7569 run_keys(&mut e, "<C-u>");
7570 assert_eq!(e.buffer().lines()[0], "");
7571 }
7572
7573 #[test]
7574 fn insert_ctrl_o_runs_one_normal_command() {
7575 let mut e = editor_with("hello world");
7576 run_keys(&mut e, "A");
7578 assert_eq!(e.vim_mode(), VimMode::Insert);
7579 e.jump_cursor(0, 0);
7581 run_keys(&mut e, "<C-o>");
7582 assert_eq!(e.vim_mode(), VimMode::Normal);
7583 run_keys(&mut e, "dw");
7584 assert_eq!(e.vim_mode(), VimMode::Insert);
7586 assert_eq!(e.buffer().lines()[0], "world");
7587 }
7588
7589 #[test]
7592 fn j_through_empty_line_preserves_column() {
7593 let mut e = editor_with("hello world\n\nanother line");
7594 run_keys(&mut e, "llllll");
7596 assert_eq!(e.cursor(), (0, 6));
7597 run_keys(&mut e, "j");
7600 assert_eq!(e.cursor(), (1, 0));
7601 run_keys(&mut e, "j");
7603 assert_eq!(e.cursor(), (2, 6));
7604 }
7605
7606 #[test]
7607 fn j_through_shorter_line_preserves_column() {
7608 let mut e = editor_with("hello world\nhi\nanother line");
7609 run_keys(&mut e, "lllllll"); run_keys(&mut e, "j"); assert_eq!(e.cursor(), (1, 1));
7612 run_keys(&mut e, "j");
7613 assert_eq!(e.cursor(), (2, 7));
7614 }
7615
7616 #[test]
7617 fn esc_from_insert_sticky_matches_visible_cursor() {
7618 let mut e = editor_with(" this is a line\n another one of a similar size");
7622 e.jump_cursor(0, 12);
7623 run_keys(&mut e, "I");
7624 assert_eq!(e.cursor(), (0, 4));
7625 run_keys(&mut e, "X<Esc>");
7626 assert_eq!(e.cursor(), (0, 4));
7627 run_keys(&mut e, "j");
7628 assert_eq!(e.cursor(), (1, 4));
7629 }
7630
7631 #[test]
7632 fn esc_from_insert_sticky_tracks_inserted_chars() {
7633 let mut e = editor_with("xxxxxxx\nyyyyyyy");
7634 run_keys(&mut e, "i");
7635 run_keys(&mut e, "abc<Esc>");
7636 assert_eq!(e.cursor(), (0, 2));
7637 run_keys(&mut e, "j");
7638 assert_eq!(e.cursor(), (1, 2));
7639 }
7640
7641 #[test]
7642 fn esc_from_insert_sticky_tracks_arrow_nav() {
7643 let mut e = editor_with("xxxxxx\nyyyyyy");
7644 run_keys(&mut e, "i");
7645 run_keys(&mut e, "abc");
7646 for _ in 0..2 {
7647 e.handle_key(KeyEvent::new(KeyCode::Left, KeyModifiers::NONE));
7648 }
7649 run_keys(&mut e, "<Esc>");
7650 assert_eq!(e.cursor(), (0, 0));
7651 run_keys(&mut e, "j");
7652 assert_eq!(e.cursor(), (1, 0));
7653 }
7654
7655 #[test]
7656 fn esc_from_insert_at_col_14_followed_by_j() {
7657 let line = "x".repeat(30);
7660 let buf = format!("{line}\n{line}");
7661 let mut e = editor_with(&buf);
7662 e.jump_cursor(0, 14);
7663 run_keys(&mut e, "i");
7664 for c in "test ".chars() {
7665 e.handle_key(KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE));
7666 }
7667 run_keys(&mut e, "<Esc>");
7668 assert_eq!(e.cursor(), (0, 18));
7669 run_keys(&mut e, "j");
7670 assert_eq!(e.cursor(), (1, 18));
7671 }
7672
7673 #[test]
7674 fn linewise_paste_resets_sticky_column() {
7675 let mut e = editor_with(" hello\naaaaaaaa\nbye");
7679 run_keys(&mut e, "llllll"); run_keys(&mut e, "yy");
7681 run_keys(&mut e, "j"); run_keys(&mut e, "p"); assert_eq!(e.cursor(), (2, 4));
7685 run_keys(&mut e, "j");
7687 assert_eq!(e.cursor(), (3, 2));
7688 }
7689
7690 #[test]
7691 fn horizontal_motion_resyncs_sticky_column() {
7692 let mut e = editor_with("hello world\n\nanother line");
7696 run_keys(&mut e, "llllll"); run_keys(&mut e, "hhh"); run_keys(&mut e, "jj");
7699 assert_eq!(e.cursor(), (2, 3));
7700 }
7701
7702 #[test]
7705 fn ctrl_v_enters_visual_block() {
7706 let mut e = editor_with("aaa\nbbb\nccc");
7707 run_keys(&mut e, "<C-v>");
7708 assert_eq!(e.vim_mode(), VimMode::VisualBlock);
7709 }
7710
7711 #[test]
7712 fn visual_block_esc_returns_to_normal() {
7713 let mut e = editor_with("aaa\nbbb\nccc");
7714 run_keys(&mut e, "<C-v>");
7715 run_keys(&mut e, "<Esc>");
7716 assert_eq!(e.vim_mode(), VimMode::Normal);
7717 }
7718
7719 #[test]
7720 fn backtick_lt_jumps_to_visual_start_mark() {
7721 let mut e = editor_with("foo bar baz\n");
7725 run_keys(&mut e, "v");
7726 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>"); assert_eq!(e.cursor(), (0, 4));
7729 run_keys(&mut e, "`<lt>");
7731 assert_eq!(e.cursor(), (0, 0));
7732 }
7733
7734 #[test]
7735 fn backtick_gt_jumps_to_visual_end_mark() {
7736 let mut e = editor_with("foo bar baz\n");
7737 run_keys(&mut e, "v");
7738 run_keys(&mut e, "w"); run_keys(&mut e, "<Esc>");
7740 run_keys(&mut e, "0"); run_keys(&mut e, "`>");
7742 assert_eq!(e.cursor(), (0, 4));
7743 }
7744
7745 #[test]
7746 fn visual_exit_sets_lt_gt_marks() {
7747 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7750 run_keys(&mut e, "V");
7752 run_keys(&mut e, "j");
7753 run_keys(&mut e, "<Esc>");
7754 let lt = e.mark('<').expect("'<' mark must be set on visual exit");
7755 let gt = e.mark('>').expect("'>' mark must be set on visual exit");
7756 assert_eq!(lt.0, 0, "'< row should be the lower bound");
7757 assert_eq!(gt.0, 1, "'> row should be the upper bound");
7758 }
7759
7760 #[test]
7761 fn visual_exit_marks_use_lower_higher_order() {
7762 let mut e = editor_with("aaa\nbbb\nccc\nddd");
7766 run_keys(&mut e, "jjj"); run_keys(&mut e, "V");
7768 run_keys(&mut e, "k"); run_keys(&mut e, "<Esc>");
7770 let lt = e.mark('<').unwrap();
7771 let gt = e.mark('>').unwrap();
7772 assert_eq!(lt.0, 2);
7773 assert_eq!(gt.0, 3);
7774 }
7775
7776 #[test]
7777 fn visualline_exit_marks_snap_to_line_edges() {
7778 let mut e = editor_with("aaaaa\nbbbbb\ncc");
7780 run_keys(&mut e, "lll"); run_keys(&mut e, "V");
7782 run_keys(&mut e, "j"); run_keys(&mut e, "<Esc>");
7784 let lt = e.mark('<').unwrap();
7785 let gt = e.mark('>').unwrap();
7786 assert_eq!(lt, (0, 0), "'< should snap to (top_row, 0)");
7787 assert_eq!(gt, (1, 4), "'> should snap to (bot_row, last_col)");
7789 }
7790
7791 #[test]
7792 fn visualblock_exit_marks_use_block_corners() {
7793 let mut e = editor_with("aaaaa\nbbbbb\nccccc");
7797 run_keys(&mut e, "llll"); run_keys(&mut e, "<C-v>");
7799 run_keys(&mut e, "j"); run_keys(&mut e, "hh"); run_keys(&mut e, "<Esc>");
7802 let lt = e.mark('<').unwrap();
7803 let gt = e.mark('>').unwrap();
7804 assert_eq!(lt, (0, 2), "'< should be top-left corner");
7806 assert_eq!(gt, (1, 4), "'> should be bottom-right corner");
7807 }
7808
7809 #[test]
7810 fn visual_block_delete_removes_column_range() {
7811 let mut e = editor_with("hello\nworld\nhappy");
7812 run_keys(&mut e, "l");
7814 run_keys(&mut e, "<C-v>");
7815 run_keys(&mut e, "jj");
7816 run_keys(&mut e, "ll");
7817 run_keys(&mut e, "d");
7818 assert_eq!(
7820 e.buffer().lines(),
7821 &["ho".to_string(), "wd".to_string(), "hy".to_string()]
7822 );
7823 }
7824
7825 #[test]
7826 fn visual_block_yank_joins_with_newlines() {
7827 let mut e = editor_with("hello\nworld\nhappy");
7828 run_keys(&mut e, "<C-v>");
7829 run_keys(&mut e, "jj");
7830 run_keys(&mut e, "ll");
7831 run_keys(&mut e, "y");
7832 assert_eq!(e.last_yank.as_deref(), Some("hel\nwor\nhap"));
7833 }
7834
7835 #[test]
7836 fn visual_block_replace_fills_block() {
7837 let mut e = editor_with("hello\nworld\nhappy");
7838 run_keys(&mut e, "<C-v>");
7839 run_keys(&mut e, "jj");
7840 run_keys(&mut e, "ll");
7841 run_keys(&mut e, "rx");
7842 assert_eq!(
7843 e.buffer().lines(),
7844 &[
7845 "xxxlo".to_string(),
7846 "xxxld".to_string(),
7847 "xxxpy".to_string()
7848 ]
7849 );
7850 }
7851
7852 #[test]
7853 fn visual_block_insert_repeats_across_rows() {
7854 let mut e = editor_with("hello\nworld\nhappy");
7855 run_keys(&mut e, "<C-v>");
7856 run_keys(&mut e, "jj");
7857 run_keys(&mut e, "I");
7858 run_keys(&mut e, "# <Esc>");
7859 assert_eq!(
7860 e.buffer().lines(),
7861 &[
7862 "# hello".to_string(),
7863 "# world".to_string(),
7864 "# happy".to_string()
7865 ]
7866 );
7867 }
7868
7869 #[test]
7870 fn block_highlight_returns_none_outside_block_mode() {
7871 let mut e = editor_with("abc");
7872 assert!(e.block_highlight().is_none());
7873 run_keys(&mut e, "v");
7874 assert!(e.block_highlight().is_none());
7875 run_keys(&mut e, "<Esc>V");
7876 assert!(e.block_highlight().is_none());
7877 }
7878
7879 #[test]
7880 fn block_highlight_bounds_track_anchor_and_cursor() {
7881 let mut e = editor_with("aaaa\nbbbb\ncccc");
7882 run_keys(&mut e, "ll"); run_keys(&mut e, "<C-v>");
7884 run_keys(&mut e, "jh"); assert_eq!(e.block_highlight(), Some((0, 1, 1, 2)));
7887 }
7888
7889 #[test]
7890 fn visual_block_delete_handles_short_lines() {
7891 let mut e = editor_with("hello\nhi\nworld");
7893 run_keys(&mut e, "l"); run_keys(&mut e, "<C-v>");
7895 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7897 assert_eq!(
7902 e.buffer().lines(),
7903 &["ho".to_string(), "h".to_string(), "wd".to_string()]
7904 );
7905 }
7906
7907 #[test]
7908 fn visual_block_yank_pads_short_lines_with_empties() {
7909 let mut e = editor_with("hello\nhi\nworld");
7910 run_keys(&mut e, "l");
7911 run_keys(&mut e, "<C-v>");
7912 run_keys(&mut e, "jjll");
7913 run_keys(&mut e, "y");
7914 assert_eq!(e.last_yank.as_deref(), Some("ell\ni\norl"));
7916 }
7917
7918 #[test]
7919 fn visual_block_replace_skips_past_eol() {
7920 let mut e = editor_with("ab\ncd\nef");
7923 run_keys(&mut e, "l");
7925 run_keys(&mut e, "<C-v>");
7926 run_keys(&mut e, "jjllllll");
7927 run_keys(&mut e, "rX");
7928 assert_eq!(
7931 e.buffer().lines(),
7932 &["aX".to_string(), "cX".to_string(), "eX".to_string()]
7933 );
7934 }
7935
7936 #[test]
7937 fn visual_block_with_empty_line_in_middle() {
7938 let mut e = editor_with("abcd\n\nefgh");
7939 run_keys(&mut e, "<C-v>");
7940 run_keys(&mut e, "jjll"); run_keys(&mut e, "d");
7942 assert_eq!(
7945 e.buffer().lines(),
7946 &["d".to_string(), "".to_string(), "h".to_string()]
7947 );
7948 }
7949
7950 #[test]
7951 fn block_insert_pads_empty_lines_to_block_column() {
7952 let mut e = editor_with("this is a line\n\nthis is a line");
7955 e.jump_cursor(0, 3);
7956 run_keys(&mut e, "<C-v>");
7957 run_keys(&mut e, "jj");
7958 run_keys(&mut e, "I");
7959 run_keys(&mut e, "XX<Esc>");
7960 assert_eq!(
7961 e.buffer().lines(),
7962 &[
7963 "thiXXs is a line".to_string(),
7964 " XX".to_string(),
7965 "thiXXs is a line".to_string()
7966 ]
7967 );
7968 }
7969
7970 #[test]
7971 fn block_insert_pads_short_lines_to_block_column() {
7972 let mut e = editor_with("aaaaa\nbb\naaaaa");
7973 e.jump_cursor(0, 3);
7974 run_keys(&mut e, "<C-v>");
7975 run_keys(&mut e, "jj");
7976 run_keys(&mut e, "I");
7977 run_keys(&mut e, "Y<Esc>");
7978 assert_eq!(
7980 e.buffer().lines(),
7981 &[
7982 "aaaYaa".to_string(),
7983 "bb Y".to_string(),
7984 "aaaYaa".to_string()
7985 ]
7986 );
7987 }
7988
7989 #[test]
7990 fn visual_block_append_repeats_across_rows() {
7991 let mut e = editor_with("foo\nbar\nbaz");
7992 run_keys(&mut e, "<C-v>");
7993 run_keys(&mut e, "jj");
7994 run_keys(&mut e, "A");
7997 run_keys(&mut e, "!<Esc>");
7998 assert_eq!(
7999 e.buffer().lines(),
8000 &["f!oo".to_string(), "b!ar".to_string(), "b!az".to_string()]
8001 );
8002 }
8003
8004 #[test]
8007 fn slash_opens_forward_search_prompt() {
8008 let mut e = editor_with("hello world");
8009 run_keys(&mut e, "/");
8010 let p = e.search_prompt().expect("prompt should be active");
8011 assert!(p.text.is_empty());
8012 assert!(p.forward);
8013 }
8014
8015 #[test]
8016 fn question_opens_backward_search_prompt() {
8017 let mut e = editor_with("hello world");
8018 run_keys(&mut e, "?");
8019 let p = e.search_prompt().expect("prompt should be active");
8020 assert!(!p.forward);
8021 }
8022
8023 #[test]
8024 fn search_prompt_typing_updates_pattern_live() {
8025 let mut e = editor_with("foo bar\nbaz");
8026 run_keys(&mut e, "/bar");
8027 assert_eq!(e.search_prompt().unwrap().text, "bar");
8028 assert!(e.search_state().pattern.is_some());
8030 }
8031
8032 #[test]
8033 fn search_prompt_backspace_and_enter() {
8034 let mut e = editor_with("hello world\nagain");
8035 run_keys(&mut e, "/worlx");
8036 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
8037 assert_eq!(e.search_prompt().unwrap().text, "worl");
8038 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8039 assert!(e.search_prompt().is_none());
8041 assert_eq!(e.last_search(), Some("worl"));
8042 assert_eq!(e.cursor(), (0, 6));
8043 }
8044
8045 #[test]
8046 fn empty_search_prompt_enter_repeats_last_search() {
8047 let mut e = editor_with("foo bar foo baz foo");
8048 run_keys(&mut e, "/foo");
8049 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8050 assert_eq!(e.cursor().1, 8);
8051 run_keys(&mut e, "/");
8053 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8054 assert_eq!(e.cursor().1, 16);
8055 assert_eq!(e.last_search(), Some("foo"));
8056 }
8057
8058 #[test]
8059 fn search_history_records_committed_patterns() {
8060 let mut e = editor_with("alpha beta gamma");
8061 run_keys(&mut e, "/alpha<CR>");
8062 run_keys(&mut e, "/beta<CR>");
8063 let history = e.vim.search_history.clone();
8065 assert_eq!(history, vec!["alpha", "beta"]);
8066 }
8067
8068 #[test]
8069 fn search_history_dedupes_consecutive_repeats() {
8070 let mut e = editor_with("foo bar foo");
8071 run_keys(&mut e, "/foo<CR>");
8072 run_keys(&mut e, "/foo<CR>");
8073 run_keys(&mut e, "/bar<CR>");
8074 run_keys(&mut e, "/bar<CR>");
8075 assert_eq!(e.vim.search_history.clone(), vec!["foo", "bar"]);
8077 }
8078
8079 #[test]
8080 fn ctrl_p_walks_history_backward() {
8081 let mut e = editor_with("alpha beta gamma");
8082 run_keys(&mut e, "/alpha<CR>");
8083 run_keys(&mut e, "/beta<CR>");
8084 run_keys(&mut e, "/");
8086 assert_eq!(e.search_prompt().unwrap().text, "");
8087 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8088 assert_eq!(e.search_prompt().unwrap().text, "beta");
8089 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8090 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8091 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8093 assert_eq!(e.search_prompt().unwrap().text, "alpha");
8094 }
8095
8096 #[test]
8097 fn ctrl_n_walks_history_forward_after_ctrl_p() {
8098 let mut e = editor_with("a b c");
8099 run_keys(&mut e, "/a<CR>");
8100 run_keys(&mut e, "/b<CR>");
8101 run_keys(&mut e, "/c<CR>");
8102 run_keys(&mut e, "/");
8103 for _ in 0..3 {
8105 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8106 }
8107 assert_eq!(e.search_prompt().unwrap().text, "a");
8108 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8109 assert_eq!(e.search_prompt().unwrap().text, "b");
8110 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8111 assert_eq!(e.search_prompt().unwrap().text, "c");
8112 e.handle_key(KeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL));
8114 assert_eq!(e.search_prompt().unwrap().text, "c");
8115 }
8116
8117 #[test]
8118 fn typing_after_history_walk_resets_cursor() {
8119 let mut e = editor_with("foo");
8120 run_keys(&mut e, "/foo<CR>");
8121 run_keys(&mut e, "/");
8122 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8123 assert_eq!(e.search_prompt().unwrap().text, "foo");
8124 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
8127 assert_eq!(e.search_prompt().unwrap().text, "foox");
8128 e.handle_key(KeyEvent::new(KeyCode::Char('p'), KeyModifiers::CONTROL));
8129 assert_eq!(e.search_prompt().unwrap().text, "foo");
8130 }
8131
8132 #[test]
8133 fn empty_backward_search_prompt_enter_repeats_last_search() {
8134 let mut e = editor_with("foo bar foo baz foo");
8135 run_keys(&mut e, "/foo");
8137 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8138 assert_eq!(e.cursor().1, 8);
8139 run_keys(&mut e, "?");
8140 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8141 assert_eq!(e.cursor().1, 0);
8142 assert_eq!(e.last_search(), Some("foo"));
8143 }
8144
8145 #[test]
8146 fn search_prompt_esc_cancels_but_keeps_last_search() {
8147 let mut e = editor_with("foo bar\nbaz");
8148 run_keys(&mut e, "/bar");
8149 e.handle_key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE));
8150 assert!(e.search_prompt().is_none());
8151 assert_eq!(e.last_search(), Some("bar"));
8152 }
8153
8154 #[test]
8155 fn search_then_n_and_shift_n_navigate() {
8156 let mut e = editor_with("foo bar foo baz foo");
8157 run_keys(&mut e, "/foo");
8158 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8159 assert_eq!(e.cursor().1, 8);
8161 run_keys(&mut e, "n");
8162 assert_eq!(e.cursor().1, 16);
8163 run_keys(&mut e, "N");
8164 assert_eq!(e.cursor().1, 8);
8165 }
8166
8167 #[test]
8168 fn question_mark_searches_backward_on_enter() {
8169 let mut e = editor_with("foo bar foo baz");
8170 e.jump_cursor(0, 10);
8171 run_keys(&mut e, "?foo");
8172 e.handle_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
8173 assert_eq!(e.cursor(), (0, 8));
8175 }
8176
8177 #[test]
8180 fn big_y_yanks_to_end_of_line() {
8181 let mut e = editor_with("hello world");
8182 e.jump_cursor(0, 6);
8183 run_keys(&mut e, "Y");
8184 assert_eq!(e.last_yank.as_deref(), Some("world"));
8185 }
8186
8187 #[test]
8188 fn big_y_from_line_start_yanks_full_line() {
8189 let mut e = editor_with("hello world");
8190 run_keys(&mut e, "Y");
8191 assert_eq!(e.last_yank.as_deref(), Some("hello world"));
8192 }
8193
8194 #[test]
8195 fn gj_joins_without_inserting_space() {
8196 let mut e = editor_with("hello\n world");
8197 run_keys(&mut e, "gJ");
8198 assert_eq!(e.buffer().lines(), &["hello world".to_string()]);
8200 }
8201
8202 #[test]
8203 fn gj_noop_on_last_line() {
8204 let mut e = editor_with("only");
8205 run_keys(&mut e, "gJ");
8206 assert_eq!(e.buffer().lines(), &["only".to_string()]);
8207 }
8208
8209 #[test]
8210 fn ge_jumps_to_previous_word_end() {
8211 let mut e = editor_with("foo bar baz");
8212 e.jump_cursor(0, 5);
8213 run_keys(&mut e, "ge");
8214 assert_eq!(e.cursor(), (0, 2));
8215 }
8216
8217 #[test]
8218 fn ge_respects_word_class() {
8219 let mut e = editor_with("foo-bar baz");
8222 e.jump_cursor(0, 5);
8223 run_keys(&mut e, "ge");
8224 assert_eq!(e.cursor(), (0, 3));
8225 }
8226
8227 #[test]
8228 fn big_ge_treats_hyphens_as_part_of_word() {
8229 let mut e = editor_with("foo-bar baz");
8232 e.jump_cursor(0, 10);
8233 run_keys(&mut e, "gE");
8234 assert_eq!(e.cursor(), (0, 6));
8235 }
8236
8237 #[test]
8238 fn ge_crosses_line_boundary() {
8239 let mut e = editor_with("foo\nbar");
8240 e.jump_cursor(1, 0);
8241 run_keys(&mut e, "ge");
8242 assert_eq!(e.cursor(), (0, 2));
8243 }
8244
8245 #[test]
8246 fn dge_deletes_to_end_of_previous_word() {
8247 let mut e = editor_with("foo bar baz");
8248 e.jump_cursor(0, 8);
8249 run_keys(&mut e, "dge");
8252 assert_eq!(e.buffer().lines()[0], "foo baaz");
8253 }
8254
8255 #[test]
8256 fn ctrl_scroll_keys_do_not_panic() {
8257 let mut e = editor_with(
8260 (0..50)
8261 .map(|i| format!("line{i}"))
8262 .collect::<Vec<_>>()
8263 .join("\n")
8264 .as_str(),
8265 );
8266 run_keys(&mut e, "<C-f>");
8267 run_keys(&mut e, "<C-b>");
8268 assert!(!e.buffer().lines().is_empty());
8270 }
8271
8272 #[test]
8279 fn count_insert_with_arrow_nav_does_not_leak_rows() {
8280 let mut e = Editor::new(
8281 hjkl_buffer::Buffer::new(),
8282 crate::types::DefaultHost::new(),
8283 crate::types::Options::default(),
8284 );
8285 e.set_content("row0\nrow1\nrow2");
8286 run_keys(&mut e, "3iX<Down><Esc>");
8288 assert!(e.buffer().lines()[0].contains('X'));
8290 assert!(
8293 !e.buffer().lines()[1].contains("row0"),
8294 "row1 leaked row0 contents: {:?}",
8295 e.buffer().lines()[1]
8296 );
8297 assert_eq!(e.buffer().lines().len(), 3);
8300 }
8301
8302 fn editor_with_rows(n: usize, viewport: u16) -> Editor {
8305 let mut e = Editor::new(
8306 hjkl_buffer::Buffer::new(),
8307 crate::types::DefaultHost::new(),
8308 crate::types::Options::default(),
8309 );
8310 let body = (0..n)
8311 .map(|i| format!(" line{}", i))
8312 .collect::<Vec<_>>()
8313 .join("\n");
8314 e.set_content(&body);
8315 e.set_viewport_height(viewport);
8316 e
8317 }
8318
8319 #[test]
8320 fn ctrl_d_moves_cursor_half_page_down() {
8321 let mut e = editor_with_rows(100, 20);
8322 run_keys(&mut e, "<C-d>");
8323 assert_eq!(e.cursor().0, 10);
8324 }
8325
8326 fn editor_with_wrap_lines(lines: &[&str], viewport: u16, text_width: u16) -> Editor {
8327 let mut e = Editor::new(
8328 hjkl_buffer::Buffer::new(),
8329 crate::types::DefaultHost::new(),
8330 crate::types::Options::default(),
8331 );
8332 e.set_content(&lines.join("\n"));
8333 e.set_viewport_height(viewport);
8334 let v = e.host_mut().viewport_mut();
8335 v.height = viewport;
8336 v.width = text_width;
8337 v.text_width = text_width;
8338 v.wrap = hjkl_buffer::Wrap::Char;
8339 e.settings_mut().wrap = hjkl_buffer::Wrap::Char;
8340 e
8341 }
8342
8343 #[test]
8344 fn scrolloff_wrap_keeps_cursor_off_bottom_edge() {
8345 let lines = ["aaaabbbbcccc"; 10];
8349 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8350 e.jump_cursor(4, 0);
8351 e.ensure_cursor_in_scrolloff();
8352 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8353 assert!(csr <= 6, "csr={csr}");
8354 }
8355
8356 #[test]
8357 fn scrolloff_wrap_keeps_cursor_off_top_edge() {
8358 let lines = ["aaaabbbbcccc"; 10];
8359 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8360 e.jump_cursor(7, 0);
8363 e.ensure_cursor_in_scrolloff();
8364 e.jump_cursor(2, 0);
8365 e.ensure_cursor_in_scrolloff();
8366 let csr = e.buffer().cursor_screen_row(e.host().viewport()).unwrap();
8367 assert!(csr >= 5, "csr={csr}");
8369 }
8370
8371 #[test]
8372 fn scrolloff_wrap_clamps_top_at_buffer_end() {
8373 let lines = ["aaaabbbbcccc"; 5];
8374 let mut e = editor_with_wrap_lines(&lines, 12, 4);
8375 e.jump_cursor(4, 11);
8376 e.ensure_cursor_in_scrolloff();
8377 let top = e.host().viewport().top_row;
8382 assert_eq!(top, 1);
8383 }
8384
8385 #[test]
8386 fn ctrl_u_moves_cursor_half_page_up() {
8387 let mut e = editor_with_rows(100, 20);
8388 e.jump_cursor(50, 0);
8389 run_keys(&mut e, "<C-u>");
8390 assert_eq!(e.cursor().0, 40);
8391 }
8392
8393 #[test]
8394 fn ctrl_f_moves_cursor_full_page_down() {
8395 let mut e = editor_with_rows(100, 20);
8396 run_keys(&mut e, "<C-f>");
8397 assert_eq!(e.cursor().0, 18);
8399 }
8400
8401 #[test]
8402 fn ctrl_b_moves_cursor_full_page_up() {
8403 let mut e = editor_with_rows(100, 20);
8404 e.jump_cursor(50, 0);
8405 run_keys(&mut e, "<C-b>");
8406 assert_eq!(e.cursor().0, 32);
8407 }
8408
8409 #[test]
8410 fn ctrl_d_lands_on_first_non_blank() {
8411 let mut e = editor_with_rows(100, 20);
8412 run_keys(&mut e, "<C-d>");
8413 assert_eq!(e.cursor().1, 2);
8415 }
8416
8417 #[test]
8418 fn ctrl_d_clamps_at_end_of_buffer() {
8419 let mut e = editor_with_rows(5, 20);
8420 run_keys(&mut e, "<C-d>");
8421 assert_eq!(e.cursor().0, 4);
8422 }
8423
8424 #[test]
8425 fn capital_h_jumps_to_viewport_top() {
8426 let mut e = editor_with_rows(100, 10);
8427 e.jump_cursor(50, 0);
8428 e.set_viewport_top(45);
8429 let top = e.host().viewport().top_row;
8430 run_keys(&mut e, "H");
8431 assert_eq!(e.cursor().0, top);
8432 assert_eq!(e.cursor().1, 2);
8433 }
8434
8435 #[test]
8436 fn capital_l_jumps_to_viewport_bottom() {
8437 let mut e = editor_with_rows(100, 10);
8438 e.jump_cursor(50, 0);
8439 e.set_viewport_top(45);
8440 let top = e.host().viewport().top_row;
8441 run_keys(&mut e, "L");
8442 assert_eq!(e.cursor().0, top + 9);
8443 }
8444
8445 #[test]
8446 fn capital_m_jumps_to_viewport_middle() {
8447 let mut e = editor_with_rows(100, 10);
8448 e.jump_cursor(50, 0);
8449 e.set_viewport_top(45);
8450 let top = e.host().viewport().top_row;
8451 run_keys(&mut e, "M");
8452 assert_eq!(e.cursor().0, top + 4);
8454 }
8455
8456 #[test]
8457 fn g_capital_m_lands_at_line_midpoint() {
8458 let mut e = editor_with("hello world!"); run_keys(&mut e, "gM");
8460 assert_eq!(e.cursor(), (0, 6));
8462 }
8463
8464 #[test]
8465 fn g_capital_m_on_empty_line_stays_at_zero() {
8466 let mut e = editor_with("");
8467 run_keys(&mut e, "gM");
8468 assert_eq!(e.cursor(), (0, 0));
8469 }
8470
8471 #[test]
8472 fn g_capital_m_uses_current_line_only() {
8473 let mut e = editor_with("a\nlonglongline"); e.jump_cursor(1, 0);
8476 run_keys(&mut e, "gM");
8477 assert_eq!(e.cursor(), (1, 6));
8478 }
8479
8480 #[test]
8481 fn capital_h_count_offsets_from_top() {
8482 let mut e = editor_with_rows(100, 10);
8483 e.jump_cursor(50, 0);
8484 e.set_viewport_top(45);
8485 let top = e.host().viewport().top_row;
8486 run_keys(&mut e, "3H");
8487 assert_eq!(e.cursor().0, top + 2);
8488 }
8489
8490 #[test]
8493 fn ctrl_o_returns_to_pre_g_position() {
8494 let mut e = editor_with_rows(50, 20);
8495 e.jump_cursor(5, 2);
8496 run_keys(&mut e, "G");
8497 assert_eq!(e.cursor().0, 49);
8498 run_keys(&mut e, "<C-o>");
8499 assert_eq!(e.cursor(), (5, 2));
8500 }
8501
8502 #[test]
8503 fn ctrl_i_redoes_jump_after_ctrl_o() {
8504 let mut e = editor_with_rows(50, 20);
8505 e.jump_cursor(5, 2);
8506 run_keys(&mut e, "G");
8507 let post = e.cursor();
8508 run_keys(&mut e, "<C-o>");
8509 run_keys(&mut e, "<C-i>");
8510 assert_eq!(e.cursor(), post);
8511 }
8512
8513 #[test]
8514 fn new_jump_clears_forward_stack() {
8515 let mut e = editor_with_rows(50, 20);
8516 e.jump_cursor(5, 2);
8517 run_keys(&mut e, "G");
8518 run_keys(&mut e, "<C-o>");
8519 run_keys(&mut e, "gg");
8520 run_keys(&mut e, "<C-i>");
8521 assert_eq!(e.cursor().0, 0);
8522 }
8523
8524 #[test]
8525 fn ctrl_o_on_empty_stack_is_noop() {
8526 let mut e = editor_with_rows(10, 20);
8527 e.jump_cursor(3, 1);
8528 run_keys(&mut e, "<C-o>");
8529 assert_eq!(e.cursor(), (3, 1));
8530 }
8531
8532 #[test]
8533 fn asterisk_search_pushes_jump() {
8534 let mut e = editor_with("foo bar\nbaz foo end");
8535 e.jump_cursor(0, 0);
8536 run_keys(&mut e, "*");
8537 let after = e.cursor();
8538 assert_ne!(after, (0, 0));
8539 run_keys(&mut e, "<C-o>");
8540 assert_eq!(e.cursor(), (0, 0));
8541 }
8542
8543 #[test]
8544 fn h_viewport_jump_is_recorded() {
8545 let mut e = editor_with_rows(100, 10);
8546 e.jump_cursor(50, 0);
8547 e.set_viewport_top(45);
8548 let pre = e.cursor();
8549 run_keys(&mut e, "H");
8550 assert_ne!(e.cursor(), pre);
8551 run_keys(&mut e, "<C-o>");
8552 assert_eq!(e.cursor(), pre);
8553 }
8554
8555 #[test]
8556 fn j_k_motion_does_not_push_jump() {
8557 let mut e = editor_with_rows(50, 20);
8558 e.jump_cursor(5, 0);
8559 run_keys(&mut e, "jjj");
8560 run_keys(&mut e, "<C-o>");
8561 assert_eq!(e.cursor().0, 8);
8562 }
8563
8564 #[test]
8565 fn jumplist_caps_at_100() {
8566 let mut e = editor_with_rows(200, 20);
8567 for i in 0..101 {
8568 e.jump_cursor(i, 0);
8569 run_keys(&mut e, "G");
8570 }
8571 assert!(e.vim.jump_back.len() <= 100);
8572 }
8573
8574 #[test]
8575 fn tab_acts_as_ctrl_i() {
8576 let mut e = editor_with_rows(50, 20);
8577 e.jump_cursor(5, 2);
8578 run_keys(&mut e, "G");
8579 let post = e.cursor();
8580 run_keys(&mut e, "<C-o>");
8581 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
8582 assert_eq!(e.cursor(), post);
8583 }
8584
8585 #[test]
8588 fn ma_then_backtick_a_jumps_exact() {
8589 let mut e = editor_with_rows(50, 20);
8590 e.jump_cursor(5, 3);
8591 run_keys(&mut e, "ma");
8592 e.jump_cursor(20, 0);
8593 run_keys(&mut e, "`a");
8594 assert_eq!(e.cursor(), (5, 3));
8595 }
8596
8597 #[test]
8598 fn ma_then_apostrophe_a_lands_on_first_non_blank() {
8599 let mut e = editor_with_rows(50, 20);
8600 e.jump_cursor(5, 6);
8602 run_keys(&mut e, "ma");
8603 e.jump_cursor(30, 4);
8604 run_keys(&mut e, "'a");
8605 assert_eq!(e.cursor(), (5, 2));
8606 }
8607
8608 #[test]
8609 fn goto_mark_pushes_jumplist() {
8610 let mut e = editor_with_rows(50, 20);
8611 e.jump_cursor(10, 2);
8612 run_keys(&mut e, "mz");
8613 e.jump_cursor(3, 0);
8614 run_keys(&mut e, "`z");
8615 assert_eq!(e.cursor(), (10, 2));
8616 run_keys(&mut e, "<C-o>");
8617 assert_eq!(e.cursor(), (3, 0));
8618 }
8619
8620 #[test]
8621 fn goto_missing_mark_is_noop() {
8622 let mut e = editor_with_rows(50, 20);
8623 e.jump_cursor(3, 1);
8624 run_keys(&mut e, "`q");
8625 assert_eq!(e.cursor(), (3, 1));
8626 }
8627
8628 #[test]
8629 fn uppercase_mark_stored_under_uppercase_key() {
8630 let mut e = editor_with_rows(50, 20);
8631 e.jump_cursor(5, 3);
8632 run_keys(&mut e, "mA");
8633 assert_eq!(e.mark('A'), Some((5, 3)));
8636 assert!(e.mark('a').is_none());
8637 }
8638
8639 #[test]
8640 fn mark_survives_document_shrink_via_clamp() {
8641 let mut e = editor_with_rows(50, 20);
8642 e.jump_cursor(40, 4);
8643 run_keys(&mut e, "mx");
8644 e.set_content("a\nb\nc\nd\ne");
8646 run_keys(&mut e, "`x");
8647 let (r, _) = e.cursor();
8649 assert!(r <= 4);
8650 }
8651
8652 #[test]
8653 fn g_semicolon_walks_back_through_edits() {
8654 let mut e = editor_with("alpha\nbeta\ngamma");
8655 e.jump_cursor(0, 0);
8658 run_keys(&mut e, "iX<Esc>");
8659 e.jump_cursor(2, 0);
8660 run_keys(&mut e, "iY<Esc>");
8661 run_keys(&mut e, "g;");
8663 assert_eq!(e.cursor(), (2, 1));
8664 run_keys(&mut e, "g;");
8666 assert_eq!(e.cursor(), (0, 1));
8667 run_keys(&mut e, "g;");
8669 assert_eq!(e.cursor(), (0, 1));
8670 }
8671
8672 #[test]
8673 fn g_comma_walks_forward_after_g_semicolon() {
8674 let mut e = editor_with("a\nb\nc");
8675 e.jump_cursor(0, 0);
8676 run_keys(&mut e, "iX<Esc>");
8677 e.jump_cursor(2, 0);
8678 run_keys(&mut e, "iY<Esc>");
8679 run_keys(&mut e, "g;");
8680 run_keys(&mut e, "g;");
8681 assert_eq!(e.cursor(), (0, 1));
8682 run_keys(&mut e, "g,");
8683 assert_eq!(e.cursor(), (2, 1));
8684 }
8685
8686 #[test]
8687 fn new_edit_during_walk_trims_forward_entries() {
8688 let mut e = editor_with("a\nb\nc\nd");
8689 e.jump_cursor(0, 0);
8690 run_keys(&mut e, "iX<Esc>"); e.jump_cursor(2, 0);
8692 run_keys(&mut e, "iY<Esc>"); run_keys(&mut e, "g;");
8695 run_keys(&mut e, "g;");
8696 assert_eq!(e.cursor(), (0, 1));
8697 run_keys(&mut e, "iZ<Esc>");
8699 run_keys(&mut e, "g,");
8701 assert_ne!(e.cursor(), (2, 1));
8703 }
8704
8705 #[test]
8711 fn capital_mark_set_and_jump() {
8712 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
8713 e.jump_cursor(2, 1);
8714 run_keys(&mut e, "mA");
8715 e.jump_cursor(0, 0);
8717 run_keys(&mut e, "'A");
8719 assert_eq!(e.cursor().0, 2);
8721 }
8722
8723 #[test]
8724 fn capital_mark_survives_set_content() {
8725 let mut e = editor_with("first buffer line\nsecond");
8726 e.jump_cursor(1, 3);
8727 run_keys(&mut e, "mA");
8728 e.set_content("totally different content\non many\nrows of text");
8730 e.jump_cursor(0, 0);
8732 run_keys(&mut e, "'A");
8733 assert_eq!(e.cursor().0, 1);
8734 }
8735
8736 #[test]
8741 fn capital_mark_shifts_with_edit() {
8742 let mut e = editor_with("a\nb\nc\nd");
8743 e.jump_cursor(3, 0);
8744 run_keys(&mut e, "mA");
8745 e.jump_cursor(0, 0);
8747 run_keys(&mut e, "dd");
8748 e.jump_cursor(0, 0);
8749 run_keys(&mut e, "'A");
8750 assert_eq!(e.cursor().0, 2);
8751 }
8752
8753 #[test]
8754 fn mark_below_delete_shifts_up() {
8755 let mut e = editor_with("a\nb\nc\nd\ne");
8756 e.jump_cursor(3, 0);
8758 run_keys(&mut e, "ma");
8759 e.jump_cursor(0, 0);
8761 run_keys(&mut e, "dd");
8762 e.jump_cursor(0, 0);
8764 run_keys(&mut e, "'a");
8765 assert_eq!(e.cursor().0, 2);
8766 assert_eq!(e.buffer().line(2).unwrap(), "d");
8767 }
8768
8769 #[test]
8770 fn mark_on_deleted_row_is_dropped() {
8771 let mut e = editor_with("a\nb\nc\nd");
8772 e.jump_cursor(1, 0);
8774 run_keys(&mut e, "ma");
8775 run_keys(&mut e, "dd");
8777 e.jump_cursor(2, 0);
8779 run_keys(&mut e, "'a");
8780 assert_eq!(e.cursor().0, 2);
8782 }
8783
8784 #[test]
8785 fn mark_above_edit_unchanged() {
8786 let mut e = editor_with("a\nb\nc\nd\ne");
8787 e.jump_cursor(0, 0);
8789 run_keys(&mut e, "ma");
8790 e.jump_cursor(3, 0);
8792 run_keys(&mut e, "dd");
8793 e.jump_cursor(2, 0);
8795 run_keys(&mut e, "'a");
8796 assert_eq!(e.cursor().0, 0);
8797 }
8798
8799 #[test]
8800 fn mark_shifts_down_after_insert() {
8801 let mut e = editor_with("a\nb\nc");
8802 e.jump_cursor(2, 0);
8804 run_keys(&mut e, "ma");
8805 e.jump_cursor(0, 0);
8807 run_keys(&mut e, "Onew<Esc>");
8808 e.jump_cursor(0, 0);
8811 run_keys(&mut e, "'a");
8812 assert_eq!(e.cursor().0, 3);
8813 assert_eq!(e.buffer().line(3).unwrap(), "c");
8814 }
8815
8816 #[test]
8819 fn forward_search_commit_pushes_jump() {
8820 let mut e = editor_with("alpha beta\nfoo target end\nmore");
8821 e.jump_cursor(0, 0);
8822 run_keys(&mut e, "/target<CR>");
8823 assert_ne!(e.cursor(), (0, 0));
8825 run_keys(&mut e, "<C-o>");
8827 assert_eq!(e.cursor(), (0, 0));
8828 }
8829
8830 #[test]
8831 fn search_commit_no_match_does_not_push_jump() {
8832 let mut e = editor_with("alpha beta\nfoo end");
8833 e.jump_cursor(0, 3);
8834 let pre_len = e.vim.jump_back.len();
8835 run_keys(&mut e, "/zzznotfound<CR>");
8836 assert_eq!(e.vim.jump_back.len(), pre_len);
8838 }
8839
8840 #[test]
8843 fn buffer_cursor_mirrors_textarea_after_horizontal_motion() {
8844 let mut e = editor_with("hello world");
8845 run_keys(&mut e, "lll");
8846 let (row, col) = e.cursor();
8847 assert_eq!(e.buffer.cursor().row, row);
8848 assert_eq!(e.buffer.cursor().col, col);
8849 }
8850
8851 #[test]
8852 fn buffer_cursor_mirrors_textarea_after_vertical_motion() {
8853 let mut e = editor_with("aaaa\nbbbb\ncccc");
8854 run_keys(&mut e, "jj");
8855 let (row, col) = e.cursor();
8856 assert_eq!(e.buffer.cursor().row, row);
8857 assert_eq!(e.buffer.cursor().col, col);
8858 }
8859
8860 #[test]
8861 fn buffer_cursor_mirrors_textarea_after_word_motion() {
8862 let mut e = editor_with("foo bar baz");
8863 run_keys(&mut e, "ww");
8864 let (row, col) = e.cursor();
8865 assert_eq!(e.buffer.cursor().row, row);
8866 assert_eq!(e.buffer.cursor().col, col);
8867 }
8868
8869 #[test]
8870 fn buffer_cursor_mirrors_textarea_after_jump_motion() {
8871 let mut e = editor_with("a\nb\nc\nd\ne");
8872 run_keys(&mut e, "G");
8873 let (row, col) = e.cursor();
8874 assert_eq!(e.buffer.cursor().row, row);
8875 assert_eq!(e.buffer.cursor().col, col);
8876 }
8877
8878 #[test]
8879 fn editor_sticky_col_tracks_horizontal_motion() {
8880 let mut e = editor_with("longline\nhi\nlongline");
8881 run_keys(&mut e, "fl");
8886 let landed = e.cursor().1;
8887 assert!(landed > 0, "fl should have moved");
8888 run_keys(&mut e, "j");
8889 assert_eq!(e.sticky_col(), Some(landed));
8892 }
8893
8894 #[test]
8895 fn buffer_content_mirrors_textarea_after_insert() {
8896 let mut e = editor_with("hello");
8897 run_keys(&mut e, "iXYZ<Esc>");
8898 let text = e.buffer().lines().join("\n");
8899 assert_eq!(e.buffer.as_string(), text);
8900 }
8901
8902 #[test]
8903 fn buffer_content_mirrors_textarea_after_delete() {
8904 let mut e = editor_with("alpha bravo charlie");
8905 run_keys(&mut e, "dw");
8906 let text = e.buffer().lines().join("\n");
8907 assert_eq!(e.buffer.as_string(), text);
8908 }
8909
8910 #[test]
8911 fn buffer_content_mirrors_textarea_after_dd() {
8912 let mut e = editor_with("a\nb\nc\nd");
8913 run_keys(&mut e, "jdd");
8914 let text = e.buffer().lines().join("\n");
8915 assert_eq!(e.buffer.as_string(), text);
8916 }
8917
8918 #[test]
8919 fn buffer_content_mirrors_textarea_after_open_line() {
8920 let mut e = editor_with("foo\nbar");
8921 run_keys(&mut e, "oNEW<Esc>");
8922 let text = e.buffer().lines().join("\n");
8923 assert_eq!(e.buffer.as_string(), text);
8924 }
8925
8926 #[test]
8927 fn buffer_content_mirrors_textarea_after_paste() {
8928 let mut e = editor_with("hello");
8929 run_keys(&mut e, "yy");
8930 run_keys(&mut e, "p");
8931 let text = e.buffer().lines().join("\n");
8932 assert_eq!(e.buffer.as_string(), text);
8933 }
8934
8935 #[test]
8936 fn buffer_selection_none_in_normal_mode() {
8937 let e = editor_with("foo bar");
8938 assert!(e.buffer_selection().is_none());
8939 }
8940
8941 #[test]
8942 fn buffer_selection_char_in_visual_mode() {
8943 use hjkl_buffer::{Position, Selection};
8944 let mut e = editor_with("hello world");
8945 run_keys(&mut e, "vlll");
8946 assert_eq!(
8947 e.buffer_selection(),
8948 Some(Selection::Char {
8949 anchor: Position::new(0, 0),
8950 head: Position::new(0, 3),
8951 })
8952 );
8953 }
8954
8955 #[test]
8956 fn buffer_selection_line_in_visual_line_mode() {
8957 use hjkl_buffer::Selection;
8958 let mut e = editor_with("a\nb\nc\nd");
8959 run_keys(&mut e, "Vj");
8960 assert_eq!(
8961 e.buffer_selection(),
8962 Some(Selection::Line {
8963 anchor_row: 0,
8964 head_row: 1,
8965 })
8966 );
8967 }
8968
8969 #[test]
8970 fn wrapscan_off_blocks_wrap_around() {
8971 let mut e = editor_with("first\nsecond\nthird\n");
8972 e.settings_mut().wrapscan = false;
8973 e.jump_cursor(2, 0);
8975 run_keys(&mut e, "/first<CR>");
8976 assert_eq!(e.cursor().0, 2, "wrapscan off should block wrap");
8978 e.settings_mut().wrapscan = true;
8980 run_keys(&mut e, "/first<CR>");
8981 assert_eq!(e.cursor().0, 0, "wrapscan on should wrap to row 0");
8982 }
8983
8984 #[test]
8985 fn smartcase_uppercase_pattern_stays_sensitive() {
8986 let mut e = editor_with("foo\nFoo\nBAR\n");
8987 e.settings_mut().ignore_case = true;
8988 e.settings_mut().smartcase = true;
8989 run_keys(&mut e, "/foo<CR>");
8992 let r1 = e
8993 .search_state()
8994 .pattern
8995 .as_ref()
8996 .unwrap()
8997 .as_str()
8998 .to_string();
8999 assert!(r1.starts_with("(?i)"), "lowercase under smartcase: {r1}");
9000 run_keys(&mut e, "/Foo<CR>");
9002 let r2 = e
9003 .search_state()
9004 .pattern
9005 .as_ref()
9006 .unwrap()
9007 .as_str()
9008 .to_string();
9009 assert!(!r2.starts_with("(?i)"), "mixed-case under smartcase: {r2}");
9010 }
9011
9012 #[test]
9013 fn enter_with_autoindent_copies_leading_whitespace() {
9014 let mut e = editor_with(" foo");
9015 e.jump_cursor(0, 7);
9016 run_keys(&mut e, "i<CR>");
9017 assert_eq!(e.buffer.line(1).unwrap(), " ");
9018 }
9019
9020 #[test]
9021 fn enter_without_autoindent_inserts_bare_newline() {
9022 let mut e = editor_with(" foo");
9023 e.settings_mut().autoindent = false;
9024 e.jump_cursor(0, 7);
9025 run_keys(&mut e, "i<CR>");
9026 assert_eq!(e.buffer.line(1).unwrap(), "");
9027 }
9028
9029 #[test]
9030 fn iskeyword_default_treats_alnum_underscore_as_word() {
9031 let mut e = editor_with("foo_bar baz");
9032 e.jump_cursor(0, 0);
9036 run_keys(&mut e, "*");
9037 let p = e
9038 .search_state()
9039 .pattern
9040 .as_ref()
9041 .unwrap()
9042 .as_str()
9043 .to_string();
9044 assert!(p.contains("foo_bar"), "default iskeyword: {p}");
9045 }
9046
9047 #[test]
9048 fn w_motion_respects_custom_iskeyword() {
9049 let mut e = editor_with("foo-bar baz");
9053 run_keys(&mut e, "w");
9054 assert_eq!(e.cursor().1, 3, "default iskeyword: {:?}", e.cursor());
9055 let mut e2 = editor_with("foo-bar baz");
9058 e2.set_iskeyword("@,_,45");
9059 run_keys(&mut e2, "w");
9060 assert_eq!(e2.cursor().1, 8, "dash-as-word: {:?}", e2.cursor());
9061 }
9062
9063 #[test]
9064 fn iskeyword_with_dash_treats_dash_as_word_char() {
9065 let mut e = editor_with("foo-bar baz");
9066 e.settings_mut().iskeyword = "@,_,45".to_string();
9067 e.jump_cursor(0, 0);
9068 run_keys(&mut e, "*");
9069 let p = e
9070 .search_state()
9071 .pattern
9072 .as_ref()
9073 .unwrap()
9074 .as_str()
9075 .to_string();
9076 assert!(p.contains("foo-bar"), "dash-as-word: {p}");
9077 }
9078
9079 #[test]
9080 fn timeoutlen_drops_pending_g_prefix() {
9081 use std::time::{Duration, Instant};
9082 let mut e = editor_with("a\nb\nc");
9083 e.jump_cursor(2, 0);
9084 run_keys(&mut e, "g");
9086 assert!(matches!(e.vim.pending, super::Pending::G));
9087 e.settings.timeout_len = Duration::from_nanos(0);
9095 e.vim.last_input_at = Some(Instant::now() - Duration::from_secs(60));
9096 e.vim.last_input_host_at = Some(Duration::ZERO);
9097 run_keys(&mut e, "g");
9101 assert_eq!(e.cursor().0, 2, "timeout must abandon g-prefix");
9103 }
9104
9105 #[test]
9106 fn undobreak_on_breaks_group_at_arrow_motion() {
9107 let mut e = editor_with("");
9108 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9110 let line = e.buffer.line(0).unwrap_or("").to_string();
9113 assert!(line.contains("aaa"), "after undobreak: {line:?}");
9114 assert!(!line.contains("bbb"), "bbb should be undone: {line:?}");
9115 }
9116
9117 #[test]
9118 fn undobreak_off_keeps_full_run_in_one_group() {
9119 let mut e = editor_with("");
9120 e.settings_mut().undo_break_on_motion = false;
9121 run_keys(&mut e, "iaaa<Left>bbb<Esc>u");
9122 assert_eq!(e.buffer.line(0).unwrap_or(""), "");
9125 }
9126
9127 #[test]
9128 fn undobreak_round_trips_through_options() {
9129 let e = editor_with("");
9130 let opts = e.current_options();
9131 assert!(opts.undo_break_on_motion);
9132 let mut e2 = editor_with("");
9133 let mut new_opts = opts.clone();
9134 new_opts.undo_break_on_motion = false;
9135 e2.apply_options(&new_opts);
9136 assert!(!e2.current_options().undo_break_on_motion);
9137 }
9138
9139 #[test]
9140 fn undo_levels_cap_drops_oldest() {
9141 let mut e = editor_with("abcde");
9142 e.settings_mut().undo_levels = 3;
9143 run_keys(&mut e, "ra");
9144 run_keys(&mut e, "lrb");
9145 run_keys(&mut e, "lrc");
9146 run_keys(&mut e, "lrd");
9147 run_keys(&mut e, "lre");
9148 assert_eq!(e.undo_stack_len(), 3);
9149 }
9150
9151 #[test]
9152 fn tab_inserts_literal_tab_when_noexpandtab() {
9153 let mut e = editor_with("");
9154 e.settings_mut().expandtab = false;
9157 e.settings_mut().softtabstop = 0;
9158 run_keys(&mut e, "i");
9159 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9160 assert_eq!(e.buffer.line(0).unwrap(), "\t");
9161 }
9162
9163 #[test]
9164 fn tab_inserts_spaces_when_expandtab() {
9165 let mut e = editor_with("");
9166 e.settings_mut().expandtab = true;
9167 e.settings_mut().tabstop = 4;
9168 run_keys(&mut e, "i");
9169 e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9170 assert_eq!(e.buffer.line(0).unwrap(), " ");
9171 }
9172
9173 #[test]
9174 fn tab_with_softtabstop_fills_to_next_boundary() {
9175 let mut e = editor_with("ab");
9177 e.settings_mut().expandtab = true;
9178 e.settings_mut().tabstop = 8;
9179 e.settings_mut().softtabstop = 4;
9180 run_keys(&mut e, "A"); e.handle_key(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
9182 assert_eq!(e.buffer.line(0).unwrap(), "ab ");
9183 }
9184
9185 #[test]
9186 fn backspace_deletes_softtab_run() {
9187 let mut e = editor_with(" x");
9190 e.settings_mut().softtabstop = 4;
9191 run_keys(&mut e, "fxi");
9193 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9194 assert_eq!(e.buffer.line(0).unwrap(), "x");
9195 }
9196
9197 #[test]
9198 fn backspace_falls_back_to_single_char_when_run_not_aligned() {
9199 let mut e = editor_with(" x");
9202 e.settings_mut().softtabstop = 4;
9203 run_keys(&mut e, "fxi");
9204 e.handle_key(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));
9205 assert_eq!(e.buffer.line(0).unwrap(), " x");
9206 }
9207
9208 #[test]
9209 fn readonly_blocks_insert_mutation() {
9210 let mut e = editor_with("hello");
9211 e.settings_mut().readonly = true;
9212 run_keys(&mut e, "iX<Esc>");
9213 assert_eq!(e.buffer.line(0).unwrap(), "hello");
9214 }
9215
9216 #[cfg(feature = "ratatui")]
9217 #[test]
9218 fn intern_ratatui_style_dedups_repeated_styles() {
9219 use ratatui::style::{Color, Style};
9220 let mut e = editor_with("");
9221 let red = Style::default().fg(Color::Red);
9222 let blue = Style::default().fg(Color::Blue);
9223 let id_r1 = e.intern_ratatui_style(red);
9224 let id_r2 = e.intern_ratatui_style(red);
9225 let id_b = e.intern_ratatui_style(blue);
9226 assert_eq!(id_r1, id_r2);
9227 assert_ne!(id_r1, id_b);
9228 assert_eq!(e.style_table().len(), 2);
9229 }
9230
9231 #[cfg(feature = "ratatui")]
9232 #[test]
9233 fn install_ratatui_syntax_spans_translates_styled_spans() {
9234 use ratatui::style::{Color, Style};
9235 let mut e = editor_with("SELECT foo");
9236 e.install_ratatui_syntax_spans(vec![vec![(0, 6, Style::default().fg(Color::Red))]]);
9237 let by_row = e.buffer_spans();
9238 assert_eq!(by_row.len(), 1);
9239 assert_eq!(by_row[0].len(), 1);
9240 assert_eq!(by_row[0][0].start_byte, 0);
9241 assert_eq!(by_row[0][0].end_byte, 6);
9242 let id = by_row[0][0].style;
9243 assert_eq!(e.style_table()[id as usize].fg, Some(Color::Red));
9244 }
9245
9246 #[cfg(feature = "ratatui")]
9247 #[test]
9248 fn install_ratatui_syntax_spans_clamps_sentinel_end() {
9249 use ratatui::style::{Color, Style};
9250 let mut e = editor_with("hello");
9251 e.install_ratatui_syntax_spans(vec![vec![(
9252 0,
9253 usize::MAX,
9254 Style::default().fg(Color::Blue),
9255 )]]);
9256 let by_row = e.buffer_spans();
9257 assert_eq!(by_row[0][0].end_byte, 5);
9258 }
9259
9260 #[cfg(feature = "ratatui")]
9261 #[test]
9262 fn install_ratatui_syntax_spans_drops_zero_width() {
9263 use ratatui::style::{Color, Style};
9264 let mut e = editor_with("abc");
9265 e.install_ratatui_syntax_spans(vec![vec![(2, 2, Style::default().fg(Color::Red))]]);
9266 assert!(e.buffer_spans()[0].is_empty());
9267 }
9268
9269 #[test]
9270 fn named_register_yank_into_a_then_paste_from_a() {
9271 let mut e = editor_with("hello world\nsecond");
9272 run_keys(&mut e, "\"ayw");
9273 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9275 run_keys(&mut e, "j0\"aP");
9277 assert_eq!(e.buffer().lines()[1], "hello second");
9278 }
9279
9280 #[test]
9281 fn capital_r_overstrikes_chars() {
9282 let mut e = editor_with("hello");
9283 e.jump_cursor(0, 0);
9284 run_keys(&mut e, "RXY<Esc>");
9285 assert_eq!(e.buffer().lines()[0], "XYllo");
9287 }
9288
9289 #[test]
9290 fn capital_r_at_eol_appends() {
9291 let mut e = editor_with("hi");
9292 e.jump_cursor(0, 1);
9293 run_keys(&mut e, "RXYZ<Esc>");
9295 assert_eq!(e.buffer().lines()[0], "hXYZ");
9296 }
9297
9298 #[test]
9299 fn capital_r_count_does_not_repeat_overstrike_char_by_char() {
9300 let mut e = editor_with("abc");
9304 e.jump_cursor(0, 0);
9305 run_keys(&mut e, "RX<Esc>");
9306 assert_eq!(e.buffer().lines()[0], "Xbc");
9307 }
9308
9309 #[test]
9310 fn ctrl_r_in_insert_pastes_named_register() {
9311 let mut e = editor_with("hello world");
9312 run_keys(&mut e, "\"ayw");
9314 assert_eq!(e.registers().read('a').unwrap().text, "hello ");
9315 run_keys(&mut e, "o");
9317 assert_eq!(e.vim_mode(), VimMode::Insert);
9318 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9319 e.handle_key(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE));
9320 assert_eq!(e.buffer().lines()[1], "hello ");
9321 assert_eq!(e.cursor(), (1, 6));
9323 assert_eq!(e.vim_mode(), VimMode::Insert);
9325 e.handle_key(KeyEvent::new(KeyCode::Char('X'), KeyModifiers::NONE));
9326 assert_eq!(e.buffer().lines()[1], "hello X");
9327 }
9328
9329 #[test]
9330 fn ctrl_r_with_unnamed_register() {
9331 let mut e = editor_with("foo");
9332 run_keys(&mut e, "yiw");
9333 run_keys(&mut e, "A ");
9334 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9336 e.handle_key(KeyEvent::new(KeyCode::Char('"'), KeyModifiers::NONE));
9337 assert_eq!(e.buffer().lines()[0], "foo foo");
9338 }
9339
9340 #[test]
9341 fn ctrl_r_unknown_selector_is_no_op() {
9342 let mut e = editor_with("abc");
9343 run_keys(&mut e, "A");
9344 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9345 e.handle_key(KeyEvent::new(KeyCode::Char('?'), KeyModifiers::NONE));
9348 e.handle_key(KeyEvent::new(KeyCode::Char('Z'), KeyModifiers::NONE));
9349 assert_eq!(e.buffer().lines()[0], "abcZ");
9350 }
9351
9352 #[test]
9353 fn ctrl_r_multiline_register_pastes_with_newlines() {
9354 let mut e = editor_with("alpha\nbeta\ngamma");
9355 run_keys(&mut e, "\"byy");
9357 run_keys(&mut e, "j\"byy");
9358 run_keys(&mut e, "ggVj\"by");
9362 let payload = e.registers().read('b').unwrap().text.clone();
9363 assert!(payload.contains('\n'));
9364 run_keys(&mut e, "Go");
9365 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL));
9366 e.handle_key(KeyEvent::new(KeyCode::Char('b'), KeyModifiers::NONE));
9367 let total_lines = e.buffer().lines().len();
9370 assert!(total_lines >= 5);
9371 }
9372
9373 #[test]
9374 fn yank_zero_holds_last_yank_after_delete() {
9375 let mut e = editor_with("hello world");
9376 run_keys(&mut e, "yw");
9377 let yanked = e.registers().read('0').unwrap().text.clone();
9378 assert!(!yanked.is_empty());
9379 run_keys(&mut e, "dw");
9381 assert_eq!(e.registers().read('0').unwrap().text, yanked);
9382 assert!(!e.registers().read('1').unwrap().text.is_empty());
9384 }
9385
9386 #[test]
9387 fn delete_ring_rotates_through_one_through_nine() {
9388 let mut e = editor_with("a b c d e f g h i j");
9389 for _ in 0..3 {
9391 run_keys(&mut e, "dw");
9392 }
9393 let r1 = e.registers().read('1').unwrap().text.clone();
9395 let r2 = e.registers().read('2').unwrap().text.clone();
9396 let r3 = e.registers().read('3').unwrap().text.clone();
9397 assert!(!r1.is_empty() && !r2.is_empty() && !r3.is_empty());
9398 assert_ne!(r1, r2);
9399 assert_ne!(r2, r3);
9400 }
9401
9402 #[test]
9403 fn capital_register_appends_to_lowercase() {
9404 let mut e = editor_with("foo bar");
9405 run_keys(&mut e, "\"ayw");
9406 let first = e.registers().read('a').unwrap().text.clone();
9407 assert!(first.contains("foo"));
9408 run_keys(&mut e, "w\"Ayw");
9410 let combined = e.registers().read('a').unwrap().text.clone();
9411 assert!(combined.starts_with(&first));
9412 assert!(combined.contains("bar"));
9413 }
9414
9415 #[test]
9416 fn zf_in_visual_line_creates_closed_fold() {
9417 let mut e = editor_with("a\nb\nc\nd\ne");
9418 e.jump_cursor(1, 0);
9420 run_keys(&mut e, "Vjjzf");
9421 assert_eq!(e.buffer().folds().len(), 1);
9422 let f = e.buffer().folds()[0];
9423 assert_eq!(f.start_row, 1);
9424 assert_eq!(f.end_row, 3);
9425 assert!(f.closed);
9426 }
9427
9428 #[test]
9429 fn zfj_in_normal_creates_two_row_fold() {
9430 let mut e = editor_with("a\nb\nc\nd\ne");
9431 e.jump_cursor(1, 0);
9432 run_keys(&mut e, "zfj");
9433 assert_eq!(e.buffer().folds().len(), 1);
9434 let f = e.buffer().folds()[0];
9435 assert_eq!(f.start_row, 1);
9436 assert_eq!(f.end_row, 2);
9437 assert!(f.closed);
9438 assert_eq!(e.cursor().0, 1);
9440 }
9441
9442 #[test]
9443 fn zf_with_count_folds_count_rows() {
9444 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9445 e.jump_cursor(0, 0);
9446 run_keys(&mut e, "zf3j");
9448 assert_eq!(e.buffer().folds().len(), 1);
9449 let f = e.buffer().folds()[0];
9450 assert_eq!(f.start_row, 0);
9451 assert_eq!(f.end_row, 3);
9452 }
9453
9454 #[test]
9455 fn zfk_folds_upward_range() {
9456 let mut e = editor_with("a\nb\nc\nd\ne");
9457 e.jump_cursor(3, 0);
9458 run_keys(&mut e, "zfk");
9459 let f = e.buffer().folds()[0];
9460 assert_eq!(f.start_row, 2);
9462 assert_eq!(f.end_row, 3);
9463 }
9464
9465 #[test]
9466 fn zf_capital_g_folds_to_bottom() {
9467 let mut e = editor_with("a\nb\nc\nd\ne");
9468 e.jump_cursor(1, 0);
9469 run_keys(&mut e, "zfG");
9471 let f = e.buffer().folds()[0];
9472 assert_eq!(f.start_row, 1);
9473 assert_eq!(f.end_row, 4);
9474 }
9475
9476 #[test]
9477 fn zfgg_folds_to_top_via_operator_pipeline() {
9478 let mut e = editor_with("a\nb\nc\nd\ne");
9479 e.jump_cursor(3, 0);
9480 run_keys(&mut e, "zfgg");
9484 let f = e.buffer().folds()[0];
9485 assert_eq!(f.start_row, 0);
9486 assert_eq!(f.end_row, 3);
9487 }
9488
9489 #[test]
9490 fn zfip_folds_paragraph_via_text_object() {
9491 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta\nepsilon");
9492 e.jump_cursor(1, 0);
9493 run_keys(&mut e, "zfip");
9495 assert_eq!(e.buffer().folds().len(), 1);
9496 let f = e.buffer().folds()[0];
9497 assert_eq!(f.start_row, 0);
9498 assert_eq!(f.end_row, 2);
9499 }
9500
9501 #[test]
9502 fn zfap_folds_paragraph_with_trailing_blank() {
9503 let mut e = editor_with("alpha\nbeta\ngamma\n\ndelta");
9504 e.jump_cursor(0, 0);
9505 run_keys(&mut e, "zfap");
9507 let f = e.buffer().folds()[0];
9508 assert_eq!(f.start_row, 0);
9509 assert_eq!(f.end_row, 3);
9510 }
9511
9512 #[test]
9513 fn zf_paragraph_motion_folds_to_blank() {
9514 let mut e = editor_with("alpha\nbeta\n\ngamma");
9515 e.jump_cursor(0, 0);
9516 run_keys(&mut e, "zf}");
9518 let f = e.buffer().folds()[0];
9519 assert_eq!(f.start_row, 0);
9520 assert_eq!(f.end_row, 2);
9521 }
9522
9523 #[test]
9524 fn za_toggles_fold_under_cursor() {
9525 let mut e = editor_with("a\nb\nc\nd");
9526 e.buffer_mut().add_fold(1, 2, true);
9527 e.jump_cursor(1, 0);
9528 run_keys(&mut e, "za");
9529 assert!(!e.buffer().folds()[0].closed);
9530 run_keys(&mut e, "za");
9531 assert!(e.buffer().folds()[0].closed);
9532 }
9533
9534 #[test]
9535 fn zr_opens_all_folds_zm_closes_all() {
9536 let mut e = editor_with("a\nb\nc\nd\ne\nf");
9537 e.buffer_mut().add_fold(0, 1, true);
9538 e.buffer_mut().add_fold(2, 3, true);
9539 e.buffer_mut().add_fold(4, 5, true);
9540 run_keys(&mut e, "zR");
9541 assert!(e.buffer().folds().iter().all(|f| !f.closed));
9542 run_keys(&mut e, "zM");
9543 assert!(e.buffer().folds().iter().all(|f| f.closed));
9544 }
9545
9546 #[test]
9547 fn ze_clears_all_folds() {
9548 let mut e = editor_with("a\nb\nc\nd");
9549 e.buffer_mut().add_fold(0, 1, true);
9550 e.buffer_mut().add_fold(2, 3, false);
9551 run_keys(&mut e, "zE");
9552 assert!(e.buffer().folds().is_empty());
9553 }
9554
9555 #[test]
9556 fn g_underscore_jumps_to_last_non_blank() {
9557 let mut e = editor_with("hello world ");
9558 run_keys(&mut e, "g_");
9559 assert_eq!(e.cursor().1, 10);
9561 }
9562
9563 #[test]
9564 fn gj_and_gk_alias_j_and_k() {
9565 let mut e = editor_with("a\nb\nc");
9566 run_keys(&mut e, "gj");
9567 assert_eq!(e.cursor().0, 1);
9568 run_keys(&mut e, "gk");
9569 assert_eq!(e.cursor().0, 0);
9570 }
9571
9572 #[test]
9573 fn paragraph_motions_walk_blank_lines() {
9574 let mut e = editor_with("first\nblock\n\nsecond\nblock\n\nthird");
9575 run_keys(&mut e, "}");
9576 assert_eq!(e.cursor().0, 2);
9577 run_keys(&mut e, "}");
9578 assert_eq!(e.cursor().0, 5);
9579 run_keys(&mut e, "{");
9580 assert_eq!(e.cursor().0, 2);
9581 }
9582
9583 #[test]
9584 fn gv_reenters_last_visual_selection() {
9585 let mut e = editor_with("alpha\nbeta\ngamma");
9586 run_keys(&mut e, "Vj");
9587 run_keys(&mut e, "<Esc>");
9589 assert_eq!(e.vim_mode(), VimMode::Normal);
9590 run_keys(&mut e, "gv");
9592 assert_eq!(e.vim_mode(), VimMode::VisualLine);
9593 }
9594
9595 #[test]
9596 fn o_in_visual_swaps_anchor_and_cursor() {
9597 let mut e = editor_with("hello world");
9598 run_keys(&mut e, "vllll");
9600 assert_eq!(e.cursor().1, 4);
9601 run_keys(&mut e, "o");
9603 assert_eq!(e.cursor().1, 0);
9604 assert_eq!(e.vim.visual_anchor, (0, 4));
9606 }
9607
9608 #[test]
9609 fn editing_inside_fold_invalidates_it() {
9610 let mut e = editor_with("a\nb\nc\nd");
9611 e.buffer_mut().add_fold(1, 2, true);
9612 e.jump_cursor(1, 0);
9613 run_keys(&mut e, "iX<Esc>");
9615 assert!(e.buffer().folds().is_empty());
9617 }
9618
9619 #[test]
9620 fn zd_removes_fold_under_cursor() {
9621 let mut e = editor_with("a\nb\nc\nd");
9622 e.buffer_mut().add_fold(1, 2, true);
9623 e.jump_cursor(2, 0);
9624 run_keys(&mut e, "zd");
9625 assert!(e.buffer().folds().is_empty());
9626 }
9627
9628 #[test]
9629 fn take_fold_ops_observes_z_keystroke_dispatch() {
9630 use crate::types::FoldOp;
9635 let mut e = editor_with("a\nb\nc\nd");
9636 e.buffer_mut().add_fold(1, 2, true);
9637 e.jump_cursor(1, 0);
9638 let _ = e.take_fold_ops();
9641 run_keys(&mut e, "zo");
9642 run_keys(&mut e, "zM");
9643 let ops = e.take_fold_ops();
9644 assert_eq!(ops.len(), 2);
9645 assert!(matches!(ops[0], FoldOp::OpenAt(1)));
9646 assert!(matches!(ops[1], FoldOp::CloseAll));
9647 assert!(e.take_fold_ops().is_empty());
9649 }
9650
9651 #[test]
9652 fn edit_pipeline_emits_invalidate_fold_op() {
9653 use crate::types::FoldOp;
9656 let mut e = editor_with("a\nb\nc\nd");
9657 e.buffer_mut().add_fold(1, 2, true);
9658 e.jump_cursor(1, 0);
9659 let _ = e.take_fold_ops();
9660 run_keys(&mut e, "iX<Esc>");
9661 let ops = e.take_fold_ops();
9662 assert!(
9663 ops.iter().any(|op| matches!(op, FoldOp::Invalidate { .. })),
9664 "expected at least one Invalidate op, got {ops:?}"
9665 );
9666 }
9667
9668 #[test]
9669 fn dot_mark_jumps_to_last_edit_position() {
9670 let mut e = editor_with("alpha\nbeta\ngamma\ndelta");
9671 e.jump_cursor(2, 0);
9672 run_keys(&mut e, "iX<Esc>");
9674 let after_edit = e.cursor();
9675 run_keys(&mut e, "gg");
9677 assert_eq!(e.cursor().0, 0);
9678 run_keys(&mut e, "'.");
9680 assert_eq!(e.cursor().0, after_edit.0);
9681 }
9682
9683 #[test]
9684 fn quote_quote_returns_to_pre_jump_position() {
9685 let mut e = editor_with_rows(50, 20);
9686 e.jump_cursor(10, 2);
9687 let before = e.cursor();
9688 run_keys(&mut e, "G");
9690 assert_ne!(e.cursor(), before);
9691 run_keys(&mut e, "''");
9693 assert_eq!(e.cursor().0, before.0);
9694 }
9695
9696 #[test]
9697 fn backtick_backtick_restores_exact_pre_jump_pos() {
9698 let mut e = editor_with_rows(50, 20);
9699 e.jump_cursor(7, 3);
9700 let before = e.cursor();
9701 run_keys(&mut e, "G");
9702 run_keys(&mut e, "``");
9703 assert_eq!(e.cursor(), before);
9704 }
9705
9706 #[test]
9707 fn macro_record_and_replay_basic() {
9708 let mut e = editor_with("foo\nbar\nbaz");
9709 run_keys(&mut e, "qaIX<Esc>jq");
9711 assert_eq!(e.buffer().lines()[0], "Xfoo");
9712 run_keys(&mut e, "@a");
9714 assert_eq!(e.buffer().lines()[1], "Xbar");
9715 run_keys(&mut e, "j@@");
9717 assert_eq!(e.buffer().lines()[2], "Xbaz");
9718 }
9719
9720 #[test]
9721 fn macro_count_replays_n_times() {
9722 let mut e = editor_with("a\nb\nc\nd\ne");
9723 run_keys(&mut e, "qajq");
9725 assert_eq!(e.cursor().0, 1);
9726 run_keys(&mut e, "3@a");
9728 assert_eq!(e.cursor().0, 4);
9729 }
9730
9731 #[test]
9732 fn macro_capital_q_appends_to_lowercase_register() {
9733 let mut e = editor_with("hello");
9734 run_keys(&mut e, "qall<Esc>q");
9735 run_keys(&mut e, "qAhh<Esc>q");
9736 let text = e.registers().read('a').unwrap().text.clone();
9739 assert!(text.contains("ll<Esc>"));
9740 assert!(text.contains("hh<Esc>"));
9741 }
9742
9743 #[test]
9744 fn buffer_selection_block_in_visual_block_mode() {
9745 use hjkl_buffer::{Position, Selection};
9746 let mut e = editor_with("aaaa\nbbbb\ncccc");
9747 run_keys(&mut e, "<C-v>jl");
9748 assert_eq!(
9749 e.buffer_selection(),
9750 Some(Selection::Block {
9751 anchor: Position::new(0, 0),
9752 head: Position::new(1, 1),
9753 })
9754 );
9755 }
9756
9757 #[test]
9760 fn n_after_question_mark_keeps_walking_backward() {
9761 let mut e = editor_with("foo bar foo baz foo end");
9764 e.jump_cursor(0, 22);
9765 run_keys(&mut e, "?foo<CR>");
9766 assert_eq!(e.cursor().1, 16);
9767 run_keys(&mut e, "n");
9768 assert_eq!(e.cursor().1, 8);
9769 run_keys(&mut e, "N");
9770 assert_eq!(e.cursor().1, 16);
9771 }
9772
9773 #[test]
9774 fn nested_macro_chord_records_literal_keys() {
9775 let mut e = editor_with("alpha\nbeta\ngamma");
9778 run_keys(&mut e, "qblq");
9780 run_keys(&mut e, "qaIX<Esc>q");
9783 e.jump_cursor(1, 0);
9785 run_keys(&mut e, "@a");
9786 assert_eq!(e.buffer().lines()[1], "Xbeta");
9787 }
9788
9789 #[test]
9790 fn shift_gt_motion_indents_one_line() {
9791 let mut e = editor_with("hello world");
9795 run_keys(&mut e, ">w");
9796 assert_eq!(e.buffer().lines()[0], " hello world");
9797 }
9798
9799 #[test]
9800 fn shift_lt_motion_outdents_one_line() {
9801 let mut e = editor_with(" hello world");
9802 run_keys(&mut e, "<lt>w");
9803 assert_eq!(e.buffer().lines()[0], " hello world");
9805 }
9806
9807 #[test]
9808 fn shift_gt_text_object_indents_paragraph() {
9809 let mut e = editor_with("alpha\nbeta\ngamma\n\nrest");
9810 e.jump_cursor(0, 0);
9811 run_keys(&mut e, ">ip");
9812 assert_eq!(e.buffer().lines()[0], " alpha");
9813 assert_eq!(e.buffer().lines()[1], " beta");
9814 assert_eq!(e.buffer().lines()[2], " gamma");
9815 assert_eq!(e.buffer().lines()[4], "rest");
9817 }
9818
9819 #[test]
9820 fn ctrl_o_runs_exactly_one_normal_command() {
9821 let mut e = editor_with("alpha beta gamma");
9824 e.jump_cursor(0, 0);
9825 run_keys(&mut e, "i");
9826 e.handle_key(KeyEvent::new(KeyCode::Char('o'), KeyModifiers::CONTROL));
9827 run_keys(&mut e, "dw");
9828 assert_eq!(e.vim_mode(), VimMode::Insert);
9830 run_keys(&mut e, "X");
9832 assert_eq!(e.buffer().lines()[0], "Xbeta gamma");
9833 }
9834
9835 #[test]
9836 fn macro_replay_respects_mode_switching() {
9837 let mut e = editor_with("hi");
9841 run_keys(&mut e, "qaiX<Esc>0q");
9842 assert_eq!(e.vim_mode(), VimMode::Normal);
9843 e.set_content("yo");
9845 run_keys(&mut e, "@a");
9846 assert_eq!(e.vim_mode(), VimMode::Normal);
9847 assert_eq!(e.cursor().1, 0);
9848 assert_eq!(e.buffer().lines()[0], "Xyo");
9849 }
9850
9851 #[test]
9852 fn macro_recorded_text_round_trips_through_register() {
9853 let mut e = editor_with("");
9857 run_keys(&mut e, "qaiX<Esc>q");
9858 let text = e.registers().read('a').unwrap().text.clone();
9859 assert!(text.starts_with("iX"));
9860 run_keys(&mut e, "@a");
9862 assert_eq!(e.buffer().lines()[0], "XX");
9863 }
9864
9865 #[test]
9866 fn dot_after_macro_replays_macros_last_change() {
9867 let mut e = editor_with("ab\ncd\nef");
9870 run_keys(&mut e, "qaIX<Esc>jq");
9873 assert_eq!(e.buffer().lines()[0], "Xab");
9874 run_keys(&mut e, "@a");
9875 assert_eq!(e.buffer().lines()[1], "Xcd");
9876 let row_before_dot = e.cursor().0;
9879 run_keys(&mut e, ".");
9880 assert!(e.buffer().lines()[row_before_dot].starts_with('X'));
9881 }
9882
9883 fn si_editor(content: &str) -> Editor {
9889 let opts = crate::types::Options {
9890 shiftwidth: 4,
9891 softtabstop: 4,
9892 expandtab: true,
9893 smartindent: true,
9894 autoindent: true,
9895 ..crate::types::Options::default()
9896 };
9897 let mut e = Editor::new(
9898 hjkl_buffer::Buffer::new(),
9899 crate::types::DefaultHost::new(),
9900 opts,
9901 );
9902 e.set_content(content);
9903 e
9904 }
9905
9906 #[test]
9907 fn smartindent_bumps_indent_after_open_brace() {
9908 let mut e = si_editor("fn foo() {");
9910 e.jump_cursor(0, 10); run_keys(&mut e, "i<CR>");
9912 assert_eq!(
9913 e.buffer().lines()[1],
9914 " ",
9915 "smartindent should bump one shiftwidth after {{"
9916 );
9917 }
9918
9919 #[test]
9920 fn smartindent_no_bump_when_off() {
9921 let mut e = si_editor("fn foo() {");
9924 e.settings_mut().smartindent = false;
9925 e.jump_cursor(0, 10);
9926 run_keys(&mut e, "i<CR>");
9927 assert_eq!(
9928 e.buffer().lines()[1],
9929 "",
9930 "without smartindent, no bump: new line copies empty leading ws"
9931 );
9932 }
9933
9934 #[test]
9935 fn smartindent_uses_tab_when_noexpandtab() {
9936 let opts = crate::types::Options {
9938 shiftwidth: 4,
9939 softtabstop: 0,
9940 expandtab: false,
9941 smartindent: true,
9942 autoindent: true,
9943 ..crate::types::Options::default()
9944 };
9945 let mut e = Editor::new(
9946 hjkl_buffer::Buffer::new(),
9947 crate::types::DefaultHost::new(),
9948 opts,
9949 );
9950 e.set_content("fn foo() {");
9951 e.jump_cursor(0, 10);
9952 run_keys(&mut e, "i<CR>");
9953 assert_eq!(
9954 e.buffer().lines()[1],
9955 "\t",
9956 "noexpandtab: smartindent bump inserts a literal tab"
9957 );
9958 }
9959
9960 #[test]
9961 fn smartindent_dedent_on_close_brace() {
9962 let mut e = si_editor("fn foo() {");
9965 e.set_content("fn foo() {\n ");
9967 e.jump_cursor(1, 4); run_keys(&mut e, "i}");
9969 assert_eq!(
9970 e.buffer().lines()[1],
9971 "}",
9972 "close brace on whitespace-only line should dedent"
9973 );
9974 assert_eq!(e.cursor(), (1, 1), "cursor should be after the `}}`");
9975 }
9976
9977 #[test]
9978 fn smartindent_no_dedent_when_off() {
9979 let mut e = si_editor("fn foo() {\n ");
9981 e.settings_mut().smartindent = false;
9982 e.jump_cursor(1, 4);
9983 run_keys(&mut e, "i}");
9984 assert_eq!(
9985 e.buffer().lines()[1],
9986 " }",
9987 "without smartindent, `}}` just appends at cursor"
9988 );
9989 }
9990
9991 #[test]
9992 fn smartindent_no_dedent_mid_line() {
9993 let mut e = si_editor(" let x = 1");
9996 e.jump_cursor(0, 13); run_keys(&mut e, "i}");
9998 assert_eq!(
9999 e.buffer().lines()[0],
10000 " let x = 1}",
10001 "mid-line `}}` should not dedent"
10002 );
10003 }
10004
10005 #[test]
10009 fn count_5x_fills_unnamed_register() {
10010 let mut e = editor_with("hello world\n");
10011 e.jump_cursor(0, 0);
10012 run_keys(&mut e, "5x");
10013 assert_eq!(e.buffer().lines()[0], " world");
10014 assert_eq!(e.cursor(), (0, 0));
10015 assert_eq!(e.yank(), "hello");
10016 }
10017
10018 #[test]
10019 fn x_fills_unnamed_register_single_char() {
10020 let mut e = editor_with("abc\n");
10021 e.jump_cursor(0, 0);
10022 run_keys(&mut e, "x");
10023 assert_eq!(e.buffer().lines()[0], "bc");
10024 assert_eq!(e.yank(), "a");
10025 }
10026
10027 #[test]
10028 fn big_x_fills_unnamed_register() {
10029 let mut e = editor_with("hello\n");
10030 e.jump_cursor(0, 3);
10031 run_keys(&mut e, "X");
10032 assert_eq!(e.buffer().lines()[0], "helo");
10033 assert_eq!(e.yank(), "l");
10034 }
10035
10036 #[test]
10038 fn g_motion_trailing_newline_lands_on_last_content_row() {
10039 let mut e = editor_with("foo\nbar\nbaz\n");
10040 e.jump_cursor(0, 0);
10041 run_keys(&mut e, "G");
10042 assert_eq!(
10044 e.cursor().0,
10045 2,
10046 "G should land on row 2 (baz), not row 3 (phantom empty)"
10047 );
10048 }
10049
10050 #[test]
10052 fn dd_last_line_clamps_cursor_to_new_last_row() {
10053 let mut e = editor_with("foo\nbar\n");
10054 e.jump_cursor(1, 0);
10055 run_keys(&mut e, "dd");
10056 assert_eq!(e.buffer().lines()[0], "foo");
10057 assert_eq!(
10058 e.cursor(),
10059 (0, 0),
10060 "cursor should clamp to row 0 after dd on last content line"
10061 );
10062 }
10063
10064 #[test]
10066 fn d_dollar_cursor_on_last_char() {
10067 let mut e = editor_with("hello world\n");
10068 e.jump_cursor(0, 5);
10069 run_keys(&mut e, "d$");
10070 assert_eq!(e.buffer().lines()[0], "hello");
10071 assert_eq!(
10072 e.cursor(),
10073 (0, 4),
10074 "d$ should leave cursor on col 4, not col 5"
10075 );
10076 }
10077
10078 #[test]
10080 fn undo_insert_clamps_cursor_to_last_valid_col() {
10081 let mut e = editor_with("hello\n");
10082 e.jump_cursor(0, 5); run_keys(&mut e, "a world<Esc>u");
10084 assert_eq!(e.buffer().lines()[0], "hello");
10085 assert_eq!(
10086 e.cursor(),
10087 (0, 4),
10088 "undo should clamp cursor to col 4 on 'hello'"
10089 );
10090 }
10091
10092 #[test]
10094 fn da_doublequote_eats_trailing_whitespace() {
10095 let mut e = editor_with("say \"hello\" there\n");
10096 e.jump_cursor(0, 6);
10097 run_keys(&mut e, "da\"");
10098 assert_eq!(e.buffer().lines()[0], "say there");
10099 assert_eq!(e.cursor().1, 4, "cursor should be at col 4 after da\"");
10100 }
10101
10102 #[test]
10104 fn dab_cursor_col_clamped_after_delete() {
10105 let mut e = editor_with("fn x() {\n body\n}\n");
10106 e.jump_cursor(1, 4);
10107 run_keys(&mut e, "daB");
10108 assert_eq!(e.buffer().lines()[0], "fn x() ");
10109 assert_eq!(
10110 e.cursor(),
10111 (0, 6),
10112 "daB should leave cursor at col 6, not 7"
10113 );
10114 }
10115
10116 #[test]
10118 fn dib_preserves_surrounding_newlines() {
10119 let mut e = editor_with("{\n body\n}\n");
10120 e.jump_cursor(1, 4);
10121 run_keys(&mut e, "diB");
10122 assert_eq!(e.buffer().lines()[0], "{");
10123 assert_eq!(e.buffer().lines()[1], "}");
10124 assert_eq!(e.cursor().0, 1, "cursor should be on the '}}' line");
10125 }
10126
10127 #[test]
10128 fn is_chord_pending_tracks_replace_state() {
10129 let mut e = editor_with("abc\n");
10130 assert!(!e.is_chord_pending());
10131 e.handle_key(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE));
10133 assert!(e.is_chord_pending(), "engine should be pending after r");
10134 e.handle_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::NONE));
10136 assert!(
10137 !e.is_chord_pending(),
10138 "engine pending should clear after replace"
10139 );
10140 }
10141
10142 #[test]
10145 fn yiw_sets_lbr_rbr_marks_around_word() {
10146 let mut e = editor_with("hello world");
10149 run_keys(&mut e, "yiw");
10150 let lo = e.mark('[').expect("'[' must be set after yiw");
10151 let hi = e.mark(']').expect("']' must be set after yiw");
10152 assert_eq!(lo, (0, 0), "'[ should be first char of yanked word");
10153 assert_eq!(hi, (0, 4), "'] should be last char of yanked word");
10154 }
10155
10156 #[test]
10157 fn yj_linewise_sets_marks_at_line_edges() {
10158 let mut e = editor_with("aaaaa\nbbbbb\nccc");
10161 run_keys(&mut e, "yj");
10162 let lo = e.mark('[').expect("'[' must be set after yj");
10163 let hi = e.mark(']').expect("']' must be set after yj");
10164 assert_eq!(lo, (0, 0), "'[ snaps to (top_row, 0) for linewise yank");
10165 assert_eq!(
10166 hi,
10167 (1, 4),
10168 "'] snaps to (bot_row, last_col) for linewise yank"
10169 );
10170 }
10171
10172 #[test]
10173 fn dd_sets_lbr_rbr_marks_to_cursor() {
10174 let mut e = editor_with("aaa\nbbb");
10177 run_keys(&mut e, "dd");
10178 let lo = e.mark('[').expect("'[' must be set after dd");
10179 let hi = e.mark(']').expect("']' must be set after dd");
10180 assert_eq!(lo, hi, "after delete both marks are at the same position");
10181 assert_eq!(lo.0, 0, "post-delete cursor row should be 0");
10182 }
10183
10184 #[test]
10185 fn dw_sets_lbr_rbr_marks_to_cursor() {
10186 let mut e = editor_with("hello world");
10189 run_keys(&mut e, "dw");
10190 let lo = e.mark('[').expect("'[' must be set after dw");
10191 let hi = e.mark(']').expect("']' must be set after dw");
10192 assert_eq!(lo, hi, "after delete both marks are at the same position");
10193 assert_eq!(lo, (0, 0), "post-dw cursor is at col 0");
10194 }
10195
10196 #[test]
10197 fn cw_then_esc_sets_lbr_at_start_rbr_at_inserted_text_end() {
10198 let mut e = editor_with("hello world");
10203 run_keys(&mut e, "cwfoo<Esc>");
10204 let lo = e.mark('[').expect("'[' must be set after cw");
10205 let hi = e.mark(']').expect("']' must be set after cw");
10206 assert_eq!(lo, (0, 0), "'[ should be start of change");
10207 assert_eq!(hi.0, 0, "'] should be on row 0");
10210 assert!(hi.1 >= 2, "'] should be at or past last char of 'foo'");
10211 }
10212
10213 #[test]
10214 fn cw_with_no_insertion_sets_marks_at_change_start() {
10215 let mut e = editor_with("hello world");
10218 run_keys(&mut e, "cw<Esc>");
10219 let lo = e.mark('[').expect("'[' must be set after cw<Esc>");
10220 let hi = e.mark(']').expect("']' must be set after cw<Esc>");
10221 assert_eq!(lo.0, 0, "'[ should be on row 0");
10222 assert_eq!(hi.0, 0, "'] should be on row 0");
10223 assert_eq!(lo, hi, "marks coincide when insert is empty");
10225 }
10226
10227 #[test]
10228 fn p_charwise_sets_marks_around_pasted_text() {
10229 let mut e = editor_with("abc xyz");
10232 run_keys(&mut e, "yiw"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after charwise paste");
10235 let hi = e.mark(']').expect("']' set after charwise paste");
10236 assert!(lo <= hi, "'[ must not exceed ']'");
10237 assert_eq!(
10239 hi.1.wrapping_sub(lo.1),
10240 2,
10241 "'] - '[ should span 2 cols for a 3-char paste"
10242 );
10243 }
10244
10245 #[test]
10246 fn p_linewise_sets_marks_at_line_edges() {
10247 let mut e = editor_with("aaa\nbbb\nccc");
10250 run_keys(&mut e, "yj"); run_keys(&mut e, "j"); run_keys(&mut e, "p"); let lo = e.mark('[').expect("'[' set after linewise paste");
10254 let hi = e.mark(']').expect("']' set after linewise paste");
10255 assert_eq!(lo.1, 0, "'[ col must be 0 for linewise paste");
10256 assert!(hi.0 > lo.0, "'] row must be below '[ row for 2-line paste");
10257 assert_eq!(hi.0 - lo.0, 1, "exactly 1 row gap for a 2-line payload");
10258 }
10259
10260 #[test]
10261 fn backtick_lbr_v_backtick_rbr_reselects_yanked_text() {
10262 let mut e = editor_with("hello world");
10266 run_keys(&mut e, "yiw"); run_keys(&mut e, "`[v`]");
10270 assert_eq!(
10272 e.cursor(),
10273 (0, 4),
10274 "visual `[v`] should land on last yanked char"
10275 );
10276 assert_eq!(
10278 e.vim_mode(),
10279 crate::VimMode::Visual,
10280 "should be in Visual mode"
10281 );
10282 }
10283
10284 #[test]
10290 fn mark_dot_jump_to_last_edit_pre_edit_cursor() {
10291 let mut e = editor_with("hello\nworld\n");
10294 e.jump_cursor(0, 0);
10295 run_keys(&mut e, "iX<Esc>j`.");
10296 assert_eq!(
10297 e.cursor(),
10298 (0, 0),
10299 "dot mark should jump to the change-start (col 0), not post-insert col"
10300 );
10301 }
10302
10303 #[test]
10306 fn count_100g_clamps_to_last_content_row() {
10307 let mut e = editor_with("foo\nbar\nbaz\n");
10310 e.jump_cursor(0, 0);
10311 run_keys(&mut e, "100G");
10312 assert_eq!(
10313 e.cursor(),
10314 (2, 0),
10315 "100G on trailing-newline buffer must clamp to row 2 (last content row)"
10316 );
10317 }
10318
10319 #[test]
10322 fn gi_resumes_last_insert_position() {
10323 let mut e = editor_with("world\nhello\n");
10329 e.jump_cursor(0, 0);
10330 run_keys(&mut e, "iHi<Esc>jgi<Esc>");
10331 assert_eq!(
10332 e.vim_mode(),
10333 crate::VimMode::Normal,
10334 "should be in Normal mode after gi<Esc>"
10335 );
10336 assert_eq!(
10337 e.cursor(),
10338 (0, 1),
10339 "gi<Esc> cursor should be at (0,1) — the insert row, step-back col"
10340 );
10341 }
10342
10343 #[test]
10347 fn visual_block_change_cursor_on_last_inserted_char() {
10348 let mut e = editor_with("foo\nbar\nbaz\n");
10352 e.jump_cursor(0, 0);
10353 run_keys(&mut e, "<C-v>jlcZZ<Esc>");
10354 let lines = e.buffer().lines().to_vec();
10355 assert_eq!(lines[0], "ZZo", "row 0 should be 'ZZo'");
10356 assert_eq!(lines[1], "ZZr", "row 1 should be 'ZZr'");
10357 assert_eq!(
10358 e.cursor(),
10359 (0, 1),
10360 "cursor should be on last char of inserted 'ZZ' (col 1)"
10361 );
10362 }
10363
10364 #[test]
10369 fn register_blackhole_delete_preserves_unnamed_register() {
10370 let mut e = editor_with("foo bar baz\n");
10377 e.jump_cursor(0, 0);
10378 run_keys(&mut e, "yiww\"_dwbp");
10379 let lines = e.buffer().lines().to_vec();
10380 assert_eq!(
10381 lines[0], "ffoooo baz",
10382 "black-hole delete must not corrupt unnamed register"
10383 );
10384 assert_eq!(
10385 e.cursor(),
10386 (0, 3),
10387 "cursor should be on last pasted char (col 3)"
10388 );
10389 }
10390
10391 #[test]
10394 fn after_z_zz_sets_viewport_pinned() {
10395 let mut e = editor_with("a\nb\nc\nd\ne");
10396 e.jump_cursor(2, 0);
10397 e.after_z('z', 1);
10398 assert!(e.vim.viewport_pinned, "zz must set viewport_pinned");
10399 }
10400
10401 #[test]
10402 fn after_z_zo_opens_fold_at_cursor() {
10403 let mut e = editor_with("a\nb\nc\nd");
10404 e.buffer_mut().add_fold(1, 2, true);
10405 e.jump_cursor(1, 0);
10406 e.after_z('o', 1);
10407 assert!(
10408 !e.buffer().folds()[0].closed,
10409 "zo must open the fold at the cursor row"
10410 );
10411 }
10412
10413 #[test]
10414 fn after_z_zm_closes_all_folds() {
10415 let mut e = editor_with("a\nb\nc\nd\ne\nf");
10416 e.buffer_mut().add_fold(0, 1, false);
10417 e.buffer_mut().add_fold(4, 5, false);
10418 e.after_z('M', 1);
10419 assert!(
10420 e.buffer().folds().iter().all(|f| f.closed),
10421 "zM must close all folds"
10422 );
10423 }
10424
10425 #[test]
10426 fn after_z_zd_removes_fold_at_cursor() {
10427 let mut e = editor_with("a\nb\nc\nd");
10428 e.buffer_mut().add_fold(1, 2, true);
10429 e.jump_cursor(1, 0);
10430 e.after_z('d', 1);
10431 assert!(
10432 e.buffer().folds().is_empty(),
10433 "zd must remove the fold at the cursor row"
10434 );
10435 }
10436
10437 #[test]
10438 fn after_z_zf_in_visual_creates_fold() {
10439 let mut e = editor_with("a\nb\nc\nd\ne");
10440 e.jump_cursor(1, 0);
10442 run_keys(&mut e, "V2j");
10443 e.after_z('f', 1);
10445 let folds = e.buffer().folds();
10446 assert_eq!(folds.len(), 1, "zf in visual must create exactly one fold");
10447 assert_eq!(folds[0].start_row, 1);
10448 assert_eq!(folds[0].end_row, 3);
10449 assert!(folds[0].closed);
10450 }
10451}